@chanmeng666/archlang 0.1.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,997 @@
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, extra) => tokens.push({ type, value, line: startLine, col: startCol, ...extra });
23
+ while (i < src.length) {
24
+ const c = peek();
25
+ const startLine = line;
26
+ const startCol = col;
27
+ if (c === " " || c === " " || c === "\r" || c === "\n") {
28
+ advance();
29
+ continue;
30
+ }
31
+ if (c === "#") {
32
+ while (i < src.length && peek() !== "\n") advance();
33
+ continue;
34
+ }
35
+ if (c === '"') {
36
+ advance();
37
+ let value = "";
38
+ let terminated = false;
39
+ while (i < src.length) {
40
+ const ch = peek();
41
+ if (ch === "\\") {
42
+ advance();
43
+ const esc = advance();
44
+ value += esc === "n" ? "\n" : esc;
45
+ continue;
46
+ }
47
+ if (ch === '"') {
48
+ advance();
49
+ terminated = true;
50
+ break;
51
+ }
52
+ if (ch === "\n") break;
53
+ value += advance();
54
+ }
55
+ if (!terminated) {
56
+ errors.push({ message: "Unterminated string literal", line: startLine, col: startCol });
57
+ }
58
+ push("string", value, startLine, startCol);
59
+ continue;
60
+ }
61
+ if (c === "(") {
62
+ advance();
63
+ push("lparen", "(", startLine, startCol);
64
+ continue;
65
+ }
66
+ if (c === ")") {
67
+ advance();
68
+ push("rparen", ")", startLine, startCol);
69
+ continue;
70
+ }
71
+ if (c === "{") {
72
+ advance();
73
+ push("lcurly", "{", startLine, startCol);
74
+ continue;
75
+ }
76
+ if (c === "}") {
77
+ advance();
78
+ push("rcurly", "}", startLine, startCol);
79
+ continue;
80
+ }
81
+ if (c === ",") {
82
+ advance();
83
+ push("comma", ",", startLine, startCol);
84
+ continue;
85
+ }
86
+ if (c === "=") {
87
+ advance();
88
+ push("equals", "=", startLine, startCol);
89
+ continue;
90
+ }
91
+ if (c === ":") {
92
+ advance();
93
+ push("colon", ":", startLine, startCol);
94
+ continue;
95
+ }
96
+ if (c === "-" && peek(1) === ">") {
97
+ advance();
98
+ advance();
99
+ push("arrow", "->", startLine, startCol);
100
+ continue;
101
+ }
102
+ if (isDigit(c) || c === "-" && isDigit(peek(1)) || c === "." && isDigit(peek(1))) {
103
+ let raw = "";
104
+ if (c === "-") raw += advance();
105
+ while (isDigit(peek())) raw += advance();
106
+ if (peek() === ".") {
107
+ raw += advance();
108
+ while (isDigit(peek())) raw += advance();
109
+ }
110
+ const first = parseFloat(raw);
111
+ if (peek() === "x" && (isDigit(peek(1)) || peek(1) === "-" && isDigit(peek(2)) || peek(1) === "." && isDigit(peek(2)))) {
112
+ advance();
113
+ let raw2 = "";
114
+ if (peek() === "-") raw2 += advance();
115
+ while (isDigit(peek())) raw2 += advance();
116
+ if (peek() === ".") {
117
+ raw2 += advance();
118
+ while (isDigit(peek())) raw2 += advance();
119
+ }
120
+ const second = parseFloat(raw2);
121
+ push("dimension", `${raw}x${raw2}`, startLine, startCol, { num: first, num2: second });
122
+ continue;
123
+ }
124
+ push("number", raw, startLine, startCol, { num: first });
125
+ continue;
126
+ }
127
+ if (isIdentStart(c)) {
128
+ let value = "";
129
+ while (i < src.length && isIdentPart(peek())) value += advance();
130
+ push("ident", value, startLine, startCol);
131
+ continue;
132
+ }
133
+ errors.push({ message: `Unexpected character ${JSON.stringify(c)}`, line: startLine, col: startCol });
134
+ advance();
135
+ }
136
+ push("eof", "", line, col);
137
+ return { tokens, errors };
138
+ }
139
+
140
+ // src/parser.ts
141
+ var ParseError = class extends Error {
142
+ constructor(message, line, col) {
143
+ super(message);
144
+ this.message = message;
145
+ this.line = line;
146
+ this.col = col;
147
+ }
148
+ message;
149
+ line;
150
+ col;
151
+ };
152
+ function parse(src) {
153
+ const { tokens, errors: lexErrors } = lex(src);
154
+ if (lexErrors.length > 0) {
155
+ return { errors: [lexErrors[0]] };
156
+ }
157
+ try {
158
+ const p = new Parser(tokens);
159
+ const plan = p.parsePlan();
160
+ return { plan, errors: [] };
161
+ } catch (e) {
162
+ if (e instanceof ParseError) {
163
+ return { errors: [{ message: e.message, line: e.line, col: e.col }] };
164
+ }
165
+ throw e;
166
+ }
167
+ }
168
+ var Parser = class {
169
+ constructor(toks) {
170
+ this.toks = toks;
171
+ }
172
+ toks;
173
+ pos = 0;
174
+ peek(o = 0) {
175
+ return this.toks[Math.min(this.pos + o, this.toks.length - 1)];
176
+ }
177
+ next() {
178
+ return this.toks[Math.min(this.pos++, this.toks.length - 1)];
179
+ }
180
+ fail(msg, t = this.peek()) {
181
+ throw new ParseError(msg, t.line, t.col);
182
+ }
183
+ isKeyword(kw, o = 0) {
184
+ const t = this.peek(o);
185
+ return t.type === "ident" && t.value === kw;
186
+ }
187
+ eatKeyword(kw) {
188
+ const t = this.peek();
189
+ if (t.type !== "ident" || t.value !== kw) this.fail(`Expected "${kw}" but found ${describe(t)}`);
190
+ return this.next();
191
+ }
192
+ eat(type) {
193
+ const t = this.peek();
194
+ if (t.type !== type) this.fail(`Expected ${type} but found ${describe(t)}`);
195
+ return this.next();
196
+ }
197
+ eatIdent() {
198
+ return this.eat("ident");
199
+ }
200
+ eatNumber() {
201
+ const t = this.eat("number");
202
+ return t.num;
203
+ }
204
+ eatString() {
205
+ return this.eat("string").value;
206
+ }
207
+ parsePlan() {
208
+ this.eatKeyword("plan");
209
+ const name = this.eatString();
210
+ this.eat("lcurly");
211
+ const plan = {
212
+ name,
213
+ units: "mm",
214
+ grid: 0,
215
+ north: "up",
216
+ walls: [],
217
+ rooms: [],
218
+ doors: [],
219
+ windows: [],
220
+ furniture: [],
221
+ dims: []
222
+ };
223
+ while (!this.isType("rcurly") && !this.isType("eof")) {
224
+ const t = this.peek();
225
+ if (t.type !== "ident") this.fail(`Expected a statement but found ${describe(t)}`);
226
+ switch (t.value) {
227
+ case "units": {
228
+ this.next();
229
+ const u = this.eatIdent().value;
230
+ if (u !== "mm") this.fail(`Unsupported units "${u}" (only "mm" is supported)`, t);
231
+ plan.units = "mm";
232
+ break;
233
+ }
234
+ case "grid":
235
+ this.next();
236
+ plan.grid = this.eatNumber();
237
+ break;
238
+ case "scale": {
239
+ this.next();
240
+ const a = this.eatNumber();
241
+ this.eat("colon");
242
+ const b = this.eatNumber();
243
+ plan.scale = `${a}:${b}`;
244
+ break;
245
+ }
246
+ case "north":
247
+ this.next();
248
+ plan.north = this.parseNorth();
249
+ break;
250
+ case "wall":
251
+ plan.walls.push(this.parseWall());
252
+ break;
253
+ case "room":
254
+ plan.rooms.push(this.parseRoom());
255
+ break;
256
+ case "door":
257
+ plan.doors.push(this.parseDoor());
258
+ break;
259
+ case "window":
260
+ plan.windows.push(this.parseWindow());
261
+ break;
262
+ case "furniture":
263
+ plan.furniture.push(this.parseFurniture());
264
+ break;
265
+ case "dim":
266
+ plan.dims.push(this.parseDim());
267
+ break;
268
+ case "title":
269
+ plan.title = this.parseTitle();
270
+ break;
271
+ default:
272
+ this.fail(`Unknown statement "${t.value}"`, t);
273
+ }
274
+ }
275
+ this.eat("rcurly");
276
+ return plan;
277
+ }
278
+ isType(type) {
279
+ return this.peek().type === type;
280
+ }
281
+ /** Optional `id=<ident>` prefix; returns "" when absent. */
282
+ parseIdOpt() {
283
+ if (this.isKeyword("id")) {
284
+ this.next();
285
+ this.eat("equals");
286
+ return this.eatIdent().value;
287
+ }
288
+ return "";
289
+ }
290
+ parseNorth() {
291
+ const t = this.peek();
292
+ if (t.type === "number") {
293
+ this.next();
294
+ return { deg: t.num };
295
+ }
296
+ if (t.type === "ident" && ["up", "down", "left", "right"].includes(t.value)) {
297
+ this.next();
298
+ return t.value;
299
+ }
300
+ this.fail(`Expected a north direction (up|down|left|right|<degrees>) but found ${describe(t)}`);
301
+ }
302
+ parsePoint() {
303
+ this.eat("lparen");
304
+ const x = this.eatNumber();
305
+ this.eat("comma");
306
+ const y = this.eatNumber();
307
+ this.eat("rparen");
308
+ return { x, y };
309
+ }
310
+ parseWall() {
311
+ const kw = this.eatKeyword("wall");
312
+ const id = this.parseIdOpt();
313
+ const kind = this.eatIdent().value;
314
+ this.eatKeyword("thickness");
315
+ const thickness = this.eatNumber();
316
+ this.eat("lcurly");
317
+ const points = [];
318
+ let closed = false;
319
+ while (!this.isType("rcurly") && !this.isType("eof")) {
320
+ if (this.isKeyword("close")) {
321
+ this.next();
322
+ closed = true;
323
+ break;
324
+ }
325
+ if (this.isType("lparen")) {
326
+ points.push(this.parsePoint());
327
+ continue;
328
+ }
329
+ this.fail(`Expected a point "(x,y)" or "close" in wall body but found ${describe(this.peek())}`);
330
+ }
331
+ this.eat("rcurly");
332
+ if (points.length < 2) this.fail("A wall needs at least two points", kw);
333
+ return { id, kind, thickness, points, closed, line: kw.line };
334
+ }
335
+ parseRoom() {
336
+ const kw = this.eatKeyword("room");
337
+ const id = this.parseIdOpt();
338
+ this.eatKeyword("at");
339
+ const at = this.parsePoint();
340
+ this.eatKeyword("size");
341
+ const dim = this.eat("dimension");
342
+ const node = { id, at, size: { w: dim.num, h: dim.num2 }, line: kw.line };
343
+ if (this.isKeyword("label")) {
344
+ this.next();
345
+ node.label = this.eatString();
346
+ }
347
+ return node;
348
+ }
349
+ parseDoor() {
350
+ const kw = this.eatKeyword("door");
351
+ const id = this.parseIdOpt();
352
+ this.eatKeyword("at");
353
+ const at = this.parsePoint();
354
+ this.eatKeyword("width");
355
+ const width = this.eatNumber();
356
+ const node = { id, at, width, hinge: "left", swing: "in", line: kw.line };
357
+ if (this.isKeyword("wall")) {
358
+ this.next();
359
+ node.wall = this.eatIdent().value;
360
+ }
361
+ if (this.isKeyword("hinge")) {
362
+ this.next();
363
+ const h = this.eatIdent().value;
364
+ if (h !== "left" && h !== "right") this.fail(`Expected hinge "left" or "right" but found "${h}"`);
365
+ node.hinge = h;
366
+ }
367
+ if (this.isKeyword("swing")) {
368
+ this.next();
369
+ const s = this.eatIdent().value;
370
+ if (s !== "in" && s !== "out") this.fail(`Expected swing "in" or "out" but found "${s}"`);
371
+ node.swing = s;
372
+ }
373
+ return node;
374
+ }
375
+ parseWindow() {
376
+ const kw = this.eatKeyword("window");
377
+ const id = this.parseIdOpt();
378
+ this.eatKeyword("at");
379
+ const at = this.parsePoint();
380
+ this.eatKeyword("width");
381
+ const width = this.eatNumber();
382
+ const node = { id, at, width, line: kw.line };
383
+ if (this.isKeyword("wall")) {
384
+ this.next();
385
+ node.wall = this.eatIdent().value;
386
+ }
387
+ return node;
388
+ }
389
+ parseFurniture() {
390
+ const kw = this.eatKeyword("furniture");
391
+ const id = this.parseIdOpt();
392
+ const kind = this.eatIdent().value;
393
+ this.eatKeyword("at");
394
+ const at = this.parsePoint();
395
+ this.eatKeyword("size");
396
+ const dim = this.eat("dimension");
397
+ const node = { id, kind, at, size: { w: dim.num, h: dim.num2 }, line: kw.line };
398
+ if (this.isKeyword("label")) {
399
+ this.next();
400
+ node.label = this.eatString();
401
+ }
402
+ return node;
403
+ }
404
+ parseDim() {
405
+ const kw = this.eatKeyword("dim");
406
+ const from = this.parsePoint();
407
+ this.eat("arrow");
408
+ const to = this.parsePoint();
409
+ const node = { id: "", from, to, offset: 300, line: kw.line };
410
+ if (this.isKeyword("offset")) {
411
+ this.next();
412
+ node.offset = this.eatNumber();
413
+ }
414
+ if (this.isKeyword("text")) {
415
+ this.next();
416
+ node.text = this.eatString();
417
+ }
418
+ return node;
419
+ }
420
+ parseTitle() {
421
+ const kw = this.eatKeyword("title");
422
+ this.eat("lcurly");
423
+ const node = { line: kw.line };
424
+ while (!this.isType("rcurly") && !this.isType("eof")) {
425
+ const t = this.peek();
426
+ if (t.type !== "ident") this.fail(`Expected a title field but found ${describe(t)}`);
427
+ switch (t.value) {
428
+ case "project":
429
+ this.next();
430
+ node.project = this.eatString();
431
+ break;
432
+ case "drawn_by":
433
+ this.next();
434
+ node.drawnBy = this.eatString();
435
+ break;
436
+ case "date":
437
+ this.next();
438
+ node.date = this.eatString();
439
+ break;
440
+ default:
441
+ this.fail(`Unknown title field "${t.value}"`, t);
442
+ }
443
+ }
444
+ this.eat("rcurly");
445
+ return node;
446
+ }
447
+ };
448
+ function describe(t) {
449
+ if (t.type === "eof") return "end of input";
450
+ if (t.type === "string") return `string ${JSON.stringify(t.value)}`;
451
+ return `"${t.value}"`;
452
+ }
453
+
454
+ // src/geometry.ts
455
+ var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
456
+ var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
457
+ var mul = (v, s) => ({ x: v.x * s, y: v.y * s });
458
+ var length = (v) => Math.hypot(v.x, v.y);
459
+ function unit(v) {
460
+ const l = length(v);
461
+ return l === 0 ? { x: 0, y: 0 } : { x: v.x / l, y: v.y / l };
462
+ }
463
+ var normal = (v) => ({ x: -v.y, y: v.x });
464
+ var emptyBounds = () => ({
465
+ minX: Infinity,
466
+ minY: Infinity,
467
+ maxX: -Infinity,
468
+ maxY: -Infinity
469
+ });
470
+ function extendBounds(b, x, y) {
471
+ if (x < b.minX) b.minX = x;
472
+ if (y < b.minY) b.minY = y;
473
+ if (x > b.maxX) b.maxX = x;
474
+ if (y > b.maxY) b.maxY = y;
475
+ }
476
+ function distPointToSegment(p, a, b) {
477
+ const abx = b.x - a.x;
478
+ const aby = b.y - a.y;
479
+ const apx = p.x - a.x;
480
+ const apy = p.y - a.y;
481
+ const len2 = abx * abx + aby * aby;
482
+ let t = len2 === 0 ? 0 : (apx * abx + apy * aby) / len2;
483
+ t = Math.max(0, Math.min(1, t));
484
+ const cx = a.x + t * abx;
485
+ const cy = a.y + t * aby;
486
+ return Math.hypot(p.x - cx, p.y - cy);
487
+ }
488
+ function rectCorners(x, y, w, h) {
489
+ return [
490
+ { x, y },
491
+ { x: x + w, y },
492
+ { x: x + w, y: y + h },
493
+ { x, y: y + h }
494
+ ];
495
+ }
496
+ function segmentRectangle(a, b, thickness) {
497
+ const d = unit(sub(b, a));
498
+ const n = normal(d);
499
+ const half = thickness / 2;
500
+ const a2 = add(a, mul(d, -half));
501
+ const b2 = add(b, mul(d, half));
502
+ return [
503
+ add(a2, mul(n, half)),
504
+ add(b2, mul(n, half)),
505
+ add(b2, mul(n, -half)),
506
+ add(a2, mul(n, -half))
507
+ ];
508
+ }
509
+ function wallSegments(plan) {
510
+ const segs = [];
511
+ for (const w of plan.walls) {
512
+ for (let k = 0; k < w.points.length - 1; k++) {
513
+ segs.push({ a: w.points[k], b: w.points[k + 1], thickness: w.thickness, kind: w.kind });
514
+ }
515
+ if (w.closed && w.points.length > 2) {
516
+ segs.push({ a: w.points[w.points.length - 1], b: w.points[0], thickness: w.thickness, kind: w.kind });
517
+ }
518
+ }
519
+ return segs;
520
+ }
521
+ function hostSegment(plan, at, wallRef) {
522
+ const walls = wallRef ? plan.walls.filter((w) => w.id === wallRef || w.kind === wallRef) : plan.walls;
523
+ let best = null;
524
+ let bestDist = Infinity;
525
+ for (const w of walls) {
526
+ const segs = [];
527
+ for (let k = 0; k < w.points.length - 1; k++) segs.push([w.points[k], w.points[k + 1]]);
528
+ if (w.closed && w.points.length > 2) segs.push([w.points[w.points.length - 1], w.points[0]]);
529
+ for (const [a, b] of segs) {
530
+ const dist = distPointToSegment(at, a, b);
531
+ if (dist < bestDist) {
532
+ bestDist = dist;
533
+ best = { a, b, thickness: w.thickness, kind: w.kind };
534
+ }
535
+ }
536
+ }
537
+ return best;
538
+ }
539
+ function planBounds(plan) {
540
+ const b = emptyBounds();
541
+ for (const seg of wallSegments(plan)) {
542
+ for (const c of segmentRectangle(seg.a, seg.b, seg.thickness)) extendBounds(b, c.x, c.y);
543
+ }
544
+ for (const r of plan.rooms) {
545
+ extendBounds(b, r.at.x, r.at.y);
546
+ extendBounds(b, r.at.x + r.size.w, r.at.y + r.size.h);
547
+ }
548
+ for (const f of plan.furniture) {
549
+ extendBounds(b, f.at.x, f.at.y);
550
+ extendBounds(b, f.at.x + f.size.w, f.at.y + f.size.h);
551
+ }
552
+ for (const d of plan.dims) {
553
+ extendBounds(b, d.from.x, d.from.y);
554
+ extendBounds(b, d.to.x, d.to.y);
555
+ }
556
+ if (!isFinite(b.minX)) {
557
+ return { minX: 0, minY: 0, maxX: 1e3, maxY: 1e3 };
558
+ }
559
+ return b;
560
+ }
561
+
562
+ // src/validate.ts
563
+ function validate(plan) {
564
+ const errors = [];
565
+ const warnings = [];
566
+ const g = plan.grid;
567
+ const snap = (v) => g > 0 ? Math.round(v / g) * g : v;
568
+ const snapPt = (p) => ({ x: snap(p.x), y: snap(p.y) });
569
+ for (const w of plan.walls) {
570
+ w.points = w.points.map(snapPt);
571
+ w.thickness = snap(w.thickness) || w.thickness;
572
+ }
573
+ for (const r of plan.rooms) {
574
+ r.at = snapPt(r.at);
575
+ r.size = { w: snap(r.size.w), h: snap(r.size.h) };
576
+ }
577
+ for (const f of plan.furniture) {
578
+ f.at = snapPt(f.at);
579
+ f.size = { w: snap(f.size.w), h: snap(f.size.h) };
580
+ }
581
+ for (const d of plan.doors) {
582
+ d.at = snapPt(d.at);
583
+ d.width = snap(d.width) || d.width;
584
+ }
585
+ for (const win of plan.windows) {
586
+ win.at = snapPt(win.at);
587
+ win.width = snap(win.width) || win.width;
588
+ }
589
+ for (const dm of plan.dims) {
590
+ dm.from = snapPt(dm.from);
591
+ dm.to = snapPt(dm.to);
592
+ }
593
+ const seen = /* @__PURE__ */ new Set();
594
+ const assign = (provided, prefix, idx, line) => {
595
+ if (provided) {
596
+ if (seen.has(provided)) {
597
+ errors.push({ message: `Duplicate id "${provided}"`, line });
598
+ }
599
+ seen.add(provided);
600
+ return provided;
601
+ }
602
+ let auto = `${prefix}_${idx}`;
603
+ while (seen.has(auto)) auto = `${auto}_`;
604
+ seen.add(auto);
605
+ return auto;
606
+ };
607
+ plan.walls.forEach((w, i) => w.id = assign(w.id, w.kind || "wall", i + 1, w.line));
608
+ plan.rooms.forEach((r, i) => r.id = assign(r.id, "room", i + 1, r.line));
609
+ plan.doors.forEach((d, i) => d.id = assign(d.id, "door", i + 1, d.line));
610
+ plan.windows.forEach((w, i) => w.id = assign(w.id, "window", i + 1, w.line));
611
+ plan.furniture.forEach((f, i) => f.id = assign(f.id, f.kind || "furniture", i + 1, f.line));
612
+ plan.dims.forEach((d, i) => d.id = assign(d.id, "dim", i + 1, d.line));
613
+ for (const r of plan.rooms) {
614
+ if (r.size.w <= 0 || r.size.h <= 0)
615
+ errors.push({ message: `Room "${r.id}" must have a positive size`, line: r.line });
616
+ }
617
+ for (const f of plan.furniture) {
618
+ if (f.size.w <= 0 || f.size.h <= 0)
619
+ errors.push({ message: `Furniture "${f.id}" must have a positive size`, line: f.line });
620
+ }
621
+ for (const d of plan.doors) {
622
+ if (d.width <= 0) errors.push({ message: `Door "${d.id}" must have a positive width`, line: d.line });
623
+ }
624
+ for (const w of plan.windows) {
625
+ if (w.width <= 0) errors.push({ message: `Window "${w.id}" must have a positive width`, line: w.line });
626
+ }
627
+ for (const w of plan.walls) {
628
+ if (w.thickness <= 0)
629
+ errors.push({ message: `Wall "${w.id}" must have a positive thickness`, line: w.line });
630
+ }
631
+ if (plan.walls.length === 0 && plan.rooms.length === 0 && plan.furniture.length === 0) {
632
+ warnings.push({ message: "Plan has no walls, rooms, or furniture \u2014 nothing to draw" });
633
+ }
634
+ const onSomeWall = (at, wallRef) => {
635
+ const candidates = wallRef ? plan.walls.filter((w) => w.id === wallRef || w.kind === wallRef) : plan.walls;
636
+ for (const w of candidates) {
637
+ const tol = w.thickness / 2 + Math.max(w.thickness, 1);
638
+ for (let k = 0; k < w.points.length - 1; k++) {
639
+ if (distPointToSegment(at, w.points[k], w.points[k + 1]) <= tol) return true;
640
+ }
641
+ if (w.closed && w.points.length > 2) {
642
+ if (distPointToSegment(at, w.points[w.points.length - 1], w.points[0]) <= tol) return true;
643
+ }
644
+ }
645
+ return false;
646
+ };
647
+ for (const d of plan.doors) {
648
+ if (plan.walls.length > 0 && !onSomeWall(d.at, d.wall))
649
+ warnings.push({ message: `Door "${d.id}" does not lie on any wall`, line: d.line });
650
+ }
651
+ for (const w of plan.windows) {
652
+ if (plan.walls.length > 0 && !onSomeWall(w.at, w.wall))
653
+ warnings.push({ message: `Window "${w.id}" does not lie on any wall`, line: w.line });
654
+ }
655
+ for (let a = 0; a < plan.rooms.length; a++) {
656
+ for (let b = a + 1; b < plan.rooms.length; b++) {
657
+ const r1 = plan.rooms[a];
658
+ const r2 = plan.rooms[b];
659
+ 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));
660
+ 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));
661
+ if (ox > 1 && oy > 1) {
662
+ warnings.push({ message: `Rooms "${r1.id}" and "${r2.id}" overlap`, line: r2.line });
663
+ }
664
+ }
665
+ }
666
+ return { errors, warnings };
667
+ }
668
+
669
+ // src/render.ts
670
+ var THEME = {
671
+ bg: "#ffffff",
672
+ pocheBase: "#e9e4db",
673
+ pocheHatch: "#b9b1a4",
674
+ wallStroke: "#1b1b1b",
675
+ roomFill: "#fbfaf7",
676
+ roomLabel: "#222222",
677
+ areaLabel: "#7a7a7a",
678
+ furnitureStroke: "#a8a29a",
679
+ furnitureFill: "#f4f2ee",
680
+ furnitureLabel: "#9a948c",
681
+ opening: "#ffffff",
682
+ doorLeaf: "#555555",
683
+ windowPane: "#3a6ea5",
684
+ dim: "#0E5484",
685
+ annotation: "#333333",
686
+ annotationMuted: "#888888"
687
+ };
688
+ function fmt(v) {
689
+ const r = Math.round(v * 100) / 100;
690
+ return Object.is(r, -0) ? "0" : String(r);
691
+ }
692
+ var pt = (p) => `${fmt(p.x)},${fmt(p.y)}`;
693
+ function xml(s) {
694
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
695
+ }
696
+ var NICE_LENGTHS = [500, 1e3, 2e3, 5e3, 1e4, 2e4, 5e4, 1e5];
697
+ function niceBarLength(target) {
698
+ let best = NICE_LENGTHS[0];
699
+ for (const v of NICE_LENGTHS) if (v <= target) best = v;
700
+ return best;
701
+ }
702
+ function render(plan, opts = {}) {
703
+ const b = planBounds(plan);
704
+ const drawW = b.maxX - b.minX;
705
+ const drawH = b.maxY - b.minY;
706
+ const refDim = Math.max(drawW, drawH, 1);
707
+ const wallStroke = refDim * 28e-4;
708
+ const thin = refDim * 16e-4;
709
+ const roomFont = refDim * 0.03;
710
+ const areaFont = refDim * 0.022;
711
+ const dimFont = refDim * 0.02;
712
+ const furnFont = refDim * 0.017;
713
+ const margin = refDim * 0.17;
714
+ const hatchGap = refDim * 0.013;
715
+ const vbX = b.minX - margin;
716
+ const vbY = b.minY - margin;
717
+ const vbW = drawW + margin * 2;
718
+ const vbH = drawH + margin * 2;
719
+ const out = [];
720
+ const svgAttrs = opts.width ? `width="${fmt(opts.width)}" height="${fmt(opts.width * vbH / vbW)}"` : "";
721
+ out.push(
722
+ `<svg xmlns="http://www.w3.org/2000/svg" ${svgAttrs} viewBox="${fmt(vbX)} ${fmt(vbY)} ${fmt(vbW)} ${fmt(vbH)}" font-family="Helvetica, Arial, sans-serif">`
723
+ );
724
+ out.push(
725
+ `<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>`
726
+ );
727
+ out.push(`<rect x="${fmt(vbX)}" y="${fmt(vbY)}" width="${fmt(vbW)}" height="${fmt(vbH)}" fill="${THEME.bg}"/>`);
728
+ for (const r of plan.rooms) {
729
+ const c = rectCorners(r.at.x, r.at.y, r.size.w, r.size.h);
730
+ out.push(`<polygon points="${c.map(pt).join(" ")}" fill="${THEME.roomFill}"/>`);
731
+ }
732
+ for (const f of plan.furniture) {
733
+ const c = rectCorners(f.at.x, f.at.y, f.size.w, f.size.h);
734
+ out.push(
735
+ `<polygon points="${c.map(pt).join(" ")}" fill="${THEME.furnitureFill}" stroke="${THEME.furnitureStroke}" stroke-width="${fmt(thin)}"/>`
736
+ );
737
+ if (f.label) {
738
+ const cx = f.at.x + f.size.w / 2;
739
+ const cy = f.at.y + f.size.h / 2;
740
+ out.push(
741
+ `<text x="${fmt(cx)}" y="${fmt(cy)}" font-size="${fmt(furnFont)}" fill="${THEME.furnitureLabel}" text-anchor="middle" dominant-baseline="central">${xml(f.label)}</text>`
742
+ );
743
+ }
744
+ }
745
+ const segs = wallSegments(plan);
746
+ for (const s of segs) {
747
+ const poly = segmentRectangle(s.a, s.b, s.thickness);
748
+ out.push(`<polygon points="${poly.map(pt).join(" ")}" fill="url(#poche)"/>`);
749
+ }
750
+ for (const s of segs) {
751
+ const d = unit(sub(s.b, s.a));
752
+ const n = normal(d);
753
+ const h = s.thickness / 2;
754
+ const fa1 = add(s.a, mul(n, h));
755
+ const fb1 = add(s.b, mul(n, h));
756
+ const fa2 = add(s.a, mul(n, -h));
757
+ const fb2 = add(s.b, mul(n, -h));
758
+ out.push(
759
+ `<line x1="${fmt(fa1.x)}" y1="${fmt(fa1.y)}" x2="${fmt(fb1.x)}" y2="${fmt(fb1.y)}" stroke="${THEME.wallStroke}" stroke-width="${fmt(wallStroke)}" stroke-linecap="square"/>`
760
+ );
761
+ out.push(
762
+ `<line x1="${fmt(fa2.x)}" y1="${fmt(fa2.y)}" x2="${fmt(fb2.x)}" y2="${fmt(fb2.y)}" stroke="${THEME.wallStroke}" stroke-width="${fmt(wallStroke)}" stroke-linecap="square"/>`
763
+ );
764
+ }
765
+ for (const dr of plan.doors) {
766
+ const seg = hostSegment(plan, dr.at, dr.wall);
767
+ if (!seg) continue;
768
+ const d = unit(sub(seg.b, seg.a));
769
+ const n = normal(d);
770
+ const h = seg.thickness / 2 + wallStroke;
771
+ const hw = dr.width / 2;
772
+ const cover = [
773
+ add(add(dr.at, mul(d, -hw)), mul(n, h)),
774
+ add(add(dr.at, mul(d, hw)), mul(n, h)),
775
+ add(add(dr.at, mul(d, hw)), mul(n, -h)),
776
+ add(add(dr.at, mul(d, -hw)), mul(n, -h))
777
+ ];
778
+ out.push(`<polygon points="${cover.map(pt).join(" ")}" fill="${THEME.opening}"/>`);
779
+ const hinge = dr.hinge === "left" ? add(dr.at, mul(d, -hw)) : add(dr.at, mul(d, hw));
780
+ const farJamb = dr.hinge === "left" ? add(dr.at, mul(d, hw)) : add(dr.at, mul(d, -hw));
781
+ const leafDir = dr.swing === "in" ? n : mul(n, -1);
782
+ const leafEnd = add(hinge, mul(leafDir, dr.width));
783
+ const cross = (leafEnd.x - hinge.x) * (farJamb.y - hinge.y) - (leafEnd.y - hinge.y) * (farJamb.x - hinge.x);
784
+ const sweep = cross < 0 ? 1 : 0;
785
+ out.push(
786
+ `<line x1="${fmt(hinge.x)}" y1="${fmt(hinge.y)}" x2="${fmt(leafEnd.x)}" y2="${fmt(leafEnd.y)}" stroke="${THEME.doorLeaf}" stroke-width="${fmt(thin * 1.3)}"/>`
787
+ );
788
+ out.push(
789
+ `<path d="M ${pt(leafEnd)} A ${fmt(dr.width)} ${fmt(dr.width)} 0 0 ${sweep} ${pt(farJamb)}" fill="none" stroke="${THEME.doorLeaf}" stroke-width="${fmt(thin)}" stroke-dasharray="${fmt(thin * 4)} ${fmt(thin * 3)}"/>`
790
+ );
791
+ }
792
+ for (const wn of plan.windows) {
793
+ const seg = hostSegment(plan, wn.at, wn.wall);
794
+ if (!seg) continue;
795
+ const d = unit(sub(seg.b, seg.a));
796
+ const n = normal(d);
797
+ const h = seg.thickness / 2;
798
+ const he = h + wallStroke;
799
+ const hw = wn.width / 2;
800
+ const cover = [
801
+ add(add(wn.at, mul(d, -hw)), mul(n, he)),
802
+ add(add(wn.at, mul(d, hw)), mul(n, he)),
803
+ add(add(wn.at, mul(d, hw)), mul(n, -he)),
804
+ add(add(wn.at, mul(d, -hw)), mul(n, -he))
805
+ ];
806
+ out.push(`<polygon points="${cover.map(pt).join(" ")}" fill="${THEME.opening}"/>`);
807
+ const jA = add(wn.at, mul(d, -hw));
808
+ const jB = add(wn.at, mul(d, hw));
809
+ for (const off of [h, -h]) {
810
+ const a = add(jA, mul(n, off));
811
+ const bb = add(jB, mul(n, off));
812
+ out.push(
813
+ `<line x1="${fmt(a.x)}" y1="${fmt(a.y)}" x2="${fmt(bb.x)}" y2="${fmt(bb.y)}" stroke="${THEME.wallStroke}" stroke-width="${fmt(thin)}"/>`
814
+ );
815
+ }
816
+ out.push(
817
+ `<line x1="${fmt(jA.x)}" y1="${fmt(jA.y)}" x2="${fmt(jB.x)}" y2="${fmt(jB.y)}" stroke="${THEME.windowPane}" stroke-width="${fmt(thin)}"/>`
818
+ );
819
+ }
820
+ for (const r of plan.rooms) {
821
+ const cx = r.at.x + r.size.w / 2;
822
+ const cy = r.at.y + r.size.h / 2;
823
+ const areaM2 = (r.size.w / 1e3 * (r.size.h / 1e3)).toFixed(1);
824
+ if (r.label) {
825
+ out.push(
826
+ `<text x="${fmt(cx)}" y="${fmt(cy - roomFont * 0.2)}" font-size="${fmt(roomFont)}" fill="${THEME.roomLabel}" text-anchor="middle" dominant-baseline="central" font-weight="600">${xml(r.label)}</text>`
827
+ );
828
+ }
829
+ out.push(
830
+ `<text x="${fmt(cx)}" y="${fmt(cy + (r.label ? roomFont * 0.9 : 0))}" font-size="${fmt(areaFont)}" fill="${THEME.areaLabel}" text-anchor="middle" dominant-baseline="central">${areaM2} m\xB2</text>`
831
+ );
832
+ }
833
+ for (const dm of plan.dims) {
834
+ const dir = unit(sub(dm.to, dm.from));
835
+ const n = normal(dir);
836
+ const off = mul(n, dm.offset);
837
+ const p1 = add(dm.from, off);
838
+ const p2 = add(dm.to, off);
839
+ const tick = refDim * 0.012;
840
+ out.push(
841
+ `<line x1="${fmt(dm.from.x)}" y1="${fmt(dm.from.y)}" x2="${fmt(p1.x)}" y2="${fmt(p1.y)}" stroke="${THEME.dim}" stroke-width="${fmt(thin * 0.7)}"/>`
842
+ );
843
+ out.push(
844
+ `<line x1="${fmt(dm.to.x)}" y1="${fmt(dm.to.y)}" x2="${fmt(p2.x)}" y2="${fmt(p2.y)}" stroke="${THEME.dim}" stroke-width="${fmt(thin * 0.7)}"/>`
845
+ );
846
+ out.push(
847
+ `<line x1="${fmt(p1.x)}" y1="${fmt(p1.y)}" x2="${fmt(p2.x)}" y2="${fmt(p2.y)}" stroke="${THEME.dim}" stroke-width="${fmt(thin)}"/>`
848
+ );
849
+ for (const p of [p1, p2]) {
850
+ const t1 = add(p, mul(unit({ x: dir.x + n.x, y: dir.y + n.y }), tick));
851
+ const t2 = add(p, mul(unit({ x: dir.x + n.x, y: dir.y + n.y }), -tick));
852
+ out.push(
853
+ `<line x1="${fmt(t1.x)}" y1="${fmt(t1.y)}" x2="${fmt(t2.x)}" y2="${fmt(t2.y)}" stroke="${THEME.dim}" stroke-width="${fmt(thin)}"/>`
854
+ );
855
+ }
856
+ const mid = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };
857
+ const tp = add(mid, mul(n, dimFont * 0.7));
858
+ let angle = Math.atan2(dir.y, dir.x) * 180 / Math.PI;
859
+ if (angle > 90) angle -= 180;
860
+ if (angle < -90) angle += 180;
861
+ const label = dm.text ?? String(Math.round(length(sub(dm.to, dm.from))));
862
+ out.push(
863
+ `<text x="${fmt(tp.x)}" y="${fmt(tp.y)}" font-size="${fmt(dimFont)}" fill="${THEME.dim}" text-anchor="middle" dominant-baseline="central" transform="rotate(${fmt(angle)} ${fmt(tp.x)} ${fmt(tp.y)})">${xml(label)}</text>`
864
+ );
865
+ }
866
+ out.push(northArrow(plan, b, margin, refDim));
867
+ out.push(scaleBar(b, margin, refDim, thin));
868
+ const tb = titleBlock(plan, b, margin, refDim, thin);
869
+ if (tb) out.push(tb);
870
+ out.push("</svg>");
871
+ return out.join("\n");
872
+ }
873
+ function northArrow(plan, b, margin, refDim) {
874
+ const r = refDim * 0.045;
875
+ const cx = b.maxX - r;
876
+ const cy = b.minY - margin * 0.55;
877
+ let deg;
878
+ switch (plan.north) {
879
+ case "up":
880
+ deg = 0;
881
+ break;
882
+ case "down":
883
+ deg = 180;
884
+ break;
885
+ case "left":
886
+ deg = 270;
887
+ break;
888
+ case "right":
889
+ deg = 90;
890
+ break;
891
+ default:
892
+ deg = typeof plan.north === "object" ? plan.north.deg : 0;
893
+ }
894
+ const fs = refDim * 0.026;
895
+ 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)}`;
896
+ const rad = deg * Math.PI / 180;
897
+ const nx = Math.sin(rad);
898
+ const ny = -Math.cos(rad);
899
+ const lx = cx + nx * (r + fs * 0.8);
900
+ const ly = cy + ny * (r + fs * 0.8);
901
+ 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>`;
902
+ }
903
+ function scaleBar(b, margin, refDim, thin) {
904
+ const barLen = niceBarLength(refDim * 0.3);
905
+ const x0 = b.minX;
906
+ const y0 = b.maxY + margin * 0.55;
907
+ const hgt = refDim * 0.014;
908
+ const fs = refDim * 0.02;
909
+ const parts = [];
910
+ const half = barLen / 2;
911
+ parts.push(`<rect x="${fmt(x0)}" y="${fmt(y0)}" width="${fmt(half)}" height="${fmt(hgt)}" fill="${THEME.annotation}"/>`);
912
+ parts.push(
913
+ `<rect x="${fmt(x0 + half)}" y="${fmt(y0)}" width="${fmt(half)}" height="${fmt(hgt)}" fill="none" stroke="${THEME.annotation}" stroke-width="${fmt(thin)}"/>`
914
+ );
915
+ parts.push(
916
+ `<text x="${fmt(x0)}" y="${fmt(y0 + hgt + fs)}" font-size="${fmt(fs)}" fill="${THEME.annotation}" text-anchor="start" dominant-baseline="central">0</text>`
917
+ );
918
+ parts.push(
919
+ `<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>`
920
+ );
921
+ return `<g>${parts.join("")}</g>`;
922
+ }
923
+ function titleBlock(plan, b, margin, refDim, thin) {
924
+ const t = plan.title;
925
+ if (!t && !plan.scale) return null;
926
+ const boxW = refDim * 0.34;
927
+ const boxH = margin * 0.82;
928
+ const x0 = b.maxX - boxW;
929
+ const y0 = b.maxY + margin * 0.15;
930
+ const fs = refDim * 0.019;
931
+ const pad = boxW * 0.05;
932
+ const lines = [];
933
+ if (t?.project) lines.push({ k: "PROJECT", v: t.project });
934
+ if (t?.drawnBy) lines.push({ k: "DRAWN BY", v: t.drawnBy });
935
+ if (t?.date) lines.push({ k: "DATE", v: t.date });
936
+ if (plan.scale) lines.push({ k: "SCALE", v: plan.scale });
937
+ const parts = [];
938
+ parts.push(
939
+ `<rect x="${fmt(x0)}" y="${fmt(y0)}" width="${fmt(boxW)}" height="${fmt(boxH)}" fill="none" stroke="${THEME.annotation}" stroke-width="${fmt(thin)}"/>`
940
+ );
941
+ const rowH = boxH / Math.max(lines.length, 1);
942
+ lines.forEach((ln, i) => {
943
+ const ly = y0 + rowH * (i + 0.5);
944
+ parts.push(
945
+ `<text x="${fmt(x0 + pad)}" y="${fmt(ly)}" font-size="${fmt(fs * 0.8)}" fill="${THEME.annotationMuted}" dominant-baseline="central">${xml(ln.k)}</text>`
946
+ );
947
+ parts.push(
948
+ `<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>`
949
+ );
950
+ if (i > 0)
951
+ parts.push(
952
+ `<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)}"/>`
953
+ );
954
+ });
955
+ return `<g>${parts.join("")}</g>`;
956
+ }
957
+
958
+ // src/index.ts
959
+ var cache = /* @__PURE__ */ new Map();
960
+ var CACHE_MAX = 64;
961
+ function compile(source, opts = {}) {
962
+ const key = JSON.stringify([source, opts.width ?? null]);
963
+ if (!opts.noCache) {
964
+ const hit = cache.get(key);
965
+ if (hit) return hit;
966
+ }
967
+ const result = compileUncached(source, opts);
968
+ if (!opts.noCache) {
969
+ if (cache.size >= CACHE_MAX) {
970
+ const oldest = cache.keys().next().value;
971
+ if (oldest !== void 0) cache.delete(oldest);
972
+ }
973
+ cache.set(key, result);
974
+ }
975
+ return result;
976
+ }
977
+ function compileUncached(source, opts) {
978
+ const { plan, errors: parseErrors } = parse(source);
979
+ if (!plan || parseErrors.length > 0) {
980
+ return { svg: "", errors: parseErrors, warnings: [] };
981
+ }
982
+ const { errors, warnings } = validate(plan);
983
+ if (errors.length > 0) {
984
+ return { svg: "", errors, warnings, ast: plan };
985
+ }
986
+ const svg = render(plan, opts);
987
+ return { svg, errors: [], warnings, ast: plan };
988
+ }
989
+ function clearCache() {
990
+ cache.clear();
991
+ }
992
+
993
+ export {
994
+ compile,
995
+ clearCache
996
+ };
997
+ //# sourceMappingURL=chunk-J5DEA2KQ.js.map