@itwin/core-frontend
Version:
iTwin.js frontend components
1,086 lines • 155 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 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