entities selection rework, separate selection in normal mode from wizard mode

This commit is contained in:
Val Erastov 2018-12-05 00:41:16 -08:00
parent 8b2ad87513
commit 14cdeb9356
47 changed files with 737 additions and 307 deletions

View file

@ -47,4 +47,21 @@ export function indexArray(array, getKey, getValue = v => v) {
return obj;
}
export function addToListInMap(map, key, value) {
let list = map.get(key);
if (!list) {
list = [];
map.set(key, list);
}
list.push(value);
}
export function removeInPlace(arr, val) {
let index = arr.indexOf(val);
if (index !== -1) {
arr.splice(index, 1);
}
return arr;
}
export const EMPTY_ARRAY = Object.freeze([]);

44
modules/gems/linkedMap.js Normal file
View file

@ -0,0 +1,44 @@
export class OrderedMap {
constructor() {
this.order = [];
this.map = new Map();
}
forEach(cb) {
this.order.forEach(key => cb(this.map.get(key), key));
}
set(key, value) {
if (!this.map.has(key)) {
this.map.set(key, value);
this.order.push(key)
}
}
get(key) {
return this.map(key);
}
has(key) {
this.map.has(key);
}
delete(key) {
this.map.delete(key);
let index = this.order.indexOf(key);
if (index !== -1) {
this.order.splice(index, 1);
}
}
clear() {
this.map.clear();
this.order.length = 0;
}
get size() {
return this.map.size;
}
}

View file

@ -26,6 +26,10 @@ export class StreamBase {
this.attach(v => stateStream.next(v));
return stateStream;
}
distinct() {
return new DistinctStream(this);
}
}
const {MapStream} = require('./map');
@ -33,3 +37,4 @@ const {FilterStream} = require('./filter');
const {StateStream} = require('./state');
const {PairwiseStream} = require('./pairwise');
const {ScanStream} = require('./scan');
const {DistinctStream} = require('./distinct');

View file

@ -0,0 +1,19 @@
import {StreamBase} from './base';
export class DistinctStream extends StreamBase {
constructor(stream) {
super();
this.stream = stream;
this.latest = undefined;
}
attach(observer) {
return this.stream.attach(v => {
if (this.latest !== v) {
observer(v);
this.latest = v;
}
});
}
}

View file

@ -12,6 +12,8 @@ export function stream() {
return new Emitter();
}
export const eventStream = stream;
export function combine(...streams) {
return new CombineStream(streams);
}

View file

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

View file

@ -1,5 +1,11 @@
@import "~ui/styles/theme.less";
.root {
display: flex;
justify-content: space-between;
align-items: baseline;
}
}
.active {
background-color: @color-highlight
}

View file

@ -5,11 +5,12 @@ import InputControl from './InputControl';
export default class NumberControl extends React.Component {
render() {
let {onChange, value} = this.props;
let {onChange, onFocus, value} = this.props;
return <InputControl type='number'
onWheel={this.onWheel}
value={ value }
onChange={this.onChange}
onChange={this.onChange}
onFocus={onFocus}
inputRef={input => this.input = input} />
}

View file

@ -5,9 +5,9 @@ import InputControl from './InputControl';
export default class TextControl extends React.Component {
render() {
let {onChange, initValue} = this.props;
let {onChange, initValue, onFocus} = this.props;
return <InputControl type='text'
defaultValue={initValue}
onChange={e => onChange(e.target.value)} />
defaultValue={initValue}
onChange={e => onChange(e.target.value)} onFocus={onFocus} />
}
}

View file

@ -10,7 +10,8 @@ export function enableAnonymousActionHint({streams, services}) {
requester: 'anonymous'
});
setTimeout(() => {
if (!streams.action.hint.value.requester === 'anonymous') {
let value = streams.action.hint.value;
if (value && value.requester !== 'anonymous') {
services.action.showHintFor(null);
}
}, 1000);

View file

@ -1,11 +1,10 @@
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';
import EntityList from '../wizard/components/form/EntityList';
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} />
<EntityList name='operandA' label='operand A' entity='shell' selectionIndex={0} />
<EntityList name='operandB' label='operand B' entity='shell' selectionIndex={1} />
</Group>;
}

View file

@ -1,10 +1,12 @@
export default {
operandA: {
type: 'shell',
markColor: 0xC9FFBC,
defaultValue: {type: 'selection'}
},
operandB: {
type: 'shell',
markColor: 0xFFBEB4,
defaultValue: {type: 'selection'}
}
};

View file

