@chanmeng666/archlang 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1330 @@
1
+ // src/lexer.ts
2
+ var isDigit = (c) => c >= "0" && c <= "9";
3
+ var isIdentStart = (c) => c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_";
4
+ var isIdentPart = (c) => isIdentStart(c) || isDigit(c);
5
+ function lex(src) {
6
+ const tokens = [];
7
+ const errors = [];
8
+ let i = 0;
9
+ let line = 1;
10
+ let col = 1;
11
+ const peek = (o = 0) => src[i + o] ?? "";
12
+ const advance = () => {
13
+ const c = src[i++];
14
+ if (c === "\n") {
15
+ line++;
16
+ col = 1;
17
+ } else {
18
+ col++;
19
+ }
20
+ return c;
21
+ };
22
+ const push = (type, value, startLine, startCol, startIdx, extra) => tokens.push({ type, value, line: startLine, col: startCol, ...extra, start: startIdx, end: i });
23
+ while (i < src.length) {
24
+ const c = peek();
25
+ const startLine = line;
26
+ const startCol = col;
27
+ const startIdx = i;
28
+ if (c === " " || c === " " || c === "\r" || c === "\n") {
29
+ advance();
30
+ continue;
31
+ }
32
+ if (c === "#") {
33
+ while (i < src.length && peek() !== "\n") advance();
34
+ continue;
35
+ }
36
+ if (c === '"') {
37
+ advance();
38
+ let value = "";
39
+ let terminated = false;
40
+ while (i < src.length) {
41
+ const ch = peek();
42
+ if (ch === "\\") {
43
+ advance();
44
+ const esc = advance();
45
+ value += esc === "n" ? "\n" : esc;
46
+ continue;
47
+ }
48
+ if (ch === '"') {
49
+ advance();
50
+ terminated = true;
51
+ break;
52
+ }
53
+ if (ch === "\n") break;
54
+ value += advance();
55
+ }
56
+ if (!terminated) {
57
+ errors.push({ message: "Unterminated string literal", span: { start: startIdx, end: i } });
58
+ }
59
+ push("string", value, startLine, startCol, startIdx);
60
+ continue;
61
+ }
62
+ if (c === "(") {
63
+ advance();
64
+ push("lparen", "(", startLine, startCol, startIdx);
65
+ continue;
66
+ }
67
+ if (c === ")") {
68
+ advance();
69
+ push("rparen", ")", startLine, startCol, startIdx);
70
+ continue;
71
+ }
72
+ if (c === "{") {
73
+ advance();
74
+ push("lcurly", "{", startLine, startCol, startIdx);
75
+ continue;
76
+ }
77
+ if (c === "}") {
78
+ advance();
79
+ push("rcurly", "}", startLine, startCol, startIdx);
80
+ continue;
81
+ }
82
+ if (c === ",") {
83
+ advance();
84
+ push("comma", ",", startLine, startCol, startIdx);
85
+ continue;
86
+ }
87
+ if (c === "=") {
88
+ advance();
89
+ push("equals", "=", startLine, startCol, startIdx);
90
+ continue;
91
+ }
92
+ if (c === ":") {
93
+ advance();
94
+ push("colon", ":", startLine, startCol, startIdx);
95
+ continue;
96
+ }
97
+ if (c === "-" && peek(1) === ">") {
98
+ advance();
99
+ advance();
100
+ push("arrow", "->", startLine, startCol, startIdx);
101
+ continue;
102
+ }
103
+ if (isDigit(c) || c === "-" && isDigit(peek(1)) || c === "." && isDigit(peek(1))) {
104
+ let raw = "";
105
+ if (c === "-") raw += advance();
106
+ while (isDigit(peek())) raw += advance();
107
+ if (peek() === ".") {
108
+ raw += advance();
109
+ while (isDigit(peek())) raw += advance();
110
+ }
111
+ const first = parseFloat(raw);
112
+ if (peek() === "x" && (isDigit(peek(1)) || peek(1) === "-" && isDigit(peek(2)) || peek(1) === "." && isDigit(peek(2)))) {
113
+ advance();
114
+ let raw2 = "";
115
+ if (peek() === "-") raw2 += advance();
116
+ while (isDigit(peek())) raw2 += advance();
117
+ if (peek() === ".") {
118
+ raw2 += advance();
119
+ while (isDigit(peek())) raw2 += advance();
120
+ }
121
+ const second = parseFloat(raw2);
122
+ push("dimension", `${raw}x${raw2}`, startLine, startCol, startIdx, { num: first, num2: second });
123
+ continue;
124
+ }
125
+ push("number", raw, startLine, startCol, startIdx, { num: first });
126
+ continue;
127
+ }
128
+ if (isIdentStart(c)) {
129
+ let value = "";
130
+ while (i < src.length && isIdentPart(peek())) value += advance();
131
+ push("ident", value, startLine, startCol, startIdx);
132
+ continue;
133
+ }
134
+ errors.push({ message: `Unexpected character ${JSON.stringify(c)}`, span: { start: startIdx, end: startIdx + 1 } });
135
+ advance();
136
+ }
137
+ push("eof", "", line, col, i);
138
+ return { tokens, errors };
139
+ }
140
+
141
+ // src/geometry.ts
142
+ var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
143
+ var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
144
+ var mul = (v, s) => ({ x: v.x * s, y: v.y * s });
145
+ var length = (v) => Math.hypot(v.x, v.y);
146
+ function unit(v) {
147
+ const l = length(v);
148
+ return l === 0 ? { x: 0, y: 0 } : { x: v.x / l, y: v.y / l };
149
+ }
150
+ var normal = (v) => ({ x: -v.y, y: v.x });
151
+ var emptyBounds = () => ({
152
+ minX: Infinity,
153
+ minY: Infinity,
154
+ maxX: -Infinity,
155
+ maxY: -Infinity
156
+ });
157
+ function extendBounds(b, x, y) {
158
+ if (x < b.minX) b.minX = x;
159
+ if (y < b.minY) b.minY = y;
160
+ if (x > b.maxX) b.maxX = x;
161
+ if (y > b.maxY) b.maxY = y;
162
+ }
163
+ function distPointToSegment(p, a, b) {
164
+ const abx = b.x - a.x;
165
+ const aby = b.y - a.y;
166
+ const apx = p.x - a.x;
167
+ const apy = p.y - a.y;
168
+ const len2 = abx * abx + aby * aby;
169
+ let t = len2 === 0 ? 0 : (apx * abx + apy * aby) / len2;
170
+ t = Math.max(0, Math.min(1, t));
171
+ const cx = a.x + t * abx;
172
+ const cy = a.y + t * aby;
173
+ return Math.hypot(p.x - cx, p.y - cy);
174
+ }
175
+ function rectCorners(x, y, w, h) {
176
+ return [
177
+ { x, y },
178
+ { x: x + w, y },
179
+ { x: x + w, y: y + h },
180
+ { x, y: y + h }
181
+ ];
182
+ }
183
+ function segmentRectangle(a, b, thickness) {
184
+ const d = unit(sub(b, a));
185
+ const n = normal(d);
186
+ const half = thickness / 2;
187
+ const a2 = add(a, mul(d, -half));
188
+ const b2 = add(b, mul(d, half));
189
+ return [
190
+ add(a2, mul(n, half)),
191
+ add(b2, mul(n, half)),
192
+ add(b2, mul(n, -half)),
193
+ add(a2, mul(n, -half))
194
+ ];
195
+ }
196
+ function segmentsOfWall(w) {
197
+ const segs = [];
198
+ for (let k = 0; k < w.points.length - 1; k++) {
199
+ segs.push({ a: w.points[k], b: w.points[k + 1], thickness: w.thickness, category: w.category });
200
+ }
201
+ if (w.closed && w.points.length > 2) {
202
+ segs.push({ a: w.points[w.points.length - 1], b: w.points[0], thickness: w.thickness, category: w.category });
203
+ }
204
+ return segs;
205
+ }
206
+ function hostSegmentForWalls(walls, at, ref) {
207
+ const candidates = ref ? walls.filter((w) => w.id === ref || w.category === ref) : walls;
208
+ let best = null;
209
+ let bestDist = Infinity;
210
+ for (const w of candidates) {
211
+ for (const s of segmentsOfWall(w)) {
212
+ const dist = distPointToSegment(at, s.a, s.b);
213
+ if (dist < bestDist) {
214
+ bestDist = dist;
215
+ best = s;
216
+ }
217
+ }
218
+ }
219
+ return best;
220
+ }
221
+ function isOnSomeWall(walls, at, ref) {
222
+ const candidates = ref ? walls.filter((w) => w.id === ref || w.category === ref) : walls;
223
+ for (const w of candidates) {
224
+ const tol = w.thickness / 2 + Math.max(w.thickness, 1);
225
+ for (const s of segmentsOfWall(w)) {
226
+ if (distPointToSegment(at, s.a, s.b) <= tol) return true;
227
+ }
228
+ }
229
+ return false;
230
+ }
231
+
232
+ // src/elements/wall.ts
233
+ var wall = {
234
+ kind: "wall",
235
+ keyword: "wall",
236
+ parse(ctx) {
237
+ const kw = ctx.eatKeyword("wall");
238
+ const id = ctx.parseIdOpt();
239
+ const category = ctx.eatIdent().value;
240
+ ctx.eatKeyword("thickness");
241
+ const thickness = ctx.eatNumber();
242
+ ctx.eat("lcurly");
243
+ const points = [];
244
+ let closed = false;
245
+ while (!ctx.isType("rcurly") && !ctx.isType("eof")) {
246
+ if (ctx.isKeyword("close")) {
247
+ ctx.next();
248
+ closed = true;
249
+ break;
250
+ }
251
+ if (ctx.isType("lparen")) {
252
+ points.push(ctx.parsePoint());
253
+ continue;
254
+ }
255
+ ctx.fail(`Expected a point "(x,y)" or "close" in wall body but found ${describe(ctx)}`);
256
+ }
257
+ ctx.eat("rcurly");
258
+ if (points.length < 2) ctx.fail("A wall needs at least two points", kw);
259
+ return { kind: "wall", id, category, thickness, points, closed, line: kw.line };
260
+ },
261
+ idPrefix: (node) => node.category || "wall",
262
+ resolve(node, ctx) {
263
+ const n = node;
264
+ const id = ctx.idOf(n);
265
+ const points = n.points.map(ctx.snapPt);
266
+ const thickness = ctx.snap(n.thickness) || n.thickness;
267
+ if (thickness <= 0) {
268
+ ctx.diag({ severity: "error", message: `Wall "${id}" must have a positive thickness`, code: "E_WALL_THICKNESS", span: n.span });
269
+ }
270
+ return { kind: "wall", id, category: n.category, thickness, points, closed: n.closed, span: n.span };
271
+ },
272
+ bounds(resolved) {
273
+ const w = resolved;
274
+ return segmentsOfWall(w).flatMap((s) => segmentRectangle(s.a, s.b, s.thickness));
275
+ },
276
+ render(resolved, ctx) {
277
+ const w = resolved;
278
+ const { fmt: fmt2, pt: pt2, theme, sizes } = ctx;
279
+ const segs = segmentsOfWall(w);
280
+ const ops = [];
281
+ for (const s of segs) {
282
+ const poly = segmentRectangle(s.a, s.b, s.thickness);
283
+ ops.push({ pass: "wallFill", svg: `<polygon points="${poly.map(pt2).join(" ")}" fill="url(#poche)"/>` });
284
+ }
285
+ for (const s of segs) {
286
+ const d = unit(sub(s.b, s.a));
287
+ const n = normal(d);
288
+ const h = s.thickness / 2;
289
+ const fa1 = add(s.a, mul(n, h));
290
+ const fb1 = add(s.b, mul(n, h));
291
+ const fa2 = add(s.a, mul(n, -h));
292
+ const fb2 = add(s.b, mul(n, -h));
293
+ ops.push({
294
+ pass: "wallFace",
295
+ svg: `<line x1="${fmt2(fa1.x)}" y1="${fmt2(fa1.y)}" x2="${fmt2(fb1.x)}" y2="${fmt2(fb1.y)}" stroke="${theme.wallStroke}" stroke-width="${fmt2(sizes.wallStroke)}" stroke-linecap="square"/>`
296
+ });
297
+ ops.push({
298
+ pass: "wallFace",
299
+ svg: `<line x1="${fmt2(fa2.x)}" y1="${fmt2(fa2.y)}" x2="${fmt2(fb2.x)}" y2="${fmt2(fb2.y)}" stroke="${theme.wallStroke}" stroke-width="${fmt2(sizes.wallStroke)}" stroke-linecap="square"/>`
300
+ });
301
+ }
302
+ return ops;
303
+ }
304
+ };
305
+ function describe(ctx) {
306
+ const t = ctx.peek();
307
+ if (t.type === "eof") return "end of input";
308
+ if (t.type === "string") return `string ${JSON.stringify(t.value)}`;
309
+ return `"${t.value}"`;
310
+ }
311
+
312
+ // src/elements/room.ts
313
+ var room = {
314
+ kind: "room",
315
+ keyword: "room",
316
+ parse(ctx) {
317
+ const kw = ctx.eatKeyword("room");
318
+ const id = ctx.parseIdOpt();
319
+ ctx.eatKeyword("at");
320
+ const at = ctx.parsePoint();
321
+ ctx.eatKeyword("size");
322
+ const dim2 = ctx.eat("dimension");
323
+ const node = { kind: "room", id, at, size: { w: dim2.num, h: dim2.num2 }, line: kw.line };
324
+ if (ctx.isKeyword("label")) {
325
+ ctx.next();
326
+ node.label = ctx.eatString();
327
+ }
328
+ return node;
329
+ },
330
+ idPrefix: () => "room",
331
+ resolve(node, ctx) {
332
+ const n = node;
333
+ const id = ctx.idOf(n);
334
+ const at = ctx.snapPt(n.at);
335
+ const size = { w: ctx.snap(n.size.w), h: ctx.snap(n.size.h) };
336
+ if (size.w <= 0 || size.h <= 0) {
337
+ ctx.diag({ severity: "error", message: `Room "${id}" must have a positive size`, code: "E_ROOM_SIZE", span: n.span });
338
+ }
339
+ return { kind: "room", id, at, size, label: n.label, span: n.span };
340
+ },
341
+ bounds(resolved) {
342
+ const r = resolved;
343
+ return rectCorners(r.at.x, r.at.y, r.size.w, r.size.h);
344
+ },
345
+ render(resolved, ctx) {
346
+ const r = resolved;
347
+ const { fmt: fmt2, pt: pt2, xml: xml2, theme, sizes } = ctx;
348
+ const ops = [];
349
+ const c = rectCorners(r.at.x, r.at.y, r.size.w, r.size.h);
350
+ ops.push({ pass: "floor", svg: `<polygon points="${c.map(pt2).join(" ")}" fill="${theme.roomFill}"/>` });
351
+ const cx = r.at.x + r.size.w / 2;
352
+ const cy = r.at.y + r.size.h / 2;
353
+ const areaM2 = (r.size.w / 1e3 * (r.size.h / 1e3)).toFixed(1);
354
+ if (r.label) {
355
+ ops.push({
356
+ pass: "labels",
357
+ svg: `<text x="${fmt2(cx)}" y="${fmt2(cy - sizes.roomFont * 0.2)}" font-size="${fmt2(sizes.roomFont)}" fill="${theme.roomLabel}" text-anchor="middle" dominant-baseline="central" font-weight="600">${xml2(r.label)}</text>`
358
+ });
359
+ }
360
+ ops.push({
361
+ pass: "labels",
362
+ svg: `<text x="${fmt2(cx)}" y="${fmt2(cy + (r.label ? sizes.roomFont * 0.9 : 0))}" font-size="${fmt2(sizes.areaFont)}" fill="${theme.areaLabel}" text-anchor="middle" dominant-baseline="central">${areaM2} m\xB2</text>`
363
+ });
364
+ return ops;
365
+ }
366
+ };
367
+
368
+ // src/elements/door.ts
369
+ var door = {
370
+ kind: "door",
371
+ keyword: "door",
372
+ parse(ctx) {
373
+ const kw = ctx.eatKeyword("door");
374
+ const id = ctx.parseIdOpt();
375
+ ctx.eatKeyword("at");
376
+ const at = ctx.parsePoint();
377
+ ctx.eatKeyword("width");
378
+ const width = ctx.eatNumber();
379
+ const node = { kind: "door", id, at, width, hinge: "left", swing: "in", line: kw.line };
380
+ if (ctx.isKeyword("wall")) {
381
+ ctx.next();
382
+ node.wall = ctx.eatIdent().value;
383
+ }
384
+ if (ctx.isKeyword("hinge")) {
385
+ ctx.next();
386
+ const h = ctx.eatIdent().value;
387
+ if (h !== "left" && h !== "right") ctx.fail(`Expected hinge "left" or "right" but found "${h}"`);
388
+ node.hinge = h;
389
+ }
390
+ if (ctx.isKeyword("swing")) {
391
+ ctx.next();
392
+ const s = ctx.eatIdent().value;
393
+ if (s !== "in" && s !== "out") ctx.fail(`Expected swing "in" or "out" but found "${s}"`);
394
+ node.swing = s;
395
+ }
396
+ return node;
397
+ },
398
+ idPrefix: () => "door",
399
+ resolve(node, ctx) {
400
+ const n = node;
401
+ const id = ctx.idOf(n);
402
+ const at = ctx.snapPt(n.at);
403
+ const width = ctx.snap(n.width) || n.width;
404
+ if (width <= 0) {
405
+ ctx.diag({ severity: "error", message: `Door "${id}" must have a positive width`, code: "E_DOOR_WIDTH", span: n.span });
406
+ }
407
+ if (ctx.walls.length > 0 && !ctx.isOnWall(at, n.wall)) {
408
+ ctx.diag({ severity: "warning", message: `Door "${id}" does not lie on any wall`, code: "W_DOOR_OFF_WALL", span: n.span });
409
+ }
410
+ return { kind: "door", id, at, width, hinge: n.hinge, swing: n.swing, host: ctx.hostSegment(at, n.wall), span: n.span };
411
+ },
412
+ bounds: () => [],
413
+ render(resolved, ctx) {
414
+ const dr = resolved;
415
+ const seg = dr.host;
416
+ if (!seg) return [];
417
+ const { fmt: fmt2, pt: pt2, theme, sizes } = ctx;
418
+ const d = unit(sub(seg.b, seg.a));
419
+ const n = normal(d);
420
+ const h = seg.thickness / 2 + sizes.wallStroke;
421
+ const hw = dr.width / 2;
422
+ const cover = [
423
+ add(add(dr.at, mul(d, -hw)), mul(n, h)),
424
+ add(add(dr.at, mul(d, hw)), mul(n, h)),
425
+ add(add(dr.at, mul(d, hw)), mul(n, -h)),
426
+ add(add(dr.at, mul(d, -hw)), mul(n, -h))
427
+ ];
428
+ const ops = [];
429
+ ops.push({ pass: "doors", svg: `<polygon points="${cover.map(pt2).join(" ")}" fill="${theme.opening}"/>` });
430
+ const hinge = dr.hinge === "left" ? add(dr.at, mul(d, -hw)) : add(dr.at, mul(d, hw));
431
+ const farJamb = dr.hinge === "left" ? add(dr.at, mul(d, hw)) : add(dr.at, mul(d, -hw));
432
+ const leafDir = dr.swing === "in" ? n : mul(n, -1);
433
+ const leafEnd = add(hinge, mul(leafDir, dr.width));
434
+ const cross = (leafEnd.x - hinge.x) * (farJamb.y - hinge.y) - (leafEnd.y - hinge.y) * (farJamb.x - hinge.x);
435
+ const sweep = cross < 0 ? 1 : 0;
436
+ ops.push({
437
+ pass: "doors",
438
+ svg: `<line x1="${fmt2(hinge.x)}" y1="${fmt2(hinge.y)}" x2="${fmt2(leafEnd.x)}" y2="${fmt2(leafEnd.y)}" stroke="${theme.doorLeaf}" stroke-width="${fmt2(sizes.thin * 1.3)}"/>`
439
+ });
440
+ ops.push({
441
+ pass: "doors",
442
+ svg: `<path d="M ${pt2(leafEnd)} A ${fmt2(dr.width)} ${fmt2(dr.width)} 0 0 ${sweep} ${pt2(farJamb)}" fill="none" stroke="${theme.doorLeaf}" stroke-width="${fmt2(sizes.thin)}" stroke-dasharray="${fmt2(sizes.thin * 4)} ${fmt2(sizes.thin * 3)}"/>`
443
+ });
444
+ return ops;
445
+ }
446
+ };
447
+
448
+ // src/elements/window.ts
449
+ var windowEl = {
450
+ kind: "window",
451
+ keyword: "window",
452
+ parse(ctx) {
453
+ const kw = ctx.eatKeyword("window");
454
+ const id = ctx.parseIdOpt();
455
+ ctx.eatKeyword("at");
456
+ const at = ctx.parsePoint();
457
+ ctx.eatKeyword("width");
458
+ const width = ctx.eatNumber();
459
+ const node = { kind: "window", id, at, width, line: kw.line };
460
+ if (ctx.isKeyword("wall")) {
461
+ ctx.next();
462
+ node.wall = ctx.eatIdent().value;
463
+ }
464
+ return node;
465
+ },
466
+ idPrefix: () => "window",
467
+ resolve(node, ctx) {
468
+ const n = node;
469
+ const id = ctx.idOf(n);
470
+ const at = ctx.snapPt(n.at);
471
+ const width = ctx.snap(n.width) || n.width;
472
+ if (width <= 0) {
473
+ ctx.diag({ severity: "error", message: `Window "${id}" must have a positive width`, code: "E_WINDOW_WIDTH", span: n.span });
474
+ }
475
+ if (ctx.walls.length > 0 && !ctx.isOnWall(at, n.wall)) {
476
+ ctx.diag({ severity: "warning", message: `Window "${id}" does not lie on any wall`, code: "W_WINDOW_OFF_WALL", span: n.span });
477
+ }
478
+ return { kind: "window", id, at, width, host: ctx.hostSegment(at, n.wall), span: n.span };
479
+ },
480
+ bounds: () => [],
481
+ render(resolved, ctx) {
482
+ const wn = resolved;
483
+ const seg = wn.host;
484
+ if (!seg) return [];
485
+ const { fmt: fmt2, pt: pt2, theme, sizes } = ctx;
486
+ const d = unit(sub(seg.b, seg.a));
487
+ const n = normal(d);
488
+ const h = seg.thickness / 2;
489
+ const he = h + sizes.wallStroke;
490
+ const hw = wn.width / 2;
491
+ const cover = [
492
+ add(add(wn.at, mul(d, -hw)), mul(n, he)),
493
+ add(add(wn.at, mul(d, hw)), mul(n, he)),
494
+ add(add(wn.at, mul(d, hw)), mul(n, -he)),
495
+ add(add(wn.at, mul(d, -hw)), mul(n, -he))
496
+ ];
497
+ const ops = [];
498
+ ops.push({ pass: "windows", svg: `<polygon points="${cover.map(pt2).join(" ")}" fill="${theme.opening}"/>` });
499
+ const jA = add(wn.at, mul(d, -hw));
500
+ const jB = add(wn.at, mul(d, hw));
501
+ for (const off of [h, -h]) {
502
+ const a = add(jA, mul(n, off));
503
+ const bb = add(jB, mul(n, off));
504
+ ops.push({
505
+ pass: "windows",
506
+ svg: `<line x1="${fmt2(a.x)}" y1="${fmt2(a.y)}" x2="${fmt2(bb.x)}" y2="${fmt2(bb.y)}" stroke="${theme.wallStroke}" stroke-width="${fmt2(sizes.thin)}"/>`
507
+ });
508
+ }
509
+ ops.push({
510
+ pass: "windows",
511
+ svg: `<line x1="${fmt2(jA.x)}" y1="${fmt2(jA.y)}" x2="${fmt2(jB.x)}" y2="${fmt2(jB.y)}" stroke="${theme.windowPane}" stroke-width="${fmt2(sizes.thin)}"/>`
512
+ });
513
+ return ops;
514
+ }
515
+ };
516
+
517
+ // src/elements/furniture.ts
518
+ var furniture = {
519
+ kind: "furniture",
520
+ keyword: "furniture",
521
+ parse(ctx) {
522
+ const kw = ctx.eatKeyword("furniture");
523
+ const id = ctx.parseIdOpt();
524
+ const category = ctx.eatIdent().value;
525
+ ctx.eatKeyword("at");
526
+ const at = ctx.parsePoint();
527
+ ctx.eatKeyword("size");
528
+ const dim2 = ctx.eat("dimension");
529
+ const node = { kind: "furniture", id, category, at, size: { w: dim2.num, h: dim2.num2 }, line: kw.line };
530
+ if (ctx.isKeyword("label")) {
531
+ ctx.next();
532
+ node.label = ctx.eatString();
533
+ }
534
+ return node;
535
+ },
536
+ idPrefix: (node) => node.category || "furniture",
537
+ resolve(node, ctx) {
538
+ const n = node;
539
+ const id = ctx.idOf(n);
540
+ const at = ctx.snapPt(n.at);
541
+ const size = { w: ctx.snap(n.size.w), h: ctx.snap(n.size.h) };
542
+ if (size.w <= 0 || size.h <= 0) {
543
+ ctx.diag({ severity: "error", message: `Furniture "${id}" must have a positive size`, code: "E_FURN_SIZE", span: n.span });
544
+ }
545
+ return { kind: "furniture", id, category: n.category, at, size, label: n.label, span: n.span };
546
+ },
547
+ bounds(resolved) {
548
+ const f = resolved;
549
+ return rectCorners(f.at.x, f.at.y, f.size.w, f.size.h);
550
+ },
551
+ render(resolved, ctx) {
552
+ const f = resolved;
553
+ const { fmt: fmt2, pt: pt2, xml: xml2, theme, sizes } = ctx;
554
+ const ops = [];
555
+ const c = rectCorners(f.at.x, f.at.y, f.size.w, f.size.h);
556
+ ops.push({
557
+ pass: "furniture",
558
+ svg: `<polygon points="${c.map(pt2).join(" ")}" fill="${theme.furnitureFill}" stroke="${theme.furnitureStroke}" stroke-width="${fmt2(sizes.thin)}"/>`
559
+ });
560
+ if (f.label) {
561
+ const cx = f.at.x + f.size.w / 2;
562
+ const cy = f.at.y + f.size.h / 2;
563
+ ops.push({
564
+ pass: "furniture",
565
+ svg: `<text x="${fmt2(cx)}" y="${fmt2(cy)}" font-size="${fmt2(sizes.furnFont)}" fill="${theme.furnitureLabel}" text-anchor="middle" dominant-baseline="central">${xml2(f.label)}</text>`
566
+ });
567
+ }
568
+ return ops;
569
+ }
570
+ };
571
+
572
+ // src/elements/dim.ts
573
+ var dim = {
574
+ kind: "dim",
575
+ keyword: "dim",
576
+ parse(ctx) {
577
+ const kw = ctx.eatKeyword("dim");
578
+ const from = ctx.parsePoint();
579
+ ctx.eat("arrow");
580
+ const to = ctx.parsePoint();
581
+ const node = { kind: "dim", id: "", from, to, offset: 300, line: kw.line };
582
+ if (ctx.isKeyword("offset")) {
583
+ ctx.next();
584
+ node.offset = ctx.eatNumber();
585
+ }
586
+ if (ctx.isKeyword("text")) {
587
+ ctx.next();
588
+ node.text = ctx.eatString();
589
+ }
590
+ return node;
591
+ },
592
+ idPrefix: () => "dim",
593
+ resolve(node, ctx) {
594
+ const n = node;
595
+ return {
596
+ kind: "dim",
597
+ id: ctx.idOf(n),
598
+ from: ctx.snapPt(n.from),
599
+ to: ctx.snapPt(n.to),
600
+ offset: n.offset,
601
+ text: n.text,
602
+ span: n.span
603
+ };
604
+ },
605
+ bounds(resolved) {
606
+ const dm = resolved;
607
+ return [dm.from, dm.to];
608
+ },
609
+ render(resolved, ctx) {
610
+ const dm = resolved;
611
+ const { fmt: fmt2, xml: xml2, theme, sizes } = ctx;
612
+ const dir = unit(sub(dm.to, dm.from));
613
+ const n = normal(dir);
614
+ const off = mul(n, dm.offset);
615
+ const p1 = add(dm.from, off);
616
+ const p2 = add(dm.to, off);
617
+ const tick = sizes.refDim * 0.012;
618
+ const ops = [];
619
+ ops.push({
620
+ pass: "dims",
621
+ svg: `<line x1="${fmt2(dm.from.x)}" y1="${fmt2(dm.from.y)}" x2="${fmt2(p1.x)}" y2="${fmt2(p1.y)}" stroke="${theme.dim}" stroke-width="${fmt2(sizes.thin * 0.7)}"/>`
622
+ });
623
+ ops.push({
624
+ pass: "dims",
625
+ svg: `<line x1="${fmt2(dm.to.x)}" y1="${fmt2(dm.to.y)}" x2="${fmt2(p2.x)}" y2="${fmt2(p2.y)}" stroke="${theme.dim}" stroke-width="${fmt2(sizes.thin * 0.7)}"/>`
626
+ });
627
+ ops.push({
628
+ pass: "dims",
629
+ svg: `<line x1="${fmt2(p1.x)}" y1="${fmt2(p1.y)}" x2="${fmt2(p2.x)}" y2="${fmt2(p2.y)}" stroke="${theme.dim}" stroke-width="${fmt2(sizes.thin)}"/>`
630
+ });
631
+ for (const p of [p1, p2]) {
632
+ const t1 = add(p, mul(unit({ x: dir.x + n.x, y: dir.y + n.y }), tick));
633
+ const t2 = add(p, mul(unit({ x: dir.x + n.x, y: dir.y + n.y }), -tick));
634
+ ops.push({
635
+ pass: "dims",
636
+ svg: `<line x1="${fmt2(t1.x)}" y1="${fmt2(t1.y)}" x2="${fmt2(t2.x)}" y2="${fmt2(t2.y)}" stroke="${theme.dim}" stroke-width="${fmt2(sizes.thin)}"/>`
637
+ });
638
+ }
639
+ const mid = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };
640
+ const tp = add(mid, mul(n, sizes.dimFont * 0.7));
641
+ let angle = Math.atan2(dir.y, dir.x) * 180 / Math.PI;
642
+ if (angle > 90) angle -= 180;
643
+ if (angle < -90) angle += 180;
644
+ const label = dm.text ?? String(Math.round(length(sub(dm.to, dm.from))));
645
+ ops.push({
646
+ pass: "dims",
647
+ svg: `<text x="${fmt2(tp.x)}" y="${fmt2(tp.y)}" font-size="${fmt2(sizes.dimFont)}" fill="${theme.dim}" text-anchor="middle" dominant-baseline="central" transform="rotate(${fmt2(angle)} ${fmt2(tp.x)} ${fmt2(tp.y)})">${xml2(label)}</text>`
648
+ });
649
+ return ops;
650
+ }
651
+ };
652
+
653
+ // src/elements/column.ts
654
+ var column = {
655
+ kind: "column",
656
+ keyword: "column",
657
+ parse(ctx) {
658
+ const kw = ctx.eatKeyword("column");
659
+ const id = ctx.parseIdOpt();
660
+ ctx.eatKeyword("at");
661
+ const at = ctx.parsePoint();
662
+ ctx.eatKeyword("size");
663
+ const dim2 = ctx.eat("dimension");
664
+ return { kind: "column", id, at, size: { w: dim2.num, h: dim2.num2 }, line: kw.line };
665
+ },
666
+ idPrefix: () => "column",
667
+ resolve(node, ctx) {
668
+ const n = node;
669
+ const id = ctx.idOf(n);
670
+ const at = ctx.snapPt(n.at);
671
+ const size = { w: ctx.snap(n.size.w), h: ctx.snap(n.size.h) };
672
+ if (size.w <= 0 || size.h <= 0) {
673
+ ctx.diag({ severity: "error", message: `Column "${id}" must have a positive size`, code: "E_COLUMN_SIZE", span: n.span });
674
+ }
675
+ return { kind: "column", id, at, size, span: n.span };
676
+ },
677
+ bounds(resolved) {
678
+ const c = resolved;
679
+ return rectCorners(c.at.x, c.at.y, c.size.w, c.size.h);
680
+ },
681
+ render(resolved, ctx) {
682
+ const c = resolved;
683
+ const { fmt: fmt2, pt: pt2, theme, sizes } = ctx;
684
+ const pts = rectCorners(c.at.x, c.at.y, c.size.w, c.size.h);
685
+ return [
686
+ {
687
+ pass: "furniture",
688
+ svg: `<polygon points="${pts.map(pt2).join(" ")}" fill="${theme.column}" stroke="${theme.wallStroke}" stroke-width="${fmt2(sizes.thin)}"/>`
689
+ }
690
+ ];
691
+ }
692
+ };
693
+
694
+ // src/elements/index.ts
695
+ var registryOrder = [];
696
+ var registry = /* @__PURE__ */ new Map();
697
+ function register(def) {
698
+ registry.set(def.keyword, def);
699
+ registryOrder.push(def);
700
+ }
701
+ register(wall);
702
+ register(room);
703
+ register(door);
704
+ register(windowEl);
705
+ register(furniture);
706
+ register(dim);
707
+ register(column);
708
+
709
+ // src/parser.ts
710
+ var SETTINGS = ["units", "grid", "scale", "north", "title"];
711
+ var STATEMENT_STARTS = /* @__PURE__ */ new Set([...SETTINGS, ...registry.keys()]);
712
+ var ParseError = class extends Error {
713
+ constructor(message, span) {
714
+ super(message);
715
+ this.message = message;
716
+ this.span = span;
717
+ }
718
+ message;
719
+ span;
720
+ };
721
+ function parse(src) {
722
+ const { tokens, errors: lexErrors } = lex(src);
723
+ const lexDiags = lexErrors.map((e) => ({
724
+ severity: "error",
725
+ message: e.message,
726
+ span: e.span
727
+ }));
728
+ const p = new Parser(tokens);
729
+ let plan;
730
+ try {
731
+ plan = p.parsePlan();
732
+ } catch (e) {
733
+ if (e instanceof ParseError) {
734
+ p.diagnostics.push({ severity: "error", message: e.message, span: e.span });
735
+ } else {
736
+ throw e;
737
+ }
738
+ }
739
+ return { plan, diagnostics: [...lexDiags, ...p.diagnostics] };
740
+ }
741
+ var Parser = class {
742
+ constructor(toks) {
743
+ this.toks = toks;
744
+ this.ctx = {
745
+ peek: (o) => this.peek(o),
746
+ next: () => this.next(),
747
+ eat: (type) => this.eat(type),
748
+ eatKeyword: (kw) => this.eatKeyword(kw),
749
+ eatIdent: () => this.eatIdent(),
750
+ eatNumber: () => this.eatNumber(),
751
+ eatString: () => this.eatString(),
752
+ isKeyword: (kw, o) => this.isKeyword(kw, o),
753
+ isType: (type) => this.isType(type),
754
+ parsePoint: () => this.parsePoint(),
755
+ parseIdOpt: () => this.parseIdOpt(),
756
+ fail: (msg, t) => this.fail(msg, t)
757
+ };
758
+ }
759
+ toks;
760
+ pos = 0;
761
+ diagnostics = [];
762
+ /** Facade passed to element parse functions (see registry.ts). */
763
+ ctx;
764
+ peek(o = 0) {
765
+ return this.toks[Math.min(this.pos + o, this.toks.length - 1)];
766
+ }
767
+ next() {
768
+ return this.toks[Math.min(this.pos++, this.toks.length - 1)];
769
+ }
770
+ fail(msg, t = this.peek()) {
771
+ throw new ParseError(msg, { start: t.start, end: t.end });
772
+ }
773
+ /** Span from a start offset to the end of the last consumed token. */
774
+ spanFrom(start) {
775
+ const last = this.toks[Math.max(0, Math.min(this.pos - 1, this.toks.length - 1))];
776
+ return { start, end: last.end };
777
+ }
778
+ /** Recover after a statement error: skip to the next statement start or block end. */
779
+ synchronize() {
780
+ if (!this.isType("rcurly") && !this.isType("eof")) this.next();
781
+ while (!this.isType("rcurly") && !this.isType("eof")) {
782
+ const t = this.peek();
783
+ if (t.type === "ident" && STATEMENT_STARTS.has(t.value)) return;
784
+ this.next();
785
+ }
786
+ }
787
+ isKeyword(kw, o = 0) {
788
+ const t = this.peek(o);
789
+ return t.type === "ident" && t.value === kw;
790
+ }
791
+ eatKeyword(kw) {
792
+ const t = this.peek();
793
+ if (t.type !== "ident" || t.value !== kw) this.fail(`Expected "${kw}" but found ${describe2(t)}`);
794
+ return this.next();
795
+ }
796
+ eat(type) {
797
+ const t = this.peek();
798
+ if (t.type !== type) this.fail(`Expected ${type} but found ${describe2(t)}`);
799
+ return this.next();
800
+ }
801
+ eatIdent() {
802
+ return this.eat("ident");
803
+ }
804
+ eatNumber() {
805
+ const t = this.eat("number");
806
+ return t.num;
807
+ }
808
+ eatString() {
809
+ return this.eat("string").value;
810
+ }
811
+ parsePlan() {
812
+ this.eatKeyword("plan");
813
+ const name = this.eatString();
814
+ this.eat("lcurly");
815
+ const plan = {
816
+ name,
817
+ units: "mm",
818
+ grid: 0,
819
+ north: "up",
820
+ elements: []
821
+ };
822
+ while (!this.isType("rcurly") && !this.isType("eof")) {
823
+ const t = this.peek();
824
+ const start = t.start;
825
+ try {
826
+ if (t.type !== "ident") this.fail(`Expected a statement but found ${describe2(t)}`);
827
+ const def = registry.get(t.value);
828
+ if (def) {
829
+ const node = def.parse(this.ctx);
830
+ node.span = this.spanFrom(start);
831
+ plan.elements.push(node);
832
+ continue;
833
+ }
834
+ switch (t.value) {
835
+ case "units": {
836
+ this.next();
837
+ const u = this.eatIdent().value;
838
+ if (u !== "mm") this.fail(`Unsupported units "${u}" (only "mm" is supported)`, t);
839
+ plan.units = "mm";
840
+ break;
841
+ }
842
+ case "grid":
843
+ this.next();
844
+ plan.grid = this.eatNumber();
845
+ break;
846
+ case "scale": {
847
+ this.next();
848
+ const a = this.eatNumber();
849
+ this.eat("colon");
850
+ const b = this.eatNumber();
851
+ plan.scale = `${a}:${b}`;
852
+ break;
853
+ }
854
+ case "north":
855
+ this.next();
856
+ plan.north = this.parseNorth();
857
+ break;
858
+ case "title": {
859
+ const n = this.parseTitle();
860
+ n.span = this.spanFrom(start);
861
+ plan.title = n;
862
+ break;
863
+ }
864
+ default:
865
+ this.fail(`Unknown statement "${t.value}"`, t);
866
+ }
867
+ } catch (e) {
868
+ if (e instanceof ParseError) {
869
+ this.diagnostics.push({ severity: "error", message: e.message, span: e.span });
870
+ this.synchronize();
871
+ } else {
872
+ throw e;
873
+ }
874
+ }
875
+ }
876
+ try {
877
+ this.eat("rcurly");
878
+ } catch (e) {
879
+ if (e instanceof ParseError) {
880
+ this.diagnostics.push({ severity: "error", message: e.message, span: e.span });
881
+ } else {
882
+ throw e;
883
+ }
884
+ }
885
+ return plan;
886
+ }
887
+ isType(type) {
888
+ return this.peek().type === type;
889
+ }
890
+ /** Optional `id=<ident>` prefix; returns "" when absent. */
891
+ parseIdOpt() {
892
+ if (this.isKeyword("id")) {
893
+ this.next();
894
+ this.eat("equals");
895
+ return this.eatIdent().value;
896
+ }
897
+ return "";
898
+ }
899
+ parseNorth() {
900
+ const t = this.peek();
901
+ if (t.type === "number") {
902
+ this.next();
903
+ return { deg: t.num };
904
+ }
905
+ if (t.type === "ident" && ["up", "down", "left", "right"].includes(t.value)) {
906
+ this.next();
907
+ return t.value;
908
+ }
909
+ this.fail(`Expected a north direction (up|down|left|right|<degrees>) but found ${describe2(t)}`);
910
+ }
911
+ parsePoint() {
912
+ this.eat("lparen");
913
+ const x = this.eatNumber();
914
+ this.eat("comma");
915
+ const y = this.eatNumber();
916
+ this.eat("rparen");
917
+ return { x, y };
918
+ }
919
+ parseTitle() {
920
+ const kw = this.eatKeyword("title");
921
+ this.eat("lcurly");
922
+ const node = { line: kw.line };
923
+ while (!this.isType("rcurly") && !this.isType("eof")) {
924
+ const t = this.peek();
925
+ if (t.type !== "ident") this.fail(`Expected a title field but found ${describe2(t)}`);
926
+ switch (t.value) {
927
+ case "project":
928
+ this.next();
929
+ node.project = this.eatString();
930
+ break;
931
+ case "drawn_by":
932
+ this.next();
933
+ node.drawnBy = this.eatString();
934
+ break;
935
+ case "date":
936
+ this.next();
937
+ node.date = this.eatString();
938
+ break;
939
+ default:
940
+ this.fail(`Unknown title field "${t.value}"`, t);
941
+ }
942
+ }
943
+ this.eat("rcurly");
944
+ return node;
945
+ }
946
+ };
947
+ function describe2(t) {
948
+ if (t.type === "eof") return "end of input";
949
+ if (t.type === "string") return `string ${JSON.stringify(t.value)}`;
950
+ return `"${t.value}"`;
951
+ }
952
+
953
+ // src/ir.ts
954
+ function resolve(ast) {
955
+ const diagnostics = [];
956
+ const g = ast.grid;
957
+ const snap = (v) => g > 0 ? Math.round(v / g) * g : v;
958
+ const snapPt = (p) => ({ x: snap(p.x), y: snap(p.y) });
959
+ const idMap = /* @__PURE__ */ new Map();
960
+ const seen = /* @__PURE__ */ new Set();
961
+ const assignId = (provided, prefix, idx, span) => {
962
+ if (provided) {
963
+ if (seen.has(provided)) {
964
+ diagnostics.push({ severity: "error", message: `Duplicate id "${provided}"`, code: "E_DUP_ID", span });
965
+ }
966
+ seen.add(provided);
967
+ return provided;
968
+ }
969
+ let auto = `${prefix}_${idx}`;
970
+ while (seen.has(auto)) auto = `${auto}_`;
971
+ seen.add(auto);
972
+ return auto;
973
+ };
974
+ for (const def of registryOrder) {
975
+ let idx = 0;
976
+ for (const node of ast.elements) {
977
+ if (node.kind !== def.kind) continue;
978
+ idx++;
979
+ idMap.set(node, assignId(node.id, def.idPrefix(node), idx, node.span));
980
+ }
981
+ }
982
+ const walls = [];
983
+ const ctx = {
984
+ grid: g,
985
+ snap,
986
+ snapPt,
987
+ idOf: (node) => idMap.get(node) ?? node.id,
988
+ walls,
989
+ hostSegment: (at, ref) => hostSegmentForWalls(walls, at, ref),
990
+ isOnWall: (at, ref) => isOnSomeWall(walls, at, ref),
991
+ diag: (d) => diagnostics.push(d)
992
+ };
993
+ const rmap = /* @__PURE__ */ new Map();
994
+ for (const def of registryOrder) {
995
+ for (const node of ast.elements) {
996
+ if (node.kind !== def.kind) continue;
997
+ const r = def.resolve(node, ctx);
998
+ rmap.set(node, r);
999
+ if (r.kind === "wall") walls.push(r);
1000
+ }
1001
+ }
1002
+ const elements = ast.elements.map((n) => rmap.get(n));
1003
+ const drawable = elements.some(
1004
+ (e) => e.kind === "wall" || e.kind === "room" || e.kind === "furniture" || e.kind === "column"
1005
+ );
1006
+ if (!drawable) {
1007
+ diagnostics.push({
1008
+ severity: "warning",
1009
+ message: "Plan has no walls, rooms, or furniture \u2014 nothing to draw",
1010
+ code: "W_EMPTY_PLAN"
1011
+ });
1012
+ }
1013
+ const rooms = elements.filter((e) => e.kind === "room");
1014
+ for (let a = 0; a < rooms.length; a++) {
1015
+ for (let b = a + 1; b < rooms.length; b++) {
1016
+ const r1 = rooms[a];
1017
+ const r2 = rooms[b];
1018
+ const ox = Math.max(0, Math.min(r1.at.x + r1.size.w, r2.at.x + r2.size.w) - Math.max(r1.at.x, r2.at.x));
1019
+ const oy = Math.max(0, Math.min(r1.at.y + r1.size.h, r2.at.y + r2.size.h) - Math.max(r1.at.y, r2.at.y));
1020
+ if (ox > 1 && oy > 1) {
1021
+ diagnostics.push({
1022
+ severity: "warning",
1023
+ message: `Rooms "${r1.id}" and "${r2.id}" overlap`,
1024
+ code: "W_ROOM_OVERLAP",
1025
+ span: r2.span
1026
+ });
1027
+ }
1028
+ }
1029
+ }
1030
+ const ir = {
1031
+ name: ast.name,
1032
+ units: ast.units,
1033
+ grid: ast.grid,
1034
+ scale: ast.scale,
1035
+ north: ast.north,
1036
+ title: ast.title,
1037
+ elements,
1038
+ walls
1039
+ };
1040
+ return { ir, diagnostics };
1041
+ }
1042
+
1043
+ // src/registry.ts
1044
+ var RENDER_PASSES = [
1045
+ "floor",
1046
+ "furniture",
1047
+ "wallFill",
1048
+ "wallFace",
1049
+ "doors",
1050
+ "windows",
1051
+ "labels",
1052
+ "dims",
1053
+ "annotations"
1054
+ ];
1055
+
1056
+ // src/render.ts
1057
+ var THEME = {
1058
+ bg: "#ffffff",
1059
+ pocheBase: "#e9e4db",
1060
+ pocheHatch: "#b9b1a4",
1061
+ wallStroke: "#1b1b1b",
1062
+ roomFill: "#fbfaf7",
1063
+ roomLabel: "#222222",
1064
+ areaLabel: "#7a7a7a",
1065
+ furnitureStroke: "#a8a29a",
1066
+ furnitureFill: "#f4f2ee",
1067
+ furnitureLabel: "#9a948c",
1068
+ opening: "#ffffff",
1069
+ doorLeaf: "#555555",
1070
+ windowPane: "#3a6ea5",
1071
+ dim: "#0E5484",
1072
+ annotation: "#333333",
1073
+ annotationMuted: "#888888",
1074
+ column: "#4a4a4a"
1075
+ };
1076
+ function fmt(v) {
1077
+ const r = Math.round(v * 100) / 100;
1078
+ return Object.is(r, -0) ? "0" : String(r);
1079
+ }
1080
+ var pt = (p) => `${fmt(p.x)},${fmt(p.y)}`;
1081
+ function xml(s) {
1082
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1083
+ }
1084
+ var NICE_LENGTHS = [500, 1e3, 2e3, 5e3, 1e4, 2e4, 5e4, 1e5];
1085
+ function niceBarLength(target) {
1086
+ let best = NICE_LENGTHS[0];
1087
+ for (const v of NICE_LENGTHS) if (v <= target) best = v;
1088
+ return best;
1089
+ }
1090
+ function planBounds(ir) {
1091
+ const b = emptyBounds();
1092
+ for (const el of ir.elements) {
1093
+ const def = registry.get(el.kind);
1094
+ if (!def) continue;
1095
+ for (const p of def.bounds(el)) extendBounds(b, p.x, p.y);
1096
+ }
1097
+ if (!isFinite(b.minX)) {
1098
+ return { minX: 0, minY: 0, maxX: 1e3, maxY: 1e3 };
1099
+ }
1100
+ return b;
1101
+ }
1102
+ function render(ir, opts = {}) {
1103
+ const b = planBounds(ir);
1104
+ const drawW = b.maxX - b.minX;
1105
+ const drawH = b.maxY - b.minY;
1106
+ const refDim = Math.max(drawW, drawH, 1);
1107
+ const sizes = {
1108
+ refDim,
1109
+ wallStroke: refDim * 28e-4,
1110
+ thin: refDim * 16e-4,
1111
+ roomFont: refDim * 0.03,
1112
+ areaFont: refDim * 0.022,
1113
+ dimFont: refDim * 0.02,
1114
+ furnFont: refDim * 0.017,
1115
+ margin: refDim * 0.17,
1116
+ hatchGap: refDim * 0.013
1117
+ };
1118
+ const { thin, margin, hatchGap } = sizes;
1119
+ const vbX = b.minX - margin;
1120
+ const vbY = b.minY - margin;
1121
+ const vbW = drawW + margin * 2;
1122
+ const vbH = drawH + margin * 2;
1123
+ const out = [];
1124
+ const svgAttrs = opts.width ? `width="${fmt(opts.width)}" height="${fmt(opts.width * vbH / vbW)}"` : "";
1125
+ out.push(
1126
+ `<svg xmlns="http://www.w3.org/2000/svg" ${svgAttrs} viewBox="${fmt(vbX)} ${fmt(vbY)} ${fmt(vbW)} ${fmt(vbH)}" font-family="Helvetica, Arial, sans-serif">`
1127
+ );
1128
+ out.push(
1129
+ `<defs><pattern id="poche" patternUnits="userSpaceOnUse" width="${fmt(hatchGap)}" height="${fmt(hatchGap)}" patternTransform="rotate(45)"><rect width="${fmt(hatchGap)}" height="${fmt(hatchGap)}" fill="${THEME.pocheBase}"/><line x1="0" y1="0" x2="0" y2="${fmt(hatchGap)}" stroke="${THEME.pocheHatch}" stroke-width="${fmt(thin * 0.7)}"/></pattern></defs>`
1130
+ );
1131
+ out.push(`<rect x="${fmt(vbX)}" y="${fmt(vbY)}" width="${fmt(vbW)}" height="${fmt(vbH)}" fill="${THEME.bg}"/>`);
1132
+ const ctx = { fmt, pt, xml, theme: THEME, sizes, bounds: b };
1133
+ const ops = ir.elements.flatMap((el) => {
1134
+ const def = registry.get(el.kind);
1135
+ return def ? def.render(el, ctx) : [];
1136
+ });
1137
+ for (const pass of RENDER_PASSES) {
1138
+ for (const op of ops) if (op.pass === pass) out.push(op.svg);
1139
+ }
1140
+ out.push(northArrow(ir, b, margin, refDim));
1141
+ out.push(scaleBar(b, margin, refDim, thin));
1142
+ const tb = titleBlock(ir, b, margin, refDim, thin);
1143
+ if (tb) out.push(tb);
1144
+ out.push("</svg>");
1145
+ return out.join("\n");
1146
+ }
1147
+ function northArrow(ir, b, margin, refDim) {
1148
+ const r = refDim * 0.045;
1149
+ const cx = b.maxX - r;
1150
+ const cy = b.minY - margin * 0.55;
1151
+ let deg;
1152
+ switch (ir.north) {
1153
+ case "up":
1154
+ deg = 0;
1155
+ break;
1156
+ case "down":
1157
+ deg = 180;
1158
+ break;
1159
+ case "left":
1160
+ deg = 270;
1161
+ break;
1162
+ case "right":
1163
+ deg = 90;
1164
+ break;
1165
+ default:
1166
+ deg = typeof ir.north === "object" ? ir.north.deg : 0;
1167
+ }
1168
+ const fs = refDim * 0.026;
1169
+ const tri = `${fmt(cx)},${fmt(cy - r)} ${fmt(cx - r * 0.5)},${fmt(cy + r * 0.6)} ${fmt(cx)},${fmt(cy + r * 0.25)} ${fmt(cx + r * 0.5)},${fmt(cy + r * 0.6)}`;
1170
+ const rad = deg * Math.PI / 180;
1171
+ const nx = Math.sin(rad);
1172
+ const ny = -Math.cos(rad);
1173
+ const lx = cx + nx * (r + fs * 0.8);
1174
+ const ly = cy + ny * (r + fs * 0.8);
1175
+ return `<g><polygon points="${tri}" fill="${THEME.annotation}" transform="rotate(${fmt(deg)} ${fmt(cx)} ${fmt(cy)})"/><text x="${fmt(lx)}" y="${fmt(ly)}" font-size="${fmt(fs)}" fill="${THEME.annotation}" text-anchor="middle" dominant-baseline="central">N</text></g>`;
1176
+ }
1177
+ function scaleBar(b, margin, refDim, thin) {
1178
+ const barLen = niceBarLength(refDim * 0.3);
1179
+ const x0 = b.minX;
1180
+ const y0 = b.maxY + margin * 0.55;
1181
+ const hgt = refDim * 0.014;
1182
+ const fs = refDim * 0.02;
1183
+ const parts = [];
1184
+ const half = barLen / 2;
1185
+ parts.push(`<rect x="${fmt(x0)}" y="${fmt(y0)}" width="${fmt(half)}" height="${fmt(hgt)}" fill="${THEME.annotation}"/>`);
1186
+ parts.push(
1187
+ `<rect x="${fmt(x0 + half)}" y="${fmt(y0)}" width="${fmt(half)}" height="${fmt(hgt)}" fill="none" stroke="${THEME.annotation}" stroke-width="${fmt(thin)}"/>`
1188
+ );
1189
+ parts.push(
1190
+ `<text x="${fmt(x0)}" y="${fmt(y0 + hgt + fs)}" font-size="${fmt(fs)}" fill="${THEME.annotation}" text-anchor="start" dominant-baseline="central">0</text>`
1191
+ );
1192
+ parts.push(
1193
+ `<text x="${fmt(x0 + barLen)}" y="${fmt(y0 + hgt + fs)}" font-size="${fmt(fs)}" fill="${THEME.annotation}" text-anchor="middle" dominant-baseline="central">${barLen / 1e3} m</text>`
1194
+ );
1195
+ return `<g>${parts.join("")}</g>`;
1196
+ }
1197
+ function titleBlock(ir, b, margin, refDim, thin) {
1198
+ const t = ir.title;
1199
+ if (!t && !ir.scale) return null;
1200
+ const boxW = refDim * 0.34;
1201
+ const boxH = margin * 0.82;
1202
+ const x0 = b.maxX - boxW;
1203
+ const y0 = b.maxY + margin * 0.15;
1204
+ const fs = refDim * 0.019;
1205
+ const pad = boxW * 0.05;
1206
+ const lines = [];
1207
+ if (t?.project) lines.push({ k: "PROJECT", v: t.project });
1208
+ if (t?.drawnBy) lines.push({ k: "DRAWN BY", v: t.drawnBy });
1209
+ if (t?.date) lines.push({ k: "DATE", v: t.date });
1210
+ if (ir.scale) lines.push({ k: "SCALE", v: ir.scale });
1211
+ const parts = [];
1212
+ parts.push(
1213
+ `<rect x="${fmt(x0)}" y="${fmt(y0)}" width="${fmt(boxW)}" height="${fmt(boxH)}" fill="none" stroke="${THEME.annotation}" stroke-width="${fmt(thin)}"/>`
1214
+ );
1215
+ const rowH = boxH / Math.max(lines.length, 1);
1216
+ lines.forEach((ln, i) => {
1217
+ const ly = y0 + rowH * (i + 0.5);
1218
+ parts.push(
1219
+ `<text x="${fmt(x0 + pad)}" y="${fmt(ly)}" font-size="${fmt(fs * 0.8)}" fill="${THEME.annotationMuted}" dominant-baseline="central">${xml(ln.k)}</text>`
1220
+ );
1221
+ parts.push(
1222
+ `<text x="${fmt(x0 + boxW - pad)}" y="${fmt(ly)}" font-size="${fmt(fs)}" fill="${THEME.annotation}" text-anchor="end" dominant-baseline="central">${xml(ln.v)}</text>`
1223
+ );
1224
+ if (i > 0)
1225
+ parts.push(
1226
+ `<line x1="${fmt(x0)}" y1="${fmt(y0 + rowH * i)}" x2="${fmt(x0 + boxW)}" y2="${fmt(y0 + rowH * i)}" stroke="${THEME.annotation}" stroke-width="${fmt(thin * 0.5)}"/>`
1227
+ );
1228
+ });
1229
+ return `<g>${parts.join("")}</g>`;
1230
+ }
1231
+
1232
+ // src/diagnostics.ts
1233
+ function offsetToLineCol(source, offset) {
1234
+ const o = Math.max(0, Math.min(offset, source.length));
1235
+ let line = 1;
1236
+ let col = 1;
1237
+ for (let k = 0; k < o; k++) {
1238
+ if (source[k] === "\n") {
1239
+ line++;
1240
+ col = 1;
1241
+ } else {
1242
+ col++;
1243
+ }
1244
+ }
1245
+ return { line, col };
1246
+ }
1247
+ function lineStart(source, offset) {
1248
+ let k = Math.max(0, Math.min(offset, source.length));
1249
+ while (k > 0 && source[k - 1] !== "\n") k--;
1250
+ return k;
1251
+ }
1252
+ function lineEnd(source, offset) {
1253
+ let k = Math.max(0, Math.min(offset, source.length));
1254
+ while (k < source.length && source[k] !== "\n") k++;
1255
+ return k;
1256
+ }
1257
+ function formatDiagnostic(source, d) {
1258
+ const codeTag = d.code ? `[${d.code}]` : "";
1259
+ const header = `${d.severity}${codeTag}: ${d.message}`;
1260
+ const lines = [header];
1261
+ if (d.span) {
1262
+ const { line, col } = offsetToLineCol(source, d.span.start);
1263
+ const ls = lineStart(source, d.span.start);
1264
+ const le = lineEnd(source, d.span.start);
1265
+ const srcLine = source.slice(ls, le);
1266
+ const caretStart = d.span.start - ls;
1267
+ const caretEnd = Math.min(d.span.end, le) - ls;
1268
+ const caretLen = Math.max(1, caretEnd - caretStart);
1269
+ const gutter = String(line);
1270
+ const pad = " ".repeat(gutter.length);
1271
+ lines.push(`${pad} --> ${line}:${col}`);
1272
+ lines.push(`${pad} |`);
1273
+ lines.push(`${gutter} | ${srcLine}`);
1274
+ lines.push(`${pad} | ${" ".repeat(caretStart)}${"^".repeat(caretLen)}`);
1275
+ for (const hint of d.hints ?? []) {
1276
+ lines.push(`${pad} = help: ${hint}`);
1277
+ }
1278
+ } else {
1279
+ for (const hint of d.hints ?? []) {
1280
+ lines.push(` = help: ${hint}`);
1281
+ }
1282
+ }
1283
+ return lines.join("\n");
1284
+ }
1285
+
1286
+ // src/index.ts
1287
+ var cache = /* @__PURE__ */ new Map();
1288
+ var CACHE_MAX = 64;
1289
+ function compile(source, opts = {}) {
1290
+ const key = JSON.stringify([source, opts.width ?? null]);
1291
+ if (!opts.noCache) {
1292
+ const hit = cache.get(key);
1293
+ if (hit) return hit;
1294
+ }
1295
+ const result = compileUncached(source, opts);
1296
+ if (!opts.noCache) {
1297
+ if (cache.size >= CACHE_MAX) {
1298
+ const oldest = cache.keys().next().value;
1299
+ if (oldest !== void 0) cache.delete(oldest);
1300
+ }
1301
+ cache.set(key, result);
1302
+ }
1303
+ return result;
1304
+ }
1305
+ function toLegacy(source, d) {
1306
+ if (!d.span) return { message: d.message };
1307
+ const { line, col } = offsetToLineCol(source, d.span.start);
1308
+ return { message: d.message, line, col };
1309
+ }
1310
+ function compileUncached(source, opts) {
1311
+ const { plan, diagnostics: parseDiags } = parse(source);
1312
+ const resolved = plan ? resolve(plan) : null;
1313
+ const diagnostics = resolved ? [...parseDiags, ...resolved.diagnostics] : [...parseDiags];
1314
+ const errs = diagnostics.filter((d) => d.severity === "error");
1315
+ const errors = errs.map((d) => toLegacy(source, d));
1316
+ const warnings = diagnostics.filter((d) => d.severity === "warning").map((d) => toLegacy(source, d));
1317
+ const svg = resolved && errs.length === 0 ? render(resolved.ir, opts) : "";
1318
+ return { svg, errors, warnings, diagnostics, ast: plan };
1319
+ }
1320
+ function clearCache() {
1321
+ cache.clear();
1322
+ }
1323
+
1324
+ export {
1325
+ offsetToLineCol,
1326
+ formatDiagnostic,
1327
+ compile,
1328
+ clearCache
1329
+ };
1330
+ //# sourceMappingURL=chunk-3YUQPQPZ.js.map