@avodado/render 0.1.1 → 0.2.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/index.js CHANGED
@@ -2083,13 +2083,20 @@ function renderErd(data) {
2083
2083
  hidden = cols.length - rows.length;
2084
2084
  }
2085
2085
  const bodyRows = rows.length + (hidden > 0 ? 1 : 0);
2086
- const h = HEAD_H + bodyRows * ROW_H + BOT_PAD;
2087
- return { name: e.name, rows, hidden, w: COL_W, h };
2086
+ return {
2087
+ name: e.name,
2088
+ cols,
2089
+ rows,
2090
+ hidden,
2091
+ pkIdx: cols.findIndex((c) => c.pk === true),
2092
+ w: COL_W,
2093
+ h: HEAD_H + bodyRows * ROW_H + BOT_PAD
2094
+ };
2088
2095
  });
2089
2096
  const byName = new Map(boxes.map((b) => [b.name, b]));
2090
2097
  const validRels = rels.filter((r) => byName.has(r.from) && byName.has(r.to));
2091
2098
  const g = new To.graphlib.Graph({ multigraph: true });
2092
- g.setGraph({ rankdir: "LR", nodesep: 34, ranksep: 86, marginx: 18, marginy: 18 });
2099
+ g.setGraph({ rankdir: "LR", nodesep: 38, ranksep: 96, marginx: 18, marginy: 18 });
2093
2100
  g.setDefaultEdgeLabel(() => ({}));
2094
2101
  for (const b of boxes) g.setNode(b.name, { width: b.w, height: b.h });
2095
2102
  validRels.forEach((r, i) => g.setEdge(r.from, r.to, {}, `e${i}`));
