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