Skip to content
Snippets Groups Projects
threeSurfer.component.ts 27.8 KiB
Newer Older
Xiao Gui's avatar
Xiao Gui committed
import { Component, Output, EventEmitter, ElementRef, OnDestroy, AfterViewInit, Optional, ChangeDetectionStrategy } from "@angular/core";
import { EnumViewerEvt, IViewer, TViewerEvent } from "src/viewerModule/viewer.interface";
import { BehaviorSubject, combineLatest, concat, forkJoin, from, merge, NEVER, Observable, of, Subject, throwError } from "rxjs";
import { catchError, debounceTime, distinctUntilChanged, filter, map, scan, shareReplay, startWith, switchMap, tap, withLatestFrom } from "rxjs/operators";
import { ComponentStore, LockError } from "src/viewerModule/componentStore";
import { select, Store } from "@ngrx/store";
Xiao Gui's avatar
Xiao Gui committed
import { MatSnackBar } from "src/sharedModules/angularMaterial.exports"
import { getUuid, switchMapWaitFor } from "src/util/fn";
Xiao Gui's avatar
Xiao Gui committed
import { AUTO_ROTATE, TInteralStatePayload, ViewerInternalStateSvc } from "src/viewerModule/viewerInternalState.service";
Xiao Gui's avatar
Xiao Gui committed
import { atlasAppearance, atlasSelection } from "src/state";
import { ThreeSurferCustomLabelLayer, ThreeSurferCustomLayer, ColorMapCustomLayer } from "src/state/atlasAppearance/const";
import { SxplrRegion } from "src/atlasComponents/sapi/sxplrTypes"
Xiao Gui's avatar
Xiao Gui committed
import { arrayEqual } from "src/util/array";
import { ThreeSurferEffects } from "../store/effects";
import { selectors, actions  } from "../store"
Xiao Gui's avatar
Xiao Gui committed
import { SAPI } from "src/atlasComponents/sapi";
Xiao Gui's avatar
Xiao Gui committed

const viewerType = 'ThreeSurfer'
type TInternalState = {
  camera: {
    x: number
    y: number
    z: number
  }
  mode: string
  hemisphere: 'left' | 'right' | 'both'
}
const pZoomFactor = 7e3
type THandlingCustomEv = {
Xiao Gui's avatar
Xiao Gui committed
  regions: SxplrRegion[]
Xiao Gui's avatar
Xiao Gui committed
  error?: string
  evMesh?: {
    faceIndex: number
    verticesIndicies: number[]
  }
}

type TLatVtxIdxRecord = LateralityRecord<{
  indexLayer: ThreeSurferCustomLabelLayer
  vertexIndices: number[]
}>

type TLatMeshRecord = LateralityRecord<{
  meshLayer: ThreeSurferCustomLayer
  mesh: TThreeGeometry
}>

type MeshVisOp = 'toggle' | 'noop'

type TApplyColorArg = LateralityRecord<{
  labelIndices: number[]
  idxReg: Record<number, SxplrRegion>
  isBaseCm: boolean
  showDelin: boolean
  selectedRegions: SxplrRegion[]
  mesh: TThreeGeometry
  vertexIndices: number[]
  map?: Map<number, number[]>
}>

type THandleCustomMouseEv = {
  latMeshRecord: TLatMeshRecord
  latLblIdxRecord: TLatVtxIdxRecord
  evDetail: any
  latLblIdxReg: TLatIdxReg
  meshVisibility: {
    label: string
Xiao Gui's avatar
Xiao Gui committed
    visible: boolean
    mesh: TThreeGeometry
  }[]
}

type TLatIdxReg = LateralityRecord<Record<number, SxplrRegion>>

type TLatCm = LateralityRecord<{
  labelIndices: number[]
  map: Map<number, number[]>
}>

type TCameraOrientation = {
Xiao Gui's avatar
Xiao Gui committed
  perspectiveOrientation: number[]
  perspectiveZoom: number
}

