From 4f9c099c1cb52d4fdac57e8bc806702442012ea4 Mon Sep 17 00:00:00 2001 From: Duc Trung LE Date: Mon, 24 Oct 2022 21:51:10 +0200 Subject: [PATCH] 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 {