@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.
Files changed (46) hide show
  1. package/{LICENSE.txt → LICENSE} +30 -27
  2. package/README.md +13 -6
  3. package/dist/xeokit-sdk.cjs.js +50322 -25848
  4. package/dist/xeokit-sdk.es.js +50322 -25848
  5. package/dist/xeokit-sdk.es5.js +7982 -4807
  6. package/dist/xeokit-sdk.min.cjs.js +5 -5
  7. package/dist/xeokit-sdk.min.es.js +5 -5
  8. package/dist/xeokit-sdk.min.es5.js +4 -4
  9. package/package.json +34 -32
  10. package/src/extras/PointerCircle/PointerCircle.js +1 -1
  11. package/src/plugins/AnnotationsPlugin/Annotation.js +45 -5
  12. package/src/plugins/CityJSONLoaderPlugin/CityJSONLoaderPlugin.js +1 -1
  13. package/src/plugins/GLTFLoaderPlugin/GLTFSceneModelLoader.js +3 -2
  14. package/src/plugins/LASLoaderPlugin/LASLoaderPlugin.js +1 -1
  15. package/src/plugins/SectionPlanesPlugin/Control.js +11 -12
  16. package/src/plugins/XKTLoaderPlugin/XKTLoaderPlugin.js +57 -2
  17. package/src/plugins/XKTLoaderPlugin/parsers/ParserV10.js +6 -0
  18. package/src/plugins/XKTLoaderPlugin/parsers/ParserV11.js +6 -0
  19. package/src/plugins/XKTLoaderPlugin/parsers/ParserV12.js +822 -0
  20. package/src/plugins/XKTLoaderPlugin/parsers/ParserV6.js +6 -0
  21. package/src/plugins/XKTLoaderPlugin/parsers/ParserV7.js +6 -0
  22. package/src/plugins/XKTLoaderPlugin/parsers/ParserV8.js +6 -0
  23. package/src/plugins/XKTLoaderPlugin/parsers/ParserV9.js +6 -0
  24. package/src/{plugins/lib → viewer/scene/libs}/earcut.js +194 -189
  25. package/src/viewer/scene/materials/EmphasisMaterial.js +5 -1
  26. package/src/viewer/scene/math/math.js +6 -2
  27. package/src/viewer/scene/model/SceneModel.js +1 -0
  28. package/src/viewer/scene/model/SceneModelEntity.js +26 -0
  29. package/src/viewer/scene/model/SceneModelMesh.js +4 -0
  30. package/src/viewer/scene/model/dtx/triangles/DTXTrianglesLayer.js +1 -1
  31. package/src/viewer/scene/model/vbo/batching/triangles/VBOBatchingTrianglesLayer.js +1 -1
  32. package/src/viewer/scene/model/vbo/batching/triangles/renderers/TrianglesColorRenderer.js +4 -1
  33. package/src/viewer/scene/model/vbo/instancing/triangles/VBOInstancingTrianglesLayer.js +1 -1
  34. package/src/viewer/scene/model/vbo/instancing/triangles/renderers/TrianglesColorRenderer.js +4 -1
  35. package/src/viewer/scene/scene/Scene.js +50 -6
  36. package/src/viewer/scene/sectionCaps/SectionCaps.js +774 -0
  37. package/src/viewer/scene/sectionCaps/index.js +1 -0
  38. package/src/viewer/scene/sectionPlane/SectionPlane.js +79 -1
  39. package/src/viewer/scene/webgl/Renderer.js +19 -10
  40. package/types/extras/PointerCircle/PointerCircle.d.ts +50 -0
  41. package/types/extras/PointerCircle/index.d.ts +1 -0
  42. package/types/plugins/WebIFCLoaderPlugin/WebIFCLoaderPlugin.d.ts +2 -1
  43. package/types/plugins/XKTLoaderPlugin/XKTLoaderPlugin.d.ts +28 -0
  44. package/types/plugins/index.d.ts +1 -0
  45. package/types/plugins/lib/ui/index.d.ts +12 -0
  46. 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 };