Skip to content
Snippets Groups Projects
Unverified Commit 43dc2aa3 authored by Xiao Gui's avatar Xiao Gui
Browse files

bugfix: fsaverage on chnage variant, showing

multiple variant meshes
bugfix: fsaverage err URL encoding selected
regions
maint: some simplification
parent e1c0808a
No related branches found
No related tags found
No related merge requests found
Showing
with 493 additions and 210 deletions
# 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
......@@ -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",
......
......@@ -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
......
......@@ -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),
......
......@@ -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', () => {
......
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`
})
)),
)
})
))
......
......@@ -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")}`
}
......
......@@ -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,
}
}
]
})
......
......@@ -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(
......
......@@ -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
}
})
)
......
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{
......
......@@ -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>
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment