@chanmeng666/archlang 0.6.0 → 0.7.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.
@@ -396,6 +396,13 @@ function distPointToSegment(p, a, b) {
396
396
  const cy = a.y + t * aby;
397
397
  return Math.hypot(p.x - cx, p.y - cy);
398
398
  }
399
+ function minorArcDegrees(center, start, end) {
400
+ const deg = (p) => Math.atan2(-(p.y - center.y), p.x - center.x) * 180 / Math.PI;
401
+ const a1 = deg(start);
402
+ const a2 = deg(end);
403
+ const ccw = ((a2 - a1) % 360 + 360) % 360;
404
+ return ccw <= 180 ? [a1, a2] : [a2, a1];
405
+ }
399
406
  function rectCorners(x, y, w, h) {
400
407
  return [
401
408
  { x, y },
@@ -547,33 +554,29 @@ var wall = {
547
554
  const w = resolved;
548
555
  return segmentsOfWall(w).flatMap((s) => segmentRectangle(s.a, s.b, s.thickness));
549
556
  },
557
+ /**
558
+ * Per-segment wall fill (poché) + two crisp face lines. This is the angled-wall
559
+ * path; orthogonal walls are unioned into clean loops in `scene-build.ts`. The
560
+ * fill always references the default poché pattern, matching v0.1.
561
+ */
550
562
  render(resolved, ctx) {
551
563
  const w = resolved;
552
- const { fmt: fmt2, pt: pt2, theme, sizes } = ctx;
564
+ const { theme, sizes } = ctx;
553
565
  const segs = segmentsOfWall(w);
554
- const ops = [];
566
+ const nodes = [];
555
567
  for (const s of segs) {
556
568
  const poly = segmentRectangle(s.a, s.b, s.thickness);
557
- ops.push({ pass: "wallFill", svg: `<polygon points="${poly.map(pt2).join(" ")}" fill="url(#poche)"/>` });
569
+ nodes.push({ layer: "wallFill", prim: { t: "polygon", pts: poly }, paint: { fill: "url(#poche)" } });
558
570
  }
559
571
  for (const s of segs) {
560
572
  const d = unit(sub(s.b, s.a));
561
573
  const n = normal(d);
562
574
  const h = s.thickness / 2;
563
- const fa1 = add(s.a, mul(n, h));
564
- const fb1 = add(s.b, mul(n, h));
565
- const fa2 = add(s.a, mul(n, -h));
566
- const fb2 = add(s.b, mul(n, -h));
567
- ops.push({
568
- pass: "wallFace",
569
- svg: `<line x1="${fmt2(fa1.x)}" y1="${fmt2(fa1.y)}" x2="${fmt2(fb1.x)}" y2="${fmt2(fb1.y)}" stroke="${theme.wallStroke}" stroke-width="${fmt2(sizes.wallStroke)}" stroke-linecap="square"/>`
570
- });
571
- ops.push({
572
- pass: "wallFace",
573
- svg: `<line x1="${fmt2(fa2.x)}" y1="${fmt2(fa2.y)}" x2="${fmt2(fb2.x)}" y2="${fmt2(fb2.y)}" stroke="${theme.wallStroke}" stroke-width="${fmt2(sizes.wallStroke)}" stroke-linecap="square"/>`
574
- });
575
+ const face = { stroke: theme.wallStroke, width: sizes.wallStroke, linecap: "square" };
576
+ nodes.push({ layer: "wallFace", prim: { t: "line", a: add(s.a, mul(n, h)), b: add(s.b, mul(n, h)) }, paint: face });
577
+ nodes.push({ layer: "wallFace", prim: { t: "line", a: add(s.a, mul(n, -h)), b: add(s.b, mul(n, -h)) }, paint: face });
575
578
  }
576
- return ops;
579
+ return nodes;
577
580
  }
578
581
  };
579
582
  function describe2(ctx) {
@@ -618,24 +621,26 @@ var room = {
618
621
  },
619
622
  render(resolved, ctx) {
620
623
  const r = resolved;
621
- const { fmt: fmt2, pt: pt2, xml: xml2, theme, sizes } = ctx;
622
- const ops = [];
624
+ const { theme, sizes } = ctx;
625
+ const nodes = [];
623
626
  const c = rectCorners(r.at.x, r.at.y, r.size.w, r.size.h);
624
- ops.push({ pass: "floor", svg: `<polygon points="${c.map(pt2).join(" ")}" fill="${theme.roomFill}"/>` });
627
+ nodes.push({ layer: "floor", prim: { t: "polygon", pts: c }, paint: { fill: theme.roomFill } });
625
628
  const cx = r.at.x + r.size.w / 2;
626
629
  const cy = r.at.y + r.size.h / 2;
627
630
  const areaM2 = (r.size.w / 1e3 * (r.size.h / 1e3)).toFixed(1);
628
631
  if (r.label) {
629
- ops.push({
630
- pass: "labels",
631
- svg: `<text x="${fmt2(cx)}" y="${fmt2(cy - sizes.roomFont * 0.2)}" font-size="${fmt2(sizes.roomFont)}" fill="${theme.roomLabel}" text-anchor="middle" dominant-baseline="central" font-weight="600">${xml2(r.label)}</text>`
632
+ nodes.push({
633
+ layer: "labels",
634
+ prim: { t: "text", at: { x: cx, y: cy - sizes.roomFont * 0.2 }, value: r.label, size: sizes.roomFont, anchor: "middle", baseline: "central", weight: 600 },
635
+ paint: { fill: theme.roomLabel }
632
636
  });
633
637
  }
634
- ops.push({
635
- pass: "labels",
636
- svg: `<text x="${fmt2(cx)}" y="${fmt2(cy + (r.label ? sizes.roomFont * 0.9 : 0))}" font-size="${fmt2(sizes.areaFont)}" fill="${theme.areaLabel}" text-anchor="middle" dominant-baseline="central">${areaM2} m\xB2</text>`
638
+ nodes.push({
639
+ layer: "labels",
640
+ prim: { t: "text", at: { x: cx, y: cy + (r.label ? sizes.roomFont * 0.9 : 0) }, value: `${areaM2} m\xB2`, size: sizes.areaFont, anchor: "middle", baseline: "central" },
641
+ paint: { fill: theme.areaLabel }
637
642
  });
638
- return ops;
643
+ return nodes;
639
644
  }
640
645
  };
641
646
 
@@ -685,11 +690,17 @@ var door = {
685
690
  return { kind: "door", id, at, width, hinge: n.hinge, swing: n.swing, host: ctx.hostSegment(at, n.wall), span: n.span };
686
691
  },
687
692
  bounds: () => [],
693
+ /**
694
+ * Opening cover + leaf line + swing arc. The swing geometry (hinge, leaf,
695
+ * far jamb, minor-arc orientation) is computed **here, once** — every backend
696
+ * (SVG, DXF, PDF) now serializes the same `arc` primitive rather than
697
+ * re-deriving it.
698
+ */
688
699
  render(resolved, ctx) {
689
700
  const dr = resolved;
690
701
  const seg = dr.host;
691
702
  if (!seg) return [];
692
- const { fmt: fmt2, pt: pt2, theme, sizes } = ctx;
703
+ const { theme, sizes } = ctx;
693
704
  const d = unit(sub(seg.b, seg.a));
694
705
  const n = normal(d);
695
706
  const h = seg.thickness / 2 + sizes.wallStroke;
@@ -700,23 +711,25 @@ var door = {
700
711
  add(add(dr.at, mul(d, hw)), mul(n, -h)),
701
712
  add(add(dr.at, mul(d, -hw)), mul(n, -h))
702
713
  ];
703
- const ops = [];
704
- ops.push({ pass: "doors", svg: `<polygon points="${cover.map(pt2).join(" ")}" fill="${theme.opening}"/>` });
714
+ const nodes = [];
715
+ nodes.push({ layer: "doors", prim: { t: "polygon", pts: cover }, paint: { fill: theme.opening } });
705
716
  const hinge = dr.hinge === "left" ? add(dr.at, mul(d, -hw)) : add(dr.at, mul(d, hw));
706
717
  const farJamb = dr.hinge === "left" ? add(dr.at, mul(d, hw)) : add(dr.at, mul(d, -hw));
707
718
  const leafDir = dr.swing === "in" ? n : mul(n, -1);
708
719
  const leafEnd = add(hinge, mul(leafDir, dr.width));
709
720
  const cross = (leafEnd.x - hinge.x) * (farJamb.y - hinge.y) - (leafEnd.y - hinge.y) * (farJamb.x - hinge.x);
710
721
  const sweep = cross < 0 ? 1 : 0;
711
- ops.push({
712
- pass: "doors",
713
- svg: `<line x1="${fmt2(hinge.x)}" y1="${fmt2(hinge.y)}" x2="${fmt2(leafEnd.x)}" y2="${fmt2(leafEnd.y)}" stroke="${theme.doorLeaf}" stroke-width="${fmt2(sizes.thin * 1.3)}"/>`
722
+ nodes.push({
723
+ layer: "doors",
724
+ prim: { t: "line", a: hinge, b: leafEnd },
725
+ paint: { stroke: theme.doorLeaf, width: sizes.thin * 1.3 }
714
726
  });
715
- ops.push({
716
- pass: "doors",
717
- svg: `<path d="M ${pt2(leafEnd)} A ${fmt2(dr.width)} ${fmt2(dr.width)} 0 0 ${sweep} ${pt2(farJamb)}" fill="none" stroke="${theme.doorLeaf}" stroke-width="${fmt2(sizes.thin)}" stroke-dasharray="${fmt2(sizes.thin * 4)} ${fmt2(sizes.thin * 3)}"/>`
727
+ nodes.push({
728
+ layer: "doors",
729
+ prim: { t: "arc", center: hinge, r: dr.width, start: leafEnd, end: farJamb, sweep },
730
+ paint: { fill: "none", stroke: theme.doorLeaf, width: sizes.thin, dash: [sizes.thin * 4, sizes.thin * 3] }
718
731
  });
719
- return ops;
732
+ return nodes;
720
733
  }
721
734
  };
722
735
 
@@ -758,7 +771,7 @@ var windowEl = {
758
771
  const wn = resolved;
759
772
  const seg = wn.host;
760
773
  if (!seg) return [];
761
- const { fmt: fmt2, pt: pt2, theme, sizes } = ctx;
774
+ const { theme, sizes } = ctx;
762
775
  const d = unit(sub(seg.b, seg.a));
763
776
  const n = normal(d);
764
777
  const h = seg.thickness / 2;
@@ -770,23 +783,23 @@ var windowEl = {
770
783
  add(add(wn.at, mul(d, hw)), mul(n, -he)),
771
784
  add(add(wn.at, mul(d, -hw)), mul(n, -he))
772
785
  ];
773
- const ops = [];
774
- ops.push({ pass: "windows", svg: `<polygon points="${cover.map(pt2).join(" ")}" fill="${theme.opening}"/>` });
786
+ const nodes = [];
787
+ nodes.push({ layer: "windows", prim: { t: "polygon", pts: cover }, paint: { fill: theme.opening } });
775
788
  const jA = add(wn.at, mul(d, -hw));
776
789
  const jB = add(wn.at, mul(d, hw));
777
790
  for (const off of [h, -h]) {
778
- const a = add(jA, mul(n, off));
779
- const bb = add(jB, mul(n, off));
780
- ops.push({
781
- pass: "windows",
782
- svg: `<line x1="${fmt2(a.x)}" y1="${fmt2(a.y)}" x2="${fmt2(bb.x)}" y2="${fmt2(bb.y)}" stroke="${theme.wallStroke}" stroke-width="${fmt2(sizes.thin)}"/>`
791
+ nodes.push({
792
+ layer: "windows",
793
+ prim: { t: "line", a: add(jA, mul(n, off)), b: add(jB, mul(n, off)) },
794
+ paint: { stroke: theme.wallStroke, width: sizes.thin }
783
795
  });
784
796
  }
785
- ops.push({
786
- pass: "windows",
787
- svg: `<line x1="${fmt2(jA.x)}" y1="${fmt2(jA.y)}" x2="${fmt2(jB.x)}" y2="${fmt2(jB.y)}" stroke="${theme.windowPane}" stroke-width="${fmt2(sizes.thin)}"/>`
797
+ nodes.push({
798
+ layer: "windows",
799
+ prim: { t: "line", a: jA, b: jB },
800
+ paint: { stroke: theme.windowPane, width: sizes.thin }
788
801
  });
789
- return ops;
802
+ return nodes;
790
803
  }
791
804
  };
792
805
 
@@ -826,22 +839,24 @@ var furniture = {
826
839
  },
827
840
  render(resolved, ctx) {
828
841
  const f = resolved;
829
- const { fmt: fmt2, pt: pt2, xml: xml2, theme, sizes } = ctx;
830
- const ops = [];
842
+ const { theme, sizes } = ctx;
843
+ const nodes = [];
831
844
  const c = rectCorners(f.at.x, f.at.y, f.size.w, f.size.h);
832
- ops.push({
833
- pass: "furniture",
834
- svg: `<polygon points="${c.map(pt2).join(" ")}" fill="${theme.furnitureFill}" stroke="${theme.furnitureStroke}" stroke-width="${fmt2(sizes.thin)}"/>`
845
+ nodes.push({
846
+ layer: "furniture",
847
+ prim: { t: "polygon", pts: c },
848
+ paint: { fill: theme.furnitureFill, stroke: theme.furnitureStroke, width: sizes.thin }
835
849
  });
836
850
  if (f.label) {
837
851
  const cx = f.at.x + f.size.w / 2;
838
852
  const cy = f.at.y + f.size.h / 2;
839
- ops.push({
840
- pass: "furniture",
841
- svg: `<text x="${fmt2(cx)}" y="${fmt2(cy)}" font-size="${fmt2(sizes.furnFont)}" fill="${theme.furnitureLabel}" text-anchor="middle" dominant-baseline="central">${xml2(f.label)}</text>`
853
+ nodes.push({
854
+ layer: "furniture",
855
+ prim: { t: "text", at: { x: cx, y: cy }, value: f.label, size: sizes.furnFont, anchor: "middle", baseline: "central" },
856
+ paint: { fill: theme.furnitureLabel }
842
857
  });
843
858
  }
844
- return ops;
859
+ return nodes;
845
860
  }
846
861
  };
847
862
 
@@ -884,33 +899,22 @@ var dim = {
884
899
  },
885
900
  render(resolved, ctx) {
886
901
  const dm = resolved;
887
- const { fmt: fmt2, xml: xml2, theme, sizes } = ctx;
902
+ const { theme, sizes } = ctx;
888
903
  const dir = unit(sub(dm.to, dm.from));
889
904
  const n = normal(dir);
890
905
  const off = mul(n, dm.offset);
891
906
  const p1 = add(dm.from, off);
892
907
  const p2 = add(dm.to, off);
893
908
  const tick = sizes.refDim * 0.012;
894
- const ops = [];
895
- ops.push({
896
- pass: "dims",
897
- svg: `<line x1="${fmt2(dm.from.x)}" y1="${fmt2(dm.from.y)}" x2="${fmt2(p1.x)}" y2="${fmt2(p1.y)}" stroke="${theme.dim}" stroke-width="${fmt2(sizes.thin * 0.7)}"/>`
898
- });
899
- ops.push({
900
- pass: "dims",
901
- svg: `<line x1="${fmt2(dm.to.x)}" y1="${fmt2(dm.to.y)}" x2="${fmt2(p2.x)}" y2="${fmt2(p2.y)}" stroke="${theme.dim}" stroke-width="${fmt2(sizes.thin * 0.7)}"/>`
902
- });
903
- ops.push({
904
- pass: "dims",
905
- svg: `<line x1="${fmt2(p1.x)}" y1="${fmt2(p1.y)}" x2="${fmt2(p2.x)}" y2="${fmt2(p2.y)}" stroke="${theme.dim}" stroke-width="${fmt2(sizes.thin)}"/>`
906
- });
909
+ const thinPaint = { stroke: theme.dim, width: sizes.thin };
910
+ const nodes = [];
911
+ nodes.push({ layer: "dims", prim: { t: "line", a: dm.from, b: p1 }, paint: { stroke: theme.dim, width: sizes.thin * 0.7 } });
912
+ nodes.push({ layer: "dims", prim: { t: "line", a: dm.to, b: p2 }, paint: { stroke: theme.dim, width: sizes.thin * 0.7 } });
913
+ nodes.push({ layer: "dims", prim: { t: "line", a: p1, b: p2 }, paint: thinPaint });
907
914
  for (const p of [p1, p2]) {
908
915
  const t1 = add(p, mul(unit({ x: dir.x + n.x, y: dir.y + n.y }), tick));
909
916
  const t2 = add(p, mul(unit({ x: dir.x + n.x, y: dir.y + n.y }), -tick));
910
- ops.push({
911
- pass: "dims",
912
- svg: `<line x1="${fmt2(t1.x)}" y1="${fmt2(t1.y)}" x2="${fmt2(t2.x)}" y2="${fmt2(t2.y)}" stroke="${theme.dim}" stroke-width="${fmt2(sizes.thin)}"/>`
913
- });
917
+ nodes.push({ layer: "dims", prim: { t: "line", a: t1, b: t2 }, paint: thinPaint });
914
918
  }
915
919
  const mid = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };
916
920
  const tp = add(mid, mul(n, sizes.dimFont * 0.7));
@@ -918,11 +922,12 @@ var dim = {
918
922
  if (angle > 90) angle -= 180;
919
923
  if (angle < -90) angle += 180;
920
924
  const label = dm.text ?? String(Math.round(length(sub(dm.to, dm.from))));
921
- ops.push({
922
- pass: "dims",
923
- svg: `<text x="${fmt2(tp.x)}" y="${fmt2(tp.y)}" font-size="${fmt2(sizes.dimFont)}" fill="${theme.dim}" text-anchor="middle" dominant-baseline="central" transform="rotate(${fmt2(angle)} ${fmt2(tp.x)} ${fmt2(tp.y)})">${xml2(label)}</text>`
925
+ nodes.push({
926
+ layer: "dims",
927
+ prim: { t: "text", at: tp, value: label, size: sizes.dimFont, anchor: "middle", baseline: "central", rotate: angle },
928
+ paint: { fill: theme.dim }
924
929
  });
925
- return ops;
930
+ return nodes;
926
931
  }
927
932
  };
928
933
 
@@ -956,12 +961,13 @@ var column = {
956
961
  },
957
962
  render(resolved, ctx) {
958
963
  const c = resolved;
959
- const { fmt: fmt2, pt: pt2, theme, sizes } = ctx;
964
+ const { theme, sizes } = ctx;
960
965
  const pts = rectCorners(c.at.x, c.at.y, c.size.w, c.size.h);
961
966
  return [
962
967
  {
963
- pass: "furniture",
964
- svg: `<polygon points="${pts.map(pt2).join(" ")}" fill="${theme.column}" stroke="${theme.wallStroke}" stroke-width="${fmt2(sizes.thin)}"/>`
968
+ layer: "furniture",
969
+ prim: { t: "polygon", pts },
970
+ paint: { fill: theme.column, stroke: theme.wallStroke, width: sizes.thin }
965
971
  }
966
972
  ];
967
973
  }
@@ -1502,19 +1508,6 @@ function resolve(ast) {
1502
1508
  return { ir, diagnostics };
1503
1509
  }
1504
1510
 
1505
- // src/registry.ts
1506
- var RENDER_PASSES = [
1507
- "floor",
1508
- "furniture",
1509
- "wallFill",
1510
- "wallFace",
1511
- "doors",
1512
- "windows",
1513
- "labels",
1514
- "dims",
1515
- "annotations"
1516
- ];
1517
-
1518
1511
  // src/geometry/union.ts
1519
1512
  function uniqSorted(values) {
1520
1513
  const out = [...new Set(values)].sort((a, b) => a - b);
@@ -1612,21 +1605,7 @@ function mergeCollinear(loop) {
1612
1605
  return out.length >= 3 ? out : loop;
1613
1606
  }
1614
1607
 
1615
- // src/render.ts
1616
- function fmt(v) {
1617
- const r = Math.round(v * 100) / 100;
1618
- return Object.is(r, -0) ? "0" : String(r);
1619
- }
1620
- var pt = (p) => `${fmt(p.x)},${fmt(p.y)}`;
1621
- function xml(s) {
1622
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1623
- }
1624
- var NICE_LENGTHS = [500, 1e3, 2e3, 5e3, 1e4, 2e4, 5e4, 1e5];
1625
- function niceBarLength(target) {
1626
- let best = NICE_LENGTHS[0];
1627
- for (const v of NICE_LENGTHS) if (v <= target) best = v;
1628
- return best;
1629
- }
1608
+ // src/scene-build.ts
1630
1609
  function planBounds(ir) {
1631
1610
  const b = emptyBounds();
1632
1611
  for (const el of ir.elements) {
@@ -1642,20 +1621,17 @@ function planBounds(ir) {
1642
1621
  function allOrthogonal(walls) {
1643
1622
  return walls.every((w) => segmentsOfWall(w).every((s) => s.a.x === s.b.x || s.a.y === s.b.y));
1644
1623
  }
1645
- function loopsToPath(loops) {
1646
- return loops.map((loop) => "M " + loop.map(pt).join(" L ") + " Z").join(" ");
1647
- }
1648
1624
  function materialsUsed(walls) {
1649
1625
  return [...new Set(walls.map((w) => w.material))].sort();
1650
1626
  }
1651
- function renderWalls(walls, ctx) {
1627
+ function lowerWalls(walls, ctx) {
1652
1628
  if (walls.length === 0) return [];
1653
- const ops = [];
1629
+ const nodes = [];
1654
1630
  for (const mat of materialsUsed(walls)) {
1655
1631
  const group = walls.filter((w) => w.material === mat);
1656
1632
  if (!allOrthogonal(group)) {
1657
1633
  const def = registry.get("wall");
1658
- ops.push(...group.flatMap((w) => def.render(w, ctx)));
1634
+ nodes.push(...group.flatMap((w) => def.render(w, ctx)));
1659
1635
  continue;
1660
1636
  }
1661
1637
  const rects = [];
@@ -1669,18 +1645,18 @@ function renderWalls(walls, ctx) {
1669
1645
  }
1670
1646
  const loops = rectUnionOutline(rects);
1671
1647
  if (loops.length === 0) continue;
1672
- const d = loopsToPath(loops);
1673
- ops.push({ pass: "wallFill", svg: `<path d="${d}" fill="url(#${patternId(mat)})" fill-rule="nonzero"/>` });
1674
- ops.push({
1675
- pass: "wallFace",
1676
- svg: `<path d="${d}" fill="none" stroke="${ctx.theme.wallStroke}" stroke-width="${ctx.fmt(ctx.sizes.wallStroke)}" stroke-linejoin="miter"/>`
1648
+ nodes.push({ layer: "wallFill", prim: { t: "region", loops }, paint: { fill: `url(#${patternId(mat)})`, fillRule: "nonzero" } });
1649
+ nodes.push({
1650
+ layer: "wallFace",
1651
+ prim: { t: "region", loops },
1652
+ paint: { fill: "none", stroke: ctx.theme.wallStroke, width: ctx.sizes.wallStroke, linejoin: "miter" }
1677
1653
  });
1678
1654
  }
1679
- return ops;
1655
+ return nodes;
1680
1656
  }
1681
- function render(ir, opts = {}) {
1682
- const THEME = sanitizeTheme(mergeTheme(DEFAULT_THEME, ir.theme, opts.theme));
1683
- const lw = THEME.lineWeight;
1657
+ function toScene(ir, opts = {}) {
1658
+ const theme = sanitizeTheme(mergeTheme(DEFAULT_THEME, ir.theme, opts.theme));
1659
+ const lw = theme.lineWeight;
1684
1660
  const b = planBounds(ir);
1685
1661
  const drawW = b.maxX - b.minX;
1686
1662
  const drawH = b.maxY - b.minY;
@@ -1696,7 +1672,100 @@ function render(ir, opts = {}) {
1696
1672
  margin: refDim * 0.17,
1697
1673
  hatchGap: refDim * 0.013
1698
1674
  };
1675
+ const ctx = { theme, sizes, bounds: b };
1676
+ const nodes = [];
1677
+ for (const el of ir.elements) {
1678
+ if (el.kind === "wall") continue;
1679
+ const def = registry.get(el.kind);
1680
+ if (def) nodes.push(...def.render(el, ctx));
1681
+ }
1682
+ nodes.push(...lowerWalls(ir.walls, ctx));
1683
+ return {
1684
+ width: drawW + sizes.margin * 2,
1685
+ height: drawH + sizes.margin * 2,
1686
+ bounds: b,
1687
+ nodes,
1688
+ theme,
1689
+ sizes,
1690
+ north: ir.north,
1691
+ scale: ir.scale,
1692
+ title: ir.title,
1693
+ name: ir.name,
1694
+ materials: materialsUsed(ir.walls)
1695
+ };
1696
+ }
1697
+
1698
+ // src/scene.ts
1699
+ var RENDER_PASSES = [
1700
+ "floor",
1701
+ "furniture",
1702
+ "wallFill",
1703
+ "wallFace",
1704
+ "doors",
1705
+ "windows",
1706
+ "labels",
1707
+ "dims",
1708
+ "annotations"
1709
+ ];
1710
+
1711
+ // src/backends/svg.ts
1712
+ function fmt(v) {
1713
+ const r = Math.round(v * 100) / 100;
1714
+ return Object.is(r, -0) ? "0" : String(r);
1715
+ }
1716
+ var pt = (p) => `${fmt(p.x)},${fmt(p.y)}`;
1717
+ function xml(s) {
1718
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1719
+ }
1720
+ var NICE_LENGTHS = [500, 1e3, 2e3, 5e3, 1e4, 2e4, 5e4, 1e5];
1721
+ function niceBarLength(target) {
1722
+ let best = NICE_LENGTHS[0];
1723
+ for (const v of NICE_LENGTHS) if (v <= target) best = v;
1724
+ return best;
1725
+ }
1726
+ function strokeAttrs(paint) {
1727
+ if (!paint.stroke) return "";
1728
+ let s = ` stroke="${paint.stroke}" stroke-width="${fmt(paint.width ?? 0)}"`;
1729
+ if (paint.linecap) s += ` stroke-linecap="${paint.linecap}"`;
1730
+ return s;
1731
+ }
1732
+ function pathPaint(paint) {
1733
+ let s = ` fill="${paint.fill ?? "none"}"`;
1734
+ if (paint.fillRule) s += ` fill-rule="${paint.fillRule}"`;
1735
+ if (paint.stroke) s += ` stroke="${paint.stroke}" stroke-width="${fmt(paint.width ?? 0)}"`;
1736
+ if (paint.linejoin) s += ` stroke-linejoin="${paint.linejoin}"`;
1737
+ if (paint.dash) s += ` stroke-dasharray="${fmt(paint.dash[0])} ${fmt(paint.dash[1])}"`;
1738
+ return s;
1739
+ }
1740
+ function regionPath(loops) {
1741
+ return loops.map((loop) => "M " + loop.map(pt).join(" L ") + " Z").join(" ");
1742
+ }
1743
+ function serialize(node) {
1744
+ const { prim, paint } = node;
1745
+ switch (prim.t) {
1746
+ case "polygon":
1747
+ return `<polygon points="${prim.pts.map(pt).join(" ")}" fill="${paint.fill ?? "none"}"${strokeAttrs(paint)}/>`;
1748
+ case "line":
1749
+ return `<line x1="${fmt(prim.a.x)}" y1="${fmt(prim.a.y)}" x2="${fmt(prim.b.x)}" y2="${fmt(prim.b.y)}" stroke="${paint.stroke ?? "none"}" stroke-width="${fmt(paint.width ?? 0)}"${paint.linecap ? ` stroke-linecap="${paint.linecap}"` : ""}/>`;
1750
+ case "region":
1751
+ return `<path d="${regionPath(prim.loops)}"${pathPaint(paint)}/>`;
1752
+ case "arc":
1753
+ return `<path d="M ${pt(prim.start)} A ${fmt(prim.r)} ${fmt(prim.r)} 0 0 ${prim.sweep} ${pt(prim.end)}"${pathPaint(paint)}/>`;
1754
+ case "text": {
1755
+ const weight = prim.weight !== void 0 ? ` font-weight="${prim.weight}"` : "";
1756
+ const transform = prim.rotate !== void 0 ? ` transform="rotate(${fmt(prim.rotate)} ${fmt(prim.at.x)} ${fmt(prim.at.y)})"` : "";
1757
+ return `<text x="${fmt(prim.at.x)}" y="${fmt(prim.at.y)}" font-size="${fmt(prim.size)}" fill="${paint.fill ?? "none"}" text-anchor="${prim.anchor}" dominant-baseline="${prim.baseline}"${weight}${transform}>${xml(prim.value)}</text>`;
1758
+ }
1759
+ }
1760
+ }
1761
+ function renderSvg(scene, opts = {}) {
1762
+ const THEME = scene.theme;
1763
+ const sizes = scene.sizes;
1764
+ const b = scene.bounds;
1765
+ const refDim = sizes.refDim;
1699
1766
  const { thin, margin, hatchGap } = sizes;
1767
+ const drawW = b.maxX - b.minX;
1768
+ const drawH = b.maxY - b.minY;
1700
1769
  const vbX = b.minX - margin;
1701
1770
  const vbY = b.minY - margin;
1702
1771
  const vbW = drawW + margin * 2;
@@ -1707,32 +1776,25 @@ function render(ir, opts = {}) {
1707
1776
  `<svg xmlns="http://www.w3.org/2000/svg" ${svgAttrs} viewBox="${fmt(vbX)} ${fmt(vbY)} ${fmt(vbW)} ${fmt(vbH)}" font-family="${THEME.font}">`
1708
1777
  );
1709
1778
  const hatchCtx = { fmt, gap: hatchGap, thin, base: THEME.pocheBase, line: THEME.pocheHatch };
1710
- const patterns = materialsUsed(ir.walls).map((m) => hatchPattern(m, hatchCtx)).join("");
1779
+ const patterns = scene.materials.map((m) => hatchPattern(m, hatchCtx)).join("");
1711
1780
  out.push(`<defs>${patterns}</defs>`);
1712
1781
  out.push(`<rect x="${fmt(vbX)}" y="${fmt(vbY)}" width="${fmt(vbW)}" height="${fmt(vbH)}" fill="${THEME.bg}"/>`);
1713
- const ctx = { fmt, pt, xml, theme: THEME, sizes, bounds: b };
1714
- const ops = ir.elements.flatMap((el) => {
1715
- if (el.kind === "wall") return [];
1716
- const def = registry.get(el.kind);
1717
- return def ? def.render(el, ctx) : [];
1718
- });
1719
- ops.push(...renderWalls(ir.walls, ctx));
1720
1782
  for (const pass of RENDER_PASSES) {
1721
- for (const op of ops) if (op.pass === pass) out.push(op.svg);
1783
+ for (const node of scene.nodes) if (node.layer === pass) out.push(serialize(node));
1722
1784
  }
1723
- out.push(northArrow(ir, b, margin, refDim, THEME));
1785
+ out.push(northArrow(scene.north, b, margin, refDim, THEME));
1724
1786
  out.push(scaleBar(b, margin, refDim, thin, THEME));
1725
- const tb = titleBlock(ir, b, margin, refDim, thin, THEME);
1787
+ const tb = titleBlock(scene.title, scene.scale, b, margin, refDim, thin, THEME);
1726
1788
  if (tb) out.push(tb);
1727
1789
  out.push("</svg>");
1728
1790
  return out.join("\n");
1729
1791
  }
1730
- function northArrow(ir, b, margin, refDim, THEME) {
1792
+ function northArrow(north, b, margin, refDim, THEME) {
1731
1793
  const r = refDim * 0.045;
1732
1794
  const cx = b.maxX - r;
1733
1795
  const cy = b.minY - margin * 0.55;
1734
1796
  let deg;
1735
- switch (ir.north) {
1797
+ switch (north) {
1736
1798
  case "up":
1737
1799
  deg = 0;
1738
1800
  break;
@@ -1746,7 +1808,7 @@ function northArrow(ir, b, margin, refDim, THEME) {
1746
1808
  deg = 90;
1747
1809
  break;
1748
1810
  default:
1749
- deg = typeof ir.north === "object" ? ir.north.deg : 0;
1811
+ deg = typeof north === "object" ? north.deg : 0;
1750
1812
  }
1751
1813
  const fs = refDim * 0.026;
1752
1814
  const tri = `${fmt(cx)},${fmt(cy - r)} ${fmt(cx - r * 0.5)},${fmt(cy + r * 0.6)} ${fmt(cx)},${fmt(cy + r * 0.25)} ${fmt(cx + r * 0.5)},${fmt(cy + r * 0.6)}`;
@@ -1777,9 +1839,8 @@ function scaleBar(b, margin, refDim, thin, THEME) {
1777
1839
  );
1778
1840
  return `<g>${parts.join("")}</g>`;
1779
1841
  }
1780
- function titleBlock(ir, b, margin, refDim, thin, THEME) {
1781
- const t = ir.title;
1782
- if (!t && !ir.scale) return null;
1842
+ function titleBlock(t, scale, b, margin, refDim, thin, THEME) {
1843
+ if (!t && !scale) return null;
1783
1844
  const boxW = refDim * 0.34;
1784
1845
  const boxH = margin * 0.82;
1785
1846
  const x0 = b.maxX - boxW;
@@ -1790,7 +1851,7 @@ function titleBlock(ir, b, margin, refDim, thin, THEME) {
1790
1851
  if (t?.project) lines.push({ k: "PROJECT", v: t.project });
1791
1852
  if (t?.drawnBy) lines.push({ k: "DRAWN BY", v: t.drawnBy });
1792
1853
  if (t?.date) lines.push({ k: "DATE", v: t.date });
1793
- if (ir.scale) lines.push({ k: "SCALE", v: ir.scale });
1854
+ if (scale) lines.push({ k: "SCALE", v: scale });
1794
1855
  const parts = [];
1795
1856
  parts.push(
1796
1857
  `<rect x="${fmt(x0)}" y="${fmt(y0)}" width="${fmt(boxW)}" height="${fmt(boxH)}" fill="none" stroke="${THEME.annotation}" stroke-width="${fmt(thin)}"/>`
@@ -1902,9 +1963,10 @@ var DxfBuilder = class {
1902
1963
  this.pair(40, num(height));
1903
1964
  this.pair(1, value.replace(/\n/g, " "));
1904
1965
  }
1905
- rect(layer, corners) {
1906
- for (let i = 0; i < corners.length; i++) {
1907
- this.line(layer, corners[i], corners[(i + 1) % corners.length]);
1966
+ /** Closed loop of points as a chain of LINEs (R12-safe; no LWPOLYLINE). */
1967
+ loop(layer, pts) {
1968
+ for (let i = 0; i < pts.length; i++) {
1969
+ this.line(layer, pts[i], pts[(i + 1) % pts.length]);
1908
1970
  }
1909
1971
  }
1910
1972
  toString() {
@@ -1912,6 +1974,27 @@ var DxfBuilder = class {
1912
1974
  }
1913
1975
  };
1914
1976
  var LAYERS = ["WALLS", "ROOMS", "DOORS", "WINDOWS", "FURNITURE", "COLUMNS", "DIMS", "LABELS"];
1977
+ function dxfLayer(layer) {
1978
+ switch (layer) {
1979
+ case "wallFill":
1980
+ case "wallFace":
1981
+ return "WALLS";
1982
+ case "floor":
1983
+ return "ROOMS";
1984
+ case "doors":
1985
+ return "DOORS";
1986
+ case "windows":
1987
+ return "WINDOWS";
1988
+ case "furniture":
1989
+ return "FURNITURE";
1990
+ case "labels":
1991
+ return "LABELS";
1992
+ case "dims":
1993
+ return "DIMS";
1994
+ default:
1995
+ return "0";
1996
+ }
1997
+ }
1915
1998
  function header() {
1916
1999
  const h = [];
1917
2000
  const p = (c, v) => h.push(String(c), String(v));
@@ -1936,124 +2019,215 @@ function header() {
1936
2019
  p(0, "ENDSEC");
1937
2020
  return h.join("\n") + "\n";
1938
2021
  }
1939
- function emitDoor(b, dr) {
1940
- const seg = dr.host;
1941
- if (!seg) return;
1942
- const d = unit(sub(seg.b, seg.a));
1943
- const n = normal(d);
1944
- const hw = dr.width / 2;
1945
- const hinge = dr.hinge === "left" ? add(dr.at, mul(d, -hw)) : add(dr.at, mul(d, hw));
1946
- const farJamb = dr.hinge === "left" ? add(dr.at, mul(d, hw)) : add(dr.at, mul(d, -hw));
1947
- const leafDir = dr.swing === "in" ? n : mul(n, -1);
1948
- const leafEnd = add(hinge, mul(leafDir, dr.width));
1949
- b.line("DOORS", hinge, leafEnd);
1950
- const deg = (p) => Math.atan2(-(p.y - hinge.y), p.x - hinge.x) * 180 / Math.PI;
1951
- const a1 = deg(leafEnd);
1952
- const a2 = deg(farJamb);
1953
- const ccw = ((a2 - a1) % 360 + 360) % 360;
1954
- if (ccw <= 180) b.arc("DOORS", hinge, dr.width, a1, a2);
1955
- else b.arc("DOORS", hinge, dr.width, a2, a1);
1956
- }
1957
- function emitWindow(b, wn) {
1958
- const seg = wn.host;
1959
- if (!seg) return;
1960
- const d = unit(sub(seg.b, seg.a));
1961
- const n = normal(d);
1962
- const hw = wn.width / 2;
1963
- const h = seg.thickness / 2;
1964
- const jA = add(wn.at, mul(d, -hw));
1965
- const jB = add(wn.at, mul(d, hw));
1966
- b.line("WINDOWS", add(jA, mul(n, h)), add(jB, mul(n, h)));
1967
- b.line("WINDOWS", add(jA, mul(n, -h)), add(jB, mul(n, -h)));
1968
- b.line("WINDOWS", jA, jB);
1969
- }
1970
- function emitDim(b, dm) {
1971
- const dd = unit(sub(dm.to, dm.from));
1972
- const dn = normal(dd);
1973
- const p1 = add(dm.from, mul(dn, dm.offset));
1974
- const p2 = add(dm.to, mul(dn, dm.offset));
1975
- b.line("DIMS", p1, p2);
1976
- if (dm.text) {
1977
- const mid = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };
1978
- b.text("DIMS", mid, 150, dm.text);
2022
+ function emit(b, node) {
2023
+ const layer = dxfLayer(node.layer);
2024
+ const prim = node.prim;
2025
+ switch (prim.t) {
2026
+ case "polygon":
2027
+ b.loop(layer, prim.pts);
2028
+ break;
2029
+ case "line":
2030
+ b.line(layer, prim.a, prim.b);
2031
+ break;
2032
+ case "region":
2033
+ for (const lp of prim.loops) b.loop(layer, lp);
2034
+ break;
2035
+ case "arc": {
2036
+ const [a0, a1] = minorArcDegrees(prim.center, prim.start, prim.end);
2037
+ b.arc(layer, prim.center, prim.r, a0, a1);
2038
+ break;
2039
+ }
2040
+ case "text":
2041
+ b.text(layer, prim.at, prim.size, prim.value);
2042
+ break;
1979
2043
  }
1980
2044
  }
1981
- function toDxf(ir) {
2045
+ function toDxf(scene) {
1982
2046
  const b = new DxfBuilder();
1983
2047
  b.pair(0, "SECTION");
1984
2048
  b.pair(2, "ENTITIES");
1985
- const labelAt = (at, w, h) => ({ x: at.x + w / 2, y: at.y + h / 2 });
1986
- for (const el of ir.elements) {
1987
- switch (el.kind) {
1988
- case "wall":
1989
- for (const s of segmentsOfWall(el)) {
1990
- const d = unit(sub(s.b, s.a));
1991
- const n = normal(d);
1992
- const off = s.thickness / 2;
1993
- b.line("WALLS", add(s.a, mul(n, off)), add(s.b, mul(n, off)));
1994
- b.line("WALLS", add(s.a, mul(n, -off)), add(s.b, mul(n, -off)));
1995
- }
1996
- break;
1997
- case "room":
1998
- b.rect("ROOMS", rectCorners(el.at.x, el.at.y, el.size.w, el.size.h));
1999
- if (el.label) b.text("LABELS", labelAt(el.at, el.size.w, el.size.h), 200, el.label);
2000
- break;
2001
- case "furniture":
2002
- b.rect("FURNITURE", rectCorners(el.at.x, el.at.y, el.size.w, el.size.h));
2003
- if (el.label) b.text("LABELS", labelAt(el.at, el.size.w, el.size.h), 150, el.label);
2004
- break;
2005
- case "column":
2006
- b.rect("COLUMNS", rectCorners(el.at.x, el.at.y, el.size.w, el.size.h));
2007
- break;
2008
- case "door":
2009
- emitDoor(b, el);
2010
- break;
2011
- case "window":
2012
- emitWindow(b, el);
2013
- break;
2014
- case "dim":
2015
- emitDim(b, el);
2016
- break;
2017
- }
2018
- }
2049
+ for (const node of scene.nodes) emit(b, node);
2019
2050
  b.pair(0, "ENDSEC");
2020
- const entities = b.toString();
2021
- return header() + entities + "0\nEOF\n";
2051
+ return header() + b.toString() + "0\nEOF\n";
2022
2052
  }
2023
2053
 
2024
2054
  // src/export/pdf.ts
2025
- function svgSize(svg) {
2026
- const w = /<svg[^>]*\bwidth="([\d.]+)/.exec(svg);
2027
- const h = /<svg[^>]*\bheight="([\d.]+)/.exec(svg);
2028
- if (w && h) return { width: parseFloat(w[1]), height: parseFloat(h[1]) };
2029
- const vb = /<svg[^>]*\bviewBox="[\d.\-]+ [\d.\-]+ ([\d.]+) ([\d.]+)"/.exec(svg);
2030
- if (vb) return { width: parseFloat(vb[1]), height: parseFloat(vb[2]) };
2031
- return { width: 800, height: 600 };
2032
- }
2033
- async function toPdf(svg) {
2055
+ function fillColor(paint, theme) {
2056
+ const f = paint.fill;
2057
+ if (!f || f === "none") return null;
2058
+ if (f.startsWith("url(")) return theme.pocheBase;
2059
+ return f;
2060
+ }
2061
+ function regionPath2(loops) {
2062
+ return loops.map((loop) => "M " + loop.map((p) => `${p.x} ${p.y}`).join(" L ") + " Z").join(" ");
2063
+ }
2064
+ function applyPaint(doc, paint, theme) {
2065
+ const fill = fillColor(paint, theme);
2066
+ const stroke = paint.stroke && paint.stroke !== "none" ? paint.stroke : null;
2067
+ if (paint.width !== void 0) doc.lineWidth(paint.width);
2068
+ doc.lineJoin(paint.linejoin ?? "miter");
2069
+ doc.lineCap(paint.linecap === "square" ? "square" : "butt");
2070
+ if (paint.dash) doc.dash(paint.dash[0], { space: paint.dash[1] });
2071
+ else doc.undash();
2072
+ if (fill && stroke) doc.fillAndStroke(fill, stroke);
2073
+ else if (fill) doc.fill(fill);
2074
+ else if (stroke) doc.stroke(stroke);
2075
+ else doc.stroke();
2076
+ }
2077
+ function drawText(doc, at, value, size, anchor, rotate, color) {
2078
+ doc.undash();
2079
+ doc.fontSize(size).fillColor(color);
2080
+ const w = doc.widthOfString(value);
2081
+ let x = at.x;
2082
+ if (anchor === "middle") x -= w / 2;
2083
+ else if (anchor === "end") x -= w;
2084
+ const y = at.y - size * 0.5;
2085
+ if (rotate !== void 0) {
2086
+ doc.save();
2087
+ doc.rotate(rotate, { origin: [at.x, at.y] });
2088
+ doc.text(value, x, y, { lineBreak: false });
2089
+ doc.restore();
2090
+ } else {
2091
+ doc.text(value, x, y, { lineBreak: false });
2092
+ }
2093
+ }
2094
+ function drawNode(doc, node, theme) {
2095
+ const { prim, paint } = node;
2096
+ switch (prim.t) {
2097
+ case "polygon":
2098
+ doc.polygon(...prim.pts.map((p) => [p.x, p.y]));
2099
+ applyPaint(doc, paint, theme);
2100
+ break;
2101
+ case "line":
2102
+ doc.moveTo(prim.a.x, prim.a.y).lineTo(prim.b.x, prim.b.y);
2103
+ applyPaint(doc, paint, theme);
2104
+ break;
2105
+ case "region":
2106
+ doc.path(regionPath2(prim.loops));
2107
+ applyPaint(doc, paint, theme);
2108
+ break;
2109
+ case "arc":
2110
+ doc.path(`M ${prim.start.x} ${prim.start.y} A ${prim.r} ${prim.r} 0 0 ${prim.sweep} ${prim.end.x} ${prim.end.y}`);
2111
+ applyPaint(doc, paint, theme);
2112
+ break;
2113
+ case "text":
2114
+ drawText(doc, prim.at, prim.value, prim.size, prim.anchor, prim.rotate, fillColor(paint, theme) ?? "#000000");
2115
+ break;
2116
+ }
2117
+ }
2118
+ async function toPdf(scene) {
2034
2119
  let PDFDocument;
2035
- let SVGtoPDF;
2036
2120
  try {
2037
2121
  PDFDocument = (await import("pdfkit")).default;
2038
- SVGtoPDF = (await import("svg-to-pdfkit")).default;
2039
2122
  } catch {
2040
2123
  throw new Error(
2041
- "PDF export needs the optional dependencies 'pdfkit' and 'svg-to-pdfkit'. Install them: npm install pdfkit svg-to-pdfkit"
2124
+ "PDF export needs the optional dependency 'pdfkit'. Install it: npm install pdfkit"
2042
2125
  );
2043
2126
  }
2044
- const { width, height } = svgSize(svg);
2045
- const doc = new PDFDocument({ size: [width, height], margin: 0 });
2127
+ const { theme, sizes, bounds: b } = scene;
2128
+ const margin = sizes.margin;
2129
+ const vbX = b.minX - margin;
2130
+ const vbY = b.minY - margin;
2131
+ const W = scene.width;
2132
+ const H = scene.height;
2133
+ const doc = new PDFDocument({ size: [W, H], margin: 0 });
2046
2134
  const chunks = [];
2047
2135
  const done = new Promise((resolve2, reject) => {
2048
2136
  doc.on("data", (c) => chunks.push(c));
2049
2137
  doc.on("end", () => resolve2());
2050
2138
  doc.on("error", (e) => reject(e));
2051
2139
  });
2052
- SVGtoPDF(doc, svg, 0, 0, { width, height, assumePt: true });
2140
+ doc.save();
2141
+ doc.translate(-vbX, -vbY);
2142
+ doc.rect(vbX, vbY, W, H).fill(theme.bg);
2143
+ for (const pass of RENDER_PASSES) {
2144
+ for (const node of scene.nodes) if (node.layer === pass) drawNode(doc, node, theme);
2145
+ }
2146
+ drawChrome(doc, scene);
2147
+ doc.restore();
2053
2148
  doc.end();
2054
2149
  await done;
2055
2150
  return concat(chunks);
2056
2151
  }
2152
+ function drawChrome(doc, scene) {
2153
+ const { theme, sizes, bounds: b } = scene;
2154
+ const refDim = sizes.refDim;
2155
+ const margin = sizes.margin;
2156
+ const thin = sizes.thin;
2157
+ {
2158
+ const r = refDim * 0.045;
2159
+ const cx = b.maxX - r;
2160
+ const cy = b.minY - margin * 0.55;
2161
+ const deg = northDegrees(scene.north);
2162
+ const fs = refDim * 0.026;
2163
+ doc.save();
2164
+ doc.rotate(deg, { origin: [cx, cy] });
2165
+ doc.polygon([cx, cy - r], [cx - r * 0.5, cy + r * 0.6], [cx, cy + r * 0.25], [cx + r * 0.5, cy + r * 0.6]).fill(theme.annotation);
2166
+ doc.restore();
2167
+ const rad = deg * Math.PI / 180;
2168
+ const lx = cx + Math.sin(rad) * (r + fs * 0.8);
2169
+ const ly = cy - Math.cos(rad) * (r + fs * 0.8);
2170
+ drawText(doc, { x: lx, y: ly }, "N", fs, "middle", void 0, theme.annotation);
2171
+ }
2172
+ {
2173
+ const barLen = niceBarLength2(refDim * 0.3);
2174
+ const x0 = b.minX;
2175
+ const y0 = b.maxY + margin * 0.55;
2176
+ const hgt = refDim * 0.014;
2177
+ const fs = refDim * 0.02;
2178
+ const half = barLen / 2;
2179
+ doc.rect(x0, y0, half, hgt).fill(theme.annotation);
2180
+ doc.lineWidth(thin).undash();
2181
+ doc.rect(x0 + half, y0, half, hgt).stroke(theme.annotation);
2182
+ drawText(doc, { x: x0, y: y0 + hgt + fs }, "0", fs, "start", void 0, theme.annotation);
2183
+ drawText(doc, { x: x0 + barLen, y: y0 + hgt + fs }, `${barLen / 1e3} m`, fs, "middle", void 0, theme.annotation);
2184
+ }
2185
+ const t = scene.title;
2186
+ if (t || scene.scale) {
2187
+ const boxW = refDim * 0.34;
2188
+ const boxH = margin * 0.82;
2189
+ const x0 = b.maxX - boxW;
2190
+ const y0 = b.maxY + margin * 0.15;
2191
+ const fs = refDim * 0.019;
2192
+ const pad = boxW * 0.05;
2193
+ const lines = [];
2194
+ if (t?.project) lines.push({ k: "PROJECT", v: t.project });
2195
+ if (t?.drawnBy) lines.push({ k: "DRAWN BY", v: t.drawnBy });
2196
+ if (t?.date) lines.push({ k: "DATE", v: t.date });
2197
+ if (scene.scale) lines.push({ k: "SCALE", v: scene.scale });
2198
+ doc.lineWidth(thin).undash();
2199
+ doc.rect(x0, y0, boxW, boxH).stroke(theme.annotation);
2200
+ const rowH = boxH / Math.max(lines.length, 1);
2201
+ lines.forEach((ln, i) => {
2202
+ const ly = y0 + rowH * (i + 0.5);
2203
+ drawText(doc, { x: x0 + pad, y: ly }, ln.k, fs * 0.8, "start", void 0, theme.annotationMuted);
2204
+ drawText(doc, { x: x0 + boxW - pad, y: ly }, ln.v, fs, "end", void 0, theme.annotation);
2205
+ if (i > 0) {
2206
+ doc.lineWidth(thin * 0.5).moveTo(x0, y0 + rowH * i).lineTo(x0 + boxW, y0 + rowH * i).stroke(theme.annotation);
2207
+ }
2208
+ });
2209
+ }
2210
+ }
2211
+ function northDegrees(north) {
2212
+ switch (north) {
2213
+ case "up":
2214
+ return 0;
2215
+ case "down":
2216
+ return 180;
2217
+ case "left":
2218
+ return 270;
2219
+ case "right":
2220
+ return 90;
2221
+ default:
2222
+ return typeof north === "object" ? north.deg : 0;
2223
+ }
2224
+ }
2225
+ var NICE_LENGTHS2 = [500, 1e3, 2e3, 5e3, 1e4, 2e4, 5e4, 1e5];
2226
+ function niceBarLength2(target) {
2227
+ let best = NICE_LENGTHS2[0];
2228
+ for (const v of NICE_LENGTHS2) if (v <= target) best = v;
2229
+ return best;
2230
+ }
2057
2231
  function concat(chunks) {
2058
2232
  let total = 0;
2059
2233
  for (const c of chunks) total += c.length;
@@ -2097,8 +2271,13 @@ function compileUncached(source, opts) {
2097
2271
  const errs = diagnostics.filter((d) => d.severity === "error");
2098
2272
  const errors = errs.map((d) => toLegacy(source, d));
2099
2273
  const warnings = diagnostics.filter((d) => d.severity === "warning").map((d) => toLegacy(source, d));
2100
- const svg = resolved && errs.length === 0 ? render(resolved.ir, opts) : "";
2101
- return { svg, errors, warnings, diagnostics, ast: plan };
2274
+ let svg = "";
2275
+ let scene;
2276
+ if (resolved && errs.length === 0) {
2277
+ scene = toScene(resolved.ir, opts);
2278
+ svg = renderSvg(scene, opts);
2279
+ }
2280
+ return { svg, errors, warnings, diagnostics, ast: plan, scene };
2102
2281
  }
2103
2282
  function clearCache() {
2104
2283
  cache.clear();
@@ -2106,6 +2285,7 @@ function clearCache() {
2106
2285
 
2107
2286
  export {
2108
2287
  resolve,
2288
+ toScene,
2109
2289
  offsetToLineCol,
2110
2290
  formatDiagnostic,
2111
2291
  toDxf,
@@ -2113,4 +2293,4 @@ export {
2113
2293
  compile,
2114
2294
  clearCache
2115
2295
  };
2116
- //# sourceMappingURL=chunk-PABYLU6Z.js.map
2296
+ //# sourceMappingURL=chunk-CPK5CI5Y.js.map