@chanmeng666/archlang 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-ICYNEDSM.js → chunk-3YUQPQPZ.js} +725 -521
- package/dist/chunk-3YUQPQPZ.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +44 -43
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-ICYNEDSM.js.map +0 -1
|
@@ -138,20 +138,577 @@ function lex(src) {
|
|
|
138
138
|
return { tokens, errors };
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
// src/geometry.ts
|
|
142
|
+
var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
|
|
143
|
+
var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
|
|
144
|
+
var mul = (v, s) => ({ x: v.x * s, y: v.y * s });
|
|
145
|
+
var length = (v) => Math.hypot(v.x, v.y);
|
|
146
|
+
function unit(v) {
|
|
147
|
+
const l = length(v);
|
|
148
|
+
return l === 0 ? { x: 0, y: 0 } : { x: v.x / l, y: v.y / l };
|
|
149
|
+
}
|
|
150
|
+
var normal = (v) => ({ x: -v.y, y: v.x });
|
|
151
|
+
var emptyBounds = () => ({
|
|
152
|
+
minX: Infinity,
|
|
153
|
+
minY: Infinity,
|
|
154
|
+
maxX: -Infinity,
|
|
155
|
+
maxY: -Infinity
|
|
156
|
+
});
|
|
157
|
+
function extendBounds(b, x, y) {
|
|
158
|
+
if (x < b.minX) b.minX = x;
|
|
159
|
+
if (y < b.minY) b.minY = y;
|
|
160
|
+
if (x > b.maxX) b.maxX = x;
|
|
161
|
+
if (y > b.maxY) b.maxY = y;
|
|
162
|
+
}
|
|
163
|
+
function distPointToSegment(p, a, b) {
|
|
164
|
+
const abx = b.x - a.x;
|
|
165
|
+
const aby = b.y - a.y;
|
|
166
|
+
const apx = p.x - a.x;
|
|
167
|
+
const apy = p.y - a.y;
|
|
168
|
+
const len2 = abx * abx + aby * aby;
|
|
169
|
+
let t = len2 === 0 ? 0 : (apx * abx + apy * aby) / len2;
|
|
170
|
+
t = Math.max(0, Math.min(1, t));
|
|
171
|
+
const cx = a.x + t * abx;
|
|
172
|
+
const cy = a.y + t * aby;
|
|
173
|
+
return Math.hypot(p.x - cx, p.y - cy);
|
|
174
|
+
}
|
|
175
|
+
function rectCorners(x, y, w, h) {
|
|
176
|
+
return [
|
|
177
|
+
{ x, y },
|
|
178
|
+
{ x: x + w, y },
|
|
179
|
+
{ x: x + w, y: y + h },
|
|
180
|
+
{ x, y: y + h }
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
function segmentRectangle(a, b, thickness) {
|
|
184
|
+
const d = unit(sub(b, a));
|
|
185
|
+
const n = normal(d);
|
|
186
|
+
const half = thickness / 2;
|
|
187
|
+
const a2 = add(a, mul(d, -half));
|
|
188
|
+
const b2 = add(b, mul(d, half));
|
|
189
|
+
return [
|
|
190
|
+
add(a2, mul(n, half)),
|
|
191
|
+
add(b2, mul(n, half)),
|
|
192
|
+
add(b2, mul(n, -half)),
|
|
193
|
+
add(a2, mul(n, -half))
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
function segmentsOfWall(w) {
|
|
197
|
+
const segs = [];
|
|
198
|
+
for (let k = 0; k < w.points.length - 1; k++) {
|
|
199
|
+
segs.push({ a: w.points[k], b: w.points[k + 1], thickness: w.thickness, category: w.category });
|
|
200
|
+
}
|
|
201
|
+
if (w.closed && w.points.length > 2) {
|
|
202
|
+
segs.push({ a: w.points[w.points.length - 1], b: w.points[0], thickness: w.thickness, category: w.category });
|
|
203
|
+
}
|
|
204
|
+
return segs;
|
|
205
|
+
}
|
|
206
|
+
function hostSegmentForWalls(walls, at, ref) {
|
|
207
|
+
const candidates = ref ? walls.filter((w) => w.id === ref || w.category === ref) : walls;
|
|
208
|
+
let best = null;
|
|
209
|
+
let bestDist = Infinity;
|
|
210
|
+
for (const w of candidates) {
|
|
211
|
+
for (const s of segmentsOfWall(w)) {
|
|
212
|
+
const dist = distPointToSegment(at, s.a, s.b);
|
|
213
|
+
if (dist < bestDist) {
|
|
214
|
+
bestDist = dist;
|
|
215
|
+
best = s;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return best;
|
|
220
|
+
}
|
|
221
|
+
function isOnSomeWall(walls, at, ref) {
|
|
222
|
+
const candidates = ref ? walls.filter((w) => w.id === ref || w.category === ref) : walls;
|
|
223
|
+
for (const w of candidates) {
|
|
224
|
+
const tol = w.thickness / 2 + Math.max(w.thickness, 1);
|
|
225
|
+
for (const s of segmentsOfWall(w)) {
|
|
226
|
+
if (distPointToSegment(at, s.a, s.b) <= tol) return true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/elements/wall.ts
|
|
233
|
+
var wall = {
|
|
234
|
+
kind: "wall",
|
|
235
|
+
keyword: "wall",
|
|
236
|
+
parse(ctx) {
|
|
237
|
+
const kw = ctx.eatKeyword("wall");
|
|
238
|
+
const id = ctx.parseIdOpt();
|
|
239
|
+
const category = ctx.eatIdent().value;
|
|
240
|
+
ctx.eatKeyword("thickness");
|
|
241
|
+
const thickness = ctx.eatNumber();
|
|
242
|
+
ctx.eat("lcurly");
|
|
243
|
+
const points = [];
|
|
244
|
+
let closed = false;
|
|
245
|
+
while (!ctx.isType("rcurly") && !ctx.isType("eof")) {
|
|
246
|
+
if (ctx.isKeyword("close")) {
|
|
247
|
+
ctx.next();
|
|
248
|
+
closed = true;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
if (ctx.isType("lparen")) {
|
|
252
|
+
points.push(ctx.parsePoint());
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
ctx.fail(`Expected a point "(x,y)" or "close" in wall body but found ${describe(ctx)}`);
|
|
256
|
+
}
|
|
257
|
+
ctx.eat("rcurly");
|
|
258
|
+
if (points.length < 2) ctx.fail("A wall needs at least two points", kw);
|
|
259
|
+
return { kind: "wall", id, category, thickness, points, closed, line: kw.line };
|
|
260
|
+
},
|
|
261
|
+
idPrefix: (node) => node.category || "wall",
|
|
262
|
+
resolve(node, ctx) {
|
|
263
|
+
const n = node;
|
|
264
|
+
const id = ctx.idOf(n);
|
|
265
|
+
const points = n.points.map(ctx.snapPt);
|
|
266
|
+
const thickness = ctx.snap(n.thickness) || n.thickness;
|
|
267
|
+
if (thickness <= 0) {
|
|
268
|
+
ctx.diag({ severity: "error", message: `Wall "${id}" must have a positive thickness`, code: "E_WALL_THICKNESS", span: n.span });
|
|
269
|
+
}
|
|
270
|
+
return { kind: "wall", id, category: n.category, thickness, points, closed: n.closed, span: n.span };
|
|
271
|
+
},
|
|
272
|
+
bounds(resolved) {
|
|
273
|
+
const w = resolved;
|
|
274
|
+
return segmentsOfWall(w).flatMap((s) => segmentRectangle(s.a, s.b, s.thickness));
|
|
275
|
+
},
|
|
276
|
+
render(resolved, ctx) {
|
|
277
|
+
const w = resolved;
|
|
278
|
+
const { fmt: fmt2, pt: pt2, theme, sizes } = ctx;
|
|
279
|
+
const segs = segmentsOfWall(w);
|
|
280
|
+
const ops = [];
|
|
281
|
+
for (const s of segs) {
|
|
282
|
+
const poly = segmentRectangle(s.a, s.b, s.thickness);
|
|
283
|
+
ops.push({ pass: "wallFill", svg: `<polygon points="${poly.map(pt2).join(" ")}" fill="url(#poche)"/>` });
|
|
284
|
+
}
|
|
285
|
+
for (const s of segs) {
|
|
286
|
+
const d = unit(sub(s.b, s.a));
|
|
287
|
+
const n = normal(d);
|
|
288
|
+
const h = s.thickness / 2;
|
|
289
|
+
const fa1 = add(s.a, mul(n, h));
|
|
290
|
+
const fb1 = add(s.b, mul(n, h));
|
|
291
|
+
const fa2 = add(s.a, mul(n, -h));
|
|
292
|
+
const fb2 = add(s.b, mul(n, -h));
|
|
293
|
+
ops.push({
|
|
294
|
+
pass: "wallFace",
|
|
295
|
+
svg: `<line x1="${fmt2(fa1.x)}" y1="${fmt2(fa1.y)}" x2="${fmt2(fb1.x)}" y2="${fmt2(fb1.y)}" stroke="${theme.wallStroke}" stroke-width="${fmt2(sizes.wallStroke)}" stroke-linecap="square"/>`
|
|
296
|
+
});
|
|
297
|
+
ops.push({
|
|
298
|
+
pass: "wallFace",
|
|
299
|
+
svg: `<line x1="${fmt2(fa2.x)}" y1="${fmt2(fa2.y)}" x2="${fmt2(fb2.x)}" y2="${fmt2(fb2.y)}" stroke="${theme.wallStroke}" stroke-width="${fmt2(sizes.wallStroke)}" stroke-linecap="square"/>`
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return ops;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
function describe(ctx) {
|
|
306
|
+
const t = ctx.peek();
|
|
307
|
+
if (t.type === "eof") return "end of input";
|
|
308
|
+
if (t.type === "string") return `string ${JSON.stringify(t.value)}`;
|
|
309
|
+
return `"${t.value}"`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/elements/room.ts
|
|
313
|
+
var room = {
|
|
314
|
+
kind: "room",
|
|
315
|
+
keyword: "room",
|
|
316
|
+
parse(ctx) {
|
|
317
|
+
const kw = ctx.eatKeyword("room");
|
|
318
|
+
const id = ctx.parseIdOpt();
|
|
319
|
+
ctx.eatKeyword("at");
|
|
320
|
+
const at = ctx.parsePoint();
|
|
321
|
+
ctx.eatKeyword("size");
|
|
322
|
+
const dim2 = ctx.eat("dimension");
|
|
323
|
+
const node = { kind: "room", id, at, size: { w: dim2.num, h: dim2.num2 }, line: kw.line };
|
|
324
|
+
if (ctx.isKeyword("label")) {
|
|
325
|
+
ctx.next();
|
|
326
|
+
node.label = ctx.eatString();
|
|
327
|
+
}
|
|
328
|
+
return node;
|
|
329
|
+
},
|
|
330
|
+
idPrefix: () => "room",
|
|
331
|
+
resolve(node, ctx) {
|
|
332
|
+
const n = node;
|
|
333
|
+
const id = ctx.idOf(n);
|
|
334
|
+
const at = ctx.snapPt(n.at);
|
|
335
|
+
const size = { w: ctx.snap(n.size.w), h: ctx.snap(n.size.h) };
|
|
336
|
+
if (size.w <= 0 || size.h <= 0) {
|
|
337
|
+
ctx.diag({ severity: "error", message: `Room "${id}" must have a positive size`, code: "E_ROOM_SIZE", span: n.span });
|
|
338
|
+
}
|
|
339
|
+
return { kind: "room", id, at, size, label: n.label, span: n.span };
|
|
340
|
+
},
|
|
341
|
+
bounds(resolved) {
|
|
342
|
+
const r = resolved;
|
|
343
|
+
return rectCorners(r.at.x, r.at.y, r.size.w, r.size.h);
|
|
344
|
+
},
|
|
345
|
+
render(resolved, ctx) {
|
|
346
|
+
const r = resolved;
|
|
347
|
+
const { fmt: fmt2, pt: pt2, xml: xml2, theme, sizes } = ctx;
|
|
348
|
+
const ops = [];
|
|
349
|
+
const c = rectCorners(r.at.x, r.at.y, r.size.w, r.size.h);
|
|
350
|
+
ops.push({ pass: "floor", svg: `<polygon points="${c.map(pt2).join(" ")}" fill="${theme.roomFill}"/>` });
|
|
351
|
+
const cx = r.at.x + r.size.w / 2;
|
|
352
|
+
const cy = r.at.y + r.size.h / 2;
|
|
353
|
+
const areaM2 = (r.size.w / 1e3 * (r.size.h / 1e3)).toFixed(1);
|
|
354
|
+
if (r.label) {
|
|
355
|
+
ops.push({
|
|
356
|
+
pass: "labels",
|
|
357
|
+
svg: `<text x="${fmt2(cx)}" y="${fmt2(cy - sizes.roomFont * 0.2)}" font-size="${fmt2(sizes.roomFont)}" fill="${theme.roomLabel}" text-anchor="middle" dominant-baseline="central" font-weight="600">${xml2(r.label)}</text>`
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
ops.push({
|
|
361
|
+
pass: "labels",
|
|
362
|
+
svg: `<text x="${fmt2(cx)}" y="${fmt2(cy + (r.label ? sizes.roomFont * 0.9 : 0))}" font-size="${fmt2(sizes.areaFont)}" fill="${theme.areaLabel}" text-anchor="middle" dominant-baseline="central">${areaM2} m\xB2</text>`
|
|
363
|
+
});
|
|
364
|
+
return ops;
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// src/elements/door.ts
|
|
369
|
+
var door = {
|
|
370
|
+
kind: "door",
|
|
371
|
+
keyword: "door",
|
|
372
|
+
parse(ctx) {
|
|
373
|
+
const kw = ctx.eatKeyword("door");
|
|
374
|
+
const id = ctx.parseIdOpt();
|
|
375
|
+
ctx.eatKeyword("at");
|
|
376
|
+
const at = ctx.parsePoint();
|
|
377
|
+
ctx.eatKeyword("width");
|
|
378
|
+
const width = ctx.eatNumber();
|
|
379
|
+
const node = { kind: "door", id, at, width, hinge: "left", swing: "in", line: kw.line };
|
|
380
|
+
if (ctx.isKeyword("wall")) {
|
|
381
|
+
ctx.next();
|
|
382
|
+
node.wall = ctx.eatIdent().value;
|
|
383
|
+
}
|
|
384
|
+
if (ctx.isKeyword("hinge")) {
|
|
385
|
+
ctx.next();
|
|
386
|
+
const h = ctx.eatIdent().value;
|
|
387
|
+
if (h !== "left" && h !== "right") ctx.fail(`Expected hinge "left" or "right" but found "${h}"`);
|
|
388
|
+
node.hinge = h;
|
|
389
|
+
}
|
|
390
|
+
if (ctx.isKeyword("swing")) {
|
|
391
|
+
ctx.next();
|
|
392
|
+
const s = ctx.eatIdent().value;
|
|
393
|
+
if (s !== "in" && s !== "out") ctx.fail(`Expected swing "in" or "out" but found "${s}"`);
|
|
394
|
+
node.swing = s;
|
|
395
|
+
}
|
|
396
|
+
return node;
|
|
397
|
+
},
|
|
398
|
+
idPrefix: () => "door",
|
|
399
|
+
resolve(node, ctx) {
|
|
400
|
+
const n = node;
|
|
401
|
+
const id = ctx.idOf(n);
|
|
402
|
+
const at = ctx.snapPt(n.at);
|
|
403
|
+
const width = ctx.snap(n.width) || n.width;
|
|
404
|
+
if (width <= 0) {
|
|
405
|
+
ctx.diag({ severity: "error", message: `Door "${id}" must have a positive width`, code: "E_DOOR_WIDTH", span: n.span });
|
|
406
|
+
}
|
|
407
|
+
if (ctx.walls.length > 0 && !ctx.isOnWall(at, n.wall)) {
|
|
408
|
+
ctx.diag({ severity: "warning", message: `Door "${id}" does not lie on any wall`, code: "W_DOOR_OFF_WALL", span: n.span });
|
|
409
|
+
}
|
|
410
|
+
return { kind: "door", id, at, width, hinge: n.hinge, swing: n.swing, host: ctx.hostSegment(at, n.wall), span: n.span };
|
|
411
|
+
},
|
|
412
|
+
bounds: () => [],
|
|
413
|
+
render(resolved, ctx) {
|
|
414
|
+
const dr = resolved;
|
|
415
|
+
const seg = dr.host;
|
|
416
|
+
if (!seg) return [];
|
|
417
|
+
const { fmt: fmt2, pt: pt2, theme, sizes } = ctx;
|
|
418
|
+
const d = unit(sub(seg.b, seg.a));
|
|
419
|
+
const n = normal(d);
|
|
420
|
+
const h = seg.thickness / 2 + sizes.wallStroke;
|
|
421
|
+
const hw = dr.width / 2;
|
|
422
|
+
const cover = [
|
|
423
|
+
add(add(dr.at, mul(d, -hw)), mul(n, h)),
|
|
424
|
+
add(add(dr.at, mul(d, hw)), mul(n, h)),
|
|
425
|
+
add(add(dr.at, mul(d, hw)), mul(n, -h)),
|
|
426
|
+
add(add(dr.at, mul(d, -hw)), mul(n, -h))
|
|
427
|
+
];
|
|
428
|
+
const ops = [];
|
|
429
|
+
ops.push({ pass: "doors", svg: `<polygon points="${cover.map(pt2).join(" ")}" fill="${theme.opening}"/>` });
|
|
430
|
+
const hinge = dr.hinge === "left" ? add(dr.at, mul(d, -hw)) : add(dr.at, mul(d, hw));
|
|
431
|
+
const farJamb = dr.hinge === "left" ? add(dr.at, mul(d, hw)) : add(dr.at, mul(d, -hw));
|
|
432
|
+
const leafDir = dr.swing === "in" ? n : mul(n, -1);
|
|
433
|
+
const leafEnd = add(hinge, mul(leafDir, dr.width));
|
|
434
|
+
const cross = (leafEnd.x - hinge.x) * (farJamb.y - hinge.y) - (leafEnd.y - hinge.y) * (farJamb.x - hinge.x);
|
|
435
|
+
const sweep = cross < 0 ? 1 : 0;
|
|
436
|
+
ops.push({
|
|
437
|
+
pass: "doors",
|
|
438
|
+
svg: `<line x1="${fmt2(hinge.x)}" y1="${fmt2(hinge.y)}" x2="${fmt2(leafEnd.x)}" y2="${fmt2(leafEnd.y)}" stroke="${theme.doorLeaf}" stroke-width="${fmt2(sizes.thin * 1.3)}"/>`
|
|
439
|
+
});
|
|
440
|
+
ops.push({
|
|
441
|
+
pass: "doors",
|
|
442
|
+
svg: `<path d="M ${pt2(leafEnd)} A ${fmt2(dr.width)} ${fmt2(dr.width)} 0 0 ${sweep} ${pt2(farJamb)}" fill="none" stroke="${theme.doorLeaf}" stroke-width="${fmt2(sizes.thin)}" stroke-dasharray="${fmt2(sizes.thin * 4)} ${fmt2(sizes.thin * 3)}"/>`
|
|
443
|
+
});
|
|
444
|
+
return ops;
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// src/elements/window.ts
|
|
449
|
+
var windowEl = {
|
|
450
|
+
kind: "window",
|
|
451
|
+
keyword: "window",
|
|
452
|
+
parse(ctx) {
|
|
453
|
+
const kw = ctx.eatKeyword("window");
|
|
454
|
+
const id = ctx.parseIdOpt();
|
|
455
|
+
ctx.eatKeyword("at");
|
|
456
|
+
const at = ctx.parsePoint();
|
|
457
|
+
ctx.eatKeyword("width");
|
|
458
|
+
const width = ctx.eatNumber();
|
|
459
|
+
const node = { kind: "window", id, at, width, line: kw.line };
|
|
460
|
+
if (ctx.isKeyword("wall")) {
|
|
461
|
+
ctx.next();
|
|
462
|
+
node.wall = ctx.eatIdent().value;
|
|
463
|
+
}
|
|
464
|
+
return node;
|
|
465
|
+
},
|
|
466
|
+
idPrefix: () => "window",
|
|
467
|
+
resolve(node, ctx) {
|
|
468
|
+
const n = node;
|
|
469
|
+
const id = ctx.idOf(n);
|
|
470
|
+
const at = ctx.snapPt(n.at);
|
|
471
|
+
const width = ctx.snap(n.width) || n.width;
|
|
472
|
+
if (width <= 0) {
|
|
473
|
+
ctx.diag({ severity: "error", message: `Window "${id}" must have a positive width`, code: "E_WINDOW_WIDTH", span: n.span });
|
|
474
|
+
}
|
|
475
|
+
if (ctx.walls.length > 0 && !ctx.isOnWall(at, n.wall)) {
|
|
476
|
+
ctx.diag({ severity: "warning", message: `Window "${id}" does not lie on any wall`, code: "W_WINDOW_OFF_WALL", span: n.span });
|
|
477
|
+
}
|
|
478
|
+
return { kind: "window", id, at, width, host: ctx.hostSegment(at, n.wall), span: n.span };
|
|
479
|
+
},
|
|
480
|
+
bounds: () => [],
|
|
481
|
+
render(resolved, ctx) {
|
|
482
|
+
const wn = resolved;
|
|
483
|
+
const seg = wn.host;
|
|
484
|
+
if (!seg) return [];
|
|
485
|
+
const { fmt: fmt2, pt: pt2, theme, sizes } = ctx;
|
|
486
|
+
const d = unit(sub(seg.b, seg.a));
|
|
487
|
+
const n = normal(d);
|
|
488
|
+
const h = seg.thickness / 2;
|
|
489
|
+
const he = h + sizes.wallStroke;
|
|
490
|
+
const hw = wn.width / 2;
|
|
491
|
+
const cover = [
|
|
492
|
+
add(add(wn.at, mul(d, -hw)), mul(n, he)),
|
|
493
|
+
add(add(wn.at, mul(d, hw)), mul(n, he)),
|
|
494
|
+
add(add(wn.at, mul(d, hw)), mul(n, -he)),
|
|
495
|
+
add(add(wn.at, mul(d, -hw)), mul(n, -he))
|
|
496
|
+
];
|
|
497
|
+
const ops = [];
|
|
498
|
+
ops.push({ pass: "windows", svg: `<polygon points="${cover.map(pt2).join(" ")}" fill="${theme.opening}"/>` });
|
|
499
|
+
const jA = add(wn.at, mul(d, -hw));
|
|
500
|
+
const jB = add(wn.at, mul(d, hw));
|
|
501
|
+
for (const off of [h, -h]) {
|
|
502
|
+
const a = add(jA, mul(n, off));
|
|
503
|
+
const bb = add(jB, mul(n, off));
|
|
504
|
+
ops.push({
|
|
505
|
+
pass: "windows",
|
|
506
|
+
svg: `<line x1="${fmt2(a.x)}" y1="${fmt2(a.y)}" x2="${fmt2(bb.x)}" y2="${fmt2(bb.y)}" stroke="${theme.wallStroke}" stroke-width="${fmt2(sizes.thin)}"/>`
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
ops.push({
|
|
510
|
+
pass: "windows",
|
|
511
|
+
svg: `<line x1="${fmt2(jA.x)}" y1="${fmt2(jA.y)}" x2="${fmt2(jB.x)}" y2="${fmt2(jB.y)}" stroke="${theme.windowPane}" stroke-width="${fmt2(sizes.thin)}"/>`
|
|
512
|
+
});
|
|
513
|
+
return ops;
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// src/elements/furniture.ts
|
|
518
|
+
var furniture = {
|
|
519
|
+
kind: "furniture",
|
|
520
|
+
keyword: "furniture",
|
|
521
|
+
parse(ctx) {
|
|
522
|
+
const kw = ctx.eatKeyword("furniture");
|
|
523
|
+
const id = ctx.parseIdOpt();
|
|
524
|
+
const category = ctx.eatIdent().value;
|
|
525
|
+
ctx.eatKeyword("at");
|
|
526
|
+
const at = ctx.parsePoint();
|
|
527
|
+
ctx.eatKeyword("size");
|
|
528
|
+
const dim2 = ctx.eat("dimension");
|
|
529
|
+
const node = { kind: "furniture", id, category, at, size: { w: dim2.num, h: dim2.num2 }, line: kw.line };
|
|
530
|
+
if (ctx.isKeyword("label")) {
|
|
531
|
+
ctx.next();
|
|
532
|
+
node.label = ctx.eatString();
|
|
533
|
+
}
|
|
534
|
+
return node;
|
|
535
|
+
},
|
|
536
|
+
idPrefix: (node) => node.category || "furniture",
|
|
537
|
+
resolve(node, ctx) {
|
|
538
|
+
const n = node;
|
|
539
|
+
const id = ctx.idOf(n);
|
|
540
|
+
const at = ctx.snapPt(n.at);
|
|
541
|
+
const size = { w: ctx.snap(n.size.w), h: ctx.snap(n.size.h) };
|
|
542
|
+
if (size.w <= 0 || size.h <= 0) {
|
|
543
|
+
ctx.diag({ severity: "error", message: `Furniture "${id}" must have a positive size`, code: "E_FURN_SIZE", span: n.span });
|
|
544
|
+
}
|
|
545
|
+
return { kind: "furniture", id, category: n.category, at, size, label: n.label, span: n.span };
|
|
546
|
+
},
|
|
547
|
+
bounds(resolved) {
|
|
548
|
+
const f = resolved;
|
|
549
|
+
return rectCorners(f.at.x, f.at.y, f.size.w, f.size.h);
|
|
550
|
+
},
|
|
551
|
+
render(resolved, ctx) {
|
|
552
|
+
const f = resolved;
|
|
553
|
+
const { fmt: fmt2, pt: pt2, xml: xml2, theme, sizes } = ctx;
|
|
554
|
+
const ops = [];
|
|
555
|
+
const c = rectCorners(f.at.x, f.at.y, f.size.w, f.size.h);
|
|
556
|
+
ops.push({
|
|
557
|
+
pass: "furniture",
|
|
558
|
+
svg: `<polygon points="${c.map(pt2).join(" ")}" fill="${theme.furnitureFill}" stroke="${theme.furnitureStroke}" stroke-width="${fmt2(sizes.thin)}"/>`
|
|
559
|
+
});
|
|
560
|
+
if (f.label) {
|
|
561
|
+
const cx = f.at.x + f.size.w / 2;
|
|
562
|
+
const cy = f.at.y + f.size.h / 2;
|
|
563
|
+
ops.push({
|
|
564
|
+
pass: "furniture",
|
|
565
|
+
svg: `<text x="${fmt2(cx)}" y="${fmt2(cy)}" font-size="${fmt2(sizes.furnFont)}" fill="${theme.furnitureLabel}" text-anchor="middle" dominant-baseline="central">${xml2(f.label)}</text>`
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
return ops;
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
// src/elements/dim.ts
|
|
573
|
+
var dim = {
|
|
574
|
+
kind: "dim",
|
|
575
|
+
keyword: "dim",
|
|
576
|
+
parse(ctx) {
|
|
577
|
+
const kw = ctx.eatKeyword("dim");
|
|
578
|
+
const from = ctx.parsePoint();
|
|
579
|
+
ctx.eat("arrow");
|
|
580
|
+
const to = ctx.parsePoint();
|
|
581
|
+
const node = { kind: "dim", id: "", from, to, offset: 300, line: kw.line };
|
|
582
|
+
if (ctx.isKeyword("offset")) {
|
|
583
|
+
ctx.next();
|
|
584
|
+
node.offset = ctx.eatNumber();
|
|
585
|
+
}
|
|
586
|
+
if (ctx.isKeyword("text")) {
|
|
587
|
+
ctx.next();
|
|
588
|
+
node.text = ctx.eatString();
|
|
589
|
+
}
|
|
590
|
+
return node;
|
|
591
|
+
},
|
|
592
|
+
idPrefix: () => "dim",
|
|
593
|
+
resolve(node, ctx) {
|
|
594
|
+
const n = node;
|
|
595
|
+
return {
|
|
596
|
+
kind: "dim",
|
|
597
|
+
id: ctx.idOf(n),
|
|
598
|
+
from: ctx.snapPt(n.from),
|
|
599
|
+
to: ctx.snapPt(n.to),
|
|
600
|
+
offset: n.offset,
|
|
601
|
+
text: n.text,
|
|
602
|
+
span: n.span
|
|
603
|
+
};
|
|
604
|
+
},
|
|
605
|
+
bounds(resolved) {
|
|
606
|
+
const dm = resolved;
|
|
607
|
+
return [dm.from, dm.to];
|
|
608
|
+
},
|
|
609
|
+
render(resolved, ctx) {
|
|
610
|
+
const dm = resolved;
|
|
611
|
+
const { fmt: fmt2, xml: xml2, theme, sizes } = ctx;
|
|
612
|
+
const dir = unit(sub(dm.to, dm.from));
|
|
613
|
+
const n = normal(dir);
|
|
614
|
+
const off = mul(n, dm.offset);
|
|
615
|
+
const p1 = add(dm.from, off);
|
|
616
|
+
const p2 = add(dm.to, off);
|
|
617
|
+
const tick = sizes.refDim * 0.012;
|
|
618
|
+
const ops = [];
|
|
619
|
+
ops.push({
|
|
620
|
+
pass: "dims",
|
|
621
|
+
svg: `<line x1="${fmt2(dm.from.x)}" y1="${fmt2(dm.from.y)}" x2="${fmt2(p1.x)}" y2="${fmt2(p1.y)}" stroke="${theme.dim}" stroke-width="${fmt2(sizes.thin * 0.7)}"/>`
|
|
622
|
+
});
|
|
623
|
+
ops.push({
|
|
624
|
+
pass: "dims",
|
|
625
|
+
svg: `<line x1="${fmt2(dm.to.x)}" y1="${fmt2(dm.to.y)}" x2="${fmt2(p2.x)}" y2="${fmt2(p2.y)}" stroke="${theme.dim}" stroke-width="${fmt2(sizes.thin * 0.7)}"/>`
|
|
626
|
+
});
|
|
627
|
+
ops.push({
|
|
628
|
+
pass: "dims",
|
|
629
|
+
svg: `<line x1="${fmt2(p1.x)}" y1="${fmt2(p1.y)}" x2="${fmt2(p2.x)}" y2="${fmt2(p2.y)}" stroke="${theme.dim}" stroke-width="${fmt2(sizes.thin)}"/>`
|
|
630
|
+
});
|
|
631
|
+
for (const p of [p1, p2]) {
|
|
632
|
+
const t1 = add(p, mul(unit({ x: dir.x + n.x, y: dir.y + n.y }), tick));
|
|
633
|
+
const t2 = add(p, mul(unit({ x: dir.x + n.x, y: dir.y + n.y }), -tick));
|
|
634
|
+
ops.push({
|
|
635
|
+
pass: "dims",
|
|
636
|
+
svg: `<line x1="${fmt2(t1.x)}" y1="${fmt2(t1.y)}" x2="${fmt2(t2.x)}" y2="${fmt2(t2.y)}" stroke="${theme.dim}" stroke-width="${fmt2(sizes.thin)}"/>`
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
const mid = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };
|
|
640
|
+
const tp = add(mid, mul(n, sizes.dimFont * 0.7));
|
|
641
|
+
let angle = Math.atan2(dir.y, dir.x) * 180 / Math.PI;
|
|
642
|
+
if (angle > 90) angle -= 180;
|
|
643
|
+
if (angle < -90) angle += 180;
|
|
644
|
+
const label = dm.text ?? String(Math.round(length(sub(dm.to, dm.from))));
|
|
645
|
+
ops.push({
|
|
646
|
+
pass: "dims",
|
|
647
|
+
svg: `<text x="${fmt2(tp.x)}" y="${fmt2(tp.y)}" font-size="${fmt2(sizes.dimFont)}" fill="${theme.dim}" text-anchor="middle" dominant-baseline="central" transform="rotate(${fmt2(angle)} ${fmt2(tp.x)} ${fmt2(tp.y)})">${xml2(label)}</text>`
|
|
648
|
+
});
|
|
649
|
+
return ops;
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
// src/elements/column.ts
|
|
654
|
+
var column = {
|
|
655
|
+
kind: "column",
|
|
656
|
+
keyword: "column",
|
|
657
|
+
parse(ctx) {
|
|
658
|
+
const kw = ctx.eatKeyword("column");
|
|
659
|
+
const id = ctx.parseIdOpt();
|
|
660
|
+
ctx.eatKeyword("at");
|
|
661
|
+
const at = ctx.parsePoint();
|
|
662
|
+
ctx.eatKeyword("size");
|
|
663
|
+
const dim2 = ctx.eat("dimension");
|
|
664
|
+
return { kind: "column", id, at, size: { w: dim2.num, h: dim2.num2 }, line: kw.line };
|
|
665
|
+
},
|
|
666
|
+
idPrefix: () => "column",
|
|
667
|
+
resolve(node, ctx) {
|
|
668
|
+
const n = node;
|
|
669
|
+
const id = ctx.idOf(n);
|
|
670
|
+
const at = ctx.snapPt(n.at);
|
|
671
|
+
const size = { w: ctx.snap(n.size.w), h: ctx.snap(n.size.h) };
|
|
672
|
+
if (size.w <= 0 || size.h <= 0) {
|
|
673
|
+
ctx.diag({ severity: "error", message: `Column "${id}" must have a positive size`, code: "E_COLUMN_SIZE", span: n.span });
|
|
674
|
+
}
|
|
675
|
+
return { kind: "column", id, at, size, span: n.span };
|
|
676
|
+
},
|
|
677
|
+
bounds(resolved) {
|
|
678
|
+
const c = resolved;
|
|
679
|
+
return rectCorners(c.at.x, c.at.y, c.size.w, c.size.h);
|
|
680
|
+
},
|
|
681
|
+
render(resolved, ctx) {
|
|
682
|
+
const c = resolved;
|
|
683
|
+
const { fmt: fmt2, pt: pt2, theme, sizes } = ctx;
|
|
684
|
+
const pts = rectCorners(c.at.x, c.at.y, c.size.w, c.size.h);
|
|
685
|
+
return [
|
|
686
|
+
{
|
|
687
|
+
pass: "furniture",
|
|
688
|
+
svg: `<polygon points="${pts.map(pt2).join(" ")}" fill="${theme.column}" stroke="${theme.wallStroke}" stroke-width="${fmt2(sizes.thin)}"/>`
|
|
689
|
+
}
|
|
690
|
+
];
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
// src/elements/index.ts
|
|
695
|
+
var registryOrder = [];
|
|
696
|
+
var registry = /* @__PURE__ */ new Map();
|
|
697
|
+
function register(def) {
|
|
698
|
+
registry.set(def.keyword, def);
|
|
699
|
+
registryOrder.push(def);
|
|
700
|
+
}
|
|
701
|
+
register(wall);
|
|
702
|
+
register(room);
|
|
703
|
+
register(door);
|
|
704
|
+
register(windowEl);
|
|
705
|
+
register(furniture);
|
|
706
|
+
register(dim);
|
|
707
|
+
register(column);
|
|
708
|
+
|
|
141
709
|
// src/parser.ts
|
|
142
|
-
var
|
|
143
|
-
|
|
144
|
-
"grid",
|
|
145
|
-
"scale",
|
|
146
|
-
"north",
|
|
147
|
-
"wall",
|
|
148
|
-
"room",
|
|
149
|
-
"door",
|
|
150
|
-
"window",
|
|
151
|
-
"furniture",
|
|
152
|
-
"dim",
|
|
153
|
-
"title"
|
|
154
|
-
]);
|
|
710
|
+
var SETTINGS = ["units", "grid", "scale", "north", "title"];
|
|
711
|
+
var STATEMENT_STARTS = /* @__PURE__ */ new Set([...SETTINGS, ...registry.keys()]);
|
|
155
712
|
var ParseError = class extends Error {
|
|
156
713
|
constructor(message, span) {
|
|
157
714
|
super(message);
|
|
@@ -184,10 +741,26 @@ function parse(src) {
|
|
|
184
741
|
var Parser = class {
|
|
185
742
|
constructor(toks) {
|
|
186
743
|
this.toks = toks;
|
|
744
|
+
this.ctx = {
|
|
745
|
+
peek: (o) => this.peek(o),
|
|
746
|
+
next: () => this.next(),
|
|
747
|
+
eat: (type) => this.eat(type),
|
|
748
|
+
eatKeyword: (kw) => this.eatKeyword(kw),
|
|
749
|
+
eatIdent: () => this.eatIdent(),
|
|
750
|
+
eatNumber: () => this.eatNumber(),
|
|
751
|
+
eatString: () => this.eatString(),
|
|
752
|
+
isKeyword: (kw, o) => this.isKeyword(kw, o),
|
|
753
|
+
isType: (type) => this.isType(type),
|
|
754
|
+
parsePoint: () => this.parsePoint(),
|
|
755
|
+
parseIdOpt: () => this.parseIdOpt(),
|
|
756
|
+
fail: (msg, t) => this.fail(msg, t)
|
|
757
|
+
};
|
|
187
758
|
}
|
|
188
759
|
toks;
|
|
189
760
|
pos = 0;
|
|
190
761
|
diagnostics = [];
|
|
762
|
+
/** Facade passed to element parse functions (see registry.ts). */
|
|
763
|
+
ctx;
|
|
191
764
|
peek(o = 0) {
|
|
192
765
|
return this.toks[Math.min(this.pos + o, this.toks.length - 1)];
|
|
193
766
|
}
|
|
@@ -217,12 +790,12 @@ var Parser = class {
|
|
|
217
790
|
}
|
|
218
791
|
eatKeyword(kw) {
|
|
219
792
|
const t = this.peek();
|
|
220
|
-
if (t.type !== "ident" || t.value !== kw) this.fail(`Expected "${kw}" but found ${
|
|
793
|
+
if (t.type !== "ident" || t.value !== kw) this.fail(`Expected "${kw}" but found ${describe2(t)}`);
|
|
221
794
|
return this.next();
|
|
222
795
|
}
|
|
223
796
|
eat(type) {
|
|
224
797
|
const t = this.peek();
|
|
225
|
-
if (t.type !== type) this.fail(`Expected ${type} but found ${
|
|
798
|
+
if (t.type !== type) this.fail(`Expected ${type} but found ${describe2(t)}`);
|
|
226
799
|
return this.next();
|
|
227
800
|
}
|
|
228
801
|
eatIdent() {
|
|
@@ -244,18 +817,20 @@ var Parser = class {
|
|
|
244
817
|
units: "mm",
|
|
245
818
|
grid: 0,
|
|
246
819
|
north: "up",
|
|
247
|
-
|
|
248
|
-
rooms: [],
|
|
249
|
-
doors: [],
|
|
250
|
-
windows: [],
|
|
251
|
-
furniture: [],
|
|
252
|
-
dims: []
|
|
820
|
+
elements: []
|
|
253
821
|
};
|
|
254
822
|
while (!this.isType("rcurly") && !this.isType("eof")) {
|
|
255
823
|
const t = this.peek();
|
|
256
824
|
const start = t.start;
|
|
257
825
|
try {
|
|
258
|
-
if (t.type !== "ident") this.fail(`Expected a statement but found ${
|
|
826
|
+
if (t.type !== "ident") this.fail(`Expected a statement but found ${describe2(t)}`);
|
|
827
|
+
const def = registry.get(t.value);
|
|
828
|
+
if (def) {
|
|
829
|
+
const node = def.parse(this.ctx);
|
|
830
|
+
node.span = this.spanFrom(start);
|
|
831
|
+
plan.elements.push(node);
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
259
834
|
switch (t.value) {
|
|
260
835
|
case "units": {
|
|
261
836
|
this.next();
|
|
@@ -280,42 +855,6 @@ var Parser = class {
|
|
|
280
855
|
this.next();
|
|
281
856
|
plan.north = this.parseNorth();
|
|
282
857
|
break;
|
|
283
|
-
case "wall": {
|
|
284
|
-
const n = this.parseWall();
|
|
285
|
-
n.span = this.spanFrom(start);
|
|
286
|
-
plan.walls.push(n);
|
|
287
|
-
break;
|
|
288
|
-
}
|
|
289
|
-
case "room": {
|
|
290
|
-
const n = this.parseRoom();
|
|
291
|
-
n.span = this.spanFrom(start);
|
|
292
|
-
plan.rooms.push(n);
|
|
293
|
-
break;
|
|
294
|
-
}
|
|
295
|
-
case "door": {
|
|
296
|
-
const n = this.parseDoor();
|
|
297
|
-
n.span = this.spanFrom(start);
|
|
298
|
-
plan.doors.push(n);
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
case "window": {
|
|
302
|
-
const n = this.parseWindow();
|
|
303
|
-
n.span = this.spanFrom(start);
|
|
304
|
-
plan.windows.push(n);
|
|
305
|
-
break;
|
|
306
|
-
}
|
|
307
|
-
case "furniture": {
|
|
308
|
-
const n = this.parseFurniture();
|
|
309
|
-
n.span = this.spanFrom(start);
|
|
310
|
-
plan.furniture.push(n);
|
|
311
|
-
break;
|
|
312
|
-
}
|
|
313
|
-
case "dim": {
|
|
314
|
-
const n = this.parseDim();
|
|
315
|
-
n.span = this.spanFrom(start);
|
|
316
|
-
plan.dims.push(n);
|
|
317
|
-
break;
|
|
318
|
-
}
|
|
319
858
|
case "title": {
|
|
320
859
|
const n = this.parseTitle();
|
|
321
860
|
n.span = this.spanFrom(start);
|
|
@@ -367,7 +906,7 @@ var Parser = class {
|
|
|
367
906
|
this.next();
|
|
368
907
|
return t.value;
|
|
369
908
|
}
|
|
370
|
-
this.fail(`Expected a north direction (up|down|left|right|<degrees>) but found ${
|
|
909
|
+
this.fail(`Expected a north direction (up|down|left|right|<degrees>) but found ${describe2(t)}`);
|
|
371
910
|
}
|
|
372
911
|
parsePoint() {
|
|
373
912
|
this.eat("lparen");
|
|
@@ -377,123 +916,13 @@ var Parser = class {
|
|
|
377
916
|
this.eat("rparen");
|
|
378
917
|
return { x, y };
|
|
379
918
|
}
|
|
380
|
-
parseWall() {
|
|
381
|
-
const kw = this.eatKeyword("wall");
|
|
382
|
-
const id = this.parseIdOpt();
|
|
383
|
-
const kind = this.eatIdent().value;
|
|
384
|
-
this.eatKeyword("thickness");
|
|
385
|
-
const thickness = this.eatNumber();
|
|
386
|
-
this.eat("lcurly");
|
|
387
|
-
const points = [];
|
|
388
|
-
let closed = false;
|
|
389
|
-
while (!this.isType("rcurly") && !this.isType("eof")) {
|
|
390
|
-
if (this.isKeyword("close")) {
|
|
391
|
-
this.next();
|
|
392
|
-
closed = true;
|
|
393
|
-
break;
|
|
394
|
-
}
|
|
395
|
-
if (this.isType("lparen")) {
|
|
396
|
-
points.push(this.parsePoint());
|
|
397
|
-
continue;
|
|
398
|
-
}
|
|
399
|
-
this.fail(`Expected a point "(x,y)" or "close" in wall body but found ${describe(this.peek())}`);
|
|
400
|
-
}
|
|
401
|
-
this.eat("rcurly");
|
|
402
|
-
if (points.length < 2) this.fail("A wall needs at least two points", kw);
|
|
403
|
-
return { id, kind, thickness, points, closed, line: kw.line };
|
|
404
|
-
}
|
|
405
|
-
parseRoom() {
|
|
406
|
-
const kw = this.eatKeyword("room");
|
|
407
|
-
const id = this.parseIdOpt();
|
|
408
|
-
this.eatKeyword("at");
|
|
409
|
-
const at = this.parsePoint();
|
|
410
|
-
this.eatKeyword("size");
|
|
411
|
-
const dim = this.eat("dimension");
|
|
412
|
-
const node = { id, at, size: { w: dim.num, h: dim.num2 }, line: kw.line };
|
|
413
|
-
if (this.isKeyword("label")) {
|
|
414
|
-
this.next();
|
|
415
|
-
node.label = this.eatString();
|
|
416
|
-
}
|
|
417
|
-
return node;
|
|
418
|
-
}
|
|
419
|
-
parseDoor() {
|
|
420
|
-
const kw = this.eatKeyword("door");
|
|
421
|
-
const id = this.parseIdOpt();
|
|
422
|
-
this.eatKeyword("at");
|
|
423
|
-
const at = this.parsePoint();
|
|
424
|
-
this.eatKeyword("width");
|
|
425
|
-
const width = this.eatNumber();
|
|
426
|
-
const node = { id, at, width, hinge: "left", swing: "in", line: kw.line };
|
|
427
|
-
if (this.isKeyword("wall")) {
|
|
428
|
-
this.next();
|
|
429
|
-
node.wall = this.eatIdent().value;
|
|
430
|
-
}
|
|
431
|
-
if (this.isKeyword("hinge")) {
|
|
432
|
-
this.next();
|
|
433
|
-
const h = this.eatIdent().value;
|
|
434
|
-
if (h !== "left" && h !== "right") this.fail(`Expected hinge "left" or "right" but found "${h}"`);
|
|
435
|
-
node.hinge = h;
|
|
436
|
-
}
|
|
437
|
-
if (this.isKeyword("swing")) {
|
|
438
|
-
this.next();
|
|
439
|
-
const s = this.eatIdent().value;
|
|
440
|
-
if (s !== "in" && s !== "out") this.fail(`Expected swing "in" or "out" but found "${s}"`);
|
|
441
|
-
node.swing = s;
|
|
442
|
-
}
|
|
443
|
-
return node;
|
|
444
|
-
}
|
|
445
|
-
parseWindow() {
|
|
446
|
-
const kw = this.eatKeyword("window");
|
|
447
|
-
const id = this.parseIdOpt();
|
|
448
|
-
this.eatKeyword("at");
|
|
449
|
-
const at = this.parsePoint();
|
|
450
|
-
this.eatKeyword("width");
|
|
451
|
-
const width = this.eatNumber();
|
|
452
|
-
const node = { id, at, width, line: kw.line };
|
|
453
|
-
if (this.isKeyword("wall")) {
|
|
454
|
-
this.next();
|
|
455
|
-
node.wall = this.eatIdent().value;
|
|
456
|
-
}
|
|
457
|
-
return node;
|
|
458
|
-
}
|
|
459
|
-
parseFurniture() {
|
|
460
|
-
const kw = this.eatKeyword("furniture");
|
|
461
|
-
const id = this.parseIdOpt();
|
|
462
|
-
const kind = this.eatIdent().value;
|
|
463
|
-
this.eatKeyword("at");
|
|
464
|
-
const at = this.parsePoint();
|
|
465
|
-
this.eatKeyword("size");
|
|
466
|
-
const dim = this.eat("dimension");
|
|
467
|
-
const node = { id, kind, at, size: { w: dim.num, h: dim.num2 }, line: kw.line };
|
|
468
|
-
if (this.isKeyword("label")) {
|
|
469
|
-
this.next();
|
|
470
|
-
node.label = this.eatString();
|
|
471
|
-
}
|
|
472
|
-
return node;
|
|
473
|
-
}
|
|
474
|
-
parseDim() {
|
|
475
|
-
const kw = this.eatKeyword("dim");
|
|
476
|
-
const from = this.parsePoint();
|
|
477
|
-
this.eat("arrow");
|
|
478
|
-
const to = this.parsePoint();
|
|
479
|
-
const node = { id: "", from, to, offset: 300, line: kw.line };
|
|
480
|
-
if (this.isKeyword("offset")) {
|
|
481
|
-
this.next();
|
|
482
|
-
node.offset = this.eatNumber();
|
|
483
|
-
}
|
|
484
|
-
if (this.isKeyword("text")) {
|
|
485
|
-
this.next();
|
|
486
|
-
node.text = this.eatString();
|
|
487
|
-
}
|
|
488
|
-
return node;
|
|
489
|
-
}
|
|
490
919
|
parseTitle() {
|
|
491
920
|
const kw = this.eatKeyword("title");
|
|
492
921
|
this.eat("lcurly");
|
|
493
922
|
const node = { line: kw.line };
|
|
494
923
|
while (!this.isType("rcurly") && !this.isType("eof")) {
|
|
495
924
|
const t = this.peek();
|
|
496
|
-
if (t.type !== "ident") this.fail(`Expected a title field but found ${
|
|
925
|
+
if (t.type !== "ident") this.fail(`Expected a title field but found ${describe2(t)}`);
|
|
497
926
|
switch (t.value) {
|
|
498
927
|
case "project":
|
|
499
928
|
this.next();
|
|
@@ -515,157 +944,24 @@ var Parser = class {
|
|
|
515
944
|
return node;
|
|
516
945
|
}
|
|
517
946
|
};
|
|
518
|
-
function
|
|
947
|
+
function describe2(t) {
|
|
519
948
|
if (t.type === "eof") return "end of input";
|
|
520
949
|
if (t.type === "string") return `string ${JSON.stringify(t.value)}`;
|
|
521
950
|
return `"${t.value}"`;
|
|
522
951
|
}
|
|
523
952
|
|
|
524
|
-
// src/
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
var length = (v) => Math.hypot(v.x, v.y);
|
|
529
|
-
function unit(v) {
|
|
530
|
-
const l = length(v);
|
|
531
|
-
return l === 0 ? { x: 0, y: 0 } : { x: v.x / l, y: v.y / l };
|
|
532
|
-
}
|
|
533
|
-
var normal = (v) => ({ x: -v.y, y: v.x });
|
|
534
|
-
var emptyBounds = () => ({
|
|
535
|
-
minX: Infinity,
|
|
536
|
-
minY: Infinity,
|
|
537
|
-
maxX: -Infinity,
|
|
538
|
-
maxY: -Infinity
|
|
539
|
-
});
|
|
540
|
-
function extendBounds(b, x, y) {
|
|
541
|
-
if (x < b.minX) b.minX = x;
|
|
542
|
-
if (y < b.minY) b.minY = y;
|
|
543
|
-
if (x > b.maxX) b.maxX = x;
|
|
544
|
-
if (y > b.maxY) b.maxY = y;
|
|
545
|
-
}
|
|
546
|
-
function distPointToSegment(p, a, b) {
|
|
547
|
-
const abx = b.x - a.x;
|
|
548
|
-
const aby = b.y - a.y;
|
|
549
|
-
const apx = p.x - a.x;
|
|
550
|
-
const apy = p.y - a.y;
|
|
551
|
-
const len2 = abx * abx + aby * aby;
|
|
552
|
-
let t = len2 === 0 ? 0 : (apx * abx + apy * aby) / len2;
|
|
553
|
-
t = Math.max(0, Math.min(1, t));
|
|
554
|
-
const cx = a.x + t * abx;
|
|
555
|
-
const cy = a.y + t * aby;
|
|
556
|
-
return Math.hypot(p.x - cx, p.y - cy);
|
|
557
|
-
}
|
|
558
|
-
function rectCorners(x, y, w, h) {
|
|
559
|
-
return [
|
|
560
|
-
{ x, y },
|
|
561
|
-
{ x: x + w, y },
|
|
562
|
-
{ x: x + w, y: y + h },
|
|
563
|
-
{ x, y: y + h }
|
|
564
|
-
];
|
|
565
|
-
}
|
|
566
|
-
function segmentRectangle(a, b, thickness) {
|
|
567
|
-
const d = unit(sub(b, a));
|
|
568
|
-
const n = normal(d);
|
|
569
|
-
const half = thickness / 2;
|
|
570
|
-
const a2 = add(a, mul(d, -half));
|
|
571
|
-
const b2 = add(b, mul(d, half));
|
|
572
|
-
return [
|
|
573
|
-
add(a2, mul(n, half)),
|
|
574
|
-
add(b2, mul(n, half)),
|
|
575
|
-
add(b2, mul(n, -half)),
|
|
576
|
-
add(a2, mul(n, -half))
|
|
577
|
-
];
|
|
578
|
-
}
|
|
579
|
-
function wallSegments(plan) {
|
|
580
|
-
const segs = [];
|
|
581
|
-
for (const w of plan.walls) {
|
|
582
|
-
for (let k = 0; k < w.points.length - 1; k++) {
|
|
583
|
-
segs.push({ a: w.points[k], b: w.points[k + 1], thickness: w.thickness, kind: w.kind });
|
|
584
|
-
}
|
|
585
|
-
if (w.closed && w.points.length > 2) {
|
|
586
|
-
segs.push({ a: w.points[w.points.length - 1], b: w.points[0], thickness: w.thickness, kind: w.kind });
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
return segs;
|
|
590
|
-
}
|
|
591
|
-
function hostSegment(plan, at, wallRef) {
|
|
592
|
-
const walls = wallRef ? plan.walls.filter((w) => w.id === wallRef || w.kind === wallRef) : plan.walls;
|
|
593
|
-
let best = null;
|
|
594
|
-
let bestDist = Infinity;
|
|
595
|
-
for (const w of walls) {
|
|
596
|
-
const segs = [];
|
|
597
|
-
for (let k = 0; k < w.points.length - 1; k++) segs.push([w.points[k], w.points[k + 1]]);
|
|
598
|
-
if (w.closed && w.points.length > 2) segs.push([w.points[w.points.length - 1], w.points[0]]);
|
|
599
|
-
for (const [a, b] of segs) {
|
|
600
|
-
const dist = distPointToSegment(at, a, b);
|
|
601
|
-
if (dist < bestDist) {
|
|
602
|
-
bestDist = dist;
|
|
603
|
-
best = { a, b, thickness: w.thickness, kind: w.kind };
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
return best;
|
|
608
|
-
}
|
|
609
|
-
function planBounds(plan) {
|
|
610
|
-
const b = emptyBounds();
|
|
611
|
-
for (const seg of wallSegments(plan)) {
|
|
612
|
-
for (const c of segmentRectangle(seg.a, seg.b, seg.thickness)) extendBounds(b, c.x, c.y);
|
|
613
|
-
}
|
|
614
|
-
for (const r of plan.rooms) {
|
|
615
|
-
extendBounds(b, r.at.x, r.at.y);
|
|
616
|
-
extendBounds(b, r.at.x + r.size.w, r.at.y + r.size.h);
|
|
617
|
-
}
|
|
618
|
-
for (const f of plan.furniture) {
|
|
619
|
-
extendBounds(b, f.at.x, f.at.y);
|
|
620
|
-
extendBounds(b, f.at.x + f.size.w, f.at.y + f.size.h);
|
|
621
|
-
}
|
|
622
|
-
for (const d of plan.dims) {
|
|
623
|
-
extendBounds(b, d.from.x, d.from.y);
|
|
624
|
-
extendBounds(b, d.to.x, d.to.y);
|
|
625
|
-
}
|
|
626
|
-
if (!isFinite(b.minX)) {
|
|
627
|
-
return { minX: 0, minY: 0, maxX: 1e3, maxY: 1e3 };
|
|
628
|
-
}
|
|
629
|
-
return b;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// src/validate.ts
|
|
633
|
-
function validate(plan) {
|
|
634
|
-
const diags = [];
|
|
635
|
-
const error = (message, code, span) => diags.push({ severity: "error", message, code, span });
|
|
636
|
-
const warn = (message, code, span) => diags.push({ severity: "warning", message, code, span });
|
|
637
|
-
const g = plan.grid;
|
|
953
|
+
// src/ir.ts
|
|
954
|
+
function resolve(ast) {
|
|
955
|
+
const diagnostics = [];
|
|
956
|
+
const g = ast.grid;
|
|
638
957
|
const snap = (v) => g > 0 ? Math.round(v / g) * g : v;
|
|
639
958
|
const snapPt = (p) => ({ x: snap(p.x), y: snap(p.y) });
|
|
640
|
-
|
|
641
|
-
w.points = w.points.map(snapPt);
|
|
642
|
-
w.thickness = snap(w.thickness) || w.thickness;
|
|
643
|
-
}
|
|
644
|
-
for (const r of plan.rooms) {
|
|
645
|
-
r.at = snapPt(r.at);
|
|
646
|
-
r.size = { w: snap(r.size.w), h: snap(r.size.h) };
|
|
647
|
-
}
|
|
648
|
-
for (const f of plan.furniture) {
|
|
649
|
-
f.at = snapPt(f.at);
|
|
650
|
-
f.size = { w: snap(f.size.w), h: snap(f.size.h) };
|
|
651
|
-
}
|
|
652
|
-
for (const d of plan.doors) {
|
|
653
|
-
d.at = snapPt(d.at);
|
|
654
|
-
d.width = snap(d.width) || d.width;
|
|
655
|
-
}
|
|
656
|
-
for (const win of plan.windows) {
|
|
657
|
-
win.at = snapPt(win.at);
|
|
658
|
-
win.width = snap(win.width) || win.width;
|
|
659
|
-
}
|
|
660
|
-
for (const dm of plan.dims) {
|
|
661
|
-
dm.from = snapPt(dm.from);
|
|
662
|
-
dm.to = snapPt(dm.to);
|
|
663
|
-
}
|
|
959
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
664
960
|
const seen = /* @__PURE__ */ new Set();
|
|
665
|
-
const
|
|
961
|
+
const assignId = (provided, prefix, idx, span) => {
|
|
666
962
|
if (provided) {
|
|
667
963
|
if (seen.has(provided)) {
|
|
668
|
-
error
|
|
964
|
+
diagnostics.push({ severity: "error", message: `Duplicate id "${provided}"`, code: "E_DUP_ID", span });
|
|
669
965
|
}
|
|
670
966
|
seen.add(provided);
|
|
671
967
|
return provided;
|
|
@@ -675,68 +971,88 @@ function validate(plan) {
|
|
|
675
971
|
seen.add(auto);
|
|
676
972
|
return auto;
|
|
677
973
|
};
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
for (const r of plan.rooms) {
|
|
685
|
-
if (r.size.w <= 0 || r.size.h <= 0)
|
|
686
|
-
error(`Room "${r.id}" must have a positive size`, "E_ROOM_SIZE", r.span);
|
|
687
|
-
}
|
|
688
|
-
for (const f of plan.furniture) {
|
|
689
|
-
if (f.size.w <= 0 || f.size.h <= 0)
|
|
690
|
-
error(`Furniture "${f.id}" must have a positive size`, "E_FURN_SIZE", f.span);
|
|
691
|
-
}
|
|
692
|
-
for (const d of plan.doors) {
|
|
693
|
-
if (d.width <= 0) error(`Door "${d.id}" must have a positive width`, "E_DOOR_WIDTH", d.span);
|
|
694
|
-
}
|
|
695
|
-
for (const w of plan.windows) {
|
|
696
|
-
if (w.width <= 0) error(`Window "${w.id}" must have a positive width`, "E_WINDOW_WIDTH", w.span);
|
|
697
|
-
}
|
|
698
|
-
for (const w of plan.walls) {
|
|
699
|
-
if (w.thickness <= 0)
|
|
700
|
-
error(`Wall "${w.id}" must have a positive thickness`, "E_WALL_THICKNESS", w.span);
|
|
701
|
-
}
|
|
702
|
-
if (plan.walls.length === 0 && plan.rooms.length === 0 && plan.furniture.length === 0) {
|
|
703
|
-
warn("Plan has no walls, rooms, or furniture \u2014 nothing to draw", "W_EMPTY_PLAN");
|
|
704
|
-
}
|
|
705
|
-
const onSomeWall = (at, wallRef) => {
|
|
706
|
-
const candidates = wallRef ? plan.walls.filter((w) => w.id === wallRef || w.kind === wallRef) : plan.walls;
|
|
707
|
-
for (const w of candidates) {
|
|
708
|
-
const tol = w.thickness / 2 + Math.max(w.thickness, 1);
|
|
709
|
-
for (let k = 0; k < w.points.length - 1; k++) {
|
|
710
|
-
if (distPointToSegment(at, w.points[k], w.points[k + 1]) <= tol) return true;
|
|
711
|
-
}
|
|
712
|
-
if (w.closed && w.points.length > 2) {
|
|
713
|
-
if (distPointToSegment(at, w.points[w.points.length - 1], w.points[0]) <= tol) return true;
|
|
714
|
-
}
|
|
974
|
+
for (const def of registryOrder) {
|
|
975
|
+
let idx = 0;
|
|
976
|
+
for (const node of ast.elements) {
|
|
977
|
+
if (node.kind !== def.kind) continue;
|
|
978
|
+
idx++;
|
|
979
|
+
idMap.set(node, assignId(node.id, def.idPrefix(node), idx, node.span));
|
|
715
980
|
}
|
|
716
|
-
|
|
981
|
+
}
|
|
982
|
+
const walls = [];
|
|
983
|
+
const ctx = {
|
|
984
|
+
grid: g,
|
|
985
|
+
snap,
|
|
986
|
+
snapPt,
|
|
987
|
+
idOf: (node) => idMap.get(node) ?? node.id,
|
|
988
|
+
walls,
|
|
989
|
+
hostSegment: (at, ref) => hostSegmentForWalls(walls, at, ref),
|
|
990
|
+
isOnWall: (at, ref) => isOnSomeWall(walls, at, ref),
|
|
991
|
+
diag: (d) => diagnostics.push(d)
|
|
717
992
|
};
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
993
|
+
const rmap = /* @__PURE__ */ new Map();
|
|
994
|
+
for (const def of registryOrder) {
|
|
995
|
+
for (const node of ast.elements) {
|
|
996
|
+
if (node.kind !== def.kind) continue;
|
|
997
|
+
const r = def.resolve(node, ctx);
|
|
998
|
+
rmap.set(node, r);
|
|
999
|
+
if (r.kind === "wall") walls.push(r);
|
|
1000
|
+
}
|
|
721
1001
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1002
|
+
const elements = ast.elements.map((n) => rmap.get(n));
|
|
1003
|
+
const drawable = elements.some(
|
|
1004
|
+
(e) => e.kind === "wall" || e.kind === "room" || e.kind === "furniture" || e.kind === "column"
|
|
1005
|
+
);
|
|
1006
|
+
if (!drawable) {
|
|
1007
|
+
diagnostics.push({
|
|
1008
|
+
severity: "warning",
|
|
1009
|
+
message: "Plan has no walls, rooms, or furniture \u2014 nothing to draw",
|
|
1010
|
+
code: "W_EMPTY_PLAN"
|
|
1011
|
+
});
|
|
725
1012
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
const
|
|
1013
|
+
const rooms = elements.filter((e) => e.kind === "room");
|
|
1014
|
+
for (let a = 0; a < rooms.length; a++) {
|
|
1015
|
+
for (let b = a + 1; b < rooms.length; b++) {
|
|
1016
|
+
const r1 = rooms[a];
|
|
1017
|
+
const r2 = rooms[b];
|
|
730
1018
|
const ox = Math.max(0, Math.min(r1.at.x + r1.size.w, r2.at.x + r2.size.w) - Math.max(r1.at.x, r2.at.x));
|
|
731
1019
|
const oy = Math.max(0, Math.min(r1.at.y + r1.size.h, r2.at.y + r2.size.h) - Math.max(r1.at.y, r2.at.y));
|
|
732
1020
|
if (ox > 1 && oy > 1) {
|
|
733
|
-
|
|
1021
|
+
diagnostics.push({
|
|
1022
|
+
severity: "warning",
|
|
1023
|
+
message: `Rooms "${r1.id}" and "${r2.id}" overlap`,
|
|
1024
|
+
code: "W_ROOM_OVERLAP",
|
|
1025
|
+
span: r2.span
|
|
1026
|
+
});
|
|
734
1027
|
}
|
|
735
1028
|
}
|
|
736
1029
|
}
|
|
737
|
-
|
|
1030
|
+
const ir = {
|
|
1031
|
+
name: ast.name,
|
|
1032
|
+
units: ast.units,
|
|
1033
|
+
grid: ast.grid,
|
|
1034
|
+
scale: ast.scale,
|
|
1035
|
+
north: ast.north,
|
|
1036
|
+
title: ast.title,
|
|
1037
|
+
elements,
|
|
1038
|
+
walls
|
|
1039
|
+
};
|
|
1040
|
+
return { ir, diagnostics };
|
|
738
1041
|
}
|
|
739
1042
|
|
|
1043
|
+
// src/registry.ts
|
|
1044
|
+
var RENDER_PASSES = [
|
|
1045
|
+
"floor",
|
|
1046
|
+
"furniture",
|
|
1047
|
+
"wallFill",
|
|
1048
|
+
"wallFace",
|
|
1049
|
+
"doors",
|
|
1050
|
+
"windows",
|
|
1051
|
+
"labels",
|
|
1052
|
+
"dims",
|
|
1053
|
+
"annotations"
|
|
1054
|
+
];
|
|
1055
|
+
|
|
740
1056
|
// src/render.ts
|
|
741
1057
|
var THEME = {
|
|
742
1058
|
bg: "#ffffff",
|
|
@@ -754,7 +1070,8 @@ var THEME = {
|
|
|
754
1070
|
windowPane: "#3a6ea5",
|
|
755
1071
|
dim: "#0E5484",
|
|
756
1072
|
annotation: "#333333",
|
|
757
|
-
annotationMuted: "#888888"
|
|
1073
|
+
annotationMuted: "#888888",
|
|
1074
|
+
column: "#4a4a4a"
|
|
758
1075
|
};
|
|
759
1076
|
function fmt(v) {
|
|
760
1077
|
const r = Math.round(v * 100) / 100;
|
|
@@ -770,19 +1087,35 @@ function niceBarLength(target) {
|
|
|
770
1087
|
for (const v of NICE_LENGTHS) if (v <= target) best = v;
|
|
771
1088
|
return best;
|
|
772
1089
|
}
|
|
773
|
-
function
|
|
774
|
-
const b =
|
|
1090
|
+
function planBounds(ir) {
|
|
1091
|
+
const b = emptyBounds();
|
|
1092
|
+
for (const el of ir.elements) {
|
|
1093
|
+
const def = registry.get(el.kind);
|
|
1094
|
+
if (!def) continue;
|
|
1095
|
+
for (const p of def.bounds(el)) extendBounds(b, p.x, p.y);
|
|
1096
|
+
}
|
|
1097
|
+
if (!isFinite(b.minX)) {
|
|
1098
|
+
return { minX: 0, minY: 0, maxX: 1e3, maxY: 1e3 };
|
|
1099
|
+
}
|
|
1100
|
+
return b;
|
|
1101
|
+
}
|
|
1102
|
+
function render(ir, opts = {}) {
|
|
1103
|
+
const b = planBounds(ir);
|
|
775
1104
|
const drawW = b.maxX - b.minX;
|
|
776
1105
|
const drawH = b.maxY - b.minY;
|
|
777
1106
|
const refDim = Math.max(drawW, drawH, 1);
|
|
778
|
-
const
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
1107
|
+
const sizes = {
|
|
1108
|
+
refDim,
|
|
1109
|
+
wallStroke: refDim * 28e-4,
|
|
1110
|
+
thin: refDim * 16e-4,
|
|
1111
|
+
roomFont: refDim * 0.03,
|
|
1112
|
+
areaFont: refDim * 0.022,
|
|
1113
|
+
dimFont: refDim * 0.02,
|
|
1114
|
+
furnFont: refDim * 0.017,
|
|
1115
|
+
margin: refDim * 0.17,
|
|
1116
|
+
hatchGap: refDim * 0.013
|
|
1117
|
+
};
|
|
1118
|
+
const { thin, margin, hatchGap } = sizes;
|
|
786
1119
|
const vbX = b.minX - margin;
|
|
787
1120
|
const vbY = b.minY - margin;
|
|
788
1121
|
const vbW = drawW + margin * 2;
|
|
@@ -796,157 +1129,27 @@ function render(plan, opts = {}) {
|
|
|
796
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>`
|
|
797
1130
|
);
|
|
798
1131
|
out.push(`<rect x="${fmt(vbX)}" y="${fmt(vbY)}" width="${fmt(vbW)}" height="${fmt(vbH)}" fill="${THEME.bg}"/>`);
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
out.push(
|
|
806
|
-
`<polygon points="${c.map(pt).join(" ")}" fill="${THEME.furnitureFill}" stroke="${THEME.furnitureStroke}" stroke-width="${fmt(thin)}"/>`
|
|
807
|
-
);
|
|
808
|
-
if (f.label) {
|
|
809
|
-
const cx = f.at.x + f.size.w / 2;
|
|
810
|
-
const cy = f.at.y + f.size.h / 2;
|
|
811
|
-
out.push(
|
|
812
|
-
`<text x="${fmt(cx)}" y="${fmt(cy)}" font-size="${fmt(furnFont)}" fill="${THEME.furnitureLabel}" text-anchor="middle" dominant-baseline="central">${xml(f.label)}</text>`
|
|
813
|
-
);
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
const segs = wallSegments(plan);
|
|
817
|
-
for (const s of segs) {
|
|
818
|
-
const poly = segmentRectangle(s.a, s.b, s.thickness);
|
|
819
|
-
out.push(`<polygon points="${poly.map(pt).join(" ")}" fill="url(#poche)"/>`);
|
|
820
|
-
}
|
|
821
|
-
for (const s of segs) {
|
|
822
|
-
const d = unit(sub(s.b, s.a));
|
|
823
|
-
const n = normal(d);
|
|
824
|
-
const h = s.thickness / 2;
|
|
825
|
-
const fa1 = add(s.a, mul(n, h));
|
|
826
|
-
const fb1 = add(s.b, mul(n, h));
|
|
827
|
-
const fa2 = add(s.a, mul(n, -h));
|
|
828
|
-
const fb2 = add(s.b, mul(n, -h));
|
|
829
|
-
out.push(
|
|
830
|
-
`<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"/>`
|
|
831
|
-
);
|
|
832
|
-
out.push(
|
|
833
|
-
`<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"/>`
|
|
834
|
-
);
|
|
835
|
-
}
|
|
836
|
-
for (const dr of plan.doors) {
|
|
837
|
-
const seg = hostSegment(plan, dr.at, dr.wall);
|
|
838
|
-
if (!seg) continue;
|
|
839
|
-
const d = unit(sub(seg.b, seg.a));
|
|
840
|
-
const n = normal(d);
|
|
841
|
-
const h = seg.thickness / 2 + wallStroke;
|
|
842
|
-
const hw = dr.width / 2;
|
|
843
|
-
const cover = [
|
|
844
|
-
add(add(dr.at, mul(d, -hw)), mul(n, h)),
|
|
845
|
-
add(add(dr.at, mul(d, hw)), mul(n, h)),
|
|
846
|
-
add(add(dr.at, mul(d, hw)), mul(n, -h)),
|
|
847
|
-
add(add(dr.at, mul(d, -hw)), mul(n, -h))
|
|
848
|
-
];
|
|
849
|
-
out.push(`<polygon points="${cover.map(pt).join(" ")}" fill="${THEME.opening}"/>`);
|
|
850
|
-
const hinge = dr.hinge === "left" ? add(dr.at, mul(d, -hw)) : add(dr.at, mul(d, hw));
|
|
851
|
-
const farJamb = dr.hinge === "left" ? add(dr.at, mul(d, hw)) : add(dr.at, mul(d, -hw));
|
|
852
|
-
const leafDir = dr.swing === "in" ? n : mul(n, -1);
|
|
853
|
-
const leafEnd = add(hinge, mul(leafDir, dr.width));
|
|
854
|
-
const cross = (leafEnd.x - hinge.x) * (farJamb.y - hinge.y) - (leafEnd.y - hinge.y) * (farJamb.x - hinge.x);
|
|
855
|
-
const sweep = cross < 0 ? 1 : 0;
|
|
856
|
-
out.push(
|
|
857
|
-
`<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)}"/>`
|
|
858
|
-
);
|
|
859
|
-
out.push(
|
|
860
|
-
`<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)}"/>`
|
|
861
|
-
);
|
|
862
|
-
}
|
|
863
|
-
for (const wn of plan.windows) {
|
|
864
|
-
const seg = hostSegment(plan, wn.at, wn.wall);
|
|
865
|
-
if (!seg) continue;
|
|
866
|
-
const d = unit(sub(seg.b, seg.a));
|
|
867
|
-
const n = normal(d);
|
|
868
|
-
const h = seg.thickness / 2;
|
|
869
|
-
const he = h + wallStroke;
|
|
870
|
-
const hw = wn.width / 2;
|
|
871
|
-
const cover = [
|
|
872
|
-
add(add(wn.at, mul(d, -hw)), mul(n, he)),
|
|
873
|
-
add(add(wn.at, mul(d, hw)), mul(n, he)),
|
|
874
|
-
add(add(wn.at, mul(d, hw)), mul(n, -he)),
|
|
875
|
-
add(add(wn.at, mul(d, -hw)), mul(n, -he))
|
|
876
|
-
];
|
|
877
|
-
out.push(`<polygon points="${cover.map(pt).join(" ")}" fill="${THEME.opening}"/>`);
|
|
878
|
-
const jA = add(wn.at, mul(d, -hw));
|
|
879
|
-
const jB = add(wn.at, mul(d, hw));
|
|
880
|
-
for (const off of [h, -h]) {
|
|
881
|
-
const a = add(jA, mul(n, off));
|
|
882
|
-
const bb = add(jB, mul(n, off));
|
|
883
|
-
out.push(
|
|
884
|
-
`<line x1="${fmt(a.x)}" y1="${fmt(a.y)}" x2="${fmt(bb.x)}" y2="${fmt(bb.y)}" stroke="${THEME.wallStroke}" stroke-width="${fmt(thin)}"/>`
|
|
885
|
-
);
|
|
886
|
-
}
|
|
887
|
-
out.push(
|
|
888
|
-
`<line x1="${fmt(jA.x)}" y1="${fmt(jA.y)}" x2="${fmt(jB.x)}" y2="${fmt(jB.y)}" stroke="${THEME.windowPane}" stroke-width="${fmt(thin)}"/>`
|
|
889
|
-
);
|
|
890
|
-
}
|
|
891
|
-
for (const r of plan.rooms) {
|
|
892
|
-
const cx = r.at.x + r.size.w / 2;
|
|
893
|
-
const cy = r.at.y + r.size.h / 2;
|
|
894
|
-
const areaM2 = (r.size.w / 1e3 * (r.size.h / 1e3)).toFixed(1);
|
|
895
|
-
if (r.label) {
|
|
896
|
-
out.push(
|
|
897
|
-
`<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>`
|
|
898
|
-
);
|
|
899
|
-
}
|
|
900
|
-
out.push(
|
|
901
|
-
`<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>`
|
|
902
|
-
);
|
|
903
|
-
}
|
|
904
|
-
for (const dm of plan.dims) {
|
|
905
|
-
const dir = unit(sub(dm.to, dm.from));
|
|
906
|
-
const n = normal(dir);
|
|
907
|
-
const off = mul(n, dm.offset);
|
|
908
|
-
const p1 = add(dm.from, off);
|
|
909
|
-
const p2 = add(dm.to, off);
|
|
910
|
-
const tick = refDim * 0.012;
|
|
911
|
-
out.push(
|
|
912
|
-
`<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)}"/>`
|
|
913
|
-
);
|
|
914
|
-
out.push(
|
|
915
|
-
`<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)}"/>`
|
|
916
|
-
);
|
|
917
|
-
out.push(
|
|
918
|
-
`<line x1="${fmt(p1.x)}" y1="${fmt(p1.y)}" x2="${fmt(p2.x)}" y2="${fmt(p2.y)}" stroke="${THEME.dim}" stroke-width="${fmt(thin)}"/>`
|
|
919
|
-
);
|
|
920
|
-
for (const p of [p1, p2]) {
|
|
921
|
-
const t1 = add(p, mul(unit({ x: dir.x + n.x, y: dir.y + n.y }), tick));
|
|
922
|
-
const t2 = add(p, mul(unit({ x: dir.x + n.x, y: dir.y + n.y }), -tick));
|
|
923
|
-
out.push(
|
|
924
|
-
`<line x1="${fmt(t1.x)}" y1="${fmt(t1.y)}" x2="${fmt(t2.x)}" y2="${fmt(t2.y)}" stroke="${THEME.dim}" stroke-width="${fmt(thin)}"/>`
|
|
925
|
-
);
|
|
926
|
-
}
|
|
927
|
-
const mid = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };
|
|
928
|
-
const tp = add(mid, mul(n, dimFont * 0.7));
|
|
929
|
-
let angle = Math.atan2(dir.y, dir.x) * 180 / Math.PI;
|
|
930
|
-
if (angle > 90) angle -= 180;
|
|
931
|
-
if (angle < -90) angle += 180;
|
|
932
|
-
const label = dm.text ?? String(Math.round(length(sub(dm.to, dm.from))));
|
|
933
|
-
out.push(
|
|
934
|
-
`<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>`
|
|
935
|
-
);
|
|
1132
|
+
const ctx = { fmt, pt, xml, theme: THEME, sizes, bounds: b };
|
|
1133
|
+
const ops = ir.elements.flatMap((el) => {
|
|
1134
|
+
const def = registry.get(el.kind);
|
|
1135
|
+
return def ? def.render(el, ctx) : [];
|
|
1136
|
+
});
|
|
1137
|
+
for (const pass of RENDER_PASSES) {
|
|
1138
|
+
for (const op of ops) if (op.pass === pass) out.push(op.svg);
|
|
936
1139
|
}
|
|
937
|
-
out.push(northArrow(
|
|
1140
|
+
out.push(northArrow(ir, b, margin, refDim));
|
|
938
1141
|
out.push(scaleBar(b, margin, refDim, thin));
|
|
939
|
-
const tb = titleBlock(
|
|
1142
|
+
const tb = titleBlock(ir, b, margin, refDim, thin);
|
|
940
1143
|
if (tb) out.push(tb);
|
|
941
1144
|
out.push("</svg>");
|
|
942
1145
|
return out.join("\n");
|
|
943
1146
|
}
|
|
944
|
-
function northArrow(
|
|
1147
|
+
function northArrow(ir, b, margin, refDim) {
|
|
945
1148
|
const r = refDim * 0.045;
|
|
946
1149
|
const cx = b.maxX - r;
|
|
947
1150
|
const cy = b.minY - margin * 0.55;
|
|
948
1151
|
let deg;
|
|
949
|
-
switch (
|
|
1152
|
+
switch (ir.north) {
|
|
950
1153
|
case "up":
|
|
951
1154
|
deg = 0;
|
|
952
1155
|
break;
|
|
@@ -960,7 +1163,7 @@ function northArrow(plan, b, margin, refDim) {
|
|
|
960
1163
|
deg = 90;
|
|
961
1164
|
break;
|
|
962
1165
|
default:
|
|
963
|
-
deg = typeof
|
|
1166
|
+
deg = typeof ir.north === "object" ? ir.north.deg : 0;
|
|
964
1167
|
}
|
|
965
1168
|
const fs = refDim * 0.026;
|
|
966
1169
|
const tri = `${fmt(cx)},${fmt(cy - r)} ${fmt(cx - r * 0.5)},${fmt(cy + r * 0.6)} ${fmt(cx)},${fmt(cy + r * 0.25)} ${fmt(cx + r * 0.5)},${fmt(cy + r * 0.6)}`;
|
|
@@ -991,9 +1194,9 @@ function scaleBar(b, margin, refDim, thin) {
|
|
|
991
1194
|
);
|
|
992
1195
|
return `<g>${parts.join("")}</g>`;
|
|
993
1196
|
}
|
|
994
|
-
function titleBlock(
|
|
995
|
-
const t =
|
|
996
|
-
if (!t && !
|
|
1197
|
+
function titleBlock(ir, b, margin, refDim, thin) {
|
|
1198
|
+
const t = ir.title;
|
|
1199
|
+
if (!t && !ir.scale) return null;
|
|
997
1200
|
const boxW = refDim * 0.34;
|
|
998
1201
|
const boxH = margin * 0.82;
|
|
999
1202
|
const x0 = b.maxX - boxW;
|
|
@@ -1004,7 +1207,7 @@ function titleBlock(plan, b, margin, refDim, thin) {
|
|
|
1004
1207
|
if (t?.project) lines.push({ k: "PROJECT", v: t.project });
|
|
1005
1208
|
if (t?.drawnBy) lines.push({ k: "DRAWN BY", v: t.drawnBy });
|
|
1006
1209
|
if (t?.date) lines.push({ k: "DATE", v: t.date });
|
|
1007
|
-
if (
|
|
1210
|
+
if (ir.scale) lines.push({ k: "SCALE", v: ir.scale });
|
|
1008
1211
|
const parts = [];
|
|
1009
1212
|
parts.push(
|
|
1010
1213
|
`<rect x="${fmt(x0)}" y="${fmt(y0)}" width="${fmt(boxW)}" height="${fmt(boxH)}" fill="none" stroke="${THEME.annotation}" stroke-width="${fmt(thin)}"/>`
|
|
@@ -1106,11 +1309,12 @@ function toLegacy(source, d) {
|
|
|
1106
1309
|
}
|
|
1107
1310
|
function compileUncached(source, opts) {
|
|
1108
1311
|
const { plan, diagnostics: parseDiags } = parse(source);
|
|
1109
|
-
const
|
|
1312
|
+
const resolved = plan ? resolve(plan) : null;
|
|
1313
|
+
const diagnostics = resolved ? [...parseDiags, ...resolved.diagnostics] : [...parseDiags];
|
|
1110
1314
|
const errs = diagnostics.filter((d) => d.severity === "error");
|
|
1111
1315
|
const errors = errs.map((d) => toLegacy(source, d));
|
|
1112
1316
|
const warnings = diagnostics.filter((d) => d.severity === "warning").map((d) => toLegacy(source, d));
|
|
1113
|
-
const svg =
|
|
1317
|
+
const svg = resolved && errs.length === 0 ? render(resolved.ir, opts) : "";
|
|
1114
1318
|
return { svg, errors, warnings, diagnostics, ast: plan };
|
|
1115
1319
|
}
|
|
1116
1320
|
function clearCache() {
|
|
@@ -1123,4 +1327,4 @@ export {
|
|
|
1123
1327
|
compile,
|
|
1124
1328
|
clearCache
|
|
1125
1329
|
};
|
|
1126
|
-
//# sourceMappingURL=chunk-
|
|
1330
|
+
//# sourceMappingURL=chunk-3YUQPQPZ.js.map
|