fluidcad 0.0.26 → 0.0.28

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 (93) hide show
  1. package/lib/dist/common/scene-object.d.ts +45 -0
  2. package/lib/dist/common/scene-object.js +121 -0
  3. package/lib/dist/common/shape-factory.d.ts +1 -1
  4. package/lib/dist/common/shape-history-tracker.d.ts +35 -0
  5. package/lib/dist/common/shape-history-tracker.js +114 -0
  6. package/lib/dist/common/shape.js +7 -1
  7. package/lib/dist/common/shapes.d.ts +0 -1
  8. package/lib/dist/common/shapes.js +0 -1
  9. package/lib/dist/common/solid.js +5 -1
  10. package/lib/dist/core/extrude.d.ts +12 -13
  11. package/lib/dist/core/extrude.js +19 -1
  12. package/lib/dist/core/part.d.ts +2 -1
  13. package/lib/dist/core/part.js +4 -1
  14. package/lib/dist/core/sketch.d.ts +4 -3
  15. package/lib/dist/core/sketch.js +4 -1
  16. package/lib/dist/features/chamfer.js +12 -6
  17. package/lib/dist/features/extrude-base.d.ts +43 -1
  18. package/lib/dist/features/extrude-base.js +141 -36
  19. package/lib/dist/features/extrude-to-face.d.ts +1 -1
  20. package/lib/dist/features/extrude-to-face.js +42 -19
  21. package/lib/dist/features/extrude-two-distances.d.ts +1 -1
  22. package/lib/dist/features/extrude-two-distances.js +41 -15
  23. package/lib/dist/features/extrude.d.ts +1 -1
  24. package/lib/dist/features/extrude.js +75 -20
  25. package/lib/dist/features/fillet.js +3 -4
  26. package/lib/dist/features/fuse.js +14 -0
  27. package/lib/dist/features/infinite-extrude.d.ts +1 -0
  28. package/lib/dist/features/infinite-extrude.js +33 -4
  29. package/lib/dist/features/loft.js +18 -5
  30. package/lib/dist/features/mirror-shape.d.ts +1 -3
  31. package/lib/dist/features/mirror-shape.js +2 -1
  32. package/lib/dist/features/revolve.js +17 -4
  33. package/lib/dist/features/rotate.js +1 -0
  34. package/lib/dist/features/simple-extruder.js +5 -0
  35. package/lib/dist/features/sweep.js +13 -2
  36. package/lib/dist/features/translate.js +3 -1
  37. package/lib/dist/filters/face/face-filter.d.ts +12 -0
  38. package/lib/dist/filters/face/face-filter.js +21 -0
  39. package/lib/dist/filters/face/torus-filter.d.ts +19 -0
  40. package/lib/dist/filters/face/torus-filter.js +38 -0
  41. package/lib/dist/helpers/scene-helpers.d.ts +10 -2
  42. package/lib/dist/helpers/scene-helpers.js +278 -10
  43. package/lib/dist/index.d.ts +1 -0
  44. package/lib/dist/oc/boolean-ops.d.ts +32 -4
  45. package/lib/dist/oc/boolean-ops.js +122 -11
  46. package/lib/dist/oc/color-transfer.d.ts +37 -0
  47. package/lib/dist/oc/color-transfer.js +135 -0
  48. package/lib/dist/oc/extrude-ops.js +25 -3
  49. package/lib/dist/oc/face-ops.d.ts +0 -1
  50. package/lib/dist/oc/face-ops.js +0 -13
  51. package/lib/dist/oc/face-query.d.ts +2 -0
  52. package/lib/dist/oc/face-query.js +30 -0
  53. package/lib/dist/oc/fillet-ops.d.ts +5 -3
  54. package/lib/dist/oc/fillet-ops.js +107 -70
  55. package/lib/dist/oc/intersection.js +6 -3
  56. package/lib/dist/oc/mesh.d.ts +25 -2
  57. package/lib/dist/oc/mesh.js +112 -35
  58. package/lib/dist/oc/shape-ops.d.ts +25 -20
  59. package/lib/dist/oc/shape-ops.js +129 -113
  60. package/lib/dist/rendering/mesh-transform.js +17 -1
  61. package/lib/dist/rendering/render-solid.js +19 -6
  62. package/lib/dist/rendering/render-wire.js +2 -0
  63. package/lib/dist/rendering/render.d.ts +12 -2
  64. package/lib/dist/rendering/render.js +195 -220
  65. package/lib/dist/scene-manager.d.ts +2 -0
  66. package/lib/dist/scene-manager.js +4 -3
  67. package/lib/dist/tests/common/scene-object-history.test.d.ts +1 -0
  68. package/lib/dist/tests/common/scene-object-history.test.js +274 -0
  69. package/lib/dist/tests/common/shape-history-tracker.test.d.ts +1 -0
  70. package/lib/dist/tests/common/shape-history-tracker.test.js +110 -0
  71. package/lib/dist/tests/features/2d/project-regression.test.d.ts +1 -0
  72. package/lib/dist/tests/features/2d/project-regression.test.js +69 -0
  73. package/lib/dist/tests/features/2d/project-user-regression.test.d.ts +1 -0
  74. package/lib/dist/tests/features/2d/project-user-regression.test.js +37 -0
  75. package/lib/dist/tests/features/color-lineage.test.d.ts +1 -0
  76. package/lib/dist/tests/features/color-lineage.test.js +213 -0
  77. package/lib/dist/tests/features/cut-symmetric-through-all.test.d.ts +1 -0
  78. package/lib/dist/tests/features/cut-symmetric-through-all.test.js +32 -0
  79. package/lib/dist/tests/features/extrude-history.test.d.ts +1 -0
  80. package/lib/dist/tests/features/extrude-history.test.js +248 -0
  81. package/lib/dist/tests/features/extrude.test.js +71 -0
  82. package/lib/dist/tests/features/fillet2d.test.js +16 -1
  83. package/lib/dist/tests/features/peer-ops-history.test.d.ts +1 -0
  84. package/lib/dist/tests/features/peer-ops-history.test.js +119 -0
  85. package/lib/dist/tests/features/select.test.js +50 -0
  86. package/lib/dist/tests/features/subtract.test.js +21 -1
  87. package/lib/dist/tests/setup.js +3 -2
  88. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  89. package/package.json +3 -3
  90. package/ui/dist/assets/{index-BeLxRMCv.js → index-BrW_x4uc.js} +37 -37
  91. package/ui/dist/index.html +1 -1
  92. package/lib/dist/common/solid-face.d.ts +0 -9
  93. package/lib/dist/common/solid-face.js +0 -22
