diff --git a/modules/lstream/utils.js b/modules/lstream/utils.js index 5ffcacf9..92c86f6a 100644 --- a/modules/lstream/utils.js +++ b/modules/lstream/utils.js @@ -1,2 +1,18 @@ export const NOT_INITIALIZED = Object.freeze({}); + +export function propsChangeTracker(props, onChange) { + + const values = props.map(() => NOT_INITIALIZED); + + return function(obj) { + for (let i = 0; i < props.length; i++) { + const prop = props[i]; + const prevValue = values[i]; + const currValue = obj[prop]; + if (prevValue !== currValue) { + onChange(obj, prop, currValue, prevValue); + } + } + } +} \ No newline at end of file diff --git a/modules/ui/components/Folder.jsx b/modules/ui/components/Folder.jsx index 8dba4e03..1b825779 100644 --- a/modules/ui/components/Folder.jsx +++ b/modules/ui/components/Folder.jsx @@ -26,11 +26,15 @@ export default class Folder extends React.Component{ render() { let {title, closable, className, children} = this.props; return
-
- - {' '}{title} -
+ {!this.isClosed() && children} </div> } } + +export function Title({title, isClosed, onClick}) { + return <div className={ls.title} onClick={onClick}> + <span className={ls.handle}><Fa fw icon={isClosed ? 'chevron-right' : 'chevron-down'}/></span> + {' '}{title} + </div>; +} diff --git a/modules/ui/components/controls/CheckboxControl.jsx b/modules/ui/components/controls/CheckboxControl.jsx index f510abdc..78f4efe5 100644 --- a/modules/ui/components/controls/CheckboxControl.jsx +++ b/modules/ui/components/controls/CheckboxControl.jsx @@ -6,6 +6,6 @@ export default class CheckboxControl extends React.Component { let {onChange, value} = this.props; return <input type='checkbox' defaultValue={value} - onChange={e => onChange(e.target.value)} /> + onChange={e => onChange(e.target.checked)} /> } } diff --git a/modules/ui/components/controls/FormSection.jsx b/modules/ui/components/controls/FormSection.jsx new file mode 100644 index 00000000..f4c2e2da --- /dev/null +++ b/modules/ui/components/controls/FormSection.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import {Title} from '../Folder'; + +export class StackSection extends React.Component { + + render() { + const {title, children} = this.props; + return <React.Fragment> + <Title title={title} /> + {children} + </React.Fragment>; + } + +} \ No newline at end of file diff --git a/web/app/brep/geom/curves/brepCurve.js b/web/app/brep/geom/curves/brepCurve.js index bdadbe5b..8b5284a8 100644 --- a/web/app/brep/geom/curves/brepCurve.js +++ b/web/app/brep/geom/curves/brepCurve.js @@ -18,8 +18,16 @@ export default class BrepCurve { this.uMid = (uMax - uMin) * 0.5; } + get degree() { + return this.impl.degree(); + } + translate(vector) { const tr = new Matrix3().translate(vector.x, vector.y, vector.z); + return this.transform(tr); + } + + transform(tr) { return new BrepCurve(this.impl.transform(tr.toArray()), this.uMin, this.uMax); } diff --git a/web/app/cad/actions/coreActions.js b/web/app/cad/actions/coreActions.js index aad54724..dedfac07 100644 --- a/web/app/cad/actions/coreActions.js +++ b/web/app/cad/actions/coreActions.js @@ -65,7 +65,7 @@ export default [ label: 'deselect all', info: 'deselect everything', }, - invoke: (context) => context.services.selection.deselectAll() + invoke: (context) => context.services.pickControl.deselectAll() }, { diff --git a/web/app/cad/craft/cutExtrude/cutExtrude.js b/web/app/cad/craft/cutExtrude/cutExtrude.js index 42aee3b3..250ed2c7 100644 --- a/web/app/cad/craft/cutExtrude/cutExtrude.js +++ b/web/app/cad/craft/cutExtrude/cutExtrude.js @@ -27,18 +27,42 @@ export function doOperation(params, {cadRegistry, sketcher}, cut) { export function resolveExtrudeVector(cadRegistry, face, params, invert) { let vector = null; - if (params.vector) { - const datumAxis = cadRegistry.findDatumAxis(params.vector); + if (params.datumAxisVector) { + const datumAxis = cadRegistry.findDatumAxis(params.datumAxisVector); if (datumAxis) { vector = datumAxis.dir; invert = false; } + } else if (params.edgeVector) { + const edge = cadRegistry.findEdge(params.edgeVector); + const curve = edge.brepEdge.curve; + if (curve.degree === 1) { + vector = edge.brepEdge.curve.tangentAtParam(edge.brepEdge.curve.uMin); + if (vector.dot(face.csys.z) < 0 === invert) { + vector = vector.negate(); + } + invert = false; + } + } else if (params.sketchSegmentVector) { + const mSegment = cadRegistry.findSketchObject(params.sketchSegmentVector); + if (mSegment.sketchPrimitive.isSegment) { + let [a, b] = mSegment.sketchPrimitive.tessellate().map(mSegment.face.sketchToWorldTransformation.apply); + vector = b.minus(a)._normalize(); + if (vector.dot(face.csys.z) < 0 === invert) { + vector._negate(); + } + invert = false; + } } if (!vector) { invert = !invert; vector = face.csys.z; } + if (params.flip) { + invert = !invert; + } + let value = params.value; if (value < 0) { value = Math.abs(value); @@ -60,12 +84,17 @@ export function getEncloseDetails(params, contours, target, csys, sketchSurface, if (invert) contour.reverse(); const lidPath = []; - let applyPrism = !math.equal(params.prism, 1); + let applyPrism = !math.equal(params.prism, 1); + let prismTr = null; + if (applyPrism) { + prismTr = new Matrix3(); + prismTr.scale(params.prism, params.prism, params.prism); + } for (let i = 0; i < basePath.length; ++i) { const curve = basePath[i]; let lidCurve = curve.translate(target); if (applyPrism) { - lidCurve = lidCurve.offset(params.prism); + lidCurve = lidCurve.transform(prismTr); } lidPath.push(lidCurve); } diff --git a/web/app/cad/craft/cutExtrude/cutOperation.js b/web/app/cad/craft/cutExtrude/cutOperation.js index 8dcaa6db..d117cffd 100644 --- a/web/app/cad/craft/cutExtrude/cutOperation.js +++ b/web/app/cad/craft/cutExtrude/cutOperation.js @@ -4,6 +4,7 @@ import {createPreviewGeomProvider} from './previewer'; import {Cut} from './cutExtrude'; import {requiresFaceSelection} from '../../actions/actionHelpers'; import schema from './schema'; +import {onParamsUpdate} from './extrudeOperation'; export default { id: 'CUT', @@ -11,6 +12,7 @@ export default { icon: 'img/cad/cut', info: 'makes a cut based on 2D sketch', paramsInfo: ({value}) => `(${r(value)})`, + onParamsUpdate, previewGeomProvider: createPreviewGeomProvider(true), run: Cut, actionParams: { diff --git a/web/app/cad/craft/cutExtrude/extrudeOperation.js b/web/app/cad/craft/cutExtrude/extrudeOperation.js index c6efaeb0..dda12b91 100644 --- a/web/app/cad/craft/cutExtrude/extrudeOperation.js +++ b/web/app/cad/craft/cutExtrude/extrudeOperation.js @@ -11,6 +11,7 @@ export default { icon: 'img/cad/extrude', info: 'extrudes 2D sketch', paramsInfo: ({value}) => `(${r(value)})`, + onParamsUpdate, previewGeomProvider: createPreviewGeomProvider(false), run: Extrude, actionParams: { @@ -20,3 +21,14 @@ export default { schema }; +const INVARIANT = ['datumAxisVector', 'edgeVector', 'sketchSegmentVector']; + +export function onParamsUpdate(params, name, value) { + if (INVARIANT.includes(name)) { + INVARIANT.forEach(param => { + if (param !== name) { + delete params[param]; + } + }) + } +} \ No newline at end of file diff --git a/web/app/cad/craft/cutExtrude/form.js b/web/app/cad/craft/cutExtrude/form.js index 89931d3f..f264cfcf 100644 --- a/web/app/cad/craft/cutExtrude/form.js +++ b/web/app/cad/craft/cutExtrude/form.js @@ -1,7 +1,8 @@ import React from 'react'; -import {NumberField} from '../wizard/components/form/Fields'; +import {CheckboxField, NumberField} from '../wizard/components/form/Fields'; import {Group} from '../wizard/components/form/Form'; import Entity from '../wizard/components/form/Entity'; +import {StackSection} from 'ui/components/controls/FormSection'; export default function (valueLabel) { return function PrismForm() { @@ -9,7 +10,12 @@ export default function (valueLabel) { <NumberField name='value' defaultValue={50} label={valueLabel}/> <NumberField name='prism' defaultValue={1} min={0} step={0.1} round={1}/> <Entity name='face'/> - <Entity name='vector' /> + <StackSection title='vector'> + <Entity label='datum axis' name='datumAxisVector' /> + <Entity label='edge' name='edgeVector' /> + <Entity label='sketch segment' name='sketchSegmentVector' /> + <CheckboxField name='flip'/> + </StackSection> </Group>; }; } \ No newline at end of file diff --git a/web/app/cad/craft/cutExtrude/schema.js b/web/app/cad/craft/cutExtrude/schema.js index 4cb5674d..5d680392 100644 --- a/web/app/cad/craft/cutExtrude/schema.js +++ b/web/app/cad/craft/cutExtrude/schema.js @@ -20,9 +20,22 @@ export default { type: 'face', defaultValue: {type: 'selection'} }, - vector: { + datumAxisVector: { type: 'datumAxis', + optional: true + }, + edgeVector: { + type: 'edge', optional: true, - defaultValue: {type: 'selection'} + accept: edge => edge.brepEdge.curve.degree === 1 + }, + sketchSegmentVector: { + type: 'sketchObject', + optional: true, + accept: obj => obj.isSegment + }, + flip: { + type: 'boolean', + defaultValue: false, } } diff --git a/web/app/cad/craft/e0/e0Plugin.js b/web/app/cad/craft/e0/e0Plugin.js index 6c83809a..731d401a 100644 --- a/web/app/cad/craft/e0/e0Plugin.js +++ b/web/app/cad/craft/e0/e0Plugin.js @@ -188,7 +188,7 @@ function readSketch(face, request, sketcher) { let paths = sketch.fetchContours().map(c => { let path = []; c.segments.forEach(s => { - if (s.isCurve()) { + if (s.isCurve) { if (s.constructor.name === 'Circle') { const dir = face.csys.z.data(); path.push({TYPE: CURVE_TYPES.CIRCLE, c: tr.apply(s.c).data(), dir, r: s.r}); diff --git a/web/app/cad/craft/materializeParams.js b/web/app/cad/craft/materializeParams.js index fb2397d1..7a78960f 100644 --- a/web/app/cad/craft/materializeParams.js +++ b/web/app/cad/craft/materializeParams.js @@ -11,7 +11,7 @@ export default function materializeParams(services, params, schema, result, erro } let value = params[field]; if (value === undefined || value === null || value === '') { - if (!md.optional) { + if (!md.optional && !md.hasOwnProperty('defaultValue')) { errors.push({path: [...parentPath, field], message: 'required'}); } } else { @@ -41,6 +41,8 @@ export default function materializeParams(services, params, schema, result, erro if (typeof value !== 'string') { errors.push({path: [...parentPath, field], message: 'not a string type'}); } + } else if (md.type === 'boolean') { + value = !!value; } else if (md.type === 'enum') { if (md.values.indexOf(value) === -1) { value = md.defaultValue || md.values[0]; diff --git a/web/app/cad/craft/operationPlugin.js b/web/app/cad/craft/operationPlugin.js index b2754c84..537b123e 100644 --- a/web/app/cad/craft/operationPlugin.js +++ b/web/app/cad/craft/operationPlugin.js @@ -88,6 +88,7 @@ function createSchemaIndex(schema) { } } return {entitiesByType, entitiesByParam, - entityParams: Object.keys(entitiesByParam) + entityParams: Object.keys(entitiesByParam), + params: Object.keys(schema) }; } \ No newline at end of file diff --git a/web/app/cad/craft/wizard/components/Wizard.jsx b/web/app/cad/craft/wizard/components/Wizard.jsx index 80fbc880..3c408af7 100644 --- a/web/app/cad/craft/wizard/components/Wizard.jsx +++ b/web/app/cad/craft/wizard/components/Wizard.jsx @@ -22,7 +22,7 @@ export default class Wizard extends React.Component { }; updateParam = (name, value) => { - this.props.context.updateParams(params => params[name] = value); + this.props.context.updateParam(name, value); }; setActiveParam = param => { diff --git a/web/app/cad/craft/wizard/wizardPlugin.js b/web/app/cad/craft/wizard/wizardPlugin.js index 85e051db..3ccac786 100644 --- a/web/app/cad/craft/wizard/wizardPlugin.js +++ b/web/app/cad/craft/wizard/wizardPlugin.js @@ -3,6 +3,8 @@ import initializeBySchema from '../intializeBySchema'; import {clone, EMPTY_OBJECT} from 'gems/objects'; import materializeParams from '../materializeParams'; import {createFunctionList} from 'gems/func'; +import {onParamsUpdate} from '../cutExtrude/extrudeOperation'; +import {propsChangeTracker} from '../../../../../modules/lstream/utils'; export function activate(ctx) { @@ -84,9 +86,18 @@ export function activate(ctx) { const state$ = state({}); const updateParams = mutator => workingRequest$.mutate(data => mutator(data.params)); const updateState = mutator => state$.mutate(state => mutator(state)); + const updateParam = (name, value) => { + updateParams(params => { + if (operation.onParamsUpdate) { + operation.onParamsUpdate(params, name, value, params[name]); + } + params[name] = value; + }); + }; + const disposerList = createFunctionList(); wizCtx = { - workingRequest$, materializedWorkingRequest$, state$, updateParams, updateState, + workingRequest$, materializedWorkingRequest$, state$, updateParams, updateParam, updateState, operation, changingHistory, addDisposer: disposerList.add, dispose: disposerList.call, diff --git a/web/app/cad/craft/wizard/wizardSelectionPlugin.js b/web/app/cad/craft/wizard/wizardSelectionPlugin.js index 87dafd7c..9be09d6d 100644 --- a/web/app/cad/craft/wizard/wizardSelectionPlugin.js +++ b/web/app/cad/craft/wizard/wizardSelectionPlugin.js @@ -31,20 +31,20 @@ export function activate(ctx) { }); } -const singleUpdater = (params, param, id) => params[param] = id; -const arrayUpdater = (params, param, id) => { - let arr = params[param]; +const singleValue = (id, current) => id; +const arrayValue = (id, arr) => { if (!arr) { - params[param] = [id]; + return [id]; } if (arr.indexOf(id) === -1) { arr.push(id); } + return arr; }; function createPickHandlerFromSchema(wizCtx) { - function update(paramsMutator, paramToMakeActive) { - wizCtx.updateParams(paramsMutator); + function update(param, value, paramToMakeActive) { + wizCtx.updateParam(param, value); wizCtx.updateState(state => { state.activeParam = paramToMakeActive; }); @@ -62,11 +62,9 @@ function createPickHandlerFromSchema(wizCtx) { const activeEntity = state.activeParam && entitiesByParam[state.activeParam]; function select(param, entity, md, id) { - const updater = md.type === 'array' ? arrayUpdater : singleUpdater; + const valueGetter = md.type === 'array' ? arrayValue : singleValue; let paramToMakeActive = getNextActiveParam(param, entity, md); - update(params => { - updater(params, param, id); - }, paramToMakeActive); + update(param, valueGetter(id, params[param]), paramToMakeActive); } function getNextActiveParam(currParam, entity, currMd) { @@ -98,16 +96,12 @@ function createPickHandlerFromSchema(wizCtx) { for (let param of entityParams) { let val = params[param]; if (val === id) { - update(params => { - params[param] = undefined; - }, param); + update(param, undefined, param); return true; } else if (Array.isArray(val)) { let index = val.indexOf(id); if (index !== -1) { - update(params => { - params[param].splice(index, 1) - }, param); + update(param, params[param].splice(index, 1), param); return true; } } diff --git a/web/app/cad/sketch/sketchModel.js b/web/app/cad/sketch/sketchModel.js index fd93ae42..0e75514c 100644 --- a/web/app/cad/sketch/sketchModel.js +++ b/web/app/cad/sketch/sketchModel.js @@ -28,10 +28,14 @@ class SketchPrimitive { return tessellation; } - isCurve() { + get isCurve() { return this.constructor.name !== 'Segment'; } + get isSegment() { + return !this.isCurve; + } + toNurbs(csys) { let verbNurbs = this.toVerbNurbs(csys.outTransformation.apply, csys); if (this.inverted) { diff --git a/web/app/cad/sketch/sketchReader.js b/web/app/cad/sketch/sketchReader.js index ee9f422b..43e6f569 100644 --- a/web/app/cad/sketch/sketchReader.js +++ b/web/app/cad/sketch/sketchReader.js @@ -144,7 +144,7 @@ function findClosedContoursFromPairedCurves(segments, result) { for (let j = i; j < segments.length; j++) { if (i == j) continue; const s2 = segments[j]; - if (s1.isCurve() && s2.isCurve()) { + if (s1.isCurve && s2.isCurve) { let paired = false; if (math.strictEqual2D(s1.a, s2.a) && math.strictEqual2D(s1.b, s2.b)) { paired = true;