From 43dc2aa3714f55f806f83a489acf691708a45e82 Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Tue, 18 Apr 2023 16:26:48 +0200
Subject: [PATCH] bugfix: fsaverage on chnage variant, showing multiple variant
 meshes bugfix: fsaverage err URL encoding selected regions maint: some
 simplification

---
 docs/releases/v2.10.1.md                      |   6 +
 src/atlasComponents/sapi/constants.ts         |   1 +
 src/atlasComponents/sapi/sapi.service.ts      |   2 +-
 .../routeStateTransform.service.ts            |   1 -
 src/state/atlasSelection/effects.spec.ts      | 135 +++--
 src/state/atlasSelection/effects.ts           |  42 +-
 .../nehuba/config.service/util.ts             |   2 +-
 .../layerCtrl.service.spec.ts                 |  12 +
 .../layerCtrl.service/layerCtrl.service.ts    |   2 -
 .../nehuba/mesh.service/mesh.service.spec.ts  |  26 +-
 .../threeSurferGlue/threeSurfer.component.ts  | 466 ++++++++++++------
 .../threeSurferGlue/threeSurfer.template.html |   8 +-
 12 files changed, 493 insertions(+), 210 deletions(-)

diff --git a/docs/releases/v2.10.1.md b/docs/releases/v2.10.1.md
index f2dba5a6f..b922189ac 100644
--- a/docs/releases/v2.10.1.md
+++ b/docs/releases/v2.10.1.md
@@ -1,5 +1,11 @@
 # v2.10.1
 
+## Bugfix
+
+- fsaverage on change variant, showing multiple meshes
+- fsaverage erroneous URL encoding of selected region
+
 ## Behind the scenes
 
 - Housekeeping CI/CD