Xiao Gui's avatar
Xiao Gui committed
type TThreeGeometry = {
  visible: boolean
}
Xiao Gui's avatar
Xiao Gui committed
type GiiInstance = unknown
Xiao Gui's avatar
Xiao Gui committed
type TThreeSurfer = {
  loadMesh: (url: string) => Promise<TThreeGeometry>
  unloadMesh: (geom: TThreeGeometry) => void
  redraw: (geom: TThreeGeometry) => void
  applyColorMap: (geom: TThreeGeometry, idxMap?: number[], custom?: { usePreset?: any, custom?: Map<number, number[]> }) => void
  loadColormap: (url: string) => Promise<GiiInstance>
  setupAnimation: () => void
  dispose: () => void
  loadVertexData: (url: string) => Promise<{
    vertex: number[]
    labels: {
      index: number
      name: string
      color: number[]
      vertices: number[]
    }[]
    readonly vertexLabels: Uint16Array
    readonly colormap: Map<number, number[]>
  }>
Xiao Gui's avatar
Xiao Gui committed
  control: any
  camera: any
  customColormap: WeakMap<TThreeGeometry, any>
  gridHelper: {
    visible: boolean
  }
Xiao Gui's avatar
Xiao Gui committed
type LateralityRecord<T> = Record<string, T>

const threshold = 1e-3

function cameraNavsAreSimilar(c1: TCameraOrientation, c2: TCameraOrientation){

  // if same reference, return true
  if (c1 === c2) return true

  // if both falsy, 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'
Xiao Gui's avatar
Xiao Gui committed
  providers: [ ComponentStore ],
  changeDetection: ChangeDetectionStrategy.OnPush
Xiao Gui's avatar
Xiao Gui committed
export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit, OnDestroy {
  #cameraEv$ = new Subject<{ position: { x: number, y: number, z: number }, zoom: number }>()
  #mouseEv$ = new Subject()
  viewerEvent = new EventEmitter<TViewerEvent<'threeSurfer'>>()

  private domEl: HTMLElement
  #storeNavigation = this.store$.pipe(
    select(atlasSelection.selectors.navigation)
  )

  #componentStoreNavigation = this.navStateStoreRelay.select(s => s)
  
  #internalNavigation = this.#cameraEv$.pipe(
    filter(v => !!v && !!(this.tsRef?.camera?.matrix)),
    map(() => {
      const { tsRef } = this
      return {
        _po: null,
        _pz: null,
        _calculate(){
          if (!tsRef) return
          const THREE = (window as any).ThreeSurfer.THREE
          
          const q = new THREE.Quaternion()
          const t = new THREE.Vector3()
          const s = new THREE.Vector3()

          /**
           * ThreeJS interpretes the scene differently to neuroglancer in subtle ways. 
           * At [0, 0, 0, 1] decomposed camera quaternion, for example,
           * - ThreeJS: view from superior -> inferior, anterior as top, right hemisphere as right
           * - NG: view from from inferior -> superior, posterior as top, left hemisphere as right
           * 
           * multiplying the exchange factor [-1, 0, 0, 0] converts ThreeJS convention to NG convention
           */
          const cameraM = tsRef.camera.matrix
          cameraM.decompose(t, q, s)
          const exchangeFactor = new THREE.Quaternion(-1, 0, 0, 0)
          this._po = q.multiply(exchangeFactor).toArray()
          this._pz = t.length() * pZoomFactor // use zoom as used in main store
        },
        get perspectiveOrientation(){
          if (!this._po) {
            this._calculate()
          }
          return this._po
        },
        get perspectiveZoom() {
          if (!this._pz) {
            this._calculate()
          }
          return this._pz
        }
      } as TCameraOrientation
    })
  )
Xiao Gui's avatar
Xiao Gui committed
  private internalStateNext: (arg: TInteralStatePayload<TInternalState>) => void
Xiao Gui's avatar
Xiao Gui committed
  private mouseoverRegions: SxplrRegion[] = []
Xiao Gui's avatar
Xiao Gui committed

  private customLayers$ = this.store$.pipe(
    select(atlasAppearance.selectors.customLayers),
    distinctUntilChanged(arrayEqual((o, n) => o.id === n.id)),
    shareReplay(1)
  )
  #meshLayers$: Observable<ThreeSurferCustomLayer[]> = this.customLayers$.pipe(
Xiao Gui's avatar
Xiao Gui committed
    map(layers => layers.filter(l => l.clType === "baselayer/threesurfer") as ThreeSurferCustomLayer[]),
    distinctUntilChanged(arrayEqual((o, n) => o.id === n.id)),
  )

  #lateralMeshRecord$ = new Subject<TLatMeshRecord>()
  lateralMeshRecord$ = concat(
    of({} as TLatMeshRecord),
    this.#lateralMeshRecord$.asObservable()
  )

  #meshVisOp$ = new Subject<{ op: MeshVisOp, label?: string }>()
  meshVisible$ = this.lateralMeshRecord$.pipe(
    map(v => {
      const returnVal: {
        label: string
Xiao Gui's avatar
Xiao Gui committed
        visible: boolean
        mesh: TThreeGeometry
      }[] = []
      for (const lat in v) {
        returnVal.push({
          visible: true,
          mesh: v[lat].mesh,
          label: lat
        })
      }
      return returnVal
    }),
    switchMap(arr => concat(
      of({ op: 'noop', label: null }),
      this.#meshVisOp$
    ).pipe(
      map(({ op, label }) => arr.map(v => {
        if (label !== v.label) {
          return v
        }
        if (op === "toggle") {
          v.visible = !v.visible
        }
        return v
      }))
    ))
  )

