basic implementation of expressions

This commit is contained in:
Val Erastov 2018-11-26 21:08:11 -08:00
parent 86786662f6
commit 0efdb74888
31 changed files with 539 additions and 133 deletions

View file

@ -41,4 +41,10 @@ export function flatten(arr, result = [], depth, _currLevel) {
return result; return result;
} }
export function indexArray(array, getKey, getValue = v => v) {
let obj = {};
array.forEach(item => obj[getKey(item)] = getValue(item))
return obj;
}
export const EMPTY_ARRAY = Object.freeze([]); export const EMPTY_ARRAY = Object.freeze([]);

41
modules/ui/bind.js Normal file
View file

@ -0,0 +1,41 @@
import React from 'react';
import context from 'context';
export default function bind(streamProvider) {
return function (Component) {
return class Connected extends React.Component {
state = {hasError: false, value: null};
onChange = value => streamProvider(context.streams, this.props).next(value);
componentWillMount() {
this.stream = streamProvider(context.streams, this.props);
this.detacher = this.stream.attach(value => {
this.setState({
hasError: false,
value
});
});
}
componentWillUnmount() {
this.detacher();
}
render() {
if (this.state.hasError) {
return null;
}
return <Component value={this.state.value}
onChange={this.onChange}
{...this.props} />;
}
componentDidCatch() {
this.setState({hasError: true});
}
};
}
}

View file