@ -11,10 +11,6 @@ const run = type => (params, services) => {
const paramsInfo = ({operandA, operandB}) => `(${operandA}, ${operandB})`;
const selectionMode = {
shell: true
};
export const intersectionOperation = {
id: 'INTERSECTION',
label: 'intersection',
@ -24,7 +20,6 @@ export const intersectionOperation = {
form: BooleanWizard,
schema,
run: run('INTERSECTION'),
selectionMode
};
export const subtractOperation = {
@ -36,7 +31,6 @@ export const subtractOperation = {
form: BooleanWizard,
schema,
run: run('SUBTRACT'),
selectionMode
};
export const unionOperation = {
@ -48,5 +42,4 @@ export const unionOperation = {
form: BooleanWizard,
schema,
run: run('UNION'),
selectionMode
};

View file

@ -1,7 +1,7 @@
import React from 'react';
import {NumberField} from '../wizard/components/form/Fields';
import {Group} from '../wizard/components/form/Form';
import SingleEntity from '../wizard/components/form/SingleEntity';
import EntityList from '../wizard/components/form/EntityList';
export default function (valueLabel) {
return function PrismForm() {
@ -10,7 +10,7 @@ export default function (valueLabel) {
<NumberField name='prism' defaultValue={1} min={0} step={0.1} round={1}/>
<NumberField name='angle' defaultValue={0}/>
<NumberField name='rotation' defaultValue={0} step={5}/>
<SingleEntity entity='face' name='face'/>
<EntityList entity='face' name='face'/>
</Group>;
};
}

View file

@ -11,7 +11,7 @@ export function createPreviewGeomProvider(inversed) {
return function previewGeomProvider(params, services) {
const face = services.cadRegistry.findFace(params.face);
if (!face) return null;
if (!face || !face.sketch) return null;
let sketch = face.sketch.fetchContours();
const encloseDetails = getEncloseDetails(params, sketch, face.csys, face.surface, !inversed);

View file

@ -1,13 +1,13 @@
import React from 'react';
import {Group} from '../../wizard/components/form/Form';
import {NumberField} from '../../wizard/components/form/Fields';
import SingleEntity from '../../wizard/components/form/SingleEntity';
import EntityList from '../../wizard/components/form/EntityList';
export default function CreateDatumWizard() {
return <Group>
<NumberField name='x' label='X' />
<NumberField name='y' label='Y' />
<NumberField name='z' label='Z' />
<SingleEntity name='face' label='off of' entity='face' />
<EntityList name='face' label='off of' entity='face' />
</Group>;
}

View file

@ -143,13 +143,15 @@ function operationHandler(id, request, services) {
return singleShellRespone(face.shell, data);
}
case 'FILLET': {
let edge = services.cadRegistry.findEdge(request.edges[0].edge);
let engineReq = Object.assign({}, request, {
let edge = services.cadRegistry.findEdge(request.edges[0]);
let engineReq = {
deflection: DEFLECTION,
solid: edge.shell.brepShell.data.externals.ptr,
edges: request.edges.map(e => Object.assign({}, e, {edge: services.cadRegistry.findEdge(e.edge).brepEdge.data.externals.ptr}))
});
edges: request.edges.map(e => ({
edge: services.cadRegistry.findEdge(e).brepEdge.data.externals.ptr,
thickness: request.thickness
}))
};
let data = callEngine(engineReq, Module._SPI_fillet);
return singleShellRespone(edge.shell, data);

View file

@ -1,16 +1,11 @@
import React from 'react';
import MultiEntity from '../wizard/components/form/MultiEntity';
import {NumberField} from '../wizard/components/form/Fields';
import filletSchema from './schema';
import EntityList from '../wizard/components/form/EntityList';
import {Group} from '../wizard/components/form/Form';
export default function FilletWizard() {
let {defaultValue: {itemField, entity}, schema} = filletSchema.edges;
return <MultiEntity schema={schema}
entity={entity}
itemField={itemField}
name='edges'>
return <Group>
<EntityList name='edges' entity='edge' />
<NumberField name='thickness' />
</MultiEntity>;
</Group>;
}

View file

@ -1,19 +1,13 @@
export default {
edges: {
type: 'array',
itemType: 'edge',
defaultValue: {
type: 'selection',
entity: 'edge',
itemField: 'edge'
},
schema: {
edge: {
type: 'edge',
},
thickness: {
type: 'number',
defaultValue: 20
}
}
},
thickness: {
type: 'number',
defaultValue: 20
}
}

View file

@ -1,4 +1,5 @@
import {ENTITIES} from '../scene/entites';
import {isEntityType} from './schemaUtils';
export default function initializeBySchema(schema, context) {
let fields = Object.keys(schema);
@ -7,21 +8,31 @@ export default function initializeBySchema(schema, context) {
let val;
let md = schema[field];
if (md.type === 'array') {
if (md.defaultValue) {
if (md.defaultValue.type === 'selection') {
let {itemField, entity} = md.defaultValue;
val = context.streams.selection[entity].value.map(s => {
let item = initializeBySchema(md.schema, context);
item[itemField] = s;
return item;
});
if (md.itemType === 'object') {
if (md.defaultValue) {
if (md.defaultValue.type === 'selection') {
let {itemField, entity} = md.defaultValue;
val = context.streams.selection[entity].value.map(s => {
let item = initializeBySchema(md.schema, context);
item[itemField] = s;
return item;
});
} else {
val = md.defaultValue;
}
} else {
val = md.defaultValue;
val = [];
}
} else if (isEntityType(md.itemType)) {
if (md.defaultValue && md.defaultValue.type === 'selection') {
val = [...context.streams.selection[md.itemType].value];
} else {
val = []
}
} else {
val = [];
throw 'unsupport';
}
} else if (ENTITIES.indexOf(md.type) !== -1 && md.defaultValue && md.defaultValue.type === 'selection') {
} else if (isEntityType(md.type) && md.defaultValue && md.defaultValue.type === 'selection') {
val = context.streams.selection[md.type].value[0];
} else if (md.type === 'object') {
val = initializeBySchema(md.schema, context);

View file

@ -1,4 +1,4 @@
import {ENTITIES} from '../scene/entites';
import {isEntityType} from './schemaUtils';
export default function materializeParams(services, params, schema, result, errors, parentPath) {
@ -45,7 +45,7 @@ export default function materializeParams(services, params, schema, result, erro
if (md.values.indexOf(value) === -1) {
errors.push({path: [...parentPath, field], message: 'invalid value'});
}
} else if (ENTITIES.indexOf(md.type) !== -1) {
} else if (isEntityType(md.type)) {
if (typeof value !== 'string') {
errors.push({path: [...parentPath, field], message: 'not a valid model reference'});
}
@ -55,17 +55,27 @@ export default function materializeParams(services, params, schema, result, erro
}
let model = services.cadRegistry.findEntity(md.type, ref);
if (!model) {
errors.push({path: [...parentPath, field], message: 'referrers to nonexistent ' + md.entity});
errors.push({path: [...parentPath, field], message: 'referrers to nonexistent ' + md.type});
}
} else if (md.type === 'array') {
if (!Array.isArray(value)) {
errors.push({path: [...parentPath, field], message: 'not an array type'});
}
value = value.map((item , i) => {
let itemResult = {};
materializeParams(services, item, md.schema, itemResult, errors, [...parentPath, i]);
return itemResult;
});
if (md.itemType === 'object') {
value = value.map((item , i) => {
let itemResult = {};
materializeParams(services, item, md.schema, itemResult, errors, [...parentPath, i]);
return itemResult;
});
} else {
if (isEntityType(md.itemType)) {
value.forEach(ref => {
if (!services.cadRegistry.findEntity(md.itemType, ref)) {
errors.push({path: [...parentPath, field], message: 'referrers to nonexistent ' + md.itemType});
}
})
}
}
}
result[field] = value;
}

View file

@ -1,4 +1,5 @@
import {state} from 'lstream';
import {isEntityType} from './schemaUtils';
export function activate(context) {
let {services} = context;
@ -25,7 +26,9 @@ export function activate(context) {
};
actions.push(opAction);
registry$.mutate(registry => registry[id] = Object.assign({appearance}, descriptor, {
let schemaIndex = createSchemaIndex(descriptor.schema);
registry$.mutate(registry => registry[id] = Object.assign({appearance, schemaIndex}, descriptor, {
run: (request, services) => runOperation(request, descriptor, services)
}));
}
@ -64,3 +67,27 @@ export function activate(context) {
handlers
};
}
function createSchemaIndex(schema) {
const entitiesByType = {};
const entitiesByParam = {};
const entityParams = [];
for (let field of Object.keys(schema)) {
let md = schema[field];
let entityType = md.type === 'array' ? md.itemType : md.type;
if (isEntityType(entityType)) {
let byType = entitiesByType[entityType];
if (!byType) {
byType = [];
entitiesByType[entityType] = byType;
}
byType.push(field);
entitiesByParam[field] = entityType;
entityParams.push(field);
}
}
return {entitiesByType, entitiesByParam,
entityParams: Object.keys(entitiesByParam)
};
}

View file

@ -1,8 +1,6 @@
import React from 'react';
import {Group} from '../../wizard/components/form/Form';
import {NumberField, RadioButtonsField, ReadOnlyValueField} from '../../wizard/components/form/Fields';
import SingleEntity from '../../wizard/components/form/SingleEntity';
import {RadioButton} from 'ui/components/controls/RadioButtons';
import {ReadOnlyValueField} from '../../wizard/components/form/Fields';
export default function PlaneWizard() {

View file

@ -1,7 +1,7 @@
import React from 'react';
import {Group} from '../../wizard/components/form/Form';
import {NumberField, RadioButtonsField} from '../../wizard/components/form/Fields';
import SingleEntity from '../../wizard/components/form/SingleEntity';
import EntityList from '../../wizard/components/form/EntityList';
import {RadioButton} from 'ui/components/controls/RadioButtons';
@ -12,7 +12,7 @@ export default function PlaneWizard() {
<RadioButton value='XZ' />
<RadioButton value='ZY' />
</RadioButtonsField>
<SingleEntity name='parallelTo' entity='face' />
<EntityList name='parallelTo' entity='face' />
<NumberField name='depth' />
</Group>;
}

View file

@ -1,14 +1,14 @@
import React from 'react';
import {CheckboxField, NumberField} from '../wizard/components/form/Fields';
import {Group} from '../wizard/components/form/Form';
import SingleEntity from '../wizard/components/form/SingleEntity';
import EntityList from '../wizard/components/form/EntityList';
export default function RevolveForm() {
return <Group>
<NumberField name='angle' />
<SingleEntity name='face' entity='face' />
<SingleEntity name='axis' entity='sketchObject' />
<EntityList name='face' entity='face' />
<EntityList name='axis' entity='sketchObject' />
<CheckboxField name='cut' />
</Group>;
}

View file

@ -0,0 +1,5 @@
import {ENTITIES} from '../scene/entites';
export function isEntityType(type) {
return ENTITIES.indexOf(type) !== -1
}

View file

@ -46,7 +46,7 @@ function FaceSection({face}) {
const ModelSection = decoratorChain(
mapContext((ctx, props) => ({
select: () => ctx.services.selection[props.type].select([props.model.id])
select: () => ctx.services.pickControl.pick(props.model)
})),
connect((streams, props) => (streams.selection[props.type] || constant([])).map(selection => ({selection}))))
(

View file

@ -15,9 +15,13 @@ import mapContext from 'ui/mapContext';
updateParam: (name, value) => {
let workingRequest$ = ctx.streams.wizard.workingRequest;
if (workingRequest$.value.params && workingRequest$.value.type) {
workingRequest$.mutate(data => data.params[name] = value)
workingRequest$.mutate(data => {
data.params[name] = value;
data.state.activeParam = name;
})
}
}
},
setActiveParam: name => ctx.streams.wizard.workingRequest.mutate(data => data.state && (data.state.activeParam = name))
}))
export default class Wizard extends React.Component {
@ -35,7 +39,7 @@ export default class Wizard extends React.Component {
return <span>operation error</span>;
}
let {left, type, params, resolveOperation, updateParam} = this.props;
let {left, type, params, state, resolveOperation, updateParam, setActiveParam} = this.props;
if (!type) {
return null;
}
@ -50,6 +54,8 @@ export default class Wizard extends React.Component {
let formContext = {
data: params,
activeParam: state.activeParam,
setActiveParam,
updateParam
};

View file

@ -0,0 +1,45 @@
import React from 'react';
import ls from './EntityList.less';
import Label from 'ui/components/controls/Label';
import Field from 'ui/components/controls/Field';
import Fa from 'ui/components/Fa';
import {attachToForm} from './Form';
import {camelCaseSplitToStr} from 'gems/camelCaseSplit';
import {EMPTY_ARRAY, removeInPlace} from '../../../../../../../modules/gems/iterables';
@attachToForm
export default class EntityList extends React.Component {
deselect = (entityId) => {
let {value, onChange} = this.props;
if (Array.isArray(value)) {
onChange(removeInPlace(value, entityId));
} else {
onChange(undefined);
}
};
render() {
let {name, label, active, setActive, value} = this.props;
if (!Array.isArray(value)) {
value = value ? asArray(value) : EMPTY_ARRAY;
}
return <Field active={active} onClick={setActive}>
<Label>{label||camelCaseSplitToStr(name)}:</Label>
<div>{value.length === 0 ?
<span className={ls.emptySelection}>{'<not selected>'}</span> :
value.map((entity, i) => <span className={ls.entityRef} key={i}>
{entity} <span className={ls.rm} onClick={() => this.deselect(entity)}> <Fa icon='times'/></span>
</span>)}
</div>
</Field>;
}
}
function asArray(val) {
_arr[0] = val;
return _arr;
}
const _arr = [];

View file

@ -0,0 +1,23 @@
.emptySelection {
font-style: initial;
color: #BFBFBF;
}
.entityRef {
background-color: #3a687d;
border-radius: 2px;
border: 1px solid #d2d0e0;
padding: 0 3px;
margin: 0 2px 2px 2px;
display: inline-block;
& .rm {
background: none;
cursor: pointer;
&:hover {
color: salmon;
}
&:active {
color: red;
}
}
}

View file

@ -13,8 +13,8 @@ export function Group({children}) {
}
export function formField(Control) {
return function FormPrimitive({label, name, ...props}) {
return <Field>
return function FormPrimitive({label, name, active, setActive, ...props}) {
return <Field active={active} onFocus={setActive} onClick={setActive}>
<Label>{label || camelCaseSplitToStr(name)}</Label>
<Control {...props} />
</Field>;
@ -27,8 +27,13 @@ export function attachToForm(Control) {
{
ctx => {
const onChange = val => ctx.updateParam(name, val);
const setActive = val => ctx.setActiveParam(name);
return <React.Fragment>
<Control value={ctx.data[name]} onChange={onChange} name={name} {...props} />
<Control value={ctx.data[name]}
onChange={onChange}
name={name} {...props}
setActive={setActive}
active={ctx.activeParam === name} />
</React.Fragment>;
}
}

View file

@ -44,7 +44,8 @@ export default class MultiEntity extends React.Component {
updateParam: (name, value) => {
data[name] = value;
ctx.updateParam(this.props.name, this.props.value);
}
},
...ctx
};
let {itemField} = this.props;
let entityId = data[itemField];

View file

@ -1,53 +0,0 @@
import React from 'react';
import mapContext from 'ui/mapContext';
import ls from './SingleEntity.less';
import Label from 'ui/components/controls/Label';
import Field from 'ui/components/controls/Field';
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}) => ({
streams,
findEntity: services.cadRegistry.findEntity
}))
export default class SingleEntity extends React.Component {
componentDidMount() {
let {streams, entity, onChange, value, selectionIndex, findEntity} = this.props;
let selection$ = streams.selection[entity];
if (value && findEntity(entity, value)) {
if (selectionIndex === 0) {
selection$.next([value]);
}
}
this.detacher = selection$.attach(selection => onChange(selection[selectionIndex]));
}
componentWillUnmount() {
this.detacher();
}
deselect = () => {
let {streams, entity} = this.props;
streams.selection[entity].next([]);
};
render() {
let {name, label, streams, selectionIndex, entity} = this.props;
let selection = streams.selection[entity].value[selectionIndex];
return <Field>
<Label>{label||camelCaseSplitToStr(name)}:</Label>
<div>{selection ?
<span>{selection} <Button type='minor' onClick={this.deselect}> <Fa icon='times'/></Button></span> :
<span className={ls.emptySelection}>{'<not selected>'}</span>}</div>
</Field>;
}
}
SingleEntity.defaultProps = {
selectionIndex: 0
};

View file

@ -1,4 +0,0 @@
.emptySelection {
font-style: initial;
color: #BFBFBF;
}

View file

@ -1,4 +1,4 @@
import {state} from 'lstream';
import {stream, state} from 'lstream';
import initializeBySchema from '../intializeBySchema';
import {clone, EMPTY_OBJECT} from 'gems/objects';
@ -45,24 +45,29 @@ export function activate(ctx) {
gotoEditHistoryModeIfNeeded(mod);
});
streams.wizard.workingRequestChanged = stream();
streams.wizard.workingRequest = streams.wizard.effectiveOperation.map(opRequest => {
if (!opRequest.type) {
return EMPTY_OBJECT;
}
let operation = ctx.services.operation.get(opRequest.type);
let params;
if (opRequest.changingHistory) {
params = clone(opRequest.params)
} else {
params = initializeBySchema(operation.schema, ctx);
if (opRequest.initialOverrides) {
applyOverrides(params, opRequest.initialOverrides);
let request = EMPTY_OBJECT;
if (opRequest.type) {
let operation = ctx.services.operation.get(opRequest.type);
let params;
if (opRequest.changingHistory) {
params = clone(opRequest.params)
} else {
params = initializeBySchema(operation.schema, ctx);
if (opRequest.initialOverrides) {
applyOverrides(params, opRequest.initialOverrides);
}
}
request = {
type: opRequest.type,
params,
state: {}
};
}
return {
type: opRequest.type,
params,
}
streams.wizard.workingRequestChanged.next(request);
return request
}).remember(EMPTY_OBJECT);
services.wizard = {
@ -80,7 +85,8 @@ export function activate(ctx) {
},
applyWorkingRequest: () => {
let request = clone(streams.wizard.workingRequest.value);
let {type, params} = streams.wizard.workingRequest.value;
let request = clone({type, params});
if (streams.wizard.insertOperation.value.type) {
ctx.services.craft.modify(request, () => streams.wizard.insertOperation.value = EMPTY_OBJECT);
} else {

View file

@ -1,13 +0,0 @@
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

@ -0,0 +1,149 @@
import {EDGE, FACE, SHELL, SKETCH_OBJECT} from '../../scene/entites';
export function activate(ctx) {
const wizardPickHandler = createPickHandlerFromSchema(ctx);
ctx.streams.wizard.workingRequestChanged.attach(({type, params}) => {
ctx.services.marker.clear();
if (type) {
ctx.services.pickControl.setPickHandler(wizardPickHandler);
} else {
ctx.services.pickControl.setPickHandler(null);
}
});
ctx.streams.wizard.workingRequest.attach(({type, params}) => {
const marker = ctx.services.marker;
marker.startSession();
if (type && params) {
let {schema, schemaIndex} = ctx.services.operation.get(type);
schemaIndex.entityParams.forEach(param => {
let color = schema[param].markColor;
let val = params[param];
let entity = schemaIndex.entitiesByParam[param];
if (Array.isArray(val)) {
val.forEach(id => marker.mark(entity, id, color));
} else {
if (val) {
marker.mark(entity, val, color);
}
}
});
}
marker.commit();
});
}
const singleUpdater = (params, param, id) => params[param] = id;
const arrayUpdater = (params, param, id) => {
let arr = params[param];
if (!arr) {
params[param] = [id];
}
if (arr.indexOf(id) === -1) {
arr.push(id);
}
};
function createPickHandlerFromSchema(ctx) {
return model => {
const modelType = model.TYPE;
const {type: opType, state, params} = ctx.streams.wizard.workingRequest.value;
let {schema, schemaIndex} = ctx.services.operation.get(opType);
const {entitiesByType, entitiesByParam, entityParams} = schemaIndex;
const activeMd = state.activeParam && schema[state.activeParam];
const activeEntity = state.activeParam && entitiesByParam[state.activeParam];
function select(param, entity, md, id) {
const updater = md.type === 'array' ? arrayUpdater : singleUpdater;
let paramToMakeActive = getNextActiveParam(param, md);
ctx.streams.wizard.workingRequest.mutate(r => {
updater(r.params, param, id);
r.state.activeParam = paramToMakeActive;
});
}
function getNextActiveParam(currParam, currMd) {
if (currMd.type !== 'array') {
let entityGroup = entitiesByType[currMd.type];
if (entityGroup) {
const index = entityGroup.indexOf(currParam);
const nextIndex = (index + 1) % entityGroup.length;
return entityGroup[nextIndex];
}
}
return currParam;
}
function selectActive(id) {
select(state.activeParam, activeEntity, activeMd, id);
}
function selectToFirst(entity, id) {
let entities = entitiesByType[entity];
if (!entities) {
return false;
}
let param = entities[0];
select(param, entity, schema[param], id);
}
function deselectIfNeeded(id) {
for (let param of entityParams) {
let val = params[param];
if (val === id) {
ctx.streams.wizard.workingRequest.mutate(r => {
r.params[param] = undefined;
r.state.activeParam = param;
});
return true;
} else if (Array.isArray(val)) {
let index = val.indexOf(id);
if (index !== -1) {
ctx.streams.wizard.workingRequest.mutate(r => {
r.params[param].splice(index, 1);
r.state.activeParam = param;
});
return true;
}
}
}
}
if (deselectIfNeeded(model.id)) {
return false;
} else if (model.shell) {
if (deselectIfNeeded(model.shell.id)) {
return false;
}
}
if (modelType === FACE) {
if (activeEntity === SHELL) {
selectActive(model.shell.id);
} else if (activeEntity === FACE) {
selectActive(model.id);
} else {
if (!selectToFirst(FACE, model.id)) {
selectToFirst(SHELL, model.shell.id)
}
}
} else if (modelType === SKETCH_OBJECT) {
if (activeEntity === SKETCH_OBJECT) {
selectActive(model.id);
} else {
selectToFirst(SKETCH_OBJECT, model.id);
}
} else if (modelType === EDGE) {
if (activeEntity === EDGE) {
selectActive(model.id);
} else {
selectToFirst(EDGE, model.id);
}
}
return false;
};
}

View file

@ -4,13 +4,13 @@ import * as DomPlugin from '../dom/domPlugin';
import * as PickControlPlugin from '../scene/controls/pickControlPlugin';
import * as MouseEventSystemPlugin from '../scene/controls/mouseEventSystemPlugin';
import * as ScenePlugin from '../scene/scenePlugin';
import * as SelectionMarkerPlugin from '../scene/selectionMarker/selectionMarkerPlugin';
import * as MarkerPlugin from '../scene/selectionMarker/markerPlugin';
import * as ActionSystemPlugin from '../actions/actionSystemPlugin';
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 WizardSelectionPlugin from '../craft/wizard/wizardSelectionPlugin';
import * as PreviewPlugin from '../preview/previewPlugin';
import * as OperationPlugin from '../craft/operationPlugin';
import * as ExtensionsPlugin from '../craft/extensionsPlugin';
@ -23,6 +23,7 @@ import * as SketcherPlugin from '../sketch/sketcherPlugin';
import * as ExportPlugin from '../exportPlugin';
import * as TpiPlugin from '../tpi/tpiPlugin';
import * as ViewSyncPlugin from '../scene/viewSyncPlugin';
import * as EntityContextPlugin from '../scene/entityContextPlugin';
import * as E0Plugin from '../craft/e0/e0Plugin';
import PartModellerPlugins from '../part/partModelerPlugins';
@ -60,12 +61,13 @@ export default function startApplication(callback) {
DomPlugin,
ScenePlugin,
MouseEventSystemPlugin,
MarkerPlugin,
PickControlPlugin,
SelectionMarkerPlugin,
EntityContextPlugin,
SketcherPlugin,
...applicationPlugins,
ViewSyncPlugin,
WizardSelectionModeSwitcherPlugin
WizardSelectionPlugin
];
let allPlugins = [...preUIPlugins, ...plugins];

View file

@ -1,7 +1,7 @@
import * as DomPlugin from './dom/domPlugin';
import * as PickControlPlugin from './scene/controls/pickControlPlugin';
import * as ScenePlugin from './scene/scenePlugin';
import * as SelectionMarkerPlugin from './scene/selectionMarker/selectionMarkerPlugin';
import * as SelectionMarkerPlugin from './scene/selectionMarker/markerPlugin';
export default [
DomPlugin,

View file

@ -0,0 +1,28 @@
import {state} from '../../../../../modules/lstream';
import {DATUM, EDGE, FACE, SHELL, SKETCH_OBJECT} from '../entites';
const SELECTABLE_ENTITIES = [FACE, EDGE, SKETCH_OBJECT, DATUM, SHELL];
export function defineDefaultSelectionState(ctx) {
ctx.streams.selection = {
};
SELECTABLE_ENTITIES.forEach(entity => {
ctx.streams.selection[entity] = state([]);
});
SELECTABLE_ENTITIES.forEach(entity => {
let entitySelectApi = {
objects: [],
single: undefined
};
ctx.services.selection[entity] = entitySelectApi;
let selectionState = streams.selection[entity];
selectionState.attach(selection => {
entitySelectApi.objects = selection.map(id => services.cadRegistry.findEntity(entity, id));
entitySelectApi.single = entitySelectApi.objects[0];
});
entitySelectApi.select = selection => selectionState.value = selection;
});
}

View file

@ -10,7 +10,7 @@ export const PICK_KIND = {
EDGE: mask.type(3)
};
const SELECTABLE_ENTITIES = [FACE, EDGE, SKETCH_OBJECT, DATUM, SHELL];
export const SELECTABLE_ENTITIES = [FACE, EDGE, SKETCH_OBJECT, DATUM, SHELL];
const DEFAULT_SELECTION_MODE = Object.freeze({
shell: false,
@ -23,7 +23,36 @@ const DEFAULT_SELECTION_MODE = Object.freeze({
export function activate(context) {
const {services, streams} = context;
initStateAndServices(context);
const defaultHandler = (model, event) => {
const type = model.TYPE;
let selectionMode = DEFAULT_SELECTION_MODE;
let modelId = model.id;
if (type === FACE) {
if (selectionMode.shell) {
if (dispatchSelection(SHELL, model.shell.id, event)) {
return false;
}
} else {
if (dispatchSelection(FACE, modelId, event)) {
services.cadScene.showGlobalCsys(model.csys);
return false;
}
}
} else if (type === SKETCH_OBJECT) {
if (dispatchSelection(SKETCH_OBJECT, modelId, event)) {
return false;
}
} else if (type === EDGE) {
if (dispatchSelection(EDGE, modelId, event)) {
return false;
}
}
return true;
};
let pickHandler = defaultHandler;
let domElement = services.viewer.sceneSetup.domElement();
domElement.addEventListener('mousedown', mousedown, false);
@ -52,44 +81,33 @@ export function activate(context) {
}
}
function selected(selection, object) {
return selection.value.indexOf(object) !== -1;
function setPickHandler(handler) {
pickHandler = handler || defaultHandler;
services.marker.clear();
}
const deselectAll = () => services.marker.clear();
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 (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)) {
return false;
}
} else if (kind === PICK_KIND.EDGE) {
if (dispatchSelection(streams.selection.edge, modelId, event)) {
return false;
}
}
return true;
});
raycastObjects(event, PICK_KIND.FACE | PICK_KIND.SKETCH | PICK_KIND.EDGE, pickHandler);
}
function dispatchSelection(selection, selectee, event) {
if (selected(selection, selectee)) {
function pick(obj) {
pickHandler(obj, null);
}
function dispatchSelection(entityType, selectee, event) {
let marker = services.marker;
if (marker.isMarked(selectee)) {
return false;
}
let multiMode = event.shiftKey;
selection.update(value => multiMode ? [...value, selectee] : [selectee]);
let multiMode = event && event.shiftKey;
if (multiMode) {
marker.markAdding(entityType, selectee)
} else {
marker.markExclusively(entityType, selectee)
}
return true;
}
@ -108,7 +126,7 @@ export function activate(context) {
if (mask.is(kind, PICK_KIND.SKETCH) && pickResult.object instanceof THREE.Line) {
let sketchObjectV = getAttribute(pickResult.object, SKETCH_OBJECT);
if (sketchObjectV) {
return !visitor(sketchObjectV, PICK_KIND.SKETCH);
return !visitor(sketchObjectV.model, event);
}
}
return false;
@ -117,7 +135,7 @@ export function activate(context) {
if (mask.is(kind, PICK_KIND.EDGE)) {
let edgeV = getAttribute(pickResult.object, EDGE);
if (edgeV) {
return !visitor(edgeV, PICK_KIND.EDGE);
return !visitor(edgeV.model, event);
}
}
return false;
@ -126,7 +144,7 @@ export function activate(context) {
if (mask.is(kind, PICK_KIND.FACE) && !!pickResult.face) {
let faceV = getAttribute(pickResult.face, FACE);
if (faceV) {
return !visitor(faceV, PICK_KIND.FACE);
return !visitor(faceV.model, event);
}
}
return false;
@ -141,64 +159,7 @@ export function activate(context) {
}
}
}
}
export function defineStreams({streams}) {
streams.selection = {
};
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
setPickHandler, deselectAll, pick
};
services.selection = {};
SELECTABLE_ENTITIES.forEach(entity => {
let entitySelectApi = {
objects: [],
single: undefined
};
services.selection[entity] = entitySelectApi;
let selectionState = streams.selection[entity];
selectionState.attach(selection => {
entitySelectApi.objects = selection.map(id => services.cadRegistry.findEntity(entity, id));
entitySelectApi.single = entitySelectApi.objects[0];
});
entitySelectApi.select = selection => selectionState.value = selection;
});
streams.craft.models.attach(() => {
withdrawAll(streams.selection)
});
}
export function withdrawAll(selectionStreams) {
Object.values(selectionStreams).forEach(stream => stream.next([]))
}

View file

@ -0,0 +1,45 @@
import {state} from 'lstream';
import {SELECTABLE_ENTITIES} from './controls/pickControlPlugin';
import {addToListInMap} from 'gems/iterables';
import {EMPTY_ARRAY} from '../../../../modules/gems/iterables';
export function defineStreams(ctx) {
ctx.streams.selection = {};
SELECTABLE_ENTITIES.forEach(entity => {
ctx.streams.selection[entity] = state([]);
});
}
export function activate(ctx) {
ctx.services.selection = {};
SELECTABLE_ENTITIES.forEach(entity => {
let entitySelectApi = {
objects: [],
single: undefined
};
ctx.services.selection[entity] = entitySelectApi;
let selectionState = ctx.streams.selection[entity];
selectionState.attach(selection => {
entitySelectApi.objects = selection.map(id => ctx.services.cadRegistry.findEntity(entity, id));
entitySelectApi.single = entitySelectApi.objects[0];
});
entitySelectApi.select = ids => ctx.services.marker.markArrayExclusively(entity, ids);
});
ctx.services.marker.$markedEntities.attach(marked => {
let byType = new Map();
marked.forEach((obj) => {
addToListInMap(byType, obj.TYPE, obj);
});
SELECTABLE_ENTITIES.forEach(entityType => {
let entities = byType.get(entityType);
if (entities) {
ctx.streams.selection[entityType].next(entities.map(obj => obj.id));
} else {
ctx.streams.selection[entityType].next(EMPTY_ARRAY);
}
});
})
}

View file

@ -0,0 +1,110 @@
import {OrderedMap} from 'gems/linkedMap';
import {eventStream} from 'lstream';
export function activate(ctx) {
ctx.services.marker = createMarker(ctx.services.cadRegistry.findEntity, ctx.services.viewer.requestRender);
ctx.streams.craft.models.attach(() => {
ctx.services.marker.clear();
});
}
function createMarker(findEntity, requestRender) {
let markingSession = new Set();
let marked = new OrderedMap();
let needUpdate = false;
let sessionInProgress = false;
let $markedEntities = eventStream();
const notify = () => $markedEntities.next(marked);
const isMarked = id => marked.has(id);
function doMark(entity, id, color) {
let mObj = findEntity(entity, id);
marked.set(id, mObj);
mObj.ext.view && mObj.ext.view.mark(color);
}
function doWithdraw(obj) {
marked.delete(obj.id);
obj.ext.view && obj.ext.view.withdraw();
}
function onUpdate() {
requestRender();
notify();
}
function clear() {
if (marked.size !== 0) {
marked.forEach(m => m.ext.view && m.ext.view.withdraw());
marked.clear();
onUpdate();
}
}
function withdrawAllOfType(entityType) {
marked.forEach(obj => {
if (obj.TYPE === entityType) {
doWithdraw(obj);
}
});
}
function markExclusively(entity, id, color) {
withdrawAllOfType(entity);
doMark(entity, id, color);
onUpdate();
}
function markArrayExclusively(entity, ids, color) {
withdrawAllOfType(entity);
ids.forEach(id => doMark(entity, id, color));
onUpdate();
}
function markAdding(entity, id, color) {
if (!marked.has(id)) {
doMark(entity, id, color);
onUpdate();
}
}
function startSession() {
markingSession.clear();
sessionInProgress = true;
needUpdate = false;
}
function mark(entity, id, color) {
if (!sessionInProgress) {
throw 'can be called only withing a session';
}
markingSession.add(id);
if (!marked.has(id)) {
doMark(entity, id, color);
needUpdate = true;
}
}
function commit() {
marked.forEach((obj) => {
if (!markingSession.has(obj.id)) {
doWithdraw(obj);
needUpdate = true;
}
});
if (needUpdate) {
onUpdate();
}
sessionInProgress = false;
needUpdate = false;
markingSession.clear();
}
return {
clear, startSession, mark, commit, markExclusively, markArrayExclusively, markAdding, isMarked, $markedEntities
};
}

View file

@ -3,15 +3,15 @@ import {AbstractSelectionMarker, setFacesColor} from "./abstractSelectionMarker"
export class SelectionMarker extends AbstractSelectionMarker {
constructor(context, selectionColor, readOnlyColor, defaultColor) {
constructor(context, markColor, readOnlyColor, defaultColor) {
super(context, 'face');
this.selectionColor = selectionColor;
this.markColor = markColor;
this.defaultColor = defaultColor;
this.readOnlyColor = readOnlyColor;
}
mark(sceneFace) {
this.setColor(sceneFace, this.selectionColor, this.readOnlyColor);
this.setColor(sceneFace, this.markColor, this.readOnlyColor);
}
unMark(sceneFace) {

View file

@ -1,27 +0,0 @@
import {EDGE, FACE, SHELL, SKETCH_OBJECT} from '../entites';
import {findDiff} from '../../../../../modules/gems/iterables';
export function activate({streams, services}) {
let selectionSync = entity => ([old, curr]) => {
let [, toWithdraw, toMark] = findDiff(old, curr);
toWithdraw.forEach(id => {
let model = services.cadRegistry.findEntity(entity, id);
if (model) {
model.ext.view.withdraw();
}
});
toMark.forEach(id => {
let model = services.cadRegistry.findEntity(entity, id);
if (model) {
model.ext.view.mark();
}
});
services.viewer.requestRender();
};
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));
}

View file

@ -0,0 +1,17 @@
import {findDiff} from 'gems/iterables';
export const selectionSynchronizer = (entity, findEntity, color) => ([old, curr]) => {
let [, toWithdraw, toMark] = findDiff(old, curr);
toWithdraw.forEach(id => {
let model = findEntity(entity, id);
if (model) {
model.ext.view.withdraw();
}
});
toMark.forEach(id => {
let model = findEntity(entity, id);
if (model) {
model.ext.view.mark(color);
}
});
};

View file

@ -52,8 +52,8 @@ export class InPlaceSketcher {
let viewer3d = this.ctx.services.viewer;
viewer3d.sceneSetup.trackballControls.removeEventListener( 'change', this.onCameraChange);
this.face = null;
this.viewer.dispose();
this.viewer.canvas.parentNode.removeChild(this.viewer.canvas);
this.viewer.dispose();
this.ctx.streams.sketcher.sketchingFace.value = null;
viewer3d.requestRender();
}