From e226d416ee4096de775ef8a1038c41e7fce0df74 Mon Sep 17 00:00:00 2001 From: Val Erastov Date: Fri, 22 Jun 2018 00:31:33 -0700 Subject: [PATCH] event stream api for organizing UI --- .babelrc | 3 +- modules/bus/store.js | 61 ---------- modules/context/index.js | 9 ++ modules/lstream/base.js | 50 ++++++++ modules/lstream/combine.js | 38 ++++++ modules/lstream/emitter.js | 41 +++++++ modules/lstream/index.js | 22 ++++ modules/lstream/state.js | 38 ++++++ modules/ui/connect.js | 29 +++++ modules/ui/{connect.jsx => connectLegacy.jsx} | 0 modules/ui/decoratorChain.js | 9 ++ modules/ui/mapContext.js | 11 ++ package.json | 16 +-- .../debug/debugger/BrepDebuggerWindow.jsx | 2 +- web/app/cad/actions/actionButtonBehavior.js | 18 ++- web/app/cad/actions/actionHelpers.js | 10 +- web/app/cad/actions/actionSystemPlugin.js | 109 ++++++++---------- web/app/cad/actions/anonHint.js | 16 +-- web/app/cad/actions/coreActions.js | 2 +- web/app/cad/actions/operationActions.js | 4 +- .../components/EdgesSelectionControl.jsx | 14 --- .../components/FaceSelectionControl.jsx | 8 -- .../craft/wizard/components/HistoryWizard.jsx | 2 +- .../cad/craft/wizard/components/MDForm.jsx | 54 --------- .../cad/craft/wizard/components/Wizard.jsx | 13 +-- .../craft/wizard/components/WizardManager.jsx | 2 +- .../wizard/components/form/MultiEntity.jsx | 24 ++-- .../wizard/components/form/SingleEntity.jsx | 22 ++-- web/app/cad/debugPlugin.js | 12 +- web/app/cad/dom/actionInfo/ActionInfo.jsx | 19 +-- web/app/cad/dom/components/AppTabs.jsx | 2 +- .../cad/dom/components/OperationHistory.jsx | 2 +- .../cad/dom/components/PlugableControlBar.jsx | 43 ++++--- .../cad/dom/components/PlugableToolbar.jsx | 36 +++--- web/app/cad/dom/components/UISystem.jsx | 9 +- web/app/cad/dom/components/WebApplication.jsx | 3 +- web/app/cad/dom/menu/MenuHolder.jsx | 42 +++---- web/app/cad/dom/menu/menuPlugin.js | 47 ++++---- web/app/cad/dom/uiEntryPointsPlugin.js | 32 +++-- web/app/cad/init/startApplication.js | 11 +- web/app/cad/keyboard/keyboardPlugin.js | 19 +-- web/app/cad/part/uiConfigPlugin.js | 26 ++--- .../cad/scene/controls/pickControlPlugin.js | 59 ++++------ .../abstractSelectionMarker.js | 6 +- 44 files changed, 532 insertions(+), 463 deletions(-) delete mode 100644 modules/bus/store.js create mode 100644 modules/context/index.js create mode 100644 modules/lstream/base.js create mode 100644 modules/lstream/combine.js create mode 100644 modules/lstream/emitter.js create mode 100644 modules/lstream/index.js create mode 100644 modules/lstream/state.js create mode 100644 modules/ui/connect.js rename modules/ui/{connect.jsx => connectLegacy.jsx} (100%) create mode 100644 modules/ui/decoratorChain.js create mode 100644 modules/ui/mapContext.js delete mode 100644 web/app/cad/craft/wizard/components/EdgesSelectionControl.jsx delete mode 100644 web/app/cad/craft/wizard/components/FaceSelectionControl.jsx delete mode 100644 web/app/cad/craft/wizard/components/MDForm.jsx diff --git a/.babelrc b/.babelrc index f5277971..f104f6f6 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,4 @@ { - "presets": ["es2015", "stage-2", "react", "flow"] + "presets": ["es2015", "stage-2", "react", "flow"], + "plugins": ["transform-decorators-legacy"] } diff --git a/modules/bus/store.js b/modules/bus/store.js deleted file mode 100644 index 758ba412..00000000 --- a/modules/bus/store.js +++ /dev/null @@ -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]; - } -} \ No newline at end of file diff --git a/modules/context/index.js b/modules/context/index.js new file mode 100644 index 00000000..26011fa8 --- /dev/null +++ b/modules/context/index.js @@ -0,0 +1,9 @@ +import Bus from '../bus'; +import {observable} from 'mobx'; + +export default { + services: {}, + streams: {}, + //@deprecated + bus: new Bus() +}; \ No newline at end of file diff --git a/modules/lstream/base.js b/modules/lstream/base.js new file mode 100644 index 00000000..c221dd23 --- /dev/null +++ b/modules/lstream/base.js @@ -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); +} diff --git a/modules/lstream/combine.js b/modules/lstream/combine.js new file mode 100644 index 00000000..bb6d1834 --- /dev/null +++ b/modules/lstream/combine.js @@ -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 = {}; \ No newline at end of file diff --git a/modules/lstream/emitter.js b/modules/lstream/emitter.js new file mode 100644 index 00000000..3a0d9444 --- /dev/null +++ b/modules/lstream/emitter.js @@ -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; + } + } +} diff --git a/modules/lstream/index.js b/modules/lstream/index.js new file mode 100644 index 00000000..baea2b91 --- /dev/null +++ b/modules/lstream/index.js @@ -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), {}); diff --git a/modules/lstream/state.js b/modules/lstream/state.js new file mode 100644 index 00000000..f74fcb48 --- /dev/null +++ b/modules/lstream/state.js @@ -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); + } +} + + diff --git a/modules/ui/connect.js b/modules/ui/connect.js new file mode 100644 index 00000000..f1f01c8c --- /dev/null +++ b/modules/ui/connect.js @@ -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 ; + + } + }; + } +} diff --git a/modules/ui/connect.jsx b/modules/ui/connectLegacy.jsx similarity index 100% rename from modules/ui/connect.jsx rename to modules/ui/connectLegacy.jsx diff --git a/modules/ui/decoratorChain.js b/modules/ui/decoratorChain.js new file mode 100644 index 00000000..4ee20e46 --- /dev/null +++ b/modules/ui/decoratorChain.js @@ -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; + } +} \ No newline at end of file diff --git a/modules/ui/mapContext.js b/modules/ui/mapContext.js new file mode 100644 index 00000000..e3fd9c98 --- /dev/null +++ b/modules/ui/mapContext.js @@ -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 + } + } +} \ No newline at end of file diff --git a/package.json b/package.json index 442c2fad..6886a159 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/web/app/brep/debug/debugger/BrepDebuggerWindow.jsx b/web/app/brep/debug/debugger/BrepDebuggerWindow.jsx index f2a993c4..a8b1fd72 100644 --- a/web/app/brep/debug/debugger/BrepDebuggerWindow.jsx +++ b/web/app/brep/debug/debugger/BrepDebuggerWindow.jsx @@ -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'; diff --git a/web/app/cad/actions/actionButtonBehavior.js b/web/app/cad/actions/actionButtonBehavior.js index 04fcad6d..57c22b14 100644 --- a/web/app/cad/actions/actionButtonBehavior.js +++ b/web/app/cad/actions/actionButtonBehavior.js @@ -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) } } }}; diff --git a/web/app/cad/actions/actionHelpers.js b/web/app/cad/actions/actionHelpers.js index 9133f806..64aa2971 100644 --- a/web/app/cad/actions/actionHelpers.js +++ b/web/app/cad/actions/actionHelpers.js @@ -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) } } diff --git a/web/app/cad/actions/actionSystemPlugin.js b/web/app/cad/actions/actionSystemPlugin.js index f56920bd..142880e3 100644 --- a/web/app/cad/actions/actionSystemPlugin.js +++ b/web/app/cad/actions/actionSystemPlugin.js @@ -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'), -}; \ No newline at end of file diff --git a/web/app/cad/actions/anonHint.js b/web/app/cad/actions/anonHint.js index c2a05140..86111de6 100644 --- a/web/app/cad/actions/anonHint.js +++ b/web/app/cad/actions/anonHint.js @@ -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); } diff --git a/web/app/cad/actions/coreActions.js b/web/app/cad/actions/coreActions.js index d4ba54b6..d58bfd5c 100644 --- a/web/app/cad/actions/coreActions.js +++ b/web/app/cad/actions/coreActions.js @@ -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) }, diff --git a/web/app/cad/actions/operationActions.js b/web/app/cad/actions/operationActions.js index 13f250b5..e4cc68b2 100644 --- a/web/app/cad/actions/operationActions.js +++ b/web/app/cad/actions/operationActions.js @@ -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) } } diff --git a/web/app/cad/craft/wizard/components/EdgesSelectionControl.jsx b/web/app/cad/craft/wizard/components/EdgesSelectionControl.jsx deleted file mode 100644 index 0d949bb7..00000000 --- a/web/app/cad/craft/wizard/components/EdgesSelectionControl.jsx +++ /dev/null @@ -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 - {edges.map((subParams, i) => - - )} - ; -} - diff --git a/web/app/cad/craft/wizard/components/FaceSelectionControl.jsx b/web/app/cad/craft/wizard/components/FaceSelectionControl.jsx deleted file mode 100644 index b55ca535..00000000 --- a/web/app/cad/craft/wizard/components/FaceSelectionControl.jsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; - -import TextControl from "ui/components/controls/TextControl"; - -export default function FaceSelectionControl(props) { - return -} - diff --git a/web/app/cad/craft/wizard/components/HistoryWizard.jsx b/web/app/cad/craft/wizard/components/HistoryWizard.jsx index 1d505ed3..48a1c97e 100644 --- a/web/app/cad/craft/wizard/components/HistoryWizard.jsx +++ b/web/app/cad/craft/wizard/components/HistoryWizard.jsx @@ -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'; diff --git a/web/app/cad/craft/wizard/components/MDForm.jsx b/web/app/cad/craft/wizard/components/MDForm.jsx deleted file mode 100644 index 33618a5f..00000000 --- a/web/app/cad/craft/wizard/components/MDForm.jsx +++ /dev/null @@ -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 - {value && value.map((itemData, i) => - - )} - - } else { - return - - { - (() => { - let commonProps = {initValue: value}; - if (type === 'number') { - return ; - } else if (type === 'face') { - return ; - } else if (type === 'choice') { - return - {options.options.map(op => )} - ; - } else { - return ; - } - })() - } - - } - }); - - } - -} - -function uiLabel(name) { - return camelCaseSplit(name).map(w => w.toLowerCase()).join(' '); -} diff --git a/web/app/cad/craft/wizard/components/Wizard.jsx b/web/app/cad/craft/wizard/components/Wizard.jsx index c729fa43..b02b1a2f 100644 --- a/web/app/cad/craft/wizard/components/Wizard.jsx +++ b/web/app/cad/craft/wizard/components/Wizard.jsx @@ -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(); diff --git a/web/app/cad/craft/wizard/components/WizardManager.jsx b/web/app/cad/craft/wizard/components/WizardManager.jsx index 575f9fb2..cb7435cd 100644 --- a/web/app/cad/craft/wizard/components/WizardManager.jsx +++ b/web/app/cad/craft/wizard/components/WizardManager.jsx @@ -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'; diff --git a/web/app/cad/craft/wizard/components/form/MultiEntity.jsx b/web/app/cad/craft/wizard/components/form/MultiEntity.jsx index f5391ded..f24ccf2a 100644 --- a/web/app/cad/craft/wizard/components/form/MultiEntity.jsx +++ b/web/app/cad/craft/wizard/components/form/MultiEntity.jsx @@ -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 } ; } +} - 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 } MultiEntity.contextTypes = { - bus: PropTypes.object + streams: PropTypes.object }; \ No newline at end of file diff --git a/web/app/cad/craft/wizard/components/form/SingleEntity.jsx b/web/app/cad/craft/wizard/components/form/SingleEntity.jsx index c258165d..a8887b48 100644 --- a/web/app/cad/craft/wizard/components/form/SingleEntity.jsx +++ b/web/app/cad/craft/wizard/components/form/SingleEntity.jsx @@ -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} ; } +} - static contextTypes = { - bus: PropTypes.object - }; -}); - -export default function SingleEntity(props, {bus}) { - return +export default function SingleEntity(props, {streams}) { + return } SingleEntity.contextTypes = { - bus: PropTypes.object + streams: PropTypes.object }; \ No newline at end of file diff --git a/web/app/cad/debugPlugin.js b/web/app/cad/debugPlugin.js index 38848c6e..f5039a02 100644 --- a/web/app/cad/debugPlugin.js +++ b/web/app/cad/debugPlugin.js @@ -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(); } @@ -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; diff --git a/web/app/cad/dom/actionInfo/ActionInfo.jsx b/web/app/cad/dom/actionInfo/ActionInfo.jsx index 09a16281..18184314 100644 --- a/web/app/cad/dom/actionInfo/ActionInfo.jsx +++ b/web/app/cad/dom/actionInfo/ActionInfo.jsx @@ -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 + + return {visible && {hint &&
{hint}
} {info &&
{info}
} @@ -19,7 +18,11 @@ function ActionInfo({actionId, x, y, info, hint, hotKey}) {
; } -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); diff --git a/web/app/cad/dom/components/AppTabs.jsx b/web/app/cad/dom/components/AppTabs.jsx index c463c7a1..10dd0145 100644 --- a/web/app/cad/dom/components/AppTabs.jsx +++ b/web/app/cad/dom/components/AppTabs.jsx @@ -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"; diff --git a/web/app/cad/dom/components/OperationHistory.jsx b/web/app/cad/dom/components/OperationHistory.jsx index b8ff2c98..3d084850 100644 --- a/web/app/cad/dom/components/OperationHistory.jsx +++ b/web/app/cad/dom/components/OperationHistory.jsx @@ -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'; diff --git a/web/app/cad/dom/components/PlugableControlBar.jsx b/web/app/cad/dom/components/PlugableControlBar.jsx index 6424a6e2..c61b0e94 100644 --- a/web/app/cad/dom/components/PlugableControlBar.jsx +++ b/web/app/cad/dom/components/PlugableControlBar.jsx @@ -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 } right={}/>; @@ -17,7 +15,7 @@ export default function PlugableControlBar() { function ButtonGroup({actions}) { return actions.map(actionRef => { let [id, overrides] = toIdAndOverrides(actionRef); - return ; + return ; }); } @@ -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 diff --git a/web/app/cad/dom/components/PlugableToolbar.jsx b/web/app/cad/dom/components/PlugableToolbar.jsx index ce9e4bf8..77bf3d41 100644 --- a/web/app/cad/dom/components/PlugableToolbar.jsx +++ b/web/app/cad/dom/components/PlugableToolbar.jsx @@ -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 } -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 => ); } -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); \ No newline at end of file +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); \ No newline at end of file diff --git a/web/app/cad/dom/components/UISystem.jsx b/web/app/cad/dom/components/UISystem.jsx index bcf900b8..1476f1e7 100644 --- a/web/app/cad/dom/components/UISystem.jsx +++ b/web/app/cad/dom/components/UISystem.jsx @@ -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 = { diff --git a/web/app/cad/dom/components/WebApplication.jsx b/web/app/cad/dom/components/WebApplication.jsx index 488f2913..852e90ad 100644 --- a/web/app/cad/dom/components/WebApplication.jsx +++ b/web/app/cad/dom/components/WebApplication.jsx @@ -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 }; } diff --git a/web/app/cad/dom/menu/MenuHolder.jsx b/web/app/cad/dom/menu/MenuHolder.jsx index 8455b792..8eaab2fa 100644 --- a/web/app/cad/dom/menu/MenuHolder.jsx +++ b/web/app/cad/dom/menu/MenuHolder.jsx @@ -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}) => ); @@ -48,26 +48,26 @@ function ActionMenuItem({label, cssIcons, icon32, icon96, enabled, hotKey, visib return ; } -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); diff --git a/web/app/cad/dom/menu/menuPlugin.js b/web/app/cad/dom/menu/menuPlugin.js index 1d8e58c9..f2e71f30 100644 --- a/web/app/cad/dom/menu/menuPlugin.js +++ b/web/app/cad/dom/menu/menuPlugin.js @@ -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.'); } diff --git a/web/app/cad/dom/uiEntryPointsPlugin.js b/web/app/cad/dom/uiEntryPointsPlugin.js index c4e9cb06..a5959b5d 100644 --- a/web/app/cad/dom/uiEntryPointsPlugin.js +++ b/web/app/cad/dom/uiEntryPointsPlugin.js @@ -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'), -}; diff --git a/web/app/cad/init/startApplication.js b/web/app/cad/init/startApplication.js index 5af615b3..afb4f24a 100644 --- a/web/app/cad/init/startApplication.js +++ b/web/app/cad/init/startApplication.js @@ -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, () => { diff --git a/web/app/cad/keyboard/keyboardPlugin.js b/web/app/cad/keyboard/keyboardPlugin.js index 582c0239..3c1a8e5e 100644 --- a/web/app/cad/keyboard/keyboardPlugin.js +++ b/web/app/cad/keyboard/keyboardPlugin.js @@ -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') -}; \ No newline at end of file diff --git a/web/app/cad/part/uiConfigPlugin.js b/web/app/cad/part/uiConfigPlugin.js index ae933aec..cc9fd2c7 100644 --- a/web/app/cad/part/uiConfigPlugin.js +++ b/web/app/cad/part/uiConfigPlugin.js @@ -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']); } \ No newline at end of file diff --git a/web/app/cad/scene/controls/pickControlPlugin.js b/web/app/cad/scene/controls/pickControlPlugin.js index 20c17dd5..a27afd28 100644 --- a/web/app/cad/scene/controls/pickControlPlugin.js +++ b/web/app/cad/scene/controls/pickControlPlugin.js @@ -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; -} - - diff --git a/web/app/cad/scene/selectionMarker/abstractSelectionMarker.js b/web/app/cad/scene/selectionMarker/abstractSelectionMarker.js index 2e03c116..d5cf769b 100644 --- a/web/app/cad/scene/selectionMarker/abstractSelectionMarker.js +++ b/web/app/cad/scene/selectionMarker/abstractSelectionMarker.js @@ -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) {