UNPKG

@itwin/core-frontend

Version:
581 lines • 22.3 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. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); exports.SelectionSet = exports.HiliteSet = exports.SelectionSetEventType = void 0; /** @packageDocumentation * @module SelectionSet */ const core_bentley_1 = require("@itwin/core-bentley"); const IModelApp_1 = require("./IModelApp"); /** Identifies the type of changes made to the [[SelectionSet]] to produce a [[SelectionSetEvent]]. * @public * @extensions */ var SelectionSetEventType; (function (SelectionSetEventType) { /** Ids have been added to the set. */ SelectionSetEventType[SelectionSetEventType["Add"] = 0] = "Add"; /** Ids have been removed from the set. */ SelectionSetEventType[SelectionSetEventType["Remove"] = 1] = "Remove"; /** Some ids have been added to the set and others have been removed. */ SelectionSetEventType[SelectionSetEventType["Replace"] = 2] = "Replace"; /** All ids have been removed from the set. */ SelectionSetEventType[SelectionSetEventType["Clear"] = 3] = "Clear"; })(SelectionSetEventType || (exports.SelectionSetEventType = SelectionSetEventType = {})); /** Holds a set of hilited entities and makes any changes to the set by passing the change * function to given `change` callback. * @internal */ class HilitedIds extends core_bentley_1.Id64.Uint32Set { _change; constructor(_change) { super(); this._change = _change; } add(low, high) { this._change(() => super.add(low, high)); } delete(low, high) { this._change(() => super.delete(low, high)); } clear() { this._change(() => super.clear()); } addIds(ids) { this._change(() => super.addIds(ids)); } deleteIds(ids) { this._change(() => super.deleteIds(ids)); } } /** A set of *hilited* elements for an [[IModelConnection]], by element id. * Hilited elements are displayed with a customizable hilite effect within a [[Viewport]]. * The set exposes 3 types of elements in 3 separate collections: [GeometricElement]($backend), [GeometricModel]($backend), and [SubCategory]($backend). * The [[models]] and [[subcategories]] can be hilited independently or as an intersection of the two sets, as specified by [[modelSubCategoryMode]]. * * Technically, the hilite effect is applied to [Feature]($common)s, not [Element]($backend)s. An element's geometry stream can contain multiple * features belonging to different subcategories. * * Because Javascript lacks efficient support for 64-bit integers, the Ids are stored as pairs of 32-bit integers via [Id64.Uint32Set]($bentley). * * @note Typically, elements are hilited by virtue of their presence in the IModelConnection's [[SelectionSet]]. The HiliteSet allows additional * elements to be displayed with the hilite effect without adding them to the [[SelectionSet]]. If you add elements to the HiliteSet directly, you * are also responsible for removing them as appropriate. * @see [[IModelConnection.hilited]] for the HiliteSet associated with an iModel. * @see [Hilite.Settings]($common) for customization of the hilite effect. * @public * @extensions */ class HiliteSet { iModel; #mode = "union"; #selectionChangesListener; #changing = false; /** The set of hilited elements. */ elements; /** The set of hilited subcategories. * @see [[modelSubCategoryMode]] to control how this set interacts with the set of hilited [[models]]. * @see [[IModelConnection.Categories]] to obtain the set of subcategories associated with one or more [Category]($backend)'s. */ subcategories; /** The set of hilited [[GeometricModelState]]s. * @see [[modelSubCategoryMode]] to control how this set interacts with the set of hilited [[subcategories]]. */ models; /** Controls how the sets of hilited [[models]] and [[subcategories]] interact with one another. * By default they are treated as a union: a [Feature]($common) is hilited if either its model **or** its subcategory is hilited. * This can be changed to an intersection such that a [Feature]($common) is hilited only if both its model **and** subcategory are hilited. * @note The sets of hilited models and subcategories are independent of the set of hilited [[elements]] - an element whose Id is present in * [[elements]] is always hilited regardless of its model or subcategories. */ get modelSubCategoryMode() { return this.#mode; } set modelSubCategoryMode(mode) { if (mode === this.#mode) { return; } this.onModelSubCategoryModeChanged.raiseEvent(mode); this.#mode = mode; } /** Event raised just before changing the value of [[modelSubCategoryMode]]. */ onModelSubCategoryModeChanged = new core_bentley_1.BeEvent(); /** Construct a HiliteSet * @param iModel The iModel containing the entities to be hilited. * @param syncWithSelectionSet If true, the hilite set contents will be synchronized with those in the `iModel`'s [[SelectionSet]]. */ constructor(iModel, syncWithSelectionSet = true) { this.iModel = iModel; this.elements = new HilitedIds((func) => this.#change(func)); this.subcategories = new HilitedIds((func) => this.#change(func)); this.models = new HilitedIds((func) => this.#change(func)); this.wantSyncWithSelectionSet = syncWithSelectionSet; } /** Control whether the hilite set will be synchronized with the contents of the [[SelectionSet]]. * By default they are synchronized. Applications that override this take responsibility for managing the set of hilited entities. * When turning synchronization off, the contents of the HiliteSet will remain unchanged. * When turning synchronization on, the current contents of the HiliteSet will be preserved, and the contents of the selection set will be added to them. */ get wantSyncWithSelectionSet() { return !!this.#selectionChangesListener; } set wantSyncWithSelectionSet(want) { if (want === this.wantSyncWithSelectionSet) { return; } if (want) { const set = this.iModel.selectionSet; this.#selectionChangesListener = set.onChanged.addListener((ev) => this.#processSelectionSetEvent(ev)); this.add(set.active); } else { this.#selectionChangesListener(); this.#selectionChangesListener = undefined; } } #onChanged() { if (!this.#changing) { IModelApp_1.IModelApp.viewManager.onSelectionSetChanged(this.iModel); } } #change(func) { const changing = this.#changing; this.#changing = true; try { func(); } finally { this.#changing = changing; } this.#onChanged(); } #processSelectionSetEvent(ev) { switch (ev.type) { case SelectionSetEventType.Add: return this.add(ev.additions); case SelectionSetEventType.Replace: return this.#change(() => { this.add(ev.additions); this.remove(ev.removals); }); case SelectionSetEventType.Remove: case SelectionSetEventType.Clear: return this.remove(ev.removals); } } /** Adds a collection of geometric element, model and subcategory ids to this hilite set. */ add(additions) { this.#change(() => { additions.elements && this.elements.addIds(additions.elements); additions.models && this.models.addIds(additions.models); additions.subcategories && this.subcategories.addIds(additions.subcategories); }); } /** Removes a collection of geometric element, model and subcategory ids from this hilite set. */ remove(removals) { this.#change(() => { removals.elements && this.elements.deleteIds(removals.elements); removals.models && this.models.deleteIds(removals.models); removals.subcategories && this.subcategories.deleteIds(removals.subcategories); }); } /** Replaces ids currently in the hilite set with the given collection. */ replace(ids) { this.#change(() => { this.clear(); this.add(ids); }); } /** Remove all elements from the hilited set. */ clear() { this.#change(() => { this.elements.clear(); this.models.clear(); this.subcategories.clear(); }); } /** Returns true if nothing is hilited. */ get isEmpty() { return this.elements.isEmpty && this.subcategories.isEmpty && this.models.isEmpty; } /** Toggle the hilited state of one or more elements. * @param arg the ID(s) of the elements whose state is to be toggled. * @param onOff True to add the elements to the hilited set, false to remove them. * @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [[add]], [[remove]], [[replace]] instead. */ setHilite(arg, onOff) { if (onOff) { this.add({ elements: arg }); } else { this.remove({ elements: arg }); } } } exports.HiliteSet = HiliteSet; /** A set of *currently selected* geometric elements, models and subcategories for an `IModelConnection`. * Generally, selected elements are displayed with a customizable hilite effect within a [[Viewport]], see [[HiliteSet]]. * @see [Hilite.Settings]($common) for customization of the hilite effect. * @public * @extensions */ class SelectionSet { iModel; #selection; /** The IDs of the selected elements. * @note Do not modify this set directly. Instead, use methods like [[SelectionSet.add]]. */ get elements() { return this.#selection.elements; } /** The IDs of the selected models. * @note Do not modify this set directly. Instead, use methods like [[SelectionSet.add]]. */ get models() { return this.#selection.models; } /** The IDs of the selected subcategories. * @note Do not modify this set directly. Instead, use methods like [[SelectionSet.add]]. */ get subcategories() { return this.#selection.subcategories; } /** Get the active selection as a collection of geometric element, model and subcategory ids. * @note Do not modify the sets in returned collection directly. Instead, use methods like [[SelectionSet.add]]. */ get active() { return { ...this.#selection }; } /** Called whenever ids are added or removed from this `SelectionSet` */ onChanged = new core_bentley_1.BeEvent(); constructor(iModel) { this.iModel = iModel; this.#selection = { elements: new Set(), models: new Set(), subcategories: new Set(), }; } #sendChangedEvent(ev) { IModelApp_1.IModelApp.viewManager.onSelectionSetChanged(this.iModel); this.onChanged.raiseEvent(ev); } /** Get the number of entries in this selection set. */ get size() { return this.elements.size + this.models.size + this.subcategories.size; } /** Check whether there are any ids in this selection set. */ get isActive() { return this.elements.size > 0 || this.models.size > 0 || this.subcategories.size > 0; } /** Return true if elemId is in this `SelectionSet`. * @see [[isSelected]] * @deprecated in 5.0 - will not be removed until after 2026-06-13. Use `SelectionSet.elements.has(elemId)` instead. */ has(elemId) { return !!elemId && this.elements.has(elemId); } /** Query whether an Id is in the selection set. * @see [[has]] * @deprecated in 5.0 - will not be removed until after 2026-06-13. Use `SelectionSet.elements.has(elemId)` instead. */ isSelected(elemId) { return !!elemId && this.elements.has(elemId); } /** Clear current selection set. * @note raises the [[onChanged]] event with [[SelectionSetEventType.Clear]]. */ emptyAll() { if (!this.isActive) { return; } const removals = this.#selection; this.#selection = { elements: new Set(), models: new Set(), subcategories: new Set(), }; this.#sendChangedEvent({ set: this, type: SelectionSetEventType.Clear, removals, removed: removals.elements }); } /** * Add one or more Ids to the current selection set. * @param elem The set of Ids to add. * @returns true if any elements were added. */ add(adds) { return !!this.#add(adds); } #add(adds, sendEvent = true) { const oldSize = this.size; const additions = {}; forEachSelectableType({ ids: adds, elements: (elementIds) => addIds({ target: this.#selection.elements, ids: elementIds, onAdd: (id) => (additions.elements ??= []).push(id), }), models: (modelIds) => addIds({ target: this.#selection.models, ids: modelIds, onAdd: (id) => (additions.models ??= []).push(id), }), subcategories: (subcategoryIds) => addIds({ target: this.#selection.subcategories, ids: subcategoryIds, onAdd: (id) => (additions.subcategories ??= []).push(id), }), }); const changed = oldSize !== this.size; if (!changed) { return undefined; } if (sendEvent) { this.#sendChangedEvent({ type: SelectionSetEventType.Add, set: this, additions, added: additions.elements ?? [], }); } return additions; } /** * Remove one or more Ids from the current selection set. * @param elem The set of Ids to remove. * @returns true if any elements were removed. */ remove(removes) { return !!this.#remove(removes); } #remove(removes, sendEvent = true) { const oldSize = this.size; const removals = {}; forEachSelectableType({ ids: removes, elements: (elementIds) => removeIds({ target: this.#selection.elements, ids: elementIds, onRemove: (id) => (removals.elements ??= []).push(id), }), models: (modelIds) => removeIds({ target: this.#selection.models, ids: modelIds, onRemove: (id) => (removals.models ??= []).push(id), }), subcategories: (subcategoryIds) => removeIds({ target: this.#selection.subcategories, ids: subcategoryIds, onRemove: (id) => (removals.subcategories ??= []).push(id), }), }); const changed = oldSize !== this.size; if (!changed) { return undefined; } if (sendEvent) { this.#sendChangedEvent({ type: SelectionSetEventType.Remove, set: this, removals, removed: removals.elements ?? [], }); } return removals; } /** * Add one set of Ids, and remove another set of Ids. Any Ids that are in both sets are removed. * @returns True if any Ids were either added or removed. */ addAndRemove(adds, removes) { const additions = this.#add(adds, false); const removals = this.#remove(removes, false); const addedElements = additions?.elements ?? []; const removedElements = removals?.elements ?? []; if (additions && removals) { this.#sendChangedEvent({ type: SelectionSetEventType.Replace, set: this, additions, added: addedElements, removals, removed: removedElements, }); } else if (additions) { this.#sendChangedEvent({ type: SelectionSetEventType.Add, set: this, additions, added: addedElements, }); } else if (removals) { this.#sendChangedEvent({ type: SelectionSetEventType.Remove, set: this, removals, removed: removedElements, }); } return !!additions || !!removals; } /** Invert the state of a set of Ids in the `SelectionSet` */ invert(ids) { const adds = {}; const removes = {}; forEachSelectableType({ ids, elements: (elementIds) => { for (const id of core_bentley_1.Id64.iterable(elementIds)) { ((this.elements.has(id) ? removes : adds).elements ??= new Set()).add(id); } }, models: (modelIds) => { for (const id of core_bentley_1.Id64.iterable(modelIds)) { ((this.models.has(id) ? removes : adds).models ??= new Set()).add(id); } }, subcategories: (subcategoryIds) => { for (const id of core_bentley_1.Id64.iterable(subcategoryIds)) { ((this.subcategories.has(id) ? removes : adds).subcategories ??= new Set()).add(id); } }, }); return this.addAndRemove(adds, removes); } /** Change selection set to be the supplied set of Ids. */ replace(ids) { if (areEqual(this.#selection, ids)) { return false; } const previousSelection = this.#selection; this.#selection = { elements: new Set(), models: new Set(), subcategories: new Set(), }; this.#add(ids, false); const additions = {}; const removals = {}; forEachSelectableType({ ids: this.#selection, elements: (elementIds) => { removeIds({ target: previousSelection.elements, ids: elementIds, onNotFound: (id) => (additions.elements ??= new Set()).add(id), }); if (previousSelection.elements.size > 0) { removals.elements = previousSelection.elements; } }, models: (modelIds) => { removeIds({ target: previousSelection.models, ids: modelIds, onNotFound: (id) => (additions.models ??= new Set()).add(id), }); if (previousSelection.models.size > 0) { removals.models = previousSelection.models; } }, subcategories: (subcategoryIds) => { removeIds({ target: previousSelection.subcategories, ids: subcategoryIds, onNotFound: (id) => (additions.subcategories ??= new Set()).add(id), }); if (previousSelection.subcategories.size > 0) { removals.subcategories = previousSelection.subcategories; } }, }); this.#sendChangedEvent({ type: SelectionSetEventType.Replace, set: this, additions, added: additions.elements ?? [], removals, removed: removals.elements ?? [], }); return true; } } exports.SelectionSet = SelectionSet; function forEachSelectableType({ ids, elements, models, subcategories, }) { if (typeof ids === "string" || Array.isArray(ids) || ids instanceof Set) { elements(ids); return { elements: ids }; } elements(ids.elements ?? []); models(ids.models ?? []); subcategories(ids.subcategories ?? []); return ids; } function areEqual(lhs, rhs) { let result = true; forEachSelectableType({ ids: rhs, elements: (elementIds) => { if (result && !areIdsEqual(lhs.elements, elementIds)) { result = false; } }, models: (modelIds) => { if (result && !areIdsEqual(lhs.models, modelIds)) { result = false; } }, subcategories: (subcategoryIds) => { if (result && !areIdsEqual(lhs.subcategories, subcategoryIds)) { result = false; } }, }); return result; } function areIdsEqual(lhs, rhs) { // Size is unreliable if input can contain duplicates... if (Array.isArray(rhs)) { rhs = core_bentley_1.Id64.toIdSet(rhs); } if (lhs.size !== core_bentley_1.Id64.sizeOf(rhs)) { return false; } for (const id of core_bentley_1.Id64.iterable(rhs)) { if (!lhs.has(id)) { return false; } } return true; } function addIds({ target, ids, onAdd }) { let size = target.size; for (const id of core_bentley_1.Id64.iterable(ids)) { target.add(id); const newSize = target.size; if (newSize !== size) { onAdd?.(id); } size = newSize; } } function removeIds({ target, ids, onRemove, onNotFound, }) { let size = target.size; for (const id of core_bentley_1.Id64.iterable(ids)) { target.delete(id); const newSize = target.size; if (newSize !== size) { onRemove?.(id); } else { onNotFound?.(id); } size = newSize; } } //# sourceMappingURL=SelectionSet.js.map