@chanmeng666/archlang 0.5.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.
@@ -352,6 +352,15 @@ function mergeTheme(...layers) {
352
352
  }
353
353
  return out;
354
354
  }
355
+ function sanitizeTheme(theme) {
356
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
357
+ const out = { ...theme };
358
+ for (const k of Object.keys(out)) {
359
+ const v = out[k];
360
+ if (typeof v === "string") out[k] = esc(v);
361
+ }
362
+ return out;
363
+ }
355
364
 
356
365
  // src/geometry.ts
357
366
  var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
@@ -387,6 +396,13 @@ function distPointToSegment(p, a, b) {
387
396
  const cy = a.y + t * aby;
388
397
  return Math.hypot(p.x - cx, p.y - cy);
389
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
+ }
390
406
  function rectCorners(x, y, w, h) {
391
407
  return [
392
408
  { x, y },
@@ -418,30 +434,23 @@ function segmentsOfWall(w) {
418
434
  }
419
435
  return segs;
420
436
  }
421
- function hostSegmentForWalls(walls, at, ref) {
437
+ function hostInfoForWalls(walls, at, ref) {
422
438
  const candidates = ref ? walls.filter((w) => w.id === ref || w.category === ref) : walls;
423
- let best = null;
439
+ let host = null;
424
440
  let bestDist = Infinity;
441
+ let onWall = false;
425
442
  for (const w of candidates) {
443
+ const tol = w.thickness / 2 + Math.max(w.thickness, 1);
426
444
  for (const s of segmentsOfWall(w)) {
427
445
  const dist = distPointToSegment(at, s.a, s.b);
428
446
  if (dist < bestDist) {
429
447
  bestDist = dist;
430
- best = s;
448
+ host = s;
431
449
  }
450
+ if (!onWall && dist <= tol) onWall = true;
432
451
  }
433
452
  }
434
- return best;
435
- }
436
- function isOnSomeWall(walls, at, ref) {
437
- const candidates = ref ? walls.filter((w) => w.id === ref || w.category === ref) : walls;
438
- for (const w of candidates) {
439
- const tol = w.thickness / 2 + Math.max(w.thickness, 1);
440
- for (const s of segmentsOfWall(w)) {
441
- if (distPointToSegment(at, s.a, s.b) <= tol) return true;
442
- }
443
- }
444
- return false;
453
+ return { host, onWall };
445
454
  }
446
455
 
447
456
  // src/hatches.ts
@@ -545,33 +554,29 @@ var wall = {
545
554
  const w = resolved;
546
555
  return segmentsOfWall(w).flatMap((s) => segmentRectangle(s.a, s.b, s.thickness));
547
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
+ */
548
562
  render(resolved, ctx) {
549
563
  const w = resolved;
550
- const { fmt: fmt2, pt: pt2, theme, sizes } = ctx;
564
+ const { theme, sizes } = ctx;
551
565
  const segs = segmentsOfWall(w);
552
- const ops = [];
566
+ const nodes = [];
553
567
  for (const s of segs) {
554
568
  const poly = segmentRectangle(s.a, s.b, s.thickness);
555
- 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)" } });
556
570
  }
557
571
  for (const s of segs) {
558
572
  const d = unit(sub(s.b, s.a));
559
573
  const n = normal(d);
560
574
  const h = s.thickness / 2;
561
- const fa1 = add(s.a, mul(n, h));
562
- const fb1 = add(s.b, mul(n, h));
563
- const fa2 = add(s.a, mul(n, -h));
564
- const fb2 = add(s.b, mul(n, -h));
565
- ops.push({
566
- pass: "wallFace",
567
- 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"/>`
568
- });
569
- ops.push({
570
- pass: "wallFace",
571
- 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"/>`
572
- });
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 });
573
578
  }
574
- return ops;
579
+ return nodes;
575
580
  }
576
581
  };
577
582
  function describe2(ctx) {
@@ -616,24 +621,26 @@ var room = {
616
621
  },
617
622
  render(resolved, ctx) {
618
623
  const r = resolved;
619
- const { fmt: fmt2, pt: pt2, xml: xml2, theme, sizes } = ctx;
620
- const ops = [];
624
+ const { theme, sizes } = ctx;
625
+ const nodes = [];
621
626
  const c = rectCorners(r.at.x, r.at.y, r.size.w, r.size.h);
622
- 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 } });
623
628
  const cx = r.at.x + r.size.w / 2;
