"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 Tools
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.ElementSetTool = exports.ElementAgenda = exports.ModifyElementSource = void 0;
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const core_geometry_1 = require("@itwin/core-geometry");
const AccuDraw_1 = require("../AccuDraw");
const ElementLocateManager_1 = require("../ElementLocateManager");
const IModelApp_1 = require("../IModelApp");
const NotificationManager_1 = require("../NotificationManager");
const Pixel_1 = require("../render/Pixel");
const ViewRect_1 = require("../common/ViewRect");
const PrimitiveTool_1 = require("./PrimitiveTool");
const SelectTool_1 = require("./SelectTool");
const Tool_1 = require("./Tool");
const ToolAssistance_1 = require("./ToolAssistance");
const ToolSettings_1 = require("./ToolSettings");
/** Identifies the source of the elements in the agenda.
 * @public
 */
var ModifyElementSource;
(function (ModifyElementSource) {
    /** The source for the element is unknown - not caused by a modification command. */
    ModifyElementSource[ModifyElementSource["Unknown"] = 0] = "Unknown";
    /** The element is selected by the user. */
    ModifyElementSource[ModifyElementSource["Selected"] = 1] = "Selected";
    /** The element is processed because it is in the selection set. */
    ModifyElementSource[ModifyElementSource["SelectionSet"] = 2] = "SelectionSet";
    /** The element is selected by the user using drag box or crossing line selection. */
    ModifyElementSource[ModifyElementSource["DragSelect"] = 3] = "DragSelect";
})(ModifyElementSource || (exports.ModifyElementSource = ModifyElementSource = {}));
/** The ElementAgenda class is used by [[ElementSetTool]] to hold the collection of elements it will operate on
 * and to manage their hilite state.
 * @see [[ElementSetTool]]
 * @public
*/
class ElementAgenda {
    iModel;
    /** The IDs of the elements in this agenda.
     * @note Prefer methods like [[ElementAgenda.add]] instead of modifying directly.
     */
    elements = [];
    /** The group source identifiers for the elements in this agenda.
     * @note Prefer methods like [[ElementAgenda.add]] instead of modifying directly.
     */
    groupMarks = [];
    manageHiliteState = true; // Whether entries are hilited/unhilited as they are added/removed...
    constructor(iModel) {
        this.iModel = iModel;
    }
    /** Get the source for the last group added to this agenda, if applicable. The "source" is merely an indication of what the collection of elements represents. */
    getSource() {
        return this.groupMarks.length === 0 ? ModifyElementSource.Unknown : this.groupMarks[this.groupMarks.length - 1].source;
    }
    /** Set the source for the last group added to this agenda. */
    setSource(val) {
        if (this.groupMarks.length > 0)
            this.groupMarks[this.groupMarks.length - 1].source = val;
    }
    get isEmpty() { return this.length === 0; }
    get count() { return this.length; }
    get length() { return this.elements.length; }
    /** Create [[OrderedId64Array]] from agenda. */
    orderIds() {
        const ids = new core_bentley_1.OrderedId64Array();
        this.elements.forEach((id) => ids.insert(id));
        return ids;
    }
    /** Create [[CompressedId64Set]] from agenda. */
    compressIds() {
        const ids = this.orderIds();
        return core_bentley_1.CompressedId64Set.compressIds(ids);
    }
    /** Empties the agenda and clears hilite state when manageHiliteState is true. */
    clear() {
        this.setEntriesHiliteState(false);
        this.elements.length = 0;
        this.groupMarks.length = 0;
    }
    setEntriesHiliteState(onOff, groupStart = 0, groupEnd = 0) {
        if (!this.manageHiliteState)
            return;
        const ss = this.iModel.selectionSet.elements.size > 0 ? this.iModel.selectionSet.elements : undefined;
        if (undefined === ss && 0 === groupEnd) {
            this.iModel.hilited[onOff ? "add" : "remove"]({ elements: this.elements });
            return;
        }
        const shouldChangeHilite = (id, index) => {
            if (undefined !== ss && ss.has(id))
                return false; // Don't turn hilite on/off for elements in current selection set...
            return (0 === groupEnd || (index >= groupStart && index < groupEnd));
        };
        const group = this.elements.filter((id, index) => shouldChangeHilite(id, index));
        this.iModel.hilited[onOff ? "add" : "remove"]({ elements: group });
    }
    /** Removes the last group of elements added to this agenda. */
    popGroup() {
        if (this.groupMarks.length <= 1) {
            this.clear();
            return;
        }
        const group = this.groupMarks.pop();
        if (undefined === group)
            return;
        this.setEntriesHiliteState(false, group.start, this.length); // make sure removed entries aren't left hilited...
        this.elements.splice(group.start);
    }
    /** Return true if elementId is already in this agenda. */
    has(id) { return this.elements.some((entry) => id === entry); }
    /** Return true if elementId is already in this agenda. */
    find(id) { return this.has(id); }
    /** Add elements to this agenda. */
    add(arg) {
        const groupStart = this.length;
        for (const id of core_bentley_1.Id64.iterable(arg))
            if (!this.has(id))
                this.elements.push(id);
        if (groupStart === this.length)
            return false;
        this.groupMarks.push({ start: groupStart, source: ModifyElementSource.Unknown });
        this.setEntriesHiliteState(true, groupStart, this.length);
        return true;
    }
    removeOne(id) {
        let pos = -1;
        const elements = this.elements;
        const groupMarks = this.groupMarks;
        elements.some((entry, index) => {
            if (id !== entry)
                return false;
            pos = index;
            return true;
        });
        if (pos === -1)
            return false;
        if (1 === elements.length || (1 === groupMarks.length && ModifyElementSource.DragSelect !== groupMarks[groupMarks.length - 1].source)) {
            this.clear();
            return true;
        }
        const groupIndex = pos;
        let groupStart = 0, groupEnd = 0;
        let markToErase = 0;
        let removeSingleEntry = false;
        for (let iMark = 0; iMark < groupMarks.length; ++iMark) {
            if (0 === groupEnd) {
                if (iMark + 1 === groupMarks.length) {
                    markToErase = iMark;
                    removeSingleEntry = (ModifyElementSource.DragSelect === groupMarks[iMark].source);
                    groupStart = groupMarks[iMark].start;
                    groupEnd = elements.length;
                }
                else if (groupMarks[iMark].start <= groupIndex && groupMarks[iMark + 1].start > groupIndex) {
                    markToErase = iMark;
                    removeSingleEntry = (ModifyElementSource.DragSelect === groupMarks[iMark].source);
                    groupStart = groupMarks[iMark].start;
                    groupEnd = groupMarks[iMark + 1].start;
                }
                continue;
            }
            if (removeSingleEntry)
                groupMarks[iMark].start -= 1; // Only removing single entry, not entire group...
            else
                groupMarks[iMark].start -= (groupEnd - groupStart); // Adjust indices...
        }
        if (removeSingleEntry) { // Only remove single entry...
            this.setEntriesHiliteState(false, groupIndex, groupIndex + 1); // make sure removed entry isn't left hilited...
            elements.splice(groupIndex, 1);
            if (groupEnd === groupStart + 1)
                groupMarks.splice(markToErase, 1);
            return true;
        }
        this.setEntriesHiliteState(false, groupStart, groupEnd); // make sure removed entries aren't left hilited...
        elements.splice(groupStart, groupEnd - groupStart);
        groupMarks.splice(markToErase, 1);
        return true;
    }
    remove(arg) {
        if (0 === this.length)
            return false;
        if (0 === core_bentley_1.Id64.sizeOf(arg))
            return false;
        let changed = false;
        for (const elId of core_bentley_1.Id64.iterable(arg))
            if (this.removeOne(elId))
                changed = true; // NOTE: Removes group associated with this element, not just a single entry...
        return changed;
    }
    /** Add elements not currently in the agenda and remove elements currently in the agenda. */
    invert(arg) {
        if (0 === this.length)
            return this.add(arg);
        if (0 === core_bentley_1.Id64.sizeOf(arg))
            return false;
        const adds = [];
        const removes = [];
        for (const id of core_bentley_1.Id64.iterable(arg)) {
            if (this.has(id))
                removes.push(id);
            else
                adds.push(id);
        }
        if (adds.length === 0 && removes.length === 0)
            return false;
        removes.forEach((id) => this.removeOne(id));
        if (adds.length > 0) {
            const groupStart = this.length;
            adds.forEach((id) => this.elements.push(id));
            this.groupMarks.push({ start: groupStart, source: ModifyElementSource.Unknown });
            this.setEntriesHiliteState(true, groupStart, this.length); // make sure added entries are hilited (when not also removing)...
        }
        return true;
    }
}
exports.ElementAgenda = ElementAgenda;
/** The ElementSetTool class is a specialization of [[PrimitiveTool]] designed to unify operations on sets of elements.
 * Use to query or modify existing elements as well as to create new elements from existing elements.
 * Basic tool sequence:
 * - Populate [[ElementSetTool.agenda]] with the element ids to query or modify.
 * - Gather any additional input and if requested, enable dynamics to preview result.
 * - Call [[ElementSetTool.processAgenda]] to apply operation to [[ElementSetTool.agenda]].
 * - Call [[ElementSetTool.onProcessComplete]] to restart or exit.
 * Common element sources:
 * - Pre-selected elements from an active [[SelectionSet]].
 * - Clicking in a view to identify elements using [[ElementLocateManager]].
 * - Drag box and crossing line selection.
 * Default behavior:
 * - Identify a single element with left-click.
 * - Immediately apply operation.
 * - Restart.
 * Sub-classes are required to opt-in to additional element sources, dynamics, AccuSnap, additional input, etc.
 * @public
 */
