brep-io-kernel 1.0.22 → 1.0.24
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 +11131 -10986
- package/package.json +1 -1
- package/src/BREP/SolidMethods/fillet.js +17 -3
- package/src/BREP/SolidMethods/visualize.js +27 -5
- package/src/BREP/fillets/fillet.js +193 -39
- package/src/features/fillet/FilletFeature.js +7 -0
package/package.json
CHANGED
|
@@ -493,6 +493,7 @@ function mergeInsetEndCapsByNormal(resultSolid, featureID, direction, dotThresho
|
|
|
493
493
|
* @param {boolean} [opts.debug=false] Enable debug visuals in fillet builder
|
|
494
494
|
* @param {boolean} [opts.showTangentOverlays=false] Show pre-inflate tangent overlays on the fillet tube
|
|
495
495
|
* @param {string} [opts.featureID='FILLET'] For naming of intermediates and result
|
|
496
|
+
* @param {number} [opts.cleanupTinyFaceIslandsArea=0.001] area threshold for face-island relabeling (<= 0 disables)
|
|
496
497
|
* @returns {import('../BetterSolid.js').Solid}
|
|
497
498
|
*/
|
|
498
499
|
export async function fillet(opts = {}) {
|
|
@@ -511,6 +512,10 @@ export async function fillet(opts = {}) {
|
|
|
511
512
|
const combineEdges = (dir !== 'INSET') && !!opts.combineEdges;
|
|
512
513
|
const showTangentOverlays = !!opts.showTangentOverlays;
|
|
513
514
|
const featureID = opts.featureID || 'FILLET';
|
|
515
|
+
const cleanupTinyFaceIslandsAreaRaw = Number(opts.cleanupTinyFaceIslandsArea);
|
|
516
|
+
const cleanupTinyFaceIslandsArea = Number.isFinite(cleanupTinyFaceIslandsAreaRaw)
|
|
517
|
+
? cleanupTinyFaceIslandsAreaRaw
|
|
518
|
+
: 0.001;
|
|
514
519
|
const SolidCtor = this?.constructor;
|
|
515
520
|
consoleLogReplacement('[Solid.fillet] Begin', {
|
|
516
521
|
featureID,
|
|
@@ -522,6 +527,7 @@ export async function fillet(opts = {}) {
|
|
|
522
527
|
debug,
|
|
523
528
|
showTangentOverlays,
|
|
524
529
|
combineEdges,
|
|
530
|
+
cleanupTinyFaceIslandsArea,
|
|
525
531
|
requestedEdgeNames: Array.isArray(opts.edgeNames) ? opts.edgeNames : [],
|
|
526
532
|
providedEdgeCount: Array.isArray(opts.edges) ? opts.edges.length : 0,
|
|
527
533
|
});
|
|
@@ -738,8 +744,16 @@ export async function fillet(opts = {}) {
|
|
|
738
744
|
}
|
|
739
745
|
|
|
740
746
|
try {
|
|
741
|
-
if (
|
|
742
|
-
await result.
|
|
747
|
+
if (cleanupTinyFaceIslandsArea > 0 && typeof result.cleanupTinyFaceIslands === 'function') {
|
|
748
|
+
await result.cleanupTinyFaceIslands(cleanupTinyFaceIslandsArea);
|
|
749
|
+
}
|
|
750
|
+
} catch (err) {
|
|
751
|
+
console.warn('[Solid.fillet] cleanupTinyFaceIslands failed', { featureID, error: err?.message || err });
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
try {
|
|
755
|
+
if (typeof result.mergeTinyFaces === 'function') {
|
|
756
|
+
await result.mergeTinyFaces(0.1);
|
|
743
757
|
}
|
|
744
758
|
} catch (err) {
|
|
745
759
|
console.warn('[Solid.fillet] mergeTinyFaces failed', { featureID, error: err?.message || err });
|
|
@@ -789,4 +803,4 @@ export async function fillet(opts = {}) {
|
|
|
789
803
|
|
|
790
804
|
function consoleLogReplacement(args){
|
|
791
805
|
if (debugMode) console.log(...args);
|
|
792
|
-
}
|
|
806
|
+
}
|
|
@@ -354,12 +354,34 @@ export function visualize(options = {}) {
|
|
|
354
354
|
const key = (aux?.materialKey || 'OVERLAY').toUpperCase();
|
|
355
355
|
const edgeMats = CADmaterials?.EDGE || {};
|
|
356
356
|
const mat = edgeMats[key] || (key === 'OVERLAY' ? edgeMats.OVERLAY : null) || edgeMats.BASE;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
357
|
+
let appliedMat = mat;
|
|
358
|
+
const wantsOverlay = key !== 'BASE';
|
|
359
|
+
if (mat && wantsOverlay) {
|
|
360
|
+
const alreadyOverlay = mat.depthTest === false && mat.depthWrite === false;
|
|
361
|
+
if (!alreadyOverlay) {
|
|
362
|
+
const shared = Object.values(edgeMats).includes(mat);
|
|
363
|
+
let cloned = false;
|
|
364
|
+
if (shared && typeof mat.clone === 'function') {
|
|
365
|
+
try {
|
|
366
|
+
appliedMat = mat.clone();
|
|
367
|
+
cloned = !!appliedMat && appliedMat !== mat;
|
|
368
|
+
} catch { appliedMat = mat; cloned = false; }
|
|
369
|
+
if (cloned) {
|
|
370
|
+
try {
|
|
371
|
+
if (mat.resolution && appliedMat.resolution && typeof appliedMat.resolution.copy === 'function') {
|
|
372
|
+
appliedMat.resolution.copy(mat.resolution);
|
|
373
|
+
}
|
|
374
|
+
} catch { }
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if ((cloned || !shared) && appliedMat) {
|
|
378
|
+
try { appliedMat.depthTest = false; } catch { }
|
|
379
|
+
try { appliedMat.depthWrite = false; } catch { }
|
|
380
|
+
}
|
|
381
|
+
}
|
|
362
382
|
}
|
|
383
|
+
if (appliedMat) edgeObj.material = appliedMat;
|
|
384
|
+
if (appliedMat) SelectionState.setBaseMaterial(edgeObj, appliedMat, { force: false });
|
|
363
385
|
try { edgeObj.computeLineDistances(); } catch { }
|
|
364
386
|
edgeObj.renderOrder = 10020;
|
|
365
387
|
} catch { }
|
|
@@ -22,6 +22,83 @@ import { computeFaceAreaFromTriangles } from "./filletGeometry.js";
|
|
|
22
22
|
export { clearFilletCaches, trimFilletCaches } from './inset.js';
|
|
23
23
|
export { fixTJunctionsAndPatchHoles } from './outset.js';
|
|
24
24
|
|
|
25
|
+
function buildPointInsideTester(solid) {
|
|
26
|
+
if (!solid) return null;
|
|
27
|
+
const tv = solid._triVerts;
|
|
28
|
+
const vp = solid._vertProperties;
|
|
29
|
+
if (!tv || !vp || typeof tv.length !== 'number' || typeof vp.length !== 'number') return null;
|
|
30
|
+
const triCount = (tv.length / 3) | 0;
|
|
31
|
+
if (triCount === 0 || vp.length < 9) return null;
|
|
32
|
+
|
|
33
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
34
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
35
|
+
for (let i = 0; i < vp.length; i += 3) {
|
|
36
|
+
const x = vp[i], y = vp[i + 1], z = vp[i + 2];
|
|
37
|
+
if (x < minX) minX = x; if (x > maxX) maxX = x;
|
|
38
|
+
if (y < minY) minY = y; if (y > maxY) maxY = y;
|
|
39
|
+
if (z < minZ) minZ = z; if (z > maxZ) maxZ = z;
|
|
40
|
+
}
|
|
41
|
+
const diag = Math.hypot(maxX - minX, maxY - minY, maxZ - minZ) || 1;
|
|
42
|
+
const jitter = 1e-6 * diag;
|
|
43
|
+
|
|
44
|
+
const rayTri = (ox, oy, oz, dx, dy, dz, ax, ay, az, bx, by, bz, cx, cy, cz) => {
|
|
45
|
+
const EPS = 1e-12;
|
|
46
|
+
const e1x = bx - ax, e1y = by - ay, e1z = bz - az;
|
|
47
|
+
const e2x = cx - ax, e2y = cy - ay, e2z = cz - az;
|
|
48
|
+
const px = dy * e2z - dz * e2y;
|
|
49
|
+
const py = dz * e2x - dx * e2z;
|
|
50
|
+
const pz = dx * e2y - dy * e2x;
|
|
51
|
+
const det = e1x * px + e1y * py + e1z * pz;
|
|
52
|
+
if (Math.abs(det) < EPS) return null;
|
|
53
|
+
const invDet = 1.0 / det;
|
|
54
|
+
const tvecx = ox - ax, tvecy = oy - ay, tvecz = oz - az;
|
|
55
|
+
const u = (tvecx * px + tvecy * py + tvecz * pz) * invDet;
|
|
56
|
+
if (u < -1e-12 || u > 1 + 1e-12) return null;
|
|
57
|
+
const qx = tvecy * e1z - tvecz * e1y;
|
|
58
|
+
const qy = tvecz * e1x - tvecx * e1z;
|
|
59
|
+
const qz = tvecx * e1y - tvecy * e1x;
|
|
60
|
+
const v = (dx * qx + dy * qy + dz * qz) * invDet;
|
|
61
|
+
if (v < -1e-12 || u + v > 1 + 1e-12) return null;
|
|
62
|
+
const tHit = (e2x * qx + e2y * qy + e2z * qz) * invDet;
|
|
63
|
+
return tHit > 1e-10 ? tHit : null;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const dirs = [
|
|
67
|
+
[1, 0, 0],
|
|
68
|
+
[0, 1, 0],
|
|
69
|
+
[0, 0, 1],
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
return (pt) => {
|
|
73
|
+
if (!pt || !Number.isFinite(pt.x) || !Number.isFinite(pt.y) || !Number.isFinite(pt.z)) return false;
|
|
74
|
+
const px = pt.x, py = pt.y, pz = pt.z;
|
|
75
|
+
let votes = 0;
|
|
76
|
+
for (let k = 0; k < dirs.length; k++) {
|
|
77
|
+
const dir = dirs[k];
|
|
78
|
+
const ox = px + (k + 1) * jitter;
|
|
79
|
+
const oy = py + (k + 2) * jitter;
|
|
80
|
+
const oz = pz + (k + 3) * jitter;
|
|
81
|
+
let hits = 0;
|
|
82
|
+
for (let t = 0; t < triCount; t++) {
|
|
83
|
+
const b = t * 3;
|
|
84
|
+
const ia = (tv[b + 0] >>> 0) * 3;
|
|
85
|
+
const ib = (tv[b + 1] >>> 0) * 3;
|
|
86
|
+
const ic = (tv[b + 2] >>> 0) * 3;
|
|
87
|
+
const hit = rayTri(
|
|
88
|
+
ox, oy, oz,
|
|
89
|
+
dir[0], dir[1], dir[2],
|
|
90
|
+
vp[ia + 0], vp[ia + 1], vp[ia + 2],
|
|
91
|
+
vp[ib + 0], vp[ib + 1], vp[ib + 2],
|
|
92
|
+
vp[ic + 0], vp[ic + 1], vp[ic + 2]
|
|
93
|
+
);
|
|
94
|
+
if (hit !== null) hits++;
|
|
95
|
+
}
|
|
96
|
+
if ((hits % 2) === 1) votes++;
|
|
97
|
+
}
|
|
98
|
+
return votes >= 2;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
25
102
|
/**
|
|
26
103
|
* Compute the fillet centerline polyline for an input edge without building the fillet solid.
|
|
27
104
|
*
|
|
@@ -823,52 +900,129 @@ export function filletSolid({ edgeToFillet, radius = 1, sideMode = 'INSET', debu
|
|
|
823
900
|
// large displacements on big models.
|
|
824
901
|
const outsetInsetMagnitude = Math.max(1e-4, Math.min(0.05, Math.abs(radius) * 0.05));
|
|
825
902
|
const wedgeInsetMagnitude = closedLoop ? 0 : ((side === 'INSET') ? Math.abs(inflate) : outsetInsetMagnitude);
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
903
|
+
const useInsideCheck = wedgeInsetMagnitude && side === 'OUTSET';
|
|
904
|
+
const pointInsideTarget = useInsideCheck
|
|
905
|
+
? buildPointInsideTester(edgeToFillet?.parentSolid || edgeToFillet?.parent || null)
|
|
906
|
+
: null;
|
|
907
|
+
let preferredDirSign = null;
|
|
908
|
+
let insideResults = null;
|
|
909
|
+
if (pointInsideTarget) {
|
|
910
|
+
insideResults = new Array(edgeWedgeCopy.length);
|
|
911
|
+
let countIn = 0;
|
|
912
|
+
let countOut = 0;
|
|
913
|
+
for (let i = 0; i < edgeWedgeCopy.length; i++) {
|
|
914
|
+
const edgeWedgePt = edgeWedgeCopy[i];
|
|
915
|
+
const centerPt = centerlineCopy[i] || centerlineCopy[centerlineCopy.length - 1];
|
|
916
|
+
if (!edgeWedgePt || !centerPt) continue;
|
|
917
|
+
const inwardDir = {
|
|
918
|
+
x: centerPt.x - edgeWedgePt.x,
|
|
919
|
+
y: centerPt.y - edgeWedgePt.y,
|
|
920
|
+
z: centerPt.z - edgeWedgePt.z
|
|
921
|
+
};
|
|
922
|
+
const inwardLength = Math.sqrt(inwardDir.x * inwardDir.x + inwardDir.y * inwardDir.y + inwardDir.z * inwardDir.z);
|
|
923
|
+
if (inwardLength <= 1e-12) continue;
|
|
924
|
+
const nx = inwardDir.x / inwardLength;
|
|
925
|
+
const ny = inwardDir.y / inwardLength;
|
|
926
|
+
const nz = inwardDir.z / inwardLength;
|
|
927
|
+
const candidateIn = {
|
|
928
|
+
x: edgeWedgePt.x + nx * wedgeInsetMagnitude,
|
|
929
|
+
y: edgeWedgePt.y + ny * wedgeInsetMagnitude,
|
|
930
|
+
z: edgeWedgePt.z + nz * wedgeInsetMagnitude
|
|
931
|
+
};
|
|
932
|
+
const candidateOut = {
|
|
933
|
+
x: edgeWedgePt.x - nx * wedgeInsetMagnitude,
|
|
934
|
+
y: edgeWedgePt.y - ny * wedgeInsetMagnitude,
|
|
935
|
+
z: edgeWedgePt.z - nz * wedgeInsetMagnitude
|
|
936
|
+
};
|
|
937
|
+
const inInside = pointInsideTarget(candidateIn);
|
|
938
|
+
const outInside = pointInsideTarget(candidateOut);
|
|
939
|
+
insideResults[i] = { inInside, outInside };
|
|
940
|
+
if (inInside !== outInside) {
|
|
941
|
+
if (inInside) countIn++; else countOut++;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (countIn || countOut) {
|
|
945
|
+
preferredDirSign = countIn >= countOut ? 1 : -1;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (wedgeInsetMagnitude) {
|
|
949
|
+
for (let i = 0; i < edgeWedgeCopy.length; i++) {
|
|
950
|
+
const edgeWedgePt = edgeWedgeCopy[i];
|
|
951
|
+
const centerPt = centerlineCopy[i] || centerlineCopy[centerlineCopy.length - 1]; // Fallback to last point
|
|
952
|
+
|
|
953
|
+
if (edgeWedgePt && centerPt) {
|
|
954
|
+
try {
|
|
955
|
+
const origWedgeEdge = { ...edgeWedgePt };
|
|
956
|
+
|
|
957
|
+
// Calculate direction from edge point toward the centerline (inward direction)
|
|
958
|
+
const inwardDir = {
|
|
959
|
+
x: centerPt.x - edgeWedgePt.x,
|
|
960
|
+
y: centerPt.y - edgeWedgePt.y,
|
|
961
|
+
z: centerPt.z - edgeWedgePt.z
|
|
848
962
|
};
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
963
|
+
const inwardLength = Math.sqrt(inwardDir.x * inwardDir.x + inwardDir.y * inwardDir.y + inwardDir.z * inwardDir.z);
|
|
964
|
+
|
|
965
|
+
if (inwardLength > 1e-12) {
|
|
966
|
+
// Normalize and apply inset
|
|
967
|
+
const normalizedInward = {
|
|
968
|
+
x: inwardDir.x / inwardLength,
|
|
969
|
+
y: inwardDir.y / inwardLength,
|
|
970
|
+
z: inwardDir.z / inwardLength
|
|
971
|
+
};
|
|
972
|
+
const candidateIn = {
|
|
973
|
+
x: origWedgeEdge.x + normalizedInward.x * wedgeInsetMagnitude,
|
|
974
|
+
y: origWedgeEdge.y + normalizedInward.y * wedgeInsetMagnitude,
|
|
975
|
+
z: origWedgeEdge.z + normalizedInward.z * wedgeInsetMagnitude
|
|
976
|
+
};
|
|
977
|
+
const candidateOut = {
|
|
978
|
+
x: origWedgeEdge.x - normalizedInward.x * wedgeInsetMagnitude,
|
|
979
|
+
y: origWedgeEdge.y - normalizedInward.y * wedgeInsetMagnitude,
|
|
980
|
+
z: origWedgeEdge.z - normalizedInward.z * wedgeInsetMagnitude
|
|
981
|
+
};
|
|
982
|
+
let chosen = null;
|
|
983
|
+
|
|
984
|
+
if (pointInsideTarget) {
|
|
985
|
+
const insideRes = insideResults ? insideResults[i] : null;
|
|
986
|
+
const inInside = insideRes ? insideRes.inInside : pointInsideTarget(candidateIn);
|
|
987
|
+
const outInside = insideRes ? insideRes.outInside : pointInsideTarget(candidateOut);
|
|
988
|
+
if (inInside !== outInside) {
|
|
989
|
+
chosen = inInside ? candidateIn : candidateOut;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (!chosen) {
|
|
994
|
+
// Fallback to previous sign-based behavior.
|
|
995
|
+
const dirSign = (preferredDirSign !== null)
|
|
996
|
+
? preferredDirSign
|
|
997
|
+
: ((side === 'INSET') ? -1 : 1);
|
|
998
|
+
const step = dirSign * wedgeInsetMagnitude;
|
|
999
|
+
chosen = {
|
|
1000
|
+
x: origWedgeEdge.x + normalizedInward.x * step,
|
|
1001
|
+
y: origWedgeEdge.y + normalizedInward.y * step,
|
|
1002
|
+
z: origWedgeEdge.z + normalizedInward.z * step
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
edgeWedgePt.x = chosen.x;
|
|
1007
|
+
edgeWedgePt.y = chosen.y;
|
|
1008
|
+
edgeWedgePt.z = chosen.z;
|
|
1009
|
+
|
|
1010
|
+
// Validate the result
|
|
1011
|
+
if (!isFiniteVec3(edgeWedgePt)) {
|
|
1012
|
+
console.warn(`Invalid wedge edge point after inset at index ${i}, reverting to original`);
|
|
1013
|
+
Object.assign(edgeWedgePt, origWedgeEdge);
|
|
1014
|
+
}
|
|
1015
|
+
} else {
|
|
1016
|
+
console.warn(`Edge point ${i} is too close to centerline, skipping wedge inset`);
|
|
861
1017
|
}
|
|
862
|
-
}
|
|
863
|
-
console.warn(`
|
|
1018
|
+
} catch (insetError) {
|
|
1019
|
+
console.warn(`Wedge edge inset failed at index ${i}: ${insetError?.message || insetError}`);
|
|
864
1020
|
}
|
|
865
|
-
} catch (insetError) {
|
|
866
|
-
console.warn(`Wedge edge inset failed at index ${i}: ${insetError?.message || insetError}`);
|
|
867
1021
|
}
|
|
868
1022
|
}
|
|
869
1023
|
}
|
|
870
1024
|
|
|
871
|
-
if (wedgeInsetMagnitude) logDebug(`Applied wedge inset of ${wedgeInsetMagnitude} units (
|
|
1025
|
+
if (wedgeInsetMagnitude) logDebug(`Applied wedge inset of ${wedgeInsetMagnitude} units (inside-aware) to ${edgeWedgeCopy.length} edge points`);
|
|
872
1026
|
|
|
873
1027
|
|
|
874
1028
|
// Do not reorder edge points. Centerline/tangent/edge points are produced in
|
|
@@ -52,6 +52,12 @@ const inputParamsSchema = {
|
|
|
52
52
|
default_value: false,
|
|
53
53
|
hint: "Show pre-inflate tangent overlays on the fillet tube",
|
|
54
54
|
},
|
|
55
|
+
cleanupTinyFaceIslandsArea: {
|
|
56
|
+
type: "number",
|
|
57
|
+
step: 0.001,
|
|
58
|
+
default_value: 0.01,
|
|
59
|
+
hint: "Relabel tiny disconnected face islands below this area threshold (<= 0 disables).",
|
|
60
|
+
},
|
|
55
61
|
debug: {
|
|
56
62
|
type: "boolean",
|
|
57
63
|
default_value: false,
|
|
@@ -96,6 +102,7 @@ export class FilletFeature {
|
|
|
96
102
|
resolution: this.inputParams?.resolution,
|
|
97
103
|
inflate: this.inputParams?.inflate,
|
|
98
104
|
showTangentOverlays: this.inputParams?.showTangentOverlays,
|
|
105
|
+
cleanupTinyFaceIslandsArea: this.inputParams?.cleanupTinyFaceIslandsArea,
|
|
99
106
|
debug: this.inputParams?.debug,
|
|
100
107
|
});
|
|
101
108
|
try { clearFilletCaches(); } catch { }
|