624
629
  const cy = r.at.y + r.size.h / 2;
625
630
  const areaM2 = (r.size.w / 1e3 * (r.size.h / 1e3)).toFixed(1);
626
631
  if (r.label) {
627
- ops.push({
628
- pass: "labels",
629
- 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 }
630
636
  });
631
637
  }
632
- ops.push({
633
- pass: "labels",
634
- 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 }
635
642
  });
636
- return ops;
643
+ return nodes;
637
644
  }
638
645
  };
639
646
 
@@ -683,11 +690,17 @@ var door = {
683
690
  return { kind: "door", id, at, width, hinge: n.hinge, swing: n.swing, host: ctx.hostSegment(at, n.wall), span: n.span };
684
691
  },
685
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
+ */
686
699
  render(resolved, ctx) {
687
700
  const dr = resolved;
688
701
  const seg = dr.host;
689
702
  if (!seg) return [];
690
- const { fmt: fmt2, pt: pt2, theme, sizes } = ctx;
703
+ const { theme, sizes } = ctx;
691
704
  const d = unit(sub(seg.b, seg.a));
692
705
  const n = normal(d);
693
706
  const h = seg.thickness / 2 + sizes.wallStroke;
@@ -698,23 +711,25 @@ var door = {
698
711
  add(add(dr.at, mul(d, hw)), mul(n, -h)),
699
712
  add(add(dr.at, mul(d, -hw)), mul(n, -h))
700
713
  ];
701
- const ops = [];
702
- 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 } });
703
716
  const hinge = dr.hinge === "left" ? add(dr.at, mul(d, -hw)) : add(dr.at, mul(d, hw));
704
717
  const farJamb = dr.hinge === "left" ? add(dr.at, mul(d, hw)) : add(dr.at, mul(d, -hw));
705
718
  const leafDir = dr.swing === "in" ? n : mul(n, -1);
706
719
  const leafEnd = add(hinge, mul(leafDir, dr.width));
707
720
  const cross = (leafEnd.x - hinge.x) * (farJamb.y - hinge.y) - (leafEnd.y - hinge.y) * (farJamb.x - hinge.x);
708
721
  const sweep = cross < 0 ? 1 : 0;
709
- ops.push({
710
- pass: "doors",
711
- 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 }
712
726
  });
713
- ops.push({
714
- pass: "doors",
715
- 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] }
716
731
  });
717
- return ops;
732
+ return nodes;
718
733
  }
719
734
  };
720
735
 
@@ -756,7 +771,7 @@ var windowEl = {
756
771
  const wn = resolved;
757
772
  const seg = wn.host;
758
773
  if (!seg) return [];
759
- const { fmt: fmt2, pt: pt2, theme, sizes } = ctx;
774
+ const { theme, sizes } = ctx;
760
775
  const d = unit(sub(seg.b, seg.a));
761
776
  const n = normal(d);
762
777
  const h = seg.thickness / 2;
@@ -768,23 +783,23 @@ var windowEl = {
768
783
  add(add(wn.at, mul(d, hw)), mul(n, -he)),
769
784
  add(add(wn.at, mul(d, -hw)), mul(n, -he))
770
785
  ];
771
- const ops = [];
772
- 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 } });
773
788
  const jA = add(wn.at, mul(d, -hw));
774
789
  const jB = add(wn.at, mul(d, hw));
775
790
  for (const off of [h, -h]) {
776
- const a = add(jA, mul(n, off));
777
- const bb = add(jB, mul(n, off));
778
- ops.push({
779
- pass: "windows",
780
- 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 }
781
795
  });
782
796
  }
783
- ops.push({
784
- pass: "windows",
785
- 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 }
786
801
  });
787
- return ops;
802
+ return nodes;
788
803
  }
789
804
  };
790
805
 
@@ -824,22 +839,24 @@ var furniture = {
824
839
  },
