brep-io-kernel 1.0.20 → 1.0.22

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 (43) hide show
  1. package/README.md +4 -1
  2. package/dist-kernel/brep-kernel.js +10858 -9938
  3. package/package.json +3 -2
  4. package/src/BREP/Edge.js +2 -0
  5. package/src/BREP/Face.js +2 -0
  6. package/src/BREP/SolidMethods/visualize.js +372 -365
  7. package/src/BREP/Vertex.js +2 -17
  8. package/src/PartHistory.js +4 -25
  9. package/src/SketchSolver2D.js +3 -0
  10. package/src/UI/AccordionWidget.js +1 -1
  11. package/src/UI/EnvMonacoEditor.js +0 -3
  12. package/src/UI/HistoryWidget.js +12 -4
  13. package/src/UI/SceneListing.js +45 -7
  14. package/src/UI/SelectionFilter.js +903 -438
  15. package/src/UI/SelectionState.js +464 -0
  16. package/src/UI/assembly/AssemblyConstraintCollectionWidget.js +41 -1
  17. package/src/UI/assembly/AssemblyConstraintsWidget.js +21 -3
  18. package/src/UI/assembly/constraintSelectionUtils.js +3 -182
  19. package/src/UI/{assembly/constraintFaceUtils.js → faceUtils.js} +30 -5
  20. package/src/UI/featureDialogs.js +154 -69
  21. package/src/UI/history/HistoryCollectionWidget.js +65 -0
  22. package/src/UI/pmi/AnnotationCollectionWidget.js +1 -0
  23. package/src/UI/pmi/BaseAnnotation.js +37 -0
  24. package/src/UI/pmi/LabelOverlay.js +32 -0
  25. package/src/UI/pmi/PMIMode.js +27 -0
  26. package/src/UI/pmi/dimensions/AngleDimensionAnnotation.js +5 -0
  27. package/src/UI/pmi/dimensions/ExplodeBodyAnnotation.js +5 -0
  28. package/src/UI/pmi/dimensions/HoleCalloutAnnotation.js +57 -0
  29. package/src/UI/pmi/dimensions/LeaderAnnotation.js +5 -0
  30. package/src/UI/pmi/dimensions/LinearDimensionAnnotation.js +22 -16
  31. package/src/UI/pmi/dimensions/NoteAnnotation.js +9 -0
  32. package/src/UI/pmi/dimensions/RadialDimensionAnnotation.js +81 -16
  33. package/src/UI/toolbarButtons/orientToFaceButton.js +3 -36
  34. package/src/UI/toolbarButtons/registerDefaultButtons.js +2 -0
  35. package/src/UI/toolbarButtons/selectionStateButton.js +206 -0
  36. package/src/UI/viewer.js +34 -13
  37. package/src/assemblyConstraints/AssemblyConstraintHistory.js +18 -42
  38. package/src/assemblyConstraints/constraints/AngleConstraint.js +1 -0
  39. package/src/assemblyConstraints/constraints/DistanceConstraint.js +1 -0
  40. package/src/features/selectionUtils.js +21 -5
  41. package/src/features/sketch/SketchFeature.js +2 -2
  42. package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +3 -2
  43. package/src/utils/selectionResolver.js +258 -0
@@ -7,6 +7,7 @@ import {
7
7
  Vertex,
8
8
  Face
9
9
  } from "../SolidShared.js";
10
+ import { SelectionState } from "../../UI/SelectionState.js";
10
11
  import { SHEET_METAL_FACE_TYPES, resolveSheetMetalFaceType } from "../../features/sheetMetal/sheetMetalFaceTypes.js";
11
12
 
12
13
 
@@ -30,403 +31,409 @@ import { SHEET_METAL_FACE_TYPES, resolveSheetMetalFaceType } from "../../feature
30
31
  * @returns {any} THREE.Group containing one child Mesh per face
31
32
  */
