brepjs 18.125.0 → 18.126.0

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.
package/dist/brepjs.cjs CHANGED
@@ -254,8 +254,8 @@ function repairMesh(mesh, opts, id) {
254
254
  if (require_errors.isErr(engine)) return engine;
255
255
  try {
256
256
  try {
257
- var _usingCtx$8 = require_shapeTypes._usingCtx();
258
- const repaired = _usingCtx$8.u(engine.value.repair_mesh(mesh.vertices, mesh.triangles, resolution, padding));
257
+ var _usingCtx$10 = require_shapeTypes._usingCtx();
258
+ const repaired = _usingCtx$10.u(engine.value.repair_mesh(mesh.vertices, mesh.triangles, resolution, padding));
259
259
  const vertexCount = repaired.positions.length / 3;
260
260
  return require_errors.ok({
261
261
  vertices: repaired.positions,
@@ -269,9 +269,9 @@ function repairMesh(mesh, opts, id) {
269
269
  }]
270
270
  });
271
271
  } catch (_) {
272
- _usingCtx$8.e = _;
272
+ _usingCtx$10.e = _;
273
273
  } finally {
274
- _usingCtx$8.d();
274
+ _usingCtx$10.d();
275
275
  }
276
276
  } catch (cause) {
277
277
  return require_errors.err(require_errors.computationError("VOXEL_REPAIR_FAILED", cause instanceof Error ? cause.message : "voxel repair failed (grid too large?).", cause));
@@ -350,12 +350,12 @@ function offsetMesh(mesh, distance, opts, id) {
350
350
  if (require_errors.isErr(engine)) return engine;
351
351
  try {
352
352
  try {
353
- var _usingCtx$7 = require_shapeTypes._usingCtx();
354
- return meshFromResult(_usingCtx$7.u(engine.value.offset_mesh(mesh.vertices, mesh.triangles, distance, params.value.resolution, params.value.padding)));
353
+ var _usingCtx$9 = require_shapeTypes._usingCtx();
354
+ return meshFromResult(_usingCtx$9.u(engine.value.offset_mesh(mesh.vertices, mesh.triangles, distance, params.value.resolution, params.value.padding)));
355
355
  } catch (_) {
356
- _usingCtx$7.e = _;
356
+ _usingCtx$9.e = _;
357
357
  } finally {
358
- _usingCtx$7.d();
358
+ _usingCtx$9.d();
359
359
  }
360
360
  } catch (cause) {
361
361
  return require_errors.err(require_errors.computationError("VOXEL_OFFSET_FAILED", cause instanceof Error ? cause.message : "voxel offset failed (grid too large?).", cause));
@@ -480,9 +480,9 @@ function resolveGridParams$1(opts) {
480
480
  */
481
481
  function meshFromField(field) {
482
482
  try {
483
- var _usingCtx$6 = require_shapeTypes._usingCtx();
483
+ var _usingCtx$8 = require_shapeTypes._usingCtx();
484
484
  const rawResult = field.contour();
485
- const { positions, normals, indices } = _usingCtx$6.u({
485
+ const { positions, normals, indices } = _usingCtx$8.u({
486
486
  value: rawResult,
487
487
  [Symbol.dispose]() {
488
488
  rawResult.free();
@@ -502,9 +502,9 @@ function meshFromField(field) {
502
502
  }]
503
503
  });
504
504
  } catch (_) {
505
- _usingCtx$6.e = _;
505
+ _usingCtx$8.e = _;
506
506
  } finally {
507
- _usingCtx$6.d();
507
+ _usingCtx$8.d();
508
508
  }
509
509
  }
510
510
  function fieldDeletable(raw) {
@@ -1072,13 +1072,13 @@ function latticeInfill(mesh, opts, id) {
1072
1072
  const { tag, period, thickness, resolution, padding } = resolved.value;
1073
1073
  try {
1074
1074
  try {
1075
- var _usingCtx$5 = require_shapeTypes._usingCtx();
1076
- const repaired = _usingCtx$5.u(engine.value.lattice_infill(mesh.vertices, mesh.triangles, resolution, padding, tag, period, thickness));
1075
+ var _usingCtx$7 = require_shapeTypes._usingCtx();
1076
+ const repaired = _usingCtx$7.u(engine.value.lattice_infill(mesh.vertices, mesh.triangles, resolution, padding, tag, period, thickness));
1077
1077
  return require_errors.ok(toMesh(repaired));
1078
1078
  } catch (_) {
1079
- _usingCtx$5.e = _;
1079
+ _usingCtx$7.e = _;
1080
1080
  } finally {
1081
- _usingCtx$5.d();
1081
+ _usingCtx$7.d();
1082
1082
  }
1083
1083
  } catch (cause) {
1084
1084
  return require_errors.err(require_errors.computationError("LATTICE_INFILL_FAILED", cause instanceof Error ? cause.message : "lattice infill failed (grid too large?).", cause));
@@ -1557,7 +1557,7 @@ function polyhedron(points, faces, options = {}) {
1557
1557
  }
1558
1558
  //#endregion
1559
1559
  //#region src/operations/straightSkeleton.ts
1560
- var EPS = 1e-10;
1560
+ var EPS$1 = 1e-10;
1561
1561
  function cross2(ax, ay, bx, by) {
1562
1562
  return ax * by - ay * bx;
1563
1563
  }
@@ -1595,7 +1595,7 @@ function bisector(poly, i) {
1595
1595
  const e2x = next.x - cur.x;
1596
1596
  const e2y = next.y - cur.y;
1597
1597
  const e2l = len2(e2x, e2y);
1598
- if (e1l < EPS || e2l < EPS) return {
1598
+ if (e1l < EPS$1 || e2l < EPS$1) return {
1599
1599
  dx: 0,
1600
1600
  dy: 0
1601
1601
  };
@@ -1606,14 +1606,14 @@ function bisector(poly, i) {
1606
1606
  let bx = n1x + n2x;
1607
1607
  let by = n1y + n2y;
1608
1608
  const bl = len2(bx, by);
1609
- if (bl < EPS) return {
1609
+ if (bl < EPS$1) return {
1610
1610
  dx: n1x,
1611
1611
  dy: n1y
1612
1612
  };
1613
1613
  bx /= bl;
1614
1614
  by /= bl;
1615
1615
  const cosHalf = dot2(bx, by, n1x, n1y);
1616
- const speed = Math.abs(cosHalf) > EPS ? 1 / cosHalf : 1;
1616
+ const speed = Math.abs(cosHalf) > EPS$1 ? 1 / cosHalf : 1;
1617
1617
  return {
1618
1618
  dx: bx * speed,
1619
1619
  dy: by * speed
@@ -1675,14 +1675,14 @@ function bisectorIntersectTime(a, b) {
1675
1675
  const ddy = a.by - b.by;
1676
1676
  const dxp = b.x - a.x;
1677
1677
  const dyp = b.y - a.y;
1678
- if (Math.abs(ddx) < EPS && Math.abs(ddy) < EPS) return null;
1678
+ if (Math.abs(ddx) < EPS$1 && Math.abs(ddy) < EPS$1) return null;
1679
1679
  let t;
1680
1680
  if (Math.abs(ddx) > Math.abs(ddy)) t = dxp / ddx;
1681
1681
  else t = dyp / ddy;
1682
- if (t < EPS) return null;
1682
+ if (t < EPS$1) return null;
1683
1683
  const otherDd = Math.abs(ddx) > Math.abs(ddy) ? ddy : ddx;
1684
1684
  const otherDp = Math.abs(ddx) > Math.abs(ddy) ? dyp : dxp;
1685
- if (Math.abs(otherDd) > EPS) {
1685
+ if (Math.abs(otherDd) > EPS$1) {
1686
1686
  const t2 = otherDp / otherDd;
1687
1687
  if (Math.abs(t - t2) > 1e-4 * Math.max(1, Math.abs(t))) return null;
1688
1688
  }
@@ -1693,16 +1693,16 @@ function raySplitTime(node, eA, eB) {
1693
1693
  const edx = eB.x - eA.x;
1694
1694
  const edy = eB.y - eA.y;
1695
1695
  const el = len2(edx, edy);
1696
- if (el < EPS) return null;
1696
+ if (el < EPS$1) return null;
1697
1697
  const enx = -edy / el;
1698
1698
  const eny = edx / el;
1699
1699
  const d0 = (node.x - eA.x) * enx + (node.y - eA.y) * eny;
1700
1700
  const relBx = node.bx - (eA.bx + eB.bx) / 2;
1701
1701
  const relBy = node.by - (eA.by + eB.by) / 2;
1702
1702
  const dRate = relBx * enx + relBy * eny;
1703
- if (Math.abs(dRate) < EPS) return null;
1703
+ if (Math.abs(dRate) < EPS$1) return null;
1704
1704
  const t = -d0 / dRate;
1705
- if (t < EPS) return null;
1705
+ if (t < EPS$1) return null;
1706
1706
  const px = node.x + t * node.bx;
1707
1707
  const py = node.y + t * node.by;
1708
1708
  const ax = eA.x + t * eA.bx;
@@ -1712,7 +1712,7 @@ function raySplitTime(node, eA, eB) {
1712
1712
  const segDx = bxx - ax;
1713
1713
  const segDy = byy - ay;
1714
1714
  const segL = len2(segDx, segDy);
1715
- if (segL < EPS) return t;
1715
+ if (segL < EPS$1) return t;
1716
1716
  const s = dot2(px - ax, py - ay, segDx, segDy) / (segL * segL);
1717
1717
  if (s < -.01 || s > 1.01) return null;
1718
1718
  return t;
@@ -1722,7 +1722,7 @@ function computeEvents(lavNodes) {
1722
1722
  for (const node of lavNodes) {
1723
1723
  if (!node.active || !node.next) continue;
1724
1724
  const t = bisectorIntersectTime(node, node.next);
1725
- if (t !== null && t > EPS) {
1725
+ if (t !== null && t > EPS$1) {
1726
1726
  const x = node.x + t * node.bx;
1727
1727
  const y = node.y + t * node.by;
1728
1728
  events.push({
@@ -1739,7 +1739,7 @@ function computeEvents(lavNodes) {
1739
1739
  let count = 0;
1740
1740
  while (cur !== node.prev && cur !== node && count < 1e3) {
1741
1741
  const st = raySplitTime(node, cur, cur.next);
1742
- if (st !== null && st > EPS) {
1742
+ if (st !== null && st > EPS$1) {
1743
1743
  const x = node.x + st * node.bx;
1744
1744
  const y = node.y + st * node.by;
1745
1745
  events.push({
@@ -1795,7 +1795,7 @@ function computeStraightSkeletonImpl(polygon) {
1795
1795
  const b = a.next;
1796
1796
  const c = b.next;
1797
1797
  const t = bisectorIntersectTime(a, b);
1798
- const time = t !== null && t > EPS ? t : 0;
1798
+ const time = t !== null && t > EPS$1 ? t : 0;
1799
1799
  const mx = (a.x + b.x + c.x) / 3 + time * (a.bx + b.bx + c.bx) / 3;
1800
1800
  const my = (a.y + b.y + c.y) / 3 + time * (a.by + b.by + c.by) / 3;
1801
1801
  const nodeIdx = skeletonNodes.length;
@@ -1934,7 +1934,7 @@ function computeStraightSkeletonImpl(polygon) {
1934
1934
  if (!sn) continue;
1935
1935
  const lastVert = faceVerts[faceVerts.length - 1];
1936
1936
  if (!lastVert) continue;
1937
- if (len2(sn.x - lastVert.x, sn.y - lastVert.y) > EPS) {
1937
+ if (len2(sn.x - lastVert.x, sn.y - lastVert.y) > EPS$1) {
1938
1938
  faceVerts.push({
1939
1939
  x: sn.x,
1940
1940
  y: sn.y
@@ -3901,7 +3901,7 @@ function drill(shape, options) {
3901
3901
  */
3902
3902
  function pocket(shape, options) {
3903
3903
  try {
3904
- var _usingCtx$4 = require_shapeTypes._usingCtx();
3904
+ var _usingCtx$6 = require_shapeTypes._usingCtx();
3905
3905
  const s = resolve(shape);
3906
3906
  const { profile, depth } = options;
3907
3907
  if (depth <= 0) return require_errors.err(require_errors.validationError("POCKET_INVALID_DEPTH", "Pocket depth must be positive"));
@@ -3910,7 +3910,7 @@ function pocket(shape, options) {
3910
3910
  const targetFace = targetResult.value;
3911
3911
  const normal = require_faceFns.normalAt(targetFace);
3912
3912
  const center = require_faceFns.faceCenter(targetFace);
3913
- const scope = _usingCtx$4.u(new require_shapeTypes.DisposalScope());
3913
+ const scope = _usingCtx$6.u(new require_shapeTypes.DisposalScope());
3914
3914
  const { wire: w, owned } = toWire(profile);
3915
3915
  if (owned) scope.register(w);
3916
3916
  const faceResult = require_surfaceBuilders.makeFace(w);
@@ -3923,9 +3923,9 @@ function pocket(shape, options) {
3923
3923
  scope.register(toolResult.value);
3924
3924
  return require_solidBuilders.cut(s, toolResult.value, { unsafe: true });
3925
3925
  } catch (_) {
3926
- _usingCtx$4.e = _;
3926
+ _usingCtx$6.e = _;
3927
3927
  } finally {
3928
- _usingCtx$4.d();
3928
+ _usingCtx$6.d();
3929
3929
  }
3930
3930
  }
3931
3931
  /**
@@ -4515,12 +4515,12 @@ function makeInternalGearProfileWire(params) {
4515
4515
  }
4516
4516
  function makeProfileWire(params, isInternal) {
4517
4517
  try {
4518
- var _usingCtx$3 = require_shapeTypes._usingCtx();
4518
+ var _usingCtx$5 = require_shapeTypes._usingCtx();
4519
4519
  const { teeth, moduleSize, pressureAngle, shift, clearance, flankThinning, samples = adaptiveSampleCount(moduleSize) } = params;
4520
4520
  if (teeth < 4) return require_errors.err(require_errors.validationError("GEAR_TEETH_TOO_FEW", `gear needs ≥ 4 teeth, got ${teeth}`));
4521
4521
  if (moduleSize <= 0) return require_errors.err(require_errors.validationError("GEAR_MODULE_NONPOSITIVE", `module must be > 0, got ${moduleSize}`));
4522
4522
  const tm = gearGeometry(teeth, moduleSize, pressureAngle, shift, clearance, flankThinning, isInternal);
4523
- const scope = _usingCtx$3.u(new require_shapeTypes.DisposalScope());
4523
+ const scope = _usingCtx$5.u(new require_shapeTypes.DisposalScope());
4524
4524
  const allEdges = [];
4525
4525
  for (let i = 0; i < teeth; i++) {
4526
4526
  const periodEdges = buildToothPeriodEdges(tm, i, teeth, samples, scope);
@@ -4531,9 +4531,9 @@ function makeProfileWire(params, isInternal) {
4531
4531
  if (require_errors.isErr(wireResult)) return wireResult;
4532
4532
  return require_errors.ok(wireResult.value);
4533
4533
  } catch (_) {
4534
- _usingCtx$3.e = _;
4534
+ _usingCtx$5.e = _;
4535
4535
  } finally {
4536
- _usingCtx$3.d();
4536
+ _usingCtx$5.d();
4537
4537
  }
4538
4538
  }
4539
4539
  //#endregion
@@ -4714,8 +4714,8 @@ function resolvePlanetaryParams(params) {
4714
4714
  }
4715
4715
  function makeOuterCircleWire(radius) {
4716
4716
  try {
4717
- var _usingCtx$2 = require_shapeTypes._usingCtx();
4718
- const circleEdge = _usingCtx$2.u(new require_shapeTypes.DisposalScope()).register(require_surfaceBuilders.makeCircle(radius, [
4717
+ var _usingCtx$4 = require_shapeTypes._usingCtx();
4718
+ const circleEdge = _usingCtx$4.u(new require_shapeTypes.DisposalScope()).register(require_surfaceBuilders.makeCircle(radius, [
4719
4719
  0,
4720
4720
  0,
4721
4721
  0
@@ -4728,9 +4728,9 @@ function makeOuterCircleWire(radius) {
4728
4728
  if (require_errors.isErr(wireResult)) return wireResult;
4729
4729
  return require_errors.ok(wireResult.value);
4730
4730
  } catch (_) {
4731
- _usingCtx$2.e = _;
4731
+ _usingCtx$4.e = _;
4732
4732
  } finally {
4733
- _usingCtx$2.d();
4733
+ _usingCtx$4.d();
4734
4734
  }
4735
4735
  }
4736
4736
  function finalizeExternalSolid(wire, thickness, bore, geom, diagnostics) {
@@ -5327,6 +5327,14 @@ function evalVec3(expr, env, where) {
5327
5327
  c
5328
5328
  ]);
5329
5329
  }
5330
+ function evalVec2(expr, env, where) {
5331
+ const r = evalExpr(expr, env);
5332
+ if (!r.ok) return r;
5333
+ const v = expectVecLen(r.value, 2, where);
5334
+ if (!v.ok) return v;
5335
+ const [a, b] = v.value;
5336
+ return require_errors.ok([a, b]);
5337
+ }
5330
5338
  function projectEnv(env, deps) {
5331
5339
  if (deps.size === 0) return Object.freeze({});
5332
5340
  const out = {};
@@ -5337,6 +5345,91 @@ function projectEnv(env, deps) {
5337
5345
  return Object.freeze(out);
5338
5346
  }
5339
5347
  //#endregion
5348
+ //#region src/csg/segments.ts
5349
+ /**
5350
+ * Serializable 2D segment vocabulary — endpoint-parametrized (SVG-style)
5351
+ * curve segments shared by the Path node (open spines) and, later, Profile
5352
+ * contours. Positions and radii are expressions, so `freeParams` propagates
5353
+ * and segment data hashes with the same FNV machinery as every node field.
5354
+ */
5355
+ function lineTo(to) {
5356
+ return {
5357
+ kind: "Line",
5358
+ to: asVec2Expr(to)
5359
+ };
5360
+ }
5361
+ function arcTo(to, radius, options) {
5362
+ return {
5363
+ kind: "Arc",
5364
+ to: asVec2Expr(to),
5365
+ radius: asScalarExpr(radius),
5366
+ largeArc: options?.largeArc ?? false,
5367
+ clockwise: options?.clockwise ?? false
5368
+ };
5369
+ }
5370
+ function bezierTo(controls, to) {
5371
+ return {
5372
+ kind: "Bezier",
5373
+ controls: controls.map(asVec2Expr),
5374
+ to: asVec2Expr(to)
5375
+ };
5376
+ }
5377
+ function ellipseArcTo(to, radii, options) {
5378
+ return {
5379
+ kind: "EllipseArc",
5380
+ to: asVec2Expr(to),
5381
+ radii: asVec2Expr(radii),
5382
+ rotation: asScalarExpr(options?.rotation ?? 0),
5383
+ largeArc: options?.largeArc ?? false,
5384
+ clockwise: options?.clockwise ?? false
5385
+ };
5386
+ }
5387
+ function hashSegments(h0, segments) {
5388
+ let h = fnvMixInt32(h0, segments.length);
5389
+ const mix = (acc, e) => fnvMixHash(acc, e.structuralHash);
5390
+ for (const s of segments) {
5391
+ h = fnvMixString(h, s.kind);
5392
+ h = mix(h, s.to);
5393
+ if (s.kind === "Arc") {
5394
+ h = mix(h, s.radius);
5395
+ h = fnvMixBool(fnvMixBool(h, s.largeArc), s.clockwise);
5396
+ } else if (s.kind === "Bezier") {
5397
+ h = fnvMixInt32(h, s.controls.length);
5398
+ for (const c of s.controls) h = mix(h, c);
5399
+ } else if (s.kind === "EllipseArc") {
5400
+ h = mix(h, s.radii);
5401
+ h = mix(h, s.rotation);
5402
+ h = fnvMixBool(fnvMixBool(h, s.largeArc), s.clockwise);
5403
+ }
5404
+ }
5405
+ return h;
5406
+ }
5407
+ function segmentFreeParams(segments) {
5408
+ const out = [];
5409
+ for (const s of segments) {
5410
+ out.push(s.to);
5411
+ if (s.kind === "Arc") out.push(s.radius);
5412
+ else if (s.kind === "Bezier") out.push(...s.controls);
5413
+ else if (s.kind === "EllipseArc") out.push(s.radii, s.rotation);
5414
+ }
5415
+ return out;
5416
+ }
5417
+ function foldSegment(s, foldExpr) {
5418
+ switch (s.kind) {
5419
+ case "Line": return lineTo(foldExpr(s.to));
5420
+ case "Arc": return arcTo(foldExpr(s.to), foldExpr(s.radius), {
5421
+ largeArc: s.largeArc,
5422
+ clockwise: s.clockwise
5423
+ });
5424
+ case "Bezier": return bezierTo(s.controls.map(foldExpr), foldExpr(s.to));
5425
+ case "EllipseArc": return ellipseArcTo(foldExpr(s.to), foldExpr(s.radii), {
5426
+ rotation: foldExpr(s.rotation),
5427
+ largeArc: s.largeArc,
5428
+ clockwise: s.clockwise
5429
+ });
5430
+ }
5431
+ }
5432
+ //#endregion
5340
5433
  //#region src/csg/builders.ts
5341
5434
  var EMPTY_DEPS = /* @__PURE__ */ new Set();
5342
5435
  function startHash(tag) {
@@ -5513,29 +5606,31 @@ function intersect$1(a, b, tolerance) {
5513
5606
  };
5514
5607
  }
5515
5608
  function fuseAll$1(shapes, tolerance) {
5516
- let h = fnvMixInt32(startHash("FuseAll"), shapes.length);
5517
- for (const s of shapes) h = mix(h, s);
5609
+ const copied = [...shapes];
5610
+ let h = fnvMixInt32(startHash("FuseAll"), copied.length);
5611
+ for (const s of copied) h = mix(h, s);
5518
5612
  h = mixOptNumber(h, tolerance);
5519
5613
  return {
5520
5614
  kind: "FuseAll",
5521
- shapes,
5615
+ shapes: copied,
5522
5616
  tolerance,
5523
5617
  structuralHash: h,
5524
- freeParams: depsOf(...shapes)
5618
+ freeParams: depsOf(...copied)
5525
5619
  };
5526
5620
  }
5527
5621
  function cutAll$1(base, tools, tolerance) {
5622
+ const copied = [...tools];
5528
5623
  let h = mix(startHash("CutAll"), base);
5529
- h = fnvMixInt32(h, tools.length);
5530
- for (const t of tools) h = mix(h, t);
5624
+ h = fnvMixInt32(h, copied.length);
5625
+ for (const t of copied) h = mix(h, t);
5531
5626
  h = mixOptNumber(h, tolerance);
5532
5627
  return {
5533
5628
  kind: "CutAll",
5534
5629
  base,
5535
- tools,
5630
+ tools: copied,
5536
5631
  tolerance,
5537
5632
  structuralHash: h,
5538
- freeParams: depsOf(base, ...tools)
5633
+ freeParams: depsOf(base, ...copied)
5539
5634
  };
5540
5635
  }
5541
5636
  function optVec3$1(v) {
@@ -5607,14 +5702,66 @@ function extrude$1(profile, vector) {
5607
5702
  freeParams: depsOf(profile, ve)
5608
5703
  };
5609
5704
  }
5705
+ /** Open (or incidentally closed) planar path in the XY plane at z = 0,
5706
+ * producing a Wire. Segments come from `lineTo`/`arcTo`/`bezierTo`/
5707
+ * `ellipseArcTo`. */
5708
+ function path(start, segments) {
5709
+ const se = asVec2Expr(start);
5710
+ const copied = [...segments];
5711
+ let h = mix(startHash("Path"), se);
5712
+ h = hashSegments(h, copied);
5713
+ return {
5714
+ kind: "Path",
5715
+ start: se,
5716
+ segments: copied,
5717
+ structuralHash: h,
5718
+ freeParams: depsOf(se, ...segmentFreeParams(copied))
5719
+ };
5720
+ }
5721
+ /** Loft through two or more face-producing sections (default ruled). */
5722
+ function loft$1(sections, options) {
5723
+ const copied = [...sections];
5724
+ const ruled = options?.ruled ?? true;
5725
+ let h = fnvMixInt32(startHash("Loft"), copied.length);
5726
+ for (const s of copied) h = mix(h, s);
5727
+ h = fnvMixBool(h, ruled);
5728
+ return {
5729
+ kind: "Loft",
5730
+ sections: copied,
5731
+ ruled,
5732
+ structuralHash: h,
5733
+ freeParams: depsOf(...copied)
5734
+ };
5735
+ }
5736
+ /** Angle is in degrees (matching `rotate`); defaults to a full revolution
5737
+ * around the Z axis at the origin. Evaluation clamps angles above 360 to one
5738
+ * revolution (kernels diverge past 2*pi) and rejects non-positive angles. */
5739
+ function revolve$1(profile, angle = 360, options) {
5740
+ const ae = asScalarExpr(angle);
5741
+ const axisE = optVec3$1(options?.axis);
5742
+ const atE = optVec3$1(options?.at);
5743
+ let h = mix(mix(startHash("Revolve"), profile), ae);
5744
+ h = mixOptExpr(h, axisE);
5745
+ h = mixOptExpr(h, atE);
5746
+ return {
5747
+ kind: "Revolve",
5748
+ profile,
5749
+ angle: ae,
5750
+ axis: axisE,
5751
+ at: atE,
5752
+ structuralHash: h,
5753
+ freeParams: depsOf(profile, ae, axisE, atE)
5754
+ };
5755
+ }
5610
5756
  function compound$1(children) {
5611
- let h = fnvMixInt32(startHash("Compound"), children.length);
5612
- for (const c of children) h = mix(h, c);
5757
+ const copied = [...children];
5758
+ let h = fnvMixInt32(startHash("Compound"), copied.length);
5759
+ for (const c of copied) h = mix(h, c);
5613
5760
  return {
5614
5761
  kind: "Compound",
5615
- children,
5762
+ children: copied,
5616
5763
  structuralHash: h,
5617
- freeParams: depsOf(...children)
5764
+ freeParams: depsOf(...copied)
5618
5765
  };
5619
5766
  }
5620
5767
  function instance$1(source, placements, fuse = false) {
@@ -5653,7 +5800,10 @@ function outputKindOf(node) {
5653
5800
  case "Rotate":
5654
5801
  case "Scale":
5655
5802
  case "Mirror": return outputKindOf(node.target);
5656
- case "Extrude": return "Solid";
5803
+ case "Extrude":
5804
+ case "Revolve":
5805
+ case "Loft": return "Solid";
5806
+ case "Path": return "Wire";
5657
5807
  case "Compound": return "Compound";
5658
5808
  case "Instance": return node.fuse ? "Solid" : "Compound";
5659
5809
  }
@@ -5933,17 +6083,289 @@ function evalInstance(node, ctx) {
5933
6083
  return materialize(instance(r.value, node.placements), { fuse: node.fuse });
5934
6084
  }
5935
6085
  //#endregion
6086
+ //#region src/csg/evaluators/profileFace.ts
6087
+ /** Evaluate a profile child and prove it is an oriented planar face — the
6088
+ * input contract shared by every profile-consuming feature node. */
6089
+ function resolveProfileFace(ctx, profile, where) {
6090
+ const p = ctx.evalNode(profile);
6091
+ if (!p.ok) return p;
6092
+ if (!require_shapeTypes.isFace(p.value)) return require_errors.err(require_errors.typeCastError("CSG_NOT_FACE", `${where}: node did not produce a Face`));
6093
+ const oriented = require_validityTypes.orientedFace(p.value);
6094
+ if (!oriented.ok) return require_errors.err(require_errors.typeCastError("CSG_NOT_FACE", `${where}: ${oriented.error}`));
6095
+ if (!require_validityTypes.isPlanarFace(oriented.value)) return require_errors.err(require_errors.typeCastError("CSG_NOT_FACE", `${where}: face is not planar`));
6096
+ return require_errors.ok(oriented.value);
6097
+ }
6098
+ //#endregion
5936
6099
  //#region src/csg/evaluators/extrude.ts
5937
6100
  function evalExtrude(node, ctx) {
5938
6101
  const v = evalVec3(node.vector, ctx.env, "Extrude.vector");
5939
6102
  if (!v.ok) return v;
5940
- const p = ctx.evalNode(node.profile);
6103
+ const face = resolveProfileFace(ctx, node.profile, "Extrude.profile");
6104
+ if (!face.ok) return face;
6105
+ return require_textBlueprints.extrude(face.value, v.value);
6106
+ }
6107
+ //#endregion
6108
+ //#region src/csg/evaluators/revolve.ts
6109
+ function evalRevolve(node, ctx) {
6110
+ const a = evalScalar(node.angle, ctx.env, "Revolve.angle");
6111
+ if (!a.ok) return a;
6112
+ if (a.value <= 0) return require_errors.err(require_errors.validationError("CSG_REVOLVE_ANGLE", `Revolve.angle must be positive (degrees), got ${a.value}`));
6113
+ const deg = Math.min(a.value, 360);
6114
+ const axis = node.axis ? evalVec3(node.axis, ctx.env, "Revolve.axis") : require_errors.ok([
6115
+ 0,
6116
+ 0,
6117
+ 1
6118
+ ]);
6119
+ if (!axis.ok) return axis;
6120
+ const at = node.at ? evalVec3(node.at, ctx.env, "Revolve.at") : require_errors.ok([
6121
+ 0,
6122
+ 0,
6123
+ 0
6124
+ ]);
6125
+ if (!at.ok) return at;
6126
+ const face = resolveProfileFace(ctx, node.profile, "Revolve.profile");
6127
+ if (!face.ok) return face;
6128
+ return require_textBlueprints.revolve(face.value, at.value, axis.value, deg * Math.PI / 180);
6129
+ }
6130
+ //#endregion
6131
+ //#region src/csg/evaluators/loft.ts
6132
+ function evalLoft(node, ctx) {
6133
+ try {
6134
+ var _usingCtx$3 = require_shapeTypes._usingCtx();
6135
+ if (node.sections.length < 2) return require_errors.err(require_errors.validationError("CSG_LOFT_SECTIONS", `Loft requires at least 2 sections, got ${node.sections.length}`));
6136
+ const scope = _usingCtx$3.u(new require_shapeTypes.DisposalScope());
6137
+ const wires = [];
6138
+ for (const [i, section] of node.sections.entries()) {
6139
+ const face = resolveProfileFace(ctx, section, `Loft.sections[${i}]`);
6140
+ if (!face.ok) return face;
6141
+ const w = require_faceFns.outerWire(face.value);
6142
+ scope.register(w);
6143
+ wires.push(w);
6144
+ }
6145
+ return require_loftFns.loft(wires, { ruled: node.ruled });
6146
+ } catch (_) {
6147
+ _usingCtx$3.e = _;
6148
+ } finally {
6149
+ _usingCtx$3.d();
6150
+ }
6151
+ }
6152
+ //#endregion
6153
+ //#region src/csg/evaluators/path.ts
6154
+ var EPS = 1e-9;
6155
+ function v3(p) {
6156
+ return [
6157
+ p[0],
6158
+ p[1],
6159
+ 0
6160
+ ];
6161
+ }
6162
+ function fail(msg) {
6163
+ return require_errors.err(require_errors.validationError("CSG_PATH_SEGMENT", `Path: ${msg}`));
6164
+ }
6165
+ /** SVG-style endpoint circular arc: recover the on-arc midpoint so the edge
6166
+ * can be built as a three-point arc. */
6167
+ function circularArcMidpoint(from, to, r, largeArc, clockwise) {
6168
+ const [x1, y1] = from;
6169
+ const [x2, y2] = to;
6170
+ const dx = x2 - x1;
6171
+ const dy = y2 - y1;
6172
+ const d = Math.hypot(dx, dy);
6173
+ if (d < EPS) return fail("Arc: coincident endpoints");
6174
+ if (d > 2 * r + 1e-6) return fail(`Arc: radius ${r} too small for chord ${d}`);
6175
+ const h = Math.sqrt(Math.max(0, r * r - d / 2 * (d / 2)));
6176
+ const ux = dx / d;
6177
+ const uy = dy / d;
6178
+ const sign = clockwise === largeArc ? 1 : -1;
6179
+ const cx = (x1 + x2) / 2 + sign * h * -uy;
6180
+ const cy = (y1 + y2) / 2 + sign * h * ux;
6181
+ const a0 = Math.atan2(y1 - cy, x1 - cx);
6182
+ let a1 = Math.atan2(y2 - cy, x2 - cx);
6183
+ if (clockwise) while (a1 >= a0 - EPS) a1 -= 2 * Math.PI;
6184
+ else while (a1 <= a0 + EPS) a1 += 2 * Math.PI;
6185
+ const amid = (a0 + a1) / 2;
6186
+ return require_errors.ok([cx + r * Math.cos(amid), cy + r * Math.sin(amid)]);
6187
+ }
6188
+ /** SVG F.6.5 endpoint-to-center conversion (math y-up; sweep = !clockwise).
6189
+ * Angles are relative to the rotated x-axis. */
6190
+ function ellipseArcParams(from, to, radii, phi, largeArc, clockwise) {
6191
+ const sweep = !clockwise;
6192
+ let [rx, ry] = radii;
6193
+ if (rx <= 0 || ry <= 0) return fail("EllipseArc: non-positive radius");
6194
+ const cosPhi = Math.cos(phi);
6195
+ const sinPhi = Math.sin(phi);
6196
+ const hx = (from[0] - to[0]) / 2;
6197
+ const hy = (from[1] - to[1]) / 2;
6198
+ const x1p = cosPhi * hx + sinPhi * hy;
6199
+ const y1p = -sinPhi * hx + cosPhi * hy;
6200
+ const lambda = x1p * x1p / (rx * rx) + y1p * y1p / (ry * ry);
6201
+ if (lambda > 1) {
6202
+ const s = Math.sqrt(lambda);
6203
+ rx *= s;
6204
+ ry *= s;
6205
+ }
6206
+ const num = rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p;
6207
+ const den = rx * rx * y1p * y1p + ry * ry * x1p * x1p;
6208
+ if (den < EPS) return fail("EllipseArc: coincident endpoints");
6209
+ const coef = (largeArc !== sweep ? 1 : -1) * Math.sqrt(Math.max(0, num / den));
6210
+ const cxp = coef * (rx * y1p / ry);
6211
+ const cyp = coef * (-(ry * x1p) / rx);
6212
+ const cx = cosPhi * cxp - sinPhi * cyp + (from[0] + to[0]) / 2;
6213
+ const cy = sinPhi * cxp + cosPhi * cyp + (from[1] + to[1]) / 2;
6214
+ const theta1 = Math.atan2((y1p - cyp) / ry, (x1p - cxp) / rx);
6215
+ let dTheta = Math.atan2((-y1p - cyp) / ry, (-x1p - cxp) / rx) - theta1;
6216
+ if (!sweep && dTheta > 0) dTheta -= 2 * Math.PI;
6217
+ if (sweep && dTheta < 0) dTheta += 2 * Math.PI;
6218
+ const t1 = dTheta >= 0 ? theta1 : theta1 + dTheta;
6219
+ const t2 = dTheta >= 0 ? theta1 + dTheta : theta1;
6220
+ return require_errors.ok({
6221
+ center: [cx, cy],
6222
+ theta1: t1,
6223
+ theta2: t2,
6224
+ rx,
6225
+ ry
6226
+ });
6227
+ }
6228
+ function ellipseEdge(from, seg, ctx) {
6229
+ if (seg.kind !== "EllipseArc") return fail("internal: not an ellipse segment");
6230
+ const to = evalVec2(seg.to, ctx.env, "Path.EllipseArc.to");
6231
+ if (!to.ok) return to;
6232
+ const radii = evalVec2(seg.radii, ctx.env, "Path.EllipseArc.radii");
6233
+ if (!radii.ok) return radii;
6234
+ const rotDeg = evalScalar(seg.rotation, ctx.env, "Path.EllipseArc.rotation");
6235
+ if (!rotDeg.ok) return rotDeg;
6236
+ const phi = rotDeg.value * Math.PI / 180;
6237
+ const p = ellipseArcParams(from, to.value, radii.value, phi, seg.largeArc, seg.clockwise);
5941
6238
  if (!p.ok) return p;
5942
- if (!require_shapeTypes.isFace(p.value)) return require_errors.err(require_errors.typeCastError("CSG_NOT_FACE", "Extrude.profile: node did not produce a Face"));
5943
- const oriented = require_validityTypes.orientedFace(p.value);
5944
- if (!oriented.ok) return require_errors.err(require_errors.typeCastError("CSG_NOT_FACE", `Extrude.profile: ${oriented.error}`));
5945
- if (!require_validityTypes.isPlanarFace(oriented.value)) return require_errors.err(require_errors.typeCastError("CSG_NOT_FACE", "Extrude.profile: face is not planar"));
5946
- return require_textBlueprints.extrude(oriented.value, v.value);
6239
+ const { center, theta1, theta2, rx, ry } = p.value;
6240
+ let arc;
6241
+ let psi;
6242
+ if (rx >= ry) {
6243
+ arc = require_surfaceBuilders.makeEllipseArc(rx, ry, theta1, theta2, v3(center), [
6244
+ 0,
6245
+ 0,
6246
+ 1
6247
+ ]);
6248
+ psi = phi;
6249
+ } else {
6250
+ arc = require_surfaceBuilders.makeEllipseArc(ry, rx, theta1 - Math.PI / 2, theta2 - Math.PI / 2, v3(center), [
6251
+ 0,
6252
+ 0,
6253
+ 1
6254
+ ]);
6255
+ psi = phi + Math.PI / 2;
6256
+ }
6257
+ if (!arc.ok) return arc;
6258
+ let edge = arc.value;
6259
+ if (Math.abs(psi) > EPS) try {
6260
+ var _usingCtx$2 = require_shapeTypes._usingCtx();
6261
+ const rotated = rotateEdgeAbout(_usingCtx$2.u(edge), psi * 180 / Math.PI, center);
6262
+ if (!rotated.ok) return rotated;
6263
+ edge = rotated.value;
6264
+ } catch (_) {
6265
+ _usingCtx$2.e = _;
6266
+ } finally {
6267
+ _usingCtx$2.d();
6268
+ }
6269
+ if (seg.clockwise) try {
6270
+ var _usingCtx3 = require_shapeTypes._usingCtx();
6271
+ const tmp = _usingCtx3.u(edge);
6272
+ edge = require_curveFns.flipOrientation(tmp);
6273
+ } catch (_) {
6274
+ _usingCtx3.e = _;
6275
+ } finally {
6276
+ _usingCtx3.d();
6277
+ }
6278
+ return require_errors.ok(edge);
6279
+ }
6280
+ /** Rotate an edge about the Z axis at `center` (degrees) via a cheap location
6281
+ * re-tag; returns a fresh, independently-disposable handle. Kernels without
6282
+ * an edge-relocation path (brepkit) surface as a Result error, not a throw. */
6283
+ function rotateEdgeAbout(edge, angleDeg, center) {
6284
+ const kernel = require_shapeTypes.getKernel();
6285
+ const { handle, dispose } = kernel.composeTransform([{
6286
+ type: "rotate",
6287
+ angle: angleDeg,
6288
+ axis: [
6289
+ 0,
6290
+ 0,
6291
+ 1
6292
+ ],
6293
+ center: [
6294
+ center[0],
6295
+ center[1],
6296
+ 0
6297
+ ]
6298
+ }]);
6299
+ try {
6300
+ return require_errors.ok(require_shapeTypes.castShape(kernel.locate(edge.wrapped, handle)));
6301
+ } catch (e) {
6302
+ return require_errors.err(require_errors.kernelError("CSG_PATH_ELLIPSE_ROTATE", "Path: kernel cannot relocate ellipse-arc edges", e, { operation: "evalPath" }));
6303
+ } finally {
6304
+ dispose();
6305
+ }
6306
+ }
6307
+ function segmentEnd(seg, ctx) {
6308
+ return evalVec2(seg.to, ctx.env, `Path.${seg.kind}.to`);
6309
+ }
6310
+ function segmentToEdge(from, seg, ctx) {
6311
+ switch (seg.kind) {
6312
+ case "Line": {
6313
+ const to = segmentEnd(seg, ctx);
6314
+ if (!to.ok) return to;
6315
+ if (Math.hypot(to.value[0] - from[0], to.value[1] - from[1]) < EPS) return fail("Line: zero length");
6316
+ return require_errors.ok(require_surfaceBuilders.makeLine(v3(from), v3(to.value)));
6317
+ }
6318
+ case "Arc": {
6319
+ const to = segmentEnd(seg, ctx);
6320
+ if (!to.ok) return to;
6321
+ const r = evalScalar(seg.radius, ctx.env, "Path.Arc.radius");
6322
+ if (!r.ok) return r;
6323
+ const mid = circularArcMidpoint(from, to.value, r.value, seg.largeArc, seg.clockwise);
6324
+ if (!mid.ok) return mid;
6325
+ return require_errors.ok(require_surfaceBuilders.makeThreePointArc(v3(from), v3(mid.value), v3(to.value)));
6326
+ }
6327
+ case "Bezier": {
6328
+ const to = segmentEnd(seg, ctx);
6329
+ if (!to.ok) return to;
6330
+ const controls = [];
6331
+ for (const c of seg.controls) {
6332
+ const cv = evalVec2(c, ctx.env, "Path.Bezier.control");
6333
+ if (!cv.ok) return cv;
6334
+ controls.push(v3(cv.value));
6335
+ }
6336
+ return require_surfaceBuilders.makeBezierCurve([
6337
+ v3(from),
6338
+ ...controls,
6339
+ v3(to.value)
6340
+ ]);
6341
+ }
6342
+ case "EllipseArc": return ellipseEdge(from, seg, ctx);
6343
+ }
6344
+ }
6345
+ function evalPath(node, ctx) {
6346
+ try {
6347
+ var _usingCtx4 = require_shapeTypes._usingCtx();
6348
+ if (node.segments.length === 0) return fail("at least one segment required");
6349
+ const start = evalVec2(node.start, ctx.env, "Path.start");
6350
+ if (!start.ok) return start;
6351
+ const scope = _usingCtx4.u(new require_shapeTypes.DisposalScope());
6352
+ const edges = [];
6353
+ let cur = start.value;
6354
+ for (const seg of node.segments) {
6355
+ const e = segmentToEdge(cur, seg, ctx);
6356
+ if (!e.ok) return e;
6357
+ scope.register(e.value);
6358
+ edges.push(e.value);
6359
+ const end = segmentEnd(seg, ctx);
6360
+ if (!end.ok) return end;
6361
+ cur = end.value;
6362
+ }
6363
+ return require_surfaceBuilders.assembleWire(edges);
6364
+ } catch (_) {
6365
+ _usingCtx4.e = _;
6366
+ } finally {
6367
+ _usingCtx4.d();
6368
+ }
5947
6369
  }
5948
6370
  //#endregion
5949
6371
  //#region src/csg/evaluate.ts
@@ -5971,6 +6393,9 @@ function dispatch(node, ctx) {
5971
6393
  case "Compound": return evalCompound(node, ctx);
5972
6394
  case "Instance": return evalInstance(node, ctx);
5973
6395
  case "Extrude": return evalExtrude(node, ctx);
6396
+ case "Revolve": return evalRevolve(node, ctx);
6397
+ case "Loft": return evalLoft(node, ctx);
6398
+ case "Path": return evalPath(node, ctx);
5974
6399
  }
5975
6400
  }
5976
6401
  function hashExprValue(h, v) {
@@ -6507,6 +6932,34 @@ function booleanToJson(n) {
6507
6932
  function optExprToJson(e) {
6508
6933
  return e ? exprToJson(e) : void 0;
6509
6934
  }
6935
+ function segmentToJson(s) {
6936
+ switch (s.kind) {
6937
+ case "Line": return {
6938
+ kind: "Line",
6939
+ to: exprToJson(s.to)
6940
+ };
6941
+ case "Arc": return {
6942
+ kind: "Arc",
6943
+ to: exprToJson(s.to),
6944
+ radius: exprToJson(s.radius),
6945
+ largeArc: s.largeArc,
6946
+ clockwise: s.clockwise
6947
+ };
6948
+ case "Bezier": return {
6949
+ kind: "Bezier",
6950
+ controls: s.controls.map(exprToJson),
6951
+ to: exprToJson(s.to)
6952
+ };
6953
+ case "EllipseArc": return {
6954
+ kind: "EllipseArc",
6955
+ to: exprToJson(s.to),
6956
+ radii: exprToJson(s.radii),
6957
+ rotation: exprToJson(s.rotation),
6958
+ largeArc: s.largeArc,
6959
+ clockwise: s.clockwise
6960
+ };
6961
+ }
6962
+ }
6510
6963
  function transformToJson(n) {
6511
6964
  switch (n.kind) {
6512
6965
  case "Translate": return {
@@ -6542,6 +6995,23 @@ function nodeToJson(n) {
6542
6995
  profile: nodeToJson(n.profile),
6543
6996
  vector: exprToJson(n.vector)
6544
6997
  };
6998
+ if (n.kind === "Revolve") return {
6999
+ kind: "Revolve",
7000
+ profile: nodeToJson(n.profile),
7001
+ angle: exprToJson(n.angle),
7002
+ axis: optExprToJson(n.axis),
7003
+ at: optExprToJson(n.at)
7004
+ };
7005
+ if (n.kind === "Loft") return {
7006
+ kind: "Loft",
7007
+ sections: n.sections.map(nodeToJson),
7008
+ ruled: n.ruled
7009
+ };
7010
+ if (n.kind === "Path") return {
7011
+ kind: "Path",
7012
+ start: exprToJson(n.start),
7013
+ segments: n.segments.map(segmentToJson)
7014
+ };
6545
7015
  if (n.kind === "Compound") return {
6546
7016
  kind: "Compound",
6547
7017
  children: n.children.map(nodeToJson)
@@ -6694,6 +7164,9 @@ function readNode(j) {
6694
7164
  case "Compound": return readCompound(j);
6695
7165
  case "Instance": return readInstance(j);
6696
7166
  case "Extrude": return readExtrude(j);
7167
+ case "Revolve": return readRevolve(j);
7168
+ case "Loft": return readLoft(j);
7169
+ case "Path": return readPath(j);
6697
7170
  default: return bad(`unknown node kind: ${String(kind)}`);
6698
7171
  }
6699
7172
  }
@@ -6855,6 +7328,93 @@ function readExtrude(j) {
6855
7328
  if (!vector.ok) return vector;
6856
7329
  return require_errors.ok(extrude$1(profile.value, vector.value));
6857
7330
  }
7331
+ function readRevolve(j) {
7332
+ const profile = readNode(j["profile"]);
7333
+ if (!profile.ok) return profile;
7334
+ const angle = readExpr(j["angle"]);
7335
+ if (!angle.ok) return angle;
7336
+ const axis = readOptExpr(j, "axis");
7337
+ if (!axis.ok) return axis;
7338
+ const at = readOptExpr(j, "at");
7339
+ if (!at.ok) return at;
7340
+ return require_errors.ok(revolve$1(profile.value, angle.value, {
7341
+ axis: axis.value,
7342
+ at: at.value
7343
+ }));
7344
+ }
7345
+ function readLoft(j) {
7346
+ const sections = readNodeArray(j["sections"], "Loft.sections");
7347
+ if (!sections.ok) return sections;
7348
+ const ruled = j["ruled"];
7349
+ if (ruled !== void 0 && typeof ruled !== "boolean") return bad("Loft.ruled: not a boolean");
7350
+ return require_errors.ok(loft$1(sections.value, { ruled }));
7351
+ }
7352
+ function readFlag(j, key) {
7353
+ const v = j[key];
7354
+ if (v === void 0) return require_errors.ok(false);
7355
+ return typeof v === "boolean" ? require_errors.ok(v) : bad(`${key}: not a boolean`);
7356
+ }
7357
+ function readSegment(j) {
7358
+ if (!isObj(j)) return bad("segment: not an object");
7359
+ const to = readExpr(j["to"]);
7360
+ if (!to.ok) return to;
7361
+ const kind = j["kind"];
7362
+ switch (kind) {
7363
+ case "Line": return require_errors.ok(lineTo(to.value));
7364
+ case "Arc": {
7365
+ const radius = readExpr(j["radius"]);
7366
+ if (!radius.ok) return radius;
7367
+ const largeArc = readFlag(j, "largeArc");
7368
+ if (!largeArc.ok) return largeArc;
7369
+ const clockwise = readFlag(j, "clockwise");
7370
+ if (!clockwise.ok) return clockwise;
7371
+ return require_errors.ok(arcTo(to.value, radius.value, {
7372
+ largeArc: largeArc.value,
7373
+ clockwise: clockwise.value
7374
+ }));
7375
+ }
7376
+ case "Bezier": {
7377
+ const raw = j["controls"];
7378
+ if (!Array.isArray(raw)) return bad("Bezier.controls: not array");
7379
+ const controls = [];
7380
+ for (const c of raw) {
7381
+ const r = readExpr(c);
7382
+ if (!r.ok) return r;
7383
+ controls.push(r.value);
7384
+ }
7385
+ return require_errors.ok(bezierTo(controls, to.value));
7386
+ }
7387
+ case "EllipseArc": {
7388
+ const radii = readExpr(j["radii"]);
7389
+ if (!radii.ok) return radii;
7390
+ const rotation = readExpr(j["rotation"]);
7391
+ if (!rotation.ok) return rotation;
7392
+ const largeArc = readFlag(j, "largeArc");
7393
+ if (!largeArc.ok) return largeArc;
7394
+ const clockwise = readFlag(j, "clockwise");
7395
+ if (!clockwise.ok) return clockwise;
7396
+ return require_errors.ok(ellipseArcTo(to.value, radii.value, {
7397
+ rotation: rotation.value,
7398
+ largeArc: largeArc.value,
7399
+ clockwise: clockwise.value
7400
+ }));
7401
+ }
7402
+ default: return bad(`unknown segment kind: ${String(kind)}`);
7403
+ }
7404
+ }
7405
+ function readPath(j) {
7406
+ const start = readExpr(j["start"]);
7407
+ if (!start.ok) return start;
7408
+ const raw = j["segments"];
7409
+ if (!Array.isArray(raw)) return bad("Path.segments: not array");
7410
+ const segments = [];
7411
+ for (const s of raw) {
7412
+ const r = readSegment(s);
7413
+ if (!r.ok) return r;
7414
+ segments.push(r.value);
7415
+ }
7416
+ return require_errors.ok(path(start.value, segments));
7417
+ }
6858
7418
  function readCompound(j) {
6859
7419
  const children = readNodeArray(j["children"], "Compound.children");
6860
7420
  return children.ok ? require_errors.ok(compound$1(children.value)) : children;
@@ -6966,7 +7526,21 @@ function optimizeNode(n) {
6966
7526
  });
6967
7527
  case "Compound": return compound$1(n.children.map(optimizeNode).filter((c) => c.kind !== "Empty"));
6968
7528
  case "Instance": return instance$1(optimizeNode(n.source), n.placements, n.fuse);
7529
+ case "Extrude":
7530
+ case "Revolve":
7531
+ case "Loft":
7532
+ case "Path": return optimizeFeature(n);
7533
+ }
7534
+ }
7535
+ function optimizeFeature(n) {
7536
+ switch (n.kind) {
6969
7537
  case "Extrude": return extrude$1(optimizeNode(n.profile), foldExpr(n.vector));
7538
+ case "Revolve": return revolve$1(optimizeNode(n.profile), foldExpr(n.angle), {
7539
+ axis: n.axis ? foldExpr(n.axis) : void 0,
7540
+ at: n.at ? foldExpr(n.at) : void 0
7541
+ });
7542
+ case "Loft": return loft$1(n.sections.map(optimizeNode), { ruled: n.ruled });
7543
+ case "Path": return path(foldExpr(n.start), n.segments.map((s) => foldSegment(s, foldExpr)));
6970
7544
  }
6971
7545
  }
6972
7546
  function optimizeFuse(a, b, tol) {
@@ -7039,7 +7613,8 @@ function rebuildChildren(n, pred, repl) {
7039
7613
  case "Circle":
7040
7614
  case "Line":
7041
7615
  case "Vertex":
7042
- case "Empty": return n;
7616
+ case "Empty":
7617
+ case "Path": return n;
7043
7618
  case "Fuse": return fuse$1(walk(n.a, pred, repl), walk(n.b, pred, repl), n.tolerance);
7044
7619
  case "Cut": return cut$1(walk(n.a, pred, repl), walk(n.b, pred, repl), n.tolerance);
7045
7620
  case "Intersect": return intersect$1(walk(n.a, pred, repl), walk(n.b, pred, repl), n.tolerance);
@@ -7058,6 +7633,11 @@ function rebuildChildren(n, pred, repl) {
7058
7633
  case "Compound": return compound$1(n.children.map((c) => walk(c, pred, repl)));
7059
7634
  case "Instance": return instance$1(walk(n.source, pred, repl), n.placements, n.fuse);
7060
7635
  case "Extrude": return extrude$1(walk(n.profile, pred, repl), n.vector);
7636
+ case "Revolve": return revolve$1(walk(n.profile, pred, repl), n.angle, {
7637
+ axis: n.axis,
7638
+ at: n.at
7639
+ });
7640
+ case "Loft": return loft$1(n.sections.map((s) => walk(s, pred, repl)), { ruled: n.ruled });
7061
7641
  }
7062
7642
  }
7063
7643
  function forEachNode(root, fn) {
@@ -7075,7 +7655,8 @@ function childrenOf(n) {
7075
7655
  case "Circle":
7076
7656
  case "Line":
7077
7657
  case "Vertex":
7078
- case "Empty": return [];
7658
+ case "Empty":
7659
+ case "Path": return [];
7079
7660
  case "Fuse":
7080
7661
  case "Cut":
7081
7662
  case "Intersect": return [n.a, n.b];
@@ -7087,7 +7668,9 @@ function childrenOf(n) {
7087
7668
  case "Mirror": return [n.target];
7088
7669
  case "Compound": return n.children;
7089
7670
  case "Instance": return [n.source];
7090
- case "Extrude": return [n.profile];
7671
+ case "Extrude":
7672
+ case "Revolve": return [n.profile];
7673
+ case "Loft": return n.sections;
7091
7674
  }
7092
7675
  }
7093
7676
  function nodeCount(root) {
@@ -7103,9 +7686,11 @@ var csg_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
7103
7686
  CSG_VERSION: () => 1,
7104
7687
  Evaluator: () => Evaluator,
7105
7688
  add: () => add,
7689
+ arcTo: () => arcTo,
7106
7690
  asScalarExpr: () => asScalarExpr,
7107
7691
  asVec2Expr: () => asVec2Expr,
7108
7692
  asVec3Expr: () => asVec3Expr,
7693
+ bezierTo: () => bezierTo,
7109
7694
  binOp: () => binOp,
7110
7695
  box: () => box$2,
7111
7696
  buildVec: () => buildVec,
@@ -7116,6 +7701,7 @@ var csg_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
7116
7701
  cut: () => cut$1,
7117
7702
  cutAll: () => cutAll$1,
7118
7703
  cylinder: () => cylinder$2,
7704
+ ellipseArcTo: () => ellipseArcTo,
7119
7705
  emptyFace: () => emptyFace,
7120
7706
  emptySolid: () => emptySolid,
7121
7707
  emptyWire: () => emptyWire,
@@ -7128,6 +7714,8 @@ var csg_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
7128
7714
  instance: () => instance$1,
7129
7715
  intersect: () => intersect$1,
7130
7716
  line: () => line$1,
7717
+ lineTo: () => lineTo,
7718
+ loft: () => loft$1,
7131
7719
  mirror: () => mirror$1,
7132
7720
  mul: () => mul,
7133
7721
  nodeCount: () => nodeCount,
@@ -7135,8 +7723,10 @@ var csg_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
7135
7723
  optimize: () => optimize,
7136
7724
  outputKindOf: () => outputKindOf,
7137
7725
  param: () => param,
7726
+ path: () => path,
7138
7727
  polygon: () => polygon$1,
7139
7728
  replaceNode: () => replaceNode,
7729
+ revolve: () => revolve$1,
7140
7730
  rotate: () => rotate$1,
7141
7731
  scale: () => scale$1,
7142
7732
  sphere: () => sphere$2,