fluidcad 0.0.36 → 0.0.37

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 (178) hide show
  1. package/LICENSE.txt +21 -504
  2. package/README.md +1 -1
  3. package/lib/dist/common/edge.d.ts +1 -1
  4. package/lib/dist/common/face.d.ts +1 -1
  5. package/lib/dist/common/scene-object.d.ts +6 -0
  6. package/lib/dist/common/scene-object.js +8 -0
  7. package/lib/dist/common/shape-factory.d.ts +1 -1
  8. package/lib/dist/common/shape-history-tracker.d.ts +1 -1
  9. package/lib/dist/common/shape.d.ts +1 -1
  10. package/lib/dist/common/solid.d.ts +1 -1
  11. package/lib/dist/common/transformable-primitive.d.ts +12 -1
  12. package/lib/dist/common/transformable-primitive.js +27 -0
  13. package/lib/dist/common/vertex.d.ts +1 -1
  14. package/lib/dist/common/wire.d.ts +1 -1
  15. package/lib/dist/core/2d/index.d.ts +1 -0
  16. package/lib/dist/core/2d/index.js +1 -0
  17. package/lib/dist/core/2d/text.d.ts +30 -0
  18. package/lib/dist/core/2d/text.js +37 -0
  19. package/lib/dist/core/helix.d.ts +20 -0
  20. package/lib/dist/core/helix.js +36 -0
  21. package/lib/dist/core/index.d.ts +3 -1
  22. package/lib/dist/core/index.js +2 -0
  23. package/lib/dist/core/interfaces.d.ts +180 -0
  24. package/lib/dist/core/wrap.d.ts +17 -0
  25. package/lib/dist/core/wrap.js +39 -0
  26. package/lib/dist/features/2d/text.d.ts +67 -0
  27. package/lib/dist/features/2d/text.js +320 -0
  28. package/lib/dist/features/cylinder.d.ts +3 -1
  29. package/lib/dist/features/cylinder.js +5 -2
  30. package/lib/dist/features/extrude-base.d.ts +1 -0
  31. package/lib/dist/features/extrude-to-face.d.ts +1 -0
  32. package/lib/dist/features/extrude-to-face.js +6 -0
  33. package/lib/dist/features/fillet.d.ts +1 -1
  34. package/lib/dist/features/helix.d.ts +41 -0
  35. package/lib/dist/features/helix.js +337 -0
  36. package/lib/dist/features/select.js +32 -8
  37. package/lib/dist/features/simple-extruder.d.ts +1 -1
  38. package/lib/dist/features/simple-extruder.js +7 -2
  39. package/lib/dist/features/sphere.d.ts +3 -1
  40. package/lib/dist/features/sphere.js +5 -2
  41. package/lib/dist/features/sweep.js +7 -2
  42. package/lib/dist/features/wrap.d.ts +39 -0
  43. package/lib/dist/features/wrap.js +116 -0
  44. package/lib/dist/filters/edge/belongs-to-face.d.ts +3 -1
  45. package/lib/dist/filters/edge/belongs-to-face.js +14 -10
  46. package/lib/dist/filters/filter.d.ts +1 -1
  47. package/lib/dist/filters/from-object.d.ts +1 -1
  48. package/lib/dist/filters/tangent-expander.d.ts +1 -1
  49. package/lib/dist/filters/tangent-expander.js +57 -40
  50. package/lib/dist/helpers/scene-helpers.d.ts +2 -0
  51. package/lib/dist/helpers/scene-helpers.js +1 -1
  52. package/lib/dist/index.d.ts +2 -0
  53. package/lib/dist/index.js +3 -1
  54. package/lib/dist/io/file-import.d.ts +7 -0
  55. package/lib/dist/io/file-import.js +28 -1
  56. package/lib/dist/io/font-registry.d.ts +45 -0
  57. package/lib/dist/io/font-registry.js +272 -0
  58. package/lib/dist/math/bspline-interpolation.d.ts +29 -0
  59. package/lib/dist/math/bspline-interpolation.js +194 -0
  60. package/lib/dist/oc/boolean-ops.d.ts +3 -1
  61. package/lib/dist/oc/boolean-ops.js +15 -1
  62. package/lib/dist/oc/color-transfer.d.ts +1 -1
  63. package/lib/dist/oc/constraints/constraint-helpers.d.ts +4 -4
  64. package/lib/dist/oc/constraints/curve/tangent-circle-solver.js +10 -9
  65. package/lib/dist/oc/constraints/curve/tangent-line-solver.js +5 -6
  66. package/lib/dist/oc/convert.d.ts +1 -1
  67. package/lib/dist/oc/draft-ops.d.ts +1 -1
  68. package/lib/dist/oc/edge-ops.d.ts +2 -2
  69. package/lib/dist/oc/edge-ops.js +13 -14
  70. package/lib/dist/oc/edge-props.d.ts +1 -1
  71. package/lib/dist/oc/edge-query.d.ts +1 -1
  72. package/lib/dist/oc/edge-query.js +3 -8
  73. package/lib/dist/oc/errors.d.ts +8 -0
  74. package/lib/dist/oc/errors.js +27 -0
  75. package/lib/dist/oc/explorer.d.ts +2 -2
  76. package/lib/dist/oc/extrude-ops.d.ts +28 -2
  77. package/lib/dist/oc/extrude-ops.js +56 -7
  78. package/lib/dist/oc/face-ops.d.ts +2 -1
  79. package/lib/dist/oc/face-ops.js +11 -0
  80. package/lib/dist/oc/face-props.d.ts +1 -1
  81. package/lib/dist/oc/face-query.d.ts +12 -1
  82. package/lib/dist/oc/face-query.js +39 -0
  83. package/lib/dist/oc/fillet-ops.d.ts +1 -1
  84. package/lib/dist/oc/fillet-ops.js +4 -4
  85. package/lib/dist/oc/geometry.d.ts +1 -1
  86. package/lib/dist/oc/geometry.js +12 -14
  87. package/lib/dist/oc/helix-ops.d.ts +37 -0
  88. package/lib/dist/oc/helix-ops.js +88 -0
  89. package/lib/dist/oc/hit-test.d.ts +1 -1
  90. package/lib/dist/oc/index.d.ts +4 -0
  91. package/lib/dist/oc/index.js +2 -0
  92. package/lib/dist/oc/init.d.ts +1 -1
  93. package/lib/dist/oc/init.js +1 -1
  94. package/lib/dist/oc/intersection.js +1 -1
  95. package/lib/dist/oc/io.d.ts +6 -6
  96. package/lib/dist/oc/io.js +31 -24
  97. package/lib/dist/oc/measure/classify.d.ts +34 -0
  98. package/lib/dist/oc/measure/classify.js +246 -0
  99. package/lib/dist/oc/measure/measure-ops.d.ts +9 -0
  100. package/lib/dist/oc/measure/measure-ops.js +210 -0
  101. package/lib/dist/oc/measure/measure-types.d.ts +39 -0
  102. package/lib/dist/oc/measure/measure-types.js +1 -0
  103. package/lib/dist/oc/measure/sampling.d.ts +9 -0
  104. package/lib/dist/oc/measure/sampling.js +77 -0
  105. package/lib/dist/oc/measure/vec.d.ts +13 -0
  106. package/lib/dist/oc/measure/vec.js +23 -0
  107. package/lib/dist/oc/mesh.d.ts +1 -1
  108. package/lib/dist/oc/mesh.js +40 -28
  109. package/lib/dist/oc/path-sampler.d.ts +29 -0
  110. package/lib/dist/oc/path-sampler.js +63 -0
  111. package/lib/dist/oc/props.d.ts +1 -1
  112. package/lib/dist/oc/props.js +4 -1
  113. package/lib/dist/oc/shape-hash.d.ts +26 -0
  114. package/lib/dist/oc/shape-hash.js +32 -0
  115. package/lib/dist/oc/shape-ops.d.ts +5 -3
  116. package/lib/dist/oc/shape-ops.js +6 -5
  117. package/lib/dist/oc/sweep-ops.d.ts +22 -1
  118. package/lib/dist/oc/sweep-ops.js +206 -18
  119. package/lib/dist/oc/text-outline.d.ts +62 -0
  120. package/lib/dist/oc/text-outline.js +212 -0
  121. package/lib/dist/oc/topology-index.d.ts +1 -1
  122. package/lib/dist/oc/vertex-ops.d.ts +1 -1
  123. package/lib/dist/oc/wire-ops.d.ts +1 -1
  124. package/lib/dist/oc/wire-ops.js +1 -1
  125. package/lib/dist/oc/wrap-development.d.ts +105 -0
  126. package/lib/dist/oc/wrap-development.js +179 -0
  127. package/lib/dist/oc/wrap-ops.d.ts +100 -0
  128. package/lib/dist/oc/wrap-ops.js +406 -0
  129. package/lib/dist/rendering/render-solid.js +10 -2
  130. package/lib/dist/scene-manager.d.ts +2 -0
  131. package/lib/dist/scene-manager.js +29 -0
  132. package/lib/dist/tests/features/cylinder-curve-filter.test.js +3 -3
  133. package/lib/dist/tests/features/extrude-to-face.test.js +38 -1
  134. package/lib/dist/tests/features/helix.test.d.ts +1 -0
  135. package/lib/dist/tests/features/helix.test.js +295 -0
  136. package/lib/dist/tests/features/repeat-primitive.test.d.ts +1 -0
  137. package/lib/dist/tests/features/repeat-primitive.test.js +60 -0
  138. package/lib/dist/tests/features/rib.test.js +6 -1
  139. package/lib/dist/tests/features/sweep.test.js +125 -1
  140. package/lib/dist/tests/features/text.test.d.ts +1 -0
  141. package/lib/dist/tests/features/text.test.js +347 -0
  142. package/lib/dist/tests/features/wrap-development.test.d.ts +1 -0
  143. package/lib/dist/tests/features/wrap-development.test.js +130 -0
  144. package/lib/dist/tests/features/wrap-extruded-target.test.d.ts +1 -0
  145. package/lib/dist/tests/features/wrap-extruded-target.test.js +106 -0
  146. package/lib/dist/tests/features/wrap-repeat.test.d.ts +1 -0
  147. package/lib/dist/tests/features/wrap-repeat.test.js +93 -0
  148. package/lib/dist/tests/features/wrap.test.d.ts +1 -0
  149. package/lib/dist/tests/features/wrap.test.js +331 -0
  150. package/lib/dist/tests/math/bspline-interpolation.test.d.ts +1 -0
  151. package/lib/dist/tests/math/bspline-interpolation.test.js +119 -0
  152. package/lib/dist/tests/measure.test.d.ts +1 -0
  153. package/lib/dist/tests/measure.test.js +288 -0
  154. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  155. package/llm-docs/api/helix.md +64 -0
  156. package/llm-docs/api/index.json +11 -2
  157. package/llm-docs/api/text.md +52 -0
  158. package/llm-docs/api/types/helix.md +105 -0
  159. package/llm-docs/api/types/text.md +138 -0
  160. package/llm-docs/api/types/wrap.md +131 -0
  161. package/llm-docs/api/wrap.md +62 -0
  162. package/llm-docs/index.json +121 -1
  163. package/mcp/dist/server.js +20 -1
  164. package/mcp/dist/tools/inspection.d.ts +17 -0
  165. package/mcp/dist/tools/inspection.js +14 -0
  166. package/package.json +7 -3
  167. package/server/dist/fluidcad-server.d.ts +10 -0
  168. package/server/dist/fluidcad-server.js +10 -0
  169. package/server/dist/index.js +4 -2
  170. package/server/dist/preferences.d.ts +4 -0
  171. package/server/dist/preferences.js +2 -0
  172. package/server/dist/routes/measure.d.ts +3 -0
  173. package/server/dist/routes/measure.js +32 -0
  174. package/server/dist/routes/preferences.js +6 -0
  175. package/server/dist/routes/sketch-edits.js +2 -1
  176. package/ui/dist/assets/{index-CDJmUpFI.css → index-dAFdg2Un.css} +1 -1
  177. package/ui/dist/assets/{index-MRqwG9Vh.js → index-no7mtr5s.js} +149 -102
  178. package/ui/dist/index.html +2 -2
