implement generic boolean operation

This commit is contained in:
Val Erastov 2018-11-29 17:10:58 -08:00
parent 7b01e228ff
commit 2a2b221f5d
21 changed files with 228 additions and 60 deletions

View file

@ -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);
}

View file

@ -35,3 +35,12 @@ export class StateStream extends Emitter {
}
}
export class DistinctStateStream extends StateStream {
next(v) {
if (this._value === v) {
return;
}
super.next(v);
}
}

View file

@ -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: {

View file

@ -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 <Group>
<SingleEntity name='operandA' label='operand A' entity='shell' selectionIndex={0} />
<SingleEntity name='operandB' label='operand B' entity='shell' selectionIndex={1} />
<BooleanChoice name='type' strict/>
</Group>;
}

View file

@ -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
}
})

View file

@ -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
};

View file

@ -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;
},

View file

@ -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() {

View file

@ -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)]
}
}
}
}

View file

@ -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 <ComboBoxField {...props}>
<ComboBoxOption value=''>{'<none>'}</ComboBoxOption>
{!strict && <ComboBoxOption value=''>{'<none>'}</ComboBoxOption>}
<ComboBoxOption value={'INTERSECT'}>intersect</ComboBoxOption>
<ComboBoxOption value={'SUBTRACT'}>subtract</ComboBoxOption>
<ComboBoxOption value={'UNION'}>union</ComboBoxOption>

View file

@ -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 <Field>
<Label>{label||camelCaseSplitToStr(name)}:</Label>
<div>{selection ?
@ -44,3 +47,7 @@ export default class SingleEntity extends React.Component {
</Field>;
}
}
SingleEntity.defaultProps = {
selectionIndex: 0
};

View file

@ -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();
}
});
}

View file

@ -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];

View file

@ -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']
},
{

View file

@ -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
])
}

View file

@ -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);

View file

@ -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 => {

View file

@ -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);
}

View file

@ -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();

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB