From 3bd6c608d3ae381ad970dceed19992357998f353 Mon Sep 17 00:00:00 2001 From: Xiao Gui <xgui3783@gmail.com> Date: Sun, 4 Jul 2021 18:11:01 +0200 Subject: [PATCH] feat: freesurfer parses global nav state it also sets global nav state --- deploy/csp/index.js | 2 +- src/index.html | 2 +- src/viewerModule/componentStore.ts | 14 +- .../threeSurferGlue/threeSurfer.component.ts | 276 +++++++++++++----- 4 files changed, 224 insertions(+), 70 deletions(-) diff --git a/deploy/csp/index.js b/deploy/csp/index.js index 928402e53..303ae384c 100644 --- a/deploy/csp/index.js +++ b/deploy/csp/index.js @@ -120,7 +120,7 @@ module.exports = (app) => { 'unpkg.com/react@16/umd/', // plugin load external lib -> react 'unpkg.com/kg-dataset-previewer@1.2.0/', // preview component 'cdnjs.cloudflare.com/ajax/libs/mathjax/', // math jax - 'https://unpkg.com/three-surfer@0.0.8/dist/bundle.js', // for threeSurfer (freesurfer support in browser) + 'https://unpkg.com/three-surfer@0.0.10/dist/bundle.js', // for threeSurfer (freesurfer support in browser) (req, res) => res.locals.nonce ? `'nonce-${res.locals.nonce}'` : null, ...SCRIPT_SRC, ...WHITE_LIST_SRC, diff --git a/src/index.html b/src/index.html index 39aafb5fb..f7c2ab295 100644 --- a/src/index.html +++ b/src/index.html @@ -15,7 +15,7 @@ <script src="https://unpkg.com/kg-dataset-previewer@1.2.0/dist/kg-dataset-previewer/kg-dataset-previewer.js" defer> </script> - <script src="https://unpkg.com/three-surfer@0.0.8/dist/bundle.js" defer></script> + <script src="https://unpkg.com/three-surfer@0.0.10/dist/bundle.js" defer></script> <title>Interactive Atlas Viewer</title> <script type="application/ld+json"> diff --git a/src/viewerModule/componentStore.ts b/src/viewerModule/componentStore.ts index ca3d8e508..5fe24c510 100644 --- a/src/viewerModule/componentStore.ts +++ b/src/viewerModule/componentStore.ts @@ -3,6 +3,8 @@ import { select } from "@ngrx/store"; import { ReplaySubject, Subject } from "rxjs"; import { shareReplay } from "rxjs/operators"; +export class LockError extends Error{} + /** * polyfill for ngrx component store * until upgrade to v11 @@ -12,13 +14,23 @@ import { shareReplay } from "rxjs/operators"; @Injectable() export class ComponentStore<T>{ private _state$: Subject<T> = new ReplaySubject<T>(1) + private _lock: boolean = false + get isLocked() { + return this._lock + } setState(state: T){ + if (this.isLocked) throw new LockError('State is locked') this._state$.next(state) } - select(selectorFn: (state: T) => unknown) { + select<V>(selectorFn: (state: T) => V) { return this._state$.pipe( select(selectorFn), shareReplay(1), ) } + getLock(): () => void { + if (this.isLocked) throw new LockError('Cannot get lock. State is locked') + this._lock = true + return () => this._lock = false + } } diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts index 69f638277..206b18cef 100644 --- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts +++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts @@ -3,22 +3,51 @@ import { EnumViewerEvt, IViewer, TViewerEvent } from "src/viewerModule/viewer.in import { TThreeSurferConfig, TThreeSurferMode } from "../types"; import { parseContext } from "../util"; import { retry, flattenRegions } from 'common/util' +import { Subject } from "rxjs"; +import { debounceTime, filter } from "rxjs/operators"; +import { ComponentStore } from "src/viewerModule/componentStore"; +import { select, Store } from "@ngrx/store"; +import { viewerStateChangeNavigation } from "src/services/state/viewerState/actions"; +import { viewerStateSelectorNavigation } from "src/services/state/viewerState/selectors"; type THandlingCustomEv = { regions: ({ name?: string, error?: string })[] - event: CustomEvent evMesh?: { faceIndex: number verticesIndicies: number[] } } +type TCameraOrientation = { + perspectiveOrientation: [number, number, number, number] + perspectiveZoom: number +} + +const threshold = 1e-3 + +function cameraNavsAreSimilar(c1: TCameraOrientation, c2: TCameraOrientation){ + if (c1 === c2) return true + if (!!c1 && !!c2) return true + + if (!c1 && !!c2) return false + if (!c2 && !!c1) return false + + if (Math.abs(c1.perspectiveZoom - c2.perspectiveZoom) > threshold) return false + if ([0, 1, 2, 3].some( + idx => Math.abs(c1.perspectiveOrientation[idx] - c2.perspectiveOrientation[idx]) > threshold + )) { + return false + } + return true +} + @Component({ selector: 'three-surfer-glue-cmp', templateUrl: './threeSurfer.template.html', styleUrls: [ './threeSurfer.style.css' - ] + ], + providers: [ ComponentStore ] }) export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, AfterViewInit, OnDestroy { @@ -37,13 +66,101 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af public modes: TThreeSurferMode[] = [] public selectedMode: string + private mainStoreCameraNav: TCameraOrientation = null + private localCameraNav: TCameraOrientation = null + public allKeys: {name: string, checked: boolean}[] = [] private regionMap: Map<string, Map<number, any>> = new Map() constructor( private el: ElementRef, + private store$: Store<any>, + private navStateStoreRelay: ComponentStore<{ perspectiveOrientation: [number, number, number, number], perspectiveZoom: number }>, ){ this.domEl = this.el.nativeElement + + /** + * subscribe to camera custom event + */ + const cameraSub = this.cameraEv$.pipe( + filter(v => !!v), + debounceTime(160) + ).subscribe(ev => { + const { position } = ev + const { x, y, z } = position + + const THREE = (window as any).ThreeSurfer.THREE + + const q = new THREE.Quaternion() + const t = new THREE.Vector3() + const s = new THREE.Vector3() + + const cameraM = this.tsRef.camera.matrix + cameraM.decompose(t, q, s) + try { + this.navStateStoreRelay.setState({ + perspectiveOrientation: q.toArray(), + perspectiveZoom: t.length() + }) + } catch (e) { + // LockError, ignore + } + }) + + this.onDestroyCb.push( + () => cameraSub.unsubscribe() + ) + + /** + * subscribe to navstore relay store and negotiate setting global state + */ + const navStateSub = this.navStateStoreRelay.select(s => s).subscribe(v => { + this.store$.dispatch( + viewerStateChangeNavigation({ + navigation: { + position: [0, 0, 0], + orientation: [0, 0, 0, 1], + zoom: 1, + perspectiveOrientation: v.perspectiveOrientation, + perspectiveZoom: v.perspectiveZoom + } + }) + ) + }) + + this.onDestroyCb.push( + () => navStateSub.unsubscribe() + ) + + /** + * subscribe to main store and negotiate with relay to set camera + */ + const navSub = this.store$.pipe( + select(viewerStateSelectorNavigation) + ).subscribe(nav => { + const { perspectiveOrientation, perspectiveZoom } = nav + this.mainStoreCameraNav = { + perspectiveOrientation, + perspectiveZoom + } + + if (!cameraNavsAreSimilar(this.mainStoreCameraNav, this.localCameraNav)) { + this.relayStoreLock = this.navStateStoreRelay.getLock() + const THREE = (window as any).ThreeSurfer.THREE + + const cameraQuat = new THREE.Quaternion(...this.mainStoreCameraNav.perspectiveOrientation) + const cameraPos = new THREE.Vector3(0, 0, this.mainStoreCameraNav.perspectiveZoom) + cameraPos.applyQuaternion(cameraQuat) + this.toTsRef(tsRef => { + tsRef.camera.position.copy(cameraPos) + if (this.relayStoreLock) this.relayStoreLock() + }) + } + }) + + this.onDestroyCb.push( + () => navSub.unsubscribe() + ) } tsRef: any @@ -55,6 +172,16 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af vIdxArr: number[] }[] = [] + private relayStoreLock: () => void = null + private tsRefInitCb: ((tsRef: any) => void)[] = [] + private toTsRef(callback: (tsRef: any) => void) { + if (this.tsRef) { + callback(this.tsRef) + return + } + this.tsRefInitCb.push(callback) + } + private unloadAllMeshes() { this.allKeys = [] while(this.loadedMeshes.length > 0) { @@ -144,7 +271,9 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af this.tsRef.dispose() this.tsRef = null } - ) + ); + (window as any).tsRef = this.tsRef + while (this.tsRefInitCb.length > 0) this.tsRefInitCb.pop()(this.tsRef) } const flattenedRegions = flattenRegions(this.selectedParcellation.regions) @@ -174,81 +303,94 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af } } - ngAfterViewInit(){ - const customEvHandler = (ev: CustomEvent) => { - const evMesh = ev.detail?.mesh && { - faceIndex: ev.detail.mesh.faceIndex, - // typo in three-surfer - verticesIndicies: ev.detail.mesh.verticesIdicies - } - const custEv: THandlingCustomEv = { - event: ev, - regions: [], - evMesh - } - - if (!ev.detail.mesh) { - return this.handleMouseoverEvent(custEv) - } + private handleCustomMouseEv(detail: any){ + const evMesh = detail.mesh && { + faceIndex: detail.mesh.faceIndex, + // typo in three-surfer + verticesIndicies: detail.mesh.verticesIdicies + } + const custEv: THandlingCustomEv = { + regions: [], + evMesh + } + + if (!detail.mesh) { + return this.handleMouseoverEvent(custEv) + } - const evGeom = ev.detail.mesh.geometry - const evVertIdx = ev.detail.mesh.verticesIdicies - const found = this.loadedMeshes.find(({ threeSurfer }) => threeSurfer === evGeom) - - if (!found) return this.handleMouseoverEvent(custEv) - - /** - * check if the mesh is toggled off - * if so, do not proceed - */ - const checkKey = this.allKeys.find(key => key.name === found.hemisphere) - if (checkKey && !checkKey.checked) return + const evGeom = detail.mesh.geometry + const evVertIdx = detail.mesh.verticesIdicies + const found = this.loadedMeshes.find(({ threeSurfer }) => threeSurfer === evGeom) + if (!found) return this.handleMouseoverEvent(custEv) - const { hemisphere: key, vIdxArr } = found + /** + * check if the mesh is toggled off + * if so, do not proceed + */ + const checkKey = this.allKeys.find(key => key.name === found.hemisphere) + if (checkKey && !checkKey.checked) return - if (!key || !evVertIdx) { - return this.handleMouseoverEvent(custEv) - } + const { hemisphere: key, vIdxArr } = found - const labelIdxSet = new Set<number>() - - for (const vIdx of evVertIdx) { - labelIdxSet.add( - vIdxArr[vIdx] - ) - } - if (labelIdxSet.size === 0) { - return this.handleMouseoverEvent(custEv) - } + if (!key || !evVertIdx) { + return this.handleMouseoverEvent(custEv) + } - const hemisphereMap = this.regionMap.get(key) + const labelIdxSet = new Set<number>() + + for (const vIdx of evVertIdx) { + labelIdxSet.add( + vIdxArr[vIdx] + ) + } + if (labelIdxSet.size === 0) { + return this.handleMouseoverEvent(custEv) + } - if (!hemisphereMap) { - custEv.regions = Array.from(labelIdxSet).map(v => { + const hemisphereMap = this.regionMap.get(key) + + if (!hemisphereMap) { + custEv.regions = Array.from(labelIdxSet).map(v => { + return { + error: `unknown#${v}` + } + }) + return this.handleMouseoverEvent(custEv) + } + + custEv.regions = Array.from(labelIdxSet) + .map(lblIdx => { + const ontoR = hemisphereMap.get(lblIdx) + if (ontoR) { + return ontoR + } else { return { - error: `unknown#${v}` + error: `unkonwn#${lblIdx}` } - }) - return this.handleMouseoverEvent(custEv) - } + } + }) + return this.handleMouseoverEvent(custEv) - custEv.regions = Array.from(labelIdxSet) - .map(lblIdx => { - const ontoR = hemisphereMap.get(lblIdx) - if (ontoR) { - return ontoR - } else { - return { - error: `unkonwn#${lblIdx}` - } - } - }) - return this.handleMouseoverEvent(custEv) - } + } + + private cameraEv$ = new Subject<{ position: { x: number, y: number, z: number }, zoom: number }>() + private handleCustomCameraEvent(detail: any){ + this.cameraEv$.next(detail) + } - this.domEl.addEventListener((window as any).ThreeSurfer.CUSTOM_EVENTNAME, customEvHandler) + ngAfterViewInit(){ + const customEvHandler = (ev: CustomEvent) => { + const { type, data } = ev.detail + if (type === 'mouseover') { + return this.handleCustomMouseEv(data) + } + if (type === 'camera') { + return this.handleCustomCameraEvent(data) + } + } + this.domEl.addEventListener((window as any).ThreeSurfer.CUSTOM_EVENTNAME_UPDATED, customEvHandler) this.onDestroyCb.push( - () => this.domEl.removeEventListener((window as any).ThreeSurfer.CUSTOM_EVENTNAME, customEvHandler) + () => this.domEl.removeEventListener((window as any).ThreeSurfer.CUSTOM_EVENTNAME_UPDATED, customEvHandler) ) } -- GitLab