@@ -0,0 +1,32 @@
1
+ import { getOC } from "./init.js";
2
+ /**
3
+ * Produces stable, `IsSame`-consistent integer keys for TopoDS shapes so they can
4
+ * be used as JS `Map`/`Set` keys.
5
+ *
6
+ * OCCT 8.0 removed `TopoDS_Shape::HashCode`. This interns shapes in a
7
+ * `TopTools_IndexedMapOfShape` — whose hasher compares keys with `IsSame` — and
8
+ * returns each shape's stable 1-based index. That gives the same bucketing the old
9
+ * `HashCode` + `IsSame` check provided, but collision-free (the same `IsSame`
10
+ * sub-shape always maps to the same key, distinct sub-shapes never collide).
11
+ *
12
+ * Wraps an OCCT map, so it owns native memory: scope one per operation and
13
+ * `delete()` it when done. Follows the same indexed-map convention as
14
+ * {@link TopologyIndex}.
15
+ */
16
+ export class ShapeHasher {
17
+ map;
18
+ constructor() {
19
+ this.map = new (getOC().TopTools_IndexedMapOfShape)();
20
+ }
21
+ /**
22
+ * Stable, `IsSame`-consistent key for `shape`. Interns the shape on first use; an
23
+ * as-yet-unseen shape gets a fresh key, so a `Map` lookup against keys from other
24
+ * shapes correctly misses.
25
+ */
26
+ key(shape) {
27
+ return this.map.Add(shape);
28
+ }
29
+ delete() {
30
+ this.map.delete();
31
+ }
32
+ }
@@ -1,4 +1,4 @@
1
- import type { TopTools_ListOfShape, TopoDS_Shape } from "occjs-wrapper";
1
+ import type { TopTools_ListOfShape, TopoDS_Shape } from "ocjs-fluidcad";
2
2
  import { Matrix4 } from "../math/matrix4.js";
