@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.
- package/dist/extensions/components/AxesHelperComponent.js +3 -0
- package/dist/extensions/components/AxesHelperComponent.js.map +1 -1
- package/dist/extensions/components/AxesHelperComponent.min.js +1 -1
- package/dist/extensions/components/AxesHelperComponent.module.js +3 -0
- package/dist/extensions/components/AxesHelperComponent.module.js.map +1 -1
- package/dist/extensions/components/ExtentsHelperComponent.js +6 -2
- package/dist/extensions/components/ExtentsHelperComponent.js.map +1 -1
- package/dist/extensions/components/ExtentsHelperComponent.min.js +1 -1
- package/dist/extensions/components/ExtentsHelperComponent.module.js +6 -2
- package/dist/extensions/components/ExtentsHelperComponent.module.js.map +1 -1
- package/dist/extensions/components/GridHelperComponent.js +1 -0
- package/dist/extensions/components/GridHelperComponent.js.map +1 -1
- package/dist/extensions/components/GridHelperComponent.min.js +1 -1
- package/dist/extensions/components/GridHelperComponent.module.js +1 -0
- package/dist/extensions/components/GridHelperComponent.module.js.map +1 -1
- package/dist/extensions/components/LightHelperComponent.js +2 -1
- package/dist/extensions/components/LightHelperComponent.js.map +1 -1
- package/dist/extensions/components/LightHelperComponent.min.js +1 -1
- package/dist/extensions/components/LightHelperComponent.module.js +2 -1
- package/dist/extensions/components/LightHelperComponent.module.js.map +1 -1
- package/dist/extensions/components/StatsPanelComponent.js +0 -1
- package/dist/extensions/components/StatsPanelComponent.js.map +1 -1
- package/dist/extensions/components/StatsPanelComponent.min.js +1 -1
- package/dist/extensions/components/StatsPanelComponent.module.js +0 -1
- package/dist/extensions/components/StatsPanelComponent.module.js.map +1 -1
- package/dist/extensions/loaders/GLTFCloudLoader.js +7 -2
- package/dist/extensions/loaders/GLTFCloudLoader.js.map +1 -1
- package/dist/extensions/loaders/GLTFCloudLoader.min.js +1 -1
- package/dist/extensions/loaders/GLTFCloudLoader.module.js +7 -2
- package/dist/extensions/loaders/GLTFCloudLoader.module.js.map +1 -1
- package/dist/extensions/loaders/GLTFFileLoader.js +2 -1
- package/dist/extensions/loaders/GLTFFileLoader.js.map +1 -1
- package/dist/extensions/loaders/GLTFFileLoader.min.js +1 -1
- package/dist/extensions/loaders/GLTFFileLoader.module.js +2 -1
- package/dist/extensions/loaders/GLTFFileLoader.module.js.map +1 -1
- package/dist/extensions/loaders/IFCXLoader.js +10 -5
- package/dist/extensions/loaders/IFCXLoader.js.map +1 -1
- package/dist/extensions/loaders/IFCXLoader.min.js +1 -1
- package/dist/extensions/loaders/IFCXLoader.module.js +10 -5
- package/dist/extensions/loaders/IFCXLoader.module.js.map +1 -1
- package/dist/viewer-three.js +1901 -569
- package/dist/viewer-three.js.map +1 -1
- package/dist/viewer-three.min.js +4 -4
- package/dist/viewer-three.module.js +1366 -451
- package/dist/viewer-three.module.js.map +1 -1
- package/extensions/components/AxesHelperComponent.ts +3 -0
- package/extensions/components/ExtentsHelperComponent.ts +5 -2
- package/extensions/components/GridHelperComponent.ts +1 -0
- package/extensions/components/LightHelperComponent.ts +2 -1
- package/extensions/components/StatsPanelComponent.ts +0 -1
- package/extensions/loaders/GLTFCloudLoader.ts +8 -2
- package/extensions/loaders/GLTFFileLoader.ts +3 -2
- package/extensions/loaders/IFCX/IFCXFileLoader.ts +11 -5
- package/lib/Viewer/Viewer.d.ts +6 -8
- package/lib/Viewer/components/CameraComponent.d.ts +1 -1
- package/lib/Viewer/components/ClippingPlaneComponent.d.ts +8 -0
- package/lib/Viewer/components/HighlighterComponent.d.ts +2 -2
- package/lib/Viewer/components/InfoComponent.d.ts +1 -1
- package/lib/Viewer/components/SectionsComponent.d.ts +15 -0
- package/lib/Viewer/components/WCSHelperComponent.d.ts +2 -2
- package/lib/Viewer/draggers/CuttingPlaneDragger.d.ts +6 -6
- package/lib/Viewer/draggers/OrbitDragger.d.ts +1 -1
- package/lib/Viewer/measurement/Snapper.d.ts +4 -4
- package/package.json +5 -5
- package/src/Viewer/Viewer.ts +59 -48
- package/src/Viewer/commands/GetSelected2.ts +1 -1
- package/src/Viewer/commands/SetSelected.ts +1 -1
- package/src/Viewer/commands/index.ts +1 -1
- package/src/Viewer/components/BackgroundComponent.ts +2 -1
- package/src/Viewer/components/CameraComponent.ts +6 -7
- package/src/Viewer/components/CanvasRemoveComponent.ts +0 -1
- package/src/Viewer/{scenes/Helpers.ts → components/ClippingPlaneComponent.ts} +22 -12
- package/src/Viewer/components/HighlighterComponent.ts +9 -5
- package/src/Viewer/components/HighlighterUtils.ts +2 -2
- package/src/Viewer/components/InfoComponent.ts +4 -4
- package/src/Viewer/components/SectionsComponent.ts +119 -0
- package/src/Viewer/components/SelectionComponent.ts +5 -3
- package/src/Viewer/components/WCSHelperComponent.ts +8 -6
- package/src/Viewer/components/index.ts +4 -0
- package/src/Viewer/draggers/CuttingPlaneDragger.ts +57 -34
- package/src/Viewer/draggers/MeasureLineDragger.ts +1 -1
- package/src/Viewer/draggers/OrbitDragger.ts +3 -3
- package/src/Viewer/helpers/SectionsHelper.js +1061 -0
- package/src/Viewer/helpers/WCSHelper.ts +31 -5
- package/src/Viewer/loaders/DynamicGltfLoader/DynamicGltfLoader.js +417 -92
- package/src/Viewer/loaders/DynamicGltfLoader/DynamicModelImpl.ts +19 -14
- package/src/Viewer/loaders/DynamicGltfLoader/GltfStructure.js +76 -9
- package/src/Viewer/loaders/GLTFBinaryParser.ts +2 -2
- package/src/Viewer/loaders/GLTFCloudDynamicLoader.ts +3 -2
- package/src/Viewer/loaders/GLTFFileDynamicLoader.ts +6 -4
- package/src/Viewer/measurement/Snapper.ts +6 -7
- package/src/Viewer/models/ModelImpl.ts +65 -28
- package/lib/Viewer/scenes/Helpers.d.ts +0 -7
- 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 };
|