UNPKG

@itwin/core-frontend

Version:
1,086 lines • 155 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 AccuDraw */ import { BentleyStatus } from "@itwin/core-bentley"; import { Arc3d, AxisOrder, CurveCurve, CurveCurveApproachType, Geometry, IModelJson as GeomJson, LineSegment3d, Matrix3d, Plane3dByOriginAndUnitNormal, Point2d, Point3d, PointString3d, Ray3d, Transform, Vector2d, Vector3d, } from "@itwin/core-geometry"; import { ColorByName, ColorDef, LinePixels } from "@itwin/core-common"; import { TentativeOrAccuSnap } from "./AccuSnap"; import { ACSDisplayOptions } from "./AuxCoordSys"; import { SnapHeat, SnapMode } from "./HitDetail"; import { IModelApp } from "./IModelApp"; import { StandardViewId } from "./StandardView"; import { BeButton, BeButtonEvent, CoordinateLockOverrides, InputCollector, InputSource } from "./tools/Tool"; import { ViewTool } from "./tools/ViewTool"; import { linePlaneIntersect } from "./LinePlaneIntersect"; import { ViewState } from "./ViewState"; import { QuantityType } from "./quantity-formatting/QuantityFormatter"; import { FormatType, ParseError, Parser } from "@itwin/core-quantity"; import { GraphicType } from "./common/render/GraphicType"; // cspell:ignore dont primitivetools /** @internal */ export var AccuDrawFlags; (function (AccuDrawFlags) { AccuDrawFlags[AccuDrawFlags["None"] = 0] = "None"; AccuDrawFlags[AccuDrawFlags["SetModePolar"] = 1] = "SetModePolar"; AccuDrawFlags[AccuDrawFlags["SetModeRect"] = 2] = "SetModeRect"; AccuDrawFlags[AccuDrawFlags["SetOrigin"] = 4] = "SetOrigin"; AccuDrawFlags[AccuDrawFlags["FixedOrigin"] = 8] = "FixedOrigin"; AccuDrawFlags[AccuDrawFlags["SetRMatrix"] = 16] = "SetRMatrix"; AccuDrawFlags[AccuDrawFlags["SetXAxis"] = 32] = "SetXAxis"; AccuDrawFlags[AccuDrawFlags["SetNormal"] = 64] = "SetNormal"; AccuDrawFlags[AccuDrawFlags["SetDistance"] = 128] = "SetDistance"; AccuDrawFlags[AccuDrawFlags["LockDistance"] = 256] = "LockDistance"; AccuDrawFlags[AccuDrawFlags["Lock_X"] = 512] = "Lock_X"; AccuDrawFlags[AccuDrawFlags["Lock_Y"] = 1024] = "Lock_Y"; AccuDrawFlags[AccuDrawFlags["Lock_Z"] = 2048] = "Lock_Z"; AccuDrawFlags[AccuDrawFlags["Disable"] = 4096] = "Disable"; AccuDrawFlags[AccuDrawFlags["OrientDefault"] = 16384] = "OrientDefault"; AccuDrawFlags[AccuDrawFlags["SetFocus"] = 32768] = "SetFocus"; AccuDrawFlags[AccuDrawFlags["OrientACS"] = 131072] = "OrientACS"; AccuDrawFlags[AccuDrawFlags["SetXAxis2"] = 262144] = "SetXAxis2"; AccuDrawFlags[AccuDrawFlags["LockAngle"] = 524288] = "LockAngle"; AccuDrawFlags[AccuDrawFlags["AlwaysSetOrigin"] = 2097156] = "AlwaysSetOrigin"; AccuDrawFlags[AccuDrawFlags["RedrawCompass"] = 4194304] = "RedrawCompass"; AccuDrawFlags[AccuDrawFlags["UpdateRotation"] = 8388608] = "UpdateRotation"; AccuDrawFlags[AccuDrawFlags["SmartRotation"] = 16777216] = "SmartRotation"; })(AccuDrawFlags || (AccuDrawFlags = {})); /** AccuDraw coordinate input mode * @public */ export var CompassMode; (function (CompassMode) { /** Coordinate input using distance and angle */ CompassMode[CompassMode["Polar"] = 0] = "Polar"; /** Coordinate input using x, y, and z deltas */ CompassMode[CompassMode["Rectangular"] = 1] = "Rectangular"; })(CompassMode || (CompassMode = {})); /** AccuDraw compass base rotation * @public */ export var RotationMode; (function (RotationMode) { /** Aligned with standard view top or ACS top when [[ToolAdmin.acsContextLock]] is enabled */ RotationMode[RotationMode["Top"] = 1] = "Top"; /** Aligned with standard view front or ACS front when [[ToolAdmin.acsContextLock]] is enabled */ RotationMode[RotationMode["Front"] = 2] = "Front"; /** Aligned with standard view right or ACS right when [[ToolAdmin.acsContextLock]] is enabled */ RotationMode[RotationMode["Side"] = 3] = "Side"; /** Aligned with view */ RotationMode[RotationMode["View"] = 4] = "View"; /** Aligned with view ACS */ RotationMode[RotationMode["ACS"] = 5] = "ACS"; /** Not aligned with a standard rotation or ACS */ RotationMode[RotationMode["Context"] = 6] = "Context"; })(RotationMode || (RotationMode = {})); /** @internal */ export var LockedStates; (function (LockedStates) { LockedStates[LockedStates["NONE_LOCKED"] = 0] = "NONE_LOCKED"; LockedStates[LockedStates["X_BM"] = 1] = "X_BM"; LockedStates[LockedStates["Y_BM"] = 2] = "Y_BM"; LockedStates[LockedStates["VEC_BM"] = 4] = "VEC_BM"; LockedStates[LockedStates["DIST_BM"] = 8] = "DIST_BM"; LockedStates[LockedStates["XY_BM"] = 3] = "XY_BM"; LockedStates[LockedStates["ANGLE_BM"] = 7] = "ANGLE_BM"; })(LockedStates || (LockedStates = {})); /** AccuDraw enabled states * @public */ export var CurrentState; (function (CurrentState) { /** Compass disabled/unwanted for this session */ CurrentState[CurrentState["NotEnabled"] = 0] = "NotEnabled"; /** Compass deactivated but CAN be activated by user */ CurrentState[CurrentState["Deactivated"] = 1] = "Deactivated"; /** Compass not displayed awaiting automatic activation (default tool state) */ CurrentState[CurrentState["Inactive"] = 2] = "Inactive"; /** Compass displayed and adjusting points */ CurrentState[CurrentState["Active"] = 3] = "Active"; })(CurrentState || (CurrentState = {})); /** @internal */ export var ContextMode; (function (ContextMode) { ContextMode[ContextMode["Locked"] = 0] = "Locked"; ContextMode[ContextMode["XAxis"] = 1] = "XAxis"; ContextMode[ContextMode["YAxis"] = 2] = "YAxis"; ContextMode[ContextMode["ZAxis"] = 3] = "ZAxis"; ContextMode[ContextMode["XAxis2"] = 4] = "XAxis2"; ContextMode[ContextMode["None"] = 15] = "None"; })(ContextMode || (ContextMode = {})); /** AccuDraw coordinate input fields * @public */ export var ItemField; (function (ItemField) { /** Distance for polar mode */ ItemField[ItemField["DIST_Item"] = 0] = "DIST_Item"; /** Angle for polar mode */ ItemField[ItemField["ANGLE_Item"] = 1] = "ANGLE_Item"; /** X delta for rectangular mode */ ItemField[ItemField["X_Item"] = 2] = "X_Item"; /** Y delta for rectangular mode */ ItemField[ItemField["Y_Item"] = 3] = "Y_Item"; /** Z delta (3d only) */ ItemField[ItemField["Z_Item"] = 4] = "Z_Item"; })(ItemField || (ItemField = {})); /** @internal */ export var KeyinStatus; (function (KeyinStatus) { KeyinStatus[KeyinStatus["Dynamic"] = 0] = "Dynamic"; KeyinStatus[KeyinStatus["Partial"] = 1] = "Partial"; })(KeyinStatus || (KeyinStatus = {})); /** @internal */ export class AccudrawData { flags = 0; // AccuDrawFlags origin = new Point3d(); // used if ACCUDRAW_SetOrigin delta = new Point3d(); // if ACCUDRAW_Lock_X, etc. rMatrix = new Matrix3d(); // if ACCUDRAW_SetRMatrix/ACCUDRAW_Set3dMatrix vector = new Vector3d(); // if ACCUDRAW_SetXAxis, etc. distance = 0; // if ACCUDRAW_SetDistance angle = 0; // if ACCUDRAW_SetAngle zero() { this.flags = this.distance = this.angle = 0; this.origin.setZero(); this.delta.setZero(); this.vector.setZero(); this.rMatrix.setIdentity(); } } /** @internal */ export class Flags { redrawCompass = false; dialogNeedsUpdate = false; rotationNeedsUpdate = true; lockedRotation = false; indexLocked = false; haveValidOrigin = false; fixedOrg = false; auxRotationPlane = RotationMode.Top; contextRotMode = 0; baseRotation = RotationMode.View; baseMode = 0; pointIsOnPlane = false; // whether rawPointOnPlane is on compass plane softAngleLock = false; bearingFixToPlane2D = false; inDataPoint = false; ignoreDataButton = false; animateRotation = false; } /** AccuDraw value round off settings. Allows dynamic distance and angle values to be rounded to the nearest increment of the specified units value(s). * @public */ export class RoundOff { /** Whether rounding is to be applied to the corresponding dynamic distance or angle value. * @note To be considered active, units must also specify at least one increment value. */ active = false; /** Round off increment value(s), meters for distance round off, and radians for angle round off. * @note When multiple values are specified, rounding is based on smallest discernable value at current view zoom. */ units = new Set(); } /** @internal */ export class SavedState { state = CurrentState.NotEnabled; mode = CompassMode.Polar; rotationMode = RotationMode.View; axes = new ThreeAxes(); origin = new Point3d(); auxRotationPlane = 0; contextRotMode = 0; fixedOrg = false; ignoreDataButton = true; // By default the data point that terminates a view tool or input collector should be ignored... ignoreFlags = 0; } /** @internal */ export class ThreeAxes { x = Vector3d.unitX(); y = Vector3d.unitY(); z = Vector3d.unitZ(); setFrom(other) { this.x.setFrom(other.x); this.y.setFrom(other.y); this.z.setFrom(other.z); } fromMatrix3d(rMatrix) { rMatrix.getRow(0, this.x); rMatrix.getRow(1, this.y); rMatrix.getRow(2, this.z); } static createFromMatrix3d(rMatrix, result) { result = result ? result : new ThreeAxes(); result.fromMatrix3d(rMatrix); return result; } toMatrix3d(out) { return Matrix3d.createRows(this.x, this.y, this.z, out); } clone() { const out = new ThreeAxes(); out.setFrom(this); return out; } equals(other) { return this.x.isExactEqual(other.x) && this.y.isExactEqual(other.y) && this.z.isExactEqual(other.z); } } /** Accudraw is an aide for entering coordinate data. * This class is public to allow applications to provide a user interface for AccuDraw, either by implementing their own, or * using the one supplied by the itwin appui package. * @note When writing an [[InteractiveTool]] it is not correct to call methods on AccuDraw directly, tools should instead * provide hints to AccuDraw using [[AccuDrawHintBuilder]]. * @public */ export class AccuDraw { _currentState = CurrentState.NotEnabled; /** Current AccuDraw state */ get currentState() { return this._currentState; } set currentState(state) { if (state === this._currentState) return; const wasActive = this.isActive; this._currentState = state; if (wasActive !== this.isActive) this.onCompassDisplayChange(wasActive ? "hide" : "show"); } /** The current compass mode */ compassMode = CompassMode.Rectangular; /** The current compass rotation */ rotationMode = RotationMode.View; /** @internal */ currentView; // will be nullptr if view not yet defined /** @internal */ published = new AccudrawData(); // Staging area for hints /** @internal */ origin = new Point3d(); // origin point...not on compass plane when z != 0.0 /** @internal */ axes = new ThreeAxes(); // X, Y and Z vectors (3d rotation matrix) /** @internal */ delta = Vector3d.unitZ(); // dialog items (x, y & z) _distance = 0; // current distance _angle = 0; // current angle /** @internal */ locked = LockedStates.NONE_LOCKED; // axis/distance locked bit mask /** @internal */ indexed = LockedStates.NONE_LOCKED; // axis/distance indexed bit mask _distanceRoundOff = new RoundOff(); // distance round off enabled and unit _angleRoundOff = new RoundOff(); // angle round off enabled and unit /** @internal */ flags = new Flags(); // current state flags _fieldLocked = [false, false, false, false, false]; // locked state of fields _keyinStatus = [KeyinStatus.Dynamic, KeyinStatus.Dynamic, KeyinStatus.Dynamic, KeyinStatus.Dynamic, KeyinStatus.Dynamic]; // state of input field /** @internal */ savedStateViewTool = new SavedState(); // Restore point for shortcuts/tools... /** @internal */ savedStateInputCollector = new SavedState(); // Restore point for shortcuts/tools... _savedDistances = []; // History of previous distances... _savedAngles = []; // History of previous angles... _savedDistanceIndex = -1; // Current saved distance index for choosing next/previous value... _savedAngleIndex = -1; // Current saved distance index for choosing next/previous value... /** @internal */ baseAxes = new ThreeAxes(); // Used for "context" base rotation to hold arbitrary rotation w/o needing to change ACS... /** @internal */ lastAxes = new ThreeAxes(); // Last result from UpdateRotation, replaces cM.rMatrix... _lastDistance = 0; // previous saved distance or distance indexing tick _tolerance = 0; // computed view based indexing tolerance _percentChanged = 0; // Compass animation state _threshold = 0; // Threshold for automatic x/y field focus change. _lastSnapDetail; /** @internal */ planePt = new Point3d(); // same as origin unless non-zero locked z value _rawDelta = new Point2d(); // used by rect fix point _rawPoint = new Point3d(); // raw uor point passed to fix point _rawPointOnPlane = new Point3d(); // adjusted rawPoint by applying hard/soft construction plane /** @internal */ point = new Point3d(); // current cursor point /** @internal */ vector = Vector3d.unitZ(); // current/last good locked direction _xIsNegative = false; // Last delta.x was negative _yIsNegative = false; // Last delta.y was negative _xIsExplicit = false; // Sign of delta.x established from user input input, don't allow +/- side flip. _yIsExplicit = false; // Sign of delta.y established from user input input, don't allow +/- side flip. /** Disable automatic focus change when user is entering input. */ dontMoveFocus = false; /** Set input field to move focus to (X_Item or Y_Item) for automatic focus change. */ newFocus = ItemField.X_Item; _rMatrix = new Matrix3d(); /** @internal */ _acsPickId; // Compass Display Preferences... /** @internal */ _compassSizeInches = 0.44; /** @internal */ _animationFrames = 12; /** @internal */ _indexToleranceInches = 0.11; /** @internal */ _frameColor = ColorDef.create(ColorByName.lightGrey); /** @internal */ _fillColor = ColorDef.create(ColorByName.blue); /** @internal */ _xColor = ColorDef.create(ColorByName.red); /** @internal */ _yColor = ColorDef.create(ColorByName.green); /** @internal */ _indexColor = ColorDef.create(ColorByName.white); /** @internal */ _frameColorNoFocus = ColorDef.create(ColorByName.darkGrey); /** @internal */ _fillColorNoFocus = ColorDef.create(ColorByName.lightGrey); /** When true improve behavior for +/- input when cursor switches side and try to automatically manage focus */ smartKeyin = true; /** When true the compass follows the origin hint as opposed to remaining at a fixed location */ floatingOrigin = true; /** When true the z input field will remain locked with it's current value after a data button */ stickyZLock = false; /** When true the compass is always active and on screen instead of following the current tools request to activate */ alwaysShowCompass = false; /** When true all tool hints are applied as opposed to only selected hints */ contextSensitive = true; /** When true the current point is adjusted to the x and y axes when within a close tolerance */ axisIndexing = true; /** When true the current point is adjusted to the last locked distance value when within a close tolerance */ distanceIndexing = true; /** When true locking the angle also moves focus to the angle input field */ autoFocusFields = true; /** When true fully specifying a point by entering both distance and angle in polar mode or XY[Z] in rectangular mode, the point is automatically accepted */ autoPointPlacement = false; /** When true and axisIndexing is allowed the current point is adjusted to the tangent or binormal vector from the last snap when within a close tolerance * @beta */ snapIndexing = true; /** Get distance round off settings */ get distanceRoundOff() { return this._distanceRoundOff; } /** Get angle round off settings */ get angleRoundOff() { return this._angleRoundOff; } static _tempRot = new Matrix3d(); /** @internal */ onInitialized() { this.enableForSession(); } /** @internal */ getRotation(rMatrix) { if (!rMatrix) rMatrix = this._rMatrix; Matrix3d.createRows(this.axes.x, this.axes.y, this.axes.z, rMatrix); return rMatrix; } /** When true AccuDraw is enabled by the application and can be used by interactive tools */ get isEnabled() { return (this.currentState > CurrentState.NotEnabled); } /** When true the compass is displayed and adjusting input points for the current interactive tool */ get isActive() { return CurrentState.Active === this.currentState; } /** When true the compass is not displayed or adjusting points, but it can be automatically activated by the current interactive tool or via shortcuts */ get isInactive() { return (CurrentState.Inactive === this.currentState); } /** When true the compass is not displayed or adjusting points, the current interactive tool has disabled automatic activation, but can still be enabled via shortcuts */ get isDeactivated() { return (CurrentState.Deactivated === this.currentState); } /** Get the current lock state for the supplied input field */ getFieldLock(index) { return this._fieldLocked[index]; } /** @internal */ getKeyinStatus(index) { return this._keyinStatus[index]; } /** Get the current keyin status for the supplied input field */ isDynamicKeyinStatus(index) { return KeyinStatus.Dynamic === this.getKeyinStatus(index); } /** Get whether AccuDraw currently has input focus */ get hasInputFocus() { return true; } /** Called to request input focus be set to AccuDraw. Focus is managed by AccuDraw UI. */ grabInputFocus() { } /** Get default focus item for the current compass mode */ defaultFocusItem() { return (CompassMode.Polar === this.compassMode ? ItemField.DIST_Item : this.newFocus); } /** @internal */ activate() { // Upgrade state to inactive so upgradeToActiveState knows it is ok to move to active... if (CurrentState.Deactivated === this.currentState) this.currentState = CurrentState.Inactive; this.upgradeToActiveState(); } /** @internal */ deactivate() { this.downgradeInactiveState(); // Don't allow compass to come back until user re-enables it... if (CurrentState.Inactive === this.currentState) this.currentState = CurrentState.Deactivated; } /** Whether to show Z input field in 3d. Sub-classes can override to restrict AccuDraw to 2d input when working in overlays where * depth is not important. * @note Intended to be used in conjunction with ViewState.allow3dManipulations returning false to also disable 3d rotation and * ToolAdmin.acsPlaneSnapLock set to true for projecting snapped points to the view's ACS plane. * @see [[ViewState.allow3dManipulations]][[ToolAdmin.acsPlaneSnapLock]] */ is3dCompass(viewport) { return viewport.view.is3d(); } /** Change current compass input mode to either polar or rectangular */ setCompassMode(mode) { if (mode === this.compassMode) return; this.compassMode = mode; this.onCompassModeChange(); } /** Change current compass orientation */ setRotationMode(mode) { if (mode === this.rotationMode) return; this.rotationMode = mode; this.onRotationModeChange(); } /** Change the lock status for the supplied input field */ setFieldLock(index, locked) { if (locked === this._fieldLocked[index]) return; this._fieldLocked[index] = locked; this.onFieldLockChange(index); } /** @internal */ setKeyinStatus(index, status) { if (status === this._keyinStatus[index]) return; this._keyinStatus[index] = status; if (KeyinStatus.Dynamic !== status) this.dontMoveFocus = true; if (KeyinStatus.Partial === status) this._threshold = Math.abs(ItemField.X_Item === index ? this._rawDelta.y : this._rawDelta.x) + this._tolerance; this.onFieldKeyinStatusChange(index); } needsRefresh(vp) { if (!this.isEnabled || this.isDeactivated) return false; // Get snap point from AccuSnap/Tentative or use raw point... let distance = 0.0; let snapPt = this._rawPoint; const ptP = this.point; const snap = TentativeOrAccuSnap.getCurrentSnap(); if (snap) { snapPt = snap.snapPoint; distance = ptP.distance(snapPt); } const isRectMode = (CompassMode.Rectangular === this.compassMode); const offsetSnap = ((TentativeOrAccuSnap.isHot || IModelApp.tentativePoint.isActive) && ((this.locked) || (distance > 0.0))); // XY Offset: if (offsetSnap) { if (isRectMode) { let xIsOffset = false, yIsOffset = false; const vec = ptP.vectorTo(this._rawPointOnPlane); const xOffset = vec.dotProduct(this.axes.x); const yOffset = vec.dotProduct(this.axes.y); xIsOffset = (Math.abs(xOffset) > 1.0); yIsOffset = (Math.abs(yOffset) > 1.0); if (xIsOffset || yIsOffset) return true; } } const isOnCompassPlane = (!vp.view.is3d() || this.flags.pointIsOnPlane || this.isZLocked(vp)); // Z Offset: if (offsetSnap) { if (isOnCompassPlane) { const zOffset = snapPt.distance(this._rawPointOnPlane); if (zOffset > 1e-12 /* Constants.SMALL_ANGLE */ || zOffset < -1e-12 /* Constants.SMALL_ANGLE */) return true; } } // Fat Point: if (offsetSnap) return true; let axisIsIndexed = false; // Axis Indexing: if (isRectMode) { if ((this.indexed & LockedStates.XY_BM) && (this.flags.pointIsOnPlane || this._fieldLocked[ItemField.Z_Item])) axisIsIndexed = true; } else { if ((this.indexed & LockedStates.ANGLE_BM || this.locked & LockedStates.ANGLE_BM) && (this.flags.pointIsOnPlane || this._fieldLocked[ItemField.Z_Item])) axisIsIndexed = true; } if (axisIsIndexed) return true; // Distance Indexing: if (this.indexed & LockedStates.DIST_BM) return true; // XY Lock: if (isRectMode) { const locked = this.locked & LockedStates.XY_BM; if ((0 !== locked) && isOnCompassPlane) { switch (locked) { case LockedStates.X_BM: case LockedStates.Y_BM: case LockedStates.XY_BM: return true; } } } return false; } updateLastSnapDetail(vp, fromSnap) { if (!this.snapIndexing || !this.axisIndexing || this.flags.indexLocked || LockedStates.NONE_LOCKED !== this.locked) { this._lastSnapDetail = undefined; return; } if (!fromSnap) { if (this._lastSnapDetail?.deselect) this._lastSnapDetail = undefined; else if (this._lastSnapDetail) this._lastSnapDetail.deselect = false; // Allow moving cursor back to previous snap to clear it... return; } const snap = TentativeOrAccuSnap.getCurrentSnap(); if (undefined === snap || this.origin.isAlmostEqual(snap.getPoint())) return; if (this._lastSnapDetail && this._lastSnapDetail.sourceId === snap.sourceId && this._lastSnapDetail.point.isAlmostEqual(snap.getPoint())) { if (false === this._lastSnapDetail.deselect) this._lastSnapDetail.deselect = true; // Stop indexing to previous snap... return; // Don't compute rotation if location hasn't changed... } const isTentative = IModelApp.tentativePoint.isSnapped; if (this._lastSnapDetail?.isTentative && !isTentative) return; // Don't update snap from tentative unless it's another tentative to support holding a snap location in a dense drawing... const rotation = AccuDraw.getSnapRotation(snap, vp); if (undefined === rotation) return; this._lastSnapDetail = { point: snap.getPoint().clone(), matrix: rotation, sourceId: snap.sourceId, isTentative }; } /** @internal */ adjustPoint(pointActive, vp, fromSnap) { if (!this.isEnabled) return false; const lastWasIndexed = (0 !== this.indexed); let pointChanged = false, handled = false; if (0.0 !== pointActive.z && !vp.isPointAdjustmentRequired) pointActive.z = 0.0; if (1.0 !== vp.view.getAspectRatioSkew()) this.downgradeInactiveState(); // Disable AccuDraw if skew is applied with AccuDraw already active... if (this.isInactive) { this.point.setFrom(pointActive); this.currentView = vp; this.fixPoint(pointActive, vp); if (!fromSnap && IModelApp.accuSnap.currHit) this.flags.redrawCompass = true; } else if (this.isActive) { this.updateLastSnapDetail(vp, fromSnap); const lastPt = this.point.clone(); this.fixPoint(pointActive, vp); pointChanged = !lastPt.isExactEqual(this.point); this.processHints(); handled = true; } else { this.currentView = vp; // Keep view up to date... } // If redraw of compass isn't required (yet!) check if needed... if (!this.flags.redrawCompass && this.isActive) { // Redraw required to erase/draw old/new indexing geometry... if (pointChanged && (lastWasIndexed || this.needsRefresh(vp))) this.flags.redrawCompass = true; } // Redraw is necessary, force decorators to be called... if (this.flags.redrawCompass) vp.invalidateDecorations(); return handled; } setDefaultOrigin(vp) { if (!vp || this.locked || this._fieldLocked[ItemField.Z_Item]) return; const view = vp.view; const rMatrix = view.getRotation(); const acsOrigin = vp.getAuxCoordOrigin(); rMatrix.multiplyVectorInPlace(acsOrigin); const origin = view.getCenter(); view.getRotation().multiplyVectorInPlace(origin); origin.z = acsOrigin.z; view.getRotation().multiplyTransposeVectorInPlace(origin); this.origin.setFrom(origin); // View center at acs z... this.planePt.setFrom(origin); } /** @internal */ isZLocked(vp) { if (this._fieldLocked[ItemField.Z_Item]) return true; if (vp.isSnapAdjustmentRequired && TentativeOrAccuSnap.isHot) return true; return false; } /** @internal */ accountForAuxRotationPlane(rot, plane) { // ACS mode now can have "front" and "side" variations... switch (plane) { case RotationMode.Top: return; case RotationMode.Front: const temp = rot.y.clone(); rot.y.setFrom(rot.z); temp.scale(-1.0, rot.z); return; case RotationMode.Side: const temp0 = rot.x.clone(); rot.x.setFrom(rot.y); rot.y.setFrom(rot.z); rot.z.setFrom(temp0); } } accountForACSContextLock(vec) { // Base rotation is relative to ACS when ACS context lock is enabled... if (!this.currentView || !this.currentView.isContextRotationRequired) return; const rMatrix = AccuDraw.getStandardRotation(StandardViewId.Top, this.currentView, true); rMatrix.multiplyTransposeVectorInPlace(vec); } static useACSContextRotation(vp, isSnap) { if (isSnap) { if (!vp.isSnapAdjustmentRequired) return false; } else { if (!vp.isContextRotationRequired) return false; } return true; } /** Gets X, Y or Z vector from top, front, (right) side, ACS, or View. */ getStandardVector(whichVec) { const vp = this.currentView; let rMatrix; let myAxes; const vecP = Vector3d.createZero(); switch (this.flags.baseRotation) { case RotationMode.Top: switch (whichVec) { case 0: vecP.x = 1.0; break; case 1: vecP.y = 1.0; break; case 2: vecP.z = 1.0; break; } this.accountForACSContextLock(vecP); break; case RotationMode.Front: switch (whichVec) { case 0: vecP.x = 1.0; break; case 1: vecP.z = 1.0; break; case 2: vecP.y = -1.0; break; } this.accountForACSContextLock(vecP); break; case RotationMode.Side: switch (whichVec) { case 0: vecP.y = 1.0; break; case 1: vecP.z = 1.0; break; case 2: vecP.x = 1.0; break; } this.accountForACSContextLock(vecP); break; case RotationMode.ACS: rMatrix = vp ? vp.getAuxCoordRotation() : Matrix3d.createIdentity(); myAxes = ThreeAxes.createFromMatrix3d(rMatrix); this.accountForAuxRotationPlane(myAxes, this.flags.auxRotationPlane); switch (whichVec) { case 0: vecP.setFrom(myAxes.x); break; case 1: vecP.setFrom(myAxes.y); break; case 2: vecP.setFrom(myAxes.z); break; } break; case RotationMode.View: rMatrix = vp ? vp.rotation : Matrix3d.createIdentity(); rMatrix.getRow(whichVec, vecP); break; case RotationMode.Context: myAxes = this.baseAxes.clone(); this.accountForAuxRotationPlane(myAxes, this.flags.auxRotationPlane); switch (whichVec) { case 0: vecP.setFrom(myAxes.x); break; case 1: vecP.setFrom(myAxes.y); break; case 2: vecP.setFrom(myAxes.z); break; } break; } return vecP; } getBestViewedRotationFromXVector(rotation, vp) { const viewZ = vp.rotation.getRow(2); const vec1 = this.getStandardVector(2); const vec2 = this.getStandardVector(1); const vec3 = this.getStandardVector(0); const rot1 = vec1.crossProduct(rotation.x); const rot2 = vec2.crossProduct(rotation.x); const rot3 = vec3.crossProduct(rotation.x); const useRot1 = (rot1.normalizeWithLength(rot1).mag > 0.00001); const useRot2 = (rot2.normalizeWithLength(rot2).mag > 0.00001); const useRot3 = (rot3.normalizeWithLength(rot3).mag > 0.00001); const dot1 = (useRot1 ? Math.abs(rotation.x.crossProduct(rot1).dotProduct(viewZ)) : -1.0); const dot2 = (useRot2 ? Math.abs(rotation.x.crossProduct(rot2).dotProduct(viewZ)) : -1.0); const dot3 = (useRot3 ? Math.abs(rotation.x.crossProduct(rot3).dotProduct(viewZ)) : -1.0); const max = Math.max(dot1, dot2, dot3); if (Geometry.isDistanceWithinTol(dot1 - dot2, 0.1) && (max !== dot3)) rotation.y.setFrom(rot1); else if (max === dot1) rotation.y.setFrom(rot1); else if (max === dot2) rotation.y.setFrom(rot2); else rotation.y.setFrom(rot3); rotation.z.setFrom(rotation.x.crossProduct(rotation.y)); } getRotationFromVector(rotation, whichVec) { let vec; switch (whichVec) { case 0: vec = this.getStandardVector(2); vec.crossProduct(rotation.x, rotation.y); if (rotation.y.normalizeWithLength(rotation.y).mag < .00001) { vec = this.getStandardVector(1); vec.crossProduct(rotation.x, rotation.y); rotation.y.normalizeInPlace(); } rotation.x.crossProduct(rotation.y, rotation.z); break; case 1: vec = this.getStandardVector(2); vec.crossProduct(rotation.y, rotation.x); if (rotation.x.normalizeWithLength(rotation.x).mag < .00001) { vec = this.getStandardVector(0); vec.crossProduct(rotation.y, rotation.x); rotation.x.normalizeInPlace(); } rotation.x.crossProduct(rotation.y, rotation.z); break; case 2: vec = this.getStandardVector(0); rotation.z.crossProduct(vec, rotation.y); if (rotation.y.normalizeWithLength(rotation.y).mag < .00001) { vec = this.getStandardVector(1); vec.crossProduct(rotation.z, rotation.x); rotation.x.normalizeInPlace(); rotation.z.crossProduct(rotation.x, rotation.y); } else { rotation.y.crossProduct(rotation.z, rotation.x); } break; } } /** @internal */ updateRotation(animate = false, newRotationIn) { let clearLocks = true; const oldRotation = this.axes.clone(); let rMatrix; let newRotation; if (!newRotationIn) newRotation = this.axes.clone(); // for axis based else newRotation = ThreeAxes.createFromMatrix3d(newRotationIn); // for animating context rotation change... const vp = this.currentView; const useACS = vp ? vp.isContextRotationRequired : false; switch (this.rotationMode) { case RotationMode.Top: // Get standard rotation relative to ACS when ACS context lock is enabled... newRotation.fromMatrix3d(AccuDraw.getStandardRotation(StandardViewId.Top, vp, useACS)); this.flags.lockedRotation = true; break; case RotationMode.Front: // Get standard rotation relative to ACS when ACS context lock is enabled... newRotation.fromMatrix3d(AccuDraw.getStandardRotation(StandardViewId.Front, vp, useACS)); this.flags.lockedRotation = true; break; case RotationMode.Side: // Get standard rotation relative to ACS when ACS context lock is enabled... newRotation.fromMatrix3d(AccuDraw.getStandardRotation(StandardViewId.Right, vp, useACS)); this.flags.lockedRotation = true; break; case RotationMode.ACS: rMatrix = vp ? vp.getAuxCoordRotation() : Matrix3d.createIdentity(); newRotation.fromMatrix3d(rMatrix); this.accountForAuxRotationPlane(newRotation, this.flags.auxRotationPlane); this.flags.lockedRotation = true; break; case RotationMode.View: rMatrix = vp ? vp.rotation : Matrix3d.createIdentity(); newRotation.fromMatrix3d(rMatrix); this.flags.lockedRotation = false; break; case RotationMode.Context: switch (this.flags.contextRotMode) { case ContextMode.XAxis: this.getRotationFromVector(newRotation, 0); clearLocks = (LockedStates.Y_BM !== this.locked || !oldRotation.x.isExactEqual(newRotation.x)); // Try to keep locked axis when tool being unsuspended... break; case ContextMode.XAxis2: if (vp) this.getBestViewedRotationFromXVector(newRotation, vp); // Use base rotation axis that results in compass being most closely aligned to view direction.... else this.getRotationFromVector(newRotation, 0); clearLocks = (LockedStates.Y_BM !== this.locked || !oldRotation.x.isExactEqual(newRotation.x)); // Try to keep locked axis when tool being unsuspended... break; case ContextMode.YAxis: this.getRotationFromVector(newRotation, 1); clearLocks = (LockedStates.X_BM !== this.locked || !oldRotation.y.isExactEqual(newRotation.y)); // Try to keep locked axis when tool being unsuspended... break; case ContextMode.ZAxis: this.getRotationFromVector(newRotation, 2); break; case ContextMode.Locked: break; } break; } const isChanged = !oldRotation.equals(newRotation); // unlock stuff if rotation has changed if (isChanged && clearLocks && (CompassMode.Rectangular === this.compassMode || !this._fieldLocked[ItemField.DIST_Item] || animate)) { this.locked = this.indexed = LockedStates.NONE_LOCKED; this.unlockAllFields(); } this.axes.setFrom(newRotation); this.lastAxes.setFrom(newRotation); this.flags.redrawCompass = true; // If animate frame preference is set... if (!animate || !vp) return; // AccuDrawAnimatorPtr animator = AccuDrawAnimator:: Create(); // viewport -> SetAnimator(* animator); // animator -> ChangeOfRotation(Matrix3d:: FromColumnVectors(oldRotation[0], oldRotation[1], oldRotation[2])); } /** * Enable AccuDraw so that it can be used by interactive tools. * This method is public to allow applications to provide a user interface to enable/disable AccuDraw. * @note Should not be called by interactive tools, those should use [[AccuDrawHintBuilder]]. AccuDraw is enabled for applications by default. * @see [[disableForSession]] */ enableForSession() { if (CurrentState.NotEnabled === this.currentState) this.currentState = CurrentState.Inactive; } /** * Disable AccuDraw so that it can not be used by interactive tools. * This method is public to allow applications to provide a user interface to enable/disable AccuDraw. * @note Should not be called by interactive tools, those should use [[AccuDrawHintBuilder]]. AccuDraw is enabled for applications by default. * @see [[enableForSession]] */ disableForSession() { this.currentState = CurrentState.NotEnabled; this.flags.redrawCompass = true; // Make sure decorators are called so we don't draw (i.e. erase AccuDraw compass) } /** @internal */ setLastPoint(pt) { const viewport = this.currentView; if (!viewport) return; const ev = new BeButtonEvent({ point: pt, rawPoint: pt, viewPoint: viewport.worldToView(pt), viewport }); IModelApp.toolAdmin.setAdjustedDataPoint(ev); } /** Emulate a mouse click at the specified location in the supplied view by sending button down/up events. */ async sendDataPoint(pt, viewport) { const ev = new BeButtonEvent({ point: pt, rawPoint: pt, viewPoint: viewport.worldToView(pt), viewport, inputSource: InputSource.Mouse, isDown: true }); // Send both down and up events... await IModelApp.toolAdmin.sendButtonEvent(ev); ev.isDown = false; return IModelApp.toolAdmin.sendButtonEvent(ev); } /** @internal */ clearTentative() { if (!IModelApp.tentativePoint.isActive) return false; const wasSnapped = IModelApp.tentativePoint.isSnapped; IModelApp.tentativePoint.clear(true); return wasSnapped; } /** @internal */ async doAutoPoint(index, mode) { const vp = this.currentView; if (!vp) return; if (CompassMode.Polar === mode) { if (!this.autoPointPlacement) return; if (this._fieldLocked[ItemField.DIST_Item] && (this._fieldLocked[ItemField.ANGLE_Item] || this.indexed & LockedStates.ANGLE_BM) && KeyinStatus.Dynamic === this._keyinStatus[index]) { this.fixPointPolar(vp); return this.sendDataPoint(this.point, vp); } return; } if (this._fieldLocked[ItemField.X_Item] && this._fieldLocked[ItemField.Y_Item]) { if (!this.isActive) { if (!vp.view.is3d() || this._fieldLocked[ItemField.Z_Item]) { const globalOrigin = new Point3d(); if (vp.view.isSpatialView()) globalOrigin.setFrom(vp.view.iModel.globalOrigin); return this.sendDataPoint(globalOrigin.plus(this.delta), vp); } return; } if (!this.autoPointPlacement || KeyinStatus.Dynamic !== this._keyinStatus[index]) return; this.origin.plus3Scaled(this.axes.x, this.delta.x, this.axes.y, this.delta.y, this.axes.z, this.delta.z, this.point); return this.sendDataPoint(this.point, vp); } if (!this.autoPointPlacement || KeyinStatus.Dynamic !== this._keyinStatus[index]) return; if ((ItemField.X_Item === index && this._fieldLocked[ItemField.X_Item] && (this.indexed & LockedStates.Y_BM)) || (ItemField.Y_Item === index && this._fieldLocked[ItemField.Y_Item] && (this.indexed & LockedStates.X_BM))) { this.origin.plus3Scaled(this.axes.x, this.delta.x, this.axes.y, this.delta.y, this.axes.z, this.delta.z, this.point); return this.sendDataPoint(this.point, vp); } } /** Get the current value for the supplied input field */ getValueByIndex(index) { switch (index) { case ItemField.X_Item: return this.delta.x; case ItemField.Y_Item: return this.delta.y; case ItemField.Z_Item: return this.delta.z; case ItemField.DIST_Item: return this._distance; case ItemField.ANGLE_Item: return this._angle; default: return 0.0; } } /** Set the current value for the supplied input field */ setValueByIndex(index, value) { switch (index) { case ItemField.X_Item: this.delta.x = value; break; case ItemField.Y_Item: this.delta.y = value; break; case ItemField.Z_Item: this.delta.z = value; break; case ItemField.DIST_Item: this._distance = value; break; case ItemField.ANGLE_Item: this._angle = value; break; } } updateVector(angle) { this.vector.set(Math.cos(angle), Math.sin(angle), 0.0); const rMatrix = this.getRotation(); rMatrix.multiplyTransposeVectorInPlace(this.vector); } /** Allow the AccuDraw user interface to supply the distance parser */ getLengthParser() { return IModelApp.quantityFormatter.findParserSpecByQuantityType(QuantityType.Length); } /** Allow the AccuDraw user interface to supply the distance parser */ getLengthFormatter() { return IModelApp.quantityFormatter.findFormatterSpecByQuantityType(QuantityType.Length); } stringToDistance(str) { const parserSpec = this.getLengthParser(); if (parserSpec) return parserSpec.parseToQuantityValue(str); return { ok: false, error: ParseError.InvalidParserSpec }; } stringFromDistance(distance) { const formatterSpec = this.getLengthFormatter(); let formattedValue = distance.toString(); if (formatterSpec) formattedValue = IModelApp.quantityFormatter.formatQuantity(distance, formatterSpec); return formattedValue; } /** Allow the AccuDraw user interface to specify bearing */ get isBearingMode() { return this.getAngleParser()?.format.type === FormatType.Bearing; } /** Allow the AccuDraw user interface to specify bearing directions are always in xy plane */ get bearingFixedToPlane2d() { return this.flags.bearingFixToPlane2D; } set bearingFixedToPlane2d(enable) { this.flags.bearingFixToPlane2D = enable; } /** Allow the AccuDraw user interface to supply the angle/direction parser */ getAngleParser() { return IModelApp.quantityFormatter.findParserSpecByQuantityType(QuantityType.Angle); } /** Allow the AccuDraw user interface to supply the angle/direction formatter */ getAngleFormatter() { return IModelApp.quantityFormatter.findFormatterSpecByQuantityType(QuantityType.Angle); } stringToAngle(inString) { const parserSpec = this.getAngleParser(); if (parserSpec) return parserSpec.parseToQuantityValue(inString); return { ok: false, error: ParseError.InvalidParserSpec }; } stringFromAngle(angle) { if (this.isBearingMode && this.flags.bearingFixToPlane2D) { const point = Vector3d.create(this.axes.x.x, this.axes.x.y, 0.0); const matrix = Matrix3d.createRows(this.axes.x, this.axes.y, this.axes.z); if (matrix.determinant() < 0) angle = -angle; // Account for left handed rotations... point.normalizeInPlace(); let adjustment = Math.acos(point.x); if (point.y < 0.0) adjustment = -adjustment; angle += adjustment; // This is the angle measured from design x... angle = (Math.PI / 2) - angle; // Account for bearing direction convention... if (angle < 0) angle = (Math.PI * 2) + angle; // Negative bearings aren't valid? } const formatterSpec = this.getAngleFormatter(); let formattedValue = angle.toString(); if (formatterSpec) formattedValue = IModelApp.quantityFormatter.formatQuantity(angle, formatterSpec); return formattedValue; } /** Can be called by sub-classes to get a formatted value to display for an AccuDraw input field * @see [[onFieldValueChange]] */ getFormattedValueByIndex(index) { const value = IModelApp.accuDraw.getValueByIndex(index); if (ItemField.ANGLE_Item === index) return this.stringFromAngle(value); return this.stringFromDistance(value); } updateFieldValue(index, input) { if (input.length === 0) return BentleyStatus.ERROR; if (input.length === 1) switch (input) { case ":": case "-": case "+": case ".": return BentleyStatus.ERROR; } let parseResult; switch (index) { case ItemField.DIST_Item: parseResult = this.stringToDistance(input); if (Parser.isParsedQuantity(parseResult)) { this._distance = parseResult.value; break; } return BentleyStatus.ERROR; case ItemField.ANGLE_Item: parseResult = this.stringToAngle(input); if (Parser.isParsedQuantity(parseResult)) { if (this.isBearingMode && this.flags.bearingFixToPlane2D) this._angle = (Math.PI / 2) - parseResult.value; else this._angle = parseResult.value; break; } return BentleyStatus.ERROR; case ItemField.X_Item: parseResult = this.stringToDistance(input); if (Parser.isParsedQuantity(parseResult)) { this.delta.x = parseResult.value; this._xIsExplicit = (input[0] === "+" || input[0] === "-"); if (!this._xI