3
3
  import { Shape } from "../common/shape.js";
4
4
  import { Face } from "../common/face.js";
@@ -26,14 +26,16 @@ export declare class ShapeOps {
26
26
  static getBoundingBox(shape: Shape | TopoDS_Shape): BoundingBox;
27
27
  static getBoundingBoxRaw(shape: TopoDS_Shape): BoundingBox;
28
28
  static makeCompound(shapes: Shape[]): Shape;
29
- static makeCompoundRaw(shapes: TopoDS_Shape[]): import("occjs-wrapper").TopoDS_Compound;
29
+ static makeCompoundRaw(shapes: TopoDS_Shape[]): import("ocjs-fluidcad").TopoDS_Compound;
30
30
  static cleanShape(shape: Shape): Shape;
31
31
  /**
32
32
  * Variant of `cleanShape` that preserves UnifySameDomain lineage via
33
33
  * `BRepTools_History`. Caller must call `dispose()` exactly once to free
34
34
  * the OC wrappers.
35
35
  */
36
- static cleanShapeWithLineage(shape: Shape): CleanShapeLineage;
36
+ static cleanShapeWithLineage(shape: Shape, opts?: {
37
+ skipSimplify?: boolean;
38
+ }): CleanShapeLineage;
37
39
  static cleanShapeRaw(shape: TopoDS_Shape): TopoDS_Shape;
38
40
  static shapeListToArray(list: TopTools_ListOfShape): TopoDS_Shape[];
39
41
  }
