fluidcad 0.0.37 → 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 (113) 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/core/plane.d.ts +26 -6
  33. package/lib/dist/core/plane.js +21 -44
  34. package/lib/dist/features/2d/connect.d.ts +2 -0
  35. package/lib/dist/features/2d/connect.js +3 -0
  36. package/lib/dist/features/2d/intersect.js +4 -1
  37. package/lib/dist/features/2d/offset.js +2 -2
  38. package/lib/dist/features/frozen-geometry.d.ts +29 -0
  39. package/lib/dist/features/frozen-geometry.js +62 -0
  40. package/lib/dist/features/loft.d.ts +38 -1
  41. package/lib/dist/features/loft.js +133 -8
  42. package/lib/dist/features/plane-from-object.d.ts +16 -4
  43. package/lib/dist/features/plane-from-object.js +101 -8
  44. package/lib/dist/features/rib.js +11 -1
  45. package/lib/dist/features/shell.d.ts +1 -1
  46. package/lib/dist/features/shell.js +9 -0
  47. package/lib/dist/features/sweep.d.ts +11 -1
  48. package/lib/dist/features/sweep.js +43 -7
  49. package/lib/dist/helpers/clone-transform.js +62 -5
  50. package/lib/dist/helpers/scene-helpers.d.ts +12 -0
  51. package/lib/dist/helpers/scene-helpers.js +26 -0
  52. package/lib/dist/math/bspline-interpolation.d.ts +33 -6
  53. package/lib/dist/math/bspline-interpolation.js +187 -84
  54. package/lib/dist/oc/loft/constrained-loft.d.ts +32 -0
  55. package/lib/dist/oc/loft/constrained-loft.js +61 -0
  56. package/lib/dist/oc/loft/curve-data.d.ts +18 -0
  57. package/lib/dist/oc/loft/curve-data.js +54 -0
  58. package/lib/dist/oc/loft/curve-eval.d.ts +23 -0
  59. package/lib/dist/oc/loft/curve-eval.js +84 -0
  60. package/lib/dist/oc/loft/guided-loft.d.ts +89 -0
  61. package/lib/dist/oc/loft/guided-loft.js +368 -0
  62. package/lib/dist/oc/loft/section-compatibility.d.ts +131 -0
  63. package/lib/dist/oc/loft/section-compatibility.js +481 -0
  64. package/lib/dist/oc/loft/section-curve.d.ts +53 -0
  65. package/lib/dist/oc/loft/section-curve.js +264 -0
  66. package/lib/dist/oc/loft/skinning.d.ts +104 -0
  67. package/lib/dist/oc/loft/skinning.js +299 -0
  68. package/lib/dist/oc/loft-ops.d.ts +28 -1
  69. package/lib/dist/oc/loft-ops.js +31 -1
  70. package/lib/dist/oc/ncollection.d.ts +19 -0
  71. package/lib/dist/oc/ncollection.js +69 -0
  72. package/lib/dist/oc/rib-ops.js +5 -17
  73. package/lib/dist/oc/sweep-ops.d.ts +7 -16
  74. package/lib/dist/oc/sweep-ops.js +67 -99
  75. package/lib/dist/oc/thin-face-maker.d.ts +0 -19
  76. package/lib/dist/oc/thin-face-maker.js +3 -68
  77. package/lib/dist/oc/wire-extend-ops.d.ts +12 -0
  78. package/lib/dist/oc/wire-extend-ops.js +42 -0
  79. package/lib/dist/oc/wire-ops.d.ts +28 -2
  80. package/lib/dist/oc/wire-ops.js +88 -4
  81. package/lib/dist/oc/wrap-ops.js +8 -15
  82. package/lib/dist/tests/features/2d/circle.test.js +0 -10
  83. package/lib/dist/tests/features/2d/ellipse.test.js +0 -30
  84. package/lib/dist/tests/features/2d/line.test.js +0 -5
  85. package/lib/dist/tests/features/2d/offset.test.js +74 -11
  86. package/lib/dist/tests/features/2d/polygon.test.js +0 -16
  87. package/lib/dist/tests/features/2d/rect.test.js +0 -11
  88. package/lib/dist/tests/features/2d/slot.test.js +0 -8
  89. package/lib/dist/tests/features/fillet2d.test.js +37 -1
  90. package/lib/dist/tests/features/loft-conditions.test.d.ts +1 -0
  91. package/lib/dist/tests/features/loft-conditions.test.js +246 -0
  92. package/lib/dist/tests/features/loft-guides.test.d.ts +1 -0
  93. package/lib/dist/tests/features/loft-guides.test.js +276 -0
  94. package/lib/dist/tests/features/plane.test.js +95 -0
  95. package/lib/dist/tests/features/repeat-sweep-borrowed-path.test.d.ts +1 -0
  96. package/lib/dist/tests/features/repeat-sweep-borrowed-path.test.js +35 -0
  97. package/lib/dist/tests/features/rib.test.js +50 -0
  98. package/lib/dist/tests/features/shell.test.js +35 -1
  99. package/lib/dist/tests/features/sweep.test.js +127 -1
  100. package/lib/dist/tests/features/text.test.js +33 -14
  101. package/lib/dist/tests/math/bspline-interpolation.test.js +97 -1
  102. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  103. package/llm-docs/api/index.json +1 -1
  104. package/llm-docs/api/types/loft.md +36 -1
  105. package/llm-docs/api/types/sweep.md +13 -1
  106. package/llm-docs/api/types/text.md +2 -2
  107. package/llm-docs/index.json +5 -5
  108. package/package.json +1 -1
  109. package/server/dist/fluidcad-server.d.ts +1 -1
  110. package/server/dist/fluidcad-server.js +11 -1
  111. package/server/dist/routes/params.js +1 -1
  112. package/ui/dist/assets/{index-no7mtr5s.js → index-D8zV21wB.js} +83 -83
  113. package/ui/dist/index.html +1 -1
@@ -9,8 +9,34 @@ export declare class WireOps {
9
9
  static makeWireFromEdges(edges: Edge[]): Wire;
10
10
  static reverseWire(wire: Wire): Wire;
11
11
  static buildWire(edges: Edge[]): Wire;
12
- static offsetWire(shape: Wire | Edge, distance: number, isOpen: boolean): Wire;
12
+ static offsetWire(shape: Wire | Edge, distance: number, isOpen: boolean, plane?: Plane): Wire;
13
+ /**
14
+ * Offsets a wire by `distance` on `plane`, handling open/closed wires and
15
+ * negative distances.
16
+ *
17
+ * The plane is passed as a reference face so BRepOffsetAPI_MakeOffset can
18
+ * resolve an offset direction even for wires with no curvature to imply a
19
+ * plane (e.g. a single straight segment, which otherwise fails outright).
20
+ *
21
+ * For open wires a negative distance offsets to the *opposite* side — the
22
+ * face-based offset does not flip sides on a sign change by itself, so the
23
+ * wire is reversed, offset by the magnitude, then reversed back. For closed
24
+ * wires the plane-less offset is tried first (the path circles rely on, where
25
+ * the sign selects inward/outward) and the face-based offset is the fallback.
26
+ */
27
+ static offsetWireOnPlane(wire: Wire, distance: number, isClosed: boolean, plane: Plane): Wire;
13
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[];
14
40
  static makeWireFromEdgesRaw(edges: TopoDS_Edge[]): TopoDS_Wire;
15
41
  static reverseWireRaw(wire: TopoDS_Wire): TopoDS_Wire;
16
42
  static buildWireRaw(edges: TopoDS_Edge[]): TopoDS_Wire;
@@ -31,5 +57,5 @@ export declare class WireOps {
31
57
  } | null;
32
58
  static groupConnectedEdges(edges: Edge[]): Edge[][];
33
59
  private static verticesMatch;
34
- static offsetWireRaw(wire: TopoDS_Wire, distance: number, isOpen: boolean): TopoDS_Wire;
60
+ static offsetWireRaw(wire: TopoDS_Wire, distance: number, isOpen: boolean, plane?: Plane): TopoDS_Wire;
35
61
  }
