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