diff --git a/docs/releases/v2.12.0.md b/docs/releases/v2.12.0.md index 15888867a8ec145a8dc8b2bbd2ad52072942bb8c..0d29b901d4b58428051f2027de1140661c99564e 100644 --- a/docs/releases/v2.12.0.md +++ b/docs/releases/v2.12.0.md @@ -5,6 +5,10 @@ - enable rat connectivity - added visual indicators for selected subject and dataset in connectivity browser +## Bugfix + +- fixed fsaverage viewer "rubber banding" + ## Behind the scene - update spotlight mechanics from in-house to angular CDK diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts index 7afa3871728516ca4ebd63c10e08b6cf88526ab7..e1648fcc7c99cc8b263ef532fc9ea01e7363fb72 100644 --- a/src/atlasComponents/sapi/sapi.service.ts +++ b/src/atlasComponents/sapi/sapi.service.ts @@ -21,7 +21,7 @@ export const useViewer = { } as const export const SIIBRA_API_VERSION_HEADER_KEY='x-siibra-api-version' -export const EXPECTED_SIIBRA_API_VERSION = '0.3.5' +export const EXPECTED_SIIBRA_API_VERSION = '0.3.8' let BS_ENDPOINT_CACHED_VALUE: Observable<string> = null diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts index e4e729b1cfcf6817d00afd57681e2bbc9c817e9c..f2a869a8ab434f68802b8b34540471f2621fd46c 100644 --- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts +++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts @@ -1,8 +1,8 @@ import { Component, Output, EventEmitter, ElementRef, OnDestroy, AfterViewInit, Inject, Optional, ChangeDetectionStrategy } from "@angular/core"; import { EnumViewerEvt, IViewer, TViewerEvent } from "src/viewerModule/viewer.interface"; import { combineLatest, concat, forkJoin, from, merge, NEVER, Observable, of, Subject } from "rxjs"; -import { catchError, debounceTime, distinctUntilChanged, filter, map, scan, shareReplay, switchMap, withLatestFrom } from "rxjs/operators"; -import { ComponentStore } from "src/viewerModule/componentStore"; +import { catchError, debounceTime, distinctUntilChanged, filter, map, scan, shareReplay, startWith, switchMap, tap, withLatestFrom } from "rxjs/operators"; +import { ComponentStore, LockError } from "src/viewerModule/componentStore"; import { select, Store } from "@ngrx/store"; import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; import { MatSnackBar } from "@angular/material/snack-bar"; @@ -27,7 +27,7 @@ type TInternalState = { mode: string hemisphere: 'left' | 'right' | 'both' } -const pZoomFactor = 5e3 +const pZoomFactor = 7e3 type THandlingCustomEv = { regions: SxplrRegion[] @@ -107,11 +107,15 @@ type LateralityRecord<T> = Record<string, T> const threshold = 1e-3 function cameraNavsAreSimilar(c1: TCameraOrientation, c2: TCameraOrientation){ + + // if same reference, return true if (c1 === c2) return true - if (!!c1 && !!c2) return true - if (!c1 && !!c2) return false - if (!c2 && !!c1) return false + // if both falsy, return true + if (!c1 && !c2) return true + + if (!c1 && c2) return false + if (!c2 && c1) return false if (Math.abs(c1.perspectiveZoom - c2.perspectiveZoom) > threshold) return false if ([0, 1, 2, 3].some( @@ -141,9 +145,57 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit viewerEvent = new EventEmitter<TViewerEvent<'threeSurfer'>>() private domEl: HTMLElement - private mainStoreCameraNav: TCameraOrientation = null - private localCameraNav: TCameraOrientation = null + #storeNavigation = this.store$.pipe( + select(atlasSelection.selectors.navigation) + ) + + #componentStoreNavigation = this.navStateStoreRelay.select(s => s) + + #internalNavigation = this.#cameraEv$.pipe( + filter(v => !!v && !!(this.tsRef?.camera?.matrix)), + map(() => { + const { tsRef } = this + return { + _po: null, + _pz: null, + _calculate(){ + if (!tsRef) return + const THREE = (window as any).ThreeSurfer.THREE + + const q = new THREE.Quaternion() + const t = new THREE.Vector3() + const s = new THREE.Vector3() + + /** + * ThreeJS interpretes the scene differently to neuroglancer in subtle ways. + * At [0, 0, 0, 1] decomposed camera quaternion, for example, + * - ThreeJS: view from superior -> inferior, anterior as top, right hemisphere as right + * - NG: view from from inferior -> superior, posterior as top, left hemisphere as right + * + * multiplying the exchange factor [-1, 0, 0, 0] converts ThreeJS convention to NG convention + */ + const cameraM = tsRef.camera.matrix + cameraM.decompose(t, q, s) + const exchangeFactor = new THREE.Quaternion(-1, 0, 0, 0) + this._po = q.multiply(exchangeFactor).toArray() + this._pz = t.length() * pZoomFactor // use zoom as used in main store + }, + get perspectiveOrientation(){ + if (!this._po) { + this._calculate() + } + return this._po + }, + get perspectiveZoom() { + if (!this._pz) { + this._calculate() + } + return this._pz + } + } as TCameraOrientation + }) + ) private internalStateNext: (arg: TInteralStatePayload<TInternalState>) => void @@ -336,9 +388,9 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit constructor( private effect: ThreeSurferEffects, - private el: ElementRef, + el: ElementRef, private store$: Store, - private navStateStoreRelay: ComponentStore<{ perspectiveOrientation: [number, number, number, number], perspectiveZoom: number }>, + private navStateStoreRelay: ComponentStore<TCameraOrientation>, private sapi: SAPI, private snackbar: MatSnackBar, @Optional() intViewerStateSvc: ViewerInternalStateSvc, @@ -379,7 +431,7 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit const handleClick = (ev: MouseEvent) => { // if does not click inside container, ignore - if (!(this.el.nativeElement as HTMLElement).contains(ev.target as HTMLElement)) { + if (!(el.nativeElement as HTMLElement).contains(ev.target as HTMLElement)) { return true } @@ -404,88 +456,87 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit ) } - this.domEl = this.el.nativeElement + this.domEl = el.nativeElement /** * subscribe to camera custom event */ - const cameraSub = this.#cameraEv$.pipe( - filter(v => !!v), - debounceTime(160) - ).subscribe(() => { - - const THREE = (window as any).ThreeSurfer.THREE - - const q = new THREE.Quaternion() - const t = new THREE.Vector3() - const s = new THREE.Vector3() - - /** - * ThreeJS interpretes the scene differently to neuroglancer in subtle ways. - * At [0, 0, 0, 1] decomposed camera quaternion, for example, - * - ThreeJS: view from superior -> inferior, anterior as top, right hemisphere as right - * - NG: view from from inferior -> superior, posterior as top, left hemisphere as right - * - * multiplying the exchange factor [-1, 0, 0, 0] converts ThreeJS convention to NG convention - */ - const cameraM = this.tsRef.camera.matrix - cameraM.decompose(t, q, s) - const exchangeFactor = new THREE.Quaternion(-1, 0, 0, 0) - + const setReconcilState = merge( + this.#internalNavigation.pipe( + filter(v => !!v), + tap(() => { + try { + this.releaseRelayLock = this.navStateStoreRelay.getLock() + } catch (e) { + if (!(e instanceof LockError)) { + throw e + } + } + }), + debounceTime(160), + tap(() => { + if (this.releaseRelayLock) { + this.releaseRelayLock() + this.releaseRelayLock = null + } else { + console.warn(`this.releaseRelayLock not aquired, component may not function properly`) + } + }) + ), + this.#storeNavigation, + ).pipe( + filter(v => !!v) + ).subscribe(nav => { try { this.navStateStoreRelay.setState({ - perspectiveOrientation: q.multiply(exchangeFactor).toArray(), - perspectiveZoom: t.length() + perspectiveOrientation: nav.perspectiveOrientation, + perspectiveZoom: nav.perspectiveZoom }) - } catch (_e) { - // LockError, ignore + } catch (e) { + if (!(e instanceof LockError)) { + throw e + } } }) this.onDestroyCb.push( - () => cameraSub.unsubscribe() + () => setReconcilState.unsubscribe() ) /** * subscribe to navstore relay store and negotiate setting global state */ - const navStateSub = this.navStateStoreRelay.select(s => s).subscribe(v => { - this.store$.dispatch( - atlasSelection.actions.setNavigation({ + const reconciliatorSub = combineLatest([ + this.#storeNavigation.pipe( + startWith(null as TCameraOrientation) + ), + this.#componentStoreNavigation.pipe( + startWith(null as TCameraOrientation), + ), + this.#internalNavigation.pipe( + startWith(null as TCameraOrientation), + ) + ]).pipe( + debounceTime(160), + filter(() => !this.navStateStoreRelay.isLocked) + ).subscribe(([ storeNav, reconcilNav, internalNav ]) => { + if (!cameraNavsAreSimilar(storeNav, reconcilNav) && reconcilNav) { + this.store$.dispatch(atlasSelection.actions.setNavigation({ navigation: { position: [0, 0, 0], orientation: [0, 0, 0, 1], zoom: 1e6, - perspectiveOrientation: v.perspectiveOrientation, - perspectiveZoom: v.perspectiveZoom * pZoomFactor + perspectiveOrientation: reconcilNav.perspectiveOrientation, + perspectiveZoom: reconcilNav.perspectiveZoom } - }) - ) - }) - - this.onDestroyCb.push( - () => navStateSub.unsubscribe() - ) - - /** - * subscribe to main store and negotiate with relay to set camera - */ - const navSub = this.store$.pipe( - select(atlasSelection.selectors.navigation), - filter(v => !!v), - ).subscribe(nav => { - const { perspectiveOrientation, perspectiveZoom } = nav - this.mainStoreCameraNav = { - perspectiveOrientation, - perspectiveZoom + })) } - if (!cameraNavsAreSimilar(this.mainStoreCameraNav, this.localCameraNav)) { - this.relayStoreLock = this.navStateStoreRelay.getLock() + if (!cameraNavsAreSimilar(reconcilNav, internalNav) && reconcilNav) { const THREE = (window as any).ThreeSurfer.THREE - const cameraQuat = new THREE.Quaternion(...this.mainStoreCameraNav.perspectiveOrientation) - const cameraPos = new THREE.Vector3(0, 0, this.mainStoreCameraNav.perspectiveZoom / pZoomFactor) + const cameraQuat = new THREE.Quaternion(...reconcilNav.perspectiveOrientation) + const cameraPos = new THREE.Vector3(0, 0, reconcilNav.perspectiveZoom / pZoomFactor) /** * ThreeJS interpretes the scene differently to neuroglancer in subtle ways. @@ -501,19 +552,18 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit cameraPos.applyQuaternion(cameraQuat) this.toTsRef(tsRef => { tsRef.camera.position.copy(cameraPos) - if (this.relayStoreLock) this.relayStoreLock() }) } }) this.onDestroyCb.push( - () => navSub.unsubscribe() + () => reconciliatorSub.unsubscribe() ) } private tsRef: TThreeSurfer - private relayStoreLock: () => void = null + private releaseRelayLock: () => void = null private tsRefInitCb: ((tsRef: any) => void)[] = [] private toTsRef(callback: (tsRef: any) => void) { if (this.tsRef) {