brepjs 18.124.8 → 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) {
@@ -5597,14 +5692,76 @@ function mirror$1(target, options) {
5597
5692
  freeParams: depsOf(target, nE, atE)
5598
5693
  };
5599
5694
  }
5695
+ function extrude$1(profile, vector) {
5696
+ const ve = asVec3Expr(vector);
5697
+ return {
5698
+ kind: "Extrude",
5699
+ profile,
5700
+ vector: ve,
5701
+ structuralHash: mix(mix(startHash("Extrude"), profile), ve),
5702
+ freeParams: depsOf(profile, ve)
5703
+ };
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
+ }
5600
5756
  function compound$1(children) {
5601
- let h = fnvMixInt32(startHash("Compound"), children.length);
5602
- 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);
5603
5760
  return {
5604
5761
  kind: "Compound",
5605
- children,
5762
+ children: copied,
5606
5763
  structuralHash: h,
5607
- freeParams: depsOf(...children)
5764
+ freeParams: depsOf(...copied)
5608
5765
  };
5609
5766
  }
5610
5767
  function instance$1(source, placements, fuse = false) {
@@ -5643,6 +5800,10 @@ function outputKindOf(node) {
5643
5800
  case "Rotate":
5644
5801
  case "Scale":
5645
5802
  case "Mirror": return outputKindOf(node.target);
5803
+ case "Extrude":
5804
+ case "Revolve":
5805
+ case "Loft": return "Solid";
5806
+ case "Path": return "Wire";
5646
5807
  case "Compound": return "Compound";
5647
5808
  case "Instance": return node.fuse ? "Solid" : "Compound";
5648
5809
  }
@@ -5922,6 +6083,291 @@ function evalInstance(node, ctx) {
5922
6083
  return materialize(instance(r.value, node.placements), { fuse: node.fuse });
5923
6084
  }
5924
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
6099
+ //#region src/csg/evaluators/extrude.ts
6100
+ function evalExtrude(node, ctx) {
6101
+ const v = evalVec3(node.vector, ctx.env, "Extrude.vector");
6102
+ if (!v.ok) return v;
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);
6238
+ if (!p.ok) return p;
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
+ }
6369
+ }
6370
+ //#endregion
5925
6371
  //#region src/csg/evaluate.ts
5926
6372
  function dispatch(node, ctx) {
5927
6373
  switch (node.kind) {
@@ -5946,6 +6392,10 @@ function dispatch(node, ctx) {
5946
6392
  case "Mirror": return evalMirror(node, ctx);
5947
6393
  case "Compound": return evalCompound(node, ctx);
5948
6394
  case "Instance": return evalInstance(node, ctx);
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);
5949
6399
  }
5950
6400
  }
5951
6401
  function hashExprValue(h, v) {
@@ -6482,6 +6932,34 @@ function booleanToJson(n) {
6482
6932
  function optExprToJson(e) {
6483
6933
  return e ? exprToJson(e) : void 0;
6484
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
+ }
6485
6963
  function transformToJson(n) {
6486
6964
  switch (n.kind) {
6487
6965
  case "Translate": return {
@@ -6512,6 +6990,28 @@ function transformToJson(n) {
6512
6990
  }
6513
6991
  }
6514
6992
  function nodeToJson(n) {
6993
+ if (n.kind === "Extrude") return {
6994
+ kind: "Extrude",
6995
+ profile: nodeToJson(n.profile),
6996
+ vector: exprToJson(n.vector)
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
+ };
6515
7015
  if (n.kind === "Compound") return {
6516
7016
  kind: "Compound",
6517
7017
  children: n.children.map(nodeToJson)
@@ -6663,6 +7163,10 @@ function readNode(j) {
6663
7163
  case "Mirror": return readTransform(kind, j);
6664
7164
  case "Compound": return readCompound(j);
6665
7165
  case "Instance": return readInstance(j);
7166
+ case "Extrude": return readExtrude(j);
7167
+ case "Revolve": return readRevolve(j);
7168
+ case "Loft": return readLoft(j);
7169
+ case "Path": return readPath(j);
6666
7170
  default: return bad(`unknown node kind: ${String(kind)}`);
6667
7171
  }
6668
7172
  }
@@ -6817,6 +7321,100 @@ function readMirror(j, target) {
6817
7321
  at: at.value
6818
7322
  }));
6819
7323
  }
7324
+ function readExtrude(j) {
7325
+ const profile = readNode(j["profile"]);
7326
+ if (!profile.ok) return profile;
7327
+ const vector = readExpr(j["vector"]);
7328
+ if (!vector.ok) return vector;
7329
+ return require_errors.ok(extrude$1(profile.value, vector.value));
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
+ }
6820
7418
  function readCompound(j) {
6821
7419
  const children = readNodeArray(j["children"], "Compound.children");
6822
7420
  return children.ok ? require_errors.ok(compound$1(children.value)) : children;
@@ -6928,6 +7526,21 @@ function optimizeNode(n) {
6928
7526
  });
6929
7527
  case "Compound": return compound$1(n.children.map(optimizeNode).filter((c) => c.kind !== "Empty"));
6930
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) {
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)));
6931
7544
  }
