Security News
Input Validation Vulnerabilities Dominate MITRE's 2024 CWE Top 25 List
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
@itwin/unified-selection
Advanced tools
Package for managing unified selection in iTwin.js applications.
Copyright © Bentley Systems, Incorporated. All rights reserved. See LICENSE.md for license terms and full copyright notice.
The @itwin/unified-selection
package provides API for managing unified selection.
The API consists of a few very basic concepts:
A Selectable
is something that can be selected and is associated with an EC instance. There are 2 types of selectables:
SelectableInstanceKey
uniquely identifies a single EC instance through a full class name and ECInstanceId.CustomSelectable
is identified by an arbitrary identifier
string and knows how to get any number of SelectableInstanceKey
associated with it.Selectables
is a container for multiple Selectable
instances. The container is structured in a way that allows to quickly find specific selectables by their identifier.
SelectionStorage
is an interface that manages Selectables
for different iModels. It allows:
The package delivers the createStorage()
function to create an instance of SelectionStorage
. Consumers are also expected to call SelectionStorage.clearStorage
whenever an iModel is closed to free up memory.
// create a global selection store (generally, somewhere in main.ts or similar)
import { createStore } from "@itwin/unified-selection";
const unifiedSelection = createStore();
// the store should to be cleaned up when iModels are closed to free up memory, e.g.:
import { IModelConnection } from "@itwin/core-frontend";
IModelConnection.onClose.addListener((imodel) => {
unifiedSelection.clearStorage(imodel.key);
});
// add a demo selection listener
import { Selectables } from "@itwin/unified-selection";
unifiedSelection.selectionChangeEvent.addListener(({ imodelKey, source, changeType, selectables }) => {
const suffix = `in ${imodelKey} iModel from ${source} component`;
const numSelectables = Selectables.size(selectables);
switch (changeType) {
case "add":
console.log(`Added ${numSelectables} items to selection ${suffix}.`);
break;
case "remove":
console.log(`Removed ${numSelectables} items from selection ${suffix}.`);
break;
case "replace":
console.log(`Replaced selection with ${numSelectables} items ${suffix}.`);
break;
case "clear":
console.log(`Cleared selection ${suffix}.`);
break;
}
});
// in some component
MyComponent.onECInstanceSelected((imodel: IModelConnection, key: { className: string; id: Id64String }) => {
unifiedSelection.addToSelection({ imodelKey: imodel.key, source: "MyComponent", selectables: [key] });
});
By default, whenever a component changes unified selection, that happens at 0th (top) selection level. And similarly, whenever a component requests current selection from the storage, by default the top selection level is used. However, there are cases when we want to have multiple levels of selection.
For example, let's say there're 3 components: A, B and C:
The behavior described above can't be achieved using just one level of selection, because as soon as selection is made in Component B, that selection would get represented in Component A, and Component B would change what it's displaying to the individual element.
That can be fixed by introducing another selection level, but before the components can be configured, here are a few key facts about selection levels:
With that in mind, the above components A, B and C can be configured as follows:
Note: hilite = highlight
@itwin/core-frontend
contains a concept called the HiliteSet. This concept is tightly related to unified selection, because, generally, we want selected elements to be highlighted in the application's graphics views. The HiliteSet
object contains IDs of 3 types of elements: GeometricModel, SubCategory and GeometricElement. On the other hand, the unified selection API allows selecting other kinds of elements too, so IDs of these elements need to be mapped to the supported ones. The rules are as follows:
BisCore.Subject
return IDs of all geometric models that are recursively under that Subject,BisCore.Model
just return its ID,BisCore.PhysicalPartition
just return ID of a model that models it,BisCore.Category
return IDs of all its SubCategories,BisCore.SubCategory
just return its ID,BisCore.GeometricElement
return ID of its own and all its child elements recursively.So for example when unified selection contains a Subject, the hilite set for it will contain all models under that Subject, it's child Subjects, their child Subjects, etc. Given such hilite set, the viewport component hilites all elements in those models.
The @itwin/unified-selection
package delivers APIs for creating a HiliteSet
or retrieving it for current selection in a SelectionStorage
:
// Components may want to get a hilite set for arbitrary set of Selectables - use `createHiliteSetProvider` for that.
import { IModelConnection } from "@itwin/core-frontend";
import { createECSqlQueryExecutor, createECSchemaProvider } from "@itwin/presentation-core-interop";
import { createHiliteSetProvider } from "@itwin/unified-selection";
const hiliteProvider = createHiliteSetProvider({
imodelAccess: {
...createECSchemaProvider(imodel),
...createECSqlQueryExecutor(imodel),
},
});
const hiliteSet = await hiliteProvider.getHiliteSet({ selectables });
// Some others may want to get a hilite set for _current_ selection in storage - use `createCachingHiliteSetProvider` for that. It's
// recommended to keep a single instance of this provider per application as it caches hilite sets per each iModel's selection.
import { createCachingHiliteSetProvider } from "@itwin/unified-selection";
const selectionHiliteProvider = createCachingHiliteSetProvider({
selectionStorage,
imodelProvider: (imodelKey: string) => getIModelByKey(imodelKey),
});
const selectionHiliteSet = await selectionHiliteProvider.getHiliteSet({ imodel.key });
// The caching provider registers a selection change listener and should be disposed, in case its lifetime
// is shorter than that of `SelectionStorage`, to unregister the listener.
selectionHiliteProvider.dispose();
Selection scopes allow decoupling of what gets picked and what gets selected. Without selection scopes, whenever a user picks an element in the viewport, its ID goes straight into unified selection storage. With selection scopes we can modify that and add something different. The input to the selection scopes' processor is a query executor, element IDs, and the scope to apply, and the output is an iterator of SelectableInstanceKey
. We get the input when the user picks some elements in the viewport, run that through the selection scope processor and put the output into unified selection storage.
Here are the scopes we support at the moment:
element
- return key of selected element.category
- return key of geometric element's category.model
- return key of element's model.functional
- return key of element's related functional element. For BisCore.GeometricElement3d
the related functional element is found using the Functional.PhysicalElementFulfillsFunction
relationship. For BisCore.GeometricElement2d
the nearest functional element is searched for using the Functional.DrawingGraphicRepresentsFunctionalElement
relationship - if the given element has a related functional element, it will be returned, otherwise the element's parent will be checked and if it also does not have a related functional, then the parent of the parent will be checked until no more ancestors can be traversed or a functional element is found. Regardless whether it is an BisCore.GeometricElement2d
or BisCore.GeometricElement3d
if no functional element is found then the element itself will be returned.The @itwin/unified-selection
package delivers a computeSelection
function for computing which elements should be added into unified selection storage based on the given element ID's and a specified selection scope:
import { computeSelection } from "@itwin/unified-selection";
import { IModelConnection } from "@itwin/core-frontend";
import { createECSqlQueryExecutor } from "@itwin/presentation-core-interop";
const queryExecutor = createECSqlQueryExecutor(imodel);
const selection = computeSelection({ queryExecutor, elementIds, scope: "element" });
element
and functional
scopes additionally allow selecting assembly elements by specifying the ancestorLevel
property in the selection scope argument of computeSelection
function. The ancestorLevel
property specifies how far "up" we should walk to find the target element. When not specified or 0
, the target element matches the request element. When set to 1
, the target element matches the direct parent element. When 2
, the target element is the parent of the parent element, and so on. In all situations when this is > 0
, we're not walking further than the last existing element, for example, when ancestorLevel = 1
(direct parent element is requested), but the request element doesn't have a parent, the request element is returned as the result. A negative value would result in the top-most element to be returned.
For the functional
scope, the ancestorLevel
property is used as follows: if an element is a BisCore.GeometricElement3d
element, its ancestor is selected based on the given ancestorLevel
the same as with non-functional elements, and then the resulting element's related functional element will be returned (using the Functional.PhysicalElementFulfillsFunction
relationship), or if it does not have one, then the resulting element will be returned. For BisCore.GeometricElement2d
elements, the nearest related functional element is found in the same way it is done when the ancestorLevel
property is not provided, and then the ancestor of that element is returned (based on the provided value of ancestorLevel
).
import { computeSelection } from "@itwin/unified-selection";
import { IModelConnection } from "@itwin/core-frontend";
import { createECSqlQueryExecutor } from "@itwin/presentation-core-interop";
const queryExecutor = createECSqlQueryExecutor(imodel);
// Returns the parent element, or the element itself if it does not have a parent, for each element specified in `elementIds` argument.
const selection = computeSelection({ queryExecutor, elementIds, scope: { id: "element", ancestorLevel: 1 } });
The @itwin/unified-selection
package delivers a enableUnifiedSelectionSyncWithIModel
function to enable selection synchronization between an iModel and a SelectionStorage
. When called, it returns a cleanup function that should be used to disable the synchronization. There should only be one active synchronization between a single iModel and a SelectionStorage
at a given time. For example, this function could be used inside a useEffect
hook in a component that holds an iModel:
import { createECSqlQueryExecutor, createECSchemaProvider } from "@itwin/presentation-core-interop";
useEffect(() => {
return enableUnifiedSelectionSyncWithIModel({
imodelAccess: {
...createECSqlQueryExecutor(imodel),
...createECSchemaProvider(imodel),
key: imodel.key,
hiliteSet: imodel.hilited,
selectionSet: imodel.selectionSet,
},
selectionStorage,
activeScopeProvider: () => "element",
});
}, [imodel]);
FAQs
Package for managing unified selection in iTwin.js applications.
The npm package @itwin/unified-selection receives a total of 5,271 weekly downloads. As such, @itwin/unified-selection popularity was classified as popular.
We found that @itwin/unified-selection demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.
Research
Security News
A threat actor's playbook for exploiting the npm ecosystem was exposed on the dark web, detailing how to build a blockchain-powered botnet.