@chanmeng666/archlang 0.3.0 → 0.5.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.
- package/dist/{chunk-3YUQPQPZ.js → chunk-GUNWYUR2.js} +668 -96
- package/dist/chunk-GUNWYUR2.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +116 -21
- package/dist/index.js +1 -1
- package/examples/parametric.arch +31 -0
- package/examples/themed.arch +40 -0
- package/package.json +1 -1
- package/dist/chunk-3YUQPQPZ.js.map +0 -1
|
@@ -100,19 +100,42 @@ function lex(src) {
|
|
|
100
100
|
push("arrow", "->", startLine, startCol, startIdx);
|
|
101
101
|
continue;
|
|
102
102
|
}
|
|
103
|
-
if (
|
|
103
|
+
if (c === "+") {
|
|
104
|
+
advance();
|
|
105
|
+
push("plus", "+", startLine, startCol, startIdx);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (c === "-") {
|
|
109
|
+
advance();
|
|
110
|
+
push("minus", "-", startLine, startCol, startIdx);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (c === "*") {
|
|
114
|
+
advance();
|
|
115
|
+
push("star", "*", startLine, startCol, startIdx);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (c === "/") {
|
|
119
|
+
advance();
|
|
120
|
+
push("slash", "/", startLine, startCol, startIdx);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (c === "%") {
|
|
124
|
+
advance();
|
|
125
|
+
push("percent", "%", startLine, startCol, startIdx);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (isDigit(c) || c === "." && isDigit(peek(1))) {
|
|
104
129
|
let raw = "";
|
|
105
|
-
if (c === "-") raw += advance();
|
|
106
130
|
while (isDigit(peek())) raw += advance();
|
|
107
131
|
if (peek() === ".") {
|
|
108
132
|
raw += advance();
|
|
109
133
|
while (isDigit(peek())) raw += advance();
|
|
110
134
|
}
|
|
111
135
|
const first = parseFloat(raw);
|
|
112
|
-
if (peek() === "x" && (isDigit(peek(1)) || peek(1) === "
|
|
136
|
+
if (peek() === "x" && (isDigit(peek(1)) || peek(1) === "." && isDigit(peek(2)))) {
|
|
113
137
|
advance();
|
|
114
138
|
let raw2 = "";
|
|
115
|
-
if (peek() === "-") raw2 += advance();
|
|
116
139
|
while (isDigit(peek())) raw2 += advance();
|
|
117
140
|
if (peek() === ".") {
|
|
118
141
|
raw2 += advance();
|
|
@@ -138,6 +161,198 @@ function lex(src) {
|
|
|
138
161
|
return { tokens, errors };
|
|
139
162
|
}
|
|
140
163
|
|
|
164
|
+
// src/expr.ts
|
|
165
|
+
var BIN_PREC = {
|
|
166
|
+
plus: 1,
|
|
167
|
+
minus: 1,
|
|
168
|
+
star: 2,
|
|
169
|
+
slash: 2,
|
|
170
|
+
percent: 2
|
|
171
|
+
};
|
|
172
|
+
var BIN_OP = {
|
|
173
|
+
plus: "+",
|
|
174
|
+
minus: "-",
|
|
175
|
+
star: "*",
|
|
176
|
+
slash: "/",
|
|
177
|
+
percent: "%"
|
|
178
|
+
};
|
|
179
|
+
function parseExpr(ts) {
|
|
180
|
+
return parseBin(ts, 1);
|
|
181
|
+
}
|
|
182
|
+
function parseBin(ts, minPrec) {
|
|
183
|
+
let left = parseUnary(ts);
|
|
184
|
+
for (; ; ) {
|
|
185
|
+
const t = ts.peek();
|
|
186
|
+
const prec = BIN_PREC[t.type];
|
|
187
|
+
if (prec === void 0 || prec < minPrec) break;
|
|
188
|
+
ts.next();
|
|
189
|
+
const right = parseBin(ts, prec + 1);
|
|
190
|
+
left = { t: "bin", op: BIN_OP[t.type], l: left, r: right };
|
|
191
|
+
}
|
|
192
|
+
return left;
|
|
193
|
+
}
|
|
194
|
+
function parseUnary(ts) {
|
|
195
|
+
const t = ts.peek();
|
|
196
|
+
if (t.type === "minus" || t.type === "plus") {
|
|
197
|
+
ts.next();
|
|
198
|
+
return { t: "unary", op: t.type === "minus" ? "-" : "+", e: parseUnary(ts) };
|
|
199
|
+
}
|
|
200
|
+
return parseAtom(ts);
|
|
201
|
+
}
|
|
202
|
+
function parseAtom(ts) {
|
|
203
|
+
const t = ts.peek();
|
|
204
|
+
if (t.type === "number") {
|
|
205
|
+
ts.next();
|
|
206
|
+
return { t: "num", value: t.num };
|
|
207
|
+
}
|
|
208
|
+
if (t.type === "ident") {
|
|
209
|
+
ts.next();
|
|
210
|
+
return { t: "ref", name: t.value, span: { start: t.start, end: t.end } };
|
|
211
|
+
}
|
|
212
|
+
if (t.type === "lparen") {
|
|
213
|
+
ts.next();
|
|
214
|
+
const e = parseExpr(ts);
|
|
215
|
+
const close = ts.peek();
|
|
216
|
+
if (close.type !== "rparen") ts.fail(`Expected ")" but found ${describe(close)}`);
|
|
217
|
+
ts.next();
|
|
218
|
+
return e;
|
|
219
|
+
}
|
|
220
|
+
return ts.fail(`Expected a number, name, or "(" but found ${describe(t)}`);
|
|
221
|
+
}
|
|
222
|
+
function evalExpr(e, env, onError) {
|
|
223
|
+
switch (e.t) {
|
|
224
|
+
case "num":
|
|
225
|
+
return e.value;
|
|
226
|
+
case "ref": {
|
|
227
|
+
const v = env.get(e.name);
|
|
228
|
+
if (v === void 0) {
|
|
229
|
+
const hint = closest(e.name, [...env.keys()]);
|
|
230
|
+
onError({
|
|
231
|
+
severity: "error",
|
|
232
|
+
message: `Unknown name "${e.name}"`,
|
|
233
|
+
code: "E_UNKNOWN_REF",
|
|
234
|
+
span: e.span,
|
|
235
|
+
hints: hint ? [`did you mean "${hint}"?`] : void 0
|
|
236
|
+
});
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
return v;
|
|
240
|
+
}
|
|
241
|
+
case "unary": {
|
|
242
|
+
const v = evalExpr(e.e, env, onError);
|
|
243
|
+
return e.op === "-" ? -v : v;
|
|
244
|
+
}
|
|
245
|
+
case "bin": {
|
|
246
|
+
const l = evalExpr(e.l, env, onError);
|
|
247
|
+
const r = evalExpr(e.r, env, onError);
|
|
248
|
+
switch (e.op) {
|
|
249
|
+
case "+":
|
|
250
|
+
return l + r;
|
|
251
|
+
case "-":
|
|
252
|
+
return l - r;
|
|
253
|
+
case "*":
|
|
254
|
+
return l * r;
|
|
255
|
+
case "/":
|
|
256
|
+
case "%":
|
|
257
|
+
if (r === 0) {
|
|
258
|
+
onError({ severity: "error", message: `${e.op === "/" ? "Division" : "Modulo"} by zero`, code: "E_DIV_ZERO" });
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
return e.op === "/" ? l / r : l % r;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function describe(t) {
|
|
267
|
+
if (t.type === "eof") return "end of input";
|
|
268
|
+
if (t.type === "string") return `string ${JSON.stringify(t.value)}`;
|
|
269
|
+
return `"${t.value}"`;
|
|
270
|
+
}
|
|
271
|
+
function closest(name, candidates) {
|
|
272
|
+
let best = null;
|
|
273
|
+
let bestDist = Infinity;
|
|
274
|
+
for (const c of candidates) {
|
|
275
|
+
const d = levenshtein(name, c);
|
|
276
|
+
if (d < bestDist) {
|
|
277
|
+
bestDist = d;
|
|
278
|
+
best = c;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const limit = Math.max(2, Math.floor(name.length / 3));
|
|
282
|
+
return best !== null && bestDist <= limit ? best : null;
|
|
283
|
+
}
|
|
284
|
+
function levenshtein(a, b) {
|
|
285
|
+
const m = a.length;
|
|
286
|
+
const n = b.length;
|
|
287
|
+
const dp = Array.from({ length: m + 1 }, (_, i) => i);
|
|
288
|
+
for (let j = 1; j <= n; j++) {
|
|
289
|
+
let prev = dp[0];
|
|
290
|
+
dp[0] = j;
|
|
291
|
+
for (let i = 1; i <= m; i++) {
|
|
292
|
+
const tmp = dp[i];
|
|
293
|
+
dp[i] = Math.min(
|
|
294
|
+
dp[i] + 1,
|
|
295
|
+
dp[i - 1] + 1,
|
|
296
|
+
prev + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
297
|
+
);
|
|
298
|
+
prev = tmp;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return dp[m];
|
|
302
|
+
}
|
|
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
|
+
|
|
141
356
|
// src/geometry.ts
|
|
142
357
|
var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
|
|
143
358
|
var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
|
|
@@ -229,6 +444,46 @@ function isOnSomeWall(walls, at, ref) {
|
|
|
229
444
|
return false;
|
|
230
445
|
}
|
|
231
446
|
|
|
447
|
+
// src/hatches.ts
|
|
448
|
+
var KNOWN_MATERIALS = ["poche", "concrete", "brick", "insulation", "tile", "none"];
|
|
449
|
+
var DEFAULT_MATERIAL = "poche";
|
|
450
|
+
function patternId(material) {
|
|
451
|
+
return material === "poche" ? "poche" : `hatch-${material}`;
|
|
452
|
+
}
|
|
453
|
+
var HATCHES = {
|
|
454
|
+
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>`,
|
|
455
|
+
// Aggregate speckle.
|
|
456
|
+
concrete: (id, c) => {
|
|
457
|
+
const w = c.gap * 1.6;
|
|
458
|
+
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>`;
|
|
459
|
+
},
|
|
460
|
+
// Running-bond brick courses.
|
|
461
|
+
brick: (id, c) => {
|
|
462
|
+
const w = c.gap * 3;
|
|
463
|
+
const h = c.gap * 1.4;
|
|
464
|
+
const sw = c.fmt(c.thin * 0.6);
|
|
465
|
+
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>`;
|
|
466
|
+
},
|
|
467
|
+
// Cross-hatch batting.
|
|
468
|
+
insulation: (id, c) => {
|
|
469
|
+
const w = c.gap * 1.2;
|
|
470
|
+
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>`;
|
|
471
|
+
},
|
|
472
|
+
// Square tile grid.
|
|
473
|
+
tile: (id, c) => {
|
|
474
|
+
const w = c.gap * 1.8;
|
|
475
|
+
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>`;
|
|
476
|
+
},
|
|
477
|
+
// Solid fill, no hatch.
|
|
478
|
+
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>`
|
|
479
|
+
};
|
|
480
|
+
function isKnownMaterial(name) {
|
|
481
|
+
return KNOWN_MATERIALS.includes(name);
|
|
482
|
+
}
|
|
483
|
+
function hatchPattern(material, c) {
|
|
484
|
+
return HATCHES[material](patternId(material), c);
|
|
485
|
+
}
|
|
486
|
+
|
|
232
487
|
// src/elements/wall.ts
|
|
233
488
|
var wall = {
|
|
234
489
|
kind: "wall",
|
|
@@ -238,7 +493,12 @@ var wall = {
|
|
|
238
493
|
const id = ctx.parseIdOpt();
|
|
239
494
|
const category = ctx.eatIdent().value;
|
|
240
495
|
ctx.eatKeyword("thickness");
|
|
241
|
-
const thickness = ctx.
|
|
496
|
+
const thickness = ctx.parseExpr();
|
|
497
|
+
let material;
|
|
498
|
+
if (ctx.isKeyword("material")) {
|
|
499
|
+
ctx.next();
|
|
500
|
+
material = ctx.eatIdent().value;
|
|
501
|
+
}
|
|
242
502
|
ctx.eat("lcurly");
|
|
243
503
|
const points = [];
|
|
244
504
|
let closed = false;
|
|
@@ -252,22 +512,34 @@ var wall = {
|
|
|
252
512
|
points.push(ctx.parsePoint());
|
|
253
513
|
continue;
|
|
254
514
|
}
|
|
255
|
-
ctx.fail(`Expected a point "(x,y)" or "close" in wall body but found ${
|
|
515
|
+
ctx.fail(`Expected a point "(x,y)" or "close" in wall body but found ${describe2(ctx)}`);
|
|
256
516
|
}
|
|
257
517
|
ctx.eat("rcurly");
|
|
258
518
|
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 };
|
|
519
|
+
return { kind: "wall", id, category, thickness, material, points, closed, line: kw.line };
|
|
260
520
|
},
|
|
261
521
|
idPrefix: (node) => node.category || "wall",
|
|
262
522
|
resolve(node, ctx) {
|
|
263
523
|
const n = node;
|
|
264
|
-
const id = ctx.
|
|
265
|
-
const points = n.points.map(ctx.snapPt);
|
|
266
|
-
const
|
|
524
|
+
const id = ctx.id;
|
|
525
|
+
const points = n.points.map((p) => ctx.snapPt(ctx.evalPt(p)));
|
|
526
|
+
const tv = ctx.eval(n.thickness);
|
|
527
|
+
const thickness = ctx.snap(tv) || tv;
|
|
267
528
|
if (thickness <= 0) {
|
|
268
529
|
ctx.diag({ severity: "error", message: `Wall "${id}" must have a positive thickness`, code: "E_WALL_THICKNESS", span: n.span });
|
|
269
530
|
}
|
|
270
|
-
|
|
531
|
+
let material = DEFAULT_MATERIAL;
|
|
532
|
+
if (n.material !== void 0) {
|
|
533
|
+
if (isKnownMaterial(n.material)) material = n.material;
|
|
534
|
+
else
|
|
535
|
+
ctx.diag({
|
|
536
|
+
severity: "warning",
|
|
537
|
+
message: `Unknown wall material "${n.material}" (known: ${KNOWN_MATERIALS.join(", ")}); using the default hatch`,
|
|
538
|
+
code: "W_UNKNOWN_MATERIAL",
|
|
539
|
+
span: n.span
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
return { kind: "wall", id, category: n.category, thickness, material, points, closed: n.closed, span: n.span };
|
|
271
543
|
},
|
|
272
544
|
bounds(resolved) {
|
|
273
545
|
const w = resolved;
|
|
@@ -302,7 +574,7 @@ var wall = {
|
|
|
302
574
|
return ops;
|
|
303
575
|
}
|
|
304
576
|
};
|
|
305
|
-
function
|
|
577
|
+
function describe2(ctx) {
|
|
306
578
|
const t = ctx.peek();
|
|
307
579
|
if (t.type === "eof") return "end of input";
|
|
308
580
|
if (t.type === "string") return `string ${JSON.stringify(t.value)}`;
|
|
@@ -319,8 +591,8 @@ var room = {
|
|
|
319
591
|
ctx.eatKeyword("at");
|
|
320
592
|
const at = ctx.parsePoint();
|
|
321
593
|
ctx.eatKeyword("size");
|
|
322
|
-
const
|
|
323
|
-
const node = { kind: "room", id, at, size
|
|
594
|
+
const size = ctx.parseDimensions();
|
|
595
|
+
const node = { kind: "room", id, at, size, line: kw.line };
|
|
324
596
|
if (ctx.isKeyword("label")) {
|
|
325
597
|
ctx.next();
|
|
326
598
|
node.label = ctx.eatString();
|
|
@@ -330,9 +602,9 @@ var room = {
|
|
|
330
602
|
idPrefix: () => "room",
|
|
331
603
|
resolve(node, ctx) {
|
|
332
604
|
const n = node;
|
|
333
|
-
const id = ctx.
|
|
334
|
-
const at = ctx.snapPt(n.at);
|
|
335
|
-
const size = { w: ctx.snap(n.size.w), h: ctx.snap(n.size.h) };
|
|
605
|
+
const id = ctx.id;
|
|
606
|
+
const at = ctx.snapPt(ctx.evalPt(n.at));
|
|
607
|
+
const size = { w: ctx.snap(ctx.eval(n.size.w)), h: ctx.snap(ctx.eval(n.size.h)) };
|
|
336
608
|
if (size.w <= 0 || size.h <= 0) {
|
|
337
609
|
ctx.diag({ severity: "error", message: `Room "${id}" must have a positive size`, code: "E_ROOM_SIZE", span: n.span });
|
|
338
610
|
}
|
|
@@ -375,7 +647,7 @@ var door = {
|
|
|
375
647
|
ctx.eatKeyword("at");
|
|
376
648
|
const at = ctx.parsePoint();
|
|
377
649
|
ctx.eatKeyword("width");
|
|
378
|
-
const width = ctx.
|
|
650
|
+
const width = ctx.parseExpr();
|
|
379
651
|
const node = { kind: "door", id, at, width, hinge: "left", swing: "in", line: kw.line };
|
|
380
652
|
if (ctx.isKeyword("wall")) {
|
|
381
653
|
ctx.next();
|
|
@@ -398,9 +670,10 @@ var door = {
|
|
|
398
670
|
idPrefix: () => "door",
|
|
399
671
|
resolve(node, ctx) {
|
|
400
672
|
const n = node;
|
|
401
|
-
const id = ctx.
|
|
402
|
-
const at = ctx.snapPt(n.at);
|
|
403
|
-
const
|
|
673
|
+
const id = ctx.id;
|
|
674
|
+
const at = ctx.snapPt(ctx.evalPt(n.at));
|
|
675
|
+
const wv = ctx.eval(n.width);
|
|
676
|
+
const width = ctx.snap(wv) || wv;
|
|
404
677
|
if (width <= 0) {
|
|
405
678
|
ctx.diag({ severity: "error", message: `Door "${id}" must have a positive width`, code: "E_DOOR_WIDTH", span: n.span });
|
|
406
679
|
}
|
|
@@ -455,7 +728,7 @@ var windowEl = {
|
|
|
455
728
|
ctx.eatKeyword("at");
|
|
456
729
|
const at = ctx.parsePoint();
|
|
457
730
|
ctx.eatKeyword("width");
|
|
458
|
-
const width = ctx.
|
|
731
|
+
const width = ctx.parseExpr();
|
|
459
732
|
const node = { kind: "window", id, at, width, line: kw.line };
|
|
460
733
|
if (ctx.isKeyword("wall")) {
|
|
461
734
|
ctx.next();
|
|
@@ -466,9 +739,10 @@ var windowEl = {
|
|
|
466
739
|
idPrefix: () => "window",
|
|
467
740
|
resolve(node, ctx) {
|
|
468
741
|
const n = node;
|
|
469
|
-
const id = ctx.
|
|
470
|
-
const at = ctx.snapPt(n.at);
|
|
471
|
-
const
|
|
742
|
+
const id = ctx.id;
|
|
743
|
+
const at = ctx.snapPt(ctx.evalPt(n.at));
|
|
744
|
+
const wv = ctx.eval(n.width);
|
|
745
|
+
const width = ctx.snap(wv) || wv;
|
|
472
746
|
if (width <= 0) {
|
|
473
747
|
ctx.diag({ severity: "error", message: `Window "${id}" must have a positive width`, code: "E_WINDOW_WIDTH", span: n.span });
|
|
474
748
|
}
|
|
@@ -525,8 +799,8 @@ var furniture = {
|
|
|
525
799
|
ctx.eatKeyword("at");
|
|
526
800
|
const at = ctx.parsePoint();
|
|
527
801
|
ctx.eatKeyword("size");
|
|
528
|
-
const
|
|
529
|
-
const node = { kind: "furniture", id, category, at, size
|
|
802
|
+
const size = ctx.parseDimensions();
|
|
803
|
+
const node = { kind: "furniture", id, category, at, size, line: kw.line };
|
|
530
804
|
if (ctx.isKeyword("label")) {
|
|
531
805
|
ctx.next();
|
|
532
806
|
node.label = ctx.eatString();
|
|
@@ -536,9 +810,9 @@ var furniture = {
|
|
|
536
810
|
idPrefix: (node) => node.category || "furniture",
|
|
537
811
|
resolve(node, ctx) {
|
|
538
812
|
const n = node;
|
|
539
|
-
const id = ctx.
|
|
540
|
-
const at = ctx.snapPt(n.at);
|
|
541
|
-
const size = { w: ctx.snap(n.size.w), h: ctx.snap(n.size.h) };
|
|
813
|
+
const id = ctx.id;
|
|
814
|
+
const at = ctx.snapPt(ctx.evalPt(n.at));
|
|
815
|
+
const size = { w: ctx.snap(ctx.eval(n.size.w)), h: ctx.snap(ctx.eval(n.size.h)) };
|
|
542
816
|
if (size.w <= 0 || size.h <= 0) {
|
|
543
817
|
ctx.diag({ severity: "error", message: `Furniture "${id}" must have a positive size`, code: "E_FURN_SIZE", span: n.span });
|
|
544
818
|
}
|
|
@@ -578,10 +852,10 @@ var dim = {
|
|
|
578
852
|
const from = ctx.parsePoint();
|
|
579
853
|
ctx.eat("arrow");
|
|
580
854
|
const to = ctx.parsePoint();
|
|
581
|
-
const node = { kind: "dim", id: "", from, to, offset: 300, line: kw.line };
|
|
855
|
+
const node = { kind: "dim", id: "", from, to, offset: { t: "num", value: 300 }, line: kw.line };
|
|
582
856
|
if (ctx.isKeyword("offset")) {
|
|
583
857
|
ctx.next();
|
|
584
|
-
node.offset = ctx.
|
|
858
|
+
node.offset = ctx.parseExpr();
|
|
585
859
|
}
|
|
586
860
|
if (ctx.isKeyword("text")) {
|
|
587
861
|
ctx.next();
|
|
@@ -594,10 +868,10 @@ var dim = {
|
|
|
594
868
|
const n = node;
|
|
595
869
|
return {
|
|
596
870
|
kind: "dim",
|
|
597
|
-
id: ctx.
|
|
598
|
-
from: ctx.snapPt(n.from),
|
|
599
|
-
to: ctx.snapPt(n.to),
|
|
600
|
-
offset: n.offset,
|
|
871
|
+
id: ctx.id,
|
|
872
|
+
from: ctx.snapPt(ctx.evalPt(n.from)),
|
|
873
|
+
to: ctx.snapPt(ctx.evalPt(n.to)),
|
|
874
|
+
offset: ctx.eval(n.offset),
|
|
601
875
|
text: n.text,
|
|
602
876
|
span: n.span
|
|
603
877
|
};
|
|
@@ -660,15 +934,15 @@ var column = {
|
|
|
660
934
|
ctx.eatKeyword("at");
|
|
661
935
|
const at = ctx.parsePoint();
|
|
662
936
|
ctx.eatKeyword("size");
|
|
663
|
-
const
|
|
664
|
-
return { kind: "column", id, at, size
|
|
937
|
+
const size = ctx.parseDimensions();
|
|
938
|
+
return { kind: "column", id, at, size, line: kw.line };
|
|
665
939
|
},
|
|
666
940
|
idPrefix: () => "column",
|
|
667
941
|
resolve(node, ctx) {
|
|
668
942
|
const n = node;
|
|
669
|
-
const id = ctx.
|
|
670
|
-
const at = ctx.snapPt(n.at);
|
|
671
|
-
const size = { w: ctx.snap(n.size.w), h: ctx.snap(n.size.h) };
|
|
943
|
+
const id = ctx.id;
|
|
944
|
+
const at = ctx.snapPt(ctx.evalPt(n.at));
|
|
945
|
+
const size = { w: ctx.snap(ctx.eval(n.size.w)), h: ctx.snap(ctx.eval(n.size.h)) };
|
|
672
946
|
if (size.w <= 0 || size.h <= 0) {
|
|
673
947
|
ctx.diag({ severity: "error", message: `Column "${id}" must have a positive size`, code: "E_COLUMN_SIZE", span: n.span });
|
|
674
948
|
}
|
|
@@ -707,7 +981,7 @@ register(dim);
|
|
|
707
981
|
register(column);
|
|
708
982
|
|
|
709
983
|
// src/parser.ts
|
|
710
|
-
var SETTINGS = ["units", "grid", "scale", "north", "title"];
|
|
984
|
+
var SETTINGS = ["units", "grid", "scale", "north", "title", "theme", "let", "component"];
|
|
711
985
|
var STATEMENT_STARTS = /* @__PURE__ */ new Set([...SETTINGS, ...registry.keys()]);
|
|
712
986
|
var ParseError = class extends Error {
|
|
713
987
|
constructor(message, span) {
|
|
@@ -752,6 +1026,8 @@ var Parser = class {
|
|
|
752
1026
|
isKeyword: (kw, o) => this.isKeyword(kw, o),
|
|
753
1027
|
isType: (type) => this.isType(type),
|
|
754
1028
|
parsePoint: () => this.parsePoint(),
|
|
1029
|
+
parseExpr: () => parseExpr(this.ctx),
|
|
1030
|
+
parseDimensions: () => this.parseDimensions(),
|
|
755
1031
|
parseIdOpt: () => this.parseIdOpt(),
|
|
756
1032
|
fail: (msg, t) => this.fail(msg, t)
|
|
757
1033
|
};
|
|
@@ -790,12 +1066,12 @@ var Parser = class {
|
|
|
790
1066
|
}
|
|
791
1067
|
eatKeyword(kw) {
|
|
792
1068
|
const t = this.peek();
|
|
793
|
-
if (t.type !== "ident" || t.value !== kw) this.fail(`Expected "${kw}" but found ${
|
|
1069
|
+
if (t.type !== "ident" || t.value !== kw) this.fail(`Expected "${kw}" but found ${describe3(t)}`);
|
|
794
1070
|
return this.next();
|
|
795
1071
|
}
|
|
796
1072
|
eat(type) {
|
|
797
1073
|
const t = this.peek();
|
|
798
|
-
if (t.type !== type) this.fail(`Expected ${type} but found ${
|
|
1074
|
+
if (t.type !== type) this.fail(`Expected ${type} but found ${describe3(t)}`);
|
|
799
1075
|
return this.next();
|
|
800
1076
|
}
|
|
801
1077
|
eatIdent() {
|
|
@@ -817,18 +1093,25 @@ var Parser = class {
|
|
|
817
1093
|
units: "mm",
|
|
818
1094
|
grid: 0,
|
|
819
1095
|
north: "up",
|
|
820
|
-
|
|
1096
|
+
components: /* @__PURE__ */ new Map(),
|
|
1097
|
+
body: []
|
|
821
1098
|
};
|
|
822
1099
|
while (!this.isType("rcurly") && !this.isType("eof")) {
|
|
823
1100
|
const t = this.peek();
|
|
824
1101
|
const start = t.start;
|
|
825
1102
|
try {
|
|
826
|
-
if (t.type !== "ident") this.fail(`Expected a statement but found ${
|
|
1103
|
+
if (t.type !== "ident") this.fail(`Expected a statement but found ${describe3(t)}`);
|
|
827
1104
|
const def = registry.get(t.value);
|
|
828
1105
|
if (def) {
|
|
829
1106
|
const node = def.parse(this.ctx);
|
|
830
1107
|
node.span = this.spanFrom(start);
|
|
831
|
-
plan.
|
|
1108
|
+
plan.body.push(node);
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
if (plan.components.has(t.value) && this.peek(1).type === "lparen") {
|
|
1112
|
+
const node = this.parseInstance();
|
|
1113
|
+
node.span = this.spanFrom(start);
|
|
1114
|
+
plan.body.push(node);
|
|
832
1115
|
continue;
|
|
833
1116
|
}
|
|
834
1117
|
switch (t.value) {
|
|
@@ -861,6 +1144,25 @@ var Parser = class {
|
|
|
861
1144
|
plan.title = n;
|
|
862
1145
|
break;
|
|
863
1146
|
}
|
|
1147
|
+
case "theme": {
|
|
1148
|
+
plan.theme = { ...plan.theme, ...this.parseTheme() };
|
|
1149
|
+
break;
|
|
1150
|
+
}
|
|
1151
|
+
case "let": {
|
|
1152
|
+
const n = this.parseLet();
|
|
1153
|
+
n.span = this.spanFrom(start);
|
|
1154
|
+
plan.body.push(n);
|
|
1155
|
+
break;
|
|
1156
|
+
}
|
|
1157
|
+
case "component": {
|
|
1158
|
+
const def2 = this.parseComponent(plan.components);
|
|
1159
|
+
def2.span = this.spanFrom(start);
|
|
1160
|
+
if (plan.components.has(def2.name)) {
|
|
1161
|
+
this.fail(`Component "${def2.name}" is already defined`, t);
|
|
1162
|
+
}
|
|
1163
|
+
plan.components.set(def2.name, def2);
|
|
1164
|
+
break;
|
|
1165
|
+
}
|
|
864
1166
|
default:
|
|
865
1167
|
this.fail(`Unknown statement "${t.value}"`, t);
|
|
866
1168
|
}
|
|
@@ -906,23 +1208,35 @@ var Parser = class {
|
|
|
906
1208
|
this.next();
|
|
907
1209
|
return t.value;
|
|
908
1210
|
}
|
|
909
|
-
this.fail(`Expected a north direction (up|down|left|right|<degrees>) but found ${
|
|
1211
|
+
this.fail(`Expected a north direction (up|down|left|right|<degrees>) but found ${describe3(t)}`);
|
|
910
1212
|
}
|
|
911
1213
|
parsePoint() {
|
|
912
1214
|
this.eat("lparen");
|
|
913
|
-
const x = this.
|
|
1215
|
+
const x = parseExpr(this.ctx);
|
|
914
1216
|
this.eat("comma");
|
|
915
|
-
const y = this.
|
|
1217
|
+
const y = parseExpr(this.ctx);
|
|
916
1218
|
this.eat("rparen");
|
|
917
1219
|
return { x, y };
|
|
918
1220
|
}
|
|
1221
|
+
/** A size: either a `WxH` literal dimension token or `<expr> x <expr>`. */
|
|
1222
|
+
parseDimensions() {
|
|
1223
|
+
if (this.isType("dimension")) {
|
|
1224
|
+
const d = this.eat("dimension");
|
|
1225
|
+
return { w: { t: "num", value: d.num }, h: { t: "num", value: d.num2 } };
|
|
1226
|
+
}
|
|
1227
|
+
const w = parseExpr(this.ctx);
|
|
1228
|
+
if (this.isKeyword("x")) this.next();
|
|
1229
|
+
else this.fail(`Expected "x" between width and height but found ${describe3(this.peek())}`);
|
|
1230
|
+
const h = parseExpr(this.ctx);
|
|
1231
|
+
return { w, h };
|
|
1232
|
+
}
|
|
919
1233
|
parseTitle() {
|
|
920
1234
|
const kw = this.eatKeyword("title");
|
|
921
1235
|
this.eat("lcurly");
|
|
922
1236
|
const node = { line: kw.line };
|
|
923
1237
|
while (!this.isType("rcurly") && !this.isType("eof")) {
|
|
924
1238
|
const t = this.peek();
|
|
925
|
-
if (t.type !== "ident") this.fail(`Expected a title field but found ${
|
|
1239
|
+
if (t.type !== "ident") this.fail(`Expected a title field but found ${describe3(t)}`);
|
|
926
1240
|
switch (t.value) {
|
|
927
1241
|
case "project":
|
|
928
1242
|
this.next();
|
|
@@ -943,20 +1257,150 @@ var Parser = class {
|
|
|
943
1257
|
this.eat("rcurly");
|
|
944
1258
|
return node;
|
|
945
1259
|
}
|
|
1260
|
+
/** `theme { key: <value> … }` — colours (strings), `lineWeight` (number), `font` (string). */
|
|
1261
|
+
parseTheme() {
|
|
1262
|
+
this.eatKeyword("theme");
|
|
1263
|
+
this.eat("lcurly");
|
|
1264
|
+
const t = {};
|
|
1265
|
+
while (!this.isType("rcurly") && !this.isType("eof")) {
|
|
1266
|
+
const keyTok = this.eatIdent();
|
|
1267
|
+
if (this.isType("colon")) this.next();
|
|
1268
|
+
const resolved = resolveThemeKey(keyTok.value);
|
|
1269
|
+
if (!resolved) {
|
|
1270
|
+
this.diagnostics.push({
|
|
1271
|
+
severity: "warning",
|
|
1272
|
+
message: `Unknown theme key "${keyTok.value}"`,
|
|
1273
|
+
code: "W_UNKNOWN_THEME_KEY",
|
|
1274
|
+
span: { start: keyTok.start, end: keyTok.end }
|
|
1275
|
+
});
|
|
1276
|
+
if (this.isType("string") || this.isType("number")) this.next();
|
|
1277
|
+
else this.fail(`Expected a value for theme key "${keyTok.value}"`);
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1280
|
+
if (isNumericThemeKey(resolved)) {
|
|
1281
|
+
t[resolved] = this.eatNumber();
|
|
1282
|
+
} else {
|
|
1283
|
+
t[resolved] = this.eatString();
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
this.eat("rcurly");
|
|
1287
|
+
return t;
|
|
1288
|
+
}
|
|
1289
|
+
parseLet() {
|
|
1290
|
+
const kw = this.eatKeyword("let");
|
|
1291
|
+
const name = this.eatIdent().value;
|
|
1292
|
+
this.eat("equals");
|
|
1293
|
+
const value = parseExpr(this.ctx);
|
|
1294
|
+
return { kind: "let", id: "", name, value, line: kw.line };
|
|
1295
|
+
}
|
|
1296
|
+
parseInstance() {
|
|
1297
|
+
const nameTok = this.eatIdent();
|
|
1298
|
+
this.eat("lparen");
|
|
1299
|
+
const args = [];
|
|
1300
|
+
while (!this.isType("rparen") && !this.isType("eof")) {
|
|
1301
|
+
args.push(parseExpr(this.ctx));
|
|
1302
|
+
if (this.isType("comma")) this.next();
|
|
1303
|
+
else break;
|
|
1304
|
+
}
|
|
1305
|
+
this.eat("rparen");
|
|
1306
|
+
return { kind: "instance", id: "", name: nameTok.value, args, line: nameTok.line };
|
|
1307
|
+
}
|
|
1308
|
+
/** `component NAME(p1, p2, …) { <statements> }`. */
|
|
1309
|
+
parseComponent(components) {
|
|
1310
|
+
const kw = this.eatKeyword("component");
|
|
1311
|
+
const name = this.eatIdent().value;
|
|
1312
|
+
this.eat("lparen");
|
|
1313
|
+
const params = [];
|
|
1314
|
+
while (!this.isType("rparen") && !this.isType("eof")) {
|
|
1315
|
+
params.push(this.eatIdent().value);
|
|
1316
|
+
if (this.isType("comma")) this.next();
|
|
1317
|
+
else break;
|
|
1318
|
+
}
|
|
1319
|
+
this.eat("rparen");
|
|
1320
|
+
this.eat("lcurly");
|
|
1321
|
+
const body = [];
|
|
1322
|
+
while (!this.isType("rcurly") && !this.isType("eof")) {
|
|
1323
|
+
const t = this.peek();
|
|
1324
|
+
const start = t.start;
|
|
1325
|
+
const def = registry.get(t.value);
|
|
1326
|
+
if (def) {
|
|
1327
|
+
const node = def.parse(this.ctx);
|
|
1328
|
+
node.span = this.spanFrom(start);
|
|
1329
|
+
body.push(node);
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
if (t.value === "let") {
|
|
1333
|
+
const n = this.parseLet();
|
|
1334
|
+
n.span = this.spanFrom(start);
|
|
1335
|
+
body.push(n);
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1338
|
+
if ((components.has(t.value) || t.value === name) && this.peek(1).type === "lparen") {
|
|
1339
|
+
const n = this.parseInstance();
|
|
1340
|
+
n.span = this.spanFrom(start);
|
|
1341
|
+
body.push(n);
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
this.fail(`Expected an element, "let", or component call in component body but found ${describe3(t)}`, t);
|
|
1345
|
+
}
|
|
1346
|
+
this.eat("rcurly");
|
|
1347
|
+
return { name, params, body, line: kw.line };
|
|
1348
|
+
}
|
|
946
1349
|
};
|
|
947
|
-
function
|
|
1350
|
+
function describe3(t) {
|
|
948
1351
|
if (t.type === "eof") return "end of input";
|
|
949
1352
|
if (t.type === "string") return `string ${JSON.stringify(t.value)}`;
|
|
950
1353
|
return `"${t.value}"`;
|
|
951
1354
|
}
|
|
952
1355
|
|
|
953
1356
|
// src/ir.ts
|
|
1357
|
+
var MAX_DEPTH = 64;
|
|
1358
|
+
function expandScope(body, env, defined, global, components, diagnostics, depth) {
|
|
1359
|
+
const diag = (d) => diagnostics.push(d);
|
|
1360
|
+
const out = [];
|
|
1361
|
+
for (const stmt of body) {
|
|
1362
|
+
if (stmt.kind === "let") {
|
|
1363
|
+
if (defined.has(stmt.name)) {
|
|
1364
|
+
diag({ severity: "error", message: `"${stmt.name}" is already defined in this scope`, code: "E_REDEF", span: stmt.span });
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
env.set(stmt.name, evalExpr(stmt.value, env, diag));
|
|
1368
|
+
defined.add(stmt.name);
|
|
1369
|
+
} else if (stmt.kind === "instance") {
|
|
1370
|
+
const comp = components.get(stmt.name);
|
|
1371
|
+
if (!comp) {
|
|
1372
|
+
const hint = closest(stmt.name, [...components.keys()]);
|
|
1373
|
+
diag({ severity: "error", message: `Unknown component "${stmt.name}"`, code: "E_UNKNOWN_COMPONENT", span: stmt.span, hints: hint ? [`did you mean "${hint}"?`] : void 0 });
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
if (depth >= MAX_DEPTH) {
|
|
1377
|
+
diag({ severity: "error", message: `Component recursion too deep (limit ${MAX_DEPTH}) instantiating "${stmt.name}"`, code: "E_RECURSION", span: stmt.span });
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
if (stmt.args.length !== comp.params.length) {
|
|
1381
|
+
diag({ severity: "error", message: `Component "${stmt.name}" expects ${comp.params.length} argument(s) but got ${stmt.args.length}`, code: "E_ARGCOUNT", span: stmt.span });
|
|
1382
|
+
}
|
|
1383
|
+
const argVals = comp.params.map((_, i) => stmt.args[i] !== void 0 ? evalExpr(stmt.args[i], env, diag) : 0);
|
|
1384
|
+
const childEnv = new Map(global);
|
|
1385
|
+
const childDefined = /* @__PURE__ */ new Set();
|
|
1386
|
+
comp.params.forEach((p, i) => {
|
|
1387
|
+
childEnv.set(p, argVals[i]);
|
|
1388
|
+
childDefined.add(p);
|
|
1389
|
+
});
|
|
1390
|
+
out.push(...expandScope(comp.body, childEnv, childDefined, global, components, diagnostics, depth + 1));
|
|
1391
|
+
} else {
|
|
1392
|
+
out.push({ node: stmt, env: new Map(env), id: "" });
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
return out;
|
|
1396
|
+
}
|
|
954
1397
|
function resolve(ast) {
|
|
955
1398
|
const diagnostics = [];
|
|
956
1399
|
const g = ast.grid;
|
|
957
1400
|
const snap = (v) => g > 0 ? Math.round(v / g) * g : v;
|
|
958
1401
|
const snapPt = (p) => ({ x: snap(p.x), y: snap(p.y) });
|
|
959
|
-
const
|
|
1402
|
+
const globalEnv = /* @__PURE__ */ new Map();
|
|
1403
|
+
const entries = expandScope(ast.body, globalEnv, /* @__PURE__ */ new Set(), globalEnv, ast.components, diagnostics, 0);
|
|
960
1404
|
const seen = /* @__PURE__ */ new Set();
|
|
961
1405
|
const assignId = (provided, prefix, idx, span) => {
|
|
962
1406
|
if (provided) {
|
|
@@ -973,33 +1417,39 @@ function resolve(ast) {
|
|
|
973
1417
|
};
|
|
974
1418
|
for (const def of registryOrder) {
|
|
975
1419
|
let idx = 0;
|
|
976
|
-
for (const
|
|
977
|
-
if (node.kind !== def.kind) continue;
|
|
1420
|
+
for (const e of entries) {
|
|
1421
|
+
if (e.node.kind !== def.kind) continue;
|
|
978
1422
|
idx++;
|
|
979
|
-
|
|
1423
|
+
e.id = assignId(e.node.id, def.idPrefix(e.node), idx, e.node.span);
|
|
980
1424
|
}
|
|
981
1425
|
}
|
|
982
1426
|
const walls = [];
|
|
1427
|
+
let activeEnv = /* @__PURE__ */ new Map();
|
|
1428
|
+
const evalNum = (e) => evalExpr(e, activeEnv, (d) => diagnostics.push(d));
|
|
1429
|
+
const evalPt = (p) => ({ x: evalNum(p.x), y: evalNum(p.y) });
|
|
983
1430
|
const ctx = {
|
|
984
1431
|
grid: g,
|
|
985
1432
|
snap,
|
|
986
1433
|
snapPt,
|
|
987
|
-
|
|
1434
|
+
eval: evalNum,
|
|
1435
|
+
evalPt,
|
|
1436
|
+
id: "",
|
|
988
1437
|
walls,
|
|
989
1438
|
hostSegment: (at, ref) => hostSegmentForWalls(walls, at, ref),
|
|
990
1439
|
isOnWall: (at, ref) => isOnSomeWall(walls, at, ref),
|
|
991
1440
|
diag: (d) => diagnostics.push(d)
|
|
992
1441
|
};
|
|
993
|
-
const rmap = /* @__PURE__ */ new Map();
|
|
994
1442
|
for (const def of registryOrder) {
|
|
995
|
-
for (const
|
|
996
|
-
if (node.kind !== def.kind) continue;
|
|
997
|
-
|
|
998
|
-
|
|
1443
|
+
for (const e of entries) {
|
|
1444
|
+
if (e.node.kind !== def.kind) continue;
|
|
1445
|
+
activeEnv = e.env;
|
|
1446
|
+
ctx.id = e.id;
|
|
1447
|
+
const r = def.resolve(e.node, ctx);
|
|
1448
|
+
e.resolved = r;
|
|
999
1449
|
if (r.kind === "wall") walls.push(r);
|
|
1000
1450
|
}
|
|
1001
1451
|
}
|
|
1002
|
-
const elements =
|
|
1452
|
+
const elements = entries.map((e) => e.resolved);
|
|
1003
1453
|
const drawable = elements.some(
|
|
1004
1454
|
(e) => e.kind === "wall" || e.kind === "room" || e.kind === "furniture" || e.kind === "column"
|
|
1005
1455
|
);
|
|
@@ -1034,6 +1484,7 @@ function resolve(ast) {
|
|
|
1034
1484
|
scale: ast.scale,
|
|
1035
1485
|
north: ast.north,
|
|
1036
1486
|
title: ast.title,
|
|
1487
|
+
theme: ast.theme,
|
|
1037
1488
|
elements,
|
|
1038
1489
|
walls
|
|
1039
1490
|
};
|
|
@@ -1053,26 +1504,104 @@ var RENDER_PASSES = [
|
|
|
1053
1504
|
"annotations"
|
|
1054
1505
|
];
|
|
1055
1506
|
|
|
1507
|
+
// src/geometry/union.ts
|
|
1508
|
+
function uniqSorted(values) {
|
|
1509
|
+
const out = [...new Set(values)].sort((a, b) => a - b);
|
|
1510
|
+
return out;
|
|
1511
|
+
}
|
|
1512
|
+
function rectUnionOutline(rects) {
|
|
1513
|
+
if (rects.length === 0) return [];
|
|
1514
|
+
const xs = uniqSorted(rects.flatMap((r) => [r.x0, r.x1]));
|
|
1515
|
+
const ys = uniqSorted(rects.flatMap((r) => [r.y0, r.y1]));
|
|
1516
|
+
const nx = xs.length - 1;
|
|
1517
|
+
const ny = ys.length - 1;
|
|
1518
|
+
const filled = (i, j) => {
|
|
1519
|
+
if (i < 0 || j < 0 || i >= nx || j >= ny) return false;
|
|
1520
|
+
const cx = (xs[i] + xs[i + 1]) / 2;
|
|
1521
|
+
const cy = (ys[j] + ys[j + 1]) / 2;
|
|
1522
|
+
return rects.some((r) => cx > r.x0 && cx < r.x1 && cy > r.y0 && cy < r.y1);
|
|
1523
|
+
};
|
|
1524
|
+
const key = (x, y) => `${x},${y}`;
|
|
1525
|
+
const starts = /* @__PURE__ */ new Map();
|
|
1526
|
+
const pushEdge = (ax, ay, bx, by) => {
|
|
1527
|
+
const k = key(ax, ay);
|
|
1528
|
+
const list = starts.get(k);
|
|
1529
|
+
if (list) list.push({ x: bx, y: by });
|
|
1530
|
+
else starts.set(k, [{ x: bx, y: by }]);
|
|
1531
|
+
};
|
|
1532
|
+
for (let i = 0; i < nx; i++) {
|
|
1533
|
+
for (let j = 0; j < ny; j++) {
|
|
1534
|
+
if (!filled(i, j)) continue;
|
|
1535
|
+
const x0 = xs[i];
|
|
1536
|
+
const x1 = xs[i + 1];
|
|
1537
|
+
const y0 = ys[j];
|
|
1538
|
+
const y1 = ys[j + 1];
|
|
1539
|
+
if (!filled(i, j - 1)) pushEdge(x1, y0, x0, y0);
|
|
1540
|
+
if (!filled(i - 1, j)) pushEdge(x0, y0, x0, y1);
|
|
1541
|
+
if (!filled(i, j + 1)) pushEdge(x0, y1, x1, y1);
|
|
1542
|
+
if (!filled(i + 1, j)) pushEdge(x1, y1, x1, y0);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
const used = /* @__PURE__ */ new Set();
|
|
1546
|
+
const edgeKey = (a, b) => `${a.x},${a.y}->${b.x},${b.y}`;
|
|
1547
|
+
const loops = [];
|
|
1548
|
+
const takeNext = (from, prevDir) => {
|
|
1549
|
+
const list = starts.get(key(from.x, from.y));
|
|
1550
|
+
if (!list) return null;
|
|
1551
|
+
const candidates = list.filter((to) => !used.has(edgeKey(from, to)));
|
|
1552
|
+
if (candidates.length === 0) return null;
|
|
1553
|
+
if (candidates.length === 1 || !prevDir) return candidates[0];
|
|
1554
|
+
let best = candidates[0];
|
|
1555
|
+
let bestScore = -Infinity;
|
|
1556
|
+
for (const c of candidates) {
|
|
1557
|
+
const dir = { x: c.x - from.x, y: c.y - from.y };
|
|
1558
|
+
const cross = prevDir.x * dir.y - prevDir.y * dir.x;
|
|
1559
|
+
if (cross > bestScore) {
|
|
1560
|
+
bestScore = cross;
|
|
1561
|
+
best = c;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
return best;
|
|
1565
|
+
};
|
|
1566
|
+
for (const [startKey, ends] of starts) {
|
|
1567
|
+
for (const firstEnd of ends) {
|
|
1568
|
+
const [sx, sy] = startKey.split(",").map(Number);
|
|
1569
|
+
const start = { x: sx, y: sy };
|
|
1570
|
+
if (used.has(edgeKey(start, firstEnd))) continue;
|
|
1571
|
+
const loop = [start];
|
|
1572
|
+
let cur = start;
|
|
1573
|
+
let next = firstEnd;
|
|
1574
|
+
let dir = null;
|
|
1575
|
+
while (next) {
|
|
1576
|
+
used.add(edgeKey(cur, next));
|
|
1577
|
+
if (next.x === start.x && next.y === start.y) break;
|
|
1578
|
+
loop.push(next);
|
|
1579
|
+
dir = { x: next.x - cur.x, y: next.y - cur.y };
|
|
1580
|
+
cur = next;
|
|
1581
|
+
next = takeNext(cur, dir);
|
|
1582
|
+
}
|
|
1583
|
+
if (loop.length >= 4) loops.push(mergeCollinear(loop));
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
return loops;
|
|
1587
|
+
}
|
|
1588
|
+
function mergeCollinear(loop) {
|
|
1589
|
+
const n = loop.length;
|
|
1590
|
+
const out = [];
|
|
1591
|
+
for (let i = 0; i < n; i++) {
|
|
1592
|
+
const prev = loop[(i - 1 + n) % n];
|
|
1593
|
+
const cur = loop[i];
|
|
1594
|
+
const next = loop[(i + 1) % n];
|
|
1595
|
+
const d1x = cur.x - prev.x;
|
|
1596
|
+
const d1y = cur.y - prev.y;
|
|
1597
|
+
const d2x = next.x - cur.x;
|
|
1598
|
+
const d2y = next.y - cur.y;
|
|
1599
|
+
if (d1x * d2y - d1y * d2x !== 0) out.push(cur);
|
|
1600
|
+
}
|
|
1601
|
+
return out.length >= 3 ? out : loop;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1056
1604
|
// 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
1605
|
function fmt(v) {
|
|
1077
1606
|
const r = Math.round(v * 100) / 100;
|
|
1078
1607
|
return Object.is(r, -0) ? "0" : String(r);
|
|
@@ -1099,15 +1628,56 @@ function planBounds(ir) {
|
|
|
1099
1628
|
}
|
|
1100
1629
|
return b;
|
|
1101
1630
|
}
|
|
1631
|
+
function allOrthogonal(walls) {
|
|
1632
|
+
return walls.every((w) => segmentsOfWall(w).every((s) => s.a.x === s.b.x || s.a.y === s.b.y));
|
|
1633
|
+
}
|
|
1634
|
+
function loopsToPath(loops) {
|
|
1635
|
+
return loops.map((loop) => "M " + loop.map(pt).join(" L ") + " Z").join(" ");
|
|
1636
|
+
}
|
|
1637
|
+
function materialsUsed(walls) {
|
|
1638
|
+
return [...new Set(walls.map((w) => w.material))].sort();
|
|
1639
|
+
}
|
|
1640
|
+
function renderWalls(walls, ctx) {
|
|
1641
|
+
if (walls.length === 0) return [];
|
|
1642
|
+
const ops = [];
|
|
1643
|
+
for (const mat of materialsUsed(walls)) {
|
|
1644
|
+
const group = walls.filter((w) => w.material === mat);
|
|
1645
|
+
if (!allOrthogonal(group)) {
|
|
1646
|
+
const def = registry.get("wall");
|
|
1647
|
+
ops.push(...group.flatMap((w) => def.render(w, ctx)));
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
const rects = [];
|
|
1651
|
+
for (const w of group) {
|
|
1652
|
+
for (const s of segmentsOfWall(w)) {
|
|
1653
|
+
const corners = segmentRectangle(s.a, s.b, s.thickness);
|
|
1654
|
+
const xsv = corners.map((c) => c.x);
|
|
1655
|
+
const ysv = corners.map((c) => c.y);
|
|
1656
|
+
rects.push({ x0: Math.min(...xsv), y0: Math.min(...ysv), x1: Math.max(...xsv), y1: Math.max(...ysv) });
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
const loops = rectUnionOutline(rects);
|
|
1660
|
+
if (loops.length === 0) continue;
|
|
1661
|
+
const d = loopsToPath(loops);
|
|
1662
|
+
ops.push({ pass: "wallFill", svg: `<path d="${d}" fill="url(#${patternId(mat)})" fill-rule="nonzero"/>` });
|
|
1663
|
+
ops.push({
|
|
1664
|
+
pass: "wallFace",
|
|
1665
|
+
svg: `<path d="${d}" fill="none" stroke="${ctx.theme.wallStroke}" stroke-width="${ctx.fmt(ctx.sizes.wallStroke)}" stroke-linejoin="miter"/>`
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
return ops;
|
|
1669
|
+
}
|
|
1102
1670
|
function render(ir, opts = {}) {
|
|
1671
|
+
const THEME = mergeTheme(DEFAULT_THEME, ir.theme, opts.theme);
|
|
1672
|
+
const lw = THEME.lineWeight;
|
|
1103
1673
|
const b = planBounds(ir);
|
|
1104
1674
|
const drawW = b.maxX - b.minX;
|
|
1105
1675
|
const drawH = b.maxY - b.minY;
|
|
1106
1676
|
const refDim = Math.max(drawW, drawH, 1);
|
|
1107
1677
|
const sizes = {
|
|
1108
1678
|
refDim,
|
|
1109
|
-
wallStroke: refDim * 28e-4,
|
|
1110
|
-
thin: refDim * 16e-4,
|
|
1679
|
+
wallStroke: refDim * 28e-4 * lw,
|
|
1680
|
+
thin: refDim * 16e-4 * lw,
|
|
1111
1681
|
roomFont: refDim * 0.03,
|
|
1112
1682
|
areaFont: refDim * 0.022,
|
|
1113
1683
|
dimFont: refDim * 0.02,
|
|
@@ -1123,28 +1693,30 @@ function render(ir, opts = {}) {
|
|
|
1123
1693
|
const out = [];
|
|
1124
1694
|
const svgAttrs = opts.width ? `width="${fmt(opts.width)}" height="${fmt(opts.width * vbH / vbW)}"` : "";
|
|
1125
1695
|
out.push(
|
|
1126
|
-
`<svg xmlns="http://www.w3.org/2000/svg" ${svgAttrs} viewBox="${fmt(vbX)} ${fmt(vbY)} ${fmt(vbW)} ${fmt(vbH)}" font-family="
|
|
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>`
|
|
1696
|
+
`<svg xmlns="http://www.w3.org/2000/svg" ${svgAttrs} viewBox="${fmt(vbX)} ${fmt(vbY)} ${fmt(vbW)} ${fmt(vbH)}" font-family="${xml(THEME.font)}">`
|
|
1130
1697
|
);
|
|
1698
|
+
const hatchCtx = { fmt, gap: hatchGap, thin, base: THEME.pocheBase, line: THEME.pocheHatch };
|
|
1699
|
+
const patterns = materialsUsed(ir.walls).map((m) => hatchPattern(m, hatchCtx)).join("");
|
|
1700
|
+
out.push(`<defs>${patterns}</defs>`);
|
|
1131
1701
|
out.push(`<rect x="${fmt(vbX)}" y="${fmt(vbY)}" width="${fmt(vbW)}" height="${fmt(vbH)}" fill="${THEME.bg}"/>`);
|
|
1132
1702
|
const ctx = { fmt, pt, xml, theme: THEME, sizes, bounds: b };
|
|
1133
1703
|
const ops = ir.elements.flatMap((el) => {
|
|
1704
|
+
if (el.kind === "wall") return [];
|
|
1134
1705
|
const def = registry.get(el.kind);
|
|
1135
1706
|
return def ? def.render(el, ctx) : [];
|
|
1136
1707
|
});
|
|
1708
|
+
ops.push(...renderWalls(ir.walls, ctx));
|
|
1137
1709
|
for (const pass of RENDER_PASSES) {
|
|
1138
1710
|
for (const op of ops) if (op.pass === pass) out.push(op.svg);
|
|
1139
1711
|
}
|
|
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);
|
|
1712
|
+
out.push(northArrow(ir, b, margin, refDim, THEME));
|
|
1713
|
+
out.push(scaleBar(b, margin, refDim, thin, THEME));
|
|
1714
|
+
const tb = titleBlock(ir, b, margin, refDim, thin, THEME);
|
|
1143
1715
|
if (tb) out.push(tb);
|
|
1144
1716
|
out.push("</svg>");
|
|
1145
1717
|
return out.join("\n");
|
|
1146
1718
|
}
|
|
1147
|
-
function northArrow(ir, b, margin, refDim) {
|
|
1719
|
+
function northArrow(ir, b, margin, refDim, THEME) {
|
|
1148
1720
|
const r = refDim * 0.045;
|
|
1149
1721
|
const cx = b.maxX - r;
|
|
1150
1722
|
const cy = b.minY - margin * 0.55;
|
|
@@ -1174,7 +1746,7 @@ function northArrow(ir, b, margin, refDim) {
|
|
|
1174
1746
|
const ly = cy + ny * (r + fs * 0.8);
|
|
1175
1747
|
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
1748
|
}
|
|
1177
|
-
function scaleBar(b, margin, refDim, thin) {
|
|
1749
|
+
function scaleBar(b, margin, refDim, thin, THEME) {
|
|
1178
1750
|
const barLen = niceBarLength(refDim * 0.3);
|
|
1179
1751
|
const x0 = b.minX;
|
|
1180
1752
|
const y0 = b.maxY + margin * 0.55;
|
|
@@ -1194,7 +1766,7 @@ function scaleBar(b, margin, refDim, thin) {
|
|
|
1194
1766
|
);
|
|
1195
1767
|
return `<g>${parts.join("")}</g>`;
|
|
1196
1768
|
}
|
|
1197
|
-
function titleBlock(ir, b, margin, refDim, thin) {
|
|
1769
|
+
function titleBlock(ir, b, margin, refDim, thin, THEME) {
|
|
1198
1770
|
const t = ir.title;
|
|
1199
1771
|
if (!t && !ir.scale) return null;
|
|
1200
1772
|
const boxW = refDim * 0.34;
|
|
@@ -1287,7 +1859,7 @@ function formatDiagnostic(source, d) {
|
|
|
1287
1859
|
var cache = /* @__PURE__ */ new Map();
|
|
1288
1860
|
var CACHE_MAX = 64;
|
|
1289
1861
|
function compile(source, opts = {}) {
|
|
1290
|
-
const key = JSON.stringify([source, opts.width ?? null]);
|
|
1862
|
+
const key = JSON.stringify([source, opts.width ?? null, opts.theme ?? null]);
|
|
1291
1863
|
if (!opts.noCache) {
|
|
1292
1864
|
const hit = cache.get(key);
|
|
1293
1865
|
if (hit) return hit;
|
|
@@ -1327,4 +1899,4 @@ export {
|
|
|
1327
1899
|
compile,
|
|
1328
1900
|
clearCache
|
|
1329
1901
|
};
|
|
1330
|
-
//# sourceMappingURL=chunk-
|
|
1902
|
+
//# sourceMappingURL=chunk-GUNWYUR2.js.map
|