fluidcad 0.0.36 → 0.0.38

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 (188) 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/plane.d.ts +26 -6
  25. package/lib/dist/core/plane.js +21 -44
  26. package/lib/dist/core/wrap.d.ts +17 -0
  27. package/lib/dist/core/wrap.js +39 -0
  28. package/lib/dist/features/2d/offset.js +2 -2
  29. package/lib/dist/features/2d/text.d.ts +67 -0
  30. package/lib/dist/features/2d/text.js +320 -0
  31. package/lib/dist/features/cylinder.d.ts +3 -1
  32. package/lib/dist/features/cylinder.js +5 -2
  33. package/lib/dist/features/extrude-base.d.ts +1 -0
  34. package/lib/dist/features/extrude-to-face.d.ts +1 -0
  35. package/lib/dist/features/extrude-to-face.js +6 -0
  36. package/lib/dist/features/fillet.d.ts +1 -1
  37. package/lib/dist/features/helix.d.ts +41 -0
  38. package/lib/dist/features/helix.js +337 -0
  39. package/lib/dist/features/plane-from-object.d.ts +16 -4
  40. package/lib/dist/features/plane-from-object.js +101 -8
  41. package/lib/dist/features/select.js +32 -8
  42. package/lib/dist/features/simple-extruder.d.ts +1 -1
  43. package/lib/dist/features/simple-extruder.js +7 -2
  44. package/lib/dist/features/sphere.d.ts +3 -1
  45. package/lib/dist/features/sphere.js +5 -2
  46. package/lib/dist/features/sweep.js +7 -2
  47. package/lib/dist/features/wrap.d.ts +39 -0
  48. package/lib/dist/features/wrap.js +116 -0
  49. package/lib/dist/filters/edge/belongs-to-face.d.ts +3 -1
  50. package/lib/dist/filters/edge/belongs-to-face.js +14 -10
  51. package/lib/dist/filters/filter.d.ts +1 -1
  52. package/lib/dist/filters/from-object.d.ts +1 -1
  53. package/lib/dist/filters/tangent-expander.d.ts +1 -1
  54. package/lib/dist/filters/tangent-expander.js +57 -40
  55. package/lib/dist/helpers/scene-helpers.d.ts +2 -0
  56. package/lib/dist/helpers/scene-helpers.js +1 -1
  57. package/lib/dist/index.d.ts +2 -0
  58. package/lib/dist/index.js +3 -1
  59. package/lib/dist/io/file-import.d.ts +7 -0
  60. package/lib/dist/io/file-import.js +28 -1
  61. package/lib/dist/io/font-registry.d.ts +45 -0
  62. package/lib/dist/io/font-registry.js +272 -0
  63. package/lib/dist/math/bspline-interpolation.d.ts +29 -0
  64. package/lib/dist/math/bspline-interpolation.js +194 -0
  65. package/lib/dist/oc/boolean-ops.d.ts +3 -1
  66. package/lib/dist/oc/boolean-ops.js +15 -1
  67. package/lib/dist/oc/color-transfer.d.ts +1 -1
  68. package/lib/dist/oc/constraints/constraint-helpers.d.ts +4 -4
  69. package/lib/dist/oc/constraints/curve/tangent-circle-solver.js +10 -9
  70. package/lib/dist/oc/constraints/curve/tangent-line-solver.js +5 -6
  71. package/lib/dist/oc/convert.d.ts +1 -1
  72. package/lib/dist/oc/draft-ops.d.ts +1 -1
  73. package/lib/dist/oc/edge-ops.d.ts +2 -2
  74. package/lib/dist/oc/edge-ops.js +13 -14
  75. package/lib/dist/oc/edge-props.d.ts +1 -1
  76. package/lib/dist/oc/edge-query.d.ts +1 -1
  77. package/lib/dist/oc/edge-query.js +3 -8
  78. package/lib/dist/oc/errors.d.ts +8 -0
  79. package/lib/dist/oc/errors.js +27 -0
  80. package/lib/dist/oc/explorer.d.ts +2 -2
  81. package/lib/dist/oc/extrude-ops.d.ts +28 -2
  82. package/lib/dist/oc/extrude-ops.js +56 -7
  83. package/lib/dist/oc/face-ops.d.ts +2 -1
  84. package/lib/dist/oc/face-ops.js +11 -0
  85. package/lib/dist/oc/face-props.d.ts +1 -1
  86. package/lib/dist/oc/face-query.d.ts +12 -1
  87. package/lib/dist/oc/face-query.js +39 -0
  88. package/lib/dist/oc/fillet-ops.d.ts +1 -1
  89. package/lib/dist/oc/fillet-ops.js +4 -4
  90. package/lib/dist/oc/geometry.d.ts +1 -1
  91. package/lib/dist/oc/geometry.js +12 -14
  92. package/lib/dist/oc/helix-ops.d.ts +37 -0
  93. package/lib/dist/oc/helix-ops.js +88 -0
  94. package/lib/dist/oc/hit-test.d.ts +1 -1
  95. package/lib/dist/oc/index.d.ts +4 -0
  96. package/lib/dist/oc/index.js +2 -0
  97. package/lib/dist/oc/init.d.ts +1 -1
  98. package/lib/dist/oc/init.js +1 -1
  99. package/lib/dist/oc/intersection.js +1 -1
  100. package/lib/dist/oc/io.d.ts +6 -6
  101. package/lib/dist/oc/io.js +31 -24
  102. package/lib/dist/oc/measure/classify.d.ts +34 -0
  103. package/lib/dist/oc/measure/classify.js +246 -0
  104. package/lib/dist/oc/measure/measure-ops.d.ts +9 -0
  105. package/lib/dist/oc/measure/measure-ops.js +210 -0
  106. package/lib/dist/oc/measure/measure-types.d.ts +39 -0
  107. package/lib/dist/oc/measure/measure-types.js +1 -0
  108. package/lib/dist/oc/measure/sampling.d.ts +9 -0
  109. package/lib/dist/oc/measure/sampling.js +77 -0
  110. package/lib/dist/oc/measure/vec.d.ts +13 -0
  111. package/lib/dist/oc/measure/vec.js +23 -0
  112. package/lib/dist/oc/mesh.d.ts +1 -1
  113. package/lib/dist/oc/mesh.js +40 -28
  114. package/lib/dist/oc/path-sampler.d.ts +29 -0
  115. package/lib/dist/oc/path-sampler.js +63 -0
  116. package/lib/dist/oc/props.d.ts +1 -1
  117. package/lib/dist/oc/props.js +4 -1
  118. package/lib/dist/oc/shape-hash.d.ts +26 -0
  119. package/lib/dist/oc/shape-hash.js +32 -0
  120. package/lib/dist/oc/shape-ops.d.ts +5 -3
  121. package/lib/dist/oc/shape-ops.js +6 -5
  122. package/lib/dist/oc/sweep-ops.d.ts +13 -1
  123. package/lib/dist/oc/sweep-ops.js +174 -18
  124. package/lib/dist/oc/text-outline.d.ts +62 -0
  125. package/lib/dist/oc/text-outline.js +212 -0
  126. package/lib/dist/oc/thin-face-maker.d.ts +0 -19
  127. package/lib/dist/oc/thin-face-maker.js +3 -68
  128. package/lib/dist/oc/topology-index.d.ts +1 -1
  129. package/lib/dist/oc/vertex-ops.d.ts +1 -1
  130. package/lib/dist/oc/wire-ops.d.ts +18 -3
  131. package/lib/dist/oc/wire-ops.js +56 -5
  132. package/lib/dist/oc/wrap-development.d.ts +105 -0
  133. package/lib/dist/oc/wrap-development.js +179 -0
  134. package/lib/dist/oc/wrap-ops.d.ts +100 -0
  135. package/lib/dist/oc/wrap-ops.js +406 -0
  136. package/lib/dist/rendering/render-solid.js +10 -2
  137. package/lib/dist/scene-manager.d.ts +2 -0
  138. package/lib/dist/scene-manager.js +29 -0
  139. package/lib/dist/tests/features/2d/offset.test.js +74 -1
  140. package/lib/dist/tests/features/cylinder-curve-filter.test.js +3 -3
  141. package/lib/dist/tests/features/extrude-to-face.test.js +38 -1
  142. package/lib/dist/tests/features/helix.test.d.ts +1 -0
  143. package/lib/dist/tests/features/helix.test.js +295 -0
  144. package/lib/dist/tests/features/plane.test.js +95 -0
  145. package/lib/dist/tests/features/repeat-primitive.test.d.ts +1 -0
  146. package/lib/dist/tests/features/repeat-primitive.test.js +60 -0
  147. package/lib/dist/tests/features/rib.test.js +6 -1
  148. package/lib/dist/tests/features/sweep.test.js +170 -1
  149. package/lib/dist/tests/features/text.test.d.ts +1 -0
  150. package/lib/dist/tests/features/text.test.js +347 -0
  151. package/lib/dist/tests/features/wrap-development.test.d.ts +1 -0
  152. package/lib/dist/tests/features/wrap-development.test.js +130 -0
  153. package/lib/dist/tests/features/wrap-extruded-target.test.d.ts +1 -0
  154. package/lib/dist/tests/features/wrap-extruded-target.test.js +106 -0
  155. package/lib/dist/tests/features/wrap-repeat.test.d.ts +1 -0
  156. package/lib/dist/tests/features/wrap-repeat.test.js +93 -0
  157. package/lib/dist/tests/features/wrap.test.d.ts +1 -0
  158. package/lib/dist/tests/features/wrap.test.js +331 -0
  159. package/lib/dist/tests/math/bspline-interpolation.test.d.ts +1 -0
  160. package/lib/dist/tests/math/bspline-interpolation.test.js +119 -0
  161. package/lib/dist/tests/measure.test.d.ts +1 -0
  162. package/lib/dist/tests/measure.test.js +288 -0
  163. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  164. package/llm-docs/api/helix.md +64 -0
  165. package/llm-docs/api/index.json +11 -2
  166. package/llm-docs/api/text.md +52 -0
  167. package/llm-docs/api/types/helix.md +105 -0
  168. package/llm-docs/api/types/text.md +138 -0
  169. package/llm-docs/api/types/wrap.md +131 -0
  170. package/llm-docs/api/wrap.md +62 -0
  171. package/llm-docs/index.json +121 -1
  172. package/mcp/dist/server.js +20 -1
  173. package/mcp/dist/tools/inspection.d.ts +17 -0
  174. package/mcp/dist/tools/inspection.js +14 -0
  175. package/package.json +7 -3
  176. package/server/dist/fluidcad-server.d.ts +11 -1
  177. package/server/dist/fluidcad-server.js +21 -1
  178. package/server/dist/index.js +4 -2
  179. package/server/dist/preferences.d.ts +4 -0
  180. package/server/dist/preferences.js +2 -0
  181. package/server/dist/routes/measure.d.ts +3 -0
  182. package/server/dist/routes/measure.js +32 -0
  183. package/server/dist/routes/params.js +1 -1
  184. package/server/dist/routes/preferences.js +6 -0
  185. package/server/dist/routes/sketch-edits.js +2 -1
  186. package/ui/dist/assets/{index-MRqwG9Vh.js → index-D8zV21wB.js} +149 -102
  187. package/ui/dist/assets/{index-CDJmUpFI.css → index-dAFdg2Un.css} +1 -1
  188. 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,17 @@ 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
+ /**
16
+ * The axis the spine's tangent rotates around, = normalize(Σ Tᵢ × Tᵢ₊₁) over
17
+ * tangents sampled along the spine. For a planar spine this is the plane
18
+ * normal; for a helix it is the coil axis. For a straight spine the tangent
19
+ * is constant, every cross product vanishes, and it returns null.
20
+ */
21
+ private static tangentRotationAxis;
22
+ /** Unit tangent of the spine wire at its first parameter. */
23
+ private static getSpineTangent;
12
24
  }
@@ -1,32 +1,107 @@
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 for MakePipeShell's `SetMode`: it locks the section's
21
+ // "up", so the profile keeps a constant angle to it instead of twisting
22
+ // along the spine. The correct direction is the axis the spine's tangent
23
+ // rotates around — the plane normal for a planar spine, the coil axis for a
24
+ // helix. The tangent keeps a constant, non-zero angle to that axis, so the
25
+ // section never flips and the result is a clean coil.
26
+ //
27
+ // The profile plane's own "up" (used previously) only works when it happens
28
+ // to equal that axis — true for a profile sketched on a world plane, but
29
+ // NOT for a plane built off a helix, whose in-plane axes are arbitrary.
30
+ // A wrong (e.g. roughly horizontal) binormal lets the helix tangent rotate
31
+ // into it, collapsing `Normal = BiNormal × Tangent` ~twice per turn and
32
+ // shredding the section into a self-intersecting ribbon. A straight spine
33
+ // has no rotation axis (the cross products vanish); there the profile's up
34
+ // is well-defined and never aligns with the constant tangent, so use it.
35
+ const spineAxis = SweepOps.tangentRotationAxis(spineWire.getShape());
36
+ const binormalVec = spineAxis ?? profilePlane.yDirection;
37
+ const [binormalDir, disposeBinormal] = Convert.toGpDir(binormalVec);
38
+ // `Add(_, false, true)` (no contact, with correction) rotates the profile
39
+ // to sit perpendicular to the spine tangent, about an axis given by
40
+ // `profile.normal × spine.tangent`. That axis is undefined when the two are
41
+ // anti-parallel — but then the profile plane is *already* perpendicular to
42
+ // the spine (its normal is ∥ -tangent), so no correction is needed: skip it
43
+ // and keep the profile's drawn position.
44
+ const spineTangent = SweepOps.getSpineTangent(spineWire.getShape());
45
+ const isAntiParallel = profilePlane.normal.dot(spineTangent) < -0.999;
46
+ const withCorrection = !isAntiParallel;
47
+ try {
48
+ for (const face of profileFaces) {
49
+ const ocFace = oc.TopoDS.Face(face.getShape());
50
+ const outerWire = oc.BRepTools.OuterWire(ocFace);
51
+ const innerWires = face.getWires()
52
+ .map(w => w.getShape())
53
+ .filter(w => !w.IsSame(outerWire));
54
+ const outer = SweepOps.sweepWire(spineWire.getShape(), outerWire, binormalDir, withCorrection);
55
+ let resultSolid = outer.solid;
56
+ let resultFirst = outer.firstFace;
57
+ let resultLast = outer.lastFace;
58
+ for (const innerWire of innerWires) {
59
+ const inner = SweepOps.sweepWire(spineWire.getShape(), oc.TopoDS.Wire(innerWire), binormalDir, withCorrection);
60
+ const stockList = new oc.TopTools_ListOfShape();
61
+ stockList.Append(resultSolid);
62
+ const toolList = new oc.TopTools_ListOfShape();
63
+ toolList.Append(inner.solid);
64
+ const cut = new oc.BRepAlgoAPI_Cut();
65
+ cut.SetArguments(stockList);
66
+ cut.SetTools(toolList);
67
+ const progress = new oc.Message_ProgressRange();
68
+ cut.Build(progress);
69
+ progress.delete();
70
+ if (!cut.IsDone()) {
71
+ cut.delete();
72
+ stockList.delete();
73
+ toolList.delete();
74
+ throw new Error("Sweep hole cut failed.");
75
+ }
76
+ const newSolid = cut.Shape();
77
+ // Track first/last faces through the cut. The outer's start/end
78
+ // face becomes a hole-bearing face after cutting through it.
79
+ const modFirst = ShapeOps.shapeListToArray(cut.Modified(resultFirst));
80
+ const modLast = ShapeOps.shapeListToArray(cut.Modified(resultLast));
81
+ if (modFirst.length > 0) {
82
+ resultFirst = modFirst[0];
83
+ }
84
+ if (modLast.length > 0) {
85
+ resultLast = modLast[0];
86
+ }
87
+ cut.delete();
88
+ stockList.delete();
89
+ toolList.delete();
90
+ resultSolid = newSolid;
91
+ }
92
+ if (!firstShape) {
93
+ firstShape = resultFirst;
94
+ lastShape = resultLast;
95
+ }
96
+ const solids = Explorer.findShapes(resultSolid, Explorer.getOcShapeType("solid"));
97
+ for (const s of solids) {
98
+ allSolids.push(Solid.fromTopoDSSolid(Explorer.toSolid(s)));
99
+ }
28
100
  }
29
101
  }
102
+ finally {
103
+ disposeBinormal();
104
+ }
30
105
  if (allSolids.length === 0) {
31
106
  throw new Error("Sweep produced no solids.");
32
107
  }
@@ -36,4 +111,85 @@ export class SweepOps {
36
111
  lastShape: lastShape,
37
112
  };
38
113
  }
114
+ /** Sweep a single wire along the spine with a fixed binormal. */
115
+ static sweepWire(spine, profile, binormalDir, withCorrection) {
116
+ const oc = getOC();
117
+ const pipe = new oc.BRepOffsetAPI_MakePipeShell(spine);
118
+ // Fixed binormal (the spine's tangent-rotation axis; see makeSweep): keeps
119
+ // the swept section from twisting — a clean coil rather than a wobbling
120
+ // ribbon — and is well-defined on straight spines, where Frenet is not
121
+ // (zero curvature ⇒ undefined normal).
122
+ pipe.SetMode(binormalDir);
123
+ // Give the swept-surface approximation enough spans for tapered/tight
124
+ // helical spines (see MAX_PIPE_SEGMENTS) — at OCCT's default budget the
125
+ // build fails on, e.g., a conical helix or a many-turn helix on a cone face.
126
+ pipe.SetMaxSegments(SweepOps.MAX_PIPE_SEGMENTS);
127
+ pipe.Add(profile, false, withCorrection);
128
+ const progress = new oc.Message_ProgressRange();
129
+ pipe.Build(progress);
130
+ progress.delete();
131
+ if (!pipe.IsDone()) {
132
+ pipe.delete();
133
+ throw new Error("Sweep operation failed.");
134
+ }
135
+ if (!pipe.MakeSolid()) {
136
+ pipe.delete();
137
+ throw new Error("Sweep failed to produce a solid.");
138
+ }
139
+ const firstFace = pipe.FirstShape();
140
+ const lastFace = pipe.LastShape();
141
+ const solid = pipe.Shape();
142
+ pipe.delete();
143
+ return { solid, firstFace, lastFace };
144
+ }
145
+ /**
146
+ * The axis the spine's tangent rotates around, = normalize(Σ Tᵢ × Tᵢ₊₁) over
147
+ * tangents sampled along the spine. For a planar spine this is the plane
148
+ * normal; for a helix it is the coil axis. For a straight spine the tangent
149
+ * is constant, every cross product vanishes, and it returns null.
150
+ */
151
+ static tangentRotationAxis(spine) {
152
+ const oc = getOC();
153
+ const adaptor = new oc.BRepAdaptor_CompCurve(spine, false);
154
+ const u0 = adaptor.FirstParameter();
155
+ const u1 = adaptor.LastParameter();
156
+ const SAMPLES = 64;
157
+ const tangents = [];
158
+ const pnt = new oc.gp_Pnt();
159
+ const vec = new oc.gp_Vec();
160
+ for (let i = 0; i <= SAMPLES; i++) {
161
+ const u = u0 + ((u1 - u0) * i) / SAMPLES;
162
+ adaptor.D1(u, pnt, vec);
163
+ const t = new Vector3d(vec.X(), vec.Y(), vec.Z());
164
+ if (t.length() > 1e-9) {
165
+ tangents.push(t.normalize());
166
+ }
167
+ }
168
+ pnt.delete();
169
+ vec.delete();
170
+ adaptor.delete();
171
+ let axis = new Vector3d(0, 0, 0);
172
+ for (let i = 0; i + 1 < tangents.length; i++) {
173
+ axis = axis.add(tangents[i].cross(tangents[i + 1]));
174
+ }
175
+ if (axis.length() < 1e-6) {
176
+ return null;
177
+ }
178
+ return axis.normalize();
179
+ }
180
+ /** Unit tangent of the spine wire at its first parameter. */
181
+ static getSpineTangent(spine) {
182
+ const oc = getOC();
183
+ const adaptor = new oc.BRepAdaptor_CompCurve(spine, false);
184
+ const u0 = adaptor.FirstParameter();
185
+ const pnt = new oc.gp_Pnt();
186
+ const tan = new oc.gp_Vec();
187
+ adaptor.D1(u0, pnt, tan);
188
+ tan.Normalize();
189
+ const tangent = new Vector3d(tan.X(), tan.Y(), tan.Z());
190
+ pnt.delete();
191
+ tan.delete();
192
+ adaptor.delete();
193
+ return tangent;
194
+ }
39
195
  }