+- Simplify some behind the scenes code
diff --git a/src/atlasComponents/sapi/constants.ts b/src/atlasComponents/sapi/constants.ts
index c558e1726..da2294c69 100644
--- a/src/atlasComponents/sapi/constants.ts
+++ b/src/atlasComponents/sapi/constants.ts
@@ -9,6 +9,7 @@ export const IDS = {
     COLIN27: "minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992",
     WAXHOLM: "minds/core/referencespace/v1.0.0/d5717c4a-0fa1-46e6-918c-b8003069ade8",
     MEBRAINS: "minds/core/referencespace/v1.0.0/MEBRAINS",
+    FSAVERAGE: 'minds/core/referencespace/v1.0.0/tmp-fsaverage'
   },
   PARCELLATION: {
     JBA29: "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290",
diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts
index 8b9206271..65a12c19b 100644
--- a/src/atlasComponents/sapi/sapi.service.ts
+++ b/src/atlasComponents/sapi/sapi.service.ts
@@ -22,7 +22,7 @@ export const useViewer = {
 } as const
 
 export const SIIBRA_API_VERSION_HEADER_KEY='x-siibra-api-version'
-export const EXPECTED_SIIBRA_API_VERSION = '0.3.0'
+export const EXPECTED_SIIBRA_API_VERSION = '0.3.1'
 
 let BS_ENDPOINT_CACHED_VALUE: Observable<string> = null
 
diff --git a/src/routerModule/routeStateTransform.service.ts b/src/routerModule/routeStateTransform.service.ts
index bbc9c3ab9..1032448ec 100644
--- a/src/routerModule/routeStateTransform.service.ts
+++ b/src/routerModule/routeStateTransform.service.ts
@@ -77,7 +77,6 @@ export class RouteStateTransformSvc {
       }
       
       const regionMap = new Map<string, SxplrRegion>(allParcellationRegions.map(region => [region.name, region]))
-      const ngIdToRegionMap: Map<string, Map<number, SxplrRegion[]>> = new Map()
 
       const [ ngMap, threeMap ] = await Promise.all([
         this.sapi.getTranslatedLabelledNgMap(selectedParcellation, selectedTemplate),
diff --git a/src/state/atlasSelection/effects.spec.ts b/src/state/atlasSelection/effects.spec.ts
index a1a820f67..603c1c4ec 100644
--- a/src/state/atlasSelection/effects.spec.ts
+++ b/src/state/atlasSelection/effects.spec.ts
@@ -12,14 +12,67 @@ import { Effect } from "./effects"
 import * as mainActions from "../actions"
 import { atlasSelection } from ".."
 import { BrowserAnimationsModule } from "@angular/platform-browser/animations"
+import { translateV3Entities } from "src/atlasComponents/sapi/translateV3"
+import { PathReturn } from "src/atlasComponents/sapi/typeV3"
 
 describe("> effects.ts", () => {
   describe("> Effect", () => {
 
     let actions$ = new Observable<Action>()
-    let hoc1left: SxplrRegion
-    let hoc1leftCentroid: SxplrRegion
-    let hoc1leftCentroidWrongSpc: SxplrRegion
+
+    let simpleHoc1: SxplrRegion = {
+      name: 'foo',
+      id: '',
+      type: "SxplrRegion",
+      parentIds: [],
+    }
+
+    let hoc1LeftMni152: PathReturn<"/regions/{region_id}"> = {
+      "@id": "",
+      versionIdentifier: '',
+      "@type": '',
+      hasAnnotation: {
+        criteriaQualityType: {},
+        internalIdentifier: "",
+        bestViewPoint: {
+          coordinateSpace: {
+            "@id": IDS.TEMPLATES.MNI152
+          } as any,
+          coordinates: [
+            {
+              value: 1,
+            },{
+              value: 2,
+            },{
+              value: 3,
+            }
+          ]
+        }
+      }
+    }
+    let hoc1LeftColin27: PathReturn<"/regions/{region_id}"> = {
+      "@id": "",
+      versionIdentifier: '',
+      "@type": '',
+      hasAnnotation: {
+        criteriaQualityType: {},
+        internalIdentifier: "",
+        bestViewPoint: {
+          coordinateSpace: {
+            "@id": IDS.TEMPLATES.COLIN27
+          } as any,
+          coordinates: [
+            {
+              value: 1,
+            },{
+              value: 2,
+            },{
+              value: 3,
+            }
+          ]
+        }
+      }
+    }
 
     beforeEach(async () => {
       TestBed.configureTestingModule({
@@ -34,26 +87,6 @@ describe("> effects.ts", () => {
           provideMockActions(() => actions$)
         ]
       })
-
-      /**
-       * only need to populate hoc1 left once
-       */
-      if (!hoc1left) {
-
-        const sapisvc = TestBed.inject(SAPI)
-        const regions = await sapisvc.getParcRegions(IDS.PARCELLATION.JBA29).toPromise()
-        hoc1left = regions.find(r => /hoc1/i.test(r.name) && /left/i.test(r.name))
-        if (!hoc1left) throw new Error(`cannot find hoc1 left`)
-        hoc1leftCentroid = JSON.parse(JSON.stringify(hoc1left)) 
-        hoc1leftCentroid.centroid = {
-          space: {
-            id: IDS.TEMPLATES.BIG_BRAIN
-          } as SxplrTemplate,
-          loc: [1, 2, 3]
-        }
-        hoc1leftCentroidWrongSpc = JSON.parse(JSON.stringify(hoc1leftCentroid))
-        hoc1leftCentroidWrongSpc.centroid.space.id = IDS.TEMPLATES.COLIN27
-      }
     })
 
     it('> can be init', () => {
@@ -229,10 +262,39 @@ describe("> effects.ts", () => {
     })
 
     describe('> onNavigateToRegion', () => {
+
+      const translatedRegion = {
+        "@id": "",
+        versionIdentifier: '',
+        "@type": '',
+        hasAnnotation: {
+          criteriaQualityType: {},
+          internalIdentifier: "",
+          bestViewPoint: {
+            coordinateSpace: {
+              "@id": IDS.TEMPLATES.MNI152
+            } as any,
+            coordinates: [
+              {
+                value: 1,
+              },{
+                value: 2,
+              },{
+                value: 3,
+              }
+            ]
+          }
+        }
+      } as PathReturn<"/regions/{region_id}">
+
+      let retrieveRegionSpy: jasmine.Spy
+
       beforeEach(async () => {
+        retrieveRegionSpy = spyOn(translateV3Entities, 'retrieveRegion')
+        
         actions$ = hot('a', {
           a: actions.navigateToRegion({
-            region: hoc1left
+            region: simpleHoc1
           })
         })
         const mockStore = TestBed.inject(MockStore)
@@ -264,6 +326,7 @@ describe("> effects.ts", () => {
             beforeEach(() => {
               const mockStore = TestBed.inject(MockStore)
               mockStore.overrideSelector(atpSelector, null)
+              retrieveRegionSpy.and.
             })
 
             it('> returns general error', () => {
@@ -302,16 +365,7 @@ describe("> effects.ts", () => {
       })
 
       describe('> if inputs are fine', () => {
-        let regionGetDetailSpy: jasmine.Spy = jasmine.createSpy()
-        beforeEach(() => {
-          const sapi = TestBed.inject(SAPI)
-          regionGetDetailSpy.and.returnValue(
-            of(hoc1leftCentroid)
-          )
-        })
-        afterEach(() => {
-          if (regionGetDetailSpy) regionGetDetailSpy.calls.reset()
-        })
+
         it('> getRegionDetailSpy is called, and calls navigateTo', () => {
           const eff = TestBed.inject(Effect)
           expect(eff.onNavigateToRegion).toBeObservable(
@@ -330,9 +384,6 @@ describe("> effects.ts", () => {
           describe('> returns null', () => {
             beforeEach(() => {
 
-              regionGetDetailSpy.and.returnValue(
-                of(null)
-              )
             })
             it('> generalactionerror', () => {
 
@@ -348,9 +399,7 @@ describe("> effects.ts", () => {
           })
           describe('> general throw', () => {
             beforeEach(() => {
-              regionGetDetailSpy.and.returnValue(
-                throwError(`oh noes`)
-              )
+
             })
             it('> generalactionerror', () => {
 
@@ -358,7 +407,7 @@ describe("> effects.ts", () => {
               expect(eff.onNavigateToRegion).toBeObservable(
                 hot(`a`, {
                   a: mainActions.generalActionError({
-                    message: `Error getting region centroid`
+                    message: `getting region detail error! cannot get coordinates`
                   })
                 })
               )
@@ -368,9 +417,7 @@ describe("> effects.ts", () => {
           describe('> does not contain props attr', () => {
 
             beforeEach(() => {
-              regionGetDetailSpy.and.returnValue(
-                of(hoc1left)
-              )
+              
             })
             it('> generalactionerror', () => {
 
diff --git a/src/state/atlasSelection/effects.ts b/src/state/atlasSelection/effects.ts
index 673a54d0f..601ea9f8e 100644
--- a/src/state/atlasSelection/effects.ts
+++ b/src/state/atlasSelection/effects.ts
@@ -1,7 +1,7 @@
 import { Injectable } from "@angular/core";
 import { Actions, createEffect, ofType } from "@ngrx/effects";
 import { forkJoin, merge, NEVER, Observable, of } from "rxjs";
-import { filter, map, mapTo, switchMap, switchMapTo, take, withLatestFrom } from "rxjs/operators";
+import { catchError, filter, map, mapTo, switchMap, switchMapTo, take, withLatestFrom } from "rxjs/operators";
 import { SAPI, SAPIRegion } from "src/atlasComponents/sapi";
 import * as mainActions from "../actions"
 import { select, Store } from "@ngrx/store";
@@ -346,27 +346,35 @@ export class Effect {
         select(selectors.selectedParcellation)
       )
     ),
-    map(([{ region: _region }, selectedTemplate, selectedAtlas, selectedParcellation]) => {
+    switchMap(([{ region: _region }, selectedTemplate, selectedAtlas, selectedParcellation]) => {
       if (!selectedAtlas || !selectedTemplate || !selectedParcellation || !_region)  {
-        return mainActions.generalActionError({
-          message: `atlas, template, parcellation or region not set`
-        })
+        return of(
+          mainActions.generalActionError({
+            message: `atlas, template, parcellation or region not set`
+          })
+        )
       }
-
-      const region = translateV3Entities.retrieveRegion(_region)
-
-      if (region.hasAnnotation?.bestViewPoint && region.hasAnnotation.bestViewPoint.coordinateSpace['@id'] === selectedTemplate["@id"]) {
-        return actions.navigateTo({
+      return this.sapiSvc.v3Get("/regions/{region_id}", {
+        path: {
+          region_id: _region.name
+        },
+        query: {
+          parcellation_id: selectedParcellation.id,
+          space_id: selectedTemplate.id
+        }
+      }).pipe(
+        map(reg => actions.navigateTo({
           animation: true,
           navigation: {
-            position: region.hasAnnotation.bestViewPoint.coordinates.map(v => v.value * 1e6)
+            position: reg.hasAnnotation.bestViewPoint.coordinates.map(v => v.value * 1e6)
           }
-        })
-      }
-      
-      return mainActions.generalActionError({
-        message: `getting region detail error! cannot get coordinates`
-      })
+        })),
+        catchError(() => of(
+          mainActions.generalActionError({
+            message: `getting region detail error! cannot get coordinates`
+          })
+        )),
+      )
     })
   ))
 
diff --git a/src/viewerModule/nehuba/config.service/util.ts b/src/viewerModule/nehuba/config.service/util.ts
index fa76f139a..ddc82c648 100644
--- a/src/viewerModule/nehuba/config.service/util.ts
+++ b/src/viewerModule/nehuba/config.service/util.ts
@@ -174,7 +174,7 @@ export function getParcNgId(atlas: SxplrAtlas, tmpl: SxplrTemplate, parc: SxplrP
       : null
   }
 
-  if (parc.id === IDS.PARCELLATION.JBA30) {
+  if (parc.id === IDS.PARCELLATION.JBA30 && tmpl.id !== IDS.TEMPLATES.FSAVERAGE) {
     return `_${MultiDimMap.GetKey(atlas.id, tmpl.id, parc.id, "whole brain")}`
   }
 
diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.spec.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.spec.ts
index 45a5aee2d..e29f976cf 100644
--- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.spec.ts
+++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.spec.ts
@@ -10,12 +10,17 @@ import {
 import { LayerCtrlEffects } from "./layerCtrl.effects"
 import { NEVER } from "rxjs"
 import { RouterService } from "src/routerModule/router.service"
+import { HttpClientModule } from "@angular/common/http"
+import { BaseService } from "../base.service/base.service"
 
 describe('> layerctrl.service.ts', () => {
   describe('> NehubaLayerControlService', () => {
     let mockStore: MockStore
     beforeEach(() => {
       TestBed.configureTestingModule({
+        imports:[
+          HttpClientModule,
+        ],
         providers: [
           {
             provide: RouterService,
@@ -30,6 +35,13 @@ describe('> layerctrl.service.ts', () => {
             useValue: {
               onATPDebounceNgLayers$: NEVER
             }
+          },
+          {
+            provide: BaseService,
+            useValue: {
+              selectedATPR$: NEVER,
+              completeNgIdLabelRegionMap$: NEVER,
+            }
           }
         ]
       })
diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
index 971f599b6..f68170be4 100644
--- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
+++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
@@ -32,8 +32,6 @@ export class NehubaLayerControlService implements OnDestroy{
 
   private defaultNgLayers$ = this.layerEffects.onATPDebounceNgLayers$
 
-  private selectedATP$ = this.baseService.selectedATP$
-
   public selectedATPR$ = this.baseService.selectedATPR$
 
   private customLayers$ = this.store$.pipe(
diff --git a/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts b/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts
index 0b7d70236..1cc0bdc7f 100644
--- a/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts
+++ b/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts
@@ -9,6 +9,8 @@ import { LayerCtrlEffects } from "../layerCtrl.service/layerCtrl.effects"
 import { NEVER, of, pipe } from "rxjs"
 import { mapTo, take } from "rxjs/operators"
 import { selectorAuxMeshes } from "../store"
+import { HttpClientModule } from "@angular/common/http"
+import { BaseService } from "../base.service/base.service"
 
 
 const fits1 = {} as SxplrRegion
@@ -59,6 +61,9 @@ describe('> mesh.service.ts', () => {
   describe('> NehubaMeshService', () => {
     beforeEach(() => {
       TestBed.configureTestingModule({
+        imports: [
+          HttpClientModule,
+        ],
         providers: [
           provideMockStore(),
           NehubaMeshService,
@@ -67,6 +72,12 @@ describe('> mesh.service.ts', () => {
             useValue: {
               onATPDebounceNgLayers$: NEVER
             }
+          },
+          {
+            provide: BaseService,
+            useValue: {
+              completeNgIdLabelRegionMap$: NEVER
+            }
           }
         ]
       })
@@ -151,11 +162,18 @@ describe('> mesh.service.ts', () => {
          * in the case of julich brain 2.9 in colin 27, we expect selecting a region will hide meshes from all relevant ngIds (both left and right)
          */
         it('> expect the emitted value to be incl all ngIds', () => {
+          const bService = TestBed.inject(BaseService)
+          bService.completeNgIdLabelRegionMap$ = of({
+            [ngId1]: {},
+            [ngId2]: {
+              [labelIndex2]: fits1
+            }
+          })
           const service = TestBed.inject(NehubaMeshService)
           expect(
             service.loadMeshes$
           ).toBeObservable(
-            hot('(ab)', {
+            hot('abc', {
               a: {
                 layer: {
                   name: ngId1
@@ -167,6 +185,12 @@ describe('> mesh.service.ts', () => {
                   name: ngId2
                 },
                 labelIndicies: [ labelIndex2 ]
+              },
+              c: {
+                layer: {
+                  name: auxMesh.ngId
+                },
+                labelIndicies: auxMesh.labelIndicies
               }
             })
           )
diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts
index 7d6634342..ecb328948 100644
--- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts
+++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts
@@ -1,13 +1,13 @@
 import { Component, Output, EventEmitter, ElementRef, OnDestroy, AfterViewInit, Inject, Optional, ChangeDetectionStrategy } from "@angular/core";
 import { EnumViewerEvt, IViewer, TViewerEvent } from "src/viewerModule/viewer.interface";
-import { combineLatest, from, merge, NEVER, Observable, Subject } from "rxjs";
-import { catchError, debounceTime, distinctUntilChanged, filter, map, scan, shareReplay, switchMap } from "rxjs/operators";
+import { combineLatest, concat, forkJoin, from, merge, NEVER, Observable, of, Subject } from "rxjs";
+import { catchError, debounceTime, distinctUntilChanged, filter, map, scan, shareReplay, switchMap, withLatestFrom } from "rxjs/operators";
 import { ComponentStore } from "src/viewerModule/componentStore";
 import { select, Store } from "@ngrx/store";
 import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util";
 import { MatSnackBar } from "@angular/material/snack-bar";
 import { CONST } from 'common/constants'
-import { getUuid } from "src/util/fn";
+import { getUuid, switchMapWaitFor } from "src/util/fn";
 import { AUTO_ROTATE, TInteralStatePayload, ViewerInternalStateSvc } from "src/viewerModule/viewerInternalState.service";
 import { atlasAppearance, atlasSelection } from "src/state";
 import { ThreeSurferCustomLabelLayer, ThreeSurferCustomLayer, ColorMapCustomLayer } from "src/state/atlasAppearance/const";
@@ -38,6 +38,48 @@ type THandlingCustomEv = {
   }
 }
 
+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
+    visible: boolean,
+    mesh: TThreeGeometry
+  }[]
+}
+
+type TLatIdxReg = LateralityRecord<Record<number, SxplrRegion>>
+
+type TLatCm = LateralityRecord<{
+  labelIndices: number[]
+  map: Map<number, number[]>
+}>
+
 type TCameraOrientation = {
   perspectiveOrientation: number[]
   perspectiveZoom: number
@@ -92,6 +134,8 @@ function cameraNavsAreSimilar(c1: TCameraOrientation, c2: TCameraOrientation){
 
 export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit, OnDestroy {
 
+  #cameraEv$ = new Subject<{ position: { x: number, y: number, z: number }, zoom: number }>()
+  #mouseEv$ = new Subject()
   
   @Output()
   viewerEvent = new EventEmitter<TViewerEvent<'threeSurfer'>>()
@@ -100,44 +144,106 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit
   private mainStoreCameraNav: TCameraOrientation = null
   private localCameraNav: TCameraOrientation = null
 
-  public lateralityMeshRecord: LateralityRecord<{
-    visible: boolean
-    meshLayer: ThreeSurferCustomLayer
-    mesh: TThreeGeometry
-  }> = {}
 
-  public latLblIdxRecord: LateralityRecord<{
-    indexLayer: ThreeSurferCustomLabelLayer
-    labelIndices: number[]
-  }> = {}
   private internalStateNext: (arg: TInteralStatePayload<TInternalState>) => void
 
   private mouseoverRegions: SxplrRegion[] = []
-  
-  private selectedRegions$ = this.store$.pipe(
-    select(atlasSelection.selectors.selectedRegions)
-  )
 
   private customLayers$ = this.store$.pipe(
     select(atlasAppearance.selectors.customLayers),
     distinctUntilChanged(arrayEqual((o, n) => o.id === n.id)),
     shareReplay(1)
   )
-  public meshLayers$: Observable<ThreeSurferCustomLayer[]> = this.customLayers$.pipe(
+  #meshLayers$: Observable<ThreeSurferCustomLayer[]> = this.customLayers$.pipe(
     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
+        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
+      }))
+    ))
+  )
+
   private vertexIndexLayers$: Observable<ThreeSurferCustomLabelLayer[]> = this.customLayers$.pipe(
     map(layers => layers.filter(l => l.clType === "baselayer/threesurfer-label") as ThreeSurferCustomLabelLayer[]),
     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 => 
+          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
+              }
+            })
+          )
+        )
+      )
+    ),
+    map(layers => {
+      const returnObj = {}
+      for (const { indexLayer, vertexIndices } of layers) {
+        returnObj[indexLayer.laterality] = { indexLayer, vertexIndices }
+      }
+      return returnObj
+    })
+  )
+
   /**
    * maps laterality to label index to sapi region
    */
-  private latLblIdxToRegionRecord: LateralityRecord<Record<number, SxplrRegion>> = {}
-  private latLblIdxToRegionRecord$: Observable<LateralityRecord<Record<number, SxplrRegion>>> = combineLatest([
+  
+  #latLblIdxToRegionRecord$: Observable<TLatIdxReg> = combineLatest([
     this.store$.pipe(
       atlasSelection.fromRootStore.distinctATP()
     ),
@@ -183,15 +289,43 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit
    * colormap in use (both base & custom)
    */
 
-  private colormapInUse: ColorMapCustomLayer
-  private colormaps$: Observable<ColorMapCustomLayer[]> = this.customLayers$.pipe(
+  #colormaps$: Observable<ColorMapCustomLayer[]> = this.customLayers$.pipe(
     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 = {}
+      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
+    })
   )
 
   /**
-   * show delination map
+   * 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
    */
-  private showDelineation: boolean = true
 
   public threeSurferSurfaceVariants$ = this.effect.onATPDebounceThreeSurferLayers$.pipe(
     map(({ surfaces }) => surfaces.reduce((acc, val) => acc.includes(val.variant) ? acc : [...acc, val.variant] ,[] as string[]))
@@ -275,7 +409,7 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit
     /**
      * subscribe to camera custom event
      */
-    const cameraSub = this.cameraEv$.pipe(
+    const cameraSub = this.#cameraEv$.pipe(
       filter(v => !!v),
       debounceTime(160)
     ).subscribe(() => {
@@ -378,7 +512,6 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit
   }
 
   private tsRef: TThreeSurfer
-  private selectedRegions: SxplrRegion[] = []
 
   private relayStoreLock: () => void = null
   private tsRefInitCb: ((tsRef: any) => void)[] = []
@@ -390,47 +523,67 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit
     this.tsRefInitCb.push(callback)
   }
 
-  private async loadMeshes(layers: ThreeSurferCustomLayer[]) {
+  async #loadMeshes(layers: ThreeSurferCustomLayer[], currMeshRecord: TLatMeshRecord) {
     if (!this.tsRef) throw new Error(`loadMeshes error: this.tsRef is not defined!!`)
-
+    const copiedCurrMeshRecord: TLatMeshRecord = {...currMeshRecord}
     /**
      * remove the layers... 
      */
     for (const layer of layers) {
-      if (!!this.lateralityMeshRecord[layer.laterality]) {
-        this.tsRef.unloadMesh(this.lateralityMeshRecord[layer.laterality].mesh)
+      if (!!copiedCurrMeshRecord[layer.laterality]) {
+        this.tsRef.unloadMesh(copiedCurrMeshRecord[layer.laterality].mesh)
       }
     }
 
     for (const layer of layers) {
       const threeMesh = await this.tsRef.loadMesh(layer.source)
-      this.lateralityMeshRecord[layer.laterality] = {
-        visible: true,
+      copiedCurrMeshRecord[layer.laterality] = {
         meshLayer: layer,
         mesh: threeMesh
       }
     }
-    this.applyColor()
+    this.#lateralMeshRecord$.next(copiedCurrMeshRecord)
   }
 
-  private async loadVertexIndexMap(layers: ThreeSurferCustomLabelLayer[]) {
-    if (!this.tsRef) throw new Error(`loadVertexIndexMap error: this.tsRef is not defined!!`)
-    for (const layer of layers) {
-      const giiInstance = await this.tsRef.loadColormap(layer.source)
-
-      let labelIndices: number[] = giiInstance[0].getData()
-      if (giiInstance[0].attributes.DataType === 'NIFTI_TYPE_INT16') {
-        labelIndices = (window as any).ThreeSurfer.GiftiBase.castF32UInt16(labelIndices)
-      }
-      this.latLblIdxRecord[layer.laterality] = {
-        indexLayer: layer,
-        labelIndices
+  #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
+        }
       }
-    }
-    this.applyColor()
-  }
-
-  private applyColor() {
+      return arg
+    })
+  )
+  private applyColor(applyArg: TApplyColorArg) {
     /**
      * on apply color map, reset mesh visibility
      * this issue is more difficult to solve than first anticiplated.
@@ -440,41 +593,81 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit
      * 2/ hide hemisphere, select region, unhide hemisphere
      * 3/ select region, hide hemisphere, deselect region
      */
-    if (!this.colormapInUse) return
+    
     if (!this.tsRef) return
     
-    const isBaseCM = this.colormapInUse?.clType === "baselayer/colormap"
+    for (const laterality in applyArg) {
+      const { labelIndices, map, mesh, showDelin, selectedRegions, isBaseCm, idxReg, vertexIndices } = applyArg[laterality]
 
-    for (const laterality in this.lateralityMeshRecord) {
-      const { mesh } = this.lateralityMeshRecord[laterality]
-      if (!this.latLblIdxRecord[laterality]) continue
-      const { labelIndices } = this.latLblIdxRecord[laterality]
-
-      const lblIdxToRegionRecord = this.latLblIdxToRegionRecord[laterality]
-      if (!lblIdxToRegionRecord) {
-        this.tsRef.applyColorMap(mesh, labelIndices)
+      if (!map) {
+        this.tsRef.applyColorMap(mesh, vertexIndices)
         continue
       }
-      const map = new Map<number, number[]>()
-      for (const lblIdx in lblIdxToRegionRecord) {
-        const region = lblIdxToRegionRecord[lblIdx]
-        let color: number[]
-        if (!this.showDelineation) {
-          color = [1,1,1]
-        } else if (isBaseCM && this.selectedRegions.length > 0 && !this.selectedRegions.includes(region)) {
-          color = [1,1,1]
-        } else {
-          color = (this.colormapInUse.colormap.get(region) || [255, 255, 255]).map(v => v/255)
+
+      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
+        })
+        return
+      }
+
+      const highlightIdx = new Set<number>()
+      if (isBaseCm && selectedRegions.length > 0) {
+        for (const [idx, region] of Object.entries(idxReg)) {
+          if (selectedRegions.indexOf(region) >= 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]
+          )
         }
-        map.set(Number(lblIdx), color)
       }
-      this.tsRef.applyColorMap(mesh, labelIndices, {
-        custom: map
+      this.tsRef.applyColorMap(mesh, vertexIndices, {
+        custom: actualApplyMap
       })
     }
   }
 
-  private handleCustomMouseEv(detail: any){
+  #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,
       // typo in three-surfer
@@ -495,25 +688,26 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit
       verticesIdicies: evVerticesIndicies,
     } = detail.mesh as { geometry: TThreeGeometry, verticesIdicies: number[] }
 
-    for (const laterality in this.lateralityMeshRecord) {
-      const meshRecord = this.lateralityMeshRecord[laterality]
+    for (const laterality in latMeshRecord) {
+      const meshRecord = latMeshRecord[laterality]
       if (meshRecord.mesh !== evGeometry) {
         continue
       }
       /**
        * if either labelindex record or colormap record is undefined for this laterality, emit empty event
        */
-      if (!this.latLblIdxRecord[laterality] || !this.latLblIdxToRegionRecord[laterality]) {
+      if (!latLblIdxRecord[laterality] || !latLblIdxReg[laterality]) {
         return this.handleMouseoverEvent(custEv)
       }
-      const labelIndexRecord = this.latLblIdxRecord[laterality]
-      const regionRecord = this.latLblIdxToRegionRecord[laterality]
+      const labelIndexRecord = latLblIdxRecord[laterality]
+      const regionRecord = latLblIdxReg[laterality]
 
       /**
        * check if the mesh is toggled off
        * if so, do not proceed
        */
-      if (!meshRecord.visible) {
+      const mVis = meshVisibility.filter(({ mesh }) => mesh === meshRecord.mesh)
+      if (!mVis.every(m => m.visible)) {
         return
       }
 
@@ -522,7 +716,7 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit
        */
       const labelIndexSet = new Set<number>()
       for (const idx of evVerticesIndicies){
-        const labelOfInterest = labelIndexRecord.labelIndices[idx]
+        const labelOfInterest = labelIndexRecord.vertexIndices[idx]
         if (!labelOfInterest) {
           continue
         }
@@ -551,31 +745,28 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit
     }
   }
 
-  private cameraEv$ = new Subject<{ position: { x: number, y: number, z: number }, zoom: number }>()
-  private handleCustomCameraEvent(detail: any){
-    if (this.internalStateNext) {
-      this.internalStateNext({
-        "@id": getUuid(),
-        "@type": 'TViewerInternalStateEmitterEvent',
-        viewerType,
-        payload: {
-          mode: '',
-          camera: detail.position,
-          hemisphere: 'both'
-        }
-      })
-    }
-    this.cameraEv$.next(detail)
-  }
-
   ngAfterViewInit(): void{
     const customEvHandler = (ev: CustomEvent) => {
       const { type, data } = ev.detail
       if (type === 'mouseover') {
-        return this.handleCustomMouseEv(data)
+        this.#mouseEv$.next(data)
+        return
       }
       if (type === 'camera') {
-        return this.handleCustomCameraEvent(data)
+        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)
@@ -596,40 +787,46 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit
       tsCb(this.tsRef)
     }
 
-    const meshSub = this.meshLayers$.pipe(
-      distinctUntilChanged(),
+    const meshSub = this.#meshLayers$.pipe(
+      switchMap(
+        switchMapWaitFor({
+          condition: () => !!this.tsRef,
+          leading: true
+        })
+      ),
       debounceTime(16),
-    ).subscribe(layers => {
-      this.loadMeshes(layers)
+      withLatestFrom(
+        this.lateralMeshRecord$
+      )
+    ).subscribe(([layers, currMeshRecord]) => {
+      this.#loadMeshes(layers, currMeshRecord)
     })
-    const vertexIdxSub = this.vertexIndexLayers$.subscribe(layers => this.loadVertexIndexMap(layers))
-    const roiSelectedSub = this.selectedRegions$.subscribe(regions => {
-      this.selectedRegions = regions
-      this.applyColor()
+    
+    const applyColorSub = this.#applyColor$.subscribe(arg => {
+      this.applyColor(arg)
     })
-    const colormapSub = this.colormaps$.subscribe(cm => {
-      this.colormapInUse = cm[0] || null
-      this.applyColor()
+
+    const mouseSub = this.#handleCustomMouseEv$.subscribe(arg => {
+      this.#handleCustomMouseEv(arg)
     })
-    const recordToRegionSub = this.latLblIdxToRegionRecord$.subscribe(val => this.latLblIdxToRegionRecord = val)
-    const hideDelineationSub = this.store$.pipe(
-      select(atlasAppearance.selectors.showDelineation)
-    ).subscribe(flag => {
-      this.showDelineation = flag
-      this.applyColor()
-      /**
-       * apply color resets mesh visibility
-       */
-      this.updateMeshVisibility()
+
+    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
+      }
     })
 
     this.onDestroyCb.push(() => {
       meshSub.unsubscribe()
-      vertexIdxSub.unsubscribe()
-      roiSelectedSub.unsubscribe()
-      colormapSub.unsubscribe()
-      recordToRegionSub.unsubscribe()
-      hideDelineationSub.unsubscribe()
+      applyColorSub.unsubscribe()
+      mouseSub.unsubscribe()
+      visibilitySub.unsubscribe()
     })
 
     this.viewerEvent.emit({
@@ -666,20 +863,11 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit
     if (this.mouseoverText === '') this.mouseoverText = null
   }
 
-  public updateMeshVisibility(): void{
-
-    for (const key in this.lateralityMeshRecord) {
-
-      const latMeshRecord = this.lateralityMeshRecord[key]
-      if (!latMeshRecord) {
-        return
-      }
-      const meshObj = this.tsRef.customColormap.get(latMeshRecord.mesh)
-      if (!meshObj) {
-        throw new Error(`mesh obj not found!`)
-      }
-      meshObj.mesh.visible = latMeshRecord.visible
-    }
+  public toggleMeshVis(label: string) {
+    this.#meshVisOp$.next({
+      label,
+      op: 'toggle'
+    })
   }
 
   switchSurfaceLayer(variant: string): void{
diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.template.html b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.template.html
index fcef001a1..778626e2c 100644
--- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.template.html
+++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.template.html
@@ -19,12 +19,12 @@
 <mat-menu #fsModeSelMenu="matMenu">
 
   <div class="sxplr-custom-cmp text sxplr-pl-2 m-2">
-    <mat-checkbox *ngFor="let item of lateralityMeshRecord | keyvalue"
+    <mat-checkbox *ngFor="let item of meshVisible$ | async "
       class="d-block"
       iav-stop="click"
-      (change)="updateMeshVisibility()"
-      [(ngModel)]="item.value.visible">
-      {{ item.key }}
+      (change)="toggleMeshVis(item.label)"
+      [checked]="item.visible">
+      {{ item.label }}
     </mat-checkbox>
   </div>
   <mat-divider></mat-divider>
-- 
GitLab