@itwin/core-frontend
Version:
iTwin.js frontend components
1,020 lines • 49.2 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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
*/
import { assert } from "@itwin/core-bentley";
import { IModelError } from "@itwin/core-common";
import { Point2d, Point3d, PolygonOps } from "@itwin/core-geometry";
import { LocateFilterStatus } from "../ElementLocateManager";
import { FuzzySearch } from "../FuzzySearch";
import { IModelApp } from "../IModelApp";
/**
* @public
* @extensions
*/
export var BeButton;
(function (BeButton) {
BeButton[BeButton["Data"] = 0] = "Data";
BeButton[BeButton["Reset"] = 1] = "Reset";
BeButton[BeButton["Middle"] = 2] = "Middle";
})(BeButton || (BeButton = {}));
/**
* @public
* @extensions
*/
export var CoordinateLockOverrides;
(function (CoordinateLockOverrides) {
CoordinateLockOverrides[CoordinateLockOverrides["None"] = 0] = "None";
CoordinateLockOverrides[CoordinateLockOverrides["ACS"] = 2] = "ACS";
CoordinateLockOverrides[CoordinateLockOverrides["Grid"] = 4] = "Grid";
CoordinateLockOverrides[CoordinateLockOverrides["All"] = 65535] = "All";
})(CoordinateLockOverrides || (CoordinateLockOverrides = {}));
/** The *source* that generated an event.
* @public
* @extensions
*/
export var InputSource;
(function (InputSource) {
/** Source not defined */
InputSource[InputSource["Unknown"] = 0] = "Unknown";
/** From a mouse or other pointing device */
InputSource[InputSource["Mouse"] = 1] = "Mouse";
/** From a touch screen */
InputSource[InputSource["Touch"] = 2] = "Touch";
})(InputSource || (InputSource = {}));
/** The *source* that generated a coordinate.
* @public
* @extensions
*/
export var CoordSource;
(function (CoordSource) {
/** Event was created by an action from the user */
CoordSource[CoordSource["User"] = 0] = "User";
/** Event was created by a program or by a precision keyin */
CoordSource[CoordSource["Precision"] = 1] = "Precision";
/** Event was created by a tentative point */
CoordSource[CoordSource["TentativePoint"] = 2] = "TentativePoint";
/** Event was created by snapping to an element */
CoordSource[CoordSource["ElemSnap"] = 3] = "ElemSnap";
})(CoordSource || (CoordSource = {}));
/** Numeric mask for a set of modifier keys (control, shift, and alt).
* @public
* @extensions
*/
export var BeModifierKeys;
(function (BeModifierKeys) {
BeModifierKeys[BeModifierKeys["None"] = 0] = "None";
BeModifierKeys[BeModifierKeys["Control"] = 1] = "Control";
BeModifierKeys[BeModifierKeys["Shift"] = 2] = "Shift";
BeModifierKeys[BeModifierKeys["Alt"] = 4] = "Alt";
})(BeModifierKeys || (BeModifierKeys = {}));
/**
* @public
* @extensions
*/
export class BeButtonState {
_downUorPt = new Point3d();
_downRawPt = new Point3d();
downTime = 0;
isDown = false;
isDoubleClick = false;
isDragging = false;
inputSource = InputSource.Unknown;
get downRawPt() { return this._downRawPt; }
set downRawPt(pt) { this._downRawPt.setFrom(pt); }
get downUorPt() { return this._downUorPt; }
set downUorPt(pt) { this._downUorPt.setFrom(pt); }
init(downUorPt, downRawPt, downTime, isDown, isDoubleClick, isDragging, source) {
this.downUorPt = downUorPt;
this.downRawPt = downRawPt;
this.downTime = downTime;
this.isDown = isDown;
this.isDoubleClick = isDoubleClick;
this.isDragging = isDragging;
this.inputSource = source;
}
}
/** Object sent to Tools that holds information about button/touch/wheel events.
* @public
* @extensions
*/
export class BeButtonEvent {
_point = new Point3d();
_rawPoint = new Point3d();
_viewPoint = new Point3d();
_movement;
/** The [[ScreenViewport]] from which this BeButtonEvent was generated. If undefined, this event is invalid. */
viewport;
/** How the coordinate values were generated (either from an action by the user or from a program.) */
coordsFrom = CoordSource.User;
/** The keyboard modifiers that were pressed when the event was generated. */
keyModifiers = BeModifierKeys.None;
/** If true, this event was generated from a mouse-down transition, false from a button-up transition. */
isDown = false;
/** If true, this is the second down in a rapid double-click of the same button. */
isDoubleClick = false;
/** If true, this event was created by pressing, holding, and then moving a mouse button. */
isDragging = false;
/** The mouse button that created this event. */
button = BeButton.Data;
/** Whether this event came from a pointing device (e.g. mouse) or a touch device. */
inputSource = InputSource.Unknown;
constructor(props) {
if (props)
this.init(props);
}
/** Determine whether this BeButtonEvent has valid data.
* @note BeButtonEvents may be constructed as "blank", and are not considered to hold valid data unless the [[viewport]] member is defined.
*/
get isValid() { return this.viewport !== undefined; }
/** The point for this event, in world coordinates.
* @note these coordinates may have been *adjusted* for some reason (e.g. snapping, locks, etc.) from the [[rawPoint]].
*/
get point() { return this._point; }
set point(pt) { this._point.setFrom(pt); }
/** The *raw* (unadjusted) point for this event, in world coordinates. */
get rawPoint() { return this._rawPoint; }
set rawPoint(pt) { this._rawPoint.setFrom(pt); }
/** The point, in screen coordinates for this event.
* @note generally the z value is not useful, but some 3d pointing devices do supply it.
*/
get viewPoint() { return this._viewPoint; }
set viewPoint(pt) { this._viewPoint.setFrom(pt); }
/** The difference in screen coordinates from previous motion event
* @internal
*/
get movement() { return this._movement; }
set movement(mov) { this._movement = mov; }
/** Mark this BeButtonEvent as invalid. Can only become valid again by calling [[init]] */
invalidate() { this.viewport = undefined; }
/** Initialize the values of this BeButtonEvent. */
init(props) {
if (undefined !== props.point)
this.point = props.point;
if (undefined !== props.rawPoint)
this.rawPoint = props.rawPoint;
if (undefined !== props.viewPoint)
this.viewPoint = props.viewPoint;
if (undefined !== props.viewport)
this.viewport = props.viewport;
if (undefined !== props.coordsFrom)
this.coordsFrom = props.coordsFrom;
if (undefined !== props.keyModifiers)
this.keyModifiers = props.keyModifiers;
if (undefined !== props.isDown)
this.isDown = props.isDown;
if (undefined !== props.isDoubleClick)
this.isDoubleClick = props.isDoubleClick;
if (undefined !== props.isDragging)
this.isDragging = props.isDragging;
if (undefined !== props.button)
this.button = props.button;
if (undefined !== props.inputSource)
this.inputSource = props.inputSource;
}
/** Determine whether the control key was pressed */
get isControlKey() { return 0 !== (this.keyModifiers & BeModifierKeys.Control); }
/** Determine whether the shift key was pressed */
get isShiftKey() { return 0 !== (this.keyModifiers & BeModifierKeys.Shift); }
/** Determine whether the alt key was pressed */
get isAltKey() { return 0 !== (this.keyModifiers & BeModifierKeys.Alt); }
/** Copy the values from another BeButtonEvent into this BeButtonEvent */
setFrom(src) {
this.point = src.point;
this.rawPoint = src.rawPoint;
this.viewPoint = src.viewPoint;
this.viewport = src.viewport;
this.coordsFrom = src.coordsFrom;
this.keyModifiers = src.keyModifiers;
this.isDown = src.isDown;
this.isDoubleClick = src.isDoubleClick;
this.isDragging = src.isDragging;
this.button = src.button;
this.inputSource = src.inputSource;
return this;
}
/** Make a copy of this BeButtonEvent. */
clone() { return new this.constructor(this); }
}
/** A ButtonEvent generated by touch input.
* @public
* @extensions
*/
export class BeTouchEvent extends BeButtonEvent {
tapCount = 0;
touchEvent;
get touchCount() { return this.touchEvent.targetTouches.length; }
get isSingleTouch() { return 1 === this.touchCount; }
get isTwoFingerTouch() { return 2 === this.touchCount; }
get isSingleTap() { return 1 === this.tapCount && 1 === this.touchCount; }
get isDoubleTap() { return 2 === this.tapCount && 1 === this.touchCount; }
get isTwoFingerTap() { return 1 === this.tapCount && 2 === this.touchCount; }
constructor(props) {
super(props);
this.touchEvent = props.touchEvent;
}
setFrom(src) {
super.setFrom(src);
this.touchEvent = src.touchEvent;
this.tapCount = src.tapCount;
return this;
}
static getTouchPosition(touch, vp) {
const rect = vp.getClientRect();
return Point2d.createFrom({ x: touch.clientX - rect.left, y: touch.clientY - rect.top });
}
static getTouchListCentroid(list, vp) {
switch (list.length) {
case 0: {
return undefined;
}
case 1: {
return this.getTouchPosition(list[0], vp);
}
case 2: {
return this.getTouchPosition(list[0], vp).interpolate(0.5, this.getTouchPosition(list[1], vp));
}
default: {
const points = [];
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < list.length; i++) {
points.push(this.getTouchPosition(list[i], vp));
}
const centroid = Point2d.createZero();
PolygonOps.centroidAndAreaXY(points, centroid);
return centroid;
}
}
}
static findTouchById(list, id) {
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < list.length; i++) {
if (id === list[i].identifier)
return list[i];
}
return undefined;
}
}
/** A BeButtonEvent generated by movement of a mouse wheel.
* @note wheel events include mouse location.
* @public
* @extensions
*/
export class BeWheelEvent extends BeButtonEvent {
wheelDelta;
time;
constructor(props) {
super(props);
this.wheelDelta = (props && props.wheelDelta !== undefined) ? props.wheelDelta : 0;
this.time = (props && props.time) ? props.time : Date.now();
}
setFrom(src) {
super.setFrom(src);
this.wheelDelta = src.wheelDelta;
this.time = src.time;
return this;
}
}
/** A Tool that performs an action. It has a *toolId* that uniquely identifies it, so it can be found via a lookup in the [[ToolRegistry]].
* Every time a tools run, a new instance of (a subclass of) this class is created and its [[run]] method is invoked.
* @see [[InteractiveTool]] for a base Tool class to handle user input events from a Viewport.
* @see [Tools]($docs/learning/frontend/tools.md)
* @public
* @extensions
*/
export class Tool {
/** If true, this Tool will not appear in the list from [[ToolRegistry.getToolList]]. This should be overridden in subclasses to hide them. */
static hidden = false;
/** The unique string that identifies this tool. This must be overridden in every subclass. */
static toolId = "";
/** The icon for this Tool. This may be overridden in subclasses to provide a tool icon.
* The value is the name of an icon WebFont entry, or if specifying an SVG symbol, use `svg:` prefix.
*/
static iconSpec = "";
/** The namespace that provides localized strings for this Tool. Subclasses should override this. */
static namespace;
/** @internal */
get ctor() { return this.constructor; }
constructor(..._args) { }
/** The minimum number of arguments allowed by [[parseAndRun]]. If subclasses override [[parseAndRun]], they should also
* override this method to indicate the minimum number of arguments their implementation expects. UI controls can use
* this information to ensure the tool has enough information to execute.
*/
static get minArgs() { return 0; }
/** The maximum number of arguments allowed by [[parseAndRun]], or undefined if there is no maximum.
* If subclasses override [[parseAndRun]], they should also override this method to indicate the maximum
* number of arguments their implementation expects.
*/
static get maxArgs() { return 0; }
/**
* Register this Tool class with the [[ToolRegistry]].
* @param namespace optional namespace to supply to [[ToolRegistry.register]]. If undefined, use namespace from superclass.
*/
static register(namespace) {
IModelApp.tools.register(this, namespace);
}
static getLocalizedKey(name) {
const key = `tools.${this.toolId}.${name}`;
const val = IModelApp.localization.getLocalizedString(key, { ns: this.namespace });
return key === val ? undefined : val; // if translation for key doesn't exist, `translate` returns the key as the result
}
/**
* Get the localized keyin string for this Tool class. This returns the value of "tools." + this.toolId + ".keyin" from
* its registered Namespace (e.g. "en/MyApp.json").
*/
static get keyin() {
const keyin = this.getLocalizedKey("keyin");
return (undefined !== keyin) ? keyin : ""; // default to empty string
}
/**
* Get the English keyin string for this Tool class. This returns the value of "tools." + this.toolId + ".keyin" from
* its registered Namespace (e.g. "en/MyApp.json").
*/
static get englishKeyin() {
const key = `tools.${this.toolId}.keyin`;
const val = IModelApp.localization.getEnglishString(this.namespace, key);
return val !== key ? val : ""; // default to empty string
}
/**
* Get the localized flyover for this Tool class. This returns the value of "tools." + this.toolId + ".flyover" from
* its registered Namespace (e.g. "en/MyApp.json"). If that key is not in the localization namespace,
* [[keyin]] is returned.
*/
static get flyover() {
const flyover = this.getLocalizedKey("flyover");
return (undefined !== flyover) ? flyover : this.keyin; // default to keyin
}
/**
* Get the localized description for this Tool class. This returns the value of "tools." + this.toolId + ".description" from
* its registered Namespace (e.g. "en/MyApp.json"). If that key is not in the localization namespace,
* [[flyover]] is returned.
*/
static get description() {
const description = this.getLocalizedKey("description");
return (undefined !== description) ? description : this.flyover; // default to flyover
}
/**
* Get the toolId string for this Tool class. This string is used to identify the Tool in the ToolRegistry and is used to localize
* the keyin, description, etc. from the current locale.
*/
get toolId() { return this.ctor.toolId; }
/** Get the localized keyin string from this Tool's class
* @see `static get keyin()`
*/
get keyin() { return this.ctor.keyin; }
/** Get the localized flyover string from this Tool's class
* @see `static get flyover()`
*/
get flyover() { return this.ctor.flyover; }
/** Get the localized description string from this Tool's class
* @see `static get description()`
*/
get description() { return this.ctor.description; }
/** Get the iconSpec from this Tool's class.
* @see `static iconSpec`
*/
get iconSpec() { return this.ctor.iconSpec; }
/**
* Run this instance of a Tool. Subclasses should override to perform some action.
* @returns `true` if the tool executed successfully.
*/
async run(..._args) { return true; }
/** Run this instance of a tool using a series of string arguments. Override this method to parse the arguments, and if they're
* acceptable, execute your [[run]] method. If the arguments aren't valid, return `false`.
* @note if you override this method, you must also override the static [[minArgs]] and [[maxArgs]] getters.
* @note Generally, implementers of this method are **not** expected to call `super.parseAndRun(...)`. Instead, call your
* [[run]] method with the appropriate (parsed) arguments directly.
*/
async parseAndRun(..._args) {
return this.run();
}
}
/**
* @public
* @extensions
*/
export var EventHandled;
(function (EventHandled) {
EventHandled[EventHandled["No"] = 0] = "No";
EventHandled[EventHandled["Yes"] = 1] = "Yes";
})(EventHandled || (EventHandled = {}));
/** A Tool that may be installed, via [[ToolAdmin]], to handle user input. The ToolAdmin manages the currently installed ViewingTool, PrimitiveTool,
* InputCollector, and IdleTool. Each must derive from this class and there may only be one of each type installed at a time.
* @public
* @extensions
*/
export class InteractiveTool extends Tool {
/** Used to avoid sending tools up events for which they did not receive the down event. */
receivedDownEvent = false;
/** Override to execute additional logic when tool is installed. Return false to prevent this tool from becoming active */
async onInstall() { return true; }
/** Override to execute additional logic after tool becomes active */
async onPostInstall() { }
/** Override Call to reset tool to initial state */
async onReinitialize() { }
/** Invoked when the tool becomes no longer active, to perform additional cleanup logic */
async onCleanup() { }
/** Notification of a ViewTool or InputCollector starting and this tool is being suspended.
* @note Applies only to PrimitiveTool and InputCollector, a ViewTool can't be suspended.
*/
async onSuspend() { }
/** Notification of a ViewTool or InputCollector exiting and this tool is being unsuspended.
* @note Applies only to PrimitiveTool and InputCollector, a ViewTool can't be suspended.
*/
async onUnsuspend() { }
/** Called to support operations on pickable decorations, like snapping. */
testDecorationHit(_id) { return false; }
/** Called to allow snapping to pickable decoration geometry.
* @note Snap geometry can be different from decoration geometry (ex. center point of a + symbol). Valid decoration geometry for snapping should be "stable" and not change based on the current cursor location.
*/
getDecorationGeometry(_hit) { return undefined; }
/**
* Called to allow an active tool to display non-element decorations in overlay mode.
* This method is NOT called while the tool is suspended by a viewing tool or input collector.
*/
decorate(_context) { }
/**
* Called to allow a suspended tool to display non-element decorations in overlay mode.
* This method is ONLY called when the tool is suspended by a viewing tool or input collector.
* @note Applies only to PrimitiveTool and InputCollector, a ViewTool can't be suspended.
*/
decorateSuspended(_context) { }
/** Invoked when the reset button is pressed.
* @return No by default. Sub-classes may ascribe special meaning to this status.
* @note To support right-press menus, a tool should put its reset event processing in onResetButtonUp instead of onResetButtonDown.
*/
async onResetButtonDown(_ev) { return EventHandled.No; }
/** Invoked when the reset button is released.
* @return No by default. Sub-classes may ascribe special meaning to this status.
*/
async onResetButtonUp(_ev) { return EventHandled.No; }
/** Invoked when the data button is pressed.
* @return No by default. Sub-classes may ascribe special meaning to this status.
*/
async onDataButtonDown(_ev) { return EventHandled.No; }
/** Invoked when the data button is released.
* @return No by default. Sub-classes may ascribe special meaning to this status.
*/
async onDataButtonUp(_ev) { return EventHandled.No; }
/** Invoked when the middle mouse button is pressed.
* @return Yes if event completely handled by tool and event should not be passed on to the IdleTool.
*/
async onMiddleButtonDown(_ev) { return EventHandled.No; }
/** Invoked when the middle mouse button is released.
* @return Yes if event completely handled by tool and event should not be passed on to the IdleTool.
*/
async onMiddleButtonUp(_ev) { return EventHandled.No; }
/** Invoked when the cursor is moving */
async onMouseMotion(_ev) { }
/** Invoked when the cursor begins moving while a button is depressed.
* @return Yes if event completely handled by tool and event should not be passed on to the IdleTool.
*/
async onMouseStartDrag(_ev) { return EventHandled.No; }
/** Invoked when the button is released after onMouseStartDrag.
* @note default placement tool behavior is to treat press, drag, and release of data button the same as click, click by calling onDataButtonDown.
* @return Yes if event completely handled by tool and event should not be passed on to the IdleTool.
*/
async onMouseEndDrag(ev) {
if (BeButton.Data !== ev.button)
return EventHandled.No;
if (ev.isDown)
return this.onDataButtonDown(ev);
const downEv = ev.clone();
downEv.isDown = true;
return this.onDataButtonDown(downEv);
}
/** Invoked when the mouse wheel moves.
* @return Yes if event completely handled by tool and event should not be passed on to the IdleTool.
*/
async onMouseWheel(_ev) { return EventHandled.No; }
/** Called when Control, Shift, or Alt modifier keys are pressed or released.
* @param _wentDown up or down key event
* @param _modifier The modifier key mask
* @param _event The event that caused this call
* @return Yes to refresh view decorations or update dynamics.
*/
async onModifierKeyTransition(_wentDown, _modifier, _event) { return EventHandled.No; }
/** Called when any key is pressed or released.
* @param _wentDown up or down key event
* @param _keyEvent The KeyboardEvent
* @return Yes to prevent further processing of this event
* @see [[onModifierKeyTransition]]
*/
async onKeyTransition(_wentDown, _keyEvent) { return EventHandled.No; }
/** Called when user adds a touch point by placing a finger or stylus on the surface. */
async onTouchStart(_ev) { }
/** Called when user removes a touch point by lifting a finger or stylus from the surface. */
async onTouchEnd(_ev) { }
/** Called when the last touch point is removed from the surface completing the current gesture. This is a convenience event sent following onTouchEnd when no target touch points remain on the surface. */
async onTouchComplete(_ev) { }
/** Called when a touch point is interrupted in some way and needs to be dropped from the list of target touches. */
async onTouchCancel(_ev) { }
/** Called when a touch point moves along the surface. */
async onTouchMove(_ev) { }
/** Called after at least one touch point has moved for an appreciable time and distance along the surface to not be considered a tap.
* @param _ev The event that caused this call
* @param _startEv The event from the last call to onTouchStart
* @return Yes if event completely handled by tool and event should not be passed on to the IdleTool.
*/
async onTouchMoveStart(_ev, _startEv) { return EventHandled.No; }
/** Called when touch point(s) are added and removed from a surface within a small time window without any touch point moving.
* @param _ev The event that caused this call
* @return Yes if event completely handled by tool and event should not be passed on to the IdleTool.
* @note A double or triple tap event will not be preceded by a single tap event.
*/
async onTouchTap(_ev) { return EventHandled.No; }
isCompatibleViewport(_vp, _isSelectedViewChange) { return true; }
isValidLocation(_ev, _isButtonEvent) { return true; }
/**
* Called when active view changes. Tool may choose to restart or exit based on current view type.
* @param previous The previously active view.
* @param current The new active view.
*/
onSelectedViewportChanged(_previous, _current) { }
/**
* Invoked before the locate tooltip is displayed to retrieve the information about the located element. Allows the tool to override the toolTip.
* @param hit The HitDetail whose info is needed.
* @return A Promise for the HTMLElement or string to describe the hit.
* @note If you override this method, you may decide whether to call your superclass' implementation or not (it is not required).
*/
async getToolTip(_hit) { return _hit.getToolTip(); }
/** Convenience method to check whether control key is currently down without needing a button event. */
get isControlDown() { return IModelApp.toolAdmin.currentInputState.isControlDown; }
/** Fill the supplied button event from the current cursor location. */
getCurrentButtonEvent(ev) { IModelApp.toolAdmin.fillEventFromCursorLocation(ev); }
/** Call to find out if dynamics are currently active. */
get isDynamicsStarted() { return IModelApp.viewManager.inDynamicsMode; }
/** Call to initialize dynamics mode. While dynamics are active onDynamicFrame will be called. Dynamics are typically only used by a PrimitiveTool that creates or modifies geometric elements. */
beginDynamics() { IModelApp.toolAdmin.beginDynamics(); }
/** Call to terminate dynamics mode. */
endDynamics() { IModelApp.toolAdmin.endDynamics(); }
/** Called to allow Tool to display dynamic elements. */
onDynamicFrame(_ev, _context) { }
/** Invoked to allow tools to filter which elements can be located.
* @return Reject if hit is unacceptable for this tool (fill out response with explanation, if it is defined)
*/
async filterHit(_hit, _out) { return LocateFilterStatus.Accept; }
/** Helper method to keep the view cursor, display of locate circle, and coordinate lock overrides consistent with [[AccuSnap.isLocateEnabled]] and [[AccuSnap.isSnapEnabled]].
* @param enableLocate Value to pass to [[AccuSnap.enableLocate]]. Tools that locate elements should always pass true to give the user feedback regarding the element at the current cursor location.
* @param enableSnap Optional value to pass to [[AccuSnap.enableSnap]]. Tools that don't care about the element pick location should not pass true. Default is false.
* @note User must also have snapping enabled [[AccuSnap.isSnapEnabledByUser]], otherwise [[TentativePoint]] is used to snap.
* @param cursor Optional tool specific cursor override. Default is either cross or dynamics cursor depending on whether dynamics are currently active.
* @param coordLockOvr Optional tool specific coordinate lock overrides. A tool that only identifies elements and does not use [[BeButtonEvent.point]] can set ToolState.coordLockOvr to CoordinateLockOverrides.ACS
* or CoordinateLockOverrides.All, otherwise locate is affected by the input point being first projected to the ACS plane. A tool that will use [[BeButtonEvent.point]], especially those that call [[AccuSnap.enableSnap]]
* should honor all locks and leave ToolState.coordLockOvr set to CoordinateLockOverrides.None, the default for ViewTool and PrimitiveTool.
*/
changeLocateState(enableLocate, enableSnap, cursor, coordLockOvr) {
const { toolAdmin, viewManager, accuSnap } = IModelApp;
if (undefined !== cursor) {
toolAdmin.setCursor(cursor);
toolAdmin.setLocateCircleOn(enableLocate);
viewManager.invalidateDecorationsAllViews();
}
else {
toolAdmin.setLocateCursor(enableLocate);
}
// Always set the one that is true first, otherwise AccuSnap will clear the TouchCursor.
if (enableLocate) {
accuSnap.enableLocate(true);
accuSnap.enableSnap(true === enableSnap);
}
else {
accuSnap.enableSnap(true === enableSnap);
accuSnap.enableLocate(false);
}
if (undefined !== coordLockOvr) {
toolAdmin.toolState.coordLockOvr = coordLockOvr;
}
else {
if (enableLocate && !accuSnap.isSnapEnabled)
toolAdmin.toolState.coordLockOvr |= CoordinateLockOverrides.ACS;
else
toolAdmin.toolState.coordLockOvr &= ~CoordinateLockOverrides.ACS;
}
}
/** Helper method for tools that need to locate existing elements.
* Initializes [[ElementLocateManager]], changes the view cursor to locate, enables display of the locate circle, and sets the appropriate coordinate lock overrides.
* @see [[changeLocateState]]
*/
initLocateElements(enableLocate = true, enableSnap, cursor, coordLockOvr) {
IModelApp.locateManager.initToolLocate();
this.changeLocateState(enableLocate, enableSnap, cursor, coordLockOvr);
}
/** @internal */
toolSettingProperties;
/** @internal */
restoreToolSettingPropertyValue(property) {
const itemValue = IModelApp.toolAdmin.toolSettingsState.getInitialToolSettingValue(this.toolId, property.name);
if (undefined === itemValue?.value)
return false;
property.dialogItemValue = itemValue;
return true;
}
/** @internal */
saveToolSettingPropertyValue(property, itemValue) {
if (undefined === itemValue.value)
return false;
property.value = itemValue.value;
IModelApp.toolAdmin.toolSettingsState.saveToolSettingProperty(this.toolId, property.item);
return true;
}
/** @internal */
syncToolSettingPropertyValue(property, isDisabled) {
if (undefined !== isDisabled)
property.isDisabled = isDisabled;
this.syncToolSettingsProperties([property.syncItem]);
}
/** @internal */
getToolSettingPropertyByName(propertyName) {
const foundProperty = this.toolSettingProperties?.get(propertyName);
if (foundProperty)
return foundProperty;
throw new Error(`property not found: ${propertyName}`);
}
/** Override to return the property that is locked by the supplied property if it is a lock property.
* Used to enable/disable the returned property according to the current lock state.
* @note Only applicable when [[getToolSettingLockProperty]] is not being used automatically enable the lock on a change of value.
* @see [[changeToolSettingPropertyValue]]
* @public
*/
getToolSettingPropertyLocked(_property) {
return undefined;
}
/** Override to return the lock property associated with the supplied non-lock property.
* Used to enable the lock property after the value of the supplied property is changed.
* @see [[changeToolSettingPropertyValue]]
* @beta
*/
getToolSettingLockProperty(_property) {
return undefined;
}
/** Helper method for responding to a tool setting property value change by updating saved settings.
* @see [[applyToolSettingPropertyChange]]
* @see [[getToolSettingPropertyLocked]] to return the corresponding locked property, if any.
* @see [[getToolSettingLockProperty]] to return the corresponding property's lock property, if any.
* @public
*/
changeToolSettingPropertyValue(syncItem) {
const property = this.getToolSettingPropertyByName(syncItem.propertyName);
if (!this.saveToolSettingPropertyValue(property, syncItem.value))
return false;
// Either enable lock when corresponding property value changes, or enable/disable property according to value of lock...
const lockProperty = this.getToolSettingLockProperty(property);
if (undefined !== lockProperty) {
if (!lockProperty.value) {
this.saveToolSettingPropertyValue(lockProperty, { value: true });
this.syncToolSettingPropertyValue(lockProperty);
}
}
else {
const propertyToLock = this.getToolSettingPropertyLocked(property);
if (undefined !== propertyToLock) {
if (undefined === this.getToolSettingLockProperty(propertyToLock))
this.syncToolSettingPropertyValue(propertyToLock, !property.value);
}
}
return true;
}
/** Helper method to establish initial values for tool setting properties from saved settings.
* @see [[supplyToolSettingsProperties]]
* @public
*/
initializeToolSettingPropertyValues(properties) {
if (undefined !== this.toolSettingProperties)
return;
this.toolSettingProperties = new Map();
for (const property of properties) {
this.toolSettingProperties.set(property.name, property);
this.restoreToolSettingPropertyValue(property);
}
}
/** Used to supply list of properties that can be used to generate ToolSettings. If undefined is returned then no ToolSettings will be displayed.
* @see [[initializeToolSettingPropertyValues]]
* @public
*/
supplyToolSettingsProperties() { return undefined; }
/** Used to receive property changes from UI. Return false if there was an error applying updatedValue.
* @see [[changeToolSettingPropertyValue]]
* @public
*/
async applyToolSettingPropertyChange(_updatedValue) { return true; }
/** Called by tool to synchronize the UI with property changes made by tool. This is typically used to provide user feedback during tool dynamics.
* If the syncData contains a quantity value and if the displayValue is not defined, the displayValue will be generated in the UI layer before displaying the value.
* @public
*/
syncToolSettingsProperties(syncData) {
IModelApp.toolAdmin.syncToolSettingsProperties(this.toolId, syncData);
}
/** Called by tool to inform UI to reload ToolSettings with new set of properties. This allows properties to be added or removed from ToolSetting
* component as tool processing progresses.
* @public
*/
reloadToolSettingsProperties() {
IModelApp.toolAdmin.reloadToolSettingsProperties();
}
/** Used to "bump" the value of a tool setting. To "bump" a setting means to toggle a boolean value or cycle through enum values.
* If no `settingIndex` param is specified, the first setting is bumped.
* Return true if the setting was successfully bumped.
* @public
*/
async bumpToolSetting(_settingIndex) { return false; }
}
/** The InputCollector class can be used to implement a command for gathering input
* (ex. get a distance by snapping to 2 points) without affecting the state of the active primitive tool.
* An InputCollector will suspend the active PrimitiveTool and can be suspended by a ViewTool.
* @public
* @extensions
*/
export class InputCollector extends InteractiveTool {
async run(..._args) {
const toolAdmin = IModelApp.toolAdmin;
// An input collector can only suspend a primitive tool, don't install if a viewing tool is active...
if (undefined !== toolAdmin.viewTool || !await toolAdmin.onInstallTool(this))
return false;
await toolAdmin.startInputCollector(this);
await toolAdmin.onPostInstallTool(this);
return true;
}
async exitTool() {
return IModelApp.toolAdmin.exitInputCollector();
}
async onResetButtonUp(_ev) {
await this.exitTool();
return EventHandled.Yes;
}
}
/** The result type of [[ToolRegistry.parseAndRun]].
* @public
* @extensions
*/
export var ParseAndRunResult;
(function (ParseAndRunResult) {
/** The tool's `parseAndRun` method was invoked and returned `true`. */
ParseAndRunResult[ParseAndRunResult["Success"] = 0] = "Success";
/** No tool matching the toolId in the keyin is registered. */
ParseAndRunResult[ParseAndRunResult["ToolNotFound"] = 1] = "ToolNotFound";
/** The number of arguments supplied does not meet the constraints of the Tool. @see [[Tool.minArgs]] and [[Tool.maxArgs]]. */
ParseAndRunResult[ParseAndRunResult["BadArgumentCount"] = 2] = "BadArgumentCount";
/** The tool's `parseAndRun` method returned `false`. */
ParseAndRunResult[ParseAndRunResult["FailedToRun"] = 3] = "FailedToRun";
/** An opening double-quote character was not paired with a closing double-quote character. */
ParseAndRunResult[ParseAndRunResult["MismatchedQuotes"] = 4] = "MismatchedQuotes";
})(ParseAndRunResult || (ParseAndRunResult = {}));
/** Possible errors resulting from [[ToolRegistry.parseKeyin]].
* @public
* @extensions
*/
export var KeyinParseError;
(function (KeyinParseError) {
/** No registered tool matching the keyin was found. */
KeyinParseError[KeyinParseError["ToolNotFound"] = 1] = "ToolNotFound";
/** The opening double-quote of an argument was not terminated with a closing double-quote. */
KeyinParseError[KeyinParseError["MismatchedQuotes"] = 4] = "MismatchedQuotes";
})(KeyinParseError || (KeyinParseError = {}));
/** The ToolRegistry holds a mapping between toolIds and their corresponding [[Tool]] class. This provides the mechanism to
* find Tools by their toolId, and also a way to iterate over the set of Tools available.
* @public
*/
export class ToolRegistry {
tools = new Map();
_keyinList;
shutdown() {
this.tools.clear();
this._keyinList = undefined;
}
/**
* Un-register a previously registered Tool class.
* @param toolId the toolId of a previously registered tool to unRegister.
*/
unRegister(toolId) {
this.tools.delete(toolId);
this._keyinList = undefined;
}
/**
* Register a Tool class. This establishes a connection between the toolId of the class and the class itself.
* @param toolClass the subclass of Tool to register.
* @param namespace the namespace for the localized strings for this tool. If undefined, use namespace from superclass.
*/
register(toolClass, namespace) {
if (namespace) // namespace is optional because it can come from superclass
toolClass.namespace = namespace;
if (toolClass.toolId.length === 0)
return; // must be an abstract class, ignore it
if (!toolClass.namespace)
throw new IModelError(-1, "Tools must have a namespace");
this.tools.set(toolClass.toolId, toolClass);
this._keyinList = undefined; // throw away the current keyinList so we'll produce a new one next time we're asked.
}
/**
* Register all the Tool classes found in a module.
* @param modelObj the module to search for subclasses of Tool.
*/
registerModule(moduleObj, namespace) {
for (const thisMember in moduleObj) { // eslint-disable-line guard-for-in
const thisTool = moduleObj[thisMember];
if (thisTool.prototype instanceof Tool) {
this.register(thisTool, namespace);
}
}
}
/** Look up a tool by toolId */
find(toolId) {
return this.tools.get(toolId);
}
/**
* Look up a tool by toolId and, if found, create an instance with the supplied arguments.
* @param toolId the toolId of the tool
* @param args arguments to pass to the constructor.
* @returns an instance of the registered Tool class, or undefined if toolId is not registered.
*/
create(toolId, ...args) {
const toolClass = this.find(toolId);
return toolClass ? new toolClass(...args) : undefined;
}
/**
* Look up a tool by toolId and, if found, create an instance with the supplied arguments and run it.
* @param toolId toolId of the immediate tool
* @param args arguments to pass to the constructor, and to run.
* @return true if the tool was found and successfully run.
*/
async run(toolId, ...args) {
const tool = this.create(toolId, ...args);
return tool !== undefined && tool.run(...args);
}
/**
* Split key-in into and array of string arguments. Handles embedded quoted strings.
* @param keyin keyin string to process
* #return an Array of string argument
*/
tokenize(keyin) {
const isWhitespace = (char) => "" === char.trim();
const tokens = [];
let index = 0;
let firstQuotedIndex;
while (index < keyin.length) {
// Looking for beginning of next token.
const ch = keyin[index];
if (isWhitespace(ch)) {
++index;
continue;
}
if ('"' !== ch) {
// Unquoted token.
let endIndex = keyin.length;
for (let i = index + 1; i < keyin.length; i++) {
if (isWhitespace(keyin[i])) {
endIndex = i;
break;
}
}
tokens.push(keyin.substring(index, endIndex));
index = endIndex;
continue;
}
// Quoted argument.
if (undefined === firstQuotedIndex)
firstQuotedIndex = tokens.length;
let endQuoteIndex;
let searchIndex = index + 1;
let anyEmbeddedQuotes = false;
while (searchIndex < keyin.length) {
searchIndex = keyin.indexOf('"', searchIndex);
if (-1 === searchIndex)
break;
// A literal " is embedded as ""
if (searchIndex + 1 > keyin.length || keyin[searchIndex + 1] !== '"') {
endQuoteIndex = searchIndex;
break;
}
anyEmbeddedQuotes = true;
searchIndex = searchIndex + 2;
}
if (undefined === endQuoteIndex) {
return { tokens, mismatchedQuotes: true };
}
else {
let token = keyin.substring(index + 1, endQuoteIndex);
if (anyEmbeddedQuotes) {
const regex = /""/g;
token = token.replace(regex, '"');
}
tokens.push(token);
index = endQuoteIndex + 1;
}
}
return { tokens, firstQuotedIndex };
}
/** Given a string consisting of a toolId followed by any number of arguments, locate the corresponding Tool and parse the arguments.
* Tokens are delimited by whitespace.
* The Tool is determined by finding the longest string of unquoted tokens starting at the beginning of the key-in string that matches a registered Tool's
* `keyin` or `englishKeyin`.
* Tokens following the Tool's keyin are parsed as arguments.
* Arguments may be quoted using "double quotes". The opening quote must be preceded by whitespace. Examples, assuming the tool Id is `my keyin`:
* - `my keyin "abc" "def"` => two arguments: `abc` and `def`
* - `my keyin abc"def"` => one argument: `abc"def"`
* A literal double-quote character can be embedded in a quoted argument as follows:
* - `my keyin "abc""def"` => one argument: `abc"def`.
* @param keyin A string consisting of a toolId followed by any number of arguments. The arguments are separated by whitespace.
* @returns The tool, if found, along with an array of parsed arguments.
* @public
*/
parseKeyin(keyin) {
const tools = this.getToolList();
let tool;
const args = [];
const findTool = (lowerKeyin) => tools.find((x) => x.keyin.toLowerCase() === lowerKeyin || x.englishKeyin.toLowerCase() === lowerKeyin);
// try the trivial, common case first
tool = findTool(keyin.toLowerCase());
if (undefined !== tool)
return { ok: true, tool, args };
// Tokenize to separate keyin from arguments
// ###TODO there's actually nothing that prevents a Tool from including leading/trailing spaces in its keyin, or sequences of more than one space...we will fail to find such tools if they exist...
const split = this.tokenize(keyin);
const tokens = split.tokens;
if (split.mismatchedQuotes)
return { ok: false, error: KeyinParseError.MismatchedQuotes };
else if (tokens.length <= 1)
return { ok: false, error: KeyinParseError.ToolNotFound };
// Find the longest starting substring that matches a tool's keyin.
const maxIndex = undefined !== split.firstQuotedIndex ? split.firstQuotedIndex - 1 : tokens.length - 2;
for (let i = maxIndex; i >= 0; i--) {
let substr = tokens[0];
for (let j = 1; j <= i; j++) {
substr += " ";
substr += tokens[j];
}
tool = findTool(substr.toLowerCase());
if (undefined !== tool) {
// Any subsequent tokens are arguments.
for (let k = i + 1; k < tokens.length; k++)
args.push(tokens[k]);
break;
}
}
return tool ? { ok: true, tool, args } : { ok: false, error: KeyinParseError.ToolNotFound };
}
/** Get a list of Tools currently registered, excluding hidden tools */
getToolList() {
if (this._keyinList === undefined) {
this._keyinList = [];
this.tools.forEach((thisTool) => {
if (!thisTool.hidden)
this._keyinList.push(thisTool);
});
}
return this._keyinList;
}
/** Given a string consisting of a toolId followed by any number of arguments, parse the keyin string and invoke the corresponding tool's `parseAndRun` method.
* @param keyin A string consisting of a toolId followed by any number of arguments.
* @returns A status indicating whether the keyin was successfully parsed and executed.
* @see [[parseKeyin]] to parse the keyin string and for a detailed description of the syntax.
* @throws any Error thrown by the tool's `parseAndRun` method.
* @public
*/
async parseAndRun(keyin) {
const parsed = this.parseKeyin(keyin);
if (!parsed.ok) {
switch (parsed.error) {
case KeyinParseError.MismatchedQuotes: return ParseAndRunResult.MismatchedQuotes;
case KeyinParseError.ToolNotFound: return ParseAndRunResult.ToolNotFound;
}
}
assert(parsed.ok); // exhaustive switch above...
const maxArgs = parsed.tool.maxArgs;
if (parsed.args.length < parsed.tool.minArgs || (undefined !== maxArgs && parsed.args.length > maxArgs))
return ParseAndRunResult.BadArgumentCount;
const tool = new parsed.tool();
return await tool.parseAndRun(...parsed.args) ? ParseAndRunResult.Success : ParseAndRunResult.FailedToRun;
}
/**
* Find a tool by its localized keyin using a FuzzySearch
* @param keyin the localized keyin string of the Tool.
* @note Make sure the i18n resources are all loaded (e.g. `await IModelApp.i81n.waitForAllRead()`) before calling this method.
* @public
*/
findPartialMatches(keyin) {
return new FuzzySearch().search(this.getToolList(), ["keyin"], keyin.toLowerCase());
}
/**
* Find a tool by its localized keyin.
* @param keyin the localized keyin string of the Tool.
* @returns the Tool class, if an exact match is found, otherwise returns undefined.
* @note Make sure the i18n resources are all loaded (e.g. `await IModelApp.i81n.waitForAllRead()`) before calling this method.
* @public
*/
findExactMatch(keyin) {
keyin = keyin.toLowerCase();
return this.getToolList().find((thisTool) => thisTool.keyin.toLowerCase() === keyin);
}
}
/** @internal */
export class CoreTools {
static namespace = "CoreTools";
static tools = "CoreTools:tools.";
static translate(prompt) { return IModelApp.localization.getLocalizedString(this.tools + prompt); }
static outputPromptByKey(key) { return IModelApp.notifications.outputPromptByKey(this.tools + key); }
}
//# sourceMappingURL=Tool.js.map