825
840
  render(resolved, ctx) {
826
841
  const f = resolved;
827
- const { fmt: fmt2, pt: pt2, xml: xml2, theme, sizes } = ctx;
828
- const ops = [];
842
+ const { theme, sizes } = ctx;
843
+ const nodes = [];
829
844
  const c = rectCorners(f.at.x, f.at.y, f.size.w, f.size.h);
830
- ops.push({
831
- pass: "furniture",
832
- 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 }
833
849
  });
834
850
  if (f.label) {
835
851
  const cx = f.at.x + f.size.w / 2;
836
852
  const cy = f.at.y + f.size.h / 2;
837
- ops.push({
838
- pass: "furniture",
839
- 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 }
840
857
  });
841
858
  }
842
- return ops;
859
+ return nodes;
843
860
  }
844
861
  };
845
862
 
@@ -882,33 +899,22 @@ var dim = {
882
899
  },
883
900
  render(resolved, ctx) {
884
901
  const dm = resolved;
885
- const { fmt: fmt2, xml: xml2, theme, sizes } = ctx;
902
+ const { theme, sizes } = ctx;
886
903
  const dir = unit(sub(dm.to, dm.from));
887
904
  const n = normal(dir);
888
905
  const off = mul(n, dm.offset);
889
906
  const p1 = add(dm.from, off);
890
907
  const p2 = add(dm.to, off);
891
908
  const tick = sizes.refDim * 0.012;
892
- const ops = [];
893
- ops.push({
894
- pass: "dims",
895
- 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)}"/>`
896
- });
897
- ops.push({
898
- pass: "dims",
899
- 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)}"/>`
900
- });
901
- ops.push({
902
- pass: "dims",
903
- 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)}"/>`
904
- });
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 });
905
914
  for (const p of [p1, p2]) {
906
915
  const t1 = add(p, mul(unit({ x: dir.x + n.x, y: dir.y + n.y }), tick));
907
916
  const t2 = add(p, mul(unit({ x: dir.x + n.x, y: dir.y + n.y }), -tick));
908
- ops.push({
909
- pass: "dims",
910
- 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)}"/>`
911
- });
917
+ nodes.push({ layer: "dims", prim: { t: "line", a: t1, b: t2 }, paint: thinPaint });
912
918
  }
913
919
  const mid = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };
914
920
  const tp = add(mid, mul(n, sizes.dimFont * 0.7));
@@ -916,11 +922,12 @@ var dim = {
916
922
  if (angle > 90) angle -= 180;
917
923
  if (angle < -90) angle += 180;
918
924
  const label = dm.text ?? String(Math.round(length(sub(dm.to, dm.from))));
919
- ops.push({
920
- pass: "dims",
921
- 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 }
922
929
  });
923
- return ops;
930
+ return nodes;
924
931
  }
925
932
  };
926
933
 
@@ -954,12 +961,13 @@ var column = {
954
961
  },
955
962
  render(resolved, ctx) {
956
963
  const c = resolved;
957
- const { fmt: fmt2, pt: pt2, theme, sizes } = ctx;
964
+ const { theme, sizes } = ctx;
958
965
  const pts = rectCorners(c.at.x, c.at.y, c.size.w, c.size.h);
959
966
  return [
960
967
  {
961
- pass: "furniture",
962
- 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 }
963
971
  }
964
972
  ];
965
973
  }
