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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brep-io-kernel",
3
- "version": "1.0.22",
3
+ "version": "1.0.24",
4
4
  "scripts": {
5
5
  "dev": "pnpm generateLicenses && pnpm build:kernel && vite --host 0.0.0.0",
6
6
  "build": "pnpm generateLicenses && vite build",
@@ -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 (dir === 'INSET' && typeof result.mergeTinyFaces === 'function') {
742
- await result.mergeTinyFaces(0.001);
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
- 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;
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
- for (let i = 0; i < edgeWedgeCopy.length; i++) {
827
- const edgeWedgePt = edgeWedgeCopy[i];
828
- const centerPt = centerlineCopy[i] || centerlineCopy[centerlineCopy.length - 1]; // Fallback to last point
829
-
830
- if (edgeWedgePt && centerPt) {
831
- try {
832
- const origWedgeEdge = { ...edgeWedgePt };
833
-
834
- // Calculate direction from edge point toward the centerline (inward direction)
835
- const inwardDir = {
836
- x: centerPt.x - edgeWedgePt.x,
837
- y: centerPt.y - edgeWedgePt.y,
838
- z: centerPt.z - edgeWedgePt.z
839
- };
840
- const inwardLength = Math.sqrt(inwardDir.x * inwardDir.x + inwardDir.y * inwardDir.y + inwardDir.z * inwardDir.z);
841
-
842
- if (inwardLength > 1e-12) {
843
- // Normalize and apply inset
844
- const normalizedInward = {
845
- x: inwardDir.x / inwardLength,
846
- y: inwardDir.y / inwardLength,
847
- z: inwardDir.z / inwardLength
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
- // Determine direction: OUTSET -> inward, INSET -> outward (opposite)
850
- const dirSign = (side === 'INSET') ? -1 : 1;
851
- const step = dirSign * wedgeInsetMagnitude;
852
- // Apply
853
- edgeWedgePt.x += normalizedInward.x * step;
854
- edgeWedgePt.y += normalizedInward.y * step;
855
- edgeWedgePt.z += normalizedInward.z * step;
856
-
857
- // Validate the result
858
- if (!isFiniteVec3(edgeWedgePt)) {
859
- console.warn(`Invalid wedge edge point after inset at index ${i}, reverting to original`);
860
- Object.assign(edgeWedgePt, origWedgeEdge);
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
- } else {
863
- console.warn(`Edge point ${i} is too close to centerline, skipping wedge inset`);
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 (${side === 'INSET' ? 'outward' : 'inward'}) to ${edgeWedgeCopy.length} edge points`);
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 { }