tsquare 0.13.1 → 0.14.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/hit.d.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * What's under a point or a line, from measureElements' entries: plain data in, plain data out, and
3
+ * nothing heavy imported, so the viewer (tsquare/viewer) can hit-test in the browser without the
4
+ * renderer. measure.ts produces the entries and re-exports all of this.
5
+ */
6
+ import type { ComponentName } from "./catalog.js";
7
+ /** A box on the board, in board pixels: the same coordinates as the rendered SVG. */
8
+ export interface MeasuredBox {
9
+ x: number;
10
+ y: number;
11
+ width: number;
12
+ height: number;
13
+ }
14
+ export interface MeasuredElement {
15
+ /** The element's line in the wireframe text; 0 when `lines` doesn't have it. */
16
+ line: number;
17
+ /** The last line of its block (its children included); `line` when it has no children. */
18
+ endLine: number;
19
+ /** Its #id, when the text gives one. */
20
+ id?: string;
21
+ /** "Button", "Screen", … */
22
+ type: Exclude<ComponentName, "Flow">;
23
+ /** Its main text: a button's label, a screen's name, a heading's text. */
24
+ label?: string;
25
+ /** The line of the screen it's on; null for the board, screens and notes beside them. */
26
+ screenLine: number | null;
27
+ /** The line of the element containing it; null for the board. */
28
+ parentLine: number | null;
29
+ /**
30
+ * Where it's drawn: the part that shows, for content cut off by its screen (past the bottom, under
31
+ * a tab bar, or under the status or browser bar) or by a drawer's panel. A screen's box is its frame. Can be 0 wide or 0 tall
32
+ * (a spacer in a row): such an element is listed, but never under a point.
33
+ */
34
+ box: MeasuredBox;
35
+ /** Screens: the band above the frame with the screen's name. */
36
+ labelBox?: MeasuredBox;
37
+ /** Its open menu, option list or date picker, and its tooltip, in drawing order (a tooltip's and a date picker's size within a few px). */
38
+ overlays?: MeasuredBox[];
39
+ /** Modals and drawers: whether the screen behind is dimmed (on unless the text says no-scrim). */
40
+ scrim?: boolean;
41
+ }
42
+ export interface MeasuredFlow {
43
+ /** The flow's line in the wireframe text; 0 when `lines` doesn't have it. */
44
+ line: number;
45
+ /** Always `line`: a flow has no children. */
46
+ endLine: number;
47
+ type: "Flow";
48
+ label?: string;
49
+ /** The ids it connects, as written in the flow line. */
50
+ from: string;
51
+ to: string;
52
+ /** The lines of the elements (or screens) it connects. */
53
+ fromLine: number;
54
+ toLine: number;
55
+ /** The drawn line as points, from the tip of one end's marker to the other's; curves and corners are sampled. */
56
+ path: [number, number][];
57
+ /** The label pill. */
58
+ labelBox?: MeasuredBox;
59
+ }
60
+ export type Measured = MeasuredElement | MeasuredFlow;
61
+ export declare const isFlow: (e: Measured) => e is MeasuredFlow;
62
+ /** Dim their screen while open (unless no-scrim): a click on the dimmed part is a click on the screen. */
63
+ export declare const COVERS_SCREEN: Set<string>;
64
+ export declare const OVERLAYS: Set<string>;
65
+ /**
66
+ * Components that hold others (the catalog's components with slots; a test keeps the two equal):
67
+ * from the type, not from the lines, so the order doesn't depend on them.
68
+ */
69
+ export declare const CONTAINERS: ReadonlySet<string>;
70
+ /**
71
+ * Everything under a point (board pixels), most likely meant first:
72
+ * 1. an open menu, option list, date picker or tooltip: the element it belongs to
73
+ * 2. a flow's label
74
+ * 3. elements that hold no others (buttons, inputs, text, …), topmost first
75
+ * 4. a flow's line, within `tolerance` px of it (default 6; pass more when the board is shown zoomed out)
76
+ * 5. stacks, cards, modals and drawers, then the screen, then the board
77
+ * A modal, drawer or toast hides the screen's content under it. One that dims its screen (modals and
78
+ * drawers, unless no-scrim) hides all of it: on the dimmed part, only the screen and the board.
79
+ */
80
+ export declare function elementsAt(elements: Measured[], x: number, y: number, opts?: {
81
+ tolerance?: number;
82
+ }): Measured[];
83
+ /**
84
+ * The element on a line and every block around it, innermost first: for a code view that follows
85
+ * the caret. A comment or blank line between an element's children gives that element. A flow's
86
+ * line gives the flow, then the board.
87
+ */
88
+ export declare function elementsAtLine(elements: Measured[], line: number): Measured[];
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export { mcpTools, MCP_INSTRUCTIONS, LINK_KINDS, DEFAULT_LINKS, type LinkKind, w
7
7
  export { upgradeWireframe, upgradeSpec, RENAMED_PROPS, type UpgradeOptions } from "./upgrade.js";
8
8
  export { renderAll, DEFAULT_SELECTOR, type RenderAllOptions, type RenderAllResult } from "./dom.js";
9
9
  export { measureElements, elementsAt, elementsAtLine, type Measured, type MeasuredElement, type MeasuredFlow, type MeasuredBox, type MeasureOptions } from "./measure.js";
10
- export { renderWireframeSvg, renderWireframePng, checkSpec, boardSize, boardLayout, type BoardItem, type RenderWireframeOptions } from "./render.js";
10
+ export { renderWireframeSvg, renderWireframePng, checkSpec, boardSize, boardLayout, measureBoardLayout, type BoardItem, type RenderWireframeOptions } from "./render.js";
11
11
  export { parseWireframeText, type TextIssue, type ParseResult } from "./text.js";
12
12
  export { catalog, componentDefinitions } from "./catalog.js";
13
13
  export { registry } from "./components.js";
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import {
2
- languageData
3
- } from "./chunks/chunk-HVGRKWIV.js";
2
+ elementsAt,
3
+ elementsAtLine,
4
+ languageData,
5
+ measureElements
6
+ } from "./chunks/chunk-NZWKURF4.js";
4
7
  import {
5
8
  DEVICES,
6
- LABEL_H,
7
- PRIMARY_PROP,
8
9
  RENAMED_PROPS,
9
- SpecError,
10
10
  WireframeError,
11
11
  boardLayout,
12
12
  boardSize,
@@ -14,32 +14,21 @@ import {
14
14
  checkSpec,
15
15
  compileWireframe,
16
16
  componentDefinitions,
17
- findAnchors,
18
- flowEnds,
19
- flowPolyline,
20
17
  formatIssues,
21
18
  languageReference,
22
- layoutFlows,
23
- loadFonts,
24
- overlayBoxes,
25
- paletteFor,
19
+ measureBoardLayout,
26
20
  parseWireframeText,
27
21
  printWireframeText,
28
- readMarkers,
29
22
  registry,
30
23
  renderWireframe,
31
24
  renderWireframePng,
32
25
  renderWireframeSvg,
33
26
  repairPrompt,
34
- tagForMeasuring,
35
27
  theme,
36
28
  upgradeSpec,
37
29
  upgradeWireframe,
38
- wireframePrompt,
39
- withBoardGap,
40
- withMarkers,
41
- withPalette
42
- } from "./chunks/chunk-K3Y2E2QA.js";
30
+ wireframePrompt
31
+ } from "./chunks/chunk-QYGOXTAG.js";
43
32
 
44
33
  // src/deflate.ts
45
34
  import { deflateRawSync, inflateRawSync } from "node:zlib";
@@ -130,7 +119,7 @@ async function renderWireframeTool(args) {
130
119
  const { spec, issues } = compileWireframe(args.text);
131
120
  if (!spec || issues.length) return problems(issues);
132
121
  const flows = args.flows !== false;
133
- const { width, height } = boardSize(spec, { flows });
122
+ const { width, height } = await measureBoardLayout(spec, { flows });
134
123
  const scale = Math.min(1, MODEL_IMAGE_MAX / Math.max(width, height));
135
124
  const png = await renderWireframePng(spec, { skipValidation: true, flows, scale });
136
125
  const shown = scale < 1 ? `, shown at ${Math.round(width * scale)}\xD7${Math.round(height * scale)}` : "";
@@ -216,7 +205,7 @@ async function renderBlock(doc, source) {
216
205
  if (!spec || issues.length) return problems2(doc, issues.map((i) => `line ${i.line}: ${i.message}`));
217
206
  try {
218
207
  const svg = await renderWireframeSvg(spec, { skipValidation: true });
219
- const { width, height } = boardLayout(spec);
208
+ const { width, height } = await measureBoardLayout(spec);
220
209
  const title = spec.elements[spec.root]?.props?.title;
221
210
  const figure = doc.createElement("figure");
222
211
  figure.className = "tsquare-board";
@@ -255,151 +244,6 @@ function addStyles(doc) {
255
244
  style.textContent = STYLES;
256
245
  doc.head.append(style);
257
246
  }
258
-
259
- // src/measure.ts
260
- import { renderToSvg } from "@json-render/image/render";
261
- var isFlow = (e) => e.type === "Flow";
262
- var COVERS_SCREEN = /* @__PURE__ */ new Set(["Modal", "Drawer"]);
263
- var OVERLAYS = /* @__PURE__ */ new Set(["Modal", "Drawer", "Toast"]);
264
- var CONTAINERS = new Set(Object.entries(componentDefinitions).filter(([, d]) => d.slots.length).map(([name]) => name));
265
- var BARS = /* @__PURE__ */ new Set(["NavBar", "TabBar"]);
266
- var toBox = (r) => ({ x: round(r.x), y: round(r.y), width: round(r.w), height: round(r.h) });
267
- var round = (n) => Math.round(n * 100) / 100;
268
- function clip(r, to) {
269
- if (!to) return r;
270
- const x = Math.max(r.x, to.x), y = Math.max(r.y, to.y);
271
- const w = Math.min(r.x + r.w, to.x + to.w) - x, h = Math.min(r.y + r.h, to.y + to.h) - y;
272
- return w >= 0 && h >= 0 && (w > 0 || r.w === 0) && (h > 0 || r.h === 0) ? { x, y, w, h } : null;
273
- }
274
- async function measureElements(spec, lines, opts = {}) {
275
- upgradeSpec(spec);
276
- if (!opts.skipValidation) {
277
- const issues = checkSpec(spec);
278
- if (issues.length) throw new SpecError(issues);
279
- }
280
- const { width, height } = boardLayout(spec, { flows: opts.flows });
281
- spec = withBoardGap(spec, opts.flows);
282
- const els = spec.elements;
283
- const anchors = findAnchors(spec);
284
- const every = Object.keys(els).filter((k) => !["Board", "Screen", "Flow"].includes(els[k].type)).map((id) => ({ id, screen: id, kind: "tooltip", field: false }));
285
- const screens = Object.keys(els).filter((k) => els[k].type === "Screen");
286
- const { tagged, colors } = tagForMeasuring(spec, [...anchors, ...every], screens);
287
- const svg = await renderToSvg(tagged, { registry: withMarkers(withPalette(paletteFor())), includeStandard: false, fonts: await loadFonts(), width, height });
288
- const boxes = readMarkers(svg, colors);
289
- const { items } = boardLayout(spec, { flows: opts.flows });
290
- const laidOut = (els[spec.root].children ?? []).filter((k) => els[k]?.type === "Screen" || els[k]?.type === "Note");
291
- const frames = /* @__PURE__ */ new Map(), bands = /* @__PURE__ */ new Map();
292
- laidOut.forEach((k, i) => {
293
- const it = items[i];
294
- if (!it || els[k].type !== "Screen") return;
295
- frames.set(k, { x: it.x, y: it.y + LABEL_H, w: it.width, h: it.height - LABEL_H });
296
- bands.set(k, { x: it.x, y: it.y, w: it.width, h: LABEL_H });
297
- });
298
- const overlays = /* @__PURE__ */ new Map();
299
- for (const o of overlayBoxes(spec, anchors, boxes)) overlays.set(o.id, [...overlays.get(o.id) ?? [], o.box]);
300
- const lineOf = (k) => k == null ? null : lines[k] ?? 0;
301
- const endLine = (k) => Math.max(lines[k] ?? 0, ...(els[k].children ?? []).filter((c) => els[c]).map(endLine));
302
- const labelOf = (k) => {
303
- const p = els[k].props ?? {};
304
- const v = p[PRIMARY_PROP[els[k].type] ?? ""];
305
- return typeof v === "string" ? v : void 0;
306
- };
307
- const out = [];
308
- const visit = (k, parent, screen, cut) => {
309
- const el = els[k];
310
- if (!el || el.type === "Flow") return;
311
- const p = el.props ?? {};
312
- const base = { line: lineOf(k), endLine: endLine(k), ...typeof p.id === "string" ? { id: p.id } : {}, type: el.type, ...labelOf(k) != null ? { label: labelOf(k) } : {}, screenLine: lineOf(screen), parentLine: lineOf(parent) };
313
- let entry = null;
314
- if (el.type === "Board") entry = { ...base, box: { x: 0, y: 0, width, height } };
315
- else if (el.type === "Screen") {
316
- const frame = frames.get(k);
317
- if (frame) entry = { ...base, box: toBox(frame), labelBox: toBox(bands.get(k)) };
318
- } else {
319
- const r = boxes[k];
320
- const seen = r && clip(r, cut);
321
- if (seen) {
322
- const view = screen ? boxes[screen] ?? frames.get(screen) : void 0;
323
- const os = (overlays.get(k) ?? []).map((o) => clip(o, view)).filter((o) => !!o && o.w > 0 && o.h > 0);
324
- entry = { ...base, box: toBox(seen), ...os.length ? { overlays: os.map(toBox) } : {}, ...COVERS_SCREEN.has(el.type) ? { scrim: p.scrim ?? true } : {} };
325
- }
326
- }
327
- if (entry) out.push(entry);
328
- const inScreen = el.type === "Screen" ? k : screen;
329
- const kids = el.children ?? [];
330
- if (el.type === "Screen") {
331
- const frame = frames.get(k) ?? null;
332
- const body = clip(boxes[`${k}#body`] ?? frame, frame);
333
- const over = (c) => OVERLAYS.has(els[c]?.type);
334
- for (const c of [...kids.filter((c2) => !over(c2)), ...kids.filter(over)]) visit(c, k, k, BARS.has(els[c]?.type) || over(c) ? frame : body);
335
- } else {
336
- const inner = el.type === "Drawer" && boxes[k] ? clip(boxes[k], cut) : cut;
337
- for (const c of kids) visit(c, k, inScreen, inner);
338
- }
339
- };
340
- visit(spec.root, null, null, null);
341
- if (opts.flows !== false) {
342
- const flows = flowEnds(spec);
343
- const resolved = flows.resolve(boxes);
344
- const ends = new Map(flows.list.map((f) => [f.key, f]));
345
- layoutFlows(spec, resolved, flows.screens, flows.gap, paletteFor()).forEach((f) => {
346
- const p = els[f.key].props ?? {};
347
- const e = ends.get(f.key);
348
- out.push({
349
- line: lineOf(f.key),
350
- endLine: lineOf(f.key),
351
- type: "Flow",
352
- ...f.label ? { label: f.label.text } : {},
353
- from: String(p.from),
354
- to: String(p.to),
355
- fromLine: lineOf(e.from),
356
- toLine: lineOf(e.to),
357
- path: flowPolyline(f).map((q) => [round(q.x), round(q.y)]),
358
- ...f.label ? { labelBox: toBox(f.label) } : {}
359
- });
360
- });
361
- }
362
- return out;
363
- }
364
- var inside = (b, x, y) => !!b && b.width > 0 && b.height > 0 && x >= b.x && x <= b.x + b.width && y >= b.y && y <= b.y + b.height;
365
- var center = (b) => [b.x + b.width / 2, b.y + b.height / 2];
366
- function distanceToPath(path, x, y) {
367
- let best = Infinity;
368
- for (let i = 1; i < path.length; i++) {
369
- const [ax, ay] = path[i - 1], [bx, by] = path[i];
370
- const dx = bx - ax, dy = by - ay, len = dx * dx + dy * dy;
371
- const t = len ? Math.max(0, Math.min(1, ((x - ax) * dx + (y - ay) * dy) / len)) : 0;
372
- best = Math.min(best, Math.hypot(x - ax - t * dx, y - ay - t * dy));
373
- }
374
- return best;
375
- }
376
- var isContainer = (e) => CONTAINERS.has(e.type);
377
- function elementsAt(elements, x, y, opts = {}) {
378
- const tolerance = opts.tolerance ?? 6;
379
- const order = new Map(elements.map((e, i) => [e, i]));
380
- const topFirst = (a, b) => order.get(b) - order.get(a);
381
- const covered = /* @__PURE__ */ new Set();
382
- const screens = elements.filter((e) => e.type === "Screen");
383
- const screenAround = (b) => screens.find((s) => inside(s.box, ...center(b)));
384
- for (const [i, m] of elements.entries()) {
385
- if (isFlow(m) || !OVERLAYS.has(m.type)) continue;
386
- const screen = screenAround(m.box);
387
- if (!screen || !inside(m.box, x, y) && !(m.scrim && inside(screen.box, x, y))) continue;
388
- for (const e of elements.slice(0, i)) if (!isFlow(e) && e.type !== "Screen" && e.type !== "Board" && screenAround(e.box) === screen) covered.add(e);
389
- }
390
- const owners = [], labels = [], leaves = [], lines = [], containers = [];
391
- for (const e of elements) {
392
- if (isFlow(e)) {
393
- if (inside(e.labelBox, x, y)) labels.push(e);
394
- else if (distanceToPath(e.path, x, y) <= tolerance) lines.push(e);
395
- } else if (e.overlays?.some((o) => inside(o, x, y))) owners.push(e);
396
- else if (!covered.has(e) && (inside(e.box, x, y) || inside(e.labelBox, x, y))) (isContainer(e) ? containers : leaves).push(e);
397
- }
398
- return [owners, labels, leaves, lines, containers].flatMap((g) => g.sort(topFirst));
399
- }
400
- function elementsAtLine(elements, line) {
401
- return elements.filter((e) => e.line > 0 && e.line <= line && line <= e.endLine).sort((a, b) => b.line - a.line);
402
- }
403
247
  export {
404
248
  DEFAULT_LINKS,
405
249
  DEFAULT_SELECTOR,
@@ -424,6 +268,7 @@ export {
424
268
  printWireframeText as formatWireframe,
425
269
  languageData,
426
270
  mcpTools,
271
+ measureBoardLayout,
427
272
  measureElements,
428
273
  parseWireframeText,
429
274
  registry,
package/dist/measure.d.ts CHANGED
@@ -8,61 +8,8 @@
8
8
  * so measure once per text and look up as often as needed (they also run on the JSON in a browser).
9
9
  */
10
10
  import type { Spec } from "@json-render/core";
11
- import { type ComponentName } from "./catalog.js";
12
- /** A box on the board, in board pixels: the same coordinates as the rendered SVG. */
13
- export interface MeasuredBox {
14
- x: number;
15
- y: number;
16
- width: number;
17
- height: number;
18
- }
19
- export interface MeasuredElement {
20
- /** The element's line in the wireframe text; 0 when `lines` doesn't have it. */
21
- line: number;
22
- /** The last line of its block (its children included); `line` when it has no children. */
23
- endLine: number;
24
- /** Its #id, when the text gives one. */
25
- id?: string;
26
- /** "Button", "Screen", … */
27
- type: Exclude<ComponentName, "Flow">;
28
- /** Its main text: a button's label, a screen's name, a heading's text. */
29
- label?: string;
30
- /** The line of the screen it's on; null for the board, screens and notes beside them. */
31
- screenLine: number | null;
32
- /** The line of the element containing it; null for the board. */
33
- parentLine: number | null;
34
- /**
35
- * Where it's drawn: the part that shows, for content cut off by its screen (past the bottom, or
36
- * under a tab bar) or by a drawer's panel. A screen's box is its frame. Can be 0 wide or 0 tall
37
- * (a spacer in a row): such an element is listed, but never under a point.
38
- */
39
- box: MeasuredBox;
40
- /** Screens: the band above the frame with the screen's name. */
41
- labelBox?: MeasuredBox;
42
- /** Its open menu, option list or date picker, and its tooltip, in drawing order (a tooltip's and a date picker's size within a few px). */
43
- overlays?: MeasuredBox[];
44
- /** Modals and drawers: whether the screen behind is dimmed (on unless the text says no-scrim). */
45
- scrim?: boolean;
46
- }
47
- export interface MeasuredFlow {
48
- /** The flow's line in the wireframe text; 0 when `lines` doesn't have it. */
49
- line: number;
50
- /** Always `line`: a flow has no children. */
51
- endLine: number;
52
- type: "Flow";
53
- label?: string;
54
- /** The ids it connects, as written in the flow line. */
55
- from: string;
56
- to: string;
57
- /** The lines of the elements (or screens) it connects. */
58
- fromLine: number;
59
- toLine: number;
60
- /** The drawn line as points, from the tip of one end's marker to the other's; curves and corners are sampled. */
61
- path: [number, number][];
62
- /** The label pill. */
63
- labelBox?: MeasuredBox;
64
- }
65
- export type Measured = MeasuredElement | MeasuredFlow;
11
+ import { type Measured, type MeasuredElement } from "./hit.js";
12
+ export { elementsAt, elementsAtLine, type Measured, type MeasuredBox, type MeasuredElement, type MeasuredFlow } from "./hit.js";
66
13
  export interface MeasureOptions {
67
14
  /** As passed to renderWireframeSvg (default true): flow labels can widen the gaps between screens, so the boxes only match a render with the same setting. */
68
15
  flows?: boolean;
@@ -76,22 +23,3 @@ export interface MeasureOptions {
76
23
  * Elements that aren't drawn (content entirely cut off, children of a closed accordion) are left out.
77
24
  */
78
25
  export declare function measureElements(spec: Spec, lines: Record<string, number>, opts?: MeasureOptions): Promise<[MeasuredElement, ...Measured[]]>;
79
- /**
80
- * Everything under a point (board pixels), most likely meant first:
81
- * 1. an open menu, option list, date picker or tooltip: the element it belongs to
82
- * 2. a flow's label
83
- * 3. elements that hold no others (buttons, inputs, text, …), topmost first
84
- * 4. a flow's line, within `tolerance` px of it (default 6; pass more when the board is shown zoomed out)
85
- * 5. stacks, cards, modals and drawers, then the screen, then the board
86
- * A modal, drawer or toast hides the screen's content under it. One that dims its screen (modals and
87
- * drawers, unless no-scrim) hides all of it: on the dimmed part, only the screen and the board.
88
- */
89
- export declare function elementsAt(elements: Measured[], x: number, y: number, opts?: {
90
- tolerance?: number;
91
- }): Measured[];
92
- /**
93
- * The element on a line and every block around it, innermost first: for a code view that follows
94
- * the caret. A comment or blank line between an element's children gives that element. A flow's
95
- * line gives the flow, then the board.
96
- */
97
- export declare function elementsAtLine(elements: Measured[], line: number): Measured[];
@@ -6,6 +6,8 @@ export interface EditorOptions {
6
6
  data?: LanguageData;
7
7
  /** Called with the whole text after every change. */
8
8
  onChange?: (text: string) => void;
9
+ /** Called with the caret's line (1 = the first) when it moves to another line. */
10
+ onCursorLine?: (line: number) => void;
9
11
  /** Line numbers in a gutter. Default true. */
10
12
  lineNumbers?: boolean;
11
13
  /** Wrap long lines instead of scrolling sideways; continuation rows are indented under their line. Default false. */
@@ -1,15 +1,16 @@
1
1
  import {
2
- languageData
3
- } from "../chunks/chunk-HVGRKWIV.js";
2
+ languageData,
3
+ measureElements
4
+ } from "../chunks/chunk-NZWKURF4.js";
4
5
  import {
5
- boardLayout,
6
6
  compileWireframe,
7
7
  componentDocs,
8
8
  formatIssues,
9
+ measureBoardLayout,
9
10
  renderWireframePng,
10
11
  renderWireframeSvg,
11
12
  wireframePrompt
12
- } from "../chunks/chunk-K3Y2E2QA.js";
13
+ } from "../chunks/chunk-QYGOXTAG.js";
13
14
 
14
15
  // src/playground/index.ts
15
16
  import { createServer } from "node:http";
@@ -494,12 +495,16 @@ function createPlaygroundHandler() {
494
495
  return true;
495
496
  }
496
497
  if (req.method === "POST" && (url.pathname === "/api/render" || url.pathname === "/api/png")) {
497
- const { spec, issues } = compileWireframe(await readBody(req));
498
+ const { spec, issues, lines } = compileWireframe(await readBody(req));
498
499
  const flows = url.searchParams.get("flows") !== "0";
499
500
  if (url.pathname === "/api/render") {
500
501
  if (!spec || issues.length) return json(res, 200, { issues });
501
- const { width, height, items } = boardLayout(spec, { flows });
502
- return json(res, 200, { issues, svg: await renderWireframeSvg(spec, { skipValidation: true, flows }), width, height, items });
502
+ const [svg, { width, height, items }, elements] = await Promise.all([
503
+ renderWireframeSvg(spec, { skipValidation: true, flows }),
504
+ measureBoardLayout(spec, { flows }),
505
+ url.searchParams.get("elements") === "1" ? measureElements(spec, lines, { skipValidation: true, flows }) : void 0
506
+ ]);
507
+ return json(res, 200, { issues, svg, width, height, items, ...elements ? { elements } : {} });
503
508
  }
504
509
  if (!spec || issues.length) return json(res, 400, { issues });
505
510
  const scale = Math.min(4, Math.max(1, Number(url.searchParams.get("scale") ?? 2)));