/
HyperModelingDecorator.ts
415 lines (344 loc) · 16 KB
/
HyperModelingDecorator.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
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module HyperModeling
*/
import { Transform, XAndY } from "@itwin/core-geometry";
import type { AbstractToolbarProps } from "@itwin/appui-abstract";
import {
ChangeFlags, DecorateContext, Decorator, IModelApp, IModelConnection, ScreenViewport, SpatialViewState, TiledGraphicsProvider, ViewClipTool,
} from "@itwin/core-frontend";
import { SectionMarker, SectionMarkerSet } from "./SectionMarkers";
import { SectionDrawingLocationState } from "./SectionDrawingLocationState";
import { createSectionGraphicsProvider } from "./SectionGraphicsProvider";
import { PopupToolbarManager, PopupToolbarProvider } from "./PopupToolbar";
import { HyperModeling } from "./HyperModeling";
import { SectionMarkerConfig } from "./HyperModelingConfig";
async function createMarkers(vp: ScreenViewport): Promise<SectionMarkerSet | undefined> {
if (!vp.view.isSpatialView())
return undefined;
const states = await SectionDrawingLocationState.queryAll(vp.iModel);
if (0 === states.length)
return undefined;
const markers = states.map((state) => new SectionMarker(state));
return new SectionMarkerSet(vp, markers);
}
class MarkerToolbarProvider implements PopupToolbarProvider {
private readonly _viewport: ScreenViewport;
public readonly marker: SectionMarker;
public readonly toolbarProps: AbstractToolbarProps;
public readonly onToolbarItemExecuted: (id: string) => void;
public constructor(marker: SectionMarker, decorator: HyperModelingDecorator) {
this.marker = marker;
this._viewport = decorator.viewport;
this.toolbarProps = HyperModeling.markerHandler.getToolbarProps(marker, decorator);
this.onToolbarItemExecuted = (id) => HyperModeling.markerHandler.executeCommand(id, marker, decorator); // eslint-disable-line @typescript-eslint/promise-function-async
}
public get overToolbarHotspot() {
return this.marker.isHilited;
}
public get toolbarLocation(): XAndY {
return { x: this.marker.rect.right, y: this.marker.rect.top };
}
public get htmlElement() {
return this._viewport.canvas;
}
}
/** A [Decorator]($frontend) that displays a [[SectionMarker]] for each [SectionDrawingLocation]($backend) in the view.
* Clicking on a marker toggles the section and the display of associated 2d graphics.
* Hovering over a marker opens a mini toolbar with additional interactions.
* @see [[SectionMarkerHandler]] to customize the marker interactions.
* @see [[HyperModeling.startOrStop]] to enable or disable the decorator for a viewport.
* @public
*/
export class HyperModelingDecorator implements Decorator {
/** The set of [[SectionMarker]]s controlled by this decorator. */
public readonly markers: SectionMarkerSet;
private _config: SectionMarkerConfig;
private readonly _removeEventListeners = new Array<() => void>();
private readonly _iModel: IModelConnection;
private _needSync = false;
private _toolbarProvider?: MarkerToolbarProvider;
private _tiledGraphicsProvider?: TiledGraphicsProvider;
private _activeMarker?: SectionMarker;
private _appliedSpatialView?: SpatialViewState;
/** @internal */
public syncImmediately = false;
/** Create a new decorator and register it with the [ViewManager]($frontend). Typically invoked indirectly via [[HyperModeling.startOrStop]]. */
public static async create(vp: ScreenViewport, config: SectionMarkerConfig): Promise<HyperModelingDecorator | undefined> {
const markers = await createMarkers(vp);
return undefined !== markers ? new HyperModelingDecorator(markers, config) : undefined;
}
/** Obtain the decorator associated with the specified viewport, if any. */
public static getForViewport(vp: ScreenViewport): HyperModelingDecorator | undefined {
for (const decorator of IModelApp.viewManager.decorators)
if (decorator instanceof HyperModelingDecorator && decorator.viewport === vp)
return decorator;
return undefined;
}
/** The viewport into which this decorator draws its [[SectionMarker]]s. */
public get viewport(): ScreenViewport {
return this.markers.viewport;
}
/** The currently active marker. A marker typically becomes active when the user clicks on it.
* @see [[setActiveMarker]] to change the active marker without user interaction.
*/
public get activeMarker(): SectionMarker | undefined {
return this._activeMarker;
}
/** @internal */
public get config(): SectionMarkerConfig {
return this._config;
}
/** Replaces the current marker display configuration, overwriting all previous settings. Passing `undefined` resets all settings to defaults.
* @see [[updateConfiguration]] to override specific aspects of the configuration
* @see [[HyperModeling.replaceConfiguration]] to replace the global configuration.
*/
public replaceConfiguration(config?: SectionMarkerConfig): void {
this._config = config ? { ...config } : {};
this.requestSync();
}
/** Overrides specific aspects of the current marker display configuration.
* Any field that is not `undefined` will be replaced in the current configuration; the rest will retain their current values.
* @see [[replaceConfiguration]] to override all settings.
* @see [[HyperModeling.updateConfiguration]] to update the global configuration.
*/
public updateConfiguration(config: SectionMarkerConfig): void {
this._config = {
ignoreModelSelector: config.ignoreModelSelector ?? this._config.ignoreModelSelector,
ignoreCategorySelector: config.ignoreCategorySelector ?? this._config.ignoreCategorySelector,
hiddenSectionTypes: config.hiddenSectionTypes ?? this._config.hiddenSectionTypes,
};
this.requestSync();
}
/** Sets the currently active marker. This function is invoked when the user clicks on a marker, but may also be called manually to produce the same result.
* Changing the active marker first deactivates the currently active marker, if any; then activates the specified marker, if supplied.
* Returns false if marker activation fails.
* @see [[activeMarker]] to obtain the currently active section marker.
* @see [[SectionMarkerHandler.activateMarker]] to control what happens when a marker is activated.
* @see [[SectionMarkerHandler.deactivateMarker]] to control what happens when a marker is deactivated.
*/
public async setActiveMarker(marker: SectionMarker | undefined): Promise<boolean> {
if (marker === this.activeMarker)
return true;
if (this.activeMarker) {
this.activeMarker.setActive(false);
await HyperModeling.markerHandler.deactivateMarker(this.activeMarker, this);
this._activeMarker = undefined;
}
if (marker) {
if (!await HyperModeling.markerHandler.activateMarker(marker, this)) {
this.requestSync();
return false;
}
marker.setActive(true);
this._activeMarker = marker;
}
this.requestSync();
return true;
}
/** @internal */
public readonly useCachedDecorations = true;
/** @internal */
public decorate(context: DecorateContext): void {
if (this.viewport.view.is3d())
this.markers.addDecoration(context);
}
private constructor(markers: SectionMarkerSet, config: SectionMarkerConfig) {
this.markers = markers;
this._config = { ...config };
this._iModel = this.viewport.iModel;
this.viewport.onChangeView.addOnce(() => {
this.requestSync();
});
this._removeEventListeners.push(this.viewport.onViewportChanged.addListener((_, changeFlags) => this.onViewportChanged(changeFlags)));
this._removeEventListeners.push(this.viewport.onDisposed.addListener(() => this.dispose()));
for (const marker of markers.markers) {
marker.onMouseEnterEvent.addListener((mkr) => this.showToolbarAfterTimeout(mkr));
marker.onMouseButtonEvent.addListener((mkr) => this.toggleMarker(mkr)); // eslint-disable-line @typescript-eslint/promise-function-async
}
this.updateMarkerVisibility();
IModelApp.viewManager.addDecorator(this);
}
private onViewportChanged(changeFlags: ChangeFlags): void {
if (this.viewport.iModel !== this._iModel) {
this.dispose();
return;
}
if (changeFlags.viewedCategories || changeFlags.viewedModels || changeFlags.viewedCategoriesPerModel)
this.requestSync();
if (changeFlags.viewState) {
// If we're looking at a different view now, and we did not initiate that, turn off the active marker.
if (this.viewport.view !== this._appliedSpatialView)
this.setActiveMarker(undefined); // eslint-disable-line @typescript-eslint/no-floating-promises
else
this._appliedSpatialView = undefined;
}
}
private async toggleMarker(marker: SectionMarker): Promise<void> {
await this.setActiveMarker(marker === this.activeMarker ? undefined : marker);
}
private dropTiledGraphicsProvider(): void {
if (undefined === this._tiledGraphicsProvider)
return;
this.viewport.dropTiledGraphicsProvider(this._tiledGraphicsProvider);
this._tiledGraphicsProvider = undefined;
}
/** @internal */
public dispose(): void {
if (!IModelApp.viewManager.dropDecorator(this))
return;
for (const remove of this._removeEventListeners)
remove();
this.dropTiledGraphicsProvider();
}
private showToolbarAfterTimeout(marker: SectionMarker): void {
if (this._toolbarProvider?.marker !== marker)
this._toolbarProvider = new MarkerToolbarProvider(marker, this);
PopupToolbarManager.showToolbarAfterTimeout(this._toolbarProvider);
}
/** Toggles whether the clip volume associated with the specified marker is applied to the view. */
public toggleClipVolume(marker: SectionMarker, enable: boolean): void {
ViewClipTool.enableClipVolume(this.viewport);
ViewClipTool.setViewClip(this.viewport, enable ? marker.state.clip : undefined);
}
/** Toggles the specified section marker.
* Enabling the section applies the frustum and clip volume of the marker's spatial view to the viewport, and displays the 2d section graphics and sheet annotations.
* Disabling the section disables the clip volume and 2d graphics.
* @see [[toggleClipVolume]] to toggle only the clip volume.
* @see [[toggleAttachment]] to toggle only the attachment graphics.
*/
public async toggleSection(marker: SectionMarker, enable: boolean): Promise<boolean> {
if (enable) {
if (this.viewport.view.is3d()) {
// Preserve the view settings; apply only the frustum and clip volume
const spatialView = await marker.state.tryLoadSpatialView();
if (!spatialView)
return false;
const aligned = await this.alignToSpatialView(marker);
if (!aligned)
return false;
this.toggleClipVolume(marker, true);
} else if (!await this.applySpatialView(marker)) {
return false;
}
} else {
this.toggleClipVolume(marker, false);
}
this.requestSync();
return this.toggleAttachment(marker, enable);
}
/** Toggles display of 2d section graphics and sheet annotations for the specified marker.
* @see [[toggleSection]] to apply the spatial view and clip volume in addition to the attachment graphics.
*/
public async toggleAttachment(marker: SectionMarker, enable: boolean): Promise<boolean> {
this.dropTiledGraphicsProvider();
if (enable) {
this._tiledGraphicsProvider = await createSectionGraphicsProvider(marker.state);
this.viewport.addTiledGraphicsProvider(this._tiledGraphicsProvider);
}
return true;
}
/** Aligns the viewport to face the specified marker's section plane. */
public alignView(marker: SectionMarker): void {
const placement = marker.state.placement.transform;
const origin = placement.origin;
const matrix = placement.matrix;
const vp = this.viewport;
const targetMatrix = matrix.multiplyMatrixMatrix(vp.rotation);
const rotateTransform = Transform.createFixedPointAndMatrix(origin, targetMatrix);
const startFrustum = vp.getFrustum();
const newFrustum = startFrustum.clone();
newFrustum.multiply(rotateTransform);
if (startFrustum.equals(newFrustum))
return;
vp.view.setupFromFrustum(newFrustum);
vp.synchWithView();
vp.animateFrustumChange();
}
/** Opens the marker's drawing view in the decorator's viewport. Returns false if the drawing view could not be loaded. */
public async openSection(marker: SectionMarker): Promise<boolean> {
const viewState = await marker.state.tryLoadDrawingView();
if (viewState)
this.viewport.changeView(viewState);
return undefined !== viewState;
}
/** Opens marker's sheet view in the decorator's viewport and zooms in on the associated [ViewAttachment]($backend). Returns false if no view
* attachment exists or the sheet view could not be loaded.
*/
public async openSheet(marker: SectionMarker): Promise<boolean> {
if (undefined === marker.state.viewAttachment)
return false;
const viewState = await marker.state.tryLoadSheetView();
if (!viewState)
return false;
this.viewport.changeView(viewState);
await this.viewport.zoomToElements(marker.state.viewAttachment.id);
return true;
}
/** Aligns the view to match the frustum of the spatial view associated with the specified marker.
* @param marker The marker whose spatial view's frustum should be applied.
* @returns false if the frustum could not be applied, e.g. because the spatial view could not be loaded or the viewport is viewing a 2d model.
* @see [[alignView]] to align to the *section plane*, which may differ.
* @see [[toggleSection]] to also apply the clip volume.
*/
public async alignToSpatialView(marker: SectionMarker): Promise<boolean> {
if (!this.viewport.view.is3d())
return false;
const spatialView = await marker.state.tryLoadSpatialView();
if (!spatialView)
return false;
this.viewport.view.setOrigin(spatialView.getOrigin());
this.viewport.view.setExtents(spatialView.getExtents());
this.viewport.view.setRotation(spatialView.getRotation());
this.viewport.synchWithView({ animateFrustumChange: true });
return true;
}
/** Applies the marker's spatial view - including its clip volume - to the decorator's viewport.
* Returns false if the spatial view could not be loaded.
* @see [[toggleSection]].
* @see [[toggleClipVolume]].
*/
public async applySpatialView(marker: SectionMarker): Promise<boolean> {
const viewState = await marker.state.tryLoadSpatialView();
if (viewState) {
this._appliedSpatialView = viewState;
this.viewport.changeView(viewState);
}
return undefined !== viewState;
}
/** Request that the visibility of the section markers be recomputed. It is only necessary to call this if you have overridden [[SectionMarkerHandler.isMarkerVisible]]; if so, you should invoke
* this method when the criteria governing your `isMarkerVisible` method have changed.
*/
public requestSync(): void {
if (this.syncImmediately) {
this.sync();
} else if (!this._needSync) {
this._needSync = true;
requestAnimationFrame(() => this.sync());
}
}
private sync(): void {
this._needSync = false;
if (HyperModeling.isInitialized && this.viewport.view.is3d() && this.updateMarkerVisibility()) {
this.markers.markDirty();
this.viewport.invalidateCachedDecorations(this);
}
}
private isMarkerVisible(marker: SectionMarker): boolean {
if (undefined !== this.activeMarker)
return marker === this.activeMarker;
return HyperModeling.markerHandler.isMarkerVisible(marker, this, this._config);
}
private updateMarkerVisibility(): boolean {
let changed = false;
for (const marker of this.markers.markers) {
const wasVisible = marker.visible;
marker.visible = this.isMarkerVisible(marker);
changed = changed || (marker.visible !== wasVisible);
}
return changed;
}
}