UNPKG

@itwin/core-frontend

Version:
1,006 lines • 55.9 kB
"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