event stream api for organizing UI

This commit is contained in:
Val Erastov 2018-06-22 00:31:33 -07:00
parent 7cbd001efc
commit e226d416ee
44 changed files with 532 additions and 463 deletions

View file

@ -1,3 +1,4 @@
{
"presets": ["es2015", "stage-2", "react", "flow"]
"presets": ["es2015", "stage-2", "react", "flow"],
"plugins": ["transform-decorators-legacy"]
}

View file

@ -1,61 +0,0 @@
export class Store {
constructor() {
this.state = {};
this.listeners = {};
this.locked = false;
}
subscribe(key, callback) {
let listenerList = this.listeners[key];
if (listenerList === undefined) {
listenerList = [];
this.listeners[key] = listenerList;
}
listenerList.push(callback);
return callback;
};
unsubscribe(key, callback) {
const listenerList = this.listeners[key];
for (let i = 0; i < listenerList.length; i++) {
if (listenerList[i] === callback) {
listenerList.splice(i, 1);
return;
}
}
};
dispatch(key, newValue, oldValue) {
if (this.locked === true) {
throw 'concurrent state modification';
}
this.locked = true;
try {
let listenerList = this.listeners[key];
if (listenerList !== undefined) {
for (let i = 0; i < listenerList.length; i++) {
const callback = listenerList[i];
try {
callback(newValue, oldValue, this);
} catch(e) {
console.error(e);
}
}
}
} finally {
this.locked = false;
}
};
set(key, value) {
let oldValue = this.state[key];
this.state[key] = value;
this.dispatch(key, value, oldValue);
}
get(key) {
return this.state[key];
}
}

9
modules/context/index.js Normal file
View file

@ -0,0 +1,9 @@
import Bus from '../bus';
import {observable} from 'mobx';
export default {
services: {},
streams: {},
//@deprecated
bus: new Bus()
};

50
modules/lstream/base.js Normal file
View file

@ -0,0 +1,50 @@
export class StreamBase {
attach() {}
next(value) {}
map(fn) {
return new MapStream(this, fn);
}
filter(predicate) {
return new FilterStream(this, predicate);
}
}
export class MapStream extends StreamBase {
constructor(stream, fn) {
super();
this.stream = stream;
this.fn = fn;
}
attach(observer) {
return this.stream.attach(val => observer(this.fn(val)));
}
static create = (stream, fn) => new MapStream(stream, fn);
}
export class FilterStream extends StreamBase {
constructor(stream, predicate) {
super();
this.stream = stream;
this.predicate = predicate;
}
attach(observer) {
return this.stream.attach(val => {
if (this.predicate(val)) {
observer(val);
}
});
}
static create = (stream, predicate) => new FilterStream(stream, predicate);
}

View file

@ -0,0 +1,38 @@
import {StreamBase} from './base';
export class CombineStream extends StreamBase {
constructor(streams) {
super();
this.streams = streams;
this.values = this.streams.map(() => NOT_INITIALIZED);
this.ready = false;
}
attach(observer) {
let detachers = new Array(this.streams.length);
this.streams.forEach((s, i) => {
detachers[i] = s.attach(value => {
this.values[i] = value;
if (!this.ready) {
this.ready = this.isReady();
}
if (this.ready) {
observer(this.values);
}
});
});
return () => detachers.forEach(d => d());
}
isReady() {
for (let val of this.values) {
if (val === NOT_INITIALIZED) {
return false;
}
}
return true;
}
}
const NOT_INITIALIZED = {};

View file

@ -0,0 +1,41 @@
import {StreamBase} from './base';
const READY = 0;
const EMITTING = 1;
export class Emitter extends StreamBase {
constructor() {
super();
this.observers = [];
this.state = READY;
}
attach(observer) {
this.observers.push(observer);
return () => this.detach(observer);
}
detach(callback) {
for (let i = this.observers.length - 1; i >= 0 ; i--) {
if (this.observers[i] === callback) {
this.observers.splice(i, 1);
}
}
};
next(value) {
if (this.state === EMITTING) {
console.warn('recursive dispatch');
return;
}
try {
this.state = EMITTING;
for (let i = 0; i < this.observers.length; i++) {
this.observers[i](value);
}
} finally {
this.state = READY;
}
}
}

22
modules/lstream/index.js Normal file
View file

@ -0,0 +1,22 @@
import {CombineStream} from './combine';
import {StateStream} from './state';
import {Emitter} from './emitter';
import {FilterStream, MapStream} from './base';
export function combine(...streams) {
return new CombineStream(streams);
}
export function stream() {
return new Emitter();
}
export function state(initialValue) {
return new StateStream(initialValue);
}
export const map = MapStream.create;
export const filter = FilterStream.create;
export const merger = states => states.reduce((acc, v) => Object.assign(acc, v), {});

38
modules/lstream/state.js Normal file
View file

@ -0,0 +1,38 @@
import {Emitter} from './emitter';
export class StateStream extends Emitter {
constructor(initialValue) {
super();
this._value = initialValue;
}
get value() {
return this._value;
}
set value(v) {
this.next(v);
}
next(v) {
this._value = v;
super.next(v);
}
update(updater) {
this.value = updater(this._value);
}
mutate(mutator) {
mutator(this._value);
this.next(this._value);
}
attach(observer) {
observer(this._value);
return super.attach(observer);
}
}

29
modules/ui/connect.js Normal file
View file

@ -0,0 +1,29 @@
import React from 'react';
import context from 'context';
export default function connect(streamProvider) {
return function (Component) {
return class Connected extends React.Component {
streamProps = {};
componentWillMount() {
let stream = streamProvider(context.streams, this.props);
this.detacher = stream.attach(data => {
this.streamProps = data;
this.forceUpdate();
});
}
componentWillUnmount() {
this.detacher();
}
render() {
return <Component {...this.streamProps}
{...this.props} />;
}
};
}
}

View file

@ -0,0 +1,9 @@
export default function decoratorChain() {
let decorators = Array.from(arguments);
return function(Component) {
for (let i = decorators.length - 1; i >= 0; i --) {
Component = decorators[i](Component);
}
return Component;
}
}

11
modules/ui/mapContext.js Normal file
View file

@ -0,0 +1,11 @@
import React from 'react';
import context from 'context';
export default function mapContext(mapper) {
return function (Component) {
return function ContextMapper(props) {
let actions = mapper(context, props);
return <Component {...actions} {...props} />
}
}
}