@@ -1427,6 +1435,15 @@ function resolve(ast) {
1427
1435
  let activeEnv = /* @__PURE__ */ new Map();
1428
1436
  const evalNum = (e) => evalExpr(e, activeEnv, (d) => diagnostics.push(d));
1429
1437
  const evalPt = (p) => ({ x: evalNum(p.x), y: evalNum(p.y) });
1438
+ let hiKey = "";
1439
+ let hiVal = null;
1440
+ const hostInfo = (at, ref) => {
1441
+ const key = `${at.x},${at.y},${ref ?? ""}`;
1442
+ if (key === hiKey && hiVal) return hiVal;
1443
+ hiKey = key;
1444
+ hiVal = hostInfoForWalls(walls, at, ref);
1445
+ return hiVal;
1446
+ };
1430
1447
  const ctx = {
1431
1448
  grid: g,
1432
1449
  snap,
@@ -1435,8 +1452,8 @@ function resolve(ast) {
1435
1452
  evalPt,
1436
1453
  id: "",
1437
1454
  walls,
1438
- hostSegment: (at, ref) => hostSegmentForWalls(walls, at, ref),
1439
- isOnWall: (at, ref) => isOnSomeWall(walls, at, ref),
1455
+ hostSegment: (at, ref) => hostInfo(at, ref).host,
1456
+ isOnWall: (at, ref) => hostInfo(at, ref).onWall,
1440
1457
  diag: (d) => diagnostics.push(d)
1441
1458
  };
1442
1459
  for (const def of registryOrder) {
@@ -1491,19 +1508,6 @@ function resolve(ast) {
1491
1508
  return { ir, diagnostics };
1492
1509
  }
1493
1510
 
1494
- // src/registry.ts
1495
- var RENDER_PASSES = [
1496
- "floor",
1497
- "furniture",
1498
- "wallFill",
1499
- "wallFace",
1500
- "doors",
1501
- "windows",
1502
- "labels",
1503
- "dims",
1504
- "annotations"
1505
- ];
1506
-
1507
1511
  // src/geometry/union.ts
1508
1512
  function uniqSorted(values) {
1509
1513
  const out = [...new Set(values)].sort((a, b) => a - b);
@@ -1601,21 +1605,7 @@ function mergeCollinear(loop) {
1601
1605
  return out.length >= 3 ? out : loop;
1602
1606
  }
1603
1607
 
1604
- // src/render.ts
1605
- function fmt(v) {
1606
- const r = Math.round(v * 100) / 100;
1607
- return Object.is(r, -0) ? "0" : String(r);
1608
- }
1609
- var pt = (p) => `${fmt(p.x)},${fmt(p.y)}`;
1610
- function xml(s) {
1611
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1612
- }
1613
- var NICE_LENGTHS = [500, 1e3, 2e3, 5e3, 1e4, 2e4, 5e4, 1e5];
1614
- function niceBarLength(target) {
1615
- let best = NICE_LENGTHS[0];
1616
- for (const v of NICE_LENGTHS) if (v <= target) best = v;
1617
- return best;
1618
- }
1608
+ // src/scene-build.ts
1619
1609
  function planBounds(ir) {
1620
1610
  const b = emptyBounds();
1621
1611
  for (const el of ir.elements) {
@@ -1631,20 +1621,17 @@ function planBounds(ir) {
1631
1621
  function allOrthogonal(walls) {
1632
1622
  return walls.every((w) => segmentsOfWall(w).every((s) => s.a.x === s.b.x || s.a.y === s.b.y));
1633
1623
  }
1634
- function loopsToPath(loops) {
1635
- return loops.map((loop) => "M " + loop.map(pt).join(" L ") + " Z").join(" ");
1636
- }
1637
1624
  function materialsUsed(walls) {
1638
1625
  return [...new Set(walls.map((w) => w.material))].sort();
1639
1626
  }
1640
- function renderWalls(walls, ctx) {
1627
+ function lowerWalls(walls, ctx) {
1641
1628
  if (walls.length === 0) return [];
1642
- const ops = [];
1629
+ const nodes = [];
1643
1630
  for (const mat of materialsUsed(walls)) {
1644
1631
  const group = walls.filter((w) => w.material === mat);
1645
1632
  if (!allOrthogonal(group)) {
1646
1633
  const def = registry.get("wall");
1647
- ops.push(...group.flatMap((w) => def.render(w, ctx)));
1634
+ nodes.push(...group.flatMap((w) => def.render(w, ctx)));
1648
1635
  continue;
1649
1636
  }
1650
1637
  const rects = [];
@@ -1658,18 +1645,18 @@ function renderWalls(walls, ctx) {
1658
1645
  }
1659
1646
  const loops = rectUnionOutline(rects);
1660
1647
  if (loops.length === 0) continue;
1661
- const d = loopsToPath(loops);
1662
- ops.push({ pass: "wallFill", svg: `<path d="${d}" fill="url(#${patternId(mat)})" fill-rule="nonzero"/>` });
1663
- ops.push({
1664
- pass: "wallFace",
1665
- 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" }
1666
1653
  });
1667
1654
  }
1668
- return ops;
1655
+ return nodes;
1669
1656
  }
1670
- function render(ir, opts = {}) {
1671
- const THEME = mergeTheme(DEFAULT_THEME, ir.theme, opts.theme);
1672
- 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;
1673
1660
  const b = planBounds(ir);
1674
1661
  const drawW = b.maxX - b.minX;
1675
1662
  const drawH = b.maxY - b.minY;
@@ -1685,7 +1672,100 @@ function render(ir, opts = {}) {
1685
1672
  margin: refDim * 0.17,
1686
1673
  hatchGap: refDim * 0.013
1687
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;
1688
1766
  const { thin, margin, hatchGap } = sizes;
1767
+ const drawW = b.maxX - b.minX;
1768
+ const drawH = b.maxY - b.minY;
1689
1769
  const vbX = b.minX - margin;
1690
1770
  const vbY = b.minY - margin;
1691
1771
  const vbW = drawW + margin * 2;
@@ -1693,35 +1773,28 @@ function render(ir, opts = {}) {
1693
1773
  const out = [];
1694
1774
  const svgAttrs = opts.width ? `width="${fmt(opts.width)}" height="${fmt(opts.width * vbH / vbW)}"` : "";
1695
1775
  out.push(
1696
- `<svg xmlns="http://www.w3.org/2000/svg" ${svgAttrs} viewBox="${fmt(vbX)} ${fmt(vbY)} ${fmt(vbW)} ${fmt(vbH)}" font-family="${xml(THEME.font)}">`
1776
+ `<svg xmlns="http://www.w3.org/2000/svg" ${svgAttrs} viewBox="${fmt(vbX)} ${fmt(vbY)} ${fmt(vbW)} ${fmt(vbH)}" font-family="${THEME.font}">`
1697
1777
  );
1698
1778
  const hatchCtx = { fmt, gap: hatchGap, thin, base: THEME.pocheBase, line: THEME.pocheHatch };
1699
- const patterns = materialsUsed(ir.walls).map((m) => hatchPattern(m, hatchCtx)).join("");
1779
+ const patterns = scene.materials.map((m) => hatchPattern(m, hatchCtx)).join("");
1700
1780
  out.push(`<defs>${patterns}</defs>`);
1701
1781
  out.push(`<rect x="${fmt(vbX)}" y="${fmt(vbY)}" width="${fmt(vbW)}" height="${fmt(vbH)}" fill="${THEME.bg}"/>`);
1702
- const ctx = { fmt, pt, xml, theme: THEME, sizes, bounds: b };
1703
- const ops = ir.elements.flatMap((el) => {
1704
- if (el.kind === "wall") return [];
1705
- const def = registry.get(el.kind);
1706
- return def ? def.render(el, ctx) : [];
1707
- });
1708
- ops.push(...renderWalls(ir.walls, ctx));
1709
1782
  for (const pass of RENDER_PASSES) {
1710
- 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));
1711
1784
  }
1712
- out.push(northArrow(ir, b, margin, refDim, THEME));
1785
+ out.push(northArrow(scene.north, b, margin, refDim, THEME));
1713
1786
  out.push(scaleBar(b, margin, refDim, thin, THEME));
1714
- const tb = titleBlock(ir, b, margin, refDim, thin, THEME);
1787
+ const tb = titleBlock(scene.title, scene.scale, b, margin, refDim, thin, THEME);
1715
1788
  if (tb) out.push(tb);
1716
1789
  out.push("</svg>");
1717
1790
  return out.join("\n");
1718
1791
  }
1719
- function northArrow(ir, b, margin, refDim, THEME) {
1792
+ function northArrow(north, b, margin, refDim, THEME) {
1720
1793
  const r = refDim * 0.045;
1721
1794
  const cx = b.maxX - r;
1722
1795
  const cy = b.minY - margin * 0.55;
1723
1796
  let deg;
1724
- switch (ir.north) {
1797
+ switch (north) {
1725
1798
  case "up":
1726
1799
  deg = 0;
1727
1800
  break;
@@ -1735,7 +1808,7 @@ function northArrow(ir, b, margin, refDim, THEME) {
1735
1808
  deg = 90;
1736
1809
  break;
1737
1810
  default:
1738
- deg = typeof ir.north === "object" ? ir.north.deg : 0;
1811
+ deg = typeof north === "object" ? north.deg : 0;
1739
1812
  }
1740
1813
  const fs = refDim * 0.026;
1741
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)}`;
@@ -1766,9 +1839,8 @@ function scaleBar(b, margin, refDim, thin, THEME) {
1766
1839
  );
1767
1840
  return `<g>${parts.join("")}</g>`;
1768
1841
  }
1769
- function titleBlock(ir, b, margin, refDim, thin, THEME) {
1770
- const t = ir.title;
1771
- if (!t && !ir.scale) return null;
1842
+ function titleBlock(t, scale, b, margin, refDim, thin, THEME) {
1843
+ if (!t && !scale) return null;
1772
1844
  const boxW = refDim * 0.34;
1773
1845
  const boxH = margin * 0.82;
1774
1846
  const x0 = b.maxX - boxW;
@@ -1779,7 +1851,7 @@ function titleBlock(ir, b, margin, refDim, thin, THEME) {
1779
1851
  if (t?.project) lines.push({ k: "PROJECT", v: t.project });
1780
1852
  if (t?.drawnBy) lines.push({ k: "DRAWN BY", v: t.drawnBy });
1781
1853
  if (t?.date) lines.push({ k: "DATE", v: t.date });
1782
- if (ir.scale) lines.push({ k: "SCALE", v: ir.scale });
1854
+ if (scale) lines.push({ k: "SCALE", v: scale });
1783
1855
  const parts = [];
1784
1856
  parts.push(
1785
1857
  `<rect x="${fmt(x0)}" y="${fmt(y0)}" width="${fmt(boxW)}" height="${fmt(boxH)}" fill="none" stroke="${THEME.annotation}" stroke-width="${fmt(thin)}"/>`
@@ -1828,8 +1900,8 @@ function lineEnd(source, offset) {
1828
1900
  }
1829
1901
  function formatDiagnostic(source, d) {
1830
1902
  const codeTag = d.code ? `[${d.code}]` : "";
1831
- const header = `${d.severity}${codeTag}: ${d.message}`;
1832
- const lines = [header];
1903
+ const header2 = `${d.severity}${codeTag}: ${d.message}`;
1904
+ const lines = [header2];
1833
1905
  if (d.span) {
1834
1906
  const { line, col } = offsetToLineCol(source, d.span.start);
1835
1907
  const ls = lineStart(source, d.span.start);
@@ -1855,6 +1927,319 @@ function formatDiagnostic(source, d) {
1855
1927
  return lines.join("\n");
1856
1928
  }
1857
1929
 
1930
+ // src/export/dxf.ts
1931
+ function num(v) {
1932
+ const r = Math.round(v * 1e4) / 1e4;
1933
+ return Object.is(r, -0) ? "0" : String(r);
1934
+ }
1935
+ var DxfBuilder = class {
1936
+ out = [];
1937
+ /** group-code / value pair. */
1938
+ pair(code, value) {
1939
+ this.out.push(String(code), String(value));
1940
+ }
1941
+ line(layer, a, b) {
1942
+ this.pair(0, "LINE");
1943
+ this.pair(8, layer);
1944
+ this.pair(10, num(a.x));
1945
+ this.pair(20, num(-a.y));
1946
+ this.pair(11, num(b.x));
1947
+ this.pair(21, num(-b.y));
1948
+ }
1949
+ arc(layer, center, radius, startDeg, endDeg) {
1950
+ this.pair(0, "ARC");
1951
+ this.pair(8, layer);
1952
+ this.pair(10, num(center.x));
1953
+ this.pair(20, num(-center.y));
1954
+ this.pair(40, num(radius));
1955
+ this.pair(50, num(startDeg));
1956
+ this.pair(51, num(endDeg));
1957
+ }
1958
+ text(layer, at, height, value) {
1959
+ this.pair(0, "TEXT");
1960
+ this.pair(8, layer);
1961
+ this.pair(10, num(at.x));
1962
+ this.pair(20, num(-at.y));
1963
+ this.pair(40, num(height));
1964
+ this.pair(1, value.replace(/\n/g, " "));
1965
+ }
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]);
1970
+ }
1971
+ }
1972
+ toString() {
1973
+ return this.out.join("\n") + "\n";
1974
+ }
1975
+ };
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
+ }
1998
+ function header() {
1999
+ const h = [];
2000
+ const p = (c, v) => h.push(String(c), String(v));
2001
+ p(0, "SECTION");
2002
+ p(2, "HEADER");
2003
+ p(9, "$ACADVER");
2004
+ p(1, "AC1009");
2005
+ p(0, "ENDSEC");
2006
+ p(0, "SECTION");
2007
+ p(2, "TABLES");
2008
+ p(0, "TABLE");
2009
+ p(2, "LAYER");
2010
+ p(70, LAYERS.length);
2011
+ for (const name of LAYERS) {
2012
+ p(0, "LAYER");
2013
+ p(2, name);
2014
+ p(70, 0);
2015
+ p(62, 7);
2016
+ p(6, "CONTINUOUS");
2017
+ }
2018
+ p(0, "ENDTAB");
2019
+ p(0, "ENDSEC");
2020
+ return h.join("\n") + "\n";
2021
+ }
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;
2043
+ }
2044
+ }
2045
+ function toDxf(scene) {
2046
+ const b = new DxfBuilder();
2047
+ b.pair(0, "SECTION");
2048
+ b.pair(2, "ENTITIES");
2049
+ for (const node of scene.nodes) emit(b, node);
2050
+ b.pair(0, "ENDSEC");
2051
+ return header() + b.toString() + "0\nEOF\n";
2052
+ }
2053
+
2054
+ // src/export/pdf.ts
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) {
2119
+ let PDFDocument;
2120
+ try {
2121
+ PDFDocument = (await import("pdfkit")).default;
2122
+ } catch {
2123
+ throw new Error(
2124
+ "PDF export needs the optional dependency 'pdfkit'. Install it: npm install pdfkit"
2125
+ );
2126
+ }
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 });
2134
+ const chunks = [];
2135
+ const done = new Promise((resolve2, reject) => {
2136
+ doc.on("data", (c) => chunks.push(c));
2137
+ doc.on("end", () => resolve2());
2138
+ doc.on("error", (e) => reject(e));
2139
+ });
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();
2148
+ doc.end();
2149
+ await done;
2150
+ return concat(chunks);
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
+ }
2231
+ function concat(chunks) {
2232
+ let total = 0;
2233
+ for (const c of chunks) total += c.length;
2234
+ const out = new Uint8Array(total);
2235
+ let offset = 0;
2236
+ for (const c of chunks) {
2237
+ out.set(c, offset);
2238
+ offset += c.length;
2239
+ }
2240
+ return out;
2241
+ }
2242
+
1858
2243
  // src/index.ts
1859
2244
  var cache = /* @__PURE__ */ new Map();
1860
2245
  var CACHE_MAX = 64;
@@ -1886,17 +2271,26 @@ function compileUncached(source, opts) {
1886
2271
  const errs = diagnostics.filter((d) => d.severity === "error");
1887
2272
  const errors = errs.map((d) => toLegacy(source, d));
1888
2273
  const warnings = diagnostics.filter((d) => d.severity === "warning").map((d) => toLegacy(source, d));
1889
- const svg = resolved && errs.length === 0 ? render(resolved.ir, opts) : "";
1890
- 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 };
1891
2281
  }
1892
2282
  function clearCache() {
1893
2283
  cache.clear();
1894
2284
  }
1895
2285
 
1896
2286
  export {
2287
+ resolve,
2288
+ toScene,
1897
2289
  offsetToLineCol,
1898
2290
  formatDiagnostic,
2291
+ toDxf,
2292
+ toPdf,
1899
2293
  compile,
1900
2294
  clearCache
1901
2295
  };
1902
- //# sourceMappingURL=chunk-GUNWYUR2.js.map
2296
+ //# sourceMappingURL=chunk-CPK5CI5Y.js.map