Xiao Gui's avatar
Xiao Gui committed
  private vertexIndexLayers$: Observable<ThreeSurferCustomLabelLayer[]> = this.customLayers$.pipe(
    map(layers => layers.filter(l => 
      l.clType === "baselayer/threesurfer-label/gii-label" || l.clType === "baselayer/threesurfer-label/annot"
    ) as ThreeSurferCustomLabelLayer[]),
Xiao Gui's avatar
Xiao Gui committed
    distinctUntilChanged(arrayEqual((o, n) => o.id === n.id)),
  )

  #latVtxIdxRecord$: Observable<TLatVtxIdxRecord> = this.vertexIndexLayers$.pipe(
    switchMap(
      switchMapWaitFor({
        condition: () => !!this.tsRef,
        leading: true
      })
    ),
    switchMap(layers => 
      forkJoin(
        layers.map(layer => {
          if (layer.clType === "baselayer/threesurfer-label/gii-label") {
            return from(
              this.tsRef.loadColormap(layer.source)
            ).pipe(
              map(giiInstance => {
                let vertexIndices: number[] = giiInstance[0].getData()
                if (giiInstance[0].attributes.DataType === 'NIFTI_TYPE_INT16') {
                  vertexIndices = (window as any).ThreeSurfer.GiftiBase.castF32UInt16(vertexIndices)
                }
                return {
                  indexLayer: layer,
                  vertexIndices
                }
              })
            )
          }
          if (layer.clType === "baselayer/threesurfer-label/annot") {
            return from(
              this.tsRef.loadVertexData(layer.source)
            ).pipe(
              map(v => {
                return {
                  indexLayer: layer,
                  vertexIndices: v.vertexLabels
                }
              })
            )
          }
          return throwError(() => new Error(`layer is neither annot nor gii-label`))
        })
      )
    ),
    map(layers => {
      const returnObj = {}
      for (const { indexLayer, vertexIndices } of layers) {
        returnObj[indexLayer.laterality] = { indexLayer, vertexIndices }
      }
      return returnObj
    })
  )

Xiao Gui's avatar
Xiao Gui committed
  /**
   * maps laterality to label index to sapi region
   */
  
  #latLblIdxToRegionRecord$: Observable<TLatIdxReg> = combineLatest([
