brep-io-kernel 1.0.97 → 1.0.99
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-kernel/brep-kernel.js +24918 -23977
- package/package.json +1 -1
- package/src/BREP/SolidMethods/booleanOps.js +20 -4
- package/src/UI/sketcher/SketchMode3D.js +157 -33
- package/src/UI/sketcher/dimensions.js +59 -0
- package/src/UI/sketcher/glyphs.js +31 -0
- package/src/UI/sketcher/highlights.js +3 -1
- package/src/features/hole/HoleFeature.js +264 -17
- package/src/features/sketch/SketchFeature.js +117 -12
- package/src/features/sketch/sketchSolver2D/ConstraintEngine.js +42 -7
- package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +104 -0
- package/src/tests/fixtures/sketchSolverTopology/README.md +46 -0
- package/src/tests/fixtures/sketchSolverTopology/coincident_chain_fixture.json +48 -0
- package/src/tests/fixtures/sketchSolverTopology/rect_width_height_fixture.json +43 -0
- package/src/tests/fixtures/sketchSolverTopology/sketch_throttel_expression_sequence_fixture.json +25 -0
- package/src/tests/partFiles/sketch_throttel_testing.BREP.json +562 -0
- package/src/tests/sketchSolverTopologyFixtureLoader.js +308 -0
- package/src/tests/test_sketch_solver_topology_stability.js +348 -0
- package/src/tests/tests.js +17 -0
|
@@ -229,6 +229,153 @@ function unionSolids(solids) {
|
|
|
229
229
|
return current;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
function triangleArea(tri) {
|
|
233
|
+
const p0 = tri?.p1;
|
|
234
|
+
const p1 = tri?.p2;
|
|
235
|
+
const p2 = tri?.p3;
|
|
236
|
+
if (!Array.isArray(p0) || !Array.isArray(p1) || !Array.isArray(p2)) return 0;
|
|
237
|
+
const ux = p1[0] - p0[0];
|
|
238
|
+
const uy = p1[1] - p0[1];
|
|
239
|
+
const uz = p1[2] - p0[2];
|
|
240
|
+
const vx = p2[0] - p0[0];
|
|
241
|
+
const vy = p2[1] - p0[1];
|
|
242
|
+
const vz = p2[2] - p0[2];
|
|
243
|
+
const cx = uy * vz - uz * vy;
|
|
244
|
+
const cy = uz * vx - ux * vz;
|
|
245
|
+
const cz = ux * vy - uy * vx;
|
|
246
|
+
return 0.5 * Math.hypot(cx, cy, cz);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function faceComponentStats(solid, faceName) {
|
|
250
|
+
let tris = [];
|
|
251
|
+
try {
|
|
252
|
+
tris = typeof solid?.getFace === 'function' ? solid.getFace(faceName) : [];
|
|
253
|
+
} catch {
|
|
254
|
+
tris = [];
|
|
255
|
+
}
|
|
256
|
+
if (!Array.isArray(tris) || tris.length === 0) {
|
|
257
|
+
return { componentCount: 0, componentAreas: [], totalArea: 0 };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const edgeToTri = new Map();
|
|
261
|
+
const triAdj = Array.from({ length: tris.length }, () => []);
|
|
262
|
+
const areas = new Float64Array(tris.length);
|
|
263
|
+
const edgeKey = (a, b) => (a < b ? `${a}|${b}` : `${b}|${a}`);
|
|
264
|
+
|
|
265
|
+
for (let t = 0; t < tris.length; t++) {
|
|
266
|
+
const tri = tris[t];
|
|
267
|
+
areas[t] = triangleArea(tri);
|
|
268
|
+
const idx = Array.isArray(tri?.indices) && tri.indices.length === 3 ? tri.indices : null;
|
|
269
|
+
if (!idx) continue;
|
|
270
|
+
const i0 = Number(idx[0]);
|
|
271
|
+
const i1 = Number(idx[1]);
|
|
272
|
+
const i2 = Number(idx[2]);
|
|
273
|
+
if (!Number.isFinite(i0) || !Number.isFinite(i1) || !Number.isFinite(i2)) continue;
|
|
274
|
+
const edges = [[i0, i1], [i1, i2], [i2, i0]];
|
|
275
|
+
for (const [a, b] of edges) {
|
|
276
|
+
const key = edgeKey(a, b);
|
|
277
|
+
let arr = edgeToTri.get(key);
|
|
278
|
+
if (!arr) {
|
|
279
|
+
arr = [];
|
|
280
|
+
edgeToTri.set(key, arr);
|
|
281
|
+
}
|
|
282
|
+
arr.push(t);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for (const triList of edgeToTri.values()) {
|
|
287
|
+
if (!Array.isArray(triList) || triList.length !== 2) continue;
|
|
288
|
+
const a = triList[0];
|
|
289
|
+
const b = triList[1];
|
|
290
|
+
triAdj[a].push(b);
|
|
291
|
+
triAdj[b].push(a);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const seen = new Uint8Array(tris.length);
|
|
295
|
+
const componentAreas = [];
|
|
296
|
+
for (let i = 0; i < tris.length; i++) {
|
|
297
|
+
if (seen[i]) continue;
|
|
298
|
+
const stack = [i];
|
|
299
|
+
seen[i] = 1;
|
|
300
|
+
let area = 0;
|
|
301
|
+
while (stack.length) {
|
|
302
|
+
const t = stack.pop();
|
|
303
|
+
area += areas[t] || 0;
|
|
304
|
+
const nbrs = triAdj[t];
|
|
305
|
+
for (let j = 0; j < nbrs.length; j++) {
|
|
306
|
+
const u = nbrs[j];
|
|
307
|
+
if (seen[u]) continue;
|
|
308
|
+
seen[u] = 1;
|
|
309
|
+
stack.push(u);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
componentAreas.push(area);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
componentAreas.sort((a, b) => b - a);
|
|
316
|
+
const totalArea = componentAreas.reduce((sum, a) => sum + a, 0);
|
|
317
|
+
return { componentCount: componentAreas.length, componentAreas, totalArea };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function cleanupModeledThreadFaceIslands(tool, faceNames, pitch) {
|
|
321
|
+
if (!tool || typeof tool.cleanupTinyFaceIslands !== 'function') return null;
|
|
322
|
+
const names = Array.isArray(faceNames) ? faceNames.filter(Boolean) : [];
|
|
323
|
+
if (!names.length) return null;
|
|
324
|
+
|
|
325
|
+
const collectStats = () => names.map((name) => {
|
|
326
|
+
const s = faceComponentStats(tool, name);
|
|
327
|
+
return {
|
|
328
|
+
name,
|
|
329
|
+
components: s.componentCount,
|
|
330
|
+
totalArea: s.totalArea,
|
|
331
|
+
largestComponentArea: s.componentAreas[0] || 0,
|
|
332
|
+
smallestIslandArea: s.componentAreas.length > 1 ? s.componentAreas[s.componentAreas.length - 1] : 0,
|
|
333
|
+
};
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const p = Math.max(1e-4, Math.abs(Number(pitch) || 0));
|
|
337
|
+
let areaThreshold = Math.max(1e-9, p * p * 1e-4);
|
|
338
|
+
const maxThreshold = Math.max(areaThreshold, p * p * 0.05);
|
|
339
|
+
const before = collectStats();
|
|
340
|
+
let passes = 0;
|
|
341
|
+
let totalReassigned = 0;
|
|
342
|
+
|
|
343
|
+
for (let pass = 0; pass < 6; pass++) {
|
|
344
|
+
passes = pass + 1;
|
|
345
|
+
const stats = names.map((name) => ({ name, ...faceComponentStats(tool, name) }));
|
|
346
|
+
const worstComponents = stats.reduce((m, s) => Math.max(m, s.componentCount), 0);
|
|
347
|
+
if (worstComponents <= 1) break;
|
|
348
|
+
|
|
349
|
+
const smallAreas = [];
|
|
350
|
+
for (const s of stats) {
|
|
351
|
+
if (s.componentAreas.length <= 1) continue;
|
|
352
|
+
for (let i = 1; i < s.componentAreas.length; i++) {
|
|
353
|
+
const a = Number(s.componentAreas[i]);
|
|
354
|
+
if (Number.isFinite(a) && a > 0) smallAreas.push(a);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
smallAreas.sort((a, b) => a - b);
|
|
358
|
+
const suggested = smallAreas.length ? Math.max(areaThreshold, smallAreas[0] * 1.5) : areaThreshold;
|
|
359
|
+
const useThreshold = Math.min(maxThreshold, suggested);
|
|
360
|
+
const reassigned = Number(tool.cleanupTinyFaceIslands(useThreshold) || 0);
|
|
361
|
+
totalReassigned += reassigned > 0 ? reassigned : 0;
|
|
362
|
+
if (reassigned <= 0) {
|
|
363
|
+
areaThreshold = Math.min(maxThreshold, areaThreshold * 4);
|
|
364
|
+
if (areaThreshold >= maxThreshold) break;
|
|
365
|
+
} else {
|
|
366
|
+
areaThreshold = Math.min(maxThreshold, Math.max(areaThreshold * 1.25, useThreshold));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const after = collectStats();
|
|
370
|
+
return {
|
|
371
|
+
before,
|
|
372
|
+
after,
|
|
373
|
+
passes,
|
|
374
|
+
totalReassigned,
|
|
375
|
+
finalAreaThreshold: areaThreshold,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
232
379
|
function getWorldPosition(obj) {
|
|
233
380
|
if (!obj) return null;
|
|
234
381
|
if (obj.isVector3) return obj.clone();
|
|
@@ -336,10 +483,8 @@ function collectSketchVertices(sketch) {
|
|
|
336
483
|
if (!sketch || !Array.isArray(sketch.children)) return verts;
|
|
337
484
|
for (const child of sketch.children) {
|
|
338
485
|
if (!child) continue;
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
const isCenter = sid === 0 || sid === '0' || name === 'P0' || name.endsWith(':P0');
|
|
342
|
-
if (isCenter) continue;
|
|
486
|
+
const isConstructionPoint = child?.userData?.isConstructionPoint === true;
|
|
487
|
+
if (isConstructionPoint) continue;
|
|
343
488
|
const isVertexLike = child.type === 'Vertex' || child.isVertex || child.userData?.isVertex || child.userData?.type === 'VERTEX';
|
|
344
489
|
if (isVertexLike) verts.push(child);
|
|
345
490
|
}
|
|
@@ -357,8 +502,8 @@ function collectSketchVerticesByName(scene, sketchName) {
|
|
|
357
502
|
const nm = obj.name || '';
|
|
358
503
|
const m = nm.match(re);
|
|
359
504
|
if (m) {
|
|
360
|
-
const
|
|
361
|
-
if (
|
|
505
|
+
const isConstructionPoint = obj?.userData?.isConstructionPoint === true;
|
|
506
|
+
if (!isConstructionPoint) verts.push(obj);
|
|
362
507
|
}
|
|
363
508
|
const children = Array.isArray(obj.children) ? obj.children : [];
|
|
364
509
|
for (const c of children) walk(c);
|
|
@@ -367,6 +512,44 @@ function collectSketchVerticesByName(scene, sketchName) {
|
|
|
367
512
|
return verts;
|
|
368
513
|
}
|
|
369
514
|
|
|
515
|
+
function dedupePlacementsByPosition(placements, tolerance = 1e-7) {
|
|
516
|
+
const out = [];
|
|
517
|
+
if (!Array.isArray(placements) || placements.length === 0) return out;
|
|
518
|
+
|
|
519
|
+
const tol = Math.max(1e-12, Number(tolerance) || 1e-7);
|
|
520
|
+
const invTol = 1 / tol;
|
|
521
|
+
const buckets = new Map();
|
|
522
|
+
const keyOf = (p) => `${Math.round(p.x * invTol)},${Math.round(p.y * invTol)},${Math.round(p.z * invTol)}`;
|
|
523
|
+
|
|
524
|
+
for (const item of placements) {
|
|
525
|
+
const p = item?.position;
|
|
526
|
+
if (!p || !Number.isFinite(p.x) || !Number.isFinite(p.y) || !Number.isFinite(p.z)) continue;
|
|
527
|
+
const key = keyOf(p);
|
|
528
|
+
const bucket = buckets.get(key);
|
|
529
|
+
let duplicate = false;
|
|
530
|
+
if (bucket) {
|
|
531
|
+
for (let i = 0; i < bucket.length; i++) {
|
|
532
|
+
const existing = out[bucket[i]]?.position;
|
|
533
|
+
if (!existing) continue;
|
|
534
|
+
if (
|
|
535
|
+
Math.abs(existing.x - p.x) <= tol &&
|
|
536
|
+
Math.abs(existing.y - p.y) <= tol &&
|
|
537
|
+
Math.abs(existing.z - p.z) <= tol
|
|
538
|
+
) {
|
|
539
|
+
duplicate = true;
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (duplicate) continue;
|
|
545
|
+
const outIndex = out.length;
|
|
546
|
+
out.push(item);
|
|
547
|
+
if (bucket) bucket.push(outIndex);
|
|
548
|
+
else buckets.set(key, [outIndex]);
|
|
549
|
+
}
|
|
550
|
+
return out;
|
|
551
|
+
}
|
|
552
|
+
|
|
370
553
|
function escapeRegExp(str) {
|
|
371
554
|
return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
372
555
|
}
|
|
@@ -634,23 +817,32 @@ export class HoleFeature {
|
|
|
634
817
|
|
|
635
818
|
const pointObjs = [];
|
|
636
819
|
const sceneSolids = collectSceneSolids(partHistory?.scene);
|
|
637
|
-
let
|
|
820
|
+
let pointPlacements = [];
|
|
638
821
|
|
|
639
|
-
// Use sketch-defined points
|
|
822
|
+
// Use sketch-defined points as hole centers (construction points are excluded).
|
|
640
823
|
const extraPts = collectSketchVertices(sketch);
|
|
641
824
|
if (extraPts.length) {
|
|
642
825
|
pointObjs.push(...extraPts);
|
|
643
|
-
|
|
826
|
+
pointPlacements = pointObjs
|
|
827
|
+
.map((o) => ({ pointObj: o, position: getWorldPosition(o) }))
|
|
828
|
+
.filter((entry) => !!entry.position);
|
|
644
829
|
}
|
|
645
|
-
if (!
|
|
830
|
+
if (!pointPlacements.length && partHistory?.scene && sketch?.name) {
|
|
646
831
|
const fallbackPts = collectSketchVerticesByName(partHistory.scene, sketch.name);
|
|
647
832
|
if (fallbackPts.length) {
|
|
648
833
|
pointObjs.push(...fallbackPts);
|
|
649
|
-
|
|
834
|
+
pointPlacements = pointObjs
|
|
835
|
+
.map((o) => ({ pointObj: o, position: getWorldPosition(o) }))
|
|
836
|
+
.filter((entry) => !!entry.position);
|
|
650
837
|
}
|
|
651
838
|
}
|
|
839
|
+
const uniquePointPlacements = dedupePlacementsByPosition(pointPlacements);
|
|
840
|
+
if (pointPlacements.length > uniquePointPlacements.length) {
|
|
841
|
+
const skipped = pointPlacements.length - uniquePointPlacements.length;
|
|
842
|
+
console.log('[HoleFeature] Skipping duplicate coincident hole points:', skipped);
|
|
843
|
+
}
|
|
652
844
|
|
|
653
|
-
const hasPoints =
|
|
845
|
+
const hasPoints = uniquePointPlacements.length > 0;
|
|
654
846
|
const normal = normalFromSketch(sketch); // keep hole axis perpendicular to the sketch plane
|
|
655
847
|
const center = centerFromObject(sketch);
|
|
656
848
|
|
|
@@ -747,14 +939,16 @@ export class HoleFeature {
|
|
|
747
939
|
|
|
748
940
|
const res = 48;
|
|
749
941
|
const backOffset = 1e-5; // small pullback to avoid coincident faces in booleans
|
|
750
|
-
const centers = hasPoints ?
|
|
751
|
-
const sourceNames = hasPoints ?
|
|
942
|
+
const centers = hasPoints ? uniquePointPlacements.map((entry) => entry.position) : [center];
|
|
943
|
+
const sourceNames = hasPoints ? uniquePointPlacements.map((entry) => entry?.pointObj?.name || entry?.pointObj?.uuid || null) : [null];
|
|
752
944
|
const tools = [];
|
|
753
945
|
const holeRecords = [];
|
|
754
946
|
const debugVisualizationObjects = []; // Store debug viz objects separately
|
|
755
947
|
centers.forEach((c, idx) => {
|
|
756
948
|
const pointName = sourceNames[idx] || null;
|
|
757
949
|
const holeFacePrefix = pointName || (featureID ? `${featureID}_${idx}` : `HOLE_${idx}`);
|
|
950
|
+
let modeledThreadFaceNames = null;
|
|
951
|
+
let modeledThreadPitch = null;
|
|
758
952
|
const { solids: toolSolids, descriptors } = makeHoleTool({
|
|
759
953
|
holeType,
|
|
760
954
|
radius,
|
|
@@ -834,6 +1028,7 @@ export class HoleFeature {
|
|
|
834
1028
|
const threadLengthEffective = threadMode === 'MODELED'
|
|
835
1029
|
? Math.max(0, threadLength + extraThreadLength * 2)
|
|
836
1030
|
: threadLength;
|
|
1031
|
+
const threadCutFaceName = `${holeFacePrefix}_THREAD_FACE`;
|
|
837
1032
|
|
|
838
1033
|
const threadSolid = threadGeomScaled.toSolid({
|
|
839
1034
|
length: threadLengthEffective,
|
|
@@ -844,11 +1039,36 @@ export class HoleFeature {
|
|
|
844
1039
|
resolution: res,
|
|
845
1040
|
segmentsPerTurn: threadSegmentsPerTurn,
|
|
846
1041
|
name: `${holeFacePrefix}_THREAD`,
|
|
847
|
-
faceName:
|
|
1042
|
+
faceName: threadCutFaceName,
|
|
848
1043
|
axis: [0, 1, 0],
|
|
849
1044
|
origin: [0, threadStartEffective, 0],
|
|
850
1045
|
xDirection: [1, 0, 0],
|
|
851
1046
|
});
|
|
1047
|
+
const canonicalCoreFaceName = `${holeFacePrefix}_THREAD_CORE`;
|
|
1048
|
+
if (threadMode === 'MODELED') {
|
|
1049
|
+
try {
|
|
1050
|
+
modeledThreadPitch = Number(threadGeomScaled?.pitch || threadGeom?.pitch || 0);
|
|
1051
|
+
modeledThreadFaceNames = [
|
|
1052
|
+
`${threadCutFaceName}:FLANK_A`,
|
|
1053
|
+
`${threadCutFaceName}:ROOT`,
|
|
1054
|
+
`${threadCutFaceName}:FLANK_B`,
|
|
1055
|
+
canonicalCoreFaceName,
|
|
1056
|
+
];
|
|
1057
|
+
const mergeIntoCore = [
|
|
1058
|
+
`${threadCutFaceName}:CREST`,
|
|
1059
|
+
`${threadCutFaceName}:CAP_START`,
|
|
1060
|
+
`${threadCutFaceName}:CAP_END`,
|
|
1061
|
+
];
|
|
1062
|
+
for (const fromName of mergeIntoCore) {
|
|
1063
|
+
threadSolid.renameFace(fromName, canonicalCoreFaceName);
|
|
1064
|
+
}
|
|
1065
|
+
if (descriptor) {
|
|
1066
|
+
threadSolid.setFaceMetadata(canonicalCoreFaceName, { hole: { ...descriptor } });
|
|
1067
|
+
}
|
|
1068
|
+
} catch {
|
|
1069
|
+
/* best-effort */
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
852
1072
|
console.log('[HoleFeature] Thread solid created:', {
|
|
853
1073
|
type: threadSolid?.constructor?.name,
|
|
854
1074
|
hasGeometry: !!threadSolid?.geometry,
|
|
@@ -873,6 +1093,9 @@ export class HoleFeature {
|
|
|
873
1093
|
const coreR0 = minorRadiusAt(0);
|
|
874
1094
|
const coreR1 = minorRadiusAt(threadLengthEffective);
|
|
875
1095
|
const coreName = `${holeFacePrefix}_THREAD_CORE`;
|
|
1096
|
+
const coreResolution = threadMode === 'MODELED'
|
|
1097
|
+
? Math.max(8, Math.min(res, threadSegmentsPerTurn * 2))
|
|
1098
|
+
: res;
|
|
876
1099
|
const coreHeight = threadLengthEffective;
|
|
877
1100
|
if (coreHeight > 0) {
|
|
878
1101
|
const coreSolid = threadGeomScaled.isTapered && Math.abs(coreR0 - coreR1) > 1e-6
|
|
@@ -880,13 +1103,13 @@ export class HoleFeature {
|
|
|
880
1103
|
r1: coreR0,
|
|
881
1104
|
r2: coreR1,
|
|
882
1105
|
h: coreHeight,
|
|
883
|
-
resolution:
|
|
1106
|
+
resolution: coreResolution,
|
|
884
1107
|
name: coreName,
|
|
885
1108
|
})
|
|
886
1109
|
: new BREP.Cylinder({
|
|
887
1110
|
radius: coreR0,
|
|
888
1111
|
height: coreHeight,
|
|
889
|
-
resolution:
|
|
1112
|
+
resolution: coreResolution,
|
|
890
1113
|
name: coreName,
|
|
891
1114
|
});
|
|
892
1115
|
coreSolid.bakeTRS({
|
|
@@ -897,6 +1120,20 @@ export class HoleFeature {
|
|
|
897
1120
|
if (descriptor) {
|
|
898
1121
|
try { coreSolid.setFaceMetadata(`${coreName}_S`, { hole: { ...descriptor } }); } catch { /* best-effort */ }
|
|
899
1122
|
}
|
|
1123
|
+
if (threadMode === 'MODELED') {
|
|
1124
|
+
try {
|
|
1125
|
+
const coreFaces = typeof coreSolid.getFaceNames === 'function' ? coreSolid.getFaceNames() : [];
|
|
1126
|
+
for (const coreFaceName of coreFaces) {
|
|
1127
|
+
if (!coreFaceName || coreFaceName === canonicalCoreFaceName) continue;
|
|
1128
|
+
coreSolid.renameFace(coreFaceName, canonicalCoreFaceName);
|
|
1129
|
+
}
|
|
1130
|
+
if (descriptor) {
|
|
1131
|
+
coreSolid.setFaceMetadata(canonicalCoreFaceName, { hole: { ...descriptor } });
|
|
1132
|
+
}
|
|
1133
|
+
} catch {
|
|
1134
|
+
/* best-effort */
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
900
1137
|
toolSolids.push(coreSolid);
|
|
901
1138
|
}
|
|
902
1139
|
|
|
@@ -1011,6 +1248,16 @@ export class HoleFeature {
|
|
|
1011
1248
|
console.log('[HoleFeature] Unioning', toolSolids.length, 'solids for hole tool');
|
|
1012
1249
|
const tool = unionSolids(toolSolids);
|
|
1013
1250
|
if (!tool) return;
|
|
1251
|
+
if (threadMode === 'MODELED' && modeledThreadFaceNames?.length) {
|
|
1252
|
+
try {
|
|
1253
|
+
const cleanupSummary = cleanupModeledThreadFaceIslands(tool, modeledThreadFaceNames, modeledThreadPitch);
|
|
1254
|
+
if (cleanupSummary) {
|
|
1255
|
+
console.log('[HoleFeature] Modeled thread face cleanup summary:', cleanupSummary);
|
|
1256
|
+
}
|
|
1257
|
+
} catch (cleanupErr) {
|
|
1258
|
+
console.warn('[HoleFeature] Modeled thread face island cleanup failed:', cleanupErr);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1014
1261
|
if (debugShowSolid) {
|
|
1015
1262
|
try {
|
|
1016
1263
|
tool.visualize();
|
|
@@ -79,6 +79,39 @@ const inputParamsSchema = {
|
|
|
79
79
|
},
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
+
function normalizeSketchPointAttributes(sketch, externalRefPointIds = null) {
|
|
83
|
+
if (!sketch || typeof sketch !== 'object') return;
|
|
84
|
+
sketch.points = Array.isArray(sketch.points) ? sketch.points : [];
|
|
85
|
+
sketch.geometries = Array.isArray(sketch.geometries) ? sketch.geometries : [];
|
|
86
|
+
sketch.constraints = Array.isArray(sketch.constraints) ? sketch.constraints : [];
|
|
87
|
+
if (sketch.points.length === 0) {
|
|
88
|
+
sketch.points.push({ id: 0, x: 0, y: 0, fixed: true, construction: true, externalReference: false });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const extIds = externalRefPointIds instanceof Set ? externalRefPointIds : new Set();
|
|
92
|
+
if (!(externalRefPointIds instanceof Set)) {
|
|
93
|
+
try {
|
|
94
|
+
for (const p of sketch.points) {
|
|
95
|
+
if (p?.externalReference === true && Number.isFinite(Number(p?.id))) {
|
|
96
|
+
extIds.add(Number(p.id));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch { /* ignore */ }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const p of sketch.points) {
|
|
103
|
+
if (!p) continue;
|
|
104
|
+
p.fixed = p.fixed === true;
|
|
105
|
+
const pid = Number(p.id);
|
|
106
|
+
const isOrigin = Number.isFinite(pid) && pid === 0;
|
|
107
|
+
const isExternalRef = Number.isFinite(pid) && extIds.has(pid);
|
|
108
|
+
p.externalReference = isExternalRef;
|
|
109
|
+
if (typeof p.construction !== 'boolean') {
|
|
110
|
+
p.construction = isOrigin || isExternalRef;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
82
115
|
export class SketchFeature {
|
|
83
116
|
static shortName = "S";
|
|
84
117
|
static longName = "Sketch";
|
|
@@ -342,6 +375,18 @@ export class SketchFeature {
|
|
|
342
375
|
y: Array.isArray(basis.y) ? basis.y.slice() : [0, 1, 0],
|
|
343
376
|
z: Array.isArray(basis.z) ? basis.z.slice() : null,
|
|
344
377
|
};
|
|
378
|
+
sceneGroup.userData.sketchFeatureId = featureId;
|
|
379
|
+
const externalRefPointIds = new Set();
|
|
380
|
+
try {
|
|
381
|
+
const refs = Array.isArray(this.persistentData?.externalRefs) ? this.persistentData.externalRefs : [];
|
|
382
|
+
for (const ref of refs) {
|
|
383
|
+
const p0 = Number(ref?.p0);
|
|
384
|
+
const p1 = Number(ref?.p1);
|
|
385
|
+
if (Number.isFinite(p0)) externalRefPointIds.add(p0);
|
|
386
|
+
if (Number.isFinite(p1)) externalRefPointIds.add(p1);
|
|
387
|
+
}
|
|
388
|
+
} catch { /* ignore */ }
|
|
389
|
+
sceneGroup.userData.externalRefPointIds = Array.from(externalRefPointIds);
|
|
345
390
|
const bO = new THREE.Vector3().fromArray(basis.origin);
|
|
346
391
|
const bX = new THREE.Vector3().fromArray(basis.x);
|
|
347
392
|
const bY = new THREE.Vector3().fromArray(basis.y);
|
|
@@ -366,13 +411,37 @@ export class SketchFeature {
|
|
|
366
411
|
};
|
|
367
412
|
|
|
368
413
|
// Start from persisted sketch
|
|
369
|
-
let sketch = this.persistentData?.sketch || { points: [{ id:0, x:0, y:0, fixed:true }], geometries: [], constraints: [{ id:0, type:"⏚", points:[0]}] };
|
|
414
|
+
let sketch = this.persistentData?.sketch || { points: [{ id:0, x:0, y:0, fixed:true, construction:true, externalReference:false }], geometries: [], constraints: [{ id:0, type:"⏚", points:[0]}] };
|
|
415
|
+
normalizeSketchPointAttributes(sketch, externalRefPointIds);
|
|
370
416
|
this.persistentData = this.persistentData || {};
|
|
371
417
|
this.persistentData.lastProfileDiagnostics = null;
|
|
418
|
+
const solveSketchForFeature = (inputSketch, {
|
|
419
|
+
iterations = 500,
|
|
420
|
+
maxPasses = 1,
|
|
421
|
+
stopWhenConstraintsClear = false,
|
|
422
|
+
} = {}) => {
|
|
423
|
+
let solved = inputSketch;
|
|
424
|
+
let prevSig = null;
|
|
425
|
+
const passCount = Math.max(1, Number(maxPasses) || 1);
|
|
426
|
+
const iters = Math.max(1, Number(iterations) || 500);
|
|
427
|
+
for (let pass = 0; pass < passCount; pass++) {
|
|
428
|
+
const engine = new ConstraintEngine(JSON.stringify(solved));
|
|
429
|
+
solved = engine.solve(iters);
|
|
430
|
+
const pointSig = JSON.stringify(solved?.points || []);
|
|
431
|
+
const hasConstraintErrors = Array.isArray(solved?.constraints)
|
|
432
|
+
? solved.constraints.some((c) => typeof c?.error === 'string' && c.error.length > 0)
|
|
433
|
+
: false;
|
|
434
|
+
if (stopWhenConstraintsClear && !hasConstraintErrors) break;
|
|
435
|
+
if (pointSig === prevSig) break;
|
|
436
|
+
prevSig = pointSig;
|
|
437
|
+
}
|
|
438
|
+
return solved;
|
|
439
|
+
};
|
|
372
440
|
|
|
373
441
|
// Evaluate any expression-backed values on points/constraints using global expressions
|
|
374
442
|
try {
|
|
375
443
|
const exprSrc = partHistory?.expressions || '';
|
|
444
|
+
let expressionDrivenValueChanged = false;
|
|
376
445
|
const runExpr = (expressions, equation) => {
|
|
377
446
|
try {
|
|
378
447
|
const fn = `${expressions}; return ${equation} ;`;
|
|
@@ -388,11 +457,19 @@ export class SketchFeature {
|
|
|
388
457
|
for (const p of sketch.points) {
|
|
389
458
|
if (typeof p.x === 'string') {
|
|
390
459
|
const n = runExpr(exprSrc, p.x);
|
|
391
|
-
if (n != null && Number.isFinite(n))
|
|
460
|
+
if (n != null && Number.isFinite(n)) {
|
|
461
|
+
const next = Number(n);
|
|
462
|
+
if (Number.isFinite(next) && Number(p.x) !== next) expressionDrivenValueChanged = true;
|
|
463
|
+
p.x = next;
|
|
464
|
+
}
|
|
392
465
|
}
|
|
393
466
|
if (typeof p.y === 'string') {
|
|
394
467
|
const n = runExpr(exprSrc, p.y);
|
|
395
|
-
if (n != null && Number.isFinite(n))
|
|
468
|
+
if (n != null && Number.isFinite(n)) {
|
|
469
|
+
const next = Number(n);
|
|
470
|
+
if (Number.isFinite(next) && Number(p.y) !== next) expressionDrivenValueChanged = true;
|
|
471
|
+
p.y = next;
|
|
472
|
+
}
|
|
396
473
|
}
|
|
397
474
|
}
|
|
398
475
|
}
|
|
@@ -405,19 +482,29 @@ export class SketchFeature {
|
|
|
405
482
|
c?.type === '⟺' &&
|
|
406
483
|
c?.displayStyle === 'diameter' &&
|
|
407
484
|
c?.valueExprMode === 'diameter';
|
|
408
|
-
|
|
485
|
+
const next = diameterExpr ? Number(n) * 0.5 : Number(n);
|
|
486
|
+
if (Number.isFinite(next) && Number(c.value) !== next) expressionDrivenValueChanged = true;
|
|
487
|
+
c.value = next;
|
|
409
488
|
}
|
|
410
489
|
} else if (typeof c?.value === 'string') {
|
|
411
490
|
const n = runExpr(exprSrc, c.value);
|
|
412
|
-
if (n != null && Number.isFinite(n))
|
|
491
|
+
if (n != null && Number.isFinite(n)) {
|
|
492
|
+
const next = Number(n);
|
|
493
|
+
if (Number.isFinite(next) && Number(c.value) !== next) expressionDrivenValueChanged = true;
|
|
494
|
+
c.value = next;
|
|
495
|
+
}
|
|
413
496
|
}
|
|
414
497
|
}
|
|
415
498
|
}
|
|
416
499
|
// Re-solve sketch with evaluated values to reflect latest expressions
|
|
417
500
|
try {
|
|
418
|
-
const
|
|
419
|
-
|
|
501
|
+
const solved = solveSketchForFeature(sketch, {
|
|
502
|
+
iterations: expressionDrivenValueChanged ? 2000 : 500,
|
|
503
|
+
maxPasses: expressionDrivenValueChanged ? 8 : 1,
|
|
504
|
+
stopWhenConstraintsClear: expressionDrivenValueChanged,
|
|
505
|
+
});
|
|
420
506
|
sketch = solved;
|
|
507
|
+
normalizeSketchPointAttributes(sketch, externalRefPointIds);
|
|
421
508
|
this.persistentData.sketch = solved;
|
|
422
509
|
} catch {}
|
|
423
510
|
} catch {}
|
|
@@ -483,7 +570,16 @@ export class SketchFeature {
|
|
|
483
570
|
const p1 = ptById.get(r.p1);
|
|
484
571
|
if (p0 && (p0.x !== uvA.u || p0.y !== uvA.v)) { p0.x = uvA.u; p0.y = uvA.v; changed = true; }
|
|
485
572
|
if (p1 && (p1.x !== uvB.u || p1.y !== uvB.v)) { p1.x = uvB.u; p1.y = uvB.v; changed = true; }
|
|
486
|
-
if (p0)
|
|
573
|
+
if (p0) {
|
|
574
|
+
p0.fixed = true;
|
|
575
|
+
p0.externalReference = true;
|
|
576
|
+
if (typeof p0.construction !== 'boolean') p0.construction = true;
|
|
577
|
+
}
|
|
578
|
+
if (p1) {
|
|
579
|
+
p1.fixed = true;
|
|
580
|
+
p1.externalReference = true;
|
|
581
|
+
if (typeof p1.construction !== 'boolean') p1.construction = true;
|
|
582
|
+
}
|
|
487
583
|
// Ensure ground constraints exist for these points so solver treats them fixed
|
|
488
584
|
const ensureGround = (pid)=>{
|
|
489
585
|
if (!sketch.constraints.some(c=>c.type==='⏚' && Array.isArray(c.points) && c.points[0]===pid)){
|
|
@@ -498,9 +594,13 @@ export class SketchFeature {
|
|
|
498
594
|
}
|
|
499
595
|
if (changed) {
|
|
500
596
|
try {
|
|
501
|
-
const
|
|
502
|
-
|
|
597
|
+
const solved = solveSketchForFeature(sketch, {
|
|
598
|
+
iterations: 2000,
|
|
599
|
+
maxPasses: 8,
|
|
600
|
+
stopWhenConstraintsClear: true,
|
|
601
|
+
});
|
|
503
602
|
sketch = solved;
|
|
603
|
+
normalizeSketchPointAttributes(sketch, externalRefPointIds);
|
|
504
604
|
this.persistentData.sketch = solved;
|
|
505
605
|
} catch {}
|
|
506
606
|
}
|
|
@@ -511,7 +611,7 @@ export class SketchFeature {
|
|
|
511
611
|
// Helper: 2D → 3D
|
|
512
612
|
const to3D = (u, v) => new THREE.Vector3().copy(bO).addScaledVector(bX, u).addScaledVector(bY, v);
|
|
513
613
|
|
|
514
|
-
// Add vertex visuals in 3D for
|
|
614
|
+
// Add vertex visuals in 3D for non-construction sketch points (including isolated points)
|
|
515
615
|
try {
|
|
516
616
|
if (Array.isArray(sketch?.points)) {
|
|
517
617
|
let autoId = 0;
|
|
@@ -519,6 +619,7 @@ export class SketchFeature {
|
|
|
519
619
|
if (p == null) continue;
|
|
520
620
|
const u = Number(p.x); const v = Number(p.y);
|
|
521
621
|
if (!Number.isFinite(u) || !Number.isFinite(v)) continue;
|
|
622
|
+
if (p.construction === true) continue;
|
|
522
623
|
const w = to3D(u, v);
|
|
523
624
|
const hasExplicitId = p.id !== undefined && p.id !== null && `${p.id}` !== '';
|
|
524
625
|
const pointLabel = hasExplicitId ? p.id : autoId++;
|
|
@@ -526,8 +627,12 @@ export class SketchFeature {
|
|
|
526
627
|
try {
|
|
527
628
|
const vertex = new BREP.Vertex([w.x, w.y, w.z], { name: vertexName });
|
|
528
629
|
vertex.userData = vertex.userData || {};
|
|
529
|
-
|
|
630
|
+
const pointId = hasExplicitId ? p.id : pointLabel;
|
|
631
|
+
const pointIdNum = Number(pointId);
|
|
632
|
+
vertex.userData.sketchPointId = pointId;
|
|
530
633
|
vertex.userData.sketchFeatureId = featureId;
|
|
634
|
+
vertex.userData.isConstructionPoint = p.construction === true;
|
|
635
|
+
vertex.userData.isExternalReference = Number.isFinite(pointIdNum) && externalRefPointIds.has(pointIdNum);
|
|
531
636
|
sceneGroup.add(vertex);
|
|
532
637
|
} catch {}
|
|
533
638
|
}
|