diff --git a/common/helpOnePager.md b/common/helpOnePager.md index 8d621680ddc456700f1bf8ad9bb57fd08c4d8e2e..2f66d4b38b167b2f9ac0d93a93f0ca347da43c3d 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 c0b0336264ddda37b1033bc99ee6278d195c7b3f..d58bf258e2a4c13885bb477112a877411c9bf2c6 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 d7b7b75a21898e4149c5db349f717671f2570cb2..2597c367bc7c37b77cb7d33c28b2adc800dcef4c 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 05d96e5caabe5e4067ce8903358cf78c2ac5a836..55dffce1d6f4f2e3f85e1220f65d75883fcf0cbc 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 00856d0160f0af6fd391bf1c1e3b68288a981dbb..859ab0f178eea24b153ccb91e4a7c6878187d562 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 2a4fa314a1999dbacca0513bea5eaf3efe877c8e..222832b7a41339f72fb97aa5177d01480385af92 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 b3d556c87fa882ef0029e362bcab45cd552f564c..386293b19153fbe8c53957cfde01fe3a307f4e37 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 4982158f5bb83596ed47eeaf5425f17981666cc8..3d1f0f2153b66af9fdf37a8c7b111aabc6de2110 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 751aa6f16aca850c8dc9fd3e951d5e1484ba87bb..78ca035e1876f9f342b9e2188b89518390808b47 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 da2c56f121e5448e0dea34081de8520d150cb40b..e9d3705cfe99ad773f08e6f2ee2896f763d8c280 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 6c7c7cd24d389a426f6b17ef134a495dedf11061..1e18596e98fe06959628aa9186d2e2dd08981f98 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 94d1cc93950caa1c85a8850b485ebb9bba51e1f8..dac942b8e0c95f4ab43ff91940546e333f0dbb54 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 6342411ebf03fabb2d1c816eac98cf15695a8451..a6930c7b46c509d52f7a6d433caf320a46c15ce4 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(