@itwin/core-frontend
Version:
iTwin.js frontend components
1,043 lines (1,042 loc) • 84.2 kB
JavaScript
"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Tools
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.WheelEventProcessor = exports.ToolAdmin = exports.CurrentInputState = exports.SuspendedToolState = exports.ToolState = exports.ToolSettingsState = exports.ManipulatorToolEvent = exports.StartOrResume = void 0;
const core_bentley_1 = require("@itwin/core-bentley");
const core_geometry_1 = require("@itwin/core-geometry");
const core_common_1 = require("@itwin/core-common");
const AccuSnap_1 = require("../AccuSnap");
const FrontendLoggerCategory_1 = require("../common/FrontendLoggerCategory");
const IModelApp_1 = require("../IModelApp");
const LinePlaneIntersect_1 = require("../LinePlaneIntersect");
const NotificationManager_1 = require("../NotificationManager");
const Sprites_1 = require("../Sprites");
const ViewContext_1 = require("../ViewContext");
const Viewport_1 = require("../Viewport");
const ViewStatus_1 = require("../ViewStatus");
const PrimitiveTool_1 = require("./PrimitiveTool");
const Tool_1 = require("./Tool");
const ToolSettings_1 = require("./ToolSettings");
/**
* @public
* @extensions
*/
var StartOrResume;
(function (StartOrResume) {
StartOrResume[StartOrResume["Start"] = 1] = "Start";
StartOrResume[StartOrResume["Resume"] = 2] = "Resume";
})(StartOrResume || (exports.StartOrResume = StartOrResume = {}));
/**
* @public
* @extensions
*/
var ManipulatorToolEvent;
(function (ManipulatorToolEvent) {
ManipulatorToolEvent[ManipulatorToolEvent["Start"] = 1] = "Start";
ManipulatorToolEvent[ManipulatorToolEvent["Stop"] = 2] = "Stop";
ManipulatorToolEvent[ManipulatorToolEvent["Suspend"] = 3] = "Suspend";
ManipulatorToolEvent[ManipulatorToolEvent["Unsuspend"] = 4] = "Unsuspend";
})(ManipulatorToolEvent || (exports.ManipulatorToolEvent = ManipulatorToolEvent = {}));
/** Maintains the state of tool settings properties for the current session.
* @see [[ToolAdmin.toolSettingsState]] to access the state for the current session.
* @public
*/
class ToolSettingsState {
/** Retrieve saved tool settings DialogItemValue by property name. */
getInitialToolSettingValue(toolId, propertyName) {
const key = `${toolId}:${propertyName}`;
const savedValue = window.sessionStorage.getItem(key);
if (null !== savedValue) {
return JSON.parse(savedValue);
}
return undefined;
}
/** Retrieve an array of DialogPropertyItem with the values latest values that were used in the session. */
getInitialToolSettingValues(toolId, propertyNames) {
const initializedProperties = [];
let propertyValue;
propertyNames.forEach((propertyName) => {
propertyValue = this.getInitialToolSettingValue(toolId, propertyName);
if (propertyValue)
initializedProperties.push({ value: propertyValue, propertyName });
});
return initializedProperties.length ? initializedProperties : undefined;
}
/** Save single tool settings value to session storage. */
saveToolSettingProperty(toolId, item) {
const key = `${toolId}:${item.propertyName}`;
const objectAsString = JSON.stringify(item.value);
window.sessionStorage.setItem(key, objectAsString);
}
/** Save an array of tool settings values to session storage */
saveToolSettingProperties(toolId, tsProps) {
tsProps.forEach((item) => this.saveToolSettingProperty(toolId, item));
}
}
exports.ToolSettingsState = ToolSettingsState;
/** @internal */
class ToolState {
coordLockOvr = Tool_1.CoordinateLockOverrides.None;
locateCircleOn = false;
setFrom(other) {
this.coordLockOvr = other.coordLockOvr;
this.locateCircleOn = other.locateCircleOn;
}
clone() {
const val = new ToolState();
val.setFrom(this);
return val;
}
}
exports.ToolState = ToolState;
/** @internal */
class SuspendedToolState {
_toolState;
_accuSnapState;
_locateOptions;
_viewCursor;
_inDynamics;
_shuttingDown = false;
constructor() {
const { toolAdmin, viewManager, accuSnap, locateManager } = IModelApp_1.IModelApp;
toolAdmin.setIncompatibleViewportCursor(true); // Don't save this
this._toolState = toolAdmin.toolState.clone();
this._accuSnapState = accuSnap.toolState.clone();
this._locateOptions = locateManager.options.clone();
this._viewCursor = viewManager.cursor;
this._inDynamics = viewManager.inDynamicsMode;
if (this._inDynamics)
viewManager.endDynamicsMode();
}
stop() {
if (this._shuttingDown)
return;
const { toolAdmin, viewManager, accuSnap, locateManager } = IModelApp_1.IModelApp;
toolAdmin.setIncompatibleViewportCursor(true); // Don't restore this
toolAdmin.toolState.setFrom(this._toolState);
accuSnap.toolState.setFrom(this._accuSnapState);
locateManager.options.setFrom(this._locateOptions);
viewManager.setViewCursor(this._viewCursor);
if (this._inDynamics)
viewManager.beginDynamicsMode();
else
viewManager.endDynamicsMode();
}
}
exports.SuspendedToolState = SuspendedToolState;
/** @internal */
class CurrentInputState {
_rawPoint = new core_geometry_1.Point3d();
_point = new core_geometry_1.Point3d();
_viewPoint = new core_geometry_1.Point3d();
qualifiers = Tool_1.BeModifierKeys.None;
viewport;
button = [new Tool_1.BeButtonState(), new Tool_1.BeButtonState(), new Tool_1.BeButtonState()];
lastButton = Tool_1.BeButton.Data;
inputSource = Tool_1.InputSource.Unknown;
lastMotion = new core_geometry_1.Point2d();
lastMotionEvent;
lastWheelEvent;
lastTouchStart;
touchTapTimer;
touchTapCount;
get rawPoint() { return this._rawPoint; }
set rawPoint(pt) { this._rawPoint.setFrom(pt); }
get point() { return this._point; }
set point(pt) { this._point.setFrom(pt); }
get viewPoint() { return this._viewPoint; }
set viewPoint(pt) { this._viewPoint.setFrom(pt); }
get isShiftDown() { return 0 !== (this.qualifiers & Tool_1.BeModifierKeys.Shift); }
get isControlDown() { return 0 !== (this.qualifiers & Tool_1.BeModifierKeys.Control); }
get isAltDown() { return 0 !== (this.qualifiers & Tool_1.BeModifierKeys.Alt); }
isDragging(button) { return this.button[button].isDragging; }
onStartDrag(button) { this.button[button].isDragging = true; }
onInstallTool() {
this.clearKeyQualifiers();
this.lastWheelEvent = this.lastMotionEvent = undefined;
this.lastTouchStart = this.touchTapTimer = this.touchTapCount = undefined;
}
clearKeyQualifiers() { this.qualifiers = Tool_1.BeModifierKeys.None; }
clearViewport(vp) {
if (vp === this.viewport)
this.viewport = undefined;
}
isAnyDragging() { return this.button.some((button) => button.isDragging); }
setKeyQualifier(qual, down) {
this.qualifiers = down ? (this.qualifiers | qual) : (this.qualifiers & (~qual));
}
setKeyQualifiers(ev) {
this.setKeyQualifier(Tool_1.BeModifierKeys.Shift, ev.shiftKey);
this.setKeyQualifier(Tool_1.BeModifierKeys.Control, ev.ctrlKey);
this.setKeyQualifier(Tool_1.BeModifierKeys.Alt, ev.altKey);
}
onMotion(pt2d) {
this.lastMotion.x = pt2d.x;
this.lastMotion.y = pt2d.y;
}
changeButtonToDownPoint(ev) {
ev.point = this.button[ev.button].downUorPt;
ev.rawPoint = this.button[ev.button].downRawPt;
if (ev.viewport)
ev.viewPoint = ev.viewport.worldToView(ev.rawPoint);
}
updateDownPoint(ev) { this.button[ev.button].downUorPt = ev.point; }
onButtonDown(button) {
let isDoubleClick = false;
const now = Date.now();
const vp = this.viewport;
if (undefined !== vp) {
const viewPt = vp.worldToView(this.button[button].downRawPt);
const center = vp.npcToView(core_common_1.NpcCenter);
viewPt.z = center.z;
isDoubleClick = ((now - this.button[button].downTime) < ToolSettings_1.ToolSettings.doubleClickTimeout.milliseconds) && (viewPt.distance(this.viewPoint) < vp.pixelsFromInches(ToolSettings_1.ToolSettings.doubleClickToleranceInches));
}
this.button[button].init(this.point, this.rawPoint, now, true, isDoubleClick, false, this.inputSource);
this.lastButton = button;
}
onButtonUp(button) {
this.button[button].isDown = false;
this.button[button].isDragging = false;
this.lastButton = button;
}
toEvent(ev, useSnap) {
let coordsFrom = Tool_1.CoordSource.User;
const point = this.point.clone();
let viewport = this.viewport;
if (useSnap) {
const snap = AccuSnap_1.TentativeOrAccuSnap.getCurrentSnap(false);
if (snap) {
coordsFrom = snap.isHot ? Tool_1.CoordSource.ElemSnap : Tool_1.CoordSource.User;
point.setFrom(snap.isPointAdjusted ? snap.adjustedPoint : snap.getPoint()); // NOTE: adjustedPoint can be set by adjustSnapPoint even when not hot...
viewport = snap.viewport;
}
else if (IModelApp_1.IModelApp.tentativePoint.isActive) {
coordsFrom = Tool_1.CoordSource.TentativePoint;
point.setFrom(IModelApp_1.IModelApp.tentativePoint.getPoint());
viewport = IModelApp_1.IModelApp.tentativePoint.viewport;
}
}
const buttonState = this.button[this.lastButton];
ev.init({
point, rawPoint: this.rawPoint, viewPoint: this.viewPoint, viewport, coordsFrom,
keyModifiers: this.qualifiers, button: this.lastButton, isDown: buttonState.isDown,
isDoubleClick: buttonState.isDoubleClick, isDragging: buttonState.isDragging,
inputSource: this.inputSource,
});
}
adjustLastDataPoint(ev) {
const state = this.button[Tool_1.BeButton.Data];
state.downUorPt = ev.point;
state.downRawPt = ev.point;
this.viewport = ev.viewport;
}
toEventFromLastDataPoint(ev) {
const state = this.button[Tool_1.BeButton.Data];
const point = state.downUorPt;
const rawPoint = state.downRawPt;
const viewPoint = this.viewport ? this.viewport.worldToView(rawPoint) : core_geometry_1.Point3d.create(); // BeButtonEvent is invalid when viewport is undefined
ev.init({
point, rawPoint, viewPoint, viewport: this.viewport, coordsFrom: Tool_1.CoordSource.User,
keyModifiers: this.qualifiers, button: Tool_1.BeButton.Data, isDown: state.isDown,
isDoubleClick: state.isDoubleClick, isDragging: state.isDragging, inputSource: state.inputSource,
});
}
fromPoint(vp, pt, source) {
this.viewport = vp;
this._viewPoint.x = pt.x;
this._viewPoint.y = pt.y;
this._viewPoint.z = vp.npcToView(core_common_1.NpcCenter).z;
vp.viewToWorld(this._viewPoint, this._rawPoint);
this.point = this._rawPoint;
this.inputSource = source;
}
fromButton(vp, pt, source, applyLocks) {
this.fromPoint(vp, pt, source);
// NOTE: Using the hit point on the element is preferable to ignoring a snap that is not "hot" completely
if (AccuSnap_1.TentativeOrAccuSnap.getCurrentSnap(false)) {
if (applyLocks)
IModelApp_1.IModelApp.toolAdmin.adjustSnapPoint();
return;
}
IModelApp_1.IModelApp.toolAdmin.adjustPoint(this._point, vp, true, applyLocks);
}
isStartDrag(button) {
// First make sure we aren't already dragging any button
if (this.isAnyDragging())
return false;
const state = this.button[button];
if (!state.isDown)
return false;
if ((Date.now() - state.downTime) <= ToolSettings_1.ToolSettings.startDragDelay.milliseconds)
return false;
const vp = this.viewport;
if (undefined === vp)
return false;
const viewPt = vp.worldToView(state.downRawPt);
const deltaX = Math.abs(this._viewPoint.x - viewPt.x);
const deltaY = Math.abs(this._viewPoint.y - viewPt.y);
return ((deltaX + deltaY) > vp.pixelsFromInches(ToolSettings_1.ToolSettings.startDragDistanceInches));
}
}
exports.CurrentInputState = CurrentInputState;
/** Controls the operation of [[Tool]]s, administering the current [[ViewTool]], [[PrimitiveTool]], and [[IdleTool]] and forwarding events to the appropriate tool.
* @see [[IModelApp.toolAdmin]] to access the session's `ToolAdmin`.
* @public
* @extensions
*/
class ToolAdmin {
markupView;
/** @internal */
currentInputState = new CurrentInputState();
/** @internal */
toolState = new ToolState();
/** Maintains the state of tool settings properties for the current session. */
toolSettingsState = new ToolSettingsState();
_canvasDecoration;
_suspendedByViewTool;
_suspendedByInputCollector;
_viewTool;
_primitiveTool;
_idleTool;
_inputCollector;
_saveCursor;
_saveLocateCircle = false;
_defaultToolId = "Select";
_defaultToolArgs;
_lastHandledMotionTime;
_mouseMoveOverTimeout;
_editCommandHandler;
/** The name of the [[PrimitiveTool]] to use as the default tool.
* Defaults to "Select", referring to [[SelectionTool]].
* @note An empty string signifies no default tool allowing more events to be handled by [[idleTool]].
* @see [[startDefaultTool]] to activate the default tool.
* @see [[defaultToolArgs]] to supply arguments when starting the tool.
*/
get defaultToolId() {
return this._defaultToolId;
}
set defaultToolId(toolId) {
this._defaultToolId = toolId;
}
/** The arguments supplied to the default [[Tool]]'s [[Tool.run]] method from [[startDefaultTool]].
* @see [[defaultToolId]] to configure the default tool.
*/
get defaultToolArgs() {
return this._defaultToolArgs;
}
set defaultToolArgs(args) {
this._defaultToolArgs = args;
}
/** Apply operations such as transform, copy or delete to all members of an assembly. */
assemblyLock = false;
/** If Grid Lock is on, project data points to grid. */
gridLock = false;
/** If ACS Snap Lock is on, project snap points to the ACS plane. */
acsPlaneSnapLock = false;
/** If ACS Plane Lock is on, standard view rotations are relative to the ACS instead of global. */
acsContextLock = false;
/** Options for how uncaught exceptions should be handled by [[ToolAdmin.exceptionHandler]]. */
static exceptionOptions = {
/** Log exception to Logger. */
log: true,
/** Show an alert box explaining that a problem happened. */
alertBox: true,
/** Include the "gory details" (e.g. stack trace) in the alert box. */
details: true,
/** break into debugger (only works if debugger is already opened) */
launchDebugger: true,
};
/** A function that catches otherwise-uncaught exceptions occurring inside ToolAdmin.eventLoop.
* To customize the behavior of this function, modify [[ToolAdmin.exceptionOptions]].
* To replace it within your own handler, simply assign to it, e.g.:
* ```ts
* ToolAdmin.exceptionHandler = (exception: any): Promise<any> => {
* ... your implementation here
* }
* ```
*/
static async exceptionHandler(exception) {
const opts = ToolAdmin.exceptionOptions;
const msg = undefined !== exception.stack ? exception.stack : exception.toString();
if (opts.log)
core_bentley_1.Logger.logError(`${FrontendLoggerCategory_1.FrontendLoggerCategory.Package}.unhandledException`, msg);
if (opts.launchDebugger) // this does nothing if the debugger window is not already opened
debugger; // eslint-disable-line no-debugger
if (!opts.alertBox)
return;
let out = `<h2>${IModelApp_1.IModelApp.localization.getLocalizedString("iModelJs:Errors.ReloadPage")}</h2>`;
if (opts.details) {
out += `<h3>${IModelApp_1.IModelApp.localization.getLocalizedString("iModelJs:Errors.Details")}</h3><h4>`;
msg.split("\n").forEach((line) => out += `${line}<br>`);
out += "</h4>";
}
const div = document.createElement("div");
div.innerHTML = out;
return IModelApp_1.IModelApp.notifications.openMessageBox(NotificationManager_1.MessageBoxType.MediumAlert, div, NotificationManager_1.MessageBoxIconType.Critical);
}
static _removals = [];
/** The registered handler method that will update the UI with any property value changes.
* @internal
*/
_toolSettingsChangeHandler = undefined;
/** Returns the handler registered by the UI layer that allows it to display property changes made by the active Tool. */
get toolSettingsChangeHandler() { return this._toolSettingsChangeHandler; }
set toolSettingsChangeHandler(handler) {
this._toolSettingsChangeHandler = handler;
}
/** The registered handler method that will inform the UI to reload tool setting with properties from active tool.
* @internal
*/
_reloadToolSettingsHandler = undefined;
/** Returns the handler registered by the UI layer that allows it to display property changes made by the active Tool. */
get reloadToolSettingsHandler() { return this._reloadToolSettingsHandler; }
set reloadToolSettingsHandler(handler) {
this._reloadToolSettingsHandler = handler;
}
/** The registered handler method that will trigger UI Sync processing.
* @internal
*/
_toolSyncUiEventDispatcher = undefined;
/** Returns the handler registered by the UI layer that will trigger UiSyncEvent processing that informs UI component to refresh their state. */
get toolSyncUiEventDispatcher() { return this._toolSyncUiEventDispatcher; }
set toolSyncUiEventDispatcher(handler) {
this._toolSyncUiEventDispatcher = handler;
}
/** Handler for keyboard events. */
static _keyEventHandler = (ev) => {
if (!ev.repeat) // we don't want repeated keyboard events. If we keep them they interfere with replacing mouse motion events, since they come as a stream.
ToolAdmin.addEvent(ev);
};
/** @internal */
onInitialized() {
if (typeof document === "undefined")
return; // if document isn't defined, we're probably running in a test environment. At any rate, we can't have interactive tools.
this._idleTool = IModelApp_1.IModelApp.tools.create("Idle");
["keydown", "keyup"].forEach((type) => {
document.addEventListener(type, ToolAdmin._keyEventHandler, false);
ToolAdmin._removals.push(() => document.removeEventListener(type, ToolAdmin._keyEventHandler, false));
});
ToolAdmin._removals.push(() => window.onfocus = null);
}
/** @internal */
onShutDown() {
this.clearMotionPromises();
this._idleTool = undefined;
Sprites_1.IconSprites.emptyAll(); // clear cache of icon sprites
ToolAdmin._removals.forEach((remove) => remove());
ToolAdmin._removals.length = 0;
}
/** Get the ScreenViewport where the cursor is currently, if any. */
get cursorView() { return this.currentInputState.viewport; }
/** Called from ViewManager.dropViewport to prevent tools from continuing to operate on the dropped viewport.
* @internal
*/
forgetViewport(vp) {
// Ignore pending motion promises on fulfillment.
this.clearMotionPromises();
// make sure tools don't think the cursor is still in this viewport.
this.onMouseLeave(vp);
// Invalidate last motion if for this viewport...
if (this.currentInputState.lastMotionEvent?.viewport === vp)
this.currentInputState.lastMotionEvent = undefined;
// Remove any events associated with this viewport.
ToolAdmin._toolEvents = ToolAdmin._toolEvents.filter((ev) => ev.vp !== vp);
}
getMousePosition(event) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return event.vp.mousePosFromEvent(event.ev);
}
getMouseMovement(event) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return event.vp.mouseMovementFromEvent(event.ev);
}
getMouseButton(button) {
switch (button) {
case 1 /* MouseButton.Middle */: return Tool_1.BeButton.Middle;
case 2 /* MouseButton.Right */: return Tool_1.BeButton.Reset;
default: return Tool_1.BeButton.Data;
}
}
async onMouseButton(event, isDown) {
const ev = event.ev;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const vp = event.vp;
const pos = this.getMousePosition(event);
const button = this.getMouseButton(ev.button);
this.currentInputState.setKeyQualifiers(ev);
return isDown ? this.onButtonDown(vp, pos, button, Tool_1.InputSource.Mouse) : this.onButtonUp(vp, pos, button, Tool_1.InputSource.Mouse);
}
async onWheel(event) {
const ev = event.ev;
const vp = event.vp;
if (undefined === vp || this.filterViewport(vp))
return Tool_1.EventHandled.Yes;
const current = this.currentInputState;
current.setKeyQualifiers(ev);
if (ev.deltaY === 0)
return Tool_1.EventHandled.No;
let delta;
switch (ev.deltaMode) {
case ev.DOM_DELTA_LINE:
delta = -ev.deltaY * ToolSettings_1.ToolSettings.wheelLineFactor; // 40
break;
case ev.DOM_DELTA_PAGE:
delta = -ev.deltaY * ToolSettings_1.ToolSettings.wheelPageFactor; // 120;
break;
default: // DOM_DELTA_PIXEL:
delta = -ev.deltaY;
break;
}
const pt2d = this.getMousePosition(event);
vp.setAnimator();
current.fromButton(vp, pt2d, Tool_1.InputSource.Mouse, true);
const wheelEvent = new Tool_1.BeWheelEvent();
wheelEvent.wheelDelta = delta;
current.toEvent(wheelEvent, true);
const overlayHit = this.pickCanvasDecoration(wheelEvent);
if (undefined !== overlayHit && undefined !== overlayHit.onWheel && overlayHit.onWheel(wheelEvent))
return Tool_1.EventHandled.Yes;
const tool = this.activeTool;
if (undefined === tool || Tool_1.EventHandled.Yes !== await tool.onMouseWheel(wheelEvent) && vp !== this.markupView)
return this.idleTool.onMouseWheel(wheelEvent);
return Tool_1.EventHandled.Yes;
}
async sendTapEvent(touchEv) {
const vp = touchEv.viewport;
if (undefined !== vp)
vp.setAnimator();
const overlayHit = this.pickCanvasDecoration(touchEv);
if (undefined !== overlayHit && undefined !== overlayHit.onMouseButton && overlayHit.onMouseButton(touchEv))
return Tool_1.EventHandled.Yes;
if (await IModelApp_1.IModelApp.accuSnap.onTouchTap(touchEv))
return Tool_1.EventHandled.Yes;
const tool = this.activeTool;
if (undefined !== tool && Tool_1.EventHandled.Yes === await tool.onTouchTap(touchEv))
return Tool_1.EventHandled.Yes;
return this.idleTool.onTouchTap(touchEv);
}
async doubleTapTimeout() {
const current = this.currentInputState;
if (undefined === current.touchTapTimer)
return;
const touchEv = current.lastTouchStart;
const numTouches = (undefined !== current.lastTouchStart ? current.lastTouchStart.touchCount : 0);
const numTaps = (undefined !== current.touchTapCount ? current.touchTapCount : 0);
current.touchTapTimer = current.touchTapCount = current.lastTouchStart = undefined;
if (undefined === touchEv || 0 > numTouches || 0 > numTaps)
return;
touchEv.tapCount = numTaps;
await this.sendTapEvent(touchEv);
}
async onTouch(event) {
const touchEvent = event.ev;
const vp = event.vp;
if (undefined === vp || this.filterViewport(vp))
return;
const ev = new Tool_1.BeTouchEvent({ touchEvent });
const current = this.currentInputState;
const pos = Tool_1.BeTouchEvent.getTouchListCentroid(0 !== touchEvent.targetTouches.length ? touchEvent.targetTouches : touchEvent.changedTouches, vp);
switch (touchEvent.type) {
case "touchstart":
if (touchEvent.changedTouches.length === touchEvent.targetTouches.length)
vp.setAnimator(); // Clear viewport animator on start of new touch input (first contact point added)...
current.setKeyQualifiers(touchEvent);
break;
case "touchend":
current.setKeyQualifiers(touchEvent);
break;
}
current.fromButton(vp, undefined !== pos ? pos : core_geometry_1.Point2d.createZero(), Tool_1.InputSource.Touch, true);
current.toEvent(ev, false);
const tool = this.activeTool;
switch (touchEvent.type) {
case "touchstart": {
current.lastTouchStart = ev;
IModelApp_1.IModelApp.accuSnap.onTouchStart(ev);
if (undefined !== tool)
await tool.onTouchStart(ev);
return;
}
case "touchend": {
IModelApp_1.IModelApp.accuSnap.onTouchEnd(ev);
if (undefined !== tool) {
await tool.onTouchEnd(ev);
if (0 === ev.touchCount)
await tool.onTouchComplete(ev);
}
if (undefined === current.lastTouchStart)
return;
if (ev.touchEvent.timeStamp - current.lastTouchStart.touchEvent.timeStamp > (2.0 * ToolSettings_1.ToolSettings.doubleTapTimeout.milliseconds))
return; // Too much time has passed from touchstart to be considered a tap...
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < ev.touchEvent.changedTouches.length; i++) {
const currTouch = ev.touchEvent.changedTouches[i];
const startTouch = Tool_1.BeTouchEvent.findTouchById(current.lastTouchStart.touchEvent.targetTouches, currTouch.identifier);
if (undefined !== startTouch) {
const currPt = Tool_1.BeTouchEvent.getTouchPosition(currTouch, vp);
const startPt = Tool_1.BeTouchEvent.getTouchPosition(startTouch, vp);
if (currPt.distance(startPt) < vp.pixelsFromInches(ToolSettings_1.ToolSettings.touchMoveDistanceInches))
continue; // Hasn't moved appreciably....
}
current.lastTouchStart = undefined; // Not a tap...
return;
}
if (0 !== ev.touchCount || undefined === current.lastTouchStart)
return;
// All fingers off, defer processing tap until we've waited long enough to detect double tap...
if (undefined === current.touchTapTimer) {
current.touchTapTimer = Date.now();
current.touchTapCount = 1;
// NOTE: We cannot await the executeAfter call below, because that prevents any other
// taps from being processed, which makes it impossible for double tap to happen.
// eslint-disable-next-line @typescript-eslint/unbound-method
void ToolSettings_1.ToolSettings.doubleTapTimeout.executeAfter(this.doubleTapTimeout, this);
}
else if (undefined !== current.touchTapCount) {
current.touchTapCount++;
}
return;
}
case "touchcancel": {
current.lastTouchStart = undefined;
IModelApp_1.IModelApp.accuSnap.onTouchCancel(ev);
if (undefined !== tool)
await tool.onTouchCancel(ev);
return;
}
case "touchmove": {
if (!IModelApp_1.IModelApp.accuSnap.onTouchMove(ev) && undefined !== tool)
await tool.onTouchMove(ev);
if (undefined === current.lastTouchStart)
return;
if (ev.touchEvent.timeStamp - current.lastTouchStart.touchEvent.timeStamp < ToolSettings_1.ToolSettings.touchMoveDelay.milliseconds)
return;
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < ev.touchEvent.changedTouches.length; ++i) {
const currTouch = ev.touchEvent.changedTouches[i];
const startTouch = Tool_1.BeTouchEvent.findTouchById(current.lastTouchStart.touchEvent.targetTouches, currTouch.identifier);
if (undefined === startTouch)
continue;
const currPt = Tool_1.BeTouchEvent.getTouchPosition(currTouch, vp);
const startPt = Tool_1.BeTouchEvent.getTouchPosition(startTouch, vp);
if (currPt.distance(startPt) < vp.pixelsFromInches(ToolSettings_1.ToolSettings.touchMoveDistanceInches))
continue; // Hasn't moved appreciably....
const touchStart = current.lastTouchStart;
current.lastTouchStart = undefined;
if (IModelApp_1.IModelApp.accuSnap.onTouchMoveStart(ev, touchStart))
return;
if (undefined === tool || Tool_1.EventHandled.Yes !== await tool.onTouchMoveStart(ev, touchStart))
await this.idleTool.onTouchMoveStart(ev, touchStart);
return;
}
return;
}
}
}
/** A first-in-first-out queue of ToolEvents. */
static _toolEvents = [];
static tryReplace(ev, vp) {
if (ToolAdmin._toolEvents.length < 1)
return false;
const last = ToolAdmin._toolEvents[ToolAdmin._toolEvents.length - 1];
const lastType = last.ev.type;
if (lastType !== ev.type || (lastType !== "mousemove" && lastType !== "touchmove"))
return false; // only mousemove and touchmove can replace previous
last.ev = ev; // sequential moves are not important. Replace the previous one with this one.
last.vp = vp;
return true;
}
/** @internal */
static getNextEvent() {
if (ToolAdmin._toolEvents.length > 1) // if there is more than one event, we're going to need another animation frame to process it.
IModelApp_1.IModelApp.requestNextAnimation();
return ToolAdmin._toolEvents.shift(); // pull first event from the queue
}
/** Called from HTML event listeners. Events are processed in the order they're received in ToolAdmin.eventLoop
* @internal
*/
static addEvent(ev, vp) {
if (!ToolAdmin.tryReplace(ev, vp)) // see if this event replaces the last event in the queue
this._toolEvents.push({ ev, vp }); // otherwise put it at the end of the queue.
IModelApp_1.IModelApp.requestNextAnimation(); // wake up event loop, if
}
/** Process the next event in the event queue, if any. */
async processNextEvent() {
const event = ToolAdmin.getNextEvent(); // pull first event from the queue
if (undefined === event)
return; // nothing in queue
switch (event.ev.type) {
case "mousedown": return this.onMouseButton(event, true);
case "mouseup": return this.onMouseButton(event, false);
case "mousemove": return this.onMouseMove(event);
case "mouseover": return this.onMouseEnter(event);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
case "mouseout": return this.onMouseLeave(event.vp);
case "wheel": return this.onWheel(event);
case "keydown": return this.onKeyTransition(event, true);
case "keyup": return this.onKeyTransition(event, false);
case "touchstart": return this.onTouch(event);
case "touchend": return this.onTouch(event);
case "touchcancel": return this.onTouch(event);
case "touchmove": return this.onTouch(event);
}
}
_processingEvent = false;
/**
* Process a single event, plus timer events. Don't start work on new events if the previous one has not finished.
* @internal
*/
async processEvent() {
if (this._processingEvent)
return; // we're still working on the previous event.
try {
this._processingEvent = true; // we can't allow any further event processing until the current event completes.
await this.processNextEvent();
}
catch (exception) {
await ToolAdmin.exceptionHandler(exception); // we don't attempt to exit here
}
finally {
this._processingEvent = false; // this event is now finished. Allow processing next time through.
}
}
/** The idleTool handles events that are not otherwise processed. */
get idleTool() {
(0, core_bentley_1.assert)(undefined !== this._idleTool);
return this._idleTool;
}
set idleTool(idleTool) {
this._idleTool = idleTool;
}
/** Return true to filter (ignore) events to the given viewport */
filterViewport(vp) {
if (undefined === vp || vp.isDisposed)
return true;
const tool = this.activeTool;
return (undefined !== tool ? !tool.isCompatibleViewport(vp, false) : false);
}
/** @internal */
async onInstallTool(tool) {
this.currentInputState.onInstallTool();
return tool.onInstall();
}
/** @internal */
async onPostInstallTool(tool) { return tool.onPostInstall(); }
get viewTool() { return this._viewTool; }
get primitiveTool() { return this._primitiveTool; }
/** The currently active InteractiveTool. May be ViewTool, InputCollector, PrimitiveTool, undefined - in that priority order. */
get activeTool() {
return this._viewTool ? this._viewTool : (this._inputCollector ? this._inputCollector : this._primitiveTool); // NOTE: Viewing tools suspend input collectors as well as primitives
}
/** The current tool. May be ViewTool, InputCollector, PrimitiveTool, or IdleTool - in that priority order. */
get currentTool() { return this.activeTool ? this.activeTool : this.idleTool; }
/** Allow applications to inhibit specific tooltips, such as for maps. */
wantToolTip(_hit) { return true; }
/** Ask the current tool to provide tooltip contents for the supplied HitDetail. */
async getToolTip(hit) { return this.currentTool.getToolTip(hit); }
/**
* Event raised whenever the active tool changes. This includes PrimitiveTool, ViewTool, and InputCollector.
* @param newTool The newly activated tool
*/
activeToolChanged = new core_bentley_1.BeEvent();
/**
* Event raised by tools that support edit manipulators like the SelectTool.
* @param tool The current tool
*/
manipulatorToolEvent = new core_bentley_1.BeEvent();
async onMouseEnter(event) {
const vp = event.vp;
const current = this.currentInputState;
current.viewport = vp;
// Detect if drag was active and button was released outside the view...
const tool = this.activeTool;
if (undefined === tool)
return;
const buttonMask = event.ev.buttons;
let cancelDrag = false;
current.button.forEach((button, buttonNum) => {
if (button.isDragging && !(buttonMask & (1 << buttonNum))) {
button.isDragging = button.isDown = false;
cancelDrag = true;
}
});
if (cancelDrag)
await tool.onReinitialize();
}
/** @internal */
onMouseLeave(vp) {
if (this._mouseMoveOverTimeout !== undefined)
clearTimeout(this._mouseMoveOverTimeout);
IModelApp_1.IModelApp.accuSnap.clear();
this.currentInputState.clearViewport(vp);
this.setCanvasDecoration(vp);
vp.invalidateDecorations(); // stop drawing locate circle...
}
/** @internal */
updateDynamics(ev, useLastData, adjustPoint) {
if (undefined === this.activeTool)
return;
if (undefined === ev) {
ev = new Tool_1.BeButtonEvent();
if (useLastData)
this.fillEventFromLastDataButton(ev);
else
this.fillEventFromCursorLocation(ev);
if (adjustPoint && undefined !== ev.viewport) {
// Use ev.rawPoint for cursor location when not snapped as ev.point gets adjusted in fromButton...
const snap = AccuSnap_1.TentativeOrAccuSnap.getCurrentSnap(false);
if (undefined !== snap) {
// Account for changes to locks, reset and re-adjust snap point...
snap.adjustedPoint.setFrom(snap.getPoint());
this.adjustSnapPoint();
ev.point.setFrom(snap.isPointAdjusted ? snap.adjustedPoint : snap.getPoint());
}
else {
ev.point.setFrom(IModelApp_1.IModelApp.tentativePoint.isActive ? IModelApp_1.IModelApp.tentativePoint.getPoint() : ev.rawPoint);
this.adjustPoint(ev.point, ev.viewport);
}
IModelApp_1.IModelApp.accuDraw.onMotion(ev);
}
}
if (undefined === ev.viewport)
return;
// Support tools requesting async information in onMouseMotion for use in decorate or onDynamicFrame...
const toolPromise = this._toolMotionPromise = this.activeTool.onMouseMotion(ev);
const tool = this.activeTool;
const vp = ev.viewport;
const motion = ev;
toolPromise.then(() => {
if (undefined === this._toolMotionPromise)
return; // Only early return if canceled, result from a previous motion is preferable to showing nothing...
this.currentInputState.lastMotionEvent = motion; // Save to use for simulation motion...
// Update decorations when dynamics are inactive...
if (!IModelApp_1.IModelApp.viewManager.inDynamicsMode) {
vp.invalidateDecorations();
return;
}
// Update dynamics and decorations only after motion...
const context = new ViewContext_1.DynamicsContext(vp);
tool.onDynamicFrame(motion, context);
context.changeDynamics();
}).catch((_) => { });
}
async sendEndDragEvent(ev) {
let tool = this.activeTool;
if (undefined !== tool) {
if (!tool.isValidLocation(ev, true))
tool = undefined;
else if (tool.receivedDownEvent)
tool.receivedDownEvent = false;
else
tool = undefined;
}
// Don't send tool end drag event if it didn't get the start drag event
if (undefined === tool || Tool_1.EventHandled.Yes !== await tool.onMouseEndDrag(ev))
return this.idleTool.onMouseEndDrag(ev);
}
setCanvasDecoration(vp, dec, ev) {
if (dec === this._canvasDecoration)
return;
if (this._canvasDecoration && this._canvasDecoration.onMouseLeave)
this._canvasDecoration.onMouseLeave();
this._canvasDecoration = dec;
if (ev && dec && dec.onMouseEnter)
dec.onMouseEnter(ev);
vp.canvas.style.cursor = dec ? (dec.decorationCursor ? dec.decorationCursor : "pointer") : IModelApp_1.IModelApp.viewManager.cursor;
vp.invalidateDecorations();
}
pickCanvasDecoration(ev) {
const vp = ev.viewport;
if (undefined === vp)
return undefined;
const decoration = (undefined === this.viewTool) ? vp.pickCanvasDecoration(ev.viewPoint) : undefined;
this.setCanvasDecoration(vp, decoration, ev);
return decoration;
}
/** Current request for locate/snap */
_snapMotionPromise;
/** Current request for active tool motion event */
_toolMotionPromise;
clearMotionPromises() {
this._snapMotionPromise = this._toolMotionPromise = undefined;
}
async forceOnMotionSnap(ev) {
// Make sure that we fire the motion snap event correctly
this._lastHandledMotionTime = undefined;
return this.onMotionSnap(ev);
}
async onMotionSnap(ev) {
try {
await this.onMotionSnapOrSkip(ev);
return true;
}
catch (error) {
if (error instanceof core_bentley_1.AbandonedError)
return false; // expected, not a problem. Just ignore this motion and return.
throw error; // unknown error
}
}
// Call accuSnap.onMotion
async onMotionSnapOrSkip(ev) {
if (this.shouldSkipOnMotionSnap())
return;
await IModelApp_1.IModelApp.accuSnap.onMotion(ev);
this._lastHandledMotionTime = core_bentley_1.BeTimePoint.now();
}
// Should the current onMotionSnap event be skipped to avoid unnecessary ReadPixel calls?
shouldSkipOnMotionSnap() {
if (this._lastHandledMotionTime === undefined)
return false;
const now = core_bentley_1.BeTimePoint.now();
const msSinceLastCall = now.milliseconds - this._lastHandledMotionTime.milliseconds;
const delay = 1000 / ToolSettings_1.ToolSettings.maxOnMotionSnapCallPerSecond;
return msSinceLastCall < delay;
}
async onStartDrag(ev, tool) {
if (undefined !== tool && Tool_1.EventHandled.Yes === await tool.onMouseStartDrag(ev))
return Tool_1.EventHandled.Yes;
// Pass start drag event to idle tool if active tool doesn't explicitly handle it
return this.idleTool.onMouseStartDrag(ev);
}
async onMotion(vp, pt2d, inputSource, forceStartDrag = false, movement) {
const current = this.currentInputState;
current.onMotion(pt2d);
if (this.filterViewport(vp)) {
this.setIncompatibleViewportCursor(false);
return;
}
// Detect when the motion stops by setting a timeout
if (this._mouseMoveOverTimeout !== undefined)
clearTimeout(this._mouseMoveOverTimeout); // If a previous timeout was up, it is cancelled: the movement is not over yet
const ev = new Tool_1.BeButtonEvent();
current.fromPoint(vp, pt2d, inputSource);
current.toEvent(ev, false);
const overlayHit = this.pickCanvasDecoration(ev);
if (undefined !== overlayHit) {
if (overlayHit.onMouseMove)
overlayHit.onMouseMove(ev);
if (undefined === overlayHit.propagateMouseMove || !overlayHit.propagateMouseMove(ev))
return; // we're inside a pickable decoration that doesn't want event sent to tool
}
this._mouseMoveOverTimeout = setTimeout(async () => {
await this.onMotionEnd(vp, pt2d, inputSource);
await processMotion();
}, 100);
const processMotion = async () => {
// Update event to account for AccuSnap adjustments...
current.fromButton(vp, pt2d, inputSource, true);
current.toEvent(ev, true);
ev.movement = movement;
IModelApp_1.IModelApp.accuDraw.onMotion(ev);
let tool = this.activeTool;
const isValidLocation = (undefined !== tool ? tool.isValidLocation(ev, false) : true);
this.setIncompatibleViewportCursor(isValidLocation);
if (forceStartDrag || current.isStartDrag(ev.button)) {
current.onStartDrag(ev.button);
current.changeButtonToDownPoint(ev);
ev.isDragging = true;
if (undefined !== tool) {
if (!isValidLocation)
tool = undefined;
else if (forceStartDrag)
tool.receivedDownEvent = true;
else if (!tool.receivedDownEvent)
tool = undefined;
}
await this.onStartDrag(ev, tool);
return;
}
this.updateDynamics(ev);
};
const snapPromise = this._snapMotionPromise = this.onMotionSnap(ev);
/** When forceStartDrag is true, make sure we don't return a fulfilled promise until we've processed the motion so callers can await it.
* The .then below happens AFTER this method returns its (fulfilled) promise so we can't use that.
*/
if (forceStartDrag) {
await snapPromise;
return processMotion();
}
if (this.isLocateCircleOn)
vp.invalidateDecorations();
snapPromise.then(async (snapOk) => {
if (!snapOk || snapPromise !== this._snapMotionPromise)
return;
return processMotion();
}).catch((_) => { });
}
// Called when we detect that the motion stopped
async onMotionEnd(vp, pos, inputSource) {
const current = this.currentInputState;
const ev = new Tool_1.BeButtonEvent();
current.fromPoint(vp, pos, inputSource);
current.toEvent(ev, false);
await this.forceOnMotionSnap(ev);
}
async onMouseMove(event) {
const vp = event.vp;
if (undefined === vp)
return;
const pos = this.getMousePosition(event);
const mov = this.getMouseMovement(event);
// Sometimes the mouse goes down in a view, but we lose focus while its down so we never receive the up event.
// That makes it look like the motion is a drag. Fix that by clearing the "isDown" based on the buttons member of the MouseEvent.
const buttonMask = event.ev.buttons;
if (!(buttonMask & 1))
this.currentInputState.button[Tool_1.BeButton.Data].isDown = false;
return this.onMotion(vp, pos, Tool_1.InputSource.Mouse, false, mov);
}
adjustPointToACS(pointActive, vp, perpendicular) {
// The "I don't want ACS lock" flag can be set by tools to override the default behavior
if (0 !== (this.toolState.coordLockOvr & Tool_1.CoordinateLockOverrides.ACS))
return;
let viewZRoot;
// Lock to the construction plane
if (vp.view.is3d() && vp.view.isCameraOn)
viewZRoot = vp.view.camera.eye.vectorTo(pointActive);
else
viewZRoot = vp.rotation.getRow(2);
const auxOriginRoot = vp.getAuxCoordOrigin();
const auxRMatrixRoot = vp.getAuxCoordRotation();
let auxNormalRoot = auxRMatrixRoot.getRow(2);
// If ACS xy plane is perpendicular to view and not snapping, project to closest xz or yz plane instead
if (auxNormalRoot.isPerpendicularTo(viewZRoot) && !AccuSnap_1.TentativeOrAccuSnap.isHot) {
const auxXRoot = auxRMatrixRoot.getRow(0);
const auxYRoot = auxRMatrixRoot.getRow(1);
auxNormalRoot = (Math.abs(auxXRoot.dotProduct(viewZRoot)) > Math.abs(auxYRoot.dotProduct(viewZRoot))) ? auxXRoot : auxYRoot;
}
(0, LinePlaneIntersect_1.linePlaneIntersect)(pointActive, pointActive, viewZRoot, auxOriginRoot, auxNormalRoot, perpendicular);
}
adjustPointToGrid(pointActive, vp) {
// The "I don't want grid lock" flag can be set by tools to override the default behavior
if (!this.gridLock || 0 !== (this.toolState.coordLockOvr & Tool_1.CoordinateLockOverrides.Grid))
return;
vp.pointToGrid(pointActive);
}
adjustPoint(pointActive, vp, projectToACS = true, applyLocks = true) {
if (Math.abs(pointActive.z) < 1.0e-7)
pointActive.z = 0.0; // remove Z fuzz introduced by active depth when near 0
let handled = false;
if (applyLocks && !(IModelApp_1.IModelApp.tentativePoint.isActive || IModelApp_1.IModelApp.accuSnap.isHot))
handled = IModelApp_1.IModelApp.accuDraw.adjustPoint(pointActive, vp, false);
// NOTE: We don't need to support axis lock, it is worthless if you have AccuDraw
if (!handled && vp.isPointAdjustmentRequired) {
if (applyLocks)
this.adjustPointToGrid(pointActive, vp);
if (projectToACS)
this.adjustPointToACS(pointActive, vp, false);
}
else if (applyLocks) {
const savePoint = pointActive.clone();
this.adjustPointToGrid(pointActive, vp);
// if grid lock changes point, resend point to accudraw
if (handled && !pointActive.isExactEqual(savePoint))
IModelApp_1.IModelApp.accuDraw.adjustPoint(pointActive, vp, false);
}
if (Math.abs(pointActive.z) < 1.0e-7)
pointActive.z = 0.0;