@@ -15,9 +15,39 @@ export class WireOps {
15
15
  static buildWire(edges) {
16
16
  return Wire.fromTopoDSWire(WireOps.buildWireRaw(edges.map(e => e.getShape())));
17
17
  }
18
- static offsetWire(shape, distance, isOpen) {
18
+ static offsetWire(shape, distance, isOpen, plane) {
19
19
  const wire = shape instanceof Wire ? shape : WireOps.makeWireFromEdges([shape]);
20
- return Wire.fromTopoDSWire(WireOps.offsetWireRaw(wire.getShape(), distance, isOpen));
20
+ return Wire.fromTopoDSWire(WireOps.offsetWireRaw(wire.getShape(), distance, isOpen, plane));
21
+ }
22
+ /**
23
+ * Offsets a wire by `distance` on `plane`, handling open/closed wires and
24
+ * negative distances.
25
+ *
26
+ * The plane is passed as a reference face so BRepOffsetAPI_MakeOffset can
27
+ * resolve an offset direction even for wires with no curvature to imply a
28
+ * plane (e.g. a single straight segment, which otherwise fails outright).
29
+ *
30
+ * For open wires a negative distance offsets to the *opposite* side — the
31
+ * face-based offset does not flip sides on a sign change by itself, so the
32
+ * wire is reversed, offset by the magnitude, then reversed back. For closed
33
+ * wires the plane-less offset is tried first (the path circles rely on, where
34
+ * the sign selects inward/outward) and the face-based offset is the fallback.
35
+ */
36
+ static offsetWireOnPlane(wire, distance, isClosed, plane) {
37
+ if (!isClosed) {
38
+ if (distance < 0) {
39
+ const reversed = WireOps.reverseWire(wire);
40
+ const offsetResult = WireOps.offsetWire(reversed, -distance, true, plane);
41
+ return WireOps.reverseWire(offsetResult);
42
+ }
43
+ return WireOps.offsetWire(wire, distance, true, plane);
44
+ }
45
+ try {
46
+ return WireOps.offsetWire(wire, distance, false);
47
+ }
48
+ catch {
49
+ return WireOps.offsetWire(wire, distance, false, plane);
50
+ }
21
51
  }
22
52
  static isCWRaw(wire, normal) {
23
53
  const oc = getOC();
@@ -40,6 +70,39 @@ export class WireOps {
40
70
  adaptor.delete();
41
71
  return signedArea < 0;
42
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
+ }
43
106
  static makeWireFromEdgesRaw(edges) {
44
107
  const oc = getOC();
45
108
  const wireMaker = new oc.BRepBuilderAPI_MakeWire();
@@ -193,10 +256,31 @@ export class WireOps {
193
256
  const dz = p1.z - p2.z;
194
257
  return (dx * dx + dy * dy + dz * dz) < 1e-14;
195
258
  }
196
- static offsetWireRaw(wire, distance, isOpen) {
259
+ static offsetWireRaw(wire, distance, isOpen, plane) {
197
260
  const oc = getOC();
198
261
  const maker = new oc.BRepOffsetAPI_MakeOffset();
199
- maker.Init(oc.GeomAbs_JoinType.GeomAbs_Arc, isOpen);
262
+ // BRepOffsetAPI_MakeOffset can't infer an offset direction from a wire with
263
+ // no curvature (e.g. a single straight segment), so the face-less Init
264
+ // fails with "Failed to offset wire". Initializing it with a planar
265
+ // reference face supplies the normal it needs — pass `plane` whenever the
266
+ // offset happens on a known sketch/target plane.
267
+ if (plane) {
268
+ const [pln, disposePlane] = Convert.toGpPln(plane);
269
+ const faceMaker = new oc.BRepBuilderAPI_MakeFace(pln);
270
+ if (!faceMaker.IsDone()) {
271
+ faceMaker.delete();
272
+ disposePlane();
273
+ maker.delete();
274
+ throw new Error("Failed to create reference face for wire offset");
275
+ }
276
+ const face = faceMaker.Face();
277
+ faceMaker.delete();
278
+ disposePlane();
279
+ maker.Init(face, oc.GeomAbs_JoinType.GeomAbs_Arc, isOpen);
280
+ }
281
+ else {
282
+ maker.Init(oc.GeomAbs_JoinType.GeomAbs_Arc, isOpen);
283
+ }
200
284
  maker.AddWire(wire);
201
285
  maker.Perform(distance, 0);
202
286
  if (!maker.IsDone()) {
@@ -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", () => {
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { setupOC, render } from "../../setup.js";
3
3
  import sketch from "../../../core/sketch.js";
4
- import { circle, hLine, vLine, offset } from "../../../core/2d/index.js";
4
+ import { circle, hLine, vLine, aLine, line, offset } from "../../../core/2d/index.js";
5
5
  import { ShapeOps } from "../../../oc/shape-ops.js";
6
6
  describe("offset", () => {
7
7
  setupOC();
@@ -42,6 +42,79 @@ describe("offset", () => {
42
42
  expect(shapes.length).toBeGreaterThan(2);
43
43
  });
44
44
  });
45
+ // Regression: a single straight segment has no curvature to imply an offset
46
+ // plane, so BRepOffsetAPI_MakeOffset needs the sketch plane as a reference
47
+ // face. Without it, the offset failed with "Failed to offset wire" and no
48
+ // offset edge was produced.
49
+ describe("offset single open line", () => {
50
+ const endpoints = (edge) => [edge.getFirstVertex().toPoint(), edge.getLastVertex().toPoint()];
51
+ it("offsets a single hLine to a parallel line at the right distance", () => {
52
+ const s = sketch("xy", () => {
53
+ hLine(100);
54
+ offset(10);
55
+ });
56
+ render();
57
+ const shapes = s.getShapes();
58
+ expect(shapes.length).toBe(2);
59
+ const [a, b] = endpoints(shapes[1]);
60
+ expect(Math.abs(a.y)).toBeCloseTo(10, 6);
61
+ expect(Math.abs(b.y)).toBeCloseTo(10, 6);
62
+ expect(Math.hypot(b.x - a.x, b.y - a.y)).toBeCloseTo(100, 6);
63
+ });
64
+ it("offsets a single vLine to a parallel line at the right distance", () => {
65
+ const s = sketch("xy", () => {
66
+ vLine(100);
67
+ offset(10);
68
+ });
69
+ render();
70
+ const shapes = s.getShapes();
71
+ expect(shapes.length).toBe(2);
72
+ const [a, b] = endpoints(shapes[1]);
73
+ expect(Math.abs(a.x)).toBeCloseTo(10, 6);
74
+ expect(Math.abs(b.x)).toBeCloseTo(10, 6);
75
+ expect(Math.hypot(b.x - a.x, b.y - a.y)).toBeCloseTo(100, 6);
76
+ });
77
+ it("offsets a single aLine, preserving its length", () => {
78
+ const s = sketch("xy", () => {
79
+ aLine(30, 100);
80
+ offset(10);
81
+ });
82
+ render();
83
+ const shapes = s.getShapes();
84
+ expect(shapes.length).toBe(2);
85
+ const [a, b] = endpoints(shapes[1]);
86
+ expect(Math.hypot(b.x - a.x, b.y - a.y)).toBeCloseTo(100, 6);
87
+ });
88
+ it("offsets a single line, preserving its length", () => {
89
+ const s = sketch("xy", () => {
90
+ line([100, 50]);
91
+ offset(10);
92
+ });
93
+ render();
94
+ const shapes = s.getShapes();
95
+ expect(shapes.length).toBe(2);
96
+ const [a, b] = endpoints(shapes[1]);
97
+ expect(Math.hypot(b.x - a.x, b.y - a.y)).toBeCloseTo(Math.hypot(100, 50), 6);
98
+ });
99
+ it("offsets a single line to the opposite side with a negative distance", () => {
100
+ const sPos = sketch("xy", () => {
101
+ hLine(100);
102
+ offset(10);
103
+ });
104
+ render();
105
+ const posY = endpoints(sPos.getShapes()[1])[0].y;
106
+ const sNeg = sketch("xy", () => {
107
+ hLine(100);
108
+ offset(-10);
109
+ });
110
+ render();
111
+ const negShapes = sNeg.getShapes();
112
+ expect(negShapes.length).toBe(2);
113
+ const negY = endpoints(negShapes[1])[0].y;
114
+ expect(Math.abs(negY)).toBeCloseTo(10, 6);
115
+ expect(Math.sign(negY)).toBe(-Math.sign(posY));
116
+ });
117
+ });
45
118
  describe("offset direction", () => {
46
119
  it("positive and negative offsets should produce different results", () => {
47
120
  const s1 = sketch("xy", () => {
@@ -86,16 +159,6 @@ describe("offset", () => {
86
159
  expect(shapes).toHaveLength(1);
87
160
  });
88
161
  });
89
- describe("specific targets (standalone)", () => {
90
- it("should offset source geometries onto a target plane", () => {
91
- const s = sketch("xy", () => {
92
- circle(40);
93
- });
94
- offset("xy", 5, false, s);
95
- render();
96
- // Offset edges should exist on the target plane
97
- });
98
- });
99
162
  describe("default distance", () => {
100
163
  it("should use default distance of 1", () => {
101
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 {};