@xeokit/xeokit-sdk 2.6.65 → 2.6.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{LICENSE.txt → LICENSE} +30 -27
- package/README.md +13 -6
- package/dist/xeokit-sdk.cjs.js +50322 -25848
- package/dist/xeokit-sdk.es.js +50322 -25848
- package/dist/xeokit-sdk.es5.js +7982 -4807
- package/dist/xeokit-sdk.min.cjs.js +5 -5
- package/dist/xeokit-sdk.min.es.js +5 -5
- package/dist/xeokit-sdk.min.es5.js +4 -4
- package/package.json +34 -32
- package/src/extras/PointerCircle/PointerCircle.js +1 -1
- package/src/plugins/AnnotationsPlugin/Annotation.js +45 -5
- package/src/plugins/CityJSONLoaderPlugin/CityJSONLoaderPlugin.js +1 -1
- package/src/plugins/GLTFLoaderPlugin/GLTFSceneModelLoader.js +3 -2
- package/src/plugins/LASLoaderPlugin/LASLoaderPlugin.js +1 -1
- package/src/plugins/SectionPlanesPlugin/Control.js +11 -12
- package/src/plugins/XKTLoaderPlugin/XKTLoaderPlugin.js +57 -2
- package/src/plugins/XKTLoaderPlugin/parsers/ParserV10.js +6 -0
- package/src/plugins/XKTLoaderPlugin/parsers/ParserV11.js +6 -0
- package/src/plugins/XKTLoaderPlugin/parsers/ParserV12.js +822 -0
- package/src/plugins/XKTLoaderPlugin/parsers/ParserV6.js +6 -0
- package/src/plugins/XKTLoaderPlugin/parsers/ParserV7.js +6 -0
- package/src/plugins/XKTLoaderPlugin/parsers/ParserV8.js +6 -0
- package/src/plugins/XKTLoaderPlugin/parsers/ParserV9.js +6 -0
- package/src/{plugins/lib → viewer/scene/libs}/earcut.js +194 -189
- package/src/viewer/scene/materials/EmphasisMaterial.js +5 -1
- package/src/viewer/scene/math/math.js +6 -2
- package/src/viewer/scene/model/SceneModel.js +1 -0
- package/src/viewer/scene/model/SceneModelEntity.js +26 -0
- package/src/viewer/scene/model/SceneModelMesh.js +4 -0
- package/src/viewer/scene/model/dtx/triangles/DTXTrianglesLayer.js +1 -1
- package/src/viewer/scene/model/vbo/batching/triangles/VBOBatchingTrianglesLayer.js +1 -1
- package/src/viewer/scene/model/vbo/batching/triangles/renderers/TrianglesColorRenderer.js +4 -1
- package/src/viewer/scene/model/vbo/instancing/triangles/VBOInstancingTrianglesLayer.js +1 -1
- package/src/viewer/scene/model/vbo/instancing/triangles/renderers/TrianglesColorRenderer.js +4 -1
- package/src/viewer/scene/scene/Scene.js +50 -6
- package/src/viewer/scene/sectionCaps/SectionCaps.js +774 -0
- package/src/viewer/scene/sectionCaps/index.js +1 -0
- package/src/viewer/scene/sectionPlane/SectionPlane.js +79 -1
- package/src/viewer/scene/webgl/Renderer.js +19 -10
- package/types/extras/PointerCircle/PointerCircle.d.ts +50 -0
- package/types/extras/PointerCircle/index.d.ts +1 -0
- package/types/plugins/WebIFCLoaderPlugin/WebIFCLoaderPlugin.d.ts +2 -1
- package/types/plugins/XKTLoaderPlugin/XKTLoaderPlugin.d.ts +28 -0
- package/types/plugins/index.d.ts +1 -0
- package/types/plugins/lib/ui/index.d.ts +12 -0
- package/types/viewer/scene/models/SceneModelMesh.d.ts +7 -0
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
import { math } from "../math/math.js";
|
|
2
|
+
import { Mesh } from "../mesh/Mesh.js";
|
|
3
|
+
import { ReadableGeometry } from "../geometry/ReadableGeometry.js";
|
|
4
|
+
import { buildLineGeometry } from "../geometry/index.js";
|
|
5
|
+
import { PhongMaterial } from "../materials/PhongMaterial.js";
|
|
6
|
+
import earcut from '../libs/earcut.js';
|
|
7
|
+
|
|
8
|
+
const epsilon = 1e-6;
|
|
9
|
+
const worldUp = [0, 1, 0];
|
|
10
|
+
const worldRight = [1, 0, 0];
|
|
11
|
+
const tempVec3a = math.vec3();
|
|
12
|
+
const tempVec3b = math.vec3();
|
|
13
|
+
const tempVec3c = math.vec3();
|
|
14
|
+
const tempVec3d = math.vec3();
|
|
15
|
+
|
|
16
|
+
function pointsEqual(p1, p2) {
|
|
17
|
+
return (
|
|
18
|
+
Math.abs(p1[0] - p2[0]) < epsilon &&
|
|
19
|
+
Math.abs(p1[1] - p2[1]) < epsilon &&
|
|
20
|
+
Math.abs(p1[2] - p2[2]) < epsilon
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @desc Implements hatching for Solid objects on a {@link Scene}.
|
|
26
|
+
*
|
|
27
|
+
* [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/slicing/SectionPlanesPlugin_Duplex_SectionCaps.html)]
|
|
28
|
+
*
|
|
29
|
+
* ##Overview
|
|
30
|
+
*
|
|
31
|
+
* The WebGL implementation for capping sliced 3D objects works by first calculating intersection segments where a cutting
|
|
32
|
+
* plane meets the object's edges. These segments form a contour that is triangulated using the Earcut algorithm, which
|
|
33
|
+
* handles any internal holes efficiently. The resulting triangulated cap is then integrated into the original mesh with
|
|
34
|
+
* appropriate normals and UVs.
|
|
35
|
+
*
|
|
36
|
+
* ##Usage
|
|
37
|
+
*
|
|
38
|
+
* In the example, we'll start by enabling readable geometry on the viewer.
|
|
39
|
+
*
|
|
40
|
+
* Then we'll position the camera, and configure the near and far perspective and orthographic
|
|
41
|
+
* clipping planes. Finally, we'll use {@link XKTLoaderPlugin} to load the Duplex model.
|
|
42
|
+
*
|
|
43
|
+
* ````javascript
|
|
44
|
+
* const viewer = new Viewer({
|
|
45
|
+
* canvasId: "myCanvas",
|
|
46
|
+
* transparent: true,
|
|
47
|
+
* readableGeometryEnabled: true
|
|
48
|
+
* });
|
|
49
|
+
*
|
|
50
|
+
* viewer.camera.eye = [-2.341298674548419, 22.43987089731119, 7.236688436028655];
|
|
51
|
+
* viewer.camera.look = [4.399999999999963, 3.7240000000000606, 8.899000000000006];
|
|
52
|
+
* viewer.camera.up = [0.9102954845584759, 0.34781746407929504, 0.22446635042673466];
|
|
53
|
+
*
|
|
54
|
+
* const cameraControl = viewer.cameraControl;
|
|
55
|
+
* cameraControl.navMode = "orbit";
|
|
56
|
+
* cameraControl.followPointer = true;
|
|
57
|
+
*
|
|
58
|
+
* const xktLoader = new XKTLoaderPlugin(viewer);
|
|
59
|
+
*
|
|
60
|
+
* var t0 = performance.now();
|
|
61
|
+
*
|
|
62
|
+
* document.getElementById("time").innerHTML = "Loading model...";
|
|
63
|
+
*
|
|
64
|
+
* const sceneModel = xktLoader.load({
|
|
65
|
+
* id: "myModel",
|
|
66
|
+
* src: "../../assets/models/xkt/v10/glTF-Embedded/Duplex_A_20110505.glTFEmbedded.xkt",
|
|
67
|
+
* edges: true
|
|
68
|
+
* });
|
|
69
|
+
*
|
|
70
|
+
* sceneModel.on("loaded", () => {
|
|
71
|
+
*
|
|
72
|
+
* var t1 = performance.now();
|
|
73
|
+
* document.getElementById("time").innerHTML = "Model loaded in " + Math.floor(t1 - t0) / 1000.0 + " seconds<br>Objects: " + sceneModel.numEntities;
|
|
74
|
+
*
|
|
75
|
+
* //------------------------------------------------------------------------------------------------------------------
|
|
76
|
+
* // Add caps materials to all objects inside the loaded model that have an opacity equal to or above 0.7
|
|
77
|
+
* //------------------------------------------------------------------------------------------------------------------
|
|
78
|
+
* const opacityThreshold = 0.7;
|
|
79
|
+
* const material = new PhongMaterial(viewer.scene,{
|
|
80
|
+
* diffuse: [1.0, 0.0, 0.0],
|
|
81
|
+
* backfaces: true
|
|
82
|
+
* });
|
|
83
|
+
* addCapsMaterialsToAllObjects(sceneModel, opacityThreshold, material);
|
|
84
|
+
*
|
|
85
|
+
* //------------------------------------------------------------------------------------------------------------------
|
|
86
|
+
* // Create a moving SectionPlane, that moves through the table models
|
|
87
|
+
* //------------------------------------------------------------------------------------------------------------------
|
|
88
|
+
*
|
|
89
|
+
* const sectionPlanes = new SectionPlanesPlugin(viewer, {
|
|
90
|
+
* overviewCanvasId: "mySectionPlanesOverviewCanvas",
|
|
91
|
+
* overviewVisible: true,
|
|
92
|
+
* });
|
|
93
|
+
*
|
|
94
|
+
* const sectionPlane = sectionPlanes.createSectionPlane({
|
|
95
|
+
* id: "mySectionPlane",
|
|
96
|
+
* pos: [0.5, 2.5, 5.0],
|
|
97
|
+
* dir: math.normalizeVec3([1.0, 0.01, 1])
|
|
98
|
+
* });
|
|
99
|
+
*
|
|
100
|
+
* sectionPlanes.showControl(sectionPlane.id);
|
|
101
|
+
*
|
|
102
|
+
* window.viewer = viewer;
|
|
103
|
+
*
|
|
104
|
+
* });
|
|
105
|
+
*
|
|
106
|
+
* function addCapsMaterialsToAllObjects(sceneModel, opacityThreshold, material) {
|
|
107
|
+
* const allObjects = sceneModel.objects;
|
|
108
|
+
* for(const key in allObjects){
|
|
109
|
+
* const object = allObjects[key];
|
|
110
|
+
* if(object.opacity >= opacityThreshold)
|
|
111
|
+
* object.capMaterial = material;
|
|
112
|
+
* }
|
|
113
|
+
* }
|
|
114
|
+
* ````
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
class SectionCaps {
|
|
118
|
+
/**
|
|
119
|
+
* @constructor
|
|
120
|
+
*/
|
|
121
|
+
constructor(scene) {
|
|
122
|
+
this.scene = scene;
|
|
123
|
+
this._resourcesAllocated = false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
_onCapMaterialUpdated(entityId, modelId) {
|
|
127
|
+
if(!this._resourcesAllocated) {
|
|
128
|
+
this._resourcesAllocated = true;
|
|
129
|
+
this._sectionPlanes = [];
|
|
130
|
+
this._verticesMap = {};
|
|
131
|
+
this._indicesMap = {};
|
|
132
|
+
this._dirtyMap = {};
|
|
133
|
+
this._prevIntersectionModelsMap = {};
|
|
134
|
+
this._sectionPlaneTimeout = null;
|
|
135
|
+
this._updateTimeout = null;
|
|
136
|
+
|
|
137
|
+
const handleSectionPlane = (sectionPlane) => {
|
|
138
|
+
|
|
139
|
+
const onSectionPlaneUpdated = () => {
|
|
140
|
+
this._setAllDirty(true);
|
|
141
|
+
this._update();
|
|
142
|
+
}
|
|
143
|
+
this._sectionPlanes.push(sectionPlane);
|
|
144
|
+
sectionPlane.on('pos', onSectionPlaneUpdated);
|
|
145
|
+
sectionPlane.on('dir', onSectionPlaneUpdated);
|
|
146
|
+
sectionPlane.once('destroyed', ((sectionPlane) => {
|
|
147
|
+
const sectionPlaneId = sectionPlane.id;
|
|
148
|
+
if (sectionPlaneId) {
|
|
149
|
+
this._sectionPlanes = this._sectionPlanes.filter((sectionPlane) => sectionPlane.id !== sectionPlaneId);
|
|
150
|
+
this._update();
|
|
151
|
+
}
|
|
152
|
+
}).bind(this));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for(const key in this.scene.sectionPlanes){
|
|
156
|
+
handleSectionPlane(this.scene.sectionPlanes[key]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this._onSectionPlaneCreated = this.scene.on('sectionPlaneCreated', handleSectionPlane)
|
|
160
|
+
|
|
161
|
+
this._onTick = this.scene.on("tick", () => {
|
|
162
|
+
//on ticks we only check if there is a model that we have saved vertices for,
|
|
163
|
+
//but it's no more available on the scene
|
|
164
|
+
for(const key in this._verticesMap) {
|
|
165
|
+
if(!this.scene.models[key]){
|
|
166
|
+
delete this._verticesMap[key];
|
|
167
|
+
delete this._indicesMap[key];
|
|
168
|
+
this._update();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if(!this._dirtyMap[modelId])
|
|
175
|
+
this._dirtyMap[modelId] = new Map();
|
|
176
|
+
|
|
177
|
+
this._dirtyMap[modelId].set(entityId, true);
|
|
178
|
+
this._update();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
_update() {
|
|
182
|
+
clearTimeout(this._updateTimeout);
|
|
183
|
+
this._deletePreviousModels();
|
|
184
|
+
this._updateTimeout = setTimeout(() => {
|
|
185
|
+
clearTimeout(this._updateTimeout);
|
|
186
|
+
const sceneModels = Object.keys(this.scene.models).map((key) => this.scene.models[key]);
|
|
187
|
+
this._addHatches(sceneModels, this._sectionPlanes);
|
|
188
|
+
this._setAllDirty(false);
|
|
189
|
+
}, 100);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
_setAllDirty(value) {
|
|
193
|
+
for(const key in this._dirty) {
|
|
194
|
+
this._dirtyMap[key].forEach((_, key2) => this._dirtyMap[key].set(key2, value));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
_addHatches(sceneModels, planes) {
|
|
199
|
+
|
|
200
|
+
if (planes.length <= 0) return;
|
|
201
|
+
|
|
202
|
+
planes.forEach((plane) => {
|
|
203
|
+
sceneModels.forEach((sceneModel) => {
|
|
204
|
+
//#region creating a plane equation
|
|
205
|
+
//we create a plane equation that will be used to slice through each triangle
|
|
206
|
+
const planeEquation = {
|
|
207
|
+
A: plane.dir[0],
|
|
208
|
+
B: plane.dir[1],
|
|
209
|
+
C: plane.dir[2],
|
|
210
|
+
D: -(plane.dir[0] * plane.pos[0] + plane.dir[1] * plane.pos[1] + plane.dir[2] * plane.pos[2])
|
|
211
|
+
}
|
|
212
|
+
//#endregion
|
|
213
|
+
|
|
214
|
+
if(!this._doesPlaneIntersectBoundingBox(sceneModel.aabb, planeEquation)) return;
|
|
215
|
+
|
|
216
|
+
if(!this._dirtyMap[sceneModel.id]) return;
|
|
217
|
+
|
|
218
|
+
//#region calculating segments in unsorted way
|
|
219
|
+
//we calculate the segments by intersecting plane with each triangle
|
|
220
|
+
const unsortedSegments = new Map();
|
|
221
|
+
const objects = sceneModel.objects;
|
|
222
|
+
// Preallocate arrays for triangle vertices to avoid repeated allocation
|
|
223
|
+
const triangle = [
|
|
224
|
+
new Float32Array(3),
|
|
225
|
+
new Float32Array(3),
|
|
226
|
+
new Float32Array(3)
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
this._dirtyMap[sceneModel.id].forEach((isDirty, objectId) => {
|
|
230
|
+
if (!isDirty) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const object = objects[objectId];
|
|
235
|
+
|
|
236
|
+
if(!this._doesPlaneIntersectBoundingBox(object.aabb, planeEquation)) return;
|
|
237
|
+
|
|
238
|
+
if(!this._verticesMap[sceneModel.id]) {
|
|
239
|
+
this._verticesMap[sceneModel.id] = new Map();
|
|
240
|
+
this._indicesMap[sceneModel.id] = new Map();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let vertices = [], indices = [];
|
|
244
|
+
|
|
245
|
+
if(!this._verticesMap[sceneModel.id].has(objectId)) {
|
|
246
|
+
const isSolid = object.meshes[0].isSolid();
|
|
247
|
+
if(isSolid && object.capMaterial) {
|
|
248
|
+
object.getEachVertex((_vertices) => {
|
|
249
|
+
vertices.push(_vertices[0], _vertices[1], _vertices[2]);
|
|
250
|
+
})
|
|
251
|
+
object.getEachIndex((_indices) => {
|
|
252
|
+
indices.push(_indices);
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
this._verticesMap[sceneModel.id].set(objectId, vertices);
|
|
256
|
+
this._indicesMap[sceneModel.id].set(objectId, indices);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
vertices = this._verticesMap[sceneModel.id].get(objectId);
|
|
260
|
+
indices = this._indicesMap[sceneModel.id].get(objectId);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const capSegments = [];
|
|
264
|
+
const vertCount = indices.length;
|
|
265
|
+
|
|
266
|
+
// Preallocate intersection result array
|
|
267
|
+
const intersectionBuffer = new Float32Array(3);
|
|
268
|
+
|
|
269
|
+
for (let i = 0; i < vertCount; i += 3) {
|
|
270
|
+
// Reuse triangle buffer instead of creating new arrays
|
|
271
|
+
for (let j = 0; j < 3; j++) {
|
|
272
|
+
const idx = indices[i + j] * 3;
|
|
273
|
+
triangle[j][0] = vertices[idx];
|
|
274
|
+
triangle[j][1] = vertices[idx + 1];
|
|
275
|
+
triangle[j][2] = vertices[idx + 2];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Early null check
|
|
279
|
+
if (!triangle[0][0] && !triangle[0][1] && !triangle[0][2]) continue;
|
|
280
|
+
|
|
281
|
+
const intersections = [];
|
|
282
|
+
for (let i = 0; i < 3; i++) {
|
|
283
|
+
const p1 = triangle[i];
|
|
284
|
+
const p2 = triangle[(i + 1) % 3];
|
|
285
|
+
|
|
286
|
+
// Inline the distance calculations to avoid function calls
|
|
287
|
+
const d1 = planeEquation.A * p1[0] + planeEquation.B * p1[1] + planeEquation.C * p1[2] + planeEquation.D;
|
|
288
|
+
const d2 = planeEquation.A * p2[0] + planeEquation.B * p2[1] + planeEquation.C * p2[2] + planeEquation.D;
|
|
289
|
+
|
|
290
|
+
if (d1 * d2 > 0) continue;
|
|
291
|
+
|
|
292
|
+
const t = -d1 / (d2 - d1);
|
|
293
|
+
// Reuse intersection buffer
|
|
294
|
+
intersectionBuffer[0] = p1[0] + t * (p2[0] - p1[0]);
|
|
295
|
+
intersectionBuffer[1] = p1[1] + t * (p2[1] - p1[1]);
|
|
296
|
+
intersectionBuffer[2] = p1[2] + t * (p2[2] - p1[2]);
|
|
297
|
+
|
|
298
|
+
// Clone the buffer for storage
|
|
299
|
+
intersections.push(new Float32Array(intersectionBuffer));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if(intersections.length === 2) capSegments.push(intersections);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (capSegments.length > 0) {
|
|
306
|
+
unsortedSegments.set(objectId, capSegments);
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
//#endregion
|
|
310
|
+
|
|
311
|
+
//#region sorting the segments
|
|
312
|
+
const orderedSegments = new Map();
|
|
313
|
+
unsortedSegments.forEach((unsortedSegment, segmentedId) => {
|
|
314
|
+
orderedSegments.set(segmentedId, [
|
|
315
|
+
[
|
|
316
|
+
unsortedSegment[0] //this is also an array of two vectors
|
|
317
|
+
]
|
|
318
|
+
]);
|
|
319
|
+
unsortedSegment.splice(0, 1);
|
|
320
|
+
let index = 0;
|
|
321
|
+
while (unsortedSegment.length > 0) {
|
|
322
|
+
const lastPoint = orderedSegments.get(segmentedId)[index][orderedSegments.get(segmentedId)[index].length - 1][1];
|
|
323
|
+
let found = false;
|
|
324
|
+
|
|
325
|
+
for (let i = 0; i < unsortedSegment.length; i++) {
|
|
326
|
+
const [start, end] = unsortedSegment[i];
|
|
327
|
+
if (pointsEqual(lastPoint, start)) {
|
|
328
|
+
orderedSegments.get(segmentedId)[index].push(unsortedSegment[i]);
|
|
329
|
+
unsortedSegment.splice(i, 1);
|
|
330
|
+
found = true;
|
|
331
|
+
break;
|
|
332
|
+
} else if (pointsEqual(lastPoint, end)) {
|
|
333
|
+
orderedSegments.get(segmentedId)[index].push([end, start]);
|
|
334
|
+
unsortedSegment.splice(i, 1);
|
|
335
|
+
found = true;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!found) {
|
|
341
|
+
if (pointsEqual(lastPoint, orderedSegments.get(segmentedId)[index][0][0])) {
|
|
342
|
+
if (unsortedSegment.length > 1) {
|
|
343
|
+
orderedSegments.get(segmentedId).push([
|
|
344
|
+
unsortedSegments.get(segmentedId)[0]
|
|
345
|
+
]);
|
|
346
|
+
unsortedSegment.splice(0, 1);
|
|
347
|
+
index++;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (!found) {
|
|
355
|
+
// console.error(`Could not find a matching segment. Loop may not be closed. Key: ${key}`);
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
//#endregion
|
|
361
|
+
|
|
362
|
+
//#region projecting the segments to 2D
|
|
363
|
+
const projectedSegments = new Map();
|
|
364
|
+
orderedSegments.forEach((orderedSegment, key) => {
|
|
365
|
+
const arr = [];
|
|
366
|
+
for (let i = 0; i < orderedSegment.length; i++) {
|
|
367
|
+
arr.push([]);
|
|
368
|
+
orderedSegment[i].forEach((segment) => {
|
|
369
|
+
arr[i].push([
|
|
370
|
+
this._projectTo2D(segment[0], plane.dir),
|
|
371
|
+
this._projectTo2D(segment[1], plane.dir)
|
|
372
|
+
])
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
projectedSegments.set(key, arr);
|
|
376
|
+
})
|
|
377
|
+
//#endregion
|
|
378
|
+
|
|
379
|
+
//#region creating caps using earcut and then projecting them back to 3D
|
|
380
|
+
const caps = new Map();
|
|
381
|
+
let arr;
|
|
382
|
+
projectedSegments.forEach((segment, segmentId) => {
|
|
383
|
+
arr = [];
|
|
384
|
+
const loops = segment;
|
|
385
|
+
|
|
386
|
+
// Group related loops (outer boundaries with their holes)
|
|
387
|
+
|
|
388
|
+
const groupedLoops = [];
|
|
389
|
+
const used = new Set();
|
|
390
|
+
|
|
391
|
+
for (let i = 0; i < loops.length; i++) {
|
|
392
|
+
if (used.has(i)) continue;
|
|
393
|
+
|
|
394
|
+
const group = [loops[i]];
|
|
395
|
+
used.add(i);
|
|
396
|
+
|
|
397
|
+
// Check remaining loops
|
|
398
|
+
for (let j = i + 1; j < loops.length; j++) {
|
|
399
|
+
if (used.has(j)) continue;
|
|
400
|
+
|
|
401
|
+
if (this._isLoopInside(loops[i], loops[j]) ||
|
|
402
|
+
this._isLoopInside(loops[j], loops[i])) {
|
|
403
|
+
group.push(loops[j]);
|
|
404
|
+
used.add(j);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
groupedLoops.push(group);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Process each group separately
|
|
412
|
+
groupedLoops.forEach(group => {
|
|
413
|
+
// Convert the segments into a flat array of vertices and find holes
|
|
414
|
+
const vertices = [];
|
|
415
|
+
const holes = [];
|
|
416
|
+
let currentIndex = 0;
|
|
417
|
+
|
|
418
|
+
// First, determine which loop has the largest area - this will be our outer boundary
|
|
419
|
+
const areas = group.map(loop => {
|
|
420
|
+
let area = 0;
|
|
421
|
+
for (let i = 0; i < loop.length; i++) {
|
|
422
|
+
const j = (i + 1) % loop.length;
|
|
423
|
+
area += loop[i][0][0] * loop[j][0][1];
|
|
424
|
+
area -= loop[j][0][0] * loop[i][0][1];
|
|
425
|
+
}
|
|
426
|
+
return Math.abs(area) / 2;
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Find index of the loop with maximum area
|
|
430
|
+
const outerLoopIndex = areas.indexOf(Math.max(...areas));
|
|
431
|
+
|
|
432
|
+
// Add the outer boundary first
|
|
433
|
+
group[outerLoopIndex].forEach(segment => {
|
|
434
|
+
vertices.push(segment[0][0], segment[0][1]);
|
|
435
|
+
currentIndex += 2;
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// Then add all other loops as holes
|
|
439
|
+
for (let i = 0; i < group.length; i++) {
|
|
440
|
+
if (i !== outerLoopIndex) {
|
|
441
|
+
// Store the starting vertex index for this hole
|
|
442
|
+
holes.push(currentIndex / 2);
|
|
443
|
+
|
|
444
|
+
group[i].forEach(segment => {
|
|
445
|
+
vertices.push(segment[0][0], segment[0][1]);
|
|
446
|
+
currentIndex += 2;
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Triangulate using earcut
|
|
452
|
+
const triangles = earcut(vertices, holes);
|
|
453
|
+
|
|
454
|
+
// // Convert triangulated 2D points back to 3D
|
|
455
|
+
const cap3D = [];
|
|
456
|
+
|
|
457
|
+
// Process each triangle
|
|
458
|
+
for (let i = 0; i < triangles.length; i += 3) {
|
|
459
|
+
const triangle = [];
|
|
460
|
+
|
|
461
|
+
// Convert each vertex
|
|
462
|
+
for (let j = 0; j < 3; j++) {
|
|
463
|
+
const idx = triangles[i + j] * 2;
|
|
464
|
+
const point2D = [vertices[idx], vertices[idx + 1]];
|
|
465
|
+
const point3D = this._convertTo3D(point2D, plane);
|
|
466
|
+
triangle.push(point3D);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
cap3D.push(triangle);
|
|
470
|
+
}
|
|
471
|
+
arr.push(cap3D);
|
|
472
|
+
});
|
|
473
|
+
caps.set(segmentId, arr);
|
|
474
|
+
})
|
|
475
|
+
//#endregion
|
|
476
|
+
|
|
477
|
+
//#region converting caps to geometry
|
|
478
|
+
const geometryData = new Map();
|
|
479
|
+
|
|
480
|
+
caps.forEach((cap, capId) => {
|
|
481
|
+
arr = [];
|
|
482
|
+
cap.forEach(capTriangles => {
|
|
483
|
+
// Create a vertex map to reuse vertices
|
|
484
|
+
const vertexMap = new Map();
|
|
485
|
+
const vertices = [];
|
|
486
|
+
const indices = [];
|
|
487
|
+
let currentIndex = 0;
|
|
488
|
+
|
|
489
|
+
capTriangles.forEach(triangle => {
|
|
490
|
+
const triangleIndices = [];
|
|
491
|
+
|
|
492
|
+
// Process each vertex of the triangle
|
|
493
|
+
triangle.forEach(vertex => {
|
|
494
|
+
// Create a key for the vertex to check for duplicates
|
|
495
|
+
const vertexKey = `${vertex[0].toFixed(6)},${vertex[1].toFixed(6)},${vertex[2].toFixed(6)}`;
|
|
496
|
+
|
|
497
|
+
if (vertexMap.has(vertexKey)) {
|
|
498
|
+
// Reuse existing vertex
|
|
499
|
+
triangleIndices.push(vertexMap.get(vertexKey));
|
|
500
|
+
} else {
|
|
501
|
+
// Add new vertex
|
|
502
|
+
vertices.push(vertex[0], vertex[1], vertex[2]);
|
|
503
|
+
vertexMap.set(vertexKey, currentIndex);
|
|
504
|
+
triangleIndices.push(currentIndex);
|
|
505
|
+
currentIndex++;
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Add triangle indices
|
|
510
|
+
indices.push(...triangleIndices);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
arr.push({
|
|
514
|
+
positions: vertices,
|
|
515
|
+
indices: indices
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
geometryData.set(capId, arr);
|
|
520
|
+
})
|
|
521
|
+
//#endregion
|
|
522
|
+
|
|
523
|
+
//#region adding meshes to the scene
|
|
524
|
+
if(!this._prevIntersectionModelsMap[sceneModel.id])
|
|
525
|
+
this._prevIntersectionModelsMap[sceneModel.id] = new Map();
|
|
526
|
+
|
|
527
|
+
// Cache plane direction values
|
|
528
|
+
const offsetX = plane.dir[0] * 0.001;
|
|
529
|
+
const offsetY = plane.dir[1] * 0.001;
|
|
530
|
+
const offsetZ = plane.dir[2] * 0.001;
|
|
531
|
+
|
|
532
|
+
geometryData.forEach((geometries, objectId) => {
|
|
533
|
+
const meshArray = new Array(geometries.size); // Pre-allocate array with known size
|
|
534
|
+
let meshIndex = 0;
|
|
535
|
+
|
|
536
|
+
geometries.forEach((geometry, index) => {
|
|
537
|
+
const vertices = geometry.positions;
|
|
538
|
+
const indices = geometry.indices;
|
|
539
|
+
const verticesLength = vertices.length;
|
|
540
|
+
|
|
541
|
+
for (let i = 0; i < verticesLength; i += 3) {
|
|
542
|
+
vertices[i] += offsetX;
|
|
543
|
+
vertices[i + 1] += offsetY;
|
|
544
|
+
vertices[i + 2] += offsetZ;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Build normals and UVs in parallel if possible
|
|
548
|
+
const meshNormals = math.buildNormals(vertices, indices);
|
|
549
|
+
const uvs = this._createUVs(vertices, plane);
|
|
550
|
+
|
|
551
|
+
// Create mesh with transformed vertices
|
|
552
|
+
meshArray[meshIndex++] = new Mesh(this.scene, {
|
|
553
|
+
id: `${plane.id}-${objectId}-${index}`,
|
|
554
|
+
geometry: new ReadableGeometry(this.scene, {
|
|
555
|
+
primitive: 'triangles',
|
|
556
|
+
positions: vertices, // Only copy what we need
|
|
557
|
+
indices,
|
|
558
|
+
normals: meshNormals,
|
|
559
|
+
uv: uvs
|
|
560
|
+
}),
|
|
561
|
+
position: [0, 0, 0],
|
|
562
|
+
rotation: [0, 0, 0],
|
|
563
|
+
material: sceneModel.objects[objectId].capMaterial
|
|
564
|
+
});
|
|
565
|
+
})
|
|
566
|
+
if(this._prevIntersectionModelsMap[sceneModel.id].has(objectId)) {
|
|
567
|
+
this._prevIntersectionModelsMap[sceneModel.id].get(objectId).push(...meshArray)
|
|
568
|
+
}
|
|
569
|
+
else
|
|
570
|
+
this._prevIntersectionModelsMap[sceneModel.id].set(objectId, meshArray);
|
|
571
|
+
})
|
|
572
|
+
//#endregion
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
_doesPlaneIntersectBoundingBox(bb, planeEquation) {
|
|
580
|
+
const min = [bb[0], bb[1], bb[2]];
|
|
581
|
+
const max = [bb[3], bb[4], bb[5]];
|
|
582
|
+
|
|
583
|
+
const corners = [
|
|
584
|
+
[min[0], min[1], min[2]], // 000
|
|
585
|
+
[max[0], min[1], min[2]], // 100
|
|
586
|
+
[min[0], max[1], min[2]], // 010
|
|
587
|
+
[max[0], max[1], min[2]], // 110
|
|
588
|
+
[min[0], min[1], max[2]], // 001
|
|
589
|
+
[max[0], min[1], max[2]], // 101
|
|
590
|
+
[min[0], max[1], max[2]], // 011
|
|
591
|
+
[max[0], max[1], max[2]] // 111
|
|
592
|
+
]
|
|
593
|
+
|
|
594
|
+
// Calculate distance from each corner to the plane
|
|
595
|
+
let hasPositive = false;
|
|
596
|
+
let hasNegative = false;
|
|
597
|
+
|
|
598
|
+
for (const corner of corners) {
|
|
599
|
+
const distance = planeEquation.A * corner[0] +
|
|
600
|
+
planeEquation.B * corner[1] +
|
|
601
|
+
planeEquation.C * corner[2] +
|
|
602
|
+
planeEquation.D;
|
|
603
|
+
|
|
604
|
+
if (distance > 0) hasPositive = true;
|
|
605
|
+
if (distance < 0) hasNegative = true;
|
|
606
|
+
|
|
607
|
+
// If we found points on both sides, the plane intersects the box
|
|
608
|
+
if (hasPositive && hasNegative) return true;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// If all points are on the same side, no intersection
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
//not used but kept for debugging
|
|
616
|
+
_buildLines(sortedSegments) {
|
|
617
|
+
for (const key in sortedSegments) {
|
|
618
|
+
for (let i = 0; i < sortedSegments[key].length; i++) {
|
|
619
|
+
const segments = sortedSegments[key][i];
|
|
620
|
+
if (segments.length <= 0) continue;
|
|
621
|
+
segments.forEach((segment, index) => {
|
|
622
|
+
new Mesh(this.scene, {
|
|
623
|
+
clippable: false,
|
|
624
|
+
geometry: new ReadableGeometry(this.scene, buildLineGeometry({
|
|
625
|
+
startPoint: segment[0],
|
|
626
|
+
endPoint: segment[1],
|
|
627
|
+
})),
|
|
628
|
+
material: new PhongMaterial(this.scene, {
|
|
629
|
+
emissive: [1, 0, 0]
|
|
630
|
+
})
|
|
631
|
+
});
|
|
632
|
+
})
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
_projectTo2D(point, normal) {
|
|
639
|
+
let u;
|
|
640
|
+
if (Math.abs(normal[0]) > Math.abs(normal[1]))
|
|
641
|
+
u = [-normal[2], 0, normal[0]];
|
|
642
|
+
else
|
|
643
|
+
u = [0, normal[2], -normal[1]];
|
|
644
|
+
|
|
645
|
+
u = math.normalizeVec3(u);
|
|
646
|
+
const normalTemp = math.vec3(normal);
|
|
647
|
+
const cross = math.cross3Vec3(normalTemp, u)
|
|
648
|
+
const v = math.normalizeVec3(cross);
|
|
649
|
+
const x = math.dotVec3(point, u);
|
|
650
|
+
const y = math.dotVec3(point, v);
|
|
651
|
+
|
|
652
|
+
return [x, y]
|
|
653
|
+
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
_isLoopInside(loop1, loop2) {
|
|
657
|
+
// Simple point-in-polygon test using the first point of loop1
|
|
658
|
+
const point = loop1[0][0]; // First point of first segment
|
|
659
|
+
let inside = false;
|
|
660
|
+
for (let i = 0, j = loop2.length - 1; i < loop2.length; j = i++) {
|
|
661
|
+
const xi = loop2[i][0][0], yi = loop2[i][0][1];
|
|
662
|
+
const xj = loop2[j][0][0], yj = loop2[j][0][1];
|
|
663
|
+
|
|
664
|
+
const intersect = ((yi > point[1]) !== (yj > point[1]))
|
|
665
|
+
&& (point[0] < (xj - xi) * (point[1] - yi) / (yj - yi) + xi);
|
|
666
|
+
|
|
667
|
+
if (intersect) inside = !inside;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return inside;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
_convertTo3D(point2D, plane) {
|
|
674
|
+
// Reconstruct the same basis vectors used in _projectTo2D
|
|
675
|
+
let u, normal = plane.dir, planePosition = plane.pos;
|
|
676
|
+
if (Math.abs(normal[0]) > Math.abs(normal[1])) {
|
|
677
|
+
u = [-normal[2], 0, normal[0]];
|
|
678
|
+
} else {
|
|
679
|
+
u = [0, normal[2], -normal[1]];
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
u = math.normalizeVec3(u);
|
|
683
|
+
const normalTemp = math.vec3(normal);
|
|
684
|
+
const cross = math.cross3Vec3(normalTemp, u);
|
|
685
|
+
const v = math.normalizeVec3(cross);
|
|
686
|
+
|
|
687
|
+
// Reconstruct 3D point using the basis vectors
|
|
688
|
+
const x = point2D[0];
|
|
689
|
+
const y = point2D[1];
|
|
690
|
+
const result = [
|
|
691
|
+
u[0] * x + v[0] * y,
|
|
692
|
+
u[1] * x + v[1] * y,
|
|
693
|
+
u[2] * x + v[2] * y
|
|
694
|
+
];
|
|
695
|
+
|
|
696
|
+
// Project the point onto the cutting plane
|
|
697
|
+
|
|
698
|
+
const t = math.dotVec3(normal, [
|
|
699
|
+
planePosition[0] - result[0],
|
|
700
|
+
planePosition[1] - result[1],
|
|
701
|
+
planePosition[2] - result[2]
|
|
702
|
+
]);
|
|
703
|
+
|
|
704
|
+
return [
|
|
705
|
+
result[0] + normal[0] * t,
|
|
706
|
+
result[1] + normal[1] * t,
|
|
707
|
+
result[2] + normal[2] * t
|
|
708
|
+
];
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
_deletePreviousModels() {
|
|
712
|
+
|
|
713
|
+
for(const sceneModelId in this._prevIntersectionModelsMap) {
|
|
714
|
+
const objects = this._prevIntersectionModelsMap[sceneModelId];
|
|
715
|
+
objects.forEach((value, objectId) => {
|
|
716
|
+
if(this._dirtyMap[sceneModelId].get(objectId)) {
|
|
717
|
+
value.forEach((mesh) => {
|
|
718
|
+
mesh.destroy();
|
|
719
|
+
})
|
|
720
|
+
this._prevIntersectionModelsMap[sceneModelId].delete(objectId);
|
|
721
|
+
}
|
|
722
|
+
})
|
|
723
|
+
if(this._prevIntersectionModelsMap[sceneModelId].size <= 0)
|
|
724
|
+
delete this._prevIntersectionModelsMap[sceneModelId];
|
|
725
|
+
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
_createUVs(vertices, plane) {
|
|
731
|
+
const O = plane.pos;
|
|
732
|
+
const D = tempVec3a;
|
|
733
|
+
D.set(plane.dir);
|
|
734
|
+
math.normalizeVec3(D);
|
|
735
|
+
const P = tempVec3b;
|
|
736
|
+
|
|
737
|
+
const uvs = [ ];
|
|
738
|
+
for (let i = 0; i < vertices.length; i += 3) {
|
|
739
|
+
P[0] = vertices[i];
|
|
740
|
+
P[1] = vertices[i + 1];
|
|
741
|
+
P[2] = vertices[i + 2];
|
|
742
|
+
|
|
743
|
+
// Project P onto the plane
|
|
744
|
+
const OP = math.subVec3(P, O, tempVec3c);
|
|
745
|
+
const dist = math.dotVec3(OP, D);
|
|
746
|
+
math.subVec3(P, math.mulVec3Scalar(D, dist, tempVec3c), P);
|
|
747
|
+
|
|
748
|
+
const right = ((Math.abs(math.dotVec3(D, worldUp)) < 0.999)
|
|
749
|
+
? math.cross3Vec3(D, worldUp, tempVec3c)
|
|
750
|
+
: worldRight);
|
|
751
|
+
const v = math.cross3Vec3(D, right, tempVec3c);
|
|
752
|
+
math.normalizeVec3(v, v);
|
|
753
|
+
|
|
754
|
+
const OP_proj = math.subVec3(P, O, P);
|
|
755
|
+
uvs.push(
|
|
756
|
+
math.dotVec3(OP_proj, math.normalizeVec3(math.cross3Vec3(v, D, tempVec3d))),
|
|
757
|
+
math.dotVec3(OP_proj, v));
|
|
758
|
+
}
|
|
759
|
+
return uvs;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
destroy() {
|
|
763
|
+
this._deletePreviousModels();
|
|
764
|
+
if(this._resourcesAllocated) {
|
|
765
|
+
this.scene.off(this._onModelLoaded);
|
|
766
|
+
this.scene.off(this._onModelUnloaded);
|
|
767
|
+
this.scene.off(this._onSectionPlaneCreated);
|
|
768
|
+
this.scene.off(this._onTick);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
export { SectionCaps };
|