@itwin/core-frontend
Version:
iTwin.js frontend components
581 lines • 22.3 kB
JavaScript
"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
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