-
Notifications
You must be signed in to change notification settings - Fork 210
/
ViewClipByElementGeometryTool.ts
193 lines (160 loc) · 7.87 KB
/
ViewClipByElementGeometryTool.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
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import {
ClipPrimitive, ClipVector, ConvexClipPlaneSet, IndexedPolyface, Point3d, PolyfaceBuilder, PolyfaceQuery, UnionOfConvexClipPlaneSets,
} from "@itwin/core-geometry";
import {
ElementMeshOptions, readElementMeshes,
} from "@itwin/core-common";
import {
BeButtonEvent, CoordinateLockOverrides, EventHandled, IModelApp, LocateResponse, ViewClipTool, Viewport,
} from "@itwin/core-frontend";
import {
ConvexMeshDecomposition, Options as DecompositionOptions,
} from "vhacd-js";
/** Settings that control the behavior of the ViewClipByElementGeometryTool. */
interface Settings extends ElementMeshOptions {
/** If true, produce convex hulls from the element geometry. Convex hulls are required for proper clipping; if this is
* set to false, make sure to only select elements that already have convex geometry.
*/
computeConvexHulls: boolean;
/** Options used to produce convex hulls, if computeConvexHulls is true. */
decomposition: DecompositionOptions;
/** An offset in meters by which to expand or contract the surfaces of the clipping polyfaces.
* This is useful primarily for expanding the clipped volume slightly so that the element(s) from which the clip was produced are not clipped out.
*/
offset?: number;
}
/** Uses vhacd-js to convert IndexedPolyfaces to convex IndexedPolyfaces. */
class ConvexDecomposer {
private readonly _impl: ConvexMeshDecomposition;
private readonly _opts: DecompositionOptions;
private constructor(impl: ConvexMeshDecomposition, options: DecompositionOptions) {
this._impl = impl;
this._opts = options;
}
public static async create(options: DecompositionOptions): Promise<ConvexDecomposer> {
const impl = await ConvexMeshDecomposition.create();
return new ConvexDecomposer(impl, options);
}
public decompose(polyfaces: IndexedPolyface[]): IndexedPolyface[] {
const decomposedPolyfaces: IndexedPolyface[] = [];
const polygon = [new Point3d(), new Point3d(), new Point3d()];
for (const polyface of polyfaces) {
if (PolyfaceQuery.isConvexByDihedralAngleCount(polyface)) {
// The polyface is already convex - don't bother performing decomposition.
decomposedPolyfaces.push(polyface);
continue;
}
const points = polyface.data.point;
// `points` is a GrowableXYZArray, which may allocate more space than it needs for the number of points it stores.
// Make sure to only pass the used portion of the allocation to vhacd-js.
const positions = new Float64Array(points.float64Data().buffer, 0, points.float64Length);
// Unfortunately we must copy the indices rather than passing them directly to vhacd-js.
const indices = new Uint32Array(polyface.data.pointIndex);
if (indices.length === 0 || positions.length === 0)
continue;
// Decompose the polyface into any number of convex hulls.
const meshes = this._impl.computeConvexHulls({ positions, indices }, this._opts);
// Convert each hull into a polyface.
for (const mesh of meshes) {
const builder = PolyfaceBuilder.create();
for (let i = 0; i < mesh.indices.length; i += 3) {
for (let j = 0; j < 3; j++)
this.getPoint(mesh.indices[i + j], mesh.positions, polygon[j]);
builder.addPolygon(polygon);
}
decomposedPolyfaces.push(builder.claimPolyface());
}
}
return decomposedPolyfaces;
}
private getPoint(index: number, positions: Float64Array, output: Point3d): void {
index *= 3;
output.set(positions[index + 0], positions[index + 1], positions[index + 2]);
}
}
// For demo purposes, settings are global and the only way to change them is to edit the code below.
const settings: Settings = {
offset: 0.025,
computeConvexHulls: true,
chordTolerance: 0.1,
decomposition: {
maxHulls: 10,
maxVerticesPerHull: 16,
},
};
/** Clips the view based on the geometry of one or more geometric elements.
* We obtain polyfaces from the backend for each element and produce convex hulls from each polyface.
* Then we create a ClipVector that clips out any geometry not inside one of the hulls.
*
* This tool exists for example purposes only. Some inefficiencies exist, including:
* - Convex mesh decomposition can take some time. Ideally it would be performed in a WebWorker so as not to block the UI thread.
* - If we had a way to determine if a polyface is already convex, we could avoid performing unnecessary decomposition on such polyfaces.
*/
export class ViewClipByElementGeometryTool extends ViewClipTool {
public static override toolId = "DtaClipByElementGeometry";
public static override iconSpec = "icon-section-element";
public override async onPostInstall() {
await super.onPostInstall();
// If some elements are already selected, immediately clip the view using their geometry.
if (this.targetView && this.targetView.iModel.selectionSet.isActive) {
await this.doClipToSelectedElements(this.targetView);
return;
}
// Wait for the user to select the elements to use for clipping.
this.initLocateElements(true, false, "default", CoordinateLockOverrides.All);
}
public override async onDataButtonDown(ev: BeButtonEvent): Promise<EventHandled> {
if (!this.targetView)
return EventHandled.No;
// Identify the element selected.
const hit = await IModelApp.locateManager.doLocate(new LocateResponse(), true, ev.point, ev.viewport, ev.inputSource);
if (!hit || !hit.isElementHit)
return EventHandled.No;
// Clip the view using the selected element's geometry.
return await this.doClipToElements(this.targetView, new Set<string>([hit.sourceId])) ? EventHandled.Yes : EventHandled.No;
}
/** Clip the view using the geometry of all elements currently in the selection set. */
private async doClipToSelectedElements(viewport: Viewport): Promise<boolean> {
if (await this.doClipToElements(viewport, viewport.iModel.selectionSet.elements))
return true;
await this.exitTool();
return false;
}
/** Clip the view using the geometry of the specified elements. */
private async doClipToElements(viewport: Viewport, ids: Set<string>): Promise<boolean> {
try {
const union = UnionOfConvexClipPlaneSets.createEmpty();
const decomposer = settings.computeConvexHulls ? await ConvexDecomposer.create(settings.decomposition) : undefined;
for (const source of ids) {
// Obtain polyfaces for this element.
const meshData = await viewport.iModel.generateElementMeshes({ ...settings, source });
let polyfaces = readElementMeshes(meshData);
// Offset if specified - typically used to expand the element envelope slightly.
// ###TODO cloneOffset should return IndexedPolyface, not Polyface?
const offset = settings.offset;
if (offset)
polyfaces = polyfaces.map((pf) => PolyfaceQuery.cloneOffset(pf, offset));
// Convert to convex hulls unless otherwise specified.
if (decomposer)
polyfaces = decomposer.decompose(polyfaces);
// Add each polyface as a clipper.
for (const polyface of polyfaces)
union.addConvexSet(ConvexClipPlaneSet.createConvexPolyface(polyface).clipper);
}
// Apply the clip to the view.
ViewClipTool.enableClipVolume(viewport);
const primitive = ClipPrimitive.createCapture(union);
const clip = ClipVector.createCapture([primitive]);
ViewClipTool.setViewClip(viewport, clip);
this._clipEventHandler?.onNewClip(viewport);
await this.onReinitialize();
return true;
} catch {
return false;
}
}
}