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.
- package/README.md +42 -2
- package/README.zh-CN.md +2 -2
- package/dist/ai/ai-sdk.cjs +23 -23
- package/dist/ai/ai-sdk.d.cts +3 -3
- package/dist/ai/ai-sdk.d.ts +3 -3
- package/dist/ai/ai-sdk.js +18 -18
- package/dist/ai/index.cjs +32 -32
- package/dist/ai/index.d.cts +4 -4
- package/dist/ai/index.d.ts +4 -4
- package/dist/ai/index.js +19 -19
- package/dist/{api-OED2jUVj.d.ts → api-1xoGiseb.d.cts} +2 -2
- package/dist/{api-BDMaX1cT.d.cts → api-CgXRSnCn.d.ts} +2 -2
- package/dist/browser.cjs +24 -24
- package/dist/browser.d.cts +3 -3
- package/dist/browser.d.ts +3 -3
- package/dist/browser.js +18 -18
- package/dist/{chunk-HILYXWGJ.cjs → chunk-35NGXDT2.cjs} +12 -12
- package/dist/{chunk-HILYXWGJ.cjs.map → chunk-35NGXDT2.cjs.map} +1 -1
- package/dist/{chunk-HJA7MIMV.js → chunk-3J4DZPZC.js} +3 -3
- package/dist/{chunk-HJA7MIMV.js.map → chunk-3J4DZPZC.js.map} +1 -1
- package/dist/{chunk-IQLZUCWI.js → chunk-3PH2MQGN.js} +3 -3
- package/dist/{chunk-IQLZUCWI.js.map → chunk-3PH2MQGN.js.map} +1 -1
- package/dist/{chunk-VZVREOTM.js → chunk-3WX24RCH.js} +3 -3
- package/dist/{chunk-VZVREOTM.js.map → chunk-3WX24RCH.js.map} +1 -1
- package/dist/{chunk-XA6XIAMN.cjs → chunk-4AC6I7KJ.cjs} +4 -4
- package/dist/{chunk-XA6XIAMN.cjs.map → chunk-4AC6I7KJ.cjs.map} +1 -1
- package/dist/{chunk-TYEQC7PV.js → chunk-4MRVJI7G.js} +166 -5
- package/dist/chunk-4MRVJI7G.js.map +1 -0
- package/dist/{chunk-6BJKQULN.cjs → chunk-4OC3CTGE.cjs} +4 -4
- package/dist/{chunk-6BJKQULN.cjs.map → chunk-4OC3CTGE.cjs.map} +1 -1
- package/dist/{chunk-4MGALM2C.cjs → chunk-5ZQRHDMQ.cjs} +4 -4
- package/dist/{chunk-4MGALM2C.cjs.map → chunk-5ZQRHDMQ.cjs.map} +1 -1
- package/dist/{chunk-PSQGLE6U.cjs → chunk-627GHE2N.cjs} +5 -5
- package/dist/{chunk-PSQGLE6U.cjs.map → chunk-627GHE2N.cjs.map} +1 -1
- package/dist/{chunk-56DSMSXK.js → chunk-64LABNTF.js} +3 -3
- package/dist/{chunk-56DSMSXK.js.map → chunk-64LABNTF.js.map} +1 -1
- package/dist/{chunk-GYTU7L4L.cjs → chunk-6QZQTASC.cjs} +12 -12
- package/dist/{chunk-GYTU7L4L.cjs.map → chunk-6QZQTASC.cjs.map} +1 -1
- package/dist/{chunk-MYVY55DO.cjs → chunk-6ZD7TCWO.cjs} +15 -15
- package/dist/{chunk-MYVY55DO.cjs.map → chunk-6ZD7TCWO.cjs.map} +1 -1
- package/dist/{chunk-7Z2GDS4G.js → chunk-AXMBXAEA.js} +3 -3
- package/dist/{chunk-7Z2GDS4G.js.map → chunk-AXMBXAEA.js.map} +1 -1
- package/dist/{chunk-VY4JOTM2.js → chunk-B4CMWA6Y.js} +3 -3
- package/dist/{chunk-VY4JOTM2.js.map → chunk-B4CMWA6Y.js.map} +1 -1
- package/dist/{chunk-2ASZMLC3.cjs → chunk-C4Y24X3U.cjs} +4 -4
- package/dist/{chunk-2ASZMLC3.cjs.map → chunk-C4Y24X3U.cjs.map} +1 -1
- package/dist/{chunk-NT6VVMLW.cjs → chunk-ENUM7GMZ.cjs} +168 -4
- package/dist/chunk-ENUM7GMZ.cjs.map +1 -0
- package/dist/{chunk-VYAUTNHC.cjs → chunk-GAQ36VFD.cjs} +4 -4
- package/dist/{chunk-VYAUTNHC.cjs.map → chunk-GAQ36VFD.cjs.map} +1 -1
- package/dist/{chunk-NTQBNBBP.cjs → chunk-GTCAJPQR.cjs} +4307 -282
- package/dist/chunk-GTCAJPQR.cjs.map +1 -0
- package/dist/{chunk-KR7PPCC4.js → chunk-GYYYULBL.js} +3 -3
- package/dist/{chunk-KR7PPCC4.js.map → chunk-GYYYULBL.js.map} +1 -1
- package/dist/{chunk-WFVRUUJW.cjs → chunk-INVLJYAE.cjs} +4 -4
- package/dist/{chunk-WFVRUUJW.cjs.map → chunk-INVLJYAE.cjs.map} +1 -1
- package/dist/{chunk-2A5HJFOX.cjs → chunk-ITI3STJ6.cjs} +4 -4
- package/dist/{chunk-2A5HJFOX.cjs.map → chunk-ITI3STJ6.cjs.map} +1 -1
- package/dist/{chunk-TMAOPJCT.js → chunk-JEMAOC2D.js} +3 -3
- package/dist/{chunk-TMAOPJCT.js.map → chunk-JEMAOC2D.js.map} +1 -1
- package/dist/{chunk-2RFFCZ3I.js → chunk-M26ORU4P.js} +3 -3
- package/dist/{chunk-2RFFCZ3I.js.map → chunk-M26ORU4P.js.map} +1 -1
- package/dist/{chunk-CURAZVOH.js → chunk-NIYB6CHH.js} +728 -4
- package/dist/chunk-NIYB6CHH.js.map +1 -0
- package/dist/{chunk-JR6JNQEZ.js → chunk-NKYR4PAS.js} +3 -3
- package/dist/{chunk-JR6JNQEZ.js.map → chunk-NKYR4PAS.js.map} +1 -1
- package/dist/{chunk-MUE45N2A.cjs → chunk-OJ3P4IC4.cjs} +4 -4
- package/dist/{chunk-MUE45N2A.cjs.map → chunk-OJ3P4IC4.cjs.map} +1 -1
- package/dist/{chunk-VMUNKEB2.js → chunk-PFZKW3HE.js} +3 -3
- package/dist/{chunk-VMUNKEB2.js.map → chunk-PFZKW3HE.js.map} +1 -1
- package/dist/{chunk-MERU76LW.js → chunk-RDYACU2G.js} +3 -3
- package/dist/{chunk-MERU76LW.js.map → chunk-RDYACU2G.js.map} +1 -1
- package/dist/{chunk-TWTUIUNK.js → chunk-SXOAAQNY.js} +3 -3
- package/dist/{chunk-TWTUIUNK.js.map → chunk-SXOAAQNY.js.map} +1 -1
- package/dist/{chunk-3DSNGR26.cjs → chunk-TX3YWZZX.cjs} +730 -6
- package/dist/chunk-TX3YWZZX.cjs.map +1 -0
- package/dist/{chunk-TP7LQ5PF.js → chunk-UFXDAIDD.js} +4126 -102
- package/dist/chunk-UFXDAIDD.js.map +1 -0
- package/dist/{chunk-7JJQEECD.cjs → chunk-VY6UZYYL.cjs} +15 -15
- package/dist/{chunk-7JJQEECD.cjs.map → chunk-VY6UZYYL.cjs.map} +1 -1
- package/dist/{chunk-V3JTVTPY.cjs → chunk-VYQXB2RC.cjs} +12 -12
- package/dist/{chunk-V3JTVTPY.cjs.map → chunk-VYQXB2RC.cjs.map} +1 -1
- package/dist/{chunk-L2NY4XEY.cjs → chunk-WJXLF42K.cjs} +4 -4
- package/dist/{chunk-L2NY4XEY.cjs.map → chunk-WJXLF42K.cjs.map} +1 -1
- package/dist/{chunk-VWIKDJNV.js → chunk-XCCXG6RR.js} +3 -3
- package/dist/{chunk-VWIKDJNV.js.map → chunk-XCCXG6RR.js.map} +1 -1
- package/dist/{chunk-7SVB3XA7.js → chunk-ZCHGIWJK.js} +3 -3
- package/dist/{chunk-7SVB3XA7.js.map → chunk-ZCHGIWJK.js.map} +1 -1
- package/dist/{diagnostics-hObcaaFC.d.ts → diagnostics-CDwnQ65n.d.cts} +1 -1
- package/dist/{diagnostics-hObcaaFC.d.cts → diagnostics-CDwnQ65n.d.ts} +1 -1
- package/dist/diagrams/blockdiagram/index.cjs +6 -6
- package/dist/diagrams/blockdiagram/index.d.cts +1 -1
- package/dist/diagrams/blockdiagram/index.d.ts +1 -1
- package/dist/diagrams/blockdiagram/index.js +2 -2
- package/dist/diagrams/circuit/index.cjs +9 -9
- package/dist/diagrams/circuit/index.d.cts +1 -1
- package/dist/diagrams/circuit/index.d.ts +1 -1
- package/dist/diagrams/circuit/index.js +2 -2
- package/dist/diagrams/ecomap/index.cjs +7 -7
- package/dist/diagrams/ecomap/index.d.cts +1 -1
- package/dist/diagrams/ecomap/index.d.ts +1 -1
- package/dist/diagrams/ecomap/index.js +2 -2
- package/dist/diagrams/entity/index.cjs +6 -6
- package/dist/diagrams/entity/index.d.cts +1 -1
- package/dist/diagrams/entity/index.d.ts +1 -1
- package/dist/diagrams/entity/index.js +2 -2
- package/dist/diagrams/fishbone/index.cjs +8 -8
- package/dist/diagrams/fishbone/index.d.cts +1 -1
- package/dist/diagrams/fishbone/index.d.ts +1 -1
- package/dist/diagrams/fishbone/index.js +2 -2
- package/dist/diagrams/flowchart/index.cjs +8 -8
- package/dist/diagrams/flowchart/index.d.cts +2 -2
- package/dist/diagrams/flowchart/index.d.ts +2 -2
- package/dist/diagrams/flowchart/index.js +2 -2
- package/dist/diagrams/genogram/index.cjs +9 -9
- package/dist/diagrams/genogram/index.d.cts +1 -1
- package/dist/diagrams/genogram/index.d.ts +1 -1
- package/dist/diagrams/genogram/index.js +2 -2
- package/dist/diagrams/ladder/index.cjs +6 -6
- package/dist/diagrams/ladder/index.d.cts +1 -1
- package/dist/diagrams/ladder/index.d.ts +1 -1
- package/dist/diagrams/ladder/index.js +2 -2
- package/dist/diagrams/logic/index.cjs +8 -8
- package/dist/diagrams/logic/index.d.cts +1 -1
- package/dist/diagrams/logic/index.d.ts +1 -1
- package/dist/diagrams/logic/index.js +2 -2
- package/dist/diagrams/orgchart/index.cjs +8 -8
- package/dist/diagrams/orgchart/index.d.cts +1 -1
- package/dist/diagrams/orgchart/index.d.ts +1 -1
- package/dist/diagrams/orgchart/index.js +2 -2
- package/dist/diagrams/pedigree/index.cjs +7 -7
- package/dist/diagrams/pedigree/index.d.cts +1 -1
- package/dist/diagrams/pedigree/index.d.ts +1 -1
- package/dist/diagrams/pedigree/index.js +2 -2
- package/dist/diagrams/phylo/index.cjs +7 -7
- package/dist/diagrams/phylo/index.d.cts +1 -1
- package/dist/diagrams/phylo/index.d.ts +1 -1
- package/dist/diagrams/phylo/index.js +2 -2
- package/dist/diagrams/sld/index.cjs +8 -8
- package/dist/diagrams/sld/index.d.cts +1 -1
- package/dist/diagrams/sld/index.d.ts +1 -1
- package/dist/diagrams/sld/index.js +2 -2
- package/dist/diagrams/sociogram/index.cjs +6 -6
- package/dist/diagrams/sociogram/index.d.cts +1 -1
- package/dist/diagrams/sociogram/index.d.ts +1 -1
- package/dist/diagrams/sociogram/index.js +2 -2
- package/dist/diagrams/timing/index.d.cts +1 -1
- package/dist/diagrams/timing/index.d.ts +1 -1
- package/dist/diagrams/venn/index.cjs +9 -9
- package/dist/diagrams/venn/index.d.cts +1 -1
- package/dist/diagrams/venn/index.d.ts +1 -1
- package/dist/diagrams/venn/index.js +2 -2
- package/dist/{index-Cq8y1aaa.d.ts → index-CYSH3_ca.d.ts} +1 -1
- package/dist/{index-BiXWjQht.d.cts → index-DNOoLmYX.d.cts} +1 -1
- package/dist/index.cjs +111 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +48 -22
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +18 -18
- package/dist/react.d.cts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +17 -17
- package/dist/{tools-BbTuTWs_.d.ts → tools-BhTti-oG.d.ts} +3 -3
- package/dist/{tools-D5dkAqNy.d.cts → tools-CbYmQ3hH.d.cts} +3 -3
- package/package.json +1 -1
- package/dist/chunk-3DSNGR26.cjs.map +0 -1
- package/dist/chunk-CURAZVOH.js.map +0 -1
- package/dist/chunk-NT6VVMLW.cjs.map +0 -1
- package/dist/chunk-NTQBNBBP.cjs.map +0 -1
- package/dist/chunk-TP7LQ5PF.js.map +0 -1
- package/dist/chunk-TYEQC7PV.js.map +0 -1
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { orgchart } from './chunk-
|
|
2
|
-
import { circuit } from './chunk-
|
|
3
|
-
import { blockdiagram } from './chunk-
|
|
4
|
-
import { ladder } from './chunk-
|
|
5
|
-
import { sld } from './chunk-
|
|
6
|
-
import { entity } from './chunk-
|
|
7
|
-
import { fishbone } from './chunk-
|
|
8
|
-
import { venn } from './chunk-
|
|
9
|
-
import { flowchart, layoutFlowchart } from './chunk-
|
|
10
|
-
import { genogram } from './chunk-
|
|
11
|
-
import { ecomap, estimateTextWidth } from './chunk-
|
|
12
|
-
import { pedigree } from './chunk-
|
|
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-
|
|
15
|
-
import { sociogram } from './chunk-
|
|
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-
|
|
18
|
-
import { resolveBaseTheme, resolveTimelineTheme, resolveStateTheme, cssCustomProperties, resolvePetriTheme, resolveNetworkTheme, resolveUmlClassTheme, DEFAULT_FONT_FAMILY, FONT_SIZE, resolveReliabilityTheme, STROKE_WIDTH, resolveBowtieTheme, resolveMindmapTheme, resolveMatrixTheme, resolveBpmnTheme } from './chunk-
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
1988
|
+
function parseRowKey(raw, ordinal2) {
|
|
1989
1989
|
const parsed = tryParseDate(raw);
|
|
1990
1990
|
if (parsed) return parsed;
|
|
1991
|
-
|
|
1991
|
+
ordinal2.index += 1;
|
|
1992
1992
|
return {
|
|
1993
|
-
value:
|
|
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,
|
|
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,
|
|
2048
|
-
end: end ? parseRowKey(end,
|
|
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
|
|
2287
|
-
const collide = labelBoxes.some((b) => Math.abs(b.y -
|
|
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(
|
|
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
|
|
2452
|
-
const collide = pinBoxes.some((b) => Math.abs(b.y -
|
|
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(
|
|
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(
|
|
7457
|
-
if (
|
|
7458
|
-
const base =
|
|
7459
|
-
return `${base} prisma-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(
|
|
7543
|
+
function renderBox(box2) {
|
|
7544
7544
|
const r6 = rect({
|
|
7545
|
-
x:
|
|
7546
|
-
y:
|
|
7547
|
-
width:
|
|
7548
|
-
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(
|
|
7552
|
-
"data-role":
|
|
7551
|
+
class: classForBox(box2),
|
|
7552
|
+
"data-role": box2.role
|
|
7553
7553
|
});
|
|
7554
|
-
const innerLeft =
|
|
7555
|
-
const innerRight =
|
|
7556
|
-
const centerX =
|
|
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
|
|
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 =
|
|
7568
|
+
let textY = box2.y + PRISMA_CONST.BOX_PAD_Y;
|
|
7569
7569
|
const textEls = [];
|
|
7570
|
-
for (const line2 of
|
|
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":
|
|
7599
|
-
"data-prisma-variant":
|
|
7600
|
-
"data-prisma-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
|
|
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(
|
|
8542
|
-
actorById2.set(a.id,
|
|
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
|
|
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(
|
|
10141
|
-
boxById.set(id,
|
|
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
|
|
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(
|
|
10360
|
-
boxById.set(id,
|
|
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
|
|
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(
|
|
10442
|
-
boxById.set(t.id,
|
|
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(
|
|
13868
|
-
const labelW = Math.max(
|
|
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:
|
|
13872
|
-
top:
|
|
13873
|
-
right:
|
|
13874
|
-
bottom:
|
|
13875
|
-
};
|
|
13876
|
-
}
|
|
13877
|
-
function edgePoint(
|
|
13878
|
-
const dx = tx -
|
|
13879
|
-
const dy = ty -
|
|
13880
|
-
if (dx === 0 && dy === 0) return { x:
|
|
13881
|
-
const hw =
|
|
13882
|
-
const hh =
|
|
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:
|
|
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
|
|
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
|
|
17640
|
+
for (const box2 of layout.cutSetBoxes) {
|
|
17641
17641
|
inner.push(
|
|
17642
17642
|
rect({
|
|
17643
|
-
x:
|
|
17644
|
-
y:
|
|
17645
|
-
width:
|
|
17646
|
-
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":
|
|
17650
|
-
"data-cutset-index": String(
|
|
17651
|
-
"data-order": String(
|
|
17652
|
-
...
|
|
17653
|
-
...
|
|
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
|
|
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)
|
|
21746
|
-
if (pi !== void 0)
|
|
21747
|
-
return
|
|
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
|
|
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(
|
|
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((
|
|
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 '
|
|
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-
|
|
36170
|
-
//# sourceMappingURL=chunk-
|
|
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
|