From db1cf335aad8827c16994699cad0c392a29accd0 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Thu, 21 Mar 2024 17:48:41 +1100 Subject: [PATCH] WIP: landmark overlay on viewer works WIP: on side-by-side mode, hiding landmarks & volume, lock inc volume --- frontend/global.d.ts | 7 + frontend/src/app/app.module.ts | 40 ++++- frontend/src/const.ts | 24 ++- frontend/src/landmarks/const.ts | 3 + frontend/src/landmarks/landmarks.module.ts | 27 ++-- .../listview/listview.component.html | 16 +- .../landmarks/listview/listview.component.ts | 24 ++- .../landmarks/overlay/overlay.component.html | 10 ++ .../landmarks/overlay/overlay.component.scss | 26 ++++ .../overlay/overlay.component.spec.ts | 23 +++ .../landmarks/overlay/overlay.component.ts | 147 ++++++++++++++++++ .../landmarks/overlay/overlayPosition.pipe.ts | 16 ++ .../mouse-interaction.directive.ts | 17 ++ frontend/src/sharedModule/sharedModule.ts | 16 ++ frontend/src/state/actions.ts | 8 + frontend/src/state/app/actions.ts | 8 + frontend/src/state/app/consts.ts | 15 +- frontend/src/state/app/effects.ts | 44 +++++- frontend/src/state/app/state.ts | 18 ++- frontend/src/styles.scss | 7 + .../nehuba-viewer-wrapper.component.ts | 48 ++++-- .../src/views/viewer/viewer.component.html | 44 ++++-- .../src/views/viewer/viewer.component.scss | 44 ++++++ frontend/src/views/viewer/viewer.component.ts | 143 +++++++++++++++-- 24 files changed, 692 insertions(+), 83 deletions(-) create mode 100644 frontend/src/landmarks/overlay/overlay.component.html create mode 100644 frontend/src/landmarks/overlay/overlay.component.scss create mode 100644 frontend/src/landmarks/overlay/overlay.component.spec.ts create mode 100644 frontend/src/landmarks/overlay/overlay.component.ts create mode 100644 frontend/src/landmarks/overlay/overlayPosition.pipe.ts create mode 100644 frontend/src/state/actions.ts diff --git a/frontend/global.d.ts b/frontend/global.d.ts index 1b407c0d..23a06a6d 100644 --- a/frontend/global.d.ts +++ b/frontend/global.d.ts @@ -45,8 +45,14 @@ declare namespace export_nehuba { } interface SliceView { + width: number + height: number + viewMatrix: mat4 invViewMatrix: mat4 centerDataPosition: vec3 + viewChanged: { + add(callback: () => void): () => void + } navigationState: { pose: { orientation: { @@ -74,6 +80,7 @@ declare namespace export_nehuba { class ManagedLayer { get layer(): any + setVisible(flag: boolean): void } interface NehubaViewer { diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 69468a6a..2e058faa 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -9,6 +9,8 @@ import { StoreModule } from '@ngrx/store'; import { reducers, metaReducers, effects } from 'src/state'; import { EffectsModule } from '@ngrx/effects'; import { SharedModule } from 'src/sharedModule/sharedModule'; +import { DEBOUNCED_WINDOW_RESIZE, SLICEVIEWS_INJECTION_TOKEN, SliceViewEvent, SliceViewProviderType, arrayEqual, sliceViewEvEql, sliceViewEvIncludes } from 'src/const'; +import { Subject, debounceTime, distinctUntilChanged, map, scan, shareReplay, throttleTime } from 'rxjs'; @NgModule({ declarations: [AppComponent], @@ -23,7 +25,43 @@ import { SharedModule } from 'src/sharedModule/sharedModule'; }), EffectsModule.forRoot(effects), ], - providers: [], + providers: [ + { + provide: DEBOUNCED_WINDOW_RESIZE, + useFactory: () => { + const resizeSub = new Subject() + window.addEventListener("resize", ev => { + resizeSub.next(ev) + }) + return resizeSub.pipe( + debounceTime(160), + shareReplay(1), + ) + } + }, + { + provide: SLICEVIEWS_INJECTION_TOKEN, + useFactory: () => { + const obs = new Subject() + return { + register: obs.next.bind(obs), + observable: obs.pipe( + scan((acc, v) => { + if (sliceViewEvIncludes(v, acc)) { + return acc + } + return acc.concat(v) + }, [] as SliceViewEvent[]), + map(v => v.slice(0, 4)), + distinctUntilChanged( + (p, c) => arrayEqual(p, c, sliceViewEvEql) + ), + shareReplay(1), + ) + } as SliceViewProviderType + } + } + ], bootstrap: [AppComponent], }) export class AppModule {} diff --git a/frontend/src/const.ts b/frontend/src/const.ts index 90c9ca01..f7ed49ad 100644 --- a/frontend/src/const.ts +++ b/frontend/src/const.ts @@ -32,4 +32,26 @@ export function arrayEqual(a: T[], b: T[], predicate: (a: T, b: T) => boolean export function isDefined(v: T|null|undefined): v is T { return v !== null && typeof v !== 'undefined' -} \ No newline at end of file +} + +export const DEBOUNCED_WINDOW_RESIZE = new InjectionToken>("DEBOUNCED_WINDOW_RESIZE") + +export type SliceViewEvent = { + element: HTMLElement, + sliceview: export_nehuba.SliceView +} + +export interface SliceViewProviderType { + observable: Observable + register: (ev: SliceViewEvent) => void +} + +export const SLICEVIEWS_INJECTION_TOKEN = new InjectionToken("SLICEVIEWS_INJECTION_TOKEN") + +export function sliceViewEvEql(a: SliceViewEvent, b: SliceViewEvent): boolean { + return a.element === b.element && a.sliceview === b.sliceview +} + +export function sliceViewEvIncludes(ev: SliceViewEvent, list: SliceViewEvent[]): boolean { + return list.some(it => sliceViewEvEql(it, ev)) +} diff --git a/frontend/src/landmarks/const.ts b/frontend/src/landmarks/const.ts index dc251ee0..32135f18 100644 --- a/frontend/src/landmarks/const.ts +++ b/frontend/src/landmarks/const.ts @@ -1 +1,4 @@ export { Landmark, LandmarkPair } from 'src/state/app/consts'; + +export const INCOMING_LM_COLOR = "#ffff00" +export const REF_LM_COLOR = "#ffffff" diff --git a/frontend/src/landmarks/landmarks.module.ts b/frontend/src/landmarks/landmarks.module.ts index 40b93df9..604291b2 100644 --- a/frontend/src/landmarks/landmarks.module.ts +++ b/frontend/src/landmarks/landmarks.module.ts @@ -1,27 +1,24 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; import { ListviewComponent } from './listview/listview.component'; -import { MatTableModule } from '@angular/material/table'; import { ToolbarComponent } from './toolbar/toolbar.component'; -import { MatIconModule } from '@angular/material/icon'; -import { MatButtonModule } from '@angular/material/button'; -import { MatDividerModule } from '@angular/material/divider'; -import { FormsModule } from '@angular/forms'; -import { MatInputModule } from '@angular/material/input'; -import { MatFormFieldModule } from '@angular/material/form-field'; +import { SharedModule } from 'src/sharedModule/sharedModule'; +import { OverlayComponent } from './overlay/overlay.component'; +import { OverlayPositionPipe } from './overlay/overlayPosition.pipe'; @NgModule({ - declarations: [ListviewComponent, ToolbarComponent], + declarations: [ + ListviewComponent, + ToolbarComponent, + OverlayComponent, + OverlayPositionPipe + ], imports: [ CommonModule, - MatTableModule, - MatIconModule, - MatButtonModule, - MatDividerModule, FormsModule, - MatInputModule, - MatFormFieldModule, + SharedModule, ], - exports: [ListviewComponent, ToolbarComponent], + exports: [ListviewComponent, ToolbarComponent, OverlayComponent], }) export class LandmarksModule {} diff --git a/frontend/src/landmarks/listview/listview.component.html b/frontend/src/landmarks/listview/listview.component.html index f78cf27e..deabfc82 100644 --- a/frontend/src/landmarks/listview/listview.component.html +++ b/frontend/src/landmarks/listview/listview.component.html @@ -1,12 +1,4 @@ - - - - - @@ -28,9 +20,9 @@ @@ -38,9 +30,9 @@ diff --git a/frontend/src/landmarks/listview/listview.component.ts b/frontend/src/landmarks/listview/listview.component.ts index ff0e4621..88bb0d7a 100644 --- a/frontend/src/landmarks/listview/listview.component.ts +++ b/frontend/src/landmarks/listview/listview.component.ts @@ -4,7 +4,9 @@ import { Input, TrackByFunction, } from '@angular/core'; -import { LandmarkPair, Landmark } from '../const'; +import { LandmarkPair, Landmark, INCOMING_LM_COLOR, REF_LM_COLOR } from '../const'; +import { Store } from '@ngrx/store'; +import { actions } from 'src/state/app'; @Component({ selector: 'voluba-landmark-listview', @@ -13,22 +15,34 @@ import { LandmarkPair, Landmark } from '../const'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ListviewComponent { + + INCOMING_LM_COLOR = INCOMING_LM_COLOR + REF_LM_COLOR = REF_LM_COLOR + @Input() landmarkPair: LandmarkPair[] = []; - displayedColumns: string[] = ['color', 'name', 'toTmpl', 'toInc']; + displayedColumns: string[] = ['name', 'toTmpl', 'toInc']; public trackBy: TrackByFunction = ( _idx: number, landmarkPair: LandmarkPair ) => landmarkPair.id; - onUpdateColor(landmarkPair: LandmarkPair, inputEvent: Event) { - console.log(landmarkPair, (inputEvent.target as HTMLInputElement).value); + constructor(private store: Store){ + } onUpdateName(landmarkPair: LandmarkPair, inputEvent: Event) { - console.log(landmarkPair, (inputEvent.target as HTMLInputElement).value); + const { id } = landmarkPair + this.store.dispatch( + actions.updateLandmarkPair({ + id, + value: { + name: (inputEvent.target as HTMLInputElement).value || '' + } + }) + ) } onClickLocation(landmark: Landmark) { diff --git a/frontend/src/landmarks/overlay/overlay.component.html b/frontend/src/landmarks/overlay/overlay.component.html new file mode 100644 index 00000000..ad9f1f7f --- /dev/null +++ b/frontend/src/landmarks/overlay/overlay.component.html @@ -0,0 +1,10 @@ + + +
+ + +
+
+
diff --git a/frontend/src/landmarks/overlay/overlay.component.scss b/frontend/src/landmarks/overlay/overlay.component.scss new file mode 100644 index 00000000..4f25f4c4 --- /dev/null +++ b/frontend/src/landmarks/overlay/overlay.component.scss @@ -0,0 +1,26 @@ +:host +{ + pointer-events: none; + position: relative; + + > * + { + position: absolute; + } +} + +.landmark-container +{ + display: inline-flex; + justify-content: center; + align-items: flex-end; + overflow: visible; + width: 0; + height: 0; + + > * + { + color: inherit; + flex: 0 0 auto; + } +} diff --git a/frontend/src/landmarks/overlay/overlay.component.spec.ts b/frontend/src/landmarks/overlay/overlay.component.spec.ts new file mode 100644 index 00000000..ea5e11b7 --- /dev/null +++ b/frontend/src/landmarks/overlay/overlay.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { OverlayComponent } from './overlay.component'; + +describe('OverlayComponent', () => { + let component: OverlayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ OverlayComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(OverlayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/landmarks/overlay/overlay.component.ts b/frontend/src/landmarks/overlay/overlay.component.ts new file mode 100644 index 00000000..63a02def --- /dev/null +++ b/frontend/src/landmarks/overlay/overlay.component.ts @@ -0,0 +1,147 @@ +import { ChangeDetectionStrategy, Component, Inject, Input } from '@angular/core'; +import { Landmark } from '../const'; +import { BehaviorSubject, Observable, Subject, combineLatest, concat, debounceTime, filter, map, merge, of, pipe, share, shareReplay, switchMap, tap, throttleTime } from 'rxjs'; +import { DEBOUNCED_WINDOW_RESIZE } from 'src/const'; +import { Store, select } from '@ngrx/store'; +import * as appState from "src/state/app" + +function isSliceView(input: export_nehuba.SliceView|null): input is export_nehuba.SliceView { + return !!input +} + +export type OverlayLm = Landmark & { color: string } + + +/** + * + * TODO + * might not be efficient/mem leak? + */ +const pipeGetSliceViewChanged = () => { + let cancel: (() => void) | null = null + let viewChanged$: Subject | null = null + return pipe( + switchMap((sliceView: export_nehuba.SliceView) => { + if (cancel) { + cancel() + cancel = null + } + if (viewChanged$) { + viewChanged$.complete() + viewChanged$ = null + } + viewChanged$ = new Subject() + cancel = sliceView.viewChanged.add( + () => { + viewChanged$?.next(null) + } + ) + return viewChanged$.asObservable() + }) + ) +} + +@Component({ + selector: 'voluba-overlay', + templateUrl: './overlay.component.html', + styleUrls: ['./overlay.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class OverlayComponent { + + #sliceView$ = new BehaviorSubject(null) + #sliceViewObs$: Observable = this.#sliceView$.pipe( + filter(isSliceView), + shareReplay(1) + ) + + @Input('overlay-sliceview') + set sliceView(sv: export_nehuba.SliceView) { + this.#sliceView$.next(sv) + } + + #landmarks$ = new BehaviorSubject([]) + #landmarkObs$ = this.#landmarks$.pipe( + shareReplay(1) + ) + + @Input('overlay-landmarks') + set landmarks(lms: OverlayLm[]) { + this.#landmarks$.next(lms) + } + + #sliceViewInfo$ = combineLatest([ + concat( + of(null), + merge( + this.dbResize, + this.store.pipe( + select(appState.selectors.isDefaultMode), + ) + ) + ).pipe( + debounceTime(32) + ), + this.#sliceViewObs$, + ]).pipe( + map(([_, sliceView]) => { + const { width, height } = sliceView + return { + sliceView, + width, + height, + } + }), + shareReplay(1), + ) + + #sliceViewChanged$ = this.#sliceViewObs$.pipe( + pipeGetSliceViewChanged(), + throttleTime(16), + ) + + + /** + * displayedLandmarks, unlike regularlandmarks, the xyz position + * has been transformed to the slice view space (in terms of x, y + * from top left of the slice view) + */ + displayedLandmarks$ = combineLatest([ + concat( + of(null), + this.#sliceViewChanged$, + ), + this.#sliceViewInfo$, + this.#landmarkObs$, + ]).pipe( + map(([_sliceViewChanged, sliceViewInfo, landmarks]) => { + const { vec3 } = export_nehuba + const { width, height, sliceView: { viewMatrix } } = sliceViewInfo + return landmarks.map(lm => { + const newPos = vec3.transformMat4(vec3.create(), lm.position as any, viewMatrix) + newPos[0] = (newPos[0] + width / 2) + newPos[1] = (newPos[1] + height / 2) + return { + ...lm, + position: Array.from(newPos) + } + }).filter(lm => { + return ( + lm.position[0] >= 0 && lm.position[0] <= width + && lm.position[1] >= 0 && lm.position[1] <= height + ) + }) + }), + shareReplay(1), + ) + + constructor( + @Inject(DEBOUNCED_WINDOW_RESIZE) + private dbResize: Observable, + private store: Store, + ){ + this.#sliceViewChanged$.subscribe(val => { + console.log("view changed", val) + }) + } +} diff --git a/frontend/src/landmarks/overlay/overlayPosition.pipe.ts b/frontend/src/landmarks/overlay/overlayPosition.pipe.ts new file mode 100644 index 00000000..4cd29167 --- /dev/null +++ b/frontend/src/landmarks/overlay/overlayPosition.pipe.ts @@ -0,0 +1,16 @@ +import { Pipe, PipeTransform, SecurityContext } from "@angular/core"; +import { DomSanitizer } from "@angular/platform-browser"; + +@Pipe({ + name: 'overlayPosition', + pure: true +}) + +export class OverlayPositionPipe implements PipeTransform{ + constructor(private sanitizer: DomSanitizer){} + transform(position: number[]) { + return this.sanitizer.sanitize( + SecurityContext.STYLE, `translate(${position[0]}px, ${position[1]}px)` + ) + } +} diff --git a/frontend/src/mouse-interactions/mouse-interaction.directive.ts b/frontend/src/mouse-interactions/mouse-interaction.directive.ts index b9c1ffec..992e5233 100644 --- a/frontend/src/mouse-interactions/mouse-interaction.directive.ts +++ b/frontend/src/mouse-interactions/mouse-interaction.directive.ts @@ -24,6 +24,13 @@ type _Subject = Subject<{ selector: '[volubaMouseInteraction]', }) export class MouseInteractionDirective implements OnDestroy, AfterViewInit { + + @Input() + ignoreElements: { has(el: HTMLElement): boolean } | null = null + + @Input() + onlyElements: { has(el: HTMLElement): boolean } | null = null + @Input() stopPropagation = false; @@ -76,6 +83,16 @@ export class MouseInteractionDirective implements OnDestroy, AfterViewInit { #getEventStream(eventname: T) { if (!this.#eventsHandled.has(eventname)) { const evh = (event: GlobalEventHandlersEventMap[T]) => { + if (this.ignoreElements + && this.ignoreElements.has(event.target as HTMLElement)) { + return + } + + if (this.onlyElements + && !this.onlyElements.has(event.target as HTMLElement)) { + return + } + if (this.stopPropagation && this.stopPropagationEvents.includes(eventname)) { event.stopPropagation(); } diff --git a/frontend/src/sharedModule/sharedModule.ts b/frontend/src/sharedModule/sharedModule.ts index 475f6910..c567c4f3 100644 --- a/frontend/src/sharedModule/sharedModule.ts +++ b/frontend/src/sharedModule/sharedModule.ts @@ -2,16 +2,32 @@ import { NgModule } from "@angular/core"; import { MatSnackBarModule } from "@angular/material/snack-bar" import { MatSliderModule } from "@angular/material/slider" import { MatButtonModule } from "@angular/material/button"; +import { MatTableModule } from '@angular/material/table'; +import { MatIconModule } from '@angular/material/icon'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatInputModule } from '@angular/material/input'; +import { MatFormFieldModule } from '@angular/material/form-field'; + @NgModule({ imports: [ MatSnackBarModule, MatSliderModule, MatButtonModule, + MatTableModule, + MatIconModule, + MatDividerModule, + MatInputModule, + MatFormFieldModule, ], exports: [ MatSnackBarModule, MatSliderModule, MatButtonModule, + MatTableModule, + MatIconModule, + MatDividerModule, + MatInputModule, + MatFormFieldModule, ] }) diff --git a/frontend/src/state/actions.ts b/frontend/src/state/actions.ts new file mode 100644 index 00000000..8f81b8e4 --- /dev/null +++ b/frontend/src/state/actions.ts @@ -0,0 +1,8 @@ +import { createAction, props } from "@ngrx/store"; + +const nameSpace = `[main]` + +export const error = createAction( + `[${nameSpace}] error`, + props<{ message: string }>() +) diff --git a/frontend/src/state/app/actions.ts b/frontend/src/state/app/actions.ts index 8d108b02..ceacf35f 100644 --- a/frontend/src/state/app/actions.ts +++ b/frontend/src/state/app/actions.ts @@ -59,3 +59,11 @@ export const setAddLandmarkMode = createAction( export const toggleMode = createAction( `[${nameSpace}] toggleMode` ) + +export const updateLandmarkPair = createAction( + `[${nameSpace}] updatelandmarkpair`, + props<{ + id: string + value: Partial> + }>() +) diff --git a/frontend/src/state/app/consts.ts b/frontend/src/state/app/consts.ts index 9f5727ec..91ec38a6 100644 --- a/frontend/src/state/app/consts.ts +++ b/frontend/src/state/app/consts.ts @@ -10,7 +10,6 @@ export const MODE = { } as const type Vec3 = [number, number, number]; -type Color = string; export type Landmark = { targetVolumeId: string; @@ -20,7 +19,6 @@ export type Landmark = { export type LandmarkPair = { tmplLm: Landmark; incLm: Landmark; - color: Color; id: string; name: string; }; @@ -39,7 +37,18 @@ export const defaultState: LocalState = { mode: MODE.DEFAULT, addingLandmark: false, incLocked: false, - landmarkPairs: [], + landmarkPairs: [{ + id: "foo-bar", + name: "hello my name is", + incLm: { + position: [0, 0, 0], + targetVolumeId: "waxholm" + }, + tmplLm: { + position: [0, 0, 0], + targetVolumeId: "bigbrain" + } + }], purgatory: null, }; diff --git a/frontend/src/state/app/effects.ts b/frontend/src/state/app/effects.ts index b66092a1..6144dd43 100644 --- a/frontend/src/state/app/effects.ts +++ b/frontend/src/state/app/effects.ts @@ -3,26 +3,63 @@ import { Actions, createEffect, ofType } from "@ngrx/effects"; import { select, Store } from "@ngrx/store"; import * as selectors from "./selectors" import * as actions from "./actions" -import { from, map, of, switchMap, withLatestFrom } from "rxjs"; +import { from, of, switchMap, withLatestFrom } from "rxjs"; import { MatSnackBar } from "src/sharedModule" +import * as outputs from "src/state/outputs" +import * as inputs from "src/state/inputs" +import * as mainInput from "src/state/actions" +import { Landmark } from "./consts"; @Injectable() export class Effects { onAddLandmarkRequest = createEffect(() => this.actions$.pipe( ofType(actions.addLandmark), withLatestFrom( + this.store.pipe( + select(inputs.selectors.selectedTemplate) + ), + this.store.pipe( + select(inputs.selectors.selectedIncoming) + ), this.store.pipe( select(selectors.purgatory) + ), + this.store.pipe( + outputs.selectors.getIncXform() ) ), - switchMap(([lm, purgatory]) => { + switchMap(([lm, refVol, incVol, purgatory, xform]) => { if (!purgatory) { + if (refVol?.["@id"] !== lm.landmark.targetVolumeId) { + return of( + mainInput.error({ + message: `First landmark must target reference volume ${refVol?.["@id"]}, but instead targets ${lm.landmark.targetVolumeId}` + }) + ) + } return of( actions.addToPurgatory({ landmark: lm.landmark }) ) } + + if (incVol?.["@id"] !== lm.landmark.targetVolumeId) { + return of( + mainInput.error({ + message: `Second landmark must target incoming volume ${incVol?.["@id"]}, but targets ${lm.landmark.targetVolumeId}` + }) + ) + } + const { mat4, vec3 } = export_nehuba + const lmPos = vec3.fromValues(...lm.landmark.position) + mat4.invert(xform, xform) + vec3.transformMat4(lmPos, lmPos, xform) + + const incLandmark: Landmark = { + position: Array.from(lmPos) as [number, number, number], + targetVolumeId: lm.landmark.targetVolumeId + } return from( [ actions.purgePurgatory(), @@ -31,10 +68,9 @@ export class Effects { : actions.addLandmarkPair({ landmarkPair: { tmplLm: purgatory, - incLm: lm.landmark, + incLm: incLandmark, name: `Untitled`, id: crypto.randomUUID(), - color: "#ff0000" } }) ] diff --git a/frontend/src/state/app/state.ts b/frontend/src/state/app/state.ts index 301138e5..b906faca 100644 --- a/frontend/src/state/app/state.ts +++ b/frontend/src/state/app/state.ts @@ -37,5 +37,21 @@ export const reducer = createReducer( mode: (state.mode === MODE.DEFAULT) ? MODE.SIDE_BY_SIDE : MODE.DEFAULT - })) + })), + on(actions.updateLandmarkPair, (state, { id, value }) => { + const found = state.landmarkPairs.find(lmp => lmp.id === id) + if (!found) { + return { ...state } + } + + const otherLmps = state.landmarkPairs.filter(lmp => lmp !== found) + const newLmp = { + ...found, + ...value + } + return { + ...state, + landmarkPairs: [...otherLmps, newLmp] + } + }) ); diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 8b4b5a2a..a6e41c4d 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -9,4 +9,11 @@ html, body height: 100%; margin: 0; padding: 0; + + overflow: hidden; +} + +.position-icon +{ + filter: drop-shadow(0 0 .2em black) } \ No newline at end of file diff --git a/frontend/src/views/nehuba-viewer-wrapper/nehuba-viewer-wrapper.component.ts b/frontend/src/views/nehuba-viewer-wrapper/nehuba-viewer-wrapper.component.ts index 51b6a418..e1c37c3d 100644 --- a/frontend/src/views/nehuba-viewer-wrapper/nehuba-viewer-wrapper.component.ts +++ b/frontend/src/views/nehuba-viewer-wrapper/nehuba-viewer-wrapper.component.ts @@ -4,15 +4,18 @@ import { Component, ElementRef, EventEmitter, + Inject, Input, OnInit, Output, inject, } from '@angular/core'; import { distinctUntilChanged, fromEvent, map, merge, takeUntil } from 'rxjs'; -import { isHtmlElement, Mat4, PatchedSymbol } from 'src/const'; +import { isHtmlElement, Mat4, PatchedSymbol, SliceViewProviderType, SLICEVIEWS_INJECTION_TOKEN } from 'src/const'; import { DestroyDirective } from 'src/util/destroy.directive'; +const NEHUBA_PATCHED = Symbol("NEHUBA_PATCHED") + export type NehubaLayer = { id: string; url: string; @@ -133,7 +136,8 @@ const darkmode = { const _config = darkmode type LayerProperties = { - transform: export_nehuba.mat4; + transform: export_nehuba.mat4 + visible: boolean }; @Component({ @@ -190,7 +194,10 @@ export class NehubaViewerWrapperComponent implements OnInit, AfterViewInit { >(); #patchedSliceViewPanels = new WeakSet(); - constructor(private el: ElementRef) {} + constructor( + private el: ElementRef, + @Inject(SLICEVIEWS_INJECTION_TOKEN) private sliceViewProvider: SliceViewProviderType + ) {} ngOnInit() { if (!(export_nehuba as any)[PatchedSymbol]) { @@ -258,11 +265,12 @@ export class NehubaViewerWrapperComponent implements OnInit, AfterViewInit { sliceViewPanel: export_nehuba.SliceViewPanel ) => { if (this.#patchedSliceViewPanels.has(sliceViewPanel)) return; - const { elementToSliceViewWeakMap } = this; + const { elementToSliceViewWeakMap, sliceViewProvider } = this; this.#patchedSliceViewPanels.add(sliceViewPanel); const originalDraw = sliceViewPanel.draw; sliceViewPanel.draw = function () { if (this.sliceView) { + sliceViewProvider.register({element: this.element, sliceview: this.sliceView}) elementToSliceViewWeakMap.set(this.element, this.sliceView); } @@ -275,23 +283,29 @@ export class NehubaViewerWrapperComponent implements OnInit, AfterViewInit { }); } - setLayerProperty(id: string, property: LayerProperties) { - const { transform } = property; + setLayerProperty(id: string, property: Partial) { + const { transform, visible } = property; const layer = this.nehubaViewer?.ngviewer.layerManager.getLayerByName(id); if (!layer) throw new Error(`layer with id '${id}' not found`); - const dataSources = layer.layer.dataSources; - if (dataSources.length !== 1) { - throw new Error( - `managed layer needs to have exactly 1 data source, but has ${dataSources.length} instead` - ); + if (typeof transform !== "undefined") { + const dataSources = layer.layer.dataSources; + if (dataSources.length !== 1) { + throw new Error( + `managed layer needs to have exactly 1 data source, but has ${dataSources.length} instead` + ); + } + if (dataSources[0].loadState) { + const loadedStateXform = dataSources[0].loadState.transform; + loadedStateXform.value = { + ...loadedStateXform.value, + transform: transform, + }; + } } - if (dataSources[0].loadState) { - const loadedStateXform = dataSources[0].loadState.transform; - loadedStateXform.value = { - ...loadedStateXform.value, - transform: transform, - }; + + if (typeof visible !== "undefined") { + layer.setVisible(visible) } } } diff --git a/frontend/src/views/viewer/viewer.component.html b/frontend/src/views/viewer/viewer.component.html index 39161ac1..804d2589 100644 --- a/frontend/src/views/viewer/viewer.component.html +++ b/frontend/src/views/viewer/viewer.component.html @@ -1,14 +1,35 @@
+
+ + + +
+ + + + + + + + + + + + +
+
- -

@@ -23,11 +44,14 @@

+ -
diff --git a/frontend/src/views/viewer/viewer.component.scss b/frontend/src/views/viewer/viewer.component.scss index 6beff8d7..c8f9b4dd 100644 --- a/frontend/src/views/viewer/viewer.component.scss +++ b/frontend/src/views/viewer/viewer.component.scss @@ -30,3 +30,47 @@ div.viewer-wrapper flex: 1 1 0; } } + +.landmark-layer-wrapper +{ + position: relative; + + > * + { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 0; + } + + > .four-quadrant + { + z-index: 1; + pointer-events: none; + } +} + +.four-quadrant +{ + display: grid; + grid-template-rows: 1fr 1fr; + grid-template-columns: 1fr 1fr; + + > :nth-child(1) + { + display: block; + background-color: rgba(255, 0, 0, 0.2); + } + > :nth-child(2) + { + display: block; + background-color: rgba(0, 255, 0, 0.2); + } + > :nth-child(3) + { + display: block; + background-color: rgba(0, 0, 255, 0.2); + } +} diff --git a/frontend/src/views/viewer/viewer.component.ts b/frontend/src/views/viewer/viewer.component.ts index f3ab3049..f2ef43ce 100644 --- a/frontend/src/views/viewer/viewer.component.ts +++ b/frontend/src/views/viewer/viewer.component.ts @@ -2,6 +2,7 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, + Inject, Injectable, ViewChild, inject, @@ -17,13 +18,19 @@ import { Subject, takeUntil, NEVER, + concat, + of, } from 'rxjs'; import { MouseInteractionDirective } from 'src/mouse-interactions/mouse-interaction.directive'; import * as app from 'src/state/app'; import * as outputs from 'src/state/outputs'; import { NehubaViewerWrapperComponent } from '../nehuba-viewer-wrapper/nehuba-viewer-wrapper.component'; -import { VOLUBA_NEHUBA_TOKEN } from 'src/const'; +import { SLICEVIEWS_INJECTION_TOKEN, SliceViewProviderType, VOLUBA_NEHUBA_TOKEN } from 'src/const'; import { DestroyDirective } from 'src/util/destroy.directive'; +import { getIncXform } from 'src/state/outputs/selectors'; +import * as appState from "src/state/app" +import { Landmark, INCOMING_LM_COLOR, REF_LM_COLOR } from 'src/landmarks/const'; + @Injectable() class NehubaSvc { @@ -76,11 +83,102 @@ export class ViewerComponent implements AfterViewInit { this.incLocked$, this.store.pipe( select(app.selectors.isDefaultMode) + ), + this.store.pipe(getIncXform()), + this.store.pipe( + select(app.selectors.landmarks) + ), + concat( + of([]), + this.sliceViewProvider.observable + ).pipe( + map(sliceViewObjs => { + type T = export_nehuba.SliceView | null + const returnArray: [T, T, T] = [null, null, null] + for (const sliceViewObj of sliceViewObjs){ + const { element, sliceview } = sliceViewObj + /** + * n.b. + * getBoundingClientRect is very slow + * it should not be called very frequently (e.g. only on layout change/resize) + */ + const { top, left } = element.getBoundingClientRect() + if (top < 5 && left < 5) { + returnArray[0] = sliceview + } + if (top < 5 && left > 5) { + returnArray[1] = sliceview + } + if (top > 5 && left < 5) { + returnArray[2] = sliceview + } + } + return returnArray + }) + ), + this.store.pipe( + select(appState.selectors.purgatory) + ), + this.store.pipe( + select(appState.selectors.addLmMode) + ), + concat( + of(null), + this.nehubaSvc.mouseover, ) ]).pipe( - map(([incLocked, isDefaultMode]) => ({ - incLocked, isDefaultMode - })) + map(([incLocked, isDefaultMode, xform, landmarks, sliceViews, purgatory, addLandmarkMode, mouseover]) => { + + const storedLandmarks = landmarks.map(lm => { + const incomingLandmarks: (Landmark & {color:string})[] = [] + if (isDefaultMode) { + const { vec3 } = export_nehuba + + + const newPos = vec3.fromValues(...lm.incLm.position) + vec3.transformMat4(newPos, newPos, xform) + + incomingLandmarks.push({ + ...lm.incLm, + position: Array.from(newPos) as [number, number, number], + color: INCOMING_LM_COLOR, + }) + } + return [ + { + ...lm.tmplLm, + color: REF_LM_COLOR + }, + ...incomingLandmarks + ] + }) + const purgatoryLandmarks = [] + if (purgatory) { + purgatoryLandmarks.push({ + ...purgatory, + color: REF_LM_COLOR + }) + } + if (addLandmarkMode && mouseover) { + purgatoryLandmarks.push({ + targetVolumeId: 'purgatory', + position: Array.from(mouseover) as [number, number, number], + color: purgatory + ? INCOMING_LM_COLOR + : REF_LM_COLOR + }) + } + + return { + incLocked, + isDefaultMode, + primaryLandmarks: [ + ...storedLandmarks.flatMap(v => v), + ...purgatoryLandmarks + ], + sliceViews, + } + }) ); toggleIncLocked() { @@ -91,7 +189,11 @@ export class ViewerComponent implements AfterViewInit { this.store.dispatch(app.actions.toggleMode()) } - constructor(private store: Store, private nehubaSvc: NehubaSvc) {} + constructor( + private store: Store, + private nehubaSvc: NehubaSvc, + @Inject(SLICEVIEWS_INJECTION_TOKEN) private sliceViewProvider: SliceViewProviderType, + ) {} onMousePosition(pos: Float32Array){ this.nehubaSvc.setMouseOver(pos) @@ -121,14 +223,17 @@ export class ViewerComponent implements AfterViewInit { } return combineLatest([ this.incLocked$, + this.store.pipe( + select(appState.selectors.isDefaultMode) + ), this.store.pipe( select(app.selectors.addLmMode) ), this.mouseInteractive.volubaDrag, ]).pipe( - filter(([incLocked, isAddingLm, _]) => !incLocked && !isAddingLm), - map(([_, _2, { movementX, movementY }]) => ({ + filter(([incLocked, isDefaultMode, isAddingLm, _]) => !incLocked && !isAddingLm && isDefaultMode), + map(([_, _2, _3, { movementX, movementY }]) => ({ movementX, movementY, sliceView, @@ -217,12 +322,22 @@ export class ViewerComponent implements AfterViewInit { }); - const applyMatrixSub = this.store - .pipe(select(outputs.selectors.incMatrixMat4)) - .subscribe((v) => { - this.viewerWrapper?.setLayerProperty('bla', { - transform: v, - }); - }); + this.store.pipe( + select(outputs.selectors.incMatrixMat4), + takeUntil(this.#destroyed$), + ).subscribe((v) => { + this.viewerWrapper?.setLayerProperty('bla', { + transform: v, + }) + }) + + this.store.pipe( + select(appState.selectors.isDefaultMode), + takeUntil(this.#destroyed$) + ).subscribe(isDefaultMode => { + this.viewerWrapper?.setLayerProperty('bla', { + visible: isDefaultMode + }) + }) } }
Color - Name To Template - To Incoming -