@ -2,6 +2,7 @@ import React from 'react';
import ls from './Folder.less' import ls from './Folder.less'
import Fa from "./Fa"; import Fa from "./Fa";
import cx from 'classnames';
export default class Folder extends React.Component{ export default class Folder extends React.Component{
@ -23,11 +24,11 @@ export default class Folder extends React.Component{
}; };
render() { render() {
let {title, closable, children} = this.props; let {title, closable, className, children} = this.props;
return <div className={ls.root}> return <div className={cx(ls.root, className)}>
<div className={ls.title} onClick={closable ? this.tweakClose : null}> <div className={ls.title} onClick={closable ? this.tweakClose : null}>
<span className={ls.handle}><Fa fw icon={this.isClosed() ? 'chevron-right' : 'chevron-down'}/></span> <span className={ls.handle}><Fa fw icon={this.isClosed() ? 'chevron-right' : 'chevron-down'}/></span>
{title} {' '}{title}
</div> </div>
{!this.isClosed() && children} {!this.isClosed() && children}
</div> </div>

View file

@ -0,0 +1,7 @@
import React from 'react';
import ls from './Row.less';
import cx from 'classnames';
export default function Row({className, props, children}) {
return <div className={cx(ls.root, className)} {...props} >{children}</div>;
}

View file

@ -0,0 +1,9 @@
.root {
line-height: 1px;
}
.root > * {
line-height: 1px;
margin: 3px;
padding: 5px 3px;
}

View file

@ -0,0 +1,7 @@
import React from 'react';
import ls from './ToolButton.less';
import cx from 'classnames';
export default function ToolButton({pressed, type, className, ...props}) {
return <button tabIndex="-1" className={cx(ls.root, ls[type], pressed && ls.pressed, className)} {...props}/>;
}

View file

@ -0,0 +1,38 @@
@import "../styles/theme.less";
.root {
border: 1px solid @bg-color;
margin: 3px;
border-radius: 3px;
padding: 5px 2px;
cursor: pointer;
background-color: @bg-color;
outline: none;
&:hover {
background-color: #9c9c9c !important;
}
&:active {
transition: 200ms;
background-color: #BFBFBF !important;
}
&.pressed {
background-color: #7d7d7d;
}
}
.accent {
background-color: @color-accent;
}
.highlight {
background-color: @color-highlight;
}
.danger {
background-color: @color-danger;
}

View file

@ -17,6 +17,10 @@
.button-behavior(@color-accent) .button-behavior(@color-accent)
} }
.highlihgt {
.button-behavior(@color-highlight)
}
.danger { .danger {
.button-behavior(@color-danger) .button-behavior(@color-danger)
} }

View file

@ -8,30 +8,22 @@ export default class NumberControl extends React.Component {
let {onChange, value} = this.props; let {onChange, value} = this.props;
return <InputControl type='number' return <InputControl type='number'
onWheel={this.onWheel} onWheel={this.onWheel}
value={ Math.round(value * 1000) / 1000 } value={ value }
onChange={this.onChange} onChange={this.onChange}
inputRef={input => this.input = input} /> inputRef={input => this.input = input} />
} }
onChange = e => { onChange = e => {
let val; this.props.onChange(e.target.value);
try {
val = parseFloat(e.target.value)
} catch (ignore) {
return;
}
if (!isNaN(val)) {
this.props.onChange(val);
}
}; };
onWheel = (e) => { onWheel = (e) => {
let {baseStep, round, min, max, onChange, accelerator} = this.props; let {baseStep, round, min, max, onChange, accelerator} = this.props;
let delta = e.deltaY; let delta = e.deltaY;
let val = e.target.value;
if (!val) val = 0;
let step = baseStep * (e.shiftKey ? accelerator : 1); let step = baseStep * (e.shiftKey ? accelerator : 1);
val = parseFloat(val) + (delta < 0 ? -step : step); let val = parseFloat(e.target.value);
if (isNaN(val)) val = 0;
val = val + (delta < 0 ? -step : step);
if (min !== undefined && val < min) { if (min !== undefined && val < min) {
val = min; val = min;
} }

View file

@ -0,0 +1,26 @@
@import "./theme.less";
@import "./table.less";
*.autoMarginLeft {
margin-left: auto !important;
}
*.floatRight {
float: right;
}
.fullWidth {
width: 100%;
}
.dangerColor {
color: @color-danger;
}
.dangerBg {
background-color: @color-danger;
}
.inlineBlock {
display: inline-block;
}

View file

@ -1,8 +1,10 @@
@import "../theme.less"; @import "../theme.less";
@import "../mixins.less"; @import "../mixins.less";
@fontSize: 11px;
html, pre { html, pre {
font: 11px 'Lucida Grande', sans-serif; font: @fontSize 'Lucida Grande', sans-serif;
} }
body { body {
@ -31,3 +33,7 @@ button {
pre { pre {
line-height: 1.5; line-height: 1.5;
} }
table {
font-size: @fontSize;
}

View file

@ -0,0 +1,37 @@
@import "./theme.less";
.stripedTable {
& tr:nth-child(even), & th {
background-color: @bg-color
}
& tr:nth-child(odd) {
background-color: @bg-color-alt
}
& tr:hover {
background-color: @color-highlight;
}
}
.delineatedTable {
border-collapse: collapse;
border-spacing: 0;
& td, & th {
padding: 5px 2px;
border: 1px solid @border-color;
}
& tr:first-child th {
border-top: 0;
}
//& tr:last-child td {
// border-bottom: 0;
//}
& tr td:first-child,
& tr th:first-child {
border-left: 0;
}
& tr td:last-child,
& tr th:last-child {
border-right: 0;
}
}

View file

@ -22,6 +22,7 @@
@color-danger: #b00; @color-danger: #b00;
@color-accent: #2B7D2B; @color-accent: #2B7D2B;
@color-neutral: #66727d; @color-neutral: #66727d;
@color-highlight: #003f5d;
//@work-area-toolbar-bg-color: ; //@work-area-toolbar-bg-color: ;
//@work-area-toolbar-font-color: ; //@work-area-toolbar-font-color: ;

View file

@ -0,0 +1,12 @@
import React, {Fragment} from 'react';
import {mapActionBehavior} from './actionButtonBehavior';
export function actionDecorator(actionId) {
let actionBehavior = mapActionBehavior(actionId);
return function (Component) {
return function ActionDecorator(props) {
return <Component {...actionBehavior} {...props}/>;
}
}
}

View file

@ -2,6 +2,8 @@ import {addModification, stepOverriding} from './craftHistoryUtils';
import {state, stream} from 'lstream'; import {state, stream} from 'lstream';
import {MShell} from '../model/mshell'; import {MShell} from '../model/mshell';
import {MDatum} from '../model/mdatum'; import {MDatum} from '../model/mdatum';
import materializeParams from './materializeParams';
import CadError from '../../utils/errors';
export function activate({streams, services}) { export function activate({streams, services}) {
@ -54,7 +56,17 @@ export function activate({streams, services}) {
throw(`unknown operation ${request.type}`); throw(`unknown operation ${request.type}`);
} }
return op.run(request.params, services); let params = {};
let errors = [];
materializeParams(services, request.params, op.schema, params, errors);
if (errors.length) {
throw new CadError({
kind: CadError.KIND.INVALID_PARAMS,
userMessage: errors.map(err => `${err.path.join('.')}: ${err.message}`).join('\n')
});
}
return op.run(params, services);
} }
function runOrGetPreRunResults(request) { function runOrGetPreRunResults(request) {
@ -92,6 +104,7 @@ export function activate({streams, services}) {
streams.craft.models.next(Array.from(models).sort(m => m.id)); streams.craft.models.next(Array.from(models).sort(m => m.id));
} catch(e) { } catch(e) {
console.error(e); console.error(e);
//TODO: need to find a way to propagate the error to the wizard.
setTimeout(() => streams.craft.modifications.next({ setTimeout(() => streams.craft.modifications.next({
...curr, ...curr,
pointer: i-1 pointer: i-1

View file

@ -25,6 +25,8 @@ export default function initializeBySchema(schema, context) {
val = context.streams.selection[md.type].value[0]; val = context.streams.selection[md.type].value[0];
} else if (md.type === 'object') { } else if (md.type === 'object') {
val = initializeBySchema(md.schema, context); val = initializeBySchema(md.schema, context);
} else if (md.type === 'number') {
val = md.defaultValue + '';
} else { } else {
val = md.defaultValue; val = md.defaultValue;
} }

View file

@ -1,8 +1,7 @@
import {ENTITIES} from '../scene/entites'; import {ENTITIES} from '../scene/entites';
export default function validateParams(services, params, schema, errors, parentPath) { export default function materializeParams(services, params, schema, result, errors, parentPath) {
errors = errors || [];
parentPath = parentPath || ROOT_PATH; parentPath = parentPath || ROOT_PATH;
for (let field of Object.keys(schema)) { for (let field of Object.keys(schema)) {
@ -17,18 +16,20 @@ export default function validateParams(services, params, schema, errors, parentP
} }
} else { } else {
if (md.type === 'number') { if (md.type === 'number') {
if (typeof value !== 'number') { try {
errors.push({path: [...parentPath, field], message: 'not a number type'}); value = services.expressions.evaluateExpression(value);
} else { } catch (e) {
if (md.min !== undefined ) { errors.push({path: [...parentPath, field], message: 'unable to evaluate expression'});
if (value < md.min) { }
errors.push({path: [...parentPath, field], message: 'less than allowed'});
} if (md.min !== undefined ) {
if (value < md.min) {
errors.push({path: [...parentPath, field], message: 'less than allowed'});
} }
if (md.max !== undefined ) { }
if (value > md.max) { if (md.max !== undefined ) {
errors.push({path: [...parentPath, field], message: 'greater than allowed'}); if (value > md.max) {
} errors.push({path: [...parentPath, field], message: 'greater than allowed'});
} }
} }
} else if (md.type === 'string') { } else if (md.type === 'string') {
@ -55,12 +56,14 @@ export default function validateParams(services, params, schema, errors, parentP
if (!Array.isArray(value)) { if (!Array.isArray(value)) {
errors.push({path: [...parentPath, field], message: 'not an array type'}); errors.push({path: [...parentPath, field], message: 'not an array type'});
} }
value.forEach((item , i) => { value = value.map((item , i) => {
validateParams(services, item, md.schema, errors, [...parentPath, i]); let itemResult = {};
materializeParams(services, item, md.schema, item, errors, [...parentPath, i]);
return itemResult;
}); });
} }
result[field] = value;
} }
return errors;
} }
} }

View file

@ -23,7 +23,6 @@ export default class Wizard extends React.Component {
state = { state = {
hasError: false, hasError: false,
validationErrors: [],
}; };
@ -51,8 +50,7 @@ export default class Wizard extends React.Component {
let formContext = { let formContext = {
data: params, data: params,
validationErrors: this.state.validationErrors, updateParam
updateParam,
}; };
let Form = operation.form; let Form = operation.form;
@ -72,15 +70,13 @@ export default class Wizard extends React.Component {
<Button type='accent' onClick={this.onOK}>OK</Button> <Button type='accent' onClick={this.onOK}>OK</Button>
</ButtonGroup> </ButtonGroup>
{this.state.hasError && <div className={ls.errorMessage}> {this.state.hasError && <div className={ls.errorMessage}>
performing operation with current parameters leads to an invalid object {this.state.algorithmError && <span>
(self-intersecting / zero-thickness / complete degeneration or unsupported cases) performing operation with current parameters leads to an invalid object
(self-intersecting / zero-thickness / complete degeneration or unsupported cases)
</span>}
{this.state.code && <div className={ls.errorCode}>{this.state.code}</div>} {this.state.code && <div className={ls.errorCode}>{this.state.code}</div>}
{this.state.userMessage && <div className={ls.userErrorMessage}>{this.state.userMessage}</div>} {this.state.userMessage && <div className={ls.userErrorMessage}>{this.state.userMessage}</div>}
</div>} </div>}
{this.state.validationErrors.length !== 0 && <div className={ls.errorMessage}>
{this.state.validationErrors.map((err, i) => <div key={i}> {err.path.join(' ')} {err.message}</div>)}
</div>}
</Stack> </Stack>
</Window>; </Window>;
} }
@ -110,17 +106,6 @@ export default class Wizard extends React.Component {
onOK = () => { onOK = () => {
try { try {
let {type, params, resolveOperation, validator} = this.props;
if (!type) {
return null;
}
let operation = resolveOperation(type);
let validationErrors = validator(params, operation.schema);
if (validationErrors.length !== 0) {
this.setState({validationErrors});
return;
}
this.props.onOK(); this.props.onOK();
} catch (error) { } catch (error) {
this.handleError(error); this.handleError(error);
@ -135,6 +120,9 @@ export default class Wizard extends React.Component {
if (error.TYPE === CadError) { if (error.TYPE === CadError) {
let {code, userMessage, kind} = error; let {code, userMessage, kind} = error;
printError = !code; printError = !code;
if (CadError.ALGORITMTHM_ERROR_KINDS.includes(kind)) {
stateUpdate.algorithmError = true
}
if (code && kind === CadError.KIND.INTERNAL_ERROR) { if (code && kind === CadError.KIND.INTERNAL_ERROR) {
console.warn('Operation Error Code: ' + code); console.warn('Operation Error Code: ' + code);
} }

View file

@ -1,15 +1,9 @@
.errorMessage { .errorMessage {
color: lightgoldenrodyellow; color: lightgoldenrodyellow;
} white-space: pre-line;
.userErrorMessage, .errorCode {
font-size: 9px;
}
.userErrorMessage {
color: white;
} }
.errorCode { .errorCode {
font-size: 9px;
font-style: italic; font-style: italic;
} }

View file

@ -3,17 +3,13 @@ import Wizard from './Wizard';
import connect from 'ui/connect'; import connect from 'ui/connect';
import decoratorChain from 'ui/decoratorChain'; import decoratorChain from 'ui/decoratorChain';
import mapContext from 'ui/mapContext'; import mapContext from 'ui/mapContext';
import {finishHistoryEditing, stepOverriding} from '../../craftHistoryUtils'; import {finishHistoryEditing} from '../../craftHistoryUtils';
import validateParams from '../../validateParams';
import {NOOP} from 'gems/func';
import {clone} from 'gems/objects';
function WizardManager({type, changingHistory, resolve, cancel, stepHistory, insertOperation, cancelHistoryEdit, applyWorkingRequest, validator}) { function WizardManager({type, changingHistory, resolve, cancel, stepHistory, insertOperation, cancelHistoryEdit, applyWorkingRequest}) {
if (!type) { if (!type) {
return null; return null;
} }
return <Wizard resolveOperation={resolve} return <Wizard resolveOperation={resolve}
validator={validator}
onCancel={changingHistory ? cancelHistoryEdit : cancel} onCancel={changingHistory ? cancelHistoryEdit : cancel}
onOK={applyWorkingRequest} /> onOK={applyWorkingRequest} />
} }
@ -22,7 +18,6 @@ export default decoratorChain(
connect(streams => streams.wizard.effectiveOperation), connect(streams => streams.wizard.effectiveOperation),
mapContext((ctx, props) => ({ mapContext((ctx, props) => ({
cancel: ctx.services.wizard.cancel, cancel: ctx.services.wizard.cancel,
validator: (params, schema) => validateParams(ctx.services, params, schema),
resolve: type => ctx.services.operation.get(type), resolve: type => ctx.services.operation.get(type),
cancelHistoryEdit: () => ctx.streams.craft.modifications.update(modifications => finishHistoryEditing(modifications)), cancelHistoryEdit: () => ctx.streams.craft.modifications.update(modifications => finishHistoryEditing(modifications)),
applyWorkingRequest: ctx.services.wizard.applyWorkingRequest applyWorkingRequest: ctx.services.wizard.applyWorkingRequest

View file

@ -1,11 +1,15 @@
import React from 'react'; import React from 'react';
import ObjectExplorer from '../../craft/ui/ObjectExplorer';
import OperationHistory from '../../craft/ui/OperationHistory';
import Folder from 'ui/components/Folder'; import Folder from 'ui/components/Folder';
import Fa from '../../../../../modules/ui/components/Fa';
import ls from './FloatView.less'; import ls from './FloatView.less';
import cx from 'classnames'; import connect from 'ui/connect';
import mapContext from 'ui/mapContext';
import Fa from 'ui/components/Fa';
import ToolButton from 'ui/components/ToolButton';
@connect(state => state.ui.floatViews.map(views => ({views})))
@mapContext(ctx => ({
getDescriptor: ctx.services.ui.getFloatView
}))
export default class FloatView extends React.Component { export default class FloatView extends React.Component {
state = { state = {
@ -13,33 +17,32 @@ export default class FloatView extends React.Component {
}; };
render() { render() {
let {views, getDescriptor} = this.props;
function view(id) {
let {title, icon, Component} = getDescriptor(id);
return <Folder className={ls.folder} title={<span> <Fa fw icon={icon}/> {title}</span>}>
<Component/>
</Folder>;
}
function icon(id) {
let {Icon} = getDescriptor(id);
return <Icon />
}
return <div className={ls.root}> return <div className={ls.root}>
<div className={ls.tabs}> <div className={ls.tabs}>
{['project', 'history'].map(tabId => <Tab selected={this.state.selected === tabId} key={tabId} {views.map(tabId => <ToolButton pressed={this.state.selected === tabId}
onClick={() => this.setState({selected: this.state.selected === tabId ? null : tabId})}>{getIcon(tabId)}</Tab>)} key={tabId}
onClick={() => this.setState({selected: this.state.selected === tabId ? null : tabId})}>
{<Fa fw icon={getDescriptor(tabId).icon}/>}
</ToolButton>)}
</div> </div>
{this.state.selected && <div className={ls.main}> {this.state.selected && <div className={ls.main}>
{this.state.selected === 'project' && <Folder title={<span> <Fa fw icon='cubes'/> Model</span>}> {view(this.state.selected)}
<ObjectExplorer/>
</Folder>}
{this.state.selected === 'history' && <Folder title={<span> <Fa fw icon='history'/> Modifications</span>}>
<OperationHistory/>
</Folder>}
</div>} </div>}
</div>; </div>;
} }
}
function Tab({children, selected, onClick}) {
return <div className={cx(ls.tab, selected && ls.selected)} onClick={onClick}>{children}</div>;
}
function getIcon(id) {
if (id === 'history') {
return <Fa fw icon='history'/>;
} else if (id === 'project') {
return <Fa fw icon='cubes'/>;
}
} }

View file

@ -11,6 +11,11 @@
font-size: 13px; font-size: 13px;
} }
.tabs button {
display: block;
font-size: 13px;
}
.main { .main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -19,24 +24,8 @@
overflow-y: scroll; overflow-y: scroll;
} }
.tab { .folder {
height: 100%;
border: 1px solid @bg-color; display: flex;
margin: 3px; flex-direction: column;
border-radius: 3px; }
padding: 5px 2px;
cursor: pointer;
background-color: @bg-color;
&:hover {
background-color: #9c9c9c !important;
}
&:active {
transition: 200ms;
background-color: #BFBFBF !important;
}
&.selected {
background-color: #7d7d7d;
}
}

View file

@ -15,18 +15,27 @@ export function defineStreams({streams}) {
sketcherControl: state([]), sketcherControl: state([]),
sketcherToolbarsVisible: state(false) sketcherToolbarsVisible: state(false)
}, },
floatViews: state([]),
sockets: {} sockets: {}
}; };
} }
export function activate({services}) { export function activate({streams, services}) {
let components = new Map(); let components = new Map();
const registerComponent = (id, Component) => components.set(id, Component); const registerComponent = (id, Component) => components.set(id, Component);
const getComponent = id => components.get(id); const getComponent = id => components.get(id);
let floatViewDescriptors = new Map();
function registerFloatView(id, Component, title, icon) {
floatViewDescriptors.set(id, {Component, title, icon});
streams.ui.floatViews.mutate(views => views.push(id));
}
const getFloatView = id => floatViewDescriptors.get(id);
services.ui = { services.ui = {
registerComponent, getComponent registerComponent, getComponent, registerFloatView, getFloatView
} }
} }

View file

@ -0,0 +1,92 @@
import React, {Fragment} from 'react';
import ls from './Expressions.less';
import cmn from 'ui/styles/common.less';
import ToolButton from 'ui/components/ToolButton';
import Fa from 'ui/components/Fa';
import Row from 'ui/components/Row';
import connect from 'ui/connect';
import mapContext from 'ui/mapContext';
import bind from 'ui/bind';
import cx from 'classnames';
import {actionDecorator} from '../actions/actionDecorators';
import {combine} from 'lstream';
import Folder from 'ui/components/Folder';
import Stack from '../../../../modules/ui/components/Stack';
@connect(streams => combine(streams.expressions.synced, streams.expressions.errors)
.map(([synced, errors])=> ({synced, errors})))
@mapContext(ctx => ({
reevaluateExpressions: ctx.services.expressions.reevaluateExpressions
}))
export default class Expressions extends React.Component {
state = {
activeTab: 'Script'
};
render() {
let {errors, synced, table, reevaluateExpressions} = this.props;
const tabBtn = (name, icon) => {
return <ToolButton onClick={() => this.setState({activeTab: name})} pressed={this.state.activeTab === name}>{icon} {name}</ToolButton>;
};
return <div className={ls.root}>
<Row className={ls.switcher}>
{tabBtn('Script', <Fa fw icon='pencil' />)}
{tabBtn('Table', <Fa fw icon='table' />)}
{errors.length > 0 && <span><Fa icon='warning' className={cx(cmn.dangerColor, cmn.inlineBlock)} /></span>}
{!synced && <ReevaluateActionButton type='accent' className={cmn.floatRight}><Fa fw icon='check'/></ReevaluateActionButton>}
</Row>
<div className={ls.workingArea}>
{this.state.activeTab === 'Script' && <Script reevaluateExpressions={reevaluateExpressions}/>}
{this.state.activeTab === 'Table' && <VarTable table={table} errors={errors}/>}
{errors.length > 0 && <Folder title={<Fragment><Fa icon='warning' className={cx(cmn.dangerColor)} /> Script Errors</Fragment>}>
<Stack>
{errors.map(err => <div key={err.line}>
line {err.line + 1}: {err.message}
</div>)}
</Stack>
</Folder>}
</div>
</div>;
}
}
const ReevaluateActionButton = actionDecorator('expressionsUpdateTable')(ToolButton);
const Script = bind(streams => streams.expressions.script)(
function Script({value, onChange, reevaluateExpressions}) {
return <textarea placeholder='for example: A = 50'
className={ls.script}
value={value}
onChange={e => onChange(e.target.value)}
onBlur={e => reevaluateExpressions(e.target.value)} />
}
);
const VarTable = bind(streams => streams.expressions.list)(
function VarTable({value}) {
return <table className={cx(cmn.fullWidth, cmn.stripedTable, cmn.delineatedTable)}>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{value.map(({name, value}, i) => <tr key={i}>
<td>{name}</td>
<td>{value}</td>
</tr>)}
</tbody>
</table>
}
);

View file

@ -0,0 +1,33 @@
@import "~ui/styles/theme.less";
.root {
display: flex;
flex-direction: column;
flex: 1;
}
.script {
flex: 1;
resize: none;
background: inherit;
border: none;
color: #C4E1A4;
padding: 2px;
outline: none;
line-height: 1.8;
}
.workingArea {
display: flex;
flex-direction: column;
flex: 1;
}
.switcher {
border-bottom: 1px solid @border-color;
width: 100%;
}
.toolBar {
}

View file

@ -0,0 +1,85 @@
import {state, stream, merge} from 'lstream';
import {indexArray} from '../../../../modules/gems/iterables';
import {NOOP} from '../../../../modules/gems/func';
export function defineStreams(ctx) {
const script = state('');
const list = state([]);
const table = list.map(varList => indexArray(varList, i => i.name, i => i.value)).remember();
const synced = merge(script.map(() => false), list.map(() => true));
ctx.streams.expressions = {
script, list, table, synced,
errors: state([])
};
}
export function activate(ctx) {
let _evaluateExpression = NOOP;
function reevaluateExpressions() {
let {varList, errors, evaluateExpression} = rebuildVariableTable(ctx.streams.expressions.script.value);
ctx.streams.expressions.list.next(varList);
ctx.streams.expressions.errors.next(errors);
_evaluateExpression = evaluateExpression;
}
function load(script) {
ctx.streams.expressions.script.next(script);
reevaluateExpressions();
}
function evaluateExpression(expr) {
if (typeof expr === 'number') {
return expr;
}
let value = ctx.streams.expressions.table.value[expr];
if (value === undefined) {
value = parseFloat(expr);
if (isNaN(value)) {
value = _evaluateExpression(expr);
}
}
return value;
}
ctx.services.expressions = {
reevaluateExpressions, load, evaluateExpression
};
ctx.services.action.registerAction({
id: 'expressionsUpdateTable',
appearance: {
info: 'reevaluate expression script (happens automatically on script focus lost)',
label: 'update expressions',
},
invoke: ({services}) => {
services.extension.reevaluateExpressions();
}
})
}
function rebuildVariableTable(script) {
let varList = [];
let errors = [];
if (script == null) return;
let lines = script.split('\n');
let evalContext = "(function() { \n";
function evaluateExpression(expr) {
return eval(evalContext + "return " + expr + "; \n})()");
}
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
let m = line.match(/^\s*([^\s]+)\s*=(.+)$/);
if (m != null && m.length === 3) {
let name = m[1];
try {
let value = evaluateExpression(m[2]);
varList.push({name, value});
evalContext += "const " + name + " = " + value + ";\n"
} catch (e) {
errors.push({
line: i,
message: e.message
});
console.log(e);
}
}
}
return {varList, errors, evaluateExpression};
}

View file

@ -21,17 +21,17 @@ import * as ProjectPlugin from '../projectPlugin';
import * as SketcherPlugin from '../sketch/sketcherPlugin'; import * as SketcherPlugin from '../sketch/sketcherPlugin';
import * as ExportPlugin from '../exportPlugin'; import * as ExportPlugin from '../exportPlugin';
import * as TpiPlugin from '../tpi/tpiPlugin'; import * as TpiPlugin from '../tpi/tpiPlugin';
import * as PartModellerPlugin from '../part/partModellerPlugin';
import * as ViewSyncPlugin from '../scene/viewSyncPlugin'; import * as ViewSyncPlugin from '../scene/viewSyncPlugin';
import PartModellerPlugins from '../part/partModelerPlugins';
import context from 'context'; import context from 'context';
import startReact from "../dom/startReact"; import startReact from "../dom/startReact";
export default function startApplication(callback) { export default function startApplication(callback) {
let applicationPlugins = [PartModellerPlugin]; let applicationPlugins = PartModellerPlugins;
let preUIPlugins = [ let preUIPlugins = [
LifecyclePlugin, LifecyclePlugin,

View file

@ -1,14 +1,11 @@
import * as UIConfigPlugin from './uiConfigPlugin'; import * as UIConfigPlugin from './uiConfigPlugin';
import * as PartOperationsPlugin from './partOperationsPlugin'; import * as PartOperationsPlugin from './partOperationsPlugin';
import * as DebugPlugin from '../debugPlugin'; import * as DebugPlugin from '../debugPlugin';
import {activatePlugins} from "../init/startApplication"; import * as ExpressionsPlugin from '../expressions/expressionsPlugin';
const PART_MODELLER_PLUGINS = [ export default [
UIConfigPlugin, UIConfigPlugin,
DebugPlugin, DebugPlugin,
ExpressionsPlugin,
PartOperationsPlugin PartOperationsPlugin
]; ];
export function activate(context) {
activatePlugins(PART_MODELLER_PLUGINS, context);
}

View file

@ -2,6 +2,10 @@ import CoreActions from '../actions/coreActions';
import OperationActions from '../actions/operationActions'; import OperationActions from '../actions/operationActions';
import HistoryActions from '../actions/historyActions'; import HistoryActions from '../actions/historyActions';
import menuConfig from './menuConfig'; import menuConfig from './menuConfig';
import ObjectExplorer from '../craft/ui/ObjectExplorer';
import React from 'react';
import OperationHistory from '../craft/ui/OperationHistory';
import Expressions from '../expressions/Expressions';
export function activate({services, streams}) { export function activate({services, streams}) {
streams.ui.controlBars.left.value = ['menu.file', 'menu.craft', 'menu.boolean', 'menu.primitives', 'Donate', 'GitHub']; streams.ui.controlBars.left.value = ['menu.file', 'menu.craft', 'menu.boolean', 'menu.primitives', 'Donate', 'GitHub'];
@ -19,4 +23,8 @@ export function activate({services, streams}) {
services.action.registerActions(HistoryActions); services.action.registerActions(HistoryActions);
services.menu.registerMenus(menuConfig); services.menu.registerMenus(menuConfig);
services.ui.registerFloatView('project', ObjectExplorer, 'Model', 'cubes');
services.ui.registerFloatView('history', OperationHistory, 'Modifications', 'history');
services.ui.registerFloatView('expressions', Expressions, 'Expressions', 'percent');
} }

View file

@ -31,16 +31,20 @@ export function activate(context) {
function save() { function save() {
let data = {}; let data = {};
data.history = streams.craft.modifications.value.history; data.history = streams.craft.modifications.value.history;
data.expressions = streams.expressions.script.value;
services.storage.set(projectStorageKey(), JSON.stringify(data)); services.storage.set(projectStorageKey(), JSON.stringify(data));
} }
function load() { function load() {
try { try {
let data = services.storage.get(services.project.projectStorageKey()); let dataStr = services.storage.get(services.project.projectStorageKey());
if (data) { if (dataStr) {
let history = JSON.parse(data).history; let data = JSON.parse(dataStr);
if (history) { if (data.history) {
services.craft.reset(history); services.craft.reset(data.history);
}
if (data.expressions) {
services.expressions.load(data.expressions);
} }
} }
} catch (e) { } catch (e) {

View file

@ -19,4 +19,8 @@ CadError.KIND = {
INTERNAL_ERROR: 'INTERNAL_ERROR', INTERNAL_ERROR: 'INTERNAL_ERROR',
UNSUPPORTED_CASE: 'UNSUPPORTED_CASE', UNSUPPORTED_CASE: 'UNSUPPORTED_CASE',
INVALID_INPUT: 'INVALID_INPUT', INVALID_INPUT: 'INVALID_INPUT',
}; INVALID_PARAMS: 'INVALID_PARAMS'
};
CadError.ALGORITMTHM_ERROR_KINDS = ['INTERNAL_ERROR', 'UNSUPPORTED_CASE', 'INVALID_INPUT'];