-
Notifications
You must be signed in to change notification settings - Fork 208
/
SelectionSet.ts
369 lines (312 loc) · 12.6 KB
/
SelectionSet.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
/*---------------------------------------------------------------------------------------------
* Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved.
* Licensed under the MIT License. See LICENSE.md in the project root for license terms.
*--------------------------------------------------------------------------------------------*/
/** @module SelectionSet */
import { BeEvent, Id64String, Id64, Id64Arg } from "@bentley/bentleyjs-core";
import { IModelConnection } from "./IModelConnection";
import { IModelApp } from "./IModelApp";
/** Identifies the type of changes made to the [[SelectionSet]] to produce a [[SelectionSetEvent]].
* @public
*/
export enum SelectionSetEventType {
/** Elements have been added to the set. */
Add,
/** Elements have been removed from the set. */
Remove,
/** Some elements have been added to the set and others have been removed. */
Replace,
/** All elements are about to be removed from the set. */
Clear,
}
/** Passed to [[SelectionSet.onChanged]] event listeners when elements are added to the selection set.
* @public
*/
export interface SelectAddEvent {
type: SelectionSetEventType.Add;
/** The Ids of the elements added to the set. */
added: Id64Arg;
/** The affected SelectionSet. */
set: SelectionSet;
}
/** Passed to [[SelectionSet.onChanged]] event listeners when elements are removed from the selection set.
* @public
*/
export interface SelectRemoveEvent {
/** The type of operation that produced this event. */
type: SelectionSetEventType.Remove | SelectionSetEventType.Clear;
/** The element Ids removed from the set. */
removed: Id64Arg;
/** The affected SelectionSet. */
set: SelectionSet;
}
/** Passed to [[SelectionSet.onChanged]] event listeners when elements are simultaneously added to and removed from the selection set.
* @public
*/
export interface SelectReplaceEvent {
type: SelectionSetEventType.Replace;
/** The element Ids added to the set. */
added: Id64Arg;
/** The element Ids removed from the set. */
removed: Id64Arg;
/** The affected SelectionSet. */
set: SelectionSet;
}
/** Payload sent to [[SelectionSet.onChanged]] event listeners to describe how the contents of the set have changed.
* The `type` property of the event serves as a type assertion. For example, the following code will output the added and/or removed Ids:
* ```ts
* processSelectionSetEvent(ev: SelectionSetEvent): void {
* if (SelectionSetEventType.Add === ev.type || SelectionSetEventType.Replace === ev.type)
* console.log("Added " + ev.added.size + " elements");
*
* if (SelectionSetEventType.Add !== ev.type)
* console.log("Removed " + ev.removed.size + " elements");
* }
* ```
* @public
*/
export type SelectionSetEvent = SelectAddEvent | SelectRemoveEvent | SelectReplaceEvent;
/** Tracks a set of hilited entities. When the set changes, notifies ViewManager so that symbology overrides can be updated in active Viewports.
* @internal
*/
class HilitedIds extends Id64.Uint32Set {
protected _iModel: IModelConnection;
protected _changing = false;
public constructor(iModel: IModelConnection) {
super();
this._iModel = iModel;
}
public add(low: number, high: number) {
super.add(low, high);
this.onChanged();
}
public delete(low: number, high: number) {
super.delete(low, high);
this.onChanged();
}
public clear() {
super.clear();
this.onChanged();
}
public addIds(ids: Id64Arg) {
this.change(() => super.addIds(ids));
}
public deleteIds(ids: Id64Arg) {
this.change(() => super.deleteIds(ids));
}
protected onChanged() {
if (!this._changing)
IModelApp.viewManager.onSelectionSetChanged(this._iModel);
}
protected change(func: () => void) {
const changing = this._changing;
this._changing = false;
func();
this._changing = changing;
this.onChanged();
}
}
/** Keeps the set of hilited elements in sync with the selection set.
* @internal
*/
class HilitedElementIds extends HilitedIds {
private _removeListener?: () => void;
public constructor(iModel: IModelConnection, syncWithSelectionSet = true) {
super(iModel);
this.wantSyncWithSelectionSet = syncWithSelectionSet;
}
public get wantSyncWithSelectionSet(): boolean { return undefined !== this._removeListener; }
public set wantSyncWithSelectionSet(want: boolean) {
if (want === this.wantSyncWithSelectionSet)
return;
if (want) {
const set = this._iModel.selectionSet;
this._removeListener = set.onChanged.addListener((ev) => this.change(() => this.processSelectionSetEvent(ev)));
this.processSelectionSetEvent({
set,
type: SelectionSetEventType.Add,
added: set.elements,
});
} else {
this._removeListener!();
this._removeListener = undefined;
}
}
private processSelectionSetEvent(ev: SelectionSetEvent): void {
if (SelectionSetEventType.Add !== ev.type)
this.deleteIds(ev.removed);
if (ev.type === SelectionSetEventType.Add || ev.type === SelectionSetEventType.Replace)
this.addIds(ev.added);
}
}
/** 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: geometric elements, subcategories, and geometric models.
* @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]].
* @see [Hilite.Settings]($common) for customization of the hilite effect.
* @alpha
*/
export class HiliteSet {
private readonly _elements: HilitedElementIds;
public readonly subcategories: Id64.Uint32Set;
public readonly models: Id64.Uint32Set;
public get elements(): Id64.Uint32Set { return this._elements; }
/** Construct a HiliteSet
* @param iModel The iModel containing the entities to be hilited.
* @param syncWithSelectionSet If true, the contents of the `elements` set will be synchronized with those in the `iModel`'s [[SelectionSet]].
*/
public constructor(public iModel: IModelConnection, syncWithSelectionSet = true) {
this._elements = new HilitedElementIds(iModel, syncWithSelectionSet);
this.subcategories = new HilitedIds(iModel);
this.models = new HilitedIds(iModel);
}
/** Control whether the hilited elements 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.
*/
public get wantSyncWithSelectionSet(): boolean { return this._elements.wantSyncWithSelectionSet; }
public set wantSyncWithSelectionSet(want: boolean) { this._elements.wantSyncWithSelectionSet = want; }
/** Remove all elements from the hilited set. */
public clear() {
this.elements.clear();
this.subcategories.clear();
this.models.clear();
}
public get isEmpty(): boolean { 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.
*/
public setHilite(arg: Id64Arg, onOff: boolean): void {
if (onOff)
Id64.forEach(arg, (id) => this.elements.addId(id));
else
Id64.forEach(arg, (id) => this.elements.deleteId(id));
IModelApp.viewManager.onSelectionSetChanged(this.iModel);
}
}
/** A set of *currently selected* elements for an IModelConnection.
* Selected elements are displayed with a customizable hilite effect within a [[Viewport]].
* @see [Hilite.Settings]($common) for customization of the hilite effect.
* @public
*/
export class SelectionSet {
private _elements = new Set<string>();
/** The IDs of the selected elements.
* @note Do not modify this set directly. Instead, use methods like [[SelectionSet.add]].
*/
public get elements(): Set<string> { return this._elements; }
/** Called whenever elements are added or removed from this SelectionSet */
public readonly onChanged = new BeEvent<(ev: SelectionSetEvent) => void>();
public constructor(public iModel: IModelConnection) { }
private sendChangedEvent(ev: SelectionSetEvent) {
IModelApp.viewManager.onSelectionSetChanged(this.iModel);
this.onChanged.raiseEvent(ev);
}
/** Get the number of entries in this selection set. */
public get size() { return this.elements.size; }
/** Check whether there are any selected elements. */
public get isActive() { return this.size !== 0; }
/** Return true if elemId is in this SelectionSet.
* @see [[isSelected]]
*/
public has(elemId?: string) { return !!elemId && this.elements.has(elemId); }
/** Query whether an Id is in the selection set.
* @see [[has]]
*/
public isSelected(elemId?: Id64String): boolean { return !!elemId && this.elements.has(elemId); }
/** Clear current selection set.
* @note raises the [[onChanged]] event with [[SelectionSetEventType.Clear]].
*/
public emptyAll(): void {
if (!this.isActive)
return;
const removed = this._elements;
this._elements = new Set<string>();
this.sendChangedEvent({ set: this, type: SelectionSetEventType.Clear, removed });
}
/**
* 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.
*/
public add(elem: Id64Arg): boolean {
return this._add(elem);
}
private _add(elem: Id64Arg, sendEvent = true): boolean {
const oldSize = this.elements.size;
Id64.forEach(elem, (id) => this.elements.add(id));
const changed = oldSize !== this.elements.size;
if (sendEvent && changed)
this.sendChangedEvent({ type: SelectionSetEventType.Add, set: this, added: elem });
return changed;
}
/**
* 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.
*/
public remove(elem: Id64Arg): boolean {
return this._remove(elem);
}
private _remove(elem: Id64Arg, sendEvent = true): boolean {
const oldSize = this.elements.size;
Id64.forEach(elem, (id) => this.elements.delete(id));
const changed = oldSize !== this.elements.size;
if (sendEvent && changed)
this.sendChangedEvent({ type: SelectionSetEventType.Remove, set: this, removed: elem });
return changed;
}
/**
* 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.
*/
public addAndRemove(adds: Id64Arg, removes: Id64Arg): boolean {
const added = this._add(adds, false);
const removed = this._remove(removes, false);
if (added && removed)
this.sendChangedEvent({ type: SelectionSetEventType.Replace, set: this, added: adds, removed: removes });
else if (added)
this.sendChangedEvent({ type: SelectionSetEventType.Add, set: this, added: adds });
else if (removed)
this.sendChangedEvent({ type: SelectionSetEventType.Remove, set: this, removed: removes });
return (added || removed);
}
/** Invert the state of a set of Ids in the SelectionSet */
public invert(elem: Id64Arg): boolean {
const elementsToAdd = new Set<string>();
const elementsToRemove = new Set<string>();
Id64.forEach(elem, (id) => {
if (this.elements.has(id))
elementsToRemove.add(id);
else
elementsToAdd.add(id);
});
return this.addAndRemove(elementsToAdd, elementsToRemove);
}
/** Change selection set to be the supplied set of Ids. */
public replace(elem: Id64Arg): void {
if (areEqual(this.elements, elem))
return;
const removed = this._elements;
this._elements = new Set<string>();
this._add(elem, false);
if (0 < removed.size) {
Id64.forEach(elem, (id) => {
if (removed.has(id))
removed.delete(id);
});
}
this.sendChangedEvent({ type: SelectionSetEventType.Replace, set: this, added: elem, removed });
}
}
function areEqual(lhs: Set<string>, rhs: Id64Arg): boolean {
// Size is unreliable if input can contain duplicates...
if (Array.isArray(rhs))
rhs = Id64.toIdSet(rhs);
if (lhs.size !== Id64.sizeOf(rhs))
return false;
return Id64.iterate(rhs, (id) => lhs.has(id));
}