UNPKG

@itwin/core-frontend

Version:
1,020 lines • 49.2 kB
/*--------------------------------------------------------------------------------------------- * 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