schematex 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +42 -2
  2. package/README.zh-CN.md +2 -2
  3. package/dist/ai/ai-sdk.cjs +23 -23
  4. package/dist/ai/ai-sdk.d.cts +3 -3
  5. package/dist/ai/ai-sdk.d.ts +3 -3
  6. package/dist/ai/ai-sdk.js +18 -18
  7. package/dist/ai/index.cjs +32 -32
  8. package/dist/ai/index.d.cts +4 -4
  9. package/dist/ai/index.d.ts +4 -4
  10. package/dist/ai/index.js +19 -19
  11. package/dist/{api-OED2jUVj.d.ts → api-1xoGiseb.d.cts} +2 -2
  12. package/dist/{api-BDMaX1cT.d.cts → api-CgXRSnCn.d.ts} +2 -2
  13. package/dist/browser.cjs +24 -24
  14. package/dist/browser.d.cts +3 -3
  15. package/dist/browser.d.ts +3 -3
  16. package/dist/browser.js +18 -18
  17. package/dist/{chunk-HILYXWGJ.cjs → chunk-35NGXDT2.cjs} +12 -12
  18. package/dist/{chunk-HILYXWGJ.cjs.map → chunk-35NGXDT2.cjs.map} +1 -1
  19. package/dist/{chunk-HJA7MIMV.js → chunk-3J4DZPZC.js} +3 -3
  20. package/dist/{chunk-HJA7MIMV.js.map → chunk-3J4DZPZC.js.map} +1 -1
  21. package/dist/{chunk-IQLZUCWI.js → chunk-3PH2MQGN.js} +3 -3
  22. package/dist/{chunk-IQLZUCWI.js.map → chunk-3PH2MQGN.js.map} +1 -1
  23. package/dist/{chunk-VZVREOTM.js → chunk-3WX24RCH.js} +3 -3
  24. package/dist/{chunk-VZVREOTM.js.map → chunk-3WX24RCH.js.map} +1 -1
  25. package/dist/{chunk-XA6XIAMN.cjs → chunk-4AC6I7KJ.cjs} +4 -4
  26. package/dist/{chunk-XA6XIAMN.cjs.map → chunk-4AC6I7KJ.cjs.map} +1 -1
  27. package/dist/{chunk-TYEQC7PV.js → chunk-4MRVJI7G.js} +166 -5
  28. package/dist/chunk-4MRVJI7G.js.map +1 -0
  29. package/dist/{chunk-6BJKQULN.cjs → chunk-4OC3CTGE.cjs} +4 -4
  30. package/dist/{chunk-6BJKQULN.cjs.map → chunk-4OC3CTGE.cjs.map} +1 -1
  31. package/dist/{chunk-4MGALM2C.cjs → chunk-5ZQRHDMQ.cjs} +4 -4
  32. package/dist/{chunk-4MGALM2C.cjs.map → chunk-5ZQRHDMQ.cjs.map} +1 -1
  33. package/dist/{chunk-PSQGLE6U.cjs → chunk-627GHE2N.cjs} +5 -5
  34. package/dist/{chunk-PSQGLE6U.cjs.map → chunk-627GHE2N.cjs.map} +1 -1
  35. package/dist/{chunk-56DSMSXK.js → chunk-64LABNTF.js} +3 -3
  36. package/dist/{chunk-56DSMSXK.js.map → chunk-64LABNTF.js.map} +1 -1
  37. package/dist/{chunk-GYTU7L4L.cjs → chunk-6QZQTASC.cjs} +12 -12
  38. package/dist/{chunk-GYTU7L4L.cjs.map → chunk-6QZQTASC.cjs.map} +1 -1
  39. package/dist/{chunk-MYVY55DO.cjs → chunk-6ZD7TCWO.cjs} +15 -15
  40. package/dist/{chunk-MYVY55DO.cjs.map → chunk-6ZD7TCWO.cjs.map} +1 -1
  41. package/dist/{chunk-7Z2GDS4G.js → chunk-AXMBXAEA.js} +3 -3
  42. package/dist/{chunk-7Z2GDS4G.js.map → chunk-AXMBXAEA.js.map} +1 -1
  43. package/dist/{chunk-VY4JOTM2.js → chunk-B4CMWA6Y.js} +3 -3
  44. package/dist/{chunk-VY4JOTM2.js.map → chunk-B4CMWA6Y.js.map} +1 -1
  45. package/dist/{chunk-2ASZMLC3.cjs → chunk-C4Y24X3U.cjs} +4 -4
  46. package/dist/{chunk-2ASZMLC3.cjs.map → chunk-C4Y24X3U.cjs.map} +1 -1
  47. package/dist/{chunk-NT6VVMLW.cjs → chunk-ENUM7GMZ.cjs} +168 -4
  48. package/dist/chunk-ENUM7GMZ.cjs.map +1 -0
  49. package/dist/{chunk-VYAUTNHC.cjs → chunk-GAQ36VFD.cjs} +4 -4
  50. package/dist/{chunk-VYAUTNHC.cjs.map → chunk-GAQ36VFD.cjs.map} +1 -1
  51. package/dist/{chunk-NTQBNBBP.cjs → chunk-GTCAJPQR.cjs} +4307 -282
  52. package/dist/chunk-GTCAJPQR.cjs.map +1 -0
  53. package/dist/{chunk-KR7PPCC4.js → chunk-GYYYULBL.js} +3 -3
  54. package/dist/{chunk-KR7PPCC4.js.map → chunk-GYYYULBL.js.map} +1 -1
  55. package/dist/{chunk-WFVRUUJW.cjs → chunk-INVLJYAE.cjs} +4 -4
  56. package/dist/{chunk-WFVRUUJW.cjs.map → chunk-INVLJYAE.cjs.map} +1 -1
  57. package/dist/{chunk-2A5HJFOX.cjs → chunk-ITI3STJ6.cjs} +4 -4
  58. package/dist/{chunk-2A5HJFOX.cjs.map → chunk-ITI3STJ6.cjs.map} +1 -1
  59. package/dist/{chunk-TMAOPJCT.js → chunk-JEMAOC2D.js} +3 -3
  60. package/dist/{chunk-TMAOPJCT.js.map → chunk-JEMAOC2D.js.map} +1 -1
  61. package/dist/{chunk-2RFFCZ3I.js → chunk-M26ORU4P.js} +3 -3
  62. package/dist/{chunk-2RFFCZ3I.js.map → chunk-M26ORU4P.js.map} +1 -1
  63. package/dist/{chunk-CURAZVOH.js → chunk-NIYB6CHH.js} +728 -4
  64. package/dist/chunk-NIYB6CHH.js.map +1 -0
  65. package/dist/{chunk-JR6JNQEZ.js → chunk-NKYR4PAS.js} +3 -3
  66. package/dist/{chunk-JR6JNQEZ.js.map → chunk-NKYR4PAS.js.map} +1 -1
  67. package/dist/{chunk-MUE45N2A.cjs → chunk-OJ3P4IC4.cjs} +4 -4
  68. package/dist/{chunk-MUE45N2A.cjs.map → chunk-OJ3P4IC4.cjs.map} +1 -1
  69. package/dist/{chunk-VMUNKEB2.js → chunk-PFZKW3HE.js} +3 -3
  70. package/dist/{chunk-VMUNKEB2.js.map → chunk-PFZKW3HE.js.map} +1 -1
  71. package/dist/{chunk-MERU76LW.js → chunk-RDYACU2G.js} +3 -3
  72. package/dist/{chunk-MERU76LW.js.map → chunk-RDYACU2G.js.map} +1 -1
  73. package/dist/{chunk-TWTUIUNK.js → chunk-SXOAAQNY.js} +3 -3
  74. package/dist/{chunk-TWTUIUNK.js.map → chunk-SXOAAQNY.js.map} +1 -1
  75. package/dist/{chunk-3DSNGR26.cjs → chunk-TX3YWZZX.cjs} +730 -6
  76. package/dist/chunk-TX3YWZZX.cjs.map +1 -0
  77. package/dist/{chunk-TP7LQ5PF.js → chunk-UFXDAIDD.js} +4126 -102
  78. package/dist/chunk-UFXDAIDD.js.map +1 -0
  79. package/dist/{chunk-7JJQEECD.cjs → chunk-VY6UZYYL.cjs} +15 -15
  80. package/dist/{chunk-7JJQEECD.cjs.map → chunk-VY6UZYYL.cjs.map} +1 -1
  81. package/dist/{chunk-V3JTVTPY.cjs → chunk-VYQXB2RC.cjs} +12 -12
  82. package/dist/{chunk-V3JTVTPY.cjs.map → chunk-VYQXB2RC.cjs.map} +1 -1
  83. package/dist/{chunk-L2NY4XEY.cjs → chunk-WJXLF42K.cjs} +4 -4
  84. package/dist/{chunk-L2NY4XEY.cjs.map → chunk-WJXLF42K.cjs.map} +1 -1
  85. package/dist/{chunk-VWIKDJNV.js → chunk-XCCXG6RR.js} +3 -3
  86. package/dist/{chunk-VWIKDJNV.js.map → chunk-XCCXG6RR.js.map} +1 -1
  87. package/dist/{chunk-7SVB3XA7.js → chunk-ZCHGIWJK.js} +3 -3
  88. package/dist/{chunk-7SVB3XA7.js.map → chunk-ZCHGIWJK.js.map} +1 -1
  89. package/dist/{diagnostics-hObcaaFC.d.ts → diagnostics-CDwnQ65n.d.cts} +1 -1
  90. package/dist/{diagnostics-hObcaaFC.d.cts → diagnostics-CDwnQ65n.d.ts} +1 -1
  91. package/dist/diagrams/blockdiagram/index.cjs +6 -6
  92. package/dist/diagrams/blockdiagram/index.d.cts +1 -1
  93. package/dist/diagrams/blockdiagram/index.d.ts +1 -1
  94. package/dist/diagrams/blockdiagram/index.js +2 -2
  95. package/dist/diagrams/circuit/index.cjs +9 -9
  96. package/dist/diagrams/circuit/index.d.cts +1 -1
  97. package/dist/diagrams/circuit/index.d.ts +1 -1
  98. package/dist/diagrams/circuit/index.js +2 -2
  99. package/dist/diagrams/ecomap/index.cjs +7 -7
  100. package/dist/diagrams/ecomap/index.d.cts +1 -1
  101. package/dist/diagrams/ecomap/index.d.ts +1 -1
  102. package/dist/diagrams/ecomap/index.js +2 -2
  103. package/dist/diagrams/entity/index.cjs +6 -6
  104. package/dist/diagrams/entity/index.d.cts +1 -1
  105. package/dist/diagrams/entity/index.d.ts +1 -1
  106. package/dist/diagrams/entity/index.js +2 -2
  107. package/dist/diagrams/fishbone/index.cjs +8 -8
  108. package/dist/diagrams/fishbone/index.d.cts +1 -1
  109. package/dist/diagrams/fishbone/index.d.ts +1 -1
  110. package/dist/diagrams/fishbone/index.js +2 -2
  111. package/dist/diagrams/flowchart/index.cjs +8 -8
  112. package/dist/diagrams/flowchart/index.d.cts +2 -2
  113. package/dist/diagrams/flowchart/index.d.ts +2 -2
  114. package/dist/diagrams/flowchart/index.js +2 -2
  115. package/dist/diagrams/genogram/index.cjs +9 -9
  116. package/dist/diagrams/genogram/index.d.cts +1 -1
  117. package/dist/diagrams/genogram/index.d.ts +1 -1
  118. package/dist/diagrams/genogram/index.js +2 -2
  119. package/dist/diagrams/ladder/index.cjs +6 -6
  120. package/dist/diagrams/ladder/index.d.cts +1 -1
  121. package/dist/diagrams/ladder/index.d.ts +1 -1
  122. package/dist/diagrams/ladder/index.js +2 -2
  123. package/dist/diagrams/logic/index.cjs +8 -8
  124. package/dist/diagrams/logic/index.d.cts +1 -1
  125. package/dist/diagrams/logic/index.d.ts +1 -1
  126. package/dist/diagrams/logic/index.js +2 -2
  127. package/dist/diagrams/orgchart/index.cjs +8 -8
  128. package/dist/diagrams/orgchart/index.d.cts +1 -1
  129. package/dist/diagrams/orgchart/index.d.ts +1 -1
  130. package/dist/diagrams/orgchart/index.js +2 -2
  131. package/dist/diagrams/pedigree/index.cjs +7 -7
  132. package/dist/diagrams/pedigree/index.d.cts +1 -1
  133. package/dist/diagrams/pedigree/index.d.ts +1 -1
  134. package/dist/diagrams/pedigree/index.js +2 -2
  135. package/dist/diagrams/phylo/index.cjs +7 -7
  136. package/dist/diagrams/phylo/index.d.cts +1 -1
  137. package/dist/diagrams/phylo/index.d.ts +1 -1
  138. package/dist/diagrams/phylo/index.js +2 -2
  139. package/dist/diagrams/sld/index.cjs +8 -8
  140. package/dist/diagrams/sld/index.d.cts +1 -1
  141. package/dist/diagrams/sld/index.d.ts +1 -1
  142. package/dist/diagrams/sld/index.js +2 -2
  143. package/dist/diagrams/sociogram/index.cjs +6 -6
  144. package/dist/diagrams/sociogram/index.d.cts +1 -1
  145. package/dist/diagrams/sociogram/index.d.ts +1 -1
  146. package/dist/diagrams/sociogram/index.js +2 -2
  147. package/dist/diagrams/timing/index.d.cts +1 -1
  148. package/dist/diagrams/timing/index.d.ts +1 -1
  149. package/dist/diagrams/venn/index.cjs +9 -9
  150. package/dist/diagrams/venn/index.d.cts +1 -1
  151. package/dist/diagrams/venn/index.d.ts +1 -1
  152. package/dist/diagrams/venn/index.js +2 -2
  153. package/dist/{index-Cq8y1aaa.d.ts → index-CYSH3_ca.d.ts} +1 -1
  154. package/dist/{index-BiXWjQht.d.cts → index-DNOoLmYX.d.cts} +1 -1
  155. package/dist/index.cjs +111 -85
  156. package/dist/index.cjs.map +1 -1
  157. package/dist/index.d.cts +4 -4
  158. package/dist/index.d.ts +4 -4
  159. package/dist/index.js +48 -22
  160. package/dist/index.js.map +1 -1
  161. package/dist/react.cjs +18 -18
  162. package/dist/react.d.cts +2 -2
  163. package/dist/react.d.ts +2 -2
  164. package/dist/react.js +17 -17
  165. package/dist/{tools-BbTuTWs_.d.ts → tools-BhTti-oG.d.ts} +3 -3
  166. package/dist/{tools-D5dkAqNy.d.cts → tools-CbYmQ3hH.d.cts} +3 -3
  167. package/package.json +1 -1
  168. package/dist/chunk-3DSNGR26.cjs.map +0 -1
  169. package/dist/chunk-CURAZVOH.js.map +0 -1
  170. package/dist/chunk-NT6VVMLW.cjs.map +0 -1
  171. package/dist/chunk-NTQBNBBP.cjs.map +0 -1
  172. package/dist/chunk-TP7LQ5PF.js.map +0 -1
  173. package/dist/chunk-TYEQC7PV.js.map +0 -1
@@ -1,21 +1,21 @@
1
- import { orgchart } from './chunk-HJA7MIMV.js';
2
- import { circuit } from './chunk-MERU76LW.js';
3
- import { blockdiagram } from './chunk-IQLZUCWI.js';
4
- import { ladder } from './chunk-VY4JOTM2.js';
5
- import { sld } from './chunk-VMUNKEB2.js';
6
- import { entity } from './chunk-TMAOPJCT.js';
7
- import { fishbone } from './chunk-KR7PPCC4.js';
8
- import { venn } from './chunk-TWTUIUNK.js';
9
- import { flowchart, layoutFlowchart } from './chunk-7Z2GDS4G.js';
10
- import { genogram } from './chunk-2RFFCZ3I.js';
11
- import { ecomap, estimateTextWidth } from './chunk-VWIKDJNV.js';
12
- import { pedigree } from './chunk-7SVB3XA7.js';
1
+ import { orgchart } from './chunk-3J4DZPZC.js';
2
+ import { circuit } from './chunk-RDYACU2G.js';
3
+ import { blockdiagram } from './chunk-3PH2MQGN.js';
4
+ import { ladder } from './chunk-B4CMWA6Y.js';
5
+ import { sld } from './chunk-PFZKW3HE.js';
6
+ import { entity } from './chunk-JEMAOC2D.js';
7
+ import { fishbone } from './chunk-GYYYULBL.js';
8
+ import { venn } from './chunk-SXOAAQNY.js';
9
+ import { flowchart, layoutFlowchart } from './chunk-AXMBXAEA.js';
10
+ import { genogram } from './chunk-M26ORU4P.js';
11
+ import { ecomap, estimateTextWidth } from './chunk-XCCXG6RR.js';
12
+ import { pedigree } from './chunk-ZCHGIWJK.js';
13
13
  import { parseFrontmatter } from './chunk-2KTQ75LN.js';
14
- import { phylo } from './chunk-VZVREOTM.js';
15
- import { sociogram } from './chunk-56DSMSXK.js';
14
+ import { phylo } from './chunk-3WX24RCH.js';
15
+ import { sociogram } from './chunk-64LABNTF.js';
16
16
  import { timing } from './chunk-MTIZIHWE.js';
17
- import { logic } from './chunk-JR6JNQEZ.js';
18
- import { resolveBaseTheme, resolveTimelineTheme, resolveStateTheme, cssCustomProperties, resolvePetriTheme, resolveNetworkTheme, resolveUmlClassTheme, DEFAULT_FONT_FAMILY, FONT_SIZE, resolveReliabilityTheme, STROKE_WIDTH, resolveBowtieTheme, resolveMindmapTheme, resolveMatrixTheme, resolveBpmnTheme } from './chunk-TYEQC7PV.js';
17
+ import { logic } from './chunk-NKYR4PAS.js';
18
+ import { resolveBaseTheme, resolveTimelineTheme, resolveStateTheme, cssCustomProperties, resolvePetriTheme, resolveNetworkTheme, resolveUmlClassTheme, DEFAULT_FONT_FAMILY, FONT_SIZE, resolveReliabilityTheme, STROKE_WIDTH, resolveBowtieTheme, resolveMindmapTheme, resolveMatrixTheme, resolveBpmnTheme, resolveFloorplanTheme, TITLE, resolvePlaybookTheme } from './chunk-4MRVJI7G.js';
19
19
  import { QUOTE_PAIRS, stripQuotes, isOpenQuote, extractQuotedString, matchQuotedTitle } from './chunk-TO6PNBT3.js';
20
20
  import { el, escapeXml, group, rect, text, line, path, circle, polygon, title, desc, svgRoot, defs, multilineText } from './chunk-SYYBKDL7.js';
21
21
 
@@ -1876,7 +1876,7 @@ function parseTimeline(src) {
1876
1876
  let i = 0;
1877
1877
  let autoId = 0;
1878
1878
  const nextId2 = (prefix) => `${prefix}-${++autoId}`;
1879
- const ordinal = { index: 0 };
1879
+ const ordinal2 = { index: 0 };
1880
1880
  const first = lines[0];
1881
1881
  if (/^timeline\b/i.test(first.text)) {
1882
1882
  const rest = first.text.replace(/^timeline\b/i, "").trim();
@@ -1947,7 +1947,7 @@ function parseTimeline(src) {
1947
1947
  i++;
1948
1948
  continue;
1949
1949
  }
1950
- const parsed2 = parseEventLine(child.text, child.line, nextId2, ordinal);
1950
+ const parsed2 = parseEventLine(child.text, child.line, nextId2, ordinal2);
1951
1951
  if (!parsed2) throw new TimelineParseError(`Unrecognized line in ${keyword}: ${child.text}`, child.line);
1952
1952
  parsed2.event.trackId = trackId;
1953
1953
  ast.events.push(parsed2.event);
@@ -1961,7 +1961,7 @@ function parseTimeline(src) {
1961
1961
  }
1962
1962
  continue;
1963
1963
  }
1964
- const parsed = parseEventLine(text2, L.line, nextId2, ordinal);
1964
+ const parsed = parseEventLine(text2, L.line, nextId2, ordinal2);
1965
1965
  if (parsed) {
1966
1966
  ast.events.push(parsed.event);
1967
1967
  i++;
@@ -1985,12 +1985,12 @@ function safeParseDate(raw, line2) {
1985
1985
  throw new TimelineParseError(msg, line2);
1986
1986
  }
1987
1987
  }
1988
- function parseRowKey(raw, ordinal) {
1988
+ function parseRowKey(raw, ordinal2) {
1989
1989
  const parsed = tryParseDate(raw);
1990
1990
  if (parsed) return parsed;
1991
- ordinal.index += 1;
1991
+ ordinal2.index += 1;
1992
1992
  return {
1993
- value: ordinal.index,
1993
+ value: ordinal2.index,
1994
1994
  raw,
1995
1995
  precision: "ordinal"
1996
1996
  };
@@ -2027,7 +2027,7 @@ function applyConfig(ast, k, v, line2) {
2027
2027
  (ast.metadata ??= {})[k] = v;
2028
2028
  }
2029
2029
  }
2030
- function parseEventLine(text2, line2, nextId2, ordinal) {
2030
+ function parseEventLine(text2, line2, nextId2, ordinal2) {
2031
2031
  const { props, rest } = parseProperties(text2, line2);
2032
2032
  const { date, end, body } = splitDateAndBody(rest, line2);
2033
2033
  let kind = end ? "range" : "point";
@@ -2044,8 +2044,8 @@ function parseEventLine(text2, line2, nextId2, ordinal) {
2044
2044
  id: nextId2("ev"),
2045
2045
  label,
2046
2046
  kind,
2047
- start: parseRowKey(date, ordinal),
2048
- end: end ? parseRowKey(end, ordinal) : void 0,
2047
+ start: parseRowKey(date, ordinal2),
2048
+ end: end ? parseRowKey(end, ordinal2) : void 0,
2049
2049
  icon: props["icon"],
2050
2050
  shape: props["shape"],
2051
2051
  color: props["color"],
@@ -2283,11 +2283,11 @@ function layoutSwimlane(ast) {
2283
2283
  ];
2284
2284
  let labelY = candidates[0];
2285
2285
  for (const y of candidates) {
2286
- const box = { x1: x - labelW / 2, x2: x + labelW / 2, y };
2287
- const collide = labelBoxes.some((b) => Math.abs(b.y - box.y) < 13 && b.x1 < box.x2 && b.x2 > box.x1);
2286
+ const box2 = { x1: x - labelW / 2, x2: x + labelW / 2, y };
2287
+ const collide = labelBoxes.some((b) => Math.abs(b.y - box2.y) < 13 && b.x1 < box2.x2 && b.x2 > box2.x1);
2288
2288
  if (!collide) {
2289
2289
  labelY = y;
2290
- labelBoxes.push(box);
2290
+ labelBoxes.push(box2);
2291
2291
  break;
2292
2292
  }
2293
2293
  }
@@ -2448,10 +2448,10 @@ function layoutGantt(ast) {
2448
2448
  let labelY = pinZoneTop + 12;
2449
2449
  let step = 0;
2450
2450
  while (step < 4) {
2451
- const box = { x1: x - labelW / 2, x2: x + labelW / 2, y: labelY };
2452
- const collide = pinBoxes.some((b) => Math.abs(b.y - box.y) < 14 && b.x1 < box.x2 && b.x2 > box.x1);
2451
+ const box2 = { x1: x - labelW / 2, x2: x + labelW / 2, y: labelY };
2452
+ const collide = pinBoxes.some((b) => Math.abs(b.y - box2.y) < 14 && b.x1 < box2.x2 && b.x2 > box2.x1);
2453
2453
  if (!collide) {
2454
- pinBoxes.push(box);
2454
+ pinBoxes.push(box2);
2455
2455
  break;
2456
2456
  }
2457
2457
  labelY += 16;
@@ -7453,10 +7453,10 @@ function arrowMarker(t) {
7453
7453
  )
7454
7454
  ]);
7455
7455
  }
7456
- function classForBox(box) {
7457
- if (box.variant === "previous") return "prisma-stage-previous";
7458
- const base = box.variant === "exclusion" ? "prisma-exclusion" : "prisma-stage";
7459
- return `${base} prisma-stage-${box.stage}`;
7456
+ function classForBox(box2) {
7457
+ if (box2.variant === "previous") return "prisma-stage-previous";
7458
+ const base = box2.variant === "exclusion" ? "prisma-exclusion" : "prisma-stage";
7459
+ return `${base} prisma-stage-${box2.stage}`;
7460
7460
  }
7461
7461
  function classForLine(style) {
7462
7462
  switch (style) {
@@ -7540,22 +7540,22 @@ function renderHeader(h) {
7540
7540
  );
7541
7541
  return group({ "data-header": h.column }, [r6, ...lines]);
7542
7542
  }
7543
- function renderBox(box) {
7543
+ function renderBox(box2) {
7544
7544
  const r6 = rect({
7545
- x: box.x,
7546
- y: box.y,
7547
- width: box.width,
7548
- height: box.height,
7545
+ x: box2.x,
7546
+ y: box2.y,
7547
+ width: box2.width,
7548
+ height: box2.height,
7549
7549
  rx: PRISMA_CONST.BOX_RADIUS,
7550
7550
  ry: PRISMA_CONST.BOX_RADIUS,
7551
- class: classForBox(box),
7552
- "data-role": box.role
7551
+ class: classForBox(box2),
7552
+ "data-role": box2.role
7553
7553
  });
7554
- const innerLeft = box.x + PRISMA_CONST.BOX_PAD_X;
7555
- const innerRight = box.x + box.width - PRISMA_CONST.BOX_PAD_X;
7556
- const centerX = box.x + box.width / 2;
7554
+ const innerLeft = box2.x + PRISMA_CONST.BOX_PAD_X;
7555
+ const innerRight = box2.x + box2.width - PRISMA_CONST.BOX_PAD_X;
7556
+ const centerX = box2.x + box2.width / 2;
7557
7557
  let widestIndented = 0;
7558
- for (const line2 of box.lines) {
7558
+ for (const line2 of box2.lines) {
7559
7559
  if ((line2.indent ?? 0) > 0) {
7560
7560
  const w = approxLineWidth(line2);
7561
7561
  if (w > widestIndented) widestIndented = w;
@@ -7565,9 +7565,9 @@ function renderBox(box) {
7565
7565
  innerLeft,
7566
7566
  Math.min(centerX - widestIndented / 2, innerRight - widestIndented)
7567
7567
  );
7568
- let textY = box.y + PRISMA_CONST.BOX_PAD_Y;
7568
+ let textY = box2.y + PRISMA_CONST.BOX_PAD_Y;
7569
7569
  const textEls = [];
7570
- for (const line2 of box.lines) {
7570
+ for (const line2 of box2.lines) {
7571
7571
  const lh = lineHeightForLine(line2.style);
7572
7572
  const baseline = textY + Math.round(lh * 0.75);
7573
7573
  const indented = (line2.indent ?? 0) > 0;
@@ -7595,9 +7595,9 @@ function renderBox(box) {
7595
7595
  }
7596
7596
  return group(
7597
7597
  {
7598
- "data-prisma-role": box.role,
7599
- "data-prisma-variant": box.variant,
7600
- "data-prisma-stage": box.stage
7598
+ "data-prisma-role": box2.role,
7599
+ "data-prisma-variant": box2.variant,
7600
+ "data-prisma-stage": box2.stage
7601
7601
  },
7602
7602
  [r6, ...textEls]
7603
7603
  );
@@ -8528,7 +8528,7 @@ function layoutUsecase(ast) {
8528
8528
  const y = cy - h / 2;
8529
8529
  const anchorX = side === "left" ? x + w : x;
8530
8530
  const anchorY = isRect ? cy : cy - 6;
8531
- const box = {
8531
+ const box2 = {
8532
8532
  actor: a,
8533
8533
  x,
8534
8534
  y,
@@ -8538,8 +8538,8 @@ function layoutUsecase(ast) {
8538
8538
  anchorX,
8539
8539
  anchorY
8540
8540
  };
8541
- actorBoxes.push(box);
8542
- actorById2.set(a.id, box);
8541
+ actorBoxes.push(box2);
8542
+ actorById2.set(a.id, box2);
8543
8543
  cy += C2.ACTOR_PITCH;
8544
8544
  }
8545
8545
  }
@@ -10126,7 +10126,7 @@ function layoutNetwork(ast, schedule) {
10126
10126
  y = cursor;
10127
10127
  cursor += C2.BOX_H + C2.V_GAP;
10128
10128
  }
10129
- const box = {
10129
+ const box2 = {
10130
10130
  id,
10131
10131
  task: t,
10132
10132
  computed,
@@ -10137,8 +10137,8 @@ function layoutNetwork(ast, schedule) {
10137
10137
  milestone: t.milestone,
10138
10138
  rank: r6
10139
10139
  };
10140
- boxes.push(box);
10141
- boxById.set(id, box);
10140
+ boxes.push(box2);
10141
+ boxById.set(id, box2);
10142
10142
  }
10143
10143
  }
10144
10144
  const edges = [];
@@ -10345,7 +10345,7 @@ function layoutSwimlane2(ast, schedule) {
10345
10345
  const w = t.milestone ? C2.MS_W : C2.BOX_W;
10346
10346
  const x = colX(r6) + (C2.BOX_W - w) / 2;
10347
10347
  const y = y0 + idx * (C2.BOX_H + C2.V_GAP);
10348
- const box = {
10348
+ const box2 = {
10349
10349
  id,
10350
10350
  task: t,
10351
10351
  computed: schedule.computed.get(id),
@@ -10356,8 +10356,8 @@ function layoutSwimlane2(ast, schedule) {
10356
10356
  milestone: t.milestone,
10357
10357
  rank: r6
10358
10358
  };
10359
- boxes.push(box);
10360
- boxById.set(id, box);
10359
+ boxes.push(box2);
10360
+ boxById.set(id, box2);
10361
10361
  });
10362
10362
  }
10363
10363
  }
@@ -10427,7 +10427,7 @@ function layoutTimescaled(ast, schedule) {
10427
10427
  const iv = intervalById.get(t.id);
10428
10428
  const lane = laneOf.get(t.id);
10429
10429
  const y = topPad + lane * (C2.TS_BOX_H + C2.TS_LANE_GAP);
10430
- const box = {
10430
+ const box2 = {
10431
10431
  id: t.id,
10432
10432
  task: t,
10433
10433
  computed: schedule.computed.get(t.id),
@@ -10438,8 +10438,8 @@ function layoutTimescaled(ast, schedule) {
10438
10438
  milestone: t.milestone,
10439
10439
  rank: 0
10440
10440
  };
10441
- boxes.push(box);
10442
- boxById.set(t.id, box);
10441
+ boxes.push(box2);
10442
+ boxById.set(t.id, box2);
10443
10443
  }
10444
10444
  const edges = [];
10445
10445
  for (const t of ast.tasks) {
@@ -13864,26 +13864,26 @@ function labelText(d) {
13864
13864
  function deviceFootprint(d) {
13865
13865
  return iconSize(d.kind);
13866
13866
  }
13867
- function effBox(box) {
13868
- const labelW = Math.max(box.w, labelText(box.device).length * NET_CONST.CHAR_W + 6);
13867
+ function effBox(box2) {
13868
+ const labelW = Math.max(box2.w, labelText(box2.device).length * NET_CONST.CHAR_W + 6);
13869
13869
  const half = labelW / 2;
13870
13870
  return {
13871
- left: box.cx - half,
13872
- top: box.y,
13873
- right: box.cx + half,
13874
- bottom: box.y + box.h + labelExtra(box.device)
13875
- };
13876
- }
13877
- function edgePoint(box, tx, ty) {
13878
- const dx = tx - box.cx;
13879
- const dy = ty - box.cy;
13880
- if (dx === 0 && dy === 0) return { x: box.cx, y: box.cy };
13881
- const hw = box.w / 2;
13882
- const hh = box.h / 2;
13871
+ left: box2.cx - half,
13872
+ top: box2.y,
13873
+ right: box2.cx + half,
13874
+ bottom: box2.y + box2.h + labelExtra(box2.device)
13875
+ };
13876
+ }
13877
+ function edgePoint(box2, tx, ty) {
13878
+ const dx = tx - box2.cx;
13879
+ const dy = ty - box2.cy;
13880
+ if (dx === 0 && dy === 0) return { x: box2.cx, y: box2.cy };
13881
+ const hw = box2.w / 2;
13882
+ const hh = box2.h / 2;
13883
13883
  const sx = dx !== 0 ? hw / Math.abs(dx) : Infinity;
13884
13884
  const sy = dy !== 0 ? hh / Math.abs(dy) : Infinity;
13885
13885
  const s = Math.min(sx, sy);
13886
- return { x: box.cx + dx * s, y: box.cy + dy * s };
13886
+ return { x: box2.cx + dx * s, y: box2.cy + dy * s };
13887
13887
  }
13888
13888
  function placeBanded(ast, ranks) {
13889
13889
  const lr = ast.direction === "lr";
@@ -17558,7 +17558,7 @@ function layoutFaultTree(ast) {
17558
17558
  bump(g.cx + g.width / 2, g.cy + g.height / 2);
17559
17559
  if (g.cond) bump(g.cond.x + g.cond.w / 2, g.cond.y + g.cond.h / 2);
17560
17560
  }
17561
- for (const box of cutSetBoxes) bump(box.x + box.width, box.y + box.height);
17561
+ for (const box2 of cutSetBoxes) bump(box2.x + box2.width, box2.y + box2.height);
17562
17562
  for (const t of transfers) bump(t.x + 22, t.y + 30);
17563
17563
  if (ast.title) bump(C2.CANVAS_PAD + ast.title.length * 8.5, 0);
17564
17564
  if (ast.analysis.probability) {
@@ -17637,20 +17637,20 @@ function renderFaultTreeLayout(layout, config) {
17637
17637
  text({ x: layout.width / 2, y: 22, class: "sx-ft-title", "font-family": fontFamily, "text-anchor": "middle" }, ast.title)
17638
17638
  );
17639
17639
  }
17640
- for (const box of layout.cutSetBoxes) {
17640
+ for (const box2 of layout.cutSetBoxes) {
17641
17641
  inner.push(
17642
17642
  rect({
17643
- x: box.x,
17644
- y: box.y,
17645
- width: box.width,
17646
- height: box.height,
17643
+ x: box2.x,
17644
+ y: box2.y,
17645
+ width: box2.width,
17646
+ height: box2.height,
17647
17647
  rx: 6,
17648
17648
  class: "sx-ft-cutset",
17649
- "data-cutset": box.cutSet.events.join(","),
17650
- "data-cutset-index": String(box.index),
17651
- "data-order": String(box.cutSet.order),
17652
- ...box.cutSet.isSpof ? { "data-spof": "true" } : {},
17653
- ...box.cutSet.prob !== void 0 ? { "data-cutset-prob": String(box.cutSet.prob) } : {}
17649
+ "data-cutset": box2.cutSet.events.join(","),
17650
+ "data-cutset-index": String(box2.index),
17651
+ "data-order": String(box2.cutSet.order),
17652
+ ...box2.cutSet.isSpof ? { "data-spof": "true" } : {},
17653
+ ...box2.cutSet.prob !== void 0 ? { "data-cutset-prob": String(box2.cutSet.prob) } : {}
17654
17654
  })
17655
17655
  );
17656
17656
  }
@@ -21735,16 +21735,16 @@ function layoutMarkov(ast) {
21735
21735
  const tag = classOf?.[st.id];
21736
21736
  const isAbsorbing = tag === "absorbing";
21737
21737
  const pi = piMap?.[st.id] ?? perClassPi[st.id];
21738
- const box = {
21738
+ const box2 = {
21739
21739
  state: st,
21740
21740
  cx: c.x,
21741
21741
  cy: c.y,
21742
21742
  r: C2.STATE_R,
21743
21743
  isAbsorbing
21744
21744
  };
21745
- if (tag) box.classTag = tag;
21746
- if (pi !== void 0) box.pi = pi;
21747
- return box;
21745
+ if (tag) box2.classTag = tag;
21746
+ if (pi !== void 0) box2.pi = pi;
21747
+ return box2;
21748
21748
  });
21749
21749
  const arcKey = (a, b) => `${a}\0${b}`;
21750
21750
  const present = /* @__PURE__ */ new Set();
@@ -24163,13 +24163,13 @@ function parseFunction2(ast, rest, lineNo) {
24163
24163
  ast.warnings.push(`Line ${lineNo}: box "${id}" redeclared \u2014 keeping first declaration.`);
24164
24164
  return;
24165
24165
  }
24166
- const box = {
24166
+ const box2 = {
24167
24167
  id,
24168
24168
  name,
24169
24169
  // analysis assigns the real number; explicit wins if given.
24170
24170
  number: explicitNumber ?? 0
24171
24171
  };
24172
- ast.boxes.push(box);
24172
+ ast.boxes.push(box2);
24173
24173
  }
24174
24174
  function parseRoleArrow(ast, role, rest, lineNo) {
24175
24175
  const idM = /^([A-Za-z_]\w*)/.exec(rest);
@@ -24441,8 +24441,8 @@ function layoutIdef0(astIn) {
24441
24441
  const C2 = IDEF0_CONST;
24442
24442
  const ox = C2.MARGIN;
24443
24443
  const oy = C2.MARGIN + C2.TITLE_H;
24444
- const boxes = ast.boxes.map((box, idx) => ({
24445
- box,
24444
+ const boxes = ast.boxes.map((box2, idx) => ({
24445
+ box: box2,
24446
24446
  x: ox + idx * C2.STEP_X,
24447
24447
  y: oy + idx * C2.STEP_Y,
24448
24448
  width: C2.BOX_W,
@@ -35971,6 +35971,4028 @@ var sfc = {
35971
35971
  }
35972
35972
  };
35973
35973
 
35974
+ // src/diagrams/floorplan/catalog.ts
35975
+ var CHAIR_W = 0.44;
35976
+ var CHAIR_D = 0.38;
35977
+ var CHAIR_GAP = 0.27;
35978
+ var CHAIR_OVERHANG = 0.5;
35979
+ var RING = 0.45;
35980
+ function chairAt(px, cx, cy, deg) {
35981
+ const body = rect({
35982
+ class: "sx-fp-chair",
35983
+ x: px(-CHAIR_W / 2),
35984
+ y: px(-CHAIR_D / 2),
35985
+ width: px(CHAIR_W),
35986
+ height: px(CHAIR_D),
35987
+ rx: px(0.09)
35988
+ });
35989
+ const rot = Math.round(deg * 10) / 10;
35990
+ return el("g", { transform: `translate(${px(cx)},${px(cy)}) rotate(${rot})` }, [body]);
35991
+ }
35992
+ function edgeChairs(c, top, bottom) {
35993
+ const n = Math.max(1, Math.round(c.w / 0.65));
35994
+ const out = [];
35995
+ for (let i = 0; i < n; i++) {
35996
+ const cx = (i + 0.5) / n * c.w;
35997
+ if (top) out.push(chairAt(c.px, cx, -CHAIR_GAP, 0));
35998
+ out.push(chairAt(c.px, cx, c.h + CHAIR_GAP, 180));
35999
+ }
36000
+ return out.join("");
36001
+ }
36002
+ function box(c, cls = "sx-fp-furn", rx = 0) {
36003
+ return rect({ class: cls, x: 0, y: 0, width: c.px(c.w), height: c.px(c.h), rx: rx ? c.px(rx) : void 0 });
36004
+ }
36005
+ function glyphText(c, label) {
36006
+ return text(
36007
+ {
36008
+ class: "sx-fp-furn-text",
36009
+ x: c.px(c.w / 2),
36010
+ y: c.px(c.h / 2),
36011
+ "text-anchor": "middle",
36012
+ "dominant-baseline": "central",
36013
+ "font-size": c.px(Math.min(0.24, c.h * 0.4))
36014
+ },
36015
+ label
36016
+ );
36017
+ }
36018
+ function bedDraw(pillows) {
36019
+ return (c) => {
36020
+ const parts = [box(c, "sx-fp-furn", 0.05)];
36021
+ const pw = pillows === 2 ? c.w / 2 - 0.18 : c.w - 0.24;
36022
+ parts.push(rect({ class: "sx-fp-furn", x: c.px(0.12), y: c.px(0.1), width: c.px(pw), height: c.px(0.42), rx: c.px(0.06) }));
36023
+ if (pillows === 2) {
36024
+ parts.push(rect({ class: "sx-fp-furn", x: c.px(c.w / 2 + 0.06), y: c.px(0.1), width: c.px(pw), height: c.px(0.42), rx: c.px(0.06) }));
36025
+ }
36026
+ parts.push(line({ class: "sx-fp-furn-line", x1: 0, y1: c.px(0.72), x2: c.px(c.w), y2: c.px(0.72) }));
36027
+ return parts.join("");
36028
+ };
36029
+ }
36030
+ function sofaDraw(cushions) {
36031
+ return (c) => {
36032
+ const parts = [box(c, "sx-fp-furn", 0.08)];
36033
+ parts.push(rect({ class: "sx-fp-furn", x: c.px(0.15), y: 0, width: c.px(c.w - 0.3), height: c.px(0.18), rx: c.px(0.05) }));
36034
+ parts.push(line({ class: "sx-fp-furn-line", x1: c.px(0.15), y1: c.px(0.18), x2: c.px(0.15), y2: c.px(c.h - 0.05) }));
36035
+ parts.push(line({ class: "sx-fp-furn-line", x1: c.px(c.w - 0.15), y1: c.px(0.18), x2: c.px(c.w - 0.15), y2: c.px(c.h - 0.05) }));
36036
+ for (let i = 1; i < cushions; i++) {
36037
+ const x = c.px(0.15 + (c.w - 0.3) * i / cushions);
36038
+ parts.push(line({ class: "sx-fp-furn-line", x1: x, y1: c.px(0.18), x2: x, y2: c.px(c.h - 0.08) }));
36039
+ }
36040
+ return parts.join("");
36041
+ };
36042
+ }
36043
+ function shelfDraw(c) {
36044
+ const parts = [box(c)];
36045
+ const n = Math.max(2, Math.round(c.w / 0.15));
36046
+ for (let i = 1; i < n; i++) {
36047
+ const x = c.px(c.w * i / n);
36048
+ parts.push(line({ class: "sx-fp-furn-line", x1: x, y1: 0, x2: x, y2: c.px(c.h) }));
36049
+ }
36050
+ return parts.join("");
36051
+ }
36052
+ function boardDraw(c) {
36053
+ return [
36054
+ rect({ class: "sx-fp-furn-solid", x: 0, y: 0, width: c.px(c.w), height: c.px(c.h) }),
36055
+ rect({ class: "sx-fp-board-inner", x: c.px(0.08), y: c.px(c.h * 0.2), width: c.px(c.w - 0.16), height: c.px(c.h * 0.6) })
36056
+ ].join("");
36057
+ }
36058
+ function applianceDraw(label, drum) {
36059
+ return (c) => {
36060
+ const parts = [box(c)];
36061
+ if (drum) {
36062
+ parts.push(circle({ class: "sx-fp-furn-line", cx: c.px(c.w / 2), cy: c.px(c.h / 2), r: c.px(Math.min(c.w, c.h) * 0.32) }));
36063
+ } else {
36064
+ parts.push(line({ class: "sx-fp-furn-line", x1: 0, y1: c.px(0.12), x2: c.px(c.w), y2: c.px(0.12) }));
36065
+ }
36066
+ parts.push(glyphText(c, label));
36067
+ return parts.join("");
36068
+ };
36069
+ }
36070
+ function roundTable(seats, diaM) {
36071
+ const nominal = diaM + 2 * RING;
36072
+ return {
36073
+ w: nominal,
36074
+ h: nominal,
36075
+ draw: (c) => {
36076
+ const half = Math.min(c.w, c.h) / 2;
36077
+ const ring = Math.min(RING, half * 0.37);
36078
+ const r6 = half - ring;
36079
+ const cx = c.w / 2;
36080
+ const cy = c.h / 2;
36081
+ const parts = [circle({ class: "sx-fp-furn", cx: c.px(cx), cy: c.px(cy), r: c.px(r6) })];
36082
+ for (let i = 0; i < seats; i++) {
36083
+ const a = i / seats * 2 * Math.PI - Math.PI / 2;
36084
+ const px0 = cx + (r6 + ring * 0.55) * Math.cos(a);
36085
+ const py0 = cy + (r6 + ring * 0.55) * Math.sin(a);
36086
+ parts.push(chairAt(c.px, px0, py0, a * 180 / Math.PI + 90));
36087
+ }
36088
+ return parts.join("");
36089
+ }
36090
+ };
36091
+ }
36092
+ function tableDraw(top, bottom) {
36093
+ return (c) => box(c) + edgeChairs(c, top);
36094
+ }
36095
+ var TREAD = 0.28;
36096
+ function treadLines(c, vert, fixed0, fixed1, from, to, dashedFrom) {
36097
+ const parts = [];
36098
+ const dir = to >= from ? 1 : -1;
36099
+ for (let d = from; dir > 0 ? d <= to : d >= to; d += TREAD * dir) {
36100
+ const dashed = dashedFrom !== void 0 && (dir > 0 ? d > dashedFrom : d < dashedFrom);
36101
+ const cls = dashed ? "sx-fp-furn-dash" : "sx-fp-furn-line";
36102
+ if (vert) parts.push(line({ class: cls, x1: c.px(fixed0), y1: c.px(d), x2: c.px(fixed1), y2: c.px(d) }));
36103
+ else parts.push(line({ class: cls, x1: c.px(d), y1: c.px(fixed0), x2: c.px(d), y2: c.px(fixed1) }));
36104
+ }
36105
+ return parts.join("");
36106
+ }
36107
+ function breakLine(c, vert, lo, hi, at) {
36108
+ const span = hi - lo;
36109
+ const dz = Math.min(0.12, span * 0.18);
36110
+ const tilt = span * 0.18;
36111
+ const pts = vert ? [
36112
+ [lo, at + tilt],
36113
+ [lo + span * 0.4, at + tilt * 0.2],
36114
+ [lo + span * 0.5 - dz, at + tilt * 0.2 + dz],
36115
+ [lo + span * 0.5 + dz, at - tilt * 0.2 - dz],
36116
+ [lo + span * 0.6, at - tilt * 0.2],
36117
+ [hi, at - tilt]
36118
+ ] : [
36119
+ [at + tilt, lo],
36120
+ [at + tilt * 0.2, lo + span * 0.4],
36121
+ [at + tilt * 0.2 + dz, lo + span * 0.5 - dz],
36122
+ [at - tilt * 0.2 - dz, lo + span * 0.5 + dz],
36123
+ [at - tilt * 0.2, lo + span * 0.6],
36124
+ [at - tilt, hi]
36125
+ ];
36126
+ const d = pts.map(([a, b], i) => `${i === 0 ? "M" : "L"} ${c.px(vert ? a : a)} ${c.px(vert ? b : b)}`).join(" ");
36127
+ return path({ class: "sx-fp-stair-break", d });
36128
+ }
36129
+ function dirArrow(c, points, labelText2) {
36130
+ const parts = [];
36131
+ const [sx, sy] = points[0];
36132
+ parts.push(circle({ class: "sx-fp-furn-line", cx: c.px(sx), cy: c.px(sy), r: c.px(0.05) }));
36133
+ const d = points.map(([x, y], i) => `${i === 0 ? "M" : "L"} ${c.px(x)} ${c.px(y)}`).join(" ");
36134
+ parts.push(path({ class: "sx-fp-furn-line", d }));
36135
+ const [ex, ey] = points[points.length - 1];
36136
+ const [px2, py2] = points[points.length - 2];
36137
+ const ang = Math.atan2(ey - py2, ex - px2);
36138
+ const hs = 0.11;
36139
+ const a1 = ang + Math.PI - 0.5;
36140
+ const a2 = ang + Math.PI + 0.5;
36141
+ parts.push(
36142
+ polygon({
36143
+ class: "sx-fp-furn-dot",
36144
+ points: `${c.px(ex)},${c.px(ey)} ${c.px(ex + hs * Math.cos(a1))},${c.px(ey + hs * Math.sin(a1))} ${c.px(ex + hs * Math.cos(a2))},${c.px(ey + hs * Math.sin(a2))}`
36145
+ })
36146
+ );
36147
+ const [nx, ny] = points[1];
36148
+ const segLen = Math.hypot(nx - sx, ny - sy) || 1;
36149
+ const ux = (nx - sx) / segLen;
36150
+ const uy = (ny - sy) / segLen;
36151
+ const lx = sx + ux * 0.42 - uy * 0.16;
36152
+ const ly = sy + uy * 0.42 + ux * 0.16;
36153
+ parts.push(
36154
+ text(
36155
+ {
36156
+ class: "sx-fp-furn-text",
36157
+ x: c.px(lx),
36158
+ y: c.px(ly),
36159
+ "text-anchor": "middle",
36160
+ "dominant-baseline": "central",
36161
+ "font-size": c.px(0.18)
36162
+ },
36163
+ labelText2
36164
+ )
36165
+ );
36166
+ return parts.join("");
36167
+ }
36168
+ function straightStairs(c) {
36169
+ const vert = c.h >= c.w;
36170
+ const len = vert ? c.h : c.w;
36171
+ const breakAt = Math.min(7 * TREAD, len * 0.62);
36172
+ const parts = [box(c, "sx-fp-furn-nofill")];
36173
+ if (vert) {
36174
+ parts.push(treadLines(c, true, 0, c.w, c.h - TREAD, 0.02, c.h - breakAt));
36175
+ parts.push(breakLine(c, true, 0, c.w, c.h - breakAt));
36176
+ parts.push(dirArrow(c, [[c.w / 2, c.h - 0.18], [c.w / 2, c.h - breakAt + 0.28]], c.label ?? "UP"));
36177
+ } else {
36178
+ parts.push(treadLines(c, false, 0, c.h, TREAD, c.w - 0.02, breakAt));
36179
+ parts.push(breakLine(c, false, 0, c.h, breakAt));
36180
+ parts.push(dirArrow(c, [[0.18, c.h / 2], [breakAt - 0.28, c.h / 2]], c.label ?? "UP"));
36181
+ }
36182
+ return parts.join("");
36183
+ }
36184
+ function lStairs(c) {
36185
+ const rw = Math.min(c.w, c.h) * 0.45;
36186
+ const parts = [
36187
+ path({
36188
+ class: "sx-fp-furn-nofill",
36189
+ d: `M 0 0 L ${c.px(c.w)} 0 L ${c.px(c.w)} ${c.px(rw)} L ${c.px(rw)} ${c.px(rw)} L ${c.px(rw)} ${c.px(c.h)} L 0 ${c.px(c.h)} Z`
36190
+ })
36191
+ ];
36192
+ parts.push(line({ class: "sx-fp-furn-line", x1: c.px(rw), y1: 0, x2: c.px(rw), y2: c.px(rw) }));
36193
+ parts.push(treadLines(c, true, 0, rw, c.h - TREAD, rw + 0.05));
36194
+ const breakAt = rw + (c.w - rw) * 0.55;
36195
+ parts.push(treadLines(c, false, 0, rw, rw + TREAD, c.w - 0.02, breakAt));
36196
+ parts.push(breakLine(c, false, 0, rw, breakAt));
36197
+ parts.push(
36198
+ dirArrow(
36199
+ c,
36200
+ [
36201
+ [rw / 2, c.h - 0.18],
36202
+ [rw / 2, rw / 2],
36203
+ [breakAt - 0.24, rw / 2]
36204
+ ],
36205
+ c.label ?? "UP"
36206
+ )
36207
+ );
36208
+ return parts.join("");
36209
+ }
36210
+ function uStairs(c) {
36211
+ const lh = Math.min(c.h * 0.3, Math.max(0.9, c.w / 2));
36212
+ const mid = c.w / 2;
36213
+ const parts = [box(c, "sx-fp-furn-nofill")];
36214
+ parts.push(line({ class: "sx-fp-furn-line", x1: 0, y1: c.px(lh), x2: c.px(c.w), y2: c.px(lh) }));
36215
+ parts.push(line({ class: "sx-fp-furn-line", x1: c.px(mid), y1: c.px(lh), x2: c.px(mid), y2: c.px(c.h) }));
36216
+ parts.push(treadLines(c, true, mid, c.w, c.h - TREAD, lh + 0.05));
36217
+ const breakAt = lh + (c.h - lh) * 0.4;
36218
+ parts.push(treadLines(c, true, 0, mid, lh + TREAD, c.h - 0.02, breakAt));
36219
+ parts.push(breakLine(c, true, 0, mid, breakAt));
36220
+ parts.push(
36221
+ dirArrow(
36222
+ c,
36223
+ [
36224
+ [mid + mid / 2, c.h - 0.18],
36225
+ [mid + mid / 2, lh / 2],
36226
+ [mid / 2, lh / 2],
36227
+ [mid / 2, breakAt - 0.24]
36228
+ ],
36229
+ c.label ?? "UP"
36230
+ )
36231
+ );
36232
+ return parts.join("");
36233
+ }
36234
+ function spiralStairs(c) {
36235
+ const r6 = Math.min(c.w, c.h) / 2 - 0.02;
36236
+ const cx = c.w / 2;
36237
+ const cy = c.h / 2;
36238
+ const parts = [
36239
+ circle({ class: "sx-fp-furn", cx: c.px(cx), cy: c.px(cy), r: c.px(r6) }),
36240
+ circle({ class: "sx-fp-furn-line", cx: c.px(cx), cy: c.px(cy), r: c.px(0.08) })
36241
+ ];
36242
+ for (let i = 0; i < 12; i++) {
36243
+ const a = i / 12 * 2 * Math.PI;
36244
+ parts.push(
36245
+ line({
36246
+ class: "sx-fp-furn-line",
36247
+ x1: c.px(cx + 0.08 * Math.cos(a)),
36248
+ y1: c.px(cy + 0.08 * Math.sin(a)),
36249
+ x2: c.px(cx + r6 * Math.cos(a)),
36250
+ y2: c.px(cy + r6 * Math.sin(a))
36251
+ })
36252
+ );
36253
+ }
36254
+ const wr = r6 * 0.62;
36255
+ const a0 = Math.PI * 0.75;
36256
+ const a1 = Math.PI * 1.9;
36257
+ const steps = 5;
36258
+ const pts = [];
36259
+ for (let i = 0; i <= steps; i++) {
36260
+ const a = a0 + (a1 - a0) * i / steps;
36261
+ pts.push([cx + wr * Math.cos(a), cy + wr * Math.sin(a)]);
36262
+ }
36263
+ parts.push(dirArrow(c, pts, c.label ?? "UP"));
36264
+ return parts.join("");
36265
+ }
36266
+ var FLOORPLAN_SYMBOLS = {
36267
+ // ── residential / living ──
36268
+ "bed-double": { w: 1.6, h: 2, draw: bedDraw(2) },
36269
+ "bed-single": { w: 0.9, h: 2, draw: bedDraw(1) },
36270
+ "bed-queen": { w: 1.55, h: 2.05, draw: bedDraw(2) },
36271
+ "bed-king": { w: 1.95, h: 2.05, draw: bedDraw(2) },
36272
+ sofa: { w: 2.2, h: 0.9, draw: sofaDraw(3) },
36273
+ loveseat: { w: 1.5, h: 0.9, draw: sofaDraw(2) },
36274
+ armchair: { w: 0.9, h: 0.9, draw: sofaDraw(1) },
36275
+ "coffee-table": { w: 1, h: 0.5, draw: (c) => box(c, "sx-fp-furn", 0.06) },
36276
+ tv: { w: 1.4, h: 0.15, draw: (c) => rect({ class: "sx-fp-furn-solid", x: 0, y: 0, width: c.px(c.w), height: c.px(c.h) }) },
36277
+ rug: {
36278
+ w: 2,
36279
+ h: 1.4,
36280
+ underlay: true,
36281
+ draw: (c) => rect({ class: "sx-fp-rug", x: 0, y: 0, width: c.px(c.w), height: c.px(c.h), rx: c.px(0.1) })
36282
+ },
36283
+ wardrobe: {
36284
+ w: 1.8,
36285
+ h: 0.6,
36286
+ draw: (c) => {
36287
+ const parts = [box(c)];
36288
+ if (c.w >= c.h) {
36289
+ parts.push(line({ class: "sx-fp-furn-line", x1: 0, y1: c.px(c.h / 2), x2: c.px(c.w), y2: c.px(c.h / 2) }));
36290
+ const n = Math.max(2, Math.round(c.w / 0.15));
36291
+ for (let i = 0; i < n; i++) {
36292
+ const x = c.px((i + 0.5) / n * c.w);
36293
+ parts.push(line({ class: "sx-fp-furn-line", x1: x, y1: c.px(c.h / 2 - 0.1), x2: x, y2: c.px(c.h / 2 + 0.1) }));
36294
+ }
36295
+ } else {
36296
+ parts.push(line({ class: "sx-fp-furn-line", x1: c.px(c.w / 2), y1: 0, x2: c.px(c.w / 2), y2: c.px(c.h) }));
36297
+ const n = Math.max(2, Math.round(c.h / 0.15));
36298
+ for (let i = 0; i < n; i++) {
36299
+ const y = c.px((i + 0.5) / n * c.h);
36300
+ parts.push(line({ class: "sx-fp-furn-line", x1: c.px(c.w / 2 - 0.1), y1: y, x2: c.px(c.w / 2 + 0.1), y2: y }));
36301
+ }
36302
+ }
36303
+ return parts.join("");
36304
+ }
36305
+ },
36306
+ dresser: {
36307
+ w: 1.2,
36308
+ h: 0.5,
36309
+ draw: (c) => [
36310
+ box(c),
36311
+ line({ class: "sx-fp-furn-line", x1: 0, y1: c.px(c.h / 2), x2: c.px(c.w), y2: c.px(c.h / 2) }),
36312
+ circle({ class: "sx-fp-furn-line", cx: c.px(c.w * 0.3), cy: c.px(c.h * 0.25), r: c.px(0.03) }),
36313
+ circle({ class: "sx-fp-furn-line", cx: c.px(c.w * 0.7), cy: c.px(c.h * 0.25), r: c.px(0.03) })
36314
+ ].join("")
36315
+ },
36316
+ nightstand: {
36317
+ w: 0.5,
36318
+ h: 0.4,
36319
+ draw: (c) => box(c) + circle({ class: "sx-fp-furn-line", cx: c.px(c.w / 2), cy: c.px(c.h / 2), r: c.px(0.08) })
36320
+ },
36321
+ bookshelf: { w: 0.9, h: 0.3, draw: shelfDraw },
36322
+ plant: {
36323
+ w: 0.5,
36324
+ h: 0.5,
36325
+ draw: (c) => {
36326
+ const r6 = Math.min(c.w, c.h) / 2;
36327
+ const parts = [circle({ class: "sx-fp-furn", cx: c.px(c.w / 2), cy: c.px(c.h / 2), r: c.px(r6) })];
36328
+ for (const a of [0, 60, 120, 180, 240, 300]) {
36329
+ const rad = a * Math.PI / 180;
36330
+ parts.push(
36331
+ line({
36332
+ class: "sx-fp-furn-line",
36333
+ x1: c.px(c.w / 2),
36334
+ y1: c.px(c.h / 2),
36335
+ x2: c.px(c.w / 2 + (r6 - 0.03) * Math.cos(rad)),
36336
+ y2: c.px(c.h / 2 + (r6 - 0.03) * Math.sin(rad))
36337
+ })
36338
+ );
36339
+ }
36340
+ return parts.join("");
36341
+ }
36342
+ },
36343
+ "dining-table": { w: 1.6, h: 0.9, envelope: [CHAIR_OVERHANG, 0, CHAIR_OVERHANG, 0], draw: tableDraw(true) },
36344
+ sectional: {
36345
+ w: 2.6,
36346
+ h: 2,
36347
+ draw: (c) => {
36348
+ const d = 0.9 * Math.min(c.h / 2, 1);
36349
+ const parts = [
36350
+ path({
36351
+ class: "sx-fp-furn",
36352
+ d: `M 0 0 L ${c.px(c.w)} 0 L ${c.px(c.w)} ${c.px(d)} L ${c.px(d)} ${c.px(d)} L ${c.px(d)} ${c.px(c.h)} L 0 ${c.px(c.h)} Z`
36353
+ }),
36354
+ rect({ class: "sx-fp-furn", x: c.px(0.12), y: 0, width: c.px(c.w - 0.24), height: c.px(0.16), rx: c.px(0.05) }),
36355
+ rect({ class: "sx-fp-furn", x: 0, y: c.px(0.12), width: c.px(0.16), height: c.px(c.h - 0.24), rx: c.px(0.05) })
36356
+ ];
36357
+ for (const fx of [0.33, 0.66]) {
36358
+ parts.push(line({ class: "sx-fp-furn-line", x1: c.px(d + (c.w - d) * fx), y1: c.px(0.16), x2: c.px(d + (c.w - d) * fx), y2: c.px(d - 0.05) }));
36359
+ }
36360
+ parts.push(line({ class: "sx-fp-furn-line", x1: c.px(0.16), y1: c.px(c.h / 2 + d / 4), x2: c.px(d - 0.05), y2: c.px(c.h / 2 + d / 4) }));
36361
+ return parts.join("");
36362
+ }
36363
+ },
36364
+ "side-table": { w: 0.5, h: 0.5, draw: (c) => box(c, "sx-fp-furn", 0.06) },
36365
+ "tv-stand": {
36366
+ w: 1.6,
36367
+ h: 0.45,
36368
+ underlay: true,
36369
+ // a surface — the TV sits on it
36370
+ draw: (c) => box(c) + line({ class: "sx-fp-furn-line", x1: 0, y1: c.px(c.h / 2), x2: c.px(c.w), y2: c.px(c.h / 2) })
36371
+ },
36372
+ fireplace: {
36373
+ w: 1.5,
36374
+ h: 0.5,
36375
+ draw: (c) => {
36376
+ const inset = Math.min(0.18, c.w * 0.15);
36377
+ return [
36378
+ box(c),
36379
+ // firebox opening toward the room (south edge)
36380
+ rect({
36381
+ class: "sx-fp-furn-line",
36382
+ x: c.px(inset),
36383
+ y: c.px(c.h * 0.3),
36384
+ width: c.px(c.w - 2 * inset),
36385
+ height: c.px(c.h * 0.7 - 0.04)
36386
+ }),
36387
+ line({ class: "sx-fp-furn-line", x1: 0, y1: c.px(c.h * 0.3), x2: c.px(inset), y2: 0 }),
36388
+ line({ class: "sx-fp-furn-line", x1: c.px(c.w), y1: c.px(c.h * 0.3), x2: c.px(c.w - inset), y2: 0 })
36389
+ ].join("");
36390
+ }
36391
+ },
36392
+ "floor-lamp": {
36393
+ w: 0.35,
36394
+ h: 0.35,
36395
+ draw: (c) => {
36396
+ const r6 = Math.min(c.w, c.h) / 2;
36397
+ return [
36398
+ circle({ class: "sx-fp-furn", cx: c.px(c.w / 2), cy: c.px(c.h / 2), r: c.px(r6) }),
36399
+ line({ class: "sx-fp-furn-line", x1: c.px(c.w / 2 - r6), y1: c.px(c.h / 2), x2: c.px(c.w / 2 + r6), y2: c.px(c.h / 2) }),
36400
+ line({ class: "sx-fp-furn-line", x1: c.px(c.w / 2), y1: c.px(c.h / 2 - r6), x2: c.px(c.w / 2), y2: c.px(c.h / 2 + r6) })
36401
+ ].join("");
36402
+ }
36403
+ },
36404
+ ottoman: { w: 0.6, h: 0.45, draw: (c) => box(c, "sx-fp-furn", 0.12) },
36405
+ piano: {
36406
+ w: 1.5,
36407
+ h: 1.7,
36408
+ draw: (c) => {
36409
+ const X = (v) => c.px(v * (c.w / 1.5));
36410
+ const Y = (v) => c.px(v * (c.h / 1.7));
36411
+ const body = [
36412
+ `M ${X(0)} ${Y(0)}`,
36413
+ `L ${X(1.5)} ${Y(0)}`,
36414
+ `L ${X(1.5)} ${Y(0.75)}`,
36415
+ `C ${X(1.5)} ${Y(1.25)} ${X(1.25)} ${Y(1.7)} ${X(0.8)} ${Y(1.7)}`,
36416
+ `C ${X(0.45)} ${Y(1.7)} ${X(0.28)} ${Y(1.45)} ${X(0.28)} ${Y(1.1)}`,
36417
+ `C ${X(0.28)} ${Y(0.85)} ${X(0.14)} ${Y(0.72)} ${X(0)} ${Y(0.68)}`,
36418
+ "Z"
36419
+ ].join(" ");
36420
+ return path({ class: "sx-fp-furn", d: body }) + line({ class: "sx-fp-furn-line", x1: X(0), y1: Y(0.14), x2: X(1.5), y2: Y(0.14) }) + chairAt(c.px, c.w / 1.5 * 0.75, -0.25, 180);
36421
+ },
36422
+ envelope: [CHAIR_OVERHANG, 0, 0, 0]
36423
+ },
36424
+ "piano-upright": {
36425
+ w: 1.5,
36426
+ h: 0.6,
36427
+ envelope: [0, 0, CHAIR_OVERHANG, 0],
36428
+ draw: (c) => box(c) + line({ class: "sx-fp-furn-line", x1: 0, y1: c.px(c.h * 0.55), x2: c.px(c.w), y2: c.px(c.h * 0.55) }) + chairAt(c.px, c.w / 2, c.h + CHAIR_GAP, 180)
36429
+ },
36430
+ "pool-table": {
36431
+ w: 2.54,
36432
+ h: 1.27,
36433
+ draw: (c) => {
36434
+ const rail = Math.min(0.12, c.h * 0.1);
36435
+ const parts = [
36436
+ box(c, "sx-fp-furn", 0.06),
36437
+ rect({ class: "sx-fp-furn-line", x: c.px(rail), y: c.px(rail), width: c.px(c.w - 2 * rail), height: c.px(c.h - 2 * rail) })
36438
+ ];
36439
+ for (const [px0, py0] of [
36440
+ [rail, rail],
36441
+ [c.w / 2, rail],
36442
+ [c.w - rail, rail],
36443
+ [rail, c.h - rail],
36444
+ [c.w / 2, c.h - rail],
36445
+ [c.w - rail, c.h - rail]
36446
+ ]) {
36447
+ parts.push(circle({ class: "sx-fp-furn-dot", cx: c.px(px0), cy: c.px(py0), r: c.px(0.045) }));
36448
+ }
36449
+ return parts.join("");
36450
+ }
36451
+ },
36452
+ crib: {
36453
+ w: 0.7,
36454
+ h: 1.3,
36455
+ draw: (c) => {
36456
+ const parts = [box(c, "sx-fp-furn", 0.05), rect({ class: "sx-fp-furn-line", x: c.px(0.08), y: c.px(0.08), width: c.px(c.w - 0.16), height: c.px(c.h - 0.16), rx: c.px(0.04) })];
36457
+ const n = Math.max(3, Math.round(c.h / 0.18));
36458
+ for (let i = 1; i < n; i++) {
36459
+ const y = c.px(0.08 + (c.h - 0.16) * i / n);
36460
+ parts.push(line({ class: "sx-fp-furn-line", x1: c.px(0.08), y1: y, x2: c.px(c.w - 0.08), y2: y }));
36461
+ }
36462
+ return parts.join("");
36463
+ }
36464
+ },
36465
+ "bunk-bed": {
36466
+ w: 0.95,
36467
+ h: 2,
36468
+ draw: (c) => bedDraw(1)(c) + line({ class: "sx-fp-furn-dash", x1: c.px(0.07), y1: c.px(0.07), x2: c.px(c.w - 0.07), y2: c.px(0.07) }) + [0.25, 0.45, 0.65].map((f) => line({ class: "sx-fp-furn-line", x1: c.px(c.w - 0.16), y1: c.px(c.h * f), x2: c.px(c.w), y2: c.px(c.h * f) })).join("")
36469
+ },
36470
+ "ceiling-fan": {
36471
+ w: 0.9,
36472
+ h: 0.9,
36473
+ underlay: true,
36474
+ // overhead fixture — never collides with floor furniture
36475
+ draw: (c) => {
36476
+ const r6 = Math.min(c.w, c.h) / 2;
36477
+ const cx = c.w / 2;
36478
+ const cy = c.h / 2;
36479
+ const parts = [
36480
+ circle({ class: "sx-fp-furn-dash", cx: c.px(cx), cy: c.px(cy), r: c.px(r6) }),
36481
+ circle({ class: "sx-fp-furn", cx: c.px(cx), cy: c.px(cy), r: c.px(r6 * 0.18) })
36482
+ ];
36483
+ for (const a of [20, 110, 200, 290]) {
36484
+ const rad = a * Math.PI / 180;
36485
+ parts.push(
36486
+ el("ellipse", {
36487
+ class: "sx-fp-furn-line",
36488
+ cx: c.px(cx + r6 * 0.55 * Math.cos(rad)),
36489
+ cy: c.px(cy + r6 * 0.55 * Math.sin(rad)),
36490
+ rx: c.px(r6 * 0.42),
36491
+ ry: c.px(r6 * 0.15),
36492
+ transform: `rotate(${a} ${c.px(cx + r6 * 0.55 * Math.cos(rad))} ${c.px(cy + r6 * 0.55 * Math.sin(rad))})`
36493
+ })
36494
+ );
36495
+ }
36496
+ return parts.join("");
36497
+ }
36498
+ },
36499
+ // ── kitchen / bath ──
36500
+ counter: {
36501
+ w: 2,
36502
+ h: 0.6,
36503
+ underlay: true,
36504
+ draw: (c) => box(c) + line({
36505
+ class: "sx-fp-furn-dash",
36506
+ x1: c.px(0.05),
36507
+ y1: c.px(c.h - 0.06),
36508
+ x2: c.px(c.w - 0.05),
36509
+ y2: c.px(c.h - 0.06)
36510
+ })
36511
+ },
36512
+ "kitchen-sink": {
36513
+ w: 0.8,
36514
+ h: 0.6,
36515
+ draw: (c) => [
36516
+ box(c),
36517
+ rect({ class: "sx-fp-furn-line", x: c.px(0.07), y: c.px(0.1), width: c.px(c.w / 2 - 0.11), height: c.px(c.h - 0.2), rx: c.px(0.04) }),
36518
+ rect({ class: "sx-fp-furn-line", x: c.px(c.w / 2 + 0.04), y: c.px(0.1), width: c.px(c.w / 2 - 0.11), height: c.px(c.h - 0.2), rx: c.px(0.04) }),
36519
+ circle({ class: "sx-fp-furn-dot", cx: c.px(c.w / 2), cy: c.px(0.06), r: c.px(0.024) })
36520
+ ].join("")
36521
+ },
36522
+ stove: {
36523
+ w: 0.6,
36524
+ h: 0.6,
36525
+ draw: (c) => {
36526
+ const parts = [box(c)];
36527
+ const inset = 0.18;
36528
+ for (const [bx, by] of [
36529
+ [inset, inset],
36530
+ [c.w - inset, inset],
36531
+ [inset, c.h - inset],
36532
+ [c.w - inset, c.h - inset]
36533
+ ]) {
36534
+ parts.push(circle({ class: "sx-fp-furn-line", cx: c.px(bx), cy: c.px(by), r: c.px(0.085) }));
36535
+ }
36536
+ return parts.join("");
36537
+ }
36538
+ },
36539
+ fridge: { w: 0.7, h: 0.7, draw: applianceDraw("REF", false) },
36540
+ dishwasher: { w: 0.6, h: 0.6, draw: applianceDraw("DW", false) },
36541
+ island: {
36542
+ w: 1.8,
36543
+ h: 0.9,
36544
+ underlay: true,
36545
+ draw: (c) => box(c) + rect({ class: "sx-fp-furn-line", x: c.px(0.08), y: c.px(0.08), width: c.px(c.w - 0.16), height: c.px(c.h - 0.16) })
36546
+ },
36547
+ toilet: {
36548
+ w: 0.5,
36549
+ h: 0.7,
36550
+ draw: (c) => [
36551
+ rect({ class: "sx-fp-furn", x: 0, y: 0, width: c.px(c.w), height: c.px(0.2), rx: c.px(0.04) }),
36552
+ el("ellipse", {
36553
+ class: "sx-fp-furn",
36554
+ cx: c.px(c.w / 2),
36555
+ cy: c.px(0.2 + (c.h - 0.24) / 2),
36556
+ rx: c.px(c.w / 2 - 0.04),
36557
+ ry: c.px((c.h - 0.28) / 2)
36558
+ })
36559
+ ].join("")
36560
+ },
36561
+ sink: {
36562
+ w: 0.55,
36563
+ h: 0.45,
36564
+ draw: (c) => [
36565
+ box(c, "sx-fp-furn", 0.05),
36566
+ el("ellipse", {
36567
+ class: "sx-fp-furn-line",
36568
+ cx: c.px(c.w / 2),
36569
+ cy: c.px(c.h / 2 + 0.02),
36570
+ rx: c.px(c.w / 2 - 0.08),
36571
+ ry: c.px(c.h / 2 - 0.1)
36572
+ }),
36573
+ circle({ class: "sx-fp-furn-dot", cx: c.px(c.w / 2), cy: c.px(0.07), r: c.px(0.024) })
36574
+ ].join("")
36575
+ },
36576
+ bathtub: {
36577
+ w: 0.8,
36578
+ h: 1.7,
36579
+ draw: (c) => [
36580
+ box(c, "sx-fp-furn", 0.08),
36581
+ rect({ class: "sx-fp-furn-line", x: c.px(0.09), y: c.px(0.09), width: c.px(c.w - 0.18), height: c.px(c.h - 0.18), rx: c.px(0.22) }),
36582
+ circle({ class: "sx-fp-furn-dot", cx: c.px(c.w / 2), cy: c.px(0.26), r: c.px(0.035) })
36583
+ ].join("")
36584
+ },
36585
+ shower: {
36586
+ w: 0.9,
36587
+ h: 0.9,
36588
+ draw: (c) => [
36589
+ box(c),
36590
+ line({ class: "sx-fp-furn-line", x1: 0, y1: 0, x2: c.px(c.w), y2: c.px(c.h) }),
36591
+ line({ class: "sx-fp-furn-line", x1: c.px(c.w), y1: 0, x2: 0, y2: c.px(c.h) }),
36592
+ circle({ class: "sx-fp-furn", cx: c.px(0.15), cy: c.px(0.15), r: c.px(0.06) })
36593
+ ].join("")
36594
+ },
36595
+ washer: { w: 0.6, h: 0.6, draw: applianceDraw("W", true) },
36596
+ dryer: { w: 0.6, h: 0.6, draw: applianceDraw("D", true) },
36597
+ "wall-cabinet": {
36598
+ w: 0.9,
36599
+ h: 0.35,
36600
+ underlay: true,
36601
+ // above the cut plane — drawn dashed over the base run
36602
+ draw: (c) => rect({ class: "sx-fp-furn-dash", x: 0, y: 0, width: c.px(c.w), height: c.px(c.h) })
36603
+ },
36604
+ "range-hood": {
36605
+ w: 0.8,
36606
+ h: 0.55,
36607
+ underlay: true,
36608
+ // above the cut plane
36609
+ draw: (c) => el("g", {}, [
36610
+ polygon({
36611
+ class: "sx-fp-furn-dash",
36612
+ points: `${c.px(0.08)},0 ${c.px(c.w - 0.08)},0 ${c.px(c.w)},${c.px(c.h)} 0,${c.px(c.h)}`
36613
+ })
36614
+ ])
36615
+ },
36616
+ "bar-stool": {
36617
+ w: 0.35,
36618
+ h: 0.35,
36619
+ draw: (c) => circle({ class: "sx-fp-chair", cx: c.px(c.w / 2), cy: c.px(c.h / 2), r: c.px(Math.min(c.w, c.h) / 2) })
36620
+ },
36621
+ vanity: {
36622
+ w: 1.5,
36623
+ h: 0.55,
36624
+ draw: (c) => {
36625
+ const parts = [box(c)];
36626
+ for (const fx of [0.27, 0.73]) {
36627
+ parts.push(
36628
+ el("ellipse", {
36629
+ class: "sx-fp-furn-line",
36630
+ cx: c.px(c.w * fx),
36631
+ cy: c.px(c.h / 2 + 0.02),
36632
+ rx: c.px(Math.min(0.22, c.w * 0.16)),
36633
+ ry: c.px(c.h / 2 - 0.12)
36634
+ })
36635
+ );
36636
+ parts.push(circle({ class: "sx-fp-furn-dot", cx: c.px(c.w * fx), cy: c.px(0.07), r: c.px(0.022) }));
36637
+ }
36638
+ return parts.join("");
36639
+ }
36640
+ },
36641
+ bidet: {
36642
+ w: 0.4,
36643
+ h: 0.6,
36644
+ draw: (c) => rect({ class: "sx-fp-furn", x: c.px(c.w * 0.15), y: 0, width: c.px(c.w * 0.7), height: c.px(0.12), rx: c.px(0.03) }) + el("ellipse", {
36645
+ class: "sx-fp-furn",
36646
+ cx: c.px(c.w / 2),
36647
+ cy: c.px(0.12 + (c.h - 0.16) / 2),
36648
+ rx: c.px(c.w / 2 - 0.03),
36649
+ ry: c.px((c.h - 0.18) / 2)
36650
+ }) + circle({ class: "sx-fp-furn-line", cx: c.px(c.w / 2), cy: c.px(c.h * 0.45), r: c.px(0.035) })
36651
+ },
36652
+ urinal: {
36653
+ w: 0.4,
36654
+ h: 0.35,
36655
+ draw: (c) => rect({ class: "sx-fp-furn", x: 0, y: 0, width: c.px(c.w), height: c.px(0.08) }) + path({
36656
+ class: "sx-fp-furn",
36657
+ d: `M ${c.px(0.05)} ${c.px(0.08)} L ${c.px(c.w - 0.05)} ${c.px(0.08)} C ${c.px(c.w - 0.05)} ${c.px(c.h * 0.75)} ${c.px(c.w * 0.7)} ${c.px(c.h)} ${c.px(c.w / 2)} ${c.px(c.h)} C ${c.px(c.w * 0.3)} ${c.px(c.h)} ${c.px(0.05)} ${c.px(c.h * 0.75)} ${c.px(0.05)} ${c.px(0.08)} Z`
36658
+ })
36659
+ },
36660
+ // ── stairs & vertical circulation ──
36661
+ stairs: { w: 1, h: 3, consumesLabel: true, draw: straightStairs },
36662
+ "stairs-l": { w: 2.2, h: 2.2, consumesLabel: true, draw: lStairs },
36663
+ "stairs-u": { w: 2, h: 3, consumesLabel: true, draw: uStairs },
36664
+ "spiral-stairs": { w: 1.5, h: 1.5, consumesLabel: true, draw: spiralStairs },
36665
+ elevator: {
36666
+ w: 1.6,
36667
+ h: 1.5,
36668
+ draw: (c) => box(c) + line({ class: "sx-fp-furn-line", x1: 0, y1: 0, x2: c.px(c.w), y2: c.px(c.h) }) + line({ class: "sx-fp-furn-line", x1: c.px(c.w), y1: 0, x2: 0, y2: c.px(c.h) })
36669
+ },
36670
+ // ── structural ──
36671
+ column: {
36672
+ w: 0.4,
36673
+ h: 0.4,
36674
+ draw: (c) => rect({ class: "sx-fp-furn-solid", x: 0, y: 0, width: c.px(c.w), height: c.px(c.h) })
36675
+ },
36676
+ // ── classroom / office ──
36677
+ "desk-chair": {
36678
+ w: 0.6,
36679
+ h: 0.75,
36680
+ draw: (c) => [
36681
+ rect({ class: "sx-fp-furn", x: 0, y: 0, width: c.px(c.w), height: c.px(c.h * 0.58) }),
36682
+ rect({
36683
+ class: "sx-fp-chair",
36684
+ x: c.px(c.w / 2 - c.w * 0.3),
36685
+ y: c.px(c.h * 0.66),
36686
+ width: c.px(c.w * 0.6),
36687
+ height: c.px(c.h * 0.32),
36688
+ rx: c.px(0.07)
36689
+ })
36690
+ ].join("")
36691
+ },
36692
+ desk: {
36693
+ w: 1.4,
36694
+ h: 0.7,
36695
+ envelope: [0, 0, CHAIR_OVERHANG, 0],
36696
+ draw: (c) => box(c) + chairAt(c.px, c.w / 2, c.h + CHAIR_GAP, 180)
36697
+ },
36698
+ chair: {
36699
+ w: 0.45,
36700
+ h: 0.45,
36701
+ draw: (c) => rect({ class: "sx-fp-chair", x: 0, y: 0, width: c.px(c.w), height: c.px(c.h), rx: c.px(0.1) }) + line({ class: "sx-fp-furn-line", x1: 0, y1: c.px(0.06), x2: 0, y2: c.px(c.h - 0.06) })
36702
+ },
36703
+ whiteboard: { w: 3, h: 0.12, draw: boardDraw },
36704
+ smartboard: { w: 2, h: 0.12, draw: boardDraw },
36705
+ bookcase: { w: 0.9, h: 0.3, draw: shelfDraw },
36706
+ "desk-l": {
36707
+ w: 1.6,
36708
+ h: 1.6,
36709
+ draw: (c) => {
36710
+ const d = 0.7 * Math.min(c.w / 1.6, c.h / 1.6);
36711
+ return path({
36712
+ class: "sx-fp-furn",
36713
+ d: `M 0 0 L ${c.px(c.w)} 0 L ${c.px(c.w)} ${c.px(d)} L ${c.px(d)} ${c.px(d)} L ${c.px(d)} ${c.px(c.h)} L 0 ${c.px(c.h)} Z`
36714
+ }) + chairAt(c.px, d + 0.32, d + 0.32, 315);
36715
+ }
36716
+ },
36717
+ "filing-cabinet": {
36718
+ w: 0.5,
36719
+ h: 0.6,
36720
+ draw: (c) => box(c) + line({ class: "sx-fp-furn-line", x1: 0, y1: c.px(c.h / 3), x2: c.px(c.w), y2: c.px(c.h / 3) }) + line({ class: "sx-fp-furn-line", x1: 0, y1: c.px(2 * c.h / 3), x2: c.px(c.w), y2: c.px(2 * c.h / 3) })
36721
+ },
36722
+ lockers: {
36723
+ w: 1.8,
36724
+ h: 0.45,
36725
+ draw: (c) => {
36726
+ const parts = [box(c)];
36727
+ const n = Math.max(2, Math.round(c.w / 0.3));
36728
+ for (let i = 1; i < n; i++) {
36729
+ const x = c.px(c.w * i / n);
36730
+ parts.push(line({ class: "sx-fp-furn-line", x1: x, y1: 0, x2: x, y2: c.px(c.h) }));
36731
+ }
36732
+ for (let i = 0; i < n; i++) {
36733
+ parts.push(circle({ class: "sx-fp-furn-dot", cx: c.px((i + 0.78) / n * c.w), cy: c.px(c.h / 2), r: c.px(0.02) }));
36734
+ }
36735
+ return parts.join("");
36736
+ }
36737
+ },
36738
+ cubbies: {
36739
+ w: 2,
36740
+ h: 0.4,
36741
+ draw: (c) => {
36742
+ const parts = [box(c)];
36743
+ const n = Math.max(2, Math.round(c.w / 0.3));
36744
+ for (let i = 1; i < n; i++) {
36745
+ const x = c.px(c.w * i / n);
36746
+ parts.push(line({ class: "sx-fp-furn-line", x1: x, y1: 0, x2: x, y2: c.px(c.h) }));
36747
+ }
36748
+ return parts.join("");
36749
+ }
36750
+ },
36751
+ "kidney-table": {
36752
+ w: 1.8,
36753
+ h: 1.2,
36754
+ draw: (c) => {
36755
+ const sx = c.w / 1.8;
36756
+ const sy = c.h / 1.2;
36757
+ const X = (v) => c.px(v * sx);
36758
+ const Y = (v) => c.px(v * sy);
36759
+ const d = [
36760
+ `M ${X(0)} ${Y(0.55)}`,
36761
+ `C ${X(0)} ${Y(0.15)} ${X(0.35)} ${Y(0)} ${X(0.9)} ${Y(0)}`,
36762
+ `C ${X(1.45)} ${Y(0)} ${X(1.8)} ${Y(0.15)} ${X(1.8)} ${Y(0.55)}`,
36763
+ `C ${X(1.8)} ${Y(0.95)} ${X(1.5)} ${Y(1.2)} ${X(1.18)} ${Y(1.2)}`,
36764
+ `Q ${X(0.9)} ${Y(0.82)} ${X(0.62)} ${Y(1.2)}`,
36765
+ `C ${X(0.3)} ${Y(1.2)} ${X(0)} ${Y(0.95)} ${X(0)} ${Y(0.55)}`,
36766
+ "Z"
36767
+ ].join(" ");
36768
+ return path({ class: "sx-fp-furn", d });
36769
+ }
36770
+ },
36771
+ "round-table-4": roundTable(4, 1.52),
36772
+ "round-table-6": roundTable(6, 1.52),
36773
+ "round-table-8": roundTable(8, 1.52),
36774
+ "round-table-10": roundTable(10, 1.83),
36775
+ "conference-table": { w: 2.4, h: 1.2, envelope: [CHAIR_OVERHANG, 0, CHAIR_OVERHANG, 0], draw: tableDraw(true) },
36776
+ // ── event / banquet ──
36777
+ "banquet-table": { w: 2.44, h: 0.76, envelope: [CHAIR_OVERHANG, 0, CHAIR_OVERHANG, 0], draw: tableDraw(true) },
36778
+ "head-table": { w: 3.7, h: 0.76, envelope: [0, 0, CHAIR_OVERHANG, 0], draw: tableDraw(false) },
36779
+ stage: {
36780
+ w: 4,
36781
+ h: 2,
36782
+ draw: (c) => box(c) + rect({
36783
+ class: "sx-fp-furn-dash",
36784
+ x: c.px(0.1),
36785
+ y: c.px(0.1),
36786
+ width: c.px(c.w - 0.2),
36787
+ height: c.px(c.h - 0.2)
36788
+ })
36789
+ },
36790
+ "dance-floor": {
36791
+ w: 4,
36792
+ h: 4,
36793
+ underlay: true,
36794
+ draw: (c) => {
36795
+ const parts = [];
36796
+ for (let d = 0.5; d < c.w + c.h; d += 0.5) {
36797
+ const x1 = Math.max(0, d - c.h);
36798
+ const y1 = Math.min(d, c.h);
36799
+ const x2 = Math.min(d, c.w);
36800
+ const y2 = Math.max(0, d - c.w);
36801
+ parts.push(line({ class: "sx-fp-hatch", x1: c.px(x1), y1: c.px(y1), x2: c.px(x2), y2: c.px(y2) }));
36802
+ }
36803
+ parts.push(rect({ class: "sx-fp-furn-nofill", x: 0, y: 0, width: c.px(c.w), height: c.px(c.h) }));
36804
+ return parts.join("");
36805
+ }
36806
+ },
36807
+ bar: {
36808
+ w: 3,
36809
+ h: 0.7,
36810
+ draw: (c) => box(c) + line({ class: "sx-fp-furn-line", x1: 0, y1: c.px(c.h - 0.15), x2: c.px(c.w), y2: c.px(c.h - 0.15) })
36811
+ },
36812
+ "dj-booth": { w: 1.2, h: 0.8, draw: (c) => box(c) + glyphText(c, "DJ") },
36813
+ "cocktail-table": {
36814
+ w: 0.76,
36815
+ h: 0.76,
36816
+ draw: (c) => circle({ class: "sx-fp-furn", cx: c.px(c.w / 2), cy: c.px(c.h / 2), r: c.px(Math.min(c.w, c.h) / 2) }) + circle({ class: "sx-fp-furn-line", cx: c.px(c.w / 2), cy: c.px(c.h / 2), r: c.px(Math.min(c.w, c.h) / 6) })
36817
+ },
36818
+ podium: {
36819
+ w: 0.6,
36820
+ h: 0.5,
36821
+ draw: (c) => polygon({
36822
+ class: "sx-fp-furn",
36823
+ points: `${c.px(c.w * 0.15)},0 ${c.px(c.w * 0.85)},0 ${c.px(c.w)},${c.px(c.h)} 0,${c.px(c.h)}`
36824
+ })
36825
+ },
36826
+ "row-chairs": {
36827
+ w: 2.2,
36828
+ h: 0.5,
36829
+ draw: (c) => {
36830
+ const n = Math.max(1, Math.floor(c.w / 0.55 + 1e-6));
36831
+ const parts = [];
36832
+ for (let i = 0; i < n; i++) {
36833
+ parts.push(chairAt(c.px, (i + 0.5) / n * c.w, c.h / 2, 0));
36834
+ }
36835
+ return parts.join("");
36836
+ }
36837
+ },
36838
+ // ── retail ──
36839
+ // Gondola run: a long fixture with a back-to-back spine and product bays.
36840
+ shelving: {
36841
+ w: 1.8,
36842
+ h: 0.6,
36843
+ draw: (c) => {
36844
+ const parts = [box(c)];
36845
+ parts.push(line({ class: "sx-fp-furn-line", x1: 0, y1: c.px(c.h / 2), x2: c.px(c.w), y2: c.px(c.h / 2) }));
36846
+ const n = Math.max(2, Math.round(c.w / 0.45));
36847
+ for (let i = 1; i < n; i++) {
36848
+ const x = c.px(c.w * i / n);
36849
+ parts.push(line({ class: "sx-fp-furn-line", x1: x, y1: 0, x2: x, y2: c.px(c.h) }));
36850
+ }
36851
+ return parts.join("");
36852
+ }
36853
+ },
36854
+ // POS counter with a register block and a belt line.
36855
+ checkout: {
36856
+ w: 1.6,
36857
+ h: 0.7,
36858
+ draw: (c) => [
36859
+ box(c),
36860
+ rect({ class: "sx-fp-furn-solid", x: c.px(c.w - 0.5), y: c.px(0.12), width: c.px(0.34), height: c.px(0.3), rx: c.px(0.04) }),
36861
+ line({ class: "sx-fp-furn-line", x1: c.px(0.12), y1: c.px(c.h * 0.6), x2: c.px(c.w - 0.62), y2: c.px(c.h * 0.6) })
36862
+ ].join("")
36863
+ },
36864
+ // Round garment rack: rail circle with radial hanger ticks.
36865
+ "clothing-rack": {
36866
+ w: 1,
36867
+ h: 1,
36868
+ draw: (c) => {
36869
+ const r6 = Math.min(c.w, c.h) / 2;
36870
+ const cx = c.w / 2;
36871
+ const cy = c.h / 2;
36872
+ const parts = [
36873
+ circle({ class: "sx-fp-furn-nofill", cx: c.px(cx), cy: c.px(cy), r: c.px(r6) }),
36874
+ circle({ class: "sx-fp-furn-dot", cx: c.px(cx), cy: c.px(cy), r: c.px(0.04) })
36875
+ ];
36876
+ for (const a of [0, 45, 90, 135, 180, 225, 270, 315]) {
36877
+ const rad = a * Math.PI / 180;
36878
+ parts.push(
36879
+ line({
36880
+ class: "sx-fp-furn-line",
36881
+ x1: c.px(cx + (r6 - 0.09) * Math.cos(rad)),
36882
+ y1: c.px(cy + (r6 - 0.09) * Math.sin(rad)),
36883
+ x2: c.px(cx + r6 * Math.cos(rad)),
36884
+ y2: c.px(cy + r6 * Math.sin(rad))
36885
+ })
36886
+ );
36887
+ }
36888
+ return parts.join("");
36889
+ }
36890
+ },
36891
+ // Changing booth: bench at the back, a mirror strip, a dashed curtain at the opening.
36892
+ "fitting-room": {
36893
+ w: 1.1,
36894
+ h: 1.1,
36895
+ draw: (c) => [
36896
+ rect({ class: "sx-fp-furn-nofill", x: 0, y: 0, width: c.px(c.w), height: c.px(c.h) }),
36897
+ rect({ class: "sx-fp-furn", x: c.px(0.12), y: c.px(0.12), width: c.px(c.w - 0.24), height: c.px(0.28), rx: c.px(0.04) }),
36898
+ line({ class: "sx-fp-furn-dash", x1: 0, y1: c.px(c.h), x2: c.px(c.w), y2: c.px(c.h) }),
36899
+ rect({ class: "sx-fp-furn-solid", x: c.px(c.w - 0.06), y: c.px(c.h * 0.5), width: c.px(0.04), height: c.px(c.h * 0.35) })
36900
+ ].join("")
36901
+ },
36902
+ // ── warehouse / industrial ──
36903
+ // Pallet racking: an open frame with bay dividers and cross-bracing.
36904
+ "pallet-rack": {
36905
+ w: 2.7,
36906
+ h: 1.1,
36907
+ draw: (c) => {
36908
+ const parts = [rect({ class: "sx-fp-furn-nofill", x: 0, y: 0, width: c.px(c.w), height: c.px(c.h) })];
36909
+ const bays = Math.max(2, Math.round(c.w / 1.35));
36910
+ for (let i = 0; i < bays; i++) {
36911
+ const x0 = c.w * i / bays;
36912
+ const x1 = c.w * (i + 1) / bays;
36913
+ if (i > 0) parts.push(line({ class: "sx-fp-furn-line", x1: c.px(x0), y1: 0, x2: c.px(x0), y2: c.px(c.h) }));
36914
+ parts.push(line({ class: "sx-fp-furn-line", x1: c.px(x0), y1: 0, x2: c.px(x1), y2: c.px(c.h) }));
36915
+ parts.push(line({ class: "sx-fp-furn-line", x1: c.px(x1), y1: 0, x2: c.px(x0), y2: c.px(c.h) }));
36916
+ }
36917
+ return parts.join("");
36918
+ }
36919
+ },
36920
+ // Dock door: roll-up door segments with two bumpers at the outer face.
36921
+ "loading-dock": {
36922
+ w: 3,
36923
+ h: 0.6,
36924
+ draw: (c) => {
36925
+ const parts = [box(c)];
36926
+ const n = Math.max(3, Math.round(c.w / 0.5));
36927
+ for (let i = 1; i < n; i++) {
36928
+ const x = c.px(c.w * i / n);
36929
+ parts.push(line({ class: "sx-fp-furn-line", x1: x, y1: 0, x2: x, y2: c.px(c.h) }));
36930
+ }
36931
+ parts.push(rect({ class: "sx-fp-furn-solid", x: c.px(0.12), y: c.px(c.h - 0.12), width: c.px(0.3), height: c.px(0.1) }));
36932
+ parts.push(rect({ class: "sx-fp-furn-solid", x: c.px(c.w - 0.42), y: c.px(c.h - 0.12), width: c.px(0.3), height: c.px(0.1) }));
36933
+ return parts.join("");
36934
+ }
36935
+ },
36936
+ // Counterbalance forklift silhouette: body, mast forks at the front, operator seat.
36937
+ forklift: {
36938
+ w: 1.2,
36939
+ h: 2.2,
36940
+ draw: (c) => [
36941
+ rect({ class: "sx-fp-furn", x: c.px(0.15), y: c.px(0.42), width: c.px(c.w - 0.3), height: c.px(c.h - 0.72), rx: c.px(0.06) }),
36942
+ rect({ class: "sx-fp-furn-solid", x: c.px(0.25), y: 0, width: c.px(0.12), height: c.px(0.42) }),
36943
+ rect({ class: "sx-fp-furn-solid", x: c.px(c.w - 0.37), y: 0, width: c.px(0.12), height: c.px(0.42) }),
36944
+ circle({ class: "sx-fp-furn-line", cx: c.px(c.w / 2), cy: c.px(c.h - 0.62), r: c.px(0.18) })
36945
+ ].join("")
36946
+ },
36947
+ // ── salon / spa ──
36948
+ // Styling station: back counter with a mirror strip and a chair facing it.
36949
+ "salon-chair": {
36950
+ w: 0.8,
36951
+ h: 1.4,
36952
+ draw: (c) => [
36953
+ rect({ class: "sx-fp-furn", x: 0, y: 0, width: c.px(c.w), height: c.px(0.32), rx: c.px(0.03) }),
36954
+ rect({ class: "sx-fp-furn-solid", x: c.px(0.08), y: c.px(0.05), width: c.px(c.w - 0.16), height: c.px(0.05) }),
36955
+ circle({ class: "sx-fp-furn", cx: c.px(c.w / 2), cy: c.px(c.h * 0.64), r: c.px(Math.min(c.w, 0.62) / 2) }),
36956
+ circle({ class: "sx-fp-furn-line", cx: c.px(c.w / 2), cy: c.px(c.h * 0.64), r: c.px(0.08) })
36957
+ ].join("")
36958
+ },
36959
+ // Backwash unit: a reclining chair with a wash basin at the head end.
36960
+ "shampoo-bowl": {
36961
+ w: 0.9,
36962
+ h: 1.5,
36963
+ draw: (c) => [
36964
+ rect({ class: "sx-fp-furn", x: c.px(0.1), y: c.px(0.5), width: c.px(c.w - 0.2), height: c.px(c.h - 0.6), rx: c.px(0.08) }),
36965
+ circle({ class: "sx-fp-furn", cx: c.px(c.w / 2), cy: c.px(0.4), r: c.px(0.32) }),
36966
+ circle({ class: "sx-fp-furn-line", cx: c.px(c.w / 2), cy: c.px(0.4), r: c.px(0.12) }),
36967
+ circle({ class: "sx-fp-furn-dot", cx: c.px(c.w / 2), cy: c.px(0.12), r: c.px(0.04) })
36968
+ ].join("")
36969
+ },
36970
+ // Manicure table: a small table with a client and a technician chair.
36971
+ "manicure-table": {
36972
+ w: 1,
36973
+ h: 0.5,
36974
+ envelope: [CHAIR_GAP + CHAIR_D, 0, CHAIR_GAP + CHAIR_D, 0],
36975
+ draw: (c) => [box(c), chairAt(c.px, c.w / 2, -CHAIR_GAP, 0), chairAt(c.px, c.w / 2, c.h + CHAIR_GAP, 180)].join("")
36976
+ },
36977
+ // ── gym / fitness ──
36978
+ // Treadmill: a deck with a running belt and a console at the front.
36979
+ treadmill: {
36980
+ w: 0.9,
36981
+ h: 2,
36982
+ draw: (c) => [
36983
+ box(c, "sx-fp-furn", 0.05),
36984
+ rect({ class: "sx-fp-furn-line", x: c.px(0.12), y: c.px(0.5), width: c.px(c.w - 0.24), height: c.px(c.h - 0.65), rx: c.px(0.04) }),
36985
+ rect({ class: "sx-fp-furn-solid", x: c.px(0.1), y: c.px(0.08), width: c.px(c.w - 0.2), height: c.px(0.16), rx: c.px(0.03) })
36986
+ ].join("")
36987
+ },
36988
+ // Flat bench with upright posts and a loaded barbell crossing it.
36989
+ "weight-bench": {
36990
+ w: 0.6,
36991
+ h: 1.8,
36992
+ envelope: [0, 0.35, 0, 0.35],
36993
+ draw: (c) => [
36994
+ rect({ class: "sx-fp-furn", x: c.px(c.w / 2 - 0.12), y: c.px(0.3), width: c.px(0.24), height: c.px(c.h - 0.4), rx: c.px(0.05) }),
36995
+ rect({ class: "sx-fp-furn-solid", x: c.px(0.06), y: c.px(0.18), width: c.px(0.1), height: c.px(0.1) }),
36996
+ rect({ class: "sx-fp-furn-solid", x: c.px(c.w - 0.16), y: c.px(0.18), width: c.px(0.1), height: c.px(0.1) }),
36997
+ line({ class: "sx-fp-furn-line", x1: c.px(-0.3), y1: c.px(0.23), x2: c.px(c.w + 0.3), y2: c.px(0.23) })
36998
+ ].join("")
36999
+ },
37000
+ // Power rack: a square frame with four corner posts and a barbell.
37001
+ "power-rack": {
37002
+ w: 1.4,
37003
+ h: 1.4,
37004
+ envelope: [0, 0.3, 0, 0.3],
37005
+ draw: (c) => {
37006
+ const post = 0.14;
37007
+ const corners = [
37008
+ [0, 0],
37009
+ [c.w - post, 0],
37010
+ [0, c.h - post],
37011
+ [c.w - post, c.h - post]
37012
+ ];
37013
+ const parts = [rect({ class: "sx-fp-furn-nofill", x: 0, y: 0, width: c.px(c.w), height: c.px(c.h) })];
37014
+ for (const [px0, py0] of corners) {
37015
+ parts.push(rect({ class: "sx-fp-furn-solid", x: c.px(px0), y: c.px(py0), width: c.px(post), height: c.px(post) }));
37016
+ }
37017
+ parts.push(line({ class: "sx-fp-furn-line", x1: c.px(-0.28), y1: c.px(c.h * 0.4), x2: c.px(c.w + 0.28), y2: c.px(c.h * 0.4) }));
37018
+ return parts.join("");
37019
+ }
37020
+ },
37021
+ // Exercise mat — an underlay surface, like a rug.
37022
+ "yoga-mat": {
37023
+ w: 0.6,
37024
+ h: 1.8,
37025
+ underlay: true,
37026
+ draw: (c) => rect({ class: "sx-fp-furn-dash", x: 0, y: 0, width: c.px(c.w), height: c.px(c.h), rx: c.px(0.08) })
37027
+ },
37028
+ // ── site / outdoor ──
37029
+ // Tree in plan: a canopy disc with a foliage ring and a trunk dot.
37030
+ tree: {
37031
+ w: 2,
37032
+ h: 2,
37033
+ draw: (c) => {
37034
+ const r6 = Math.min(c.w, c.h) / 2;
37035
+ return [
37036
+ circle({ class: "sx-fp-furn", cx: c.px(c.w / 2), cy: c.px(c.h / 2), r: c.px(r6) }),
37037
+ circle({ class: "sx-fp-furn-line", cx: c.px(c.w / 2), cy: c.px(c.h / 2), r: c.px(r6 * 0.6) }),
37038
+ circle({ class: "sx-fp-furn-dot", cx: c.px(c.w / 2), cy: c.px(c.h / 2), r: c.px(0.07) })
37039
+ ].join("");
37040
+ }
37041
+ },
37042
+ // Car in plan (parking-stall footprint): body, glazing lines, four wheels.
37043
+ car: {
37044
+ w: 1.8,
37045
+ h: 4.4,
37046
+ draw: (c) => {
37047
+ const parts = [rect({ class: "sx-fp-furn", x: c.px(0.12), y: c.px(0.1), width: c.px(c.w - 0.24), height: c.px(c.h - 0.2), rx: c.px(0.35) })];
37048
+ parts.push(line({ class: "sx-fp-furn-line", x1: c.px(0.3), y1: c.px(c.h * 0.26), x2: c.px(c.w - 0.3), y2: c.px(c.h * 0.26) }));
37049
+ parts.push(line({ class: "sx-fp-furn-line", x1: c.px(0.3), y1: c.px(c.h * 0.72), x2: c.px(c.w - 0.3), y2: c.px(c.h * 0.72) }));
37050
+ for (const wy of [c.h * 0.3, c.h * 0.7]) {
37051
+ parts.push(rect({ class: "sx-fp-furn-solid", x: 0, y: c.px(wy - 0.18), width: c.px(0.14), height: c.px(0.36), rx: c.px(0.03) }));
37052
+ parts.push(rect({ class: "sx-fp-furn-solid", x: c.px(c.w - 0.14), y: c.px(wy - 0.18), width: c.px(0.14), height: c.px(0.36), rx: c.px(0.03) }));
37053
+ }
37054
+ return parts.join("");
37055
+ }
37056
+ }
37057
+ };
37058
+ var FURNITURE_TYPES = Object.keys(FLOORPLAN_SYMBOLS);
37059
+
37060
+ // src/diagrams/floorplan/parser.ts
37061
+ var FloorplanParseError = class extends Error {
37062
+ line;
37063
+ constructor(message, line2) {
37064
+ super(`line ${line2}: ${message}`);
37065
+ this.name = "FloorplanParseError";
37066
+ this.line = line2;
37067
+ }
37068
+ };
37069
+ var isStr = (t) => t !== void 0 && "str" in t;
37070
+ var isWord = (t, w) => t !== void 0 && "word" in t && (w === void 0 || t.word === w);
37071
+ function normalizeQuotes3(line2) {
37072
+ return line2.replace(/[“”「」『』]/g, '"').replace(/[‘’]/g, "'");
37073
+ }
37074
+ function tokenize7(line2) {
37075
+ const out = [];
37076
+ const re = /"([^"]*)"|(\S+)/g;
37077
+ let m;
37078
+ while (m = re.exec(line2)) {
37079
+ if (m[1] !== void 0) out.push({ str: m[1] });
37080
+ else out.push({ word: m[2] });
37081
+ }
37082
+ return out;
37083
+ }
37084
+ function parseNum(t, what, ln) {
37085
+ if (!isWord(t)) throw new FloorplanParseError(`expected a number for ${what}`, ln);
37086
+ const v = Number(t.word);
37087
+ if (!Number.isFinite(v)) throw new FloorplanParseError(`expected a number for ${what}, got "${t.word}"`, ln);
37088
+ return v;
37089
+ }
37090
+ function parseCoord2(t, what, ln) {
37091
+ if (!isWord(t)) throw new FloorplanParseError(`expected "x,y" for ${what}`, ln);
37092
+ const m = /^(-?\d*\.?\d+),(-?\d*\.?\d+)$/.exec(t.word);
37093
+ if (!m) throw new FloorplanParseError(`expected "x,y" for ${what}, got "${t.word}"`, ln);
37094
+ return { x: Number(m[1]), y: Number(m[2]) };
37095
+ }
37096
+ function parseDims(t, what, ln) {
37097
+ if (!isWord(t)) throw new FloorplanParseError(`expected "WxH" for ${what}`, ln);
37098
+ const m = /^(\d*\.?\d+)x(\d*\.?\d+)$/i.exec(t.word);
37099
+ if (!m) throw new FloorplanParseError(`expected "WxH" for ${what}, got "${t.word}"`, ln);
37100
+ return { w: Number(m[1]), h: Number(m[2]) };
37101
+ }
37102
+ function parsePct(t, ln) {
37103
+ if (!isWord(t)) throw new FloorplanParseError(`expected a percentage for "at"`, ln);
37104
+ const v = Number(t.word.replace(/%$/, ""));
37105
+ if (!Number.isFinite(v)) throw new FloorplanParseError(`expected a percentage for "at", got "${t.word}"`, ln);
37106
+ return v;
37107
+ }
37108
+ function parseId(t, what, ln) {
37109
+ if (!isWord(t)) throw new FloorplanParseError(`expected ${what}`, ln);
37110
+ return t.word;
37111
+ }
37112
+ var SIDES = ["north", "south", "east", "west"];
37113
+ var REL_HOW = ["right-of", "left-of", "above", "below"];
37114
+ function parseFurnitureType(t, ln) {
37115
+ const word = isWord(t) ? t.word : "";
37116
+ if (!FURNITURE_TYPES.includes(word)) {
37117
+ throw new FloorplanParseError(
37118
+ `unknown furniture type "${word}". Valid types: ${FURNITURE_TYPES.join(", ")}`,
37119
+ ln
37120
+ );
37121
+ }
37122
+ return word;
37123
+ }
37124
+ function parseHeader3(tok, ast, ln) {
37125
+ while (tok.length) {
37126
+ const t = tok.shift();
37127
+ if (isStr(t)) ast.title = t.str;
37128
+ else if (t.word === "unit") {
37129
+ const u = parseId(tok.shift(), "unit (m|ft)", ln);
37130
+ if (u !== "m" && u !== "ft") throw new FloorplanParseError(`unit must be "m" or "ft", got "${u}"`, ln);
37131
+ ast.unit = u;
37132
+ } else throw new FloorplanParseError(`floorplan: unexpected token "${t.word}"`, ln);
37133
+ }
37134
+ }
37135
+ function parseRoom(tok, ast, ln) {
37136
+ const id = parseId(tok.shift(), "a room id", ln);
37137
+ if (ast.rooms.some((r6) => r6.id === id)) {
37138
+ throw new FloorplanParseError(`duplicate room id "${id}"`, ln);
37139
+ }
37140
+ const room = { id, label: id, w: 4, h: 3, line: ln };
37141
+ while (tok.length) {
37142
+ const t = tok.shift();
37143
+ if (isStr(t)) room.label = t.str;
37144
+ else if (t.word === "at") room.at = parseCoord2(tok.shift(), "at", ln);
37145
+ else if (REL_HOW.includes(t.word)) {
37146
+ room.rel = {
37147
+ how: t.word,
37148
+ ref: parseId(tok.shift(), `a room id after "${t.word}"`, ln),
37149
+ offset: void 0,
37150
+ align: void 0
37151
+ };
37152
+ } else if (t.word === "offset") {
37153
+ if (!room.rel) throw new FloorplanParseError(`"offset" requires a relative placement (right-of/left-of/above/below)`, ln);
37154
+ room.rel.offset = parseNum(tok.shift(), "offset", ln);
37155
+ } else if (t.word === "align") {
37156
+ if (!room.rel) throw new FloorplanParseError(`"align" requires a relative placement (right-of/left-of/above/below)`, ln);
37157
+ const a = parseId(tok.shift(), "align (start|center|end)", ln);
37158
+ if (a !== "start" && a !== "center" && a !== "end") {
37159
+ throw new FloorplanParseError(`align must be start|center|end, got "${a}"`, ln);
37160
+ }
37161
+ room.rel.align = a;
37162
+ } else if (t.word === "size") {
37163
+ const d = parseDims(tok.shift(), "size", ln);
37164
+ room.w = d.w;
37165
+ room.h = d.h;
37166
+ } else if (t.word === "fill") room.fill = parseId(tok.shift(), "a fill color", ln);
37167
+ else if (t.word === "nolabel") room.nolabel = true;
37168
+ else throw new FloorplanParseError(`room: unexpected token "${t.word}"`, ln);
37169
+ }
37170
+ ast.rooms.push(room);
37171
+ }
37172
+ function parseExtend(tok, ast, ln) {
37173
+ const room = parseId(tok.shift(), "a room id", ln);
37174
+ const ext = { room, w: 2, h: 2, line: ln };
37175
+ while (tok.length) {
37176
+ const t = tok.shift();
37177
+ if (!isWord(t)) throw new FloorplanParseError(`extend: unexpected string "${t.str}"`, ln);
37178
+ else if (t.word === "at") ext.at = parseCoord2(tok.shift(), "at", ln);
37179
+ else if (REL_HOW.includes(t.word)) {
37180
+ ext.rel = {
37181
+ how: t.word,
37182
+ ref: parseId(tok.shift(), `a room id after "${t.word}"`, ln),
37183
+ offset: void 0,
37184
+ align: void 0
37185
+ };
37186
+ } else if (t.word === "offset") {
37187
+ if (!ext.rel) throw new FloorplanParseError(`"offset" requires a relative placement (right-of/left-of/above/below)`, ln);
37188
+ ext.rel.offset = parseNum(tok.shift(), "offset", ln);
37189
+ } else if (t.word === "align") {
37190
+ if (!ext.rel) throw new FloorplanParseError(`"align" requires a relative placement (right-of/left-of/above/below)`, ln);
37191
+ const a = parseId(tok.shift(), "align (start|center|end)", ln);
37192
+ if (a !== "start" && a !== "center" && a !== "end") {
37193
+ throw new FloorplanParseError(`align must be start|center|end, got "${a}"`, ln);
37194
+ }
37195
+ ext.rel.align = a;
37196
+ } else if (t.word === "size") {
37197
+ const d = parseDims(tok.shift(), "size", ln);
37198
+ ext.w = d.w;
37199
+ ext.h = d.h;
37200
+ } else throw new FloorplanParseError(`extend: unexpected token "${t.word}"`, ln);
37201
+ }
37202
+ ast.extensions.push(ext);
37203
+ }
37204
+ var DOOR_TYPES = ["single", "double", "sliding", "pocket", "bifold"];
37205
+ var WINDOW_TYPES = ["fixed", "sliding", "casement", "bay"];
37206
+ function parseOpening(kind, tok, ast, ln) {
37207
+ const op = {
37208
+ kind,
37209
+ pct: 50,
37210
+ width: 0,
37211
+ // resolved after the form is known
37212
+ hinge: "left",
37213
+ swing: "in",
37214
+ doorType: "single",
37215
+ windowType: "fixed",
37216
+ line: ln
37217
+ };
37218
+ const t0 = tok.shift();
37219
+ if (isWord(t0, "between")) {
37220
+ op.between = [parseId(tok.shift(), "a room id", ln), parseId(tok.shift(), "a second room id", ln)];
37221
+ } else {
37222
+ op.room = parseId(t0, `a room id or "between"`, ln);
37223
+ const side = parseId(tok.shift(), "a wall side", ln);
37224
+ if (!SIDES.includes(side)) {
37225
+ throw new FloorplanParseError(`expected a wall side north|south|east|west, got "${side}"`, ln);
37226
+ }
37227
+ op.side = side;
37228
+ }
37229
+ while (tok.length) {
37230
+ const t = tok.shift();
37231
+ if (!isWord(t)) throw new FloorplanParseError(`${kind}: unexpected string "${t.str}"`, ln);
37232
+ else if (t.word === "at") op.pct = parsePct(tok.shift(), ln);
37233
+ else if (t.word === "width") op.width = parseNum(tok.shift(), "width", ln);
37234
+ else if (t.word === "hinge") {
37235
+ const h = parseId(tok.shift(), "hinge (left|right)", ln);
37236
+ if (h !== "left" && h !== "right") throw new FloorplanParseError(`hinge must be left|right, got "${h}"`, ln);
37237
+ op.hinge = h;
37238
+ } else if (t.word === "swing") {
37239
+ const s = parseId(tok.shift(), "swing (in|out)", ln);
37240
+ if (s !== "in" && s !== "out") throw new FloorplanParseError(`swing must be in|out, got "${s}"`, ln);
37241
+ op.swing = s;
37242
+ } else if (t.word === "type") {
37243
+ const d = parseId(tok.shift(), `${kind} type`, ln);
37244
+ if (kind === "window") {
37245
+ if (!WINDOW_TYPES.includes(d)) {
37246
+ throw new FloorplanParseError(`window type must be ${WINDOW_TYPES.join("|")}, got "${d}"`, ln);
37247
+ }
37248
+ op.windowType = d;
37249
+ } else {
37250
+ if (!DOOR_TYPES.includes(d)) {
37251
+ throw new FloorplanParseError(`door type must be ${DOOR_TYPES.join("|")}, got "${d}"`, ln);
37252
+ }
37253
+ op.doorType = d;
37254
+ }
37255
+ } else throw new FloorplanParseError(`${kind}: unexpected token "${t.word}"`, ln);
37256
+ }
37257
+ if (op.width === 0) {
37258
+ op.width = kind === "window" ? 1.2 : kind === "opening" ? 1 : op.between ? 0.8 : 0.9;
37259
+ }
37260
+ ast.openings.push(op);
37261
+ }
37262
+ function parseFurniture(tok, ast, ln) {
37263
+ const type = parseFurnitureType(tok.shift(), ln);
37264
+ const f = { type, x: 0, y: 0, rotate: 0, line: ln };
37265
+ while (tok.length) {
37266
+ const t = tok.shift();
37267
+ if (isStr(t)) f.label = t.str;
37268
+ else if (t.word === "in") f.room = parseId(tok.shift(), `a room id after "in"`, ln);
37269
+ else if (t.word === "at") {
37270
+ const c = parseCoord2(tok.shift(), "at", ln);
37271
+ f.x = c.x;
37272
+ f.y = c.y;
37273
+ } else if (t.word === "size") f.size = parseDims(tok.shift(), "size", ln);
37274
+ else if (t.word === "rotate") f.rotate = parseNum(tok.shift(), "rotate", ln);
37275
+ else throw new FloorplanParseError(`furniture: unexpected token "${t.word}"`, ln);
37276
+ }
37277
+ ast.furniture.push(f);
37278
+ }
37279
+ function parseArray(mode, tok, ast, ln) {
37280
+ const type = parseFurnitureType(tok.shift(), ln);
37281
+ const a = {
37282
+ mode,
37283
+ type,
37284
+ rows: 1,
37285
+ cols: 1,
37286
+ count: Infinity,
37287
+ rotate: 0,
37288
+ line: ln
37289
+ };
37290
+ while (tok.length) {
37291
+ const t = tok.shift();
37292
+ if (!isWord(t)) throw new FloorplanParseError(`${mode}: unexpected string "${t.str}"`, ln);
37293
+ else if (t.word === "in") a.room = parseId(tok.shift(), `a room id after "in"`, ln);
37294
+ else if (t.word === "rows") a.rows = parseNum(tok.shift(), "rows", ln);
37295
+ else if (t.word === "cols") a.cols = parseNum(tok.shift(), "cols", ln);
37296
+ else if (t.word === "count") a.count = parseNum(tok.shift(), "count", ln);
37297
+ else if (t.word === "area") {
37298
+ a.p1 = parseCoord2(tok.shift(), "area p1", ln);
37299
+ a.p2 = parseCoord2(tok.shift(), "area p2", ln);
37300
+ } else if (t.word === "itemsize") a.itemsize = parseDims(tok.shift(), "itemsize", ln);
37301
+ else if (t.word === "rotate") a.rotate = parseNum(tok.shift(), "rotate", ln);
37302
+ else if (t.word === "center") a.center = parseCoord2(tok.shift(), "center", ln);
37303
+ else if (t.word === "radius") a.radius = parseNum(tok.shift(), "radius", ln);
37304
+ else if (t.word === "from") a.fromDeg = parseNum(tok.shift(), "from", ln);
37305
+ else if (t.word === "to") a.toDeg = parseNum(tok.shift(), "to", ln);
37306
+ else throw new FloorplanParseError(`${mode}: unexpected token "${t.word}"`, ln);
37307
+ }
37308
+ ast.arrays.push(a);
37309
+ }
37310
+ function parseFloorplan(text2) {
37311
+ const ast = {
37312
+ type: "floorplan",
37313
+ title: "Floor Plan",
37314
+ unit: "m",
37315
+ rooms: [],
37316
+ extensions: [],
37317
+ openings: [],
37318
+ furniture: [],
37319
+ arrays: []
37320
+ };
37321
+ let sawHeader = false;
37322
+ const lines = text2.split(/\r?\n/);
37323
+ for (let i = 0; i < lines.length; i++) {
37324
+ const ln = i + 1;
37325
+ const raw = normalizeQuotes3(lines[i]).trim();
37326
+ if (!raw) continue;
37327
+ const all = tokenize7(raw);
37328
+ const tok = [];
37329
+ for (let k = 0; k < all.length; k++) {
37330
+ const t = all[k];
37331
+ if (isWord(t) && (t.word.startsWith("#") || t.word.startsWith("//"))) {
37332
+ const prev = all[k - 1];
37333
+ if (t.word.startsWith("#") && isWord(prev, "fill")) {
37334
+ tok.push(t);
37335
+ continue;
37336
+ }
37337
+ break;
37338
+ }
37339
+ tok.push(t);
37340
+ }
37341
+ if (tok.length === 0) continue;
37342
+ const head = tok.shift();
37343
+ if (!isWord(head)) throw new FloorplanParseError(`unexpected string at line start`, ln);
37344
+ const kw = head.word.toLowerCase();
37345
+ if (kw === "floorplan") {
37346
+ parseHeader3(tok, ast, ln);
37347
+ sawHeader = true;
37348
+ } else if (!sawHeader) {
37349
+ throw new FloorplanParseError(`the first statement must be the "floorplan" header`, ln);
37350
+ } else if (kw === "room") parseRoom(tok, ast, ln);
37351
+ else if (kw === "north") {
37352
+ ast.north = tok.length ? parseNum(tok.shift(), "north rotation (degrees)", ln) : 0;
37353
+ if (tok.length) throw new FloorplanParseError(`north: unexpected trailing tokens`, ln);
37354
+ } else if (kw === "extend") parseExtend(tok, ast, ln);
37355
+ else if (kw === "door" || kw === "window" || kw === "opening") parseOpening(kw, tok, ast, ln);
37356
+ else if (kw === "furniture") parseFurniture(tok, ast, ln);
37357
+ else if (kw === "grid" || kw === "row" || kw === "arc") parseArray(kw, tok, ast, ln);
37358
+ else {
37359
+ throw new FloorplanParseError(
37360
+ `unknown keyword "${kw}". Expected: floorplan, room, extend, door, window, opening, furniture, grid, row, arc`,
37361
+ ln
37362
+ );
37363
+ }
37364
+ }
37365
+ return ast;
37366
+ }
37367
+
37368
+ // src/diagrams/floorplan/layout.ts
37369
+ var FT = 0.3048;
37370
+ var FLOORPLAN_CONST = {
37371
+ /** Wall band thickness, meters (§5). */
37372
+ wallT: 0.2,
37373
+ /** Default render scale, px per meter (§4.1). */
37374
+ scale: 55,
37375
+ /** Jamb margin an opening keeps from the segment ends, meters (§4.3). */
37376
+ jamb: 0.05,
37377
+ /** Dimension band depth outside the plan, meters. */
37378
+ dimBand: 1,
37379
+ /** Outer padding, meters. */
37380
+ pad: 0.45,
37381
+ /** Offsets of the major / minor dimension rows from the plan edge, meters. */
37382
+ dimMajorOff: 0.62,
37383
+ dimMinorOff: 0.3
37384
+ };
37385
+ function formatLength(m, unit) {
37386
+ if (unit === "m") {
37387
+ const v = Math.round(m * 100) / 100;
37388
+ return `${v} m`;
37389
+ }
37390
+ const ftv = m / FT;
37391
+ let f = Math.floor(ftv + 1e-6);
37392
+ let inches = Math.round((ftv - f) * 12);
37393
+ if (inches >= 12) {
37394
+ f += 1;
37395
+ inches = 0;
37396
+ }
37397
+ return inches ? `${f}'${inches}"` : `${f}'`;
37398
+ }
37399
+ function formatArea(areaM2, unit) {
37400
+ if (unit === "m") return `${areaM2.toFixed(1)} m\xB2`;
37401
+ return `${Math.round(areaM2 / (FT * FT))} sq ft`;
37402
+ }
37403
+ var fmtNum = (v) => String(Math.round(v * 100) / 100);
37404
+ var snap = (v) => Math.round(v * 1e6) / 1e6;
37405
+ var ADJ_EPS = 0.051;
37406
+ var MIN_OVERLAP = 0.3;
37407
+ function rectSharedEdge(a, b) {
37408
+ if (Math.abs(a.x + a.w - b.x) < ADJ_EPS || Math.abs(b.x + b.w - a.x) < ADJ_EPS) {
37409
+ const along = Math.abs(a.x + a.w - b.x) < ADJ_EPS ? a.x + a.w : b.x + b.w;
37410
+ const lo = Math.max(a.y, b.y);
37411
+ const hi = Math.min(a.y + a.h, b.y + b.h);
37412
+ if (hi - lo >= MIN_OVERLAP) return { vertical: true, along, lo, hi };
37413
+ }
37414
+ if (Math.abs(a.y + a.h - b.y) < ADJ_EPS || Math.abs(b.y + b.h - a.y) < ADJ_EPS) {
37415
+ const along = Math.abs(a.y + a.h - b.y) < ADJ_EPS ? a.y + a.h : b.y + b.h;
37416
+ const lo = Math.max(a.x, b.x);
37417
+ const hi = Math.min(a.x + a.w, b.x + b.w);
37418
+ if (hi - lo >= MIN_OVERLAP) return { vertical: false, along, lo, hi };
37419
+ }
37420
+ return null;
37421
+ }
37422
+ function roomSharedEdge(a, b) {
37423
+ let best = null;
37424
+ for (const pa of a.parts) {
37425
+ for (const pb of b.parts) {
37426
+ const e = rectSharedEdge(pa, pb);
37427
+ if (e && (!best || e.hi - e.lo > best.edge.hi - best.edge.lo)) {
37428
+ best = { edge: e, aPart: pa, bPart: pb };
37429
+ }
37430
+ }
37431
+ }
37432
+ return best;
37433
+ }
37434
+ function rectOverlap(a, b) {
37435
+ return {
37436
+ ox: Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x),
37437
+ oy: Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y)
37438
+ };
37439
+ }
37440
+ function obbCorners(x, y, w, h, rotDeg, margins) {
37441
+ const [mt, mr, mb, ml] = margins;
37442
+ const cx = x + w / 2;
37443
+ const cy = y + h / 2;
37444
+ const corners = [
37445
+ [x - ml, y - mt],
37446
+ [x + w + mr, y - mt],
37447
+ [x + w + mr, y + h + mb],
37448
+ [x - ml, y + h + mb]
37449
+ ];
37450
+ if (!rotDeg) return corners;
37451
+ const rad = rotDeg * Math.PI / 180;
37452
+ const cos = Math.cos(rad);
37453
+ const sin = Math.sin(rad);
37454
+ return corners.map(([px, py]) => {
37455
+ const dx = px - cx;
37456
+ const dy = py - cy;
37457
+ return [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos];
37458
+ });
37459
+ }
37460
+ function rotatedAabb(x, y, w, h, rotDeg, margins) {
37461
+ const corners = obbCorners(x, y, w, h, rotDeg, margins);
37462
+ let minX = Infinity;
37463
+ let minY = Infinity;
37464
+ let maxX = -Infinity;
37465
+ let maxY = -Infinity;
37466
+ for (const [px, py] of corners) {
37467
+ minX = Math.min(minX, px);
37468
+ minY = Math.min(minY, py);
37469
+ maxX = Math.max(maxX, px);
37470
+ maxY = Math.max(maxY, py);
37471
+ }
37472
+ return { minX, minY, maxX, maxY };
37473
+ }
37474
+ function obbPenetration(a, b) {
37475
+ let minPen = Infinity;
37476
+ for (const poly of [a, b]) {
37477
+ for (let i = 0; i < 4; i++) {
37478
+ const j = (i + 1) % 4;
37479
+ let ax = poly[j][1] - poly[i][1];
37480
+ let ay = poly[i][0] - poly[j][0];
37481
+ const len = Math.hypot(ax, ay);
37482
+ if (len < 1e-12) continue;
37483
+ ax /= len;
37484
+ ay /= len;
37485
+ let aLo = Infinity;
37486
+ let aHi = -Infinity;
37487
+ for (const [px, py] of a) {
37488
+ const v = px * ax + py * ay;
37489
+ aLo = Math.min(aLo, v);
37490
+ aHi = Math.max(aHi, v);
37491
+ }
37492
+ let bLo = Infinity;
37493
+ let bHi = -Infinity;
37494
+ for (const [px, py] of b) {
37495
+ const v = px * ax + py * ay;
37496
+ bLo = Math.min(bLo, v);
37497
+ bHi = Math.max(bHi, v);
37498
+ }
37499
+ const pen = Math.min(aHi, bHi) - Math.max(aLo, bLo);
37500
+ if (pen <= 0) return 0;
37501
+ minPen = Math.min(minPen, pen);
37502
+ }
37503
+ }
37504
+ return minPen === Infinity ? 0 : minPen;
37505
+ }
37506
+ function resolvePlacement(p, w, h, byId, rooms, u, who, errors) {
37507
+ if (p.at) return { x: snap(p.at.x * u), y: snap(p.at.y * u) };
37508
+ if (p.rel) {
37509
+ const refIdx = byId.get(p.rel.ref);
37510
+ if (refIdx === void 0) {
37511
+ errors.push(`${who}: unknown reference room "${p.rel.ref}" \u2014 declare it first`);
37512
+ return null;
37513
+ }
37514
+ const ref = rooms[refIdx];
37515
+ const off = (p.rel.offset ?? 0) * u;
37516
+ const alignPos = (refStart, refLen, len) => {
37517
+ if (p.rel.align === "center") return refStart + (refLen - len) / 2 + off;
37518
+ if (p.rel.align === "end") return refStart + refLen - len + off;
37519
+ return refStart + off;
37520
+ };
37521
+ let x = 0;
37522
+ let y = 0;
37523
+ switch (p.rel.how) {
37524
+ case "right-of":
37525
+ x = ref.x + ref.w;
37526
+ y = alignPos(ref.y, ref.h, h);
37527
+ break;
37528
+ case "left-of":
37529
+ x = ref.x - w;
37530
+ y = alignPos(ref.y, ref.h, h);
37531
+ break;
37532
+ case "below":
37533
+ y = ref.y + ref.h;
37534
+ x = alignPos(ref.x, ref.w, w);
37535
+ break;
37536
+ case "above":
37537
+ y = ref.y - h;
37538
+ x = alignPos(ref.x, ref.w, w);
37539
+ break;
37540
+ }
37541
+ return { x: snap(x), y: snap(y) };
37542
+ }
37543
+ return { x: 0, y: 0 };
37544
+ }
37545
+ function refreshRoomBounds(room, unit) {
37546
+ let minX = Infinity;
37547
+ let minY = Infinity;
37548
+ let maxX = -Infinity;
37549
+ let maxY = -Infinity;
37550
+ let area = 0;
37551
+ for (const p of room.parts) {
37552
+ minX = Math.min(minX, p.x);
37553
+ minY = Math.min(minY, p.y);
37554
+ maxX = Math.max(maxX, p.x + p.w);
37555
+ maxY = Math.max(maxY, p.y + p.h);
37556
+ area += p.w * p.h;
37557
+ }
37558
+ room.x = snap(minX);
37559
+ room.y = snap(minY);
37560
+ room.w = snap(maxX - minX);
37561
+ room.h = snap(maxY - minY);
37562
+ room.areaM2 = area;
37563
+ room.areaText = formatArea(area, unit);
37564
+ }
37565
+ function subtractIntervals(lo, hi, cuts) {
37566
+ let pieces = [[lo, hi]];
37567
+ for (const [cLo, cHi] of cuts) {
37568
+ const next = [];
37569
+ for (const [pLo, pHi] of pieces) {
37570
+ if (cHi <= pLo + 1e-9 || cLo >= pHi - 1e-9) {
37571
+ next.push([pLo, pHi]);
37572
+ continue;
37573
+ }
37574
+ if (cLo > pLo + 1e-9) next.push([pLo, Math.min(cLo, pHi)]);
37575
+ if (cHi < pHi - 1e-9) next.push([Math.max(cHi, pLo), pHi]);
37576
+ }
37577
+ pieces = next;
37578
+ }
37579
+ return pieces.filter(([a, b]) => b - a > 1e-9);
37580
+ }
37581
+ function sideSegments(room, side) {
37582
+ const segs = [];
37583
+ for (const p of room.parts) {
37584
+ const vertical = side === "west" || side === "east";
37585
+ const along = side === "north" ? p.y : side === "south" ? p.y + p.h : side === "west" ? p.x : p.x + p.w;
37586
+ const lo = vertical ? p.y : p.x;
37587
+ const hi = vertical ? p.y + p.h : p.x + p.w;
37588
+ const cuts = [];
37589
+ for (const q of room.parts) {
37590
+ if (q === p) continue;
37591
+ const qNear = side === "north" ? q.y + q.h : side === "south" ? q.y : side === "west" ? q.x + q.w : q.x;
37592
+ if (Math.abs(qNear - along) >= ADJ_EPS) continue;
37593
+ const cLo = vertical ? Math.max(lo, q.y) : Math.max(lo, q.x);
37594
+ const cHi = vertical ? Math.min(hi, q.y + q.h) : Math.min(hi, q.x + q.w);
37595
+ if (cHi > cLo) cuts.push([cLo, cHi]);
37596
+ }
37597
+ for (const [sLo, sHi] of subtractIntervals(lo, hi, cuts)) {
37598
+ if (sHi - sLo >= MIN_OVERLAP) segs.push({ along, lo: sLo, hi: sHi });
37599
+ }
37600
+ }
37601
+ return segs.sort((a, b) => a.lo - b.lo || a.along - b.along);
37602
+ }
37603
+ function layoutFloorplan(ast) {
37604
+ const u = ast.unit === "ft" ? FT : 1;
37605
+ const errors = [];
37606
+ const warnings = [];
37607
+ const rooms = [];
37608
+ const byId = /* @__PURE__ */ new Map();
37609
+ const stmts = [
37610
+ ...ast.rooms.map((room) => ({ line: room.line ?? 0, room })),
37611
+ ...ast.extensions.map((ext) => ({ line: ext.line ?? 0, ext }))
37612
+ ].sort((a, b) => a.line - b.line);
37613
+ for (const stmt of stmts) {
37614
+ if (stmt.room) {
37615
+ const r6 = stmt.room;
37616
+ const w = r6.w * u;
37617
+ const h = r6.h * u;
37618
+ const pos = resolvePlacement(r6, w, h, byId, rooms, u, `room "${r6.id}"`, errors) ?? { x: 0, y: 0 };
37619
+ const part = { x: pos.x, y: pos.y, w, h };
37620
+ const room = {
37621
+ id: r6.id,
37622
+ label: r6.label,
37623
+ x: part.x,
37624
+ y: part.y,
37625
+ w,
37626
+ h,
37627
+ parts: [part],
37628
+ areaM2: 0,
37629
+ areaText: "",
37630
+ fill: r6.fill,
37631
+ nolabel: r6.nolabel ?? false
37632
+ };
37633
+ refreshRoomBounds(room, ast.unit);
37634
+ byId.set(r6.id, rooms.length);
37635
+ rooms.push(room);
37636
+ } else if (stmt.ext) {
37637
+ const e = stmt.ext;
37638
+ const idx = byId.get(e.room);
37639
+ if (idx === void 0) {
37640
+ errors.push(`extend: unknown room "${e.room}" \u2014 declare it first`);
37641
+ continue;
37642
+ }
37643
+ const room = rooms[idx];
37644
+ const w = e.w * u;
37645
+ const h = e.h * u;
37646
+ const pos = resolvePlacement(e, w, h, byId, rooms, u, `extend "${e.room}"`, errors);
37647
+ if (!pos) continue;
37648
+ const part = { x: pos.x, y: pos.y, w, h };
37649
+ let touches = false;
37650
+ let overlaps = false;
37651
+ for (const p of room.parts) {
37652
+ const { ox, oy } = rectOverlap(p, part);
37653
+ if (ox > ADJ_EPS && oy > ADJ_EPS) overlaps = true;
37654
+ if (rectSharedEdge(p, part)) touches = true;
37655
+ }
37656
+ if (overlaps) {
37657
+ errors.push(`extend "${e.room}": extension overlaps the room's existing area \u2014 place it edge-to-edge`);
37658
+ continue;
37659
+ }
37660
+ if (!touches) {
37661
+ errors.push(`extend "${e.room}": extension does not touch the room \u2014 extensions must share an edge`);
37662
+ continue;
37663
+ }
37664
+ room.parts.push(part);
37665
+ refreshRoomBounds(room, ast.unit);
37666
+ }
37667
+ }
37668
+ for (let i = 0; i < rooms.length; i++) {
37669
+ for (let j = i + 1; j < rooms.length; j++) {
37670
+ const a = rooms[i];
37671
+ const b = rooms[j];
37672
+ let worst = null;
37673
+ for (const pa of a.parts) {
37674
+ for (const pb of b.parts) {
37675
+ const { ox, oy } = rectOverlap(pa, pb);
37676
+ if (ox > ADJ_EPS && oy > ADJ_EPS && (!worst || ox * oy > worst.ox * worst.oy)) {
37677
+ worst = { ox, oy };
37678
+ }
37679
+ }
37680
+ }
37681
+ if (worst) {
37682
+ errors.push(
37683
+ `rooms "${a.id}" and "${b.id}" overlap by ${worst.ox.toFixed(2)}\xD7${worst.oy.toFixed(2)} m \u2014 move "${b.id}" right-of "${a.id}" or shrink size`
37684
+ );
37685
+ }
37686
+ }
37687
+ }
37688
+ const seams = [];
37689
+ for (let ri = 0; ri < rooms.length; ri++) {
37690
+ const parts = rooms[ri].parts;
37691
+ for (let i = 0; i < parts.length; i++) {
37692
+ for (let j = i + 1; j < parts.length; j++) {
37693
+ const e = rectSharedEdge(parts[i], parts[j]);
37694
+ if (e) seams.push({ vertical: e.vertical, along: e.along, lo: e.lo, hi: e.hi, room: ri });
37695
+ }
37696
+ }
37697
+ }
37698
+ const openings = [];
37699
+ for (const op of ast.openings) {
37700
+ const geom = resolveOpening(op, rooms, byId, u, ast.unit, errors, warnings);
37701
+ if (geom) openings.push(geom);
37702
+ }
37703
+ const items = [];
37704
+ const seqByType = /* @__PURE__ */ new Map();
37705
+ const place = (type, roomIdx, localX, localY, w, h, rotate, label) => {
37706
+ const room = rooms[roomIdx];
37707
+ const seq = (seqByType.get(type) ?? 0) + 1;
37708
+ seqByType.set(type, seq);
37709
+ items.push({ type, x: room.x + localX, y: room.y + localY, w, h, rotate, label, roomId: room.id, seq });
37710
+ };
37711
+ const roomIdxOf = (stmt, roomId, line2) => {
37712
+ if (!roomId) {
37713
+ errors.push(`${stmt}${line2 ? ` (line ${line2})` : ""}: missing "in <room>"`);
37714
+ return void 0;
37715
+ }
37716
+ const idx = byId.get(roomId);
37717
+ if (idx === void 0) {
37718
+ errors.push(`${stmt}: unknown room "${roomId}"`);
37719
+ return void 0;
37720
+ }
37721
+ return idx;
37722
+ };
37723
+ for (const f of ast.furniture) {
37724
+ const def = FLOORPLAN_SYMBOLS[f.type];
37725
+ const idx = roomIdxOf(`furniture ${f.type}`, f.room, f.line);
37726
+ if (idx === void 0) continue;
37727
+ const w = f.size ? f.size.w * u : def.w;
37728
+ const h = f.size ? f.size.h * u : def.h;
37729
+ place(f.type, idx, f.x * u, f.y * u, w, h, f.rotate, f.label);
37730
+ }
37731
+ for (const a of ast.arrays) {
37732
+ const def = FLOORPLAN_SYMBOLS[a.type];
37733
+ const idx = roomIdxOf(`${a.mode} ${a.type}`, a.room, a.line);
37734
+ if (idx === void 0) continue;
37735
+ const room = rooms[idx];
37736
+ const iw = a.itemsize ? a.itemsize.w * u : def.w;
37737
+ const ih = a.itemsize ? a.itemsize.h * u : def.h;
37738
+ const p1 = a.p1 ? { x: a.p1.x * u, y: a.p1.y * u } : { x: 0.5 * u, y: 0.5 * u };
37739
+ const p2 = a.p2 ? { x: a.p2.x * u, y: a.p2.y * u } : { x: room.w - 0.5 * u, y: room.h - 0.5 * u };
37740
+ if (a.mode === "grid" || a.mode === "row") {
37741
+ const nRows = a.mode === "row" ? 1 : Math.max(1, Math.round(a.rows));
37742
+ const nCols = Math.max(1, Math.round(a.cols));
37743
+ const cap2 = Number.isFinite(a.count) ? a.count : nRows * nCols;
37744
+ const spanW = p2.x - p1.x;
37745
+ const spanH = p2.y - p1.y;
37746
+ let placed = 0;
37747
+ for (let r6 = 0; r6 < nRows && placed < cap2; r6++) {
37748
+ for (let col = 0; col < nCols && placed < cap2; col++) {
37749
+ const cx = p1.x + (nCols === 1 ? spanW / 2 : col * spanW / (nCols - 1));
37750
+ const cy = p1.y + (nRows === 1 ? spanH / 2 : r6 * spanH / (nRows - 1));
37751
+ place(a.type, idx, cx - iw / 2, cy - ih / 2, iw, ih, a.rotate);
37752
+ placed++;
37753
+ }
37754
+ }
37755
+ } else {
37756
+ const n = Math.max(1, Number.isFinite(a.count) ? a.count : Math.max(1, Math.round(a.cols)));
37757
+ const cen = a.center ? { x: a.center.x * u, y: a.center.y * u } : { x: room.w / 2, y: room.h * 0.6 };
37758
+ const radius = a.radius !== void 0 ? a.radius * u : Math.min(room.w, room.h) / 3;
37759
+ const a0 = (a.fromDeg ?? 200) * Math.PI / 180;
37760
+ const a1 = (a.toDeg ?? 340) * Math.PI / 180;
37761
+ for (let i = 0; i < n; i++) {
37762
+ const th = a0 + (a1 - a0) * i / Math.max(n - 1, 1);
37763
+ const cx = cen.x + radius * Math.cos(th);
37764
+ const cy = cen.y + radius * Math.sin(th);
37765
+ const facing = th * 180 / Math.PI + 270;
37766
+ place(a.type, idx, cx - iw / 2, cy - ih / 2, iw, ih, facing + a.rotate);
37767
+ }
37768
+ }
37769
+ }
37770
+ const roomOf = /* @__PURE__ */ new Map();
37771
+ for (const r6 of rooms) roomOf.set(r6.id, r6);
37772
+ const warnItems = /* @__PURE__ */ new Set();
37773
+ for (const it of items) {
37774
+ const room = roomOf.get(it.roomId);
37775
+ if (!room) continue;
37776
+ const bb = rotatedAabb(it.x, it.y, it.w, it.h, it.rotate, [0, 0, 0, 0]);
37777
+ const over = Math.max(
37778
+ room.x - bb.minX,
37779
+ bb.maxX - (room.x + room.w),
37780
+ room.y - bb.minY,
37781
+ bb.maxY - (room.y + room.h)
37782
+ );
37783
+ if (over > 0.011) {
37784
+ errors.push(
37785
+ `furniture ${it.type} #${it.seq} extends ${fmtNum(over)} m outside room "${it.roomId}" \u2014 move it or shrink size`
37786
+ );
37787
+ continue;
37788
+ }
37789
+ if (room.parts.length > 1) {
37790
+ const bw = bb.maxX - bb.minX;
37791
+ const bh = bb.maxY - bb.minY;
37792
+ let covered = 0;
37793
+ for (const p of room.parts) {
37794
+ const ox = Math.min(p.x + p.w, bb.maxX) - Math.max(p.x, bb.minX);
37795
+ const oy = Math.min(p.y + p.h, bb.maxY) - Math.max(p.y, bb.minY);
37796
+ if (ox > 0 && oy > 0) covered += ox * oy;
37797
+ }
37798
+ const uncovered = bw * bh - covered;
37799
+ if (uncovered > 0.01) {
37800
+ errors.push(
37801
+ `furniture ${it.type} #${it.seq} sits outside room "${it.roomId}"'s L-shape (${fmtNum(uncovered)} m\xB2 past the notch) \u2014 move it onto a room part`
37802
+ );
37803
+ }
37804
+ }
37805
+ }
37806
+ const envelopes = items.map((it) => {
37807
+ const def = FLOORPLAN_SYMBOLS[it.type];
37808
+ return obbCorners(it.x, it.y, it.w, it.h, it.rotate, def.envelope ?? [0, 0, 0, 0]);
37809
+ });
37810
+ for (let i = 0; i < items.length; i++) {
37811
+ const a = items[i];
37812
+ if (FLOORPLAN_SYMBOLS[a.type].underlay) continue;
37813
+ for (let j = i + 1; j < items.length; j++) {
37814
+ const b = items[j];
37815
+ if (FLOORPLAN_SYMBOLS[b.type].underlay) continue;
37816
+ const pen = obbPenetration(envelopes[i], envelopes[j]);
37817
+ if (pen > 0.011) {
37818
+ warnings.push(
37819
+ `${a.type} #${a.seq} overlaps ${b.type} #${b.seq} by ${fmtNum(pen)} m \u2014 increase spacing or reduce cols`
37820
+ );
37821
+ warnItems.add(i);
37822
+ warnItems.add(j);
37823
+ }
37824
+ }
37825
+ }
37826
+ let minX = Infinity;
37827
+ let minY = Infinity;
37828
+ let maxX = -Infinity;
37829
+ let maxY = -Infinity;
37830
+ for (const r6 of rooms) {
37831
+ minX = Math.min(minX, r6.x);
37832
+ minY = Math.min(minY, r6.y);
37833
+ maxX = Math.max(maxX, r6.x + r6.w);
37834
+ maxY = Math.max(maxY, r6.y + r6.h);
37835
+ }
37836
+ if (rooms.length === 0) {
37837
+ minX = minY = 0;
37838
+ maxX = maxY = 1;
37839
+ errors.push('no rooms defined \u2014 declare at least one: room id "Label" at 0,0 size 4x3');
37840
+ }
37841
+ const dims = [];
37842
+ if (rooms.length > 0) {
37843
+ dims.push({
37844
+ vertical: false,
37845
+ at: minY - FLOORPLAN_CONST.dimMajorOff,
37846
+ lo: minX,
37847
+ hi: maxX,
37848
+ label: formatLength(maxX - minX, ast.unit),
37849
+ minor: false
37850
+ });
37851
+ dims.push({
37852
+ vertical: true,
37853
+ at: minX - FLOORPLAN_CONST.dimMajorOff,
37854
+ lo: minY,
37855
+ hi: maxY,
37856
+ label: formatLength(maxY - minY, ast.unit),
37857
+ minor: false
37858
+ });
37859
+ const topSegs = [];
37860
+ for (const r6 of rooms) {
37861
+ for (const sg of sideSegments(r6, "north")) {
37862
+ if (Math.abs(sg.along - minY) < 0.01) topSegs.push({ lo: sg.lo, hi: sg.hi });
37863
+ }
37864
+ }
37865
+ if (topSegs.length > 1) {
37866
+ for (const sg of topSegs.sort((a, b) => a.lo - b.lo)) {
37867
+ dims.push({
37868
+ vertical: false,
37869
+ at: minY - FLOORPLAN_CONST.dimMinorOff,
37870
+ lo: sg.lo,
37871
+ hi: sg.hi,
37872
+ label: formatLength(sg.hi - sg.lo, ast.unit),
37873
+ minor: true
37874
+ });
37875
+ }
37876
+ }
37877
+ const leftSegs = [];
37878
+ for (const r6 of rooms) {
37879
+ for (const sg of sideSegments(r6, "west")) {
37880
+ if (Math.abs(sg.along - minX) < 0.01) leftSegs.push({ lo: sg.lo, hi: sg.hi });
37881
+ }
37882
+ }
37883
+ if (leftSegs.length > 1) {
37884
+ for (const sg of leftSegs.sort((a, b) => a.lo - b.lo)) {
37885
+ dims.push({
37886
+ vertical: true,
37887
+ at: minX - FLOORPLAN_CONST.dimMinorOff,
37888
+ lo: sg.lo,
37889
+ hi: sg.hi,
37890
+ label: formatLength(sg.hi - sg.lo, ast.unit),
37891
+ minor: true
37892
+ });
37893
+ }
37894
+ }
37895
+ }
37896
+ return {
37897
+ title: ast.title,
37898
+ unit: ast.unit,
37899
+ north: ast.north,
37900
+ rooms,
37901
+ seams,
37902
+ openings,
37903
+ items,
37904
+ dims,
37905
+ bounds: { minX, minY, maxX, maxY },
37906
+ wallT: FLOORPLAN_CONST.wallT,
37907
+ totalAreaM2: rooms.reduce((s, r6) => s + r6.areaM2, 0),
37908
+ errors,
37909
+ warnings,
37910
+ warnItems: [...warnItems]
37911
+ };
37912
+ }
37913
+ function resolveOpening(op, rooms, byId, u, unit, errors, warnings) {
37914
+ let seg = null;
37915
+ let owner = -1;
37916
+ let negRoom;
37917
+ let posRoom;
37918
+ let inward = 1;
37919
+ if (op.between) {
37920
+ const ia = byId.get(op.between[0]);
37921
+ const ib = byId.get(op.between[1]);
37922
+ if (ia === void 0 || ib === void 0) {
37923
+ errors.push(`${op.kind}: unknown room "${ia === void 0 ? op.between[0] : op.between[1]}"`);
37924
+ return null;
37925
+ }
37926
+ const a = rooms[ia];
37927
+ const b = rooms[ib];
37928
+ const found = roomSharedEdge(a, b);
37929
+ if (!found) {
37930
+ const gapX = Math.max(a.x, b.x) - Math.min(a.x + a.w, b.x + b.w);
37931
+ const gapY = Math.max(a.y, b.y) - Math.min(a.y + a.h, b.y + b.h);
37932
+ const axis = gapX >= gapY ? "x" : "y";
37933
+ const gap = Math.max(gapX, gapY);
37934
+ errors.push(
37935
+ gap > 0 ? `${op.kind} between "${a.id}" and "${b.id}": rooms share no wall (gap ${fmtNum(gap)} m on ${axis}-axis)` : `${op.kind} between "${a.id}" and "${b.id}": rooms share no wall`
37936
+ );
37937
+ return null;
37938
+ }
37939
+ seg = found.edge;
37940
+ owner = ia;
37941
+ if (seg.vertical) {
37942
+ const aIsNeg = found.aPart.x + found.aPart.w / 2 < seg.along;
37943
+ negRoom = aIsNeg ? ia : ib;
37944
+ posRoom = aIsNeg ? ib : ia;
37945
+ inward = aIsNeg ? -1 : 1;
37946
+ } else {
37947
+ const aIsNeg = found.aPart.y + found.aPart.h / 2 < seg.along;
37948
+ negRoom = aIsNeg ? ia : ib;
37949
+ posRoom = aIsNeg ? ib : ia;
37950
+ inward = aIsNeg ? -1 : 1;
37951
+ }
37952
+ } else {
37953
+ const idx = byId.get(op.room);
37954
+ if (idx === void 0) {
37955
+ errors.push(`${op.kind}: unknown room "${op.room}"`);
37956
+ return null;
37957
+ }
37958
+ owner = idx;
37959
+ const r6 = rooms[idx];
37960
+ const side = op.side;
37961
+ const segs = sideSegments(r6, side);
37962
+ if (segs.length === 0) {
37963
+ errors.push(`${op.kind} on "${r6.id}" ${side}: that side has no exterior wall segment`);
37964
+ return null;
37965
+ }
37966
+ const total = segs.reduce((s, sg) => s + (sg.hi - sg.lo), 0);
37967
+ const pct2 = Math.min(100, Math.max(0, op.pct));
37968
+ let target = total * pct2 / 100;
37969
+ let chosen = segs[segs.length - 1];
37970
+ for (const sg of segs) {
37971
+ const len = sg.hi - sg.lo;
37972
+ if (target <= len) {
37973
+ chosen = sg;
37974
+ break;
37975
+ }
37976
+ target -= len;
37977
+ }
37978
+ const within = Math.min(1, Math.max(0, target / (chosen.hi - chosen.lo)));
37979
+ op = { ...op, pct: within * 100 };
37980
+ seg = { vertical: side === "west" || side === "east", along: chosen.along, lo: chosen.lo, hi: chosen.hi };
37981
+ switch (side) {
37982
+ case "north":
37983
+ inward = 1;
37984
+ posRoom = idx;
37985
+ break;
37986
+ case "south":
37987
+ inward = -1;
37988
+ negRoom = idx;
37989
+ break;
37990
+ case "west":
37991
+ inward = 1;
37992
+ posRoom = idx;
37993
+ break;
37994
+ case "east":
37995
+ inward = -1;
37996
+ negRoom = idx;
37997
+ break;
37998
+ }
37999
+ }
38000
+ const jamb = FLOORPLAN_CONST.jamb;
38001
+ const segLen = seg.hi - seg.lo;
38002
+ const avail = segLen - 2 * jamb;
38003
+ let wd = op.width * u;
38004
+ if (wd > avail) {
38005
+ warnings.push(
38006
+ `${op.kind}${op.room ? ` on "${op.room}" ${op.side}` : op.between ? ` between "${op.between[0]}" and "${op.between[1]}"` : ""}: width ${formatLength(wd, unit)} clamped to ${formatLength(avail, unit)} to fit the wall segment`
38007
+ );
38008
+ wd = avail;
38009
+ }
38010
+ const pct = Math.min(100, Math.max(0, op.pct));
38011
+ const c = seg.lo + segLen * pct / 100;
38012
+ const lo = Math.max(seg.lo + jamb, Math.min(c - wd / 2, seg.hi - jamb - wd));
38013
+ const hi = lo + wd;
38014
+ const arcDir = op.kind === "door" && op.swing === "out" ? inward === 1 ? -1 : 1 : inward;
38015
+ return {
38016
+ kind: op.kind,
38017
+ doorType: op.doorType,
38018
+ windowType: op.windowType,
38019
+ vertical: seg.vertical,
38020
+ along: seg.along,
38021
+ lo,
38022
+ hi,
38023
+ inward: arcDir,
38024
+ hinge: op.hinge,
38025
+ negRoom,
38026
+ posRoom,
38027
+ owner
38028
+ };
38029
+ }
38030
+
38031
+ // src/diagrams/floorplan/renderer.ts
38032
+ var r24 = (n) => Math.round(n * 100) / 100;
38033
+ function buildCss13(t) {
38034
+ return `
38035
+ .sx-fp { font-family: ${DEFAULT_FONT_FAMILY}; }
38036
+ .sx-fp-title { font: ${TITLE.weight} ${TITLE.size}px sans-serif; fill: ${t.text}; }
38037
+ .sx-fp-wall { fill: ${t.wallFill}; stroke: none; }
38038
+ .sx-fp-furn { fill: ${t.furnFill}; stroke: ${t.furnStroke}; stroke-width: 1.2; }
38039
+ .sx-fp-furn-nofill { fill: none; stroke: ${t.furnStroke}; stroke-width: 1.2; }
38040
+ .sx-fp-furn-line { fill: none; stroke: ${t.furnStroke}; stroke-width: 1; }
38041
+ .sx-fp-furn-dash { fill: none; stroke: ${t.furnStroke}; stroke-width: 1; stroke-dasharray: 4 3; }
38042
+ .sx-fp-furn-dot { fill: ${t.furnStroke}; stroke: none; }
38043
+ .sx-fp-furn-solid { fill: ${t.furnSolid}; stroke: none; }
38044
+ .sx-fp-board-inner { fill: ${t.boardInner}; stroke: none; }
38045
+ .sx-fp-chair { fill: ${t.chairFill}; stroke: ${t.furnStroke}; stroke-width: 1; }
38046
+ .sx-fp-rug { fill: none; stroke: ${t.rugStroke}; stroke-width: 1.2; stroke-dasharray: 5 4; }
38047
+ .sx-fp-hatch { fill: none; stroke: ${t.hatchStroke}; stroke-width: 1; }
38048
+ .sx-fp-furn-text { font-weight: 600; font-family: sans-serif; fill: ${t.furnLabel}; paint-order: stroke; stroke: ${t.floorFill}; stroke-width: 2.5px; stroke-linejoin: round; }
38049
+ .sx-fp-furn-label { font: 11px sans-serif; fill: ${t.furnLabel}; paint-order: stroke; stroke: ${t.floorFill}; stroke-width: 3px; stroke-linejoin: round; }
38050
+ .sx-fp-door-leaf { fill: none; stroke: ${t.doorLeaf}; stroke-width: 1.6; }
38051
+ .sx-fp-door-arc { fill: none; stroke: ${t.doorArc}; stroke-width: 1; }
38052
+ .sx-fp-window { fill: none; stroke: ${t.windowStroke}; stroke-width: 1.3; }
38053
+ .sx-fp-jamb { fill: none; stroke: ${t.furnStroke}; stroke-width: 1.2; }
38054
+ .sx-fp-room-name { font: 600 13.5px sans-serif; fill: ${t.roomName}; paint-order: stroke; stroke: ${t.floorFill}; stroke-width: 3px; stroke-linejoin: round; }
38055
+ .sx-fp-room-area { font: 11px sans-serif; fill: ${t.roomArea}; paint-order: stroke; stroke: ${t.floorFill}; stroke-width: 3px; stroke-linejoin: round; }
38056
+ .sx-fp-stair-break { fill: none; stroke: ${t.furnStroke}; stroke-width: 1.6; }
38057
+ .sx-fp-compass { fill: none; stroke: ${t.dimStroke}; stroke-width: 1.2; }
38058
+ .sx-fp-compass-n { font: 700 11px sans-serif; fill: ${t.dimText}; }
38059
+ .sx-fp-dim { fill: none; stroke: ${t.dimStroke}; stroke-width: 1; }
38060
+ .sx-fp-dim-text { font: 10.5px sans-serif; fill: ${t.dimText}; }
38061
+ .sx-fp-dim-text-minor { font: 9px sans-serif; fill: ${t.dimText}; }
38062
+ .sx-fp-warn-item { fill: none; stroke: ${t.negative}; stroke-width: 1.5; stroke-dasharray: 4 3; }
38063
+ .sx-fp-warn { font: 11px ui-monospace, Menlo, monospace; fill: ${t.warn}; }
38064
+ .sx-fp-error-box { fill: ${t.bg}; stroke: ${t.negative}; stroke-width: 1.5; }
38065
+ .sx-fp-error-title { font: 700 13px ui-monospace, Menlo, monospace; fill: ${t.negative}; }
38066
+ .sx-fp-error-line { font: 12px ui-monospace, Menlo, monospace; fill: ${t.negative}; }
38067
+ `.trim();
38068
+ }
38069
+ function renderErrorPanel(lay, t) {
38070
+ const lines = lay.errors;
38071
+ const w = Math.max(560, ...lines.map((l) => l.length * 6.6 + 48));
38072
+ const h = 56 + lines.length * 19;
38073
+ return svgRoot(
38074
+ { viewBox: `0 0 ${r24(w)} ${h}`, width: r24(w), height: h, class: "sx-fp", role: "img" },
38075
+ [
38076
+ title(lay.title),
38077
+ desc(`Floor plan validation failed with ${lines.length} error${lines.length === 1 ? "" : "s"}.`),
38078
+ el("style", {}, buildCss13(t)),
38079
+ rect({ class: "sx-fp-error-box", x: 1, y: 1, width: r24(w - 2), height: h - 2, rx: 6 }),
38080
+ text({ class: "sx-fp-error-title", x: 16, y: 26 }, `floorplan: ${lines.length} validation error${lines.length === 1 ? "" : "s"}`),
38081
+ ...lines.map((e, i) => text({ class: "sx-fp-error-line", x: 16, y: 50 + i * 19 }, `\u26A0 ${e}`))
38082
+ ]
38083
+ );
38084
+ }
38085
+ function gapFill(lay, t, roomIdx) {
38086
+ if (roomIdx === void 0) return t.bg;
38087
+ return lay.rooms[roomIdx]?.fill ?? t.floorFill;
38088
+ }
38089
+ function punchGap(o, lay, c) {
38090
+ const tpx = c.px(c.wallT);
38091
+ const negFill = gapFill(lay, c.t, o.negRoom);
38092
+ const posFill = gapFill(lay, c.t, o.posRoom);
38093
+ if (o.vertical) {
38094
+ const x2 = c.X(o.along);
38095
+ const y2 = c.Y(o.lo);
38096
+ const h = c.px(o.hi - o.lo);
38097
+ return rect({ class: "sx-fp-gap", fill: negFill, x: r24(x2 - tpx / 2 - 0.5), y: y2, width: r24(tpx / 2 + 0.5), height: h }) + rect({ class: "sx-fp-gap", fill: posFill, x: x2, y: y2, width: r24(tpx / 2 + 0.5), height: h });
38098
+ }
38099
+ const y = c.Y(o.along);
38100
+ const x = c.X(o.lo);
38101
+ const w = c.px(o.hi - o.lo);
38102
+ return rect({ class: "sx-fp-gap", fill: negFill, x, y: r24(y - tpx / 2 - 0.5), width: w, height: r24(tpx / 2 + 0.5) }) + rect({ class: "sx-fp-gap", fill: posFill, x, y, width: w, height: r24(tpx / 2 + 0.5) });
38103
+ }
38104
+ function windowSymbol(o, c) {
38105
+ const tpx = c.px(c.wallT);
38106
+ const parts = [];
38107
+ const outward = o.inward === 1 ? -1 : 1;
38108
+ if (o.vertical) {
38109
+ const x = c.X(o.along);
38110
+ const mid = c.Y((o.lo + o.hi) / 2);
38111
+ if (o.windowType === "sliding") {
38112
+ parts.push(line({ class: "sx-fp-window", x1: r24(x - tpx * 0.22), y1: c.Y(o.lo), x2: r24(x - tpx * 0.22), y2: mid }));
38113
+ parts.push(line({ class: "sx-fp-window", x1: r24(x + tpx * 0.22), y1: mid, x2: r24(x + tpx * 0.22), y2: c.Y(o.hi) }));
38114
+ } else {
38115
+ for (const k of [-1, 0, 1]) {
38116
+ parts.push(line({ class: "sx-fp-window", x1: r24(x + k * tpx * 0.38), y1: c.Y(o.lo), x2: r24(x + k * tpx * 0.38), y2: c.Y(o.hi) }));
38117
+ }
38118
+ }
38119
+ for (const yy of [o.lo, o.hi]) {
38120
+ parts.push(line({ class: "sx-fp-window", x1: r24(x - tpx / 2), y1: c.Y(yy), x2: r24(x + tpx / 2), y2: c.Y(yy) }));
38121
+ }
38122
+ if (o.windowType === "casement") {
38123
+ const wd = c.px(o.hi - o.lo);
38124
+ const leafX = r24(x + outward * wd);
38125
+ const sweep = outward === 1 ? 1 : 0;
38126
+ parts.push(line({ class: "sx-fp-door-leaf", x1: x, y1: c.Y(o.lo), x2: leafX, y2: c.Y(o.lo) }));
38127
+ parts.push(path({ class: "sx-fp-door-arc", d: `M ${leafX} ${c.Y(o.lo)} A ${wd} ${wd} 0 0 ${sweep} ${x} ${r24(c.Y(o.lo) + wd)}` }));
38128
+ }
38129
+ if (o.windowType === "bay") {
38130
+ const proj = c.px(0.45) * outward;
38131
+ const splay = c.px(0.3);
38132
+ const x0 = r24(x + outward * tpx / 2);
38133
+ parts.push(
38134
+ path({
38135
+ class: "sx-fp-window",
38136
+ d: `M ${x0} ${c.Y(o.lo)} L ${r24(x0 + proj)} ${r24(c.Y(o.lo) + splay)} L ${r24(x0 + proj)} ${r24(c.Y(o.hi) - splay)} L ${x0} ${c.Y(o.hi)}`
38137
+ })
38138
+ );
38139
+ }
38140
+ } else {
38141
+ const y = c.Y(o.along);
38142
+ const mid = c.X((o.lo + o.hi) / 2);
38143
+ if (o.windowType === "sliding") {
38144
+ parts.push(line({ class: "sx-fp-window", x1: c.X(o.lo), y1: r24(y - tpx * 0.22), x2: mid, y2: r24(y - tpx * 0.22) }));
38145
+ parts.push(line({ class: "sx-fp-window", x1: mid, y1: r24(y + tpx * 0.22), x2: c.X(o.hi), y2: r24(y + tpx * 0.22) }));
38146
+ } else {
38147
+ for (const k of [-1, 0, 1]) {
38148
+ parts.push(line({ class: "sx-fp-window", x1: c.X(o.lo), y1: r24(y + k * tpx * 0.38), x2: c.X(o.hi), y2: r24(y + k * tpx * 0.38) }));
38149
+ }
38150
+ }
38151
+ for (const xx of [o.lo, o.hi]) {
38152
+ parts.push(line({ class: "sx-fp-window", x1: c.X(xx), y1: r24(y - tpx / 2), x2: c.X(xx), y2: r24(y + tpx / 2) }));
38153
+ }
38154
+ if (o.windowType === "casement") {
38155
+ const wd = c.px(o.hi - o.lo);
38156
+ const leafY = r24(y + outward * wd);
38157
+ const sweep = outward === 1 ? 0 : 1;
38158
+ parts.push(line({ class: "sx-fp-door-leaf", x1: c.X(o.lo), y1: y, x2: c.X(o.lo), y2: leafY }));
38159
+ parts.push(path({ class: "sx-fp-door-arc", d: `M ${c.X(o.lo)} ${leafY} A ${wd} ${wd} 0 0 ${sweep} ${r24(c.X(o.lo) + wd)} ${y}` }));
38160
+ }
38161
+ if (o.windowType === "bay") {
38162
+ const proj = c.px(0.45) * outward;
38163
+ const splay = c.px(0.3);
38164
+ const y0 = r24(y + outward * tpx / 2);
38165
+ parts.push(
38166
+ path({
38167
+ class: "sx-fp-window",
38168
+ d: `M ${c.X(o.lo)} ${y0} L ${r24(c.X(o.lo) + splay)} ${r24(y0 + proj)} L ${r24(c.X(o.hi) - splay)} ${r24(y0 + proj)} L ${c.X(o.hi)} ${y0}`
38169
+ })
38170
+ );
38171
+ }
38172
+ }
38173
+ return parts.join("");
38174
+ }
38175
+ function jambLines(o, c) {
38176
+ const tpx = c.px(c.wallT);
38177
+ if (o.vertical) {
38178
+ const x = c.X(o.along);
38179
+ return [o.lo, o.hi].map((yy) => line({ class: "sx-fp-jamb", x1: r24(x - tpx / 2), y1: c.Y(yy), x2: r24(x + tpx / 2), y2: c.Y(yy) })).join("");
38180
+ }
38181
+ const y = c.Y(o.along);
38182
+ return [o.lo, o.hi].map((xx) => line({ class: "sx-fp-jamb", x1: c.X(xx), y1: r24(y - tpx / 2), x2: c.X(xx), y2: r24(y + tpx / 2) })).join("");
38183
+ }
38184
+ function swingLeaf(o, c, hingeAtLo, widthM) {
38185
+ const dir = o.inward;
38186
+ const rpx = c.px(widthM);
38187
+ if (o.vertical) {
38188
+ const x = c.X(o.along);
38189
+ const hy = hingeAtLo ? c.Y(o.lo) : c.Y(o.hi);
38190
+ const sy = hingeAtLo ? 1 : -1;
38191
+ const leafX = r24(x + dir * rpx);
38192
+ const sweep2 = dir === 1 === hingeAtLo ? 1 : 0;
38193
+ return line({ class: "sx-fp-door-leaf", x1: x, y1: hy, x2: leafX, y2: hy }) + path({ class: "sx-fp-door-arc", d: `M ${leafX} ${hy} A ${rpx} ${rpx} 0 0 ${sweep2} ${x} ${r24(hy + sy * rpx)}` });
38194
+ }
38195
+ const y = c.Y(o.along);
38196
+ const hx = hingeAtLo ? c.X(o.lo) : c.X(o.hi);
38197
+ const sx = hingeAtLo ? 1 : -1;
38198
+ const leafY = r24(y + dir * rpx);
38199
+ const sweep = dir === 1 !== hingeAtLo ? 1 : 0;
38200
+ return line({ class: "sx-fp-door-leaf", x1: hx, y1: y, x2: hx, y2: leafY }) + path({ class: "sx-fp-door-arc", d: `M ${hx} ${leafY} A ${rpx} ${rpx} 0 0 ${sweep} ${r24(hx + sx * rpx)} ${y}` });
38201
+ }
38202
+ function doorSymbol(o, c) {
38203
+ const wd = o.hi - o.lo;
38204
+ if (o.doorType === "double") {
38205
+ const half = { ...o };
38206
+ const mid = (o.lo + o.hi) / 2;
38207
+ const left = { ...half, hi: mid };
38208
+ const right = { ...half, lo: mid };
38209
+ return jambLines(o, c) + swingLeaf(left, c, true, wd / 2) + swingLeaf(right, c, false, wd / 2);
38210
+ }
38211
+ if (o.doorType === "bifold") {
38212
+ const peak = c.px((o.hi - o.lo) * 0.22) * o.inward;
38213
+ const q1 = o.lo + (o.hi - o.lo) * 0.25;
38214
+ const mid = (o.lo + o.hi) / 2;
38215
+ const q3 = o.lo + (o.hi - o.lo) * 0.75;
38216
+ const parts = [jambLines(o, c)];
38217
+ if (o.vertical) {
38218
+ const x = c.X(o.along);
38219
+ parts.push(
38220
+ path({
38221
+ class: "sx-fp-door-leaf",
38222
+ d: `M ${x} ${c.Y(o.lo)} L ${r24(x + peak)} ${c.Y(q1)} L ${x} ${c.Y(mid)} L ${r24(x + peak)} ${c.Y(q3)} L ${x} ${c.Y(o.hi)}`
38223
+ })
38224
+ );
38225
+ } else {
38226
+ const y = c.Y(o.along);
38227
+ parts.push(
38228
+ path({
38229
+ class: "sx-fp-door-leaf",
38230
+ d: `M ${c.X(o.lo)} ${y} L ${c.X(q1)} ${r24(y + peak)} L ${c.X(mid)} ${y} L ${c.X(q3)} ${r24(y + peak)} L ${c.X(o.hi)} ${y}`
38231
+ })
38232
+ );
38233
+ }
38234
+ return parts.join("");
38235
+ }
38236
+ if (o.doorType === "sliding" || o.doorType === "pocket") {
38237
+ const off = c.px(c.wallT) * 0.28;
38238
+ const mid = (o.lo + o.hi) / 2;
38239
+ const parts = [jambLines(o, c)];
38240
+ if (o.vertical) {
38241
+ const x = c.X(o.along);
38242
+ parts.push(line({ class: "sx-fp-door-leaf", x1: r24(x - off), y1: c.Y(o.lo), x2: r24(x - off), y2: c.Y(mid) }));
38243
+ if (o.doorType === "sliding") {
38244
+ parts.push(line({ class: "sx-fp-door-leaf", x1: r24(x + off), y1: c.Y(mid), x2: r24(x + off), y2: c.Y(o.hi) }));
38245
+ }
38246
+ } else {
38247
+ const y = c.Y(o.along);
38248
+ parts.push(line({ class: "sx-fp-door-leaf", x1: c.X(o.lo), y1: r24(y - off), x2: c.X(mid), y2: r24(y - off) }));
38249
+ if (o.doorType === "sliding") {
38250
+ parts.push(line({ class: "sx-fp-door-leaf", x1: c.X(mid), y1: r24(y + off), x2: c.X(o.hi), y2: r24(y + off) }));
38251
+ }
38252
+ }
38253
+ return parts.join("");
38254
+ }
38255
+ return jambLines(o, c) + swingLeaf(o, c, o.hinge !== "right", wd);
38256
+ }
38257
+ function renderDim(d, c) {
38258
+ const cls = d.minor ? "sx-fp-dim-text-minor" : "sx-fp-dim-text";
38259
+ const tick = 3.6;
38260
+ if (!d.vertical) {
38261
+ const y = c.Y(d.at);
38262
+ const x1 = c.X(d.lo);
38263
+ const x2 = c.X(d.hi);
38264
+ return line({ class: "sx-fp-dim", x1, y1: y, x2, y2: y }) + line({ class: "sx-fp-dim", x1: r24(x1 - tick), y1: r24(y + tick), x2: r24(x1 + tick), y2: r24(y - tick) }) + line({ class: "sx-fp-dim", x1: r24(x2 - tick), y1: r24(y + tick), x2: r24(x2 + tick), y2: r24(y - tick) }) + text({ class: cls, x: r24((x1 + x2) / 2), y: r24(y - 4), "text-anchor": "middle" }, d.label);
38265
+ }
38266
+ const x = c.X(d.at);
38267
+ const y1 = c.Y(d.lo);
38268
+ const y2 = c.Y(d.hi);
38269
+ const mid = r24((y1 + y2) / 2);
38270
+ return line({ class: "sx-fp-dim", x1: x, y1, x2: x, y2 }) + line({ class: "sx-fp-dim", x1: r24(x - tick), y1: r24(y1 + tick), x2: r24(x + tick), y2: r24(y1 - tick) }) + line({ class: "sx-fp-dim", x1: r24(x - tick), y1: r24(y2 + tick), x2: r24(x + tick), y2: r24(y2 - tick) }) + text(
38271
+ { class: cls, x: r24(x - 5), y: mid, "text-anchor": "middle", transform: `rotate(-90 ${r24(x - 5)} ${mid})` },
38272
+ d.label
38273
+ );
38274
+ }
38275
+ function renderFloorplanLayout(lay, config) {
38276
+ const t = resolveFloorplanTheme(config?.theme ?? "default");
38277
+ if (lay.errors.length > 0) return renderErrorPanel(lay, t);
38278
+ const scale = FLOORPLAN_CONST.scale;
38279
+ const px = (m) => r24(m * scale);
38280
+ const band = FLOORPLAN_CONST.dimBand + FLOORPLAN_CONST.pad;
38281
+ let outward = 0;
38282
+ for (const o of lay.openings) {
38283
+ if (o.kind !== "window") continue;
38284
+ if (o.windowType === "casement") outward = Math.max(outward, o.hi - o.lo + 0.1);
38285
+ if (o.windowType === "bay") outward = Math.max(outward, 0.6);
38286
+ }
38287
+ const tail = FLOORPLAN_CONST.pad + lay.wallT + outward;
38288
+ const ox = -lay.bounds.minX + band;
38289
+ const oy = -lay.bounds.minY + band;
38290
+ const X = (m) => px(m + ox);
38291
+ const Y = (m) => px(m + oy);
38292
+ const ctx = { X, Y, px, t, wallT: lay.wallT };
38293
+ const titleH = TITLE.bandH;
38294
+ const warnH = lay.warnings.length ? lay.warnings.length * 17 + 10 : 0;
38295
+ const W2 = px(lay.bounds.maxX - lay.bounds.minX + band + tail);
38296
+ const H2 = px(lay.bounds.maxY - lay.bounds.minY + band + tail) + titleH + warnH;
38297
+ const titleX = r24(X((lay.bounds.minX + lay.bounds.maxX) / 2));
38298
+ const floors = [];
38299
+ const furniture = [];
38300
+ const walls = [];
38301
+ const openings = [];
38302
+ const labels = [];
38303
+ const dims = [];
38304
+ for (const r6 of lay.rooms) {
38305
+ floors.push(
38306
+ group(
38307
+ { class: "sx-fp-floor", "data-room": r6.id },
38308
+ r6.parts.map(
38309
+ (p) => rect({
38310
+ fill: r6.fill ?? t.floorFill,
38311
+ x: X(p.x),
38312
+ y: Y(p.y),
38313
+ width: px(p.w),
38314
+ height: px(p.h)
38315
+ })
38316
+ )
38317
+ )
38318
+ );
38319
+ if (!r6.nolabel) {
38320
+ const main = r6.parts.reduce((a, b) => b.w * b.h > a.w * a.h ? b : a);
38321
+ const cx = X(main.x + main.w / 2);
38322
+ const cy = Y(main.y + main.h / 2);
38323
+ labels.push(text({ class: "sx-fp-room-name", x: cx, y: r24(cy - 3), "text-anchor": "middle" }, r6.label));
38324
+ labels.push(text({ class: "sx-fp-room-area", x: cx, y: r24(cy + 13), "text-anchor": "middle" }, r6.areaText));
38325
+ }
38326
+ }
38327
+ const warnSet = new Set(lay.warnItems);
38328
+ lay.items.forEach((it, idx) => {
38329
+ const def = FLOORPLAN_SYMBOLS[it.type];
38330
+ const wpx = px(it.w);
38331
+ const hpx = px(it.h);
38332
+ const cx = r24(X(it.x) + wpx / 2);
38333
+ const cy = r24(Y(it.y) + hpx / 2);
38334
+ const rot = Math.round(it.rotate * 10) / 10;
38335
+ const children = [def.draw({ w: it.w, h: it.h, px, label: it.label })];
38336
+ if (warnSet.has(idx)) {
38337
+ children.push(rect({ class: "sx-fp-warn-item", x: -1, y: -1, width: r24(wpx + 2), height: r24(hpx + 2) }));
38338
+ }
38339
+ furniture.push(
38340
+ group(
38341
+ {
38342
+ class: "sx-fp-item",
38343
+ "data-furniture": it.type,
38344
+ transform: `translate(${cx},${cy})${rot ? ` rotate(${rot})` : ""} translate(${r24(-wpx / 2)},${r24(-hpx / 2)})`
38345
+ },
38346
+ children
38347
+ )
38348
+ );
38349
+ if (it.label && !def.consumesLabel) {
38350
+ labels.push(text({ class: "sx-fp-furn-label", x: cx, y: r24(cy + 4), "text-anchor": "middle" }, it.label));
38351
+ }
38352
+ });
38353
+ const tw = lay.wallT;
38354
+ for (const r6 of lay.rooms) {
38355
+ for (const p of r6.parts) {
38356
+ walls.push(rect({ class: "sx-fp-wall", x: r24(X(p.x) - px(tw / 2)), y: r24(Y(p.y) - px(tw / 2)), width: r24(px(p.w + tw)), height: px(tw) }));
38357
+ walls.push(rect({ class: "sx-fp-wall", x: r24(X(p.x) - px(tw / 2)), y: r24(Y(p.y + p.h) - px(tw / 2)), width: r24(px(p.w + tw)), height: px(tw) }));
38358
+ walls.push(rect({ class: "sx-fp-wall", x: r24(X(p.x) - px(tw / 2)), y: r24(Y(p.y) - px(tw / 2)), width: px(tw), height: r24(px(p.h + tw)) }));
38359
+ walls.push(rect({ class: "sx-fp-wall", x: r24(X(p.x + p.w) - px(tw / 2)), y: r24(Y(p.y) - px(tw / 2)), width: px(tw), height: r24(px(p.h + tw)) }));
38360
+ }
38361
+ }
38362
+ for (const s of lay.seams) {
38363
+ const fill = lay.rooms[s.room]?.fill ?? t.floorFill;
38364
+ const tpx = px(tw);
38365
+ if (s.vertical) {
38366
+ openings.push(
38367
+ rect({ class: "sx-fp-gap", fill, x: r24(X(s.along) - tpx / 2 - 0.5), y: Y(s.lo), width: r24(tpx + 1), height: px(s.hi - s.lo) })
38368
+ );
38369
+ } else {
38370
+ openings.push(
38371
+ rect({ class: "sx-fp-gap", fill, x: X(s.lo), y: r24(Y(s.along) - tpx / 2 - 0.5), width: px(s.hi - s.lo), height: r24(tpx + 1) })
38372
+ );
38373
+ }
38374
+ }
38375
+ for (const o of lay.openings) {
38376
+ openings.push(punchGap(o, lay, ctx));
38377
+ if (o.kind === "window") openings.push(windowSymbol(o, ctx));
38378
+ else if (o.kind === "door") openings.push(doorSymbol(o, ctx));
38379
+ else openings.push(jambLines(o, ctx));
38380
+ }
38381
+ for (const d of lay.dims) dims.push(renderDim(d, ctx));
38382
+ if (lay.north !== void 0) {
38383
+ const ncx = r24(X(lay.bounds.maxX) + px(0.55));
38384
+ const ncy = r24(Y(lay.bounds.minY) - px(0.62));
38385
+ const rr = px(0.32);
38386
+ const rot = Math.round(lay.north * 10) / 10;
38387
+ dims.push(
38388
+ group({ class: "sx-fp-compass-g", transform: rot ? `rotate(${rot} ${ncx} ${ncy})` : "" }, [
38389
+ el("circle", { class: "sx-fp-compass", cx: ncx, cy: ncy, r: rr }),
38390
+ line({ class: "sx-fp-compass", x1: ncx, y1: r24(ncy + rr * 0.7), x2: ncx, y2: r24(ncy - rr * 0.55) }),
38391
+ el("polygon", {
38392
+ class: "sx-fp-compass",
38393
+ fill: t.dimText,
38394
+ points: `${ncx},${r24(ncy - rr * 0.85)} ${r24(ncx - rr * 0.22)},${r24(ncy - rr * 0.3)} ${r24(ncx + rr * 0.22)},${r24(ncy - rr * 0.3)}`
38395
+ }),
38396
+ text({ class: "sx-fp-compass-n", x: r24(ncx + rr + 4), y: r24(ncy - rr * 0.45), "text-anchor": "start" }, "N")
38397
+ ])
38398
+ );
38399
+ }
38400
+ const warnBlock = [];
38401
+ if (lay.warnings.length) {
38402
+ const y0 = H2 - warnH + 4;
38403
+ lay.warnings.forEach((w, i) => {
38404
+ warnBlock.push(text({ class: "sx-fp-warn", x: 10, y: r24(y0 + i * 17 + 10) }, `\u26A0 ${w}`));
38405
+ });
38406
+ }
38407
+ const nRooms = lay.rooms.length;
38408
+ const descText = `${nRooms} room${nRooms === 1 ? "" : "s"}, ${formatArea(lay.totalAreaM2, lay.unit)} total. ${lay.items.length} furniture item${lay.items.length === 1 ? "" : "s"}.` + (lay.warnings.length ? ` Warnings: ${lay.warnings.join("; ")}.` : "");
38409
+ return svgRoot(
38410
+ { viewBox: `0 0 ${W2} ${H2}`, width: W2, height: H2, class: "sx-fp", role: "img" },
38411
+ [
38412
+ title(lay.title),
38413
+ desc(descText),
38414
+ el("style", {}, buildCss13(t)),
38415
+ rect({ fill: t.bg, x: 0, y: 0, width: W2, height: H2 }),
38416
+ text({ class: "sx-fp-title", x: titleX, y: TITLE.y, "text-anchor": "middle" }, lay.title),
38417
+ group({ transform: `translate(0,${titleH})` }, [
38418
+ group({ class: "sx-fp-floors" }, floors),
38419
+ group({ class: "sx-fp-furniture" }, furniture),
38420
+ group({ class: "sx-fp-walls" }, walls),
38421
+ group({ class: "sx-fp-openings" }, openings),
38422
+ group({ class: "sx-fp-labels" }, labels),
38423
+ group({ class: "sx-fp-dims" }, dims)
38424
+ ]),
38425
+ ...warnBlock
38426
+ ]
38427
+ );
38428
+ }
38429
+ function renderFloorplan(text2, config) {
38430
+ return renderFloorplanLayout(layoutFloorplan(parseFloorplan(text2)), config);
38431
+ }
38432
+
38433
+ // src/diagrams/floorplan/index.ts
38434
+ var floorplan = {
38435
+ type: "floorplan",
38436
+ detect(text2) {
38437
+ for (const raw of text2.split(/\r?\n/)) {
38438
+ const t = raw.trim();
38439
+ if (!t) continue;
38440
+ if (t.startsWith("#") || t.startsWith("//")) continue;
38441
+ return /^floorplan\b/i.test(t);
38442
+ }
38443
+ return false;
38444
+ },
38445
+ parse: parseFloorplan,
38446
+ render(text2, config) {
38447
+ return renderFloorplan(text2, config);
38448
+ },
38449
+ lint(text2) {
38450
+ try {
38451
+ const lay = layoutFloorplan(parseFloorplan(text2));
38452
+ return [
38453
+ ...lay.errors.map(
38454
+ (message) => ({
38455
+ severity: "error",
38456
+ code: "floorplan/validation",
38457
+ message,
38458
+ fatal: false
38459
+ })
38460
+ ),
38461
+ ...lay.warnings.map(
38462
+ (message) => ({
38463
+ severity: "warning",
38464
+ code: "floorplan/warning",
38465
+ message,
38466
+ fatal: false
38467
+ })
38468
+ )
38469
+ ];
38470
+ } catch {
38471
+ return [];
38472
+ }
38473
+ }
38474
+ };
38475
+
38476
+ // src/diagrams/playbook/parser.ts
38477
+ var PlaybookParseError = class extends Error {
38478
+ line;
38479
+ constructor(message, line2) {
38480
+ super(`line ${line2}: ${message}`);
38481
+ this.name = "PlaybookParseError";
38482
+ this.line = line2;
38483
+ }
38484
+ };
38485
+ var isStr2 = (t) => t !== void 0 && "str" in t;
38486
+ var isWord2 = (t, w) => t !== void 0 && "word" in t && (w === void 0 || t.word.toLowerCase() === w);
38487
+ var tokDisplay = (t) => "word" in t ? t.word : `"${t.str}"`;
38488
+ var normalizeQuotes4 = (s) => s.replace(/[“”「」『』]/g, '"').replace(/[‘’]/g, "'");
38489
+ function tokenize8(line2) {
38490
+ const out = [];
38491
+ const re = /"([^"]*)"|(\S+)/g;
38492
+ let m;
38493
+ while (m = re.exec(line2)) {
38494
+ if (m[1] !== void 0) out.push({ str: m[1] });
38495
+ else out.push({ word: m[2] });
38496
+ }
38497
+ return out;
38498
+ }
38499
+ function parseNum2(t, what, ln) {
38500
+ if (!isWord2(t)) throw new PlaybookParseError(`expected a number for ${what}`, ln);
38501
+ const v = Number(t.word);
38502
+ if (!Number.isFinite(v)) throw new PlaybookParseError(`expected a number for ${what}, got "${t.word}"`, ln);
38503
+ return v;
38504
+ }
38505
+ function parseId2(t, what, ln) {
38506
+ if (!isWord2(t)) throw new PlaybookParseError(`expected ${what}`, ln);
38507
+ return t.word;
38508
+ }
38509
+ var COORD = /^([+-]?\d*\.?\d+),([+-]?\d*\.?\d+)$/;
38510
+ var POSITIONS = ["o", "c", "ol", "qb", "rb", "wr", "te", "x", "dl", "lb", "db", "s", "gk"];
38511
+ var NAMED_ROUTES = [
38512
+ "go",
38513
+ "fly",
38514
+ "streak",
38515
+ "vertical",
38516
+ "slant",
38517
+ "flat",
38518
+ "hitch",
38519
+ "out",
38520
+ "in",
38521
+ "dig",
38522
+ "curl",
38523
+ "comeback",
38524
+ "corner",
38525
+ "flag",
38526
+ "post",
38527
+ "wheel",
38528
+ "cross",
38529
+ "drag",
38530
+ "seam",
38531
+ "screen",
38532
+ "dive",
38533
+ "iso",
38534
+ "power",
38535
+ "counter",
38536
+ "sweep",
38537
+ "toss",
38538
+ "draw",
38539
+ "trap"
38540
+ ];
38541
+ var RUN_CONCEPTS = /* @__PURE__ */ new Set(["dive", "iso", "power", "counter", "sweep", "toss", "draw", "trap"]);
38542
+ var FORMATIONS = [
38543
+ "i-form",
38544
+ "shotgun",
38545
+ "singleback",
38546
+ "pistol",
38547
+ "trips",
38548
+ "spread",
38549
+ "trips-right",
38550
+ "trips-left",
38551
+ "empty",
38552
+ "goal-line",
38553
+ "wishbone",
38554
+ "4-4-2",
38555
+ "4-3-3",
38556
+ "4-2-3-1",
38557
+ "3-5-2",
38558
+ "4-4-1-1",
38559
+ "4-5-1",
38560
+ "3-4-3",
38561
+ "horns",
38562
+ "1-4-high",
38563
+ "1-4-low",
38564
+ "box",
38565
+ "spread-pnr",
38566
+ "5-out",
38567
+ "4-out"
38568
+ ];
38569
+ var DEFENSES = [
38570
+ "4-3",
38571
+ "3-4",
38572
+ "4-4",
38573
+ "nickel",
38574
+ "dime",
38575
+ "cover-0",
38576
+ "cover-1",
38577
+ "cover-2",
38578
+ "cover-3",
38579
+ "cover-4",
38580
+ "cover-6",
38581
+ "man",
38582
+ "zone-2-3",
38583
+ "zone-3-2",
38584
+ "zone-1-3-1",
38585
+ "low-block",
38586
+ "mid-block",
38587
+ "high-press"
38588
+ ];
38589
+ var DIRS = ["left", "right"];
38590
+ function parseHeader4(tok, ast, ln) {
38591
+ while (tok.length) {
38592
+ const t = tok.shift();
38593
+ if (isStr2(t)) ast.title = t.str;
38594
+ else if (isWord2(t, "sport")) {
38595
+ const s = parseId2(tok.shift(), "sport", ln).toLowerCase();
38596
+ if (s !== "football" && s !== "basketball" && s !== "soccer") {
38597
+ throw new PlaybookParseError(`sport must be football|basketball|soccer, got "${s}"`, ln);
38598
+ }
38599
+ ast.sport = s;
38600
+ } else throw new PlaybookParseError(`playbook: unexpected token "${tokDisplay(t)}"`, ln);
38601
+ }
38602
+ }
38603
+ function parseField(tok, ast, ln) {
38604
+ while (tok.length) {
38605
+ const t = tok.shift();
38606
+ if (isWord2(t, "down")) ast.down = parseNum2(tok.shift(), "down", ln);
38607
+ else if (isWord2(t, "distance") || isWord2(t, "togo")) ast.distance = parseNum2(tok.shift(), "distance", ln);
38608
+ else if (isWord2(t, "los") || isWord2(t, "ball")) ast.losYard = parseNum2(tok.shift(), "los", ln);
38609
+ else if (isWord2(t, "goal") || isWord2(t, "togoal")) ast.toGoal = parseNum2(tok.shift(), "goal", ln);
38610
+ else if (isWord2(t, "view")) {
38611
+ const v = parseId2(tok.shift(), "view (full|half)", ln).toLowerCase();
38612
+ if (v !== "full" && v !== "half" && v !== "auto") throw new PlaybookParseError(`view must be full|half`, ln);
38613
+ ast.view = v;
38614
+ } else if (isWord2(t, "hash")) {
38615
+ const h = parseId2(tok.shift(), "hash (nfl|college|none)", ln).toLowerCase();
38616
+ if (h !== "nfl" && h !== "college" && h !== "none") throw new PlaybookParseError(`hash must be nfl|college|none`, ln);
38617
+ ast.hash = h;
38618
+ } else throw new PlaybookParseError(`field: unexpected token "${tokDisplay(t)}"`, ln);
38619
+ }
38620
+ }
38621
+ function parseFormation(tok, ast, ln) {
38622
+ let name = parseId2(tok.shift(), "a formation", ln).toLowerCase();
38623
+ let side;
38624
+ const hy = /^(.*)-(left|right)$/.exec(name);
38625
+ if (hy && FORMATIONS.includes(hy[1]) && hy[1] !== "trips") {
38626
+ name = hy[1];
38627
+ side = hy[2];
38628
+ }
38629
+ if (!FORMATIONS.includes(name)) {
38630
+ throw new PlaybookParseError(`unknown formation "${name}"`, ln);
38631
+ }
38632
+ ast.formation = name;
38633
+ while (tok.length) {
38634
+ const t = tok.shift();
38635
+ if (isWord2(t, "left") || isWord2(t, "right")) side = t.word.toLowerCase();
38636
+ else throw new PlaybookParseError(`formation: unexpected token "${tokDisplay(t)}"`, ln);
38637
+ }
38638
+ if (name === "trips-right") ast.formationSide = "right";
38639
+ else if (name === "trips-left") ast.formationSide = "left";
38640
+ else if (side) ast.formationSide = side;
38641
+ }
38642
+ function parseDefense(tok, ast, ln) {
38643
+ const d = parseId2(tok.shift(), "a defensive scheme", ln).toLowerCase();
38644
+ if (!DEFENSES.includes(d)) throw new PlaybookParseError(`unknown defense "${d}"`, ln);
38645
+ ast.defense = d;
38646
+ }
38647
+ function parsePlayer(tok, ast, ln) {
38648
+ const id = parseId2(tok.shift(), "a player id", ln);
38649
+ const posRaw = parseId2(tok.shift(), "a position", ln).toLowerCase();
38650
+ if (!POSITIONS.includes(posRaw)) throw new PlaybookParseError(`unknown position "${posRaw}"`, ln);
38651
+ const isDef = posRaw === "x" || posRaw === "dl" || posRaw === "lb" || posRaw === "db" || posRaw === "s";
38652
+ const p = { id, side: isDef ? "defense" : "offense", pos: posRaw, label: id, line: ln };
38653
+ while (tok.length) {
38654
+ const t = tok.shift();
38655
+ if (isStr2(t)) p.label = t.str;
38656
+ else if (isWord2(t, "label")) {
38657
+ const lt = tok.shift();
38658
+ p.label = isStr2(lt) ? lt.str : parseId2(lt, "a label", ln);
38659
+ } else if (isWord2(t, "at")) {
38660
+ const c = COORD.exec(parseId2(tok.shift(), "x,y", ln));
38661
+ if (!c) throw new PlaybookParseError(`expected "x,y" after at`, ln);
38662
+ p.at = { x: Number(c[1]), y: Number(c[2]) };
38663
+ } else if (isWord2(t, "side")) {
38664
+ const sv = parseId2(tok.shift(), "side", ln).toLowerCase();
38665
+ if (sv !== "offense" && sv !== "defense") throw new PlaybookParseError(`side must be offense|defense`, ln);
38666
+ p.side = sv;
38667
+ } else throw new PlaybookParseError(`player: unexpected token "${tokDisplay(t)}"`, ln);
38668
+ }
38669
+ ast.players.push(p);
38670
+ }
38671
+ function splitNameDir(token) {
38672
+ const m = /^(.*)-(left|right)$/.exec(token.toLowerCase());
38673
+ if (m) return { name: m[1], dir: m[2] };
38674
+ return { name: token.toLowerCase() };
38675
+ }
38676
+ function parseDests(tok) {
38677
+ const pts = [];
38678
+ while (tok.length) {
38679
+ const t = tok.shift();
38680
+ if (isWord2(t, "to") || isWord2(t, "then") || isWord2(t, "->")) continue;
38681
+ if (!isWord2(t)) continue;
38682
+ const c = COORD.exec(t.word);
38683
+ if (c) {
38684
+ const rel = /^[+]/.test(c[1]) || /^[+]/.test(c[2]);
38685
+ pts.push({ x: Number(c[1]), y: Number(c[2]), rel });
38686
+ } else {
38687
+ pts.push({ ref: t.word });
38688
+ }
38689
+ }
38690
+ return pts;
38691
+ }
38692
+ function parseRouteRun(kind, tok, ast, ln) {
38693
+ const player = parseId2(tok.shift(), "a player id", ln);
38694
+ const m = { player, kind, line: ln };
38695
+ const first = tok[0];
38696
+ if (isWord2(first, "to") || isWord2(first, "path")) {
38697
+ tok.shift();
38698
+ m.points = parseDests(tok);
38699
+ ast.moves.push(m);
38700
+ return;
38701
+ }
38702
+ if (isWord2(first)) {
38703
+ const { name, dir } = splitNameDir(first.word);
38704
+ if (NAMED_ROUTES.includes(name)) {
38705
+ tok.shift();
38706
+ m.named = name;
38707
+ if (dir) m.dir = dir;
38708
+ if (kind === "route" && RUN_CONCEPTS.has(name)) m.kind = "run";
38709
+ while (tok.length) {
38710
+ const t = tok.shift();
38711
+ if (isWord2(t) && DIRS.includes(t.word.toLowerCase())) m.dir = t.word.toLowerCase();
38712
+ else if (isWord2(t) && Number.isFinite(Number(t.word))) m.depth = Number(t.word);
38713
+ else if (isWord2(t, "depth")) m.depth = parseNum2(tok.shift(), "depth", ln);
38714
+ else throw new PlaybookParseError(`${kind}: unexpected token "${tokDisplay(t)}"`, ln);
38715
+ }
38716
+ ast.moves.push(m);
38717
+ return;
38718
+ }
38719
+ }
38720
+ m.points = parseDests(tok);
38721
+ if (m.points.length === 0) throw new PlaybookParseError(`${kind} ${player}: needs a route name or destination`, ln);
38722
+ ast.moves.push(m);
38723
+ }
38724
+ function parsePoly(kind, tok, ast, ln) {
38725
+ const player = parseId2(tok.shift(), "a player id", ln);
38726
+ const points = parseDests(tok);
38727
+ if (points.length === 0) throw new PlaybookParseError(`${kind} ${player}: needs a destination (x,y or a landmark)`, ln);
38728
+ ast.moves.push({ player, kind, points, line: ln });
38729
+ }
38730
+ function parseTargeted(kind, tok, ast, ln, optionalTarget = false) {
38731
+ const player = parseId2(tok.shift(), "a player id", ln);
38732
+ const nxt = tok[0];
38733
+ if (isWord2(nxt, "to")) tok.shift();
38734
+ const tgt = tok.shift();
38735
+ if (!tgt) {
38736
+ if (optionalTarget) {
38737
+ ast.moves.push({ player, kind, line: ln });
38738
+ return;
38739
+ }
38740
+ throw new PlaybookParseError(`${kind} ${player}: needs a target (player id, landmark, or x,y)`, ln);
38741
+ }
38742
+ const target = isWord2(tgt) ? tgt.word : tgt.str;
38743
+ ast.moves.push({ player, kind, target, line: ln });
38744
+ }
38745
+ function parsePull(tok, ast, ln) {
38746
+ const player = parseId2(tok.shift(), "a lineman id", ln);
38747
+ const dt = tok.shift();
38748
+ const { dir } = splitNameDir(isWord2(dt) ? dt.word : "");
38749
+ let d = dir;
38750
+ if (!d && isWord2(dt) && DIRS.includes(dt.word.toLowerCase())) d = dt.word.toLowerCase();
38751
+ if (!d) throw new PlaybookParseError(`pull: expected left|right`, ln);
38752
+ ast.moves.push({ player, kind: "pull", named: "power", dir: d, line: ln });
38753
+ }
38754
+ function parseMotion(tok, ast, ln) {
38755
+ const player = parseId2(tok.shift(), "a player id", ln);
38756
+ const m = { player, kind: "motion", line: ln };
38757
+ const dt = tok.shift();
38758
+ if (isWord2(dt) && DIRS.includes(dt.word.toLowerCase())) {
38759
+ m.dir = dt.word.toLowerCase();
38760
+ const n = tok.shift();
38761
+ if (isWord2(n) && Number.isFinite(Number(n.word))) m.depth = Number(n.word);
38762
+ } else if (isWord2(dt, "to")) {
38763
+ m.points = parseDests(tok);
38764
+ } else if (isWord2(dt)) {
38765
+ const c = COORD.exec(dt.word);
38766
+ if (c) m.points = [{ x: Number(c[1]), y: Number(c[2]), rel: /^[+]/.test(c[1]) }];
38767
+ else m.points = [{ ref: dt.word }];
38768
+ } else throw new PlaybookParseError(`motion: expected left|right or a destination`, ln);
38769
+ ast.moves.push(m);
38770
+ }
38771
+ function parseZone(tok, ast, ln) {
38772
+ const z = { x: 0, y: 0, rx: 6, ry: 5, line: ln };
38773
+ while (tok.length) {
38774
+ const t = tok.shift();
38775
+ if (isStr2(t)) z.label = t.str;
38776
+ else if (isWord2(t, "at")) {
38777
+ const c = COORD.exec(parseId2(tok.shift(), "x,y", ln));
38778
+ if (!c) throw new PlaybookParseError(`zone: expected "x,y" after at`, ln);
38779
+ z.x = Number(c[1]);
38780
+ z.y = Number(c[2]);
38781
+ } else if (isWord2(t, "size")) {
38782
+ const st = tok.shift();
38783
+ const m = /^(\d*\.?\d+)x(\d*\.?\d+)$/i.exec(isWord2(st) ? st.word : "");
38784
+ if (!m) throw new PlaybookParseError(`zone size expects "rxXry"`, ln);
38785
+ z.rx = Number(m[1]);
38786
+ z.ry = Number(m[2]);
38787
+ } else if (isWord2(t, "label")) {
38788
+ const lt = tok.shift();
38789
+ z.label = isStr2(lt) ? lt.str : parseId2(lt, "a label", ln);
38790
+ } else throw new PlaybookParseError(`zone: unexpected token "${tokDisplay(t)}"`, ln);
38791
+ }
38792
+ ast.zones.push(z);
38793
+ }
38794
+ function parseHandoff(tok, ast, ln) {
38795
+ const from = parseId2(tok.shift(), "the source id", ln);
38796
+ const to = parseId2(tok.shift(), "the target id", ln);
38797
+ ast.moves.push({ player: from, kind: "handoff", target: to, line: ln });
38798
+ }
38799
+ function parsePlaybook(text2) {
38800
+ const ast = {
38801
+ type: "playbook",
38802
+ title: "Football Play",
38803
+ sport: "football",
38804
+ down: 0,
38805
+ distance: 0,
38806
+ hash: "nfl",
38807
+ view: "auto",
38808
+ formationSide: "right",
38809
+ players: [],
38810
+ moves: [],
38811
+ zones: []
38812
+ };
38813
+ let sawHeader = false;
38814
+ let titleSetByUser = false;
38815
+ const lines = text2.split(/\r?\n/);
38816
+ for (let i = 0; i < lines.length; i++) {
38817
+ const ln = i + 1;
38818
+ const raw = normalizeQuotes4(lines[i]).trim();
38819
+ if (!raw) continue;
38820
+ const all = tokenize8(raw);
38821
+ const tok = [];
38822
+ for (const t of all) {
38823
+ if (isWord2(t) && (t.word.startsWith("#") || t.word.startsWith("//"))) break;
38824
+ tok.push(t);
38825
+ }
38826
+ if (tok.length === 0) continue;
38827
+ const head = tok.shift();
38828
+ if (!isWord2(head)) throw new PlaybookParseError(`unexpected string at line start`, ln);
38829
+ const kw = head.word.toLowerCase();
38830
+ if (kw === "playbook") {
38831
+ const before = ast.title;
38832
+ parseHeader4(tok, ast, ln);
38833
+ if (ast.title !== before) titleSetByUser = true;
38834
+ sawHeader = true;
38835
+ continue;
38836
+ }
38837
+ if (!sawHeader) throw new PlaybookParseError(`the first statement must be the "playbook" header`, ln);
38838
+ if (kw === "field") parseField(tok, ast, ln);
38839
+ else if (kw === "view") {
38840
+ const v = parseId2(tok.shift(), "view (full|half)", ln).toLowerCase();
38841
+ if (v !== "full" && v !== "half" && v !== "auto") throw new PlaybookParseError(`view must be full|half`, ln);
38842
+ ast.view = v;
38843
+ } else if (kw === "formation" || kw === "set") parseFormation(tok, ast, ln);
38844
+ else if (kw === "defense" || kw === "def") parseDefense(tok, ast, ln);
38845
+ else if (kw === "player") parsePlayer(tok, ast, ln);
38846
+ else if (kw === "route") parseRouteRun("route", tok, ast, ln);
38847
+ else if (kw === "run") parseRouteRun("run", tok, ast, ln);
38848
+ else if (kw === "cut") parsePoly("cut", tok, ast, ln);
38849
+ else if (kw === "move") parsePoly("move", tok, ast, ln);
38850
+ else if (kw === "dribble" || kw === "drive") parsePoly("dribble", tok, ast, ln);
38851
+ else if (kw === "pass") parseTargeted("pass", tok, ast, ln);
38852
+ else if (kw === "screen") parseTargeted("screen", tok, ast, ln);
38853
+ else if (kw === "block") parseTargeted("block", tok, ast, ln);
38854
+ else if (kw === "shot" || kw === "shoot") parseTargeted("shot", tok, ast, ln, true);
38855
+ else if (kw === "pull") parsePull(tok, ast, ln);
38856
+ else if (kw === "handoff") parseHandoff(tok, ast, ln);
38857
+ else if (kw === "motion") parseMotion(tok, ast, ln);
38858
+ else if (kw === "zone") parseZone(tok, ast, ln);
38859
+ else throw new PlaybookParseError(`unknown keyword "${kw}"`, ln);
38860
+ }
38861
+ if (!titleSetByUser) {
38862
+ ast.title = ast.sport === "basketball" ? "Basketball Play" : ast.sport === "soccer" ? "Soccer Tactic" : "Football Play";
38863
+ }
38864
+ return ast;
38865
+ }
38866
+
38867
+ // src/diagrams/playbook/geometry/football.ts
38868
+ var K = {
38869
+ scale: 14,
38870
+ olSplit: 1.4,
38871
+ wrSplit: 13,
38872
+ slotSplit: 6,
38873
+ minHalfWidth: 20,
38874
+ margin: 2,
38875
+ hashNfl: 3.08,
38876
+ hashCollege: 6.67
38877
+ };
38878
+ var r25 = (n) => Math.round(n * 100) / 100;
38879
+ var round22 = (p) => ({ x: Math.round(p.x * 100) / 100, y: Math.round(p.y * 100) / 100 });
38880
+ var dirSign = (d) => d === "right" ? 1 : -1;
38881
+ function offensiveLine() {
38882
+ return [
38883
+ { id: "LT", pos: "ol", label: "T", x: -2 * K.olSplit, y: 0 },
38884
+ { id: "LG", pos: "ol", label: "G", x: -1 * K.olSplit, y: 0 },
38885
+ { id: "C", pos: "c", label: "C", x: 0, y: 0 },
38886
+ { id: "RG", pos: "ol", label: "G", x: 1 * K.olSplit, y: 0 },
38887
+ { id: "RT", pos: "ol", label: "T", x: 2 * K.olSplit, y: 0 }
38888
+ ];
38889
+ }
38890
+ function tightEnd(side) {
38891
+ const s = side === "right" ? 1 : -1;
38892
+ return { id: "Y", pos: "te", label: "Y", x: s * (2 * K.olSplit + 1.3), y: 0 };
38893
+ }
38894
+ function formationRoster(formation, side) {
38895
+ const s = side === "right" ? 1 : -1;
38896
+ const ol = offensiveLine();
38897
+ const X = { id: "X", pos: "wr", label: "X", x: -s * K.wrSplit, y: 0 };
38898
+ const Z = { id: "Z", pos: "wr", label: "Z", x: s * K.wrSplit, y: -1 };
38899
+ switch (formation) {
38900
+ case "i-form":
38901
+ return [
38902
+ ...ol,
38903
+ tightEnd(side),
38904
+ { id: "QB", pos: "qb", label: "QB", x: 0, y: -1.5 },
38905
+ { id: "FB", pos: "rb", label: "F", x: 0, y: -3.6 },
38906
+ { id: "RB", pos: "rb", label: "RB", x: 0, y: -6.2 },
38907
+ X,
38908
+ Z
38909
+ ];
38910
+ case "wishbone":
38911
+ return [
38912
+ ...ol,
38913
+ tightEnd(side),
38914
+ { id: "QB", pos: "qb", label: "QB", x: 0, y: -1.5 },
38915
+ { id: "FB", pos: "rb", label: "F", x: 0, y: -3.4 },
38916
+ { id: "LH", pos: "rb", label: "H", x: -3.5, y: -5.2 },
38917
+ { id: "RB", pos: "rb", label: "H", x: 3.5, y: -5.2 },
38918
+ X
38919
+ ];
38920
+ case "singleback":
38921
+ return [
38922
+ ...ol,
38923
+ tightEnd(side),
38924
+ { id: "QB", pos: "qb", label: "QB", x: 0, y: -1.5 },
38925
+ { id: "RB", pos: "rb", label: "RB", x: 0, y: -5 },
38926
+ X,
38927
+ { id: "H", pos: "wr", label: "H", x: s * K.slotSplit, y: -1 },
38928
+ Z
38929
+ ];
38930
+ case "pistol":
38931
+ return [
38932
+ ...ol,
38933
+ tightEnd(side),
38934
+ { id: "QB", pos: "qb", label: "QB", x: 0, y: -4 },
38935
+ { id: "RB", pos: "rb", label: "RB", x: 0, y: -7 },
38936
+ X,
38937
+ Z
38938
+ ];
38939
+ case "shotgun":
38940
+ return [
38941
+ ...ol,
38942
+ tightEnd(side),
38943
+ { id: "QB", pos: "qb", label: "QB", x: 0, y: -5 },
38944
+ { id: "RB", pos: "rb", label: "RB", x: s * 1.8, y: -5 },
38945
+ X,
38946
+ Z
38947
+ ];
38948
+ case "spread":
38949
+ return [
38950
+ ...ol,
38951
+ { id: "QB", pos: "qb", label: "QB", x: 0, y: -5 },
38952
+ { id: "RB", pos: "rb", label: "RB", x: s * 1.8, y: -5 },
38953
+ { id: "X", pos: "wr", label: "X", x: -13 - 1, y: 0 },
38954
+ { id: "H", pos: "wr", label: "H", x: -6, y: -1 },
38955
+ { id: "Y", pos: "wr", label: "Y", x: K.slotSplit, y: -1 },
38956
+ { id: "Z", pos: "wr", label: "Z", x: K.wrSplit + 1, y: 0 }
38957
+ ];
38958
+ case "trips":
38959
+ case "trips-right":
38960
+ case "trips-left": {
38961
+ const t = formation === "trips-left" ? -1 : formation === "trips-right" ? 1 : s;
38962
+ return [
38963
+ ...ol,
38964
+ { id: "QB", pos: "qb", label: "QB", x: 0, y: -5 },
38965
+ { id: "RB", pos: "rb", label: "RB", x: -t * 1.8, y: -5 },
38966
+ { id: "X", pos: "wr", label: "X", x: -t * (K.wrSplit + 1), y: 0 },
38967
+ { id: "Z", pos: "wr", label: "Z", x: t * (K.wrSplit + 1), y: 0 },
38968
+ { id: "H", pos: "wr", label: "H", x: t * 9, y: -1 },
38969
+ { id: "Y", pos: "wr", label: "Y", x: t * 5, y: -1 }
38970
+ ];
38971
+ }
38972
+ case "empty":
38973
+ return [
38974
+ ...ol,
38975
+ { id: "QB", pos: "qb", label: "QB", x: 0, y: -5 },
38976
+ { id: "X", pos: "wr", label: "X", x: -13 - 1, y: 0 },
38977
+ { id: "H", pos: "wr", label: "H", x: -6 - 1, y: -1 },
38978
+ { id: "Z", pos: "wr", label: "Z", x: K.wrSplit + 1, y: 0 },
38979
+ { id: "Y", pos: "wr", label: "Y", x: K.slotSplit + 1, y: -1 },
38980
+ { id: "H2", pos: "wr", label: "h", x: K.slotSplit - 2, y: -1 }
38981
+ ];
38982
+ case "goal-line":
38983
+ return [
38984
+ ...ol,
38985
+ { id: "Y", pos: "te", label: "Y", x: 2 * K.olSplit + 1.3, y: 0 },
38986
+ { id: "TE2", pos: "te", label: "Y", x: -4.1, y: 0 },
38987
+ { id: "QB", pos: "qb", label: "QB", x: 0, y: -1.5 },
38988
+ { id: "FB", pos: "rb", label: "F", x: 0, y: -3.4 },
38989
+ { id: "RB", pos: "rb", label: "RB", x: 0, y: -5.4 },
38990
+ { id: "Z", pos: "wr", label: "Z", x: s * 9, y: 0 }
38991
+ ];
38992
+ default:
38993
+ return [...ol, { id: "QB", pos: "qb", label: "QB", x: 0, y: -1.5 }];
38994
+ }
38995
+ }
38996
+ function front43(side) {
38997
+ const s = side === "right" ? 1 : -1;
38998
+ return [
38999
+ { id: "DE_W", pos: "dl", label: "E", x: -3.6, y: 1 },
39000
+ { id: "DT_W", pos: "dl", label: "T", x: -1.2, y: 1 },
39001
+ { id: "DT_S", pos: "dl", label: "T", x: 1.2, y: 1 },
39002
+ { id: "DE_S", pos: "dl", label: "E", x: 3.6, y: 1 },
39003
+ { id: "WLB", pos: "lb", label: "W", x: -4.5, y: 4.3 },
39004
+ { id: "MLB", pos: "lb", label: "M", x: 0, y: 4.8 },
39005
+ { id: "SLB", pos: "lb", label: "S", x: s * 4.5, y: 4.3 }
39006
+ ];
39007
+ }
39008
+ function front34(side) {
39009
+ const s = side === "right" ? 1 : -1;
39010
+ return [
39011
+ { id: "DE_W", pos: "dl", label: "E", x: -2.6, y: 1 },
39012
+ { id: "NT", pos: "dl", label: "N", x: 0, y: 1 },
39013
+ { id: "DE_S", pos: "dl", label: "E", x: 2.6, y: 1 },
39014
+ { id: "WOLB", pos: "lb", label: "W", x: -5.4, y: 3.6 },
39015
+ { id: "WILB", pos: "lb", label: "M", x: -1.6, y: 4.6 },
39016
+ { id: "SILB", pos: "lb", label: "M", x: 1.6, y: 4.6 },
39017
+ { id: "SOLB", pos: "lb", label: "S", x: s * 5.4, y: 3.6 }
39018
+ ];
39019
+ }
39020
+ function defensePreset(scheme, side) {
39021
+ const s = side === "right" ? 1 : -1;
39022
+ const cb = (sign, y, label = "C") => ({ id: sign < 0 ? "LCB" : "RCB", pos: "db", label, x: sign * K.wrSplit, y });
39023
+ let front;
39024
+ let cover;
39025
+ let showZones = true;
39026
+ if (scheme === "3-4") {
39027
+ front = front34(side);
39028
+ cover = "cover-2";
39029
+ showZones = false;
39030
+ } else if (scheme === "4-4") {
39031
+ front = [...front43(side), { id: "SS", pos: "s", label: "$", x: s * 6, y: 4.6 }];
39032
+ cover = "cover-3";
39033
+ } else if (scheme === "nickel") {
39034
+ front = front43(side).slice(0, 6);
39035
+ cover = "cover-2";
39036
+ } else if (scheme === "dime") {
39037
+ front = front43(side).slice(0, 5);
39038
+ cover = "cover-3";
39039
+ } else if (scheme.startsWith("cover")) {
39040
+ front = front43(side);
39041
+ cover = scheme;
39042
+ } else {
39043
+ front = front43(side);
39044
+ cover = "cover-2";
39045
+ showZones = false;
39046
+ }
39047
+ const sec = [];
39048
+ const zones = [];
39049
+ switch (cover) {
39050
+ case "cover-0":
39051
+ sec.push(cb(-1, 5.5), cb(1, 5.5), { id: "FS", pos: "s", label: "F", x: -6, y: 5 }, { id: "SS", pos: "s", label: "$", x: s * 6, y: 5 });
39052
+ break;
39053
+ case "cover-1":
39054
+ sec.push(cb(-1, 6), cb(1, 6), { id: "SS", pos: "s", label: "$", x: s * 6, y: 5 }, { id: "FS", pos: "s", label: "F", x: 0, y: 13.5 });
39055
+ zones.push({ x: 0, y: 13.5, rx: 5, ry: 3.5, label: "deep middle" });
39056
+ break;
39057
+ case "cover-2":
39058
+ sec.push(cb(-1, 5), cb(1, 5), { id: "FS", pos: "s", label: "F", x: -9, y: 12.5 }, { id: "SS", pos: "s", label: "$", x: 9, y: 12.5 });
39059
+ zones.push({ x: -9, y: 12.5, rx: 9, ry: 4, label: "deep half" }, { x: 9, y: 12.5, rx: 9, ry: 4, label: "deep half" });
39060
+ break;
39061
+ case "cover-3":
39062
+ sec.push(cb(-1, 11.5), cb(1, 11.5), { id: "FS", pos: "s", label: "F", x: 0, y: 14 }, { id: "SS", pos: "s", label: "$", x: s * 6, y: 5 });
39063
+ zones.push({ x: -13.5, y: 12, rx: 5.8, ry: 3.6, label: "deep \u2153" }, { x: 0, y: 14.5, rx: 5.8, ry: 3.6, label: "deep \u2153" }, { x: 13.5, y: 12, rx: 5.8, ry: 3.6, label: "deep \u2153" });
39064
+ break;
39065
+ case "cover-4":
39066
+ sec.push(cb(-1, 10), cb(1, 10), { id: "FS", pos: "s", label: "F", x: -6, y: 12.5 }, { id: "SS", pos: "s", label: "$", x: 6, y: 12.5 });
39067
+ zones.push({ x: -13, y: 12, rx: 6.5, ry: 4, label: "deep \xBC" }, { x: -5, y: 13, rx: 6.5, ry: 4, label: "deep \xBC" }, { x: 5, y: 13, rx: 6.5, ry: 4, label: "deep \xBC" }, { x: 13, y: 12, rx: 6.5, ry: 4, label: "deep \xBC" });
39068
+ break;
39069
+ case "cover-6":
39070
+ sec.push(cb(-1, 10), cb(1, 5), { id: "FS", pos: "s", label: "F", x: -7, y: 12.5 }, { id: "SS", pos: "s", label: "$", x: 9, y: 12.5 });
39071
+ zones.push({ x: -7, y: 12.5, rx: 7, ry: 4, label: "deep \xBC" }, { x: -13, y: 12, rx: 6.5, ry: 4, label: "deep \xBC" }, { x: 9, y: 12.5, rx: 9, ry: 4, label: "deep \xBD" });
39072
+ break;
39073
+ default:
39074
+ sec.push(cb(-1, 5), cb(1, 5), { id: "FS", pos: "s", label: "F", x: -9, y: 12.5 }, { id: "SS", pos: "s", label: "$", x: 9, y: 12.5 });
39075
+ }
39076
+ return { defenders: [...front, ...sec].slice(0, 11), zones: showZones ? zones : [] };
39077
+ }
39078
+ function passRoute(named, depth, outSign, inSign) {
39079
+ const d = depth;
39080
+ switch (named) {
39081
+ case "go":
39082
+ case "fly":
39083
+ case "streak":
39084
+ case "vertical":
39085
+ return [{ x: 0, y: 0 }, { x: 0, y: d ?? 16 }];
39086
+ case "seam":
39087
+ return [{ x: 0, y: 0 }, { x: inSign * 0.5, y: d ?? 14 }];
39088
+ case "slant":
39089
+ return [{ x: 0, y: 0 }, { x: 0, y: 1.6 }, { x: inSign * 4.5, y: 6 }];
39090
+ case "flat":
39091
+ return [{ x: 0, y: 0 }, { x: outSign * 1.5, y: 1 }, { x: outSign * 5.5, y: 2.2 }];
39092
+ case "hitch":
39093
+ return [{ x: 0, y: 0 }, { x: 0, y: d ?? 5.5 }, { x: inSign * 1, y: (d ?? 5.5) - 1.2 }];
39094
+ case "out":
39095
+ return [{ x: 0, y: 0 }, { x: 0, y: d ?? 10 }, { x: outSign * 5.5, y: d ?? 10 }];
39096
+ case "in":
39097
+ case "dig":
39098
+ return [{ x: 0, y: 0 }, { x: 0, y: d ?? 10 }, { x: inSign * 6.5, y: d ?? 10 }];
39099
+ case "curl":
39100
+ return [{ x: 0, y: 0 }, { x: 0, y: d ?? 12 }, { x: inSign * 1.6, y: (d ?? 12) - 1.6 }];
39101
+ case "comeback":
39102
+ return [{ x: 0, y: 0 }, { x: 0, y: d ?? 14 }, { x: outSign * 2.2, y: (d ?? 14) - 2.4 }];
39103
+ case "corner":
39104
+ case "flag":
39105
+ return [{ x: 0, y: 0 }, { x: 0, y: d ?? 10 }, { x: outSign * 5.5, y: (d ?? 10) + 5 }];
39106
+ case "post":
39107
+ return [{ x: 0, y: 0 }, { x: 0, y: d ?? 10 }, { x: inSign * 5.5, y: (d ?? 10) + 6 }];
39108
+ case "wheel":
39109
+ return [{ x: 0, y: 0 }, { x: outSign * 3, y: 1 }, { x: outSign * 4, y: d ?? 11 }];
39110
+ case "cross":
39111
+ case "drag":
39112
+ return [{ x: 0, y: 0 }, { x: 0, y: 1.5 }, { x: inSign * 14, y: 3.2 }];
39113
+ case "screen":
39114
+ return [{ x: 0, y: 0 }, { x: 0, y: -1 }, { x: outSign * 4.5, y: -1 }];
39115
+ default:
39116
+ return [{ x: 0, y: 0 }, { x: 0, y: d ?? 10 }];
39117
+ }
39118
+ }
39119
+ function runPath(named, start, dir) {
39120
+ const k = dirSign(dir);
39121
+ switch (named) {
39122
+ case "dive":
39123
+ case "trap":
39124
+ return [start, { x: k * 1, y: 0.5 }, { x: k * 1.6, y: 3.2 }];
39125
+ case "iso":
39126
+ return [start, { x: k * 0.4, y: 0.5 }, { x: k * 0.8, y: 3.4 }];
39127
+ case "power":
39128
+ return [start, { x: start.x + k * 1.4, y: start.y + 1.5 }, { x: k * 2.6, y: 0.6 }, { x: k * 3.6, y: 4.2 }];
39129
+ case "counter":
39130
+ return [start, { x: -k * 1.6, y: start.y + 0.3 }, { x: k * 2.2, y: 0.6 }, { x: k * 3.2, y: 4.2 }];
39131
+ case "sweep":
39132
+ case "toss":
39133
+ return [start, { x: k * 5, y: start.y + 0.6 }, { x: k * 7, y: 1.2 }, { x: k * 7, y: 5 }];
39134
+ case "draw":
39135
+ return [start, { x: 0, y: start.y + 1.5 }, { x: k * 1, y: 3.4 }];
39136
+ default:
39137
+ return [start, { x: k * 1.4, y: 3.2 }];
39138
+ }
39139
+ }
39140
+ var footballModule = {
39141
+ scale: K.scale,
39142
+ yUp: true,
39143
+ buildPlayers(ast) {
39144
+ const players = [];
39145
+ const byId = /* @__PURE__ */ new Map();
39146
+ const add = (p) => {
39147
+ if (byId.has(p.id)) players[byId.get(p.id)] = p;
39148
+ else {
39149
+ byId.set(p.id, players.length);
39150
+ players.push(p);
39151
+ }
39152
+ };
39153
+ if (ast.formation) for (const sl of formationRoster(ast.formation, ast.formationSide)) add({ id: sl.id, side: "offense", pos: sl.pos, label: sl.label, x: sl.x, y: sl.y });
39154
+ if (ast.defense) for (const d of defensePreset(ast.defense, ast.formationSide).defenders) add({ id: d.id, side: "defense", pos: d.pos, label: d.label, x: d.x, y: d.y });
39155
+ for (const p of ast.players) {
39156
+ const at = p.at ?? (byId.has(p.id) ? { x: players[byId.get(p.id)].x, y: players[byId.get(p.id)].y } : { x: 0, y: 0 });
39157
+ add({ id: p.id, side: p.side, pos: p.pos, label: p.label, x: at.x, y: at.y });
39158
+ }
39159
+ return players;
39160
+ },
39161
+ buildZones(ast) {
39162
+ const zones = [];
39163
+ if (ast.defense) zones.push(...defensePreset(ast.defense, ast.formationSide).zones);
39164
+ for (const z of ast.zones) zones.push({ x: z.x, y: z.y, rx: z.rx, ry: z.ry, label: z.label });
39165
+ return zones;
39166
+ },
39167
+ resolveNamed(m, src, players, byId) {
39168
+ const endFor2 = (kind) => m.end ?? ("arrow");
39169
+ if (m.kind === "pull") {
39170
+ const k = dirSign(m.dir ?? "right");
39171
+ const offTackle = 2 * K.olSplit + 0.8;
39172
+ const pts = [{ x: src.x, y: 0 }, { x: src.x, y: -1.3 }, { x: k * offTackle, y: -1.3 }, { x: k * offTackle, y: 1.6 }];
39173
+ return { player: m.player, kind: "pull", style: "solid", points: pts.map(round22), end: "arrow" };
39174
+ }
39175
+ if ((m.kind === "run" || m.kind === "route") && m.named) {
39176
+ if (m.kind === "run") {
39177
+ return { player: m.player, kind: "run", style: "solid", points: runPath(m.named, src, m.dir ?? "right").map(round22), end: "arrow" };
39178
+ }
39179
+ const outSign = m.dir ? dirSign(m.dir) : src.x >= 0 ? 1 : -1;
39180
+ const rel = passRoute(m.named, m.depth, outSign, -outSign);
39181
+ return { player: m.player, kind: "route", style: "solid", points: rel.map((p) => round22({ x: src.x + p.x, y: src.y + p.y })), end: endFor2() };
39182
+ }
39183
+ return null;
39184
+ },
39185
+ bounds(ast, players, moves, zones) {
39186
+ let minX = -20, maxX = K.minHalfWidth, minY = -7, maxY = 6;
39187
+ const ext = (x, y) => {
39188
+ minX = Math.min(minX, x);
39189
+ maxX = Math.max(maxX, x);
39190
+ minY = Math.min(minY, y);
39191
+ maxY = Math.max(maxY, y);
39192
+ };
39193
+ for (const p of players) ext(p.x, p.y);
39194
+ for (const mv of moves) for (const pt of mv.points) ext(pt.x, pt.y);
39195
+ for (const z of zones) {
39196
+ ext(z.x - z.rx, z.y - z.ry);
39197
+ ext(z.x + z.rx, z.y + z.ry);
39198
+ }
39199
+ if (ast.toGoal !== void 0) maxY = Math.max(maxY, ast.toGoal + 11);
39200
+ const m = K.margin;
39201
+ return { minX: minX - m, maxX: maxX + m, minY: minY - m, maxY: maxY + m };
39202
+ },
39203
+ drawField(lay, ctx, t) {
39204
+ const b = lay.bounds;
39205
+ const parts = [];
39206
+ const x1 = ctx.X(b.minX), x2 = ctx.X(b.maxX);
39207
+ if (lay.toGoal !== void 0) {
39208
+ const gl = lay.toGoal;
39209
+ const ezTop = Math.min(gl + 10, b.maxY);
39210
+ parts.push(rect({ class: "sx-pb-endzone", x: r25(x1), y: r25(ctx.Y(ezTop)), width: r25((b.maxX - b.minX) * K.scale), height: r25((ezTop - gl) * K.scale) }));
39211
+ parts.push(line({ class: "sx-pb-goalline", x1: r25(x1), y1: r25(ctx.Y(gl)), x2: r25(x2), y2: r25(ctx.Y(gl)) }));
39212
+ const gy = ctx.Y(Math.min(gl + 10, b.maxY) - 0.2);
39213
+ const cx = ctx.X(0);
39214
+ const up = ctx.px(2.2);
39215
+ const cross = ctx.px(3);
39216
+ parts.push(line({ class: "sx-pb-goalpost", x1: r25(cx), y1: r25(gy), x2: r25(cx), y2: r25(gy + ctx.px(1)) }));
39217
+ parts.push(line({ class: "sx-pb-goalpost", x1: r25(cx - cross / 2), y1: r25(gy), x2: r25(cx + cross / 2), y2: r25(gy) }));
39218
+ parts.push(line({ class: "sx-pb-goalpost", x1: r25(cx - cross / 2), y1: r25(gy), x2: r25(cx - cross / 2), y2: r25(gy - up) }));
39219
+ parts.push(line({ class: "sx-pb-goalpost", x1: r25(cx + cross / 2), y1: r25(gy), x2: r25(cx + cross / 2), y2: r25(gy - up) }));
39220
+ }
39221
+ const startY = Math.ceil(b.minY / 5) * 5;
39222
+ for (let yd = startY; yd <= b.maxY; yd += 5) {
39223
+ if (Math.abs(yd) < 1e-6) continue;
39224
+ if (lay.toGoal !== void 0 && yd > lay.toGoal + 0.01) continue;
39225
+ parts.push(line({ class: "sx-pb-yard", x1: r25(x1), y1: r25(ctx.Y(yd)), x2: r25(x2), y2: r25(ctx.Y(yd)) }));
39226
+ if (lay.toGoal !== void 0) {
39227
+ const yardNum = lay.toGoal - yd;
39228
+ if (yardNum > 0 && yardNum % 10 === 0) {
39229
+ parts.push(text({ class: "sx-pb-yardnum", x: r25(x1 + 12), y: r25(ctx.Y(yd) + 4), "text-anchor": "middle" }, String(yardNum)));
39230
+ parts.push(text({ class: "sx-pb-yardnum", x: r25(x2 - 12), y: r25(ctx.Y(yd) + 4), "text-anchor": "middle" }, String(yardNum)));
39231
+ }
39232
+ }
39233
+ }
39234
+ if (lay.hash !== "none") {
39235
+ const hx = lay.hash === "college" ? K.hashCollege : K.hashNfl;
39236
+ const top = lay.toGoal !== void 0 ? Math.min(b.maxY, lay.toGoal) : b.maxY;
39237
+ for (let yd = Math.ceil(b.minY); yd <= top; yd += 1) {
39238
+ for (const sgn of [-1, 1]) {
39239
+ const xc = ctx.X(sgn * hx);
39240
+ parts.push(line({ class: "sx-pb-hash", x1: r25(xc - 2), y1: r25(ctx.Y(yd)), x2: r25(xc + 2), y2: r25(ctx.Y(yd)) }));
39241
+ }
39242
+ }
39243
+ }
39244
+ parts.push(line({ class: "sx-pb-los", x1: r25(x1), y1: r25(ctx.Y(0)), x2: r25(x2), y2: r25(ctx.Y(0)) }));
39245
+ return group({ class: "sx-pb-field-g" }, parts);
39246
+ },
39247
+ legend(lay) {
39248
+ const items = [
39249
+ { kind: "offense", label: "Offense" },
39250
+ { kind: "defense", label: "Defense" },
39251
+ { kind: "run", label: "Route / run" },
39252
+ { kind: "screen", label: "Block" },
39253
+ { kind: "motion", label: "Motion" }
39254
+ ];
39255
+ if (lay.zones.length) items.push({ kind: "zone", label: "Zone" });
39256
+ return items;
39257
+ }
39258
+ };
39259
+
39260
+ // src/diagrams/playbook/geometry/basketball.ts
39261
+ var K2 = {
39262
+ scale: 11,
39263
+ // x ∈ [-25, 25]
39264
+ half: 47,
39265
+ rimY: 5.25,
39266
+ laneHalf: 8,
39267
+ // NBA lane half-width
39268
+ ftY: 15,
39269
+ ftR: 6,
39270
+ threeR: 23.75,
39271
+ cornerX: 22,
39272
+ cornerMeetY: 14.2,
39273
+ restrictedR: 4,
39274
+ centerR: 6,
39275
+ margin: 2.5
39276
+ };
39277
+ var r26 = (n) => Math.round(n * 100) / 100;
39278
+ var LANDMARKS = {
39279
+ top: { x: 0, y: 28 },
39280
+ rslot: { x: 8, y: 26 },
39281
+ lslot: { x: -8, y: 26 },
39282
+ slot: { x: 8, y: 26 },
39283
+ rwing: { x: 19, y: 17 },
39284
+ lwing: { x: -19, y: 17 },
39285
+ wing: { x: 19, y: 17 },
39286
+ rcorner: { x: 22, y: 3 },
39287
+ lcorner: { x: -22, y: 3 },
39288
+ corner: { x: 22, y: 3 },
39289
+ "rshort-corner": { x: 12, y: 2 },
39290
+ "lshort-corner": { x: -12, y: 2 },
39291
+ relbow: { x: 8, y: 15 },
39292
+ lelbow: { x: -8, y: 15 },
39293
+ elbow: { x: 8, y: 15 },
39294
+ rblock: { x: 8, y: 7 },
39295
+ lblock: { x: -8, y: 7 },
39296
+ block: { x: 8, y: 7 },
39297
+ rdunker: { x: 9, y: 4 },
39298
+ ldunker: { x: -9, y: 4 },
39299
+ "high-post": { x: 0, y: 15 },
39300
+ ft: { x: 0, y: 15 },
39301
+ "free-throw": { x: 0, y: 15 },
39302
+ "rlow-post": { x: 9, y: 8 },
39303
+ "llow-post": { x: -9, y: 8 },
39304
+ rim: { x: 0, y: 5.25 },
39305
+ basket: { x: 0, y: 5.25 },
39306
+ hoop: { x: 0, y: 5.25 },
39307
+ paint: { x: 0, y: 10 }
39308
+ };
39309
+ function setRoster(formation) {
39310
+ const L = LANDMARKS;
39311
+ const at = (lm) => L[lm];
39312
+ const slots = (pts) => pts.map((p, i) => ({ id: String(i + 1), label: String(i + 1), x: p.x, y: p.y }));
39313
+ switch (formation) {
39314
+ case "horns":
39315
+ return slots([at("top"), at("lcorner"), at("rcorner"), at("lelbow"), at("relbow")]);
39316
+ case "1-4-high":
39317
+ return slots([at("top"), at("lwing"), at("rwing"), at("lelbow"), at("relbow")]);
39318
+ case "1-4-low":
39319
+ return slots([at("top"), at("lcorner"), at("rcorner"), at("lblock"), at("rblock")]);
39320
+ case "box":
39321
+ return slots([at("top"), at("lelbow"), at("relbow"), at("lblock"), at("rblock")]);
39322
+ case "spread-pnr":
39323
+ return slots([at("top"), at("lcorner"), at("rcorner"), at("lwing"), { x: 8, y: 24 }]);
39324
+ case "4-out":
39325
+ return slots([at("top"), at("lwing"), at("rwing"), at("lcorner"), at("rblock")]);
39326
+ case "5-out":
39327
+ default:
39328
+ return slots([at("top"), at("lwing"), at("rwing"), at("lcorner"), at("rcorner")]);
39329
+ }
39330
+ }
39331
+ function defenseRoster(scheme, offense) {
39332
+ const zones = [];
39333
+ if (scheme === "man") {
39334
+ const defenders2 = offense.filter((p) => p.side === "offense").slice(0, 5).map((o) => {
39335
+ const dx = -o.x * 0.12;
39336
+ const dy = (K2.rimY - o.y) * 0.18;
39337
+ return { id: "X" + o.id, label: "X" + o.label, x: o.x + dx, y: o.y + dy };
39338
+ });
39339
+ return { defenders: defenders2, zones };
39340
+ }
39341
+ let pts;
39342
+ if (scheme === "zone-3-2") pts = [{ x: 0, y: 20 }, { x: -13, y: 17 }, { x: 13, y: 17 }, { x: -8, y: 8 }, { x: 8, y: 8 }];
39343
+ else if (scheme === "zone-1-3-1") pts = [{ x: 0, y: 22 }, { x: -12, y: 14 }, { x: 0, y: 13 }, { x: 12, y: 14 }, { x: 0, y: 5.5 }];
39344
+ else pts = [{ x: -7, y: 18 }, { x: 7, y: 18 }, { x: 0, y: 9 }, { x: -13, y: 6 }, { x: 13, y: 6 }];
39345
+ const defenders = pts.map((p, i) => ({ id: "X" + (i + 1), label: "X", x: p.x, y: p.y }));
39346
+ return { defenders, zones };
39347
+ }
39348
+ var basketballModule = {
39349
+ scale: K2.scale,
39350
+ yUp: false,
39351
+ buildPlayers(ast) {
39352
+ const players = [];
39353
+ const byId = /* @__PURE__ */ new Map();
39354
+ const add = (p) => {
39355
+ if (byId.has(p.id)) players[byId.get(p.id)] = p;
39356
+ else {
39357
+ byId.set(p.id, players.length);
39358
+ players.push(p);
39359
+ }
39360
+ };
39361
+ for (const sl of setRoster(ast.formation)) add({ id: sl.id, side: "offense", pos: "o", label: sl.label, x: sl.x, y: sl.y });
39362
+ if (ast.defense) for (const d of defenseRoster(ast.defense, players).defenders) add({ id: d.id, side: "defense", pos: "x", label: d.label, x: d.x, y: d.y });
39363
+ for (const p of ast.players) {
39364
+ const at = p.at ?? (byId.has(p.id) ? { x: players[byId.get(p.id)].x, y: players[byId.get(p.id)].y } : { x: 0, y: 0 });
39365
+ add({ id: p.id, side: p.side, pos: p.pos === "c" || p.pos === "ol" ? "o" : p.pos, label: p.label, x: at.x, y: at.y });
39366
+ }
39367
+ return players;
39368
+ },
39369
+ buildZones(ast) {
39370
+ return ast.zones.map((z) => ({ x: z.x, y: z.y, rx: z.rx, ry: z.ry, label: z.label }));
39371
+ },
39372
+ resolveLandmark(name) {
39373
+ return LANDMARKS[name.toLowerCase()] ?? null;
39374
+ },
39375
+ bounds(_ast, players, moves, zones) {
39376
+ let minX = -25, maxX = 25, minY = 0, maxY = 30;
39377
+ const ext = (x, y) => {
39378
+ minX = Math.min(minX, x);
39379
+ maxX = Math.max(maxX, x);
39380
+ minY = Math.min(minY, y);
39381
+ maxY = Math.max(maxY, y);
39382
+ };
39383
+ for (const p of players) ext(p.x, p.y);
39384
+ for (const mv of moves) for (const pt of mv.points) ext(pt.x, pt.y);
39385
+ for (const z of zones) {
39386
+ ext(z.x - z.rx, z.y - z.ry);
39387
+ ext(z.x + z.rx, z.y + z.ry);
39388
+ }
39389
+ const m = K2.margin;
39390
+ return { minX: Math.min(-25, minX) - m * 0.4, maxX: Math.max(25, maxX) + m * 0.4, minY: Math.min(0, minY) - m * 0.4, maxY: Math.min(K2.half, maxY + m * 0.6) };
39391
+ },
39392
+ drawField(_lay, ctx, t) {
39393
+ const parts = [];
39394
+ const X = ctx.X, Y = ctx.Y, px = ctx.px;
39395
+ parts.push(rect({ class: "sx-pb-court-line", fill: "none", x: r26(X(-8)), y: r26(Y(0)), width: r26(px(2 * K2.laneHalf)), height: r26(px(K2.ftY)) }));
39396
+ parts.push(circle({ class: "sx-pb-court-line", fill: "none", cx: r26(X(0)), cy: r26(Y(K2.ftY)), r: r26(px(K2.ftR)) }));
39397
+ parts.push(line({ class: "sx-pb-court-line", x1: r26(X(-3)), y1: r26(Y(4)), x2: r26(X(3)), y2: r26(Y(4)) }));
39398
+ parts.push(circle({ class: "sx-pb-rim", fill: "none", cx: r26(X(0)), cy: r26(Y(K2.rimY)), r: r26(px(0.75)) }));
39399
+ parts.push(path({ class: "sx-pb-court-line", fill: "none", d: `M ${r26(X(-4))} ${r26(Y(K2.rimY))} A ${r26(px(K2.restrictedR))} ${r26(px(K2.restrictedR))} 0 0 0 ${r26(X(K2.restrictedR))} ${r26(Y(K2.rimY))}` }));
39400
+ parts.push(path({
39401
+ class: "sx-pb-court-line",
39402
+ fill: "none",
39403
+ d: `M ${r26(X(-22))} ${r26(Y(0))} L ${r26(X(-22))} ${r26(Y(K2.cornerMeetY))} A ${r26(px(K2.threeR))} ${r26(px(K2.threeR))} 0 0 0 ${r26(X(K2.cornerX))} ${r26(Y(K2.cornerMeetY))} L ${r26(X(K2.cornerX))} ${r26(Y(0))}`
39404
+ }));
39405
+ parts.push(path({ class: "sx-pb-court-line", fill: "none", d: `M ${r26(X(-6))} ${r26(Y(K2.half))} A ${r26(px(K2.centerR))} ${r26(px(K2.centerR))} 0 0 1 ${r26(X(K2.centerR))} ${r26(Y(K2.half))}` }));
39406
+ return group({ class: "sx-pb-field-g" }, parts);
39407
+ },
39408
+ legend() {
39409
+ return [
39410
+ { kind: "offense", label: "Offense (1\u20135)" },
39411
+ { kind: "defense", label: "Defense (X)" },
39412
+ { kind: "run", label: "Cut" },
39413
+ { kind: "pass", label: "Pass" },
39414
+ { kind: "dribble", label: "Dribble" },
39415
+ { kind: "screen", label: "Screen" }
39416
+ ];
39417
+ }
39418
+ };
39419
+
39420
+ // src/diagrams/playbook/geometry/soccer.ts
39421
+ var K3 = {
39422
+ scale: 8,
39423
+ L: 105,
39424
+ W: 68,
39425
+ circleR: 9.15,
39426
+ paDepth: 16.5,
39427
+ paHalf: 20.16,
39428
+ // 40.32 / 2
39429
+ gaDepth: 5.5,
39430
+ gaHalf: 9.16,
39431
+ // 18.32 / 2
39432
+ penDist: 11,
39433
+ goalHalf: 3.66,
39434
+ // 7.32 / 2
39435
+ cornerR: 1,
39436
+ margin: 3
39437
+ };
39438
+ var r27 = (n) => Math.round(n * 100) / 100;
39439
+ function formationRoster2(formation) {
39440
+ const p = (id, x, y, gk2 = false) => ({ id, label: id, x, y, gk: gk2 });
39441
+ const back4 = [p("2", 20, 12), p("4", 20, 27), p("5", 20, 41), p("3", 20, 56)];
39442
+ const gk = p("1", 5, 34, true);
39443
+ switch (formation) {
39444
+ case "4-4-2":
39445
+ return [gk, ...back4, p("7", 50, 10), p("6", 50, 27), p("8", 50, 41), p("11", 50, 58), p("9", 80, 28), p("10", 80, 40)];
39446
+ case "4-2-3-1":
39447
+ return [gk, ...back4, p("6", 40, 28), p("8", 40, 40), p("7", 65, 12), p("10", 65, 34), p("11", 65, 56), p("9", 85, 34)];
39448
+ case "4-5-1":
39449
+ return [gk, ...back4, p("6", 48, 27), p("8", 48, 41), p("7", 52, 10), p("11", 52, 58), p("10", 62, 34), p("9", 85, 34)];
39450
+ case "4-4-1-1":
39451
+ return [gk, ...back4, p("7", 50, 10), p("6", 50, 27), p("8", 50, 41), p("11", 50, 58), p("10", 68, 34), p("9", 84, 34)];
39452
+ case "3-5-2":
39453
+ return [gk, p("4", 20, 20), p("5", 20, 34), p("6", 20, 48), p("2", 52, 6), p("3", 52, 62), p("8", 50, 25), p("10", 55, 43), p("7", 45, 34), p("9", 82, 28), p("11", 82, 40)];
39454
+ case "3-4-3":
39455
+ return [gk, p("4", 20, 20), p("5", 20, 34), p("6", 20, 48), p("2", 48, 8), p("8", 48, 28), p("10", 48, 40), p("3", 48, 60), p("7", 82, 14), p("9", 82, 34), p("11", 82, 54)];
39456
+ case "4-3-3":
39457
+ default:
39458
+ return [gk, ...back4, p("6", 42, 34), p("8", 52, 24), p("10", 52, 44), p("7", 82, 12), p("9", 82, 34), p("11", 82, 56)];
39459
+ }
39460
+ }
39461
+ function opponentBlock(scheme) {
39462
+ const cx = scheme === "high-press" ? 45 : scheme === "mid-block" ? 70 : 88;
39463
+ const p = (i, x, y) => ({ id: "X" + i, label: "X", x, y });
39464
+ return [
39465
+ p(1, cx + 12, 34),
39466
+ p(2, cx, 12),
39467
+ p(3, cx, 27),
39468
+ p(4, cx, 41),
39469
+ p(5, cx, 56),
39470
+ p(6, cx - 14, 14),
39471
+ p(7, cx - 14, 28),
39472
+ p(8, cx - 14, 40),
39473
+ p(9, cx - 14, 54),
39474
+ p(10, cx - 26, 28),
39475
+ p(11, cx - 26, 40)
39476
+ ];
39477
+ }
39478
+ var LANDMARKS2 = {
39479
+ center: { x: 52.5, y: 34 },
39480
+ "centre-spot": { x: 52.5, y: 34 },
39481
+ box: { x: 96, y: 34 },
39482
+ "top-box": { x: 88.5, y: 34 },
39483
+ d: { x: 84.85, y: 34 },
39484
+ "penalty-arc": { x: 84.85, y: 34 },
39485
+ "penalty-spot": { x: 94, y: 34 },
39486
+ "near-post": { x: 105, y: 30.5 },
39487
+ "far-post": { x: 105, y: 37.7 },
39488
+ goal: { x: 105, y: 34 },
39489
+ "rcorner": { x: 105, y: 2 },
39490
+ "lcorner": { x: 105, y: 66 },
39491
+ "six-yard": { x: 101, y: 34 }
39492
+ };
39493
+ var soccerModule = {
39494
+ scale: K3.scale,
39495
+ yUp: false,
39496
+ buildPlayers(ast) {
39497
+ const players = [];
39498
+ const byId = /* @__PURE__ */ new Map();
39499
+ const add = (pl2) => {
39500
+ if (byId.has(pl2.id)) players[byId.get(pl2.id)] = pl2;
39501
+ else {
39502
+ byId.set(pl2.id, players.length);
39503
+ players.push(pl2);
39504
+ }
39505
+ };
39506
+ for (const sl of formationRoster2(ast.formation)) add({ id: sl.id, side: "offense", pos: sl.gk ? "gk" : "o", label: sl.label, x: sl.x, y: sl.y });
39507
+ if (ast.defense) for (const d of opponentBlock(ast.defense)) add({ id: d.id, side: "defense", pos: "x", label: d.label, x: d.x, y: d.y });
39508
+ for (const pl2 of ast.players) {
39509
+ const at = pl2.at ?? (byId.has(pl2.id) ? { x: players[byId.get(pl2.id)].x, y: players[byId.get(pl2.id)].y } : { x: 0, y: 0 });
39510
+ add({ id: pl2.id, side: pl2.side, pos: pl2.pos === "c" || pl2.pos === "ol" ? "o" : pl2.pos, label: pl2.label, x: at.x, y: at.y });
39511
+ }
39512
+ return players;
39513
+ },
39514
+ buildZones(ast) {
39515
+ return ast.zones.map((z) => ({ x: z.x, y: z.y, rx: z.rx, ry: z.ry, label: z.label }));
39516
+ },
39517
+ resolveLandmark(name) {
39518
+ return LANDMARKS2[name.toLowerCase()] ?? null;
39519
+ },
39520
+ bounds(ast) {
39521
+ const m = K3.margin;
39522
+ if (ast.view === "half") return { minX: 52.5 - m, maxX: K3.L + m, minY: -m, maxY: K3.W + m };
39523
+ return { minX: -m, maxX: K3.L + m, minY: -m, maxY: K3.W + m };
39524
+ },
39525
+ drawField(_lay, ctx, t) {
39526
+ const X = ctx.X, Y = ctx.Y, px = ctx.px;
39527
+ const parts = [];
39528
+ const ln = (x1, y1, x2, y2) => line({ class: "sx-pb-pitch-line", x1: r27(X(x1)), y1: r27(Y(y1)), x2: r27(X(x2)), y2: r27(Y(y2)) });
39529
+ const bands = 10;
39530
+ for (let i = 0; i < bands; i++) {
39531
+ if (i % 2 === 0) continue;
39532
+ parts.push(rect({ class: "sx-pb-stripe", x: r27(X(i * K3.L / bands)), y: r27(Y(0)), width: r27(px(K3.L / bands)), height: r27(px(K3.W)) }));
39533
+ }
39534
+ const cr = px(K3.cornerR);
39535
+ const corners = [[0, 0, 0], [K3.L, 0, 1], [0, K3.W, 2], [K3.L, K3.W, 3]];
39536
+ for (const [cxF, cyF, q] of corners) {
39537
+ const cx = X(cxF), cy = Y(cyF);
39538
+ const sx = cxF === 0 ? 1 : -1;
39539
+ const sy = cyF === 0 ? 1 : -1;
39540
+ parts.push(path({ class: "sx-pb-pitch-line", fill: "none", d: `M ${r27(cx + sx * cr)} ${r27(cy)} A ${r27(cr)} ${r27(cr)} 0 0 ${q === 1 || q === 2 ? 1 : 0} ${r27(cx)} ${r27(cy + sy * cr)}` }));
39541
+ }
39542
+ parts.push(ln(52.5, 0, 52.5, K3.W));
39543
+ parts.push(circle({ class: "sx-pb-pitch-line", fill: "none", cx: r27(X(52.5)), cy: r27(Y(34)), r: r27(px(K3.circleR)) }));
39544
+ parts.push(circle({ class: "sx-pb-pitch-dot", cx: r27(X(52.5)), cy: r27(Y(34)), r: 1.6 }));
39545
+ const topY = (half) => Math.min(Y(34 - half), Y(34 + half));
39546
+ for (const end of [0, 1]) {
39547
+ const gx = end === 0 ? 0 : K3.L;
39548
+ const sgn = end === 0 ? 1 : -1;
39549
+ parts.push(rect({ class: "sx-pb-pitch-line", fill: "none", x: r27(X(Math.min(gx, gx + sgn * K3.paDepth))), y: r27(topY(K3.paHalf)), width: r27(px(K3.paDepth)), height: r27(px(2 * K3.paHalf)) }));
39550
+ parts.push(rect({ class: "sx-pb-pitch-line", fill: "none", x: r27(X(Math.min(gx, gx + sgn * K3.gaDepth))), y: r27(topY(K3.gaHalf)), width: r27(px(K3.gaDepth)), height: r27(px(2 * K3.gaHalf)) }));
39551
+ const spotX = gx + sgn * K3.penDist;
39552
+ parts.push(circle({ class: "sx-pb-pitch-dot", cx: r27(X(spotX)), cy: r27(Y(34)), r: 1.4 }));
39553
+ const edgeX = gx + sgn * K3.paDepth;
39554
+ const dy = Math.sqrt(K3.circleR * K3.circleR - (K3.paDepth - K3.penDist) * (K3.paDepth - K3.penDist));
39555
+ const sweep = end === 0 ? 1 : 0;
39556
+ parts.push(path({ class: "sx-pb-pitch-line", fill: "none", d: `M ${r27(X(edgeX))} ${r27(Y(34 - dy))} A ${r27(px(K3.circleR))} ${r27(px(K3.circleR))} 0 0 ${sweep} ${r27(X(edgeX))} ${r27(Y(34 + dy))}` }));
39557
+ const goalX = gx - sgn * 2.2;
39558
+ parts.push(rect({ class: "sx-pb-goalbox", x: r27(X(Math.min(gx, goalX))), y: r27(topY(K3.goalHalf)), width: r27(px(2.2)), height: r27(px(2 * K3.goalHalf)) }));
39559
+ }
39560
+ return group({ class: "sx-pb-field-g" }, parts);
39561
+ },
39562
+ legend() {
39563
+ return [
39564
+ { kind: "offense", label: "Team" },
39565
+ { kind: "gk", label: "Keeper" },
39566
+ { kind: "defense", label: "Opponent" },
39567
+ { kind: "pass", label: "Pass" },
39568
+ { kind: "run", label: "Run" },
39569
+ { kind: "dribble", label: "Dribble" },
39570
+ { kind: "shot", label: "Shot" }
39571
+ ];
39572
+ }
39573
+ };
39574
+
39575
+ // src/diagrams/playbook/layout.ts
39576
+ var MODULES = {
39577
+ football: footballModule,
39578
+ basketball: basketballModule,
39579
+ soccer: soccerModule
39580
+ };
39581
+ function sportModule(sport) {
39582
+ return MODULES[sport];
39583
+ }
39584
+ var round23 = (p) => ({ x: Math.round(p.x * 100) / 100, y: Math.round(p.y * 100) / 100 });
39585
+ function styleFor(sport, kind) {
39586
+ if (kind === "dribble") return "wavy";
39587
+ if (kind === "shot") return sport === "soccer" ? "double" : "solid";
39588
+ if (kind === "pass") return sport === "soccer" ? "solid" : "dashed";
39589
+ if (kind === "motion") return "dashed";
39590
+ if (sport === "soccer" && (kind === "run" || kind === "cut" || kind === "move")) return "dashed";
39591
+ return "solid";
39592
+ }
39593
+ function endFor(kind, override) {
39594
+ if (override) return override;
39595
+ if (kind === "screen" || kind === "block") return "tee";
39596
+ if (kind === "handoff") return "none";
39597
+ return "arrow";
39598
+ }
39599
+ function findPlayer(ref, byId) {
39600
+ if (byId.has(ref)) return byId.get(ref);
39601
+ const lower = ref.toLowerCase();
39602
+ for (const [k, v] of byId) if (k.toLowerCase() === lower) return v;
39603
+ return void 0;
39604
+ }
39605
+ function resolveRef(token, byId, players, mod) {
39606
+ const coord = /^(-?\d*\.?\d+),(-?\d*\.?\d+)$/.exec(token);
39607
+ if (coord) return { x: Number(coord[1]), y: Number(coord[2]) };
39608
+ const pi = findPlayer(token, byId);
39609
+ if (pi !== void 0) return { x: players[pi].x, y: players[pi].y };
39610
+ const lm = mod.resolveLandmark?.(token);
39611
+ if (lm) return { x: lm.x, y: lm.y };
39612
+ return null;
39613
+ }
39614
+ function layoutPlaybook(ast) {
39615
+ const mod = MODULES[ast.sport];
39616
+ const errors = [];
39617
+ const warnings = [];
39618
+ const players = mod.buildPlayers(ast);
39619
+ const byId = /* @__PURE__ */ new Map();
39620
+ players.forEach((p, i) => byId.set(p.id, i));
39621
+ if (players.length === 0) {
39622
+ errors.push("no players \u2014 add a `formation`/set or explicit `player` statements");
39623
+ }
39624
+ const zones = mod.buildZones(ast, players);
39625
+ const moves = [];
39626
+ for (const m of ast.moves) {
39627
+ const srcIdx = findPlayer(m.player, byId);
39628
+ if (srcIdx === void 0) {
39629
+ warnings.push(`move on unknown player "${m.player}" \u2014 skipped`);
39630
+ continue;
39631
+ }
39632
+ const src = { x: players[srcIdx].x, y: players[srcIdx].y };
39633
+ const named = mod.resolveNamed?.(m, src, players, byId, warnings);
39634
+ if (named) {
39635
+ moves.push(named);
39636
+ continue;
39637
+ }
39638
+ const geom = resolveGeneric(m, src, byId, players, mod, ast.sport, warnings);
39639
+ if (geom) moves.push(geom);
39640
+ }
39641
+ const bounds = mod.bounds(ast, players, moves, zones);
39642
+ return {
39643
+ title: ast.title,
39644
+ sport: ast.sport,
39645
+ down: ast.down,
39646
+ distance: ast.distance,
39647
+ losYard: ast.losYard,
39648
+ toGoal: ast.toGoal,
39649
+ hash: ast.hash,
39650
+ view: ast.view === "half" ? "half" : "full",
39651
+ players,
39652
+ moves,
39653
+ zones,
39654
+ bounds,
39655
+ errors,
39656
+ warnings
39657
+ };
39658
+ }
39659
+ function resolveGeneric(m, src, byId, players, mod, sport, warnings) {
39660
+ const style = styleFor(sport, m.kind);
39661
+ if (m.points && m.points.length) {
39662
+ const pts = [{ x: src.x, y: src.y }];
39663
+ let cur = { x: src.x, y: src.y };
39664
+ for (const p of m.points) {
39665
+ if (p.ref) {
39666
+ const r6 = resolveRef(p.ref, byId, players, mod);
39667
+ if (!r6) {
39668
+ warnings.push(`${m.kind} ${m.player}: unknown destination "${p.ref}" \u2014 skipped`);
39669
+ return null;
39670
+ }
39671
+ cur = r6;
39672
+ } else if (p.rel) {
39673
+ cur = { x: cur.x + (p.x ?? 0), y: cur.y + (p.y ?? 0) };
39674
+ } else {
39675
+ cur = { x: p.x ?? cur.x, y: p.y ?? cur.y };
39676
+ }
39677
+ pts.push(cur);
39678
+ }
39679
+ return { player: m.player, kind: m.kind, style, points: pts.map(round23), end: endFor(m.kind, m.end) };
39680
+ }
39681
+ let dest = null;
39682
+ if (m.target) dest = resolveRef(m.target, byId, players, mod);
39683
+ else if (m.kind === "shot") dest = mod.resolveLandmark?.("rim") ?? mod.resolveLandmark?.("goal") ?? null;
39684
+ if (!dest) {
39685
+ warnings.push(`${m.kind} ${m.player}: needs a target/destination \u2014 skipped`);
39686
+ return null;
39687
+ }
39688
+ if (m.kind === "screen" || m.kind === "block") {
39689
+ const dx = dest.x - src.x, dy = dest.y - src.y;
39690
+ const len = Math.hypot(dx, dy) || 1;
39691
+ const gap = sport === "football" ? 0.6 : sport === "basketball" ? 1.2 : 1.6;
39692
+ const stop = Math.max(0, len - gap);
39693
+ const end = { x: src.x + dx / len * stop, y: src.y + dy / len * stop };
39694
+ return { player: m.player, kind: m.kind, style, points: [round23(src), round23(end)], end: "tee" };
39695
+ }
39696
+ return { player: m.player, kind: m.kind, style, points: [round23(src), round23(dest)], end: endFor(m.kind, m.end) };
39697
+ }
39698
+
39699
+ // src/diagrams/playbook/renderer.ts
39700
+ var r28 = (n) => Math.round(n * 100) / 100;
39701
+ function buildCss14(t) {
39702
+ return `
39703
+ .sx-pb { font-family: ${DEFAULT_FONT_FAMILY}; }
39704
+ .sx-pb-title { font: ${TITLE.weight} ${TITLE.size}px sans-serif; fill: ${t.text}; }
39705
+ .sx-pb-field, .sx-pb-turf { fill: ${t.surface}; }
39706
+ .sx-pb-court { fill: ${t.courtSurface}; }
39707
+ .sx-pb-surround { fill: ${t.surround}; }
39708
+ .sx-pb-surround-court { fill: ${t.courtSurround}; }
39709
+ .sx-pb-boundary { fill: none; stroke: ${t.lineBold}; stroke-width: 2.6; }
39710
+ .sx-pb-boundary-court { fill: none; stroke: ${t.courtLine}; stroke-width: 2.6; }
39711
+ .sx-pb-stripe { fill: ${t.surfaceAlt}; }
39712
+ .sx-pb-yard, .sx-pb-pitch-line, .sx-pb-goalbox { fill: none; stroke: ${t.lineSoft}; stroke-width: 1.4; }
39713
+ .sx-pb-court-line { fill: none; stroke: ${t.courtLine}; stroke-width: 1.5; }
39714
+ .sx-pb-hash { fill: none; stroke: ${t.lineSoft}; stroke-width: 1.2; }
39715
+ .sx-pb-los, .sx-pb-goalline { fill: none; stroke: ${t.lineBold}; stroke-width: 2.4; }
39716
+ .sx-pb-goalline { stroke: ${t.goalAccent}; }
39717
+ .sx-pb-goalpost { fill: none; stroke: ${t.goalAccent}; stroke-width: 2.4; stroke-linecap: round; }
39718
+ .sx-pb-endzone { fill: ${t.endzoneFill}; }
39719
+ .sx-pb-yardnum { font: 600 11px sans-serif; fill: ${t.surfaceText}; }
39720
+ .sx-pb-rim { stroke: ${t.rim}; stroke-width: 2.4; }
39721
+ .sx-pb-pitch-dot { fill: ${t.lineBold}; }
39722
+ .sx-pb-zone { fill: ${t.zoneFill}; stroke: ${t.zoneStroke}; stroke-width: 1.3; stroke-dasharray: 5 4; }
39723
+ .sx-pb-zone-text { font: 9px sans-serif; fill: ${t.zoneStroke}; }
39724
+ .sx-pb-move { fill: none; stroke: ${t.moveStroke}; stroke-width: 2.4; stroke-linejoin: round; stroke-linecap: round; }
39725
+ .sx-pb-move-dash { fill: none; stroke: ${t.moveStroke}; stroke-width: 2.2; stroke-dasharray: 6 4; stroke-linejoin: round; stroke-linecap: round; }
39726
+ .sx-pb-shot { fill: none; stroke: ${t.shotStroke}; stroke-width: 3.4; stroke-linecap: round; }
39727
+ .sx-pb-motion { fill: none; stroke: ${t.motionStroke}; stroke-width: 1.9; stroke-dasharray: 4 3; }
39728
+ .sx-pb-move-fill { fill: ${t.moveStroke}; stroke: none; }
39729
+ .sx-pb-shot-fill { fill: ${t.shotStroke}; stroke: none; }
39730
+ .sx-pb-motion-fill { fill: ${t.motionStroke}; stroke: none; }
39731
+ .sx-pb-o { fill: ${t.offenseFill}; stroke: ${t.offenseStroke}; stroke-width: 2; }
39732
+ .sx-pb-gk { fill: ${t.gkFill}; stroke: ${t.offenseStroke}; stroke-width: 2; }
39733
+ .sx-pb-o-text { font: 700 10.5px sans-serif; fill: ${t.offenseLabel}; }
39734
+ .sx-pb-x { fill: none; stroke: ${t.defenseStroke}; stroke-width: 2.6; stroke-linecap: round; }
39735
+ .sx-pb-x-text { font: 700 9px sans-serif; fill: ${t.defenseStroke}; }
39736
+ .sx-pb-ball { fill: ${t.ballFill}; stroke: ${t.lineBold}; stroke-width: 0.8; }
39737
+ .sx-pb-anno { font: 600 12px sans-serif; fill: ${t.annotation}; }
39738
+ .sx-pb-legend { font: 11px sans-serif; fill: ${t.annotation}; }
39739
+ .sx-pb-error-box { fill: ${t.bg}; stroke: ${t.negative}; stroke-width: 1.5; }
39740
+ .sx-pb-error-title { font: 700 13px ui-monospace, Menlo, monospace; fill: ${t.negative}; }
39741
+ .sx-pb-error-line { font: 12px ui-monospace, Menlo, monospace; fill: ${t.negative}; }
39742
+ `.trim();
39743
+ }
39744
+ function renderErrorPanel2(lay, t) {
39745
+ const lines = lay.errors;
39746
+ const w = Math.max(520, ...lines.map((l) => l.length * 6.6 + 48));
39747
+ const h = 56 + lines.length * 19;
39748
+ return svgRoot(
39749
+ { viewBox: `0 0 ${r28(w)} ${h}`, width: r28(w), height: h, class: "sx-pb", role: "img" },
39750
+ [
39751
+ title(lay.title),
39752
+ desc(`Playbook validation failed with ${lines.length} error${lines.length === 1 ? "" : "s"}.`),
39753
+ el("style", {}, buildCss14(t)),
39754
+ rect({ class: "sx-pb-error-box", x: 1, y: 1, width: r28(w - 2), height: h - 2, rx: 6 }),
39755
+ text({ class: "sx-pb-error-title", x: 16, y: 26 }, `playbook: ${lines.length} validation error${lines.length === 1 ? "" : "s"}`),
39756
+ ...lines.map((e, i) => text({ class: "sx-pb-error-line", x: 16, y: 50 + i * 19 }, `\u26A0 ${e}`))
39757
+ ]
39758
+ );
39759
+ }
39760
+ function wavyPath(pts, amp = 3.4, wl = 11) {
39761
+ const flat = [];
39762
+ let acc = 0;
39763
+ for (let i = 1; i < pts.length; i++) {
39764
+ const a = pts[i - 1], b = pts[i];
39765
+ const dx = b.x - a.x, dy = b.y - a.y;
39766
+ const segLen = Math.hypot(dx, dy) || 1;
39767
+ const ux = dx / segLen, uy = dy / segLen;
39768
+ const nx = -uy, ny = ux;
39769
+ const lastSeg = i === pts.length - 1;
39770
+ const straightTail = lastSeg ? 9 : 0;
39771
+ const usable = Math.max(0, segLen - straightTail);
39772
+ const steps = Math.max(2, Math.round(usable / 2));
39773
+ for (let s = 0; s <= steps; s++) {
39774
+ const d = usable * s / steps;
39775
+ const off = amp * Math.sin(2 * Math.PI * (acc + d) / wl);
39776
+ flat.push({ x: a.x + ux * d + nx * off, y: a.y + uy * d + ny * off });
39777
+ }
39778
+ acc += usable;
39779
+ if (lastSeg) flat.push({ x: b.x, y: b.y });
39780
+ }
39781
+ return flat.map((p, i) => `${i === 0 ? "M" : "L"} ${r28(p.x)} ${r28(p.y)}`).join(" ");
39782
+ }
39783
+ function arrowHead4(pts, cls) {
39784
+ if (pts.length < 2) return "";
39785
+ const a = pts[pts.length - 2], b = pts[pts.length - 1];
39786
+ const ang = Math.atan2(b.y - a.y, b.x - a.x);
39787
+ const size = 8, wing = 0.42;
39788
+ const p2 = `${r28(b.x - size * Math.cos(ang - wing))},${r28(b.y - size * Math.sin(ang - wing))}`;
39789
+ const p3 = `${r28(b.x - size * Math.cos(ang + wing))},${r28(b.y - size * Math.sin(ang + wing))}`;
39790
+ return polygon({ class: cls, points: `${r28(b.x)},${r28(b.y)} ${p2} ${p3}` });
39791
+ }
39792
+ function teeBar(pts, cls) {
39793
+ if (pts.length < 2) return "";
39794
+ const a = pts[pts.length - 2], b = pts[pts.length - 1];
39795
+ const ang = Math.atan2(b.y - a.y, b.x - a.x) + Math.PI / 2;
39796
+ const half = 7;
39797
+ return line({ class: cls, x1: r28(b.x - half * Math.cos(ang)), y1: r28(b.y - half * Math.sin(ang)), x2: r28(b.x + half * Math.cos(ang)), y2: r28(b.y + half * Math.sin(ang)) });
39798
+ }
39799
+ function renderMove(mv, ctx) {
39800
+ const pts = mv.points.map((p) => ({ x: ctx.X(p.x), y: ctx.Y(p.y) }));
39801
+ const strokeCls = mv.kind === "motion" ? "sx-pb-motion" : mv.kind === "shot" ? "sx-pb-shot" : mv.style === "dashed" ? "sx-pb-move-dash" : "sx-pb-move";
39802
+ const headCls = mv.kind === "motion" ? "sx-pb-motion-fill" : mv.kind === "shot" ? "sx-pb-shot-fill" : "sx-pb-move-fill";
39803
+ const parts = [];
39804
+ if (mv.style === "wavy") {
39805
+ parts.push(path({ class: strokeCls, d: wavyPath(pts) }));
39806
+ } else {
39807
+ parts.push(path({ class: strokeCls, d: pts.map((p, i) => `${i === 0 ? "M" : "L"} ${r28(p.x)} ${r28(p.y)}`).join(" ") }));
39808
+ }
39809
+ if (mv.end === "arrow") parts.push(arrowHead4(pts, headCls));
39810
+ else if (mv.end === "tee") parts.push(teeBar(pts, strokeCls));
39811
+ return group({ class: "sx-pb-move-g", "data-kind": mv.kind, "data-player": mv.player }, parts);
39812
+ }
39813
+ function playerSymbol(p, ctx) {
39814
+ const cx = ctx.X(p.x), cy = ctx.Y(p.y), r6 = 10;
39815
+ const parts = [];
39816
+ if (p.side === "defense" || p.pos === "x") {
39817
+ const k = r6 * 0.78;
39818
+ parts.push(line({ class: "sx-pb-x", x1: r28(cx - k), y1: r28(cy - k), x2: r28(cx + k), y2: r28(cy + k) }));
39819
+ parts.push(line({ class: "sx-pb-x", x1: r28(cx - k), y1: r28(cy + k), x2: r28(cx + k), y2: r28(cy - k) }));
39820
+ if (p.label) parts.push(text({ class: "sx-pb-x-text", x: r28(cx + k + 5), y: r28(cy - k + 2), "text-anchor": "middle" }, p.label));
39821
+ } else if (p.pos === "gk") {
39822
+ const h = r6 * 1.15;
39823
+ parts.push(polygon({ class: "sx-pb-gk", points: `${r28(cx)},${r28(cy - h)} ${r28(cx + h)},${r28(cy + h * 0.8)} ${r28(cx - h)},${r28(cy + h * 0.8)}` }));
39824
+ parts.push(text({ class: "sx-pb-o-text", x: r28(cx), y: r28(cy + 6), "text-anchor": "middle" }, p.label));
39825
+ } else if (p.pos === "c") {
39826
+ parts.push(rect({ class: "sx-pb-o", x: r28(cx - r6 * 0.82), y: r28(cy - r6 * 0.82), width: r28(r6 * 1.64), height: r28(r6 * 1.64) }));
39827
+ parts.push(text({ class: "sx-pb-o-text", x: r28(cx), y: r28(cy + 3.6), "text-anchor": "middle" }, p.label));
39828
+ } else {
39829
+ parts.push(circle({ class: "sx-pb-o", cx: r28(cx), cy: r28(cy), r: r6 }));
39830
+ parts.push(text({ class: "sx-pb-o-text", x: r28(cx), y: r28(cy + 3.6), "text-anchor": "middle" }, p.label));
39831
+ }
39832
+ return group({ class: "sx-pb-player", "data-side": p.side, "data-id": p.id }, parts);
39833
+ }
39834
+ function legendSwatch(kind, sport) {
39835
+ const moveCls = (dashed) => dashed ? "sx-pb-move-dash" : "sx-pb-move";
39836
+ const arrow2 = polygon({ class: "sx-pb-move-fill", points: "20,0 13,-3.5 13,3.5" });
39837
+ switch (kind) {
39838
+ case "offense":
39839
+ return circle({ class: "sx-pb-o", cx: 8, cy: 0, r: 6 });
39840
+ case "gk":
39841
+ return polygon({ class: "sx-pb-gk", points: "8,-7 15,5 1,5" });
39842
+ case "defense":
39843
+ return `${line({ class: "sx-pb-x", x1: 3, y1: -5, x2: 13, y2: 5 })}${line({ class: "sx-pb-x", x1: 3, y1: 5, x2: 13, y2: -5 })}`;
39844
+ case "run":
39845
+ return `${line({ class: moveCls(sport === "soccer"), x1: 0, y1: 0, x2: 14, y2: 0 })}${arrow2}`;
39846
+ case "pass":
39847
+ return `${line({ class: moveCls(sport !== "soccer"), x1: 0, y1: 0, x2: 14, y2: 0 })}${arrow2}`;
39848
+ case "dribble":
39849
+ return `${path({ class: "sx-pb-move", d: "M0,0 Q3,-4 6,0 T12,0" })}${polygon({ class: "sx-pb-move-fill", points: "20,0 13,-3.5 13,3.5" })}`;
39850
+ case "screen":
39851
+ return `${line({ class: "sx-pb-move", x1: 0, y1: 0, x2: 16, y2: 0 })}${line({ class: "sx-pb-move", x1: 16, y1: -5, x2: 16, y2: 5 })}`;
39852
+ case "shot":
39853
+ return `${line({ class: "sx-pb-shot", x1: 0, y1: 0, x2: 14, y2: 0 })}${polygon({ class: "sx-pb-shot-fill", points: "20,0 13,-3.5 13,3.5" })}`;
39854
+ case "motion":
39855
+ return `${line({ class: "sx-pb-motion", x1: 0, y1: 0, x2: 14, y2: 0 })}${polygon({ class: "sx-pb-motion-fill", points: "20,0 13,-3.5 13,3.5" })}`;
39856
+ case "zone":
39857
+ return rect({ class: "sx-pb-zone", x: 0, y: -6, width: 18, height: 12, rx: 6 });
39858
+ default:
39859
+ return "";
39860
+ }
39861
+ }
39862
+ function renderLegend4(items, y, sport) {
39863
+ const parts = [];
39864
+ let cx = 12;
39865
+ for (const it of items) {
39866
+ parts.push(group({ transform: `translate(${r28(cx)},${r28(y)})` }, [legendSwatch(it.kind, sport)]));
39867
+ parts.push(text({ class: "sx-pb-legend", x: r28(cx + 26), y: r28(y + 4) }, it.label));
39868
+ cx += 26 + it.label.length * 6.5 + 20;
39869
+ }
39870
+ return group({ class: "sx-pb-legend-g" }, parts);
39871
+ }
39872
+ function ordinal(n) {
39873
+ return n === 1 ? "1st" : n === 2 ? "2nd" : n === 3 ? "3rd" : n === 4 ? "4th" : `${n}th`;
39874
+ }
39875
+ function renderPlaybookLayout(lay, config) {
39876
+ let themeName = config?.theme ?? "default";
39877
+ if (lay.sport === "soccer" && themeName === "dark") themeName = "default";
39878
+ const t = resolvePlaybookTheme(themeName);
39879
+ if (lay.errors.length > 0) return renderErrorPanel2(lay, t);
39880
+ const mod = sportModule(lay.sport);
39881
+ const scale = mod.scale;
39882
+ const b = lay.bounds;
39883
+ const titleH = TITLE.bandH;
39884
+ const annoH = lay.sport === "football" && (lay.down || lay.distance || lay.losYard !== void 0) ? 20 : 0;
39885
+ const topH = titleH + annoH;
39886
+ const legendH = 30;
39887
+ const EDGE = 16;
39888
+ const fieldW = (b.maxX - b.minX) * scale;
39889
+ const fieldH = (b.maxY - b.minY) * scale;
39890
+ const W2 = r28(fieldW + EDGE * 2);
39891
+ const fieldTop = topH + EDGE;
39892
+ const H2 = r28(fieldH + topH + EDGE * 2 + legendH);
39893
+ const X = (u) => r28((u - b.minX) * scale + EDGE);
39894
+ const Y = (v) => r28(mod.yUp ? (b.maxY - v) * scale + fieldTop : (v - b.minY) * scale + fieldTop);
39895
+ const px = (u) => r28(u * scale);
39896
+ const ctx = { X, Y, px };
39897
+ const isCourt = lay.sport === "basketball";
39898
+ const surfaceCls = isCourt ? "sx-pb-court" : lay.sport === "soccer" ? "sx-pb-turf" : "sx-pb-field";
39899
+ const surroundCls = isCourt ? "sx-pb-surround-court" : "sx-pb-surround";
39900
+ const boundaryCls = isCourt ? "sx-pb-boundary-court" : "sx-pb-boundary";
39901
+ const fieldRx = 7;
39902
+ const surround = rect({ class: surroundCls, x: 2, y: r28(topH), width: r28(W2 - 4), height: r28(fieldH + EDGE * 2), rx: 12 });
39903
+ const surfaceBase = rect({ class: surfaceCls, x: EDGE, y: r28(fieldTop), width: r28(fieldW), height: r28(fieldH), rx: fieldRx });
39904
+ const boundary = rect({ class: boundaryCls, x: EDGE, y: r28(fieldTop), width: r28(fieldW), height: r28(fieldH), rx: fieldRx });
39905
+ const clipId = "sx-pb-clip";
39906
+ const clip6 = el("clipPath", { id: clipId }, [rect({ x: EDGE, y: r28(fieldTop), width: r28(fieldW), height: r28(fieldH), rx: fieldRx })]);
39907
+ const field = group({ "clip-path": `url(#${clipId})` }, [mod.drawField(lay, ctx, t)]);
39908
+ const zones = [];
39909
+ for (const z of lay.zones) {
39910
+ zones.push(el("ellipse", { class: "sx-pb-zone", cx: X(z.x), cy: Y(z.y), rx: px(z.rx), ry: px(z.ry) }));
39911
+ if (z.label) zones.push(text({ class: "sx-pb-zone-text", x: X(z.x), y: r28(Y(z.y) - px(z.ry) + (mod.yUp ? 11 : -4)), "text-anchor": "middle" }, z.label));
39912
+ }
39913
+ const moves = lay.moves.map((m) => renderMove(m, ctx));
39914
+ const players = lay.players.map((p) => playerSymbol(p, ctx));
39915
+ const ball = lay.sport === "football" ? el("ellipse", { class: "sx-pb-ball", cx: X(0), cy: Y(0), rx: 4.5, ry: 2.7 }) : "";
39916
+ const annoParts = [];
39917
+ if (annoH) {
39918
+ const bits = [];
39919
+ if (lay.down) bits.push(ordinal(lay.down) + (lay.distance ? ` & ${lay.distance}` : ""));
39920
+ else if (lay.distance) bits.push(`${lay.distance} to go`);
39921
+ if (lay.losYard !== void 0) bits.push(`ball on ${lay.losYard}`);
39922
+ annoParts.push(text({ class: "sx-pb-anno", x: 8, y: titleH + 14 }, bits.join(" \xB7 ")));
39923
+ }
39924
+ const nOff = lay.players.filter((p) => p.side === "offense").length;
39925
+ const nDef = lay.players.filter((p) => p.side === "defense").length;
39926
+ const descText = `${lay.sport} play. ${nOff} ${lay.sport === "football" ? "offensive players" : "players"}, ${nDef} ${lay.sport === "football" ? "defenders" : "opponents"}, ${lay.moves.length} assignment${lay.moves.length === 1 ? "" : "s"}.` + (lay.warnings.length ? ` Warnings: ${lay.warnings.join("; ")}.` : "");
39927
+ return svgRoot(
39928
+ { viewBox: `0 0 ${W2} ${H2}`, width: W2, height: H2, class: "sx-pb", role: "img" },
39929
+ [
39930
+ title(lay.title),
39931
+ desc(descText),
39932
+ el("style", {}, buildCss14(t)),
39933
+ rect({ fill: t.bg, x: 0, y: 0, width: W2, height: H2 }),
39934
+ el("defs", {}, [clip6]),
39935
+ text({ class: "sx-pb-title", x: r28(W2 / 2), y: TITLE.y, "text-anchor": "middle" }, lay.title),
39936
+ ...annoParts,
39937
+ surround,
39938
+ surfaceBase,
39939
+ field,
39940
+ group({ class: "sx-pb-zones", "clip-path": `url(#${clipId})` }, zones),
39941
+ group({ class: "sx-pb-moves" }, moves),
39942
+ boundary,
39943
+ group({ class: "sx-pb-players" }, players),
39944
+ ball,
39945
+ renderLegend4(mod.legend(lay), fieldH + topH + EDGE * 2 + 18, lay.sport)
39946
+ ]
39947
+ );
39948
+ }
39949
+ function renderPlaybook(text2, config) {
39950
+ return renderPlaybookLayout(layoutPlaybook(parsePlaybook(text2)), config);
39951
+ }
39952
+
39953
+ // src/diagrams/playbook/index.ts
39954
+ var playbook = {
39955
+ type: "playbook",
39956
+ detect(text2) {
39957
+ for (const raw of text2.split(/\r?\n/)) {
39958
+ const t = raw.trim();
39959
+ if (!t) continue;
39960
+ if (t.startsWith("#") || t.startsWith("//")) continue;
39961
+ return /^playbook\b/i.test(t);
39962
+ }
39963
+ return false;
39964
+ },
39965
+ parse: parsePlaybook,
39966
+ render(text2, config) {
39967
+ return renderPlaybook(text2, config);
39968
+ },
39969
+ lint(text2) {
39970
+ try {
39971
+ const lay = layoutPlaybook(parsePlaybook(text2));
39972
+ return [
39973
+ ...lay.errors.map(
39974
+ (message) => ({
39975
+ severity: "error",
39976
+ code: "playbook/validation",
39977
+ message,
39978
+ fatal: false
39979
+ })
39980
+ ),
39981
+ ...lay.warnings.map(
39982
+ (message) => ({
39983
+ severity: "warning",
39984
+ code: "playbook/warning",
39985
+ message,
39986
+ fatal: false
39987
+ })
39988
+ )
39989
+ ];
39990
+ } catch {
39991
+ return [];
39992
+ }
39993
+ }
39994
+ };
39995
+
35974
39996
  // src/core/api.ts
35975
39997
  var plugins = [
35976
39998
  genogram,
@@ -36017,7 +40039,9 @@ var plugins = [
36017
40039
  epc,
36018
40040
  idef0,
36019
40041
  threatmodel,
36020
- welding
40042
+ welding,
40043
+ floorplan,
40044
+ playbook
36021
40045
  ];
36022
40046
  function detectPlugin(text2, config) {
36023
40047
  if (config?.type) {
@@ -36028,7 +40052,7 @@ function detectPlugin(text2, config) {
36028
40052
  if (plugin.detect(text2)) return plugin;
36029
40053
  }
36030
40054
  throw new Error(
36031
- "Cannot detect diagram type. Start your text with 'genogram', 'ecomap', 'pedigree', 'phylo', 'sociogram', 'timing', 'logic', 'circuit', 'blockdiagram', 'ladder', 'sld', 'entity-structure', 'fishbone', 'venn', 'flowchart', 'mindmap', 'matrix', 'orgchart', 'state', 'pid', 'erd', 'breadboard', 'bpmn', 'fbd', 'sfc', 'prisma', 'usecase', 'pert', 'sequence', 'petri', 'network', 'umlclass', 'faulttree', or 'bowtie'."
40055
+ "Cannot detect diagram type. Start your text with 'genogram', 'ecomap', 'pedigree', 'phylo', 'sociogram', 'timing', 'logic', 'circuit', 'blockdiagram', 'ladder', 'sld', 'entity-structure', 'fishbone', 'venn', 'flowchart', 'mindmap', 'matrix', 'orgchart', 'state', 'pid', 'erd', 'breadboard', 'bpmn', 'fbd', 'sfc', 'prisma', 'usecase', 'pert', 'sequence', 'petri', 'network', 'umlclass', 'faulttree', 'bowtie', 'floorplan', or 'playbook'."
36032
40056
  );
36033
40057
  }
36034
40058
  function stripCodeFences(text2) {
@@ -36165,6 +40189,6 @@ function renderWithPlugin(prepared, plugin, config) {
36165
40189
  return plugin.render(prepared, renderConfig);
36166
40190
  }
36167
40191
 
36168
- export { GEOMETRY, bowtie2 as bowtie, causalloop, decisiontree, drawDeviceIcon, epc, eventtree, faulttree, fmea, gitgraph, iconSize, idef0, markov, network, parse, parseResult, pert, petri, pid, prisma, render, renderEquip, renderPreview, renderResult, sequence, state, threatmodel, timeline, umlclass, usecase, welding };
36169
- //# sourceMappingURL=chunk-TP7LQ5PF.js.map
36170
- //# sourceMappingURL=chunk-TP7LQ5PF.js.map
40192
+ export { FLOORPLAN_SYMBOLS, GEOMETRY, bowtie2 as bowtie, causalloop, decisiontree, drawDeviceIcon, epc, eventtree, faulttree, fmea, gitgraph, iconSize, idef0, markov, network, parse, parseResult, pert, petri, pid, prisma, render, renderEquip, renderPreview, renderResult, sequence, state, threatmodel, timeline, umlclass, usecase, welding };
40193
+ //# sourceMappingURL=chunk-UFXDAIDD.js.map
40194
+ //# sourceMappingURL=chunk-UFXDAIDD.js.map