32
33
  export function visualize(options = {}) {
33
- const Solid = this.constructor;
34
- // Clear existing children and dispose resources
35
- for (let i = this.children.length - 1; i >= 0; i--) {
36
- const child = this.children[i];
37
- this.remove(child);
38
- if (child.geometry && typeof child.geometry.dispose === 'function') child.geometry.dispose();
39
- const mat = child.material;
40
- if (mat) {
41
- if (Array.isArray(mat)) mat.forEach(m => m && m.dispose && m.dispose());
42
- else if (typeof mat.dispose === 'function') mat.dispose();
43
- }
34
+ // stack trace here
35
+ //console.trace();
36
+
37
+
38
+
39
+ const Solid = this.constructor;
40
+ // Clear existing children and dispose resources
41
+ for (let i = this.children.length - 1; i >= 0; i--) {
42
+ const child = this.children[i];
43
+ this.remove(child);
44
+ if (child.geometry && typeof child.geometry.dispose === 'function') child.geometry.dispose();
45
+ const mat = child.material;
46
+ if (mat) {
47
+ if (Array.isArray(mat)) mat.forEach(m => m && m.dispose && m.dispose());
48
+ else if (typeof mat.dispose === 'function') mat.dispose();
44
49
  }
50
+ }
45
51
 
46
- const { showEdges = true, forceAuthoring = false, authoringOnly = false } = options;
47
- let faces; let usedFallback = false;
48
- if (!forceAuthoring && !authoringOnly) {
49
- try {
50
- faces = this.getFaces(false);
51
- } catch (err) {
52
- console.warn('[Solid.visualize] getFaces failed, falling back to raw arrays:', err?.message || err);
53
- usedFallback = true;
54
- }
55
- } else {
52
+ const { showEdges = true, forceAuthoring = false, authoringOnly = false } = options;
53
+ let faces; let usedFallback = false;
54
+ if (!forceAuthoring && !authoringOnly) {
55
+ try {
56
+ faces = this.getFaces(false);
57
+ } catch (err) {
58
+ console.warn('[Solid.visualize] getFaces failed, falling back to raw arrays:', err?.message || err);
56
59
  usedFallback = true;
57
60
  }
58
- if (usedFallback || !faces) {
59
- // Fallback: group authored triangles by face name directly from arrays.
60
- // This enables visualization even if manifoldization failed, which helps debugging.
61
- const vp = this._vertProperties || [];
62
- const tv = this._triVerts || [];
63
- const ids = this._triIDs || [];
64
- const nameOf = (id) => this._idToFaceName && this._idToFaceName.get ? this._idToFaceName.get(id) : String(id);
65
- const nameToTris = new Map();
66
- const triCount = (tv.length / 3) | 0;
67
- for (let t = 0; t < triCount; t++) {
68
- const id = ids[t];
69
- const name = nameOf(id);
70
- if (!name) continue;
71
- let arr = nameToTris.get(name);
72
- if (!arr) { arr = []; nameToTris.set(name, arr); }
73
- const i0 = tv[t * 3 + 0], i1 = tv[t * 3 + 1], i2 = tv[t * 3 + 2];
74
- const p0 = [vp[i0 * 3 + 0], vp[i0 * 3 + 1], vp[i0 * 3 + 2]];
75
- const p1 = [vp[i1 * 3 + 0], vp[i1 * 3 + 1], vp[i1 * 3 + 2]];
76
- const p2 = [vp[i2 * 3 + 0], vp[i2 * 3 + 1], vp[i2 * 3 + 2]];
77
- arr.push({ faceName: name, indices: [i0, i1, i2], p1: p0, p2: p1, p3: p2 });
78
- }
79
- faces = [];
80
- for (const [faceName, triangles] of nameToTris.entries()) faces.push({ faceName, triangles });
61
+ } else {
62
+ usedFallback = true;
63
+ }
64
+ if (usedFallback || !faces) {
65
+ // Fallback: group authored triangles by face name directly from arrays.
66
+ // This enables visualization even if manifoldization failed, which helps debugging.
67
+ const vp = this._vertProperties || [];
68
+ const tv = this._triVerts || [];
69
+ const ids = this._triIDs || [];
70
+ const nameOf = (id) => this._idToFaceName && this._idToFaceName.get ? this._idToFaceName.get(id) : String(id);
71
+ const nameToTris = new Map();
72
+ const triCount = (tv.length / 3) | 0;
73
+ for (let t = 0; t < triCount; t++) {
74
+ const id = ids[t];
75
+ const name = nameOf(id);
76
+ if (!name) continue;
77
+ let arr = nameToTris.get(name);
78
+ if (!arr) { arr = []; nameToTris.set(name, arr); }
79
+ const i0 = tv[t * 3 + 0], i1 = tv[t * 3 + 1], i2 = tv[t * 3 + 2];
80
+ const p0 = [vp[i0 * 3 + 0], vp[i0 * 3 + 1], vp[i0 * 3 + 2]];
81
+ const p1 = [vp[i1 * 3 + 0], vp[i1 * 3 + 1], vp[i1 * 3 + 2]];
82
+ const p2 = [vp[i2 * 3 + 0], vp[i2 * 3 + 1], vp[i2 * 3 + 2]];
83
+ arr.push({ faceName: name, indices: [i0, i1, i2], p1: p0, p2: p1, p3: p2 });
81
84
  }
85
+ faces = [];
86
+ for (const [faceName, triangles] of nameToTris.entries()) faces.push({ faceName, triangles });
87
+ }
82
88
 
83
- // Build Face meshes and index by name
84
- const faceMap = new Map();
85
- for (const { faceName, triangles } of faces) {
86
- if (!triangles.length) continue;
87
- const positions = new Float32Array(triangles.length * 9);
88
- let w = 0;
89
- for (let t = 0; t < triangles.length; t++) {
90
- const tri = triangles[t];
91
- const p0 = tri.p1, p1 = tri.p2, p2 = tri.p3;
92
-
93
- if (debugMode) {
94
- // Validate triangle coordinates before adding to geometry
95
- const coords = [p0[0], p0[1], p0[2], p1[0], p1[1], p1[2], p2[0], p2[1], p2[2]];
96
- const hasInvalidCoords = coords.some(coord => !isFinite(coord));
97
-
98
- if (hasInvalidCoords) {
99
- console.error(`Invalid triangle coordinates in face ${faceName}, triangle ${t}:`);
100
- console.error('p0:', p0, 'p1:', p1, 'p2:', p2);
101
- console.error('Triangle data:', tri);
102
- // Skip this triangle by not incrementing w and not setting positions
103
- continue;
104
- }
89
+ // Build Face meshes and index by name
90
+ const faceMap = new Map();
91
+ for (const { faceName, triangles } of faces) {
92
+ if (!triangles.length) continue;
93
+ const positions = new Float32Array(triangles.length * 9);
94
+ let w = 0;
95
+ for (let t = 0; t < triangles.length; t++) {
96
+ const tri = triangles[t];
97
+ const p0 = tri.p1, p1 = tri.p2, p2 = tri.p3;
105
98
 
106
- // Degenerate triangle check (area ~ 0) and log its points
107
- // Compute squared area via cross product of edges (robust to uniform scale)
108
- try {
109
- const ux = p1[0] - p0[0], uy = p1[1] - p0[1], uz = p1[2] - p0[2];
110
- const vx = p2[0] - p0[0], vy = p2[1] - p0[1], vz = p2[2] - p0[2];
111
- const nx = uy * vz - uz * vy;
112
- const ny = uz * vx - ux * vz;
113
- const nz = ux * vy - uy * vx;
114
- const area2 = nx * nx + ny * ny + nz * nz;
115
- // Use same threshold as viewer diagnostics
116
- if (area2 <= 1e-30) {
117
- console.warn(`[Solid.visualize] Degenerate triangle in face ${faceName} @ index ${t}`);
118
- console.warn('points:', {p0, p1, p2});
119
- }
120
- } catch { /* best-effort logging only */ }
99
+ if (debugMode) {
100
+ // Validate triangle coordinates before adding to geometry
101
+ const coords = [p0[0], p0[1], p0[2], p1[0], p1[1], p1[2], p2[0], p2[1], p2[2]];
102
+ const hasInvalidCoords = coords.some(coord => !isFinite(coord));
103
+
104
+ if (hasInvalidCoords) {
105
+ console.error(`Invalid triangle coordinates in face ${faceName}, triangle ${t}:`);
106
+ console.error('p0:', p0, 'p1:', p1, 'p2:', p2);
107
+ console.error('Triangle data:', tri);
108
+ // Skip this triangle by not incrementing w and not setting positions
109
+ continue;
121
110
  }
122
-
123
- positions[w++] = p0[0]; positions[w++] = p0[1]; positions[w++] = p0[2];
124
- positions[w++] = p1[0]; positions[w++] = p1[1]; positions[w++] = p1[2];
125
- positions[w++] = p2[0]; positions[w++] = p2[1]; positions[w++] = p2[2];
111
+
112
+ // Degenerate triangle check (area ~ 0) and log its points
113
+ // Compute squared area via cross product of edges (robust to uniform scale)
114
+ try {
115
+ const ux = p1[0] - p0[0], uy = p1[1] - p0[1], uz = p1[2] - p0[2];
116
+ const vx = p2[0] - p0[0], vy = p2[1] - p0[1], vz = p2[2] - p0[2];
117
+ const nx = uy * vz - uz * vy;
118
+ const ny = uz * vx - ux * vz;
119
+ const nz = ux * vy - uy * vx;
120
+ const area2 = nx * nx + ny * ny + nz * nz;
121
+ // Use same threshold as viewer diagnostics
122
+ if (area2 <= 1e-30) {
123
+ console.warn(`[Solid.visualize] Degenerate triangle in face ${faceName} @ index ${t}`);
124
+ console.warn('points:', { p0, p1, p2 });
125
+ }
126
+ } catch { /* best-effort logging only */ }
126
127
  }
127
128
 
128
- const geom = new THREE.BufferGeometry();
129
- geom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
130
- geom.computeVertexNormals();
131
- geom.computeBoundingBox();
132
- geom.computeBoundingSphere();
133
-
134
- const faceObj = new Face(geom);
135
- faceObj.name = faceName;
136
- faceObj.userData.faceName = faceName;
137
- faceObj.userData.__defaultMaterial = faceObj.material;
138
- faceObj.parentSolid = this;
139
- // Tag with the owning feature for inspector/debug traceability.
140
- try { faceObj.owningFeatureID = this?.owningFeatureID || null; } catch { }
141
- faceMap.set(faceName, faceObj);
142
- this.add(faceObj);
129
+ positions[w++] = p0[0]; positions[w++] = p0[1]; positions[w++] = p0[2];
130
+ positions[w++] = p1[0]; positions[w++] = p1[1]; positions[w++] = p1[2];
131
+ positions[w++] = p2[0]; positions[w++] = p2[1]; positions[w++] = p2[2];
143
132
  }
144
133
 
145
- if (showEdges) {
146
- if (!usedFallback) {
147
- let polylines = [];
148
- try { polylines = this.getBoundaryEdgePolylines() || []; } catch { polylines = []; }
149
- // Safety net: if manifold-based extraction yielded no edges (e.g., faceID missing),
150
- // fall back to authoring-based boundary extraction so we still visualize edges.
151
- if (!Array.isArray(polylines) || polylines.length === 0) {
152
- try { usedFallback = true; } catch { }
153
- }
154
- for (const e of polylines) {
155
- const positions = new Float32Array(e.positions.length * 3);
156
- let w = 0;
157
- for (let i = 0; i < e.positions.length; i++) {
158
- const p = e.positions[i];
159
- positions[w++] = p[0]; positions[w++] = p[1]; positions[w++] = p[2];
160
- }
161
- const g = new LineGeometry();
162
- g.setPositions(Array.from(positions));
163
- try { g.computeBoundingSphere(); } catch { }
164
-
165
- const edgeObj = new Edge(g);
166
- edgeObj.name = e.name;
167
- edgeObj.closedLoop = !!e.closedLoop;
168
- edgeObj.userData = {
169
- faceA: e.faceA,
170
- faceB: e.faceB,
171
- polylineLocal: e.positions,
172
- closedLoop: !!e.closedLoop,
173
- };
174
- edgeObj.userData.__defaultMaterial = edgeObj.material;
175
- annotateEdgeFromMetadata(edgeObj, this);
176
- // For convenience in feature code, mirror THREE's parent with an explicit handle
177
- edgeObj.parentSolid = this;
178
- const fa = faceMap.get(e.faceA);
179
- const fb = faceMap.get(e.faceB);
180
- if (fa) fa.edges.push(edgeObj);
181
- if (fb) fb.edges.push(edgeObj);
182
- if (fa) edgeObj.faces.push(fa);
183
- if (fb) edgeObj.faces.push(fb);
184
- this.add(edgeObj);
134
+ const geom = new THREE.BufferGeometry();
135
+ geom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
136
+ geom.computeVertexNormals();
137
+ geom.computeBoundingBox();
138
+ geom.computeBoundingSphere();
139
+
140
+ const faceObj = new Face(geom);
141
+ faceObj.name = faceName;
142
+ faceObj.userData.faceName = faceName;
143
+ faceObj.userData.__defaultMaterial = faceObj.material;
144
+ faceObj.parentSolid = this;
145
+ // Tag with the owning feature for inspector/debug traceability.
146
+ try { faceObj.owningFeatureID = this?.owningFeatureID || null; } catch { }
147
+ faceMap.set(faceName, faceObj);
148
+ this.add(faceObj);
149
+ }
150
+
151
+ if (showEdges) {
152
+ if (!usedFallback) {
153
+ let polylines = [];
154
+ try { polylines = this.getBoundaryEdgePolylines() || []; } catch { polylines = []; }
155
+ // Safety net: if manifold-based extraction yielded no edges (e.g., faceID missing),
156
+ // fall back to authoring-based boundary extraction so we still visualize edges.
157
+ if (!Array.isArray(polylines) || polylines.length === 0) {
158
+ try { usedFallback = true; } catch { }
159
+ }
160
+ for (const e of polylines) {
161
+ const positions = new Float32Array(e.positions.length * 3);
162
+ let w = 0;
163
+ for (let i = 0; i < e.positions.length; i++) {
164
+ const p = e.positions[i];
165
+ positions[w++] = p[0]; positions[w++] = p[1]; positions[w++] = p[2];
185
166
  }
167
+ const g = new LineGeometry();
168
+ g.setPositions(Array.from(positions));
169
+ try { g.computeBoundingSphere(); } catch { }
170
+
171
+ const edgeObj = new Edge(g);
172
+ edgeObj.name = e.name;
173
+ edgeObj.closedLoop = !!e.closedLoop;
174
+ edgeObj.userData = {
175
+ faceA: e.faceA,
176
+ faceB: e.faceB,
177
+ polylineLocal: e.positions,
178
+ closedLoop: !!e.closedLoop,
179
+ };
180
+ edgeObj.userData.__defaultMaterial = edgeObj.material;
181
+ annotateEdgeFromMetadata(edgeObj, this);
182
+ // For convenience in feature code, mirror THREE's parent with an explicit handle
183
+ edgeObj.parentSolid = this;
184
+ const fa = faceMap.get(e.faceA);
185
+ const fb = faceMap.get(e.faceB);
186
+ if (fa) fa.edges.push(edgeObj);
187
+ if (fb) fb.edges.push(edgeObj);
188
+ if (fa) edgeObj.faces.push(fa);
189
+ if (fb) edgeObj.faces.push(fb);
190
+ this.add(edgeObj);
186
191
  }
187
- if (usedFallback) {
188
- // Fallback boundary extraction from raw authoring arrays.
189
- try {
190
- const vp = this._vertProperties || [];
191
- const tv = this._triVerts || [];
192
- const ids = this._triIDs || [];
193
- const nv = (vp.length / 3) | 0;
194
- const triCount = (tv.length / 3) | 0;
195
- const NV = BigInt(Math.max(1, nv));
196
- const ukey = (a, b) => { const A = BigInt(a), B = BigInt(b); return A < B ? A * NV + B : B * NV + A; };
197
- const e2t = new Map(); // key -> [{id,a,b,tri}...]
198
- for (let t = 0; t < triCount; t++) {
199
- const id = ids[t];
200
- const base = t * 3;
201
- const i0 = tv[base + 0] >>> 0, i1 = tv[base + 1] >>> 0, i2 = tv[base + 2] >>> 0;
202
- const edges = [[i0, i1], [i1, i2], [i2, i0]];
203
- for (let k = 0; k < 3; k++) {
204
- const a = edges[k][0], b = edges[k][1];
205
- const key = ukey(a, b);
206
- let arr = e2t.get(key);
207
- if (!arr) { arr = []; e2t.set(key, arr); }
208
- arr.push({ id, a, b, tri: t });
209
- }
210
- }
211
- // Create polyline objects between differing face IDs (authoring labels)
212
- const nameOf = (id) => this._idToFaceName && this._idToFaceName.get ? this._idToFaceName.get(id) : String(id);
213
- const pairToEdges = new Map(); // pairKey -> array of [u,v]
214
- for (const [key, arr] of e2t.entries()) {
215
- if (arr.length !== 2) continue;
216
- const a = arr[0], b = arr[1];
217
- if (a.id === b.id) continue;
218
- const nameA = nameOf(a.id), nameB = nameOf(b.id);
219
- const pair = nameA < nameB ? [nameA, nameB] : [nameB, nameA];
220
- const pairKey = JSON.stringify(pair);
221
- let list = pairToEdges.get(pairKey);
222
- if (!list) { list = []; pairToEdges.set(pairKey, list); }
223
- const u = Math.min(a.a, a.b), v = Math.max(a.a, a.b);
224
- list.push([u, v]);
192
+ }
193
+ if (usedFallback) {
194
+ // Fallback boundary extraction from raw authoring arrays.
195
+ try {
196
+ const vp = this._vertProperties || [];
197
+ const tv = this._triVerts || [];
198
+ const ids = this._triIDs || [];
199
+ const nv = (vp.length / 3) | 0;
200
+ const triCount = (tv.length / 3) | 0;
201
+ const NV = BigInt(Math.max(1, nv));
202
+ const ukey = (a, b) => { const A = BigInt(a), B = BigInt(b); return A < B ? A * NV + B : B * NV + A; };
203
+ const e2t = new Map(); // key -> [{id,a,b,tri}...]
204
+ for (let t = 0; t < triCount; t++) {
205
+ const id = ids[t];
206
+ const base = t * 3;
207
+ const i0 = tv[base + 0] >>> 0, i1 = tv[base + 1] >>> 0, i2 = tv[base + 2] >>> 0;
208
+ const edges = [[i0, i1], [i1, i2], [i2, i0]];
209
+ for (let k = 0; k < 3; k++) {
210
+ const a = edges[k][0], b = edges[k][1];
211
+ const key = ukey(a, b);
212
+ let arr = e2t.get(key);
213
+ if (!arr) { arr = []; e2t.set(key, arr); }
214
+ arr.push({ id, a, b, tri: t });
225
215
  }
216
+ }
217
+ // Create polyline objects between differing face IDs (authoring labels)
218
+ const nameOf = (id) => this._idToFaceName && this._idToFaceName.get ? this._idToFaceName.get(id) : String(id);
219
+ const pairToEdges = new Map(); // pairKey -> array of [u,v]
220
+ for (const [key, arr] of e2t.entries()) {
221
+ if (arr.length !== 2) continue;
222
+ const a = arr[0], b = arr[1];
223
+ if (a.id === b.id) continue;
224
+ const nameA = nameOf(a.id), nameB = nameOf(b.id);
225
+ const pair = nameA < nameB ? [nameA, nameB] : [nameB, nameA];
226
+ const pairKey = JSON.stringify(pair);
227
+ let list = pairToEdges.get(pairKey);
228
+ if (!list) { list = []; pairToEdges.set(pairKey, list); }
229
+ const u = Math.min(a.a, a.b), v = Math.max(a.a, a.b);
230
+ list.push([u, v]);
231
+ }
226
232
 
227
- const addPolyline = (nameA, nameB, indices) => {
228
- const visited = new Set();
229
- const adj = new Map();
230
- const ek = (u, v) => (u < v ? `${u},${v}` : `${v},${u}`);
231
- for (const [u, v] of indices) {
232
- if (!adj.has(u)) adj.set(u, new Set());
233
- if (!adj.has(v)) adj.set(v, new Set());
234
- adj.get(u).add(v); adj.get(v).add(u);
235
- }
236
- const verts = (idx) => [vp[idx * 3 + 0], vp[idx * 3 + 1], vp[idx * 3 + 2]];
237
- for (const [u0] of adj.entries()) {
238
- // find start (degree 1) or any if loop
239
- if ([...adj.get(u0)].length !== 1) continue;
240
- const poly = [];
241
- let u = u0, prev = -1;
242
- while (true) {
243
- const nbrs = [...adj.get(u)];
244
- let v = nbrs[0];
245
- if (v === prev && nbrs.length > 1) v = nbrs[1];
246
- if (v === undefined) break;
247
- const key = ek(u, v);
248
- if (visited.has(key)) break;
249
- visited.add(key);
250
- poly.push(verts(u));
251
- prev = u; u = v;
252
- if (!adj.has(u)) break;
253
- }
233
+ const addPolyline = (nameA, nameB, indices) => {
234
+ const visited = new Set();
235
+ const adj = new Map();
236
+ const ek = (u, v) => (u < v ? `${u},${v}` : `${v},${u}`);
237
+ for (const [u, v] of indices) {
238
+ if (!adj.has(u)) adj.set(u, new Set());
239
+ if (!adj.has(v)) adj.set(v, new Set());
240
+ adj.get(u).add(v); adj.get(v).add(u);
241
+ }
242
+ const verts = (idx) => [vp[idx * 3 + 0], vp[idx * 3 + 1], vp[idx * 3 + 2]];
243
+ for (const [u0] of adj.entries()) {
244
+ // find start (degree 1) or any if loop
245
+ if ([...adj.get(u0)].length !== 1) continue;
246
+ const poly = [];
247
+ let u = u0, prev = -1;
248
+ while (true) {
249
+ const nbrs = [...adj.get(u)];
250
+ let v = nbrs[0];
251
+ if (v === prev && nbrs.length > 1) v = nbrs[1];
252
+ if (v === undefined) break;
253
+ const key = ek(u, v);
254
+ if (visited.has(key)) break;
255
+ visited.add(key);
254
256
  poly.push(verts(u));
255
- if (poly.length >= 2) {
256
- // Validate polyline coordinates before creating geometry
257
- const flatCoords = poly.flat();
258
- const hasInvalidCoords = flatCoords.some(coord => !isFinite(coord));
259
-
260
- if (hasInvalidCoords) {
261
- console.error('Invalid coordinates detected in edge polyline:');
262
- console.error('Poly coordinates:', poly);
263
- console.error('Flat coordinates:', flatCoords);
264
- console.error('Face names:', nameA, '|', nameB);
265
- continue; // Skip this edge
266
- }
267
-
268
- const g = new LineGeometry();
269
- g.setPositions(flatCoords);
270
- try { g.computeBoundingSphere(); } catch { }
271
- const edgeObj = new Edge(g);
272
- edgeObj.name = `${nameA}|${nameB}`;
273
- edgeObj.closedLoop = false;
274
- edgeObj.userData = {
275
- faceA: nameA,
276
- faceB: nameB,
277
- polylineLocal: poly,
278
- closedLoop: false,
279
- };
280
- edgeObj.userData.__defaultMaterial = edgeObj.material;
281
- annotateEdgeFromMetadata(edgeObj, this);
282
- edgeObj.parentSolid = this;
283
- const fa = faceMap.get(nameA); const fb = faceMap.get(nameB);
284
- if (fa) fa.edges.push(edgeObj); if (fb) fb.edges.push(edgeObj);
285
- if (fa) edgeObj.faces.push(fa); if (fb) edgeObj.faces.push(fb);
286
- try { edgeObj.computeLineDistances(); } catch { }
287
- this.add(edgeObj);
257
+ prev = u; u = v;
258
+ if (!adj.has(u)) break;
259
+ }
260
+ poly.push(verts(u));
261
+ if (poly.length >= 2) {
262
+ // Validate polyline coordinates before creating geometry
263
+ const flatCoords = poly.flat();
264
+ const hasInvalidCoords = flatCoords.some(coord => !isFinite(coord));
265
+
266
+ if (hasInvalidCoords) {
267
+ console.error('Invalid coordinates detected in edge polyline:');
268
+ console.error('Poly coordinates:', poly);
269
+ console.error('Flat coordinates:', flatCoords);
270
+ console.error('Face names:', nameA, '|', nameB);
271
+ continue; // Skip this edge
288
272
  }
273
+
274
+ const g = new LineGeometry();
275
+ g.setPositions(flatCoords);
276
+ try { g.computeBoundingSphere(); } catch { }
277
+ const edgeObj = new Edge(g);
278
+ edgeObj.name = `${nameA}|${nameB}`;
279
+ edgeObj.closedLoop = false;
280
+ edgeObj.userData = {
281
+ faceA: nameA,
282
+ faceB: nameB,
283
+ polylineLocal: poly,
284
+ closedLoop: false,
285
+ };
286
+ edgeObj.userData.__defaultMaterial = edgeObj.material;
287
+ annotateEdgeFromMetadata(edgeObj, this);
288
+ edgeObj.parentSolid = this;
289
+ const fa = faceMap.get(nameA); const fb = faceMap.get(nameB);
290
+ if (fa) fa.edges.push(edgeObj); if (fb) fb.edges.push(edgeObj);
291
+ if (fa) edgeObj.faces.push(fa); if (fb) edgeObj.faces.push(fb);
292
+ try { edgeObj.computeLineDistances(); } catch { }
293
+ this.add(edgeObj);
289
294
  }
290
- };
291
- for (const [pairKey, edgeList] of pairToEdges.entries()) {
292
- const [a, b] = JSON.parse(pairKey);
293
- addPolyline(a, b, edgeList);
294
295
  }
295
- } catch (_) { /* ignore fallback edge errors */ }
296
- }
296
+ };
297
+ for (const [pairKey, edgeList] of pairToEdges.entries()) {
298
+ const [a, b] = JSON.parse(pairKey);
299
+ addPolyline(a, b, edgeList);
300
+ }
301
+ } catch (_) { /* ignore fallback edge errors */ }
297
302
  }
303
+ }
298
304
 
299
- // Add auxiliary edges stored on this solid (e.g., centerlines)
300
- try {
301
- if (Array.isArray(this._auxEdges) && this._auxEdges.length) {
302
- for (const aux of this._auxEdges) {
303
- const pts = Array.isArray(aux?.points) ? aux.points.filter(p => Array.isArray(p) && p.length === 3) : [];
304
- if (pts.length < 2) continue;
305
- const flat = [];
306
- for (const p of pts) { flat.push(p[0], p[1], p[2]); }
307
-
308
- // If the auxiliary edge is marked as a closed loop, ensure the
309
- // rendered polyline has an explicit closing segment by duplicating
310
- // the first point at the end if necessary. This affects rendering
311
- // only; stored userData remains unchanged for downstream consumers.
312
- if (aux?.closedLoop && pts.length >= 2) {
313
- const f = pts[0];
314
- const l = pts[pts.length - 1];
315
- const dx = l[0] - f[0];
316
- const dy = l[1] - f[1];
317
- const dz = l[2] - f[2];
318
- const needsClosure = (dx !== 0) || (dy !== 0) || (dz !== 0);
319
- if (needsClosure) {
320
- flat.push(f[0], f[1], f[2]);
321
- }
322
- }
323
-
324
- // Validate auxiliary edge coordinates
325
- const hasInvalidCoords = flat.some(coord => !isFinite(coord));
326
- if (hasInvalidCoords) {
327
- console.error('Invalid coordinates in auxiliary edge:', aux?.name || 'CENTERLINE');
328
- console.error('Points:', pts);
329
- console.error('Flat coordinates:', flat);
330
- continue; // Skip this auxiliary edge
305
+ // Add auxiliary edges stored on this solid (e.g., centerlines)
306
+ try {
307
+ if (Array.isArray(this._auxEdges) && this._auxEdges.length) {
308
+ for (const aux of this._auxEdges) {
309
+ const pts = Array.isArray(aux?.points) ? aux.points.filter(p => Array.isArray(p) && p.length === 3) : [];
310
+ if (pts.length < 2) continue;
311
+ const flat = [];
312
+ for (const p of pts) { flat.push(p[0], p[1], p[2]); }
313
+
314
+ // If the auxiliary edge is marked as a closed loop, ensure the
315
+ // rendered polyline has an explicit closing segment by duplicating
316
+ // the first point at the end if necessary. This affects rendering
317
+ // only; stored userData remains unchanged for downstream consumers.
318
+ if (aux?.closedLoop && pts.length >= 2) {
319
+ const f = pts[0];
320
+ const l = pts[pts.length - 1];
321
+ const dx = l[0] - f[0];
322
+ const dy = l[1] - f[1];
323
+ const dz = l[2] - f[2];
324
+ const needsClosure = (dx !== 0) || (dy !== 0) || (dz !== 0);
325
+ if (needsClosure) {
326
+ flat.push(f[0], f[1], f[2]);
331
327
  }
332
-
333
- const g = new LineGeometry();
334
- g.setPositions(flat);
335
- try { g.computeBoundingSphere(); } catch { }
336
- const edgeObj = new Edge(g);
337
- edgeObj.name = aux?.name || 'CENTERLINE';
338
- edgeObj.closedLoop = !!aux?.closedLoop;
339
- edgeObj.userData = {
340
- ...(edgeObj.userData || {}),
341
- polylineLocal: pts,
342
- polylineWorld: !!aux?.polylineWorld,
343
- centerline: !!aux?.centerline,
344
- auxEdge: true,
345
- };
346
- edgeObj.parentSolid = this;
347
- try {
348
- const key = (aux?.materialKey || 'OVERLAY').toUpperCase();
349
- const edgeMats = CADmaterials?.EDGE || {};
350
- const mat = edgeMats[key] || (key === 'OVERLAY' ? edgeMats.OVERLAY : null) || edgeMats.BASE;
351
- if (mat) edgeObj.material = mat;
352
- if (edgeObj.material && (key !== 'BASE')) {
353
- edgeObj.material.depthTest = false;
354
- edgeObj.material.depthWrite = false;
355
- }
356
- try { edgeObj.computeLineDistances(); } catch { }
357
- edgeObj.renderOrder = 10020;
358
- } catch { }
359
- if (!edgeObj.userData) edgeObj.userData = {};
360
- edgeObj.userData.__defaultMaterial = edgeObj.material;
361
- this.add(edgeObj);
362
328
  }
363
- }
364
- } catch { /* ignore aux edge errors */ }
365
329
 
366
- // Helper function to generate deterministic vertex names based on meeting edges
367
- const generateVertexName = (position, meetingEdges) => {
368
- if (!meetingEdges || meetingEdges.length === 0) {
369
- return `VERTEX(${position[0]},${position[1]},${position[2]})`;
370
- }
371
- // Sort edge names for consistency, then join them
372
- const sortedEdgeNames = [...meetingEdges].sort();
373
- return `VERTEX[${sortedEdgeNames.join('+')}]`;
374
- };
330
+ // Validate auxiliary edge coordinates
331
+ const hasInvalidCoords = flat.some(coord => !isFinite(coord));
332
+ if (hasInvalidCoords) {
333
+ console.error('Invalid coordinates in auxiliary edge:', aux?.name || 'CENTERLINE');
334
+ console.error('Points:', pts);
335
+ console.error('Flat coordinates:', flat);
336
+ continue; // Skip this auxiliary edge
337
+ }
375
338
 
376
- // Generate unique vertex objects at the start and end points of all edges
377
- try {
378
- const endpoints = new Map();
379
- const vertexToEdges = new Map(); // Track which edges meet at each vertex
380
- const usedVertexNames = new Set();
381
-
382
- // First pass: collect all endpoint positions and track which edges meet at each vertex
383
- for (const ch of this.children) {
384
- if (!ch || ch.type !== 'EDGE') continue;
385
- const poly = ch.userData && Array.isArray(ch.userData.polylineLocal) ? ch.userData.polylineLocal : null;
386
- if (!poly || poly.length === 0) continue;
387
-
388
- const edgeName = ch.name || 'UNNAMED_EDGE';
389
- const first = poly[0];
390
- const last = poly[poly.length - 1];
391
-
392
- const addEP = (p) => {
393
- if (!p || p.length !== 3) return;
394
- const k = `${p[0]},${p[1]},${p[2]}`;
395
- if (!endpoints.has(k)) endpoints.set(k, p);
396
-
397
- // Track which edges meet at this vertex position
398
- if (!vertexToEdges.has(k)) {
399
- vertexToEdges.set(k, new Set());
400
- }
401
- vertexToEdges.get(k).add(edgeName);
339
+ const g = new LineGeometry();
340
+ g.setPositions(flat);
341
+ try { g.computeBoundingSphere(); } catch { }
342
+ const edgeObj = new Edge(g);
343
+ edgeObj.name = aux?.name || 'CENTERLINE';
344
+ edgeObj.closedLoop = !!aux?.closedLoop;
345
+ edgeObj.userData = {
346
+ ...(edgeObj.userData || {}),
347
+ polylineLocal: pts,
348
+ polylineWorld: !!aux?.polylineWorld,
349
+ centerline: !!aux?.centerline,
350
+ auxEdge: true,
402
351
  };
403
-
404
- addEP(first);
405
- addEP(last);
352
+ edgeObj.parentSolid = this;
353
+ try {
354
+ const key = (aux?.materialKey || 'OVERLAY').toUpperCase();
355
+ const edgeMats = CADmaterials?.EDGE || {};
356
+ const mat = edgeMats[key] || (key === 'OVERLAY' ? edgeMats.OVERLAY : null) || edgeMats.BASE;
357
+ if (mat) edgeObj.material = mat;
358
+ if (mat) SelectionState.setBaseMaterial(edgeObj, mat, { force: false });
359
+ if (edgeObj.material && (key !== 'BASE')) {
360
+ edgeObj.material.depthTest = false;
361
+ edgeObj.material.depthWrite = false;
362
+ }
363
+ try { edgeObj.computeLineDistances(); } catch { }
364
+ edgeObj.renderOrder = 10020;
365
+ } catch { }
366
+ if (!edgeObj.userData) edgeObj.userData = {};
367
+ edgeObj.userData.__defaultMaterial = edgeObj.material;
368
+ this.add(edgeObj);
406
369
  }
370
+ }
371
+ } catch { /* ignore aux edge errors */ }
407
372
 
408
- // Second pass: create vertices with deterministic names based on meeting edges
409
- if (endpoints.size) {
410
- for (const [positionKey, position] of endpoints.entries()) {
411
- try {
412
- const meetingEdges = vertexToEdges.get(positionKey);
413
- let vertexName = generateVertexName(position, meetingEdges ? Array.from(meetingEdges) : []);
414
- if (usedVertexNames.has(vertexName)) {
415
- let suffix = 1;
416
- while (usedVertexNames.has(`${vertexName}[${suffix}]`)) {
417
- suffix++;
418
- }
419
- vertexName = `${vertexName}[${suffix}]`;
420
- }
421
- usedVertexNames.add(vertexName);
422
- this.add(new Vertex(position, { name: vertexName }));
423
- } catch { }
373
+ // Helper function to generate deterministic vertex names based on meeting edges
374
+ const generateVertexName = (position, meetingEdges) => {
375
+ if (!meetingEdges || meetingEdges.length === 0) {
376
+ return `VERTEX(${position[0]},${position[1]},${position[2]})`;
377
+ }
378
+ // Sort edge names for consistency, then join them
379
+ const sortedEdgeNames = [...meetingEdges].sort();
380
+ return `VERTEX[${sortedEdgeNames.join('+')}]`;
381
+ };
382
+
383
+ // Generate unique vertex objects at the start and end points of all edges
384
+ try {
385
+ const endpoints = new Map();
386
+ const vertexToEdges = new Map(); // Track which edges meet at each vertex
387
+ const usedVertexNames = new Set();
388
+
389
+ // First pass: collect all endpoint positions and track which edges meet at each vertex
390
+ for (const ch of this.children) {
391
+ if (!ch || ch.type !== 'EDGE') continue;
392
+ const poly = ch.userData && Array.isArray(ch.userData.polylineLocal) ? ch.userData.polylineLocal : null;
393
+ if (!poly || poly.length === 0) continue;
394
+
395
+ const edgeName = ch.name || 'UNNAMED_EDGE';
396
+ const first = poly[0];
397
+ const last = poly[poly.length - 1];
398
+
399
+ const addEP = (p) => {
400
+ if (!p || p.length !== 3) return;
401
+ const k = `${p[0]},${p[1]},${p[2]}`;
402
+ if (!endpoints.has(k)) endpoints.set(k, p);
403
+
404
+ // Track which edges meet at this vertex position
405
+ if (!vertexToEdges.has(k)) {
406
+ vertexToEdges.set(k, new Set());
424
407
  }
408
+ vertexToEdges.get(k).add(edgeName);
409
+ };
410
+
411
+ addEP(first);
412
+ addEP(last);
413
+ }
414
+
415
+ // Second pass: create vertices with deterministic names based on meeting edges
416
+ if (endpoints.size) {
417
+ for (const [positionKey, position] of endpoints.entries()) {
418
+ try {
419
+ const meetingEdges = vertexToEdges.get(positionKey);
420
+ let vertexName = generateVertexName(position, meetingEdges ? Array.from(meetingEdges) : []);
421
+ if (usedVertexNames.has(vertexName)) {
422
+ let suffix = 1;
423
+ while (usedVertexNames.has(`${vertexName}[${suffix}]`)) {
424
+ suffix++;
425
+ }
426
+ vertexName = `${vertexName}[${suffix}]`;
427
+ }
428
+ usedVertexNames.add(vertexName);
429
+ this.add(new Vertex(position, { name: vertexName }));
430
+ } catch { }
425
431
  }
426
- } catch { /* best-effort vertices */ }
432
+ }
433
+ } catch { /* best-effort vertices */ }
434
+
435
+ return this;
427
436
 
428
- return this;
429
-
430
437
  }
431
438
 
432
439
  function annotateEdgeFromMetadata(edgeObj, solid) {