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