@inweb/viewer-visualize
Version:
JavaScript library for rendering CAD and BIM files in a browser using VisualizeJS
264 lines (214 loc) • 8.44 kB
text/typescript
///////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2002-2025, Open Design Alliance (the "Alliance").
// All rights reserved.
//
// This software and its documentation and related materials are owned by
// the Alliance. The software may only be incorporated into application
// programs owned by members of the Alliance, subject to a signed
// Membership Agreement and Supplemental Software License Agreement with the
// Alliance. The structure and organization of this software are the valuable
// trade secrets of the Alliance and its suppliers. The software is also
// protected by copyright law and international treaty provisions. Application
// programs incorporating this software must include the following statement
// with their copyright notices:
//
// This application incorporates Open Design Alliance software pursuant to a
// license agreement with Open Design Alliance.
// Open Design Alliance Copyright (C) 2002-2025 by Open Design Alliance.
// All rights reserved.
//
// By use of this software, its documentation or related materials, you
// acknowledge and accept the above terms.
///////////////////////////////////////////////////////////////////////////////
import { Viewer } from "../../Viewer";
import { OrbitAction } from "../Actions/OrbitAction";
import { PanAction } from "../Actions/PanAction";
import { ZoomAction } from "../Actions/ZoomAction";
import { Point2d } from "./Geometry";
import { OdBaseDragger } from "./OdBaseDragger";
export enum GestureAction {
None,
Orbit,
Pan,
Zoom,
}
export class GestureManager extends OdBaseDragger {
private _previousEvents = new Map<number, PointerEvent>();
private _currentEvents = new Map<number, PointerEvent>();
private _lastGestureAction = GestureAction.None;
private _orbitAction: OrbitAction;
private _panAction: PanAction;
private _zoomAction: ZoomAction;
private _initialDistance: number;
private readonly _maxInitialDistanceDifference = 30 * window.devicePixelRatio;
private _isSingleTouchEnabled = false;
public get isSingleTouchEnabled(): boolean {
return this._isSingleTouchEnabled;
}
public set isSingleTouchEnabled(value: boolean) {
this._isSingleTouchEnabled = value;
}
public constructor(subject: Viewer) {
super(subject);
this._orbitAction = new OrbitAction(this.m_module, this.subject, this.beginInteractivity, this.endInteractivity);
this._panAction = new PanAction(
this.m_module,
this.subject,
this.beginInteractivity,
this.endInteractivity,
this.getViewParams,
this.setViewParams
);
this._zoomAction = new ZoomAction(this.m_module, this.subject);
}
private getMiddlePoint(events: Map<number, PointerEvent>): Point2d {
if (events.size !== 2) {
return undefined;
}
const keys = this.getKeys(events);
const point0 = this.relativeCoords(events.get(keys[0]));
const point1 = this.relativeCoords(events.get(keys[1]));
return {
x: Math.floor((point0.x + point1.x) / 2),
y: Math.floor((point0.y + point1.y) / 2),
};
}
private getFirstPoint(events: Map<number, PointerEvent>): Point2d {
if (events.size < 1) {
return undefined;
}
const keys = this.getKeys(events);
return this.relativeCoords(events.get(keys[0]));
}
private getDistance(events: Map<number, PointerEvent>): number {
if (events.size !== 2) {
return -1;
}
const keys = this.getKeys(events);
const point0 = this.relativeCoords(events.get(keys[0]));
const point1 = this.relativeCoords(events.get(keys[1]));
return Math.hypot(point0.x - point1.x, point0.y - point1.y);
}
private updateEvent(event: PointerEvent) {
const eventNotInCurrentEvents = !this._currentEvents.get(event.pointerId);
if (eventNotInCurrentEvents && this._currentEvents.size === 2) {
return;
}
const previousEvent = this._currentEvents.get(event.pointerId);
if (previousEvent) {
this._previousEvents.set(previousEvent.pointerId, previousEvent);
}
this._currentEvents.set(event.pointerId, event);
if (eventNotInCurrentEvents) {
this._initialDistance = this.getDistance(this._currentEvents);
}
}
private removeEvent(event: PointerEvent) {
this._currentEvents.delete(event.pointerId);
this._previousEvents.delete(event.pointerId);
if (this._currentEvents.size < 2) {
this._initialDistance = -1;
}
}
private getKeys(map: Map<number, PointerEvent>): number[] {
return Array.from(map.keys());
}
private analyzeGesture() {
if (this._currentEvents.size === 2) {
const currentDistance = this.getDistance(this._currentEvents);
const previousDistance = this.getDistance(this._previousEvents);
const currentDistanceEqualsInitialDistance =
Math.abs(this._initialDistance - currentDistance) <= this._maxInitialDistanceDifference;
if (currentDistanceEqualsInitialDistance) {
this.executePanAction(this.getMiddlePoint(this._currentEvents));
} else {
if (previousDistance !== -1 && currentDistance !== previousDistance) {
this.executeZoomAction(currentDistance, previousDistance);
}
}
} else if (this._currentEvents.size === 1 && this.isSingleTouchEnabled) {
this.executeOrbitAction(this.getFirstPoint(this._currentEvents));
}
}
private executeZoomAction(currentDistance: number, previousDistance: number) {
if (this._lastGestureAction !== GestureAction.Zoom) {
this.executeEndAction(this._lastGestureAction);
this._lastGestureAction = GestureAction.Zoom;
OdBaseDragger.isGestureActive = true;
}
const zoomSpeed = (currentDistance - previousDistance) / 700;
const zoomFactor = 1 + zoomSpeed;
const middlePoint = this.getMiddlePoint(this._currentEvents);
this._zoomAction.action(middlePoint.x, middlePoint.y, zoomFactor, middlePoint.x, middlePoint.y);
this.subject.update();
}
private executePanAction(currentPoint: Point2d) {
if (this._lastGestureAction !== GestureAction.Pan) {
this.executeEndAction(this._lastGestureAction);
this._lastGestureAction = GestureAction.Pan;
OdBaseDragger.isGestureActive = true;
this._panAction.beginAction(currentPoint.x, currentPoint.y, currentPoint.x, currentPoint.y);
}
this._panAction.action(currentPoint.x, currentPoint.y, currentPoint.x, currentPoint.y);
this.subject.update();
}
private executeOrbitAction(currentPoint: Point2d) {
if (this._lastGestureAction !== GestureAction.Orbit) {
this.executeEndAction(this._lastGestureAction);
this._lastGestureAction = GestureAction.Orbit;
OdBaseDragger.isGestureActive = true;
this._orbitAction.beginAction(currentPoint.x, currentPoint.y);
}
this._orbitAction.action(currentPoint.x, currentPoint.y);
this.subject.update();
}
private executeEndAction(gestureAction: GestureAction) {
if (gestureAction === GestureAction.Orbit) this._orbitAction.endAction();
if (gestureAction === GestureAction.Pan) this._panAction.endAction();
OdBaseDragger.isGestureActive = false;
}
private needIgnoreEvent(event: PointerEvent): boolean {
return (
!this.subject.options.enableZoomWheel || !this.subject.options.enableGestures || !this.eventIsTouchEvent(event)
);
}
private eventIsTouchEvent(event: PointerEvent): boolean {
return event.pointerType === "touch" || event.pointerType === "";
}
override pointerdown(event: PointerEvent) {
if (this.needIgnoreEvent(event)) {
return;
}
this.updateEvent(event);
}
override pointermove(event: PointerEvent) {
if (this.needIgnoreEvent(event)) {
return;
}
this.updateEvent(event);
this.analyzeGesture();
}
override pointerup(event: PointerEvent) {
if (this.needIgnoreEvent(event)) {
return;
}
this.removeEvent(event);
if (this._currentEvents.size < 2) {
this.executeEndAction(this._lastGestureAction);
OdBaseDragger.isGestureActive = false;
}
this._lastGestureAction = GestureAction.None;
}
override pointercancel(event: PointerEvent) {
if (this.needIgnoreEvent(event)) {
return;
}
this.pointerup(event);
}
pointerleave(event: PointerEvent) {
if (this.needIgnoreEvent(event)) {
return;
}
this.pointerup(event);
}
}