diff --git a/docs/releases/v2.2.0.md b/docs/releases/v2.2.0.md index 3db6c411eb5a39d176a35a22109accf96b58e349..0ac4117cc17eaecba0a735745ed120ebd492a955 100644 --- a/docs/releases/v2.2.0.md +++ b/docs/releases/v2.2.0.md @@ -2,4 +2,5 @@ ## New features: -- [sane url sharing](../usage/sharing.md) \ No newline at end of file +- [sane url sharing](../usage/sharing.md) +- allow `pinch rotate` motion to be used for oblique rotation on touch enabled devices diff --git a/docs/usage/navigating.md b/docs/usage/navigating.md index ff6d085a04c6f6896df91b496000ac54002dcb9b..4f812f362d9a02d27d464d1fe6cc1f118f0ee2b7 100644 --- a/docs/usage/navigating.md +++ b/docs/usage/navigating.md @@ -7,7 +7,7 @@ The interactive atlas viewer can be accessed from either a desktop or an Android | | Desktop | Mobile | | --- | --- | --- | | Translating / Panning | `click drag` on any _slice views_ | `touchmove` on any _slice views_ | -| Oblique rotation | `shift` + `click drag` on any _slice views_ | hold `ðŸŒ` + `drag up/down` to switch rotation mode<br> hold 🌠+ `drag left/right` to rotate | +| Oblique rotation | `shift` + `click drag` on any _slice views_ | `pinch rotate` | | Zooming (_slice view_, _3d view_) | `mouse wheel` | `pinch zoom` | | Next slice (_slice view_) | `ctrl` + `mouse wheel` | | | Next 10 slice (_slice view_) | `ctrl` + `shift` + `mouse wheel` | | diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index c1d8dbee4211896d56cbee45983a5fb0f1e04132..ebe8ffe713ed59ca1d2f58881b78301818dc61a8 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -291,6 +291,7 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { /** * TODO deprecated + * TODO what the??? is this? */ this.subscriptions.push( this.ngLayerNames$.pipe( diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html index 9bc7caaae3e33df0a16685b5d4cb6edb4ab4ffe8..9c276d266d39ecfc6559250f1b281e08181be643 100644 --- a/src/atlasViewer/atlasViewer.template.html +++ b/src/atlasViewer/atlasViewer.template.html @@ -42,8 +42,15 @@ <ng-template #viewerBody> <div class="atlas-container" (drag-drop)="localFileService.handleFileDrop($event)"> <ui-nehuba-container - #uiNehubaContainer - iav-mouse-hover + iav-viewer-touch-interface + [iav-viewer-touch-interface-v-panels]="uiNehubaContainer.viewPanels" + [iav-viewer-touch-interface-vp-to-data]="uiNehubaContainer.nehubaViewer?.viewportToDatas" + [iav-viewer-touch-interface-ngviewer]="uiNehubaContainer.nehubaViewer?.nehubaViewer?.ngviewer" + [iav-viewer-touch-interface-nehuba-config]="uiNehubaContainer.selectedTemplate?.nehubaConfig" + #iavNehubaViewerTouch="iavNehubaViewerTouch" + + #uiNehubaContainer="uiNehubaContainer" + iav-mouse-hover #iavMouseHoverEl="iavMouseHover" [currentOnHoverObs$]="iavMouseHoverEl.currentOnHoverObs$" [currentOnHover]="iavMouseHoverEl.currentOnHoverObs$ | async" @@ -89,6 +96,7 @@ </button> + <!-- visible status card when mat drawer is closed --> <mat-card *ngIf="!sideNavDrawer.opened" (click)="toggleSideNavMenu(false)" diff --git a/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.component.ts b/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.component.ts index df18cdd160fa371af8df8a9d4ac229898b199358..4948b3a38be2f43149491447ec4c79a33237ae36 100644 --- a/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.component.ts +++ b/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.component.ts @@ -1,7 +1,20 @@ -import { Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from "@angular/core"; -import { combineLatest, concat, fromEvent, merge, Observable, of, Subject } from "rxjs"; -import { filter, map, scan, switchMap, takeUntil } from "rxjs/operators"; +import { Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild, Inject, TemplateRef, ViewChildren, QueryList } from "@angular/core"; +import { combineLatest, concat, fromEvent, merge, Observable, of, Subject, BehaviorSubject } from "rxjs"; +import { filter, map, scan, switchMap, takeUntil, startWith, pairwise, tap, shareReplay, distinctUntilChanged, switchMapTo, reduce } from "rxjs/operators"; import { clamp } from "src/util/generator"; +import { DOCUMENT } from "@angular/common"; + +const TOUCHMOVE_THRESHOLD = 50 +const SUBMENU_IXOBS_CONFIG = { + +} + +export interface ITunableProp{ + name: string + displayName?: string + values: string[] + selected?: any +} @Component({ selector : 'mobile-overlay', @@ -29,15 +42,31 @@ div:not(.active) > span:before }) export class MobileOverlay implements OnInit, OnDestroy { - @Input() public tunableProperties: string [] = [] - @Output() public deltaValue: EventEmitter<{delta: number, selectedProp: string}> = new EventEmitter() + + @Input('iav-mobile-overlay-guide-tmpl') + public guideTmpl: TemplateRef<any> + + @Input('iav-mobile-overlay-hide-ctrl-btn') + public hideCtrlBtn: boolean = false + + @Input('iav-mobile-overlay-ctrl-btn-pos') + public ctrlBtnPosition: { left: string, top: string } = { left: '50%', top: '50%' } + + @Input() public tunableProperties: ITunableProp[] = [] + + @Output() public tunablePropertySelected: EventEmitter<ITunableProp> = new EventEmitter() + @Output() public deltaValue: EventEmitter<{delta: number, selectedProp: ITunableProp}> = new EventEmitter() + @Output() public valueSelected: EventEmitter<{ value: string, selectedProp: ITunableProp }> = new EventEmitter() + @ViewChild('initiator', {read: ElementRef, static: true}) public initiator: ElementRef @ViewChild('mobileMenuContainer', {read: ElementRef, static: true}) public menuContainer: ElementRef @ViewChild('intersector', {read: ElementRef, static: true}) public intersector: ElementRef + @ViewChild('subMenuObserver', {read: ElementRef, static: false}) public subMenuIx: ElementRef + @ViewChild('setValueContainer', { read: ElementRef, static: false }) public setValueContainer?: ElementRef private _onDestroySubject: Subject<boolean> = new Subject() - private _focusedProperties: string + private _focusedProperties: ITunableProp get focusedProperty() { return this._focusedProperties ? this._focusedProperties @@ -49,20 +78,37 @@ export class MobileOverlay implements OnInit, OnDestroy { : 0 } + private initiatorSingleTouchStart$: Observable<any> + public showScreen$: Observable<boolean> public showProperties$: Observable<boolean> public showDelta$: Observable<boolean> public showInitiator$: Observable<boolean> private _drag$: Observable<any> + private _thresholdDrag$: Observable<any> private intersectionObserver: IntersectionObserver + private subMenuIxObs: IntersectionObserver + + constructor( + @Inject(DOCUMENT) private document: Document + ){ + + } public ngOnDestroy() { this._onDestroySubject.next(true) this._onDestroySubject.complete() + this.intersectionObserver && this.intersectionObserver.disconnect() + this.subMenuIxObs && this.subMenuIxObs.disconnect() } public ngOnInit() { + this.initiatorSingleTouchStart$ = fromEvent(this.initiator.nativeElement, 'touchstart').pipe( + filter((ev: TouchEvent) => ev.touches.length === 1), + shareReplay(1) + ) + const itemCount = this.tunableProperties.length const config = { @@ -79,52 +125,55 @@ export class MobileOverlay implements OnInit, OnDestroy { this.intersectionObserver.observe(this.menuContainer.nativeElement) - const scanDragScanAccumulator: (acc: TouchEvent[], item: TouchEvent, idx: number) => TouchEvent[] = (acc, curr) => acc.length < 2 - ? acc.concat(curr) - : acc.slice(1).concat(curr) - - this._drag$ = fromEvent(this.initiator.nativeElement, 'touchmove').pipe( - takeUntil(fromEvent(this.initiator.nativeElement, 'touchend').pipe( + this._drag$ = fromEvent(this.document, 'touchmove').pipe( + filter((ev: TouchEvent) => ev.touches.length === 1), + takeUntil(fromEvent(this.document, 'touchend').pipe( filter((ev: TouchEvent) => ev.touches.length === 0), )), - map((ev: TouchEvent) => (ev.preventDefault(), ev.stopPropagation(), ev)), - filter((ev: TouchEvent) => ev.touches.length === 1), - scan(scanDragScanAccumulator, []), - filter(ev => ev.length === 2), ) - this.showProperties$ = concat( - of(false), - fromEvent(this.initiator.nativeElement, 'touchstart').pipe( - switchMap(() => concat( - this._drag$.pipe( - map(double => ({ - deltaX : double[1].touches[0].screenX - double[0].touches[0].screenX, - deltaY : double[1].touches[0].screenY - double[0].touches[0].screenY, - })), - scan((acc, _curr) => acc), - map(v => v.deltaY ** 2 > v.deltaX ** 2), - ), - of(false), - )), - ), + this._thresholdDrag$ = this._drag$.pipe( + distinctUntilChanged((o, n) => { + const deltaX = o.touches[0].screenX - n.touches[0].screenX + const deltaY = o.touches[0].screenY - n.touches[0].screenY + return (deltaX ** 2 + deltaY ** 2) < TOUCHMOVE_THRESHOLD + }), ) - this.showDelta$ = concat( - of(false), - fromEvent(this.initiator.nativeElement, 'touchstart').pipe( - switchMap(() => concat( - this._drag$.pipe( - map(double => ({ - deltaX : double[1].touches[0].screenX - double[0].touches[0].screenX, - deltaY : double[1].touches[0].screenY - double[0].touches[0].screenY, - })), - scan((acc, _curr) => acc), - map(v => v.deltaX ** 2 > v.deltaY ** 2), - ), - of(false), - )), - ), + this.showProperties$ = this.initiatorSingleTouchStart$.pipe( + switchMap(() => concat( + this._thresholdDrag$.pipe( + pairwise(), + map(double => ({ + deltaX : double[1].touches[0].screenX - double[0].touches[0].screenX, + deltaY : double[1].touches[0].screenY - double[0].touches[0].screenY, + })), + map(v => v.deltaY ** 2 > v.deltaX ** 2), + scan((acc, _curr) => acc), + ), + of(false) + )), + startWith(false), + distinctUntilChanged(), + shareReplay(1) + ) + + this.showDelta$ = this.initiatorSingleTouchStart$.pipe( + switchMap(() => concat( + this._thresholdDrag$.pipe( + pairwise(), + map(double => ({ + deltaX : double[1].touches[0].screenX - double[0].touches[0].screenX, + deltaY : double[1].touches[0].screenY - double[0].touches[0].screenY, + })), + scan((acc, _curr) => acc), + map(v => v.deltaX ** 2 > v.deltaY ** 2), + ), + of(false), + )), + startWith(false), + distinctUntilChanged(), + shareReplay(1) ) this.showInitiator$ = combineLatest( @@ -144,50 +193,44 @@ export class MobileOverlay implements OnInit, OnDestroy { map(([ev, showInitiator]: [TouchEvent, boolean]) => showInitiator && ev.touches.length === 1), ) - fromEvent(this.initiator.nativeElement, 'touchstart').pipe( - switchMap(() => this._drag$.pipe( - map(double => ({ - deltaX : double[1].touches[0].screenX - double[0].touches[0].screenX, - deltaY : double[1].touches[0].screenY - double[0].touches[0].screenY, - })), - scan((acc, curr: any) => ({ - pass: acc.pass === null - ? curr.deltaX ** 2 > curr.deltaY ** 2 - : acc.pass, - delta: curr.deltaX, - }), { - pass: null, - delta : null, - }), - filter(ev => ev.pass), - map(ev => ev.delta), + this.showDelta$.pipe( + filter(flag => flag), + switchMapTo(this._thresholdDrag$.pipe( + pairwise(), + map(double => double[1].touches[0].screenX - double[0].touches[0].screenX) )), - takeUntil(this._onDestroySubject), - ).subscribe(ev => this.deltaValue.emit({ - delta : ev, - selectedProp : this.focusedProperty, - })) + takeUntil(this._onDestroySubject) + ).subscribe(ev => { + this.deltaValue.emit({ + delta: ev, + selectedProp: this.focusedProperty + }) + }) - const offsetObs$ = fromEvent(this.initiator.nativeElement, 'touchstart').pipe( - switchMap(() => concat( - this._drag$.pipe( - scan((acc, curr) => [acc[0], curr[1]]), - map(double => ({ - deltaX : double[1].touches[0].screenX - double[0].touches[0].screenX, - deltaY : double[1].touches[0].screenY - double[0].touches[0].screenY, - })), - ), - )), + const offsetObs$ = this.initiatorSingleTouchStart$.pipe( + switchMap(() => this._drag$) ) + combineLatest( this.showProperties$, offsetObs$, ).pipe( filter(v => v[0]), map(v => v[1]), + scan((acc, curr) => { + const { startY } = acc + const { screenY } = curr.touches[0] + return { + startY: startY || screenY, + totalDeltaY: screenY - (startY || 0) + } + }, { + startY: null, + totalDeltaY: 0 + }), takeUntil(this._onDestroySubject), ).subscribe(v => { - const deltaY = v.deltaY + const deltaY = v.totalDeltaY const cellHeight = this.menuContainer && this.tunableProperties && this.tunableProperties.length > 0 && this.menuContainer.nativeElement.offsetHeight / this.tunableProperties.length const adjHeight = - this.focusedIndex * cellHeight - cellHeight * 0.5 @@ -206,8 +249,82 @@ export class MobileOverlay implements OnInit, OnDestroy { } }) + this.showDelta$.pipe( + tap(flag => { + this.highlightedSubmenu$.next(this.focusedProperty.values[0]) + if (!flag && !!this.subMenuIxObs) { + this.subMenuIxObs.disconnect() + this.subMenuIxObs = null + } + }), + filter(v => !!v), + // when options show again, options may have changed, so need to recalculate + tap(() => { + this.setValueContainerClampStart = null + this.setValueContainerWidth = null + this.setValueContainerClampEnd = null + this.setValueContainerOffset = null + }), + switchMapTo(this._drag$.pipe( + scan((acc, curr) => { + const { startX } = acc + const { screenX } = curr.touches[0] + return { + startX: startX || screenX, + totalDeltaX: screenX - (startX || 0) + } + }, { + startX: null, + totalDeltaX: 0 + }) + )), + takeUntil(this._onDestroySubject) + ).subscribe(({ totalDeltaX }) => { + if (!this.subMenuIxObs && this.subMenuIx) { + this.subMenuIxObs = new IntersectionObserver(ixs => { + const ix = ixs.find(({ intersectionRatio }) => intersectionRatio < 0.7) + if (!ix) return console.log(ixs) + const value = ix.target.getAttribute('data-submenu-value') + this.highlightedSubmenu$.next(value) + }, { + root: this.subMenuIx.nativeElement, + threshold: [ 0.1, 0.3, 0.5, 0.7, 0.9 ] + }) + + for (const btn of this.setValueContainer.nativeElement.children) { + this.subMenuIxObs.observe(btn) + } + } + if (!this.setValueContainerWidth) { + if (!this.setValueContainer) return + if (this.setValueContainer.nativeElement.children.length === 0) return + const { children, clientWidth } = this.setValueContainer.nativeElement + + this.setValueContainerWidth = clientWidth + const firstChildWidth = children[0].clientWidth + const lastChildWidth = children[children.length - 1].clientWidth + + this.setValueContainerOffset = firstChildWidth / -2 + this.setValueContainerClampStart = firstChildWidth / -2 + this.setValueContainerClampEnd = lastChildWidth / 2 - clientWidth + } + const actualDeltaX = clamp(totalDeltaX + this.setValueContainerOffset, this.setValueContainerClampStart, this.setValueContainerClampEnd) + this.subMenuTransform = `translate(${actualDeltaX}px , 0px)` + }) + + this.showDelta$.pipe( + takeUntil(this._onDestroySubject), + filter(v => !v) + ).subscribe(() => this.valueSelected.emit({ selectedProp: this.focusedProperty, value: this.highlightedSubmenu$.value })) } + public highlightedSubmenu$: BehaviorSubject<string> = new BehaviorSubject(null) + + private setValueContainerOffset = null + private setValueContainerClampEnd = null + private setValueContainerClampStart = null + private setValueContainerWidth = null + public subMenuTransform = `translate(0px, 0px)` public menuTransform = `translate(0px, 0px)` public focusItemIndex: number = 0 diff --git a/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.style.css b/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.style.css index a37da10afb7571d0f5a2010aff618cb2b4f223bf..8d548523eaefd725bb5baa490dfc38e3e7abdc2e 100644 --- a/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.style.css +++ b/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.style.css @@ -17,11 +17,13 @@ top: 0; left: 0; position: absolute; + z-index: 99999; color : black; background-color: rgba(255, 255, 255, 0.5); -} + transition: all 200ms linear; +} :host-context([darktheme="true"]) [screen] { @@ -65,7 +67,44 @@ background-color: rgba(128, 128, 200, 0.2); } -[guide] +.base-container +{ + position: relative; + width: 100%; + left: 0; + top: 50%; + height: 0; + z-index: 9999; +} + +div[delta] +{ + white-space: nowrap +} + +.popup { - z-index:9999; + transition: all 120ms linear; + transform-origin: 50% 100%; } + +.scale-y-0 +{ + transform: scale(0.5, 0); + opacity: 0; +} + +.subMenu +{ + bottom: 15px; +} + +.w-50 +{ + width: 50%; +} + +.sliver +{ + width: 1px; +} \ No newline at end of file diff --git a/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.template.html b/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.template.html index 9770b46d5a6473606f49bd94c70893e50db71c61..791edddf1b9dda7ae39043f7746afa8703f87bef 100644 --- a/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.template.html +++ b/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.template.html @@ -7,20 +7,76 @@ class = "btn btn-default theme-controlled property scrollFocus"> <!-- scrollFocus class --> <span> - {{ p }} + {{ p.displayName || p.name }} </span> </div> </div> </div> </div> -<ng-content *ngIf="showDelta$ | async" select="[delta]" guide> -</ng-content> +<!-- container class --> +<div class="d-flex flex-column-reverse flex-nowrap align-items-center base-container position-relative"> -<ng-content *ngIf="showScreen$ | async" select="[guide]" guide> -</ng-content> + <!-- ctrl nub --> + <div class="h-0 d-inline-flex align-items-center" [hidden]="!(showInitiator$ | async)" #initiator> + <div (contextmenu)="$event.stopPropagation(); $event.preventDefault();" + [ngStyle]="ctrlBtnPosition" + class="pe-all" + initiator> + <button mat-mini-fab color="primary"> + <i class="fas fa-globe"></i> + </button> + </div> + </div> + + <!-- guide text --> + <mat-card [ngClass]="{ 'scale-y-0': !(showScreen$ | async) }" + class="mb-4 popup muted position-absolute subMenu"> + <mat-card-content> + <ng-container *ngTemplateOutlet="guideTmpl"> + </ng-container> + </mat-card-content> + </mat-card> + + <!-- mobile set value --> + <div *ngIf="showDelta$ | async" class="position-absolute h-0 w-100 d-flex flex-row justify-content-end align-items-end"> + + <!-- intersection observer --> + <div class="w-50 d-flex flex-row flex-nowrap" #subMenuObserver> + + <!-- value selection container --> + <div class="position-relative mb-4" [style.transform]="subMenuTransform" #setValueContainer> + <!-- value selections --> + <ng-container *ngFor="let value of focusedProperty.values"> + <!-- selected button --> + <ng-template + [ngIf]="focusedProperty.selected === value" + [ngIfElse]="notSelectedTmpl"> + <button + [attr.data-submenu-value]="value" + mat-flat-button + [ngClass]="{ 'muted': (highlightedSubmenu$ | async) !== value }" + color="primary" + class="mr-2"> + {{ value }} + </button> + </ng-template> + + <!-- not selected button --> + <ng-template #notSelectedTmpl> + <button + [attr.data-submenu-value]="value" + mat-flat-button + [ngClass]="{ 'muted': (highlightedSubmenu$ | async) !== value }" + color="default" + class="mr-2"> + {{ value }} + </button> + </ng-template> -<div [hidden]="!(showInitiator$ | async)" #initiator> - <ng-content select="[initiator]"> - </ng-content> -</div> \ No newline at end of file + </ng-container> + </div> + </div> + </div> + +</div> diff --git a/src/ui/nehubaContainer/nehubaContainer.component.ts b/src/ui/nehubaContainer/nehubaContainer.component.ts index 0ddb3de683a2764633c34c412e2262322a7968cf..1e3a5df8048b4fd229045ded55a012fb3e523685 100644 --- a/src/ui/nehubaContainer/nehubaContainer.component.ts +++ b/src/ui/nehubaContainer/nehubaContainer.component.ts @@ -16,7 +16,6 @@ import { switchMap, switchMapTo, take, - takeUntil, tap, withLatestFrom } from "rxjs/operators"; @@ -25,11 +24,12 @@ import { FOUR_PANEL, H_ONE_THREE, NEHUBA_READY, NG_VIEWER_ACTION_TYPES, SINGLE_P import { SELECT_REGIONS_WITH_ID, VIEWERSTATE_ACTION_TYPES } from "src/services/state/viewerState.store"; import { ADD_NG_LAYER, generateLabelIndexId, getMultiNgIdsRegionsLabelIndexMap, getNgIds, ILandmark, IOtherLandmarkGeometry, IPlaneLandmarkGeometry, IPointLandmarkGeometry, isDefined, MOUSE_OVER_LANDMARK, NgViewerStateInterface, REMOVE_NG_LAYER, safeFilter, ViewerStateInterface } from "src/services/stateStore.service"; import { getExportNehuba, isSame } from "src/util/fn"; -import { AtlasViewerAPIServices, IUserLandmark } from "../../atlasViewer/atlasViewer.apiService.service"; -import { AtlasViewerConstantsServices } from "../../atlasViewer/atlasViewer.constantService.service"; -import { computeDistance, NehubaViewerUnit } from "./nehubaViewer/nehubaViewer.component"; +import { AtlasViewerAPIServices, IUserLandmark } from "src/atlasViewer/atlasViewer.apiService.service"; +import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service"; +import { NehubaViewerUnit } from "./nehubaViewer/nehubaViewer.component"; import { getFourPanel, getHorizontalOneThree, getSinglePanel, getVerticalOneThree, calculateSliceZoomFactor } from "./util"; import { NehubaViewerContainerDirective } from "./nehubaViewerInterface/nehubaViewerInterface.directive"; +import { ITunableProp } from "./mobileOverlay/mobileOverlay.component"; const isFirstRow = (cell: HTMLElement) => { const { parentElement: row } = cell @@ -68,6 +68,7 @@ const scanFn: (acc: [boolean, boolean, boolean], curr: CustomEvent) => [boolean, styleUrls : [ `./nehubaContainer.style.css`, ], + exportAs: 'uiNehubaContainer', }) export class NehubaContainer implements OnInit, OnChanges, OnDestroy { @@ -133,7 +134,7 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy { public nanometersToOffsetPixelsFn: Array<(...arg) => any> = [] - private viewPanels: [HTMLElement, HTMLElement, HTMLElement, HTMLElement] = [null, null, null, null] + public viewPanels: [HTMLElement, HTMLElement, HTMLElement, HTMLElement] = [null, null, null, null] public panelMode$: Observable<string> private panelOrder: string @@ -142,8 +143,6 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy { public hoveredPanelIndices$: Observable<number> - private ngPanelTouchMove$: Observable<any> - constructor( private constantService: AtlasViewerConstantsServices, private apiService: AtlasViewerAPIServices, @@ -291,22 +290,6 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy { ? state.layers.findIndex(l => l.mixability === 'nonmixable') >= 0 : false), ) - - this.ngPanelTouchMove$ = fromEvent(this.elementRef.nativeElement, 'touchstart').pipe( - switchMap((touchStartEv: TouchEvent) => fromEvent(this.elementRef.nativeElement, 'touchmove').pipe( - tap((ev: TouchEvent) => ev.preventDefault()), - scan((acc, curr: TouchEvent) => [curr, ...acc.slice(0, 1)], []), - map((touchMoveEvs: TouchEvent[]) => { - return { - touchStartEv, - touchMoveEvs, - } - }), - takeUntil(fromEvent(this.elementRef.nativeElement, 'touchend').pipe( - filter((ev: TouchEvent) => ev.touches.length === 0)), - ), - )), - ) } public useMobileUI$: Observable<boolean> @@ -331,75 +314,6 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy { public ngOnInit() { - // translation on mobile - this.subscriptions.push( - this.ngPanelTouchMove$.pipe( - filter(({ touchMoveEvs }) => touchMoveEvs.length > 1 && (touchMoveEvs as TouchEvent[]).every(ev => ev.touches.length === 1)), - ).subscribe(({ touchMoveEvs, touchStartEv }) => { - - // get deltaX and deltaY of touchmove - const deltaX = touchMoveEvs[1].touches[0].screenX - touchMoveEvs[0].touches[0].screenX - const deltaY = touchMoveEvs[1].touches[0].screenY - touchMoveEvs[0].touches[0].screenY - - // figure out the target of touch start - const panelIdx = this.findPanelIndex(touchStartEv.target as HTMLElement) - - // translate if panelIdx < 3 - if (panelIdx >= 0 && panelIdx < 3) { - const { position } = this.nehubaViewer.nehubaViewer.ngviewer.navigationState - const pos = position.spatialCoordinates - this.exportNehuba.vec3.set(pos, deltaX, deltaY, 0) - this.exportNehuba.vec3.transformMat4(pos, pos, this.nehubaViewer.viewportToDatas[panelIdx]) - position.changed.dispatch() - } else if (panelIdx === 3) { - const {perspectiveNavigationState} = this.nehubaViewer.nehubaViewer.ngviewer - const { vec3 } = this.exportNehuba - perspectiveNavigationState.pose.rotateRelative(vec3.fromValues(0, 1, 0), -deltaX / 4.0 * Math.PI / 180.0) - perspectiveNavigationState.pose.rotateRelative(vec3.fromValues(1, 0, 0), deltaY / 4.0 * Math.PI / 180.0) - this.nehubaViewer.nehubaViewer.ngviewer.perspectiveNavigationState.changed.dispatch() - } else { - this.log.warn(`panelIdx not found`) - } - }), - ) - - // perspective reorientation on mobile - this.subscriptions.push( - this.ngPanelTouchMove$.pipe( - filter(({ touchMoveEvs }) => touchMoveEvs.length > 1 && (touchMoveEvs as TouchEvent[]).every(ev => ev.touches.length === 2)), - ).subscribe(({ touchMoveEvs, touchStartEv }) => { - - const d1 = computeDistance( - [touchMoveEvs[1].touches[0].screenX, touchMoveEvs[1].touches[0].screenY], - [touchMoveEvs[1].touches[1].screenX, touchMoveEvs[1].touches[1].screenY], - ) - const d2 = computeDistance( - [touchMoveEvs[0].touches[0].screenX, touchMoveEvs[0].touches[0].screenY], - [touchMoveEvs[0].touches[1].screenX, touchMoveEvs[0].touches[1].screenY], - ) - const factor = d1 / d2 - - // figure out the target of touch start - const panelIdx = this.findPanelIndex(touchStartEv.target as HTMLElement) - - // zoom slice view if slice - if (panelIdx >= 0 && panelIdx < 3) { - this.nehubaViewer.nehubaViewer.ngviewer.navigationState.zoomBy(factor) - } else if (panelIdx === 3) { - const { minZoom = null, maxZoom = null } = (this.selectedTemplate.nehubaConfig - && this.selectedTemplate.nehubaConfig.layout - && this.selectedTemplate.nehubaConfig.layout.useNehubaPerspective - && this.selectedTemplate.nehubaConfig.layout.useNehubaPerspective.restrictZoomLevel) - || {} - - const { zoomFactor } = this.nehubaViewer.nehubaViewer.ngviewer.perspectiveNavigationState - if (!!minZoom && zoomFactor.value * factor < minZoom) { return } - if (!!maxZoom && zoomFactor.value * factor > maxZoom) { return } - zoomFactor.zoomBy(factor) - } - }), - ) - this.hoveredPanelIndices$ = fromEvent(this.elementRef.nativeElement, 'mouseover').pipe( switchMap((ev: MouseEvent) => merge( of(this.findPanelIndex(ev.target as HTMLElement)), @@ -826,6 +740,10 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy { this.subscriptions.forEach(s => s.unsubscribe()) } + public test(){ + console.log('test') + } + public toggleMaximiseMinimise(index: number) { this.store.dispatch({ type: NG_VIEWER_ACTION_TYPES.TOGGLE_MAXIMISE, @@ -835,26 +753,10 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy { }) } - public tunableMobileProperties = ['Oblique Rotate X', 'Oblique Rotate Y', 'Oblique Rotate Z', 'Remove extra layers'] - public selectedProp = null - - public handleMobileOverlayTouchEnd(focusItemIndex) { - if (this.tunableMobileProperties[focusItemIndex] === 'Remove extra layers') { - this.store.dispatch({ - type: NG_VIEWER_ACTION_TYPES.REMOVE_ALL_NONBASE_LAYERS, - }) - } - } + public tunableMobileProperties: ITunableProp[] = [] - public handleMobileOverlayEvent(obj: any) { - const {delta, selectedProp} = obj - this.selectedProp = selectedProp - - const idx = this.tunableMobileProperties.findIndex(p => p === selectedProp) - if (idx === 0) { this.nehubaViewer.obliqueRotateX(delta) } - if (idx === 1) { this.nehubaViewer.obliqueRotateY(delta) } - if (idx === 2) { this.nehubaViewer.obliqueRotateZ(delta) } - } + + public selectedProp = null public returnTruePos(quadrant: number, data: any) { const pos = quadrant > 2 ? diff --git a/src/ui/nehubaContainer/nehubaContainer.style.css b/src/ui/nehubaContainer/nehubaContainer.style.css index 9737b28faebcf1d5c24d7e3ff7be187d708031c5..795c4084b41770f293712671d0709ccb87c23938 100644 --- a/src/ui/nehubaContainer/nehubaContainer.style.css +++ b/src/ui/nehubaContainer/nehubaContainer.style.css @@ -101,67 +101,6 @@ div.loadingIndicator div.spinnerAnimationCircle color:rgba(255,255,255,0.8); } -div[mobileObliqueCtrl] -{ - font-size: 200%; - position: absolute; - top: 50%; - left: 50%; - width: 0; - height: 0; - - display: flex; - align-items: center; - justify-content: center; - pointer-events: all; -} - -div[mobileObliqueScreen] -{ - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color:rgba(128,128,128,0.2); - transition: all 0.5s linear; - pointer-events: all; -} - -div.base -{ - position : absolute; - top: 50%; - left: 50%; - width: 0; - height: 0; - display:flex; - flex-direction: column-reverse; - align-items: center; -} - -div[delta] -{ - white-space: nowrap -} - -div[mobileObliqueGuide] -{ - background-color: rgba(250,250,250,0.8); -} - -div[mobileObliqueGuide] > * -{ - white-space: nowrap; -} - -:host-context([darktheme="true"]) div[mobileObliqueGuide] -{ - - background-color: rgba(50,50,50,0.8); - color: white; -} - div#scratch-pad { position: absolute; diff --git a/src/ui/nehubaContainer/nehubaContainer.template.html b/src/ui/nehubaContainer/nehubaContainer.template.html index 175c62c6372131e4e3dfe8541e9c1897d1181d2f..b6ecacc3753003334269b2c610ad29a022859704 100644 --- a/src/ui/nehubaContainer/nehubaContainer.template.html +++ b/src/ui/nehubaContainer/nehubaContainer.template.html @@ -39,59 +39,52 @@ <div id="scratch-pad"> </div> -<!-- mobile nub, allowing for ooblique slicing in mobile --> +<!-- mobile nub. may be required when more advanced control is required on mobile. for now, disabled --> <mobile-overlay - *ngIf="(useMobileUI$ | async) && viewerLoaded" - (touchend)="handleMobileOverlayTouchEnd(mobileOverlayEl.focusItemIndex)" + *ngIf="false && (useMobileUI$ | async) && viewerLoaded" + [iav-mobile-overlay-guide-tmpl]="mobileOverlayGuide" [tunableProperties]="tunableMobileProperties" - (deltaValue)="handleMobileOverlayEvent($event)" - #mobileOverlayEl> - <div class="base" delta> - <div mobileObliqueGuide class="p-2 mb-4 shadow"> - {{ selectedProp }} - </div> - </div> - <div class="base" guide> - <div - mobileObliqueGuide - class="p-2 mb-4 shadow"> - <div> - <i class="fas fa-arrows-alt-v"></i> oblique mode - </div> - <div> - <i class="fas fa-arrows-alt-h"></i> rotate slice - </div> - </div> + [iav-mobile-overlay-hide-ctrl-btn]="(panelMode$ | async) !== 'SINGLE_PANEL'" + [iav-mobile-overlay-ctrl-btn-pos]="panelMode$ | async | mobileControlNubStylePipe"> + +</mobile-overlay> + +<ng-template #mobileOverlayGuide> + <div> + <i class="fas fa-arrows-alt-v"></i> + <span> + Select item + </span> </div> - <div - (contextmenu)="$event.stopPropagation(); $event.preventDefault();" - [ngStyle]="panelMode$ | async | mobileControlNubStylePipe" - *ngIf="(panelMode$ | async) !== 'SINGLE_PANEL'" - mobileObliqueCtrl - initiator> - <button mat-mini-fab color="primary"> - <i class="fas fa-globe"></i> - </button> + <div> + <i class="fas fa-arrows-alt-h"></i> + <span> + Modify item value + </span> </div> -</mobile-overlay> +</ng-template> <!-- overlay templates --> <!-- inserted using ngTemplateOutlet --> <ng-template #overlayi> <layout-floating-container pos00 landmarkContainer> <nehuba-2dlandmark-unit *ngFor="let spatialData of (selectedPtLandmarks$ | async)" - (mouseenter)="handleMouseEnterLandmark(spatialData)" (mouseleave)="handleMouseLeaveLandmark(spatialData)" + (mouseenter)="handleMouseEnterLandmark(spatialData)" + (mouseleave)="handleMouseLeaveLandmark(spatialData)" [highlight]="spatialData.highlight ? spatialData.highlight : false" [fasClass]="spatialData.type === 'userLandmark' ? 'fa-chevron-down' : 'fa-map-marker'" - [positionX]="getPositionX(0,spatialData)" [positionY]="getPositionY(0,spatialData)" + [positionX]="getPositionX(0,spatialData)" + [positionY]="getPositionY(0,spatialData)" [positionZ]="getPositionZ(0,spatialData)"> </nehuba-2dlandmark-unit> <!-- maximise/minimise button --> <maximise-panel-button + (touchend)="toggleMaximiseMinimise(0)" (click)="toggleMaximiseMinimise(0)" [ngClass]="{ onHover: (panelOrder$ | async | reorderPanelIndexPipe : ( hoveredPanelIndices$ | async )) === 0 }" - [touch-side-class]="0 " class="pe-all"> + [touch-side-class]="0" + class="pe-all"> </maximise-panel-button> <div *ngIf="sliceViewLoading0$ | async" class="loadingIndicator"> @@ -104,44 +97,52 @@ <ng-template #overlayii> <layout-floating-container pos01 landmarkContainer> - <nehuba-2dlandmark-unit *ngFor="let spatialData of (selectedPtLandmarks$ | async)" - (mouseenter)="handleMouseEnterLandmark(spatialData)" (mouseleave)="handleMouseLeaveLandmark(spatialData)" - [highlight]="spatialData.highlight ? spatialData.highlight : false" - [fasClass]="spatialData.type === 'userLandmark' ? 'fa-chevron-down' : 'fa-map-marker'" - [positionX]="getPositionX(1,spatialData)" [positionY]="getPositionY(1,spatialData)" - [positionZ]="getPositionZ(1,spatialData)"> - </nehuba-2dlandmark-unit> - - <!-- maximise/minimise button --> - <maximise-panel-button - (click)="toggleMaximiseMinimise(1)" - [ngClass]="{ onHover: (panelOrder$ | async | reorderPanelIndexPipe : ( hoveredPanelIndices$ | async )) === 1 }" - [touch-side-class]="1 " class="pe-all"> - </maximise-panel-button> - - <div *ngIf="sliceViewLoading1$ | async" class="loadingIndicator"> - <div class="spinnerAnimationCircle"> - - </div> + <nehuba-2dlandmark-unit *ngFor="let spatialData of (selectedPtLandmarks$ | async)" + (mouseenter)="handleMouseEnterLandmark(spatialData)" + (mouseleave)="handleMouseLeaveLandmark(spatialData)" + [highlight]="spatialData.highlight ? spatialData.highlight : false" + [fasClass]="spatialData.type === 'userLandmark' ? 'fa-chevron-down' : 'fa-map-marker'" + [positionX]="getPositionX(1,spatialData)" + [positionY]="getPositionY(1,spatialData)" + [positionZ]="getPositionZ(1,spatialData)"> + </nehuba-2dlandmark-unit> + + <!-- maximise/minimise button --> + <maximise-panel-button + (touchend)="toggleMaximiseMinimise(1)" + (click)="toggleMaximiseMinimise(1)" + [ngClass]="{ onHover: (panelOrder$ | async | reorderPanelIndexPipe : ( hoveredPanelIndices$ | async )) === 1 }" + [touch-side-class]="1" + class="pe-all"> + </maximise-panel-button> + + <div *ngIf="sliceViewLoading1$ | async" class="loadingIndicator"> + <div class="spinnerAnimationCircle"> + </div> - </layout-floating-container> + </div> + </layout-floating-container> </ng-template> <ng-template #overlayiii> <layout-floating-container pos10 landmarkContainer> <nehuba-2dlandmark-unit *ngFor="let spatialData of (selectedPtLandmarks$ | async)" - (mouseenter)="handleMouseEnterLandmark(spatialData)" (mouseleave)="handleMouseLeaveLandmark(spatialData)" + (mouseenter)="handleMouseEnterLandmark(spatialData)" + (mouseleave)="handleMouseLeaveLandmark(spatialData)" [highlight]="spatialData.highlight ? spatialData.highlight : false" [fasClass]="spatialData.type === 'userLandmark' ? 'fa-chevron-down' : 'fa-map-marker'" - [positionX]="getPositionX(2,spatialData)" [positionY]="getPositionY(2,spatialData)" + [positionX]="getPositionX(2,spatialData)" + [positionY]="getPositionY(2,spatialData)" [positionZ]="getPositionZ(2,spatialData)"> </nehuba-2dlandmark-unit> <!-- maximise/minimise button --> <maximise-panel-button + (touchend)="toggleMaximiseMinimise(2)" (click)="toggleMaximiseMinimise(2)" [ngClass]="{ onHover: (panelOrder$ | async | reorderPanelIndexPipe : ( hoveredPanelIndices$ | async )) === 2 }" - [touch-side-class]="2 " class="pe-all"> + [touch-side-class]="2" + class="pe-all"> </maximise-panel-button> <div *ngIf="sliceViewLoading2$ | async" class="loadingIndicator"> @@ -157,9 +158,11 @@ <!-- maximise/minimise button --> <maximise-panel-button + (touchend)="toggleMaximiseMinimise(3)" (click)="toggleMaximiseMinimise(3)" [ngClass]="{ onHover: (panelOrder$ | async | reorderPanelIndexPipe : ( hoveredPanelIndices$ | async )) === 3 }" - [touch-side-class]="3 " class="pe-all"> + [touch-side-class]="3" + class="pe-all"> </maximise-panel-button> <div *ngIf="perspectiveViewLoading$ | async" class="loadingIndicator"> diff --git a/src/ui/nehubaContainer/nehubaViewerInterface/nehubaViewerInterface.directive.ts b/src/ui/nehubaContainer/nehubaViewerInterface/nehubaViewerInterface.directive.ts index 7e771d624656a01040bd7f41d0503a2dd02f553f..35d07e84051f7597b6ac657cf61e39780d4522d4 100644 --- a/src/ui/nehubaContainer/nehubaViewerInterface/nehubaViewerInterface.directive.ts +++ b/src/ui/nehubaContainer/nehubaViewerInterface/nehubaViewerInterface.directive.ts @@ -3,7 +3,7 @@ import { NehubaViewerUnit } from "../nehubaViewer/nehubaViewer.component"; import { Store, select } from "@ngrx/store"; import { IavRootStoreInterface } from "src/services/stateStore.service"; import { Subscription, Observable } from "rxjs"; -import { distinctUntilChanged, filter, switchMap, debounceTime, shareReplay, scan, map, throttleTime } from "rxjs/operators"; +import { distinctUntilChanged, filter, debounceTime, shareReplay, scan, map, throttleTime } from "rxjs/operators"; import { StateInterface as ViewerConfigStateInterface } from "src/services/state/viewerConfig.store"; import { getNavigationStateFromConfig } from "../util"; import { NEHUBA_LAYER_CHANGED, CHANGE_NAVIGATION, VIEWERSTATE_ACTION_TYPES } from "src/services/state/viewerState.store"; diff --git a/src/ui/nehubaContainer/nehubaViewerInterface/nehubaViewerTouch.directive.ts b/src/ui/nehubaContainer/nehubaViewerInterface/nehubaViewerTouch.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..fe3591052eaee13878de6cb8fe6d6f5a83c8a615 --- /dev/null +++ b/src/ui/nehubaContainer/nehubaViewerInterface/nehubaViewerTouch.directive.ts @@ -0,0 +1,260 @@ +import { Directive, ElementRef, Input, HostListener, Output, OnDestroy } from "@angular/core"; +import { Observable, fromEvent, merge, Subscription } from "rxjs"; +import { map, filter, shareReplay, switchMap, pairwise, takeUntil, tap, switchMapTo } from "rxjs/operators"; +import { getExportNehuba } from 'src/util/fn' +import { computeDistance } from "../nehubaViewer/nehubaViewer.component"; + +@Directive({ + selector: '[iav-viewer-touch-interface]', + exportAs: 'iavNehubaViewerTouch' +}) + +export class NehubaViewerTouchDirective implements OnDestroy{ + + @Input('iav-viewer-touch-interface-v-panels') + viewerPanels: [HTMLElement, HTMLElement, HTMLElement, HTMLElement] + + @Input('iav-viewer-touch-interface-vp-to-data') + viewportToData: [any, any, any, any] + + @Input('iav-viewer-touch-interface-ngviewer') + ngViewer: any + + @Input('iav-viewer-touch-interface-nehuba-config') + nehubaConfig: any + + private touchMove$: Observable<any> + private singleTouchStart$: Observable<TouchEvent> + private touchEnd$: Observable<TouchEvent> + private multiTouchStart$: Observable<any> + + public translate$: Observable<any> + + private findPanelIndex = (panel: HTMLElement) => this.viewerPanels.indexOf(panel) + + private _exportNehuba: any + private get exportNehuba(){ + if (!this._exportNehuba) { + this._exportNehuba = getExportNehuba() + } + return this._exportNehuba + } + + private s: Subscription[] = [] + + constructor( + private el: ElementRef, + ){ + + /** + * Touchend also needs to be listened to, as user could start + * with multitouch, and end up as single touch + */ + const touchStart$ = fromEvent(this.el.nativeElement, 'touchstart').pipe( + tap((ev: TouchEvent) => ev.preventDefault()), + shareReplay(1), + ) + this.singleTouchStart$ = merge( + touchStart$, + fromEvent(this.el.nativeElement, 'touchend') + ).pipe( + filter((ev: TouchEvent) => ev.touches.length === 1), + shareReplay(1), + ) + + this.multiTouchStart$ = touchStart$.pipe( + filter((ev: TouchEvent) => ev.touches.length > 1), + ) + + this.touchEnd$ = fromEvent(this.el.nativeElement, 'touchend').pipe( + map(ev => ev as TouchEvent), + ) + + this.touchMove$ = fromEvent(this.el.nativeElement, 'touchmove') + + const multiTouch$ = this.multiTouchStart$.pipe( + // only tracks first 2 touches + map((ev: TouchEvent) => [ this.findPanelIndex(ev.touches[0].target as HTMLElement), this.findPanelIndex(ev.touches[0].target as HTMLElement) ]), + filter(indicies => indicies[0] >= 0 && indicies[0] === indicies[1]), + map(indicies => indicies[0]), + switchMap(panelIndex => fromEvent(this.el.nativeElement, 'touchmove').pipe( + filter((ev: TouchEvent) => ev.touches.length > 1), + pairwise(), + map(([ev0, ev1]) => { + return { + panelIndex, + ev0, + ev1 + } + }), + takeUntil(this.touchEnd$.pipe( + filter(ev => ev.touches.length < 2) + )) + )), + shareReplay(1) + ) + + const multitouchSliceView$ = multiTouch$.pipe( + filter(({ panelIndex }) => panelIndex < 3) + ) + + const multitouchPerspective$ = multiTouch$.pipe( + filter(({ panelIndex }) => panelIndex === 3) + ) + + const rotationByMultiTouch$ = multitouchSliceView$ + + const zoomByMultiTouch$ = multitouchSliceView$.pipe( + map(({ ev1, ev0 }) => { + const d1 = computeDistance( + [ev0.touches[0].screenX, ev0.touches[0].screenY], + [ev0.touches[1].screenX, ev0.touches[1].screenY], + ) + const d2 = computeDistance( + [ev1.touches[0].screenX, ev1.touches[0].screenY], + [ev1.touches[1].screenX, ev1.touches[1].screenY], + ) + const factor = d1 / d2 + return factor + }) + ) + + const translateByMultiTouch$ = multitouchSliceView$.pipe( + map(({ ev0, ev1, panelIndex }) => { + + const av0X = (ev0.touches[0].screenX + ev0.touches[1].screenX) / 2 + const av0Y = (ev0.touches[0].screenY + ev0.touches[1].screenY) / 2 + + const av1X = (ev1.touches[0].screenX + ev1.touches[1].screenX) / 2 + const av1Y = (ev1.touches[0].screenY + ev1.touches[1].screenY) / 2 + + const deltaX = av0X - av1X + const deltaY = av0Y - av1Y + return { + panelIndex, + deltaX, + deltaY + } + }), + ) + + const translateBySingleTouch$ = this.singleTouchStart$.pipe( + map(ev => this.findPanelIndex(ev.target as HTMLElement)), + filter(panelIndex => !!this.ngViewer && panelIndex >= 0 && panelIndex < 3), + switchMap(panelIndex => this.touchMove$.pipe( + pairwise(), + map(([ ev0, ev1 ]: [TouchEvent, TouchEvent]) => { + const deltaX = ev0.touches[0].screenX - ev1.touches[0].screenX + const deltaY = ev0.touches[0].screenY - ev1.touches[0].screenY + return { + panelIndex, + deltaX, + deltaY + } + }), + takeUntil( + merge( + this.touchEnd$, + this.multiTouchStart$ + ) + ) + )) + ) + + const changePerspectiveView$ = this.singleTouchStart$.pipe( + map(ev => this.findPanelIndex(ev.target as HTMLElement)), + filter(panelIndex => panelIndex === 3 ), + switchMapTo(this.touchMove$.pipe( + pairwise(), + map(([ev0, ev1]) => { + return { ev0, ev1 } + }), + takeUntil( + merge( + this.touchEnd$, + this.multiTouchStart$ + ) + ), + )), + ) + + this.s.push( + changePerspectiveView$.subscribe(({ ev1, ev0 }) => { + const { perspectiveNavigationState } = this.ngViewer + + const { vec3 } = this.exportNehuba + + const deltaX = ev0.touches[0].screenX - ev1.touches[0].screenX + const deltaY = ev0.touches[0].screenY - ev1.touches[0].screenY + perspectiveNavigationState.pose.rotateRelative(vec3.fromValues(0, 1, 0), -deltaX / 4.0 * Math.PI / 180.0) + perspectiveNavigationState.pose.rotateRelative(vec3.fromValues(1, 0, 0), deltaY / 4.0 * Math.PI / 180.0) + perspectiveNavigationState.changed.dispatch() + }), + multitouchPerspective$.subscribe(({ ev1, ev0 }) => { + const d1 = computeDistance( + [ev0.touches[0].screenX, ev0.touches[0].screenY], + [ev0.touches[1].screenX, ev0.touches[1].screenY], + ) + const d2 = computeDistance( + [ev1.touches[0].screenX, ev1.touches[0].screenY], + [ev1.touches[1].screenX, ev1.touches[1].screenY], + ) + const factor = d1 / d2 + const { minZoom = null, maxZoom = null } = this.nehubaConfig?.layout?.useNehubaPerspective?.restrictZoomLevel || {} + const { zoomFactor } = this.ngViewer.perspectiveNavigationState + if (!!minZoom && zoomFactor.value * factor < minZoom) { return } + if (!!maxZoom && zoomFactor.value * factor > maxZoom) { return } + zoomFactor.zoomBy(factor) + }), + rotationByMultiTouch$.subscribe(({ panelIndex, ev0, ev1 }) => { + + const dY0 = ev0.touches[1].screenY - ev0.touches[0].screenY + const dX0 = ev0.touches[1].screenX - ev0.touches[0].screenX + const m0 = dY0 / dX0 + + const dY1 = ev1.touches[1].screenY - ev1.touches[0].screenY + const dX1 = ev1.touches[1].screenX - ev1.touches[0].screenX + const m1 = dY1 / dX1 + + const theta = Math.atan( (m1 - m0) / ( 1 + m1 * m0 ) ) + if (isNaN(theta)) return + + const { vec3 } = this.exportNehuba + + const axis = vec3.fromValues( + ...[ + [0, -1, 0], + [1, 0, 0], + [0, 0, 1] + ][panelIndex] + ) + const ori = this.ngViewer.navigationState.pose.orientation.orientation + vec3.transformQuat(axis, axis, ori) + + this.ngViewer.navigationState.pose.rotateRelative(axis, theta) + }), + zoomByMultiTouch$.subscribe(factor => { + if (isNaN(factor)) return + this.ngViewer.navigationState.zoomBy(factor) + }), + merge( + translateBySingleTouch$, + translateByMultiTouch$, + ).subscribe(({ panelIndex, deltaX, deltaY }) => { + if (isNaN(deltaX) || isNaN(deltaX)) return + const { position } = this.ngViewer.navigationState + const pos = position.spatialCoordinates + this.exportNehuba.vec3.set(pos, deltaX, deltaY, 0) + this.exportNehuba.vec3.transformMat4(pos, pos, this.viewportToData[panelIndex]) + + position.changed.dispatch() + }) + ) + } + + ngOnDestroy(){ + while(this.s.length > 0){ + this.s.pop().unsubscribe() + } + } +} diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts index 8703dcbda670840759a482e6dd6a7db3bf618394..4c2ef9b1c0ffe225583cf88052d82b2609242885 100644 --- a/src/ui/ui.module.ts +++ b/src/ui/ui.module.ts @@ -86,6 +86,7 @@ import { StateModule } from "src/state"; import { AuthModule } from "src/auth"; import { FabSpeedDialModule } from "src/components/fabSpeedDial"; import { ActionDialog } from "./actionDialog/actionDialog.component"; +import { NehubaViewerTouchDirective } from "./nehubaContainer/nehubaViewerInterface/nehubaViewerTouch.directive"; @NgModule({ imports : [ @@ -179,6 +180,7 @@ import { ActionDialog } from "./actionDialog/actionDialog.component"; TouchSideClass, ElementOutClickDirective, FixedMouseContextualContainerDirective, + NehubaViewerTouchDirective, ], entryComponents : [ @@ -211,7 +213,8 @@ import { ActionDialog } from "./actionDialog/actionDialog.component"; ViewerStateMini, RegionMenuComponent, FixedMouseContextualContainerDirective, - LandmarkUIComponent + LandmarkUIComponent, + NehubaViewerTouchDirective, ], schemas: [ CUSTOM_ELEMENTS_SCHEMA,