6932
7545
  }
6933
7546
  function optimizeFuse(a, b, tol) {
@@ -7000,7 +7613,8 @@ function rebuildChildren(n, pred, repl) {
7000
7613
  case "Circle":
7001
7614
  case "Line":
7002
7615
  case "Vertex":
7003
- case "Empty": return n;
7616
+ case "Empty":
7617
+ case "Path": return n;
7004
7618
  case "Fuse": return fuse$1(walk(n.a, pred, repl), walk(n.b, pred, repl), n.tolerance);
7005
7619
  case "Cut": return cut$1(walk(n.a, pred, repl), walk(n.b, pred, repl), n.tolerance);
7006
7620
  case "Intersect": return intersect$1(walk(n.a, pred, repl), walk(n.b, pred, repl), n.tolerance);
@@ -7018,6 +7632,12 @@ function rebuildChildren(n, pred, repl) {
7018
7632
  });
7019
7633
  case "Compound": return compound$1(n.children.map((c) => walk(c, pred, repl)));
7020
7634
  case "Instance": return instance$1(walk(n.source, pred, repl), n.placements, n.fuse);
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 });
7021
7641
  }
7022
7642
  }
7023
7643
  function forEachNode(root, fn) {
@@ -7035,7 +7655,8 @@ function childrenOf(n) {
7035
7655
  case "Circle":
7036
7656
  case "Line":
7037
7657
  case "Vertex":
7038
- case "Empty": return [];
7658
+ case "Empty":
7659
+ case "Path": return [];
7039
7660
  case "Fuse":
7040
7661
  case "Cut":
7041
7662
  case "Intersect": return [n.a, n.b];
@@ -7047,6 +7668,9 @@ function childrenOf(n) {
7047
7668
  case "Mirror": return [n.target];
7048
7669
  case "Compound": return n.children;
7049
7670
  case "Instance": return [n.source];
7671
+ case "Extrude":
7672
+ case "Revolve": return [n.profile];
7673
+ case "Loft": return n.sections;
7050
7674
  }
7051
7675
  }
7052
7676
  function nodeCount(root) {
@@ -7062,9 +7686,11 @@ var csg_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
7062
7686
  CSG_VERSION: () => 1,
7063
7687
  Evaluator: () => Evaluator,
7064
7688
  add: () => add,
7689
+ arcTo: () => arcTo,
7065
7690
  asScalarExpr: () => asScalarExpr,
7066
7691
  asVec2Expr: () => asVec2Expr,
7067
7692
  asVec3Expr: () => asVec3Expr,
7693
+ bezierTo: () => bezierTo,
7068
7694
  binOp: () => binOp,
7069
7695
  box: () => box$2,
7070
7696
  buildVec: () => buildVec,
@@ -7075,9 +7701,11 @@ var csg_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
7075
7701
  cut: () => cut$1,
7076
7702
  cutAll: () => cutAll$1,
7077
7703
  cylinder: () => cylinder$2,
7704
+ ellipseArcTo: () => ellipseArcTo,
7078
7705
  emptyFace: () => emptyFace,
7079
7706
  emptySolid: () => emptySolid,
7080
7707
  emptyWire: () => emptyWire,
7708
+ extrude: () => extrude$1,
7081
7709
  foldExpr: () => foldExpr,
7082
7710
  forEachNode: () => forEachNode,
7083
7711
  fromJSON: () => fromJSON,
@@ -7086,6 +7714,8 @@ var csg_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
7086
7714
  instance: () => instance$1,
7087
7715
  intersect: () => intersect$1,
7088
7716
  line: () => line$1,
7717
+ lineTo: () => lineTo,
7718
+ loft: () => loft$1,
7089
7719
  mirror: () => mirror$1,
7090
7720
  mul: () => mul,
7091
7721
  nodeCount: () => nodeCount,
@@ -7093,8 +7723,10 @@ var csg_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
7093
7723
  optimize: () => optimize,
7094
7724
  outputKindOf: () => outputKindOf,
7095
7725
  param: () => param,
7726
+ path: () => path,
7096
7727
  polygon: () => polygon$1,
7097
7728
  replaceNode: () => replaceNode,
7729
+ revolve: () => revolve$1,
7098
7730
  rotate: () => rotate$1,
7099
7731
  scale: () => scale$1,
7100
7732
  sphere: () => sphere$2,