class ElementSetTool extends PrimitiveTool_1.PrimitiveTool {
    _agenda;
    _useSelectionSet = false;
    _processDataButtonUp = false;
    /** The accept point for a selection set, drag select, or final located element. */
    anchorPoint;
    /** The button down location that initiated box or crossing line selection. */
    dragStartPoint;
    /** Get the [[ElementAgenda]] the tool will operate on. */
    get agenda() {
        if (undefined === this._agenda)
            this._agenda = new ElementAgenda(this.iModel);
        return this._agenda;
    }
    /** Convenience method to get current count from [[ElementSetTool.agenda]]. */
    get currentElementCount() { return undefined !== this._agenda ? this._agenda.count : 0; }
    /** Minimum required number of elements for tool to be able to complete.
     * @return number to compare with [[ElementSetTool.currentElementCount]] to determine if more elements remain to be identified.
     * @note A tool to subtract elements is an example where returning 2 would be necessary.
     */
    get requiredElementCount() { return 1; }
    /** Whether to allow element identification by drag box or crossing line selection.
     * @return true to allow drag select as an element source when the ctrl key is down.
     * @note Use ctrl+left drag for box selection. Inside/overlap is based on left/right direction (shift key inverts).
     * @note Use ctrl+right drag for crossing line selection.
     */
    get allowDragSelect() { return false; }
    /** Support operations on groups/assemblies independent of selection scope.
     * @return true to add or remove all members of an assembly from [[ElementSetTool.agenda]] when any single member is identified.
     * @note Applies to [[ElementSetTool.getLocateCandidates]] only.
     */
    get allowGroups() { return false; }
    /** Whether [[ElementSetTool.agenda]] should be populated from an active selection set.
     * @return true to allow selection sets as an element source.
     * @note A selection set must have at least [[ElementSetTool.requiredElementCount]] elements to be considered.
     */
    get allowSelectionSet() { return false; }
    /** Whether to clear the active selection set for tools that return false for [[ElementSetTool.allowSelectionSet]].
     * @return true to clear unsupported selection sets (desired default behavior).
     * @note It is expected that the selection set be cleared before using [[ElementLocateManager]] to identify elements.
     * This allows the element hilite to be a visual representation of the [[ElementSetTool.agenda]] contents.
     */
    get clearSelectionSet() { return !this.allowSelectionSet; }
    /** Whether a selection set should be processed immediately upon installation or require a data button to accept.
     * @return false only for tools without settings or a need for confirmation.
     * @note A tool to delete elements is an example where returning false could be desirable.
     */
    get requireAcceptForSelectionSetOperation() { return true; }
    /** Whether to begin dynamics for a selection set immediately or wait for a data button.
     * @return false for tools that can start showing dynamics without any additional input.
     * @note A tool to rotate elements by an active angle setting is an example where returning false could be desirable.
     */
    get requireAcceptForSelectionSetDynamics() { return true; }
    /** Whether original source of elements being modified was the active selection set.
     * @return true when [[ElementSetTool.allowSelectionSet]] and active selection set count >= [[ElementSetTool.requiredElementCount]].
     */
    get isSelectionSetModify() { return this._useSelectionSet; }
    /** Whether drag box or crossing line selection is currently active.
     * @return true when [[ElementSetTool.allowDragSelect]] and corner points are currently being defined.
     */
    get isSelectByPoints() { return undefined !== this.dragStartPoint; }
    /** Whether to continue selection of additional elements by holding the ctrl key down.
     * @return true to continue the element identification phase beyond [[ElementSetTool.requiredElementCount]] by holding down the ctrl key.
     */
    get controlKeyContinuesSelection() { return false; }
    /** Whether to invert selection of elements identified with the ctrl key held down.
     * @return true to allow ctrl to deselect already selected elements.
     */
    get controlKeyInvertsSelection() { return this.controlKeyContinuesSelection; }
    /** Whether [[ElementSetTool.setupAndPromptForNextAction]] should call [[AccuSnap.enableSnap]] for current tool phase.
     * @return true to enable snapping to elements.
     * @note A tool that just needs to identify elements and doesn't care about location should not enable snapping.
     */
    get wantAccuSnap() { return false; }
    /** Whether to automatically start element dynamics after all required elements have been identified.
     * @return true if tool will implement [[InteractiveTool.onDynamicFrame]] to show element dynamics.
     */
    get wantDynamics() { return false; }
    /** Whether tool is done identifying elements and is ready to move to the next phase.
     * @return true when [[ElementSetTool.requiredElementCount]] is not yet satisfied or ctrl key is being used to extend selection.
     */
    get wantAdditionalElements() {
        if (this.isSelectionSetModify)
            return false;
        if (this.currentElementCount < this.requiredElementCount)
            return true;
        // A defined anchor indicates input collection phase has begun and ctrl should no longer extend selection...
        return undefined === this.anchorPoint && this.controlKeyContinuesSelection && this.isControlDown;
    }
    /** Whether the tool has gathered enough input to call [[ElementSetTool.processAgenda]].
     * Sub-classes should override to check for additional point input they collected in [[ElementSetTool.wantProcessAgenda]].
     * @return true if tool does not yet have enough information to complete.
     * @note When [[ElementSetTool.wantDynamics]] is true an additional point is automatically required to support the dynamic preview.
     */
    get wantAdditionalInput() { return (!this.isDynamicsStarted && this.wantDynamics); }
    /** Whether the tool is ready for [[ElementSetTool.processAgenda]] to be called to complete the tool operation.
     * Sub-classes should override to collect additional point input before calling super or [[ElementSetTool.wantAdditionalInput]].
     * @return true if tool has enough information and is ready to complete.
     */
    wantProcessAgenda(_ev) { return !this.wantAdditionalInput; }
    /** Whether tool should operate on an existing selection set or instead prompt user to identity elements.
     * Unsupported selection sets will be cleared when [[ElementSetTool.clearSelectionSet]] is true.
    */
    setPreferredElementSource() {
        this._useSelectionSet = false;
        if (!this.iModel.selectionSet.isActive)
            return;
        const isSelectionSetValid = () => {
            if (0 === this.iModel.selectionSet.elements.size) {
                IModelApp_1.IModelApp.notifications.outputMessage(new NotificationManager_1.NotifyMessageDetails(NotificationManager_1.OutputMessagePriority.Info, Tool_1.CoreTools.translate("ElementSet.Error.ActiveSSWithoutElems")));
                return false;
            }
            return (this.iModel.selectionSet.elements.size >= this.requiredElementCount);
        };
        if (this.allowSelectionSet && isSelectionSetValid())
            this._useSelectionSet = true;
        else if (this.clearSelectionSet)
            this.iModel.selectionSet.emptyAll();
    }
    /** Get element ids to process from the active selection set.
     * Sub-classes may override to support selection scopes or apply tool specific filtering.
     */
    async getSelectionSetCandidates(ss) {
        const ids = new Set();
        ss.elements.forEach((val) => {
            if (this.isElementIdValid(val, ModifyElementSource.SelectionSet))
                ids.add(val);
        });
        return ids;
    }
    /** Populate [[ElementSetTool.agenda]] from a [[SelectionSet]].
     * @see [[ElementSetTool.getSelectionSetCandidates]] to filter or augment the set of elements.
     */
    async buildSelectionSetAgenda(ss) {
        const candidates = await this.getSelectionSetCandidates(ss);
        if (core_bentley_1.Id64.sizeOf(candidates) < this.requiredElementCount || !this.agenda.add(candidates)) {
            IModelApp_1.IModelApp.notifications.outputMessage(new NotificationManager_1.NotifyMessageDetails(NotificationManager_1.OutputMessagePriority.Info, Tool_1.CoreTools.translate("ElementSet.Error.NoSSElems")));
            return false;
        }
        this.agenda.setSource(ModifyElementSource.SelectionSet);
        await this.onAgendaModified();
        return true;
    }
    /** If the supplied element is part of an assembly, return all member ids. */
    async getGroupIds(id) {
        const ids = new Set();
        ids.add(id);
        try {
            // When assembly parent is selected, pick all geometric elements with it as the parent.
            // When assembly member is selected, pick the parent as well as all the other members.
            const ecsql = `SELECT ECInstanceId as id, Parent.Id as parentId FROM BisCore.GeometricElement WHERE Parent.Id IN (SELECT Parent.Id as parentId FROM BisCore.GeometricElement WHERE (parent.Id IS NOT NULL AND ECInstanceId IN (${id})) OR parent.Id IN (${id}))`;
            // eslint-disable-next-line @typescript-eslint/no-deprecated
            for await (const row of this.iModel.createQueryReader(ecsql, undefined, { rowFormat: core_common_1.QueryRowFormat.UseJsPropertyNames })) {
                ids.add(row.parentId);
                ids.add(row.id);
            }
        }
        catch { }
        return ids;
    }
    /** Get element id(s) to process from a [[HitDetail]] already accepted by [[ElementSetTool.isElementValidForOperation]].
     * Sub-classes may override to support selection scopes.
     */
    async getLocateCandidates(hit) {
        if (!this.allowGroups)
            return hit.sourceId;
        return this.getGroupIds(hit.sourceId);
    }
    /** Populate [[ElementSetTool.agenda]] from a [[HitDetail]].
     * @see [[ElementSetTool.getLocateCandidates]] to add additional elements.
     */
    async buildLocateAgenda(hit) {
        if (this.agenda.find(hit.sourceId)) {
            if (this.isControlDown && this.controlKeyInvertsSelection && this.agenda.remove(hit.sourceId)) {
                await this.onAgendaModified();
                return true;
            }
            return false;
        }
        const candidates = await this.getLocateCandidates(hit);
        if (!this.agenda.add(candidates))
            return false;
        this.agenda.setSource(ModifyElementSource.Selected);
        await this.onAgendaModified();
        return true;
    }
    /** Get ids of spatial elements to process from a clip volume created by drag box selection. */
    static async getVolumeSelectionCandidates(vp, origin, corner, allowOverlaps, filter) {
        const contents = new Set();
        if (!vp.view.isSpatialView())
            return contents;
        const boxRange = core_geometry_1.Range2d.createXYXY(origin.x, origin.y, corner.x, corner.y);
        if (boxRange.isNull || boxRange.isAlmostZeroX || boxRange.isAlmostZeroY)
            return contents;
        const getClipPlane = (viewPt, viewDir, negate) => {
            const point = vp.viewToWorld(core_geometry_1.Point3d.createFrom(viewPt));
            const boresite = AccuDraw_1.AccuDrawHintBuilder.getBoresite(point, vp);
            const normal = viewDir.crossProduct(boresite.direction);
            if (negate)
                normal.negate(normal);
            return core_geometry_1.ClipPlane.createNormalAndPoint(normal, point);
        };
        const planeSet = core_geometry_1.ConvexClipPlaneSet.createEmpty();
        planeSet.addPlaneToConvexSet(getClipPlane(boxRange.low, vp.rotation.rowX(), true));
        planeSet.addPlaneToConvexSet(getClipPlane(boxRange.low, vp.rotation.rowY(), true));
        planeSet.addPlaneToConvexSet(getClipPlane(boxRange.high, vp.rotation.rowX(), false));
        planeSet.addPlaneToConvexSet(getClipPlane(boxRange.high, vp.rotation.rowY(), false));
        if (0 === planeSet.planes.length)
            return contents;
        const clip = core_geometry_1.ClipVector.createCapture([core_geometry_1.ClipPrimitive.createCapture(planeSet)]);
        const viewRange = vp.computeViewRange();
        const range = core_geometry_1.ClipUtilities.rangeOfClipperIntersectionWithRange(clip, viewRange);
        if (range.isNull)
            return contents;
        // TODO: Possible to make UnionOfComplexClipPlaneSets from view clip and planes work and remove 2nd containment check?
        const viewClip = (vp.viewFlags.clipVolume ? vp.view.getViewClip()?.clone() : undefined);
        if (viewClip) {
            const viewClipRange = core_geometry_1.ClipUtilities.rangeOfClipperIntersectionWithRange(viewClip, viewRange);
            if (viewClipRange.isNull || !viewClipRange.intersectsRange(range))
                return contents;
        }
        const candidates = [];
        const categories = new Set();
        try {
            const viewedModels = [...vp.view.modelSelector.models].join(",");
            const viewedCategories = [...vp.view.categorySelector.categories].join(",");
            const ecsql = `SELECT e.ECInstanceId, Category.Id as category FROM bis.SpatialElement e JOIN bis.SpatialIndex i ON e.ECInstanceId=i.ECInstanceId WHERE Model.Id IN (${viewedModels}) AND Category.Id IN (${viewedCategories}) AND i.MinX <= ${range.xHigh} AND i.MinY <= ${range.yHigh} AND i.MinZ <= ${range.zHigh} AND i.MaxX >= ${range.xLow} AND i.MaxY >= ${range.yLow} AND i.MaxZ >= ${range.zLow}`;
            const reader = vp.iModel.createQueryReader(ecsql, undefined, { rowFormat: core_common_1.QueryRowFormat.UseECSqlPropertyNames });
            for await (const row of reader) {
                candidates.push(row.ECInstanceId);
                categories.add(row.category);
            }
        }
        catch { }
        if (0 === candidates.length)
            return contents;
        let offSubCategories;
        if (0 !== categories.size) {
            for (const categoryId of categories) {
                const subcategories = vp.iModel.subcategories.getSubCategories(categoryId);
                if (undefined === subcategories)
                    continue;
                for (const subCategoryId of subcategories) {
                    const appearance = vp.iModel.subcategories.getSubCategoryAppearance(subCategoryId);
                    if (undefined === appearance || (!appearance.invisible && !appearance.dontLocate))
                        continue;
                    if (undefined === offSubCategories)
                        offSubCategories = new Array;
                    offSubCategories.push(subCategoryId);
                }
            }
        }
        const requestProps = {
            candidates,
            clip: clip.toJSON(),
            allowOverlaps,
            viewFlags: vp.viewFlags.toJSON(),
            offSubCategories,
        };
        const result = await vp.iModel.getGeometryContainment(requestProps);
        if (core_bentley_1.BentleyStatus.SUCCESS !== result.status || undefined === result.candidatesContainment)
            return contents;
        result.candidatesContainment.forEach((status, index) => {
            if (core_geometry_1.ClipPlaneContainment.StronglyOutside !== status && (undefined === filter || filter(candidates[index])))
                contents.add(candidates[index]);
        });
        if (0 !== contents.size && viewClip) {
            requestProps.clip = viewClip.toJSON();
            requestProps.candidates.length = 0;
            for (const id of contents)
                requestProps.candidates.push(id);
            contents.clear();
            const resultViewClip = await vp.iModel.getGeometryContainment(requestProps);
            if (core_bentley_1.BentleyStatus.SUCCESS !== resultViewClip.status || undefined === resultViewClip.candidatesContainment)
                return contents;
            resultViewClip.candidatesContainment.forEach((status, index) => {
                if (core_geometry_1.ClipPlaneContainment.StronglyOutside !== status)
                    contents.add(candidates[index]);
            });
        }
        return contents;
    }
    /** Get ids of visible elements to process from drag box or crossing line selection. */
    static getAreaSelectionCandidates(vp, origin, corner, method, allowOverlaps, filter) {
        let contents = new Set();
        const pts = [];
        pts[0] = new core_geometry_1.Point2d(Math.floor(origin.x + 0.5), Math.floor(origin.y + 0.5));
        pts[1] = new core_geometry_1.Point2d(Math.floor(corner.x + 0.5), Math.floor(corner.y + 0.5));
        const range = core_geometry_1.Range2d.createArray(pts);
        const rect = new ViewRect_1.ViewRect();
        rect.initFromRange(range);
        vp.readPixels(rect, Pixel_1.Pixel.Selector.Feature, (pixels) => {
            if (undefined === pixels)
                return;
            const sRange = core_geometry_1.Range2d.createNull();
            sRange.extendPoint(core_geometry_1.Point2d.create(vp.cssPixelsToDevicePixels(range.low.x), vp.cssPixelsToDevicePixels(range.low.y)));
            sRange.extendPoint(core_geometry_1.Point2d.create(vp.cssPixelsToDevicePixels(range.high.x), vp.cssPixelsToDevicePixels(range.high.y)));
            pts[0].x = vp.cssPixelsToDevicePixels(pts[0].x);
            pts[0].y = vp.cssPixelsToDevicePixels(pts[0].y);
            pts[1].x = vp.cssPixelsToDevicePixels(pts[1].x);
            pts[1].y = vp.cssPixelsToDevicePixels(pts[1].y);
            const testPoint = core_geometry_1.Point2d.createZero();
            const getPixelElementId = (pixel) => {
                if (undefined === pixel.elementId || core_bentley_1.Id64.isInvalid(pixel.elementId))
                    return undefined; // no geometry at this location...
                if (!vp.isPixelSelectable(pixel))
                    return undefined; // reality model, terrain, etc - not selectable
                if (undefined !== filter && !filter(pixel.elementId))
                    return undefined;
                return pixel.elementId;
            };
            if (SelectTool_1.SelectionMethod.Box === method) {
                const outline = allowOverlaps ? undefined : new Set();
                const offset = sRange.clone();
                offset.expandInPlace(-2);
                for (testPoint.x = sRange.low.x; testPoint.x <= sRange.high.x; ++testPoint.x) {
                    for (testPoint.y = sRange.low.y; testPoint.y <= sRange.high.y; ++testPoint.y) {
                        const pixel = pixels.getPixel(testPoint.x, testPoint.y);
                        const elementId = getPixelElementId(pixel);
                        if (undefined === elementId)
                            continue;
                        if (undefined !== outline && !offset.containsPoint(testPoint))
                            outline.add(elementId.toString());
                        else
                            contents.add(elementId.toString());
                    }
                }
                if (undefined !== outline && 0 !== outline.size) {
                    const inside = new Set();
                    contents.forEach((id) => {
                        if (!outline.has(id))
                            inside.add(id);
                    });
                    contents = inside;
                }
            }
            else {
                const closePoint = core_geometry_1.Point2d.createZero();
                for (testPoint.x = sRange.low.x; testPoint.x <= sRange.high.x; ++testPoint.x) {
                    for (testPoint.y = sRange.low.y; testPoint.y <= sRange.high.y; ++testPoint.y) {
                        const pixel = pixels.getPixel(testPoint.x, testPoint.y);
                        const elementId = getPixelElementId(pixel);
                        if (undefined === elementId)
                            continue;
                        const fraction = testPoint.fractionOfProjectionToLine(pts[0], pts[1], 0.0);
                        pts[0].interpolate(fraction, pts[1], closePoint);
                        if (closePoint.distance(testPoint) < 1.5)
                            contents.add(elementId.toString());
                    }
                }
            }
        }, true);
        return contents;
    }
    /** Get ids of elements to process from drag box or crossing line selection using either the depth buffer or clip vector...
     * @internal
     */
    static async getAreaOrVolumeSelectionCandidates(vp, origin, corner, method, allowOverlaps, filter, includeDecorationsForVolume) {
        let contents;
        if (ToolSettings_1.ToolSettings.enableVolumeSelection && SelectTool_1.SelectionMethod.Box === method && vp.view.isSpatialView()) {
            contents = await ElementSetTool.getVolumeSelectionCandidates(vp, origin, corner, allowOverlaps, filter);
            // Use area select to identify pickable transients...
            if (includeDecorationsForVolume) {
                const acceptTransientsFilter = (id) => { return core_bentley_1.Id64.isTransient(id) && (undefined === filter || filter(id)); };
                const transients = ElementSetTool.getAreaSelectionCandidates(vp, origin, corner, method, allowOverlaps, acceptTransientsFilter);
                for (const id of transients)
                    contents.add(id);
            }
        }
        else {
            contents = ElementSetTool.getAreaSelectionCandidates(vp, origin, corner, method, allowOverlaps, filter);
        }
        return contents;
    }
    /** Get element ids to process from drag box or crossing line selection.
     * Sub-classes may override to support selection scopes or apply tool specific filtering.
     */
    async getDragSelectCandidates(vp, origin, corner, method, overlap) {
        const filter = (id) => { return this.isElementIdValid(id, ModifyElementSource.DragSelect); };
        return ElementSetTool.getAreaOrVolumeSelectionCandidates(vp, origin, corner, method, overlap, filter, IModelApp_1.IModelApp.locateManager.options.allowDecorations);
    }
    /** Populate [[ElementSetTool.agenda]] by drag box or crossing line information.
     * @see [[ElementSetTool.getDragSelectCandidates]] to filter or augment the set of elements.
     */
    async buildDragSelectAgenda(vp, origin, corner, method, overlap) {
        const candidates = await this.getDragSelectCandidates(vp, origin, corner, method, overlap);
        if (!this.isControlDown || !this.controlKeyInvertsSelection) {
            if (!this.agenda.add(candidates))
                return false;
        }
        else {
            if (!this.agenda.invert(candidates))
                return false;
        }
        if (ModifyElementSource.Unknown === this.agenda.getSource())
            this.agenda.setSource(ModifyElementSource.DragSelect); // Don't set source if invert only removed entries...
        await this.onAgendaModified();
        return true;
    }
    /** Quick id validity check. Sub-classes that wish to allow pickable decorations from selection sets can override. */
    isElementIdValid(id, source) {
        switch (source) {
            case ModifyElementSource.Selected:
                return true; // Locate options already checked prior to calling isElementValidForOperation...
            case ModifyElementSource.SelectionSet:
                return (!core_bentley_1.Id64.isInvalid(id) && !core_bentley_1.Id64.isTransient(id)); // Locate options are invalid, locate isn't enabled when processing a selection set...
            case ModifyElementSource.DragSelect:
                return (!core_bentley_1.Id64.isInvalid(id) && (IModelApp_1.IModelApp.locateManager.options.allowDecorations || !core_bentley_1.Id64.isTransient(id))); // Locate options are valid but have not yet been checked...
            default:
                return false;
        }
    }
    /** Sub-classes should override to apply tool specific filtering and to provide an explanation for rejection. */
    async isElementValidForOperation(hit, _out) {
        return this.isElementIdValid(hit.sourceId, ModifyElementSource.Selected);
    }
    /** Called from [[ElementSetTool.doLocate]] as well as auto-locate to accept or reject elements under the cursor. */
    async filterHit(hit, out) {
        // Support deselect using control key and don't show "not" cursor over an already selected element...
        if (undefined !== this._agenda && this._agenda.find(hit.sourceId)) {
            const status = (this.isControlDown || !this.controlKeyInvertsSelection) ? ElementLocateManager_1.LocateFilterStatus.Accept : ElementLocateManager_1.LocateFilterStatus.Reject;
            if (out && ElementLocateManager_1.LocateFilterStatus.Reject === status)
                out.explanation = Tool_1.CoreTools.translate(`ElementSet.Error.AlreadySelected`);
            return status;
        }
        return await this.isElementValidForOperation(hit, out) ? ElementLocateManager_1.LocateFilterStatus.Accept : ElementLocateManager_1.LocateFilterStatus.Reject;
    }
    /** Identify an element and update the element agenda.
     * @param newSearch true to locate new elements, false to cycle between elements within locate tolerance from a previous locate.
     * @return true if [[ElementSetTool.agenda]] was changed.
     */
    async doLocate(ev, newSearch) {
        const hit = await IModelApp_1.IModelApp.locateManager.doLocate(new ElementLocateManager_1.LocateResponse(), newSearch, ev.point, ev.viewport, ev.inputSource);
        if (newSearch)
            return (undefined !== hit && this.buildLocateAgenda(hit));
        // If next element is already in agenda (part of a group, etc.) don't re-add group...
        const addNext = (undefined !== hit && !this.agenda.has(hit.sourceId));
        this.agenda.popGroup();
        if (!addNext || !await this.buildLocateAgenda(hit))
            await this.onAgendaModified(); // only change was popGroup...
        return true;
    }
    /** Whether drag box selection only identifies elements that are wholly inside or also allows those that overlap
     * the selection rectangle.
     * @note Inside/overlap is based on left/right direction of corner points (shift key inverts check).
     */
    useOverlapSelection(ev) {
        if (undefined === ev.viewport || undefined === this.dragStartPoint)
            return false;
        const pt1 = ev.viewport.worldToView(this.dragStartPoint);
        const pt2 = ev.viewport.worldToView(ev.point);
        const overlapMode = (pt1.x > pt2.x);
        return (ev.isShiftKey ? !overlapMode : overlapMode); // Shift inverts inside/overlap selection...
    }
    /** Initiate tool state for start of drag selection. */
    async selectByPointsStart(ev) {
        if (Tool_1.BeButton.Data !== ev.button && Tool_1.BeButton.Reset !== ev.button)
            return false;
        if (!ev.isControlKey || !this.allowDragSelect || !this.wantAdditionalElements)
            return false;
        this.dragStartPoint = ev.point.clone();
        this.setupAndPromptForNextAction();
        return true;
    }
    /** Finish drag selection and update [[ElementSetTool.agenda]] with any elements that may have been identified. */
    async selectByPointsEnd(ev) {
        if (undefined === this.dragStartPoint)
            return false;
        const vp = ev.viewport;
        if (vp === undefined) {
            this.dragStartPoint = undefined;
            this.setupAndPromptForNextAction();
            return false;
        }
        const origin = vp.worldToView(this.dragStartPoint);
        const corner = vp.worldToView(ev.point);
        if (Tool_1.BeButton.Reset === ev.button)
            await this.buildDragSelectAgenda(vp, origin, corner, SelectTool_1.SelectionMethod.Line, true);
        else
            await this.buildDragSelectAgenda(vp, origin, corner, SelectTool_1.SelectionMethod.Box, this.useOverlapSelection(ev));
        this.dragStartPoint = undefined;
        this.setupAndPromptForNextAction();
        vp.invalidateDecorations();
        return true;
    }
    /** Display drag box and crossing line selection graphics. */
    selectByPointsDecorate(context) {
        if (undefined === this.dragStartPoint)
            return;
        const ev = new Tool_1.BeButtonEvent();
        IModelApp_1.IModelApp.toolAdmin.fillEventFromCursorLocation(ev);
        if (undefined === ev.viewport)
            return;
        const vp = context.viewport;
        const bestContrastIsBlack = (core_common_1.ColorDef.black === vp.getContrastToBackgroundColor());
        const crossingLine = (Tool_1.BeButton.Reset === ev.button);
        const overlapSelection = (crossingLine || this.useOverlapSelection(ev));
        const position = vp.worldToView(this.dragStartPoint);
        position.x = Math.floor(position.x) + 0.5;
        position.y = Math.floor(position.y) + 0.5;
        const position2 = vp.worldToView(ev.point);
        position2.x = Math.floor(position2.x) + 0.5;
        position2.y = Math.floor(position2.y) + 0.5;
        const offset = position2.minus(position);
        const drawDecoration = (ctx) => {
            ctx.strokeStyle = bestContrastIsBlack ? "black" : "white";
            ctx.lineWidth = 1;
            if (overlapSelection)
                ctx.setLineDash([5, 5]);
            if (crossingLine) {
                ctx.beginPath();
                ctx.moveTo(0, 0);
                ctx.lineTo(offset.x, offset.y);
                ctx.stroke();
            }
            else {
                ctx.strokeRect(0, 0, offset.x, offset.y);
                ctx.fillStyle = bestContrastIsBlack ? "rgba(0,0,0,.06)" : "rgba(255,255,255,.06)";
                ctx.fillRect(0, 0, offset.x, offset.y);
            }
        };
        context.addCanvasDecoration({ position, drawDecoration });
    }
    /** Show graphics for when drag selection is active. */
    decorate(context) { this.selectByPointsDecorate(context); }
    /** Make sure drag selection graphics are updated when mouse moves. */
    async onMouseMotion(ev) {
        if (undefined !== ev.viewport && this.isSelectByPoints)
            ev.viewport.invalidateDecorations();
    }
    /** Support initiating drag selection on mouse start drag event when [[ElementSetTool.allowDragSelect]] is true. */
    async onMouseStartDrag(ev) {
        if (await this.selectByPointsStart(ev))
            return Tool_1.EventHandled.Yes;
        return super.onMouseStartDrag(ev);
    }
    /** Support completing active drag selection on mouse end drag event and update [[ElementSetTool.agenda]]. */
    async onMouseEndDrag(ev) {
        if (await this.selectByPointsEnd(ev))
            return Tool_1.EventHandled.Yes;
        return super.onMouseEndDrag(ev);
    }
    /** Update prompts, cursor, graphics, etc. as appropriate on ctrl and shift key transitions. */
    async onModifierKeyTransition(_wentDown, modifier, _event) {
        if (this.isSelectionSetModify)
            return Tool_1.EventHandled.No;
        if (this.isSelectByPoints)
            return (Tool_1.BeModifierKeys.Shift === modifier ? Tool_1.EventHandled.Yes : Tool_1.EventHandled.No);
        if (Tool_1.BeModifierKeys.Control !== modifier || undefined !== this.anchorPoint || !this.controlKeyContinuesSelection)
            return Tool_1.EventHandled.No;
        if (this.currentElementCount < this.requiredElementCount && !this.wantAccuSnap)
            return Tool_1.EventHandled.No; // Can only early return if AccuSnap doesn't need to be disabled for ctrl selection...
        this.setupAndPromptForNextAction(); // Enable/disable auto-locate, AccuSnap, update prompts...
        return Tool_1.EventHandled.Yes;
    }
    /** Allow reset to cycle between elements identified for overlapping the locate circle.
     * Advances to next pre-located hit from [[AccuSnap.aSnapHits]] or changes last accepted hit to next hit from [[ElementLocateManger.hitList]].
     * @returns EventHandled.Yes if onReinitialize was called to restart or exit tool.
     */
    async chooseNextHit(ev) {
        if (this.isSelectionSetModify) {
            await this.onReinitialize();
            return Tool_1.EventHandled.Yes;
        }
        if (0 !== this.currentElementCount) {
            let autoLocateChooseNext = false;
            if (this.wantAdditionalElements) {
                const lastHit = IModelApp_1.IModelApp.locateManager.currHit;
                const autoHit = IModelApp_1.IModelApp.accuSnap.currHit;
                // Choose next using auto-locate or normal locate?
                if (undefined !== autoHit && (undefined === lastHit || !autoHit.isSameHit(lastHit)))
                    autoLocateChooseNext = true;
            }
            if (!autoLocateChooseNext) {
                await this.doLocate(ev, false);
                if (this.agenda.isEmpty) {
                    await this.onReinitialize();
                    return Tool_1.EventHandled.Yes;
                }
                this.setupAndPromptForNextAction();
                return Tool_1.EventHandled.No;
            }
        }
        await IModelApp_1.IModelApp.accuSnap.resetButton();
        return Tool_1.EventHandled.No;
    }
    /** Orchestrates updating the internal state of the tool on a reset button event.
     * @returns EventHandled.Yes if onReinitialize was called to restart or exit tool.
     */
    async processResetButton(ev) {
        if (ev.isDown)
            return Tool_1.EventHandled.No;
        return this.chooseNextHit(ev);
    }
    async onResetButtonUp(ev) {
        return this.processResetButton(ev);
    }
    async onResetButtonDown(ev) {
        return this.processResetButton(ev);
    }
    /** Collect element input until tool has a sufficient number to complete. */
    async gatherElements(ev) {
        if (this.isSelectionSetModify) {
            if (this.agenda.isEmpty && !await this.buildSelectionSetAgenda(this.iModel.selectionSet)) {
                await this.onReinitialize();
                return Tool_1.EventHandled.Yes;
            }
        }
        if (this.wantAdditionalElements) {
            if (ev.isDown && ev.isControlKey && this.allowDragSelect) {
                this._processDataButtonUp = true;
                return Tool_1.EventHandled.No; // Defer locate to up event so that box select can be initiated while over an element...
            }
            if (!await this.doLocate(ev, true))
                return Tool_1.EventHandled.No;
            if (this.wantAdditionalElements) {
                this.setupAndPromptForNextAction();
                return Tool_1.EventHandled.No; // Continue identifying elements...
            }
        }
        return undefined;
    }
    /** Collect point input until tool has a sufficient number to complete. */
    async gatherInput(ev) {
        if (undefined === this.anchorPoint) {
            this.anchorPoint = ev.point.clone();
            const hints = new AccuDraw_1.AccuDrawHintBuilder();
            hints.setOriginAlways = true;
            hints.setOrigin(this.anchorPoint);
            hints.sendHints(false); // Default activation on start of dynamics...
        }
        if (!this.wantProcessAgenda(ev)) {
            if (this.wantDynamics)
                await this.initAgendaDynamics();
            this.setupAndPromptForNextAction();
            return Tool_1.EventHandled.No;
        }
        return undefined;
    }
    /** Orchestrates advancing the internal state of the tool on a data button event.
     * - Collect elements: Add to the element agenda until no additional elements are requested.
     * - Gather input: Initiates element dynamics and accepts additional points as required.
     * - Complete operation: Process agenda entries, restart or exit tool.
     * @returns EventHandled.Yes if onReinitialize was called to restart or exit tool.
     */
    async processDataButton(ev) {
        if (!ev.isDown && !this._processDataButtonUp)
            return Tool_1.EventHandled.No;
        this._processDataButtonUp = false;
        const elementStatus = await this.gatherElements(ev);
        if (undefined !== elementStatus)
            return elementStatus;
        const inputStatus = await this.gatherInput(ev);
        if (undefined !== inputStatus)
            return inputStatus;
        await this.processAgenda(ev);
        await this.onProcessComplete();
        return Tool_1.EventHandled.Yes;
    }
    async onDataButtonUp(ev) {
        return this.processDataButton(ev);
    }
    async onDataButtonDown(ev) {
        return this.processDataButton(ev);
    }
    async initAgendaDynamics() {
        if (this.isDynamicsStarted)
            return false;
        this.beginDynamics();
        return true;
    }
    /** Sub-classes can override to be notified of [[ElementSetTool.agenda]] changes by other methods.
     * @note Tools should not modify [[ElementSetTool.agenda]] in this method, it should merely serve as a convenient place
     * to update information, such as element graphics once dynamics has started, ex. [[ElementSetTool.chooseNextHit]].
    */
    async onAgendaModified() { }
    /** Sub-classes can override to continue with current [[ElementSetTool.agenda]] or restart after processing has completed. */
    async onProcessComplete() { return this.onReinitialize(); }
    /** Sub-classes that return false for [[ElementSetTool.requireAcceptForSelectionSetOperation]] should override to apply the tool operation to [[ElementSetTool.agenda]]. */
    async processAgendaImmediate() { }
    /** Sub-classes that require and use the accept point should override to apply the tool operation to [[ElementSetTool.agenda]].
     * @note Not called for [[ElementSetTool.isSelectionSetModify]] when [[ElementSetTool.requireAcceptForSelectionSetOperation]] is false.
     */
    async processAgenda(_ev) { return this.processAgendaImmediate(); }
    /** Support either [[ElementSetTool.requireAcceptForSelectionSetOperation]] or [[ElementSetTool.requireAcceptForSelectionSetDynamics]] returning false. */
    async doProcessSelectionSetImmediate() {
        const buildImmediate = (!this.requireAcceptForSelectionSetOperation || (this.wantDynamics && !this.requireAcceptForSelectionSetDynamics));
        if (!buildImmediate)
            return;
        if (!await this.buildSelectionSetAgenda(this.iModel.selectionSet))
            return this.onReinitialize();
        if (!this.requireAcceptForSelectionSetOperation) {
            await this.processAgendaImmediate();
            await this.onProcessComplete();
        }
        else {
            await this.initAgendaDynamics();
        }
    }
    /** Setup initial element state, prompts, check [[SelectionSet]], etc. */
    async onPostInstall() {
        await super.onPostInstall();
        this.setPreferredElementSource();
        this.setupAndPromptForNextAction();
        if (this.isSelectionSetModify)
            await this.doProcessSelectionSetImmediate();
    }
    /** Make sure elements from [[ElementSetTool.agenda]] that aren't also from [[SelectionSet]] aren't left hilited. */
    async onCleanup() {
        await super.onCleanup();
        if (undefined !== this._agenda)
            this._agenda.clear();
    }
    /** Exit and start default tool when [[ElementSetTool.isSelectionSetModify]] is true to allow [[SelectionSet]] to be modified,
     * or call [[PrimitiveTool.onRestartTool]] to install a new tool instance.
     */
    async onReinitialize() {
        if (this.isSelectionSetModify)
            return this.exitTool();
        return this.onRestartTool();
    }
    /** Restore tool assistance after no longer being suspended by either a [[ViewTool]] or [[InputCollector]]. */
    async onUnsuspend() {
        this.provideToolAssistance();
    }
    get shouldEnableLocate() { return this.isSelectByPoints ? false : this.wantAdditionalElements; }
    get shouldEnableSnap() { return this.isSelectByPoints ? false : (this.wantAccuSnap && (!this.isControlDown || !this.controlKeyContinuesSelection || !this.wantAdditionalElements)); }
    /** Setup auto-locate, AccuSnap, AccuDraw, and supply tool assistance. */
    setupAndPromptForNextAction() {
        this.initLocateElements(this.shouldEnableLocate, this.shouldEnableSnap);
        this.provideToolAssistance();
    }
    /** Sub-classes should override to provide tool specific instructions. */
    provideToolAssistance(mainInstrText, additionalInstr) {
        let mainMsg;
        let leftMsg;
        let rightMsg;
        let addDragInstr = false;
        if (this.isSelectionSetModify) {
            if (this.wantAdditionalInput) {
                mainMsg = "ElementSet.Prompts.IdentifyPoint";
                leftMsg = "ElementSet.Inputs.AcceptPoint";
                rightMsg = "ElementSet.Inputs.Exit";
            }
            else if (0 === this.currentElementCount) {
                mainMsg = "ElementSet.Prompts.ConfirmSelection";
                leftMsg = "ElementSet.Inputs.AcceptSelection";
                rightMsg = "ElementSet.Inputs.RejectSelection";
            }
            else {
                mainMsg = "ElementSet.Inputs.Complete";
                leftMsg = "ElementSet.Inputs.Accept";
                rightMsg = "ElementSet.Inputs.Exit";
            }
        }
        else {
            if (this.isSelectByPoints) {
                mainMsg = "ElementSet.Prompts.OppositeCorner";
                leftMsg = "ElementSet.Inputs.BoxCorners";
                rightMsg = "ElementSet.Inputs.CrossingLine";
            }
            else if (this.wantAdditionalElements) {
                mainMsg = "ElementSet.Prompts.IdentifyElement";
                leftMsg = "ElementSet.Inputs.AcceptElement";
                rightMsg = "ElementSet.Inputs.Cancel";
                addDragInstr = this.allowDragSelect;
            }
            else if (this.wantAdditionalInput) {
                mainMsg = "ElementSet.Prompts.IdentifyPoint";
                leftMsg = "ElementSet.Inputs.AcceptPoint";
                rightMsg = "ElementSet.Inputs.Cancel";
            }
            else {
                mainMsg = "ElementSet.Inputs.Complete";
                leftMsg = "ElementSet.Inputs.Accept";
                rightMsg = "ElementSet.Inputs.Restart";
            }
        }
        const mouseInstructions = [];
        const touchInstructions = [];
        if (!ToolAssistance_1.ToolAssistance.createTouchCursorInstructions(touchInstructions))
            touchInstructions.push(ToolAssistance_1.ToolAssistance.createInstruction(ToolAssistance_1.ToolAssistanceImage.OneTouchTap, Tool_1.CoreTools.translate(leftMsg), false, ToolAssistance_1.ToolAssistanceInputMethod.Touch));
        mouseInstructions.push(ToolAssistance_1.ToolAssistance.createInstruction(ToolAssistance_1.ToolAssistanceImage.LeftClick, Tool_1.CoreTools.translate(leftMsg), false, ToolAssistance_1.ToolAssistanceInputMethod.Mouse));
        touchInstructions.push(ToolAssistance_1.ToolAssistance.createInstruction(ToolAssistance_1.ToolAssistanceImage.TwoTouchTap, Tool_1.CoreTools.translate(rightMsg), false, ToolAssistance_1.ToolAssistanceInputMethod.Touch));
        mouseInstructions.push(ToolAssistance_1.ToolAssistance.createInstruction(ToolAssistance_1.ToolAssistanceImage.RightClick, Tool_1.CoreTools.translate(rightMsg), false, ToolAssistance_1.ToolAssistanceInputMethod.Mouse));
        if (addDragInstr) {
            mouseInstructions.push(ToolAssistance_1.ToolAssistance.createModifierKeyInstruction(ToolAssistance_1.ToolAssistance.ctrlKey, ToolAssistance_1.ToolAssistanceImage.LeftClickDrag, Tool_1.CoreTools.translate("ElementSet.Inputs.BoxCorners"), false, ToolAssistance_1.ToolAssistanceInputMethod.Mouse));
            mouseInstructions.push(ToolAssistance_1.ToolAssistance.createModifierKeyInstruction(ToolAssistance_1.ToolAssistance.ctrlKey, ToolAssistance_1.ToolAssistanceImage.RightClickDrag, Tool_1.CoreTools.translate("ElementSet.Inputs.CrossingLine"), false, ToolAssistance_1.ToolAssistanceInputMethod.Mouse));
            mouseInstructions.push(ToolAssistance_1.ToolAssistance.createModifierKeyInstruction(ToolAssistance_1.ToolAssistance.shiftKey, ToolAssistance_1.ToolAssistanceImage.LeftClickDrag, Tool_1.CoreTools.translate("ElementSet.Inputs.OverlapSelection"), false, ToolAssistance_1.ToolAssistanceInputMethod.Mouse));
        }
        if (undefined !== additionalInstr) {
            for (const instr of additionalInstr) {
                if (ToolAssistance_1.ToolAssistanceInputMethod.Touch === instr.inputMethod)
                    touchInstructions.push(instr);
                else
                    mouseInstructions.push(instr);
            }
        }
        const sections = [];
        sections.push(ToolAssistance_1.ToolAssistance.createSection(mouseInstructions, ToolAssistance_1.ToolAssistance.inputsLabel));
        sections.push(ToolAssistance_1.ToolAssistance.createSection(touchInstructions, ToolAssistance_1.ToolAssistance.inputsLabel));
        const mainInstruction = ToolAssistance_1.ToolAssistance.createInstruction(this.iconSpec, undefined !== mainInstrText ? mainInstrText : Tool_1.CoreTools.translate(mainMsg));
        const instructions = ToolAssistance_1.ToolAssistance.createInstructions(mainInstruction, sections);
        IModelApp_1.IModelApp.notifications.setToolAssistance(instructions);
    }
}
exports.ElementSetTool = ElementSetTool;
//# sourceMappingURL=ElementSetTool.js.map