UNPKG

@itwin/core-frontend

Version:
384 lines • 18 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.ElementLocateManager = exports.ElementPicker = exports.LocateResponse = exports.LocateOptions = exports.SnapStatus = exports.LocateFilterStatus = exports.LocateAction = void 0; const core_bentley_1 = require("@itwin/core-bentley"); const core_geometry_1 = require("@itwin/core-geometry"); const HitDetail_1 = require("./HitDetail"); const IModelApp_1 = require("./IModelApp"); const Pixel_1 = require("./render/Pixel"); const Tool_1 = require("./tools/Tool"); const ViewRect_1 = require("./common/ViewRect"); /** The possible actions for which a locate filter can be called. * @public * @extensions */ var LocateAction; (function (LocateAction) { LocateAction[LocateAction["Identify"] = 0] = "Identify"; LocateAction[LocateAction["AutoLocate"] = 1] = "AutoLocate"; })(LocateAction || (exports.LocateAction = LocateAction = {})); /** Values to return from a locate filter. * Return `Reject` to indicate the element is unacceptable. * @public * @extensions */ var LocateFilterStatus; (function (LocateFilterStatus) { LocateFilterStatus[LocateFilterStatus["Accept"] = 0] = "Accept"; LocateFilterStatus[LocateFilterStatus["Reject"] = 1] = "Reject"; })(LocateFilterStatus || (exports.LocateFilterStatus = LocateFilterStatus = {})); /** * @public * @extensions */ var SnapStatus; (function (SnapStatus) { SnapStatus[SnapStatus["Success"] = 0] = "Success"; SnapStatus[SnapStatus["Aborted"] = 1] = "Aborted"; SnapStatus[SnapStatus["NoElements"] = 2] = "NoElements"; SnapStatus[SnapStatus["Disabled"] = 100] = "Disabled"; SnapStatus[SnapStatus["NoSnapPossible"] = 200] = "NoSnapPossible"; SnapStatus[SnapStatus["NotSnappable"] = 300] = "NotSnappable"; SnapStatus[SnapStatus["FilteredByApp"] = 600] = "FilteredByApp"; SnapStatus[SnapStatus["FilteredByAppQuietly"] = 700] = "FilteredByAppQuietly"; })(SnapStatus || (exports.SnapStatus = SnapStatus = {})); /** Options that customize the way element location (i.e. *picking*) works. * @public * @extensions */ class LocateOptions { /** If true, also test graphics from view decorations. */ allowDecorations = false; /** If true, also test graphics with non-locatable flag set. */ allowNonLocatable = false; /** Maximum number of hits to return. */ maxHits = 20; /** The [[HitSource]] identifying the caller. */ hitSource = HitDetail_1.HitSource.DataPoint; /** If true, also test graphics from an IModelConnection other than the one associated with the Viewport. This can occur if, e.g., a * [[TiledGraphicsProvider]] is used to display graphics from a different iModel into the [[Viewport]]. * @note If you override this, you must be prepared to properly handle [[HitDetail]]s originating from other IModelConnections. * @see [[HitDetail.iModel]] and [[HitDetail.isExternalIModelHit]] */ allowExternalIModels = false; /** If true, then the world point of a hit on a model will preserve any transforms applied to the model at display time, * such as those supplied by a [[ModelDisplayTransformProvider]] or [PlanProjectionSettings.elevation]($common). * Otherwise, the world point will be multiplied by the inverse of any such transforms to correlate it with the model's true coordinate space. */ preserveModelDisplayTransforms = false; /** Make a copy of this LocateOptions. */ clone() { const other = new LocateOptions(); other.allowDecorations = this.allowDecorations; other.allowNonLocatable = this.allowNonLocatable; other.maxHits = this.maxHits; other.hitSource = this.hitSource; other.allowExternalIModels = this.allowExternalIModels; return other; } setFrom(other) { this.allowDecorations = other.allowDecorations; this.allowNonLocatable = other.allowNonLocatable; this.maxHits = other.maxHits; this.hitSource = other.hitSource; this.allowExternalIModels = other.allowExternalIModels; } init() { this.allowDecorations = this.allowNonLocatable = this.allowExternalIModels = false; this.maxHits = 20; this.hitSource = HitDetail_1.HitSource.DataPoint; } } exports.LocateOptions = LocateOptions; /** * @public * @extensions */ class LocateResponse { snapStatus = SnapStatus.Success; reason; explanation = ""; /** @internal */ clone() { const other = new LocateResponse(); other.snapStatus = this.snapStatus; other.reason = this.reason; other.explanation = this.explanation; return other; } /** @internal */ setFrom(other) { this.snapStatus = other.snapStatus; this.reason = other.reason; this.explanation = other.explanation; } } exports.LocateResponse = LocateResponse; /** * @public * @extensions */ class ElementPicker { viewport; pickPointWorld = new core_geometry_1.Point3d(); hitList; empty() { this.pickPointWorld.setZero(); this.viewport = undefined; if (this.hitList) this.hitList.empty(); else this.hitList = new HitDetail_1.HitList(); } /** return the HitList for the last Pick performed. Optionally allows the caller to take ownership of the list. */ getHitList(takeOwnership) { const list = (0, core_bentley_1.expectDefined)(this.hitList); if (takeOwnership) this.hitList = undefined; return list; } getNextHit() { return this.hitList ? this.hitList.getNextHit() : undefined; } /** Return a hit from the list of hits created the last time pickElements was called. */ getHit(i) { return this.hitList ? this.hitList.getHit(i) : undefined; } resetCurrentHit() { if (this.hitList) this.hitList.resetCurrentHit(); } comparePixel(pixel1, pixel2, distXY1, distXY2) { const priority1 = pixel1.computeHitPriority(); const priority2 = pixel2.computeHitPriority(); if (priority1 < priority2) return -1; if (priority1 > priority2) return 1; if (distXY1 < distXY2) return -1; if (distXY1 > distXY2) return 1; if (pixel1.distanceFraction > pixel2.distanceFraction) return -1; if (pixel1.distanceFraction < pixel2.distanceFraction) return 1; return 0; } /** Generate a list of elements that are close to a given point. * @param vp Viewport to use for pick * @param pickPointWorld Pick location in world coordinates * @param pickRadiusView Pick radius in pixels * @param options Pick options to use * @param excludedElements Optional ids to not draw during pick. Allows hits for geometry obscured by these ids to be returned. * @returns The number of hits in the hitList of this object. */ doPick(vp, pickPointWorld, pickRadiusView, options, excludedElements) { if (this.hitList && this.hitList.length > 0 && vp === this.viewport && pickPointWorld.isAlmostEqual(this.pickPointWorld)) { this.hitList.resetCurrentHit(); return this.hitList.length; } this.empty(); // empty the hit list this.viewport = vp; this.pickPointWorld.setFrom(pickPointWorld); const pickPointView = vp.worldToView(pickPointWorld); const testPointView = new core_geometry_1.Point2d(Math.floor(pickPointView.x + 0.5), Math.floor(pickPointView.y + 0.5)); let pixelRadius = Math.floor(pickRadiusView + 0.5); const rect = new ViewRect_1.ViewRect(testPointView.x - pixelRadius, testPointView.y - pixelRadius, testPointView.x + pixelRadius, testPointView.y + pixelRadius); if (rect.isNull) return 0; const receiver = (pixels) => { if (undefined === pixels) return; testPointView.x = vp.cssPixelsToDevicePixels(testPointView.x); testPointView.y = vp.cssPixelsToDevicePixels(testPointView.y); pixelRadius = vp.cssPixelsToDevicePixels(pixelRadius); const elmHits = new Map(); const testPoint = core_geometry_1.Point2d.createZero(); for (testPoint.x = testPointView.x - pixelRadius; testPoint.x <= testPointView.x + pixelRadius; ++testPoint.x) { for (testPoint.y = testPointView.y - pixelRadius; testPoint.y <= testPointView.y + pixelRadius; ++testPoint.y) { const pixel = pixels.getPixel(testPoint.x, testPoint.y); if (undefined === pixel || undefined === pixel.elementId || core_bentley_1.Id64.isInvalid(pixel.elementId)) continue; // no geometry at this location... const distXY = testPointView.distance(testPoint); if (distXY > pixelRadius) continue; // ignore corners. it's a locate circle not square... const oldPoint = elmHits.get(pixel.elementId); if (undefined !== oldPoint) { if (this.comparePixel(pixel, pixels.getPixel(oldPoint.x, oldPoint.y), distXY, testPointView.distance(oldPoint)) < 0) oldPoint.setFrom(testPoint); // new hit is better, update location... } else { elmHits.set(pixel.elementId, testPoint.clone()); } } } if (0 === elmHits.size) return; for (const elmPoint of elmHits.values()) { const pixel = pixels.getPixel(elmPoint.x, elmPoint.y); if (undefined === pixel || undefined === pixel.elementId) continue; const hitPointWorld = vp.getPixelDataWorldPoint({ pixels, x: elmPoint.x, y: elmPoint.y, preserveModelDisplayTransforms: options.preserveModelDisplayTransforms, }); if (!hitPointWorld) continue; const hit = new HitDetail_1.HitDetail({ ...pixel.toHitProps(vp), testPoint: pickPointWorld, viewport: vp, hitSource: options.hitSource, hitPoint: hitPointWorld, distXY: testPointView.distance(elmPoint), }); const hitList = (0, core_bentley_1.expectDefined)(this.hitList); hitList.addHit(hit); if (hitList.hits.length > options.maxHits) hitList.hits.length = options.maxHits; // truncate array... } result = (0, core_bentley_1.expectDefined)(this.hitList).length; }; const args = { receiver, rect, selector: Pixel_1.Pixel.Selector.All, excludeNonLocatable: !options.allowNonLocatable, excludedElements, }; let result = 0; vp.readPixels(args); return result; } testHit(hit, vp, pickPointWorld, pickRadiusView, options) { if (0 === this.doPick(vp, pickPointWorld, pickRadiusView, options)) return false; return (0, core_bentley_1.expectDefined)(this.hitList).hits.some((thisHit) => hit.isSameHit(thisHit)); } } exports.ElementPicker = ElementPicker; /** * @public * @extensions */ class ElementLocateManager { hitList; currHit; options = new LocateOptions(); picker = new ElementPicker(); /** get the full message key for a locate failure */ static getFailureMessageKey(key) { return `LocateFailure.${key}`; } onInitialized() { } get apertureInches() { return 0.11; } get touchApertureInches() { return 0.22; } clear() { this.setCurrHit(undefined); } setHitList(list) { this.hitList = list; } setCurrHit(hit) { this.currHit = hit; } getNextHit() { return this.hitList ? this.hitList.getNextHit() : undefined; } /** return the current path from either the snapping logic or the pre-locating systems. */ getPreLocatedHit() { // NOTE: Check AccuSnap first as Tentative is used to build intersect snap. For normal snaps when a Tentative is active there should be no AccuSnap. const fromAccuSnap = IModelApp_1.IModelApp.accuSnap.getHitAndList(this); const preLocated = fromAccuSnap ?? IModelApp_1.IModelApp.tentativePoint.getHitAndList(this); if (preLocated) { const excludedElements = (preLocated.isElementHit ? new Set([preLocated.sourceId]) : undefined); if (excludedElements || !fromAccuSnap) { // NOTE: For tentative snap, get new hit list at snap point; want reset to cycle hits using adjusted point location... const point = (fromAccuSnap ? preLocated.hitPoint : preLocated.getPoint()); const vp = preLocated.viewport; this.picker.empty(); this.picker.doPick(vp, point, (vp.pixelsFromInches(this.apertureInches) / 2.0) + 1.5, this.options, excludedElements); this.setHitList(this.picker.getHitList(true)); if (excludedElements) { if (undefined === this.hitList) this.hitList = new HitDetail_1.HitList(); this.hitList.insertHit(0, preLocated); } } } if (this.hitList) this.hitList.resetCurrentHit(); return preLocated; } async filterHit(hit, _action, out) { // Tools must opt-in to locate of transient geometry as it requires special treatment. if (!this.options.allowDecorations && !hit.isElementHit) { if (hit.isModelHit) out.reason = ElementLocateManager.getFailureMessageKey("RealityModel"); else if (hit.isMapHit) out.reason = ElementLocateManager.getFailureMessageKey("Map"); else out.reason = ElementLocateManager.getFailureMessageKey("Transient"); return LocateFilterStatus.Reject; } // Tools must opt-in to locate geometry from external iModels. if (!this.options.allowExternalIModels && hit.isExternalIModelHit) { out.reason = ElementLocateManager.getFailureMessageKey("ExternalIModel"); return LocateFilterStatus.Reject; } if (undefined !== hit.subCategoryId && !hit.isExternalIModelHit) { const appearance = hit.viewport.getSubCategoryAppearance(hit.subCategoryId); if (appearance.dontLocate) { out.reason = ElementLocateManager.getFailureMessageKey("NotLocatableSubCategory"); return LocateFilterStatus.Reject; } } const tool = IModelApp_1.IModelApp.toolAdmin.activeTool; if (!(tool && tool instanceof Tool_1.InteractiveTool)) return LocateFilterStatus.Accept; const status = await tool.filterHit(hit, out); if (LocateFilterStatus.Reject === status) out.reason = ElementLocateManager.getFailureMessageKey("ByApp"); return status; } initLocateOptions() { this.options.init(); } initToolLocate() { this.initLocateOptions(); this.clear(); this.picker.empty(); IModelApp_1.IModelApp.tentativePoint.clear(true); } async _doLocate(response, newSearch, testPoint, vp, source, filterHits) { if (!vp) return; // the "newSearch" flag indicates whether the caller wants us to conduct a new search at the testPoint, or just continue returning paths from the previous search. if (newSearch) { const hit = this.getPreLocatedHit(); // if we're snapped to something, that path has the highest priority and becomes the active hit. if (hit) { if (!filterHits || LocateFilterStatus.Accept === await this.filterHit(hit, LocateAction.Identify, response)) return hit; response = new LocateResponse(); // we have the reason and explanation we want. } this.picker.empty(); this.picker.doPick(vp, testPoint, (vp.pixelsFromInches(Tool_1.InputSource.Touch === source ? this.touchApertureInches : this.apertureInches) / 2.0) + 1.5, this.options); const hitList = this.picker.getHitList(true); this.setHitList(hitList); } let newHit; while (undefined !== (newHit = this.getNextHit())) { if (!filterHits || LocateFilterStatus.Accept === await this.filterHit(newHit, LocateAction.Identify, response)) return newHit; response = new LocateResponse(); // we have the reason and explanation we want. } return undefined; } async doLocate(response, newSearch, testPoint, view, source, filterHits = true) { response.reason = ElementLocateManager.getFailureMessageKey("NoElements"); response.explanation = ""; const hit = await this._doLocate(response, newSearch, testPoint, view, source, filterHits); this.setCurrHit(hit); // if we found a hit, remove it from the list of remaining hits near the current search point. if (hit && this.hitList) this.hitList.removeHitsFrom(hit.sourceId); return hit; } } exports.ElementLocateManager = ElementLocateManager; //# sourceMappingURL=ElementLocateManager.js.map