Skip to content
Snippets Groups Projects
Commit 3bd6c608 authored by Xiao Gui's avatar Xiao Gui
Browse files

feat: freesurfer parses global nav state

it also sets global nav state
parent e2b621dc
No related branches found
No related tags found
No related merge requests found
......@@ -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,
......
......@@ -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">
......
......@@ -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
}
}
......@@ -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)
)
}
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment