fluidcad 0.0.38 → 0.0.39

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 (98) hide show
  1. package/lib/dist/core/2d/aline.d.ts +0 -8
  2. package/lib/dist/core/2d/aline.js +3 -22
  3. package/lib/dist/core/2d/arc.d.ts +1 -23
  4. package/lib/dist/core/2d/arc.js +11 -28
  5. package/lib/dist/core/2d/circle.d.ts +1 -8
  6. package/lib/dist/core/2d/circle.js +7 -24
  7. package/lib/dist/core/2d/ellipse.d.ts +1 -17
  8. package/lib/dist/core/2d/ellipse.js +7 -45
  9. package/lib/dist/core/2d/hline.d.ts +0 -7
  10. package/lib/dist/core/2d/hline.js +5 -27
  11. package/lib/dist/core/2d/intersect.d.ts +0 -7
  12. package/lib/dist/core/2d/intersect.js +0 -13
  13. package/lib/dist/core/2d/line.d.ts +1 -8
  14. package/lib/dist/core/2d/line.js +6 -24
  15. package/lib/dist/core/2d/offset.d.ts +1 -11
  16. package/lib/dist/core/2d/offset.js +0 -16
  17. package/lib/dist/core/2d/polygon.d.ts +1 -17
  18. package/lib/dist/core/2d/polygon.js +18 -37
  19. package/lib/dist/core/2d/project.d.ts +0 -7
  20. package/lib/dist/core/2d/project.js +0 -13
  21. package/lib/dist/core/2d/rect.d.ts +1 -9
  22. package/lib/dist/core/2d/rect.js +0 -19
  23. package/lib/dist/core/2d/slot.d.ts +0 -23
  24. package/lib/dist/core/2d/slot.js +18 -37
  25. package/lib/dist/core/2d/text.d.ts +0 -7
  26. package/lib/dist/core/2d/text.js +0 -14
  27. package/lib/dist/core/2d/vline.d.ts +0 -7
  28. package/lib/dist/core/2d/vline.js +5 -23
  29. package/lib/dist/core/fillet.js +7 -0
  30. package/lib/dist/core/index.d.ts +1 -1
  31. package/lib/dist/core/interfaces.d.ts +43 -0
  32. package/lib/dist/features/2d/connect.d.ts +2 -0
  33. package/lib/dist/features/2d/connect.js +3 -0
  34. package/lib/dist/features/2d/intersect.js +4 -1
  35. package/lib/dist/features/frozen-geometry.d.ts +29 -0
  36. package/lib/dist/features/frozen-geometry.js +62 -0
  37. package/lib/dist/features/loft.d.ts +38 -1
  38. package/lib/dist/features/loft.js +133 -8
  39. package/lib/dist/features/rib.js +11 -1
  40. package/lib/dist/features/shell.d.ts +1 -1
  41. package/lib/dist/features/shell.js +9 -0
  42. package/lib/dist/features/sweep.d.ts +11 -1
  43. package/lib/dist/features/sweep.js +43 -7
  44. package/lib/dist/helpers/clone-transform.js +62 -5
  45. package/lib/dist/helpers/scene-helpers.d.ts +12 -0
  46. package/lib/dist/helpers/scene-helpers.js +26 -0
  47. package/lib/dist/math/bspline-interpolation.d.ts +33 -6
  48. package/lib/dist/math/bspline-interpolation.js +187 -84
  49. package/lib/dist/oc/loft/constrained-loft.d.ts +32 -0
  50. package/lib/dist/oc/loft/constrained-loft.js +61 -0
  51. package/lib/dist/oc/loft/curve-data.d.ts +18 -0
  52. package/lib/dist/oc/loft/curve-data.js +54 -0
  53. package/lib/dist/oc/loft/curve-eval.d.ts +23 -0
  54. package/lib/dist/oc/loft/curve-eval.js +84 -0
  55. package/lib/dist/oc/loft/guided-loft.d.ts +89 -0
  56. package/lib/dist/oc/loft/guided-loft.js +368 -0
  57. package/lib/dist/oc/loft/section-compatibility.d.ts +131 -0
  58. package/lib/dist/oc/loft/section-compatibility.js +481 -0
  59. package/lib/dist/oc/loft/section-curve.d.ts +53 -0
  60. package/lib/dist/oc/loft/section-curve.js +264 -0
  61. package/lib/dist/oc/loft/skinning.d.ts +104 -0
  62. package/lib/dist/oc/loft/skinning.js +299 -0
  63. package/lib/dist/oc/loft-ops.d.ts +28 -1
  64. package/lib/dist/oc/loft-ops.js +31 -1
  65. package/lib/dist/oc/ncollection.d.ts +19 -0
  66. package/lib/dist/oc/ncollection.js +69 -0
  67. package/lib/dist/oc/rib-ops.js +5 -17
  68. package/lib/dist/oc/wire-extend-ops.d.ts +12 -0
  69. package/lib/dist/oc/wire-extend-ops.js +42 -0
  70. package/lib/dist/oc/wire-ops.d.ts +11 -0
  71. package/lib/dist/oc/wire-ops.js +33 -0
  72. package/lib/dist/oc/wrap-ops.js +8 -15
  73. package/lib/dist/tests/features/2d/circle.test.js +0 -10
  74. package/lib/dist/tests/features/2d/ellipse.test.js +0 -30
  75. package/lib/dist/tests/features/2d/line.test.js +0 -5
  76. package/lib/dist/tests/features/2d/offset.test.js +0 -10
  77. package/lib/dist/tests/features/2d/polygon.test.js +0 -16
  78. package/lib/dist/tests/features/2d/rect.test.js +0 -11
  79. package/lib/dist/tests/features/2d/slot.test.js +0 -8
  80. package/lib/dist/tests/features/fillet2d.test.js +37 -1
  81. package/lib/dist/tests/features/loft-conditions.test.d.ts +1 -0
  82. package/lib/dist/tests/features/loft-conditions.test.js +246 -0
  83. package/lib/dist/tests/features/loft-guides.test.d.ts +1 -0
  84. package/lib/dist/tests/features/loft-guides.test.js +276 -0
  85. package/lib/dist/tests/features/repeat-sweep-borrowed-path.test.d.ts +1 -0
  86. package/lib/dist/tests/features/repeat-sweep-borrowed-path.test.js +35 -0
  87. package/lib/dist/tests/features/rib.test.js +50 -0
  88. package/lib/dist/tests/features/shell.test.js +35 -1
  89. package/lib/dist/tests/features/sweep.test.js +81 -0
  90. package/lib/dist/tests/features/text.test.js +33 -14
  91. package/lib/dist/tests/math/bspline-interpolation.test.js +97 -1
  92. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  93. package/llm-docs/api/index.json +1 -1
  94. package/llm-docs/api/types/loft.md +36 -1
  95. package/llm-docs/api/types/sweep.md +13 -1
  96. package/llm-docs/api/types/text.md +2 -2
  97. package/llm-docs/index.json +5 -5
  98. package/package.json +1 -1
@@ -1,5 +1,32 @@
1
1
  import { Solid } from "../common/solid.js";
2
2
  import { Wire } from "../common/wire.js";
3
+ import { LoftEndCondition, LoftConditionKind } from "./loft/constrained-loft.js";
4
+ export type { LoftEndCondition, LoftConditionKind };
5
+ export interface LoftOptions {
6
+ /** Constrains how the surface leaves the first profile. */
7
+ startCondition?: LoftEndCondition;
8
+ /** Constrains how the surface arrives at the last profile. */
9
+ endCondition?: LoftEndCondition;
10
+ /** One or two side rails the loft surface must follow. */
11
+ guides?: Wire[];
12
+ }
3
13
  export declare class LoftOps {
4
- static makeLoft(wires: Wire[]): Solid[];
14
+ /**
15
+ * Lofts through the section wires. Plain lofts run OCC's ThruSections;
16
+ * guides dispatch to `GuidedLoft` (virtual sections carried onto the
17
+ * rails) and start/end conditions to `ConstrainedLoft` (derivative-pinned
18
+ * skinning) — both in-house skins, since OCC can neither constrain end
19
+ * tangency nor follow rails without distorting sections. Conditions
20
+ * compose with guides: the condition fades out around each guide contact
21
+ * (the rails own their sides of the surface).
22
+ */
23
+ static makeLoft(wires: Wire[], options?: LoftOptions): Solid[];
24
+ /**
25
+ * Thin-walled loft with start/end conditions: both walls are skinned with
26
+ * the same conditions and assembled directly with ring caps — see
27
+ * `ConstrainedLoft.buildThin`. (The condition-less thin loft stays on the
28
+ * legacy ThruSections + boolean path in the feature layer.)
29
+ */
30
+ static makeThinLoft(outerWires: Wire[], innerWires: Wire[], options: LoftOptions): Solid[];
31
+ private static makeThruSectionsLoft;
5
32
  }
@@ -1,8 +1,38 @@
1
1
  import { getOC } from "./init.js";
2
2
  import { Explorer } from "./explorer.js";
3
3
  import { Solid } from "../common/solid.js";
4
+ import { ConstrainedLoft } from "./loft/constrained-loft.js";
5
+ import { GuidedLoft } from "./loft/guided-loft.js";
4
6
  export class LoftOps {
5
- static makeLoft(wires) {
7
+ /**
8
+ * Lofts through the section wires. Plain lofts run OCC's ThruSections;
9
+ * guides dispatch to `GuidedLoft` (virtual sections carried onto the
10
+ * rails) and start/end conditions to `ConstrainedLoft` (derivative-pinned
11
+ * skinning) — both in-house skins, since OCC can neither constrain end
12
+ * tangency nor follow rails without distorting sections. Conditions
13
+ * compose with guides: the condition fades out around each guide contact
14
+ * (the rails own their sides of the surface).
15
+ */
16
+ static makeLoft(wires, options) {
17
+ const guides = options?.guides ?? [];
18
+ if (guides.length > 0) {
19
+ return GuidedLoft.build(wires, guides, options?.startCondition, options?.endCondition);
20
+ }
21
+ if (options?.startCondition || options?.endCondition) {
22
+ return ConstrainedLoft.build(wires, options.startCondition, options.endCondition);
23
+ }
24
+ return LoftOps.makeThruSectionsLoft(wires);
25
+ }
26
+ /**
27
+ * Thin-walled loft with start/end conditions: both walls are skinned with
28
+ * the same conditions and assembled directly with ring caps — see
29
+ * `ConstrainedLoft.buildThin`. (The condition-less thin loft stays on the
30
+ * legacy ThruSections + boolean path in the feature layer.)
31
+ */
32
+ static makeThinLoft(outerWires, innerWires, options) {
33
+ return ConstrainedLoft.buildThin(outerWires, innerWires, options.startCondition, options.endCondition);
34
+ }
35
+ static makeThruSectionsLoft(wires) {
6
36
  const oc = getOC();
7
37
  const thruSections = new oc.BRepOffsetAPI_ThruSections(true, false, 1e-6);
8
38
  for (const wire of wires) {
@@ -0,0 +1,19 @@
1
+ import type { NCollection_Array1_double, NCollection_Array1_int, NCollection_Array1_gp_Pnt, NCollection_Array1_gp_Pnt2d, NCollection_Array2_gp_Pnt, NCollection_Array2_double } from "ocjs-fluidcad";
2
+ import type { Disposable } from "./convert.js";
3
+ import { Point2D } from "../math/point.js";
4
+ /**
5
+ * Builders for the 1-based NCollection arrays that OCC geometry constructors
6
+ * consume. Each returns the codebase's `Disposable` pair so call sites free
7
+ * the native array (and nothing else) once the consumer has copied it.
8
+ */
9
+ export declare class NCollections {
10
+ static toArray1Double(values: ReadonlyArray<number>): Disposable<NCollection_Array1_double>;
11
+ static toArray1Int(values: ReadonlyArray<number>): Disposable<NCollection_Array1_int>;
12
+ /** Each point is an [x, y, z] triple. */
13
+ static toArray1Pnt(points: ReadonlyArray<ReadonlyArray<number>>): Disposable<NCollection_Array1_gp_Pnt>;
14
+ static toArray1Pnt2d(points: ReadonlyArray<Point2D>): Disposable<NCollection_Array1_gp_Pnt2d>;
15
+ /** Grid of [x, y, z] triples, indexed [row][column]. */
16
+ static toArray2Pnt(grid: ReadonlyArray<ReadonlyArray<ReadonlyArray<number>>>): Disposable<NCollection_Array2_gp_Pnt>;
17
+ /** Grid of numbers, indexed [row][column]. */
18
+ static toArray2Double(grid: ReadonlyArray<ReadonlyArray<number>>): Disposable<NCollection_Array2_double>;
19
+ }
@@ -0,0 +1,69 @@
1
+ import { getOC } from "./init.js";
2
+ /**
3
+ * Builders for the 1-based NCollection arrays that OCC geometry constructors
4
+ * consume. Each returns the codebase's `Disposable` pair so call sites free
5
+ * the native array (and nothing else) once the consumer has copied it.
6
+ */
7
+ export class NCollections {
8
+ static toArray1Double(values) {
9
+ const oc = getOC();
10
+ const array = new oc.NCollection_Array1_double(1, values.length);
11
+ for (let i = 0; i < values.length; i++) {
12
+ array.SetValue(i + 1, values[i]);
13
+ }
14
+ return [array, () => array.delete()];
15
+ }
16
+ static toArray1Int(values) {
17
+ const oc = getOC();
18
+ const array = new oc.NCollection_Array1_int(1, values.length);
19
+ for (let i = 0; i < values.length; i++) {
20
+ array.SetValue(i + 1, values[i]);
21
+ }
22
+ return [array, () => array.delete()];
23
+ }
24
+ /** Each point is an [x, y, z] triple. */
25
+ static toArray1Pnt(points) {
26
+ const oc = getOC();
27
+ const array = new oc.NCollection_Array1_gp_Pnt(1, points.length);
28
+ for (let i = 0; i < points.length; i++) {
29
+ const point = new oc.gp_Pnt(points[i][0], points[i][1], points[i][2]);
30
+ array.SetValue(i + 1, point);
31
+ point.delete();
32
+ }
33
+ return [array, () => array.delete()];
34
+ }
35
+ static toArray1Pnt2d(points) {
36
+ const oc = getOC();
37
+ const array = new oc.NCollection_Array1_gp_Pnt2d(1, points.length);
38
+ for (let i = 0; i < points.length; i++) {
39
+ const point = new oc.gp_Pnt2d(points[i].x, points[i].y);
40
+ array.SetValue(i + 1, point);
41
+ point.delete();
42
+ }
43
+ return [array, () => array.delete()];
44
+ }
45
+ /** Grid of [x, y, z] triples, indexed [row][column]. */
46
+ static toArray2Pnt(grid) {
47
+ const oc = getOC();
48
+ const array = new oc.NCollection_Array2_gp_Pnt(1, grid.length, 1, grid[0].length);
49
+ for (let row = 0; row < grid.length; row++) {
50
+ for (let col = 0; col < grid[row].length; col++) {
51
+ const point = new oc.gp_Pnt(grid[row][col][0], grid[row][col][1], grid[row][col][2]);
52
+ array.SetValue(row + 1, col + 1, point);
53
+ point.delete();
54
+ }
55
+ }
56
+ return [array, () => array.delete()];
57
+ }
58
+ /** Grid of numbers, indexed [row][column]. */
59
+ static toArray2Double(grid) {
60
+ const oc = getOC();
61
+ const array = new oc.NCollection_Array2_double(1, grid.length, 1, grid[0].length);
62
+ for (let row = 0; row < grid.length; row++) {
63
+ for (let col = 0; col < grid[row].length; col++) {
64
+ array.SetValue(row + 1, col + 1, grid[row][col]);
65
+ }
66
+ }
67
+ return [array, () => array.delete()];
68
+ }
69
+ }
@@ -4,6 +4,7 @@ import { Point } from "../math/point.js";
4
4
  import { Vector3d } from "../math/vector3d.js";
5
5
  import { Matrix4 } from "../math/matrix4.js";
6
6
  import { WireOps } from "./wire-ops.js";
7
+ import { WireExtendOps } from "./wire-extend-ops.js";
7
8
  import { FaceOps } from "./face-ops.js";
8
9
  import { EdgeOps } from "./edge-ops.js";
9
10
  import { ShapeOps } from "./shape-ops.js";
@@ -144,17 +145,9 @@ export class RibOps {
144
145
  // overlaps every cavity boundary regardless of curvature (drafted cones,
145
146
  // fillets, etc.) — so the cut produces a clean blend on every face it touches.
146
147
  static extendSpineWire(spineWire, scopeShapes, _plane) {
147
- const edges = spineWire.getEdges();
148
- if (edges.length === 0) {
148
+ if (spineWire.getEdges().length === 0) {
149
149
  return spineWire;
150
150
  }
151
- const firstVertex = spineWire.getFirstVertex().toPoint();
152
- const lastVertex = spineWire.getLastVertex().toPoint();
153
- const lastEdge = edges[edges.length - 1];
154
- const endTangent = EdgeOps.getEdgeTangentAtEnd(lastEdge).normalize();
155
- const firstEdge = edges[0];
156
- const firstEdgeEnd = EdgeOps.getLastVertex(firstEdge).toPoint();
157
- const startTangent = firstVertex.vectorTo(firstEdgeEnd).normalize();
158
151
  let minX = Infinity, minY = Infinity, minZ = Infinity;
159
152
  let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
160
153
  for (const s of scopeShapes) {
@@ -184,14 +177,9 @@ export class RibOps {
184
177
  return spineWire;
185
178
  }
186
179
  const ext = 2 * diag;
187
- const startExtPoint = firstVertex.add(startTangent.multiply(-ext));
188
- const endExtPoint = lastVertex.add(endTangent.multiply(ext));
189
- const newEdges = [
190
- EdgeOps.makeLineEdge(startExtPoint, firstVertex),
191
- ...edges,
192
- EdgeOps.makeLineEdge(lastVertex, endExtPoint),
193
- ];
194
- return WireOps.makeWireFromEdges(newEdges);
180
+ let extended = WireExtendOps.extendWire(spineWire, "start", ext);
181
+ extended = WireExtendOps.extendWire(extended, "end", ext);
182
+ return extended;
195
183
  }
196
184
  static computeExtrudeDistanceAlongDirection(direction, origin, scopeShapes) {
197
185
  let maxDist = 0;
@@ -0,0 +1,12 @@
1
+ import { Wire } from "../common/wire.js";
2
+ export type WireEnd = "start" | "end";
3
+ export declare class WireExtendOps {
4
+ /**
5
+ * Extends an open wire beyond one end by `length`, continuing straight along
6
+ * the wire's true tangent there. The new straight edge joins the existing
7
+ * first/last vertex, so the result is a single connected wire.
8
+ *
9
+ * No-op for closed wires, empty wires, or `length <= 0`.
10
+ */
11
+ static extendWire(wire: Wire, side: WireEnd, length: number): Wire;
12
+ }
@@ -0,0 +1,42 @@
1
+ import { EdgeOps } from "./edge-ops.js";
2
+ import { WireOps } from "./wire-ops.js";
3
+ import { PathSampler } from "./path-sampler.js";
4
+ export class WireExtendOps {
5
+ /**
6
+ * Extends an open wire beyond one end by `length`, continuing straight along
7
+ * the wire's true tangent there. The new straight edge joins the existing
8
+ * first/last vertex, so the result is a single connected wire.
9
+ *
10
+ * No-op for closed wires, empty wires, or `length <= 0`.
11
+ */
12
+ static extendWire(wire, side, length) {
13
+ if (length <= 0) {
14
+ return wire;
15
+ }
16
+ const edges = wire.getEdges();
17
+ if (edges.length === 0) {
18
+ return wire;
19
+ }
20
+ const sampler = new PathSampler(wire);
21
+ try {
22
+ if (sampler.closed) {
23
+ return wire;
24
+ }
25
+ if (side === "start") {
26
+ // Tangent at the start points into the wire; extend the opposite way.
27
+ const startPt = wire.getFirstVertex().toPoint();
28
+ const inward = sampler.evalAt(0).tangent;
29
+ const extPt = startPt.add(inward.multiply(-length));
30
+ return WireOps.makeWireFromEdges([EdgeOps.makeLineEdge(extPt, startPt), ...edges]);
31
+ }
32
+ // Tangent at the end already points out of the wire.
33
+ const endPt = wire.getLastVertex().toPoint();
34
+ const outward = sampler.evalAt(sampler.length).tangent;
35
+ const extPt = endPt.add(outward.multiply(length));
36
+ return WireOps.makeWireFromEdges([...edges, EdgeOps.makeLineEdge(endPt, extPt)]);
37
+ }
38
+ finally {
39
+ sampler.dispose();
40
+ }
41
+ }
42
+ }
@@ -26,6 +26,17 @@ export declare class WireOps {
26
26
  */
27
27
  static offsetWireOnPlane(wire: Wire, distance: number, isClosed: boolean, plane: Plane): Wire;
28
28
  static isCWRaw(wire: TopoDS_Wire, normal: Vector3d): boolean;
29
+ /**
30
+ * Groups edges into connected chains — one wire per chain — via
31
+ * `ShapeAnalysis_FreeBounds.ConnectEdgesToWires`. Edge ends within
32
+ * `tolerance` of each other connect; disconnected groups come back as
33
+ * separate wires (e.g. a guide sketch holding a curve and its mirror).
34
+ *
35
+ * V8-binding gotcha: the plain-name dispatcher can resolve to the
36
+ * deprecated out-parameter overload, which returns a `{ wires }` envelope
37
+ * instead of the sequence itself — unwrap at runtime.
38
+ */
39
+ static connectEdgesToWires(edges: Edge[], tolerance?: number): Wire[];
29
40
  static makeWireFromEdgesRaw(edges: TopoDS_Edge[]): TopoDS_Wire;
30
41
  static reverseWireRaw(wire: TopoDS_Wire): TopoDS_Wire;
31
42
  static buildWireRaw(edges: TopoDS_Edge[]): TopoDS_Wire;
@@ -70,6 +70,39 @@ export class WireOps {
70
70
  adaptor.delete();
71
71
  return signedArea < 0;
72
72
  }
73
+ /**
74
+ * Groups edges into connected chains — one wire per chain — via
75
+ * `ShapeAnalysis_FreeBounds.ConnectEdgesToWires`. Edge ends within
76
+ * `tolerance` of each other connect; disconnected groups come back as
77
+ * separate wires (e.g. a guide sketch holding a curve and its mirror).
78
+ *
79
+ * V8-binding gotcha: the plain-name dispatcher can resolve to the
80
+ * deprecated out-parameter overload, which returns a `{ wires }` envelope
81
+ * instead of the sequence itself — unwrap at runtime.
82
+ */
83
+ static connectEdgesToWires(edges, tolerance = 1e-6) {
84
+ const oc = getOC();
85
+ const input = new oc.NCollection_HSequence_TopoDS_Shape();
86
+ for (const edge of edges) {
87
+ input.Append(edge.getShape());
88
+ }
89
+ const result = oc.ShapeAnalysis_FreeBounds.ConnectEdgesToWires(input, tolerance, false);
90
+ const isEnvelope = result && typeof result.Sequence !== "function" && result.wires;
91
+ const handle = isEnvelope ? result.wires : result;
92
+ const wires = [];
93
+ const sequence = handle.Sequence();
94
+ for (let i = 1; i <= sequence.Length(); i++) {
95
+ wires.push(Wire.fromTopoDSWire(oc.TopoDS.Wire(sequence.Value(i))));
96
+ }
97
+ input.delete();
98
+ if (isEnvelope && typeof result[Symbol.dispose] === "function") {
99
+ result[Symbol.dispose]();
100
+ }
101
+ else if (typeof handle.delete === "function") {
102
+ handle.delete();
103
+ }
104
+ return wires;
105
+ }
73
106
  static makeWireFromEdgesRaw(edges) {
74
107
  const oc = getOC();
75
108
  const wireMaker = new oc.BRepBuilderAPI_MakeWire();
@@ -4,6 +4,8 @@ import { Explorer } from "./explorer.js";
4
4
  import { FaceQuery } from "./face-query.js";
5
5
  import { WireOps } from "./wire-ops.js";
6
6
  import { ShapeFactory } from "../common/shape-factory.js";
7
+ import { NCollections } from "./ncollection.js";
8
+ import { Point2D } from "../math/point.js";
7
9
  import { ConeDevelopment, CylinderDevelopment, } from "./wrap-development.js";
8
10
  import { interpolateBSpline2d } from "../math/bspline-interpolation.js";
9
11
  /** Sample points per curved sketch edge before fitting its UV pcurve. */
@@ -284,22 +286,13 @@ export class WrapOps {
284
286
  static fitUvCurve(samples) {
285
287
  const oc = getOC();
286
288
  const data = interpolateBSpline2d(samples.map(s => ({ x: s.u, y: s.v })));
287
- const poles = new oc.NCollection_Array1_gp_Pnt2d(1, data.poles.length);
288
- for (let i = 0; i < data.poles.length; i++) {
289
- const point = new oc.gp_Pnt2d(data.poles[i].x, data.poles[i].y);
290
- poles.SetValue(i + 1, point);
291
- point.delete();
292
- }
293
- const knots = new oc.NCollection_Array1_double(1, data.knots.length);
294
- const multiplicities = new oc.NCollection_Array1_int(1, data.knots.length);
295
- for (let i = 0; i < data.knots.length; i++) {
296
- knots.SetValue(i + 1, data.knots[i]);
297
- multiplicities.SetValue(i + 1, data.multiplicities[i]);
298
- }
289
+ const [poles, disposePoles] = NCollections.toArray1Pnt2d(data.poles.map(p => new Point2D(p.x, p.y)));
290
+ const [knots, disposeKnots] = NCollections.toArray1Double(data.knots);
291
+ const [multiplicities, disposeMultiplicities] = NCollections.toArray1Int(data.multiplicities);
299
292
  const curve = new oc.Geom2d_BSplineCurve(poles, knots, multiplicities, data.degree);
300
- poles.delete();
301
- knots.delete();
302
- multiplicities.delete();
293
+ disposePoles();
294
+ disposeKnots();
295
+ disposeMultiplicities();
303
296
  return curve;
304
297
  }
305
298
  /** Thickens the wrapped face along the surface normal into a solid. */
@@ -51,14 +51,4 @@ describe("circle", () => {
51
51
  expect(getFacesByType(solid, "cylinder")).toHaveLength(1);
52
52
  });
53
53
  });
54
- describe("standalone with targetPlane", () => {
55
- it("should create a circle on a specific plane", () => {
56
- circle("xy", 60);
57
- const e = extrude(10);
58
- render();
59
- const solid = e.getShapes()[0];
60
- const bbox = ShapeOps.getBoundingBox(solid);
61
- expect(bbox.maxX - bbox.minX).toBeCloseTo(60, 0);
62
- });
63
- });
64
54
  });
@@ -66,35 +66,5 @@ describe("ellipse", () => {
66
66
  render();
67
67
  expect(negEllipse?.getError()).toMatch(/positive/i);
68
68
  });
69
- it("throws when given a plane inside a sketch", () => {
70
- expect(() => {
71
- sketch("xy", () => {
72
- ellipse("xy", 30, 20);
73
- });
74
- render();
75
- }).toThrow();
76
- });
77
- });
78
- describe("standalone with targetPlane", () => {
79
- it("creates an ellipse on a specific plane", () => {
80
- ellipse("xy", 60, 40);
81
- const e = extrude(10);
82
- render();
83
- const solid = e.getShapes()[0];
84
- const bbox = ShapeOps.getBoundingBox(solid);
85
- expect(bbox.maxX - bbox.minX).toBeCloseTo(120, 0);
86
- expect(bbox.maxY - bbox.minY).toBeCloseTo(80, 0);
87
- });
88
- it("creates an ellipse on a plane at a given center", () => {
89
- ellipse("xy", [10, 20], 30, 15);
90
- const e = extrude(10);
91
- render();
92
- const solid = e.getShapes()[0];
93
- const bbox = ShapeOps.getBoundingBox(solid);
94
- expect(bbox.centerX).toBeCloseTo(10, 0);
95
- expect(bbox.centerY).toBeCloseTo(20, 0);
96
- expect(bbox.maxX - bbox.minX).toBeCloseTo(60, 0);
97
- expect(bbox.maxY - bbox.minY).toBeCloseTo(30, 0);
98
- });
99
69
  });
100
70
  });
@@ -32,11 +32,6 @@ describe("line functions", () => {
32
32
  expect(bbox.maxX - bbox.minX).toBeCloseTo(80, 0);
33
33
  expect(bbox.maxY - bbox.minY).toBeCloseTo(40, 0);
34
34
  });
35
- it("should support standalone mode with targetPlane", () => {
36
- hLine("xy", 50);
37
- render();
38
- // Just verify no error — standalone line doesn't form a closed shape
39
- });
40
35
  });
41
36
  describe("vLine", () => {
42
37
  it("should create a vertical line and form a closed rect", () => {
@@ -159,16 +159,6 @@ describe("offset", () => {
159
159
  expect(shapes).toHaveLength(1);
160
160
  });
161
161
  });
162
- describe("specific targets (standalone)", () => {
163
- it("should offset source geometries onto a target plane", () => {
164
- const s = sketch("xy", () => {
165
- circle(40);
166
- });
167
- offset("xy", 5, false, s);
168
- render();
169
- // Offset edges should exist on the target plane
170
- });
171
- });
172
162
  describe("default distance", () => {
173
163
  it("should use default distance of 1", () => {
174
164
  const s = sketch("xy", () => {
@@ -131,20 +131,4 @@ describe("polygon", () => {
131
131
  expect(bbox.centerY).toBeCloseTo(30, 0);
132
132
  });
133
133
  });
134
- describe("standalone with targetPlane", () => {
135
- it("should create a polygon on a specific plane", () => {
136
- polygon("xy", 5, 50);
137
- const e = extrude(10);
138
- render();
139
- const solid = e.getShapes()[0];
140
- expect(solid.getFaces()).toHaveLength(7);
141
- });
142
- it("should create a circumscribed polygon on a specific plane", () => {
143
- polygon("xy", 6, 60, "circumscribed");
144
- const e = extrude(10);
145
- render();
146
- const solid = e.getShapes()[0];
147
- expect(solid.getFaces()).toHaveLength(8);
148
- });
149
- });
150
134
  });
@@ -52,15 +52,4 @@ describe("rect", () => {
52
52
  expect(solid.getEdges().length).toBeGreaterThan(12);
53
53
  });
54
54
  });
55
- describe("standalone with targetPlane", () => {
56
- it("should create a rectangle on a specific plane", () => {
57
- rect("xy", 80, 40);
58
- const e = extrude(10);
59
- render();
60
- const solid = e.getShapes()[0];
61
- const bbox = ShapeOps.getBoundingBox(solid);
62
- expect(bbox.maxX - bbox.minX).toBeCloseTo(80, 0);
63
- expect(bbox.maxY - bbox.minY).toBeCloseTo(40, 0);
64
- });
65
- });
66
55
  });
@@ -33,12 +33,4 @@ describe("slot", () => {
33
33
  expect(arcEdges.length).toBeGreaterThan(0);
34
34
  });
35
35
  });
36
- describe("standalone with targetPlane", () => {
37
- it("should create a slot on a specific plane", () => {
38
- slot("xy", 60, 10);
39
- const e = extrude(10);
40
- render();
41
- expect(e.getShapes()).toHaveLength(1);
42
- });
43
- });
44
36
  });
@@ -4,7 +4,7 @@ import sketch from "../../core/sketch.js";
4
4
  import extrude from "../../core/extrude.js";
5
5
  import fillet from "../../core/fillet.js";
6
6
  import fuse from "../../core/fuse.js";
7
- import { hMove, rect } from "../../core/2d/index.js";
7
+ import { arc, hMove, rect, vLine, hLine, vMove, back } from "../../core/2d/index.js";
8
8
  import { getEdgesByType } from "../utils.js";
9
9
  describe("fillet2d", () => {
10
10
  setupOC();
@@ -51,6 +51,42 @@ describe("fillet2d", () => {
51
51
  // 4 shortened sides per face (8) + 4 vertical edges each split into segments (8) = 16
52
52
  expect(getEdgesByType(solid, "line")).toHaveLength(16);
53
53
  });
54
+ it("should fillet a corner passed as radius-first spread args", () => {
55
+ const s = sketch("xy", () => {
56
+ const l1 = hLine(40);
57
+ const l2 = vLine(-40);
58
+ fillet(8, l1, l2);
59
+ });
60
+ render();
61
+ // The corner is rounded: each line is shortened and one fillet arc inserted.
62
+ // Before the radius-first spread dispatch existed, fillet(8, l1, l2) was a
63
+ // silent no-op (returned undefined) and produced 0 arcs.
64
+ expect(getEdgesByType(s.getEdges(), "arc")).toHaveLength(1);
65
+ expect(getEdgesByType(s.getEdges(), "line")).toHaveLength(2);
66
+ });
67
+ it("fillets an arc/line corner passed as radius-first spread args", () => {
68
+ // The exact shape from the original bug report: a fillet between an arc (l1)
69
+ // and a line (l2) requested as fillet(radius, l2, l1). Before the radius-first
70
+ // spread form was dispatched, this was a silent no-op while fillet(8) (all
71
+ // corners) rounded the same arc/line corner.
72
+ const s = sketch("front", () => {
73
+ vMove(10);
74
+ const gl = hLine(140).guide().centered();
75
+ back();
76
+ vMove(33 + 16);
77
+ hLine(8);
78
+ vLine(-16);
79
+ hLine(15 - 8);
80
+ const l1 = arc(gl.end()).radius(55);
81
+ const l2 = vLine(-10);
82
+ fillet(8, l2, l1);
83
+ });
84
+ render();
85
+ // Baseline (no fillet) is 1 arc; rounding the arc/line corner shortens the
86
+ // original arc and inserts the fillet arc -> 2 arcs. The array form
87
+ // fillet([l2, l1], 8) produces the identical result.
88
+ expect(getEdgesByType(s.getEdges(), "arc")).toHaveLength(2);
89
+ });
54
90
  });
55
91
  describe("fillet radius", () => {
56
92
  it("should use default radius of 1", () => {
@@ -0,0 +1 @@
1
+ export {};