Newer
Older
import { Component, Input, Output, EventEmitter, ElementRef, OnChanges, OnDestroy, AfterViewInit } from "@angular/core";
import { EnumViewerEvt, IViewer, TViewerEvent } from "src/viewerModule/viewer.interface";
import { TThreeSurferConfig, TThreeSurferMode } from "../types";
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 })[]
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'
export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, AfterViewInit, OnDestroy {
@Input()
selectedTemplate: any
@Input()
selectedParcellation: any
@Output()
viewerEvent = new EventEmitter<TViewerEvent<'threeSurfer'>>()
private domEl: HTMLElement
private config: TThreeSurferConfig
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()
private store$: Store<any>,
private navStateStoreRelay: ComponentStore<{ perspectiveOrientation: [number, number, number, number], perspectiveZoom: number }>,
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/**
* 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()
)
threeSurfer: any
mesh: string
colormap: string
hemisphere: string
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)
}
while(this.loadedMeshes.length > 0) {
const m = this.loadedMeshes.pop()
}
}
public async loadMode(mode: TThreeSurferMode) {
this.unloadAllMeshes()
this.selectedMode = mode.name
const { meshes } = mode
await retry(async () => {
for (const singleMesh of meshes) {
const { hemisphere } = singleMesh
if (!this.regionMap.has(hemisphere)) throw new Error(`regionmap does not have hemisphere defined!`)
}
}, {
timeout: 32,
retries: 10
})
this.allKeys.push({name: hemisphere, checked: true})
const tsM = await this.tsRef.loadMesh(
parseContext(mesh, [this.config['@context']])
)
if (!this.regionMap.has(hemisphere)) continue
const rMap = this.regionMap.get(hemisphere)
const applyCM = new Map()
for (const [ lblIdx, region ] of rMap.entries()) {
applyCM.set(lblIdx, (region.rgb || [200, 200, 200]).map(v => v/255))
}
const tsC = await this.tsRef.loadColormap(
parseContext(colormap, [this.config['@context']])
)
let colorIdx = tsC[0].getData()
if (tsC[0].attributes.DataType === 'NIFTI_TYPE_INT16') {
colorIdx = (window as any).ThreeSurfer.GiftiBase.castF32UInt16(colorIdx)
}
this.loadedMeshes.push({
threeSurfer: tsM,
colormap,
mesh,
hemisphere,
vIdxArr: colorIdx
})
this.tsRef.applyColorMap(tsM, colorIdx,
{
custom: applyCM
}
)
if (this.tsRef) {
this.ngOnDestroy()
this.ngAfterViewInit()
}
/**
* wait until threesurfer is defined in window
*/
await retry(async () => {
if (typeof (window as any).ThreeSurfer === 'undefined') throw new Error('ThreeSurfer not yet defined')
}, {
timeout: 160,
retries: 10,
})
this.config = this.selectedTemplate['three-surfer']
// somehow curv ... cannot be parsed properly by gifti parser... something about points missing
this.modes = this.config.modes.filter(m => !/curv/.test(m.name))
if (!this.tsRef) {
this.tsRef = new (window as any).ThreeSurfer(this.domEl, {highlightHovered: true})
this.onDestroyCb.push(
() => {
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)
for (const region of flattenedRegions) {
if (region.labelIndex) {
const hemisphere = /left/.test(region.name)
? 'left'
: /right/.test(region.name)
? 'right'
: null
if (!hemisphere) throw new Error(`region ${region.name} does not have hemisphere defined`)
if (!this.regionMap.has(hemisphere)) {
this.regionMap.set(hemisphere, new Map())
}
const rMap = this.regionMap.get(hemisphere)
rMap.set(region.labelIndex, region)
// load mode0 by default
this.loadMode(this.config.modes[0])
this.viewerEvent.emit({
type: EnumViewerEvt.VIEWERLOADED,
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 = detail.mesh.geometry
const evVertIdx = 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 { hemisphere: key, vIdxArr } = found
if (!key || !evVertIdx) {
return this.handleMouseoverEvent(custEv)
}
const labelIdxSet = new Set<number>()
for (const vIdx of evVertIdx) {
labelIdxSet.add(
vIdxArr[vIdx]
)
}
if (labelIdxSet.size === 0) {
return this.handleMouseoverEvent(custEv)
}
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 this.handleMouseoverEvent(custEv)
}
private cameraEv$ = new Subject<{ position: { x: number, y: number, z: number }, zoom: number }>()
private handleCustomCameraEvent(detail: any){
this.cameraEv$.next(detail)
}
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.domEl.removeEventListener((window as any).ThreeSurfer.CUSTOM_EVENTNAME_UPDATED, customEvHandler)
private handleMouseoverEvent(ev: THandlingCustomEv){
const { regions: mouseover, evMesh } = ev
this.viewerEvent.emit({
type: EnumViewerEvt.VIEWER_CTX,
data: {
viewerType: 'threeSurfer',
payload: {
fsversion: this.selectedMode,
faceIndex: evMesh?.faceIndex,
vertexIndices: evMesh?.verticesIndicies,
position: [],
_mouseoverRegion: mouseover.filter(el => !el.error)
}
}
})
this.mouseoverText = mouseover.length === 0 ?
null :
mouseover.map(
el => el.name || el.error
).join(' / ')
public handleCheckBox(key: { name: string, checked: boolean }, flag: boolean){
const foundMesh = this.loadedMeshes.find(m => m.hemisphere === key.name)
if (!foundMesh) {
throw new Error(`Cannot find mesh with name: ${key.name}`)
}
const meshObj = this.tsRef.customColormap.get(foundMesh.threeSurfer)
if (!meshObj) {
throw new Error(`mesh obj not found!`)
}
meshObj.mesh.visible = flag
}