@itwin/core-frontend
Version:
iTwin.js frontend components
1,006 lines • 55.9 kB
JavaScript
"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module LocatingElements
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.TentativeOrAccuSnap = exports.AccuSnap = exports.TouchCursor = void 0;
const core_bentley_1 = require("@itwin/core-bentley");
const core_geometry_1 = require("@itwin/core-geometry");
const ElementLocateManager_1 = require("./ElementLocateManager");
const HitDetail_1 = require("./HitDetail");
const IModelApp_1 = require("./IModelApp");
const Sprites_1 = require("./Sprites");
const Tool_1 = require("./tools/Tool");
const ToolSettings_1 = require("./tools/ToolSettings");
const Symbols_1 = require("./common/internal/Symbols");
const AccuDraw_1 = require("./AccuDraw");
// cspell:ignore dont primitivetools
/** Virtual cursor for using AccuSnap with touch input.
* @internal
*/
class TouchCursor {
position = new core_geometry_1.Point3d();
viewport;
_offsetPosition = new core_geometry_1.Point3d();
_size;
_yOffset;
_isSelected = false;
_isDragging = false;
_inTouchTap = false;
constructor(vp) {
this._size = vp.pixelsFromInches(0.3);
this._yOffset = this._size * 1.75;
this.viewport = vp;
}
setPosition(vp, worldLocation) {
const pointNpc = vp.worldToNpc(worldLocation);
if (pointNpc.z < 0.0 || pointNpc.z > 1.0)
pointNpc.z = 0.5; // move inside frustum.
const viewLocation = vp.npcToView(pointNpc);
if (!vp.viewRect.containsPoint(viewLocation))
return false; // outside this viewport rect
viewLocation.x = Math.floor(viewLocation.x) + 0.5;
viewLocation.y = Math.floor(viewLocation.y) + 0.5;
viewLocation.z = 0.0;
const offsetLocation = new core_geometry_1.Point3d(viewLocation.x, viewLocation.y - this._yOffset, viewLocation.z);
if (!vp.viewRect.containsPoint(offsetLocation))
return false; // outside this viewport rect
this.position.setFrom(viewLocation);
this._offsetPosition.setFrom(offsetLocation);
if (vp !== this.viewport) {
this.viewport.invalidateDecorations();
this.viewport = vp;
}
vp.invalidateDecorations();
return true;
}
drawHandle(ctx, filled) {
ctx.beginPath();
ctx.moveTo(-this._size, 0);
ctx.bezierCurveTo(-this._size, -this._size * 0.85, -this._size * 0.6, -this._yOffset * 0.6, 0, -this._yOffset * 0.8);
ctx.bezierCurveTo(this._size * 0.6, -this._yOffset * 0.6, this._size, -this._size * 0.85, this._size, 0);
ctx.arc(0, 0, this._size, 0, Math.PI);
if (filled)
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.arc(0, 0, this._size * 0.75, 0, 2 * Math.PI);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(-this._size * 0.4, 0);
ctx.lineTo(this._size * 0.4, 0);
ctx.moveTo(-this._size * 0.4, this._size * 0.25);
ctx.lineTo(this._size * 0.4, this._size * 0.25);
ctx.stroke();
}
drawDecoration(ctx) {
ctx.lineWidth = 1;
ctx.strokeStyle = "rgba(0,0,0,.75)";
ctx.fillStyle = "white";
ctx.strokeRect(-2, -(this._yOffset + 2), 5, 5);
ctx.fillRect(-1, -(this._yOffset + 1), 3, 3);
ctx.lineWidth = 3.5;
ctx.lineCap = "round";
ctx.fillStyle = this._isSelected ? "rgba(35,187,252,.25)" : "rgba(255,215,0,.25)";
ctx.shadowColor = "black";
ctx.shadowBlur = 10;
this.drawHandle(ctx, true);
ctx.lineWidth = 1.5;
ctx.strokeStyle = this._isSelected ? "rgba(35,187,252,.75)" : "rgba(255,215,0,.75)";
ctx.shadowBlur = 0;
this.drawHandle(ctx, false);
}
isSelected(pt) { return this.position.distance(core_geometry_1.Point3d.create(pt.x, pt.y)) < this._size; }
isButtonHandled(ev) { return (Tool_1.BeButton.Data === ev.button && Tool_1.InputSource.Touch === ev.inputSource && !this._inTouchTap); }
doTouchMove(ev) {
if (undefined === ev.viewport || !ev.isSingleTouch)
return false;
if (!this._isDragging || !this.setPosition(ev.viewport, ev.point))
return false;
ev.viewPoint = this._offsetPosition;
IModelApp_1.IModelApp.toolAdmin.convertTouchMoveToMotion(ev); // eslint-disable-line @typescript-eslint/no-floating-promises
return true;
}
doTouchMoveStart(ev, startEv) {
if (undefined === ev.viewport || !ev.isSingleTouch)
return false;
return (this._isDragging = this.isSelected(startEv.viewPoint));
}
doTouchStart(ev) {
this._isSelected = ev.isSingleTouch && this.isSelected(ev.viewPoint);
if (undefined !== ev.viewport)
ev.viewport.invalidateDecorations();
}
doTouchEnd(ev) {
if (this._isDragging && undefined !== ev.viewport)
IModelApp_1.IModelApp.toolAdmin.currentInputState.fromPoint(ev.viewport, this._offsetPosition, Tool_1.InputSource.Touch); // Current location should reflect virtual cursor offset...
this._isSelected = this._isDragging = false;
if (undefined !== ev.viewport)
ev.viewport.invalidateDecorations();
}
async doTouchTap(ev) {
if (undefined === ev.viewport || !ev.isSingleTouch || 1 !== ev.tapCount)
return false;
if (!this.isSelected(ev.viewPoint)) {
if (!this.setPosition(ev.viewport, ev.point))
return false;
ev.viewPoint = this._offsetPosition;
IModelApp_1.IModelApp.toolAdmin.convertTouchMoveToMotion(ev); // eslint-disable-line @typescript-eslint/no-floating-promises
return false;
}
ev.viewPoint = this._offsetPosition;
this._inTouchTap = true;
await IModelApp_1.IModelApp.toolAdmin.convertTouchTapToButtonDownAndUp(ev);
this._inTouchTap = false;
return true;
}
static createFromTouchTap(ev) {
if (undefined === ev.viewport || !ev.isSingleTouch || 1 !== ev.tapCount)
return undefined;
const touchCursor = new TouchCursor(ev.viewport);
if (!touchCursor.setPosition(ev.viewport, ev.point) && !touchCursor.setPosition(ev.viewport, ev.viewport.view.getCenter()))
return undefined;
ev.viewPoint = touchCursor._offsetPosition;
IModelApp_1.IModelApp.toolAdmin.convertTouchMoveToMotion(ev); // eslint-disable-line @typescript-eslint/no-floating-promises
return touchCursor;
}
}
exports.TouchCursor = TouchCursor;
/** AccuSnap is an aide for snapping to interesting points on elements or decorations as the cursor moves over them.
* @see [Using AccuSnap]($docs/learning/frontend/primitivetools.md#AccuSnap)
* @public
* @extensions
*/
class AccuSnap {
/** Currently active hit */
currHit;
/** Current list of hits. */
aSnapHits;
/** Views that need to be flashed */
needFlash = new Set();
/** Views that are already flashed */
areFlashed = new Set();
/** The "+" that indicates where the snap point is */
cross = new Sprites_1.SpriteLocation();
/** The icon that indicates what type of snap is active */
icon = new Sprites_1.SpriteLocation();
/** The icon that indicates an error */
errorIcon = new Sprites_1.SpriteLocation();
/** Reason key for last error */
errorKey;
/** localized message explaining why last error was generated. */
explanation;
/** Number of times "suppress" has been called -- unlike suspend this is not automatically cleared by tools */
_suppressed = 0;
/** Location of cursor when we last checked for motion */
_lastCursorPos = new core_geometry_1.Point2d();
/** @internal */
toolState = new AccuSnap.ToolState();
/** @internal */
_settings = new AccuSnap.Settings();
/** @internal */
touchCursor;
/** Current request for tooltip message. */
_toolTipPromise;
/** @internal */
onInitialized() { }
get _searchDistance() { return this.isLocateEnabled ? 1.0 : this._settings.searchDistance; }
get _hotDistanceInches() { return IModelApp_1.IModelApp.locateManager.apertureInches * this._settings.hotDistanceFactor; }
/** Whether locate of elements under the cursor is enabled by the current InteractiveTool. */
get isLocateEnabled() { return this.toolState.locate; }
/** Whether snapping to elements under the cursor is enabled by the current InteractiveTool. */
get isSnapEnabled() { return this.toolState.enabled; }
/** Whether the user setting for snapping is enabled. Snapping is done only when both the user and current InteractiveTool have enabled it. */
get isSnapEnabledByUser() { return this._settings.enableFlag; }
/** AccuSnap user settings */
get userSettings() { return this._settings; }
isFlashed(view) { return (this.areFlashed.has(view)); }
needsFlash(view) { return (this.needFlash.has(view)); }
setNeedsFlash(view) {
this.needFlash.add(view);
this.clearIsFlashed(view);
view.invalidateDecorations();
}
setIsFlashed(view) { this.areFlashed.add(view); }
clearIsFlashed(view) { this.areFlashed.delete(view); }
static toSnapDetail(hit) { return (hit && hit instanceof HitDetail_1.SnapDetail) ? hit : undefined; }
/** Currently active snap */
getCurrSnapDetail() { return AccuSnap.toSnapDetail(this.currHit); }
/** Determine whether there is a current hit that is *hot*. */
get isHot() {
const currSnap = this.getCurrSnapDetail();
return !currSnap ? false : currSnap.isHot;
}
/** Optional ids to never flash. Useful for tools like "trim curve" that won't want a flashed segment to obscure a result preview.
* @note Cleared when a primitive or view tool is started and saved/restored when a primitive tool is suspended/unsuspended.
* @public
*/
neverFlash(ids) {
if (undefined === ids) {
this.toolState.neverFlash = undefined;
return;
}
const newIds = new Set();
for (const id of core_bentley_1.Id64.iterable(ids))
newIds.add(id);
this.toolState.neverFlash = (0 !== newIds.size ? newIds : undefined);
}
/** @internal */
destroy() {
this.currHit = undefined;
this.aSnapHits = undefined;
}
get _doSnapping() { return this.isSnapEnabled && this.isSnapEnabledByUser && !this._isSnapSuspended; }
get _isSnapSuspended() { return (0 !== this._suppressed || 0 !== this.toolState.suspended); }
/** Get the current snap divisor to use to use for SnapMode.NearestKeypoint.
* @public
*/
get keypointDivisor() { return this._settings.keypointDivisor; }
/** Get the current active SnapModes. SnapMode position determines priority, with the first entry being the highest. The SnapDetail will be returned for the first SnapMode that produces a hot snap.
* @public
*/
getActiveSnapModes() {
const snaps = [];
snaps.push(HitDetail_1.SnapMode.NearestKeypoint);
return snaps;
}
/** Can be implemented by a subclass of AccuSnap to implement a SnapMode override that applies only to the next point.
* This method will be called whenever a new tool is installed and on button events.
* @internal
*/
synchSnapMode() { }
/** Check whether current tentative snap has valid curve geometry for finding extended intersections. */
get _searchForExtendedIntersections() {
const snap = IModelApp_1.IModelApp.tentativePoint.getCurrSnap();
return (undefined !== snap && undefined !== snap.primitive);
}
/**
* Check to see whether its appropriate to generate an AccuSnap point, given the current user
* and command settings, and whether a tentative point is currently active.
*/
get isActive() {
// Unless we're snapping in intersect mode (to find extended intersections), skip if tentative point active...
if (IModelApp_1.IModelApp.tentativePoint.isActive) {
if (!this._doSnapping || !this._searchForExtendedIntersections)
return false;
const snaps = this.getActiveSnapModes();
for (const snap of snaps) {
if (snap === HitDetail_1.SnapMode.Intersection)
return true;
}
return false;
}
return this._doSnapping || this.isLocateEnabled;
}
initializeForCheckMotion() {
this._lastCursorPos.setFrom(IModelApp_1.IModelApp.toolAdmin.currentInputState.lastMotion);
}
/** Clear the current AccuSnap info. */
clear() { this.setCurrHit(undefined); }
/** @internal */
setCurrHit(newHit) {
const newSnap = AccuSnap.toSnapDetail(newHit);
const currSnap = this.getCurrSnapDetail();
const sameElem = (undefined !== newHit && newHit.isSameHit(this.currHit));
const sameHit = (sameElem && !newSnap);
const sameSnap = (sameElem && undefined !== newSnap && undefined !== currSnap);
const samePt = (sameHit || (sameSnap && newSnap.snapPoint.isAlmostEqual(currSnap.snapPoint)));
const sameHot = (sameHit || (sameSnap && (this.isHot === newSnap.isHot)));
const sameBaseSnapMode = (!newSnap || !currSnap || newSnap.snapMode === currSnap.snapMode);
const sameType = (sameHot && (!currSnap || (currSnap.getHitType() === newHit.getHitType())));
// see if it is the same point on the same element, the hot flags are the same multiple snaps, and the snap modes are the same
if (samePt && sameType && sameBaseSnapMode) {
// we know that nothing about the screen could change, just save the new hit and return to avoid screen flash
this.currHit = newHit;
return;
}
this.erase();
// if we hit the same element with the same "hotness" as last time, we don't need to erase it
// multiple snaps: but only if the old and new snap modes are the same
if (!sameHot || !sameBaseSnapMode) {
this.unFlashViews();
this.setFlashHit(newHit);
}
// if we didn't get a new hit, we're done
if (undefined === (this.currHit = newHit))
return;
// draw sprites for this hit
this.showSnapSprite();
}
/** flash a hit in a single view. */
flashHitInView(hit, context) {
const viewport = context.viewport;
if (!viewport || !this.hitShouldBeHilited(hit) || !this.needsFlash(viewport))
return;
hit.draw(context);
this.setIsFlashed(viewport);
}
setNeedsFlashView(view) {
if (!this.isFlashed(view) && !this.needsFlash(view))
this.setNeedsFlash(view);
}
/** flash a hit in its view. */
setFlashHit(hit) {
if (hit !== undefined && this.hitShouldBeHilited(hit))
this.setNeedsFlashView(hit.viewport);
}
/** @internal */
erase() {
this.clearToolTip(undefined); // make sure there's no tooltip up.
this.clearSprites(); // remove all sprites from the screen
}
showElemInfo(viewPt, vp, hit, delay) {
if (!IModelApp_1.IModelApp.viewManager.doesHostHaveFocus || undefined !== this._toolTipPromise)
return;
if (!IModelApp_1.IModelApp.toolAdmin.wantToolTip(hit))
return;
const promise = this._toolTipPromise = delay.executeAfter(async () => {
if (promise !== this._toolTipPromise)
return; // we abandoned this request during delay
try {
const msg = await IModelApp_1.IModelApp.toolAdmin.getToolTip(hit);
if (this._toolTipPromise === promise) // have we abandoned this request while awaiting getToolTip?
this.showLocateMessage(viewPt, vp, msg);
}
catch { } // happens if getToolTip was canceled
});
}
showLocateMessage(viewPt, vp, msg) {
if (IModelApp_1.IModelApp.viewManager.doesHostHaveFocus)
vp.openToolTip(msg, viewPt);
}
/** @internal */
displayToolTip(viewPt, vp, uorPt) {
// if the tooltip is already displayed, or if user doesn't want it, quit.
if (!this._settings.toolTip || !IModelApp_1.IModelApp.notifications.isToolTipSupported || IModelApp_1.IModelApp.notifications.isToolTipOpen)
return;
const accuSnapHit = this.currHit;
const tpHit = IModelApp_1.IModelApp.tentativePoint.getCurrSnap();
// if we don't have either an AccuSnap or a tentative point hit, quit.
if (!accuSnapHit && !tpHit && !this.errorIcon.isActive)
return;
let theHit;
// determine which type of hit
if (tpHit) {
if (uorPt) {
// see if he came back somewhere near the currently snapped element
const aperture = (this._settings.stickyFactor * vp.pixelsFromInches(IModelApp_1.IModelApp.locateManager.apertureInches) / 2.0) + 1.5;
if (!IModelApp_1.IModelApp.locateManager.picker.testHit(tpHit, vp, uorPt, aperture, IModelApp_1.IModelApp.locateManager.options))
return;
}
theHit = tpHit;
}
else {
theHit = accuSnapHit;
}
// if we're currently showing an error, get the error message...otherwise display hit info...
if (!this.errorIcon.isActive && theHit) {
this.showElemInfo(viewPt, vp, theHit, this._settings.toolTipDelay);
return;
}
// If we have an error explanation...use it as is!
if (this.explanation) {
this.showLocateMessage(viewPt, vp, this.explanation);
return;
}
// if we don't have an explanation yet, translate the error code.
if (!this.errorKey)
return;
this.explanation = IModelApp_1.IModelApp.localization.getLocalizedString(`iModelJs:${this.errorKey}`);
if (!this.explanation)
return;
this.showLocateMessage(viewPt, vp, this.explanation);
}
/** @internal */
clearToolTip(ev) {
// Throw away any stale request for a tooltip message
this._toolTipPromise = undefined;
if (!IModelApp_1.IModelApp.notifications.isToolTipOpen)
return;
if (ev && (5 > ev.viewPoint.distanceXY(IModelApp_1.IModelApp.notifications.toolTipLocation)))
return;
IModelApp_1.IModelApp.notifications.clearToolTip();
}
/** Display the sprites for the current snap to indicate its position on the screen and what snap mode it represents. */
showSnapSprite() {
const snap = this.getCurrSnapDetail();
if (!snap)
return;
const crossPt = snap.snapPoint;
const viewport = snap.viewport;
const crossSprite = Sprites_1.IconSprites.getSpriteFromUrl(`${IModelApp_1.IModelApp.publicPath}${snap.isHot ? "sprites/SnapCross.png" : "sprites/SnapUnfocused.png"}`);
this.cross.activate(crossSprite, viewport, crossPt);
const snapSprite = snap.sprite;
if (snapSprite)
this.icon.activate(snapSprite, viewport, AccuSnap.adjustIconLocation(viewport, crossPt, snapSprite.size));
}
static adjustIconLocation(vp, input, iconSize) {
const out = vp.worldToView(input);
out.x += (iconSize.x / 3.0);
out.y -= (iconSize.y * 1.3);
return vp.viewToWorld(out, out);
}
/** try to indicate what's wrong with the current point (why we're not snapping). */
showSnapError(out, ev) {
this.explanation = out.explanation;
this.errorKey = out.reason;
this.errorIcon.deactivate();
const vp = ev.viewport;
if (undefined === vp)
return;
let errorSprite;
switch (out.snapStatus) {
case ElementLocateManager_1.SnapStatus.FilteredByApp:
errorSprite = Sprites_1.IconSprites.getSpriteFromUrl(`${IModelApp_1.IModelApp.publicPath}sprites/SnapAppFiltered.png`);
break;
case ElementLocateManager_1.SnapStatus.FilteredByAppQuietly:
this.errorKey = undefined;
break;
case ElementLocateManager_1.SnapStatus.NotSnappable:
errorSprite = Sprites_1.IconSprites.getSpriteFromUrl(`${IModelApp_1.IModelApp.publicPath}sprites/SnapNotSnappable.png`);
this.errorKey = ElementLocateManager_1.ElementLocateManager.getFailureMessageKey("NotSnappable");
break;
}
if (!errorSprite)
return;
const spriteSize = errorSprite.size;
const pt = AccuSnap.adjustIconLocation(vp, ev.rawPoint, spriteSize);
this.errorIcon.activate(errorSprite, vp, pt);
}
clearSprites() {
this.errorIcon.deactivate();
this.cross.deactivate();
this.icon.deactivate();
}
/** determine whether a hit should be hilited or not. */
hitShouldBeHilited(hit) {
if (!hit)
return false;
if (hit.isModelHit || hit.isMapHit)
return false; // Avoid annoying flashing of reality models.
if (this.toolState.neverFlash && this.toolState.neverFlash.has(hit.sourceId))
return false;
const snap = AccuSnap.toSnapDetail(hit);
return !snap || snap.isHot || this._settings.hiliteColdHits;
}
unFlashViews() {
this.needFlash.clear();
for (const vp of this.areFlashed)
vp.flashedId = undefined;
this.areFlashed.clear();
}
/** @internal */
adjustPointIfHot(pt, view) {
const currSnap = this.getCurrSnapDetail();
if (!currSnap || !currSnap.isHot || view !== currSnap.viewport)
return;
pt.setFrom(currSnap.adjustedPoint);
}
/** Implemented by sub-classes to update ui to show current enabled state.
* @internal
*/
onEnabledStateChange(_isEnabled, _wasEnabled) { }
/** @internal */
getHitAndList(holder) {
const hit = this.currHit;
if (hit) {
holder.setHitList(this.aSnapHits);
this.aSnapHits = undefined;
}
return hit;
}
initCmdState() {
this.toolState.suspended = 0;
this.toolState.neverFlash = undefined;
}
/** @internal */
suspend(doSuspend) {
const previousDoSnapping = this._doSnapping;
if (doSuspend)
this.toolState.suspended++;
else if (this.toolState.suspended > 0)
this.toolState.suspended--;
this.onEnabledStateChange(this._doSnapping, previousDoSnapping);
}
/** @internal */
suppress(doSuppress) {
const previousDoSnapping = this._doSnapping;
if (doSuppress)
this._suppressed++;
else if (this._suppressed > 0)
this._suppressed--;
this.onEnabledStateChange(this._doSnapping, previousDoSnapping);
return this._suppressed;
}
/** Turn AccuSnap on or off */
enableSnap(yesNo) {
const previousDoSnapping = this._doSnapping;
this.toolState.enabled = yesNo;
if (!yesNo) {
this.clear();
if (undefined !== this.touchCursor && !this.wantVirtualCursor) {
this.touchCursor = undefined;
IModelApp_1.IModelApp.viewManager.invalidateDecorationsAllViews();
}
}
this.onEnabledStateChange(this._doSnapping, previousDoSnapping);
}
/** @internal */
intersectXY(tpSnap, second) {
// Get single segment curve from each snap to intersect...
const tpSegment = tpSnap.getCurvePrimitive();
if (undefined === tpSegment)
return undefined;
const segment = second.getCurvePrimitive();
if (undefined === segment)
return undefined;
const worldToView = second.viewport.worldToViewMap.transform0;
const detail = core_geometry_1.CurveCurve.intersectionProjectedXYPairs(worldToView, tpSegment, true, segment, true);
if (0 === detail.length)
return undefined;
let closeIndex = 0;
if (detail.length > 1) {
const snapPt = worldToView.multiplyPoint3d(HitDetail_1.HitGeomType.Point === tpSnap.geomType && HitDetail_1.HitGeomType.Point !== second.geomType ? second.getPoint() : tpSnap.getPoint(), 1); // Don't check distance from arc centers...
let lastDist;
for (let i = 0; i < detail.length; i++) {
const testPt = worldToView.multiplyPoint3d(detail[i].detailA.point, 1);
const testDist = snapPt.realDistanceXY(testPt);
if (undefined !== testDist && (undefined === lastDist || testDist < lastDist)) {
lastDist = testDist;
closeIndex = i;
}
}
}
const intersect = new HitDetail_1.IntersectDetail(tpSnap, HitDetail_1.SnapHeat.InRange, detail[closeIndex].detailA.point, segment, second.sourceId); // Should be ok to share hit detail with tentative...
intersect.primitive = tpSegment; // Just save single segment that was intersected for line strings/shapes...
return intersect;
}
static doPostProcessSnapMode(snap, snapMode) {
const accuDraw = IModelApp_1.IModelApp.accuDraw;
if (!accuDraw.isEnabled || accuDraw.isDeactivated)
return ElementLocateManager_1.SnapStatus.Disabled; // AccuDraw is require for this snap mode...
if (HitDetail_1.HitGeomType.Surface === snap.geomType)
return ElementLocateManager_1.SnapStatus.NoSnapPossible; // Only valid for edge and curve hits...
const curve = snap.getCurvePrimitive();
if (undefined === curve)
return ElementLocateManager_1.SnapStatus.NoSnapPossible;
const rMatrix = AccuDraw_1.AccuDraw.getSnapRotation(snap, snap.viewport);
if (undefined === rMatrix)
return ElementLocateManager_1.SnapStatus.NoSnapPossible;
// Compute snap from AccuDraw origin when active or set AccuDraw rotation if accepted...
if (!accuDraw.isActive) {
accuDraw.setContext(AccuDraw_1.AccuDrawFlags.SmartRotation); // Automatically orient compass to snap location if accepted...
snap.setSnapMode(snapMode);
return ElementLocateManager_1.SnapStatus.Success;
}
const zVec = rMatrix.rowZ(); // This is a row matrix...
const spacePoint = AccuDraw_1.AccuDrawHintBuilder.projectPointToPlaneInView(accuDraw.origin, snap.getPoint(), zVec, snap.viewport, true);
if (undefined === spacePoint)
return ElementLocateManager_1.SnapStatus.NoSnapPossible;
let detail;
if (HitDetail_1.SnapMode.PerpendicularPoint === snapMode)
detail = curve.closestPoint(spacePoint, true);
else
detail = curve.closestTangent(spacePoint, { hintPoint: snap.getPoint(), vectorToEye: zVec, extend: true });
if (undefined === detail?.curve)
return ElementLocateManager_1.SnapStatus.NoSnapPossible;
// Close point may not be perpendicular when curve can't be extended...
if (HitDetail_1.SnapMode.PerpendicularPoint === snapMode && !curve.isExtensibleFractionSpace) {
const curvePlanePoint = AccuDraw_1.AccuDrawHintBuilder.projectPointToPlaneInView(accuDraw.origin, detail.point, zVec, snap.viewport, true);
if (undefined === curvePlanePoint)
return ElementLocateManager_1.SnapStatus.NoSnapPossible;
const curveNormal = detail.point.vectorTo(curvePlanePoint);
const curveTangent = curve.fractionToPointAndUnitTangent(detail.fraction);
if (!curveTangent.getDirectionRef().isPerpendicularTo(curveNormal)) {
const curveExtensionPoint = AccuDraw_1.AccuDrawHintBuilder.projectPointToLineInView(accuDraw.origin, curveTangent.getOriginRef(), curveTangent.getDirectionRef(), snap.viewport, true);
if (undefined === curveExtensionPoint)
return ElementLocateManager_1.SnapStatus.NoSnapPossible;
detail.point.setFrom(curveExtensionPoint);
}
}
const point = AccuDraw_1.AccuDrawHintBuilder.projectPointToPlaneInView(detail.point, accuDraw.origin, zVec, snap.viewport, true);
if (undefined === point)
return ElementLocateManager_1.SnapStatus.NoSnapPossible;
snap.setSnapPoint(point, HitDetail_1.SnapHeat.InRange); // Force hot snap...
snap.setSnapMode(snapMode);
return ElementLocateManager_1.SnapStatus.Success;
}
/** @internal */
static async requestSnap(thisHit, snapModes, hotDistanceInches, keypointDivisor, hitList, out) {
if (thisHit.isModelHit || thisHit.isMapHit || thisHit.isClassifier) {
if (snapModes.includes(HitDetail_1.SnapMode.Nearest)) {
if (out)
out.snapStatus = ElementLocateManager_1.SnapStatus.Success;
return new HitDetail_1.SnapDetail(thisHit, HitDetail_1.SnapMode.Nearest, HitDetail_1.SnapHeat.InRange);
}
else if (1 === snapModes.length && snapModes.includes(HitDetail_1.SnapMode.Intersection)) {
if (out)
out.snapStatus = ElementLocateManager_1.SnapStatus.NoSnapPossible;
return undefined;
}
else {
if (out)
out.snapStatus = ElementLocateManager_1.SnapStatus.Success;
const realitySnap = new HitDetail_1.SnapDetail(thisHit, HitDetail_1.SnapMode.Nearest, HitDetail_1.SnapHeat.None);
realitySnap.sprite = undefined; // Don't show a snap mode that isn't applicable, but still accept hit point...
return realitySnap;
}
}
let hitVp;
if (thisHit.path) {
hitVp = thisHit.path.sectionDrawingAttachment?.viewport ?? thisHit.path.viewAttachment?.viewport;
}
hitVp = hitVp ?? thisHit.viewport;
if (undefined !== thisHit.subCategoryId && !thisHit.isExternalIModelHit) {
const appearance = hitVp.getSubCategoryAppearance(thisHit.subCategoryId);
if (appearance.dontSnap) {
if (out) {
out.snapStatus = ElementLocateManager_1.SnapStatus.NotSnappable;
out.explanation = IModelApp_1.IModelApp.localization.getLocalizedString(`iModelJs:${ElementLocateManager_1.ElementLocateManager.getFailureMessageKey("NotSnappableSubCategory")}`);
}
return undefined;
}
}
const haveTangentPoint = snapModes.includes(HitDetail_1.SnapMode.TangentPoint);
const havePerpendicularPoint = snapModes.includes(HitDetail_1.SnapMode.PerpendicularPoint);
const postProcessSnapMode = (havePerpendicularPoint ? HitDetail_1.SnapMode.PerpendicularPoint : (haveTangentPoint ? HitDetail_1.SnapMode.TangentPoint : undefined));
if (undefined !== postProcessSnapMode) {
// NOTE: These are not valid backend snap modes. Instead make the snap request using nearest
// snap in order to get the candidate curve to use to compute the desired snap point...
snapModes = snapModes.filter(snapMode => (snapMode !== HitDetail_1.SnapMode.PerpendicularPoint && snapMode !== HitDetail_1.SnapMode.TangentPoint));
if (!snapModes.includes(HitDetail_1.SnapMode.Nearest))
snapModes.push(HitDetail_1.SnapMode.Nearest);
}
const requestProps = {
id: thisHit.sourceId,
testPoint: thisHit.testPoint,
closePoint: thisHit.hitPoint,
worldToView: hitVp.worldToViewMap.transform0.toJSON(),
viewFlags: hitVp.viewFlags,
snapModes,
snapAperture: hitVp.pixelsFromInches(hotDistanceInches),
snapDivisor: keypointDivisor,
subCategoryId: thisHit.subCategoryId,
geometryClass: thisHit.geometryClass,
modelToWorld: thisHit.transformFromSourceIModel?.toJSON(),
};
const thisGeom = (thisHit.isElementHit ? IModelApp_1.IModelApp.viewManager.overrideElementGeometry(thisHit) : IModelApp_1.IModelApp.viewManager.getDecorationGeometry(thisHit));
if (undefined !== thisGeom) {
requestProps.decorationGeometry = [{ id: thisHit.sourceId, geometryStream: thisGeom }];
}
else if (!thisHit.isElementHit) {
if (out)
out.snapStatus = ElementLocateManager_1.SnapStatus.NoSnapPossible;
return undefined;
}
if (snapModes.includes(HitDetail_1.SnapMode.Intersection)) {
if (undefined !== hitList) {
for (const hit of hitList.hits) {
if (thisHit.sourceId === hit.sourceId || thisHit.iModel !== hit.iModel)
continue;
const geom = (hit.isElementHit ? IModelApp_1.IModelApp.viewManager.overrideElementGeometry(hit) : IModelApp_1.IModelApp.viewManager.getDecorationGeometry(hit));
if (undefined !== geom) {
if (undefined === requestProps.decorationGeometry)
requestProps.decorationGeometry = [{ id: thisHit.sourceId, geometryStream: geom }];
else
requestProps.decorationGeometry.push({ id: thisHit.sourceId, geometryStream: geom });
}
else if (!hit.isElementHit) {
continue;
}
if (undefined === requestProps.intersectCandidates)
requestProps.intersectCandidates = [hit.sourceId];
else
requestProps.intersectCandidates.push(hit.sourceId);
if (5 === requestProps.intersectCandidates.length)
break; // Search for intersection with a few of the next best hits...
}
}
if (1 === snapModes.length && undefined === requestProps.intersectCandidates) {
if (out)
out.snapStatus = ElementLocateManager_1.SnapStatus.NoSnapPossible;
return undefined; // Don't make back end request when only doing intersection snap when we don't have another hit to intersect with...
}
}
try {
const result = await thisHit.iModel[Symbols_1._requestSnap](requestProps);
if (out)
out.snapStatus = result.status;
if (result.status !== ElementLocateManager_1.SnapStatus.Success)
return undefined;
const parseCurve = (json) => {
const parsed = undefined !== json ? core_geometry_1.IModelJson.Reader.parse(json) : undefined;
return parsed instanceof core_geometry_1.GeometryQuery && "curvePrimitive" === parsed.geometryCategory ? parsed : undefined;
};
let displayTransform;
if (undefined !== thisHit.modelId) {
displayTransform = thisHit.viewport.view.computeDisplayTransform({
modelId: thisHit.modelId,
elementId: thisHit.sourceId,
viewAttachmentId: thisHit.path?.viewAttachment?.id,
inSectionDrawingAttachment: undefined !== thisHit.path?.sectionDrawingAttachment,
});
}
const snapPoint = core_geometry_1.Point3d.fromJSON(result.snapPoint);
displayTransform?.multiplyPoint3d(snapPoint, snapPoint);
const snap = new HitDetail_1.SnapDetail(thisHit, result.snapMode, result.heat, snapPoint);
snap.setCurvePrimitive(parseCurve(result.curve), displayTransform, result.geomType);
if (undefined !== result.parentGeomType)
snap.parentGeomType = result.parentGeomType;
// Update hitPoint from readPixels with exact point location corrected to surface/edge geometry.
if (undefined !== result.hitPoint) {
snap.hitPoint.setFromJSON(result.hitPoint);
displayTransform?.multiplyPoint3d(snap.hitPoint, snap.hitPoint);
}
// Apply display transform to normal.
if (undefined !== result.normal) {
snap.normal = core_geometry_1.Vector3d.fromJSON(result.normal);
displayTransform?.matrix.multiplyVector(snap.normal, snap.normal);
snap.normal.normalizeInPlace();
}
if (undefined !== postProcessSnapMode && HitDetail_1.SnapMode.Nearest === result.snapMode) {
if (ElementLocateManager_1.SnapStatus.Success !== this.doPostProcessSnapMode(snap, postProcessSnapMode))
return undefined;
return snap;
}
if (HitDetail_1.SnapMode.Intersection !== snap.snapMode)
return snap;
if (undefined === result.intersectId)
return undefined;
const otherPrimitive = parseCurve(result.intersectCurve);
if (undefined === otherPrimitive)
return undefined;
const intersect = new HitDetail_1.IntersectDetail(snap, snap.heat, snap.snapPoint, otherPrimitive, result.intersectId);
return intersect;
}
catch {
if (out)
out.snapStatus = ElementLocateManager_1.SnapStatus.Aborted;
return undefined;
}
}
async getAccuSnapDetail(hitList, out) {
const thisHit = hitList.getNextHit();
if (undefined === thisHit)
return undefined;
const filterStatus = (this.isLocateEnabled ? await IModelApp_1.IModelApp.locateManager.filterHit(thisHit, ElementLocateManager_1.LocateAction.AutoLocate, out) : ElementLocateManager_1.LocateFilterStatus.Accept);
if (ElementLocateManager_1.LocateFilterStatus.Accept !== filterStatus) {
out.snapStatus = IModelApp_1.IModelApp.toolAdmin.wantToolTip(thisHit) ? ElementLocateManager_1.SnapStatus.FilteredByApp : ElementLocateManager_1.SnapStatus.FilteredByAppQuietly;
return undefined;
}
let snapModes;
if (IModelApp_1.IModelApp.tentativePoint.isActive) {
snapModes = [];
snapModes.push(HitDetail_1.SnapMode.Nearest); // Special case: isActive only allows snapping with tentative to find extended intersections...
}
else {
snapModes = this.getActiveSnapModes(); // Get the list of point snap modes to consider
}
const thisSnap = await AccuSnap.requestSnap(thisHit, snapModes, this._hotDistanceInches, this.keypointDivisor, hitList, out);
if (undefined === thisSnap)
return undefined;
if (IModelApp_1.IModelApp.tentativePoint.isActive) {
const tpSnap = IModelApp_1.IModelApp.tentativePoint.getCurrSnap();
if (undefined === tpSnap)
return undefined;
const intersectSnap = this.intersectXY(tpSnap, thisSnap);
if (undefined === intersectSnap)
return undefined;
hitList.setCurrentHit(thisHit);
return intersectSnap;
}
IModelApp_1.IModelApp.accuDraw.onSnap(thisSnap); // AccuDraw can adjust nearest snap to intersection of circle (polar distance lock) or line (axis lock) with snapped to curve...
hitList.setCurrentHit(thisHit);
return thisSnap;
}
/** Request a snap from the backend for the supplied HitDetail.
* @param hit The HitDetail to snap to.
* @param snapMode Optional SnapMode, uses active snap modes if not specified.
* @return A Promise for the SnapDetail or undefined if no snap could be created.
*/
async doSnapRequest(hit, snapMode) {
let snapModes;
if (undefined === snapMode)
snapModes = this.getActiveSnapModes();
else
snapModes = [snapMode];
return AccuSnap.requestSnap(hit, snapModes, this._hotDistanceInches, this.keypointDivisor);
}
findHits(ev, force = false) {
// When using AccuSnap to locate elements, we have to start with the datapoint adjusted
// for locks and not the raw point. Otherwise, when grid/unit lock are on, we locate elements by
// points not on the grid. This causes them to be "pulled" off the grid when they are accepted. On
// the other hand, when NOT locating, we need to use the raw point so we can snap to elements
// away from the grid.
const vp = ev.viewport;
if (undefined === vp)
return ElementLocateManager_1.SnapStatus.NoElements;
const testPoint = this.isLocateEnabled ? ev.point : ev.rawPoint;
const picker = IModelApp_1.IModelApp.locateManager.picker;
const options = IModelApp_1.IModelApp.locateManager.options.clone(); // Copy to avoid changing out from under active Tool...
// NOTE: Since TestHit will use the same HitSource as the input hit we only need to sets this for DoPick...
options.hitSource = this.isSnapEnabled ? HitDetail_1.HitSource.AccuSnap : HitDetail_1.HitSource.MotionLocate;
let aperture = (vp.pixelsFromInches(IModelApp_1.IModelApp.locateManager.apertureInches) / 2.0) + 1.5;
this.initializeForCheckMotion();
aperture *= this._searchDistance;
if (0 === picker.doPick(vp, testPoint, aperture, options)) {
this.aSnapHits = undefined; // Clear any previous hit list so reset won't cycle through hits cursor is no longer over, etc.
return ElementLocateManager_1.SnapStatus.NoElements;
}
this.aSnapHits = picker.getHitList(true); // take ownership of the pickElem hit list.
// see if we should keep the current hit
const canBeSticky = !force && this.aSnapHits.length > 1 && this.currHit && (HitDetail_1.HitDetailType.Intersection !== this.currHit.getHitType() && this.currHit.priority < HitDetail_1.HitPriority.PlanarSurface);
if (canBeSticky) {
for (let iHit = 1; iHit < this.aSnapHits.length; ++iHit) {
const thisHit = this.aSnapHits.hits[iHit];
if (!thisHit.isSameHit(this.currHit))
continue;
this.aSnapHits.removeHit(iHit);
this.aSnapHits.insertHit(0, thisHit);
break;
}
}
return ElementLocateManager_1.SnapStatus.Success;
}
async findLocatableHit(ev, newSearch, out) {
out.snapStatus = ElementLocateManager_1.SnapStatus.NoElements;
if (newSearch) {
this.aSnapHits = undefined;
// search for new hits, but if the cursor is still close to the current hit, don't throw away list.
if (ElementLocateManager_1.SnapStatus.Success !== (out.snapStatus = this.findHits(ev)))
return undefined;
}
else {
if (!this.aSnapHits) {
out.snapStatus = ElementLocateManager_1.SnapStatus.NoElements;
return undefined;
}
}
const thisList = this.aSnapHits;
if (undefined === thisList)
return undefined;
let thisHit;
let firstRejected;
const filterResponse = new ElementLocateManager_1.LocateResponse();
// keep looking through hits until we find one that is accu-snappable.
while (undefined !== (thisHit = thisList.getNextHit())) {
if (ElementLocateManager_1.LocateFilterStatus.Accept === await IModelApp_1.IModelApp.locateManager.filterHit(thisHit, ElementLocateManager_1.LocateAction.AutoLocate, filterResponse))
return thisHit;
// we only care about the status of the first hit.
if (undefined !== firstRejected)
continue;
firstRejected = filterResponse.clone();
firstRejected.snapStatus = IModelApp_1.IModelApp.toolAdmin.wantToolTip(thisHit) ? ElementLocateManager_1.SnapStatus.FilteredByApp : ElementLocateManager_1.SnapStatus.FilteredByAppQuietly;
}
if (undefined !== firstRejected)
out.setFrom(firstRejected);
// Reset current hit index to go back to first hit on next AccuSnap reset event...
thisList.resetCurrentHit();
return undefined;
}
/** When in auto-locate mode, advance to the next hit without searching again.
* @internal
*/
async resetButton() {
let hit;
const out = new ElementLocateManager_1.LocateResponse();
out.snapStatus = ElementLocateManager_1.SnapStatus.Disabled;
this.clearToolTip(undefined);
const ev = new Tool_1.BeButtonEvent();
IModelApp_1.IModelApp.toolAdmin.fillEventFromCursorLocation(ev);
if (this._doSnapping) {
// if we don't have any more candidate hits, get a new list at the current location
if (!this.aSnapHits || (0 === this.aSnapHits.length)) {
out.snapStatus = this.findHits(ev);
hit = (ElementLocateManager_1.SnapStatus.Success !== out.snapStatus || undefined === this.aSnapHits) ? undefined : await this.getAccuSnapDetail(this.aSnapHits, out);
}
else {
// drop the current hit from the list and then retest the list (without the dropped hit) to find the new snap
this.aSnapHits.removeCurrentHit();
hit = await this.getAccuSnapDetail(this.aSnapHits, out);
}
if (!this._doSnapping)
hit = undefined; // Snap no longer requested...
}
else if (this.isLocateEnabled) {
hit = await this.findLocatableHit(ev, false, out); // get next AccuSnap path (or undefined)
if (!this.isLocateEnabled)
hit = undefined; // Hit no longer requested...
}
// set the current hit
if (hit || this.currHit)
this.setCurrHit(hit);
// indicate errors
this.showSnapError(out, ev);
return out.snapStatus;
}
/** Find the best snap point according to the current cursor location
* @internal
*/
async onMotion(ev) {
this.clearToolTip(ev);
const vp = ev.viewport;
if (undefined === vp)
return;
const out = new ElementLocateManager_1.LocateResponse();
out.snapStatus = ElementLocateManager_1.SnapStatus.Disabled;
let hit;
if (this.isActive) {
if (this._doSnapping) {
out.snapStatus = this.findHits(ev);
hit = (ElementLocateManager_1.SnapStatus.Success !== out.snapStatus || undefined === this.aSnapHits) ? undefined : await this.getAccuSnapDetail(this.aSnapHits, out);
if (!this._doSnapping)
hit = undefined; // Snap no longer requested...
}
else if (this.isLocateEnabled) {
hit = await this.findLocatableHit(ev, true, out);
if (!this.isLocateEnabled)
hit = undefined; // Hit no longer requested...
}
}
// set the current hit and display the sprite (based on snap's KeypointType)
if (hit || this.currHit)
this.setCurrHit(hit);
// set up active error before calling displayToolTip to indicate error or show locate message...
this.showSnapError(out, ev);
this.displayToolTip(ev.viewPoint, vp, ev.rawPoint);
if (undefined !== this.touchCursor && Tool_1.InputSource.Mouse === ev.inputSource) {
this.touchCursor = undefined;
IModelApp_1.IModelApp.viewManager.invalidateDecorationsAllViews();
}
}
/** @internal */
onPreButtonEvent(ev) {
return (undefined !== this.touchCursor) ? this.touchCursor.isButtonHandled(ev) : false;
}
/** @internal */
onTouchStart(ev) {
if (undefined !== this.touchCursor)
this.touchCursor.doTouchStart(ev);
}
/** @internal */
onTouchEnd(ev) {
if (undefined !== this.touchCursor && 0 === ev.touchCount)
this.touchCursor.doTouchEnd(ev);
}
/** @internal */
onTouchCancel(ev) {
if (undefined !== this.touchCursor)
this.touchCursor.doTouchEnd(ev);
}
/** @internal */
onTouchMove(ev) {
return (undefined !== this.touchCursor) ? this.touchCursor.doTouchMove(ev) : false;
}
/** @internal */
onTouchMoveStart(ev, startEv) {
return (undefined !== this.touchCursor) ? this.touchCursor.doTouchMoveStart(ev, startEv) : false;
}
/** @internal */
get wantVirtualCursor() {
return this._doSnapping || (this.isLocateEnabled && ToolSettings_1.ToolSettings.enableVirtualCursorForLocate);
}
/** @internal */
async onTouchTap(ev) {
if (undefined !== this.touchCursor)
return this.touchCursor.doTouchTap(ev);
if (!this.wantVirtualCursor)
return false;
this.touchCursor = TouchCursor.createFromTouchTap(ev);
if (undefined === this.touchCursor)
return false;
// Give active tool an opportunity to update it's tool assistance since event won't be passed along...
const tool = IModelApp_1.IModelApp.toolAdmin.activeTool;
if (undefined === tool)
return true;
await tool.onSuspend();
await tool.onUnsuspend();
return true;
}
flashElements(context) {
const viewport = context.viewport;
if (this.currHit