Xiao Gui's avatar
Xiao Gui committed
    this.store$.pipe(
Xiao Gui's avatar
Xiao Gui committed
      atlasSelection.fromRootStore.distinctATP()
Xiao Gui's avatar
Xiao Gui committed
    ),
    this.store$.pipe(
      select(atlasSelection.selectors.selectedParcAllRegions),
    )
  ]).pipe(
Xiao Gui's avatar
Xiao Gui committed
    switchMap(([ { parcellation, template },  regions]) => {
Xiao Gui's avatar
Xiao Gui committed
      return merge(
        ...regions.map(region => 
          from(this.sapi.getRegionLabelIndices(template, parcellation, region)).pipe(
Xiao Gui's avatar
Xiao Gui committed
            map(label => ({ region, label })),
            catchError(() => NEVER)
Xiao Gui's avatar
Xiao Gui committed
          )
        )
      ).pipe(
        scan((acc, curr) => {
          const { label, region } = curr
          
Xiao Gui's avatar
Xiao Gui committed
          let key : 'left' | 'right'
          if (
            /left/i.test(region.name) || /^lh/i.test(region.name)
          ) key = 'left'
          if (
            /right/i.test(region.name) || /^rh/i.test(region.name)
          ) key = 'right'

Xiao Gui's avatar
Xiao Gui committed
          if (!key) {
            /**
             * TODO
             * there are ... more regions than expected, which has label index without laterality
             */
Xiao Gui's avatar
Xiao Gui committed
            return { ...acc }
Xiao Gui's avatar
Xiao Gui committed
          return {
            ...acc,
            [key]: {
              ...acc[key],
              [label]: region
            }
          }
        }, {'left': {}, 'right': {}})
      )
Xiao Gui's avatar
Xiao Gui committed
    })
  )

  /**
   * colormap in use (both base & custom)
   */

  #colormaps$: Observable<ColorMapCustomLayer[]> = this.customLayers$.pipe(
Xiao Gui's avatar
Xiao Gui committed
    map(layers => layers.filter(l => l.clType === "baselayer/colormap" || l.clType === "customlayer/colormap") as ColorMapCustomLayer[]),
    distinctUntilChanged(arrayEqual((o, n) => o.id === n.id))
  )

  #latLblIdxToCm$ = combineLatest([
    this.#latLblIdxToRegionRecord$,
    this.#colormaps$
  ]).pipe(
    map(([ latIdxReg, cms ]) => {
      const cm = cms[0]
      const returnValue: TLatCm = {}
      if (!cm) {
        return returnValue
      }
      for (const lat in latIdxReg) {
        returnValue[lat] = {
          labelIndices: [],
          map: new Map()
        }
        for (const lblIdx in latIdxReg[lat]) {
          returnValue[lat].labelIndices.push(Number(lblIdx))
          const reg = latIdxReg[lat][lblIdx]
          returnValue[lat].map.set(
            Number(lblIdx), (cm.colormap.get(reg) || [255, 255, 255]).map(v => v/255)
          )
        }
      }
      return returnValue
    })
   * when do we need to call apply color?
   * - when mesh loads
   * - when vertex index layer changes
   * - selected region changes
   * - custom color map added (by plugin, etc)
   * - show delineation updates
Xiao Gui's avatar
Xiao Gui committed
  public threeSurferSurfaceVariants$ = this.effect.onATPDebounceThreeSurferLayers$.pipe(
    map(({ surfaces }) => surfaces.reduce((acc, val) => acc.includes(val.variant) ? acc : [...acc, val.variant] ,[] as string[]))
Xiao Gui's avatar
Xiao Gui committed
  )
  public selectedSurfaceLayerId$ = this.store$.pipe(
Xiao Gui's avatar
Xiao Gui committed
    select(selectors.getSelectedSurfaceVariant)
  constructor(
Xiao Gui's avatar
Xiao Gui committed
    private effect: ThreeSurferEffects,
    el: ElementRef,
Xiao Gui's avatar
Xiao Gui committed
    private store$: Store,
    private navStateStoreRelay: ComponentStore<TCameraOrientation>,
Xiao Gui's avatar
Xiao Gui committed
    private sapi: SAPI,
    private snackbar: MatSnackBar,
Xiao Gui's avatar
Xiao Gui committed
    @Optional() intViewerStateSvc: ViewerInternalStateSvc,
Xiao Gui's avatar
Xiao Gui committed
    if (intViewerStateSvc) {
      const {
        done,
        next,
      } = intViewerStateSvc.registerEmitter({
        "@type": 'TViewerInternalStateEmitter',
        viewerType,
        applyState: arg => {
Xiao Gui's avatar
Xiao Gui committed
          if (arg.viewerType === AUTO_ROTATE) {
            const autoPlayFlag = (arg.payload as any).play
            const reverseFlag = (arg.payload as any).reverse
            const autoplaySpeed = (arg.payload as any).speed
            this.toTsRef(tsRef => {
              tsRef.control.autoRotate = autoPlayFlag
              tsRef.control.autoRotateSpeed = autoplaySpeed * (reverseFlag ? -1 : 1)
            })
            return
          }
Xiao Gui's avatar
Xiao Gui committed
          if (arg.viewerType !== viewerType) return
          this.toTsRef(tsRef => {
            tsRef.camera.position.copy((arg.payload as any).camera)
          })
        }
      })
      this.internalStateNext = next
      this.onDestroyCb.push(() => done())
    }
    this.domEl = el.nativeElement

    /**
     * subscribe to camera custom event
     */
    const setReconcilState = merge(
      this.#internalNavigation.pipe(
        filter(v => !!v),
        tap(() => {
          try {
            this.releaseRelayLock = this.navStateStoreRelay.getLock()
          } catch (e) {
            if (!(e instanceof LockError)) {
              throw e
            }
          }
        }),
        debounceTime(160),
        tap(() => {
          if (this.releaseRelayLock) {
            this.releaseRelayLock()
            this.releaseRelayLock = null
          } else {
            console.warn(`this.releaseRelayLock not aquired, component may not function properly`)
          }  
        })
      ),
      this.#storeNavigation,
    ).pipe(
      filter(v => !!v)
    ).subscribe(nav => {
      try {
        this.navStateStoreRelay.setState({
          perspectiveOrientation: nav.perspectiveOrientation,
          perspectiveZoom: nav.perspectiveZoom
      } catch (e) {
        if (!(e instanceof LockError)) {
          throw e
        }
      }
    })

    this.onDestroyCb.push(
      () => setReconcilState.unsubscribe()
    )

    /**
     * subscribe to navstore relay store and negotiate setting global state
     */
    const reconciliatorSub = combineLatest([
      this.#storeNavigation.pipe(
        startWith(null as TCameraOrientation)
      ),
      this.#componentStoreNavigation.pipe(
        startWith(null as TCameraOrientation),
      ),
      this.#internalNavigation.pipe(
        startWith(null as TCameraOrientation),
      )
    ]).pipe(
      debounceTime(160),
      filter(() => !this.navStateStoreRelay.isLocked)
    ).subscribe(([ storeNav, reconcilNav, internalNav ]) => {
      if (!cameraNavsAreSimilar(storeNav, reconcilNav) && reconcilNav) {
        this.store$.dispatch(atlasSelection.actions.setNavigation({
          navigation: {
            position: [0, 0, 0],
            orientation: [0, 0, 0, 1],
            zoom: 1e6,
            perspectiveOrientation: reconcilNav.perspectiveOrientation,
            perspectiveZoom: reconcilNav.perspectiveZoom
      if (!cameraNavsAreSimilar(reconcilNav, internalNav) && reconcilNav) {
        const THREE = (window as any).ThreeSurfer.THREE
        
        const cameraQuat = new THREE.Quaternion(...reconcilNav.perspectiveOrientation)
        const cameraPos = new THREE.Vector3(0, 0, reconcilNav.perspectiveZoom / pZoomFactor)
        
        /**
         * ThreeJS interpretes the scene differently to neuroglancer in subtle ways. 
         * At [0, 0, 0, 1] decomposed camera quaternion, for example,
         * - ThreeJS: view from superior -> inferior, anterior as top, right hemisphere as right
         * - NG: view from from inferior -> superior, posterior as top, left hemisphere as right
         * 
         * multiplying the exchange factor [-1, 0, 0, 0] converts ThreeJS convention to NG convention
         */
        const exchangeFactor = new THREE.Quaternion(-1, 0, 0, 0)
        cameraQuat.multiply(exchangeFactor)

        cameraPos.applyQuaternion(cameraQuat)
        this.toTsRef(tsRef => {
          tsRef.camera.position.copy(cameraPos)
        })
      }
    })

    this.onDestroyCb.push(
      () => reconciliatorSub.unsubscribe()
Xiao Gui's avatar
Xiao Gui committed
  private tsRef: TThreeSurfer

  private releaseRelayLock: () => void = null
  private tsRefInitCb: ((tsRef: any) => void)[] = []
  private toTsRef(callback: (tsRef: any) => void) {
    if (this.tsRef) {
      callback(this.tsRef)
      return
    }
    this.tsRefInitCb.push(callback)
  }

  async #loadMeshes(layers: ThreeSurferCustomLayer[], currMeshRecord: TLatMeshRecord) {
Xiao Gui's avatar
Xiao Gui committed
    if (!this.tsRef) throw new Error(`loadMeshes error: this.tsRef is not defined!!`)
    const copiedCurrMeshRecord: TLatMeshRecord = {...currMeshRecord}
Xiao Gui's avatar
Xiao Gui committed
    /**
     * remove the layers... 
     */
    for (const layer of layers) {
      if (!!copiedCurrMeshRecord[layer.laterality]) {
        this.tsRef.unloadMesh(copiedCurrMeshRecord[layer.laterality].mesh)
Xiao Gui's avatar
Xiao Gui committed
    for (const layer of layers) {
      const threeMesh = await this.tsRef.loadMesh(layer.source)
      copiedCurrMeshRecord[layer.laterality] = {
Xiao Gui's avatar
Xiao Gui committed
        meshLayer: layer,
        mesh: threeMesh
    this.#lateralMeshRecord$.next(copiedCurrMeshRecord)
  #applyColor$ = combineLatest([
    combineLatest([
      this.lateralMeshRecord$,
      this.store$.pipe(
        select(atlasSelection.selectors.selectedRegions),
        distinctUntilChanged(arrayEqual((o, n) => o.name === n.name))
      ),
      this.#colormaps$.pipe(
        map(cms => cms[0]),
        distinctUntilChanged((o, n) => o?.id === n?.id)
      ),
      this.store$.pipe(
        select(atlasAppearance.selectors.showDelineation),
        distinctUntilChanged()
      ),
      this.#latLblIdxToCm$,
      this.#latLblIdxToRegionRecord$,
    ]),
    this.#latVtxIdxRecord$
  ]).pipe(
    debounceTime(16),
    map(([[ latMeshDict, selReg, cm, showDelFlag, latLblIdxToCm, latLblIdxToRegionRecord ], latVtxIdx]) => {
      const arg: TApplyColorArg = {}
      for (const lat in latMeshDict) {
        arg[lat] = {
          mesh: latMeshDict[lat].mesh,
          selectedRegions: selReg,
          showDelin: showDelFlag,
          isBaseCm: cm.clType === "baselayer/colormap",
          labelIndices: latLblIdxToCm[lat].labelIndices,
          idxReg: latLblIdxToRegionRecord[lat],
          map: latLblIdxToCm[lat].map,
          vertexIndices: latVtxIdx[lat].vertexIndices
        }
      return arg
    })
  )
  private applyColor(applyArg: TApplyColorArg) {
    /**
     * on apply color map, reset mesh visibility
     * this issue is more difficult to solve than first anticiplated.
     * test scenarios:
     * 
     * 1/ hide hemisphere, select region
     * 2/ hide hemisphere, select region, unhide hemisphere
     * 3/ select region, hide hemisphere, deselect region
     */
    if (!this.tsRef) return
    
    for (const laterality in applyArg) {
      const { labelIndices, map, mesh, showDelin, selectedRegions, isBaseCm, idxReg, vertexIndices } = applyArg[laterality]
      if (!map) {
        this.tsRef.applyColorMap(mesh, vertexIndices)
Xiao Gui's avatar
Xiao Gui committed
        continue

      const actualApplyMap = new Map<number, number[]>()

      if (!showDelin) {
        for (const lblIdx of labelIndices){
          actualApplyMap.set(lblIdx, [1, 1, 1])
        }
        this.tsRef.applyColorMap(mesh, vertexIndices, {
          custom: actualApplyMap
        })
      }

      const highlightIdx = new Set<number>()
      if (isBaseCm && selectedRegions.length > 0) {
        for (const [idx, region] of Object.entries(idxReg)) {
          if (selectedRegions.findIndex(r => r.name === region.name) >= 0) {
            highlightIdx.add(Number(idx))
          }
        }
      }
      if (isBaseCm && selectedRegions.length > 0) {
        for (const lblIdx of labelIndices) {
          actualApplyMap.set(
            Number(lblIdx),
            highlightIdx.has(lblIdx)
            ? map.get(lblIdx) || [1, 0.8, 0.8]
            : [1, 1, 1]
          )
        }
      } else {
        for (const lblIdx of labelIndices) {
          actualApplyMap.set(
            Number(lblIdx),
            map.get(lblIdx) || [1, 0.8, 0.8]
          )
      this.tsRef.applyColorMap(mesh, vertexIndices, {
        custom: actualApplyMap
  #handleCustomMouseEv$ = this.#mouseEv$.pipe(
    withLatestFrom(
      this.lateralMeshRecord$,
      this.#latLblIdxToRegionRecord$,
      this.meshVisible$,
      this.#latVtxIdxRecord$,
    )
  ).pipe(
    map(([ evDetail, latMeshRecord, latLblIdxReg, meshVis, latVtxIdx ]) => {
      const returnVal: THandleCustomMouseEv = {
        evDetail,
        meshVisibility: meshVis,
        latLblIdxReg: latLblIdxReg,
        latMeshRecord: latMeshRecord,
        latLblIdxRecord: latVtxIdx
      }
      return returnVal
    })
  )
  #handleCustomMouseEv(arg: THandleCustomMouseEv){
    const { evDetail: detail, latMeshRecord, latLblIdxRecord, latLblIdxReg, meshVisibility } = arg
    const evMesh = detail.mesh && {
      faceIndex: detail.mesh.faceIndex,
      verticesIndicies: detail.mesh.verticesIndicies,
      vertexIndex: detail.mesh.vertexIndex,
    }
    const custEv: THandlingCustomEv = {
      regions: [],
      evMesh
    }
    
    if (!detail.mesh) {
      return this.handleMouseoverEvent(custEv)
    }
Xiao Gui's avatar
Xiao Gui committed
    const {
      geometry: evGeometry,
      vertexIndex
    } = detail.mesh as { geometry: TThreeGeometry, verticesIndicies: number[], vertexIndex: number }
    for (const laterality in latMeshRecord) {
      const meshRecord = latMeshRecord[laterality]
Xiao Gui's avatar
Xiao Gui committed
      if (meshRecord.mesh !== evGeometry) {
        continue
      }
      /**
       * if either labelindex record or colormap record is undefined for this laterality, emit empty event
       */
      if (!latLblIdxRecord[laterality] || !latLblIdxReg[laterality]) {
Xiao Gui's avatar
Xiao Gui committed
        return this.handleMouseoverEvent(custEv)
      }
      const labelIndexRecord = latLblIdxRecord[laterality]
      const regionRecord = latLblIdxReg[laterality]
Xiao Gui's avatar
Xiao Gui committed
      /**
       * check if the mesh is toggled off
       * if so, do not proceed
       */
      const mVis = meshVisibility.filter(({ mesh }) => mesh === meshRecord.mesh)
      if (!mVis.every(m => m.visible)) {
Xiao Gui's avatar
Xiao Gui committed
        return
      }
Xiao Gui's avatar
Xiao Gui committed
      /**
       * translate vertex indices to label indicies via set, to remove duplicates
       */
      const labelIndexSet = new Set<number>()
      if (labelIndexRecord.vertexIndices[vertexIndex]) {
        labelIndexSet.add(labelIndexRecord.vertexIndices[vertexIndex])
      /**
       * old implementation (perhaps less CPU intensive)
       * gets all vertices and label them
       */
      // for (const idx of evVerticesIndicies){
      //   const labelOfInterest = labelIndexRecord.vertexIndices[idx]
      //   if (!labelOfInterest) {
      //     continue
      //   }
      //   labelIndexSet.add(labelOfInterest)
      // }

Xiao Gui's avatar
Xiao Gui committed
      /**
       * decode label index to region
       */
      if (labelIndexSet.size === 0) {
        return this.handleMouseoverEvent(custEv)
      }
      for (const labelIndex of Array.from(labelIndexSet)) {
        if (!regionRecord[labelIndex]) {
          custEv.error = `${custEv.error || ''} Cannot decode label index ${labelIndex}`
          continue
Xiao Gui's avatar
Xiao Gui committed
        const region = regionRecord[labelIndex]
        custEv.regions.push(region)
      }
Xiao Gui's avatar
Xiao Gui committed
      /**
       * return handle event
       */
      return this.handleMouseoverEvent(custEv)
    }
Xiao Gui's avatar
Xiao Gui committed
  ngAfterViewInit(): void{
    const customEvHandler = (ev: CustomEvent) => {
      const { type, data } = ev.detail
      if (type === 'mouseover') {
        this.#mouseEv$.next(data)
        return
      }
      if (type === 'camera') {
        if (this.internalStateNext) {
          this.internalStateNext({
            "@id": getUuid(),
            "@type": 'TViewerInternalStateEmitterEvent',
            viewerType,
            payload: {
              mode: '',
              camera: data.position,
              hemisphere: 'both'
            }
          })
        }
        this.#cameraEv$.next(data)
        return
      }
    }
    this.domEl.addEventListener((window as any).ThreeSurfer.CUSTOM_EVENTNAME_UPDATED, customEvHandler)
    this.onDestroyCb.push(
      () => this.domEl.removeEventListener((window as any).ThreeSurfer.CUSTOM_EVENTNAME_UPDATED, customEvHandler)
Xiao Gui's avatar
Xiao Gui committed
    this.tsRef = new (window as any).ThreeSurfer(this.domEl, {highlightHovered: true})
    window['tsViewer'] = this.tsRef
Xiao Gui's avatar
Xiao Gui committed

    this.onDestroyCb.push(
      () => {
        this.tsRef.dispose()
        this.tsRef = null
        window['tsViewer'] = null
Xiao Gui's avatar
Xiao Gui committed
      }
    )
    this.tsRef.control.enablePan = false
    while (this.tsRefInitCb.length > 0) {
      const tsCb = this.tsRefInitCb.pop()
      tsCb(this.tsRef)
    }
    const meshSub = this.#meshLayers$.pipe(
      switchMap(
        switchMapWaitFor({
          condition: () => !!this.tsRef,
          leading: true
        })
      ),
Xiao Gui's avatar
Xiao Gui committed
      debounceTime(16),
      withLatestFrom(
        this.lateralMeshRecord$
      )
    ).subscribe(([layers, currMeshRecord]) => {
      this.#loadMeshes(layers, currMeshRecord)
    
    const applyColorSub = this.#applyColor$.subscribe(arg => {
      this.applyColor(arg)

    const mouseSub = this.#handleCustomMouseEv$.subscribe(arg => {
      this.#handleCustomMouseEv(arg)

    const visibilitySub = this.meshVisible$.subscribe(arr => {
      for (const { visible, mesh } of arr) {
        mesh.visible = visible
        
        const meshObj = this.tsRef.customColormap.get(mesh)
        if (!meshObj) {
          throw new Error(`mesh obj not found!`)
        }
        meshObj.mesh.visible = visible
      }
Xiao Gui's avatar
Xiao Gui committed
    })

    this.onDestroyCb.push(() => {
      meshSub.unsubscribe()
      applyColorSub.unsubscribe()
      mouseSub.unsubscribe()
      visibilitySub.unsubscribe()
Xiao Gui's avatar
Xiao Gui committed
    })

    this.viewerEvent.emit({
      type: EnumViewerEvt.VIEWERLOADED,
      data: true
    })
Xiao Gui's avatar
Xiao Gui committed
  public mouseoverText: string
  private handleMouseoverEvent(ev: THandlingCustomEv){
Xiao Gui's avatar
Xiao Gui committed
    const { regions: mouseover, evMesh, error } = ev
    this.mouseoverRegions = mouseover
    this.viewerEvent.emit({
      type: EnumViewerEvt.VIEWER_CTX,
      data: {
        viewerType: 'threeSurfer',
        payload: {
Xiao Gui's avatar
Xiao Gui committed
          fsversion: '',
          faceIndex: evMesh?.faceIndex,
          vertexIndices: evMesh?.verticesIndicies,
          position: [],
Xiao Gui's avatar
Xiao Gui committed
          regions: mouseover,
          error
Xiao Gui's avatar
Xiao Gui committed
    this.mouseoverText = ''
    if (error) {
      this.mouseoverText += `::error: ${error}`
    }
  public toggleMeshVis(label: string) {
    this.#meshVisOp$.next({
      label,
      op: 'toggle'
    })
Xiao Gui's avatar
Xiao Gui committed
  switchSurfaceLayer(variant: string): void{
Xiao Gui's avatar
Xiao Gui committed
    this.store$.dispatch(
Xiao Gui's avatar
Xiao Gui committed
      actions.selectSurfaceVariant({
        variant
  gridVisible$ = new BehaviorSubject<boolean>(true)
  setGridVisibility(newFlag: boolean){
    this.tsRef.gridHelper.visible = newFlag
    this.gridVisible$.next(newFlag)
  }

  private onDestroyCb: (() => void) [] = []

Xiao Gui's avatar
Xiao Gui committed
  ngOnDestroy(): void {
    while (this.onDestroyCb.length > 0) this.onDestroyCb.pop()()
  }
}