diff --git a/modules/lstream/index.js b/modules/lstream/index.js index effc8227..10582200 100644 --- a/modules/lstream/index.js +++ b/modules/lstream/index.js @@ -1,5 +1,5 @@ import {CombineStream} from './combine'; -import {StateStream} from './state'; +import {DistinctStateStream, StateStream} from './state'; import {Emitter} from './emitter'; import {ExternalStateStream} from './external'; import {MergeStream} from './merge'; @@ -24,6 +24,10 @@ export function state(initialValue) { return new StateStream(initialValue); } +export function distinctState(initialValue) { + return new DistinctStateStream(initialValue); +} + export function externalState(get, set) { return new ExternalStateStream(get, set); } diff --git a/modules/lstream/state.js b/modules/lstream/state.js index de80b75d..bc0cc0a1 100644 --- a/modules/lstream/state.js +++ b/modules/lstream/state.js @@ -35,3 +35,12 @@ export class StateStream extends Emitter { } } +export class DistinctStateStream extends StateStream { + + next(v) { + if (this._value === v) { + return; + } + super.next(v); + } +} diff --git a/web/app/cad/actions/operationActions.js b/web/app/cad/actions/operationActions.js index e4cc68b2..af6f3b66 100644 --- a/web/app/cad/actions/operationActions.js +++ b/web/app/cad/actions/operationActions.js @@ -1,6 +1,7 @@ -import * as Operations from '../craft/operations' -import * as ActionHelpers from './actionHelpers' +import * as Operations from '../craft/operations'; +import * as ActionHelpers from './actionHelpers'; +// L E G A C Y const OPERATION_ACTIONS = [ { id: 'SHELL', @@ -9,33 +10,6 @@ const OPERATION_ACTIONS = [ }, ...requiresFaceSelection(1) }, - { - id: 'SPHERE', - appearance: { - info: 'creates new object sphere' - }, - }, - { - id: 'INTERSECTION', - appearance: { - info: 'intersection operation on two solids', - }, - ...requiresSolidSelection(2) - }, - { - id: 'DIFFERENCE', - appearance: { - info: 'difference operation on two solids', - }, - ...requiresSolidSelection(2) - }, - { - id: 'UNION', - appearance: { - info: 'union operation on two solids', - }, - ...requiresSolidSelection(2) - }, { id: 'IMPORT_STL', appearance: { diff --git a/web/app/cad/craft/boolean/BooleanWizard.jsx b/web/app/cad/craft/boolean/BooleanWizard.jsx new file mode 100644 index 00000000..982bbd88 --- /dev/null +++ b/web/app/cad/craft/boolean/BooleanWizard.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import {Group} from '../wizard/components/form/Form'; +import BooleanChoice from '../wizard/components/form/BooleanChioce'; +import SingleEntity from '../wizard/components/form/SingleEntity'; + +export default function BooleanWizard() { + return + + + + ; +} \ No newline at end of file diff --git a/web/app/cad/craft/boolean/booleanOpSchema.js b/web/app/cad/craft/boolean/booleanOpSchema.js new file mode 100644 index 00000000..f59ef23c --- /dev/null +++ b/web/app/cad/craft/boolean/booleanOpSchema.js @@ -0,0 +1,15 @@ +export default defaultValue => ({ + operandA: { + type: 'shell', + defaultValue: {type: 'selection'} + }, + operandB: { + type: 'shell', + defaultValue: {type: 'selection'} + }, + type: { + type: 'enum', + values: ['INTERSECT', 'SUBTRACT', 'UNION'], + defaultValue + } +}) diff --git a/web/app/cad/craft/boolean/booleanOperation.js b/web/app/cad/craft/boolean/booleanOperation.js new file mode 100644 index 00000000..b21fe157 --- /dev/null +++ b/web/app/cad/craft/boolean/booleanOperation.js @@ -0,0 +1,52 @@ +import schema from './booleanOpSchema'; +import BooleanWizard from './BooleanWizard'; + +function run(params, services) { + return services.craftEngine.boolean({ + type: params.type, + operandsA: [services.cadRegistry.findShell(params.operandA)], + operandsB: [services.cadRegistry.findShell(params.operandB)] + }); +} + +const paramsInfo = ({operandA, operandB}) => `(${operandA}, ${operandB})`; + +const selectionMode = { + shell: true +}; + +export const intersectionOperation = { + id: 'INTERSECTION', + label: 'intersection', + icon: 'img/cad/intersection', + info: 'intersection operation on two shells', + paramsInfo, + form: BooleanWizard, + schema: schema('INTERSECTION'), + run, + selectionMode +}; + +export const subtractOperation = { + id: 'SUBTRACT', + label: 'subtract', + icon: 'img/cad/subtract', + info: 'subtract operation on two shells', + paramsInfo, + form: BooleanWizard, + schema: schema('SUBTRACT'), + run, + selectionMode +}; + +export const unionOperation = { + id: 'UNION', + label: 'union', + icon: 'img/cad/union', + info: 'union operation on two shells', + paramsInfo, + form: BooleanWizard, + schema: schema('UNION'), + run, + selectionMode +}; diff --git a/web/app/cad/craft/cadRegistryPlugin.js b/web/app/cad/craft/cadRegistryPlugin.js index 6fb5b786..bf2654c7 100644 --- a/web/app/cad/craft/cadRegistryPlugin.js +++ b/web/app/cad/craft/cadRegistryPlugin.js @@ -1,4 +1,4 @@ -import {DATUM, EDGE, FACE, SKETCH_OBJECT} from '../scene/entites'; +import {DATUM, EDGE, FACE, SHELL, SKETCH_OBJECT} from '../scene/entites'; import {MShell} from '../model/mshell'; @@ -14,7 +14,17 @@ export function activate({streams, services}) { function getAllShells() { return streams.cadRegistry.shells.value; } - + + function findShell(shellId) { + let shells = getAllShells(); + for (let shell of shells) { + if (shell.id === shellId) { + return shell; + } + } + return null; + } + function findFace(faceId) { let shells = getAllShells(); for (let shell of shells) { @@ -55,6 +65,7 @@ export function activate({streams, services}) { function findEntity(entity, id) { switch (entity) { case FACE: return findFace(id); + case SHELL: return findShell(id); case EDGE: return findEdge(id); case SKETCH_OBJECT: return findSketchObject(id); case DATUM: return findDatum(id); @@ -63,7 +74,7 @@ export function activate({streams, services}) { } services.cadRegistry = { - getAllShells, findFace, findEdge, findSketchObject, findEntity, findDatum, + getAllShells, findShell, findFace, findEdge, findSketchObject, findEntity, findDatum, get modelIndex() { return streams.cadRegistry.modelIndex.value; }, diff --git a/web/app/cad/craft/defaultCraftEngine.js b/web/app/cad/craft/defaultCraftEngine.js index 8d8681ce..6d2d111f 100644 --- a/web/app/cad/craft/defaultCraftEngine.js +++ b/web/app/cad/craft/defaultCraftEngine.js @@ -1,12 +1,11 @@ export default { - createBox: params => notImplemented, createSphere: params => notImplemented, createCone: params => notImplemented, createCylinder: params => notImplemented, createTorus: params => notImplemented, - + boolean: params => notImplemented, } function notImplemented() { diff --git a/web/app/cad/craft/e0/e0Plugin.js b/web/app/cad/craft/e0/e0Plugin.js index 6257a450..6ae7d016 100644 --- a/web/app/cad/craft/e0/e0Plugin.js +++ b/web/app/cad/craft/e0/e0Plugin.js @@ -23,12 +23,15 @@ export function activate(ctx) { loadWasm(ctx); ctx.services.operation.handlers.push(operationHandler); + function shellsToPointers(shells) { + return shells.filter(managedByE0).map(m => m.brepShell.data.externals.ptr); + } function booleanBasedOperation(engineParams, params, impl) { engineParams.deflection = DEFLECTION; if (params.boolean && BOOLEAN_TYPES[params.boolean.type] > 0) { engineParams.boolean = { type: BOOLEAN_TYPES[params.boolean.type], - operands: params.boolean.operands.filter(managedByE0).map(m => m.brepShell.data.externals.ptr), + operands: shellsToPointers(params.boolean.operands), tolerance: TOLERANCE, } } @@ -83,6 +86,21 @@ export function activate(ctx) { csys: writeCsys(params.csys), r: params.radius, }, params, Module._SPI_sphere); + }, + boolean: function({type, operandsA, operandsB}) { + let engineParams = { + type: BOOLEAN_TYPES[type], + operandsA: shellsToPointers(operandsA), + operandsB: shellsToPointers(operandsB), + tolerance: TOLERANCE, + deflection: DEFLECTION, + }; + let data = callEngine(engineParams, Module._SPI_boolean); + let consumed = [...operandsA, ...operandsB]; + return { + consumed, + created: [readShellData(data.result, consumed, operandsA[0].csys)] + } } } } diff --git a/web/app/cad/craft/wizard/components/form/BooleanChioce.jsx b/web/app/cad/craft/wizard/components/form/BooleanChioce.jsx index c7dd000e..7baeb55c 100644 --- a/web/app/cad/craft/wizard/components/form/BooleanChioce.jsx +++ b/web/app/cad/craft/wizard/components/form/BooleanChioce.jsx @@ -2,9 +2,9 @@ import React from 'react'; import {ComboBoxOption} from 'ui/components/controls/ComboBoxControl'; import {ComboBoxField} from './Fields'; -export default function BooleanChoice(props) { +export default function BooleanChoice({strict, ...props}) { return - {''} + {!strict && {''}} intersect subtract union diff --git a/web/app/cad/craft/wizard/components/form/SingleEntity.jsx b/web/app/cad/craft/wizard/components/form/SingleEntity.jsx index 4ecfefb2..0944e195 100644 --- a/web/app/cad/craft/wizard/components/form/SingleEntity.jsx +++ b/web/app/cad/craft/wizard/components/form/SingleEntity.jsx @@ -7,6 +7,7 @@ import Fa from 'ui/components/Fa'; import Button from 'ui/components/controls/Button'; import {attachToForm} from './Form'; import {camelCaseSplitToStr} from 'gems/camelCaseSplit'; +import NumberControl from '../../../../../../../modules/ui/components/controls/NumberControl'; @attachToForm @mapContext(({streams, services}) => ({ @@ -16,12 +17,14 @@ import {camelCaseSplitToStr} from 'gems/camelCaseSplit'; export default class SingleEntity extends React.Component { componentDidMount() { - let {streams, entity, onChange, value, findEntity} = this.props; + let {streams, entity, onChange, value, selectionIndex, findEntity} = this.props; let selection$ = streams.selection[entity]; if (findEntity(entity, value)) { - selection$.next([value]); + if (selectionIndex === 0) { + selection$.next([value]); + } } - this.detacher = selection$.attach(selection => onChange(selection[0])); + this.detacher = selection$.attach(selection => onChange(selection[selectionIndex])); } componentWillUnmount() { @@ -34,8 +37,8 @@ export default class SingleEntity extends React.Component { }; render() { - let {name, label, streams, entity} = this.props; - let selection = streams.selection[entity].value[0]; + let {name, label, streams, selectionIndex, entity} = this.props; + let selection = streams.selection[entity].value[selectionIndex]; return
{selection ? @@ -44,3 +47,7 @@ export default class SingleEntity extends React.Component { ; } } + +SingleEntity.defaultProps = { + selectionIndex: 0 +}; \ No newline at end of file diff --git a/web/app/cad/craft/wizard/wizardSelectionModeSwitcherPlugin.js b/web/app/cad/craft/wizard/wizardSelectionModeSwitcherPlugin.js new file mode 100644 index 00000000..c50168c7 --- /dev/null +++ b/web/app/cad/craft/wizard/wizardSelectionModeSwitcherPlugin.js @@ -0,0 +1,13 @@ + +export function activate(ctx) { + ctx.streams.wizard.workingRequest.attach(({type}) => { + if (type) { + let operation = ctx.services.operation.get(type); + if (operation.selectionMode) { + ctx.services.pickControl.setSelectionMode(operation.selectionMode); + } + } else { + ctx.services.pickControl.switchToDefaultSelectionMode(); + } + }); +} \ No newline at end of file diff --git a/web/app/cad/init/startApplication.js b/web/app/cad/init/startApplication.js index fb564c4c..d14b97b1 100644 --- a/web/app/cad/init/startApplication.js +++ b/web/app/cad/init/startApplication.js @@ -10,6 +10,7 @@ import * as UiPlugin from '../dom/uiPlugin'; import * as MenuPlugin from '../dom/menu/menuPlugin'; import * as KeyboardPlugin from '../keyboard/keyboardPlugin'; import * as WizardPlugin from '../craft/wizard/wizardPlugin'; +import * as WizardSelectionModeSwitcherPlugin from '../craft/wizard/wizardSelectionModeSwitcherPlugin'; import * as PreviewPlugin from '../preview/previewPlugin'; import * as OperationPlugin from '../craft/operationPlugin'; import * as ExtensionsPlugin from '../craft/extensionsPlugin'; @@ -63,7 +64,8 @@ export default function startApplication(callback) { SelectionMarkerPlugin, SketcherPlugin, ...applicationPlugins, - ViewSyncPlugin + ViewSyncPlugin, + WizardSelectionModeSwitcherPlugin ]; let allPlugins = [...preUIPlugins, ...plugins]; diff --git a/web/app/cad/part/menuConfig.js b/web/app/cad/part/menuConfig.js index f9470840..b00c9659 100644 --- a/web/app/cad/part/menuConfig.js +++ b/web/app/cad/part/menuConfig.js @@ -22,14 +22,14 @@ export default [ label: 'bool', cssIcons: ['pie-chart'], info: 'set of available boolean operations', - actions: ['INTERSECTION', 'DIFFERENCE', 'UNION'] + actions: ['INTERSECTION', 'SUBTRACT', 'UNION'] }, { id: 'main', label: 'start', cssIcons: ['rocket'], info: 'common set of actions', - actions: ['EXTRUDE', 'CUT', 'SHELL', '-', 'INTERSECTION', 'DIFFERENCE', 'UNION', '-', 'PLANE', 'BOX', 'SPHERE', '-', + actions: ['EXTRUDE', 'CUT', 'SHELL', '-', 'INTERSECTION', 'SUBTRACT', 'UNION', '-', 'PLANE', 'BOX', 'SPHERE', '-', 'EditFace', '-', 'DeselectAll', 'RefreshSketches'] }, { diff --git a/web/app/cad/part/partOperationsPlugin.js b/web/app/cad/part/partOperationsPlugin.js index e42f23f2..afaf18f2 100644 --- a/web/app/cad/part/partOperationsPlugin.js +++ b/web/app/cad/part/partOperationsPlugin.js @@ -12,6 +12,7 @@ import sphereOperation from '../craft/primitives/sphere/sphereOperation'; import cylinderOperation from '../craft/primitives/cylinder/cylinderOperation'; import torusOperation from '../craft/primitives/torus/torusOperation'; import coneOperation from '../craft/primitives/cone/coneOperation'; +import {intersectionOperation, subtractOperation, unionOperation} from '../craft/boolean/booleanOperation'; export function activate({services}) { services.operation.registerOperations([ @@ -28,6 +29,9 @@ export function activate({services}) { sphereOperation, cylinderOperation, torusOperation, - coneOperation + coneOperation, + intersectionOperation, + subtractOperation, + unionOperation ]) } \ No newline at end of file diff --git a/web/app/cad/part/uiConfigPlugin.js b/web/app/cad/part/uiConfigPlugin.js index 268778b5..8a0b2ab8 100644 --- a/web/app/cad/part/uiConfigPlugin.js +++ b/web/app/cad/part/uiConfigPlugin.js @@ -15,7 +15,7 @@ export function activate({services, streams}) { ['ShowSketches', {label: 'sketches'}], ['DeselectAll', {label: null}], ['ToggleCameraMode', {label: null}] ]; - streams.ui.toolbars.headsUp.value = ['PLANE', 'EditFace', 'EXTRUDE', 'CUT', 'REVOLVE', 'FILLET', 'INTERSECTION', 'DIFFERENCE', 'UNION']; + streams.ui.toolbars.headsUp.value = ['PLANE', 'EditFace', 'EXTRUDE', 'CUT', 'REVOLVE', 'FILLET', 'INTERSECTION', 'SUBTRACT', 'UNION']; streams.ui.toolbars.auxiliary.value = ['Save', 'StlExport']; services.action.registerActions(CoreActions); diff --git a/web/app/cad/scene/controls/pickControlPlugin.js b/web/app/cad/scene/controls/pickControlPlugin.js index b88b919a..7c6140d8 100644 --- a/web/app/cad/scene/controls/pickControlPlugin.js +++ b/web/app/cad/scene/controls/pickControlPlugin.js @@ -1,7 +1,8 @@ import * as mask from 'gems/mask' import {getAttribute, setAttribute} from 'scene/objectData'; -import {FACE, EDGE, SKETCH_OBJECT, DATUM} from '../entites'; +import {FACE, EDGE, SKETCH_OBJECT, DATUM, SHELL} from '../entites'; import {state} from 'lstream'; +import {distinctState} from '../../../../../modules/lstream'; export const PICK_KIND = { FACE: mask.type(1), @@ -9,7 +10,16 @@ export const PICK_KIND = { EDGE: mask.type(3) }; -const SELECTABLE_ENTITIES = [FACE, EDGE, SKETCH_OBJECT, DATUM]; +const SELECTABLE_ENTITIES = [FACE, EDGE, SKETCH_OBJECT, DATUM, SHELL]; + +const DEFAULT_SELECTION_MODE = Object.freeze({ + shell: false, + vertex: false, + face: true, + edge: true, + sketchObject: true, + datum: true +}); export function activate(context) { const {services, streams} = context; @@ -48,11 +58,18 @@ export function activate(context) { function handlePick(event) { raycastObjects(event, PICK_KIND.FACE | PICK_KIND.SKETCH | PICK_KIND.EDGE, (view, kind) => { + let selectionMode = streams.pickControl.selectionMode.value; let modelId = view.model.id; if (kind === PICK_KIND.FACE) { - if (dispatchSelection(streams.selection.face, modelId, event)) { - services.cadScene.showGlobalCsys(view.model.csys); - return false; + if (selectionMode.shell) { + if (dispatchSelection(streams.selection.shell, view.model.shell.id, event)) { + return false; + } + } else { + if (dispatchSelection(streams.selection.face, modelId, event)) { + services.cadScene.showGlobalCsys(view.model.csys); + return false; + } } } else if (kind === PICK_KIND.SKETCH) { if (dispatchSelection(streams.selection.sketchObject, modelId, event)) { @@ -132,10 +149,35 @@ export function defineStreams({streams}) { SELECTABLE_ENTITIES.forEach(entity => { streams.selection[entity] = state([]); }); + streams.pickControl = { + selectionMode: distinctState(DEFAULT_SELECTION_MODE) + } } function initStateAndServices({streams, services}) { + streams.pickControl.selectionMode.pairwise().attach(([prev, curr]) => { + SELECTABLE_ENTITIES.forEach(entity => { + if (prev[entity] !== curr[entity]) { + streams.selection[entity].next([]); + } + }); + }); + + function setSelectionMode(selectionMode) { + streams.pickControl.selectionMode.next({ + ...DEFAULT_SELECTION_MODE, ...selectionMode + }); + } + + function switchToDefaultSelectionMode() { + streams.pickControl.selectionMode.next(DEFAULT_SELECTION_MODE); + } + + services.pickControl = { + setSelectionMode, switchToDefaultSelectionMode + }; + services.selection = {}; SELECTABLE_ENTITIES.forEach(entity => { diff --git a/web/app/cad/scene/selectionMarker/selectionMarkerPlugin.js b/web/app/cad/scene/selectionMarker/selectionMarkerPlugin.js index 781b084f..9edf18fc 100644 --- a/web/app/cad/scene/selectionMarker/selectionMarkerPlugin.js +++ b/web/app/cad/scene/selectionMarker/selectionMarkerPlugin.js @@ -1,4 +1,4 @@ -import {EDGE, FACE, SKETCH_OBJECT} from '../entites'; +import {EDGE, FACE, SHELL, SKETCH_OBJECT} from '../entites'; import {findDiff} from '../../../../../modules/gems/iterables'; export function activate({streams, services}) { @@ -20,10 +20,8 @@ export function activate({streams, services}) { }; streams.selection.face.pairwise([]).attach(selectionSync(FACE)); + streams.selection.shell.pairwise([]).attach(selectionSync(SHELL)); streams.selection.edge.pairwise([]).attach(selectionSync(EDGE)); streams.selection.sketchObject.pairwise([]).attach(selectionSync(SKETCH_OBJECT)); - // new SelectionMarker(context, 0xFAFAD2, 0xFF0000, null); - // new SketchSelectionMarker(context, createLineMaterial(0xFF0000, 6 / DPR)); - // new EdgeSelectionMarker(context, 0xFA8072); } diff --git a/web/app/cad/scene/views/shellView.js b/web/app/cad/scene/views/shellView.js index 032ec2cd..e0be6ab3 100644 --- a/web/app/cad/scene/views/shellView.js +++ b/web/app/cad/scene/views/shellView.js @@ -2,7 +2,7 @@ import {View} from './view'; import * as SceneGraph from '../../../../../modules/scene/sceneGraph'; import {setAttribute} from '../../../../../modules/scene/objectData'; import {createSolidMaterial} from '../wrappers/sceneObject'; -import {FaceView} from './faceView'; +import {FaceView, SELECTION_COLOR} from './faceView'; import {EdgeView} from './edgeView'; import {SHELL} from '../entites'; @@ -44,8 +44,16 @@ export class ShellView extends View { SceneGraph.addToGroup(this.edgeGroup, edgeView.rootGroup); this.edgeViews.push(edgeView); } - } - + } + + mark(color) { + this.faceViews.forEach(faceView => faceView.setColor(color || SELECTION_COLOR)); + } + + withdraw(color) { + this.faceViews.forEach(faceView => faceView.setColor(null)); + } + dispose() { for (let faceView of this.faceViews) { faceView.dispose(); diff --git a/web/img/cad/difference32.png b/web/img/cad/subtract32.png similarity index 100% rename from web/img/cad/difference32.png rename to web/img/cad/subtract32.png diff --git a/web/img/cad/difference96.png b/web/img/cad/subtract96.png similarity index 100% rename from web/img/cad/difference96.png rename to web/img/cad/subtract96.png