From f537e3bdb7e48782fa6f3e55f352897f866c46e1 Mon Sep 17 00:00:00 2001 From: Xiao Gui <xgui3783@gmail.com> Date: Wed, 2 Aug 2023 16:00:28 +0200 Subject: [PATCH] fix z plane traversal --- common/helpOnePager.md | 2 +- docs/releases/v2.12.4.md | 1 + src/atlasComponents/sapi/translateV3.ts | 3 +- src/state/userPreference/actions.ts | 7 ++ src/state/userPreference/selectors.ts | 5 ++ src/state/userPreference/store.ts | 11 ++++ src/ui/config/configCmp/config.component.ts | 64 ++++++++++++++++++- src/ui/config/configCmp/config.template.html | 20 ++++++ src/ui/config/module.ts | 4 +- src/viewerModule/module.ts | 45 ++++++++++++- .../layerCtrl.service/layerCtrl.util.ts | 9 +++ .../nehubaViewer/nehubaViewer.component.ts | 55 ++++++++++++++-- .../perspectiveViewSlider.component.ts | 25 ++------ 13 files changed, 218 insertions(+), 33 deletions(-) diff --git a/common/helpOnePager.md b/common/helpOnePager.md index 8d621680d..2f66d4b38 100644 --- a/common/helpOnePager.md +++ b/common/helpOnePager.md @@ -5,7 +5,7 @@ | Zoom | `[mousewheel]` | `[pinch zoom]` | | Zoom | `[hover]` on any slice views > `[click]` magnifier | `[tap]` on magnifier | | Next slice | `<ctrl>` + `[mousewheel]` | - | -| Next 10 slice | `<ctrl>` + `<shift>` + `[mousewheel]` | - | +| Next 10 slice | `<shift>` + `[mousewheel]` | - | | Toggle delineation | `[q]` | - | | Toggle cross hair | `[a]` | - | | Multiple region select | `<ctrl>` + `[click]` on region | - | diff --git a/docs/releases/v2.12.4.md b/docs/releases/v2.12.4.md index c0b033626..d58bf258e 100644 --- a/docs/releases/v2.12.4.md +++ b/docs/releases/v2.12.4.md @@ -4,3 +4,4 @@ - minor fix of ng-layer-tune incorrectly applying color map - prepare for MEBRAINS update +- fixed traverse in z direction (`<ctrl>` + `[wheel]`/`<shift>` + `[wheel]`) diff --git a/src/atlasComponents/sapi/translateV3.ts b/src/atlasComponents/sapi/translateV3.ts index d7b7b75a2..2597c367b 100644 --- a/src/atlasComponents/sapi/translateV3.ts +++ b/src/atlasComponents/sapi/translateV3.ts @@ -157,7 +157,8 @@ class TranslateV3 { const { resolution, size } = _info.scales[0] const info = { voxel: size as [number, number, number], - real: [0, 1, 2].map(idx => resolution[idx] * size[idx]) as [number, number, number] + real: [0, 1, 2].map(idx => resolution[idx] * size[idx]) as [number, number, number], + resolution: resolution as [number, number, number] } returnObj.push({ source: `precomputed://${url}`, diff --git a/src/state/userPreference/actions.ts b/src/state/userPreference/actions.ts index 05d96e5ca..55dffce1d 100644 --- a/src/state/userPreference/actions.ts +++ b/src/state/userPreference/actions.ts @@ -44,3 +44,10 @@ export const setShowExperimental = createAction( flag: boolean }>() ) + +export const setZMultiplier = createAction( + `${nameSpace} setZMultiplier`, + props<{ + value: number + }>() +) diff --git a/src/state/userPreference/selectors.ts b/src/state/userPreference/selectors.ts index 00856d016..859ab0f17 100644 --- a/src/state/userPreference/selectors.ts +++ b/src/state/userPreference/selectors.ts @@ -4,6 +4,11 @@ import { UserPreference } from "./store" const storeSelector = store => store[nameSpace] as UserPreference +export const overrideZTraversalMultiplier = createSelector( + storeSelector, + state => state.overrideZTraversalMultiplier +) + export const useAnimation = createSelector( storeSelector, state => state.useAnimation diff --git a/src/state/userPreference/store.ts b/src/state/userPreference/store.ts index 2a4fa314a..222832b7a 100644 --- a/src/state/userPreference/store.ts +++ b/src/state/userPreference/store.ts @@ -7,6 +7,8 @@ import { maxGpuLimit, CSP } from "./const" export const defaultGpuLimit = maxGpuLimit export type UserPreference = { + overrideZTraversalMultiplier: number + useMobileUi: boolean gpuLimit: number useAnimation: boolean @@ -19,6 +21,8 @@ export type UserPreference = { } export const defaultState: UserPreference = { + overrideZTraversalMultiplier: null, + useMobileUi: JSON.parse(localStorage.getItem(LOCAL_STORAGE_CONST.MOBILE_UI)), gpuLimit: Number(localStorage.getItem(LOCAL_STORAGE_CONST.GPU_LIMIT)) || defaultGpuLimit, useAnimation: !localStorage.getItem(LOCAL_STORAGE_CONST.ANIMATION), @@ -100,5 +104,12 @@ export const reducer = createReducer( ...state, showExperimental: flag }) + ), + on( + actions.setZMultiplier, + (state, { value }) => ({ + ...state, + overrideZTraversalMultiplier: value + }) ) ) diff --git a/src/ui/config/configCmp/config.component.ts b/src/ui/config/configCmp/config.component.ts index b3d556c87..386293b19 100644 --- a/src/ui/config/configCmp/config.component.ts +++ b/src/ui/config/configCmp/config.component.ts @@ -1,13 +1,17 @@ -import { Component, OnDestroy, OnInit } from '@angular/core' +import { Component, Inject, OnDestroy, OnInit, Optional } from '@angular/core' import { select, Store } from '@ngrx/store'; -import { combineLatest, Observable, Subscription } from 'rxjs'; +import { combineLatest, Observable, of, Subscription } from 'rxjs'; import { debounceTime, distinctUntilChanged, map, startWith } from 'rxjs/operators'; import { isIdentityQuat } from 'src/viewerModule/nehuba/util'; import { MatSlideToggleChange } from "@angular/material/slide-toggle"; import { MatSliderChange } from "@angular/material/slider"; import { atlasSelection, userPreference, userInterface } from 'src/state'; import { environment } from "src/environments/environment" +import { Z_TRAVERSAL_MULTIPLIER } from 'src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util'; +import { FormControl, FormGroup } from '@angular/forms'; + +const Z_TRAVERSAL_TOOLTIP = `Value to use when traversing z level. If toggled off, will use the voxel dimension of the template.` const GPU_TOOLTIP = `Higher GPU usage can cause crashes on lower end machines` const ANIMATION_TOOLTIP = `Animation can cause slowdowns in lower end machines` const MOBILE_UI_TOOLTIP = `Mobile UI enables touch controls` @@ -24,6 +28,15 @@ const OBLIQUE_ROOT_TEXT_ORDER: [string, string, string, string] = ['Slice View 1 export class ConfigComponent implements OnInit, OnDestroy { + customZFormGroup = new FormGroup({ + customZValue: new FormControl<number>({ + value: 1, + disabled: true + }), + customZFlag: new FormControl<boolean>(false) + }) + + public Z_TRAVERSAL_TOOLTIP = Z_TRAVERSAL_TOOLTIP public GPU_TOOLTIP = GPU_TOOLTIP public ANIMATION_TOOLTIP = ANIMATION_TOOLTIP public MOBILE_UI_TOOLTIP = MOBILE_UI_TOOLTIP @@ -70,6 +83,7 @@ export class ConfigComponent implements OnInit, OnDestroy { constructor( private store: Store<any>, + @Optional() @Inject(Z_TRAVERSAL_MULTIPLIER) private zTraversalMult$: Observable<number> = of(1) ) { this.gpuLimit$ = this.store.pipe( @@ -112,6 +126,52 @@ export class ConfigComponent implements OnInit, OnDestroy { public ngOnInit() { this.subscriptions.push( this.panelOrder$.subscribe(panelOrder => this.panelOrder = panelOrder), + combineLatest([ + this.zTraversalMult$, + this.store.pipe( + select(userPreference.selectors.overrideZTraversalMultiplier), + map(val => !!val) + ), + ]).subscribe(([ zVal, customZFlag ]) => { + this.customZFormGroup.setValue({ + customZFlag: customZFlag, + customZValue: zVal + }) + }), + this.customZFormGroup.valueChanges.pipe( + map(v => v.customZFlag), + distinctUntilChanged(), + ).subscribe(customZFlag => { + if (customZFlag !== this.customZFormGroup.controls.customZValue.enabled) { + if (customZFlag) { + this.customZFormGroup.controls.customZValue.enable() + } else { + this.customZFormGroup.controls.customZValue.disable() + } + } + }), + this.customZFormGroup.valueChanges.pipe( + debounceTime(160) + ).subscribe(() => { + const { customZFlag, customZValue } = this.customZFormGroup.value + // if customzflag is unset, unset zmultiplier + if (!customZFlag) { + this.store.dispatch( + userPreference.actions.setZMultiplier({ + value: null + }) + ) + return + } + // if the entered value cannot be parsed to number, skip for now. + if (customZValue) { + this.store.dispatch( + userPreference.actions.setZMultiplier({ + value: customZValue + }) + ) + } + }) ) } diff --git a/src/ui/config/configCmp/config.template.html b/src/ui/config/configCmp/config.template.html index 4982158f5..3d1f0f215 100644 --- a/src/ui/config/configCmp/config.template.html +++ b/src/ui/config/configCmp/config.template.html @@ -48,6 +48,26 @@ {{ gpuLimit$ | async }} MB </span> </div> + + <!-- set z increment multiplier --> + <div class="flex flex-row align-items-center justify-content start"> + + <form [formGroup]="customZFormGroup"> + <mat-slide-toggle formControlName="customZFlag"></mat-slide-toggle> + <small iav-stop="click mousedown mouseup" [matTooltip]="Z_TRAVERSAL_TOOLTIP" class="ml-2 fas fa-question"></small> + <mat-form-field class="ml-2" + [ngClass]="{ + 'muted-3': !customZFormGroup.value.customZFlag + }"> + <mat-label> + Custom Z Multiplier + </mat-label> + <input type="number" + matInput + formControlName="customZValue"> + </mat-form-field> + </form> + </div> </div> </mat-tab> diff --git a/src/ui/config/module.ts b/src/ui/config/module.ts index 751aa6f16..78ca035e1 100644 --- a/src/ui/config/module.ts +++ b/src/ui/config/module.ts @@ -3,12 +3,14 @@ import { NgModule } from "@angular/core"; import { LayoutModule } from "src/layouts/layout.module"; import { AngularMaterialModule } from "src/sharedModules"; import { ConfigComponent } from "./configCmp/config.component"; +import { ReactiveFormsModule } from "@angular/forms"; @NgModule({ imports: [ CommonModule, AngularMaterialModule, LayoutModule, + ReactiveFormsModule, ], declarations: [ ConfigComponent, @@ -17,4 +19,4 @@ import { ConfigComponent } from "./configCmp/config.component"; ConfigComponent, ] }) -export class ConfigModule{} \ No newline at end of file +export class ConfigModule{} diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts index da2c56f12..e9d3705cf 100644 --- a/src/viewerModule/module.ts +++ b/src/viewerModule/module.ts @@ -1,6 +1,6 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { Observable } from "rxjs"; +import { Observable, combineLatest, of } from "rxjs"; import { ComponentsModule } from "src/components"; import { ContextMenuModule, ContextMenuService, TContextMenuReg } from "src/contextMenuModule"; import { LayoutModule } from "src/layouts/layout.module"; @@ -14,11 +14,11 @@ import { UserAnnotationsModule } from "src/atlasComponents/userAnnotations"; import { QuickTourModule } from "src/ui/quickTour/module"; import { INJ_ANNOT_TARGET } from "src/atlasComponents/userAnnotations/tools/type"; import { NEHUBA_INSTANCE_INJTKN } from "./nehuba/util"; -import { map } from "rxjs/operators"; +import { map, switchMap } from "rxjs/operators"; import { TContextArg } from "./viewer.interface"; import { KeyFrameModule } from "src/keyframesModule/module"; import { ViewerInternalStateSvc } from "./viewerInternalState.service"; -import { SAPIModule } from 'src/atlasComponents/sapi'; +import { SAPI, SAPIModule } from 'src/atlasComponents/sapi'; import { NehubaVCtxToBbox } from "./pipes/nehubaVCtxToBbox.pipe"; import { SapiViewsModule, SapiViewsUtilModule } from "src/atlasComponents/sapiViews"; import { DialogModule } from "src/ui/dialogInfo/module"; @@ -34,6 +34,9 @@ import { FeatureModule } from "src/features"; import { NgLayerCtlModule } from "./nehuba/ngLayerCtlModule/module"; import { SmartChipModule } from "src/components/smartChip"; import { ReactiveFormsModule } from "@angular/forms"; +import { CURRENT_TEMPLATE_DIM_INFO, TemplateInfo, Z_TRAVERSAL_MULTIPLIER } from "./nehuba/layerCtrl.service/layerCtrl.util"; +import { Store } from "@ngrx/store"; +import { atlasSelection, userPreference } from "src/state"; @NgModule({ imports: [ @@ -91,6 +94,42 @@ import { ReactiveFormsModule } from "@angular/forms"; deps: [ ContextMenuService ] }, ViewerInternalStateSvc, + + { + provide: Z_TRAVERSAL_MULTIPLIER, + useFactory: (store: Store, templateInfo: Observable<TemplateInfo>) => { + return combineLatest([ + store.select(userPreference.selectors.overrideZTraversalMultiplier), + templateInfo + ]).pipe( + map(([ override, tmplInfo ]) => override || tmplInfo.resolution?.[0] || 1e3) + ) + }, + deps: [ Store, CURRENT_TEMPLATE_DIM_INFO ] + }, + { + provide: CURRENT_TEMPLATE_DIM_INFO, + useFactory: (store: Store, sapi: SAPI) => store.pipe( + atlasSelection.fromRootStore.distinctATP(), + switchMap(({ template }) => + template + ? sapi.getVoxelTemplateImage(template).pipe( + switchMap(defaultImage => { + if (defaultImage.length === 0) { + return of(null) + } + const img = defaultImage[0] + return of({ + ...img.info || {}, + transform: img.transform + }) + }) + ) + : of(null) + ) + ), + deps: [ Store, SAPI ] + }, ], exports: [ ViewerCmp, diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts index 6c7c7cd24..1e18596e9 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts @@ -65,3 +65,12 @@ export const SET_COLORMAP_OBS = new InjectionToken<Observable<IColorMap>>('SET_C export const SET_LAYER_VISIBILITY = new InjectionToken<Observable<string[]>>('SET_LAYER_VISIBILITY') export const SET_SEGMENT_VISIBILITY = new InjectionToken<Observable<string[]>>('SET_SEGMENT_VISIBILITY') export const NG_LAYER_CONTROL = new InjectionToken<TNgLayerCtrl<keyof INgLayerCtrl>>('NG_LAYER_CONTROL') +export const Z_TRAVERSAL_MULTIPLIER = new InjectionToken<Observable<number>>('Z_TRAVERSAL_MULTIPLIER') +export const CURRENT_TEMPLATE_DIM_INFO = new InjectionToken<Observable<TemplateInfo>>('CURRENT_TEMPLATE_DIM_INFO') + +export type TemplateInfo = { + transform: number[][] + voxel?: [number, number, number] + real?: [number, number, number] + resolution?: [number, number, number] +} diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts index 94d1cc939..dac942b8e 100644 --- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts +++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts @@ -11,7 +11,7 @@ import { IColorMap, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl. /** * import of nehuba js files moved to angular.json */ -import { INgLayerCtrl, NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY, TNgLayerCtrl } from "../layerCtrl.service/layerCtrl.util"; +import { INgLayerCtrl, NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY, TNgLayerCtrl, Z_TRAVERSAL_MULTIPLIER } from "../layerCtrl.service/layerCtrl.util"; import { NgCoordinateSpace, Unit } from "../types"; import { PeriodicSvc } from "src/util/periodic.service"; @@ -116,6 +116,8 @@ export class NehubaViewerUnit implements OnDestroy { #triggerMeshLoad$ = new BehaviorSubject(null) + multplier = new Float32Array(1) + constructor( public elementRef: ElementRef, private log: LoggingService, @@ -127,7 +129,15 @@ export class NehubaViewerUnit implements OnDestroy { @Optional() @Inject(SET_LAYER_VISIBILITY) private layerVis$: Observable<string[]>, @Optional() @Inject(SET_SEGMENT_VISIBILITY) private segVis$: Observable<string[]>, @Optional() @Inject(NG_LAYER_CONTROL) private layerCtrl$: Observable<TNgLayerCtrl<keyof INgLayerCtrl>>, + @Optional() @Inject(Z_TRAVERSAL_MULTIPLIER) multiplier$: Observable<number>, ) { + if (multiplier$) { + this.ondestroySubscriptions.push( + multiplier$.subscribe(val => this.multplier[0] = val) + ) + } else { + this.multplier[0] = 1 + } if (this.nehubaViewer$) { this.nehubaViewer$.next(this) @@ -356,9 +366,24 @@ export class NehubaViewerUnit implements OnDestroy { */ /* creation of the layout is done on next frame, hence the settimeout */ - setTimeout(() => { - window['viewer'].display.panels.forEach(patchSliceViewPanel) - }) + const patchSliceview = async () => { + + const viewer = window['viewer'] + viewer.inputEventBindings.sliceView.set("at:wheel", "proxy-wheel") + viewer.inputEventBindings.sliceView.set("at:control+shift+wheel", "proxy-wheel-alt") + await (async () => { + let lenPanels = 0 + + while (lenPanels === 0) { + lenPanels = viewer.display.panels.size + await new Promise(rs => setTimeout(rs, 150)) + } + })() + viewer.inputEventBindings.sliceView.set("at:wheel", "proxy-wheel-1") + viewer.inputEventBindings.sliceView.set("at:control+shift+wheel", "proxy-wheel-10") + viewer.display.panels.forEach(sliceView => patchSliceViewPanel(sliceView, this.exportNehuba, this.multplier)) + } + patchSliceview() this.newViewerInit() window['nehubaViewer'] = this.nehubaViewer @@ -839,7 +864,9 @@ export class NehubaViewerUnit implements OnDestroy { } } -const patchSliceViewPanel = (sliceViewPanel: any) => { +const patchSliceViewPanel = (sliceViewPanel: any, exportNehuba: any, mulitplier: Float32Array) => { + + // patch draw calls to dispatch viewerportToData const originalDraw = sliceViewPanel.draw sliceViewPanel.draw = function(this) { @@ -855,6 +882,24 @@ const patchSliceViewPanel = (sliceViewPanel: any) => { originalDraw.call(this) } + + // patch ctrl+wheel & shift+wheel + const { navigationState } = sliceViewPanel + const { registerActionListener, vec3 } = exportNehuba + const tempVec3 = vec3.create() + + for (const val of [1, 10]) { + registerActionListener(sliceViewPanel.element, `proxy-wheel-${val}`, event => { + const e = event.detail + + const offset = tempVec3 + const delta = e.deltaY !== 0 ? e.deltaY : e.deltaX + offset[0] = 0 + offset[1] = 0 + offset[2] = (delta > 0 ? -1 : 1) * mulitplier[0] * val + navigationState.pose.translateVoxelsRelative(offset) + }) + } } export interface ViewerState { diff --git a/src/viewerModule/nehuba/viewerCtrl/perspectiveViewSlider/perspectiveViewSlider.component.ts b/src/viewerModule/nehuba/viewerCtrl/perspectiveViewSlider/perspectiveViewSlider.component.ts index 6342411eb..a6930c7b4 100644 --- a/src/viewerModule/nehuba/viewerCtrl/perspectiveViewSlider/perspectiveViewSlider.component.ts +++ b/src/viewerModule/nehuba/viewerCtrl/perspectiveViewSlider/perspectiveViewSlider.component.ts @@ -2,10 +2,9 @@ import { Component, OnDestroy, Inject, ViewChild, ChangeDetectionStrategy } from import { FormControl } from "@angular/forms"; import { select, Store } from "@ngrx/store"; import { combineLatest, concat, NEVER, Observable, of, Subject, Subscription } from "rxjs"; -import { switchMap, distinctUntilChanged, map, debounceTime, shareReplay, take, withLatestFrom } from "rxjs/operators"; +import { switchMap, distinctUntilChanged, map, debounceTime, shareReplay, take, withLatestFrom, filter } from "rxjs/operators"; import { SAPI } from "src/atlasComponents/sapi"; import { SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes" -import { fromRootStore } from "src/state/atlasSelection"; import { selectedTemplate } from "src/state/atlasSelection/selectors"; import { panelMode, panelOrder } from "src/state/userInterface/selectors"; import { ResizeObserverDirective } from "src/util/windowResize"; @@ -15,6 +14,7 @@ import { NEHUBA_INSTANCE_INJTKN } from "../../util"; import { EnumClassicalView } from "src/atlasComponents/constants" import { atlasSelection } from "src/state"; import { floatEquality } from "common/util" +import { CURRENT_TEMPLATE_DIM_INFO, TemplateInfo } from "../../layerCtrl.service/layerCtrl.util"; const MAX_DIM = 200 @@ -146,24 +146,8 @@ export class PerspectiveViewSlider implements OnDestroy { map(ctrl => ctrl?.rangeOrientation === "vertical") ) - private currentTemplateSize$ = this.store$.pipe( - fromRootStore.distinctATP(), - switchMap(({ template }) => - template - ? this.sapi.getVoxelTemplateImage(template).pipe( - switchMap(defaultImage => { - if (defaultImage.length == 0) { - // template hs no ng volume, which is the case for threesurfer - return NEVER - } - const img = defaultImage[0] - return of({ - ...img.info || {}, - transform: img.transform - }) - }) - ) - : NEVER), + private currentTemplateSize$ = this.tmplInfo$.pipe( + filter(val => !!val) ) private useMinimap$: Observable<EnumClassicalView> = this.maximisedPanelIndex$.pipe( @@ -347,6 +331,7 @@ export class PerspectiveViewSlider implements OnDestroy { private store$: Store, private sapi: SAPI, @Inject(NEHUBA_INSTANCE_INJTKN) private nehubaViewer$: Observable<NehubaViewerUnit>, + @Inject(CURRENT_TEMPLATE_DIM_INFO) private tmplInfo$: Observable<TemplateInfo>, ) { this.subscriptions.push( -- GitLab