@@ -2097,30 +2104,36 @@ function renderErd(data) {
2097
2104
  const graph = g.graph();
2098
2105
  const W2 = Math.ceil(graph.width ?? 0);
2099
2106
  const H2 = Math.ceil(graph.height ?? 0);
2100
- const topLeft = /* @__PURE__ */ new Map();
2107
+ const at2 = /* @__PURE__ */ new Map();
2101
2108
  for (const b of boxes) {
2102
2109
  const n = g.node(b.name);
2103
- topLeft.set(b.name, { x: n.x - b.w / 2, y: n.y - b.h / 2 });
2110
+ at2.set(b.name, { x: n.x - b.w / 2, y: n.y - b.h / 2 });
2104
2111
  }
2105
2112
  let s = `<svg viewBox="0 0 ${W2} ${H2}" role="img"><title>Entity-relationship diagram</title>`;
2106
- validRels.forEach((r, i) => {
2107
- const e = g.edge(r.from, r.to, `e${i}`);
2108
- const pts = e?.points;
2109
- if (!pts || pts.length < 2) return;
2110
- const first = pts[0];
2111
- const last = pts[pts.length - 1];
2112
- const prev = pts[pts.length - 2];
2113
- if (first === void 0 || last === void 0 || prev === void 0) return;
2114
- const line = pts.map((p2) => `${round(p2.x)},${round(p2.y)}`).join(" ");
2115
- s += `<polyline points="${line}" fill="none" stroke="var(--gray)" stroke-width="1.5"/><circle cx="${round(first.x)}" cy="${round(first.y)}" r="2.6" fill="var(--gray)"/>` + arrowHead(prev, last);
2116
- const mid = r.card !== void 0 ? pts[Math.floor(pts.length / 2)] : void 0;
2117
- if (r.card !== void 0 && mid !== void 0) {
2118
- const w2 = 30;
2119
- s += `<rect x="${round(mid.x - w2 / 2)}" y="${round(mid.y - 9)}" width="${w2}" height="18" rx="9" fill="var(--white)" stroke="var(--rule)"/><text x="${round(mid.x)}" y="${round(mid.y + 3)}" class="edge-label">${escapeHtml(r.card)}</text>`;
2113
+ validRels.forEach((r) => {
2114
+ const src = byName.get(r.from);
2115
+ const tgt = byName.get(r.to);
2116
+ const sp = at2.get(r.from);
2117
+ const tp = at2.get(r.to);
2118
+ if (!src || !tgt || !sp || !tp) return;
2119
+ const fkY = rowAnchorY(src, sp.y, pickFkIndex(src.cols, tgt.name));
2120
+ const pkY = rowAnchorY(tgt, tp.y, tgt.pkIdx);
2121
+ const rightward = tp.x + tgt.w / 2 >= sp.x + src.w / 2;
2122
+ const sx = rightward ? sp.x + src.w : sp.x;
2123
+ const tx = rightward ? tp.x : tp.x + tgt.w;
2124
+ const lo = Math.min(sx, tx) + 10;
2125
+ const hi = Math.max(sx, tx) - 10;
2126
+ const midX = hi > lo ? clamp((sx + tx) / 2, lo, hi) : (sx + tx) / 2;
2127
+ const card = parseCard(r.card);
2128
+ s += `<path d="M${round(sx)},${round(fkY)} H${round(midX)} V${round(pkY)} H${round(tx)}" fill="none" stroke="var(--gray)" stroke-width="1.5"/>` + crowFoot(sx, fkY, rightward ? 1 : -1, card.fromMany) + crowFoot(tx, pkY, rightward ? -1 : 1, card.toMany);
2129
+ if (r.label !== void 0 && r.label !== "") {
2130
+ const w2 = Math.max(30, r.label.length * 6.4);
2131
+ const cy = (fkY + pkY) / 2;
2132
+ s += `<rect x="${round(midX - w2 / 2)}" y="${round(cy - 9)}" width="${round(w2)}" height="18" rx="9" fill="var(--white)" stroke="var(--rule)"/><text x="${round(midX)}" y="${round(cy + 3)}" class="edge-label">${escapeHtml(r.label)}</text>`;
2120
2133
  }
2121
2134
  });
2122
2135
  for (const b of boxes) {
2123
- const p2 = topLeft.get(b.name);
2136
+ const p2 = at2.get(b.name);
2124
2137
  if (!p2) continue;
2125
2138
  const { x: x2, y } = p2;
2126
2139
  s += `<rect x="${round(x2)}" y="${round(y)}" width="${b.w}" height="${b.h}" rx="5" fill="var(--white)" stroke="var(--navy)"/><path d="M${round(x2)},${round(y + HEAD_H)} v${ -27} a5,5 0 0 1 5,-5 h${b.w - 10} a5,5 0 0 1 5,5 v${HEAD_H - 5} z" fill="var(--navy)"/><text x="${round(x2 + b.w / 2)}" y="${round(y + 21)}" class="er-head-text">${escapeHtml(b.name)}</text>`;
@@ -2152,16 +2165,37 @@ function renderErd(data) {
2152
2165
  };
2153
2166
  return diagramFrame(opts, s);
2154
2167
  }
2155
- function arrowHead(from, to) {
2156
- const ang = Math.atan2(to.y - from.y, to.x - from.x);
2157
- const len = 10;
2158
- const spread = 0.42;
2159
- const b1x = to.x - len * Math.cos(ang - spread);
2160
- const b1y = to.y - len * Math.sin(ang - spread);
2161
- const b2x = to.x - len * Math.cos(ang + spread);
2162
- const b2y = to.y - len * Math.sin(ang + spread);
2163
- return `<path d="M${round(b1x)},${round(b1y)} L${round(to.x)},${round(to.y)} L${round(b2x)},${round(b2y)}" fill="none" stroke="var(--navy)" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/>`;
2168
+ function rowAnchorY(box, topY, idx) {
2169
+ if (idx >= 0 && idx < box.rows.length) return topY + HEAD_H + idx * ROW_H + ROW_H / 2;
2170
+ return topY + box.h / 2;
2171
+ }
2172
+ function pickFkIndex(columns, toName) {
2173
+ const fks = columns.map((c, i) => ({ c, i })).filter((x2) => x2.c.fk === true);
2174
+ const first = fks[0];
2175
+ if (first === void 0) return -1;
2176
+ const t = toName.toLowerCase();
2177
+ const singular = t.replace(/s$/, "");
2178
+ const match = fks.find((x2) => {
2179
+ const n = x2.c.name.toLowerCase();
2180
+ return n.includes(t) || n.includes(singular);
2181
+ });
2182
+ return (match ?? first).i;
2183
+ }
2184
+ function parseCard(card) {
2185
+ if (card === void 0) return { fromMany: true, toMany: false };
2186
+ const parts = card.split(":");
2187
+ const many = (p2) => p2 !== void 0 && p2.trim().toUpperCase() !== "1";
2188
+ return { fromMany: many(parts[0]), toMany: many(parts[1]) };
2164
2189
  }
2190
+ function crowFoot(bx, y, outward, many) {
2191
+ if (many) {
2192
+ const ax = bx + outward * 14;
2193
+ return `<path d="M${round(ax)},${round(y)} L${round(bx)},${round(y - 7)} M${round(ax)},${round(y)} L${round(bx)},${round(y)} M${round(ax)},${round(y)} L${round(bx)},${round(y + 7)}" fill="none" stroke="var(--navy)" stroke-width="1.4" stroke-linecap="round"/>`;
2194
+ }
2195
+ const tx = bx + outward * 9;
2196
+ return `<line x1="${round(tx)}" y1="${round(y - 6)}" x2="${round(tx)}" y2="${round(y + 6)}" stroke="var(--navy)" stroke-width="1.4" stroke-linecap="round"/>`;
2197
+ }
2198
+ var clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
2165
2199
  var round = (n) => Math.round(n * 10) / 10;
2166
2200
 
2167
2201
  // src/blocks/kanban.ts
@@ -2698,6 +2732,51 @@ function edgePill(p2, label, err = false) {
2698
2732
  return `<g><rect x="${p2.lx - w2 / 2}" y="${p2.ly - 9}" width="${w2}" height="18" rx="9" fill="#fff" stroke="#d1d5db"/><text x="${p2.lx}" y="${p2.ly + 3}" class="edge-label${errClass}">${escapeHtml(label)}</text></g>`;
2699
2733
  }
2700
2734
 
2735
+ // src/blocks/autoLayout.ts
2736
+ function ensureGrid(items, edges, rankdir) {
2737
+ const allPlaced = items.length > 0 && items.every((n) => n.col !== void 0 && n.row !== void 0);
2738
+ if (allPlaced) {
2739
+ return items.map((n) => ({ ...n, col: n.col, row: n.row }));
2740
+ }
2741
+ const grid = autoGrid(
2742
+ items.map((n) => n.id),
2743
+ edges,
2744
+ rankdir
2745
+ );
2746
+ return items.map((n) => {
2747
+ const g = grid.get(n.id) ?? { col: 1, row: 1 };
2748
+ return { ...n, col: g.col, row: g.row };
2749
+ });
2750
+ }
2751
+ function autoGrid(ids, edges, rankdir) {
2752
+ const idSet = new Set(ids);
2753
+ const g = new To.graphlib.Graph();
2754
+ g.setGraph({ rankdir, nodesep: 16, ranksep: 16, marginx: 0, marginy: 0 });
2755
+ g.setDefaultEdgeLabel(() => ({}));
2756
+ for (const id of ids) g.setNode(id, { width: 10, height: 10 });
2757
+ for (const e of edges) {
2758
+ if (e.from !== e.to && idSet.has(e.from) && idSet.has(e.to)) g.setEdge(e.from, e.to);
2759
+ }
2760
+ To.layout(g);
2761
+ const pos = ids.map((id) => {
2762
+ const n = g.node(id);
2763
+ return { id, x: Math.round(n?.x ?? 0), y: Math.round(n?.y ?? 0) };
2764
+ });
2765
+ const out = /* @__PURE__ */ new Map();
2766
+ if (rankdir === "TB") {
2767
+ const ranks = [...new Set(pos.map((p2) => p2.y))].sort((a, b) => a - b);
2768
+ ranks.forEach((yv, ri) => {
2769
+ pos.filter((p2) => p2.y === yv).sort((a, b) => a.x - b.x).forEach((p2, ci) => out.set(p2.id, { col: ci + 1, row: ri + 1 }));
2770
+ });
2771
+ } else {
2772
+ const ranks = [...new Set(pos.map((p2) => p2.x))].sort((a, b) => a - b);
2773
+ ranks.forEach((xv, ci) => {
2774
+ pos.filter((p2) => p2.x === xv).sort((a, b) => a.y - b.y).forEach((p2, ri) => out.set(p2.id, { col: ci + 1, row: ri + 1 }));
2775
+ });
2776
+ }
2777
+ return out;
2778
+ }
2779
+
2701
2780
  // src/blocks/flow.ts
2702
2781
  function flowStyle(kind) {
2703
2782
  switch (kind ?? "process") {
@@ -2713,8 +2792,8 @@ function flowStyle(kind) {
2713
2792
  }
2714
2793
  var ERR_LABEL_RE = /^(no|fail|error|reject)/i;
2715
2794
  function renderFlowSvg(data) {
2716
- const nodes = data.nodes ?? [];
2717
2795
  const edges = data.edges ?? [];
2796
+ const nodes = ensureGrid(data.nodes ?? [], edges, "TB");
2718
2797
  const cellW = 176;
2719
2798
  const cellH = 70;
2720
2799
  const gapX = 60;
@@ -2805,8 +2884,8 @@ function pillCls(kind) {
2805
2884
  return "pill pill-active";
2806
2885
  }
2807
2886
  function renderState(data) {
2808
- const states = data.states ?? [];
2809
2887
  const trans = data.transitions ?? [];
2888
+ const states = ensureGrid(data.states ?? [], trans, "LR");
2810
2889
  const cellW = 168;
2811
2890
  const cellH = 64;
2812
2891
  const gapX = 74;
@@ -2878,8 +2957,8 @@ function dfdStyle(kind) {
2878
2957
  }
2879
2958
  }
2880
2959
  function renderDfd(data) {
2881
- const nodes = data.nodes ?? [];
2882
2960
  const edges = data.edges ?? [];
2961
+ const nodes = ensureGrid(data.nodes ?? [], edges, "LR");
2883
2962
  const cellW = 168;
2884
2963
  const cellH = 76;
2885
2964
  const gapX = 60;
@@ -3401,8 +3480,8 @@ var LEGEND = [
3401
3480
  { sw: "#f3f4f6", label: "External" }
3402
3481
  ];
3403
3482
  function renderC4(data) {
3404
- const nodes = data.nodes ?? [];
3405
3483
  const edges = data.edges ?? [];
3484
+ const nodes = ensureGrid(data.nodes ?? [], edges, "TB");
3406
3485
  const byId = new Map(nodes.map((n) => [n.id, n]));
3407
3486
  const cellW = 212;
3408
3487
  const cellH = 102;
@@ -3501,8 +3580,8 @@ function umlRel(kind) {
3501
3580
  }
3502
3581
  }
3503
3582
  function renderUml(data) {
3504
- const classes = data.classes ?? [];
3505
3583
  const rels = data.rels ?? [];
3584
+ const classes = ensureGrid(data.classes ?? [], rels, "TB");
3506
3585
  const colW = 204;
3507
3586
  const gapX = 64;
3508
3587
  const gapY = 50;