View file

@ -28,22 +28,23 @@
"babel-core": "6.26.0",
"babel-eslint": "8.0.2",
"babel-loader": "7.1.2",
"babel-preset-es2015": "6.24.1",
"babel-preset-stage-2": "6.24.1",
"babel-plugin-transform-decorators-legacy": "1.3.5",
"babel-polyfill": "6.26.0",
"babel-preset-react": "6.24.1",
"babel-preset-es2015": "6.24.1",
"babel-preset-flow": "6.23.0",
"babel-preset-react": "6.24.1",
"babel-preset-stage-2": "6.24.1",
"css-loader": "0.28.7",
"less-loader": "4.0.5",
"eslint": "4.13.1",
"eslint-plugin-babel": "4.1.2",
"eslint-plugin-import": "2.8.0",
"eslint-plugin-react": "7.5.1",
"grunt": "1.0.1",
"grunt-contrib-copy": "1.0.0",
"less-loader": "4.0.5",
"style-loader": "0.13.1",
"webpack": "3.10.0",
"webpack-dev-server": "2.9.7",
"grunt": "1.0.1",
"grunt-contrib-copy": "1.0.0"
"webpack-dev-server": "2.9.7"
},
"dependencies": {
"classnames": "2.2.5",
@ -56,7 +57,6 @@
"json-loader": "0.5.4 ",
"less": "2.7.3",
"libtess": "1.2.2",
"mobx": "^4.3.0",
"mousetrap": "1.6.1",
"numeric": "1.2.6",
"prop-types": "15.6.0",

View file

@ -1,7 +1,7 @@
import React from 'react';
import Window from 'ui/components/Window';
import BrepDebugger from './brepDebugger';
import connect, {PROPAGATE_SELF_PROPS} from 'ui/connect';
import connect, {PROPAGATE_SELF_PROPS} from 'ui/connectLegacy';
import {addToGroup, clearGroup, createGroup, removeFromGroup} from 'scene/sceneGraph';
import {createToken} from 'bus';
import Fa from 'ui/components/Fa';

View file

@ -1,13 +1,11 @@
import {TOKENS as ACTION_TOKENS} from "./actionSystemPlugin";
export function mapActionBehavior(actionIdGetter) {
return ({dispatch}, props) => {
return ({services}, props) => {
const actionId = actionIdGetter(props);
const actionRunToken = ACTION_TOKENS.actionRun(actionId);
let request = {actionId, x:0, y:0};
let canceled = true;
let showed = false;
let shown = false;
function updateCoords({pageX, pageY}) {
request.x = pageX + 10;
@ -15,23 +13,23 @@ export function mapActionBehavior(actionIdGetter) {
}
return {
onClick: data => dispatch(actionRunToken, data),
onClick: e => services.action.run(actionId, e),
onMouseEnter: e => {
updateCoords(e);
canceled = false;
showed = false;
shown = false;
setTimeout(() => {
if (!canceled) {
showed = true;
dispatch(ACTION_TOKENS.SHOW_HINT_FOR, request)
shown = true;
services.action.showHintFor(request)
}
}, 500);
},
onMouseMove: updateCoords,
onMouseLeave: () => {
canceled = true;
if (showed) {
dispatch(ACTION_TOKENS.SHOW_HINT_FOR, null)
if (shown) {
services.action.showHintFor(null)
}
}
}};

View file

@ -1,6 +1,6 @@
export function checkForSelectedFaces(amount) {
return (state, context) => {
state.enabled = context.services.selection.face.objects.length >= amount;
return (state, selection) => {
state.enabled = selection.length >= amount;
if (!state.enabled) {
state.hint = amount === 1 ? 'requires a face to be selected' : 'requires ' + amount + ' faces to be selected';
}
@ -8,7 +8,7 @@ export function checkForSelectedFaces(amount) {
}
export function checkForSelectedSolids(amount) {
return (state, context) => {
return (state, selection, context) => {
state.enabled = context.services.selection.face.objects.length >= amount;
if (!state.enabled) {
state.hint = amount === 1 ? 'requires a solid to be selected' : 'requires ' + amount + ' solids to be selected';
@ -18,14 +18,14 @@ export function checkForSelectedSolids(amount) {
export function requiresFaceSelection(amount) {
return {
listens: ['selection_face'],
listens: streams => streams.selection.face,
update: checkForSelectedFaces(amount)
}
}
export function requiresSolidSelection(amount) {
return {
listens: ['selection_face'],
listens: streams => streams.selection.face,
update: checkForSelectedSolids(amount)
}
}

View file

@ -1,56 +1,62 @@
import {createToken} from 'bus';
import {enableAnonymousActionHint} from "./anonHint";
import {enableAnonymousActionHint} from './anonHint';
import * as stream from 'lstream';
export function activate(context) {
let {bus} = context;
let {bus, streams} = context;
streams.action = {
appearance: {},
state: {},
hint: stream.state(null)
};
let runners = {};
let showAnonymousActionHint = enableAnonymousActionHint(context);
function run(id, data) {
bus.dispatch(TOKENS.actionRun(id), data);
let state = streams.action.state[id].value;
let runner = runners[id];
if (!state||!runner) {
console.warn('request to run nonexistent action')
return;
}
if (state.enabled) {
runner(context, data);
} else {
showAnonymousActionHint(id);
}
}
function register(action) {
bus.enableState(TOKENS.actionAppearance(action.id), action.appearance);
streams.action.appearance[action.id] = stream.state(action.appearance);
runners[action.id] = action.invoke;
let stateToken = TOKENS.actionState(action.id);
let initialState = {
hint: '',
enabled: true,
visible: true
};
if (action.update) {
action.update(initialState, context);
}
bus.enableState(stateToken, initialState);
let actionStateStream = stream.state(initialState);
streams.action.state[action.id] = actionStateStream;
if (action.update && action.listens) {
const stateUpdater = () => {
bus.updateState(stateToken, (actionState) => {
actionState.hint = '';
actionState.enabled = true;
actionState.visible = true;
action.update(actionState, context);
return actionState;
});
};
for (let event of action.listens) {
bus.subscribe(event, stateUpdater);
}
action.listens(streams).attach(data => {
actionStateStream.mutate(v => {
v.hint = '';
v.enabled = true;
v.visible = true;
action.update(v, data, context)
return v;
})
});
}
bus.subscribe(TOKENS.actionRun(action.id), data => {
if (bus.state[stateToken].enabled) {
action.invoke(context, data)
} else {
showAnonymousActionHint(action.id);
}
});
}
bus.enableState(TOKENS.HINT, null);
function registerAction(action) {
register(action);
}
@ -58,40 +64,27 @@ export function activate(context) {
function registerActions(actions) {
actions.forEach(action => register(action));
}
synchActionHint(bus);
context.services.action = {run, registerAction, registerActions}
}
function synchActionHint(bus) {
bus.subscribe(TOKENS.SHOW_HINT_FOR, request => {
function showHintFor(request) {
if (request) {
let {actionId, x, y} = request;
let actionState = bus.getState(TOKENS.actionState(actionId));
let actionAppearance = bus.getState(TOKENS.actionAppearance(actionId));
let {actionId, x, y, requester} = request;
let actionState = streams.action.state[actionId].value;
let actionAppearance = streams.action.appearance[actionId].value;
if (actionState && actionAppearance) {
bus.dispatch(TOKENS.HINT, {
actionId, x, y,
streams.action.hint.value = {
actionId, x, y, requester,
info: actionAppearance.info,
hint: actionState.hint
});
};
}
} else {
bus.dispatch(TOKENS.HINT, null);
if (streams.action.hint.value !== null) {
streams.action.hint.value = null;
}
}
});
}
context.services.action = {run, registerAction, registerActions, showHintFor}
}
export const ACTION_NS = 'action';
export const TOKENS = {
actionState: (actionId) => createToken(ACTION_NS, 'state', actionId),
actionAppearance: (actionId) => createToken(ACTION_NS, 'appearance', actionId),
actionRun: (actionId) => createToken(ACTION_NS, 'run', actionId),
SHOW_HINT_FOR: createToken(ACTION_NS, 'showHintFor'),
HINT: createToken(ACTION_NS, 'hint'),
};

View file

@ -1,21 +1,17 @@
import {TOKENS} from "./actionSystemPlugin";
export function enableAnonymousActionHint({bus, services}) {
export function enableAnonymousActionHint({streams, services}) {
let autoHideCanceled = true;
bus.subscribe(TOKENS.SHOW_HINT_FOR, () => autoHideCanceled = true);
return function(actionId) {
let {left, top} = services.dom.viewerContainer.getBoundingClientRect();
bus.dispatch(TOKENS.SHOW_HINT_FOR, {
services.action.showHintFor({
actionId,
x: left + 100,
y: top + 10
y: top + 10,
requester: 'anonymous'
});
autoHideCanceled = false;
setTimeout(() => {
if (!autoHideCanceled) {
bus.dispatch(TOKENS.SHOW_HINT_FOR, null);
if (!streams.action.hint.value.requester === 'anonymous') {
services.action.showHintFor(null);
}
}, 1000);
}

View file

@ -9,7 +9,7 @@ export default [
icon96: 'img/cad/face-edit96.png',
info: 'open sketcher for a face/plane',
},
listens: ['selection_face'],
listens: streams => streams.selection.face,
update: ActionHelpers.checkForSelectedFaces(1),
invoke: ({services}) => services.sketcher.sketchFace(services.selection.face.single)
},

View file

@ -58,14 +58,14 @@ OPERATION_ACTIONS.forEach(action => mergeInfo(action));
function requiresFaceSelection(amount) {
return {
listens: ['selection_face'],
listens: streams => streams.selection.face,
update: ActionHelpers.checkForSelectedFaces(amount)
}
}
function requiresSolidSelection(amount) {
return {
listens: ['selection_face'],
listens: streams => streams.selection.face,
update: ActionHelpers.checkForSelectedSolids(amount)
}
}

View file

@ -1,14 +0,0 @@
import React from 'react';
import TextControl from "ui/components/controls/TextControl";
import Folder from '../../../../../../modules/ui/components/Folder';
import MDForm from './MDForm';
export default function EdgesSelectionControl({label, edges, onUpdate, itemMetadata}, {services}) {
return <Folder title={label}>
{edges.map((subParams, i) =>
<MDForm metadata={itemMetadata} params={subParams} onUpdate={onUpdate} key={i} />
)}
</Folder>;
}

View file

@ -1,8 +0,0 @@
import React from 'react';
import TextControl from "ui/components/controls/TextControl";
export default function FaceSelectionControl(props) {
return <TextControl {...props} />
}

View file

@ -1,5 +1,5 @@
import React from 'react';
import connect from '../../../../../../modules/ui/connect';
import connect from '../../../../../../modules/ui/connectLegacy';
import {TOKENS as CRAFT_TOKENS} from '../../craftPlugin';
import Wizard from './Wizard';
import {finishHistoryEditing, stepOverridingParams} from '../../craftHistoryUtils';

View file

@ -1,54 +0,0 @@
import React from 'react';
import camelCaseSplit from 'gems/camelCaseSplit';
import Label from 'ui/components/controls/Label';
import NumberControl from 'ui/components/controls/NumberControl';
import RadioButtons, {RadioButton} from 'ui/components/controls/RadioButtons';
import TextControl from 'ui/components/controls/TextControl';
import Field from 'ui/components/controls/Field';
import FaceSelectionControl from './FaceSelectionControl';
import Folder from 'ui/components/Folder';
export default class MDForm extends React.Component {
render() {
let {metadata, data} = this.props;
return metadata.map(({name, label, type, ...options}, index) => {
label = label || uiLabel(name);
let value = data[name];
if (type === 'array') {
return <Folder title={label}>
{value && value.map((itemData, i) =>
<MDForm metadata={options.metadata} data={itemData} key={i} />
)}
</Folder>
} else {
return <Field key={index}>
<Label>{label}</Label>
{
(() => {
let commonProps = {initValue: value};
if (type === 'number') {
return <NumberControl {...commonProps} {...options} />;
} else if (type === 'face') {
return <FaceSelectionControl {...commonProps} {...options} />;
} else if (type === 'choice') {
return <RadioButtons {...commonProps}>
{options.options.map(op => <RadioButton value={op} label={op} key={op}/>)}
</RadioButtons>;
} else {
return <TextControl {...commonProps} {...options} />;
}
})()
}
</Field>
}
});
}
}
function uiLabel(name) {
return camelCaseSplit(name).map(w => w.toLowerCase()).join(' ');
}

View file

@ -9,29 +9,28 @@ import {CURRENT_SELECTION} from '../wizardPlugin';
import ls from './Wizard.less';
import CadError from '../../../../utils/errors';
import {createPreviewer} from '../../../preview/scenePreviewer';
import {observable} from 'mobx';
import {entitySelectionToken} from '../../../scene/controls/pickControlPlugin';
import {EDGE} from '../../../scene/entites';
import {FormContext} from './form/Form';
export default class Wizard extends React.Component {
state = {hasError: false};
constructor({type}) {
constructor({initialState}) {
super();
this.formContext = {
data: {},
data: initialState || {},
onChange: noop
};
}
componentDidMount() {
let {services} = this.context;
let {previewGeomProvider} = services.operation.get(this.props.type);
let previewer = createPreviewer(previewGeomProvider, services);
let preview = previewer(this.formContext.data);
this.formContext.onChange = () => preview.update(this.formContext.data);
this.dispose = () => {
preview.dispose();

View file

@ -1,6 +1,6 @@
import React, {Fragment} from 'react';
import {TOKENS as WIZARD_TOKENS} from '../wizardPlugin';
import connect from 'ui/connect';
import connect from 'ui/connectLegacy';
import Wizard from './Wizard';
import HistoryWizard from './HistoryWizard';

View file

@ -4,10 +4,13 @@ import {entitySelectionToken} from '../../../../scene/controls/pickControlPlugin
import {attachToForm} from './Form';
import Stack from 'ui/components/Stack';
import {FormContext} from '../form/Form';
import mapContext from 'ui/mapContext';
const MultiEntityImpl = attachToForm(class MultiEntityImpl extends React.Component {
@attachToForm
@mapContext(({streams}) => ({streams}))
class MultiEntityImpl extends React.Component {
constructor({entity, itemName, initValue}, {bus}) {
constructor({entity, itemName, initValue}, ) {
super();
this.state = {
value: initValue
@ -24,11 +27,12 @@ const MultiEntityImpl = attachToForm(class MultiEntityImpl extends React.Compone
};
componentDidMount() {
this.context.bus.subscribe(entitySelectionToken(this.props.entity), this.selectionChanged);
let {streams, entity} = this.props;
this.detacher = streams.selection[entity].attach(this.selectionChanged);
}
componentWillUnmount() {
this.context.bus.unsubscribe(entitySelectionToken(this.props.entity), this.selectionChanged);
this.detacher();
}
render() {
@ -51,19 +55,15 @@ const MultiEntityImpl = attachToForm(class MultiEntityImpl extends React.Compone
}
</FormContext.Consumer>;
}
}
static contextTypes = {
bus: PropTypes.object
};
});
export default function MultiEntity(props, {bus}) {
let defaultValue = bus.state[entitySelectionToken(props.entity)].map(id => ({
export default function MultiEntity(props, {streams}) {
let defaultValue = streams.selection[props.entity].value.map(id => ({
[props.itemName]: id
}));
return <MultiEntityImpl defaultValue={defaultValue} {...props}/>
}
MultiEntity.contextTypes = {
bus: PropTypes.object
streams: PropTypes.object
};

View file

@ -2,8 +2,11 @@ import React from 'react';
import PropTypes from 'prop-types';
import {entitySelectionToken} from '../../../../scene/controls/pickControlPlugin';
import {attachToForm} from './Form';
import mapContext from 'ui/mapContext';
const SingleEntityImpl = attachToForm(class SingleEntityImpl extends React.Component {
@attachToForm
@mapContext(({streams}) => ({streams}))
class SingleEntityImpl extends React.Component {
constructor({initValue}) {
super();
@ -19,11 +22,12 @@ const SingleEntityImpl = attachToForm(class SingleEntityImpl extends React.Compo
};
componentDidMount() {
this.context.bus.subscribe(entitySelectionToken(this.props.entity), this.selectionChanged);
let {streams, entity} = this.props;
this.detacher = streams.selection[entity].attach(this.selectionChanged);
}
componentWillUnmount() {
this.context.bus.unsubscribe(entitySelectionToken(this.props.entity), this.selectionChanged);
this.detacher();
}
render() {
@ -31,16 +35,12 @@ const SingleEntityImpl = attachToForm(class SingleEntityImpl extends React.Compo
{this.props.name}: {this.state.selectedItem}
</div>;
}
}
static contextTypes = {
bus: PropTypes.object
};
});
export default function SingleEntity(props, {bus}) {
return <SingleEntityImpl defaultValue={bus.state[entitySelectionToken(props.entity)][0]} {...props}/>
export default function SingleEntity(props, {streams}) {
return <SingleEntityImpl defaultValue={streams.selection[props.entity].value[0]} {...props}/>
}
SingleEntity.contextTypes = {
bus: PropTypes.object
streams: PropTypes.object
};

View file

@ -15,11 +15,13 @@ import curveTess from '../brep/geom/impl/curve/curve-tess';
import tessellateSurface from '../brep/geom/surfaces/surfaceTess';
export function activate({bus, services}) {
export function activate({bus, services, streams}) {
addGlobalDebugActions(services);
services.action.registerActions(DebugActions);
services.menu.registerMenus([DebugMenuConfig]);
bus.updateState(UI_TOKENS.CONTROL_BAR_LEFT, actions => [...actions, 'menu.debug']);
streams.ui.controlBars.left.update(actions => [...actions, 'menu.debug']);
bus.enableState(BREP_DEBUG_WINDOW_VISIBLE, false);
contributeComponent(<BrepDebuggerWindow key='debug.BrepDebuggerWindow' auxGroup={services.cadScene.auxGroup} />);
}
@ -287,7 +289,7 @@ const DebugActions = [
label: 'print face',
info: 'print a face out as JSON',
},
listens: ['selection_face'],
listens: streams => streams.selection.face,
update: checkForSelectedFaces(1),
invoke: ({services: {selection}}) => {
let s = selection.face.single;
@ -305,7 +307,7 @@ const DebugActions = [
label: 'print face id',
info: 'print a face id',
},
listens: ['selection_face'],
listens: streams => streams.selection.face,
update: checkForSelectedFaces(1),
invoke: ({services: {selection}}) => {
console.log(selection.face.single.id);
@ -319,7 +321,7 @@ const DebugActions = [
label: 'print face sketch',
info: 'print face sketch stripping constraints and boundary',
},
listens: ['selection_face'],
listens: streams => streams.selection.face,
update: checkForSelectedFaces(1),
invoke: ({services: {selection, project}}) => {
const faceId = selection.face.single.id;

View file

@ -3,14 +3,13 @@ import ls from './ActionInfo.less';
import AuxWidget from 'ui/components/AuxWidget';
import connect from 'ui/connect';
import {TOKENS as ACTION_TOKENS} from '../../actions/actionSystemPlugin';
import {TOKENS as KeyboardTokens} from '../../keyboard/keyboardPlugin';
import {combine} from 'lstream';
function ActionInfo({actionId, x, y, info, hint, hotKey}) {
let visible = !!(actionId && (info || hint || hotKey));
return <AuxWidget visible={visible}
left={x} top={y} className={ls.root} zIndex={550}>
return <AuxWidget visible={visible}
left={x} top={y} className={ls.root} zIndex={550}>
{visible && <Fragment>
{hint && <div className={ls.hint}>{hint}</div>}
{info && <div className={ls.info}>{info}</div>}
@ -19,7 +18,11 @@ function ActionInfo({actionId, x, y, info, hint, hotKey}) {
</AuxWidget>;
}
export default connect(ActionInfo, [ACTION_TOKENS.HINT, KeyboardTokens.KEYMAP], {
mapProps: ([ hintInfo, keymap ]) => (Object.assign({hotKey: hintInfo && keymap[hintInfo.actionId]}, hintInfo))
});
export default connect(streams =>
combine(
streams.action.hint,
streams.ui.keymap)
.map(([hintInfo, keymap]) => Object.assign({hotKey: hintInfo && keymap[hintInfo.actionId]}, hintInfo)
))
(ActionInfo);

View file

@ -5,7 +5,7 @@ import View3d from './View3d';
import ls from './AppTabs.less';
import TabSwitcher, {Tab} from 'ui/components/TabSwticher';
import connect from 'ui/connect';
import connect from 'ui/connectLegacy';
import {TOKENS as APP_TABS_TOKENS} from "../appTabsPlugin";
import Card from "ui/components/Card";

View file

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import Stack from 'ui/components/Stack';
import connect from 'ui/connect';
import connect from 'ui/connectLegacy';
import Fa from 'ui/components/Fa';
import ImgIcon from 'ui/components/ImgIcon';
import ls from './OperationHistory.less';

View file

@ -1,14 +1,12 @@
import React, {Fragment} from 'react';
import React from 'react';
import ControlBar, {ControlBarButton} from './ControlBar';
import connect from 'ui/connect';
import Fa from 'ui/components/Fa';
import {TOKENS as UI_TOKENS} from '../uiEntryPointsPlugin';
import {TOKENS as ACTION_TOKENS} from '../../actions/actionSystemPlugin';
import {toIdAndOverrides} from "../../actions/actionRef";
import {mapActionBehavior} from "../../actions/actionButtonBehavior";
import {DEFAULT_MAPPER} from "ui/connect";
import {isMenuAction} from "../menu/menuPlugin";
import {toIdAndOverrides} from '../../actions/actionRef';
import {isMenuAction} from '../menu/menuPlugin';
import {combine, merger} from 'lstream';
import mapContext from 'ui/mapContext';
import decoratorChain from '../../../../../modules/ui/decoratorChain';
export default function PlugableControlBar() {
return <ControlBar left={<LeftGroup />} right={<RightGroup />}/>;
@ -17,7 +15,7 @@ export default function PlugableControlBar() {
function ButtonGroup({actions}) {
return actions.map(actionRef => {
let [id, overrides] = toIdAndOverrides(actionRef);
return <ConnectedActionButton key={id} actionId={id} {...overrides}/>;
return <ConnectedActionButton key={id} actionId={id} {...overrides} />;
});
}
@ -39,22 +37,21 @@ class ActionButton extends React.Component {
}
}
const BUTTON_CONNECTOR = {
mapProps: ([actions]) => ({actions})
};
const LeftGroup = connect(streams => streams.ui.controlBars.left.map(actions => ({actions})))(ButtonGroup);
const RightGroup = connect(streams => streams.ui.controlBars.right.map(actions => ({actions})))(ButtonGroup);
const LeftGroup = connect(ButtonGroup, UI_TOKENS.CONTROL_BAR_LEFT, BUTTON_CONNECTOR);
const RightGroup = connect(ButtonGroup, UI_TOKENS.CONTROL_BAR_RIGHT, BUTTON_CONNECTOR);
const ConnectedActionButton = decoratorChain(
const ConnectedActionButton = connect(ActionButton,
props => [ACTION_TOKENS.actionAppearance(props.actionId),
ACTION_TOKENS.actionState(props.actionId)],
{
mapProps: (state, props) => Object.assign(DEFAULT_MAPPER(state), props),
mapActions: mapActionBehavior(props => props.actionId),
}
);
connect(
(streams, props) => combine(
streams.action.appearance[props.actionId],
streams.action.state[props.actionId]).map(merger)),
mapContext(({services}, props) => ({
onClick: data => services.action.run(props.actionId, data)
}))
)
(ActionButton);
function getMenuData(el) {
//TODO: make more generic

View file

@ -1,15 +1,14 @@
import React, {Fragment} from 'react';
import React from 'react';
import connect from 'ui/connect';
import Fa from 'ui/components/Fa';
import {TOKENS as UI_TOKENS} from '../uiEntryPointsPlugin';
import {TOKENS as ACTION_TOKENS} from '../../actions/actionSystemPlugin';
import Toolbar, {ToolbarButton} from 'ui/components/Toolbar';
import ImgIcon from 'ui/components/ImgIcon';
import {toIdAndOverrides} from '../../actions/actionRef';
import {mapActionBehavior} from '../../actions/actionButtonBehavior';
import {DEFAULT_MAPPER} from 'ui/connect';
import capitalize from 'gems/capitalize';
import decoratorChain from 'ui/decoratorChain';
import {combine, merger} from 'lstream';
import mapContext from '../../../../../modules/ui/mapContext';
function ConfigurableToolbar({actions, small, ...props}) {
@ -34,19 +33,20 @@ function ActionButton({label, icon96, cssIcons, small, enabled, visible, actionI
</ToolbarButton>
}
const ConnectedActionButton = connect(ActionButton,
({actionId}) => [ACTION_TOKENS.actionAppearance(actionId), ACTION_TOKENS.actionState(actionId)], {
mapProps: (state, props) => Object.assign(DEFAULT_MAPPER(state), props),
mapActions: mapActionBehavior(props => props.actionId),
});
const ConnectedActionButton = decoratorChain(
connect((streams, {actionId}) => combine(streams.action.appearance[actionId], streams.action.state[actionId]).map(merger)),
mapContext(mapActionBehavior(props => props.actionId))
)
(ActionButton);
export function createPlugableToolbar(configToken, small) {
return connect(ConfigurableToolbar, configToken, {
staticProps: {small},
mapProps: ([actions]) => ({actions})
});
export function createPlugableToolbar(streamSelector, small) {
return decoratorChain(
connect(streams => streamSelector(streams).map(actions => ({actions}))),
mapContext(mapActionBehavior(props => props.actionId))
)
(props => <ConfigurableToolbar {...props} small={small} />);
}
export const PlugableToolbarLeft = createPlugableToolbar(UI_TOKENS.TOOLBAR_BAR_LEFT);
export const PlugableToolbarLeftSecondary = createPlugableToolbar(UI_TOKENS.TOOLBAR_BAR_LEFT_SECONDARY);
export const PlugableToolbarRight = createPlugableToolbar(UI_TOKENS.TOOLBAR_BAR_RIGHT, true);
export const PlugableToolbarLeft = createPlugableToolbar(streams => streams.ui.toolbars.left);
export const PlugableToolbarLeftSecondary = createPlugableToolbar(streams => streams.ui.toolbars.leftSecondary);
export const PlugableToolbarRight = createPlugableToolbar(streams => streams.ui.toolbars.right, true);

View file

@ -1,7 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import MenuHolder from '../menu/MenuHolder';
import {TOKENS as MENU_TOKENS} from '../menu/menuPlugin';
import WindowSystem from 'ui/WindowSystem';
import ActionInfo from '../actionInfo/ActionInfo';
@ -25,10 +24,8 @@ export default class UISystem extends React.Component {
}
closeAllUpPopups = () => {
let openedMenus = this.context.bus.state[MENU_TOKENS.OPENED];
if (openedMenus && openedMenus.length !== 0) {
this.context.bus.dispatch(MENU_TOKENS.CLOSE_ALL);
}
this.context.services.menu.closeAll();
this.context.services.action.showHintFor(null);
};
getChildContext() {
@ -38,7 +35,7 @@ export default class UISystem extends React.Component {
}
static contextTypes = {
bus: PropTypes.object
services: PropTypes.object
};
static childContextTypes = {

View file

@ -24,6 +24,7 @@ export default class WebApplication extends React.Component {
static childContextTypes = {
bus: PropTypes.object,
services: PropTypes.object
services: PropTypes.object,
streams: PropTypes.object
};
}

View file

@ -1,12 +1,12 @@
import React from 'react';
import connect from 'ui/connect';
import {TOKENS as MENU_TOKENS} from './menuPlugin';
import {TOKENS as ACTION_TOKENS} from '../../actions/actionSystemPlugin';
import Menu, {MenuItem, MenuSeparator} from 'ui/components/Menu';
import Filler from 'ui/components/Filler';
import Fa from 'ui/components/Fa';
import {TOKENS as KeyboardTokens} from '../../keyboard/keyboardPlugin';
import {mapActionBehavior} from '../../actions/actionButtonBehavior';
import connect from 'ui/connect';
import {combine, merger} from 'lstream';
import mapContext from 'ui/mapContext';
import decoratorChain from 'ui/decoratorChain';
function MenuHolder({menus}) {
return menus.map(({id, actions}) => <ConnectedActionMenu key={id} menuId={id} actions={actions} />);
@ -48,26 +48,26 @@ function ActionMenuItem({label, cssIcons, icon32, icon96, enabled, hotKey, visib
return <MenuItem {...{label, icon, style, disabled: !enabled, hotKey, ...props}} />;
}
const ConnectedActionMenu = connect(ActionMenu,
({menuId}) => [MENU_TOKENS.menuState(menuId), KeyboardTokens.KEYMAP],
{
mapProps: ([menuState, keymap], {actions}) => Object.assign({keymap, actions}, menuState)
});
const ConnectedActionMenu = connect((streams, props) =>
combine(
streams.ui.menu.states[props.menuId],
streams.ui.keymap)
.map(([s, keymap]) => ({...s, keymap})))
(ActionMenu);
let ConnectedMenuItem = connect(ActionMenuItem,
({actionId}) => [ACTION_TOKENS.actionState(actionId), ACTION_TOKENS.actionAppearance(actionId)],
{
mapProps: ([{enabled, visible}, {label, cssIcons, icon32, icon96}]) => ({
enabled, visible, label, cssIcons, icon32, icon96
}),
mapActions: mapActionBehavior(props => props.actionId)
}
);
let ConnectedMenuItem = decoratorChain(
export default connect(MenuHolder, MENU_TOKENS.MENUS, {
mapProps: ([menus]) => ({menus})
});
connect((streams, {actionId}) =>
combine(
streams.action.state[actionId],
streams.action.appearance[actionId]).map(merger)),
mapContext(mapActionBehavior(props => props.actionId))
)
(ActionMenuItem);
export default connect(streams => streams.ui.menu.all.map(menus => ({menus})))(MenuHolder);

View file

@ -1,53 +1,54 @@
import {createToken} from 'bus';
import {state} from '../../../../../modules/lstream';
export function activate({bus, services}) {
export function activate({bus, services, streams}) {
bus.enableState(TOKENS.MENUS, []);
bus.enableState(TOKENS.OPENED, []);
streams.ui.menu = {
all: state([]),
opened: state([]),
states: {}
};
function registerMenus(menus) {
let menusToAdd = [];
let showMenuActions = [];
menus.forEach(({id, actions, ...appearance}) => {
let stateToken = TOKENS.menuState(id);
bus.enableState(stateToken, {
let menuState = state({
visible: false,
orientationUp: false,
x: undefined,
y: undefined
});
streams.ui.menu.states[id] = menuState;
if (!appearance.label) {
appearance.label = id;
}
showMenuActions.push({
id: 'menu.' + id,
appearance,
invoke: (ctx, hints) => bus.updateStates([stateToken, TOKENS.OPENED],
([state, opened]) => [Object.assign(state, {visible: true}, hints), [id, ...opened]]
)
invoke: (ctx, hints) => {
menuState.mutate(v => {
Object.assign(v, hints);
v.visible = true;
});
streams.ui.menu.opened.mutate(v => v.push(id));
}
});
menusToAdd.push({id, actions});
});
services.action.registerActions(showMenuActions);
bus.updateState(TOKENS.MENUS, menus => [...menus, ...menusToAdd]);
streams.ui.menu.all.update(menus => [...menus, ...menusToAdd]);
}
bus.subscribe(TOKENS.CLOSE_ALL, () => {
bus.state[TOKENS.OPENED].forEach(openedMenu => bus.setState(TOKENS.menuState(openedMenu), {visible: false}));
bus.updateState(TOKENS.OPENED, () => []);
});
function closeAll() {
if (streams.ui.menu.opened.value.length > 0) {
streams.ui.menu.opened.value.forEach(id => streams.ui.menu.states[id].mutate(s => s.visible = false));
streams.ui.menu.opened.mutate(opened => opened.length = 0);
}
}
services.menu = { registerMenus }
services.menu = { registerMenus, closeAll }
}
export const TOKENS = {
menuState: id => createToken('menu', 'state', id),
MENUS: createToken('menus'),
CLOSE_ALL: createToken('menus', 'closeAll'),
OPENED: createToken('menus', 'opened')
};
export function isMenuAction(actionId) {
return actionId.startsWith('menu.');
}

View file

@ -1,25 +1,19 @@
import {createToken} from 'bus';
import {state} from 'lstream';
export function activate({bus, streams}) {
export function activate({bus}) {
streams.ui = {
controlBars: {
left: state([]),
right: state([])
},
toolbars: {
left: state([]),
leftSecondary: state([]),
right: state([])
}
};
bus.enableState(TOKENS.CONTROL_BAR_LEFT, []);
bus.enableState(TOKENS.CONTROL_BAR_RIGHT, []);
bus.enableState(TOKENS.TOOLBAR_BAR_LEFT, []);
bus.enableState(TOKENS.TOOLBAR_BAR_LEFT_SECONDARY, []);
bus.enableState(TOKENS.TOOLBAR_BAR_RIGHT, []);
}
const NS = 'ui.config';
export const TOKENS = {
CONTROL_BAR_LEFT: createToken(NS, 'controlBar.left'),
CONTROL_BAR_RIGHT: createToken(NS, 'controlBar.right'),
TOOLBAR_BAR_LEFT: createToken(NS, 'toolbar.left'),
TOOLBAR_BAR_LEFT_SECONDARY: createToken(NS, 'toolbar.left.secondary'),
TOOLBAR_BAR_RIGHT: createToken(NS, 'toolbar.right'),
};

View file

@ -1,4 +1,3 @@
import Bus from 'bus';
import * as LifecyclePlugin from './lifecyclePlugin';
import * as AppTabsPlugin from '../dom/appTabsPlugin';
import * as DomPlugin from '../dom/domPlugin';
@ -19,9 +18,10 @@ import * as ProjectPlugin from '../projectPlugin';
import * as SketcherPlugin from '../sketch/sketcherPlugin';
import * as tpiPlugin from '../tpi/tpiPlugin';
import * as PartModellerPlugin from '../part/partModellerPlugin';
import context from 'context';
import startReact from "../dom/startReact";
import {APP_READY_TOKEN} from './lifecyclePlugin';
@ -35,8 +35,8 @@ export default function startApplication(callback) {
StoragePlugin,
AppTabsPlugin,
ActionSystemPlugin,
MenuPlugin,
UiEntryPointsPlugin,
MenuPlugin,
KeyboardPlugin,
WizardPlugin,
CraftEnginesPlugin,
@ -55,11 +55,6 @@ export default function startApplication(callback) {
...applicationPlugins,
];
let context = {
bus: new Bus(),
services: {}
};
activatePlugins(preUIPlugins, context);
startReact(context, () => {

View file

@ -1,21 +1,17 @@
import Mousetrap from 'mousetrap';
import DefaultKeymap from './keymaps/default';
import {isMenuAction} from '../dom/menu/menuPlugin';
import {state} from 'lstream';
import {createToken} from "bus";
import {TOKENS as ACTION_TOKENS} from "../actions/actionSystemPlugin";
import {isMenuAction, TOKENS as MENU_TOKENS} from "../dom/menu/menuPlugin";
export function activate({bus, services}) {
bus.enableState(TOKENS.KEYMAP, DefaultKeymap);
export function activate({services, streams}) {
streams.ui.keymap = state(DefaultKeymap);
let keymap = DefaultKeymap;
//to attach to a dom element: Mousetrap(domElement).bind(...
for (let action of Object.keys(keymap)) {
const dataProvider = getDataProvider(action, services);
let actionToken = ACTION_TOKENS.actionRun(action);
Mousetrap.bind(keymap[action], () => bus.dispatch(actionToken, dataProvider ? dataProvider() : undefined));
Mousetrap.bind(keymap[action], () => services.action.run(actionToken, dataProvider ? dataProvider() : undefined));
}
Mousetrap.bind('esc', () => bus.dispatch(MENU_TOKENS.CLOSE_ALL));
Mousetrap.bind('esc', services.menu.closeAll)
}
function getDataProvider(action, services) {
@ -33,6 +29,3 @@ function getDataProvider(action, services) {
}
export const TOKENS = {
KEYMAP: createToken('keymap')
};

View file

@ -1,25 +1,23 @@
import CoreActions from '../actions/coreActions';
import OperationActions from '../actions/operationActions';
import HistoryActions from '../actions/historyActions';
import {TOKENS as UI_TOKENS} from '../dom/uiEntryPointsPlugin';
import menuConfig from "./menuConfig";
import menuConfig from './menuConfig';
export function activate({bus, services}) {
export function activate({bus, services, streams}) {
streams.ui.controlBars.left.value = ['menu.file', 'menu.craft', 'menu.boolean', 'menu.primitives', 'Donate', 'GitHub'];
streams.ui.controlBars.right.value = [
['Info', {label: null}],
['RefreshSketches', {label: null}],
['ShowSketches', {label: 'sketches'}], ['DeselectAll', {label: null}], ['ToggleCameraMode', {label: null}]
];
streams.ui.toolbars.left.value = ['PLANE', 'EditFace', 'EXTRUDE', 'CUT', 'REVOLVE'];
streams.ui.toolbars.leftSecondary.value = ['INTERSECTION', 'DIFFERENCE', 'UNION'];
streams.ui.toolbars.right.value = ['Save', 'StlExport'];
services.action.registerActions(CoreActions);
services.action.registerActions(OperationActions);
services.action.registerActions(HistoryActions);
services.menu.registerMenus(menuConfig);
bus.dispatch(UI_TOKENS.CONTROL_BAR_LEFT, ['menu.file', 'menu.craft', 'menu.boolean', 'menu.primitives', 'Donate', 'GitHub']);
bus.dispatch(UI_TOKENS.CONTROL_BAR_RIGHT, [
['Info', {label: null}],
['RefreshSketches', {label: null}],
['ShowSketches', {label: 'sketches'}], ['DeselectAll', {label: null}], ['ToggleCameraMode', {label: null}]
]);
bus.dispatch(UI_TOKENS.TOOLBAR_BAR_LEFT, ['PLANE', 'EditFace', 'EXTRUDE', 'CUT', 'REVOLVE']);
bus.dispatch(UI_TOKENS.TOOLBAR_BAR_LEFT_SECONDARY, ['INTERSECTION', 'DIFFERENCE', 'UNION']);
bus.dispatch(UI_TOKENS.TOOLBAR_BAR_RIGHT, ['Save', 'StlExport']);
}

View file

@ -1,7 +1,7 @@
import * as mask from 'gems/mask'
import {getAttribute, setAttribute} from '../../../../../modules/scene/objectData';
import {TOKENS as UI_TOKENS} from '../../dom/uiEntryPointsPlugin';
import {FACE, EDGE, SKETCH_OBJECT} from '../entites';
import {state} from '../../../../../modules/lstream';
export const PICK_KIND = {
FACE: mask.type(1),
@ -12,8 +12,9 @@ export const PICK_KIND = {
const SELECTABLE_ENTITIES = [FACE, EDGE, SKETCH_OBJECT];
export function activate(context) {
const {services, streams} = context;
initStateAndServices(context);
let domElement = context.services.viewer.sceneSetup.domElement();
let domElement = services.viewer.sceneSetup.domElement();
domElement.addEventListener('mousedown', mousedown, false);
domElement.addEventListener('mouseup', mouseup, false);
@ -41,26 +42,25 @@ export function activate(context) {
}
}
function selected(key, object) {
let selection = context.bus.state[key];
return selection !== undefined && selection.indexOf(object) !== -1;
function selected(selection, object) {
return selection.value.indexOf(object) !== -1;
}
function handlePick(event) {
raycastObjects(event, PICK_KIND.FACE | PICK_KIND.SKETCH | PICK_KIND.EDGE, (object, kind) => {
if (kind === PICK_KIND.FACE) {
if (!selected('selection_face', object.id)) {
context.services.cadScene.showBasis(object.basis(), object.depth());
context.bus.dispatch('selection_face', [object.id]);
if (!selected(streams.selection.face, object.id)) {
services.cadScene.showBasis(object.basis(), object.depth());
streams.selection.face.next([object.id]);
return false;
}
} else if (kind === PICK_KIND.SKETCH) {
if (!selected('selection_sketchObject', object.id)) {
context.bus.dispatch('selection_sketchObject', [object.id]);
if (!selected(streams.selection.sketchObject, object.id)) {
streams.selection.sketchObject.next([object.id]);
return false;
}
} else if (kind === PICK_KIND.EDGE) {
if (dispatchSelection('selection_edge', object.id, event)) {
if (dispatchSelection(streams.selection.edge, object.id, event)) {
return false;
}
}
@ -68,25 +68,25 @@ export function activate(context) {
});
}
function dispatchSelection(selectionToken, selectee, event) {
if (selected(selectionToken, selectee)) {
function dispatchSelection(selection, selectee, event) {
if (selected(selection, selectee)) {
return false;
}
let multiMode = event.shiftKey;
context.bus.updateState(selectionToken, selection => multiMode ? [...selection, selectee] : [selectee]);
selection.update(value => multiMode ? [...value, selectee] : [selectee]);
return true;
}
function handleSolidPick(e) {
raycastObjects(e, PICK_KIND.FACE, (sketchFace) => {
context.bus.dispatch('selection_solid', sketchFace.solid);
context.services.viewer.render();
streams.selection.solid.next([sketchFace.solid]);
services.viewer.render();
return false;
});
}
function raycastObjects(event, kind, visitor) {
let pickResults = context.services.viewer.raycast(event, context.services.cadScene.workGroup);
let pickResults = services.viewer.raycast(event, services.cadScene.workGroup);
const pickers = [
(pickResult) => {
if (mask.is(kind, PICK_KIND.SKETCH) && pickResult.object instanceof THREE.Line) {
@ -127,39 +127,30 @@ export function activate(context) {
}
}
function initStateAndServices({bus, services}) {
function initStateAndServices({streams, services}) {
services.selection = {
};
streams.selection = {
};
SELECTABLE_ENTITIES.forEach(entity => {
let entitySelectApi = {
objects: [],
single: undefined
};
services.selection[entity] = entitySelectApi;
let selType = entitySelectionToken(entity);
bus.enableState(selType, []);
bus.subscribe(selType, selection => {
let selectionState = state([]);
streams.selection[entity] = selectionState;
selectionState.attach(selection => {
entitySelectApi.objects = selection.map(id => services.cadRegistry.findEntity(entity, id));
entitySelectApi.single = entitySelectApi.objects[0];
});
entitySelectApi.select = selection => bus.dispatch(selType, selection);
entitySelectApi.select = selection => selectionState.value = selection;
});
}
const selectionTokenMap = {};
SELECTABLE_ENTITIES.forEach(e => selectionTokenMap[e] = `selection_${e}`);
export function entitySelectionToken(entity) {
let token = selectionTokenMap[entity];
if (!token) {
throw "entity isn't selectable " + entity;
}
return token;
}

View file

@ -7,7 +7,7 @@ export class AbstractSelectionMarker {
this.context = context;
this.entity = entity;
this.selection = [];
this.context.bus.subscribe(entitySelectionToken(entity), this.update);
this.context.streams.selection[entity].attach(this.update);
}
update = () => {
@ -19,7 +19,7 @@ export class AbstractSelectionMarker {
}
this.selection = [];
}
this.context.bus.dispatch('scene:update');
this.context.services.viewer.render();
return;
}
@ -33,7 +33,7 @@ export class AbstractSelectionMarker {
this.selection.splice(this.selection.indexOf(obj), 1);
this.unMark(obj);
}
this.context.bus.dispatch('scene:update');
this.context.services.viewer.render();
};
mark(obj) {