diff --git a/frontend/global.d.ts b/frontend/global.d.ts index 23a06a6d..88864bf8 100644 --- a/frontend/global.d.ts +++ b/frontend/global.d.ts @@ -1,16 +1,16 @@ declare namespace export_nehuba { class vec3 extends Float32Array { static fromValues(...arr: number[]): vec3 - static transformMat4(rec: vec3, src: vec3, mat: mat4): vec3 + static transformMat4(rec: vec3|number[], src: vec3|number[], mat: mat4): vec3 static transformQuat(rec: vec3, src: vec3, quat: quat): vec3 static sub(rec: vec3, src: vec3, dst: vec3): vec3 static create(): vec3 static add(rec: vec3, src: vec3, dst: vec3): vec3 - static mul(rec: vec3, src: vec3, m: vec3): vec3 + static mul(rec: vec3|number[], src: vec3|number[], m: vec3|number[]): vec3|number[] static inverse(rec: vec3, src: vec3): vec3 static scale(rec: vec3, src: vec3, s: number): vec3 static divide(out: vec3, a: vec3, b: vec3): vec3 - static div(out: vec3, a: vec3, b: vec3): vec3 + static div(out: vec3|number[], a: vec3|number[], b: vec3|number[]): vec3|number[] } class quat extends Float32Array { @@ -37,6 +37,7 @@ declare namespace export_nehuba { static invert(out: mat4, src: mat4): mat4 static fromTranslation(out: mat4, transl: vec3): mat4 static fromRotation(out: mat4, angle: number, axis: vec3 ): mat4 + static transpose(out: mat4, src: mat4): mat4 } class UrlHashBinding { @@ -83,6 +84,11 @@ declare namespace export_nehuba { setVisible(flag: boolean): void } + interface NgJsonable { + restoreState(state: any): void + toJSON(): any + } + interface NehubaViewer { ngviewer: { layerManager: { @@ -94,6 +100,12 @@ declare namespace export_nehuba { changed: { add: (callback: () => void) => void } + }, + navigationState: { + pose: { + orientation: NgJsonable + position: NgJsonable + } } } readonly navigationState: { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b92f9967..6a4f3162 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20,7 +20,7 @@ "@angular/router": "^15.0.0", "@ngrx/effects": "^15.1.0", "@ngrx/store": "^15.1.0", - "export-nehuba": "^0.1.0-dev.3", + "export-nehuba": "^0.1.7", "prettier": "^2.8.3", "rxjs": "~7.5.0", "tslib": "^2.3.0", @@ -6128,9 +6128,9 @@ } }, "node_modules/export-nehuba": { - "version": "0.1.0-dev.3", - "resolved": "https://registry.npmjs.org/export-nehuba/-/export-nehuba-0.1.0-dev.3.tgz", - "integrity": "sha512-o5Syi4MEfHdsITPPFWAJivvMZeZZiTqtHDgFIvjbre1oC+tEDXRc0atFEyptxhJato2tjNLP1jFGzsZGc5cCJQ==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/export-nehuba/-/export-nehuba-0.1.7.tgz", + "integrity": "sha512-LakXeWGkEtHwWrV69snlM2GGmeVP+jGnTaevOpWQJePkdkPq6DvCkCSH0mLBriR8yOPyO0e+VHuE3V4AnV4fPA==", "dependencies": { "pako": "^1.0.6" } @@ -16757,9 +16757,9 @@ } }, "export-nehuba": { - "version": "0.1.0-dev.3", - "resolved": "https://registry.npmjs.org/export-nehuba/-/export-nehuba-0.1.0-dev.3.tgz", - "integrity": "sha512-o5Syi4MEfHdsITPPFWAJivvMZeZZiTqtHDgFIvjbre1oC+tEDXRc0atFEyptxhJato2tjNLP1jFGzsZGc5cCJQ==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/export-nehuba/-/export-nehuba-0.1.7.tgz", + "integrity": "sha512-LakXeWGkEtHwWrV69snlM2GGmeVP+jGnTaevOpWQJePkdkPq6DvCkCSH0mLBriR8yOPyO0e+VHuE3V4AnV4fPA==", "requires": { "pako": "^1.0.6" } diff --git a/frontend/package.json b/frontend/package.json index 7ad142d4..85bff5d4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,7 +22,7 @@ "@angular/router": "^15.0.0", "@ngrx/effects": "^15.1.0", "@ngrx/store": "^15.1.0", - "export-nehuba": "^0.1.0-dev.3", + "export-nehuba": "^0.1.7", "prettier": "^2.8.3", "rxjs": "~7.5.0", "tslib": "^2.3.0", diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 2e058faa..7bb1495a 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -1,16 +1,17 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { LayoutModule } from 'src/layout/layout.module'; +import { isDefaultMode } from "src/state/app/selectors" import { AppComponent } from './app.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ViewsModule } from 'src/views/views.module'; -import { StoreModule } from '@ngrx/store'; +import { Store, StoreModule, select } 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'; +import { DEBOUNCED_WINDOW_RESIZE } from 'src/const'; +import { Subject, debounceTime, map, merge, shareReplay } from 'rxjs'; @NgModule({ declarations: [AppComponent], @@ -28,39 +29,25 @@ import { Subject, debounceTime, distinctUntilChanged, map, scan, shareReplay, th providers: [ { provide: DEBOUNCED_WINDOW_RESIZE, - useFactory: () => { + useFactory: (store: Store) => { const resizeSub = new Subject() window.addEventListener("resize", ev => { resizeSub.next(ev) }) - return resizeSub.pipe( + + return merge( + store.pipe( + select(isDefaultMode), + map(flag => new UIEvent(`x-resize-default-mode-${flag ? 'on' : 'off'}`)) + ), + resizeSub, + ).pipe( debounceTime(160), shareReplay(1), ) - } + }, + deps: [ Store ] }, - { - 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], }) diff --git a/frontend/src/const.ts b/frontend/src/const.ts index f7ed49ad..0430455b 100644 --- a/frontend/src/const.ts +++ b/frontend/src/const.ts @@ -1,5 +1,6 @@ import { InjectionToken } from "@angular/core"; import { Observable } from "rxjs"; +import { Landmark } from "./landmarks/const"; export type RecursivePartial> = Partial<{ [K in keyof T]: Partial; @@ -27,7 +28,19 @@ export function isVec3(input: unknown): input is Vec3 { } export function arrayEqual(a: T[], b: T[], predicate: (a: T, b: T) => boolean = (a, b) => a === b): boolean { - return a.every((v, idx) => predicate(v, b[idx])) && a.length === b.length + return a.length === b.length && a.every((v, idx) => predicate(v, b[idx])) +} + +export function FloatArrayEql(a: Float32Array, b: Float32Array): boolean { + if (a.length !== b.length) { + return false + } + for (let i = 0; i < a.length; i++){ + if (a[i] !== b[i]) { + return false + } + } + return true } export function isDefined(v: T|null|undefined): v is T { @@ -41,13 +54,6 @@ export type SliceViewEvent = { 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 } @@ -55,3 +61,7 @@ export function sliceViewEvEql(a: SliceViewEvent, b: SliceViewEvent): boolean { export function sliceViewEvIncludes(ev: SliceViewEvent, list: SliceViewEvent[]): boolean { return list.some(it => sliceViewEvEql(it, ev)) } + +export const EXPORT_LANDMARKS_TYPE = 'https://voluba.apps.hbp.eu/@types/landmarks' + +export type OverlayLm = Landmark & { color: string, id: string, highlighted?: boolean } diff --git a/frontend/src/index.html b/frontend/src/index.html index 1dfcbcf8..1fc5bce7 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -13,7 +13,8 @@ diff --git a/frontend/src/io/loadFile.directive.ts b/frontend/src/io/loadFile.directive.ts new file mode 100644 index 00000000..6a2ab73a --- /dev/null +++ b/frontend/src/io/loadFile.directive.ts @@ -0,0 +1,39 @@ +import { Directive, EventEmitter, HostListener, Input, Output } from "@angular/core"; + +@Directive({ + selector: '[voluba-load-from-file]', + exportAs: 'volubaLoadFromFile', +}) + +export class LoadFromFileDirective { + @Input('from-file-extensions') + extensionsToLoad: string = '.json' + + @Output('from-file-content') + emitContent = new EventEmitter() + + @HostListener('click') + load(){ + const input = document.createElement("input") + input.type = 'file' + input.accept = this.extensionsToLoad + document.body.appendChild(input) + input.onchange = () => { + if (input.files?.length !== 1) { + throw new Error(`Expected one and only one file selected, but got ${input.files?.length}`) + } + const file = input.files[0] + const reader = new FileReader() + reader.onload = () => { + const out = reader.result + if (!out) throw new Error(`Could not get any content!`) + this.emitContent.emit(out as string) + } + reader.onerror = e => { throw e } + reader.readAsText(file, 'utf-8') + + document.body.removeChild(input) + } + input.click() + } +} diff --git a/frontend/src/io/module.ts b/frontend/src/io/module.ts new file mode 100644 index 00000000..a0cebb18 --- /dev/null +++ b/frontend/src/io/module.ts @@ -0,0 +1,16 @@ +import { NgModule } from "@angular/core"; +import { SaveToFileDirective } from "./saveFile.directive"; +import { LoadFromFileDirective } from "./loadFile.directive"; + +@NgModule({ + declarations: [ + SaveToFileDirective, + LoadFromFileDirective, + ], + exports: [ + SaveToFileDirective, + LoadFromFileDirective, + ] +}) + +export class IOModule{} diff --git a/frontend/src/io/saveFile.directive.ts b/frontend/src/io/saveFile.directive.ts new file mode 100644 index 00000000..64af45ce --- /dev/null +++ b/frontend/src/io/saveFile.directive.ts @@ -0,0 +1,35 @@ +import { Directive, HostListener, Input } from "@angular/core"; + +const textEncoder = new TextEncoder() + +@Directive({ + selector: '[voluba-save-to-file]', + exportAs: 'volubaSaveToFile' +}) + +export class SaveToFileDirective{ + @Input('to-file-content') + textToSave: string|undefined + + @Input('to-file-filename') + filename: string = 'savedFile.json' + + @Input('to-file-mimetype') + mimetype: string = 'application/json' + + @HostListener('click') + onClick(){ + const { mimetype, filename, textToSave } = this + const blob = new Blob([ textEncoder.encode(textToSave) || ''], {type: mimetype}) + const _url = URL.createObjectURL(blob) + + const link = document.createElement('a') + link.setAttribute('href', _url) + link.setAttribute('download', filename) + link.style.visibility = 'hidden' + + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + } +} diff --git a/frontend/src/landmarks/landmarks.module.ts b/frontend/src/landmarks/landmarks.module.ts index 604291b2..aeb79561 100644 --- a/frontend/src/landmarks/landmarks.module.ts +++ b/frontend/src/landmarks/landmarks.module.ts @@ -6,18 +6,22 @@ import { ToolbarComponent } from './toolbar/toolbar.component'; import { SharedModule } from 'src/sharedModule/sharedModule'; import { OverlayComponent } from './overlay/overlay.component'; import { OverlayPositionPipe } from './overlay/overlayPosition.pipe'; +import { OverlayStemStylePipe } from './overlay/overlayStemStyle.pipe'; +import { IOModule } from 'src/io/module'; @NgModule({ declarations: [ ListviewComponent, ToolbarComponent, OverlayComponent, - OverlayPositionPipe + OverlayPositionPipe, + OverlayStemStylePipe, ], imports: [ CommonModule, FormsModule, SharedModule, + IOModule, ], exports: [ListviewComponent, ToolbarComponent, OverlayComponent], }) diff --git a/frontend/src/landmarks/listview/listview.component.html b/frontend/src/landmarks/listview/listview.component.html index deabfc82..b535d70a 100644 --- a/frontend/src/landmarks/listview/listview.component.html +++ b/frontend/src/landmarks/listview/listview.component.html @@ -20,8 +20,12 @@ To Template - @@ -30,14 +34,41 @@ To Incoming - + + + Delete + + + + - + + + + - \ No newline at end of file + +
+ No landmarks added +
+
\ No newline at end of file diff --git a/frontend/src/landmarks/listview/listview.component.scss b/frontend/src/landmarks/listview/listview.component.scss index e69de29b..a537f308 100644 --- a/frontend/src/landmarks/listview/listview.component.scss +++ b/frontend/src/landmarks/listview/listview.component.scss @@ -0,0 +1,20 @@ +.fallback-text +{ + display: flex; + justify-content: center; + margin: 2rem; + color: rgba(0, 0, 0, 0.5); +} + +table:has(.hovered) +{ + tr:not(.hovered, [mat-header-row]) + { + background-color: rgba(128, 128, 128, 0.2); + } +} + +tr +{ + transition: background-color 160ms ease-in-out; +} diff --git a/frontend/src/landmarks/listview/listview.component.ts b/frontend/src/landmarks/listview/listview.component.ts index 88bb0d7a..102a5f70 100644 --- a/frontend/src/landmarks/listview/listview.component.ts +++ b/frontend/src/landmarks/listview/listview.component.ts @@ -5,8 +5,12 @@ import { TrackByFunction, } from '@angular/core'; import { LandmarkPair, Landmark, INCOMING_LM_COLOR, REF_LM_COLOR } from '../const'; -import { Store } from '@ngrx/store'; -import { actions } from 'src/state/app'; +import { Store, select } from '@ngrx/store'; +import * as appState from "src/state/app" +import * as inputs from "src/state/inputs" +import * as outputs from "src/state/outputs" +import { combineLatest, firstValueFrom } from 'rxjs'; + @Component({ selector: 'voluba-landmark-listview', @@ -19,10 +23,14 @@ export class ListviewComponent { INCOMING_LM_COLOR = INCOMING_LM_COLOR REF_LM_COLOR = REF_LM_COLOR + hoveredLMP$ = this.store.pipe( + select(appState.selectors.hoveredLandmarkPair) + ) + @Input() landmarkPair: LandmarkPair[] = []; - displayedColumns: string[] = ['name', 'toTmpl', 'toInc']; + displayedColumns: string[] = ['delete', 'name', 'toTmpl', 'toInc']; public trackBy: TrackByFunction = ( _idx: number, @@ -30,13 +38,12 @@ export class ListviewComponent { ) => landmarkPair.id; constructor(private store: Store){ - } onUpdateName(landmarkPair: LandmarkPair, inputEvent: Event) { const { id } = landmarkPair this.store.dispatch( - actions.updateLandmarkPair({ + appState.actions.updateLandmarkPair({ id, value: { name: (inputEvent.target as HTMLInputElement).value || '' @@ -45,7 +52,41 @@ export class ListviewComponent { ) } - onClickLocation(landmark: Landmark) { - console.log(landmark); + async onClickLocation(landmark: Landmark) { + const [ inc, xform ] = await firstValueFrom( + combineLatest([ + this.store.pipe( + select(inputs.selectors.selectedIncoming) + ), + this.store.pipe( + outputs.selectors.getIncXform() + ) + ]) + ) + const position = [...landmark.position] + const { vec3 } = export_nehuba + if (landmark.targetVolumeId === inc?.['@id']) { + vec3.transformMat4(position, position, xform) + } + + this.store.dispatch( + appState.actions.navigateTo({ + position + }) + ) + } + + handleHoverLm(landmark: Landmark|null) { + this.store.dispatch( + appState.actions.hoverLandmark({ landmark }) + ) + } + + deleteLmp(landmarkPair: LandmarkPair) { + this.store.dispatch( + appState.actions.deleteLandmarkPair({ + landmarkPair + }) + ) } } diff --git a/frontend/src/landmarks/overlay/overlay.component.html b/frontend/src/landmarks/overlay/overlay.component.html index ad9f1f7f..a6782358 100644 --- a/frontend/src/landmarks/overlay/overlay.component.html +++ b/frontend/src/landmarks/overlay/overlay.component.html @@ -1,10 +1,23 @@
- + + +
diff --git a/frontend/src/landmarks/overlay/overlay.component.scss b/frontend/src/landmarks/overlay/overlay.component.scss index 4f25f4c4..c54cd3d5 100644 --- a/frontend/src/landmarks/overlay/overlay.component.scss +++ b/frontend/src/landmarks/overlay/overlay.component.scss @@ -9,18 +9,68 @@ } } +:host-context(.disabled) +{ + .landmark-container + { + pointer-events: none; + } +} + .landmark-container { display: inline-flex; - justify-content: center; - align-items: flex-end; + align-items: center; overflow: visible; width: 0; height: 0; + pointer-events: all; + + &.above-plane + { + flex-direction: column; + justify-content: flex-end; + } + + &.below-plane + { + flex-direction: column; + justify-content: flex-start; + } > * { color: inherit; flex: 0 0 auto; } + + > .stem + { + width: 1px; + } +} + + +@keyframes jumponce { + 0% { + transform: scale(1, 1); + } + 30% { + transform: scale(1.1, 0.8); + } + 40% { + transform: scale(0.8, 1.1) translateY(-5px); + } + 45% { + transform: scale(1, 1) translateY(-6px); + } + 80% { + transform: translateY(-4px); + } +} + + +.highlighted +{ + animation: jumponce 400ms normal; } diff --git a/frontend/src/landmarks/overlay/overlay.component.ts b/frontend/src/landmarks/overlay/overlay.component.ts index 63a02def..2fb00f64 100644 --- a/frontend/src/landmarks/overlay/overlay.component.ts +++ b/frontend/src/landmarks/overlay/overlay.component.ts @@ -1,7 +1,7 @@ -import { ChangeDetectionStrategy, Component, Inject, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, Inject, Input, Output } 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 { BehaviorSubject, Observable, Subject, combineLatest, concat, debounceTime, distinctUntilChanged, filter, map, merge, of, pipe, share, shareReplay, switchMap, tap, throttleTime } from 'rxjs'; +import { DEBOUNCED_WINDOW_RESIZE, OverlayLm, arrayEqual } from 'src/const'; import { Store, select } from '@ngrx/store'; import * as appState from "src/state/app" @@ -9,9 +9,6 @@ function isSliceView(input: export_nehuba.SliceView|null): input is export_nehub return !!input } -export type OverlayLm = Landmark & { color: string } - - /** * * TODO @@ -36,7 +33,13 @@ const pipeGetSliceViewChanged = () => { viewChanged$?.next(null) } ) - return viewChanged$.asObservable() + return viewChanged$.pipe( + /** + * n.b. + * delay seems to be necessary. secondary viewer landmarks do not seem to update accurately otherwise + */ + throttleTime(16) + ) }) ) } @@ -65,6 +68,13 @@ export class OverlayComponent { shareReplay(1) ) + @Output('overlay-hover-landmark') + hoverLandmark = new EventEmitter() + + + @Output('overlay-mousedown-landmark') + mousedownLandmark = new EventEmitter() + @Input('overlay-landmarks') set landmarks(lms: OverlayLm[]) { this.#landmarks$.next(lms) @@ -73,12 +83,7 @@ export class OverlayComponent { #sliceViewInfo$ = combineLatest([ concat( of(null), - merge( - this.dbResize, - this.store.pipe( - select(appState.selectors.isDefaultMode), - ) - ) + this.dbResize, ).pipe( debounceTime(32) ), @@ -132,16 +137,28 @@ export class OverlayComponent { ) }) }), + distinctUntilChanged((o, n) => + arrayEqual(o, n, (a, b) => { + return ( + a.highlighted === b.highlighted + && a.color === b.color + && arrayEqual(a.position, b.position) + ) + }) + ), shareReplay(1), ) constructor( @Inject(DEBOUNCED_WINDOW_RESIZE) private dbResize: Observable, - private store: Store, - ){ - this.#sliceViewChanged$.subscribe(val => { - console.log("view changed", val) - }) + ){} + + handleHoverLandmark(lm: OverlayLm|null){ + this.hoverLandmark.emit(lm) + } + + handleMouseDown(lm: OverlayLm) { + this.mousedownLandmark.emit(lm) } } diff --git a/frontend/src/landmarks/overlay/overlayPosition.pipe.ts b/frontend/src/landmarks/overlay/overlayPosition.pipe.ts index 4cd29167..67469200 100644 --- a/frontend/src/landmarks/overlay/overlayPosition.pipe.ts +++ b/frontend/src/landmarks/overlay/overlayPosition.pipe.ts @@ -10,7 +10,9 @@ 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)` + SecurityContext.STYLE, { + transform: `translate(${position[0]}px, ${position[1]}px)`, + } ) } } diff --git a/frontend/src/landmarks/overlay/overlayStemStyle.pipe.ts b/frontend/src/landmarks/overlay/overlayStemStyle.pipe.ts new file mode 100644 index 00000000..8df46b68 --- /dev/null +++ b/frontend/src/landmarks/overlay/overlayStemStyle.pipe.ts @@ -0,0 +1,23 @@ +import { Pipe, PipeTransform, SecurityContext } from "@angular/core"; +import { DomSanitizer } from "@angular/platform-browser"; + +@Pipe({ + name: 'overlayStemStyle', + pure: true +}) + +export class OverlayStemStylePipe implements PipeTransform{ + constructor(private sanitizer: DomSanitizer){ + + } + transform(landmark: {color: string, position: number[]}) { + return this.sanitizer.sanitize( + SecurityContext.STYLE, + { + height: `${Math.abs(landmark.position[2])}px`, + backgroundColor: landmark.color, + order: landmark.position[2] < 0 ? -1 : 0 + } + ) + } +} diff --git a/frontend/src/landmarks/toolbar/toolbar.component.html b/frontend/src/landmarks/toolbar/toolbar.component.html index f8e898ec..7b9a48cb 100644 --- a/frontend/src/landmarks/toolbar/toolbar.component.html +++ b/frontend/src/landmarks/toolbar/toolbar.component.html @@ -1,26 +1,32 @@ - + - + - + - + - + - + - + + + diff --git a/frontend/src/landmarks/toolbar/toolbar.component.ts b/frontend/src/landmarks/toolbar/toolbar.component.ts index 2dec0d63..167c4cc1 100644 --- a/frontend/src/landmarks/toolbar/toolbar.component.ts +++ b/frontend/src/landmarks/toolbar/toolbar.component.ts @@ -1,11 +1,38 @@ import { ChangeDetectionStrategy, Component, Inject, Optional, inject } from '@angular/core'; import { select, Store } from '@ngrx/store'; -import { NEVER, filter, map, of, switchMap, takeUntil, withLatestFrom } from 'rxjs'; -import { VOLUBA_NEHUBA_TOKEN, VolubeNehuba, isVec3 } from 'src/const'; +import { NEVER, combineLatest, filter, finalize, firstValueFrom, lastValueFrom, map, of, switchMap, takeUntil, withLatestFrom } from 'rxjs'; +import { EXPORT_LANDMARKS_TYPE, VOLUBA_NEHUBA_TOKEN, VolubeNehuba, isVec3 } from 'src/const'; import * as app from "src/state/app" import * as inputs from "src/state/inputs" +import * as outputs from "src/state/outputs" +import * as generalActions from "src/state/actions" import { DestroyDirective } from 'src/util/destroy.directive'; +type L = { + id: string + name: string + coord: number[] // in mm + active: true +} +type LP = { + id: string + refId: string + incId: string + name: string + active: true + // color: string // ignore for now +} + + +type LMJson = { + reference_volume: string + incoming_volume: string + reference_landmarks: L[] + incoming_landmarks: L[] + landmark_pairs: LP[] + '@type': 'EXPORT_LANDMARKS_TYPE' +} + @Component({ selector: 'voluba-landmark-toolbar', templateUrl: './toolbar.component.html', @@ -19,10 +46,6 @@ export class ToolbarComponent { #destroyed$ = inject(DestroyDirective).destroyed$ - bla = this.store.pipe( - select(app.selectors.purgatory), - ) - #addLmMode: boolean = false addLmMode$ = this.store.pipe( select(app.selectors.addLmMode) @@ -37,6 +60,74 @@ export class ToolbarComponent { : of(null)) ) + view$ = combineLatest([ + this.store.pipe( + select(app.selectors.landmarks) + ), + this.store.pipe( + select(inputs.selectors.inputFilesName) + ), + this.store.pipe( + select(inputs.selectors.selectedTemplate) + ), + this.store.pipe( + select(inputs.selectors.selectedIncoming) + ), + this.store.pipe( + outputs.selectors.getIncXform() + ) + ]).pipe( + map(([ landmarks, inputFilesName, ref, inc, xform ]) => { + + const { mat4, vec3 } = export_nehuba + + const refLm: L[] = landmarks.map(lm => { + return { + id: `${lm.id}-ref`, + active: true, + coord: lm.tmplLm.position.map(v => v/1e6), + name: `${lm.name}-ref` + } + }) + + const incLm: L[] = landmarks.map(lm => { + const posInIncSpace = vec3.transformMat4( + vec3.create(), + vec3.fromValues(...lm.incLm.position), + xform) + return { + id: `${lm.id}-inc`, + active: true, + coord: Array.from(posInIncSpace).map(v => v * 1e6), + name: `${lm.name}-inc`, + } + }) + + const lmP: LP[] = landmarks.map(lm => { + return { + id: lm.id, + name: lm.name, + active: true, + incId: `${lm.id}-inc`, + refId: `${lm.id}-ref`, + } + }) + const landmarkContent = { + reference_volume: ref?.name || 'Untitled Reference Volume', + incoming_volume: inc?.name || 'Untitled Incoming Volume', + reference_landmarks: refLm, + incoming_landmarks: incLm, + landmark_pairs: lmP, + ['@type']: EXPORT_LANDMARKS_TYPE + } + + return { + landmarkContent: JSON.stringify(landmarkContent, null, 2), + landmarkName: `${inputFilesName}-landmarks.json`, + } + }) + ) + constructor( private store: Store, @Optional() @Inject(VOLUBA_NEHUBA_TOKEN) private vn: VolubeNehuba @@ -98,20 +189,6 @@ export class ToolbarComponent { }) } - #tmp = 0 - _tmp() { - this.#tmp ++ - console.log(this.#tmp) - this.store.dispatch( - app.actions.addLandmark({ - landmark: { - position: [0, 0, 0], - targetVolumeId: `foo-bar-${this.#tmp}` - } - }) - ) - } - toggleLandmarkMode() { this.store.dispatch( app.actions.setAddLandmarkMode({ @@ -119,13 +196,79 @@ export class ToolbarComponent { }) ) } + async handleLoadJson(jsonTxt: string){ + const lm: LMJson = JSON.parse(jsonTxt) + const { selectedRefenceVolume, selectedIncomingVolume, landmarks } = await firstValueFrom( + combineLatest([ + this.store.pipe( + select(inputs.selectors.selectedTemplate) + ), + this.store.pipe( + select(inputs.selectors.selectedIncoming) + ), + this.store.pipe( + select(app.selectors.landmarks) + ) + ]).pipe( + map(([ selectedRefenceVolume, selectedIncomingVolume, landmarks ]) => ({ selectedRefenceVolume, selectedIncomingVolume, landmarks })), + ) + ) + if (!selectedRefenceVolume || !selectedIncomingVolume) { + throw new Error(`Ref volume or incoming volume not yet selected`) + } - onSave() { - console.log('onSave'); - } + const currLmIdSet = new Set(landmarks.map(lm => lm.id)) + + const { reference_landmarks, incoming_landmarks, landmark_pairs } = lm + + const info: string[] = [] + + const existingLms = landmark_pairs.filter(lmp => currLmIdSet.has(lmp.id)) + if (existingLms.length > 0) { + info.push(`Landmarks with ids ${existingLms.map(lm => lm.id)} already exist. Skipped.`) + } + const newLms = landmark_pairs.filter(lmp => !currLmIdSet.has(lmp.id)) - onLoad() { - console.log('onLoad'); + let addedLmCounter = 0 + for (const lm of newLms) { + const { id, incId, refId, name } = lm + const inc = incoming_landmarks.find(incLm => incLm.id === incId) + const ref = reference_landmarks.find(refLm => refLm.id === refId) + if (!inc || !ref) { + info.push(`Landmark with id ${id}, invalid ref or inc reference.`) + continue + } + console.log(inc.coord, ref.coord) + this.store.dispatch( + app.actions.addLandmarkPair({ + landmarkPair: { + id, + incLm: { + position: inc.coord.map(v => v*1e6) as [number, number, number], + targetVolumeId: selectedIncomingVolume['@id'] + }, + tmplLm: { + position: ref.coord.map(v => v*1e6) as [number, number, number], + targetVolumeId: selectedRefenceVolume['@id'] + }, + name + } + }) + ) + addedLmCounter ++ + } + + if (addedLmCounter > 0) { + info.push(`Added ${addedLmCounter} landmark pairs.`) + } + + if (info.length > 0) { + this.store.dispatch( + generalActions.info({ + message: info.join("\n") + }) + ) + } } onCalculate() { diff --git a/frontend/src/layer-tune/tune-ui/tune-ui.component.html b/frontend/src/layer-tune/tune-ui/tune-ui.component.html index 19ed0e6c..adea60fc 100644 --- a/frontend/src/layer-tune/tune-ui/tune-ui.component.html +++ b/frontend/src/layer-tune/tune-ui/tune-ui.component.html @@ -20,33 +20,45 @@ -
- - Isotropic - -
- -
- - - Scale X - - - - - - Scale Y - - - - - - Scale Z - - - -
+ + + +
+ + Isotropic + +
+ +
+ + + Scale X + + + + + + Scale Y + + + + + + Scale Z + + + +
+
+ + + + +
+ + +
diff --git a/frontend/src/views/viewer/viewer.component.scss b/frontend/src/views/viewer/viewer.component.scss index c8f9b4dd..117ee319 100644 --- a/frontend/src/views/viewer/viewer.component.scss +++ b/frontend/src/views/viewer/viewer.component.scss @@ -50,6 +50,37 @@ div.viewer-wrapper z-index: 1; pointer-events: none; } + + > .add-lm-overlay + { + z-index: 2; + pointer-events: all; + width: 100%; + height: 100%; + background-color: rgba(128, 128, 128, 0.8); + + display: flex; + align-items: center; + + &.align-right + { + justify-content: flex-end; + text-align: right; + } + + &.align-left + { + justify-content: flex-start; + text-align: left; + } + + > * + { + flex: 0 0 2rem; + font-size: xx-large; + line-height: 1; + } + } } .four-quadrant @@ -74,3 +105,8 @@ div.viewer-wrapper background-color: rgba(0, 0, 255, 0.2); } } + +.navigation-card +{ + margin: 1rem; +} diff --git a/frontend/src/views/viewer/viewer.component.ts b/frontend/src/views/viewer/viewer.component.ts index f2ef43ce..85386fb0 100644 --- a/frontend/src/views/viewer/viewer.component.ts +++ b/frontend/src/views/viewer/viewer.component.ts @@ -2,9 +2,10 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, - Inject, Injectable, + QueryList, ViewChild, + ViewChildren, inject, } from '@angular/core'; import { select, Store } from '@ngrx/store'; @@ -20,25 +21,55 @@ import { NEVER, concat, of, + BehaviorSubject, + firstValueFrom, + merge, } 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 { 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'; +import * as inputs from 'src/state/inputs'; +import { NehubaNavigation, NehubaViewerWrapperComponent } from '../nehuba-viewer-wrapper/nehuba-viewer-wrapper.component'; +import { Mat4, OverlayLm, VOLUBA_NEHUBA_TOKEN } from 'src/const'; +import { INCOMING_LM_COLOR, Landmark, LandmarkPair, REF_LM_COLOR } from 'src/landmarks/const'; +import { Actions, ofType } from '@ngrx/effects'; + + +const REF_VOL_ID = 'reference-volume' +const INC_VOL_ID = 'incoming-volume' + +const _BIGBRAIN_NEHUBA_LAYER = { + id: REF_VOL_ID, + url: 'precomputed://https://neuroglancer.humanbrainproject.org/precomputed/BigBrainRelease.2015/8bit' && 'precomputed://http://127.0.0.1:8080/sharded/BigBrainRelease.2015/8bit', + transform: [ + [1, 0, 0, -70677184], + [0, 1, 0, -70010000], + [0, 0, 1, -58788284], + [0, 0, 0, 1], + ] as Mat4, +} + +const _INC_LAYER = { + id: INC_VOL_ID, + url: 'precomputed://https://neuroglancer.humanbrainproject.eu/precomputed/JuBrain/v2.2c/colin27_seg' && 'precomputed://http://127.0.0.1:8080/sharded/WHS_SD_rat/templates/v1.01/t2star_masked/', + transform: [ + [1.0, 0.0, 0.0, 0], + [0.0, 1.0, 0.0, 0], + [0.0, 0.0, 1.0, 0], + [0.0, 0.0, 0.0, 1.0], + ] as Mat4, +} @Injectable() class NehubaSvc { #mouseover = new Subject() #mousedown = new Subject() + #mouseup = new Subject() mouseover = this.#mouseover.asObservable() mousedown = this.#mousedown.asObservable() + mouseup = this.#mouseup.asObservable() setMouseOver(val: Float32Array){ this.#mouseover.next(val) @@ -46,6 +77,64 @@ class NehubaSvc { setMouseDown(ev: MouseEvent) { this.#mousedown.next(ev) } + setMouseUp(ev: MouseEvent) { + this.#mouseup.next(ev) + } +} + +class LandmarkSvc { + static REF_LM_COLOR = "#ffffff" + static INCOMING_LM_COLOR = "#ffff00" + + static GetXformToOverlay(xform: export_nehuba.mat4, hoveredLMP: LandmarkPair|undefined|null) { + const { vec3 } = export_nehuba + return { + getReference: function(landmark: LandmarkPair): OverlayLm { + return { + ...landmark.tmplLm, + color: LandmarkSvc.REF_LM_COLOR, + id: `${landmark.id}-ref`, + highlighted: hoveredLMP?.id === landmark.id, + } + }, + getIncoming: function(landmark: LandmarkPair): OverlayLm { + + const newPos = vec3.fromValues(...landmark.incLm.position) + vec3.transformMat4(newPos, newPos, xform) + return { + ...landmark.incLm, + color: LandmarkSvc.INCOMING_LM_COLOR, + position: Array.from(newPos), + id: `${landmark.id}-inc`, + highlighted: hoveredLMP?.id === landmark.id, + } + } + } + } + + static TranslateOverlayLmToLm(landmarks: LandmarkPair[], overlayLm: OverlayLm): { + landmarkPair: LandmarkPair + landmark: Landmark + } | null { + let searchId: string|null = null + let key: 'tmplLm'|'incLm'|undefined + if (overlayLm.id.endsWith("-inc")) { + searchId = overlayLm.id.replace(/-inc$/, '') + key = 'incLm' + } + if (overlayLm.id.endsWith("-ref")) { + searchId = overlayLm.id.replace(/-ref$/, '') + key = 'tmplLm' + } + const foundLmp = searchId && landmarks.find(lm => lm.id === searchId) + if (!foundLmp){ + return null + } + return { + landmarkPair: foundLmp, + landmark: foundLmp[key!] + } + } } @Component({ @@ -64,103 +153,108 @@ class NehubaSvc { } ], hostDirectives: [ - DestroyDirective + MouseInteractionDirective, ] }) export class ViewerComponent implements AfterViewInit { - #destroyed$ = inject(DestroyDirective).destroyed$ + + selfMouseEv = inject(MouseInteractionDirective) + #destroyed$ = inject(MouseInteractionDirective).destroyed$ @ViewChild(NehubaViewerWrapperComponent) - viewerWrapper: NehubaViewerWrapperComponent | undefined; + viewerWrapper: NehubaViewerWrapperComponent | undefined + + @ViewChildren(NehubaViewerWrapperComponent) + viewerWrappers: QueryList | undefined @ViewChild(MouseInteractionDirective) - mouseInteractive: MouseInteractionDirective | undefined; + mouseInteractive: MouseInteractionDirective | undefined - public incLocked$ = this.store.pipe(select(app.selectors.incLocked)); + public incLocked$ = this.store.pipe(select(appState.selectors.incLocked)) - view$ = combineLatest([ - this.incLocked$, + #primaryNehubaNav = new BehaviorSubject({ + orientation: new Float32Array([0, 0, 0, 1]), + position: new Float32Array([0, 0, 0]), + zoom: 1e5, + }) + + setPrimaryNav(nav: NehubaNavigation){ + this.#primaryNehubaNav.next(nav) + } + + lmView$ = combineLatest([ this.store.pipe( - select(app.selectors.isDefaultMode) + select(appState.selectors.landmarks) ), - this.store.pipe(getIncXform()), this.store.pipe( - select(app.selectors.landmarks) + select(appState.selectors.addLmMode) ), - 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.hoveredLandmarkPair), ), this.store.pipe( select(appState.selectors.purgatory) ), + ]).pipe( + map(([ landmarks, addLandmarkMode, hoveredLmp, purgatory ]) => { + return { + landmarks, addLandmarkMode, hoveredLmp, purgatory + } + }) + ) + + view$ = combineLatest([ + + this.lmView$, + this.incLocked$, this.store.pipe( - select(appState.selectors.addLmMode) + select(appState.selectors.isDefaultMode) ), + this.store.pipe(outputs.selectors.getIncXform()), concat( of(null), this.nehubaSvc.mouseover, + ), + this.#primaryNehubaNav, + concat( + of(false), + merge( + this.selfMouseEv.mousedown.pipe( + map(() => true) + ), + this.selfMouseEv.mouseup.pipe( + map(() => false) + ) + ) ) ]).pipe( - map(([incLocked, isDefaultMode, xform, landmarks, sliceViews, purgatory, addLandmarkMode, mouseover]) => { + map(([ { landmarks, addLandmarkMode, hoveredLmp, purgatory }, incLocked, isDefaultMode, xform, mouseover, primaryNavigation, isDraggingViewer ]) => { + const { mat4 } = export_nehuba + + const { getIncoming, getReference } = LandmarkSvc.GetXformToOverlay(xform, hoveredLmp) const storedLandmarks = landmarks.map(lm => { - const incomingLandmarks: (Landmark & {color:string})[] = [] + const incomingLandmarks: OverlayLm[] = [] 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, - }) + incomingLandmarks.push(getIncoming(lm)) } return [ - { - ...lm.tmplLm, - color: REF_LM_COLOR - }, + getReference(lm), ...incomingLandmarks ] }) - const purgatoryLandmarks = [] + const purgatoryLandmarks: OverlayLm[] = [] if (purgatory) { purgatoryLandmarks.push({ ...purgatory, - color: REF_LM_COLOR + color: REF_LM_COLOR, + id: 'purgatory' }) } if (addLandmarkMode && mouseover) { purgatoryLandmarks.push({ + id: 'purgatory', targetVolumeId: 'purgatory', position: Array.from(mouseover) as [number, number, number], color: purgatory @@ -169,6 +263,29 @@ export class ViewerComponent implements AfterViewInit { }) } + const secondaryLandmarks: OverlayLm[] = [] + + if (!isDefaultMode) { + secondaryLandmarks.push( + ...landmarks.map(lm => { + return getIncoming(lm) + }) + ) + + if (addLandmarkMode && mouseover && purgatory) { + secondaryLandmarks.push({ + id: 'purgatory', + targetVolumeId: 'purgatory', + position: Array.from(mouseover), + color: INCOMING_LM_COLOR + }) + } + } + + + const tXform = mat4.transpose(mat4.create(), xform) + const atXform = Array.from(tXform) + return { incLocked, isDefaultMode, @@ -176,23 +293,38 @@ export class ViewerComponent implements AfterViewInit { ...storedLandmarks.flatMap(v => v), ...purgatoryLandmarks ], - sliceViews, + secondaryLandmarks, + showAddingRefLmOverlay: !isDefaultMode && addLandmarkMode && !purgatory, + showAddingIncLmOverlay: !isDefaultMode && addLandmarkMode && purgatory, + primaryLayers: [ _BIGBRAIN_NEHUBA_LAYER, _INC_LAYER ], + secondaryLayers: [ { + ..._INC_LAYER, + transform: [ + atXform.slice(0, 4), + atXform.slice(4, 8), + atXform.slice(8, 12), + atXform.slice(12, 16), + ] as Mat4 + }], + primaryNavigation, + isDraggingViewer, + addLandmarkMode, } }) ); toggleIncLocked() { - this.store.dispatch(app.actions.toggleIncLocked()); + this.store.dispatch(appState.actions.toggleIncLocked()); } toggleMode() { - this.store.dispatch(app.actions.toggleMode()) + this.store.dispatch(appState.actions.toggleMode()) } constructor( private store: Store, private nehubaSvc: NehubaSvc, - @Inject(SLICEVIEWS_INJECTION_TOKEN) private sliceViewProvider: SliceViewProviderType, + private actions$: Actions ) {} onMousePosition(pos: Float32Array){ @@ -203,6 +335,10 @@ export class ViewerComponent implements AfterViewInit { this.nehubaSvc.setMouseDown(ev) } + onMouseUp(ev: MouseEvent) { + this.nehubaSvc.setMouseUp(ev) + } + ngAfterViewInit(): void { if (!this.mouseInteractive) { throw new Error(`Cannot find mouse interactive directive`); @@ -227,7 +363,7 @@ export class ViewerComponent implements AfterViewInit { select(appState.selectors.isDefaultMode) ), this.store.pipe( - select(app.selectors.addLmMode) + select(appState.selectors.addLmMode) ), this.mouseInteractive.volubaDrag, @@ -241,7 +377,7 @@ export class ViewerComponent implements AfterViewInit { })) ) }) - ); + ) dragOnIncVol$.pipe( filter((v) => !!v && !v.mouseDownEvent.shiftKey), @@ -268,8 +404,8 @@ export class ViewerComponent implements AfterViewInit { outputs.actions.setIncTranslation({ array: Array.from(pos), }) - ); - }); + ) + }) dragOnIncVol$.pipe( filter((v) => !!v && v.mouseDownEvent.shiftKey), @@ -319,14 +455,14 @@ export class ViewerComponent implements AfterViewInit { array: Array.from(finalRotation), }) ); - }); + }) this.store.pipe( - select(outputs.selectors.incMatrixMat4), + outputs.selectors.getIncXform(), takeUntil(this.#destroyed$), ).subscribe((v) => { - this.viewerWrapper?.setLayerProperty('bla', { + this.viewerWrapper?.setLayerProperty(INC_VOL_ID, { transform: v, }) }) @@ -335,9 +471,156 @@ export class ViewerComponent implements AfterViewInit { select(appState.selectors.isDefaultMode), takeUntil(this.#destroyed$) ).subscribe(isDefaultMode => { - this.viewerWrapper?.setLayerProperty('bla', { + this.viewerWrapper?.setLayerProperty(INC_VOL_ID, { visible: isDefaultMode }) }) + + this.actions$.pipe( + ofType(appState.actions.navigateTo), + takeUntil(this.#destroyed$), + ).subscribe(({ position }) => { + if (!this.viewerWrappers) { + return + } + this.viewerWrappers.forEach(vw => { + vw.setPosition(position) + }) + }) + + this.#mousedownLm.pipe( + takeUntil(this.#destroyed$), + withLatestFrom( + this.store.pipe( + select(appState.selectors.hoveredLandmark), + ), + this.store.pipe( + select(appState.selectors.hoveredLandmarkPair), + ), + this.store.pipe( + select(inputs.selectors.selectedTemplate), + ), + this.store.pipe( + select(inputs.selectors.selectedIncoming), + ), + this.store.pipe( + outputs.selectors.getIncXform() + ) + ), + filter(([ _, hoveredLandmark ]) => { + return !!hoveredLandmark + }), + switchMap(([ + _, + hoveredLandmark, + hoveredLandmarkPair, + selectedTemplate, + selectedIncoming, + xform, + ]) => this.nehubaSvc.mouseover.pipe( + filter(v => !!v), + takeUntil(this.nehubaSvc.mouseup), + map(position => { + return { + hoveredLandmark, + hoveredLandmarkPair, + selectedTemplate, + selectedIncoming, + position, + xform, + } + }), + )) + ).subscribe(({ hoveredLandmark, hoveredLandmarkPair, selectedTemplate, selectedIncoming, position, xform }) => { + let lmkey: 'incLm' | 'tmplLm' | null = null + if (!hoveredLandmark?.targetVolumeId || !hoveredLandmarkPair?.id) { + console.error(`hoveredLandmark.targetVolumeId must be defined!`) + return + } + + let pos: export_nehuba.vec3 + if (hoveredLandmark?.targetVolumeId === selectedTemplate?.['@id']) { + lmkey = 'tmplLm' + pos = position + } + if (hoveredLandmark?.targetVolumeId === selectedIncoming?.['@id']) { + lmkey = 'incLm' + const { vec3, mat4 } = export_nehuba + const invert = mat4.invert(mat4.create(), xform) + pos = vec3.transformMat4(vec3.create(), position, invert) + } + + if (!lmkey) { + // cannot find if the hovered lm is targeting inc or ref, maybe it's targetting purgatory? + return + } + + this.store.dispatch( + appState.actions.updateLandmarkPair({ + id: hoveredLandmarkPair.id, + value: { + [lmkey]: { + targetVolumeId: hoveredLandmark?.targetVolumeId, + position: Array.from(pos!) + } + } + }) + ) + }) + + } + + async resetOrientation(target: 'reference'|'incoming'){ + if (!this.viewerWrappers) { + return + } + if (target === "reference") { + this.viewerWrappers.forEach(wrapper => { + wrapper.setOrientation([0, 0, 0, 1]) + }) + } + if (target === "incoming") { + const xform = await firstValueFrom( + this.store.pipe( + outputs.selectors.getIncXform(), + ) + ) + const { mat4, quat } = export_nehuba + const orientation = mat4.getRotation(quat.create(), xform) + const orientationArray = Array.from(orientation) + this.viewerWrappers.forEach(wrapper => { + wrapper.setOrientation(orientationArray) + }) + } + } + + #mousedownLm = new Subject() + handleMousedownLandmark(overlayLm: OverlayLm) { + this.#mousedownLm.next(overlayLm) + } + + async handleHoverLandmark(overlayLm: OverlayLm|null){ + if (!overlayLm) { + this.store.dispatch( + appState.actions.hoverLandmark({ + landmark: null + }) + ) + return + } + + const landmarks = await firstValueFrom( + this.store.pipe( + select(appState.selectors.landmarks) + ) + ) + + const foundLm = LandmarkSvc.TranslateOverlayLmToLm(landmarks, overlayLm) + + this.store.dispatch( + appState.actions.hoverLandmark({ + landmark: foundLm && foundLm.landmark + }) + ) } } diff --git a/frontend/src/views/views.module.ts b/frontend/src/views/views.module.ts index f16bb740..9dbada66 100644 --- a/frontend/src/views/views.module.ts +++ b/frontend/src/views/views.module.ts @@ -13,11 +13,14 @@ import { MatIconModule } from '@angular/material/icon'; import { LandmarksModule } from 'src/landmarks/landmarks.module'; import { HistoryModule } from 'src/history/history.module'; import { NehubaViewerWrapperComponent } from './nehuba-viewer-wrapper/nehuba-viewer-wrapper.component'; -import { MouseInteractionsModule } from 'src/mouse-interactions/mouse-interactions.module'; import { MatDialogModule } from '@angular/material/dialog'; import { UtilModule } from 'src/util/util.module'; import { DragDropModule } from '@angular/cdk/drag-drop'; import { LayerTuneModule } from 'src/layer-tune/layer-tune.module'; +import { ShareExportComponent } from './shareExport/shareExport.component'; +import { SharedModule } from 'src/sharedModule/sharedModule'; +import { DisplayNumArrayPipe } from './viewer/displayNumArray.pipe'; +import { MouseInteractionDirective } from 'src/mouse-interactions/mouse-interaction.directive'; @NgModule({ declarations: [ @@ -26,6 +29,8 @@ import { LayerTuneModule } from 'src/layer-tune/layer-tune.module'; ViewerComponent, ControlMenuComponent, NehubaViewerWrapperComponent, + ShareExportComponent, + DisplayNumArrayPipe, ], imports: [ CommonModule, @@ -37,11 +42,16 @@ import { LayerTuneModule } from 'src/layer-tune/layer-tune.module'; MatIconModule, LandmarksModule, HistoryModule, - MouseInteractionsModule, MatDialogModule, UtilModule, DragDropModule, LayerTuneModule, + SharedModule, + + /** + * standalone directives + */ + MouseInteractionDirective, ], exports: [WelcomeCardComponent, ViewerComponent], })