@@ -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
+ }
@@ -11,19 +11,6 @@ export declare class ThinFaceMaker {
11
11
  static make(edges: (Wire | Edge)[], plane: Plane, offset1: number, offset2?: number): ThinFaceResult;
12
12
  private static makeSingleOffsetFace;
13
13
  private static makeDualOffsetFace;
14
- /**
15
- * Offsets a wire by the given distance, handling both closed and open wires.
16
- * For closed wires, WireOps.offsetWire handles negative distances natively.
17
- * For open wires, negative distances are handled by reversing the wire,
18
- * offsetting with the absolute value, then reversing back.
19
- *
20
- * If the wire-only offset throws (e.g. "Offset wire is not closed." on
21
- * wires whose corners are GeomAbs_OffsetCurve segments from `offset()` over
22
- * a drafted body's filleted bottom), retries with a planar face as the
23
- * offset spine — that path supplies an explicit normal which keeps the
24
- * algorithm stable on the same input.
25
- */
26
- private static doOffset;
27
14
  /**
28
15
  * Merges adjacent edges that share the same underlying curve into a single
29
16
  * edge (e.g. two conic-arc segments at a filleted corner produced by
@@ -33,12 +20,6 @@ export declare class ThinFaceMaker {
33
20
  * upgrader produces no usable result.
34
21
  */
35
22
  private static unifyWireEdges;
36
- /**
37
- * Offsets an open wire on a given plane, using a planar face as reference
38
- * so that BRepOffsetAPI_MakeOffset knows the offset direction.
39
- * Only handles positive distances — use doOffset for sign handling.
40
- */
41
- private static offsetWireOnPlane;
42
23
  /**
43
24
  * Finds face edges that geometrically match the given wire edges by comparing midpoints.
44
25
  * This is needed because wire reversal (ShapeExtend_WireData.Reverse) creates new TShapes,