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