From 41ae7ee774732b7fa8e698be2d9860a376982721 Mon Sep 17 00:00:00 2001 From: martinRenou Date: Tue, 11 Oct 2022 16:40:21 +0200 Subject: [PATCH 1/4] Click binding (cherry picked from commit 8870f5503baf2f3ed044c1d531b75235ceb20ce0) --- src/mainview.tsx | 2 + src/model.ts | 4 ++ src/panelview/model.ts | 10 ++++ src/panelview/objectproperties.tsx | 79 +++++++++++++++++------------- src/toolbar/operatortoolbar.tsx | 1 - src/types.ts | 1 + 6 files changed, 63 insertions(+), 34 deletions(-) diff --git a/src/mainview.tsx b/src/mainview.tsx index a5b1a055..cb03dc25 100644 --- a/src/mainview.tsx +++ b/src/mainview.tsx @@ -396,6 +396,8 @@ export class MainView extends React.Component { private _onClick(e: MouseEvent) { this._selectedMesh = this._pick(); + + this._model.syncSelectedObject(this._selectedMesh !== null ? this._selectedMesh.name : null); } private shapeToMesh = (payload: IDisplayShape['payload']) => { diff --git a/src/model.ts b/src/model.ts index 3d6d6abb..a7f2e4c4 100644 --- a/src/model.ts +++ b/src/model.ts @@ -145,6 +145,10 @@ export class JupyterCadModel implements IJupyterCadModel { this.sharedModel.awareness.setLocalStateField('mouse', pos); } + syncSelectedObject(name: string | null): void { + this.sharedModel.awareness.setLocalStateField('selected', name); + } + getClientId(): number { return this.sharedModel.awareness.clientID; } diff --git a/src/panelview/model.ts b/src/panelview/model.ts index 9a6e616b..c45c0db0 100644 --- a/src/panelview/model.ts +++ b/src/panelview/model.ts @@ -20,33 +20,43 @@ export class ControlPanelModel implements IControlPanelModel { this._tracker = options.tracker; this._documentChanged = this._tracker.currentChanged; } + get state(): ObservableMap { return this._state; } + get stateChanged(): ISateChangedSignal { return this._stateChanged; } + get documentChanged(): ISignal { return this._documentChanged; } + get filePath(): string | undefined { return this._tracker.currentWidget?.context.localPath; } + get jcadModel(): IJupyterCadModel | undefined { return this._tracker.currentWidget?.context.model; } + get sharedModel(): IJupyterCadDoc | undefined { return this._tracker.currentWidget?.context.model.sharedModel; } + set(key: keyof IControlPanelState, value: IStateValue): void { this._state.set(key, value); } + get(key: keyof IControlPanelState): IStateValue | undefined { return this._state.get(key); } + has(key: keyof IControlPanelState): boolean { return this._state.has(key); } + disconnect(f: any): void { this._tracker.forEach(w => w.context.model.sharedModelChanged.disconnect(f) diff --git a/src/panelview/objectproperties.tsx b/src/panelview/objectproperties.tsx index 8d4f595c..797fa560 100644 --- a/src/panelview/objectproperties.tsx +++ b/src/panelview/objectproperties.tsx @@ -33,6 +33,7 @@ interface IProps { cpModel: IControlPanelModel; } + class ObjectPropertiesReact extends React.Component { constructor(props: IProps) { super(props); @@ -64,39 +65,6 @@ class ObjectPropertiesReact extends React.Component { }); } }); - this.props.cpModel.stateChanged.connect((changed, value) => { - const selected = '' + value.newValue; - if (selected.length === 0) { - this.setState(old => ({ - ...old, - schema: undefined, - selectedObjectData: undefined - })); - return; - } - if (selected.includes('#')) { - const name = selected.split('#')[0]; - const objectData = this.props.cpModel.jcadModel?.getAllObject(); - if (objectData) { - let schema; - const selectedObj = itemFromName(name, objectData); - if (!selectedObj) { - return; - } - - if (selectedObj.shape) { - schema = formSchema[selectedObj.shape]; - } - const selectedObjectData = selectedObj['parameters']; - this.setState(old => ({ - ...old, - selectedObjectData, - selectedObject: name, - schema - })); - } - } - }); } sharedJcadModelChanged = (_, changed: IJupyterCadDocChange): void => { @@ -124,6 +92,51 @@ class ObjectPropertiesReact extends React.Component { }; } }); + + const awareness = this.props.cpModel.jcadModel?.sharedModel.awareness; + awareness?.on('change', () => { + const localState = awareness.getLocalState(); + if (localState === null) { + return; + } + + const name: string | null = localState['selected']; + + if (name === this.state['selectedObject'] || (name === null && this.state['selectedObject'] === undefined)) { + return; + } + + if (name === null) { + this.setState(old => ({ + ...old, + schema: undefined, + selectedObject: undefined, + selectedObjectData: undefined + })); + return; + } + + const objectData = this.props.cpModel.jcadModel?.getAllObject(); + if (objectData) { + let schema; + const selectedObj = itemFromName(name, objectData); + if (!selectedObj) { + return; + } + + if (selectedObj.shape) { + schema = formSchema[selectedObj.shape]; + } + const selectedObjectData = selectedObj['parameters']; + + this.setState(old => ({ + ...old, + selectedObjectData, + selectedObject: name, + schema + })); + } + }); }; syncObjectProperties( diff --git a/src/toolbar/operatortoolbar.tsx b/src/toolbar/operatortoolbar.tsx index 9a3ebae3..830baaa5 100644 --- a/src/toolbar/operatortoolbar.tsx +++ b/src/toolbar/operatortoolbar.tsx @@ -53,7 +53,6 @@ export class OperatorToolbarReact extends React.Component { }; const model = this.props.toolbarModel.sharedModel; if (model) { - console.log(parameters); const base = model.getObjectByName(parameters['Base']); const tool = model.getObjectByName(parameters['Tool']); const object = new Y.Map(Object.entries(objectModel)); diff --git a/src/types.ts b/src/types.ts index 5d3d1a32..855a9396 100644 --- a/src/types.ts +++ b/src/types.ts @@ -144,6 +144,7 @@ export interface IJupyterCadModel extends DocumentRegistry.IModel { getContent(): IJCadContent; getAllObject(): IJCadModel; syncCamera(pos: Position | undefined): void; + syncSelectedObject(name: string | null): void; getClientId(): number; } export interface IControlPanelState { From 743bc21631986d629c8d45bb91ca02088c7f147f Mon Sep 17 00:00:00 2001 From: Duc Trung LE Date: Fri, 21 Oct 2022 12:11:38 +0200 Subject: [PATCH 2/4] Update `ObjectProperties` --- src/panelview/objectproperties.tsx | 81 +++++++++++++----------------- 1 file changed, 34 insertions(+), 47 deletions(-) diff --git a/src/panelview/objectproperties.tsx b/src/panelview/objectproperties.tsx index 797fa560..33675113 100644 --- a/src/panelview/objectproperties.tsx +++ b/src/panelview/objectproperties.tsx @@ -33,7 +33,6 @@ interface IProps { cpModel: IControlPanelModel; } - class ObjectPropertiesReact extends React.Component { constructor(props: IProps) { super(props); @@ -65,6 +64,39 @@ class ObjectPropertiesReact extends React.Component { }); } }); + this.props.cpModel.stateChanged.connect((changed, value) => { + const selected = '' + value.newValue; + if (selected.length === 0) { + this.setState(old => ({ + ...old, + schema: undefined, + selectedObjectData: undefined + })); + return; + } + if (selected.includes('#')) { + const name = selected.split('#')[0]; + const objectData = this.props.cpModel.jcadModel?.getAllObject(); + if (objectData) { + let schema; + const selectedObj = itemFromName(name, objectData); + if (!selectedObj) { + return; + } + + if (selectedObj.shape) { + schema = formSchema[selectedObj.shape]; + } + const selectedObjectData = selectedObj['parameters']; + this.setState(old => ({ + ...old, + selectedObjectData, + selectedObject: name, + schema + })); + } + } + }); } sharedJcadModelChanged = (_, changed: IJupyterCadDocChange): void => { @@ -92,51 +124,6 @@ class ObjectPropertiesReact extends React.Component { }; } }); - - const awareness = this.props.cpModel.jcadModel?.sharedModel.awareness; - awareness?.on('change', () => { - const localState = awareness.getLocalState(); - if (localState === null) { - return; - } - - const name: string | null = localState['selected']; - - if (name === this.state['selectedObject'] || (name === null && this.state['selectedObject'] === undefined)) { - return; - } - - if (name === null) { - this.setState(old => ({ - ...old, - schema: undefined, - selectedObject: undefined, - selectedObjectData: undefined - })); - return; - } - - const objectData = this.props.cpModel.jcadModel?.getAllObject(); - if (objectData) { - let schema; - const selectedObj = itemFromName(name, objectData); - if (!selectedObj) { - return; - } - - if (selectedObj.shape) { - schema = formSchema[selectedObj.shape]; - } - const selectedObjectData = selectedObj['parameters']; - - this.setState(old => ({ - ...old, - selectedObjectData, - selectedObject: name, - schema - })); - } - }); }; syncObjectProperties( @@ -180,4 +167,4 @@ export namespace ObjectProperties { export interface IOptions extends Panel.IOptions { controlPanelModel: IControlPanelModel; } -} +} \ No newline at end of file From 890151bae8c966444a334d15b3b01f226a03e5bc Mon Sep 17 00:00:00 2001 From: Duc Trung LE Date: Mon, 24 Oct 2022 21:51:10 +0200 Subject: [PATCH 3/4] Sync awareness between components --- src/mainview.tsx | 140 +++++++++++++++-------------- src/model.ts | 26 +++--- src/panelview/objectproperties.tsx | 101 +++++++++++++-------- src/panelview/objecttree.tsx | 85 ++++++++++++++++-- src/types.ts | 6 +- 5 files changed, 234 insertions(+), 124 deletions(-) diff --git a/src/mainview.tsx b/src/mainview.tsx index cb03dc25..70eaacf4 100644 --- a/src/mainview.tsx +++ b/src/mainview.tsx @@ -79,7 +79,7 @@ export class MainView extends React.Component { this._model.themeChanged.connect((_, arg) => { this.handleThemeChange(); }); - this._model.cameraChanged.connect(this._onCameraChanged); + this._model.clientStateChanged.connect(this._onClientSharedStateChanged); }); } @@ -251,20 +251,23 @@ export class MainView extends React.Component { canvas.addEventListener('mouseleave', event => { this._model.syncCamera(undefined); }); - ['wheel', 'mousemove'].forEach(evtName => { - canvas.addEventListener( - evtName as any, - (event: MouseEvent | WheelEvent) => { - this._model.syncCamera({ - offsetX: event.offsetX, - offsetY: event.offsetY, - x: this._camera.position.x, - y: this._camera.position.y, - z: this._camera.position.z - }); - } - ); - }); + // ['wheel', 'mousemove'].forEach(evtName => { + // canvas.addEventListener( + // evtName as any, + // (event: MouseEvent | WheelEvent) => { + // this._model.syncCamera( + // { + // offsetX: event.offsetX, + // offsetY: event.offsetY, + // x: this._camera.position.x, + // y: this._camera.position.y, + // z: this._camera.position.z + // }, + // this.state.id + // ); + // } + // ); + // }); } }; @@ -397,7 +400,10 @@ export class MainView extends React.Component { private _onClick(e: MouseEvent) { this._selectedMesh = this._pick(); - this._model.syncSelectedObject(this._selectedMesh !== null ? this._selectedMesh.name : null); + this._model.syncSelectedObject( + this._selectedMesh !== null ? this._selectedMesh.name : null, + this.state.id + ); } private shapeToMesh = (payload: IDisplayShape['payload']) => { @@ -533,61 +539,63 @@ export class MainView extends React.Component { } }; - private _onCameraChanged = ( + private _onClientSharedStateChanged = ( sender: JupyterCadModel, clients: Map ): void => { - clients.forEach((client, key) => { - if (this._context.model.getClientId() !== key) { - const id = key.toString(); - const mouse = client.mouse as Position; - if (mouse && this._cameraClients[id]) { - if (mouse.offsetX > 0) { - this._cameraClients[id]!.style.left = mouse.offsetX + 'px'; - } - if (mouse.offsetY > 0) { - this._cameraClients[id]!.style.top = mouse.offsetY + 'px'; - } - if (!this._mouseDown) { - this._camera.position.set(mouse.x, mouse.y, mouse.z); - } - } else if (mouse && !this._cameraClients[id]) { - const el = document.createElement('div'); - el.className = 'jpcad-camera-client'; - el.style.left = mouse.offsetX + 'px'; - el.style.top = mouse.offsetY + 'px'; - el.style.backgroundColor = client.user.color; - el.innerText = client.user.name; - this._cameraClients[id] = el; - this._cameraRef.current?.appendChild(el); - } else if (!mouse && this._cameraClients[id]) { - this._cameraRef.current?.removeChild(this._cameraClients[id]!); - this._cameraClients[id] = undefined; + const clientId = this._context.model.getClientId(); + // TODO Handle state changes from another user in follow mode. + const targetId: number | null = null; + if (targetId) { + const remoteState = clients.get(targetId); + const mouse = remoteState.mouse.value as Position; + if (mouse && this._cameraClients[targetId]) { + if (mouse.offsetX > 0) { + this._cameraClients[targetId]!.style.left = mouse.offsetX + 'px'; } - - // if (client.mouse) { - // const cameraPos = client.mouse as Position; - // if (el) { - // el.style.left = cameraPos.offsetX + 'px'; - // el.style.top = cameraPos.offsetY + 'px'; - // } else { - // const newEl = document.createElement('div'); - // newEl.className = 'jpcad-camera-client'; - // newEl.style.left = cameraPos.offsetX + 'px'; - // newEl.style.top = cameraPos.offsetY + 'px'; - // newEl.style.backgroundColor = client.user.color; - // newEl.innerText = client.user.name; - // this._cameraClients[id] = el; - // this._cameraRef.current?.appendChild(newEl); - // } - // } else { - // if (el) { - // this._cameraRef.current?.removeChild(el); - // this._cameraClients[id] = undefined; - // } - // } + if (mouse.offsetY > 0) { + this._cameraClients[targetId]!.style.top = mouse.offsetY + 'px'; + } + if (!this._mouseDown) { + this._camera.position.set(mouse.x, mouse.y, mouse.z); + } + } else if (mouse && !this._cameraClients[targetId]) { + const el = document.createElement('div'); + el.className = 'jpcad-camera-client'; + el.style.left = mouse.offsetX + 'px'; + el.style.top = mouse.offsetY + 'px'; + el.style.backgroundColor = remoteState.user.color; + el.innerText = remoteState.user.name; + this._cameraClients[targetId] = el; + this._cameraRef.current?.appendChild(el); + } else if (!mouse && this._cameraClients[targetId]) { + this._cameraRef.current?.removeChild(this._cameraClients[targetId]!); + this._cameraClients[targetId] = undefined; } - }); + } else { + // We handle here the local state updated by other components + + const localState = clients.get(clientId); + + if (localState) { + if ( + localState['selected'] && + localState['selected']['emitter'] && + localState['selected']['emitter'] !== this.state.id + ) { + this._meshGroup?.children.forEach(obj => { + if (obj.name === localState['selected']['value']) { + this._selectedMesh = obj as THREE.Mesh< + THREE.BufferGeometry, + THREE.MeshBasicMaterial + >; + } + }); + } else { + this._selectedMesh = null; + } + } + } }; render(): JSX.Element { diff --git a/src/model.ts b/src/model.ts index a7f2e4c4..1423c28e 100644 --- a/src/model.ts +++ b/src/model.ts @@ -21,7 +21,7 @@ export class JupyterCadModel implements IJupyterCadModel { constructor(languagePreference?: string, modelDB?: IModelDB) { this.modelDB = modelDB || new ModelDB(); this.sharedModel.changed.connect(this._onSharedModelChanged); - this.sharedModel.awareness.on('change', this._onCameraChanged); + this.sharedModel.awareness.on('change', this._onClientStateChanged); } get isDisposed(): boolean { @@ -141,25 +141,31 @@ export class JupyterCadModel implements IJupyterCadModel { return all; } - syncCamera(pos: Position | undefined): void { - this.sharedModel.awareness.setLocalStateField('mouse', pos); + syncCamera(pos: Position | undefined, emitter?: any): void { + this.sharedModel.awareness.setLocalStateField('mouse', { + value: pos, + emitter: emitter + }); } - syncSelectedObject(name: string | null): void { - this.sharedModel.awareness.setLocalStateField('selected', name); + syncSelectedObject(name: string | null, emitter?: any): void { + this.sharedModel.awareness.setLocalStateField('selected', { + value: name, + emitter: emitter + }); } getClientId(): number { return this.sharedModel.awareness.clientID; } - get cameraChanged(): ISignal> { - return this._cameraChanged; + get clientStateChanged(): ISignal> { + return this._clientStateChanged; } - private _onCameraChanged = () => { + private _onClientStateChanged = () => { const clients = this.sharedModel.awareness.getStates(); - this._cameraChanged.emit(clients); + this._clientStateChanged.emit(clients); }; private _onSharedModelChanged = ( @@ -180,7 +186,7 @@ export class JupyterCadModel implements IJupyterCadModel { private _contentChanged = new Signal(this); private _stateChanged = new Signal>(this); private _themeChanged = new Signal>(this); - private _cameraChanged = new Signal>(this); + private _clientStateChanged = new Signal>(this); private _sharedModelChanged = new Signal(this); static worker: Worker; } diff --git a/src/panelview/objectproperties.tsx b/src/panelview/objectproperties.tsx index 33675113..64f7332c 100644 --- a/src/panelview/objectproperties.tsx +++ b/src/panelview/objectproperties.tsx @@ -4,7 +4,12 @@ import { Panel } from '@lumino/widgets'; import * as React from 'react'; import { itemFromName } from '../tools'; -import { IControlPanelModel, IDict, IJupyterCadDocChange } from '../types'; +import { + IControlPanelModel, + IDict, + IJupyterCadDocChange, + IJupyterCadModel +} from '../types'; import { IJCadModel } from '../_interface/jcad'; import { ObjectPropertiesForm } from './formbuilder'; import formSchema from '../_interface/forms.json'; @@ -27,6 +32,7 @@ interface IStates { selectedObjectData?: IDict; selectedObject?: string; schema?: IDict; + clientId: number | null; } interface IProps { @@ -38,7 +44,8 @@ class ObjectPropertiesReact extends React.Component { super(props); this.state = { filePath: this.props.cpModel.filePath, - jcadObject: this.props.cpModel.jcadModel?.getAllObject() + jcadObject: this.props.cpModel.jcadModel?.getAllObject(), + clientId: null }; this.props.cpModel.jcadModel?.sharedModelChanged.connect( this.sharedJcadModelChanged @@ -48,10 +55,14 @@ class ObjectPropertiesReact extends React.Component { changed.context.model.sharedModelChanged.connect( this.sharedJcadModelChanged ); + changed.context.model.clientStateChanged.connect( + this._onClientSharedStateChanged + ); this.setState(old => ({ ...old, filePath: changed.context.localPath, - jcadObject: this.props.cpModel.jcadModel?.getAllObject() + jcadObject: this.props.cpModel.jcadModel?.getAllObject(), + clientId: changed.context.model.getClientId() })); } else { this.setState({ @@ -64,39 +75,6 @@ class ObjectPropertiesReact extends React.Component { }); } }); - this.props.cpModel.stateChanged.connect((changed, value) => { - const selected = '' + value.newValue; - if (selected.length === 0) { - this.setState(old => ({ - ...old, - schema: undefined, - selectedObjectData: undefined - })); - return; - } - if (selected.includes('#')) { - const name = selected.split('#')[0]; - const objectData = this.props.cpModel.jcadModel?.getAllObject(); - if (objectData) { - let schema; - const selectedObj = itemFromName(name, objectData); - if (!selectedObj) { - return; - } - - if (selectedObj.shape) { - schema = formSchema[selectedObj.shape]; - } - const selectedObjectData = selectedObj['parameters']; - this.setState(old => ({ - ...old, - selectedObjectData, - selectedObject: name, - schema - })); - } - } - }); } sharedJcadModelChanged = (_, changed: IJupyterCadDocChange): void => { @@ -145,6 +123,55 @@ class ObjectPropertiesReact extends React.Component { } } + private _onClientSharedStateChanged = ( + sender: IJupyterCadModel, + clients: Map + ): void => { + const targetId: number | null = null; + const clientId = this.state.clientId; + if (targetId) { + //TODO Sync with remote user in the follow-mode + } else { + // Update from other components of current client + const localState = clientId ? clients.get(clientId) : null; + + if (localState) { + if (localState['selected']) { + const selected = '' + localState['selected'].value; + if (selected.length === 0) { + this.setState(old => ({ + ...old, + schema: undefined, + selectedObjectData: undefined + })); + return; + } + + const name = selected; + const objectData = this.props.cpModel.jcadModel?.getAllObject(); + if (objectData) { + let schema; + const selectedObj = itemFromName(name, objectData); + if (!selectedObj) { + return; + } + + if (selectedObj.shape) { + schema = formSchema[selectedObj.shape]; + } + const selectedObjectData = selectedObj['parameters']; + this.setState(old => ({ + ...old, + selectedObjectData, + selectedObject: name, + schema + })); + } + } + } + } + }; + render(): React.ReactNode { return this.state.schema && this.state.selectedObjectData ? ( { this.state = { filePath: this.props.cpModel.filePath, jcadObject: this.props.cpModel.jcadModel?.getAllObject(), - lightTheme + lightTheme, + selectedNode: null, + clientId: null, + id: uuid(), + openNodes: [] }; this.props.cpModel.jcadModel?.sharedModelChanged.connect( this.sharedJcadModelChanged @@ -100,10 +114,14 @@ class ObjectTreeReact extends React.Component { changed.context.model.themeChanged.connect((_, arg) => { this.handleThemeChange(); }); + changed.context.model.clientStateChanged.connect( + this._onClientSharedStateChanged + ); this.setState(old => ({ ...old, filePath: changed.context.localPath, - jcadObject: this.props.cpModel.jcadModel?.getAllObject() + jcadObject: this.props.cpModel.jcadModel?.getAllObject(), + clientId: changed.context.model.getClientId() })); } else { this.setState({ @@ -168,21 +186,73 @@ class ObjectTreeReact extends React.Component { } } + private _onClientSharedStateChanged = ( + sender: IJupyterCadModel, + clients: Map + ): void => { + const targetId: number | null = null; + const clientId = this.state.clientId; + if (targetId) { + //TODO Sync with remote user in the follow-mode + } else { + // Update from other components of current client + const localState = clientId ? clients.get(clientId) : null; + if (localState) { + if ( + localState['selected'] && + localState['selected']['emitter'] && + localState['selected']['emitter'] !== this.state.id + ) { + const selectedNode = localState['selected'].value; + this.setState(old => ({ + ...old, + selectedNode, + openNodes: [...old.openNodes, selectedNode] + })); + } + } + } + }; + render(): React.ReactNode { const data = this.stateToTree(); - + let selectedNode: (number | string)[] = []; + if (this.state.selectedNode) { + const parentNode = data.filter( + node => node.id === this.state.selectedNode + ); + if (parentNode.length > 0 && parentNode[0].items.length > 0) { + selectedNode = [parentNode[0].items[0].id]; + } + } return (
{ if (id && id.length > 0) { this.props.cpModel.set('activatedObject', id[0]); + let name = id[0] as string; + if (name.includes('#')) { + name = name.split('#')[0]; + + this.props.cpModel.jcadModel?.syncSelectedObject( + name, + this.state.id + ); + } + } else { + this.props.cpModel.jcadModel?.syncSelectedObject(null); } }} + onToggleOpenNodes={nodes => + this.setState(old => ({ ...old, openNodes: nodes })) + } RenderNode={options => { // const paddingLeft = 25 * (options.level + 1); const jcadObj = this.getObjectFromName( @@ -233,10 +303,6 @@ class ObjectTreeReact extends React.Component { }} icon={visible ? visibilityIcon : visibilityOffIcon} /> - {/* - {visible ? 'Hide' : 'Show'} - */} - {/* */} { @@ -245,6 +311,9 @@ class ObjectTreeReact extends React.Component { objectId ); this.props.cpModel.set('activatedObject', ''); + this.props.cpModel.jcadModel?.syncSelectedObject( + null + ); }} icon={closeIcon} /> diff --git a/src/types.ts b/src/types.ts index 855a9396..0e05a35a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -138,13 +138,13 @@ export interface IJupyterCadModel extends DocumentRegistry.IModel { IJupyterCadModel, IChangedArgs >; - cameraChanged: ISignal>; + clientStateChanged: ISignal>; sharedModel: IJupyterCadDoc; getWorker(): Worker; getContent(): IJCadContent; getAllObject(): IJCadModel; - syncCamera(pos: Position | undefined): void; - syncSelectedObject(name: string | null): void; + syncCamera(pos: Position | undefined, emitter?: any): void; + syncSelectedObject(name: string | null, emitter?: any): void; getClientId(): number; } export interface IControlPanelState { From 9e485e07f4efc0096067376be30c325ec1afa180 Mon Sep 17 00:00:00 2001 From: Duc Trung LE Date: Tue, 25 Oct 2022 13:35:14 +0200 Subject: [PATCH 4/4] Code cleanup --- src/mainview.tsx | 129 +++++++++++++++-------------- src/model.ts | 20 +++-- src/panelview/model.ts | 35 ++------ src/panelview/objectproperties.tsx | 78 ++++++++++------- src/panelview/objecttree.tsx | 59 ++++++------- src/types.ts | 36 +++----- 6 files changed, 173 insertions(+), 184 deletions(-) diff --git a/src/mainview.tsx b/src/mainview.tsx index 70eaacf4..25b34a5a 100644 --- a/src/mainview.tsx +++ b/src/mainview.tsx @@ -11,10 +11,10 @@ import { JupyterCadModel } from './model'; import { IDict, IDisplayShape, + IJupyterCadClientState, IMainMessage, IWorkerMessage, MainAction, - Position, WorkerAction } from './types'; @@ -32,7 +32,8 @@ interface IProps { } interface IStates { - id: string; + id: string; // ID of the component, it is used to identify which component + //is the source of awareness updates. loading: boolean; lightTheme: boolean; } @@ -45,10 +46,7 @@ export class MainView extends React.Component { this._geometry.setDrawRange(0, 3 * 10000); this._refLength = 0; this._sceneAxe = []; - // this.shapeGroup = new THREE.Group(); - // this.sceneScaled = false; - // this.computedScene = {}; - // this.progressData = { time_step: -1, data: {} }; + this._resizeTimeout = null; const lightTheme = @@ -76,15 +74,13 @@ export class MainView extends React.Component { { action: WorkerAction.REGISTER, payload: { id: this.state.id } }, this._messageChannel.port2 ); - this._model.themeChanged.connect((_, arg) => { - this.handleThemeChange(); - }); + this._model.themeChanged.connect(this._handleThemeChange); this._model.clientStateChanged.connect(this._onClientSharedStateChanged); }); } componentDidMount(): void { - window.addEventListener('resize', this.handleWindowResize); + window.addEventListener('resize', this._handleWindowResize); this.generateScene(); } @@ -94,7 +90,7 @@ export class MainView extends React.Component { componentWillUnmount(): void { window.cancelAnimationFrame(this._requestID); - window.removeEventListener('resize', this.handleWindowResize); + window.removeEventListener('resize', this._handleWindowResize); this._controls.dispose(); this.postMessage({ action: WorkerAction.CLOSE_FILE, @@ -102,21 +98,10 @@ export class MainView extends React.Component { fileName: this._context.path } }); + this._model.themeChanged.disconnect(this._handleThemeChange); + this._model.clientStateChanged.disconnect(this._onClientSharedStateChanged); } - handleThemeChange = (): void => { - const lightTheme = - document.body.getAttribute('data-jp-theme-light') === 'true'; - this.setState(old => ({ ...old, lightTheme })); - }; - - handleWindowResize = () => { - clearTimeout(this._resizeTimeout); - this._resizeTimeout = setTimeout(() => { - this.forceUpdate(); - }, 500); - }; - addSceneAxe = (dir: THREE.Vector3, color: number): void => { const origin = new THREE.Vector3(0, 0, 0); const length = 20; @@ -251,23 +236,23 @@ export class MainView extends React.Component { canvas.addEventListener('mouseleave', event => { this._model.syncCamera(undefined); }); - // ['wheel', 'mousemove'].forEach(evtName => { - // canvas.addEventListener( - // evtName as any, - // (event: MouseEvent | WheelEvent) => { - // this._model.syncCamera( - // { - // offsetX: event.offsetX, - // offsetY: event.offsetY, - // x: this._camera.position.x, - // y: this._camera.position.y, - // z: this._camera.position.z - // }, - // this.state.id - // ); - // } - // ); - // }); + ['wheel', 'mousemove'].forEach(evtName => { + canvas.addEventListener( + evtName as any, + (event: MouseEvent | WheelEvent) => { + this._model.syncCamera( + { + offsetX: event.offsetX, + offsetY: event.offsetY, + x: this._camera.position.x, + y: this._camera.position.y, + z: this._camera.position.z + }, + this.state.id + ); + } + ); + }); } }; @@ -398,12 +383,17 @@ export class MainView extends React.Component { } private _onClick(e: MouseEvent) { - this._selectedMesh = this._pick(); - - this._model.syncSelectedObject( - this._selectedMesh !== null ? this._selectedMesh.name : null, - this.state.id - ); + const selectedMesh = this._pick(); + if (selectedMesh) { + if (selectedMesh === this._selectedMesh) { + this._selectedMesh = null; + } else { + this._selectedMesh = selectedMesh; + } + if (this._selectedMesh) { + this._model.syncSelectedObject(this._selectedMesh.name, this.state.id); + } + } } private shapeToMesh = (payload: IDisplayShape['payload']) => { @@ -541,14 +531,14 @@ export class MainView extends React.Component { private _onClientSharedStateChanged = ( sender: JupyterCadModel, - clients: Map + clients: Map ): void => { const clientId = this._context.model.getClientId(); // TODO Handle state changes from another user in follow mode. const targetId: number | null = null; if (targetId) { - const remoteState = clients.get(targetId); - const mouse = remoteState.mouse.value as Position; + const remoteState = clients.get(targetId)!; + const mouse = remoteState?.mouse.value; if (mouse && this._cameraClients[targetId]) { if (mouse.offsetX > 0) { this._cameraClients[targetId]!.style.left = mouse.offsetX + 'px'; @@ -573,24 +563,24 @@ export class MainView extends React.Component { this._cameraClients[targetId] = undefined; } } else { - // We handle here the local state updated by other components + // Sync local state updated by other components const localState = clients.get(clientId); - if (localState) { if ( - localState['selected'] && - localState['selected']['emitter'] && - localState['selected']['emitter'] !== this.state.id + localState.selected?.emitter && + localState.selected?.emitter !== this.state.id ) { - this._meshGroup?.children.forEach(obj => { - if (obj.name === localState['selected']['value']) { - this._selectedMesh = obj as THREE.Mesh< - THREE.BufferGeometry, - THREE.MeshBasicMaterial - >; - } - }); + if (this._selectedMesh?.name !== localState.selected.value) { + this._meshGroup?.children.forEach(obj => { + if (obj.name === localState.selected.value) { + this._selectedMesh = obj as THREE.Mesh< + THREE.BufferGeometry, + THREE.MeshBasicMaterial + >; + } + }); + } } else { this._selectedMesh = null; } @@ -598,6 +588,19 @@ export class MainView extends React.Component { } }; + private _handleThemeChange = (): void => { + const lightTheme = + document.body.getAttribute('data-jp-theme-light') === 'true'; + this.setState(old => ({ ...old, lightTheme })); + }; + + private _handleWindowResize = (): void => { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = setTimeout(() => { + this.forceUpdate(); + }, 500); + }; + render(): JSX.Element { return (
> { + get clientStateChanged(): ISignal> { return this._clientStateChanged; } - private _onClientStateChanged = () => { - const clients = this.sharedModel.awareness.getStates(); + private _onClientStateChanged = changed => { + const clients = this.sharedModel.awareness.getStates() as Map< + number, + IJupyterCadClientState + >; + this._clientStateChanged.emit(clients); }; @@ -186,7 +191,10 @@ export class JupyterCadModel implements IJupyterCadModel { private _contentChanged = new Signal(this); private _stateChanged = new Signal>(this); private _themeChanged = new Signal>(this); - private _clientStateChanged = new Signal>(this); + private _clientStateChanged = new Signal< + this, + Map + >(this); private _sharedModelChanged = new Signal(this); static worker: Worker; } diff --git a/src/panelview/model.ts b/src/panelview/model.ts index c45c0db0..97cbb98f 100644 --- a/src/panelview/model.ts +++ b/src/panelview/model.ts @@ -1,34 +1,19 @@ -import { ObservableMap } from '@jupyterlab/observables'; import { ISignal } from '@lumino/signaling'; import { IJupyterCadTracker } from '../token'; import { IControlPanelModel, - IControlPanelState, IJupyterCadDoc, IJupyterCadModel, - IJupyterCadWidget, - ISateChangedSignal, - IStateValue + IJupyterCadWidget } from '../types'; export class ControlPanelModel implements IControlPanelModel { constructor(options: ControlPanelModel.IOptions) { - const state = { activatedObject: '', visibleObjects: [] }; - this._state = new ObservableMap({ values: state }); - this._stateChanged = this._state.changed; this._tracker = options.tracker; this._documentChanged = this._tracker.currentChanged; } - get state(): ObservableMap { - return this._state; - } - - get stateChanged(): ISateChangedSignal { - return this._stateChanged; - } - get documentChanged(): ISignal { return this._documentChanged; } @@ -45,26 +30,16 @@ export class ControlPanelModel implements IControlPanelModel { return this._tracker.currentWidget?.context.model.sharedModel; } - set(key: keyof IControlPanelState, value: IStateValue): void { - this._state.set(key, value); - } - - get(key: keyof IControlPanelState): IStateValue | undefined { - return this._state.get(key); - } - - has(key: keyof IControlPanelState): boolean { - return this._state.has(key); - } - disconnect(f: any): void { this._tracker.forEach(w => w.context.model.sharedModelChanged.disconnect(f) ); + this._tracker.forEach(w => w.context.model.themeChanged.disconnect(f)); + this._tracker.forEach(w => + w.context.model.clientStateChanged.disconnect(f) + ); } - private readonly _stateChanged: ISateChangedSignal; - private readonly _state: ObservableMap; private readonly _tracker: IJupyterCadTracker; private _documentChanged: ISignal< IJupyterCadTracker, diff --git a/src/panelview/objectproperties.tsx b/src/panelview/objectproperties.tsx index 64f7332c..46c06ab1 100644 --- a/src/panelview/objectproperties.tsx +++ b/src/panelview/objectproperties.tsx @@ -7,12 +7,15 @@ import { itemFromName } from '../tools'; import { IControlPanelModel, IDict, + IJupyterCadClientState, IJupyterCadDocChange, IJupyterCadModel } from '../types'; import { IJCadModel } from '../_interface/jcad'; import { ObjectPropertiesForm } from './formbuilder'; import formSchema from '../_interface/forms.json'; +import { v4 as uuid } from 'uuid'; + export class ObjectProperties extends PanelWithToolbar { constructor(params: ObjectProperties.IOptions) { super(params); @@ -32,7 +35,9 @@ interface IStates { selectedObjectData?: IDict; selectedObject?: string; schema?: IDict; - clientId: number | null; + clientId: number | null; // ID of the yjs client + id: string; // ID of the component, it is used to identify which component + //is the source of awareness updates. } interface IProps { @@ -45,15 +50,19 @@ class ObjectPropertiesReact extends React.Component { this.state = { filePath: this.props.cpModel.filePath, jcadObject: this.props.cpModel.jcadModel?.getAllObject(), - clientId: null + clientId: null, + id: uuid() }; this.props.cpModel.jcadModel?.sharedModelChanged.connect( - this.sharedJcadModelChanged + this._sharedJcadModelChanged ); this.props.cpModel.documentChanged.connect((_, changed) => { if (changed) { + this.props.cpModel.disconnect(this._sharedJcadModelChanged); + this.props.cpModel.disconnect(this._onClientSharedStateChanged); + changed.context.model.sharedModelChanged.connect( - this.sharedJcadModelChanged + this._sharedJcadModelChanged ); changed.context.model.clientStateChanged.connect( this._onClientSharedStateChanged @@ -77,7 +86,7 @@ class ObjectPropertiesReact extends React.Component { }); } - sharedJcadModelChanged = (_, changed: IJupyterCadDocChange): void => { + _sharedJcadModelChanged = (_, changed: IJupyterCadDocChange): void => { this.setState(old => { if (old.selectedObject) { const jcadObject = this.props.cpModel.jcadModel?.getAllObject(); @@ -125,7 +134,7 @@ class ObjectPropertiesReact extends React.Component { private _onClientSharedStateChanged = ( sender: IJupyterCadModel, - clients: Map + clients: Map ): void => { const targetId: number | null = null; const clientId = this.state.clientId; @@ -136,36 +145,41 @@ class ObjectPropertiesReact extends React.Component { const localState = clientId ? clients.get(clientId) : null; if (localState) { - if (localState['selected']) { - const selected = '' + localState['selected'].value; - if (selected.length === 0) { - this.setState(old => ({ - ...old, - schema: undefined, - selectedObjectData: undefined - })); - return; - } - - const name = selected; - const objectData = this.props.cpModel.jcadModel?.getAllObject(); - if (objectData) { - let schema; - const selectedObj = itemFromName(name, objectData); - if (!selectedObj) { + if ( + localState.selected?.emitter && + localState.selected.emitter !== this.state.id && + localState.selected?.value + ) { + const selected = '' + localState.selected.value; + if (selected !== this.state.selectedObject) { + if (selected.length === 0) { + this.setState(old => ({ + ...old, + schema: undefined, + selectedObjectData: undefined + })); return; } - if (selectedObj.shape) { - schema = formSchema[selectedObj.shape]; + const objectData = this.props.cpModel.jcadModel?.getAllObject(); + if (objectData) { + let schema; + const selectedObj = itemFromName(selected, objectData); + if (!selectedObj) { + return; + } + + if (selectedObj.shape) { + schema = formSchema[selectedObj.shape]; + } + const selectedObjectData = selectedObj['parameters']; + this.setState(old => ({ + ...old, + selectedObjectData, + selectedObject: selected, + schema + })); } - const selectedObjectData = selectedObj['parameters']; - this.setState(old => ({ - ...old, - selectedObjectData, - selectedObject: name, - schema - })); } } } diff --git a/src/panelview/objecttree.tsx b/src/panelview/objecttree.tsx index da4aea05..b9dc1abc 100644 --- a/src/panelview/objecttree.tsx +++ b/src/panelview/objecttree.tsx @@ -16,6 +16,7 @@ import { IJCadModel, IJCadObject } from '../_interface/jcad'; import { IControlPanelModel, IDict, + IJupyterCadClientState, IJupyterCadDocChange, IJupyterCadModel } from '../types'; @@ -77,8 +78,9 @@ interface IStates { jcadObject?: IJCadModel; lightTheme: boolean; selectedNode: string | null; - clientId: number | null; - id: string; + clientId: number | null; // ID of the yjs client + id: string; // ID of the component, it is used to identify which component + //is the source of awareness updates. openNodes: (string | number)[]; } @@ -103,17 +105,18 @@ class ObjectTreeReact extends React.Component { openNodes: [] }; this.props.cpModel.jcadModel?.sharedModelChanged.connect( - this.sharedJcadModelChanged + this._sharedJcadModelChanged ); this.props.cpModel.documentChanged.connect((_, changed) => { if (changed) { - this.props.cpModel.disconnect(this.sharedJcadModelChanged); + this.props.cpModel.disconnect(this._sharedJcadModelChanged); + this.props.cpModel.disconnect(this._handleThemeChange); + this.props.cpModel.disconnect(this._onClientSharedStateChanged); + changed.context.model.sharedModelChanged.connect( - this.sharedJcadModelChanged + this._sharedJcadModelChanged ); - changed.context.model.themeChanged.connect((_, arg) => { - this.handleThemeChange(); - }); + changed.context.model.themeChanged.connect(this._handleThemeChange); changed.context.model.clientStateChanged.connect( this._onClientSharedStateChanged ); @@ -133,19 +136,6 @@ class ObjectTreeReact extends React.Component { }); } - handleThemeChange = (): void => { - const lightTheme = - document.body.getAttribute('data-jp-theme-light') === 'true'; - this.setState(old => ({ ...old, lightTheme })); - }; - - sharedJcadModelChanged = (_, changed: IJupyterCadDocChange): void => { - this.setState(old => ({ - ...old, - jcadObject: this.props.cpModel.jcadModel?.getAllObject() - })); - }; - stateToTree = () => { if (this.state.jcadObject) { return this.state.jcadObject.map(obj => { @@ -186,9 +176,25 @@ class ObjectTreeReact extends React.Component { } } + private _handleThemeChange = (): void => { + const lightTheme = + document.body.getAttribute('data-jp-theme-light') === 'true'; + this.setState(old => ({ ...old, lightTheme })); + }; + + private _sharedJcadModelChanged = ( + _, + changed: IJupyterCadDocChange + ): void => { + this.setState(old => ({ + ...old, + jcadObject: this.props.cpModel.jcadModel?.getAllObject() + })); + }; + private _onClientSharedStateChanged = ( sender: IJupyterCadModel, - clients: Map + clients: Map ): void => { const targetId: number | null = null; const clientId = this.state.clientId; @@ -199,11 +205,10 @@ class ObjectTreeReact extends React.Component { const localState = clientId ? clients.get(clientId) : null; if (localState) { if ( - localState['selected'] && - localState['selected']['emitter'] && - localState['selected']['emitter'] !== this.state.id + localState.selected?.emitter && + localState.selected.emitter !== this.state.id ) { - const selectedNode = localState['selected'].value; + const selectedNode = localState.selected.value!; this.setState(old => ({ ...old, selectedNode, @@ -236,7 +241,6 @@ class ObjectTreeReact extends React.Component { themes={TREE_THEMES} onToggleSelectedNodes={id => { if (id && id.length > 0) { - this.props.cpModel.set('activatedObject', id[0]); let name = id[0] as string; if (name.includes('#')) { name = name.split('#')[0]; @@ -310,7 +314,6 @@ class ObjectTreeReact extends React.Component { this.props.cpModel.jcadModel?.sharedModel.removeObjectByName( objectId ); - this.props.cpModel.set('activatedObject', ''); this.props.cpModel.jcadModel?.syncSelectedObject( null ); diff --git a/src/types.ts b/src/types.ts index 0e05a35a..a2f4cd87 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,6 @@ import { IJCadObject } from './_interface/jcad.d'; import { IChangedArgs } from '@jupyterlab/coreutils'; import { DocumentRegistry, IDocumentWidget } from '@jupyterlab/docregistry'; -import { IObservableMap, ObservableMap } from '@jupyterlab/observables'; import { MapChange, YDocument } from '@jupyterlab/shared-models'; import { ReactWidget } from '@jupyterlab/ui-components'; import { ISignal, Signal } from '@lumino/signaling'; @@ -103,13 +102,6 @@ export interface IJcadObjectDocChange { objectChange?: MapChange; } -// export interface IJcadObjectDoc extends Y.Map { -// getObject(): IJcadObject; -// getProperty(key: keyof IJcadObject): ValueOf | undefined; -// setProperty(key: keyof IJcadObject, value: ValueOf): void; -// changed: ISignal; -// } - export interface IJupyterCadDocChange { contextChange?: MapChange; contentChange?: MapChange; @@ -131,6 +123,11 @@ export interface IJupyterCadDoc extends YDocument { setOption(key: string, value: any): void; } +export interface IJupyterCadClientState { + mouse: { value?: Position | null; emitter?: string | null }; + selected: { value?: string | null; emitter?: string | null }; + user: any; +} export interface IJupyterCadModel extends DocumentRegistry.IModel { isDisposed: boolean; sharedModelChanged: ISignal; @@ -138,32 +135,21 @@ export interface IJupyterCadModel extends DocumentRegistry.IModel { IJupyterCadModel, IChangedArgs >; - clientStateChanged: ISignal>; + clientStateChanged: ISignal< + IJupyterCadModel, + Map + >; sharedModel: IJupyterCadDoc; getWorker(): Worker; getContent(): IJCadContent; getAllObject(): IJCadModel; - syncCamera(pos: Position | undefined, emitter?: any): void; - syncSelectedObject(name: string | null, emitter?: any): void; + syncCamera(pos: Position | undefined, emitter?: string): void; + syncSelectedObject(name: string | null, emitter?: string): void; getClientId(): number; } -export interface IControlPanelState { - activatedObject: string; -} - -export type IStateValue = string | number | any[]; -export type ISateChangedSignal = ISignal< - ObservableMap, - IObservableMap.IChangedArgs ->; export type IJupyterCadWidget = IDocumentWidget; export interface IControlPanelModel { - state: ObservableMap; - stateChanged: ISateChangedSignal; - set(key: keyof IControlPanelState, value: IStateValue): void; - get(key: keyof IControlPanelState): IStateValue | undefined; - has(key: keyof IControlPanelState): boolean; disconnect(f: any): void; documentChanged: ISignal; filePath: string | undefined;