@inweb/viewer-three 27.4.7 → 27.6.0

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 (94) hide show
  1. package/dist/extensions/components/AxesHelperComponent.js +3 -0
  2. package/dist/extensions/components/AxesHelperComponent.js.map +1 -1
  3. package/dist/extensions/components/AxesHelperComponent.min.js +1 -1
  4. package/dist/extensions/components/AxesHelperComponent.module.js +3 -0
  5. package/dist/extensions/components/AxesHelperComponent.module.js.map +1 -1
  6. package/dist/extensions/components/ExtentsHelperComponent.js +6 -2
  7. package/dist/extensions/components/ExtentsHelperComponent.js.map +1 -1
  8. package/dist/extensions/components/ExtentsHelperComponent.min.js +1 -1
  9. package/dist/extensions/components/ExtentsHelperComponent.module.js +6 -2
  10. package/dist/extensions/components/ExtentsHelperComponent.module.js.map +1 -1
  11. package/dist/extensions/components/GridHelperComponent.js +1 -0
  12. package/dist/extensions/components/GridHelperComponent.js.map +1 -1
  13. package/dist/extensions/components/GridHelperComponent.min.js +1 -1
  14. package/dist/extensions/components/GridHelperComponent.module.js +1 -0
  15. package/dist/extensions/components/GridHelperComponent.module.js.map +1 -1
  16. package/dist/extensions/components/LightHelperComponent.js +2 -1
  17. package/dist/extensions/components/LightHelperComponent.js.map +1 -1
  18. package/dist/extensions/components/LightHelperComponent.min.js +1 -1
  19. package/dist/extensions/components/LightHelperComponent.module.js +2 -1
  20. package/dist/extensions/components/LightHelperComponent.module.js.map +1 -1
  21. package/dist/extensions/components/StatsPanelComponent.js +0 -1
  22. package/dist/extensions/components/StatsPanelComponent.js.map +1 -1
  23. package/dist/extensions/components/StatsPanelComponent.min.js +1 -1
  24. package/dist/extensions/components/StatsPanelComponent.module.js +0 -1
  25. package/dist/extensions/components/StatsPanelComponent.module.js.map +1 -1
  26. package/dist/extensions/loaders/GLTFCloudLoader.js +7 -2
  27. package/dist/extensions/loaders/GLTFCloudLoader.js.map +1 -1
  28. package/dist/extensions/loaders/GLTFCloudLoader.min.js +1 -1
  29. package/dist/extensions/loaders/GLTFCloudLoader.module.js +7 -2
  30. package/dist/extensions/loaders/GLTFCloudLoader.module.js.map +1 -1
  31. package/dist/extensions/loaders/GLTFFileLoader.js +2 -1
  32. package/dist/extensions/loaders/GLTFFileLoader.js.map +1 -1
  33. package/dist/extensions/loaders/GLTFFileLoader.min.js +1 -1
  34. package/dist/extensions/loaders/GLTFFileLoader.module.js +2 -1
  35. package/dist/extensions/loaders/GLTFFileLoader.module.js.map +1 -1
  36. package/dist/extensions/loaders/IFCXLoader.js +10 -5
  37. package/dist/extensions/loaders/IFCXLoader.js.map +1 -1
  38. package/dist/extensions/loaders/IFCXLoader.min.js +1 -1
  39. package/dist/extensions/loaders/IFCXLoader.module.js +10 -5
  40. package/dist/extensions/loaders/IFCXLoader.module.js.map +1 -1
  41. package/dist/viewer-three.js +1901 -569
  42. package/dist/viewer-three.js.map +1 -1
  43. package/dist/viewer-three.min.js +4 -4
  44. package/dist/viewer-three.module.js +1366 -451
  45. package/dist/viewer-three.module.js.map +1 -1
  46. package/extensions/components/AxesHelperComponent.ts +3 -0
  47. package/extensions/components/ExtentsHelperComponent.ts +5 -2
  48. package/extensions/components/GridHelperComponent.ts +1 -0
  49. package/extensions/components/LightHelperComponent.ts +2 -1
  50. package/extensions/components/StatsPanelComponent.ts +0 -1
  51. package/extensions/loaders/GLTFCloudLoader.ts +8 -2
  52. package/extensions/loaders/GLTFFileLoader.ts +3 -2
  53. package/extensions/loaders/IFCX/IFCXFileLoader.ts +11 -5
  54. package/lib/Viewer/Viewer.d.ts +6 -8
  55. package/lib/Viewer/components/CameraComponent.d.ts +1 -1
  56. package/lib/Viewer/components/ClippingPlaneComponent.d.ts +8 -0
  57. package/lib/Viewer/components/HighlighterComponent.d.ts +2 -2
  58. package/lib/Viewer/components/InfoComponent.d.ts +1 -1
  59. package/lib/Viewer/components/SectionsComponent.d.ts +15 -0
  60. package/lib/Viewer/components/WCSHelperComponent.d.ts +2 -2
  61. package/lib/Viewer/draggers/CuttingPlaneDragger.d.ts +6 -6
  62. package/lib/Viewer/draggers/OrbitDragger.d.ts +1 -1
  63. package/lib/Viewer/measurement/Snapper.d.ts +4 -4
  64. package/package.json +5 -5
  65. package/src/Viewer/Viewer.ts +59 -48
  66. package/src/Viewer/commands/GetSelected2.ts +1 -1
  67. package/src/Viewer/commands/SetSelected.ts +1 -1
  68. package/src/Viewer/commands/index.ts +1 -1
  69. package/src/Viewer/components/BackgroundComponent.ts +2 -1
  70. package/src/Viewer/components/CameraComponent.ts +6 -7
  71. package/src/Viewer/components/CanvasRemoveComponent.ts +0 -1
  72. package/src/Viewer/{scenes/Helpers.ts → components/ClippingPlaneComponent.ts} +22 -12
  73. package/src/Viewer/components/HighlighterComponent.ts +9 -5
  74. package/src/Viewer/components/HighlighterUtils.ts +2 -2
  75. package/src/Viewer/components/InfoComponent.ts +4 -4
  76. package/src/Viewer/components/SectionsComponent.ts +119 -0
  77. package/src/Viewer/components/SelectionComponent.ts +5 -3
  78. package/src/Viewer/components/WCSHelperComponent.ts +8 -6
  79. package/src/Viewer/components/index.ts +4 -0
  80. package/src/Viewer/draggers/CuttingPlaneDragger.ts +57 -34
  81. package/src/Viewer/draggers/MeasureLineDragger.ts +1 -1
  82. package/src/Viewer/draggers/OrbitDragger.ts +3 -3
  83. package/src/Viewer/helpers/SectionsHelper.js +1061 -0
  84. package/src/Viewer/helpers/WCSHelper.ts +31 -5
  85. package/src/Viewer/loaders/DynamicGltfLoader/DynamicGltfLoader.js +417 -92
  86. package/src/Viewer/loaders/DynamicGltfLoader/DynamicModelImpl.ts +19 -14
  87. package/src/Viewer/loaders/DynamicGltfLoader/GltfStructure.js +76 -9
  88. package/src/Viewer/loaders/GLTFBinaryParser.ts +2 -2
  89. package/src/Viewer/loaders/GLTFCloudDynamicLoader.ts +3 -2
  90. package/src/Viewer/loaders/GLTFFileDynamicLoader.ts +6 -4
  91. package/src/Viewer/measurement/Snapper.ts +6 -7
  92. package/src/Viewer/models/ModelImpl.ts +65 -28
  93. package/lib/Viewer/scenes/Helpers.d.ts +0 -7
  94. package/src/Viewer/postprocessing/SSAARenderPass.js +0 -245