@@ -0,0 +1,135 @@
1
+ import { getOC } from "./init.js";
2
+ import { Explorer } from "./explorer.js";
3
+ import { ShapeOps } from "./shape-ops.js";
4
+ import { Face } from "../common/face.js";
5
+ /**
6
+ * Walk each source shape's `colorMap`, find where each colored face ended up in
7
+ * the result shapes via `maker.Modified()` (falling back to the unchanged face
8
+ * if `!IsDeleted`), and apply the color to whichever result shape now owns it.
9
+ *
10
+ * Works for any `BRepBuilderAPI_MakeShape`-derived maker — fuse, cut, fillet,
11
+ * chamfer, transform, etc.
12
+ */
13
+ export class ColorTransfer {
14
+ static applyThroughMaker(sources, results, maker) {
15
+ const oc = getOC();
16
+ const FACE = oc.TopAbs_ShapeEnum.TopAbs_FACE;
17
+ for (const source of sources) {
18
+ if (!source.hasColors()) {
19
+ continue;
20
+ }
21
+ for (const entry of source.colorMap) {
22
+ const modifiedRaws = ShapeOps.shapeListToArray(maker.Modified(entry.shape))
23
+ .filter(s => s.ShapeType() === FACE);
24
+ let targets;
25
+ if (modifiedRaws.length > 0) {
26
+ targets = modifiedRaws;
27
+ }
28
+ else if (!maker.IsDeleted(entry.shape)) {
29
+ targets = [entry.shape];
30
+ }
31
+ else {
32
+ continue;
33
+ }
34
+ for (const target of targets) {
35
+ for (const result of results) {
36
+ const faces = Explorer.findShapes(result.getShape(), FACE);
37
+ if (faces.some(f => f.IsSame(target))) {
38
+ result.setColor(target, entry.color);
39
+ break;
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ /**
47
+ * Color bleed pass: spreads colors to result faces that came from new
48
+ * geometry (tool inputs, generated faces, or just brand-new) by walking
49
+ * face-edge adjacency in each result solid.
50
+ *
51
+ * Faces that came from `sceneSources` (whether modified or unchanged) are
52
+ * NOT bled — those represent existing geometry whose color state the user
53
+ * explicitly chose. Faces NOT from any sceneSource are eligible: this
54
+ * covers tool extrusions, fillet/chamfer-generated surfaces, and cut
55
+ * section faces.
56
+ *
57
+ * Iterates until stable so newly-bled faces can spread color further.
58
+ * Call AFTER `applyThroughMaker` so the colored seeds are in place.
59
+ */
60
+ static applyBleeding(sceneSources, results, maker) {
61
+ const oc = getOC();
62
+ const FACE = oc.TopAbs_ShapeEnum.TopAbs_FACE;
63
+ const EDGE = oc.TopAbs_ShapeEnum.TopAbs_EDGE;
64
+ const protectedFaces = new oc.TopTools_MapOfShape();
65
+ for (const scene of sceneSources) {
66
+ for (const inputFace of Explorer.findShapes(scene.getShape(), FACE)) {
67
+ const modified = ShapeOps.shapeListToArray(maker.Modified(inputFace))
68
+ .filter(s => s.ShapeType() === FACE);
69
+ if (modified.length > 0) {
70
+ for (const r of modified) {
71
+ protectedFaces.Add(r);
72
+ }
73
+ }
74
+ else if (!maker.IsDeleted(inputFace)) {
75
+ protectedFaces.Add(inputFace);
76
+ }
77
+ }
78
+ }
79
+ for (const result of results) {
80
+ const allFaces = Explorer.findShapes(result.getShape(), FACE);
81
+ // Cache edges per face — repeated `findShapes` is expensive.
82
+ const faceEdges = allFaces.map(f => Explorer.findShapes(f, EDGE));
83
+ let changed = true;
84
+ while (changed) {
85
+ changed = false;
86
+ for (let i = 0; i < allFaces.length; i++) {
87
+ const face = allFaces[i];
88
+ if (protectedFaces.Contains(face)) {
89
+ continue;
90
+ }
91
+ if (result.getColor(face)) {
92
+ continue;
93
+ }
94
+ const myEdges = faceEdges[i];
95
+ for (let j = 0; j < allFaces.length; j++) {
96
+ if (i === j) {
97
+ continue;
98
+ }
99
+ const otherEdges = faceEdges[j];
100
+ const adjacent = myEdges.some(me => otherEdges.some(oe => me.IsSame(oe)));
101
+ if (!adjacent) {
102
+ continue;
103
+ }
104
+ const otherColor = result.getColor(allFaces[j]);
105
+ if (otherColor) {
106
+ result.setColor(face, otherColor);
107
+ changed = true;
108
+ break;
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
114
+ protectedFaces.delete();
115
+ }
116
+ /**
117
+ * Transfer colors from a pre-clean source shape through a `cleanShapeWithLineage`
118
+ * cleanup's `BRepTools_History` onto the post-clean result. Use this when an
119
+ * op is chained as `maker → cleanShape` — first apply `applyThroughMaker` to
120
+ * move colors from the original source onto the pre-clean result, then call
121
+ * this to chain them through the cleanup's UnifySameDomain history.
122
+ */
123
+ static applyThroughCleanup(source, cleanup) {
124
+ for (const entry of source.colorMap) {
125
+ const preFace = Face.fromTopoDSFace(Explorer.toFace(entry.shape));
126
+ const postFaces = cleanup.remapFace(preFace);
127
+ if (!postFaces) {
128
+ continue;
129
+ }
130
+ for (const postFace of postFaces) {
131
+ cleanup.shape.setColor(postFace.getShape(), entry.color);
132
+ }
133
+ }
134
+ }
135
+ }
@@ -59,11 +59,33 @@ export class ExtrudeOps {
59
59
  static makeRevol(shape, axis, angle) {
60
60
  const oc = getOC();
61
61
  const [ax1, disposeAx1] = Convert.toGpAx1(axis);
62
- const revol = new oc.BRepPrimAPI_MakeRevol(shape.getShape(), ax1, angle, true);
63
- const result = revol.Shape();
62
+ let revol;
63
+ try {
64
+ revol = new oc.BRepPrimAPI_MakeRevol(shape.getShape(), ax1, angle, true);
65
+ }
66
+ catch {
67
+ disposeAx1();
68
+ throw new Error("Revolution failed");
69
+ }
70
+ if (!revol.IsDone()) {
71
+ revol.delete();
72
+ disposeAx1();
73
+ throw new Error("Revolution failed");
74
+ }
75
+ const rawResult = revol.Shape();
64
76
  revol.delete();
65
77
  disposeAx1();
66
- const clean = ShapeOps.cleanShapeRaw(result);
78
+ // A profile face whose normal points "backwards" relative to the axis
79
+ // produces a closed solid with inverted shell orientation. Volume can
80
+ // still be positive but downstream boolean ops fail. OrientClosedSolid
81
+ // flips the shell to outward-facing when needed.
82
+ let oriented = rawResult;
83
+ if (Explorer.isSolid(rawResult)) {
84
+ const solid = Explorer.toSolid(rawResult);
85
+ oc.BRepLib.OrientClosedSolid(solid);
86
+ oriented = solid;
87
+ }
88
+ const clean = ShapeOps.cleanShapeRaw(oriented);
67
89
  return ShapeFactory.fromShape(clean);
68
90
  }
69
91
  static applyDraftOnSideFaces(solid, firstFace, lastFace, plane, angle) {
@@ -20,7 +20,6 @@ export declare class FaceOps {
20
20
  static fixFaceOrientation(face: Face | TopoDS_Face): Face;
21
21
  static makeFaceWithHoles(outerWire: Wire, holes: Wire[]): Face;
22
22
  static isPointInsideFace(point: Point, face: Face | TopoDS_Face): boolean;
23
- static getFreeBoundsWire(compound: any): TopoDS_Wire | null;
24
23
  static makeFaceFromPlane2(plane: gp_Pln): TopoDS_Face;
25
24
  static makeFaceFromPlane(plane: gp_Pln): TopoDS_Face;
26
25
  static makeFaceFromCylinder(cylinder: gp_Cylinder): TopoDS_Face;
@@ -197,19 +197,6 @@ export class FaceOps {
197
197
  disposePnt();
198
198
  return isInside;
199
199
  }
200
- static getFreeBoundsWire(compound) {
201
- const oc = getOC();
202
- const freeBounds = new oc.ShapeAnalysis_FreeBounds(compound, oc.Precision.Confusion(), true, true);
203
- const closedWiresCompound = freeBounds.GetClosedWires();
204
- const explorer = new oc.TopExp_Explorer(closedWiresCompound, oc.TopAbs_ShapeEnum.TopAbs_WIRE, oc.TopAbs_ShapeEnum.TopAbs_SHAPE);
205
- let result = null;
206
- if (explorer.More()) {
207
- result = oc.TopoDS.Wire(explorer.Current());
208
- }
209
- explorer.delete();
210
- freeBounds.delete();
211
- return result;
212
- }
213
200
  static makeFaceFromPlane2(plane) {
214
201
  const oc = getOC();
215
202
  const faceMaker = new oc.BRepBuilderAPI_MakeFace(plane, -1000, 1000, -1000, 1000);
@@ -8,6 +8,7 @@ export declare class FaceQuery {
8
8
  static isConeFace(face: Shape): boolean;
9
9
  static isCylinderFace(face: Shape, diameter?: number): boolean;
10
10
  static isCylinderCurveFace(face: Shape, diameter?: number): boolean;
11
+ static isTorusFace(face: Shape, majorRadius?: number, minorRadius?: number): boolean;
11
12
  static isFaceOnPlane(face: Shape, plane: Plane): boolean;
12
13
  static doesFaceIntersectPlane(face: Shape, plane: Plane): boolean;
13
14
  static isFaceParallelToPlane(face: Shape, plane: Plane): boolean;
@@ -23,6 +24,7 @@ export declare class FaceQuery {
23
24
  static isConeFaceRaw(face: TopoDS_Shape): boolean;
24
25
  static isCylinderFaceRaw(face: TopoDS_Shape, diameter?: number): boolean;
25
26
  static isCylinderCurveFaceRaw(face: TopoDS_Shape, diameter?: number): boolean;
27
+ static isTorusFaceRaw(face: TopoDS_Shape, majorRadius?: number, minorRadius?: number): boolean;
26
28
  static isFaceOnPlaneRaw(face: TopoDS_Shape, plane: gp_Pln): boolean;
27
29
  static isFaceParallelToPlaneRaw(face: TopoDS_Shape, plane: gp_Pln): boolean;
28
30
  static isPlanarFaceRaw(face: TopoDS_Shape): boolean;
@@ -17,6 +17,9 @@ export class FaceQuery {
17
17
  static isCylinderCurveFace(face, diameter) {
18
18
  return FaceQuery.isCylinderCurveFaceRaw(face.getShape(), diameter);
19
19
  }
20
+ static isTorusFace(face, majorRadius, minorRadius) {
21
+ return FaceQuery.isTorusFaceRaw(face.getShape(), majorRadius, minorRadius);
22
+ }
20
23
  static isFaceOnPlane(face, plane) {
21
24
  const [gpPln, dispose] = Convert.toGpPln(plane);
22
25
  const result = FaceQuery.isFaceOnPlaneRaw(face.getShape(), gpPln);
@@ -198,6 +201,33 @@ export class FaceQuery {
198
201
  }
199
202
  return false;
200
203
  }
204
+ static isTorusFaceRaw(face, majorRadius, minorRadius) {
205
+ const oc = getOC();
206
+ const ocFace = oc.TopoDS.Face(face);
207
+ const faceAdaptor = new oc.BRepAdaptor_Surface(ocFace, true);
208
+ const type = faceAdaptor.GetType();
209
+ if (type !== oc.GeomAbs_SurfaceType.GeomAbs_Torus) {
210
+ faceAdaptor.delete();
211
+ return false;
212
+ }
213
+ if (majorRadius === undefined && minorRadius === undefined) {
214
+ faceAdaptor.delete();
215
+ return true;
216
+ }
217
+ const torus = faceAdaptor.Torus();
218
+ const actualMajor = torus.MajorRadius();
219
+ const actualMinor = torus.MinorRadius();
220
+ torus.delete();
221
+ faceAdaptor.delete();
222
+ const tol = oc.Precision.Confusion();
223
+ if (majorRadius !== undefined && Math.abs(actualMajor - majorRadius) > tol) {
224
+ return false;
225
+ }
226
+ if (minorRadius !== undefined && Math.abs(actualMinor - minorRadius) > tol) {
227
+ return false;
228
+ }
229
+ return true;
230
+ }
201
231
  static isFaceOnPlaneRaw(face, plane) {
202
232
  const oc = getOC();
203
233
  return FaceOps.faceOnPlane(oc.TopoDS.Face(face), plane);
@@ -2,12 +2,14 @@ import type { gp_Pln, TopoDS_Wire } from "occjs-wrapper";
2
2
  import { Shape } from "../common/shape.js";
3
3
  import { Edge } from "../common/edge.js";
4
4
  import { Face } from "../common/face.js";
5
+ import { Solid } from "../common/solid.js";
5
6
  import { Wire } from "../common/wire.js";
6
7
  import { Plane } from "../math/plane.js";
7
8
  export declare class FilletOps {
8
- static makeFillet(solid: Shape, edges: Edge[], radius: number): Shape;
9
- static makeChamfer(solid: Shape, edges: Edge[], distance: number): Shape;
10
- static makeChamferTwoDistances(solid: Shape, edges: Edge[], distance1: number, distance2: number, faces: Face[], isAngle?: boolean): Shape;
9
+ static makeFillet(solid: Shape, edges: Edge[], radius: number): Solid[];
10
+ static makeChamfer(solid: Shape, edges: Edge[], distance: number): Solid[];
11
+ private static wrapResultSolids;
12
+ static makeChamferTwoDistances(solid: Shape, edges: Edge[], distance1: number, distance2: number, faces: Face[], isAngle?: boolean): Solid[];
11
13
  static fillet2d(shape: Wire | Edge, plane: Plane, radius: number): Wire;
12
14
  static fillet2dRaw(wire: TopoDS_Wire, plane: gp_Pln, radius: number): TopoDS_Wire;
13
15
  }
@@ -1,9 +1,11 @@
1
1
  import { getOC } from "./init.js";
2
2
  import { Convert } from "./convert.js";
3
+ import { Solid } from "../common/solid.js";
3
4
  import { Wire } from "../common/wire.js";
4
- import { ShapeFactory } from "../common/shape-factory.js";
5
5
  import { WireOps } from "./wire-ops.js";
6
6
  import { rad } from "../helpers/math-helpers.js";
7
+ import { ColorTransfer } from "./color-transfer.js";
8
+ import { Explorer } from "./explorer.js";
7
9
  export class FilletOps {
8
10
  static makeFillet(solid, edges, radius) {
9
11
  const oc = getOC();
@@ -19,8 +21,11 @@ export class FilletOps {
19
21
  throw new Error("Failed to create fillet.");
20
22
  }
21
23
  const result = maker.Shape();
24
+ const solids = FilletOps.wrapResultSolids(result);
25
+ ColorTransfer.applyThroughMaker([solid], solids, maker);
26
+ ColorTransfer.applyBleeding([solid], solids, maker);
22
27
  maker.delete();
23
- return ShapeFactory.fromShape(result);
28
+ return solids;
24
29
  }
25
30
  static makeChamfer(solid, edges, distance) {
26
31
  const oc = getOC();
@@ -36,8 +41,19 @@ export class FilletOps {
36
41
  throw new Error("Failed to create chamfer.");
37
42
  }
38
43
  const result = maker.Shape();
44
+ const solids = FilletOps.wrapResultSolids(result);
45
+ ColorTransfer.applyThroughMaker([solid], solids, maker);
46
+ ColorTransfer.applyBleeding([solid], solids, maker);
39
47
  maker.delete();
40
- return ShapeFactory.fromShape(result);
48
+ return solids;
49
+ }
50
+ static wrapResultSolids(result) {
51
+ const oc = getOC();
52
+ if (Explorer.isSolid(result)) {
53
+ return [Solid.fromTopoDSSolid(Explorer.toSolid(result))];
54
+ }
55
+ const solidRaws = Explorer.findShapes(result, oc.TopAbs_ShapeEnum.TopAbs_SOLID);
56
+ return solidRaws.map(r => Solid.fromTopoDSSolid(Explorer.toSolid(r)));
41
57
  }
42
58
  static makeChamferTwoDistances(solid, edges, distance1, distance2, faces, isAngle = false) {
43
59
  const oc = getOC();
@@ -62,8 +78,11 @@ export class FilletOps {
62
78
  throw new Error("Failed to create chamfer.");
63
79
  }
64
80
  const result = maker.Shape();
81
+ const solids = FilletOps.wrapResultSolids(result);
82
+ ColorTransfer.applyThroughMaker([solid], solids, maker);
83
+ ColorTransfer.applyBleeding([solid], solids, maker);
65
84
  maker.delete();
66
- return ShapeFactory.fromShape(result);
85
+ return solids;
67
86
  }
68
87
  static fillet2d(shape, plane, radius) {
69
88
  const wire = shape instanceof Wire ? shape : WireOps.makeWireFromEdges([shape]);
@@ -74,80 +93,98 @@ export class FilletOps {
74
93
  }
75
94
  static fillet2dRaw(wire, plane, radius) {
76
95
  const oc = getOC();
77
- let currentWire = wire;
78
- let cornerIndex = 0;
79
96
  const isClosed = wire.Closed();
80
- try {
81
- while (true) {
82
- const edges = [];
83
- const explorer = new oc.BRepTools_WireExplorer(currentWire);
84
- while (explorer.More()) {
85
- edges.push(oc.TopoDS.Edge(explorer.Current()));
86
- explorer.Next();
87
- }
88
- explorer.delete();
89
- const maxCorners = isClosed ? edges.length : edges.length - 1;
90
- if (cornerIndex >= maxCorners) {
91
- edges.forEach(e => e.delete());
92
- break;
97
+ const ownedEdges = [];
98
+ // Extract edges in wire traversal order and canonicalize: for each REVERSED edge,
99
+ // build a new FORWARD edge whose natural parameterization matches the wire traversal.
100
+ // ChFi2d_FilletAPI returns modified edges whose natural direction matches the input's
101
+ // natural direction, so aligning natural direction with wire traversal makes the
102
+ // modEdges directly usable when rebuilding the final wire.
103
+ const wireEdges = [];
104
+ {
105
+ const explorer = new oc.BRepTools_WireExplorer(wire);
106
+ while (explorer.More()) {
107
+ const raw = oc.TopoDS.Edge(explorer.Current());
108
+ const isReversed = raw.Orientation().value === oc.TopAbs_Orientation.TopAbs_REVERSED.value;
109
+ if (!isReversed) {
110
+ wireEdges.push(raw);
111
+ ownedEdges.push(raw);
93
112
  }
94
- const edge1 = edges[cornerIndex];
95
- const edge2 = edges[(cornerIndex + 1) % edges.length];
96
- const sharedVertex = oc.TopExp.LastVertex(edge1, false);
97
- const sharedPoint = oc.BRep_Tool.Pnt(sharedVertex);
98
- sharedVertex.delete();
99
- const pairWireBuilder = new oc.BRepBuilderAPI_MakeWire(edge1, edge2);
100
- const pairWire = pairWireBuilder.Wire();
101
- const filletAPI = new oc.ChFi2d_FilletAPI(pairWire, plane);
102
- const success = filletAPI.Perform(radius);
103
- if (!success) {
104
- filletAPI.delete();
105
- pairWire.delete();
106
- pairWireBuilder.delete();
107
- edges.forEach(e => e.delete());
108
- cornerIndex++;
109
- continue;
113
+ else {
114
+ const adaptor = new oc.BRepAdaptor_Curve(raw);
115
+ const edgeFirst = adaptor.FirstParameter();
116
+ const edgeLast = adaptor.LastParameter();
117
+ adaptor.delete();
118
+ const curveHandle = oc.BRep_Tool.Curve(raw, 0, 1);
119
+ if (!curveHandle || curveHandle.IsNull()) {
120
+ raw.delete();
121
+ explorer.delete();
122
+ ownedEdges.forEach(e => e.delete());
123
+ throw new Error("fillet2d: edge has no 3D curve");
124
+ }
125
+ const curve = curveHandle.get();
126
+ const reversedHandle = curve.Reversed();
127
+ const newFirst = curve.ReversedParameter(edgeLast);
128
+ const newLast = curve.ReversedParameter(edgeFirst);
129
+ const maker = new oc.BRepBuilderAPI_MakeEdge(reversedHandle, newFirst, newLast);
130
+ const newEdge = oc.TopoDS.Edge(maker.Edge());
131
+ maker.delete();
132
+ reversedHandle.delete();
133
+ curveHandle.delete();
134
+ raw.delete();
135
+ wireEdges.push(newEdge);
136
+ ownedEdges.push(newEdge);
110
137
  }
111
- const modEdge1 = new oc.TopoDS_Edge();
112
- const modEdge2 = new oc.TopoDS_Edge();
113
- const filletEdge = filletAPI.Result(sharedPoint, modEdge1, modEdge2, -1);
138
+ explorer.Next();
139
+ }
140
+ explorer.delete();
141
+ }
142
+ const currentEdges = wireEdges.slice();
143
+ const filletArcs = new Map();
144
+ const maxCorners = isClosed ? currentEdges.length : currentEdges.length - 1;
145
+ for (let cornerIndex = 0; cornerIndex < maxCorners; cornerIndex++) {
146
+ const nextIndex = (cornerIndex + 1) % currentEdges.length;
147
+ const edge1 = currentEdges[cornerIndex];
148
+ const edge2 = currentEdges[nextIndex];
149
+ const sharedVertex = oc.TopExp.LastVertex(edge1, true);
150
+ const sharedPoint = oc.BRep_Tool.Pnt(sharedVertex);
151
+ sharedVertex.delete();
152
+ const filletAPI = new oc.ChFi2d_FilletAPI(edge1, edge2, plane);
153
+ const success = filletAPI.Perform(radius);
154
+ if (!success || filletAPI.NbResults(sharedPoint) === 0) {
114
155
  sharedPoint.delete();
115
156
  filletAPI.delete();
116
- pairWire.delete();
117
- pairWireBuilder.delete();
118
- const newWireBuilder = new oc.BRepBuilderAPI_MakeWire();
119
- const nextIndex = (cornerIndex + 1) % edges.length;
120
- for (let i = 0; i < edges.length; i++) {
121
- if (i === cornerIndex) {
122
- newWireBuilder.Add(modEdge1);
123
- newWireBuilder.Add(filletEdge);
124
- }
125
- else if (i === nextIndex) {
126
- newWireBuilder.Add(modEdge2);
127
- }
128
- else {
129
- newWireBuilder.Add(edges[i]);
130
- }
131
- }
132
- const prevWire = currentWire;
133
- currentWire = newWireBuilder.Wire();
134
- newWireBuilder.delete();
135
- if (prevWire !== wire) {
136
- prevWire.delete();
137
- }
138
- modEdge1.delete();
139
- modEdge2.delete();
140
- filletEdge.delete();
141
- edges.forEach(e => e.delete());
142
- cornerIndex += 2;
157
+ continue;
143
158
  }
144
- return currentWire;
159
+ const modEdge1 = new oc.TopoDS_Edge();
160
+ const modEdge2 = new oc.TopoDS_Edge();
161
+ const filletEdge = filletAPI.Result(sharedPoint, modEdge1, modEdge2, -1);
162
+ sharedPoint.delete();
163
+ filletAPI.delete();
164
+ currentEdges[cornerIndex] = modEdge1;
165
+ currentEdges[nextIndex] = modEdge2;
166
+ filletArcs.set(cornerIndex, filletEdge);
167
+ ownedEdges.push(modEdge1, modEdge2, filletEdge);
145
168
  }
146
- catch (e) {
147
- if (currentWire !== wire) {
148
- currentWire.delete();
169
+ const edgeList = new oc.TopTools_ListOfShape();
170
+ for (let i = 0; i < currentEdges.length; i++) {
171
+ edgeList.Append(oc.TopoDS.Edge(currentEdges[i]));
172
+ const arc = filletArcs.get(i);
173
+ if (arc) {
174
+ edgeList.Append(oc.TopoDS.Edge(arc));
149
175
  }
150
- throw e;
151
176
  }
177
+ const wireBuilder = new oc.BRepBuilderAPI_MakeWire();
178
+ wireBuilder.Add(edgeList);
179
+ edgeList.delete();
180
+ if (!wireBuilder.IsDone()) {
181
+ wireBuilder.delete();
182
+ ownedEdges.forEach(e => e.delete());
183
+ throw new Error("fillet2d: failed to build filleted wire");
184
+ }
185
+ const result = wireBuilder.Wire();
186
+ wireBuilder.delete();
187
+ ownedEdges.forEach(e => e.delete());
188
+ return result;
152
189
  }
153
190
  }
@@ -27,9 +27,12 @@ export class ProjectionOps {
27
27
  // handle.delete();
28
28
  }
29
29
  if (wirePlane && wirePlane.isParallelTo(targetPlane)) {
30
- const distance = wirePlane.distanceToPlane(targetPlane);
31
- console.log('Wire is coplanar with plane, applying translation if necessary');
32
- const translation = targetPlane.normal.multiply(-distance);
30
+ // Translation along the target normal that moves a point from the wire
31
+ // plane onto the target plane. Use the *signed* distance — `distanceToPlane`
32
+ // is abs-valued and picks the wrong direction when the wire sits on the
33
+ // negative side of the target normal.
34
+ const signedDist = targetPlane.signedDistanceToPoint(wirePlane.origin);
35
+ const translation = targetPlane.normal.multiply(-signedDist);
33
36
  const matrix = Matrix4.fromTranslation(translation.x, translation.y, translation.z);
34
37
  const transformed = ShapeOps.transform(wire, matrix);
35
38
  return [transformed];
@@ -1,4 +1,4 @@
1
- import type { TopoDS_Face, TopoDS_Shape } from "occjs-wrapper";
1
+ import type { TopoDS_Edge, TopoDS_Face, TopoDS_Shape } from "occjs-wrapper";
2
2
  import { Face } from "../common/face.js";
3
3
  import { Shape } from "../common/shape.js";
4
4
  export interface MeshData {
@@ -7,11 +7,34 @@ export interface MeshData {
7
7
  indices: number[];
8
8
  count?: number;
9
9
  }
10
+ export interface EnsureTriangulatedOptions {
11
+ linDefl?: number;
12
+ angDefl?: number;
13
+ parallel?: boolean;
14
+ relative?: boolean;
15
+ checkFreeEdges?: boolean;
16
+ }
10
17
  export declare class Mesh {
11
18
  static triangulateFace(face: Face, vertexOffset?: number): MeshData | null;
12
19
  static discretizeEdge(edge: Shape): MeshData;
13
- static premeshShape(shape: TopoDS_Shape): void;
20
+ /**
21
+ * Triangulates `shape` only if it doesn't already have an up-to-date
22
+ * triangulation at the requested deflection. Returns true when a fresh
23
+ * mesh was built, false when the stored one was reused.
24
+ */
25
+ static ensureTriangulated(shape: TopoDS_Shape, opts?: EnsureTriangulatedOptions): boolean;
14
26
  static triangulateFaceRaw(face: TopoDS_Face, vertexOffset?: number): MeshData | null;
15
27
  static extractFaceTriangulationRaw(face: TopoDS_Face, vertexOffset?: number): MeshData | null;
28
+ /**
29
+ * Reads the polyline stored for `edge` as a polygon-on-triangulation of
30
+ * `face`. Node indices point into the face's triangulation, so the edge
31
+ * samples coincide exactly with the face mesh vertices (watertight).
32
+ */
33
+ static discretizeEdgeOnFace(edge: TopoDS_Edge, face: TopoDS_Face): MeshData | null;
34
+ /**
35
+ * Reads the stored 3D polygon for a free edge (one not attached to a
36
+ * meshed face). Caller must have already run `ensureTriangulated` on the
37
+ * edge or its parent wire.
38
+ */
16
39
  static discretizeEdgeRaw(edge: TopoDS_Shape): MeshData;
17
40
  }