@chanmeng666/archlang 0.4.0 → 0.6.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.
@@ -301,6 +301,67 @@ function levenshtein(a, b) {
301
301
  return dp[m];
302
302
  }
303
303
 
304
+ // src/theme.ts
305
+ var DEFAULT_THEME = {
306
+ bg: "#ffffff",
307
+ pocheBase: "#e9e4db",
308
+ pocheHatch: "#b9b1a4",
309
+ wallStroke: "#1b1b1b",
310
+ roomFill: "#fbfaf7",
311
+ roomLabel: "#222222",
312
+ areaLabel: "#7a7a7a",
313
+ furnitureStroke: "#a8a29a",
314
+ furnitureFill: "#f4f2ee",
315
+ furnitureLabel: "#9a948c",
316
+ opening: "#ffffff",
317
+ doorLeaf: "#555555",
318
+ windowPane: "#3a6ea5",
319
+ dim: "#0E5484",
320
+ annotation: "#333333",
321
+ annotationMuted: "#888888",
322
+ column: "#4a4a4a",
323
+ lineWeight: 1,
324
+ font: "Helvetica, Arial, sans-serif"
325
+ };
326
+ var ALIASES = {
327
+ background: "bg",
328
+ wall: "wallStroke",
329
+ wallFill: "pocheBase",
330
+ wallHatch: "pocheHatch",
331
+ room: "roomFill",
332
+ furniture: "furnitureFill",
333
+ door: "doorLeaf",
334
+ window: "windowPane"
335
+ };
336
+ function resolveThemeKey(key) {
337
+ if (key in DEFAULT_THEME) return key;
338
+ if (key in ALIASES) return ALIASES[key];
339
+ return null;
340
+ }
341
+ function isNumericThemeKey(key) {
342
+ return key === "lineWeight";
343
+ }
344
+ function mergeTheme(...layers) {
345
+ const out = { ...DEFAULT_THEME };
346
+ for (const layer of layers) {
347
+ if (!layer) continue;
348
+ for (const k of Object.keys(layer)) {
349
+ const v = layer[k];
350
+ if (v !== void 0) out[k] = v;
351
+ }
352
+ }
353
+ return out;
354
+ }
355
+ function sanitizeTheme(theme) {
356
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
357
+ const out = { ...theme };
358
+ for (const k of Object.keys(out)) {
359
+ const v = out[k];
360
+ if (typeof v === "string") out[k] = esc(v);
361
+ }
362
+ return out;
363
+ }
364
+
304
365
  // src/geometry.ts
305
366
  var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
306
367
  var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
@@ -366,30 +427,63 @@ function segmentsOfWall(w) {
366
427
  }
367
428
  return segs;
368
429
  }
369
- function hostSegmentForWalls(walls, at, ref) {
430
+ function hostInfoForWalls(walls, at, ref) {
370
431
  const candidates = ref ? walls.filter((w) => w.id === ref || w.category === ref) : walls;
371
- let best = null;
432
+ let host = null;
372
433
  let bestDist = Infinity;
434
+ let onWall = false;
373
435
  for (const w of candidates) {
436
+ const tol = w.thickness / 2 + Math.max(w.thickness, 1);
374
437
  for (const s of segmentsOfWall(w)) {
375
438
  const dist = distPointToSegment(at, s.a, s.b);
376
439
  if (dist < bestDist) {
377
440
  bestDist = dist;
378
- best = s;
441
+ host = s;
379
442
  }
443
+ if (!onWall && dist <= tol) onWall = true;
380
444
  }
381
445
  }
382
- return best;
446
+ return { host, onWall };
383
447
  }
384
- function isOnSomeWall(walls, at, ref) {
385
- const candidates = ref ? walls.filter((w) => w.id === ref || w.category === ref) : walls;
386
- for (const w of candidates) {
387
- const tol = w.thickness / 2 + Math.max(w.thickness, 1);
388
- for (const s of segmentsOfWall(w)) {
389
- if (distPointToSegment(at, s.a, s.b) <= tol) return true;
390
- }
391
- }
392
- return false;
448
+
449
+ // src/hatches.ts
450
+ var KNOWN_MATERIALS = ["poche", "concrete", "brick", "insulation", "tile", "none"];
451
+ var DEFAULT_MATERIAL = "poche";
452
+ function patternId(material) {
453
+ return material === "poche" ? "poche" : `hatch-${material}`;
454
+ }
455
+ var HATCHES = {
456
+ poche: (id, c) => `<pattern id="${id}" patternUnits="userSpaceOnUse" width="${c.fmt(c.gap)}" height="${c.fmt(c.gap)}" patternTransform="rotate(45)"><rect width="${c.fmt(c.gap)}" height="${c.fmt(c.gap)}" fill="${c.base}"/><line x1="0" y1="0" x2="0" y2="${c.fmt(c.gap)}" stroke="${c.line}" stroke-width="${c.fmt(c.thin * 0.7)}"/></pattern>`,
457
+ // Aggregate speckle.
458
+ concrete: (id, c) => {
459
+ const w = c.gap * 1.6;
460
+ return `<pattern id="${id}" patternUnits="userSpaceOnUse" width="${c.fmt(w)}" height="${c.fmt(w)}"><rect width="${c.fmt(w)}" height="${c.fmt(w)}" fill="${c.base}"/><circle cx="${c.fmt(w * 0.25)}" cy="${c.fmt(w * 0.3)}" r="${c.fmt(c.thin * 0.9)}" fill="${c.line}"/><circle cx="${c.fmt(w * 0.7)}" cy="${c.fmt(w * 0.62)}" r="${c.fmt(c.thin * 0.6)}" fill="${c.line}"/><circle cx="${c.fmt(w * 0.45)}" cy="${c.fmt(w * 0.85)}" r="${c.fmt(c.thin * 0.75)}" fill="${c.line}"/></pattern>`;
461
+ },
462
+ // Running-bond brick courses.
463
+ brick: (id, c) => {
464
+ const w = c.gap * 3;
465
+ const h = c.gap * 1.4;
466
+ const sw = c.fmt(c.thin * 0.6);
467
+ return `<pattern id="${id}" patternUnits="userSpaceOnUse" width="${c.fmt(w)}" height="${c.fmt(h)}"><rect width="${c.fmt(w)}" height="${c.fmt(h)}" fill="${c.base}"/><line x1="0" y1="${c.fmt(h)}" x2="${c.fmt(w)}" y2="${c.fmt(h)}" stroke="${c.line}" stroke-width="${sw}"/><line x1="0" y1="${c.fmt(h / 2)}" x2="${c.fmt(w)}" y2="${c.fmt(h / 2)}" stroke="${c.line}" stroke-width="${sw}"/><line x1="${c.fmt(w / 2)}" y1="0" x2="${c.fmt(w / 2)}" y2="${c.fmt(h / 2)}" stroke="${c.line}" stroke-width="${sw}"/><line x1="0" y1="${c.fmt(h / 2)}" x2="0" y2="${c.fmt(h)}" stroke="${c.line}" stroke-width="${sw}"/><line x1="${c.fmt(w)}" y1="${c.fmt(h / 2)}" x2="${c.fmt(w)}" y2="${c.fmt(h)}" stroke="${c.line}" stroke-width="${sw}"/></pattern>`;
468
+ },
469
+ // Cross-hatch batting.
470
+ insulation: (id, c) => {
471
+ const w = c.gap * 1.2;
472
+ return `<pattern id="${id}" patternUnits="userSpaceOnUse" width="${c.fmt(w)}" height="${c.fmt(w)}"><rect width="${c.fmt(w)}" height="${c.fmt(w)}" fill="${c.base}"/><path d="M0,0 L${c.fmt(w)},${c.fmt(w)} M${c.fmt(w)},0 L0,${c.fmt(w)}" stroke="${c.line}" stroke-width="${c.fmt(c.thin * 0.5)}" fill="none"/></pattern>`;
473
+ },
474
+ // Square tile grid.
475
+ tile: (id, c) => {
476
+ const w = c.gap * 1.8;
477
+ return `<pattern id="${id}" patternUnits="userSpaceOnUse" width="${c.fmt(w)}" height="${c.fmt(w)}"><rect width="${c.fmt(w)}" height="${c.fmt(w)}" fill="${c.base}"/><rect x="0" y="0" width="${c.fmt(w)}" height="${c.fmt(w)}" fill="none" stroke="${c.line}" stroke-width="${c.fmt(c.thin * 0.6)}"/></pattern>`;
478
+ },
479
+ // Solid fill, no hatch.
480
+ none: (id, c) => `<pattern id="${id}" patternUnits="userSpaceOnUse" width="${c.fmt(c.gap)}" height="${c.fmt(c.gap)}"><rect width="${c.fmt(c.gap)}" height="${c.fmt(c.gap)}" fill="${c.base}"/></pattern>`
481
+ };
482
+ function isKnownMaterial(name) {
483
+ return KNOWN_MATERIALS.includes(name);
484
+ }
485
+ function hatchPattern(material, c) {
486
+ return HATCHES[material](patternId(material), c);
393
487
  }
394
488
 
395
489
  // src/elements/wall.ts
@@ -402,6 +496,11 @@ var wall = {
402
496
  const category = ctx.eatIdent().value;
403
497
  ctx.eatKeyword("thickness");
404
498
  const thickness = ctx.parseExpr();
499
+ let material;
500
+ if (ctx.isKeyword("material")) {
501
+ ctx.next();
502
+ material = ctx.eatIdent().value;
503
+ }
405
504
  ctx.eat("lcurly");
406
505
  const points = [];
407
506
  let closed = false;
@@ -419,7 +518,7 @@ var wall = {
419
518
  }
420
519
  ctx.eat("rcurly");
421
520
  if (points.length < 2) ctx.fail("A wall needs at least two points", kw);
422
- return { kind: "wall", id, category, thickness, points, closed, line: kw.line };
521
+ return { kind: "wall", id, category, thickness, material, points, closed, line: kw.line };
423
522
  },
424
523
  idPrefix: (node) => node.category || "wall",
425
524
  resolve(node, ctx) {
@@ -431,7 +530,18 @@ var wall = {
431
530
  if (thickness <= 0) {
432
531
  ctx.diag({ severity: "error", message: `Wall "${id}" must have a positive thickness`, code: "E_WALL_THICKNESS", span: n.span });
433
532
  }
434
- return { kind: "wall", id, category: n.category, thickness, points, closed: n.closed, span: n.span };
533
+ let material = DEFAULT_MATERIAL;
534
+ if (n.material !== void 0) {
535
+ if (isKnownMaterial(n.material)) material = n.material;
536
+ else
537
+ ctx.diag({
538
+ severity: "warning",
539
+ message: `Unknown wall material "${n.material}" (known: ${KNOWN_MATERIALS.join(", ")}); using the default hatch`,
540
+ code: "W_UNKNOWN_MATERIAL",
541
+ span: n.span
542
+ });
543
+ }
544
+ return { kind: "wall", id, category: n.category, thickness, material, points, closed: n.closed, span: n.span };
435
545
  },
436
546
  bounds(resolved) {
437
547
  const w = resolved;
@@ -873,7 +983,7 @@ register(dim);
873
983
  register(column);
874
984
 
875
985
  // src/parser.ts
876
- var SETTINGS = ["units", "grid", "scale", "north", "title", "let", "component"];
986
+ var SETTINGS = ["units", "grid", "scale", "north", "title", "theme", "let", "component"];
877
987
  var STATEMENT_STARTS = /* @__PURE__ */ new Set([...SETTINGS, ...registry.keys()]);
878
988
  var ParseError = class extends Error {
879
989
  constructor(message, span) {
@@ -1036,6 +1146,10 @@ var Parser = class {
1036
1146
  plan.title = n;
1037
1147
  break;
1038
1148
  }
1149
+ case "theme": {
1150
+ plan.theme = { ...plan.theme, ...this.parseTheme() };
1151
+ break;
1152
+ }
1039
1153
  case "let": {
1040
1154
  const n = this.parseLet();
1041
1155
  n.span = this.spanFrom(start);
@@ -1145,6 +1259,35 @@ var Parser = class {
1145
1259
  this.eat("rcurly");
1146
1260
  return node;
1147
1261
  }
1262
+ /** `theme { key: <value> … }` — colours (strings), `lineWeight` (number), `font` (string). */
1263
+ parseTheme() {
1264
+ this.eatKeyword("theme");
1265
+ this.eat("lcurly");
1266
+ const t = {};
1267
+ while (!this.isType("rcurly") && !this.isType("eof")) {
1268
+ const keyTok = this.eatIdent();
1269
+ if (this.isType("colon")) this.next();
1270
+ const resolved = resolveThemeKey(keyTok.value);
1271
+ if (!resolved) {
1272
+ this.diagnostics.push({
1273
+ severity: "warning",
1274
+ message: `Unknown theme key "${keyTok.value}"`,
1275
+ code: "W_UNKNOWN_THEME_KEY",
1276
+ span: { start: keyTok.start, end: keyTok.end }
1277
+ });
1278
+ if (this.isType("string") || this.isType("number")) this.next();
1279
+ else this.fail(`Expected a value for theme key "${keyTok.value}"`);
1280
+ continue;
1281
+ }
1282
+ if (isNumericThemeKey(resolved)) {
1283
+ t[resolved] = this.eatNumber();
1284
+ } else {
1285
+ t[resolved] = this.eatString();
1286
+ }
1287
+ }
1288
+ this.eat("rcurly");
1289
+ return t;
1290
+ }
1148
1291
  parseLet() {
1149
1292
  const kw = this.eatKeyword("let");
1150
1293
  const name = this.eatIdent().value;
@@ -1286,6 +1429,15 @@ function resolve(ast) {
1286
1429
  let activeEnv = /* @__PURE__ */ new Map();
1287
1430
  const evalNum = (e) => evalExpr(e, activeEnv, (d) => diagnostics.push(d));
1288
1431
  const evalPt = (p) => ({ x: evalNum(p.x), y: evalNum(p.y) });
1432
+ let hiKey = "";
1433
+ let hiVal = null;
1434
+ const hostInfo = (at, ref) => {
1435
+ const key = `${at.x},${at.y},${ref ?? ""}`;
1436
+ if (key === hiKey && hiVal) return hiVal;
1437
+ hiKey = key;
1438
+ hiVal = hostInfoForWalls(walls, at, ref);
1439
+ return hiVal;
1440
+ };
1289
1441
  const ctx = {
1290
1442
  grid: g,
1291
1443
  snap,
@@ -1294,8 +1446,8 @@ function resolve(ast) {
1294
1446
  evalPt,
1295
1447
  id: "",
1296
1448
  walls,
1297
- hostSegment: (at, ref) => hostSegmentForWalls(walls, at, ref),
1298
- isOnWall: (at, ref) => isOnSomeWall(walls, at, ref),
1449
+ hostSegment: (at, ref) => hostInfo(at, ref).host,
1450
+ isOnWall: (at, ref) => hostInfo(at, ref).onWall,
1299
1451
  diag: (d) => diagnostics.push(d)
1300
1452
  };
1301
1453
  for (const def of registryOrder) {
@@ -1343,6 +1495,7 @@ function resolve(ast) {
1343
1495
  scale: ast.scale,
1344
1496
  north: ast.north,
1345
1497
  title: ast.title,
1498
+ theme: ast.theme,
1346
1499
  elements,
1347
1500
  walls
1348
1501
  };
@@ -1362,26 +1515,104 @@ var RENDER_PASSES = [
1362
1515
  "annotations"
1363
1516
  ];
1364
1517
 
1518
+ // src/geometry/union.ts
1519
+ function uniqSorted(values) {
1520
+ const out = [...new Set(values)].sort((a, b) => a - b);
1521
+ return out;
1522
+ }
1523
+ function rectUnionOutline(rects) {
1524
+ if (rects.length === 0) return [];
1525
+ const xs = uniqSorted(rects.flatMap((r) => [r.x0, r.x1]));
1526
+ const ys = uniqSorted(rects.flatMap((r) => [r.y0, r.y1]));
1527
+ const nx = xs.length - 1;
1528
+ const ny = ys.length - 1;
1529
+ const filled = (i, j) => {
1530
+ if (i < 0 || j < 0 || i >= nx || j >= ny) return false;
1531
+ const cx = (xs[i] + xs[i + 1]) / 2;
1532
+ const cy = (ys[j] + ys[j + 1]) / 2;
1533
+ return rects.some((r) => cx > r.x0 && cx < r.x1 && cy > r.y0 && cy < r.y1);
1534
+ };
1535
+ const key = (x, y) => `${x},${y}`;
1536
+ const starts = /* @__PURE__ */ new Map();
1537
+ const pushEdge = (ax, ay, bx, by) => {
1538
+ const k = key(ax, ay);
1539
+ const list = starts.get(k);
1540
+ if (list) list.push({ x: bx, y: by });
1541
+ else starts.set(k, [{ x: bx, y: by }]);
1542
+ };
1543
+ for (let i = 0; i < nx; i++) {
1544
+ for (let j = 0; j < ny; j++) {
1545
+ if (!filled(i, j)) continue;
1546
+ const x0 = xs[i];
1547
+ const x1 = xs[i + 1];
1548
+ const y0 = ys[j];
1549
+ const y1 = ys[j + 1];
1550
+ if (!filled(i, j - 1)) pushEdge(x1, y0, x0, y0);
1551
+ if (!filled(i - 1, j)) pushEdge(x0, y0, x0, y1);
1552
+ if (!filled(i, j + 1)) pushEdge(x0, y1, x1, y1);
1553
+ if (!filled(i + 1, j)) pushEdge(x1, y1, x1, y0);
1554
+ }
1555
+ }
1556
+ const used = /* @__PURE__ */ new Set();
1557
+ const edgeKey = (a, b) => `${a.x},${a.y}->${b.x},${b.y}`;
1558
+ const loops = [];
1559
+ const takeNext = (from, prevDir) => {
1560
+ const list = starts.get(key(from.x, from.y));
1561
+ if (!list) return null;
1562
+ const candidates = list.filter((to) => !used.has(edgeKey(from, to)));
1563
+ if (candidates.length === 0) return null;
1564
+ if (candidates.length === 1 || !prevDir) return candidates[0];
1565
+ let best = candidates[0];
1566
+ let bestScore = -Infinity;
1567
+ for (const c of candidates) {
1568
+ const dir = { x: c.x - from.x, y: c.y - from.y };
1569
+ const cross = prevDir.x * dir.y - prevDir.y * dir.x;
1570
+ if (cross > bestScore) {
1571
+ bestScore = cross;
1572
+ best = c;
1573
+ }
1574
+ }
1575
+ return best;
1576
+ };
1577
+ for (const [startKey, ends] of starts) {
1578
+ for (const firstEnd of ends) {
1579
+ const [sx, sy] = startKey.split(",").map(Number);
1580
+ const start = { x: sx, y: sy };
1581
+ if (used.has(edgeKey(start, firstEnd))) continue;
1582
+ const loop = [start];
1583
+ let cur = start;
1584
+ let next = firstEnd;
1585
+ let dir = null;
1586
+ while (next) {
1587
+ used.add(edgeKey(cur, next));
1588
+ if (next.x === start.x && next.y === start.y) break;
1589
+ loop.push(next);
1590
+ dir = { x: next.x - cur.x, y: next.y - cur.y };
1591
+ cur = next;
1592
+ next = takeNext(cur, dir);
1593
+ }
1594
+ if (loop.length >= 4) loops.push(mergeCollinear(loop));
1595
+ }
1596
+ }
1597
+ return loops;
1598
+ }
1599
+ function mergeCollinear(loop) {
1600
+ const n = loop.length;
1601
+ const out = [];
1602
+ for (let i = 0; i < n; i++) {
1603
+ const prev = loop[(i - 1 + n) % n];
1604
+ const cur = loop[i];
1605
+ const next = loop[(i + 1) % n];
1606
+ const d1x = cur.x - prev.x;
1607
+ const d1y = cur.y - prev.y;
1608
+ const d2x = next.x - cur.x;
1609
+ const d2y = next.y - cur.y;
1610
+ if (d1x * d2y - d1y * d2x !== 0) out.push(cur);
1611
+ }
1612
+ return out.length >= 3 ? out : loop;
1613
+ }
1614
+
1365
1615
  // src/render.ts
1366
- var THEME = {
1367
- bg: "#ffffff",
1368
- pocheBase: "#e9e4db",
1369
- pocheHatch: "#b9b1a4",
1370
- wallStroke: "#1b1b1b",
1371
- roomFill: "#fbfaf7",
1372
- roomLabel: "#222222",
1373
- areaLabel: "#7a7a7a",
1374
- furnitureStroke: "#a8a29a",
1375
- furnitureFill: "#f4f2ee",
1376
- furnitureLabel: "#9a948c",
1377
- opening: "#ffffff",
1378
- doorLeaf: "#555555",
1379
- windowPane: "#3a6ea5",
1380
- dim: "#0E5484",
1381
- annotation: "#333333",
1382
- annotationMuted: "#888888",
1383
- column: "#4a4a4a"
1384
- };
1385
1616
  function fmt(v) {
1386
1617
  const r = Math.round(v * 100) / 100;
1387
1618
  return Object.is(r, -0) ? "0" : String(r);
@@ -1408,15 +1639,56 @@ function planBounds(ir) {
1408
1639
  }
1409
1640
  return b;
1410
1641
  }
1642
+ function allOrthogonal(walls) {
1643
+ return walls.every((w) => segmentsOfWall(w).every((s) => s.a.x === s.b.x || s.a.y === s.b.y));
1644
+ }
1645
+ function loopsToPath(loops) {
1646
+ return loops.map((loop) => "M " + loop.map(pt).join(" L ") + " Z").join(" ");
1647
+ }
1648
+ function materialsUsed(walls) {
1649
+ return [...new Set(walls.map((w) => w.material))].sort();
1650
+ }
1651
+ function renderWalls(walls, ctx) {
1652
+ if (walls.length === 0) return [];
1653
+ const ops = [];
1654
+ for (const mat of materialsUsed(walls)) {
1655
+ const group = walls.filter((w) => w.material === mat);
1656
+ if (!allOrthogonal(group)) {
1657
+ const def = registry.get("wall");
1658
+ ops.push(...group.flatMap((w) => def.render(w, ctx)));
1659
+ continue;
1660
+ }
1661
+ const rects = [];
1662
+ for (const w of group) {
1663
+ for (const s of segmentsOfWall(w)) {
1664
+ const corners = segmentRectangle(s.a, s.b, s.thickness);
1665
+ const xsv = corners.map((c) => c.x);
1666
+ const ysv = corners.map((c) => c.y);
1667
+ rects.push({ x0: Math.min(...xsv), y0: Math.min(...ysv), x1: Math.max(...xsv), y1: Math.max(...ysv) });
1668
+ }
1669
+ }
1670
+ const loops = rectUnionOutline(rects);
1671
+ if (loops.length === 0) continue;
1672
+ const d = loopsToPath(loops);
1673
+ ops.push({ pass: "wallFill", svg: `<path d="${d}" fill="url(#${patternId(mat)})" fill-rule="nonzero"/>` });
1674
+ ops.push({
1675
+ pass: "wallFace",
1676
+ svg: `<path d="${d}" fill="none" stroke="${ctx.theme.wallStroke}" stroke-width="${ctx.fmt(ctx.sizes.wallStroke)}" stroke-linejoin="miter"/>`
1677
+ });
1678
+ }
1679
+ return ops;
1680
+ }
1411
1681
  function render(ir, opts = {}) {
1682
+ const THEME = sanitizeTheme(mergeTheme(DEFAULT_THEME, ir.theme, opts.theme));
1683
+ const lw = THEME.lineWeight;
1412
1684
  const b = planBounds(ir);
1413
1685
  const drawW = b.maxX - b.minX;
1414
1686
  const drawH = b.maxY - b.minY;
1415
1687
  const refDim = Math.max(drawW, drawH, 1);
1416
1688
  const sizes = {
1417
1689
  refDim,
1418
- wallStroke: refDim * 28e-4,
1419
- thin: refDim * 16e-4,
1690
+ wallStroke: refDim * 28e-4 * lw,
1691
+ thin: refDim * 16e-4 * lw,
1420
1692
  roomFont: refDim * 0.03,
1421
1693
  areaFont: refDim * 0.022,
1422
1694
  dimFont: refDim * 0.02,
@@ -1432,28 +1704,30 @@ function render(ir, opts = {}) {
1432
1704
  const out = [];
1433
1705
  const svgAttrs = opts.width ? `width="${fmt(opts.width)}" height="${fmt(opts.width * vbH / vbW)}"` : "";
1434
1706
  out.push(
1435
- `<svg xmlns="http://www.w3.org/2000/svg" ${svgAttrs} viewBox="${fmt(vbX)} ${fmt(vbY)} ${fmt(vbW)} ${fmt(vbH)}" font-family="Helvetica, Arial, sans-serif">`
1436
- );
1437
- out.push(
1438
- `<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>`
1707
+ `<svg xmlns="http://www.w3.org/2000/svg" ${svgAttrs} viewBox="${fmt(vbX)} ${fmt(vbY)} ${fmt(vbW)} ${fmt(vbH)}" font-family="${THEME.font}">`
1439
1708
  );
1709
+ const hatchCtx = { fmt, gap: hatchGap, thin, base: THEME.pocheBase, line: THEME.pocheHatch };
1710
+ const patterns = materialsUsed(ir.walls).map((m) => hatchPattern(m, hatchCtx)).join("");
1711
+ out.push(`<defs>${patterns}</defs>`);
1440
1712
  out.push(`<rect x="${fmt(vbX)}" y="${fmt(vbY)}" width="${fmt(vbW)}" height="${fmt(vbH)}" fill="${THEME.bg}"/>`);
1441
1713
  const ctx = { fmt, pt, xml, theme: THEME, sizes, bounds: b };
1442
1714
  const ops = ir.elements.flatMap((el) => {
1715
+ if (el.kind === "wall") return [];
1443
1716
  const def = registry.get(el.kind);
1444
1717
  return def ? def.render(el, ctx) : [];
1445
1718
  });
1719
+ ops.push(...renderWalls(ir.walls, ctx));
1446
1720
  for (const pass of RENDER_PASSES) {
1447
1721
  for (const op of ops) if (op.pass === pass) out.push(op.svg);
1448
1722
  }
1449
- out.push(northArrow(ir, b, margin, refDim));
1450
- out.push(scaleBar(b, margin, refDim, thin));
1451
- const tb = titleBlock(ir, b, margin, refDim, thin);
1723
+ out.push(northArrow(ir, b, margin, refDim, THEME));
1724
+ out.push(scaleBar(b, margin, refDim, thin, THEME));
1725
+ const tb = titleBlock(ir, b, margin, refDim, thin, THEME);
1452
1726
  if (tb) out.push(tb);
1453
1727
  out.push("</svg>");
1454
1728
  return out.join("\n");
1455
1729
  }
1456
- function northArrow(ir, b, margin, refDim) {
1730
+ function northArrow(ir, b, margin, refDim, THEME) {
1457
1731
  const r = refDim * 0.045;
1458
1732
  const cx = b.maxX - r;
1459
1733
  const cy = b.minY - margin * 0.55;
@@ -1483,7 +1757,7 @@ function northArrow(ir, b, margin, refDim) {
1483
1757
  const ly = cy + ny * (r + fs * 0.8);
1484
1758
  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>`;
1485
1759
  }
1486
- function scaleBar(b, margin, refDim, thin) {
1760
+ function scaleBar(b, margin, refDim, thin, THEME) {
1487
1761
  const barLen = niceBarLength(refDim * 0.3);
1488
1762
  const x0 = b.minX;
1489
1763
  const y0 = b.maxY + margin * 0.55;
@@ -1503,7 +1777,7 @@ function scaleBar(b, margin, refDim, thin) {
1503
1777
  );
1504
1778
  return `<g>${parts.join("")}</g>`;
1505
1779
  }
1506
- function titleBlock(ir, b, margin, refDim, thin) {
1780
+ function titleBlock(ir, b, margin, refDim, thin, THEME) {
1507
1781
  const t = ir.title;
1508
1782
  if (!t && !ir.scale) return null;
1509
1783
  const boxW = refDim * 0.34;
@@ -1565,8 +1839,8 @@ function lineEnd(source, offset) {
1565
1839
  }
1566
1840
  function formatDiagnostic(source, d) {
1567
1841
  const codeTag = d.code ? `[${d.code}]` : "";
1568
- const header = `${d.severity}${codeTag}: ${d.message}`;
1569
- const lines = [header];
1842
+ const header2 = `${d.severity}${codeTag}: ${d.message}`;
1843
+ const lines = [header2];
1570
1844
  if (d.span) {
1571
1845
  const { line, col } = offsetToLineCol(source, d.span.start);
1572
1846
  const ls = lineStart(source, d.span.start);
@@ -1592,11 +1866,211 @@ function formatDiagnostic(source, d) {
1592
1866
  return lines.join("\n");
1593
1867
  }
1594
1868
 
1869
+ // src/export/dxf.ts
1870
+ function num(v) {
1871
+ const r = Math.round(v * 1e4) / 1e4;
1872
+ return Object.is(r, -0) ? "0" : String(r);
1873
+ }
1874
+ var DxfBuilder = class {
1875
+ out = [];
1876
+ /** group-code / value pair. */
1877
+ pair(code, value) {
1878
+ this.out.push(String(code), String(value));
1879
+ }
1880
+ line(layer, a, b) {
1881
+ this.pair(0, "LINE");
1882
+ this.pair(8, layer);
1883
+ this.pair(10, num(a.x));
1884
+ this.pair(20, num(-a.y));
1885
+ this.pair(11, num(b.x));
1886
+ this.pair(21, num(-b.y));
1887
+ }
1888
+ arc(layer, center, radius, startDeg, endDeg) {
1889
+ this.pair(0, "ARC");
1890
+ this.pair(8, layer);
1891
+ this.pair(10, num(center.x));
1892
+ this.pair(20, num(-center.y));
1893
+ this.pair(40, num(radius));
1894
+ this.pair(50, num(startDeg));
1895
+ this.pair(51, num(endDeg));
1896
+ }
1897
+ text(layer, at, height, value) {
1898
+ this.pair(0, "TEXT");
1899
+ this.pair(8, layer);
1900
+ this.pair(10, num(at.x));
1901
+ this.pair(20, num(-at.y));
1902
+ this.pair(40, num(height));
1903
+ this.pair(1, value.replace(/\n/g, " "));
1904
+ }
1905
+ rect(layer, corners) {
1906
+ for (let i = 0; i < corners.length; i++) {
1907
+ this.line(layer, corners[i], corners[(i + 1) % corners.length]);
1908
+ }
1909
+ }
1910
+ toString() {
1911
+ return this.out.join("\n") + "\n";
1912
+ }
1913
+ };
1914
+ var LAYERS = ["WALLS", "ROOMS", "DOORS", "WINDOWS", "FURNITURE", "COLUMNS", "DIMS", "LABELS"];
1915
+ function header() {
1916
+ const h = [];
1917
+ const p = (c, v) => h.push(String(c), String(v));
1918
+ p(0, "SECTION");
1919
+ p(2, "HEADER");
1920
+ p(9, "$ACADVER");
1921
+ p(1, "AC1009");
1922
+ p(0, "ENDSEC");
1923
+ p(0, "SECTION");
1924
+ p(2, "TABLES");
1925
+ p(0, "TABLE");
1926
+ p(2, "LAYER");
1927
+ p(70, LAYERS.length);
1928
+ for (const name of LAYERS) {
1929
+ p(0, "LAYER");
1930
+ p(2, name);
1931
+ p(70, 0);
1932
+ p(62, 7);
1933
+ p(6, "CONTINUOUS");
1934
+ }
1935
+ p(0, "ENDTAB");
1936
+ p(0, "ENDSEC");
1937
+ return h.join("\n") + "\n";
1938
+ }
1939
+ function emitDoor(b, dr) {
1940
+ const seg = dr.host;
1941
+ if (!seg) return;
1942
+ const d = unit(sub(seg.b, seg.a));
1943
+ const n = normal(d);
1944
+ const hw = dr.width / 2;
1945
+ const hinge = dr.hinge === "left" ? add(dr.at, mul(d, -hw)) : add(dr.at, mul(d, hw));
1946
+ const farJamb = dr.hinge === "left" ? add(dr.at, mul(d, hw)) : add(dr.at, mul(d, -hw));
1947
+ const leafDir = dr.swing === "in" ? n : mul(n, -1);
1948
+ const leafEnd = add(hinge, mul(leafDir, dr.width));
1949
+ b.line("DOORS", hinge, leafEnd);
1950
+ const deg = (p) => Math.atan2(-(p.y - hinge.y), p.x - hinge.x) * 180 / Math.PI;
1951
+ const a1 = deg(leafEnd);
1952
+ const a2 = deg(farJamb);
1953
+ const ccw = ((a2 - a1) % 360 + 360) % 360;
1954
+ if (ccw <= 180) b.arc("DOORS", hinge, dr.width, a1, a2);
1955
+ else b.arc("DOORS", hinge, dr.width, a2, a1);
1956
+ }
1957
+ function emitWindow(b, wn) {
1958
+ const seg = wn.host;
1959
+ if (!seg) return;
1960
+ const d = unit(sub(seg.b, seg.a));
1961
+ const n = normal(d);
1962
+ const hw = wn.width / 2;
1963
+ const h = seg.thickness / 2;
1964
+ const jA = add(wn.at, mul(d, -hw));
1965
+ const jB = add(wn.at, mul(d, hw));
1966
+ b.line("WINDOWS", add(jA, mul(n, h)), add(jB, mul(n, h)));
1967
+ b.line("WINDOWS", add(jA, mul(n, -h)), add(jB, mul(n, -h)));
1968
+ b.line("WINDOWS", jA, jB);
1969
+ }
1970
+ function emitDim(b, dm) {
1971
+ const dd = unit(sub(dm.to, dm.from));
1972
+ const dn = normal(dd);
1973
+ const p1 = add(dm.from, mul(dn, dm.offset));
1974
+ const p2 = add(dm.to, mul(dn, dm.offset));
1975
+ b.line("DIMS", p1, p2);
1976
+ if (dm.text) {
1977
+ const mid = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };
1978
+ b.text("DIMS", mid, 150, dm.text);
1979
+ }
1980
+ }
1981
+ function toDxf(ir) {
1982
+ const b = new DxfBuilder();
1983
+ b.pair(0, "SECTION");
1984
+ b.pair(2, "ENTITIES");
1985
+ const labelAt = (at, w, h) => ({ x: at.x + w / 2, y: at.y + h / 2 });
1986
+ for (const el of ir.elements) {
1987
+ switch (el.kind) {
1988
+ case "wall":
1989
+ for (const s of segmentsOfWall(el)) {
1990
+ const d = unit(sub(s.b, s.a));
1991
+ const n = normal(d);
1992
+ const off = s.thickness / 2;
1993
+ b.line("WALLS", add(s.a, mul(n, off)), add(s.b, mul(n, off)));
1994
+ b.line("WALLS", add(s.a, mul(n, -off)), add(s.b, mul(n, -off)));
1995
+ }
1996
+ break;
1997
+ case "room":
1998
+ b.rect("ROOMS", rectCorners(el.at.x, el.at.y, el.size.w, el.size.h));
1999
+ if (el.label) b.text("LABELS", labelAt(el.at, el.size.w, el.size.h), 200, el.label);
2000
+ break;
2001
+ case "furniture":
2002
+ b.rect("FURNITURE", rectCorners(el.at.x, el.at.y, el.size.w, el.size.h));
2003
+ if (el.label) b.text("LABELS", labelAt(el.at, el.size.w, el.size.h), 150, el.label);
2004
+ break;
2005
+ case "column":
2006
+ b.rect("COLUMNS", rectCorners(el.at.x, el.at.y, el.size.w, el.size.h));
2007
+ break;
2008
+ case "door":
2009
+ emitDoor(b, el);
2010
+ break;
2011
+ case "window":
2012
+ emitWindow(b, el);
2013
+ break;
2014
+ case "dim":
2015
+ emitDim(b, el);
2016
+ break;
2017
+ }
2018
+ }
2019
+ b.pair(0, "ENDSEC");
2020
+ const entities = b.toString();
2021
+ return header() + entities + "0\nEOF\n";
2022
+ }
2023
+
2024
+ // src/export/pdf.ts
2025
+ function svgSize(svg) {
2026
+ const w = /<svg[^>]*\bwidth="([\d.]+)/.exec(svg);
2027
+ const h = /<svg[^>]*\bheight="([\d.]+)/.exec(svg);
2028
+ if (w && h) return { width: parseFloat(w[1]), height: parseFloat(h[1]) };
2029
+ const vb = /<svg[^>]*\bviewBox="[\d.\-]+ [\d.\-]+ ([\d.]+) ([\d.]+)"/.exec(svg);
2030
+ if (vb) return { width: parseFloat(vb[1]), height: parseFloat(vb[2]) };
2031
+ return { width: 800, height: 600 };
2032
+ }
2033
+ async function toPdf(svg) {
2034
+ let PDFDocument;
2035
+ let SVGtoPDF;
2036
+ try {
2037
+ PDFDocument = (await import("pdfkit")).default;
2038
+ SVGtoPDF = (await import("svg-to-pdfkit")).default;
2039
+ } catch {
2040
+ throw new Error(
2041
+ "PDF export needs the optional dependencies 'pdfkit' and 'svg-to-pdfkit'. Install them: npm install pdfkit svg-to-pdfkit"
2042
+ );
2043
+ }
2044
+ const { width, height } = svgSize(svg);
2045
+ const doc = new PDFDocument({ size: [width, height], margin: 0 });
2046
+ const chunks = [];
2047
+ const done = new Promise((resolve2, reject) => {
2048
+ doc.on("data", (c) => chunks.push(c));
2049
+ doc.on("end", () => resolve2());
2050
+ doc.on("error", (e) => reject(e));
2051
+ });
2052
+ SVGtoPDF(doc, svg, 0, 0, { width, height, assumePt: true });
2053
+ doc.end();
2054
+ await done;
2055
+ return concat(chunks);
2056
+ }
2057
+ function concat(chunks) {
2058
+ let total = 0;
2059
+ for (const c of chunks) total += c.length;
2060
+ const out = new Uint8Array(total);
2061
+ let offset = 0;
2062
+ for (const c of chunks) {
2063
+ out.set(c, offset);
2064
+ offset += c.length;
2065
+ }
2066
+ return out;
2067
+ }
2068
+
1595
2069
  // src/index.ts
1596
2070
  var cache = /* @__PURE__ */ new Map();
1597
2071
  var CACHE_MAX = 64;
1598
2072
  function compile(source, opts = {}) {
1599
- const key = JSON.stringify([source, opts.width ?? null]);
2073
+ const key = JSON.stringify([source, opts.width ?? null, opts.theme ?? null]);
1600
2074
  if (!opts.noCache) {
1601
2075
  const hit = cache.get(key);
1602
2076
  if (hit) return hit;
@@ -1631,9 +2105,12 @@ function clearCache() {
1631
2105
  }
1632
2106
 
1633
2107
  export {
2108
+ resolve,
1634
2109
  offsetToLineCol,
1635
2110
  formatDiagnostic,
2111
+ toDxf,
2112
+ toPdf,
1636
2113
  compile,
1637
2114
  clearCache
1638
2115
  };
1639
- //# sourceMappingURL=chunk-DHNWMOP7.js.map
2116
+ //# sourceMappingURL=chunk-PABYLU6Z.js.map