brep-io-kernel 1.0.21 → 1.0.23
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/README.md +4 -1
- package/dist-kernel/brep-kernel.js +17545 -16874
- package/package.json +3 -2
- package/src/BREP/Edge.js +2 -0
- package/src/BREP/Face.js +2 -0
- package/src/BREP/SolidMethods/fillet.js +17 -3
- package/src/BREP/SolidMethods/visualize.js +372 -365
- package/src/BREP/Vertex.js +2 -17
- package/src/BREP/fillets/fillet.js +193 -39
- package/src/PartHistory.js +4 -25
- package/src/SketchSolver2D.js +3 -0
- package/src/UI/AccordionWidget.js +1 -1
- package/src/UI/EnvMonacoEditor.js +0 -3
- package/src/UI/HistoryWidget.js +3 -0
- package/src/UI/SceneListing.js +45 -7
- package/src/UI/SelectionFilter.js +469 -442
- package/src/UI/SelectionState.js +464 -0
- package/src/UI/assembly/AssemblyConstraintCollectionWidget.js +40 -1
- package/src/UI/assembly/AssemblyConstraintsWidget.js +17 -3
- package/src/UI/assembly/constraintSelectionUtils.js +3 -182
- package/src/UI/{assembly/constraintFaceUtils.js → faceUtils.js} +30 -5
- package/src/UI/featureDialogs.js +99 -69
- package/src/UI/pmi/LabelOverlay.js +32 -0
- package/src/UI/pmi/PMIMode.js +23 -0
- package/src/UI/pmi/dimensions/HoleCalloutAnnotation.js +7 -1
- package/src/UI/toolbarButtons/orientToFaceButton.js +3 -36
- package/src/UI/toolbarButtons/registerDefaultButtons.js +2 -0
- package/src/UI/toolbarButtons/selectionStateButton.js +206 -0
- package/src/UI/viewer.js +16 -16
- package/src/assemblyConstraints/AssemblyConstraintHistory.js +18 -42
- package/src/assemblyConstraints/constraints/AngleConstraint.js +1 -0
- package/src/assemblyConstraints/constraints/DistanceConstraint.js +1 -0
- package/src/features/fillet/FilletFeature.js +7 -0
- package/src/features/selectionUtils.js +21 -5
- package/src/features/sketch/SketchFeature.js +2 -2
- package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +3 -2
- 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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
405
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
}
|
|
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) {
|