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/docs/releases/v2.12.0.md b/docs/releases/v2.12.0.md index d6c600ed49b32102affd2d6a34e027dc68a73c16..d06017301861d4aec9d2f6dd809bc016632b6fba 100644 --- a/docs/releases/v2.12.0.md +++ b/docs/releases/v2.12.0.md @@ -7,3 +7,4 @@ ## 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..8e7f6b6a39ea268baf93a705910d5bae30c429ea 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-dev.8", "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-dev.8", + "resolved": "https://registry.npmjs.org/export-nehuba/-/export-nehuba-0.1.0-dev.8.tgz", + "integrity": "sha512-nSiGMclXztCG5N9tNL9xf1Q8Eq2WN2FwtjJpXEtUxsIj/rzVWdU0EkEHjv6APDNTbDA9kgU2Nc61HLqcWWdIgQ==", "dependencies": { "pako": "^1.0.6" } diff --git a/package.json b/package.json index 9414f748771618ee48197f0ef0e3e58c898c0f0c..e0c290ded11056934fb23cf9ec04454531840cf8 100644 --- a/package.json +++ b/package.json @@ -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-dev.8", "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/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/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/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..e3e280cbc541f9a891dc6d7fd36f54dbb5ef385d --- /dev/null +++ b/src/util/periodic.service.ts @@ -0,0 +1,57 @@ +import { Injectable } from "@angular/core"; +import { Subject, combineLatest, interval, merge } from "rxjs"; +import { filter, map, scan } from "rxjs/operators"; +import { getUuid } from "./fn"; + +type Queue = { + callback: () => boolean + uuid: string +} + +@Injectable({ + providedIn: 'root' +}) +export class PeriodicSvc{ + #queue$ = new Subject<Queue>() + #dequeue$ = new Subject<Queue>() + #scannedQueue$ = merge<{ queue?: Queue, dequeue?: Queue }>( + this.#queue$.pipe( + map(queue => ({ queue })), + ), + this.#dequeue$.pipe( + map(dequeue => ({ dequeue })) + ) + ).pipe( + scan((acc, curr) => { + const { queue, dequeue } = curr + if (queue) { + return [...acc, queue] + } + if (dequeue) { + return acc.filter(q => q.uuid !== dequeue.uuid) + } + console.warn(`neither queue nor dequeue were defined!`) + return acc + }, [] as Queue[]) + ) + + addToQueue(callback: () => boolean){ + this.#queue$.next({ callback, uuid: getUuid() }) + } + constructor(){ + combineLatest([ + this.#scannedQueue$, + interval(160) + ]).pipe( + map(([queues, _]) => queues), + filter(queues => queues.length > 0), + ).subscribe(queues => { + for (const queue of queues) { + const { callback } = queue + if (callback()) { + this.#dequeue$.next(queue) + } + } + }) + } +} 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..1bdcedc1685bc6605bb95f56f9e881daebadf6f0 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,6 +477,27 @@ 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 + } + ) + } viewer.layerManager.addManagedLayer( viewer.layerSpecification.getLayer(key, layerObj[key])) @@ -509,7 +525,6 @@ export class NehubaViewerUnit implements OnDestroy { name: ngId, }) } - this.nehubaViewer.showSegment(0, { name: ngId, }) @@ -524,7 +539,6 @@ export class NehubaViewerUnit implements OnDestroy { name: ngId, }) } - this.nehubaViewer.hideSegment(0, { name: ngId, }) @@ -604,7 +618,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 +634,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 +644,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 +683,29 @@ export class NehubaViewerUnit implements OnDestroy { } private newViewerInit() { + + while (this.#newViewerSubs.length > 0) { + this.#newViewerSubs.pop().unsubscribe() + } - /* 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 }) => { + this.#newViewerSubs.push( + + /* 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 +728,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 +851,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/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/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/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) - } }