@@ -78,11 +78,13 @@ export class ShapeOps {
78
78
  * `BRepTools_History`. Caller must call `dispose()` exactly once to free
79
79
  * the OC wrappers.
80
80
  */
81
- static cleanShapeWithLineage(shape) {
81
+ static cleanShapeWithLineage(shape, opts) {
82
82
  const oc = getOC();
83
83
  const FACE = oc.TopAbs_ShapeEnum.TopAbs_FACE;
84
84
  const EDGE = oc.TopAbs_ShapeEnum.TopAbs_EDGE;
85
- const unify = new oc.ShapeUpgrade_UnifySameDomain(shape.getShape(), false, true, false);
85
+ // skipSimplify: pass unifyFaces=false to avoid the slow face-merging step
86
+ // that hangs on tangent contact along curves (e.g., helix sweep + cylinder).
87
+ const unify = new oc.ShapeUpgrade_UnifySameDomain(shape.getShape(), false, opts?.skipSimplify ? false : true, false);
86
88
  unify.Build();
87
89
  const cleanedRaw = unify.Shape();
88
90
  // Pre-compute which faces/edges this cleanup saw so the remap can
@@ -127,15 +129,14 @@ export class ShapeOps {
127
129
  dispose,
128
130
  };
129
131
  }
130
- const historyHandle = unify.History();
131
- const history = historyHandle.get();
132
+ const history = unify.History();
132
133
  let disposed = false;
133
134
  const dispose = () => {
134
135
  if (disposed) {
135
136
  return;
136
137
  }
137
138
  disposed = true;
138
- historyHandle.delete();
139
+ history.delete();
139
140
  unify.delete();
140
141
  knownFaces.delete();
141
142
  knownEdges.delete();
@@ -1,4 +1,4 @@
1
- import type { TopoDS_Shape } from "occjs-wrapper";
1
+ import type { TopoDS_Shape } from "ocjs-fluidcad";
2
2
  import { Solid } from "../common/solid.js";
3
3
  import { Wire } from "../common/wire.js";
4
4
  import { Face } from "../common/face.js";
@@ -8,5 +8,26 @@ export interface SweepResult {
8
8
  lastShape: TopoDS_Shape;
9
9
  }
10
10
  export declare class SweepOps {
11
+ private static readonly MAX_PIPE_SEGMENTS;
11
12
  static makeSweep(spineWire: Wire, profileFaces: Face[]): SweepResult;
13
+ /** Sweep a single wire along the spine with a fixed binormal. */
14
+ private static sweepWire;
15
+ /** Unit tangent of the spine wire at its first parameter. */
16
+ private static getSpineTangent;
17
+ /** World-space centroid of a planar face (uses surface-area properties). */
18
+ private static getFaceCentroid;
19
+ /**
20
+ * Build a world-to-world trsf that lays a planar face flat in world XY
21
+ * with its centroid at the world origin.
22
+ *
23
+ * `gp_Trsf::SetTransformation(A, B)` builds the transformation that maps
24
+ * frame A onto frame B (i.e., A's origin → B's origin, A's axes → B's
25
+ * axes). To move the profile *from* its current frame *to* the canonical
26
+ * frame, we pass the canonical frame as A and the profile's frame as B —
27
+ * counterintuitive but matches OCC's convention as verified empirically.
28
+ *
29
+ * The canonical frame is what OCC's MakePipeShell expects when
30
+ * WithContact/WithCorrection are both false: `Saxe = gp_Ax3(0, +Z, +X)`.
31
+ */
32
+ private static profileToCanonicalFrameTrsf;
12
33
  }
@@ -1,32 +1,127 @@
1
1
  import { getOC } from "./init.js";
2
+ import { Convert } from "./convert.js";
2
3
  import { Explorer } from "./explorer.js";
4
+ import { ShapeOps } from "./shape-ops.js";
3
5
  import { Solid } from "../common/solid.js";
6
+ import { Vector3d } from "../math/vector3d.js";
4
7
  export class SweepOps {
8
+ // Ceiling for MakePipeShell's swept-surface approximation. OCCT's default
9
+ // (~30) is too small for tapered or tightly-coiled helical spines, whose
10
+ // swept surfaces need many spans to fit within tolerance — at the default the
11
+ // build silently fails (BRepBuilderAPI_PipeNotDone). This only caps the
12
+ // adaptive fit; simple spines converge far below it at no extra cost.
13
+ static MAX_PIPE_SEGMENTS = 1000;
5
14
  static makeSweep(spineWire, profileFaces) {
6
15
  const oc = getOC();
7
16
  const allSolids = [];
8
17
  let firstShape = null;
9
18
  let lastShape = null;
10
- for (const face of profileFaces) {
11
- const pipe = new oc.BRepOffsetAPI_MakePipe(spineWire.getShape(), face.getShape());
12
- const progress = new oc.Message_ProgressRange();
13
- pipe.Build(progress);
14
- progress.delete();
15
- if (!pipe.IsDone()) {
16
- pipe.delete();
17
- throw new Error("Sweep operation failed.");
18
- }
19
- if (!firstShape) {
20
- firstShape = pipe.FirstShape();
21
- lastShape = pipe.LastShape();
22
- }
23
- const result = pipe.Shape();
24
- pipe.delete();
25
- const solids = Explorer.findShapes(result, Explorer.getOcShapeType("solid"));
26
- for (const s of solids) {
27
- allSolids.push(Solid.fromTopoDSSolid(Explorer.toSolid(s)));
19
+ const profilePlane = profileFaces[0].getPlane();
20
+ // Fixed binormal: profile plane's "up" direction (perpendicular to xDir
21
+ // and to the face normal). Locking it via SetMode stops the swept profile
22
+ // from twisting along helical spines (clean spring instead of a wobbling
23
+ // ribbon). The binormal stays in WORLD coords — it's the user's intended
24
+ // "up" direction even when we pre-canonicalize the profile below.
25
+ const binormalVec = profilePlane.yDirection;
26
+ const [binormalDir, disposeBinormal] = Convert.toGpDir(binormalVec);
27
+ // Decide whether to pre-canonicalize the profile.
28
+ //
29
+ // OCC's `MakePipeShell.Add(_, false, true)` (no contact, with correction)
30
+ // works for most profile orientations: it rotates the profile to align
31
+ // its plane normal with the spine tangent, around an axis given by
32
+ // `profile.normal × spine.tangent`. That axis is well-defined unless
33
+ // the two are *anti-parallel* — in which case the cross product is
34
+ // zero, the rotation axis is undefined, and OCC produces a degenerate
35
+ // sweep.
36
+ //
37
+ // For the well-behaved case we keep the user's drawn position (so the
38
+ // swept solid lands where the user expects). For the antiparallel case
39
+ // we manually canonicalize to (origin, XY plane), then call
40
+ // `Add(_, false, false)`. The canonicalization is necessary because
41
+ // `GeomFill_SectionPlacement::Transformation` builds its trsf as
42
+ // `Tf.SetTransformation(Saxe = gp_Ax3(0, +Z, +X), Paxe = trihedron)` —
43
+ // it implicitly assumes the section is at the world origin in XY and
44
+ // produces offset placements otherwise.
45
+ const spineTangent = SweepOps.getSpineTangent(spineWire.getShape());
46
+ const isAntiParallel = profilePlane.normal.dot(spineTangent) < -0.999;
47
+ let trsf = null;
48
+ let withCorrection = true;
49
+ if (isAntiParallel) {
50
+ const profileCentroid = SweepOps.getFaceCentroid(profileFaces[0].getShape());
51
+ trsf = SweepOps.profileToCanonicalFrameTrsf(profileCentroid, profilePlane.normal, profilePlane.xDirection);
52
+ withCorrection = false;
53
+ }
54
+ try {
55
+ for (const face of profileFaces) {
56
+ let workingFace;
57
+ let transformer = null;
58
+ if (trsf) {
59
+ transformer = new oc.BRepBuilderAPI_Transform(trsf);
60
+ transformer.Perform(face.getShape(), true);
61
+ workingFace = transformer.Shape();
62
+ }
63
+ else {
64
+ workingFace = face.getShape();
65
+ }
66
+ const ocFace = oc.TopoDS.Face(workingFace);
67
+ const outerWire = oc.BRepTools.OuterWire(ocFace);
68
+ const innerWires = trsf
69
+ ? Explorer.findShapes(workingFace, Explorer.getOcShapeType("wire"))
70
+ .filter(w => !w.IsSame(outerWire))
71
+ : face.getWires().map(w => w.getShape()).filter(w => !w.IsSame(outerWire));
72
+ const outer = SweepOps.sweepWire(spineWire.getShape(), outerWire, binormalDir, withCorrection);
73
+ let resultSolid = outer.solid;
74
+ let resultFirst = outer.firstFace;
75
+ let resultLast = outer.lastFace;
76
+ for (const innerWire of innerWires) {
77
+ const inner = SweepOps.sweepWire(spineWire.getShape(), oc.TopoDS.Wire(innerWire), binormalDir, withCorrection);
78
+ const stockList = new oc.TopTools_ListOfShape();
79
+ stockList.Append(resultSolid);
80
+ const toolList = new oc.TopTools_ListOfShape();
81
+ toolList.Append(inner.solid);
82
+ const cut = new oc.BRepAlgoAPI_Cut();
83
+ cut.SetArguments(stockList);
84
+ cut.SetTools(toolList);
85
+ const progress = new oc.Message_ProgressRange();
86
+ cut.Build(progress);
87
+ progress.delete();
88
+ if (!cut.IsDone()) {
89
+ cut.delete();
90
+ stockList.delete();
91
+ toolList.delete();
92
+ throw new Error("Sweep hole cut failed.");
93
+ }
94
+ const newSolid = cut.Shape();
95
+ // Track first/last faces through the cut. The outer's start/end
96
+ // face becomes a hole-bearing face after cutting through it.
97
+ const modFirst = ShapeOps.shapeListToArray(cut.Modified(resultFirst));
98
+ const modLast = ShapeOps.shapeListToArray(cut.Modified(resultLast));
99
+ if (modFirst.length > 0) {
100
+ resultFirst = modFirst[0];
101
+ }
102
+ if (modLast.length > 0) {
103
+ resultLast = modLast[0];
104
+ }
105
+ cut.delete();
106
+ stockList.delete();
107
+ toolList.delete();
108
+ resultSolid = newSolid;
109
+ }
110
+ if (!firstShape) {
111
+ firstShape = resultFirst;
112
+ lastShape = resultLast;
113
+ }
114
+ const solids = Explorer.findShapes(resultSolid, Explorer.getOcShapeType("solid"));
115
+ for (const s of solids) {
116
+ allSolids.push(Solid.fromTopoDSSolid(Explorer.toSolid(s)));
117
+ }
118
+ transformer?.delete();
28
119
  }
29
120
  }
121
+ finally {
122
+ trsf?.delete();
123
+ disposeBinormal();
124
+ }
30
125
  if (allSolids.length === 0) {
31
126
  throw new Error("Sweep produced no solids.");
32
127
  }
@@ -36,4 +131,97 @@ export class SweepOps {
36
131
  lastShape: lastShape,
37
132
  };
38
133
  }
134
+ /** Sweep a single wire along the spine with a fixed binormal. */
135
+ static sweepWire(spine, profile, binormalDir, withCorrection) {
136
+ const oc = getOC();
137
+ const pipe = new oc.BRepOffsetAPI_MakePipeShell(spine);
138
+ // Fixed binormal (the profile plane's "up"): keeps the swept profile from
139
+ // twisting along the spine — a clean spring rather than a wobbling ribbon —
140
+ // and is well-defined on straight spines, where Frenet is not (zero
141
+ // curvature ⇒ undefined normal).
142
+ pipe.SetMode(binormalDir);
143
+ // Give the swept-surface approximation enough spans for tapered/tight
144
+ // helical spines (see MAX_PIPE_SEGMENTS) — at OCCT's default budget the
145
+ // build fails on, e.g., a conical helix or a many-turn helix on a cone face.
146
+ pipe.SetMaxSegments(SweepOps.MAX_PIPE_SEGMENTS);
147
+ pipe.Add(profile, false, withCorrection);
148
+ const progress = new oc.Message_ProgressRange();
149
+ pipe.Build(progress);
150
+ progress.delete();
151
+ if (!pipe.IsDone()) {
152
+ pipe.delete();
153
+ throw new Error("Sweep operation failed.");
154
+ }
155
+ if (!pipe.MakeSolid()) {
156
+ pipe.delete();
157
+ throw new Error("Sweep failed to produce a solid.");
158
+ }
159
+ const firstFace = pipe.FirstShape();
160
+ const lastFace = pipe.LastShape();
161
+ const solid = pipe.Shape();
162
+ pipe.delete();
163
+ return { solid, firstFace, lastFace };
164
+ }
165
+ /** Unit tangent of the spine wire at its first parameter. */
166
+ static getSpineTangent(spine) {
167
+ const oc = getOC();
168
+ const adaptor = new oc.BRepAdaptor_CompCurve(spine, false);
169
+ const u0 = adaptor.FirstParameter();
170
+ const pnt = new oc.gp_Pnt();
171
+ const tan = new oc.gp_Vec();
172
+ adaptor.D1(u0, pnt, tan);
173
+ tan.Normalize();
174
+ const tangent = new Vector3d(tan.X(), tan.Y(), tan.Z());
175
+ pnt.delete();
176
+ tan.delete();
177
+ adaptor.delete();
178
+ return tangent;
179
+ }
180
+ /** World-space centroid of a planar face (uses surface-area properties). */
181
+ static getFaceCentroid(face) {
182
+ const oc = getOC();
183
+ const ocFace = oc.TopoDS.Face(face);
184
+ const props = new oc.GProp_GProps();
185
+ oc.BRepGProp.SurfaceProperties(ocFace, props, false, false);
186
+ const c = props.CentreOfMass();
187
+ const out = new Vector3d(c.X(), c.Y(), c.Z());
188
+ c.delete();
189
+ props.delete();
190
+ return out;
191
+ }
192
+ /**
193
+ * Build a world-to-world trsf that lays a planar face flat in world XY
194
+ * with its centroid at the world origin.
195
+ *
196
+ * `gp_Trsf::SetTransformation(A, B)` builds the transformation that maps
197
+ * frame A onto frame B (i.e., A's origin → B's origin, A's axes → B's
198
+ * axes). To move the profile *from* its current frame *to* the canonical
199
+ * frame, we pass the canonical frame as A and the profile's frame as B —
200
+ * counterintuitive but matches OCC's convention as verified empirically.
201
+ *
202
+ * The canonical frame is what OCC's MakePipeShell expects when
203
+ * WithContact/WithCorrection are both false: `Saxe = gp_Ax3(0, +Z, +X)`.
204
+ */
205
+ static profileToCanonicalFrameTrsf(centroid, normal, xDir) {
206
+ const oc = getOC();
207
+ const [originPnt, disposeOriginPnt] = Convert.toGpPnt(centroid);
208
+ const [normalDirGp, disposeNormalDir] = Convert.toGpDir(normal);
209
+ const [xDirGp, disposeXDirGp] = Convert.toGpDir(xDir);
210
+ const profileAx3 = new oc.gp_Ax3(originPnt, normalDirGp, xDirGp);
211
+ const zeroPnt = new oc.gp_Pnt(0, 0, 0);
212
+ const zDir = new oc.gp_Dir(0, 0, 1);
213
+ const xDirWorld = new oc.gp_Dir(1, 0, 0);
214
+ const canonicalAx3 = new oc.gp_Ax3(zeroPnt, zDir, xDirWorld);
215
+ const trsf = new oc.gp_Trsf();
216
+ trsf.SetTransformation(canonicalAx3, profileAx3);
217
+ profileAx3.delete();
218
+ canonicalAx3.delete();
219
+ zeroPnt.delete();
220
+ zDir.delete();
221
+ xDirWorld.delete();
222
+ disposeOriginPnt();
223
+ disposeNormalDir();
224
+ disposeXDirGp();
225
+ return trsf;
226
+ }
39
227
  }
@@ -0,0 +1,62 @@
1
+ import type { Font } from "fontkit";
2
+ import { Edge } from "../common/edge.js";
3
+ import { Plane } from "../math/plane.js";
4
+ import { Point, Point2D } from "../math/point.js";
5
+ import { Vector3d } from "../math/vector3d.js";
6
+ export type TextAlign = "left" | "center" | "right" | "start" | "end" | "space-between" | "space-around";
7
+ export interface TextLayoutOptions {
8
+ /** Em size in model units (mm). */
9
+ size: number;
10
+ align: TextAlign;
11
+ /** Multiplier applied to the font's natural line height (default 1). */
12
+ lineSpacing: number;
13
+ /** Extra advance added between glyphs, in model units (default 0). */
14
+ letterSpacing: number;
15
+ }
16
+ export interface TextPathOptions {
17
+ /**
18
+ * Evaluates a point + unit tangent at an arc-length distance along the
19
+ * path (expected to wrap on closed paths and extrapolate on open ones).
20
+ */
21
+ evalAt(s: number): {
22
+ point: Point;
23
+ tangent: Vector3d;
24
+ };
25
+ /** Total path length in model units; used for alignment. */
26
+ length: number;
27
+ /** Unit normal of the path's plane. Glyph "up" is `normal × tangent`. */
28
+ normal: Vector3d;
29
+ /** Perpendicular baseline shift in model units (toward glyph "up"). */
30
+ offset: number;
31
+ /** Extra arc-length shift of the text start along the path. */
32
+ startAt: number;
33
+ /** Mirror the text to the other side of the path, reversing direction. */
34
+ flip: boolean;
35
+ /** Whether the path is a closed loop (affects `space-between` gap distribution). */
36
+ closed: boolean;
37
+ }
38
+ /**
39
+ * Turns a text string into outline edges laid out on `plane`, suitable for
40
+ * FaceMaker2 (which resolves letter counters like the holes in `o`/`A`/`e` as
41
+ * faces-with-holes). Glyph outlines come from fontkit as M/L/Q/C/Z commands in
42
+ * font units; quadratic/cubic curves map to OCCT Bézier edges, lines to
43
+ * segment edges.
44
+ */
45
+ export declare class TextOutline {
46
+ static buildEdges(font: Font, text: string, opts: TextLayoutOptions, plane: Plane, origin: Point2D): Edge[];
47
+ /**
48
+ * Lays out `text` along a curve: each glyph is placed rigidly (not bent) at
49
+ * the arc-length position of its advance midpoint, rotated to the local
50
+ * tangent. Spacing is measured as arc length, so kerning/letterSpacing
51
+ * carry over from straight text. Multi-line strings stack below the
52
+ * baseline via perpendicular offsets.
53
+ */
54
+ static buildEdgesAlongPath(font: Font, text: string, opts: TextLayoutOptions, path: TextPathOptions): Edge[];
55
+ private static buildLineAlongPath;
56
+ /** Maps the path-friendly alignment synonyms onto the base values. */
57
+ private static resolveAlign;
58
+ /** Total advance of a laid-out line (for alignment), excluding trailing letter spacing. */
59
+ private static totalAdvance;
60
+ private static buildLine;
61
+ private static buildGlyph;
62
+ }
@@ -0,0 +1,212 @@
1
+ import { Geometry } from "./geometry.js";
2
+ import { Point2D } from "../math/point.js";
3
+ /**
4
+ * Minimum edge length (model units) below which a segment/curve is treated as
5
+ * degenerate and skipped. Safely above OCCT's Precision::Confusion (1e-7) and
6
+ * far below any real glyph feature at typical sizes.
7
+ */
8
+ const EPS = 1e-6;
9
+ /**
10
+ * Turns a text string into outline edges laid out on `plane`, suitable for
11
+ * FaceMaker2 (which resolves letter counters like the holes in `o`/`A`/`e` as
12
+ * faces-with-holes). Glyph outlines come from fontkit as M/L/Q/C/Z commands in
13
+ * font units; quadratic/cubic curves map to OCCT Bézier edges, lines to
14
+ * segment edges.
15
+ */
16
+ export class TextOutline {
17
+ static buildEdges(font, text, opts, plane, origin) {
18
+ const scale = opts.size / font.unitsPerEm;
19
+ const lineHeight = (font.ascent - font.descent + font.lineGap) * scale * opts.lineSpacing;
20
+ const lines = text.split(/\r?\n/);
21
+ const edges = [];
22
+ for (let li = 0; li < lines.length; li++) {
23
+ const lineY = origin.y - li * lineHeight;
24
+ this.buildLine(font, lines[li], opts, scale, plane, new Point2D(origin.x, lineY), edges);
25
+ }
26
+ return edges;
27
+ }
28
+ /**
29
+ * Lays out `text` along a curve: each glyph is placed rigidly (not bent) at
30
+ * the arc-length position of its advance midpoint, rotated to the local
31
+ * tangent. Spacing is measured as arc length, so kerning/letterSpacing
32
+ * carry over from straight text. Multi-line strings stack below the
33
+ * baseline via perpendicular offsets.
34
+ */
35
+ static buildEdgesAlongPath(font, text, opts, path) {
36
+ const scale = opts.size / font.unitsPerEm;
37
+ const lineHeight = (font.ascent - font.descent + font.lineGap) * scale * opts.lineSpacing;
38
+ const lines = text.split(/\r?\n/);
39
+ const edges = [];
40
+ for (let li = 0; li < lines.length; li++) {
41
+ this.buildLineAlongPath(font, lines[li], opts, scale, path, -li * lineHeight, edges);
42
+ }
43
+ return edges;
44
+ }
45
+ static buildLineAlongPath(font, line, opts, scale, path, lineOffset, out) {
46
+ if (line.length === 0) {
47
+ return;
48
+ }
49
+ const run = font.layout(line);
50
+ const total = this.totalAdvance(run.positions, opts, scale);
51
+ const align = this.resolveAlign(opts.align);
52
+ let s0 = path.startAt;
53
+ let justifyGap = 0;
54
+ if (align === "center") {
55
+ s0 += (path.length - total) / 2;
56
+ }
57
+ else if (align === "right") {
58
+ s0 += path.length - total;
59
+ }
60
+ else if (align === "space-between" && run.glyphs.length > 1) {
61
+ // Justify across the whole path: distribute the leftover arc length
62
+ // evenly between glyphs. On a closed loop the wrap-around gap counts
63
+ // too, so the glyphs end up evenly spaced around the loop.
64
+ const gaps = path.closed ? run.glyphs.length : run.glyphs.length - 1;
65
+ justifyGap = (path.length - total) / gaps;
66
+ }
67
+ else if (align === "space-around" && run.glyphs.length > 0) {
68
+ // Every glyph gets an equal share of the leftover arc length, half on
69
+ // each side — so the run starts and ends half a gap from the path's
70
+ // ends (on a closed loop this just phase-shifts the even spacing).
71
+ justifyGap = (path.length - total) / run.glyphs.length;
72
+ s0 += justifyGap / 2;
73
+ }
74
+ let pen = 0;
75
+ for (let i = 0; i < run.glyphs.length; i++) {
76
+ const pos = run.positions[i];
77
+ const adv = pos.xAdvance * scale;
78
+ // Anchor each glyph at the midpoint of its advance so it straddles the
79
+ // curve symmetrically (less lift-off on tight curvature).
80
+ const sMid = s0 + pen + adv / 2;
81
+ const frame = path.evalAt(path.flip ? path.length - sMid : sMid);
82
+ const tangent = path.flip ? frame.tangent.multiply(-1) : frame.tangent;
83
+ const up = path.normal.cross(tangent).normalize();
84
+ const anchor = frame.point;
85
+ const toWorld = (p) => {
86
+ const dx = ((pos.xOffset || 0) + p.x) * scale - adv / 2;
87
+ const dy = ((pos.yOffset || 0) + p.y) * scale + path.offset + lineOffset;
88
+ return anchor.add(tangent.multiply(dx)).add(up.multiply(dy));
89
+ };
90
+ this.buildGlyph(run.glyphs[i], scale, toWorld, out);
91
+ pen += adv + opts.letterSpacing + justifyGap;
92
+ }
93
+ }
94
+ /** Maps the path-friendly alignment synonyms onto the base values. */
95
+ static resolveAlign(align) {
96
+ if (align === "start") {
97
+ return "left";
98
+ }
99
+ if (align === "end") {
100
+ return "right";
101
+ }
102
+ return align;
103
+ }
104
+ /** Total advance of a laid-out line (for alignment), excluding trailing letter spacing. */
105
+ static totalAdvance(positions, opts, scale) {
106
+ let total = 0;
107
+ for (const pos of positions) {
108
+ total += pos.xAdvance * scale + opts.letterSpacing;
109
+ }
110
+ if (positions.length > 0) {
111
+ total -= opts.letterSpacing;
112
+ }
113
+ return total;
114
+ }
115
+ static buildLine(font, line, opts, scale, plane, origin, out) {
116
+ if (line.length === 0) {
117
+ return;
118
+ }
119
+ const run = font.layout(line);
120
+ const total = this.totalAdvance(run.positions, opts, scale);
121
+ const align = this.resolveAlign(opts.align);
122
+ let penX = origin.x;
123
+ if (align === "center") {
124
+ penX -= total / 2;
125
+ }
126
+ else if (align === "right") {
127
+ penX -= total;
128
+ }
129
+ for (let i = 0; i < run.glyphs.length; i++) {
130
+ const pos = run.positions[i];
131
+ const gx = penX + (pos.xOffset || 0) * scale;
132
+ const gy = origin.y + (pos.yOffset || 0) * scale;
133
+ const toWorld = (p) => plane.localToWorld(new Point2D(gx + p.x * scale, gy + p.y * scale));
134
+ this.buildGlyph(run.glyphs[i], scale, toWorld, out);
135
+ penX += pos.xAdvance * scale + opts.letterSpacing;
136
+ }
137
+ }
138
+ static buildGlyph(glyph, scale, toWorld, out) {
139
+ const commands = glyph.path?.commands;
140
+ if (!commands || commands.length === 0) {
141
+ return;
142
+ }
143
+ const dist = (a, b) => Math.hypot((a.x - b.x) * scale, (a.y - b.y) * scale);
144
+ const addSegment = (a, b) => {
145
+ if (dist(a, b) < EPS) {
146
+ return;
147
+ }
148
+ try {
149
+ out.push(Geometry.makeEdge(Geometry.makeSegment(toWorld(a), toWorld(b))));
150
+ }
151
+ catch {
152
+ // Skip a segment OCCT rejects; one bad edge shouldn't drop the glyph.
153
+ }
154
+ };
155
+ const addBezier = (poles) => {
156
+ if (poles.every(p => dist(p, poles[0]) < EPS)) {
157
+ return;
158
+ }
159
+ try {
160
+ out.push(Geometry.makeEdgeFromBezier(Geometry.makeBezierCurve(poles.map(toWorld))));
161
+ }
162
+ catch {
163
+ // Fall back to a straight chord if the curve can't be built.
164
+ addSegment(poles[0], poles[poles.length - 1]);
165
+ }
166
+ };
167
+ let cur = null;
168
+ let startPt = null;
169
+ const closeContour = () => {
170
+ if (cur && startPt) {
171
+ addSegment(cur, startPt);
172
+ }
173
+ cur = null;
174
+ startPt = null;
175
+ };
176
+ for (const cmd of commands) {
177
+ const a = cmd.args;
178
+ switch (cmd.command) {
179
+ case "moveTo":
180
+ closeContour();
181
+ cur = { x: a[0], y: a[1] };
182
+ startPt = cur;
183
+ break;
184
+ case "lineTo":
185
+ if (cur) {
186
+ const next = { x: a[0], y: a[1] };
187
+ addSegment(cur, next);
188
+ cur = next;
189
+ }
190
+ break;
191
+ case "quadraticCurveTo":
192
+ if (cur) {
193
+ const next = { x: a[2], y: a[3] };
194
+ addBezier([cur, { x: a[0], y: a[1] }, next]);
195
+ cur = next;
196
+ }
197
+ break;
198
+ case "bezierCurveTo":
199
+ if (cur) {
200
+ const next = { x: a[4], y: a[5] };
201
+ addBezier([cur, { x: a[0], y: a[1] }, { x: a[2], y: a[3] }, next]);
202
+ cur = next;
203
+ }
204
+ break;
205
+ case "closePath":
206
+ closeContour();
207
+ break;
208
+ }
209
+ }
210
+ closeContour();
211
+ }
212
+ }
@@ -1,4 +1,4 @@
1
- import type { TopoDS_Shape, TopTools_IndexedDataMapOfShapeListOfShape, TopTools_MapOfShape } from "occjs-wrapper";
1
+ import type { TopoDS_Shape, TopTools_IndexedDataMapOfShapeListOfShape, TopTools_MapOfShape } from "ocjs-fluidcad";
2
2
  export declare class TopologyIndex {
3
3
  static buildEdgeToFaces(root: TopoDS_Shape): TopTools_IndexedDataMapOfShapeListOfShape;
4
4
  static buildShapeSet(shapes: TopoDS_Shape[]): TopTools_MapOfShape;
@@ -1,4 +1,4 @@
1
- import type { TopoDS_Vertex } from "occjs-wrapper";
1
+ import type { TopoDS_Vertex } from "ocjs-fluidcad";
2
2
  import { Point } from "../math/point.js";
3
3
  import { Vertex } from "../common/vertex.js";
4
4
  export declare class VertexOps {
@@ -1,4 +1,4 @@
1
- import type { TopoDS_Edge, TopoDS_Wire, TopoDS_Face } from "occjs-wrapper";
1
+ import type { TopoDS_Edge, TopoDS_Wire, TopoDS_Face } from "ocjs-fluidcad";
2
2
  import { Vector3d } from "../math/vector3d.js";
3
3
  import { Wire } from "../common/wire.js";
4
4
  import { Edge } from "../common/edge.js";