diff --git a/angular.json b/angular.json
index af94adeb567bf3ab3406beeba94e8191e7e97c11..dcbadb4911f1203a6204541cc20e00b2ec00947b 100644
--- a/angular.json
+++ b/angular.json
@@ -65,7 +65,9 @@
               "input": "third_party/vanilla_nehuba.js",
               "inject": false,
               "bundleName": "vanilla_nehuba"
-            },{
+            },
+            
+            {
               "input": "export-nehuba/dist/min/main.bundle.js",
               "inject": false,
               "bundleName": "main.bundle"
@@ -73,7 +75,22 @@
               "input": "export-nehuba/dist/min/chunk_worker.bundle.js",
               "inject": false,
               "bundleName": "chunk_worker.bundle"
+            },
+            {
+              "input": "export-nehuba/dist/min/draco.bundle.js",
+              "inject": false,
+              "bundleName": "draco.bundle"
             },{
+              "input": "export-nehuba/dist/min/async_computation.bundle.js",
+              "inject": false,
+              "bundleName": "async_computation.bundle"
+            },{
+              "input": "export-nehuba/dist/min/blosc.bundle.js",
+              "inject": false,
+              "bundleName": "blosc.bundle"
+            },
+            
+            {
               "inject": false,
               "input": "third_party/leap-0.6.4.js",
               "bundleName": "leap-0.6.4"
diff --git a/deploy/csp/index.js b/deploy/csp/index.js
index e48f7f155e90e027e0274590f854d7281ca83bad..92ee364fd5cd7691811cf45956712bf98fd184c6 100644
--- a/deploy/csp/index.js
+++ b/deploy/csp/index.js
@@ -115,7 +115,7 @@ module.exports = {
           'https://unpkg.com/d3@6.2.0/', // required for preview component
           'https://unpkg.com/mathjax@3.1.2/', // math jax
           'https://unpkg.com/three-surfer@0.0.13/dist/bundle.js', // for threeSurfer (freesurfer support in browser)
-          'https://unpkg.com/ng-layer-tune@0.0.13/dist/ng-layer-tune/', // needed for ng layer control
+          'https://unpkg.com/ng-layer-tune@0.0.14/dist/ng-layer-tune/', // needed for ng layer control
           'https://unpkg.com/hbp-connectivity-component@0.6.6/', // needed for connectivity component
           (req, res) => res.locals.nonce ? `'nonce-${res.locals.nonce}'` : null,
           ...SCRIPT_SRC,
diff --git a/docs/releases/v2.12.0.md b/docs/releases/v2.12.0.md
new file mode 100644
index 0000000000000000000000000000000000000000..ba23e2fa1c0a5e1dc3cdb00fdfef918654e02bf0
--- /dev/null
+++ b/docs/releases/v2.12.0.md
@@ -0,0 +1,16 @@
+# v2.12.0
+
+## Feature
+
+- added opacity slider for external volumes, even if the more detail is collapsed.
+- enable rat connectivity
+- added visual indicators for selected subject and dataset in connectivity browser
+
+## Bugfix
+
+- fixed fsaverage viewer "rubber banding"
+
+## Behind the scene
+
+- update spotlight mechanics from in-house to angular CDK
+- Updated neuroglancer/nehuba dependency. This allows volumes with non-rigid affine to be displayed properly.
diff --git a/package-lock.json b/package-lock.json
index 792fff775330a11e4a0c83b26158e1694e7836f7..c6e536b6eb6532373f96bd0d1c06c34e690ec417 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "siibra-explorer",
-  "version": "2.11.2",
+  "version": "2.12.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "siibra-explorer",
-      "version": "2.11.2",
+      "version": "2.12.0",
       "license": "apache-2.0",
       "dependencies": {
         "@angular/animations": "^14.2.12",
@@ -23,7 +23,7 @@
         "@ngrx/effects": "^14.3.2",
         "@ngrx/store": "^14.3.2",
         "acorn": "^8.4.1",
-        "export-nehuba": "0.0.12",
+        "export-nehuba": "^0.1.0",
         "file-loader": "^6.2.0",
         "jszip": "^3.6.0",
         "postcss": "^8.3.6",
@@ -26966,9 +26966,9 @@
       "dev": true
     },
     "node_modules/export-nehuba": {
-      "version": "0.0.12",
-      "resolved": "https://registry.npmjs.org/export-nehuba/-/export-nehuba-0.0.12.tgz",
-      "integrity": "sha512-pf3hAwpXaOqlfBfgmPLYQ+uLqJ+ElyvE1bDrrCrf5Qf0Otsekw+8CcyAJhP5O15Yacmhe7Py3G96tw5bbvZyIA==",
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/export-nehuba/-/export-nehuba-0.1.0.tgz",
+      "integrity": "sha512-49mp9MiR6n+1zzeoVOfYTmr1g9CWBXrCtXK6PxwnRj+VBFrmjbp5PzBjVsGr5HsODrhwBWCLInK7zXmXaDnE/Q==",
       "dependencies": {
         "pako": "^1.0.6"
       }
diff --git a/package.json b/package.json
index 5b30b70658b3879a994a8c321d9c4cd07816e8ca..5362349f32bea024d80e831723fa469611008978 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "siibra-explorer",
-  "version": "2.11.4",
+  "version": "2.12.0",
   "description": "siibra-explorer - explore brain atlases. Based on humanbrainproject/nehuba & google/neuroglancer. Built with angular",
   "scripts": {
     "lint": "eslint src --ext .ts",
@@ -66,7 +66,7 @@
     "@ngrx/effects": "^14.3.2",
     "@ngrx/store": "^14.3.2",
     "acorn": "^8.4.1",
-    "export-nehuba": "0.0.12",
+    "export-nehuba": "^0.1.0",
     "file-loader": "^6.2.0",
     "jszip": "^3.6.0",
     "postcss": "^8.3.6",
diff --git a/src/atlasComponents/annotations/annotation.service.ts b/src/atlasComponents/annotations/annotation.service.ts
index 3969bc420cdea13c69fef90bbc765de4b047e0de..f0a63f7aa298c0cad46efcd96a1dfbe452f1098b 100644
--- a/src/atlasComponents/annotations/annotation.service.ts
+++ b/src/atlasComponents/annotations/annotation.service.ts
@@ -167,9 +167,6 @@ export class AnnotationLayer {
   }
 
   private parseNgSpecType(spec: AnnotationSpec): _AnnotationSpec{
-    const voxelSize = this.viewer.navigationState.voxelSize.toJSON()
-    const sanitizePoint = (p: [number, number, number]) => p.map((v, idx) => v / voxelSize[idx]) as [number, number, number]
-    const needSanitizePosition = voxelSize[0] !== 1 || voxelSize[1] !== 1 || voxelSize[2] !== 1
     const overwrite: Partial<_AnnotationSpec> = {}
     switch (spec.type) {
     case "point": {
@@ -187,15 +184,6 @@ export class AnnotationLayer {
     default: throw new Error(`overwrite type lookup failed for ${(spec as any).type}`)
     }
 
-    /**
-     * The unit of annotation(s) depends on voxel size. If it is 1,1,1 then it would be in um, but often it is not.
-     * If not sanitized, the annotation can be miles off.
-     */
-    if (needSanitizePosition) {
-      for (const key of ['point', 'pointA', 'pointB'] ) {
-        if (!!spec[key]) overwrite[key] = sanitizePoint(spec[key])
-      }
-    }
     return {
       ...spec,
       ...overwrite,
diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts
index 132a094b78f971512edbd8e6a7b64a9329b555a1..e1648fcc7c99cc8b263ef532fc9ea01e7363fb72 100644
--- a/src/atlasComponents/sapi/sapi.service.ts
+++ b/src/atlasComponents/sapi/sapi.service.ts
@@ -296,7 +296,7 @@ export class SAPI{
     switchMap(atlases => forkJoin(
       atlases.items.map(atlas => translateV3Entities.translateAtlas(atlas))
     )),
-    map(atlases => atlases.sort((a, b) => speciesOrder.indexOf(a.species) - speciesOrder.indexOf(b.species))),
+    map(atlases => atlases.sort((a, b) => (speciesOrder as string[]).indexOf(a.species) - (speciesOrder as string[]).indexOf(b.species))),
     tap(() => {
       const respVersion = SAPI.API_VERSION
       if (respVersion !== EXPECTED_SIIBRA_API_VERSION) {
diff --git a/src/extra_styles.css b/src/extra_styles.css
index 82f71ff959dc619fe28f06c56fab628041b69b03..38120840830934317ea6c65d369467f99202f362 100644
--- a/src/extra_styles.css
+++ b/src/extra_styles.css
@@ -821,10 +821,10 @@ mat-list.sm mat-list-item
   display: grid;
 }
 
-.grid.grid-col-3
+.grid.grid-col-4
 {
   grid-auto-columns: 1fr;
-  grid-template-columns: 1fr auto auto;
+  grid-template-columns: 1fr auto auto auto;
   gap: 0.2rem 0.2rem;
 }
 
diff --git a/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts b/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts
index 77b69d99dccae4d04870b68d7ac6a223b59548df..b90f25adb9706e80ae2cf8c433acc58bacceff38 100644
--- a/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts
+++ b/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts
@@ -370,6 +370,7 @@ export class ConnectivityBrowserComponent implements OnChanges, OnDestroy {
         selectedView
       }
     }),
+    distinctUntilChanged((o, n) => o?.feature_id === n?.feature_id && o?.subject === n?.subject && o?.selectedView === n?.selectedView && o?.parcellation?.id === n?.parcellation?.id),
     shareReplay(1),
   )
 
@@ -450,6 +451,7 @@ export class ConnectivityBrowserComponent implements OnChanges, OnDestroy {
   )
   
   view$ = combineLatest([
+    this.busy$,
     this.selectedDataset$,
     this.formValue$,
     this.#fetchingMatrix$,
@@ -459,13 +461,16 @@ export class ConnectivityBrowserComponent implements OnChanges, OnDestroy {
     ),
     this.region$,
   ]).pipe(
-    map(([sDs, form, fetchingMatrix, pureConnections, region]) => {
+    map(([busy, sDs, form, fetchingMatrix, pureConnections, region]) => {
       return {
         showSubject: sDs && form.selectedView === "subject",
         numSubjects: sDs?.subjects.length,
-        fetchingMatrix,
         connections: pureConnections,
         region,
+        showAverageToggle: form.selectedCohort !== null && typeof form.selectedCohort !== "undefined",
+        busy: busy || fetchingMatrix,
+        selectedSubject: (sDs?.subjects || [])[form.selectedSubjectIndex],
+        selectedDataset: form?.selectedDatasetIndex
       }
     }),
     shareReplay(1),
diff --git a/src/features/connectivity/connectivityBrowser/connectivityBrowser.template.html b/src/features/connectivity/connectivityBrowser/connectivityBrowser.template.html
index 416763f02dab74f3dd141ab7d954d7155739a5b7..1b56fa9e49a8f6b2209bd5c76a68c5eb18133585 100644
--- a/src/features/connectivity/connectivityBrowser/connectivityBrowser.template.html
+++ b/src/features/connectivity/connectivityBrowser/connectivityBrowser.template.html
@@ -32,7 +32,7 @@
                         </mat-form-field>
 
                         
-                        <ng-template [ngIf]="formValue$ | async | getProperty : 'selectedCohort'">
+                        <ng-template [ngIf]="view$ | async | getProperty : 'showAverageToggle'">
                             <mat-radio-group formControlName="selectedView">
                                 <mat-radio-button value="average" class="m-2" color="primary">
                                     Average
@@ -54,7 +54,7 @@
                 <div class="flex-grow-0 flex-shrink-0 d-flex flex-nowrap align-items-center">
                     <div class="flex-grow-1 flex-shrink-1 w-100">
                         <mat-label>
-                            Dataset
+                            Dataset: {{ view$ | async | getProperty : 'selectedDataset' }}
                         </mat-label>
                         <mat-slider [min]="0"
                             [max]="cohortDatasets.length - 1"
@@ -72,7 +72,7 @@
             class="flex-grow-0 flex-shrink-0 d-flex flex-nowrap align-items-center">
             <div class="flex-grow-1 flex-shrink-1 w-100">
                 <mat-label>
-                    Subject
+                    Subject: {{ view$ | async | getProperty : 'selectedSubject' }}
                 </mat-label>
                 <mat-slider [min]="0"
                     [max]="(view$ | async | getProperty : 'numSubjects') - 1"
@@ -90,15 +90,11 @@
     <ng-template [ngIf]="view$ | async | getProperty : 'region'" let-region>
 
         <!-- loading spinner -->
-        <ng-template [ngIf]="view$ | async | getProperty : 'fetchingMatrix'"
+        <ng-template [ngIf]="view$ | async | getProperty : 'busy'"
             [ngIfElse]="profileTmpl">
-            <div class="d-flex justify-content-center">
-                <mat-spinner></mat-spinner>
-            </div>
         </ng-template>
 
         <!-- profile -->
-        <!-- <pre>{{ view$ | async | json }}</pre> -->
         <ng-template #profileTmpl>
             
             <ng-template #noConnTmpl>
@@ -154,3 +150,13 @@
     </button>
     <button mat-menu-item (click)="exportFullConnectivity()">Dataset</button>
 </mat-menu>
+
+<ng-template [ngIf]="view$ | async | getProperty : 'busy'">
+    <div class="d-flex justify-content-center">
+        <ng-template [ngTemplateOutlet]="loadingTmpl"></ng-template>
+    </div>
+</ng-template>
+
+<ng-template #loadingTmpl>
+    <mat-spinner></mat-spinner>
+</ng-template>
diff --git a/src/features/entry/entry.component.ts b/src/features/entry/entry.component.ts
index 610af8724d495501def8df5fc6ead5e761f9b17f..e4cfbe75e126083681838c1fe815bca654737345 100644
--- a/src/features/entry/entry.component.ts
+++ b/src/features/entry/entry.component.ts
@@ -10,6 +10,7 @@ import { CategoryAccDirective } from "../category-acc.directive"
 import { combineLatest, concat, forkJoin, merge, of, Subject, Subscription } from 'rxjs';
 import { DsExhausted, IsAlreadyPulling, PulledDataSource } from 'src/util/pullable';
 import { TranslatedFeature } from '../list/list.directive';
+import { SPECIES_ENUM } from 'src/util/constants';
 
 const categoryAcc = <T extends Record<string, unknown>>(categories: T[]) => {
   const returnVal: Record<string, T[]> = {}
@@ -148,10 +149,10 @@ export class EntryComponent extends FeatureBase implements AfterViewInit, OnDest
 
   public showConnectivity$ = combineLatest([
     this.selectedAtlas$.pipe(
-      map(atlas => atlas?.species === "Homo sapiens")
+      map(atlas => atlas?.species === SPECIES_ENUM.HOMO_SAPIENS || atlas?.species === SPECIES_ENUM.RATTUS_NORVEGICUS)
     ),
     this.TPRBbox$.pipe(
-      map(({ parcellation }) => parcellation?.id === IDS.PARCELLATION.JBA29)
+      map(({ parcellation }) => parcellation?.id === IDS.PARCELLATION.JBA29 || parcellation?.id === IDS.PARCELLATION.WAXHOLMV4)
     )
   ]).pipe(
     map(flags => flags.every(f => f))
diff --git a/src/index.html b/src/index.html
index 1ed2d8187899e51442bc39c8ab9305ac15c7ea0f..625cc65955195124a32d39c78486382edb8a9737 100644
--- a/src/index.html
+++ b/src/index.html
@@ -14,7 +14,7 @@
   <script src="extra_js.js"></script>
   <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.13/dist/bundle.js" defer></script>
-  <script type="module" src="https://unpkg.com/ng-layer-tune@0.0.13/dist/ng-layer-tune/ng-layer-tune.esm.js"></script>
+  <script type="module" src="https://unpkg.com/ng-layer-tune@0.0.14/dist/ng-layer-tune/ng-layer-tune.esm.js"></script>
   <script type="module" src="https://unpkg.com/hbp-connectivity-component@0.6.6/dist/connectivity-component/connectivity-component.js" ></script>
   <script defer src="https://unpkg.com/mathjax@3.1.2/es5/tex-svg.js"></script>
   <script defer src="https://unpkg.com/d3@6.2.0/dist/d3.min.js"></script>
diff --git a/src/messagingGlue.ts b/src/messagingGlue.ts
index ae415e68cff96abaaf964e6606a2a93df8cbc782..994bf30d8aab393eccfa769dc1ac17f1c5fcf8ce 100644
--- a/src/messagingGlue.ts
+++ b/src/messagingGlue.ts
@@ -86,7 +86,8 @@ export class MessagingGlue implements IWindowMessaging, OnDestroy {
           "1"
         ],
         transform: transform,
-        clType: 'customlayer/nglayer' as const
+        clType: 'customlayer/nglayer' as const,
+        type: 'segmentation',
       }
 
       this.store.dispatch(
diff --git a/src/spotlight/const.ts b/src/spotlight/const.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7af90c565452372b76df5ac438d4c7b5859e0120
--- /dev/null
+++ b/src/spotlight/const.ts
@@ -0,0 +1,3 @@
+import { InjectionToken, TemplateRef } from "@angular/core";
+
+export const TMPL_INJ_TOKEN = new InjectionToken<TemplateRef<any>>('TMPL_INJ_TOKEN')
\ No newline at end of file
diff --git a/src/spotlight/sl-service.service.ts b/src/spotlight/sl-service.service.ts
index 241fe068cb335726119d8532d31d3a06ff6351f1..f45aad652b3b60aca9a117e15798a4836df0c86f 100644
--- a/src/spotlight/sl-service.service.ts
+++ b/src/spotlight/sl-service.service.ts
@@ -1,44 +1,57 @@
-import { Injectable, OnDestroy, ComponentFactoryResolver, Injector, ComponentRef, ApplicationRef, EmbeddedViewRef, TemplateRef, ComponentFactory } from '@angular/core';
-import './sl-style.css'
-import { SpotlightBackdropComponent } from './spotlight-backdrop/spotlight-backdrop.component';
+import { Injectable, Injector, OnDestroy, TemplateRef } from '@angular/core';
 import { Subject } from 'rxjs';
+import { Overlay, OverlayRef } from '@angular/cdk/overlay';
+import { ComponentPortal } from '@angular/cdk/portal';
+import { SpotlightBackdropComponent } from './spotlight-backdrop/spotlight-backdrop.component';
+import { TMPL_INJ_TOKEN } from './const';
 
 @Injectable({
   providedIn: 'root'
 })
 export class SlServiceService implements OnDestroy{
 
-  private backdropRef: ComponentRef<SpotlightBackdropComponent>
-  private dom: HTMLElement
-  private cf: ComponentFactory<SpotlightBackdropComponent>
   onClick: Subject<MouseEvent> = new Subject()
-  
+  private overlayRef: OverlayRef
+
   constructor(
-    cfr: ComponentFactoryResolver,
+    private overlay: Overlay,
     private injector: Injector,
-    private appRef: ApplicationRef
   ) {
-    this.cf = cfr.resolveComponentFactory(SpotlightBackdropComponent)
   }
 
-  /**
-   * TODO use angular cdk overlay
-   */
-  public showBackdrop(tmp?: TemplateRef<any>){
+  public showBackdrop(tmp: TemplateRef<any>){
     this.hideBackdrop()
 
-    this.backdropRef = this.cf.create(this.injector)
-    this.backdropRef.instance.slService = this
-    this.backdropRef.instance.insert = tmp
+    const positionStrategy = this.overlay.position()
+      .global()
+      .centerHorizontally()
+      .centerVertically()
+    
+    this.overlayRef = this.overlay.create({
+      positionStrategy,
+      hasBackdrop: true,
+    })
 
-    this.appRef.attachView(this.backdropRef.hostView)
-    this.dom = (this.backdropRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement
-    document.body.appendChild(this.dom)
+    const injector = Injector.create({
+      parent: this.injector,
+      providers: [{
+        provide: SlServiceService,
+        useValue: this
+      }, {
+        provide: TMPL_INJ_TOKEN,
+        useValue: tmp
+      }]
+    })
+    const portal = new ComponentPortal(SpotlightBackdropComponent, null, injector)
+    this.overlayRef.attach(portal)
+    
   }
 
   public hideBackdrop(){
-    this.backdropRef && this.appRef.detachView(this.backdropRef.hostView)
-    this.backdropRef && this.backdropRef.destroy()
+    if (this.overlayRef) {
+      this.overlayRef.dispose()
+      this.overlayRef = null
+    }
   }
 
   ngOnDestroy(){
diff --git a/src/spotlight/sl-style.css b/src/spotlight/sl-style.css
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/src/spotlight/spot-light.module.ts b/src/spotlight/spot-light.module.ts
index a485e0d23622a2d4637334154cc6d6c0b9207c97..d7346501769b6080ed30fd570a0061086fe0645a 100644
--- a/src/spotlight/spot-light.module.ts
+++ b/src/spotlight/spot-light.module.ts
@@ -4,16 +4,20 @@ import { SlSpotlightDirective } from './sl-spotlight.directive';
 import { SpotlightBackdropComponent } from './spotlight-backdrop/spotlight-backdrop.component';
 import { SpotLightOverlayDirective } from './spot-light-overlay.directive';
 import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
+import { OverlayModule } from '@angular/cdk/overlay';
+import { PortalModule } from '@angular/cdk/portal';
 
 @NgModule({
   declarations: [
     SlSpotlightDirective,
     SpotlightBackdropComponent,
-    SpotLightOverlayDirective
+    SpotLightOverlayDirective,
   ],
   imports: [
     BrowserAnimationsModule,
-    CommonModule
+    CommonModule,
+    OverlayModule,
+    PortalModule,
   ],
   exports: [
     SlSpotlightDirective
diff --git a/src/spotlight/spotlight-backdrop/spotlight-backdrop.component.ts b/src/spotlight/spotlight-backdrop/spotlight-backdrop.component.ts
index fbf9495a48f4540a456e3b7fb4f9e827f75fae07..7e258082c75b86b473a18a06b3df95276e009681 100644
--- a/src/spotlight/spotlight-backdrop/spotlight-backdrop.component.ts
+++ b/src/spotlight/spotlight-backdrop/spotlight-backdrop.component.ts
@@ -1,6 +1,7 @@
-import { Component, HostListener, TemplateRef, HostBinding } from '@angular/core';
+import { Component, HostListener, TemplateRef, HostBinding, Inject } from '@angular/core';
 import { SlServiceService } from '../sl-service.service';
 import { transition, animate, state, style, trigger } from '@angular/animations';
+import { TMPL_INJ_TOKEN } from '../const';
 
 @Component({
   selector: 'sl-spotlight-backdrop',
@@ -25,9 +26,6 @@ import { transition, animate, state, style, trigger } from '@angular/animations'
 })
 export class SpotlightBackdropComponent {
 
-  // TODO use DI for service injection ?
-  public slService: SlServiceService
-
   @HostBinding('@onShownOnDismiss')
   animation: string = 'attach'
 
@@ -36,5 +34,9 @@ export class SpotlightBackdropComponent {
     this.slService && this.slService.onClick.next(ev)
   }
 
-  insert: TemplateRef<any>
+  constructor(
+    private slService: SlServiceService,
+    @Inject(TMPL_INJ_TOKEN) public insert: TemplateRef<any>,
+  ){
+  }
 }
diff --git a/src/state/atlasAppearance/const.ts b/src/state/atlasAppearance/const.ts
index 41c9ebd7216465719aab792025b8ad2b5b268f6d..9804bc43091e3350d89ab2f0d028ba889f15d738 100644
--- a/src/state/atlasAppearance/const.ts
+++ b/src/state/atlasAppearance/const.ts
@@ -31,7 +31,7 @@ export type NgLayerCustomLayer = {
   transform?: number[][]
   opacity?: number
   segments?: (number|string)[]
-  // type?: string
+  type?: string
 
   // annotation?: string // TODO what is this used for?
 } & CustomLayerBase
diff --git a/src/state/atlasSelection/effects.spec.ts b/src/state/atlasSelection/effects.spec.ts
index fe99266715ac18a342335b5b46591f29c2140444..9ba912441b896aaf519a14893496e1db2beb9a27 100644
--- a/src/state/atlasSelection/effects.spec.ts
+++ b/src/state/atlasSelection/effects.spec.ts
@@ -158,13 +158,13 @@ describe("> effects.ts", () => {
                 },
                 previous: {
                   atlas: {
-                    "@id": IDS.ATLAES.RAT
+                    id: IDS.ATLAES.RAT
                   } as any,
                   parcellation: {
-                    "@id": IDS.PARCELLATION.WAXHOLMV4
+                    id: IDS.PARCELLATION.WAXHOLMV4
                   } as any,
                   template: {
-                    "@id": IDS.TEMPLATES.WAXHOLM
+                    id: IDS.TEMPLATES.WAXHOLM
                   } as any,
                 }
               })
@@ -186,24 +186,24 @@ describe("> effects.ts", () => {
               const obs = hook({
                 current: {
                   atlas: {
-                    "@id": IDS.ATLAES.HUMAN
+                    id: IDS.ATLAES.HUMAN
                   } as any,
                   parcellation: {
-                    "@id": IDS.PARCELLATION.JBA29
+                    id: IDS.PARCELLATION.JBA29
                   } as any,
                   template: {
-                    "@id": IDS.TEMPLATES.MNI152
+                    id: IDS.TEMPLATES.MNI152
                   } as any,
                 },
                 previous: {
                   atlas: {
-                    "@id": IDS.ATLAES.RAT
+                    id: IDS.ATLAES.RAT
                   } as any,
                   parcellation: {
-                    "@id": IDS.PARCELLATION.WAXHOLMV4
+                    id: IDS.PARCELLATION.WAXHOLMV4
                   } as any,
                   template: {
-                    "@id": IDS.TEMPLATES.WAXHOLM
+                    id: IDS.TEMPLATES.WAXHOLM
                   } as any,
                 }
               })
diff --git a/src/state/atlasSelection/effects.ts b/src/state/atlasSelection/effects.ts
index 816c23c2439b953c511d15bc6168348b1a00c2db..5967e510255e45ba3eb1c980d7cf65a53018c429 100644
--- a/src/state/atlasSelection/effects.ts
+++ b/src/state/atlasSelection/effects.ts
@@ -63,19 +63,17 @@ export class Effect {
         })
       }
 
-      /**
-       * if either space name is undefined, return default state for navigation
-       */
-      if (!prevSpcName || !currSpcName) {
-        return of({
-          navigation: atlasSelection.defaultState.navigation
-        })
-      }
       return this.store.pipe(
         select(atlasSelection.selectors.navigation),
         take(1),
         switchMap(({ position, ...rest }) => 
-          this.interSpaceCoordXformSvc.transform(prevSpcName, currSpcName, position as [number, number, number]).pipe(
+        
+          /**
+           * if either space name is undefined, return default state for navigation
+           */
+          !prevSpcName || !currSpcName
+          ? of({ navigation: { position, ...rest } })
+          : this.interSpaceCoordXformSvc.transform(prevSpcName, currSpcName, position as [number, number, number]).pipe(
             map(value => {
               if (value.status === "error") {
                 return {}
diff --git a/src/util/constants.ts b/src/util/constants.ts
index 001b1283945568cc84378cc55b4f06a41f02f866..763bc0695829eec89caa1c15c41d86be61d720ad 100644
--- a/src/util/constants.ts
+++ b/src/util/constants.ts
@@ -126,14 +126,21 @@ export const UNSUPPORTED_PREVIEW = [{
 
 export const UNSUPPORTED_INTERVAL = 7000
 
+export const SPECIES_ENUM = {
+  HOMO_SAPIENS: "Homo sapiens",
+  MACACA_FASCICULARIS: "Macaca fascicularis",
+  RATTUS_NORVEGICUS: "Rattus norvegicus",
+  MUS_MUSCULUS: "Mus musculus",
+} as const
+
 /**
  * atlas should follow the following order
  */
 export const speciesOrder = [
-  "Homo sapiens",
-  "Macaca fascicularis",
-  "Rattus norvegicus",
-  "Mus musculus"
+  SPECIES_ENUM.HOMO_SAPIENS,
+  SPECIES_ENUM.MACACA_FASCICULARIS,
+  SPECIES_ENUM.RATTUS_NORVEGICUS,
+  SPECIES_ENUM.MUS_MUSCULUS,
 ]
 
 export const parcBanList: string[] = [
diff --git a/src/util/fn.ts b/src/util/fn.ts
index 6457ccf78a80061da34734ae0c3b2e20b0ffe238..a859bad0898788e5c06c1e8292ddf2c66cafb452 100644
--- a/src/util/fn.ts
+++ b/src/util/fn.ts
@@ -1,18 +1,6 @@
 import { interval, Observable, of } from 'rxjs'
 import { filter, mapTo, take } from 'rxjs/operators'
 
-export function getViewer() {
-  return (window as any).viewer
-}
-
-export function setViewer(viewer) {
-  (window as any).viewer = viewer
-}
-
-export function setNehubaViewer(nehubaViewer) {
-  (window as any).nehubaViewer = nehubaViewer
-}
-
 export function getDebug() {
   return (window as any).__DEBUG__
 }
diff --git a/src/util/periodic.service.ts b/src/util/periodic.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d32637ced9f44e3db18dd5a4de9d80767cac5b64
--- /dev/null
+++ b/src/util/periodic.service.ts
@@ -0,0 +1,21 @@
+import { Injectable } from "@angular/core";
+import { wait } from "./fn";
+
+@Injectable({
+  providedIn: 'root'
+})
+export class PeriodicSvc{
+  /**
+   * @description retry a callback until it succeeds
+   * @param callback 
+   */
+  async addToQueue(callback: () => boolean) {
+    // eslint-disable-next-line no-constant-condition
+    while (true) {
+      if (callback()) {
+        break
+      }
+      await wait(160)
+    }
+  }
+}
diff --git a/src/viewerModule/nehuba/config.service/util.ts b/src/viewerModule/nehuba/config.service/util.ts
index ddc82c6482332f12aeb7adef4013e51bb442217f..2ef5a1cc94d1602c6106ac34e17b983a766e33d1 100644
--- a/src/viewerModule/nehuba/config.service/util.ts
+++ b/src/viewerModule/nehuba/config.service/util.ts
@@ -8,6 +8,7 @@ import {
   RecursivePartial,
 } from "./type"
 import { translateV3Entities } from "src/atlasComponents/sapi/translateV3"
+import { PERSPECTIVE_ZOOM_FUDGE_FACTOR } from "../constants"
 // fsaverage uses threesurfer, which, whilst do not use ngId, uses 'left' and 'right' as keys 
 const fsAverageKeyVal = {
   [IDS.PARCELLATION.JBA29]: {
@@ -374,8 +375,8 @@ export function getNehubaConfig(space: SxplrTemplate): NehubaConfig {
         "drawSubstrates": drawSubstrates,
         "drawZoomLevels": drawZoomLevels,
         "restrictZoomLevel": {
-          "minZoom": 1200000 * scale,
-          "maxZoom": 3500000 * scale
+          "minZoom": 1200000 * scale * PERSPECTIVE_ZOOM_FUDGE_FACTOR,
+          "maxZoom": 3500000 * scale * PERSPECTIVE_ZOOM_FUDGE_FACTOR
         }
       }
     }
diff --git a/src/viewerModule/nehuba/constants.ts b/src/viewerModule/nehuba/constants.ts
index 667c5b7147246a276e3a02f59cd436851db83932..d35b8393c26bbb08bbfccf2b67561200b778d006 100644
--- a/src/viewerModule/nehuba/constants.ts
+++ b/src/viewerModule/nehuba/constants.ts
@@ -35,3 +35,10 @@ export type TNehubaViewerUnit = {
 export const SET_MESHES_TO_LOAD = new InjectionToken<Observable<IMeshesToLoad>>('SET_MESHES_TO_LOAD')
 
 export const PMAP_LAYER_NAME = 'regional-pmap'
+
+/**
+ * since export_nehuba@0.1.0 onwards (the big update that changed a lot of neuroglancer's internals)
+ * there is now a multiplier bewteen old and new perspective views
+ * to maintain interop with previous states, translate the multiplier
+ */
+export const PERSPECTIVE_ZOOM_FUDGE_FACTOR = 82.842712474619
diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts
index b8e5e4b26a8f59e6f0dce71eaf8ee227d6e46e80..a37e9608e7b8b96c48673a3d3043ecfb99688997 100644
--- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts
+++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts
@@ -75,7 +75,8 @@ export class LayerCtrlEffects {
                       highThreshold: meta.max,
                       lowThreshold: meta.min,
                       removeBg: true,
-                    })
+                    }),
+                    type: 'image'
                   }
                 })
               )
diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts
index 18b8805b2eb9f6000918879eeb557fe307bffe69..ed281a389241e4a8fbaec5c004557601f2af1aa2 100644
--- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts
+++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts
@@ -5,6 +5,7 @@ import { LoggingModule, LoggingService } from "src/logging"
 import { IMeshesToLoad, SET_MESHES_TO_LOAD } from "../constants"
 import { Subject } from "rxjs"
 import { IColorMap, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl.service"
+import { rgbToHex } from 'common/util'
 
 describe('> nehubaViewer.component.ts', () => {
   describe('> #scanFn', () => {
@@ -305,15 +306,42 @@ describe('> nehubaViewer.component.ts', () => {
 
     describe('> # setColorMap', () => {
       let nehubaViewerSpy: any
+      let ngViewerStatechildrenGetSpy = jasmine.createSpy('get')
+      let toJsonSpy = jasmine.createSpy('toJsonSpy')
+      let restoreStateSpy = jasmine.createSpy('restoreStateSpy')
+
+      const ngId1 = 'foo-bar'
+      const ngId2 = 'hello-world'
       beforeEach(() => {
         nehubaViewerSpy = {
-          batchAddAndUpdateSegmentColors: jasmine.createSpy(),
           dispose(){
 
+          },
+          ngviewer: {
+            state: {
+              children: {
+                get: ngViewerStatechildrenGetSpy
+              }
+            }
           }
         }
+
+        ngViewerStatechildrenGetSpy.and.returnValue({
+          toJSON: toJsonSpy,
+          restoreState: restoreStateSpy,
+        })
+        toJsonSpy.and.returnValue([{
+          name: ngId1
+        }, {
+          name: ngId2
+        }])
       })
-      it('> calls nehubaViewer.batchAddAndUpdateSegmentColors', () => {
+      afterEach(() => {
+        ngViewerStatechildrenGetSpy.calls.reset()
+        toJsonSpy.calls.reset()
+        restoreStateSpy.calls.reset()
+      })
+      it('> calls nehubaViewer.restoreState', () => {
         const fixture = TestBed.createComponent(NehubaViewerUnit)
         fixture.componentInstance.nehubaViewer = nehubaViewerSpy
         fixture.detectChanges()
@@ -322,28 +350,28 @@ describe('> nehubaViewer.component.ts', () => {
         const fooBarMap = new Map()
         fooBarMap.set(1, {red: 100, green: 100, blue: 100})
         fooBarMap.set(2, {red: 200, green: 200, blue: 200})
-        mainMap.set('foo-bar', fooBarMap)
+        mainMap.set(ngId1, fooBarMap)
 
         const helloWorldMap = new Map()
         helloWorldMap.set(1, {red: 10, green: 10, blue: 10})
         helloWorldMap.set(2, {red: 20, green: 20, blue: 20})
-        mainMap.set('hello-world', helloWorldMap)
+        mainMap.set(ngId2, helloWorldMap)
 
         fixture.componentInstance['setColorMap'](mainMap)
 
-        expect(
-          nehubaViewerSpy.batchAddAndUpdateSegmentColors
-        ).toHaveBeenCalledTimes(2)
-
-        expect(nehubaViewerSpy.batchAddAndUpdateSegmentColors).toHaveBeenCalledWith(
-          fooBarMap,
-          { name: 'foo-bar' }
-        )
-
-        expect(nehubaViewerSpy.batchAddAndUpdateSegmentColors).toHaveBeenCalledWith(
-          helloWorldMap,
-          { name: 'hello-world' }
-        )
+        expect(restoreStateSpy).toHaveBeenCalledOnceWith([{
+          name: ngId1,
+          segmentColors: {
+            1: rgbToHex([100, 100, 100]),
+            2: rgbToHex([200, 200, 200]),
+          }
+        }, {
+          name: ngId2,
+          segmentColors: {
+            1: rgbToHex([10, 10, 10]),
+            2: rgbToHex([20, 20, 20]),
+          }
+        }])
       })
     })
 
diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
index e0e25cd158aedca533bbe1b9a336832ff2f82298..017187edb71c29fc59bc0dd589790a6dc3bb4e02 100644
--- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
+++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
@@ -1,17 +1,27 @@
 import { Component, ElementRef, EventEmitter, OnDestroy, Output, Inject, Optional } from "@angular/core";
-import { Subscription, BehaviorSubject, Observable, Subject, of, interval } from 'rxjs'
-import { debounceTime, filter, scan, switchMap, take, distinctUntilChanged, debounce } from "rxjs/operators";
+import { Subscription, BehaviorSubject, Observable, Subject, of, interval, combineLatest } from 'rxjs'
+import { debounceTime, filter, scan, switchMap, take, distinctUntilChanged, debounce, map } from "rxjs/operators";
 import { LoggingService } from "src/logging";
-import { bufferUntil, getExportNehuba, getViewer, setNehubaViewer, switchMapWaitFor } from "src/util/fn";
+import { bufferUntil, getExportNehuba, switchMapWaitFor } from "src/util/fn";
 import { deserializeSegment, NEHUBA_INSTANCE_INJTKN } from "../util";
-import { arrayOrderedEql } from 'common/util'
-import { IMeshesToLoad, SET_MESHES_TO_LOAD } from "../constants";
+import { arrayOrderedEql, rgbToHex } from 'common/util'
+import { IMeshesToLoad, SET_MESHES_TO_LOAD, PERSPECTIVE_ZOOM_FUDGE_FACTOR } from "../constants";
 import { IColorMap, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl.service";
 
 /**
  * import of nehuba js files moved to angular.json
  */
 import { INgLayerCtrl, NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY, TNgLayerCtrl } from "../layerCtrl.service/layerCtrl.util";
+import { NgCoordinateSpace, Unit } from "../types";
+import { PeriodicSvc } from "src/util/periodic.service";
+
+function translateUnit(unit: Unit) {
+  if (unit === "m") {
+    return 1e9
+  }
+
+  throw new Error(`Cannot translate unit: ${unit}`)
+}
 
 export const IMPORT_NEHUBA_INJECT_TOKEN = `IMPORT_NEHUBA_INJECT_TOKEN`
 
@@ -49,10 +59,11 @@ export const scanFn = (acc: LayerLabelIndex[], curr: LayerLabelIndex) => {
 
 export class NehubaViewerUnit implements OnDestroy {
 
+  #translateVoxelToReal: (voxels: number[]) => number[]
 
   public ngIdSegmentsMap: Record<string, number[]> = {}
 
-  public viewerPosInVoxel$ = new BehaviorSubject(null)
+  public viewerPosInVoxel$ = new BehaviorSubject<number[]>(null)
   public viewerPosInReal$ = new BehaviorSubject<[number, number, number]>(null)
   public mousePosInVoxel$ = new BehaviorSubject<[number, number, number]>(null)
   public mousePosInReal$ = new BehaviorSubject(null)
@@ -97,33 +108,18 @@ export class NehubaViewerUnit implements OnDestroy {
       : [1.5e9, 1.5e9, 1.5e9]
   }
 
-  public _s2$: any = null
-  public _s3$: any = null
-  public _s4$: any = null
-  public _s5$: any = null
-  public _s6$: any = null
-  public _s7$: any = null
-  public _s8$: any = null
-
-  public _s$: any[] = [
-    this._s2$,
-    this._s3$,
-    this._s4$,
-    this._s5$,
-    this._s6$,
-    this._s7$,
-    this._s8$,
-  ]
+  #newViewerSubs: { unsubscribe: () => void }[] = []
 
   public ondestroySubscriptions: Subscription[] = []
 
   public nehubaLoaded: boolean = false
 
-  public landmarksLoaded: boolean = false
+  #triggerMeshLoad$ = new BehaviorSubject(null)
 
   constructor(
     public elementRef: ElementRef,
     private log: LoggingService,
+    private periodicSvc: PeriodicSvc,
     @Inject(IMPORT_NEHUBA_INJECT_TOKEN) getImportNehubaPr: () => Promise<any>,
     @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) private nehubaViewer$: Subject<NehubaViewerUnit>,
     @Optional() @Inject(SET_MESHES_TO_LOAD) private injSetMeshesToLoad$: Observable<IMeshesToLoad>,
@@ -152,9 +148,10 @@ export class NehubaViewerUnit implements OnDestroy {
         this.loadNehuba()
 
         const viewer = this.nehubaViewer.ngviewer
-        this.layersChangedHandler = viewer.layerManager.layersChanged.add(() => {
+
+        this.layersChangedHandler = viewer.layerManager.readyStateChanged.add(() => {
           this.layersChanged.emit(null)
-          const readiedLayerNames: string[] = viewer.layerManager.managedLayers.filter(l => l.layer).map(l => l.name)
+          const readiedLayerNames: string[] = viewer.layerManager.managedLayers.filter(l => l.isReady()).map(l => l.name)
           for (const layerName in this.ngIdSegmentsMap) {
             if (!readiedLayerNames.includes(layerName)) {
               return
@@ -291,9 +288,13 @@ export class NehubaViewerUnit implements OnDestroy {
 
     if (this.injSetMeshesToLoad$) {
       this.subscriptions.push(
-        this.injSetMeshesToLoad$.pipe(
-          scan(scanFn, []),
-          debounceTime(16),
+        combineLatest([
+          this.#triggerMeshLoad$,
+          this.injSetMeshesToLoad$.pipe(
+            scan(scanFn, []),
+          ),
+        ]).pipe(
+          map(([_, val]) => val),
           debounce(() => this._nehubaReady
             ? of(true)
             : interval(160).pipe(
@@ -325,14 +326,6 @@ export class NehubaViewerUnit implements OnDestroy {
     }
   }
 
-  public navPosReal: [number, number, number] = [0, 0, 0]
-  public navPosVoxel: [number, number, number] = [0, 0, 0]
-
-  public mousePosReal: [number, number, number] = [0, 0, 0]
-  public mousePosVoxel: [number, number, number] = [0, 0, 0]
-
-  public viewerState: ViewerState
-
   private _multiNgIdColorMap: Map<string, Map<number, {red: number, green: number, blue: number}>>
   get multiNgIdColorMap() {
     return this._multiNgIdColorMap
@@ -353,7 +346,9 @@ export class NehubaViewerUnit implements OnDestroy {
     this.nehubaViewer = this.exportNehuba.createNehubaViewer(this.config, (err: string) => {
       /* print in debug mode */
       this.log.error(err)
-    })
+    });
+
+    (window as any).nehubaViewer = this.nehubaViewer
 
     /**
      * Hide all layers except the base layer (template)
@@ -362,15 +357,15 @@ export class NehubaViewerUnit implements OnDestroy {
 
     /* creation of the layout is done on next frame, hence the settimeout */
     setTimeout(() => {
-      getViewer().display.panels.forEach(patchSliceViewPanel)
+      window['viewer'].display.panels.forEach(patchSliceViewPanel)
     })
 
     this.newViewerInit()
-    this.loadNewParcellation()
-
-    setNehubaViewer(this.nehubaViewer)
+    window['nehubaViewer'] = this.nehubaViewer
 
-    this.onDestroyCb.push(() => setNehubaViewer(null))
+    this.onDestroyCb.push(() => {
+      window['nehubaViewer'] = null
+    })
   }
 
   public ngOnDestroy() {
@@ -380,10 +375,10 @@ export class NehubaViewerUnit implements OnDestroy {
     while (this.subscriptions.length > 0) {
       this.subscriptions.pop().unsubscribe()
     }
-
-    this._s$.forEach(_s$ => {
-      if (_s$) { _s$.unsubscribe() }
-    })
+    while (this.#newViewerSubs.length > 0) {
+      this.#newViewerSubs.pop().unsubscribe()
+    }
+    
     this.ondestroySubscriptions.forEach(s => s.unsubscribe())
     while (this.onDestroyCb.length > 0) {
       this.onDestroyCb.pop()()
@@ -482,8 +477,37 @@ export class NehubaViewerUnit implements OnDestroy {
         /* if the layer exists, it will not be loaded */
         !viewer.layerManager.getLayerByName(key))
       .map(key => {
+        /**
+         * new implementation of neuroglancer treats swc as a mesh layer of segmentation layer
+         * But it cannot *directly* be accessed by nehuba's setMeshesToLoad, since it filters by 
+         * UserSegmentationLayer.
+         * 
+         * The below monkey patch sets the mesh to load, allow the SWC to be shown
+         */
+        const isSwc = layerObj[key]['source'].includes("swc://")
+        const hasSegment = (layerObj[key]["segments"] || []).length > 0
+        if (isSwc && hasSegment) {
+          this.periodicSvc.addToQueue(
+            () => {
+              const layer = viewer.layerManager.getLayerByName(key)
+              if (!(layer?.layer)) {
+                return false
+              }
+              layer.layer.displayState.visibleSegments.setMeshesToLoad([1])
+              return true
+            }
+          )
+        }
+        const { transform=null, ...rest } = layerObj[key]
+
+        const combined = {
+          type: 'image',
+          ...rest,
+          ...(transform ? { transform } : {})
+        }
+        console.log(combined)
         viewer.layerManager.addManagedLayer(
-          viewer.layerSpecification.getLayer(key, layerObj[key]))
+          viewer.layerSpecification.getLayer(key, combined))
 
         return layerObj[key]
       })
@@ -509,7 +533,6 @@ export class NehubaViewerUnit implements OnDestroy {
           name: ngId,
         })
       }
-
       this.nehubaViewer.showSegment(0, {
         name: ngId,
       })
@@ -524,7 +547,6 @@ export class NehubaViewerUnit implements OnDestroy {
           name: ngId,
         })
       }
-
       this.nehubaViewer.hideSegment(0, {
         name: ngId,
       })
@@ -604,7 +626,7 @@ export class NehubaViewerUnit implements OnDestroy {
     } = newViewerState || {}
 
     if ( perspectiveZoom ) {
-      this.nehubaViewer.ngviewer.perspectiveNavigationState.zoomFactor.restoreState(perspectiveZoom)
+      this.nehubaViewer.ngviewer.perspectiveNavigationState.zoomFactor.restoreState(perspectiveZoom * PERSPECTIVE_ZOOM_FUDGE_FACTOR)
     }
     if ( zoom ) {
       this.nehubaViewer.ngviewer.navigationState.zoomFactor.restoreState(zoom)
@@ -620,18 +642,6 @@ export class NehubaViewerUnit implements OnDestroy {
     }
   }
 
-  public obliqueRotateX(amount: number) {
-    this.nehubaViewer.ngviewer.navigationState.pose.rotateRelative(this.vec3([0, 1, 0]), -amount / 4.0 * Math.PI / 180.0)
-  }
-
-  public obliqueRotateY(amount: number) {
-    this.nehubaViewer.ngviewer.navigationState.pose.rotateRelative(this.vec3([1, 0, 0]), amount / 4.0 * Math.PI / 180.0)
-  }
-
-  public obliqueRotateZ(amount: number) {
-    this.nehubaViewer.ngviewer.navigationState.pose.rotateRelative(this.vec3([0, 0, 1]), amount / 4.0 * Math.PI / 180.0)
-  }
-
   public toggleOctantRemoval(flag?: boolean) {
     const ctrl = this.nehubaViewer?.ngviewer?.showPerspectiveSliceViews
     if (!ctrl) {
@@ -642,13 +652,6 @@ export class NehubaViewerUnit implements OnDestroy {
       ? !ctrl.value
       : flag
     ctrl.restoreState(newVal)
-
-    if (this.landmarksLoaded) {
-      /**
-       * showPerspectSliceView -> ! meshTransparency
-       */
-      this.setMeshTransparency(!newVal)
-    }
   }
 
   private setLayerTransparency(layerName: string, alpha: number) {
@@ -688,29 +691,29 @@ export class NehubaViewerUnit implements OnDestroy {
   }
 
   private newViewerInit() {
+    
+    while (this.#newViewerSubs.length > 0) {
+      this.#newViewerSubs.pop().unsubscribe()
+    }
+
+    this.#newViewerSubs.push(
 
-    /* isn't this layer specific? */
-    /* TODO this is layer specific. need a way to distinguish between different segmentation layers */
-    this._s2$ = this.nehubaViewer.mouseOver.segment
-      .subscribe(({ segment, layer }) => {
+      /* isn't this layer specific? */
+      /* TODO this is layer specific. need a way to distinguish between different segmentation layers */
+      this.nehubaViewer.mouseOver.segment.subscribe(({ segment, layer }) => {
         this.mouseOverSegment = segment
         this.mouseOverLayer = { ...layer }
-      })
-
-    if (this.initNav) {
-      this.setNavigationState(this.initNav)
-      this.initNav = null
-    }
+      }),
 
-    this._s8$ = this.nehubaViewer.mouseOver.segment.subscribe(({segment: segmentId, layer }) => {
-      this.mouseoverSegmentEmitter.emit({
-        layer,
-        segmentId,
-      })
-    })
+      this.nehubaViewer.mouseOver.segment.subscribe(({segment: segmentId, layer }) => {
+        this.mouseoverSegmentEmitter.emit({
+          layer,
+          segmentId,
+        })
+      }),
 
-    // nehubaViewer.navigationState.all emits every time a new layer is added or removed from the viewer
-    this._s3$ = this.nehubaViewer.navigationState.all
+      // nehubaViewer.navigationState.all emits every time a new layer is added or removed from the viewer
+      this.nehubaViewer.navigationState.all
       .distinctUntilChanged((a, b) => {
         const {
           orientation: o1,
@@ -733,71 +736,99 @@ export class NehubaViewerUnit implements OnDestroy {
           [0, 1, 2].every(idx => p1[idx] === p2[idx]) &&
           z1 === z2
       })
-      .filter(() => !this.initNav)
+      /**
+       * somewhat another fudge factor
+       * navigationState.all occassionally emits slice zoom and perspective zoom that maeks no sense
+       * filter those out
+       * 
+       * TODO find out why, and perhaps inform pavel about this
+       */
+      .filter(val => !this.initNav && val?.perspectiveZoom > 10)
       .subscribe(({ orientation, perspectiveOrientation, perspectiveZoom, position, zoom }) => {
-        this.viewerState = {
-          orientation,
-          perspectiveOrientation,
-          perspectiveZoom,
-          zoom,
-          position,
-          positionReal : false,
-        }
 
         this.viewerPositionChange.emit({
           orientation : Array.from(orientation),
           perspectiveOrientation : Array.from(perspectiveOrientation),
-          perspectiveZoom,
+          perspectiveZoom: perspectiveZoom / PERSPECTIVE_ZOOM_FUDGE_FACTOR,
           zoom,
           position: Array.from(position),
           positionReal : true,
         })
-      })
+      }),
+
+      this.nehubaViewer.navigationState.position.inVoxels
+        .filter(v => typeof v !== 'undefined' && v !== null)
+        .subscribe((v: Float32Array) => {
+          const coordInVoxel = Array.from(v)
+          this.viewerPosInVoxel$.next(coordInVoxel)
+          if (this.#translateVoxelToReal) {
+            
+            const coordInReal = this.#translateVoxelToReal(coordInVoxel)
+            this.viewerPosInReal$.next(coordInReal as [number, number, number])
+          }
+        }),
 
-    this._s4$ = this.nehubaViewer.navigationState.position.inRealSpace
-      .filter(v => typeof v !== 'undefined' && v !== null)
-      .subscribe(v => {
-        this.navPosReal = Array.from(v) as [number, number, number]
-        this.viewerPosInReal$.next(Array.from(v) as [number, number, number])
-      })
-    this._s5$ = this.nehubaViewer.navigationState.position.inVoxels
-      .filter(v => typeof v !== 'undefined' && v !== null)
-      .subscribe(v => {
-        this.navPosVoxel = Array.from(v) as [number, number, number]
-        this.viewerPosInVoxel$.next(Array.from(v))
-      })
-    this._s6$ = this.nehubaViewer.mousePosition.inRealSpace
-      .filter(v => typeof v !== 'undefined' && v !== null)
-      .subscribe(v => {
-        this.mousePosReal = Array.from(v) as [number, number, number]
-        this.mousePosInReal$.next(Array.from(v))
-      })
-    this._s7$ = this.nehubaViewer.mousePosition.inVoxels
-      .filter(v => typeof v !== 'undefined' && v !== null)
-      .subscribe(v => {
-        this.mousePosVoxel = Array.from(v) as [number, number, number]
-        this.mousePosInVoxel$.next(Array.from(v) as [number, number, number] )
-      })
-  }
+      this.nehubaViewer.mousePosition.inVoxels
+        .filter((v: Float32Array) => typeof v !== 'undefined' && v !== null)
+        .subscribe((v: Float32Array) => {
+          const coordInVoxel = Array.from(v) as [number, number, number]
+          this.mousePosInVoxel$.next( coordInVoxel )
+          if (this.#translateVoxelToReal) {
+            
+            const coordInReal = this.#translateVoxelToReal(coordInVoxel)
+            this.mousePosInReal$.next( coordInReal )
+          }
+        }),
 
-  private loadNewParcellation() {
+    )
 
-    this._s$.forEach(_s$ => {
-      if (_s$) { _s$.unsubscribe() }
+    const coordSpListener = this.nehubaViewer.ngviewer.coordinateSpace.changed.add(() => {
+      const coordSp = this.nehubaViewer.ngviewer.coordinateSpace.value as NgCoordinateSpace
+      if (coordSp.valid) {
+        this.#translateVoxelToReal = (coordInVoxel: number[]) => {
+          return coordInVoxel.map((voxel, idx) => (
+            translateUnit(coordSp.units[idx])
+            * coordSp.scales[idx]
+            * voxel
+          ))
+        }
+      }
     })
+    this.nehubaViewer.ngviewer.registerDisposer(coordSpListener)
+
+    if (this.initNav) {
+      this.setNavigationState(this.initNav)
+      this.initNav = null
+    }
+    
   }
 
   private setColorMap(map: Map<string, Map<number, {red: number, green: number, blue: number}>>) {
     this.multiNgIdColorMap = map
+    const mainDict: Record<string, Record<number, string>> = {}
     for (const [ ngId, cMap ] of map.entries()) {
-      const nMap = new Map()
+      const nRecord: Record<number, string> = {}
       for (const [ key, cm ] of cMap.entries()) {
-        nMap.set(Number(key), cm)
+        nRecord[key] = rgbToHex([cm.red, cm.green, cm.blue])
+      }
+      mainDict[ngId] = nRecord
+
+      /**
+       * n.b.
+       * cannot restoreState on each individual layer
+       * it seems to create duplicated datasources, which eats memory, and wrecks opacity
+       */
+    }
+
+    const layersManager = this.nehubaViewer.ngviewer.state.children.get("layers")
+    const layerJson = layersManager.toJSON()
+    for (const layer of layerJson) {
+      if (layer.name in mainDict) {
+        layer['segmentColors'] = mainDict[layer.name]
       }
-      this.nehubaViewer.batchAddAndUpdateSegmentColors(
-        nMap,
-        { name : ngId })
     }
+    layersManager.restoreState(layerJson)
+    this.#triggerMeshLoad$.next(null)
   }
 }
 
@@ -828,51 +859,4 @@ export interface ViewerState {
   zoom: number
 }
 
-export const ICOSAHEDRON = `# vtk DataFile Version 2.0
-Converted using https://github.com/HumanBrainProject/neuroglancer-scripts
-ASCII
-DATASET POLYDATA
-POINTS 12 float
--525731.0 0.0 850651.0
-525731.0 0.0 850651.0
--525731.0 0.0 -850651.0
-525731.0 0.0 -850651.0
-0.0 850651.0 525731.0
-0.0 850651.0 -525731.0
-0.0 -850651.0 525731.0
-0.0 -850651.0 -525731.0
-850651.0 525731.0 0.0
--850651.0 525731.0 0.0
-850651.0 -525731.0 0.0
--850651.0 -525731.0 0.0
-POLYGONS 20 80
-3 1 4 0
-3 4 9 0
-3 4 5 9
-3 8 5 4
-3 1 8 4
-3 1 10 8
-3 10 3 8
-3 8 3 5
-3 3 2 5
-3 3 7 2
-3 3 10 7
-3 10 6 7
-3 6 11 7
-3 6 0 11
-3 6 1 0
-3 10 1 6
-3 11 0 9
-3 2 11 9
-3 5 2 9
-3 11 2 7`
-
-declare const TextEncoder
-
-export const _encoder = new TextEncoder()
-export const ICOSAHEDRON_VTK_URL = URL.createObjectURL( new Blob([ _encoder.encode(ICOSAHEDRON) ], {type : 'application/octet-stream'} ))
-
-export const FRAGMENT_MAIN_WHITE = `void main(){emitRGB(vec3(1.0,1.0,1.0));}`
-export const FRAGMENT_EMIT_WHITE = `emitRGB(vec3(1.0, 1.0, 1.0));`
-export const FRAGMENT_EMIT_RED = `emitRGB(vec3(1.0, 0.1, 0.12));`
 export const computeDistance = (pt1: [number, number], pt2: [number, number]) => ((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2) ** 0.5
diff --git a/src/viewerModule/nehuba/types.ts b/src/viewerModule/nehuba/types.ts
index c7684e637dd9a156c722db3ca806b7f381171bab..aaa7016009cf34950b64b07bba5d40be49a8953e 100644
--- a/src/viewerModule/nehuba/types.ts
+++ b/src/viewerModule/nehuba/types.ts
@@ -13,3 +13,25 @@ export type TNehubaContextInfo = {
     regions: SxplrRegion[]
   }[]
 }
+
+export type Unit = 'm'
+type Bound = {
+  lowerBounds: Float64Array
+  upperBounds: Float64Array
+}
+type BBox = {
+  transform: Float64Array
+  box: Bound
+}
+
+export type NgCoordinateSpace = {
+  valid: boolean
+  rank: number
+  names: string[]
+  timestamps: number[]
+  ids: number[]
+  units: Unit[]
+  scales: Float64Array
+  boundingBoxes:BBox[]
+  bounds: Bound
+}
diff --git a/src/viewerModule/nehuba/userLayers/module.ts b/src/viewerModule/nehuba/userLayers/module.ts
index 6de5343d80b664d3c7949eccc231829b644f59f5..ae8068454f9ba777fd59132ae5be62afe2f8f255 100644
--- a/src/viewerModule/nehuba/userLayers/module.ts
+++ b/src/viewerModule/nehuba/userLayers/module.ts
@@ -8,6 +8,8 @@ import { UserLayerService } from "./service"
 import { MatButtonModule } from "@angular/material/button"
 import { MatTooltipModule } from "@angular/material/tooltip"
 import { UserLayerInfoCmp } from "./userlayerInfo/userlayerInfo.component"
+import { UtilModule } from "src/util"
+import { SpinnerModule } from "src/components/spinner"
 
 @NgModule({
   imports: [
@@ -17,6 +19,8 @@ import { UserLayerInfoCmp } from "./userlayerInfo/userlayerInfo.component"
     MatDialogModule,
     MatButtonModule,
     MatTooltipModule,
+    UtilModule,
+    SpinnerModule,
   ],
   declarations: [UserLayerDragDropDirective, UserLayerInfoCmp],
   exports: [UserLayerDragDropDirective],
diff --git a/src/viewerModule/nehuba/userLayers/service.ts b/src/viewerModule/nehuba/userLayers/service.ts
index 3fe9d0a7db656354ed6217ebd13a0fd0354deae4..41d16e71bc3e20aa74f717d11ec8a598d13b3c42 100644
--- a/src/viewerModule/nehuba/userLayers/service.ts
+++ b/src/viewerModule/nehuba/userLayers/service.ts
@@ -77,6 +77,7 @@ export class UserLayerService implements OnDestroy {
         options: {
           segments: ["1"],
           transform: xform,
+          type: "segmentation"
         },
       }
     }
@@ -121,6 +122,7 @@ export class UserLayerService implements OnDestroy {
           lowThreshold: meta.min || 0,
           highThreshold: meta.max || 1,
         }),
+        type: 'image'
       },
     }
   }
diff --git a/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.component.ts b/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.component.ts
index fb6603223551791853fd9825aea84168a4bd48ce..204da49bf5170ca9998ed52f26fc31ad1df05471 100644
--- a/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.component.ts
+++ b/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.component.ts
@@ -1,6 +1,9 @@
-import { Component, Inject } from "@angular/core";
+import { Component, Inject, ViewChild } from "@angular/core";
 import { MAT_DIALOG_DATA } from "@angular/material/dialog";
 import { ARIA_LABELS, CONST } from 'common/constants'
+import { BehaviorSubject, Subject, combineLatest, concat, of, timer } from "rxjs";
+import { map, switchMap, take } from "rxjs/operators";
+import { MediaQueryDirective } from "src/util/directives/mediaQuery.directive";
 
 export type UserLayerInfoData = {
   layerName: string
@@ -21,10 +24,51 @@ export type UserLayerInfoData = {
 export class UserLayerInfoCmp {
   ARIA_LABELS = ARIA_LABELS
   CONST = CONST
+  public HIDE_NG_TUNE_CTRL = {
+    ONLY_SHOW_OPACITY: 'lower_threshold,higher_threshold,brightness,contrast,colormap,hide-threshold-checkbox,hide-zero-value-checkbox'
+  }
+
+  #mediaQuery = new Subject<MediaQueryDirective>()
+
+  @ViewChild(MediaQueryDirective, { read: MediaQueryDirective })
+  set mediaQuery(val: MediaQueryDirective) {
+    this.#mediaQuery.next(val)
+  }
+
   constructor(
     @Inject(MAT_DIALOG_DATA) public data: UserLayerInfoData
   ){
 
   }
-  public showMoreInfo = false
+
+  #showMore = new BehaviorSubject(false)
+
+  view$ = concat(
+    timer(1000).pipe(
+      take(1),
+      map(() => null as { showMore: boolean, compact: boolean })
+    ),
+    combineLatest([
+      this.#showMore,
+      concat(
+        of(null as MediaQueryDirective),
+        this.#mediaQuery,
+      ).pipe(
+        switchMap(mediaQueryD => mediaQueryD
+          ? mediaQueryD.mediaBreakPoint$.pipe(
+            map(val => val >= 2)
+          )
+          : of(false))
+      )
+    ]).pipe(
+      map(([ showMore, compact ]) => ({
+        showMore,
+        compact,
+      }))
+    )
+  )
+
+  toggleShowMore(){
+    this.#showMore.next(!this.#showMore.value)
+  }
 }
diff --git a/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.style.css b/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.style.css
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e8ba5734cbfb8ef8fd69eba5bd1cdc98907f848a 100644
--- a/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.style.css
+++ b/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.style.css
@@ -0,0 +1,11 @@
+.spinner
+{
+    justify-self: center;
+    display: inline-block;
+    margin: 1rem;
+}
+
+:has(> .spinner)
+{
+    display: grid;
+}
diff --git a/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.template.html b/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.template.html
index 0f878b732221bfd13c24feb573e51d9013fea03a..c999b1c1befd8d650096bf38c6b23a8aadf63192 100644
--- a/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.template.html
+++ b/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.template.html
@@ -1,37 +1,62 @@
-<div class="grid grid-col-3">
+<!-- TODO replace with hostdirective after upgrading to angular 15 -->
+<div iav-media-query></div>
 
-  <span class="ml-2 text-truncate v-center-text-span">
-    <i class="fas fa-file"></i>
-    {{ data.filename }}
-  </span>
-  
-  <button
-    [matTooltip]="ARIA_LABELS.VOLUME_TUNING_EXPAND"
-    mat-icon-button
-    [color]="showMoreInfo ? 'primary' : 'basic'"
-    (click)="showMoreInfo = !showMoreInfo">
-    <i class="fas fa-sliders-h"></i>
-  </button>
+<ng-template [ngIf]="view$ | async" [ngIfElse]="spinnerTmpl" let-view>
 
-  <button
-    [matTooltip]="ARIA_LABELS.CLOSE"
-    color="warn"
-    mat-icon-button
-    mat-dialog-close>
-    <i class="fas fa-trash"></i>
-  </button>
+  <div class="grid grid-col-4">
 
-  <div *ngIf="showMoreInfo"
-    class="sxplr-custom-cmp darker-bg overflow-hidden grid-wide-3">
-    <ng-layer-tune
-      advanced-control="true"
-      [ngLayerName]="data.layerName"
-      [thresholdMin]="data.min"
-      [thresholdMax]="data.max">
-    </ng-layer-tune>
-    <ul>
+    <span class="ml-2 text-truncate v-center-text-span">
+      <i class="fas fa-file"></i>
+      {{ data.filename }} {{ data.filename }}
+    </span>
+  
+    <ng-template [ngIf]="!view.showMore && !view.compact">
+      <ng-template [ngTemplateOutlet]="ngLayerController" [ngTemplateOutletContext]="{ onlyOpacity: true }">
+      </ng-template>
+    </ng-template>
+    
+    <button
+      [matTooltip]="ARIA_LABELS.VOLUME_TUNING_EXPAND"
+      mat-icon-button
+      [color]="view.showMore ? 'primary' : 'basic'"
+      (click)="toggleShowMore()">
+      <i class="fas fa-sliders-h"></i>
+    </button>
+  
+    <button
+      [matTooltip]="ARIA_LABELS.CLOSE"
+      color="warn"
+      mat-icon-button
+      mat-dialog-close>
+      <i class="fas fa-trash"></i>
+    </button>
+  
+    <div *ngIf="view.showMore || view.compact"
+      class="sxplr-custom-cmp darker-bg overflow-hidden grid-wide-3">
+      <ng-template [ngTemplateOutlet]="ngLayerController" [ngTemplateOutletContext]="{ onlyOpacity: !view.showMore }">
+      </ng-template>
+    </div>
+    
+    <ul class="grid-wide-3 sxplr-custom-cmp darker-bg" *ngIf="view.showMore">
       <li *ngFor="let warn of data.warning">{{ warn }}</li>
     </ul>
+  
+  </div>
+</ng-template>
+
+<ng-template #spinnerTmpl>
+  <div>
+    <spinner-cmp class="spinner"></spinner-cmp>
   </div>
+</ng-template>
+
 
-</div>
\ No newline at end of file
+<ng-template #ngLayerController let-onlyOpacity="onlyOpacity">
+  <ng-layer-tune
+    [hideCtrl]="onlyOpacity ? HIDE_NG_TUNE_CTRL.ONLY_SHOW_OPACITY : ''"
+    advanced-control="true"
+    [ngLayerName]="data.layerName"
+    [thresholdMin]="data.min"
+    [thresholdMax]="data.max">
+  </ng-layer-tune>
+</ng-template>
diff --git a/src/viewerModule/nehuba/util.ts b/src/viewerModule/nehuba/util.ts
index c8d56c7e5ba037ab8366e4ca40d0066481146789..0af076efd9c18e061e1e9f539c2911445aae4c77 100644
--- a/src/viewerModule/nehuba/util.ts
+++ b/src/viewerModule/nehuba/util.ts
@@ -1,7 +1,6 @@
 import { InjectionToken } from '@angular/core'
 import { Observable, pipe } from 'rxjs'
 import { filter, scan, take } from 'rxjs/operators'
-import { getViewer } from 'src/util/fn'
 import { NehubaViewerUnit } from './nehubaViewer/nehubaViewer.component'
 import { userInterface } from 'src/state'
 
@@ -202,7 +201,7 @@ export const takeOnePipe = () => {
        *
        * 4 ???
        */
-      const panels = getViewer()['display']['panels']
+      const panels = window['viewer']['display']['panels']
       const panelEls = Array.from(panels).map(({ element }) => element)
 
       const identifySrcElement = (element: HTMLElement) => {
diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts
index 60ba8899d52403374120fdcd20c5460a3afd1734..522d54c51836e83527b2742b529f3f39a946fa26 100644
--- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts
+++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts
@@ -1,8 +1,8 @@
 import { Component, Output, EventEmitter, ElementRef, OnDestroy, AfterViewInit, Inject, Optional, ChangeDetectionStrategy } from "@angular/core";
 import { EnumViewerEvt, IViewer, TViewerEvent } from "src/viewerModule/viewer.interface";
 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 { 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";
 import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util";
 import { MatSnackBar } from "@angular/material/snack-bar";
@@ -27,7 +27,7 @@ type TInternalState = {
   mode: string
   hemisphere: 'left' | 'right' | 'both'
 }
-const pZoomFactor = 5e3
+const pZoomFactor = 7e3
 
 type THandlingCustomEv = {
   regions: SxplrRegion[]
@@ -107,11 +107,15 @@ 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 (!!c1 && !!c2) return true
 
-  if (!c1 && !!c2) return false
-  if (!c2 && !!c1) return false
+  // 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(
@@ -141,9 +145,57 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit
   viewerEvent = new EventEmitter<TViewerEvent<'threeSurfer'>>()
 
   private domEl: HTMLElement
-  private mainStoreCameraNav: TCameraOrientation = null
-  private localCameraNav: TCameraOrientation = null
 
+  #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
+    })
+  )
 
   private internalStateNext: (arg: TInteralStatePayload<TInternalState>) => void
 
@@ -336,9 +388,9 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit
 
   constructor(
     private effect: ThreeSurferEffects,
-    private el: ElementRef,
+    el: ElementRef,
     private store$: Store,
-    private navStateStoreRelay: ComponentStore<{ perspectiveOrientation: [number, number, number, number], perspectiveZoom: number }>,
+    private navStateStoreRelay: ComponentStore<TCameraOrientation>,
     private sapi: SAPI,
     private snackbar: MatSnackBar,
     @Optional() intViewerStateSvc: ViewerInternalStateSvc,
@@ -379,7 +431,7 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit
       const handleClick = (ev: MouseEvent) => {
 
         // if does not click inside container, ignore
-        if (!(this.el.nativeElement as HTMLElement).contains(ev.target as HTMLElement)) {
+        if (!(el.nativeElement as HTMLElement).contains(ev.target as HTMLElement)) {
           return true
         }
         
@@ -404,88 +456,87 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit
       )
     }
     
-    this.domEl = this.el.nativeElement
+    this.domEl = el.nativeElement
 
     /**
      * subscribe to camera custom event
      */
-    const cameraSub = this.#cameraEv$.pipe(
-      filter(v => !!v),
-      debounceTime(160)
-    ).subscribe(() => {
-      
-      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 = this.tsRef.camera.matrix
-      cameraM.decompose(t, q, s)
-      const exchangeFactor = new THREE.Quaternion(-1, 0, 0, 0)
-
+    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: q.multiply(exchangeFactor).toArray(),
-          perspectiveZoom: t.length()
+          perspectiveOrientation: nav.perspectiveOrientation,
+          perspectiveZoom: nav.perspectiveZoom
         })
-      } catch (_e) {
-        // LockError, ignore
+      } catch (e) {
+        if (!(e instanceof LockError)) {
+          throw e
+        }
       }
     })
 
     this.onDestroyCb.push(
-      () => cameraSub.unsubscribe()
+      () => setReconcilState.unsubscribe()
     )
 
     /**
      * subscribe to navstore relay store and negotiate setting global state
      */
-    const navStateSub = this.navStateStoreRelay.select(s => s).subscribe(v => {
-      this.store$.dispatch(
-        atlasSelection.actions.setNavigation({
+    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: v.perspectiveOrientation,
-            perspectiveZoom: v.perspectiveZoom * pZoomFactor
+            perspectiveOrientation: reconcilNav.perspectiveOrientation,
+            perspectiveZoom: reconcilNav.perspectiveZoom
           }
-        })
-      )
-    })
-
-    this.onDestroyCb.push(
-      () => navStateSub.unsubscribe()
-    )
-
-    /**
-     * subscribe to main store and negotiate with relay to set camera
-     */
-    const navSub = this.store$.pipe(
-      select(atlasSelection.selectors.navigation),
-      filter(v => !!v),
-    ).subscribe(nav => {
-      const { perspectiveOrientation, perspectiveZoom } = nav
-      this.mainStoreCameraNav = {
-        perspectiveOrientation,
-        perspectiveZoom
+        }))
       }
 
-      if (!cameraNavsAreSimilar(this.mainStoreCameraNav, this.localCameraNav)) {
-        this.relayStoreLock = this.navStateStoreRelay.getLock()
+      if (!cameraNavsAreSimilar(reconcilNav, internalNav) && reconcilNav) {
         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 / pZoomFactor)
+        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. 
@@ -501,19 +552,18 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit
         cameraPos.applyQuaternion(cameraQuat)
         this.toTsRef(tsRef => {
           tsRef.camera.position.copy(cameraPos)
-          if (this.relayStoreLock) this.relayStoreLock()
         })
       }
     })
 
     this.onDestroyCb.push(
-      () => navSub.unsubscribe()
+      () => reconciliatorSub.unsubscribe()
     )
   }
 
   private tsRef: TThreeSurfer
 
-  private relayStoreLock: () => void = null
+  private releaseRelayLock: () => void = null
   private tsRefInitCb: ((tsRef: any) => void)[] = []
   private toTsRef(callback: (tsRef: any) => void) {
     if (this.tsRef) {
diff --git a/third_party/vanilla.html b/third_party/vanilla.html
index 38206ec40b244a8b3011c6850dc46aa41f047b2f..be223e1e38ab861e649e4c4666e894d0a48c6f8c 100644
--- a/third_party/vanilla.html
+++ b/third_party/vanilla.html
@@ -8,6 +8,7 @@
   
   <script src="main.bundle.js"></script>
   <link rel="stylesheet" href="vanilla_styles.css">
+  <link rel="stylesheet" href="main.css">
 </head>
 <body>
   <div id="neuroglancer-container"></div>
diff --git a/worker/worker.js b/worker/worker.js
index ab8cc83715853cc94fb09e4a2bfce4b5f2597b6a..314e0838032142545197a572ce968eb0320d7ed1 100644
--- a/worker/worker.js
+++ b/worker/worker.js
@@ -11,15 +11,6 @@ if (typeof self.importScripts === 'function')  self.importScripts('./worker-plot
 if (typeof self.importScripts === 'function')  self.importScripts('./worker-nifti.js')
 if (typeof self.importScripts === 'function')  self.importScripts('./worker-typedarray.js')
 
-/**
- * TODO migrate processing functionalities to other scripts
- * see worker-plotly.js
- */
-
-const validTypes = [
-  'GET_USERLANDMARKS_VTK',
-  'PROPAGATE_PARC_REGION_ATTR'
-]
 
 const VALID_METHOD = {
   PROCESS_PLOTLY: `PROCESS_PLOTLY`,
@@ -39,177 +30,10 @@ const VALID_METHODS = [
   VALID_METHOD.PROCESS_TYPED_ARRAY_RAW,
 ]
 
-const validOutType = [
-  'ASSEMBLED_USERLANDMARKS_VTK',
-]
-
-const getVertexHeader = (numVertex) => `POINTS ${numVertex} float`
-
-const getPolyHeader = (numPoly) => `POLYGONS ${numPoly} ${4 * numPoly}`
-
-const getLabelHeader = (numVertex) => `POINT_DATA ${numVertex}
-SCALARS label unsigned_char 1
-LOOKUP_TABLE none`
-
-//pos in nm
-const getIcoVertex = (pos, scale) => `-525731.0 0.0 850651.0
-525731.0 0.0 850651.0
--525731.0 0.0 -850651.0
-525731.0 0.0 -850651.0
-0.0 850651.0 525731.0
-0.0 850651.0 -525731.0
-0.0 -850651.0 525731.0
-0.0 -850651.0 -525731.0
-850651.0 525731.0 0.0
--850651.0 525731.0 0.0
-850651.0 -525731.0 0.0
--850651.0 -525731.0 0.0`
-  .split('\n')
-  .map(line =>
-    line
-      .split(' ')
-      .map((string, idx) => (Number(string) * (scale ? scale : 1) + pos[idx]).toString() )
-      .join(' ')
-    )
-  .join('\n')
-
-
-const getIcoPoly = (startingIdx) => `3 1 4 0
-3 4 9 0
-3 4 5 9
-3 8 5 4
-3 1 8 4
-3 1 10 8
-3 10 3 8
-3 8 3 5
-3 3 2 5
-3 3 7 2
-3 3 10 7
-3 10 6 7
-3 6 11 7
-3 6 0 11
-3 6 1 0
-3 10 1 6
-3 11 0 9
-3 2 11 9
-3 5 2 9
-3 11 2 7`
-  .split('\n')
-  .map((line) =>
-    line
-      .split(' ')
-      .map((v,idx) => idx === 0 ? v : (Number(v) + startingIdx).toString() )
-      .join(' ')
-    )
-  .join('\n')
-
-const getMeshVertex = (vertices) =>  vertices.map(vertex => vertex.join(' ')).join('\n')
-const getMeshPoly = (polyIndices, currentIdx) => polyIndices.map(triplet =>
-  '3 '.concat(triplet.map(index =>
-    index + currentIdx
-  ).join(' '))
-).join('\n')
-
-
 const encoder = new TextEncoder()
 
-const parseLmToVtk = (landmarks, scale) => {
-
-  const reduce = landmarks.reduce((acc,curr,idx) => {
-    //curr : null | [number,number,number] | [ [number,number,number], [number,number,number], [number,number,number] ][]
-    if(curr === null) return acc
-    if(!isNaN(curr[0]))
-      /**
-       * point primitive, render icosahedron
-       */
-      return {
-        currentVertexIndex : acc.currentVertexIndex + 12,
-        vertexString : acc.vertexString.concat(getIcoVertex(curr, scale)),
-        polyCount : acc.polyCount + 20,
-        polyString : acc.polyString.concat(getIcoPoly(acc.currentVertexIndex)),
-        labelString : acc.labelString.concat(Array(12).fill(idx.toString()).join('\n'))
-      }
-    else{
-      //curr[0] : [number,number,number][] vertices
-      //curr[1] : [number,number,number][] indices for the vertices that poly forms
-
-      /**
-       * poly primitive
-       */
-      const vertices = curr[0]
-      const polyIndices = curr[1]
-
-      return {
-        currentVertexIndex : acc.currentVertexIndex + vertices.length,
-        vertexString : acc.vertexString.concat(getMeshVertex(vertices)),
-        polyCount : acc.polyCount + polyIndices.length,
-        polyString : acc.polyString.concat(getMeshPoly(polyIndices, acc.currentVertexIndex)),
-        labelString : acc.labelString.concat(Array(vertices.length).fill(idx.toString()).join('\n'))
-      }
-    }
-  }, {
-    currentVertexIndex : 0,
-    vertexString : [],
-    polyCount : 0,
-    polyString: [],
-    labelString : [],
-  })
-
-  // if no vertices are been rendered, do not replace old
-  if(reduce.currentVertexIndex === 0)
-    return false
-
-  return vtkHeader
-    .concat('\n')
-    .concat(getVertexHeader(reduce.currentVertexIndex))
-    .concat('\n')
-    .concat(reduce.vertexString.join('\n'))
-    .concat('\n')
-    .concat(getPolyHeader(reduce.polyCount))
-    .concat('\n')
-    .concat(reduce.polyString.join('\n'))
-    .concat('\n')
-    .concat(getLabelHeader(reduce.currentVertexIndex))
-    .concat('\n')
-    .concat(reduce.labelString.join('\n'))
-}
-
 let userLandmarkVtkUrl
 
-const getuserLandmarksVtk = (action) => {
-  const landmarks = action.landmarks
-  const scale = action.scale
-    ? action.scale
-    : 2.8
-
-  /**
-   * if userlandmarks vtk is empty, that means user removed all landmarks
-   * thus, removing revoking URL, and send null as assembled userlandmark vtk
-   */
-  if (landmarks.length === 0) {
-
-    if(userLandmarkVtkUrl) URL.revokeObjectURL(userLandmarkVtkUrl)
-
-    postMessage({
-      type: 'ASSEMBLED_USERLANDMARKS_VTK'
-    })
-
-    return
-  }
-
-  const vtk = parseLmToVtk(landmarks, scale)
-  if(!vtk) return
-
-  if(userLandmarkVtkUrl)
-    URL.revokeObjectURL(userLandmarkVtkUrl)
-
-  userLandmarkVtkUrl = URL.createObjectURL(new Blob( [encoder.encode(vtk)], {type : 'application/octet-stream'} ))
-  postMessage({
-    type : 'ASSEMBLED_USERLANDMARKS_VTK',
-    url : userLandmarkVtkUrl
-  })
-}
-
 let plotyVtkUrl
 
 onmessage = (message) => {
@@ -369,16 +193,4 @@ onmessage = (message) => {
     })
     return
   }
-
-  if(validTypes.findIndex(type => type === message.data.type) >= 0){
-    switch(message.data.type){
-      case 'GET_USERLANDMARKS_VTK':
-        getuserLandmarksVtk(message.data)
-        return
-      default:
-        console.warn('unhandled worker action', message)
-    }
-  } else {
-    console.warn('unhandled worker action', message)
-  }
 }