@@ -0,0 +1,1061 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2002-2026, Open Design Alliance (the "Alliance").
3
+ // All rights reserved.
4
+ //
5
+ // This software and its documentation and related materials are owned by
6
+ // the Alliance. The software may only be incorporated into application
7
+ // programs owned by members of the Alliance, subject to a signed
8
+ // Membership Agreement and Supplemental Software License Agreement with the
9
+ // Alliance. The structure and organization of this software are the valuable
10
+ // trade secrets of the Alliance and its suppliers. The software is also
11
+ // protected by copyright law and international treaty provisions. Application
12
+ // programs incorporating this software must include the following statement
13
+ // with their copyright notices:
14
+ //
15
+ // This application incorporates Open Design Alliance software pursuant to a
16
+ // license agreement with Open Design Alliance.
17
+ // Open Design Alliance Copyright (C) 2002-2026 by Open Design Alliance.
18
+ // All rights reserved.
19
+ //
20
+ // By use of this software, its documentation or related materials, you
21
+ // acknowledge and accept the above terms.
22
+ ///////////////////////////////////////////////////////////////////////////////
23
+
24
+ import {
25
+ Box3,
26
+ BufferGeometry,
27
+ Color,
28
+ DoubleSide,
29
+ Float32BufferAttribute,
30
+ Mesh,
31
+ Object3D,
32
+ Points,
33
+ PointsMaterial,
34
+ ShaderMaterial,
35
+ ShapeUtils,
36
+ Sphere,
37
+ Vector2,
38
+ Vector3,
39
+ } from "three";
40
+ import { LineSegmentsGeometry } from "three/examples/jsm/lines/LineSegmentsGeometry.js";
41
+ import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
42
+ import { LineSegments2 } from "three/examples/jsm/lines/LineSegments2.js";
43
+
44
+ /**
45
+ * Spatial Hash Grid class. Used for merging vertices that are practically at the same coordinate (within
46
+ * the given tolerance). This is critical for eliminating gaps caused by floating-point precision
47
+ * errors.
48
+ */
49
+ class PointHashGrid {
50
+ constructor(tolerance = 1e-5) {
51
+ // Tolerance dictates how close two points must be to be considered identical
52
+ this.tolerance = tolerance;
53
+
54
+ // Array holding the actual Three.Vector3 objects
55
+ this.points = [];
56
+
57
+ // Map storing cell string keys to an array of point indices
58
+ this.grid = new Map();
59
+ }
60
+
61
+ // Creates a unique string key based on integer cell coordinates
62
+ _hash(hx, hy, hz) {
63
+ return `${hx},${hy},${hz}`;
64
+ }
65
+
66
+ // Adds a vector to the grid, or returns the index of an existing identical vector
67
+ add(v) {
68
+ // Divide coordinates by tolerance and round to get integer grid coordinates
69
+ const hx = Math.round(v.x / this.tolerance);
70
+ const hy = Math.round(v.y / this.tolerance);
71
+ const hz = Math.round(v.z / this.tolerance);
72
+
73
+ // Search the current cell and all 26 adjacent cells to ensure
74
+ // we catch points that fall exactly on cell boundaries
75
+ for (let i = -1; i <= 1; i++) {
76
+ for (let j = -1; j <= 1; j++) {
77
+ for (let k = -1; k <= 1; k++) {
78
+ // Calculate the hash key for the neighbor cell
79
+ const hash = this._hash(hx + i, hy + j, hz + k);
80
+
81
+ // Retrieve the cell array from the grid
82
+ const cell = this.grid.get(hash);
83
+
84
+ if (cell) {
85
+ // Iterate over point IDs present in this cell
86
+ for (const id of cell) {
87
+ // If distance is within tolerance, it's a duplicate. Return the existing ID.
88
+ if (this.points[id].distanceTo(v) <= this.tolerance) return id;
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ // If point does not exist, assign a new index based on array length
96
+ const id = this.points.length;
97
+
98
+ // Clone the vector to prevent reference mutations and push to array
99
+ this.points.push(v.clone());
100
+
101
+ // Get the hash for the central cell
102
+ const centerHash = this._hash(hx, hy, hz);
103
+
104
+ // Initialize an empty array for the cell if it doesn't exist
105
+ if (!this.grid.has(centerHash)) this.grid.set(centerHash, []);
106
+
107
+ // Push the new point ID into the respective cell
108
+ this.grid.get(centerHash).push(id);
109
+
110
+ return id;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Sections Helper Class Manages all meshes related to clipping planes: Fill (Caps), Outlines, and Debug
116
+ * Points
117
+ */
118
+ class SectionsHelper extends Object3D {
119
+ constructor() {
120
+ super();
121
+
122
+ this.type = "SectionsHelper";
123
+
124
+ // Configuration flags for visual settings
125
+ this.flags = {
126
+ fillEnabled: true,
127
+ fillColor: "#fffde7",
128
+ hatchEnabled: true,
129
+ hatchColor: "#000000",
130
+ hatchScale: 8.0,
131
+ outlineEnabled: true,
132
+ outlineColor: "#000000",
133
+ outlineWidth: 2,
134
+ boundaryOnly: true,
135
+ showDebugSeams: false,
136
+ showDebugPoints: false,
137
+ showDebugSegments: false,
138
+ showDebugGaps: false,
139
+ showDebugInfo: false,
140
+ useObjFillColor: false,
141
+ useObjOutlineColor: false,
142
+ };
143
+
144
+ // Arrays storing corresponding meshes for each clipping plane
145
+ this._caps = [];
146
+ this._outlines = [];
147
+ this._debugPoints = [];
148
+ this._debugSegments = [];
149
+ this._debugGaps = [];
150
+
151
+ // Pre-allocated vectors to prevent excessive object instantiation during render loops
152
+ this._vA = new Vector3();
153
+ this._vB = new Vector3();
154
+ this._vC = new Vector3();
155
+
156
+ // Pre-allocated bounding box object
157
+ this._worldBox = new Box3();
158
+ }
159
+
160
+ // Cleanup method to properly dispose of geometries and materials and free GPU memory
161
+ dispose() {
162
+ // Helper arrow function to dispose an individual item
163
+ const disposeMesh = (item) => {
164
+ if (item.geometry) item.geometry.dispose();
165
+ if (item.material) item.material.dispose();
166
+ this.remove(item);
167
+ };
168
+
169
+ // Execute cleanup for all mesh arrays
170
+ this._caps.forEach(disposeMesh);
171
+ this._outlines.forEach(disposeMesh);
172
+ this._debugPoints.forEach(disposeMesh);
173
+ this._debugSegments.forEach(disposeMesh);
174
+ this._debugGaps.forEach(disposeMesh);
175
+
176
+ // Clear the internal array references
177
+ this._caps.length = 0;
178
+ this._outlines.length = 0;
179
+ this._debugPoints.length = 0;
180
+ this._debugSegments.length = 0;
181
+ this._debugGaps.length = 0;
182
+ }
183
+
184
+ // Update the resolution uniform for fat lines when viewport changes
185
+ setSize(width, height) {
186
+ this._outlines.forEach((o) => {
187
+ if (o.material.resolution) o.material.resolution.set(width, height);
188
+ });
189
+
190
+ this._debugSegments.forEach((s) => {
191
+ if (s.material.resolution) s.material.resolution.set(width, height);
192
+ });
193
+ }
194
+
195
+ // Creates necessary helper meshes based on the amount of active clipping planes
196
+ _ensureHelpersCount(count) {
197
+ // Custom vertex shader for caps rendering
198
+ // Extracts screen-space position into world-space format to allow steady hatch drawing
199
+ const hatchVertexShader = `
200
+ #include <common>
201
+ #include <logdepthbuf_pars_vertex>
202
+ #include <clipping_planes_pars_vertex>
203
+
204
+ attribute float aHatchDir;
205
+ attribute vec3 aFillColor;
206
+
207
+ varying vec3 vWP;
208
+ varying float vHatchDir;
209
+ varying vec3 vFillColor;
210
+
211
+ void main() {
212
+ vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
213
+ #include <clipping_planes_vertex>
214
+
215
+ vWP = (modelMatrix * vec4(position, 1.0)).xyz;
216
+ vHatchDir = aHatchDir;
217
+ vFillColor = aFillColor;
218
+
219
+ gl_Position = projectionMatrix * mvPosition;
220
+ #include <logdepthbuf_vertex>
221
+ }
222
+ `;
223
+
224
+ // Custom fragment shader for caps rendering
225
+ // Uses modulo arithmetic to draw alternating hatch lines independent of zoom level
226
+ const hatchFragmentShader = `
227
+ #include <common>
228
+ #include <logdepthbuf_pars_fragment>
229
+ #include <clipping_planes_pars_fragment>
230
+
231
+ uniform vec3 lineColor;
232
+ uniform float uHatchScale;
233
+ uniform float uHatchEnabled;
234
+
235
+ varying vec3 vWP;
236
+ varying float vHatchDir;
237
+ varying vec3 vFillColor;
238
+
239
+ void main() {
240
+ #include <clipping_planes_fragment>
241
+ #include <logdepthbuf_fragment>
242
+
243
+ float v1 = mod(gl_FragCoord.x + gl_FragCoord.y, uHatchScale);
244
+ float v2 = mod(gl_FragCoord.x - gl_FragCoord.y, uHatchScale);
245
+ float v = mix(v1, v2, vHatchDir);
246
+
247
+ float hatchMask = step(v, 1.5) * uHatchEnabled;
248
+ gl_FragColor = vec4(mix(vFillColor, lineColor, hatchMask), 1.0);
249
+ }
250
+ `;
251
+
252
+ // Generate new meshes if the active plane count exceeds our pool
253
+ while (this._caps.length < count) {
254
+ // Cap material defining the cross-section surface
255
+ const capMat = new ShaderMaterial({
256
+ uniforms: {
257
+ lineColor: { value: new Color() },
258
+ uHatchScale: { value: 10.0 },
259
+ uHatchEnabled: { value: 1.0 },
260
+ },
261
+ vertexShader: hatchVertexShader,
262
+ fragmentShader: hatchFragmentShader,
263
+ side: DoubleSide,
264
+ clipping: true,
265
+ depthTest: true,
266
+ // STABLE LOGIC: Disabling depth write forces the fill to render,
267
+ // but not push to Z-buffer, allowing Outline meshes (rendered later) to naturally
268
+ // sit perfectly on top without struggling with Z-fighting.
269
+ depthWrite: false,
270
+ });
271
+
272
+ const capMesh = new Mesh(new BufferGeometry(), capMat);
273
+ // Render fills early
274
+ capMesh.renderOrder = 5;
275
+ this.add(capMesh);
276
+ this._caps.push(capMesh);
277
+
278
+ // Material for the thick intersection contour
279
+ const lineMat = new LineMaterial({
280
+ color: 0xffffff,
281
+ linewidth: 2,
282
+ resolution: new Vector2(window.innerWidth, window.innerHeight),
283
+ depthTest: true,
284
+ clipping: true,
285
+ vertexColors: true,
286
+ });
287
+
288
+ const lineObj = new LineSegments2(new LineSegmentsGeometry(), lineMat);
289
+ // Render outlines late so they sit atop the non-depth-writing fill mesh
290
+ lineObj.renderOrder = 100;
291
+ this.add(lineObj);
292
+ this._outlines.push(lineObj);
293
+
294
+ // Material for rendering discrete intersection points (Debugging)
295
+ const ptsMat = new PointsMaterial({
296
+ color: 0x00aaff,
297
+ size: 6,
298
+ sizeAttenuation: false,
299
+ depthTest: false,
300
+ transparent: true,
301
+ depthWrite: false,
302
+ });
303
+
304
+ const pointsObj = new Points(new BufferGeometry(), ptsMat);
305
+ pointsObj.renderOrder = 200;
306
+ this.add(pointsObj);
307
+ this._debugPoints.push(pointsObj);
308
+
309
+ // Material for rendering discrete unconnected intersection segments (Debugging)
310
+ const debugSegMat = new LineMaterial({
311
+ color: 0x00ff00,
312
+ linewidth: 4,
313
+ resolution: new Vector2(window.innerWidth, window.innerHeight),
314
+ depthTest: false,
315
+ transparent: true,
316
+ depthWrite: false,
317
+ clipping: true,
318
+ });
319
+
320
+ const debugSegObj = new LineSegments2(new LineSegmentsGeometry(), debugSegMat);
321
+ debugSegObj.renderOrder = 150;
322
+ this.add(debugSegObj);
323
+ this._debugSegments.push(debugSegObj);
324
+
325
+ // Material to display failure points where loops didn't close (Debugging)
326
+ const gapPtsMat = new PointsMaterial({
327
+ size: 6,
328
+ sizeAttenuation: false,
329
+ depthTest: false,
330
+ transparent: true,
331
+ depthWrite: false,
332
+ vertexColors: true,
333
+ });
334
+
335
+ const gapsObj = new Points(new BufferGeometry(), gapPtsMat);
336
+ gapsObj.renderOrder = 250;
337
+ this.add(gapsObj);
338
+ this._debugGaps.push(gapsObj);
339
+ }
340
+
341
+ // If planes were disabled, hide their associated meshes from the scene
342
+ for (let i = count; i < this._caps.length; i++) {
343
+ this._caps[i].visible = false;
344
+ this._outlines[i].visible = false;
345
+ this._debugPoints[i].visible = false;
346
+ this._debugSegments[i].visible = false;
347
+ this._debugGaps[i].visible = false;
348
+ }
349
+ }
350
+
351
+ // Main update loop processing geometry intersections
352
+ update(objects, extents, planes) {
353
+ const t0 = performance.now();
354
+
355
+ // Initialize the correct amount of helper meshes
356
+ this._ensureHelpersCount(planes.length);
357
+
358
+ // Exit early if no planes are active
359
+ if (planes.length === 0) return;
360
+
361
+ // Determine mathematical bounding radius
362
+ const sphere = extents.getBoundingSphere(new Sphere());
363
+ const globalRadius = Math.max(sphere.radius, 1e-3);
364
+
365
+ // Bias prevents geometry cut surfaces and generated caps from competing mathematically
366
+ const clippingBias = globalRadius * 1e-4;
367
+
368
+ // Prepare a slightly shifted copy of planes specifically for clipping the caps
369
+ const biasedPlanes = planes.map((p) => {
370
+ const bp = p.clone();
371
+ bp.constant += clippingBias;
372
+ return bp;
373
+ });
374
+
375
+ // Gather all relevant mesh objects from the scene hierarchy
376
+ const targetMeshes = [];
377
+ objects.forEach((obj) => {
378
+ if (obj.isMesh && obj.material) {
379
+ const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
380
+ if (mats.some((m) => m.clippingPlanes)) targetMeshes.push(obj);
381
+ }
382
+ });
383
+
384
+ // Iterate over every active clipping plane
385
+ planes.forEach((plane, pIdx) => {
386
+ // Fetch corresponding helper objects
387
+ const capMesh = this._caps[pIdx];
388
+ const outlineMesh = this._outlines[pIdx];
389
+ const debugPtsMesh = this._debugPoints[pIdx];
390
+ const debugSegsMesh = this._debugSegments[pIdx];
391
+ const debugGapsMesh = this._debugGaps[pIdx];
392
+
393
+ // Cap shader writes the values straight into the framebuffer, so we need
394
+ // to bypass Three.js's sRGB→linear color management and push sRGB values
395
+ // to the GPU because the Viewer uses LinearSRGBColorSpace.
396
+ const hatchColor = new Color(this.flags.hatchColor);
397
+ hatchColor.convertLinearToSRGB();
398
+
399
+ // Set uniforms
400
+ capMesh.material.uniforms.lineColor.value.set(hatchColor);
401
+ capMesh.material.uniforms.uHatchScale.value = this.flags.hatchScale;
402
+ capMesh.material.uniforms.uHatchEnabled.value = this.flags.hatchEnabled ? 1.0 : 0.0;
403
+ outlineMesh.material.linewidth = this.flags.outlineWidth;
404
+
405
+ // Exclude current plane from array to avoid self-clipping
406
+ const otherBiasedPlanes = biasedPlanes.filter((_, i) => i !== pIdx);
407
+
408
+ capMesh.material.clippingPlanes = otherBiasedPlanes;
409
+ outlineMesh.material.clippingPlanes = otherBiasedPlanes;
410
+ debugSegsMesh.material.clippingPlanes = otherBiasedPlanes;
411
+
412
+ // Establish 2D Local Space on the plane surface using Cross Products
413
+ const n = plane.normal;
414
+ const planeOrigin = n.clone().multiplyScalar(-plane.constant);
415
+
416
+ const up = new Vector3(0, 1, 0);
417
+
418
+ // Fallback 'up' vector if plane is completely horizontal
419
+ if (Math.abs(n.dot(up)) > 0.999) up.set(1, 0, 0);
420
+
421
+ // Create U and V axis vectors laying perfectly flat on the clipping plane
422
+ const uAxis = new Vector3().crossVectors(up, n).normalize();
423
+ const vAxis = new Vector3().crossVectors(n, uAxis).normalize();
424
+
425
+ // Initialize arrays collecting vertex data across all intercepted meshes
426
+ const positions = [];
427
+ const indices = [];
428
+ const hatchDirs = [];
429
+ const fillColors = [];
430
+ const combinedOutlinePoints = [];
431
+ const combinedOutlineColors = [];
432
+ const rawPts = [];
433
+ const rawSegs = [];
434
+ const rawGaps = [];
435
+ const rawGapColors = [];
436
+
437
+ // Scan every object in the model to see if it intercepts this plane
438
+ targetMeshes.forEach((mesh, meshIndex) => {
439
+ // Compute bounding structures if missing
440
+ if (!mesh.geometry.boundingBox) mesh.geometry.computeBoundingBox();
441
+ if (!mesh.geometry.boundingSphere) mesh.geometry.computeBoundingSphere();
442
+
443
+ // Execute broad-phase culling
444
+ this._worldBox.copy(mesh.geometry.boundingBox).applyMatrix4(mesh.matrixWorld);
445
+ if (!plane.intersectsBox(this._worldBox)) return;
446
+
447
+ // Compute contextual tolerances based on scale
448
+ const localScale = new Vector3().setFromMatrixScale(mesh.matrixWorld);
449
+ const maxScale = Math.max(localScale.x, localScale.y, localScale.z);
450
+ const localRadius = Math.max(mesh.geometry.boundingSphere.radius * maxScale, 1e-3);
451
+
452
+ // Adaptive tolerances to maintain mathematical precision
453
+ const localHashTolerance = Math.max(localRadius * 1e-4, 1e-6);
454
+ const localEps = Math.max(localRadius * 1e-5, 1e-7);
455
+
456
+ // Capture object color to optionally apply to the cross section
457
+ const baseColor = new Color(0xffffff);
458
+ const om = mesh.userData.originalMaterial; // <- highlighted object
459
+ const mm = om ?? (Array.isArray(mesh.material) ? mesh.material[0] : mesh.material);
460
+ if (mm.color) baseColor.copy(mm.color);
461
+
462
+ // Darken base colors slightly for better contrast on section faces
463
+ const objFillColor = baseColor.clone().lerp(new Color(0x000000), 0.2);
464
+ const objOutlineColor = baseColor.clone().lerp(new Color(0x000000), 0.85);
465
+
466
+ // Create distinct color ID for debug views
467
+ const hue = ((meshIndex * 137.5) % 360) / 360;
468
+ const meshGapColor = new Color().setHSL(hue, 1.0, 0.5);
469
+
470
+ // Alternate hatching direction index
471
+ const currentHatchDir = meshIndex % 2 === 0 ? 0.0 : 1.0;
472
+
473
+ // Map desired color based on user UI flag.
474
+ const fillColor = this.flags.useObjFillColor ? objFillColor : new Color(this.flags.fillColor);
475
+ const outlineColor = this.flags.useObjOutlineColor ? objOutlineColor : new Color(this.flags.outlineColor);
476
+
477
+ // Cap and outline shaders write the values straight into the framebuffer, so we
478
+ // need to bypass Three.js's sRGB→linear color management and push sRGB values
479
+ // to the GPU because the Viewer uses LinearSRGBColorSpace.
480
+ meshGapColor.convertLinearToSRGB();
481
+ fillColor.convertLinearToSRGB();
482
+ outlineColor.convertLinearToSRGB();
483
+
484
+ // Map tracking frequency of line segments and Spatial grid for points
485
+ const localEdgeStats = new Map();
486
+ const localPointGrid = new PointHashGrid(localHashTolerance);
487
+
488
+ // Gather intersections
489
+ this._calculateMeshSegmentsUndirected(mesh, plane, localEdgeStats, localPointGrid, localEps);
490
+
491
+ if (localEdgeStats.size > 0) {
492
+ const boundaryEdges = [];
493
+
494
+ // Iterate over gathered edges to find outer boundaries
495
+ for (const [key, stat] of localEdgeStats.entries()) {
496
+ // Segments appearing an odd number of times represent the outermost silhouette
497
+ const isBoundary = stat.count % 2 !== 0;
498
+ const ids = key.split("-");
499
+ const id0 = Number(ids[0]);
500
+ const id1 = Number(ids[1]);
501
+
502
+ // Fetch corresponding vector structures
503
+ const p1 = localPointGrid.points[id0];
504
+ const p2 = localPointGrid.points[id1];
505
+
506
+ // Determine if this edge should be pushed to the active visual outline
507
+ if (this.flags.showDebugSeams || (this.flags.boundaryOnly ? isBoundary : true)) {
508
+ combinedOutlinePoints.push(p1.x, p1.y, p1.z, p2.x, p2.y, p2.z);
509
+ combinedOutlineColors.push(outlineColor.r, outlineColor.g, outlineColor.b);
510
+ combinedOutlineColors.push(outlineColor.r, outlineColor.g, outlineColor.b);
511
+ }
512
+
513
+ if (isBoundary) boundaryEdges.push([id0, id1]);
514
+
515
+ if (this.flags.showDebugSegments) rawSegs.push(p1.x, p1.y, p1.z, p2.x, p2.y, p2.z);
516
+ }
517
+
518
+ if (this.flags.showDebugPoints) {
519
+ for (const p of localPointGrid.points) rawPts.push(p.x, p.y, p.z);
520
+ }
521
+
522
+ // If we have enough edges to form a polygon, proceed to loop-building
523
+ if (this.flags.fillEnabled && boundaryEdges.length >= 3) {
524
+ const currentAdj = new Map();
525
+
526
+ // Populate adjacency map to build node-to-node relationships
527
+ for (const edge of boundaryEdges) {
528
+ const a = edge[0];
529
+ const b = edge[1];
530
+ if (!currentAdj.has(a)) currentAdj.set(a, []);
531
+ if (!currentAdj.has(b)) currentAdj.set(b, []);
532
+ currentAdj.get(a).push(b);
533
+ currentAdj.get(b).push(a);
534
+ }
535
+
536
+ const degree1 = [];
537
+
538
+ // Detect vertices that have only 1 neighbor (unclosed chains)
539
+ for (const [node, neighbors] of currentAdj.entries()) {
540
+ if (neighbors.length === 1) degree1.push(node);
541
+ }
542
+
543
+ const stitchTol = Math.max(localRadius * 0.05, 1e-4);
544
+
545
+ // Attempt to auto-stitch dead ends by snapping them to the nearest valid edge
546
+ for (let i = 0; i < degree1.length; i++) {
547
+ const n1 = degree1[i];
548
+ if (currentAdj.get(n1).length !== 1) continue;
549
+
550
+ const p1 = localPointGrid.points[n1];
551
+ let bestEdgeIdx = -1;
552
+ let bestProj = null;
553
+ let bestT = 0;
554
+ let minDist = stitchTol;
555
+
556
+ const edgeCount = boundaryEdges.length;
557
+
558
+ for (let eIdx = 0; eIdx < edgeCount; eIdx++) {
559
+ const edge = boundaryEdges[eIdx];
560
+ const eA = edge[0];
561
+ const eB = edge[1];
562
+ if (eA === n1 || eB === n1) continue;
563
+
564
+ const pA = localPointGrid.points[eA];
565
+ const pB = localPointGrid.points[eB];
566
+
567
+ const lineVec = new Vector3().subVectors(pB, pA);
568
+ const lineLenSq = lineVec.lengthSq();
569
+
570
+ let proj;
571
+ let t;
572
+
573
+ // Line Math: Find the closest projected point on the segment
574
+ if (lineLenSq < 1e-12) {
575
+ proj = pA.clone();
576
+ t = 0;
577
+ } else {
578
+ const ptVec = new Vector3().subVectors(p1, pA);
579
+ t = ptVec.dot(lineVec) / lineLenSq;
580
+ t = Math.max(0, Math.min(1, t));
581
+ proj = new Vector3().copy(pA).addScaledVector(lineVec, t);
582
+ }
583
+
584
+ const dist = p1.distanceTo(proj);
585
+
586
+ // Track the absolute best matching candidate
587
+ if (dist < minDist) {
588
+ minDist = dist;
589
+ bestEdgeIdx = eIdx;
590
+ bestProj = proj;
591
+ bestT = t;
592
+ }
593
+ }
594
+
595
+ // If a candidate was found, merge nodes and inject into adjacency logic
596
+ if (bestEdgeIdx !== -1) {
597
+ const edge = boundaryEdges[bestEdgeIdx];
598
+ const eA = edge[0];
599
+ const eB = edge[1];
600
+
601
+ if (bestT < 0.001) {
602
+ boundaryEdges.push([n1, eA]);
603
+ currentAdj.get(n1).push(eA);
604
+ currentAdj.get(eA).push(n1);
605
+ p1.copy(localPointGrid.points[eA]);
606
+ } else if (bestT > 0.999) {
607
+ boundaryEdges.push([n1, eB]);
608
+ currentAdj.get(n1).push(eB);
609
+ currentAdj.get(eB).push(n1);
610
+ p1.copy(localPointGrid.points[eB]);
611
+ } else {
612
+ const newNodeId = localPointGrid.add(bestProj);
613
+
614
+ edge[1] = newNodeId;
615
+ boundaryEdges.push([newNodeId, eB]);
616
+ boundaryEdges.push([n1, newNodeId]);
617
+
618
+ const neighborsA = currentAdj.get(eA);
619
+ neighborsA[neighborsA.indexOf(eB)] = newNodeId;
620
+
621
+ const neighborsB = currentAdj.get(eB);
622
+ neighborsB[neighborsB.indexOf(eA)] = newNodeId;
623
+
624
+ if (!currentAdj.has(newNodeId)) currentAdj.set(newNodeId, []);
625
+ currentAdj.get(newNodeId).push(eA, eB, n1);
626
+
627
+ currentAdj.get(n1).push(newNodeId);
628
+ p1.copy(bestProj);
629
+ }
630
+ }
631
+ }
632
+
633
+ // Mark still unresolved vertices for debugging purposes
634
+ if (this.flags.showDebugGaps) {
635
+ for (const [node, neighbors] of currentAdj.entries()) {
636
+ if (neighbors.length !== 2) {
637
+ const p = localPointGrid.points[node];
638
+ rawGaps.push(p.x, p.y, p.z);
639
+ rawGapColors.push(meshGapColor.r, meshGapColor.g, meshGapColor.b);
640
+ }
641
+ }
642
+ }
643
+
644
+ // Chain edges to discover sequential closed loops
645
+ const loops = this._assembleLoopsUndirected(boundaryEdges, localPointGrid, uAxis, vAxis);
646
+
647
+ if (loops.length > 0) {
648
+ // Submit valid loops for earcutting (Triangulation) to create solid geometry
649
+ this._triangulateTreeOptimized(
650
+ loops,
651
+ planeOrigin,
652
+ uAxis,
653
+ vAxis,
654
+ positions,
655
+ indices,
656
+ localRadius,
657
+ fillColor,
658
+ currentHatchDir,
659
+ hatchDirs,
660
+ fillColors
661
+ );
662
+ }
663
+ }
664
+ }
665
+ });
666
+
667
+ // Build and apply Fill BufferGeometry
668
+ if (indices.length > 0) {
669
+ capMesh.geometry.dispose();
670
+ capMesh.geometry = new BufferGeometry();
671
+ capMesh.geometry.setAttribute("position", new Float32BufferAttribute(positions, 3));
672
+ capMesh.geometry.setAttribute("aHatchDir", new Float32BufferAttribute(hatchDirs, 1));
673
+ capMesh.geometry.setAttribute("aFillColor", new Float32BufferAttribute(fillColors, 3));
674
+ capMesh.geometry.setIndex(indices);
675
+ capMesh.geometry.computeVertexNormals();
676
+ capMesh.visible = this.flags.fillEnabled;
677
+ } else {
678
+ capMesh.visible = false;
679
+ }
680
+
681
+ // Build and apply Outline BufferGeometry
682
+ if (outlineMesh.geometry) outlineMesh.geometry.dispose();
683
+ outlineMesh.geometry = new LineSegmentsGeometry();
684
+
685
+ if (this.flags.outlineEnabled && combinedOutlinePoints.length >= 6) {
686
+ outlineMesh.geometry.setPositions(new Float32Array(combinedOutlinePoints));
687
+ outlineMesh.geometry.setColors(new Float32Array(combinedOutlineColors));
688
+ outlineMesh.visible = true;
689
+ } else {
690
+ outlineMesh.visible = false;
691
+ }
692
+
693
+ // Push Debugging visual data if toggled
694
+ if (this.flags.showDebugPoints && rawPts.length > 0) {
695
+ debugPtsMesh.geometry.setAttribute("position", new Float32BufferAttribute(rawPts, 3));
696
+ debugPtsMesh.visible = true;
697
+ } else {
698
+ debugPtsMesh.visible = false;
699
+ }
700
+
701
+ if (debugSegsMesh.geometry) debugSegsMesh.geometry.dispose();
702
+ debugSegsMesh.geometry = new LineSegmentsGeometry();
703
+
704
+ if (this.flags.showDebugSegments && rawSegs.length >= 6) {
705
+ debugSegsMesh.geometry.setPositions(new Float32Array(rawSegs));
706
+ debugSegsMesh.visible = true;
707
+ } else {
708
+ debugSegsMesh.visible = false;
709
+ }
710
+
711
+ if (debugGapsMesh.geometry) debugGapsMesh.geometry.dispose();
712
+ debugGapsMesh.geometry = new BufferGeometry();
713
+
714
+ if (this.flags.showDebugGaps && rawGaps.length > 0) {
715
+ debugGapsMesh.geometry.setAttribute("position", new Float32BufferAttribute(rawGaps, 3));
716
+ debugGapsMesh.geometry.setAttribute("color", new Float32BufferAttribute(rawGapColors, 3));
717
+ debugGapsMesh.visible = true;
718
+ } else {
719
+ debugGapsMesh.visible = false;
720
+ }
721
+ });
722
+
723
+ if (this.flags.showDebugInfo) {
724
+ console.log(`[SectionsHelper] v7.00 Updated in ${(performance.now() - t0).toFixed(2)} ms`);
725
+ }
726
+ }
727
+
728
+ // Walks connected nodes to trace out distinct polygon paths
729
+ _assembleLoopsUndirected(edges, pointGrid, uAxis, vAxis) {
730
+ const adj = new Map();
731
+
732
+ // Construct basic adjacency array list
733
+ for (const edge of edges) {
734
+ const a = edge[0];
735
+ const b = edge[1];
736
+
737
+ if (!adj.has(a)) adj.set(a, []);
738
+ if (!adj.has(b)) adj.set(b, []);
739
+
740
+ adj.get(a).push(b);
741
+ adj.get(b).push(a);
742
+ }
743
+
744
+ const loops = [];
745
+
746
+ // Keep resolving paths until node map is depleted
747
+ while (adj.size > 0) {
748
+ let startNode = -1;
749
+
750
+ for (const key of adj.keys()) {
751
+ if (adj.get(key).length > 0) {
752
+ startNode = key;
753
+ break;
754
+ }
755
+ }
756
+
757
+ if (startNode === -1) break;
758
+
759
+ let current = startNode;
760
+ let prev = -1;
761
+ const path = [];
762
+ const pathIndices = new Map();
763
+
764
+ while (true) {
765
+ path.push(current);
766
+ pathIndices.set(current, path.length - 1);
767
+
768
+ const neighbors = adj.get(current);
769
+ if (!neighbors || neighbors.length === 0) break;
770
+
771
+ let nextIdx = 0;
772
+
773
+ // Sharpest turn heuristic mapping for resolving complex overlapping intersections
774
+ if (neighbors.length > 1 && prev !== -1) {
775
+ const pPrev = pointGrid.points[prev];
776
+ const pCurr = pointGrid.points[current];
777
+
778
+ const vIn = new Vector3().subVectors(pCurr, pPrev);
779
+ const in2d = new Vector2(vIn.dot(uAxis), vIn.dot(vAxis));
780
+
781
+ if (in2d.lengthSq() > 1e-10) {
782
+ in2d.normalize();
783
+ let minAngle = Infinity;
784
+
785
+ for (let i = 0; i < neighbors.length; i++) {
786
+ const pNext = pointGrid.points[neighbors[i]];
787
+ const vOut = new Vector3().subVectors(pNext, pCurr);
788
+ const out2d = new Vector2(vOut.dot(uAxis), vOut.dot(vAxis));
789
+
790
+ if (out2d.lengthSq() > 1e-10) {
791
+ out2d.normalize();
792
+ const angle = Math.atan2(in2d.cross(out2d), in2d.dot(out2d));
793
+
794
+ if (angle < minAngle) {
795
+ minAngle = angle;
796
+ nextIdx = i;
797
+ }
798
+ }
799
+ }
800
+ }
801
+ }
802
+
803
+ const next = neighbors[nextIdx];
804
+ neighbors.splice(nextIdx, 1);
805
+
806
+ const nextNeighbors = adj.get(next);
807
+ if (nextNeighbors) {
808
+ const revIdx = nextNeighbors.indexOf(current);
809
+ if (revIdx !== -1) nextNeighbors.splice(revIdx, 1);
810
+ }
811
+
812
+ prev = current;
813
+ current = next;
814
+
815
+ // Circular logic check - If we've seen this node before, slice out the loop
816
+ if (pathIndices.has(current)) {
817
+ const loopStartIdx = pathIndices.get(current);
818
+ const loopNodes = path.slice(loopStartIdx);
819
+
820
+ if (loopNodes.length >= 3) loops.push(loopNodes.map((id) => pointGrid.points[id]));
821
+
822
+ for (let i = loopStartIdx; i < path.length; i++) pathIndices.delete(path[i]);
823
+
824
+ path.length = loopStartIdx;
825
+ prev = path.length > 1 ? path[path.length - 2] : -1;
826
+ }
827
+ }
828
+
829
+ // Cleanup isolated node references
830
+ for (const key of adj.keys()) {
831
+ if (adj.get(key).length === 0) adj.delete(key);
832
+ }
833
+ }
834
+
835
+ return loops;
836
+ }
837
+
838
+ // Determines hole/boundary hierarchy and triangulates accordingly via Earcut algorithm
839
+ _triangulateTreeOptimized(
840
+ loops,
841
+ planeOrigin,
842
+ uAxis,
843
+ vAxis,
844
+ positionsBuffer,
845
+ indicesBuffer,
846
+ localRadius,
847
+ fillColor,
848
+ hatchDir,
849
+ hatchDirsBuffer,
850
+ fillColorsBuffer
851
+ ) {
852
+ const shapesData = [];
853
+ const minArea = localRadius * 1e-5 * (localRadius * 1e-5);
854
+
855
+ // Reduce 3D vectors to 2D coordinates residing strictly on the cutting plane
856
+ loops.forEach((loop) => {
857
+ const pts2d = loop.map((p) => {
858
+ const pv = p.clone().sub(planeOrigin);
859
+ return new Vector2(pv.dot(uAxis), pv.dot(vAxis));
860
+ });
861
+
862
+ const cleaned = [];
863
+ for (let k = 0; k < pts2d.length; k++) {
864
+ const prev = k === 0 ? pts2d[pts2d.length - 1] : pts2d[k - 1];
865
+ if (pts2d[k].distanceTo(prev) > 1e-5) cleaned.push(pts2d[k]);
866
+ }
867
+ if (cleaned.length < 3) return;
868
+
869
+ const area = ShapeUtils.area(cleaned);
870
+
871
+ // Bypass rendering microscopic artifacts
872
+ if (Math.abs(area) > minArea) {
873
+ shapesData.push({ pts2d: cleaned, absArea: Math.abs(area), depth: 0, parent: -1, holes: [] });
874
+ }
875
+ });
876
+
877
+ // Largest boundaries process first
878
+ shapesData.sort((a, b) => b.absArea - a.absArea);
879
+
880
+ // Process Parent-Child Hierarchies
881
+ for (let i = 0; i < shapesData.length; i++) {
882
+ for (let j = i - 1; j >= 0; j--) {
883
+ if (shapesData[i].absArea > shapesData[j].absArea * 0.98) continue;
884
+
885
+ // If bounds encompass the child completely, it dictates its depth
886
+ if (this._isLoopInside(shapesData[i].pts2d, shapesData[j].pts2d, localRadius)) {
887
+ shapesData[i].parent = j;
888
+ shapesData[i].depth = shapesData[j].depth + 1;
889
+ break;
890
+ }
891
+ }
892
+ }
893
+
894
+ // Push odd depths into parent as holes
895
+ for (let i = 0; i < shapesData.length; i++) {
896
+ const shape = shapesData[i];
897
+ if (shape.depth % 2 === 1 && shape.parent !== -1) {
898
+ shapesData[shape.parent].holes.push(shape.pts2d);
899
+ }
900
+ }
901
+
902
+ // Dispatch triangulation execution
903
+ for (let i = 0; i < shapesData.length; i++) {
904
+ const shapeData = shapesData[i];
905
+
906
+ if (shapeData.depth % 2 !== 0) continue;
907
+
908
+ // Reverse vertex winding order correctly
909
+ if (ShapeUtils.area(shapeData.pts2d) < 0) {
910
+ shapeData.pts2d.reverse();
911
+ }
912
+
913
+ shapeData.holes.forEach((h) => {
914
+ if (ShapeUtils.area(h) > 0) h.reverse();
915
+ });
916
+
917
+ const allPoints = [...shapeData.pts2d];
918
+ shapeData.holes.forEach((h) => allPoints.push(...h));
919
+
920
+ const faces = ShapeUtils.triangulateShape(shapeData.pts2d, shapeData.holes);
921
+ const vertexOffset = positionsBuffer.length / 3;
922
+
923
+ // Extrapolate resulting 2D triangles back into absolute World 3D coordinates
924
+ for (const pt of allPoints) {
925
+ const p3d = planeOrigin.clone().addScaledVector(uAxis, pt.x).addScaledVector(vAxis, pt.y);
926
+
927
+ positionsBuffer.push(p3d.x, p3d.y, p3d.z);
928
+ hatchDirsBuffer.push(hatchDir);
929
+ fillColorsBuffer.push(fillColor.r, fillColor.g, fillColor.b);
930
+ }
931
+
932
+ for (let f = 0; f < faces.length; f++) {
933
+ indicesBuffer.push(vertexOffset + faces[f][0], vertexOffset + faces[f][1], vertexOffset + faces[f][2]);
934
+ }
935
+ }
936
+ }
937
+
938
+ // Ray-Casting algorithm for point-in-polygon checks
939
+ _isPointInPoly(pt, poly) {
940
+ let inside = false;
941
+ const py = pt.y + 1.119e-7;
942
+ const px = pt.x;
943
+
944
+ for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
945
+ if (
946
+ poly[i].y > py !== poly[j].y > py &&
947
+ px < ((poly[j].x - poly[i].x) * (py - poly[i].y)) / (poly[j].y - poly[i].y) + poly[i].x
948
+ ) {
949
+ inside = !inside;
950
+ }
951
+ }
952
+
953
+ return inside;
954
+ }
955
+
956
+ // Checks whether an entire polygon resides within another
957
+ _isLoopInside(child, parent, localRadius) {
958
+ let minX1 = Infinity;
959
+ let maxX1 = -Infinity;
960
+ let minY1 = Infinity;
961
+ let maxY1 = -Infinity;
962
+
963
+ for (const p of child) {
964
+ if (p.x < minX1) minX1 = p.x;
965
+ if (p.x > maxX1) maxX1 = p.x;
966
+ if (p.y < minY1) minY1 = p.y;
967
+ if (p.y > maxY1) maxY1 = p.y;
968
+ }
969
+
970
+ let minX2 = Infinity;
971
+ let maxX2 = -Infinity;
972
+ let minY2 = Infinity;
973
+ let maxY2 = -Infinity;
974
+
975
+ for (const p of parent) {
976
+ if (p.x < minX2) minX2 = p.x;
977
+ if (p.x > maxX2) maxX2 = p.x;
978
+ if (p.y < minY2) minY2 = p.y;
979
+ if (p.y > maxY2) maxY2 = p.y;
980
+ }
981
+
982
+ const margin = Math.max(localRadius * 1e-4, 1e-5);
983
+
984
+ // Box rejection filter
985
+ if (minX1 < minX2 - margin || maxX1 > maxX2 + margin || minY1 < minY2 - margin || maxY1 > maxY2 + margin) {
986
+ return false;
987
+ }
988
+
989
+ let insideCount = 0;
990
+
991
+ // Verify actual point casting array
992
+ for (let i = 0; i < child.length; i++) {
993
+ if (this._isPointInPoly(child[i], parent)) {
994
+ insideCount++;
995
+ }
996
+ }
997
+
998
+ // 85% rule avoids rejecting geometry that strictly overlaps edges precisely
999
+ return insideCount >= child.length * 0.85;
1000
+ }
1001
+
1002
+ // High volume segment calculation extracting lines where geometry intercepts plane
1003
+ _calculateMeshSegmentsUndirected(mesh, plane, edgeStats, grid, eps) {
1004
+ const geom = mesh.geometry;
1005
+ const pos = geom.attributes.position;
1006
+ const index = geom.index;
1007
+ const world = mesh.matrixWorld;
1008
+ const count = index ? index.count : pos.count;
1009
+
1010
+ for (let i = 0; i < count; i += 3) {
1011
+ const i1 = index ? index.getX(i) : i;
1012
+ const i2 = index ? index.getX(i + 1) : i + 1;
1013
+ const i3 = index ? index.getX(i + 2) : i + 2;
1014
+
1015
+ const v1 = this._vA.fromBufferAttribute(pos, i1).applyMatrix4(world);
1016
+ const v2 = this._vB.fromBufferAttribute(pos, i2).applyMatrix4(world);
1017
+ const v3 = this._vC.fromBufferAttribute(pos, i3).applyMatrix4(world);
1018
+
1019
+ let d1 = plane.distanceToPoint(v1);
1020
+ let d2 = plane.distanceToPoint(v2);
1021
+ let d3 = plane.distanceToPoint(v3);
1022
+
1023
+ if (Math.abs(d1) <= eps) d1 = eps;
1024
+ if (Math.abs(d2) <= eps) d2 = eps;
1025
+ if (Math.abs(d3) <= eps) d3 = eps;
1026
+
1027
+ const s1 = d1 > 0 ? 1 : -1;
1028
+ const s2 = d2 > 0 ? 1 : -1;
1029
+ const s3 = d3 > 0 ? 1 : -1;
1030
+
1031
+ // Discard entirely invisible triangles naturally bypassed by the plane
1032
+ if (s1 === s2 && s2 === s3) continue;
1033
+
1034
+ const intersections = [];
1035
+
1036
+ if (s1 !== s2) {
1037
+ intersections.push(new Vector3().lerpVectors(v1, v2, Math.abs(d1) / (Math.abs(d1) + Math.abs(d2))));
1038
+ }
1039
+ if (s2 !== s3) {
1040
+ intersections.push(new Vector3().lerpVectors(v2, v3, Math.abs(d2) / (Math.abs(d2) + Math.abs(d3))));
1041
+ }
1042
+ if (s3 !== s1) {
1043
+ intersections.push(new Vector3().lerpVectors(v3, v1, Math.abs(d3) / (Math.abs(d3) + Math.abs(d1))));
1044
+ }
1045
+
1046
+ if (intersections.length >= 2) {
1047
+ const id1 = grid.add(intersections[0]);
1048
+ const id2 = grid.add(intersections[1]);
1049
+
1050
+ if (id1 !== id2) {
1051
+ const key = id1 < id2 ? `${id1}-${id2}` : `${id2}-${id1}`;
1052
+ const stat = edgeStats.get(key) || { count: 0 };
1053
+ stat.count++;
1054
+ edgeStats.set(key, stat);
1055
+ }
1056
+ }
1057
+ }
1058
+ }
1059
+ }
1060
+
1061
+ export { SectionsHelper };