umlimator 0.19.0 → 0.21.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/lib/umlimator.js CHANGED
@@ -120,7 +120,11 @@ function registerDiagramType(name, def = {}) {
120
120
  "none", which emits reparent (the element itself and what it holds are left out; its owner is added when not offered).
121
121
  None, or not a list: no Owner field.
122
122
  - layout.pins false: pos is {}, no ranking; layout.rank false: pos is the view's pins alone, no ranking (risks.md P1).
123
- reroute is no registry field: reroute: false goes in the dg the type's own layout passes to layoutTyped (src/typed/layout.js). */
123
+ reroute is no registry field: reroute: false goes in the dg the type's own layout passes to layoutTyped (src/typed/layout.js).
124
+ Stage 4's (notes/stage-4.md §12.1, §15): edits.rename(model, view, id, text); a hook's { pick, then }; name: false and
125
+ opens on a palette entry; a line's cut, its label's width (150); rows(model, view, id) and word(model, id), the PDF's.
126
+ Stage 5's: a line's fromPort, toPort, inner, portRank, a box's pad, round, a view's labelGaps (src/typed/layout.js);
127
+ edits.member, edits.key (src/typed/edit.js). */
124
128
 
125
129
  /* A view's type: the map for a view with no diagram, the registered type, or null for one this release does not draw */
126
130
  const diagramOf = view => TYPES.get(view && view.diagram != null ? view.diagram : 'map') || null;
@@ -523,14 +527,13 @@ const SHAPE = {
523
527
  // src/typed/draw.js: from its text), and ports(n, side, k), k line ends spread evenly along a side ('L', 'R', 'T', 'B'), each on the outline
524
528
  for (const S of Object.values(SHAPE)) { S.size ||= n => ({ w: mapNodeW(n), h: 64 }); S.ports = (n, side, k = 1) => portsOn(S, n, side, k); }
525
529
  const SHAPE_NAMES = ['box', 'person', 'browser', 'mobile', 'cylinder', 'pipe', 'document', 'hexagon', 'robot', 'terminal', 'window', 'folder', 'node']; // all 13
526
- // a card's shape in a notation: its own `shape` first; box draws every card as before (a hook stays a hexagon there too)
527
- const shapeOf = (n, notation) => { const sh = String(n.shape ?? '').toLowerCase(); if (SHAPE_NAMES.includes(sh)) return sh;
528
- const T = hasKey(NOTED, notation) && NOTED[notation], k = String(n.kind ?? '').toLowerCase(); return T ? (hasKey(T, k) && T[k]) || (hasKey(T, n.k) && T[n.k]) || 'box' : 'box'; };
529
- const noteOf = x => NOTATIONS.includes(x) ? x : null;
530
- // a card as its notation draws it (null for a box): its shape, and where its head, folder tab and title bar go, clear of its
530
+ // a card's shape: its own `shape` first, then (a map's card: byKind) the one its kind draws, else a box
531
+ const shapeOf = (n, byKind = true) => { const sh = String(n.shape ?? '').toLowerCase(); if (SHAPE_NAMES.includes(sh)) return sh;
532
+ const k = String(n.kind ?? '').toLowerCase(); return byKind && ((hasKey(NOTED, k) && NOTED[k]) || (hasKey(NOTED, n.k) && NOTED[n.k])) || 'box'; };
533
+ // a card as it is drawn (null for a box): its shape, and where its head, folder tab and title bar go, clear of its
531
534
  // tabs, ⤢ and change badge. Drawing only: layout never sees it. ports: the card's highest line ends on its sides (sidePorts)
532
- function cardLook(n, notation, hint, ports) {
533
- const sh = shapeOf(n, notation); if (sh === 'box') return null;
535
+ function cardLook(n, byKind, hint, ports) {
536
+ const sh = shapeOf(n, byKind); if (sh === 'box') return null;
534
537
  const S = SHAPE[sh], L = { ...n, sh, S, portTop: ports ? Math.min(ports.L, ports.R) : Infinity, portL: ports ? ports.L : Infinity }, tabs = mapTabs(n), spans = []; let tx = n.x + 12;
535
538
  // a step keeps 16 px above the highest line into the left side (svgOf): where that is above a person's or robot's shoulder,
536
539
  // the shoulder comes up under it, 5 px below its middle (it floated 4 px above the shoulder)
@@ -623,23 +626,23 @@ function svgOf(geo, opts = {}) {
623
626
  const marker = (n, c, k = 1) => `<marker id="${id}-${n}" markerWidth="${10 * k}" markerHeight="${8 * k}" refX="${9 * k}" refY="${4 * k}" orient="auto-start-reverse" markerUnits="userSpaceOnUse"><path d="M0 0 L${10 * k} ${4 * k} L0 ${8 * k} Z" fill="${c}"/></marker>`;
624
627
  const MC = { d: LINE, g: 'var(--um-green)', r: 'var(--um-rose)', v: 'var(--um-violet)', h: 'var(--um-cyan)', ...Object.fromEntries(WIRES.map(w => [w[0], `var(--um-w-${w[0]})`])) };
625
628
  const heavy = [...new Set(edges.filter(e => e.weight > 1).map(e => lstyle(e).m + '-w' + e.weight))]; // a heavier line's arrowhead grows with it
626
- // the notation (0.15.0): each card's look, and each line as drawn, its ends on the cards' outlines (a map's cards only)
627
- const notation = map ? noteOf(opts.shapes) || 'box' : 'box', kidsOf = n => n.kids != null ? n.kids : opts.childCount ? opts.childCount(n.id) : 0;
629
+ // each card's look (0.15.0), and each line as drawn, its ends on the cards' outlines (a map's cards only)
630
+ const kidsOf = n => n.kids != null ? n.kids : opts.childCount ? opts.childCount(n.id) : 0;
628
631
  // another view's node is drawn in its own shape, when it names one (1.5): its lines end where its ports() put them, on
629
632
  // its outline already, so they are drawn as given
630
- const ports = map ? sidePorts(geo) : null, looks = new Map(map ? nodes.map(n => [n, cardLook(n, notation, !!(kidsOf(n) || n.opens) && opts.openHint !== false, ports.get(n))])
631
- : nodes.map(n => [n, n.shape != null && cardLook(n, 'box', false, null)]).filter(x => x[1]));
633
+ const ports = map ? sidePorts(geo) : null, looks = new Map(map ? nodes.map(n => [n, cardLook(n, true, !!(kidsOf(n) || n.opens) && opts.openHint !== false, ports.get(n))])
634
+ : nodes.map(n => [n, n.shape != null && cardLook(n, false, false, null)]).filter(x => x[1]));
632
635
  const drawn = edges.map(e => map && looks.size ? drawnPts(e, n => looks.get(n) || null) : e.pts);
633
636
  const T = !map && TYPED.lines ? TYPED.lines({ id, edges, drawn, lstyle, MC }) : null; // a typed view's line ends and end labels (src/typed/draw.js)
634
637
  let s = `<svg viewBox="${B.x} ${B.y} ${B.w} ${B.h}" data-w="${B.w}" data-h="${B.h}" role="group" aria-label="${esc(opts.title || 'Diagram')}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
635
638
  <defs>${marker('d', LINE)}${marker('g', 'var(--um-green)')}${marker('r', 'var(--um-rose)')}${marker('v', 'var(--um-violet)')}${marker('h', 'var(--um-cyan)')}${map ? WIRES.map(w => marker(w[0], `var(--um-w-${w[0]})`)).join('') : ''}${heavy.map(m => { const [c, k] = m.split('-w'); return marker(m, MC[c], 1 + (k - 1) * .35); }).join('')}${T ? T.defs : ''}</defs>`;
636
639
  const families = geo.families || [], familyDepth = f => f.parent ? 1 + familyDepth(families.find(p => p.id === f.parent) || {}) : 0;
637
- families.slice().sort((a,b) => familyDepth(a)-familyDepth(b)).forEach(f => { // in c4 every box is a boundary, dashed as Structurizr draws one (a toned box is dashed in any notation)
640
+ families.slice().sort((a,b) => familyDepth(a)-familyDepth(b)).forEach(f => {
638
641
  // a typed view's box whose shape draws a frame (notes/stage-2.md §5): the shape's markup, in the group the canvas selects. It keeps
639
642
  // .family-title (the audit, the canvas's "Box …, selected") and draws its border with class family-box: the canvas outlines a
640
643
  // selected box and the box under a drag by .family-box alone (src/canvas/styles.js)
641
644
  if (!map && hasKey(SHAPE, f.shape) && SHAPE[f.shape].frame) { s += `<g class="family" data-family="${esc(f.id)}"${f.parent ? ` data-parent="${esc(f.parent)}"` : ''}${f.aria ? ` role="img" aria-label="${esc(f.aria)}"` : ''}>${SHAPE[f.shape].frame(f)}</g>`; return; }
642
- s += `<g class="family${f.tone ? ' toned' : ''}" data-family="${esc(f.id)}"${f.parent ? ` data-parent="${esc(f.parent)}"` : ''}${f.tone ? ` style="--um-tone:${toneOf(f)}"` : ''}><rect class="family-box" x="${f.x}" y="${f.y}" width="${f.w}" height="${f.h}" rx="14" stroke="var(--um-${familyDepth(f) ? 'cyan' : 'border-2'})"${notation === 'c4' ? ' stroke-dasharray="7 5"' : ''}/>
645
+ s += `<g class="family${f.tone ? ' toned' : ''}" data-family="${esc(f.id)}"${f.parent ? ` data-parent="${esc(f.parent)}"` : ''}${f.tone ? ` style="--um-tone:${toneOf(f)}"` : ''}><rect class="family-box" x="${f.x}" y="${f.y}" width="${f.w}" height="${f.h}" rx="14" stroke="var(--um-${familyDepth(f) ? 'cyan' : 'border-2'})"/>
643
646
  <text class="family-title" x="${f.x+12}" y="${f.y+18}" fill="var(--um-muted)" data-t="ft" data-o="${esc(f.id)}">${esc(!map && f.title || f.label || f.id)}</text>${(f.tabs || []).map(t => t.fold ? `
644
647
  <g class="fold-hint"><title>Fold back into one card (E, or Alt + click a card in it)</title><rect x="${t.x}" y="${f.y - 8}" width="${t.w}" height="16" rx="8" class="n-mask" stroke="var(--um-accent)"/><text x="${t.x + t.w / 2}" y="${f.y + 4}" text-anchor="middle" class="badge-tx" fill="var(--um-accent)">⤡</text></g>` : `
645
648
  <g class="pub-tab" data-badge="${t.badge}"><rect x="${t.x}" y="${f.y - 8}" width="${t.w}" height="15" rx="4" fill="${t.fill}"/><text x="${t.x + t.w / 2}" y="${f.y + 3}" text-anchor="middle" class="tab-tx">${esc(t.text)}</text></g>`).join('')}</g>`;
@@ -700,8 +703,8 @@ function svgOf(geo, opts = {}) {
700
703
  if (!map && hasKey(SHAPE, n.shape) && SHAPE[n.shape].draw) { s += SHAPE[n.shape].draw(n, col(n.k), fill(n.k)); return; } // a typed view's own shape: a class box (1.5), a note (1.9)
701
704
  const c = col(n.k), f = fill(n.k), removed = n.change === 'removed', planned = n.change === 'planned';
702
705
  const L = looks.get(n) || null, S = L && L.S; // a shaped card (0.15.0); null: a box, drawn as before
703
- const pub = map && n.k === 'm-public', priv = map && n.k === 'm-script', ext = map && n.k === 'm-ext', verb = map && n.k === 'm-verb', hook = map && n.k === 'm-hook' && String(n.shape ?? '').toLowerCase() !== 'box';
704
- const dash = removed || ext || planned || (map && notation !== 'box' && n.k === 'external'); // an external card is dashed where a notation draws types (the legend says so in every notation)
706
+ const pub = map && n.k === 'm-public', priv = map && n.k === 'm-script', ext = map && n.k === 'm-ext', verb = map && n.k === 'm-verb';
707
+ const dash = removed || ext || planned || (map && n.k === 'external'); // an external card is dashed (the legend says so)
705
708
  const rx = verb ? n.h / 2 : map ? 10 : 9;
706
709
  const kids = kidsOf(n), stacked = !!(kids || n.opens); // opens a view, loaded or not (#47)
707
710
  const subject = opts.subject === n.id;
@@ -738,13 +741,11 @@ function svgOf(geo, opts = {}) {
738
741
  else {
739
742
  if (stacked) parts.push(`<rect x="${n.x + 8}" y="${n.y + 8}" width="${n.w}" height="${n.h}" rx="${rx}" class="n-stack" stroke="${c}"/><rect x="${n.x + 4}" y="${n.y + 4}" width="${n.w}" height="${n.h}" rx="${rx}" class="n-stack" stroke="${c}"/>`);
740
743
  if (pub) parts.push(`<rect x="${n.x - 4}" y="${n.y - 4}" width="${n.w + 8}" height="${n.h + 8}" rx="14" class="n-ring" stroke="${c}"/>`);
741
- if (hook) { const hx = Math.min(16, n.h / 3), pts = `${n.x + hx},${n.y} ${n.x + n.w - hx},${n.y} ${n.x + n.w},${n.y + n.h / 2} ${n.x + n.w - hx},${n.y + n.h} ${n.x + hx},${n.y + n.h} ${n.x},${n.y + n.h / 2}`;
742
- parts.push(`<polygon points="${pts}" class="n-mask"/><polygon points="${pts}" class="n-body" fill="${f}" stroke="${c}" stroke-linejoin="round"/>`); }
743
- else parts.push(`<rect x="${n.x}" y="${n.y}" width="${n.w}" height="${n.h}" rx="${rx}" class="n-mask"/>
744
+ parts.push(`<rect x="${n.x}" y="${n.y}" width="${n.w}" height="${n.h}" rx="${rx}" class="n-mask"/>
744
745
  <rect x="${n.x}" y="${n.y}" width="${n.w}" height="${n.h}" rx="${rx}" class="n-body" fill="${f}" stroke="${c}" ${dash ? 'stroke-dasharray="6 4"' : ''} ${removed ? 'opacity=".6"' : ''}/>`); }
745
- // the mark, centred on the face; in the uml notation a box's own sigil gives way to UML's component icon
746
- const glyph = L && SIGIL[L.sh] ? L.sh : notation === 'uml' && !L && !String(n.k).startsWith('m-') ? 'component' : n.k; // a person's, robot's, phone's, terminal's, folder's or node's own mark
747
- parts.push((() => { const I = (opts.icons || {})[n.icon], x = map ? n.x + 14 : n.x + (verb ? 14 : hook ? 17 : 9), y = map ? fy - 14 : n.y + (priv ? 8 : 11), M = map ? 28 : 16; // map: one 28 px mark, left, centred
746
+ // the mark, centred on the face
747
+ const glyph = L && SIGIL[L.sh] ? L.sh : n.k; // a person's, robot's, phone's, terminal's, folder's or node's own mark
748
+ parts.push((() => { const I = (opts.icons || {})[n.icon], x = map ? n.x + 14 : n.x + (verb ? 14 : 9), y = map ? fy - 14 : n.y + (priv ? 8 : 11), M = map ? 28 : 16; // map: one 28 px mark, left, centred
748
749
  return I && I.src ? `<rect x="${x - M * 3 / 28}" y="${y - M * 3 / 28}" width="${M * 34 / 28}" height="${M * 34 / 28}" rx="${M / 4}" class="n-logo${I.dark ? ' dark' : ''}"/><image href="${I.src}" x="${x}" y="${y}" width="${M}" height="${M}"/>`
749
750
  : I && I.g ? `<g transform="translate(${x} ${y}) scale(${M / 24})" class="n-sigil" fill="none" stroke="${c}" stroke-width="${1.75 * 24 / M}" stroke-linecap="round" stroke-linejoin="round">${I.g}</g>`
750
751
  : `<g transform="translate(${x} ${y}) scale(${M / 16})" class="n-sigil" fill="none" stroke="${c}" stroke-width="${1.75 * 16 / M}" stroke-linecap="round" stroke-linejoin="round">${SIGIL[glyph]}</g>`; })());
@@ -839,7 +840,7 @@ const hit = (a, b, pad = 0) => a.x < b.x + b.w + pad && b.x < a.x + a.w + pad &&
839
840
  // a line whose route layoutPass draws itself in a typed view: 'loop' (a node to itself) or 'fan'. A symbol, so no field of a
840
841
  // model's line can set it
841
842
  const OWN_ROUTE = Symbol('own route');
842
- const labelW = e => textW(fit((e.step != null ? e.step + ' · ' : '') + (e.label || ''), 10, 150), 10) + 12;
843
+ const labelW = e => textW(fit((e.step != null ? e.step + ' · ' : '') + (e.label || ''), 10, e.cut || 150), 10) + 12;
843
844
 
844
845
  /* Layered auto-layout for views that carry no col/row: longest-path layers (cycles broken by DFS),
845
846
  barycentre ordering sweeps, integer rows so row gaps stay free as routing channels. */
@@ -1047,7 +1048,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
1047
1048
  const p = 12, x = Math.min(...rects.map(r => r.x)) - p - (TB ? 24 : 0), y = Math.min(...rects.map(r => r.y)) - p - (TB ? 0 : 24);
1048
1049
  const b = {...f, x, y, w:Math.max(...rects.map(r => r.x+r.w)) + p - x, h:Math.max(...rects.map(r => r.y+r.h)) + p - y};
1049
1050
  if (!map) b[TB ? 'h' : 'w'] = Math.max(b[TB ? 'h' : 'w'], headW(f) + 16);
1050
- return cache[f.id] = b;
1051
+ return cache[f.id] = dg.typed ? TYPED.frame(b) : b;
1051
1052
  };
1052
1053
  const out = familyDefs.map(f => frameOf(f)).filter(Boolean);
1053
1054
  if (map) out.forEach(f => { // the box fits its title and its badges, which are tabs on its top border, at the right
@@ -1114,13 +1115,13 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
1114
1115
  bend penalty. Earlier routes may never be overlapped and are crossed only when no clean
1115
1116
  route exists; earlier labels are obstacles, so later routes keep clear of them. */
1116
1117
  // an end may be a container: the line ends on its box's border, whose title the ports keep clear of (not a box and
1117
- // something inside it: that has no line)
1118
+ // something inside it: that has no line, but a typed view's inner line)
1118
1119
  const headOf = f => Math.min((TB ? f.h : f.w) - 16, headW(f)); // the length of a box's title along its header
1119
1120
  const boxEnd = Object.fromEntries(families.map(f => [f.id, { id: f.id, label: f.label || f.id, box: true, x: f.x, y: f.y, w: f.w, h: f.h, hd: TB ? headOf(f) : 0,
1120
1121
  decoT: TB ? [] : [[f.x + 8, headOf(f)], ...(f.tabs || []).map(t => [t.x - 6, t.w + 12])], decoB: [] }]));
1121
1122
  const endOf = Object.assign(Object.create(null), boxEnd, byId), end = id => endOf[id], holds = (b, n) => b.box && n !== b && n.x >= b.x && n.y >= b.y && n.x + n.w <= b.x + b.w && n.y + n.h <= b.y + b.h;
1122
1123
  const edges = (dg.edges || []).filter(e => { const s = end(e.from), t = end(e.to); if (!s || !t || (e.from === e.to && !loops[e.from])) return false;
1123
- if (holds(s, t) || holds(t, s)) { warn(`no line for edge ${e.id ?? e.from + '->' + e.to}: it joins the box "${(holds(s, t) ? s : t).id}" and something inside it`); return false; }
1124
+ if (!(LP && e[e.inner + 'Port'] != null) && (holds(s, t) || holds(t, s))) { warn(`no line for edge ${e.id ?? e.from + '->' + e.to}: it joins the box "${(holds(s, t) ? s : t).id}" and something inside it`); return false; }
1124
1125
  return true; }).map(e => ({ ...e, s: end(e.from), t: end(e.to) }));
1125
1126
  const segsOf = pts => pts.slice(0, -1).map((a, i) => [a, pts[i + 1]]);
1126
1127
  const segHitsRect = (a, b, r) => { const x1 = Math.min(a[0], b[0]), x2 = Math.max(a[0], b[0]), y1 = Math.min(a[1], b[1]), y2 = Math.max(a[1], b[1]); return x2 > r.x && x1 < r.x + r.w && y2 > r.y && y1 < r.y + r.h; };
@@ -1178,7 +1179,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
1178
1179
  });
1179
1180
  free.forEach(e => { pushSide(e.s, e.fs, e, e.t); pushSide(e.t, e.ts, e, e.s); });
1180
1181
  edges.filter(e => e.under > 0).forEach(e => pushSide(e.t, 'R', e, e.s)); // a loop under the boxes takes its port with the side's other lines
1181
- if (LP) LP.ports(pushSide);
1182
+ if (LP) LP.ports(pushSide, sides);
1182
1183
  const portAt = (it, side, p) => { if (it.n === it.e.s && it.e.fs === side) it.e.p1 = p; else it.e.p2 = p; };
1183
1184
  Object.entries(sides).sort(([, a], [, b]) => !!a[0].n.box - !!b[0].n.box).forEach(([key, list]) => { // cards first: a box's port lines up with them
1184
1185
  const side = key.split('|').pop(), vertical = side === 'L' || side === 'R';
@@ -1424,7 +1425,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
1424
1425
  const put = (e, s, full) => { placed.push(s.box); reserved.push(s.grow); e.lbl = { ...s.lbl, short: s.lbl.text !== full }; };
1425
1426
  const unput = e => { const L = e.lbl, same = m => r => r.x === L.x - m && r.y === L.y - m && r.w === L.w + 2 * m && r.h === L.h + 2 * m;
1426
1427
  const p = placed.findIndex(same(0)), q = reserved.findIndex(same(3)); if (p >= 0) placed.splice(p, 1); if (q >= 0) reserved.splice(q, 1); e.lbl = null; };
1427
- const fullOf = e => map ? fit(e.label || '', 10.5, e.under ? 320 : 440) : fit((e.step != null ? e.step + ' · ' : '') + (e.label || ''), 10, 150);
1428
+ const fullOf = e => map ? fit(e.label || '', 10.5, e.under ? 320 : 440) : fit((e.step != null ? e.step + ' · ' : '') + (e.label || ''), 10, e.cut || 150);
1428
1429
  function placeLabel(e) {
1429
1430
  if (!e.label && e.step == null) return;
1430
1431
  const full = fullOf(e);
@@ -1563,7 +1564,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
1563
1564
  if ((map || dg.typed) && dg.repair !== false) free.forEach(e => {
1564
1565
  const P = e.pts, ax = e.fs === 'L' || e.fs === 'R' ? 1 : 0, d = P[P.length - 1][ax] - P[0][ax];
1565
1566
  if (P.length !== 4 || !d || Math.abs(d) > 16 || OUT[e.fs][1 - ax] !== -OUT[e.ts][1 - ax]) return;
1566
- const lone = (n, side) => !n.box && sides[n.id + '|' + side].length === 1, [k, n, side, v] = lone(e.t, e.ts) ? ['p2', e.t, e.ts, P[0][ax]] : lone(e.s, e.fs) ? ['p1', e.s, e.fs, P[3][ax]] : [];
1567
+ const lone = (n, side) => !n.box && sides[n.id + '|' + side]?.length === 1, [k, n, side, v] = lone(e.t, e.ts) ? ['p2', e.t, e.ts, P[0][ax]] : lone(e.s, e.fs) ? ['p1', e.s, e.fs, P[3][ax]] : [];
1567
1568
  const sp = n && (ax ? [n.y + n.h * .08, n.y + n.h * .92] : sideSpan(n, side) || [n.x + n.w * .08, n.x + n.w * .92]);
1568
1569
  if (!k || v < sp[0] || v > sp[1]) return;
1569
1570
  const was = e[k], before = issueCount(new Set([e])), snap = [e.pts, e.lbl, e.how], snapR = [routed.slice(), reserved.slice(), placed.slice()];
@@ -1581,11 +1582,11 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
1581
1582
  // the label's width, else the row gap by its height and 14 px; a label in neither, or with no spot, the gap its line's longest
1582
1583
  // run crosses, a column's first. layoutGraph lays the view out again with it, and keeps that layout if it is better
1583
1584
  let labelRoom = null;
1584
- const tight = !last && !dg.quick && map && !dg.abs ? free.filter(e => (e.label || e.step != null) && (!e.lbl || e.lbl.short || families.some(f => onBorder(e.lbl, f)))) : [];
1585
+ const tight = !last && !dg.quick && (map || dg.labelGaps) && !dg.abs ? free.filter(e => (e.label || e.step != null) && (!e.lbl || e.lbl.short || families.some(f => onBorder(e.lbl, f)))) : [];
1585
1586
  if (tight.length) {
1586
1587
  const cs = Object.keys(colX).map(Number), rs = nodes.map(n => n.row), r0 = Math.floor(Math.min(...rs)), r1 = Math.ceil(Math.max(...rs)), need = { x: {}, y: {} };
1587
1588
  const gaps = [...cs.filter(c => colX[c + 1] != null).map(c => ['x', c, colX[c] + (colW[c] || NW), colX[c + 1]]),
1588
- ...Array.from({ length: Math.max(0, r1 - r0) }, (_, k) => ['y', r0 + k, rowY(r0 + k) + NH, rowY(r0 + k + 1)])];
1589
+ ...Array.from({ length: map && r1 - r0 }, (_, k) => ['y', r0 + k, rowY(r0 + k) + NH, rowY(r0 + k + 1)])];
1589
1590
  tight.forEach(e => { const { w, hgt: h } = sizeOf(e, fullOf(e)), L = e.lbl, c = L && { x: L.x + L.w / 2, y: L.y + L.h / 2 };
1590
1591
  let best = c && ['x', 'y'].map(k => gaps.find(([gk, g, p, q]) => gk === k && c[k] >= p && c[k] <= q)).find(Boolean);
1591
1592
  if (!best) segsOf(e.pts).forEach(([a, b]) => { const k = a[1] === b[1] ? 'x' : 'y', i = k === 'x' ? 0 : 1, lo = Math.min(a[i], b[i]), hi = Math.max(a[i], b[i]);
@@ -1700,7 +1701,9 @@ function routeAudit(geo, where, only, refs) { // only: a set of edges; counts ju
1700
1701
  const box = n => ({ x: n.x, y: n.y, w: n.w, h: n.h });
1701
1702
  function overlapIssues(geo, where, refs) {
1702
1703
  const issues = [], nodes = geo.nodes, say = teller(issues, refs);
1703
- for (let i = 0; i < nodes.length; i++) for (let j = i + 1; j < nodes.length; j++) if (hit(box(nodes[i]), box(nodes[j]), -1)) say(`${where}: nodes "${nodes[i].label}" and "${nodes[j].label}" overlap`, nodes[i], nodes[j]);
1704
+ const inside = (a, b) => a.x >= b.x - 1 && a.y >= b.y - 1 && a.x + a.w <= b.x + b.w + 1 && a.y + a.h <= b.y + b.h + 1;
1705
+ const holds = (a, b) => (a.holder && inside(b, a)) || (b.holder && inside(a, b)); // a holder (a sequence's frame) may hold what it meets, not cross it
1706
+ for (let i = 0; i < nodes.length; i++) for (let j = i + 1; j < nodes.length; j++) if (hit(box(nodes[i]), box(nodes[j]), -1) && !holds(nodes[i], nodes[j])) say(`${where}: nodes "${nodes[i].label}" and "${nodes[j].label}" overlap`, nodes[i], nodes[j]);
1704
1707
  return issues;
1705
1708
  }
1706
1709
  function leaveIssues(geo, where, refs) {
@@ -1723,7 +1726,7 @@ function leaveIssues(geo, where, refs) {
1723
1726
  function auditSvg(svg, geo, where) {
1724
1727
  const issues = overlapIssues(geo, where), R = box;
1725
1728
  const T = [...svg.querySelectorAll('text[data-t]')].map(t => { let b; try { b = t.getBBox(); } catch (_) { return null; } return { t: t.dataset.t, o: t.dataset.o, txt: t.textContent, b: { x: b.x, y: b.y, w: b.width, h: b.height } }; }).filter(x => x && x.b.w);
1726
- const nodes = geo.nodes, faces = Object.fromEntries([...svg.querySelectorAll('.node[data-face]')].map(g => [g.dataset.id, g.dataset.face.split(' ').map(Number)])); // a shape's face (0.15.0)
1729
+ const nodes = geo.nodes.filter(n => !n.holder), faces = Object.fromEntries([...svg.querySelectorAll('.node[data-face]')].map(g => [g.dataset.id, g.dataset.face.split(' ').map(Number)])); // a shape's face (0.15.0)
1727
1730
  T.forEach(a => {
1728
1731
  if (a.t === 'lt' || a.t === 'ft') {
1729
1732
  nodes.forEach(n => { if (hit(a.b, R(n), -1)) issues.push(`${where}: lane "${a.txt}": header covers node "${n.label}"`); });
@@ -1770,7 +1773,7 @@ const ALIAS = { client: 'frontend', ui: 'frontend', web: 'frontend', page: 'fron
1770
1773
  user: 'external', human: 'external', thirdparty: 'external', vendor: 'external',
1771
1774
  doc: 'file', config: 'file', skill: 'file', md: 'file', template: 'file', output: 'file',
1772
1775
  agent: 'ai', llm: 'ai', model: 'ai' };
1773
- const hasKey = (o, k) => Object.hasOwn(o, k); // a table's own key: "constructor" or "__proto__" is no kind, shape or notation
1776
+ const hasKey = (o, k) => Object.hasOwn(o, k); // a table's own key: "constructor" or "__proto__" is no kind or shape
1774
1777
  const kindOf = (k, map) => { k = String(k || '').toLowerCase(); if (map && hasKey(MAPK, k)) return MAPK[k]; return hasKey(KIND, k) ? k : hasKey(ALIAS, k) ? ALIAS[k] : map ? 'm-script' : 'backend'; };
1775
1778
  const SIGIL = {
1776
1779
  frontend: '<rect x="2" y="3" width="12" height="8.5" rx="1.2"/><path d="M5.5 14h5M8 11.5V14"/>',
@@ -1797,25 +1800,17 @@ const SIGIL = {
1797
1800
  terminal: '<rect x="1.8" y="2.6" width="12.4" height="10.8" rx="1.6"/><path d="m4.6 6.2 2 1.8-2 1.8M8.4 10.2h3"/>',
1798
1801
  folder: '<path d="M2 4.3c0-.7.5-1.2 1.2-1.2h3l1.4 1.6h5.2c.7 0 1.2.5 1.2 1.2v6.2c0 .7-.5 1.2-1.2 1.2H3.2C2.5 13.3 2 12.8 2 12.1Z"/>',
1799
1802
  node: '<path d="M2.4 5.6 5 3h8.6v7.4L11 13H2.4Z"/><path d="M2.4 5.6H11V13M11 5.6 13.6 3"/>',
1800
- // UML's component icon (§11.6.4): a box's mark in the uml notation
1801
- component: '<path d="M4.5 4.5V2.3a.8.8 0 0 1 .8-.8h7.9a.8.8 0 0 1 .8.8v11.4a.8.8 0 0 1-.8.8H5.3a.8.8 0 0 1-.8-.8v-2.2M4.5 7v2"/><rect x="2" y="4.5" width="5" height="2.5" rx=".6"/><rect x="2" y="9" width="5" height="2.5" rx=".6"/>',
1802
- };
1803
- /* ================= card shapes (0.15.0) =================
1804
- A notation draws each card as a shape: box (the default: every card a rounded box, as before), c4 (people, browser apps,
1805
- phones, databases, queues and files as the C4 model's tools draw them) or uml (UML 2.5's actor, node, artifact, package
1806
- and data store, and the component icon as a box's mark). A card's own `shape` wins in any notation. Each shape is drawn
1807
- inside its card's own box, so layout, routing, widths and the layout check never see it: an outline (for the mask, the
1808
- body, the stack, rings, the minimap and the glow), a face (the rows its mark and text use), details drawn over the body
1809
- (one of them its accent, which lights as something arrives) and inset(side, t), how far inside the box's side the
1810
- outline runs at t, which slides a line's end from the box onto the outline. */
1811
- const NOTATIONS = ['box', 'c4', 'uml'];
1812
- // what a notation draws: by the kind word the model wrote, then by the kind it means (docs/module-api.md, 0.15.0)
1813
- const NOTED = {
1814
- c4: { person: 'person', user: 'person', human: 'person', mobile: 'mobile', agent: 'robot', cli: 'terminal',
1815
- frontend: 'browser', database: 'cylinder', queue: 'pipe', file: 'document', 'm-hook': 'hexagon' },
1816
- uml: { person: 'person', user: 'person', human: 'person', agent: 'robot', module: 'folder', package: 'folder',
1817
- cloud: 'node', database: 'cylinder', file: 'document', 'm-hook': 'hexagon' },
1818
1803
  };
1804
+ /* ================= card shapes (0.15.0; one style since 0.21.0) =================
1805
+ Maps draw each card as a shape (the union of what the C4 model's tools and UML 2.5 draw): people, browser apps, phones,
1806
+ terminals, queues, packages, hosts, robots, data stores, files and hooks, and a rounded box where no shape says more. A
1807
+ card's own `shape` wins. Each shape is drawn inside its card's own box, so layout, routing, widths and the layout check
1808
+ never see it: an outline (for the mask, the body, the stack, rings, the minimap and the glow), a face (the rows its mark
1809
+ and text use), details drawn over the body (one of them its accent, which lights as something arrives) and
1810
+ inset(side, t), how far inside the box's side the outline runs at t, which slides a line's end from the box onto the outline. */
1811
+ // the shape a card is drawn as: by the kind word the model wrote, then by the kind it means (docs/module-api.md)
1812
+ const NOTED = { person: 'person', user: 'person', human: 'person', agent: 'robot', mobile: 'mobile', cli: 'terminal', frontend: 'browser',
1813
+ queue: 'pipe', module: 'folder', package: 'folder', cloud: 'node', database: 'cylinder', file: 'document', 'm-hook': 'hexagon' };
1819
1814
  /* The map: today's notation, and the default diagram type. A view of a version-1 model, or a version-2 view with no
1820
1815
  `diagram`, is a map and takes exactly today's code path. */
1821
1816
  const MAP = registerDiagramType('map', { title: 'architecture map' });
@@ -2172,16 +2167,9 @@ function mapContext(model, opts = {}) {
2172
2167
  return pos;
2173
2168
  }
2174
2169
 
2175
- function buildView(vid, opts = {}) {
2176
- const g = buildGeo(vid);
2177
- return { g, ...drawView(vid, g, opts) };
2178
- }
2179
- // a view's notation (0.15.0): its own `shapes`, else the model's, else box; drawView: the picture of a geometry in one,
2180
- // with no layout (a change of notation draws the cards again and nothing else)
2181
- const notationOf = vid => noteOf((V[vid] || {}).shapes) || noteOf(D.shapes) || 'box';
2182
- function drawView(vid, g, opts = {}) {
2183
- const v = V[vid], shapes = noteOf(opts.shapes) || notationOf(vid), T = ownLayout(vid), L = T && T.draw && T.draw(g, { esc, r2, textW }); // its two layers only
2184
- return { markup: svgOf(g, { title: v.title, subject: subjectOf(v), icons: D.icons, broken: brokenIn(vid), ...opts, shapes, ...(L && { under: L.under, over: L.over }) }), shapes };
2170
+ function buildView(vid, opts = {}) { // a view's geometry and its picture
2171
+ const g = buildGeo(vid), v = V[vid], T = ownLayout(vid), L = T && T.draw && T.draw(g, { esc, r2, textW }); // its two layers only
2172
+ return { g, markup: svgOf(g, { title: v.title, subject: subjectOf(v), icons: D.icons, broken: brokenIn(vid), ...opts, ...(L && { under: L.under, over: L.over }) }) };
2185
2173
  }
2186
2174
  // the rules a view's lines break (#70), by model edge id: svgOf draws such a line rose, with a mark that names them
2187
2175
  let ruled = null;
@@ -2348,7 +2336,7 @@ function mapContext(model, opts = {}) {
2348
2336
  const viewKey = vid => {
2349
2337
  const v = V[vid], ids = (v.nodeIds || []).filter(i => N[i]), ns = ids.map(i => nodeIn(i, vid));
2350
2338
  const icons = [...new Set(ns.map(n => n.icon).filter(Boolean))].map(k => (D.icons || {})[k]), who = [...new Set(ns.map(n => n.author).filter(Boolean))].map(k => (D.authors || {})[k]);
2351
- return JSON.stringify([{ ...v, shapes: undefined }, ns, viewEdges(v), ids.map(i => [kidCount(i, vid), opensOf(i, vid)]), icons, who, boxesOf(vid)]); // a view's notation lays nothing out; a type's boxes do
2339
+ return JSON.stringify([v, ns, viewEdges(v), ids.map(i => [kidCount(i, vid), opensOf(i, vid)]), icons, who, boxesOf(vid)]); // a type's boxes lay it out
2352
2340
  };
2353
2341
  /* ---------- unfolding in place (#71): a stacked card opens into a box on the view it is on ----------
2354
2342
  The card becomes a box with its id and name, holding its inner view's own cards: those not on this view already (the
@@ -2359,11 +2347,11 @@ function mapContext(model, opts = {}) {
2359
2347
  the cards under it in its column move down by its height, so the rest of the picture keeps its places (a hub view, which
2360
2348
  has no columns, is laid out again). The inner view's boxes that hold its own cards come along inside, and a numbered
2361
2349
  (sequence) view's run order: its numbers, its loops back and its lanes (#82). Not in a numbered view: its own steps
2362
- would be numbered again; that card opens. */
2350
+ would be numbered again; that card opens. Nor into another kind of diagram: that card opens too. */
2363
2351
  // the inner view's own cards: those this view shows neither as cards nor as boxes (a card unfolded here already is one, #98)
2364
2352
  const ownOf = (v, C, nid) => (C.nodeIds || []).filter(i => N[i] && i !== nid && !(v.nodeIds || []).includes(i) && !(v.containers || []).some(b => b && b.id === i));
2365
2353
  const unfoldable = (nid, vid) => { const v = V[vid], ids = (v && v.nodeIds) || [], c = ids.includes(nid) && N[nid] && v.render !== 'sequence' ? childOf(nid, vid) : null;
2366
- if (!c || c === vid || chain(vid).includes(c) || (v.containers || []).some(b => b && b.id === nid)) return null;
2354
+ if (!c || c === vid || chain(vid).includes(c) || (v.containers || []).some(b => b && b.id === nid) || V[c].diagram != v.diagram) return null;
2367
2355
  return ownOf(v, V[c], nid).length ? c : null; };
2368
2356
  function unfold(vid, nid) {
2369
2357
  const v = V[vid], c = unfoldable(nid, vid); if (!c) return false;
@@ -2470,7 +2458,7 @@ function mapContext(model, opts = {}) {
2470
2458
  for (let more = true; more && todo.length;) { more = false; todo = todo.filter(nid => { if (!unfold(vid, nid)) return true; (unfolded[vid] ||= []).push(nid); return !(more = true); }); } }))();
2471
2459
  const geoCache = {};
2472
2460
  return { D, views, nodes, N, V, rootId, nodeIn, childOf, opensOf, subjectOf, viewsWith, kidsOf, chain, viewEdges, isState, planOf: withM(planOf), planCache,
2473
- layoutCache, geoCache, memos, buildGeo: withM(buildGeo), buildView: withM(buildView), drawView: withM(drawView), notationOf, optimize: withM(optimize), flowPos, kidCount, viewKey, brokenIn, unfoldable, unfolded };
2461
+ layoutCache, geoCache, memos, buildGeo: withM(buildGeo), buildView: withM(buildView), optimize: withM(optimize), flowPos, kidCount, viewKey, brokenIn, unfoldable, unfolded };
2474
2462
  }
2475
2463
  const edgeRefIds = e => [...new Set((e.all || [e]).map(x => x._id).filter(Boolean))];
2476
2464
 
@@ -2516,20 +2504,21 @@ export function validate(model) {
2516
2504
  if (n.owner != null && typeof n.owner !== 'string') warn(at + '.owner', 'an owner is a name: ignored');
2517
2505
  shapeIs(n, at);
2518
2506
  };
2519
- const shapeIs = (n, at) => { if (n && n.shape != null && !SHAPE_NAMES.includes(String(n.shape).toLowerCase())) warn(at + '.shape', `unknown shape "${n.shape}": the notation draws it by its kind (the shapes: ${SHAPE_NAMES.join(', ')})`); };
2520
- const notation = (x, at) => { if (x != null && !noteOf(x)) warn(at, `"${x}" is not a notation (${NOTATIONS.join(', ')}): drawn as box`); }; // card shapes (0.15.0)
2521
- notation(m.shapes, 'shapes');
2507
+ const shapeIs = (n, at) => { if (n && n.shape != null && !SHAPE_NAMES.includes(String(n.shape).toLowerCase())) warn(at + '.shape', `unknown shape "${n.shape}": drawn by its kind (the shapes: ${SHAPE_NAMES.join(', ')})`); };
2508
+ const gone = (x, at) => { if (x && x.shapes !== undefined) warn(at + 'shapes', 'shapes is no longer read: maps draw in one style'); }; // the box/c4/uml choice, removed in 0.21.0
2509
+ gone(m, '');
2522
2510
  arr(m.nodes).forEach((n, i) => card(n, `nodes[${i}]`));
2523
2511
  views.forEach((v, i) => arr(v.nodes).forEach((n, j) => card(n, `views[${i}].nodes[${j}]`)));
2524
2512
  views.forEach((v, i) => { if (typeof v.id !== 'string' || !v.id) err(`views[${i}].id`, 'a view needs an id'); else if (vids.has(v.id)) err(`views[${i}].id`, `view id "${v.id}" is used twice`); else vids.add(v.id); });
2525
2513
  if (m.root != null && !vids.has(m.root)) err('root', `root "${m.root}" is not a view`);
2526
2514
  const later = (x, at) => { if (x && x.opensView && !vids.has(x.opensView)) warn(at + '.opensView', `opens "${x.opensView}", a view the model does not have: fine if the host loads it when it is opened`); };
2527
2515
  arr(m.nodes).forEach((n, i) => later(n, `nodes[${i}]`));
2528
- views.forEach((v, i) => notation(v.shapes, `views[${i}].shapes`));
2529
- // c4 draws a frontend card as a browser: a person's icon on one says the model means a person (0.15.0)
2530
- const inC4 = new Set(views.filter(v => (noteOf(v.shapes) || noteOf(m.shapes)) === 'c4').flatMap(v => arr(v.nodeIds)));
2531
- arr(m.nodes).forEach((n, i) => { if (!n || !inC4.has(n.id) || n.shape != null || !['user', 'users', 'usercheck'].includes(n.icon)) return;
2532
- if (shapeOf({ ...n, k: kindOf(n.kind, true) }, 'c4') === 'browser') warn(`nodes[${i}].kind`, `"${n.label || n.id}" has a person's icon, but c4 draws a ${n.kind} card as a browser: kind "person" (or shape "person") draws a person`); });
2516
+ views.forEach((v, i) => gone(v, `views[${i}].`));
2517
+ // a frontend card is drawn as a browser: a person's icon on one says the model means a person (0.15.0); not said of a card
2518
+ // only typed views hold, which keep their own notation (0.21.0)
2519
+ const typedOnly = id => { const vs = views.filter(v => arr(v.nodeIds).includes(id)); return vs.length && vs.every(v => typedView(v) && diagramOf(v)); };
2520
+ arr(m.nodes).forEach((n, i) => { if (!n || n.shape != null || !['user', 'users', 'usercheck'].includes(n.icon) || typedOnly(n.id)) return;
2521
+ if (shapeOf({ ...n, k: kindOf(n.kind, true) }) === 'browser') warn(`nodes[${i}].kind`, `"${n.label || n.id}" has a person's icon, but a ${n.kind} card is drawn as a browser: kind "person" (or shape "person") draws a person`); });
2533
2522
  const line = (e, at, ends) => {
2534
2523
  if (!e || typeof e !== 'object') return err(at, 'a line is an object with from and to');
2535
2524
  ['from', 'to'].forEach(k => { if (!ends.has(e[k])) err(`${at}.${k}`, `"${e[k]}" is neither a card nor a box`); });
@@ -3820,7 +3809,7 @@ export function render(container, model, options = {}) {
3820
3809
  event says which of its keys changed, once a frame at most (on a 16 ms timer, so a page in a hidden tab still says it).
3821
3810
  The camera is the exception: a pan, zoom, glide or drag is one state event, once it comes to rest; until then the
3822
3811
  event's camera is where it last rested (handle.state reads it live) */
3823
- const KEYS = ['view', 'navigating', 'selection', 'expanded', 'lens', 'overlays', 'story', 'tour', 'replay', 'paused', 'camera', 'flow', 'shapes', 'motion', 'gesture', 'destroyed'];
3812
+ const KEYS = ['view', 'navigating', 'selection', 'expanded', 'lens', 'overlays', 'story', 'tour', 'replay', 'paused', 'camera', 'flow', 'motion', 'gesture', 'destroyed'];
3824
3813
  let lastState = null, stateT = 0, camRest = null, restT = 0; // the last state event's snapshot and the flush to come; the camera at rest, and its check to come
3825
3814
  const listensState = () => !!((options.on || {}).state || (subs.state || []).length);
3826
3815
  function touch() { if (!stateT && !dead && listensState()) stateT = setTimeout(flushState, 16); }
@@ -3831,18 +3820,10 @@ export function render(container, model, options = {}) {
3831
3820
  function camMoved() { clearTimeout(restT); restT = !dead && listensState() ? setTimeout(rested, 150) : 0; }
3832
3821
  function rested() { restT = 0; if (dead) return; if (cam && (cam.anim || cam.ptrs.size) || nav) return camMoved(); camRest = camNow(); touch(); }
3833
3822
  let cam = null, geo = null, svg = null, cur = null, sel = null, more = [], busy = false, paused = false, dead = false;
3834
- // the notation (0.15.0): the host's (render()'s shapes, then handle.shapes) for every view, else each view's own
3835
- if (options.shapes != null && !noteOf(options.shapes)) warn(`render(…, { shapes: ${JSON.stringify(options.shapes)} }): the notations are ${NOTATIONS.join(', ')}; each view draws its own`);
3836
- let shapesSet = noteOf(options.shapes);
3837
- const noteFor = vid => shapesSet || ctx.notationOf(vid);
3838
- const pic = vid => { // a view's picture in its notation: laid out once, drawn again only when the notation changes
3839
- const want = noteFor(vid), o = { tour: P.tour !== false, openHint: P.openHint !== false, shapes: want };
3840
- let c = ctx.geoCache[vid]; if (!c) c = ctx.geoCache[vid] = ctx.buildView(vid, o); else if (c.shapes !== want) Object.assign(c, ctx.drawView(vid, c.g, o));
3841
- return c; };
3842
- // each card's look in the notation shown, for what is drawn over the picture: rings, the minimap, a tour's or replay's dot
3843
- // (in box too: a card's own shape is drawn there)
3844
- const lookIn = (nt, g) => { const m = new Map(), ports = sidePorts(g); return n => { if (!m.has(n)) m.set(n, cardLook(n, nt, !!(n.kids || n.opens) && P.openHint !== false, ports.get(n))); return m.get(n); }; };
3845
- const looker = () => lookIn(noteFor(cur), geo);
3823
+ const pic = vid => ctx.geoCache[vid] ||= ctx.buildView(vid, { tour: P.tour !== false, openHint: P.openHint !== false }); // a view's picture: laid out once
3824
+ // each card's look, for what is drawn over the picture: rings, the minimap, a tour's or replay's dot
3825
+ const lookIn = g => { const m = new Map(), ports = sidePorts(g); return n => { if (!m.has(n)) m.set(n, g.map ? cardLook(n, true, !!(n.kids || n.opens) && P.openHint !== false, ports.get(n)) : cardLook(n, false, false, null)); return m.get(n); }; };
3826
+ const looker = () => lookIn(geo);
3846
3827
  const timers = new Set(), later = (f, ms) => { const t = setTimeout(() => { timers.delete(t); if (!dead) f(); }, ms); timers.add(t); return t; };
3847
3828
  const sleeping = new Set(); // a sleep resolves on its own, or when the map is destroyed: nothing that awaits one is left pending
3848
3829
  const sleep = ms => new Promise(r => { if (dead) return r(); const wake = () => { sleeping.delete(wake); r(); }; sleeping.add(wake); later(wake, still() ? 0 : ms); });
@@ -3946,9 +3927,9 @@ export function render(container, model, options = {}) {
3946
3927
  /* minimap */
3947
3928
  const mini = {};
3948
3929
  function buildMini() {
3949
- const b = geo.bounds, el = q('.minimap'), bw = Math.max(2, b.w / 300), c4 = noteFor(cur) === 'c4';
3950
- // a box's tone, as its region; and a box dashed in the picture (any box in c4, a toned box in any notation) dashed here
3951
- const rg = (geo.families || []).filter(f => f.tone || c4).map(f => `<rect x="${f.x}" y="${f.y}" width="${f.w}" height="${f.h}" rx="14" ${f.tone ? `style="--um-tone:${toneOf(f)}" ` : ''}fill="${f.tone ? 'var(--um-tone, var(--um-slate))' : 'none'}" fill-opacity=".14" stroke="${f.tone ? 'var(--um-tone, var(--um-slate))' : 'var(--um-border-2)'}" stroke-width="${bw}" stroke-dasharray="${bw * 4} ${bw * 3}"/>`).join('');
3930
+ const b = geo.bounds, el = q('.minimap'), bw = Math.max(2, b.w / 300);
3931
+ // a box's tone, as its region, dashed as in the picture
3932
+ const rg = (geo.families || []).filter(f => f.tone).map(f => `<rect x="${f.x}" y="${f.y}" width="${f.w}" height="${f.h}" rx="14" style="--um-tone:${toneOf(f)}" fill="var(--um-tone, var(--um-slate))" fill-opacity=".14" stroke="var(--um-tone, var(--um-slate))" stroke-width="${bw}" stroke-dasharray="${bw * 4} ${bw * 3}"/>`).join('');
3952
3933
  const ln = (geo.lanes || []).map(l => `<rect x="${l.x}" y="${l.y}" width="${l.w}" height="${l.h}" rx="14" fill="none" stroke="var(--um-border-2)" stroke-width="${Math.max(2, b.w / 300)}"/>`).join('');
3953
3934
  const look = looker(), r = geo.nodes.map(n => { const L = look(n), paint = `fill="var(--um-${KIND[n.k][0]})" opacity="${n.k === 'm-public' ? .95 : .55}"`;
3954
3935
  return L ? `<path d="${L.S.out(L)}" ${paint}/>` : `<rect x="${n.x}" y="${n.y}" width="${n.w}" height="${n.h}" rx="8" ${paint}/>`; }).join('');
@@ -4166,21 +4147,6 @@ export function render(container, model, options = {}) {
4166
4147
  return true;
4167
4148
  }
4168
4149
  function flash(nid) { const g = svg.querySelector(`.node[data-id="${CSS.escape(nid)}"]`); if (!g) return; g.classList.add('flash'); later(() => g.classList.remove('flash'), 1400); }
4169
- /* the notation every view is drawn in (0.15.0): the host's, a reader's (the page's toggle and link), or null for each
4170
- view's own. The view shown is drawn again from the geometry it has, with its camera, selection, lens and marks, and
4171
- nothing is laid out. False for a notation that isn't one (with a warning). */
4172
- function setShapes(x) {
4173
- if (dead) return false;
4174
- const want = x == null ? null : noteOf(x);
4175
- if (x != null && !want) { warn(`shapes = ${JSON.stringify(x)}: the notations are ${NOTATIONS.join(', ')}`); return false; }
4176
- shapesSet = want;
4177
- if (cur && ctx.geoCache[cur] && ctx.geoCache[cur].shapes !== noteFor(cur)) {
4178
- const keep = sel, keepMore = more, touched = cam.touched;
4179
- mount(cur); cam.set({}); cam.touched = touched;
4180
- if (keep && exists(keep)) { sel = keep; more = keepMore.filter(exists); paint(); }
4181
- emitSel(); }
4182
- touch(); return true;
4183
- }
4184
4150
  function setFlow(on, save = true) { // save: the page keeps it as the reader's choice
4185
4151
  stage.classList.toggle('flowing', on);
4186
4152
  const b = q('[data-z="flow"]'); if (b) { b.classList.toggle('on', on); b.setAttribute('aria-pressed', on); }
@@ -4589,7 +4555,7 @@ export function render(container, model, options = {}) {
4589
4555
  return; }
4590
4556
  if (!ctx.V[cur]) { // the view shown is gone: the root, or the empty card of a model with no views
4591
4557
  const from = cur; mount(ctx.rootId ?? null); cam.set(startView()); cam.touched = false; emitSel(); if (cur) emit('viewchange', { view: cur, from }); return; }
4592
- if (ctx.geoCache[cur] && keys[cur] === ctx.viewKey(cur) && ctx.geoCache[cur].shapes === noteFor(cur)) { if (lensId != null) { relens(); drawMarks(); } if (pc) pc.chrome(); if (P.onMount) P.onMount(inner); return; } // this view's picture did not change (its lens or the views may have; the page reads the new model, issue 133)
4558
+ if (ctx.geoCache[cur] && keys[cur] === ctx.viewKey(cur)) { if (lensId != null) { relens(); drawMarks(); } if (pc) pc.chrome(); if (P.onMount) P.onMount(inner); return; } // this view's picture did not change (its lens or the views may have; the page reads the new model, issue 133)
4593
4559
  const keep = sel, keepMore = more, touched = cam.touched, prev = [geo, svg];
4594
4560
  mount(cur);
4595
4561
  if (touched) cam.set({}); else cam.set(startView());
@@ -4655,7 +4621,7 @@ export function render(container, model, options = {}) {
4655
4621
  const tone = t => 'var(--um-' + String(t || 'accent').replace(/[^\w-]/g, '') + ')', op = m => m.level < 1 ? ` opacity="${m.level}"` : '';
4656
4622
  const cards = Object.fromEntries([...s.querySelectorAll('.node')].map(x => [x.dataset.id, x]));
4657
4623
  let under = '', over = '';
4658
- const look = lookIn(noteFor(vid), g);
4624
+ const look = lookIn(g);
4659
4625
  g.nodes.forEach(n => { const m = markOf2(L => (L.nodes || {})[n.id]), el = cards[n.id]; if (!m || !el) return;
4660
4626
  if (m.level < 1) el.setAttribute('opacity', m.level);
4661
4627
  const sh = (m.tone || m.pulse) && look(n); // a shape's ring: its outline, 5 px out (0.15.0)
@@ -4966,7 +4932,7 @@ export function render(container, model, options = {}) {
4966
4932
 
4967
4933
  const inner = { ctx: () => ctx, geo: () => geo, cam: () => cam, svg: () => svg, stage, host, go, open, up, flash, setFlow, pause, key, startView, standalone, play,
4968
4934
  select, busy: () => busy, flowing: () => stage.classList.contains('flowing'), view: () => cur, unfold: gesture, lens: setLens,
4969
- highlight, overlay, focus: focusMap, editable: !!E, shapes: () => shapesNow(), setShapes, override: () => shapesSet }; // a story's pointer is the pieces' own, from the story event (0.14.0)
4935
+ highlight, overlay, focus: focusMap, editable: !!E }; // a story's pointer is the pieces' own, from the story event (0.14.0)
4970
4936
 
4971
4937
  // the pieces a host asked for (#99): they follow the map's selection and views, and take their keys wherever the focus is
4972
4938
  // in the map (Enter and Space press their buttons; the map's other keys reach the canvas, as in the page)
@@ -4978,13 +4944,12 @@ export function render(container, model, options = {}) {
4978
4944
  if (options.nav) { q('.um-navbtn').addEventListener('click', () => pc.setTree(!frame.classList.contains('tree-open'))); pc.setTree(ctx.views.length > 1); }
4979
4945
  }
4980
4946
  const motionOf = () => instant ? 'instant' : reducedMotion() ? 'reduced' : 'full';
4981
- const shapesNow = () => cur ? noteFor(cur) : shapesSet || noteOf(ctx.D.shapes) || 'box';
4982
4947
  const camNow = () => { const r = (x, d) => Math.round(x * d) / d; return cam ? { x: r(cam.s.cx, 10), y: r(cam.s.cy, 10), zoom: r(cam.s.k, 1000) } : { x: 0, y: 0, zoom: 1 }; };
4983
4948
  function snapshot(event = false) { // MapState: built fresh on every read, frozen all the way down, plain JSON; event: the camera at rest
4984
4949
  const deep = o => { Object.values(o).forEach(v => v && typeof v === 'object' && deep(v)); return Object.freeze(o); }, st = story, t = trip;
4985
4950
  return deep({ view: cur, navigating: nav ? nav.to : null, selection: picked(), expanded: [...(expanded[cur] || [])], lens: lensId, overlays: [...layers.keys()],
4986
4951
  story: st ? { id: st.s.id, step: st.at, steps: st.steps.length, playing: !!st.auto } : null, tour: !!(t && t.kind === 'tour'), replay: t && t.kind === 'replay' ? { playing: !t.paused } : null, paused: !!paused,
4987
- camera: event && camRest ? { ...camRest } : camNow(), flow: stage.classList.contains('flowing'), shapes: shapesNow(), motion: motionOf(), gesture: gestureNow(), destroyed: dead });
4952
+ camera: event && camRest ? { ...camRest } : camNow(), flow: stage.classList.contains('flowing'), motion: motionOf(), gesture: gestureNow(), destroyed: dead });
4988
4953
  }
4989
4954
  const first = () => {
4990
4955
  if (dead) return;
@@ -5023,8 +4988,6 @@ export function render(container, model, options = {}) {
5023
4988
  get destroyed() { return dead; },
5024
4989
  get flow() { return stage.classList.contains('flowing'); }, // the animated flow along lines (≋, F)
5025
4990
  set flow(on) { if (!dead) setFlow(!!on, false); }, // a host's choice, not the reader's: the page saves only ≋ and F
5026
- get shapes() { return shapesNow(); }, // the notation the view shown is drawn in: 'box', 'c4' or 'uml' (0.15.0)
5027
- set shapes(x) { setShapes(x); }, // every view in this one (null: each view's own); the cards are drawn again, nothing is laid out
5028
4991
  get motion() { return motionOf(); }, // 'instant' (the page's #check, #still, #shot), 'reduced' (the reader's setting), else 'full'
5029
4992
  get story() { return story ? story.ctl : null; }, // the player of the story shown (play()'s, the page menu's or a link's), else null
5030
4993
  overlay: (name, layer) => overlay(name, layer),
@@ -5074,26 +5037,22 @@ function detailBullets(body) {
5074
5037
  const SHAPE_SAYS = { person: ['Person', 'someone who uses it'], browser: ['Browser app', 'runs in a web browser'], mobile: ['Mobile app', 'runs on a phone'],
5075
5038
  cylinder: ['Data store', 'a database or a store'], pipe: ['Queue', 'hands work on, in order'], document: ['File', 'a document or a config'],
5076
5039
  hexagon: ['Hexagon', 'a guard or a service'], robot: ['Agent', 'an AI agent'], terminal: ['Command line', 'a CLI or a script'],
5077
- window: ['Desktop app', 'runs in a window'], folder: ['Package', 'a module or a folder of code'], node: ['Host', 'a machine or runtime it runs on'],
5078
- component: ['Component', 'a part that runs (UML)'] };
5040
+ window: ['Desktop app', 'runs in a window'], folder: ['Package', 'a module or a folder of code'], node: ['Host', 'a machine or runtime it runs on'] };
5079
5041
  const swatchOf = (sh, k, c, f, dashed) => { const n = { x: 0, y: 0, w: 100, h: 64 }, S = SHAPE[sh]; // a legend's key: the card's outline, small
5080
5042
  return `<svg class="lgs" viewBox="-4 -4 108 72" aria-hidden="true"><path d="${S.out(n)}" fill="${f}" stroke="${c}"${dashed ? ' stroke-dasharray="3 2"' : ''}/>${S.det ? S.det(n, c, k) : ''}</svg>`; };
5081
- function legendItems(gs, opens = 'double-click to open', notes = [], icons = {}) { // what these pictures draw, each with what it means: the view's legend, and the PDF's (#93). notes: each picture's notation (0.15.0)
5043
+ function legendItems(gs, opens = 'double-click to open') { // what these pictures draw, each with what it means: the view's legend, and the PDF's (#93)
5082
5044
  const typedKeys = [...new Set(gs.flatMap(g => g.legend || []))]; // a typed view's type writes its own keys (1.9); its cards and lines are not a map's
5083
5045
  const ns = gs.flatMap(g => g.legend ? [] : g.nodes), es = gs.flatMap(g => g.legend ? [] : g.edges), used = [...new Set(ns.map(n => n.k))];
5084
- // each card as its picture's notation draws it: its shape, its dash and its mark (the legend draws what the map draws)
5085
- const cards = gs.flatMap((g, i) => { if (g.legend) return []; const nt = g.map ? noteOf(notes[i]) || 'box' : 'box';
5086
- return g.nodes.map(n => { const sh = shapeOf(n, nt), I = (icons || {})[n.icon]; // an icon the table lacks leaves the card the component mark, as svgOf draws it
5087
- return { k: n.k, sh, dash: nt !== 'box' && n.k === 'external', comp: nt === 'uml' && sh === 'box' && !(I && (I.src || I.g)) && !String(n.k).startsWith('m-') }; }); });
5046
+ // each card as its picture draws it: its shape and its dash (the legend draws what the map draws)
5047
+ const cards = gs.flatMap(g => g.legend ? [] : g.nodes.map(n => ({ k: n.k, sh: shapeOf(n, !!g.map), dash: !!g.map && n.k === 'external' })));
5088
5048
  const kindShape = k => { const s = [...new Set(cards.filter(d => d.k === k).map(d => d.sh))]; return s.length === 1 && s[0] !== 'box' && !(k === 'm-hook' && s[0] === 'hexagon') ? s[0] : null; };
5089
5049
  const dashOf = k => k === 'm-ext' || cards.some(d => d.k === k && d.dash); // an external card is dashed only where it is drawn so
5090
5050
  const order = ['m-public', 'm-step', 'm-lane', 'm-ext', 'm-verb', 'm-board', 'm-hook', 'm-script', 'frontend', 'backend', 'ai', 'queue', 'database', 'security', 'cloud', 'external', 'file'];
5091
5051
  const shape = k => k === 'm-verb' ? 'p' : dashOf(k) ? 'd' : k === 'm-hook' ? 'h' : '';
5092
5052
  const kinds = order.filter(k => used.includes(k)), own = new Set(kinds.map(kindShape).filter(Boolean)), tone = k => `var(--um-${KIND[k][0]})`;
5093
- // a shape the kinds' keys don't show already: in a neutral outline, as colour is the kinds' (and UML's component mark)
5053
+ // a shape the kinds' keys don't show already: in a neutral outline, as colour is the kinds'
5094
5054
  const shapes = SHAPE_NAMES.filter(sh => sh !== 'box' && !own.has(sh) && cards.some(d => d.sh === sh && !(sh === 'hexagon' && d.k === 'm-hook')))
5095
- .map(sh => `<span class="lgi" title="${esc(SHAPE_SAYS[sh].join(' — '))}">${swatchOf(sh, 'file', 'var(--um-muted)', 'var(--um-panel)')}<b>${SHAPE_SAYS[sh][0]}</b><span>${SHAPE_SAYS[sh][1]}</span></span>`)
5096
- .concat(cards.some(d => d.comp) ? [`<span class="lgi" title="${esc(SHAPE_SAYS.component.join(' — '))}"><svg class="lgs" viewBox="0 0 16 16" aria-hidden="true"><g fill="none" stroke="var(--um-muted)" stroke-linecap="round" stroke-linejoin="round">${SIGIL.component}</g></svg><b>${SHAPE_SAYS.component[0]}</b><span>${SHAPE_SAYS.component[1]}</span></span>`] : []);
5055
+ .map(sh => `<span class="lgi" title="${esc(SHAPE_SAYS[sh].join(' — '))}">${swatchOf(sh, 'file', 'var(--um-muted)', 'var(--um-panel)')}<b>${SHAPE_SAYS[sh][0]}</b><span>${SHAPE_SAYS[sh][1]}</span></span>`);
5097
5056
  return kinds.map(k => `<span class="lgi" title="${esc(KIND[k][1] + ' — ' + KIND[k][2])}">${kindShape(k) ? swatchOf(kindShape(k), k, tone(k), `var(--um-${KIND[k][0]}-f)`, dashOf(k)) : `<i class="${shape(k)}" style="border-color:${tone(k)};background:var(--um-${KIND[k][0]}-f)${k === 'm-hook' ? `;background:${tone(k)}` : ''}"></i>`}<b>${KIND[k][1]}</b><span>${KIND[k][2]}</span></span>`)
5098
5057
  .concat(shapes)
5099
5058
  .concat(ns.some(n => n.kids || n.opens) ? [`<span class="lgi"><i class="s"></i><b>⤢ Stacked</b><span>${opens}</span></span>`] : [])
@@ -5112,7 +5071,7 @@ function pieces(X, el, o) {
5112
5071
 
5113
5072
  /* the legend: the kinds of cards and lines the view draws, in a box that folds away */
5114
5073
  function legend() {
5115
- const items = legendItems([X().geo()], undefined, [X().shapes ? X().shapes() : 'box'], X().ctx().D.icons), back = el.legend.contains(el.legend.getRootNode().activeElement);
5074
+ const items = legendItems([X().geo()]), back = el.legend.contains(el.legend.getRootNode().activeElement);
5116
5075
  el.legend.hidden = !items.length;
5117
5076
  el.legend.classList.toggle('folded', folded);
5118
5077
  el.legend.innerHTML = `<div class="t"><span>LEGEND</span><button type="button" id="m-lfold" aria-label="${folded ? 'Show' : 'Hide'} legend" aria-expanded="${!folded}">${folded ? '+' : '–'}</button></div>${items.join('')}`;
@@ -5376,7 +5335,7 @@ export function mountPage(container, model, options = {}) {
5376
5335
  <span class="mbrand"><i></i><span>${esc(D.title || 'Map')}</span></span>
5377
5336
  <nav class="crumbs" id="m-crumbs" aria-label="Breadcrumb"></nav>
5378
5337
  <button class="findbtn" id="m-find" type="button" aria-label="Search nodes and views (${modK})"><span aria-hidden="true">⌕</span><span class="ft">Find a node or view…</span><span class="kbd">${modK}</span></button>
5379
- <button class="ibtn" id="m-theme" title="Light / dark" aria-label="Dark theme" aria-pressed="false">◐</button><button class="ibtn txt" id="m-shapes" type="button" title="Card shapes: box, c4 or uml">⬡<span class="lbl"></span></button>${(D.stories || []).length ? `
5338
+ <button class="ibtn" id="m-theme" title="Light / dark" aria-label="Dark theme" aria-pressed="false">◐</button>${(D.stories || []).length ? `
5380
5339
  <div class="menu"><button class="ibtn txt" id="m-stories" aria-haspopup="true" aria-expanded="false" title="Stories: walk-throughs of this map">▶<span class="lbl"> Stories</span></button>
5381
5340
  <div class="menu-pop" id="m-stories-pop" hidden>${D.stories.map(s => `<button data-story="${esc(s && s.id)}">${esc((s && (s.title || s.id)) || '')}</button>`).join('')}</div></div>` : ''}${arrOf(D.lenses).some(l => l && l.id) ? `
5382
5341
  <div class="menu"><button class="ibtn txt" id="m-lenses" aria-haspopup="true" aria-expanded="false" title="Lenses: ways to look at this map">◎<span class="lbl"> Lenses</span></button>
@@ -5441,7 +5400,6 @@ export function mountPage(container, model, options = {}) {
5441
5400
  const p = new URLSearchParams(); p.set('view', cur); if (sel) p.set('node', sel);
5442
5401
  if (told) { p.set('story', told.id); p.set('step', told.step + 1); } // the story shown, at its step (#69)
5443
5402
  if (lensOn) p.set('lens', lensOn); // the lens on the map (#94)
5444
- if (X.override()) p.set('shapes', X.override()); // the notation a reader or the link picked (0.15.0)
5445
5403
  if (Q.get('theme')) p.set('theme', document.documentElement.dataset.theme || Q.get('theme'));
5446
5404
  if (Q.get('reader')) p.set('reader', Q.get('reader')); // check, shot and still describe one run: never in the link a reader shares (issue 136)
5447
5405
  const h = '#' + p.toString();
@@ -5454,8 +5412,6 @@ export function mountPage(container, model, options = {}) {
5454
5412
  const idle = () => new Promise(r => { const t = () => X.busy() ? setTimeout(t, 30) : r(); t(); }); // once no move is under way
5455
5413
  async function follow(load = false) {
5456
5414
  const p = new URLSearchParams(location.hash.slice(1)), my = ++follows, lacks = [], stories = arrOf(D.stories);
5457
- if (noteOf(p.get('shapes')) && noteOf(p.get('shapes')) !== handle.shapes) handle.shapes = p.get('shapes'); // the link's card shapes (0.15.0)
5458
- else if (p.get('shapes') && !noteOf(p.get('shapes'))) lacks.push(`no card shapes “${p.get('shapes')}”`);
5459
5415
  const lens = p.get('lens') || null, sid = p.get('story'), at = Math.max(1, Math.floor(+p.get('step')) || 1) - 1;
5460
5416
  const def = sid ? stories.find(s => s && s.id === sid) : null, steps = def ? arrOf(def.steps).filter(s => s && typeof s === 'object') : [], step = steps[Math.min(at, steps.length - 1)];
5461
5417
  linking = { view: null };
@@ -5686,10 +5642,8 @@ export function mountPage(container, model, options = {}) {
5686
5642
  let flowPref = null; try { flowPref = reducedMotion() ? null : localStorage.getItem('visual-flow'); } catch (_) {} // the reader's ≋ and F; under reduced motion no flow is drawn
5687
5643
  const p0 = new URLSearchParams(location.hash.slice(1));
5688
5644
  const v0 = V[p0.get('view')] ? p0.get('view') : rootId;
5689
- const shapesKey = 'visual-shapes:' + location.pathname; // the reader's notation, kept for this page (0.15.0)
5690
- const shapes0 = noteOf(p0.get('shapes')) || (() => { try { return noteOf(localStorage.getItem(shapesKey)); } catch (_) { return null; } })() || noteOf(options.shapes);
5691
5645
  const handle = render($('m-canvas'), model, {
5692
- view: v0, minimap: true, flow: flowPref ? flowPref === '1' : undefined, shapes: shapes0 || undefined,
5646
+ view: v0, minimap: true, flow: flowPref ? flowPref === '1' : undefined,
5693
5647
  on: { story: e => { told = e.step == null ? null : { id: e.story, step: e.step }; app.classList.toggle('story', !!told); pc.story(e);
5694
5648
  const sv = told && stepView(told); if (X && !X.busy() && (!sv || sv === cur || !V[sv])) write(); } }, // a step on another view: in that view's entry, once it shows (#69)
5695
5649
  _page: {
@@ -5716,13 +5670,6 @@ export function mountPage(container, model, options = {}) {
5716
5670
  if (linking && linking.view === e.view) return; // a link's own move: its entry, written once the link has settled
5717
5671
  linking = null; write(true); }); // a new view, a new entry (a move the reader started while a link was followed, too)
5718
5672
  handle.on('lens', e => { lensOn = e.lens; showLens(); write(); });
5719
- // the header's card shapes (0.15.0): box, c4 and uml in turn, for every view; the page keeps the reader's choice
5720
- const showShapes = () => { const b = $('m-shapes'), n = handle.shapes, next = NOTATIONS[(NOTATIONS.indexOf(n) + 1) % NOTATIONS.length];
5721
- b.querySelector('.lbl').textContent = ' ' + n; b.setAttribute('aria-label', `Card shapes: ${n}. Show them as ${next}`); };
5722
- $('m-shapes').addEventListener('click', () => { const next = NOTATIONS[(NOTATIONS.indexOf(handle.shapes) + 1) % NOTATIONS.length];
5723
- handle.shapes = next; try { localStorage.setItem(shapesKey, next); } catch (_) {} showShapes(); write(); status(`Card shapes: ${next}`); });
5724
- handle.on('state', e => { if (e.changed.includes('shapes')) showShapes(); });
5725
- showShapes();
5726
5673
  // #pdf=1 (umlimator.py pdf, #93): the page becomes a document for Chrome to print (printDoc), once the fonts are in,
5727
5674
  // after the map's own redraw for them
5728
5675
  if (Q.get('pdf')) (document.fonts ? document.fonts.ready : Promise.resolve()).then(() => printDoc(X, model, Q));
@@ -5734,13 +5681,14 @@ export function mountPage(container, model, options = {}) {
5734
5681
  - The cover: the title, the date, the model's file, a summary line and the contents, each view with its page.
5735
5682
  - Each view, from a page of its own: a header (the view's path and its caption), its picture (toSVG()'s, in vectors)
5736
5683
  as large as the page allows and never larger than drawn, the legend of what it draws, then tables of its cards,
5737
- lines and boxes (on a typed view: each element's type and members, as its type prints them, and each line's kind).
5684
+ lines and boxes (on a typed view, "Elements": each element's type and members, or its type's word and rows, and each
5685
+ line's kind).
5738
5686
  They fill two columns below the picture, or one column beside a picture too tall to leave room below. A table that
5739
5687
  runs out of room goes on in the next column, under its heading and header row again; a row is never cut, and a
5740
5688
  heading never ends a column.
5741
5689
  - The rules, when the model has any: each holding or broken in the views printed, and what breaks it. */
5742
5690
  function printDoc(X, model, Q) {
5743
- const { D, views, V, N, chain, nodeIn, viewEdges, geoCache } = X.ctx();
5691
+ const { D, views, V, N, chain, viewEdges, geoCache } = X.ctx();
5744
5692
  const only = (Q.get('views') || '').split(',').filter(Boolean), want = v => !only.length || only.some(p => p.endsWith('*') ? v.id.startsWith(p.slice(0, -1)) : v.id === p);
5745
5693
  const shown = views.filter(want).map(v => [v, X.standalone(1, v.id)]).filter(([, s]) => s), on = new Set(shown.map(([v]) => v.id));
5746
5694
  const rs = shown.length ? arrOf(model.rules).filter(r => r && typeof r === 'object' && (r.from || r.to || r.noCycles)) : []; // as rules() reads them; none without a view
@@ -5748,7 +5696,6 @@ function printDoc(X, model, Q) {
5748
5696
  const [pw, ph] = Q.get('paper') === 'letter' ? [279.4, 215.9] : [297, 210], title = esc(D.title || 'Map'); // the paper across, in mm
5749
5697
  const count = (k, w, ws = w + 's') => `${k} ${k === 1 ? w : ws}`, T = v => esc(v.title || v.id), full = c => Object.assign(c, { full: true });
5750
5698
  const cards = v => arrOf(v.nodeIds).filter(i => N[i]), lines = v => viewEdges(v).filter(e => e.from !== e.to), boxes = v => arrOf(v.containers).filter(c => c && c.id); // as describe() reads a view
5751
- const name = (v, id) => esc(N[id] ? nodeIn(id, v.id).label || id : (boxes(v).find(c => c.id === id) || {}).label || id);
5752
5699
  document.title = D.title || 'Map'; document.body.removeAttribute('class');
5753
5700
  document.body.innerHTML = `<style>@page { size: ${pw}mm ${ph}mm; margin: 11mm 13mm 9mm; background: var(--um-bg); }
5754
5701
  html, body { -webkit-print-color-adjust: exact; print-color-adjust: exact; background: var(--um-bg); } body { margin: 0; background-image: none; }
@@ -5825,14 +5772,11 @@ function printDoc(X, model, Q) {
5825
5772
  ...(rs.length ? ['<tr><td>Rules</td><td class="pg" data-pg="rules"></td></tr>'] : [])] }]);
5826
5773
 
5827
5774
  shown.forEach(([v, svg], vi) => {
5828
- const g = geoCache[v.id].g, ids = cards(v), es = lines(v), seq = v.render === 'sequence', stepped = es.some(e => e.step != null), at = Object.fromEntries(g.nodes.map(n => [n.id, n]));
5829
- const wk = new Map(g.edges.flatMap(d => edgeRefIds(d).map(id => [id, d.wk]))), held = boxes(v).map(c => [c, arrOf(c.nodeIds).filter(i => ids.includes(i))]).filter(([, ins]) => ins.length);
5830
- const typed = (v.diagram ?? 'map') !== 'map', word = t => String(t || '').split('.').pop().replace(/([a-z])([A-Z])/g, (_, a, b) => a + ' ' + b.toLowerCase()); // 'uml.DataType': "Data type"
5831
- const asText = (diagramOf(v)?.member || {}).print || (m => m.label ?? m.id); // a member as its type writes it, else its name
5775
+ const g = geoCache[v.id].g, made = tablesOf(X.ctx(), v, g), [ids, es, held] = made.map(t => t.rows), tables = made.filter(x => x.rows.length);
5832
5776
  const up = chain(v.id).slice(0, -1).map(id => `<span class="up">${T(V[id])} › </span>`).join('');
5833
5777
  const head = more => `<header>${more ? `<div class="h2">${up}${T(v)} <span class="up">(continued)</span></div>` : `<h2>${up}${T(v)}</h2>`}<span class="meta">${[count(ids.length, 'card'), count(es.length, 'line'), ...(held.length ? [count(held.length, 'box', 'boxes')] : [])].join(' · ')}</span></header>${!more && v.caption ? `<p class="cap">${esc(v.caption)}</p>` : ''}`;
5834
5778
  const body = sheet(head(false), vi);
5835
- body.insertAdjacentHTML('beforeend', `<figure>${svg}</figure><div class="lg"><b>Legend</b>${legendItems([g], 'opens a view of its own', [geoCache[v.id].shapes], D.icons).join('')}</div>`);
5779
+ body.insertAdjacentHTML('beforeend', `<figure>${svg}</figure><div class="lg"><b>Legend</b>${legendItems([g], 'opens a view of its own').join('')}</div>`);
5836
5780
  // the picture as large as the page allows, never larger than drawn: below it the legend, then two columns of tables;
5837
5781
  // one too tall for that keeps the page's height, with the legend and one column beside it. Smaller (down to 70%, and
5838
5782
  // to a scale of 0.6 at least) when that keeps the tables on its page; else they go on over the next pages.
@@ -5842,15 +5786,6 @@ function printDoc(X, model, Q) {
5842
5786
  body.classList.toggle('side', side);
5843
5787
  const lay = (f, more) => { s.setAttribute('width', b.width * k * f); s.setAttribute('height', b.height * k * f); body.querySelector('.cols')?.remove();
5844
5788
  const first = cols(body, side ? 1 : 2); if (side) first[0].append(lg); return pour(paging(more && head, 2, first), tables); };
5845
- const tables = [
5846
- { title: `Cards <span>${ids.length}</span>`, cls: 'cards', head: `<th>Card</th><th>Kind</th><th>${typed ? 'Members' : 'What it is'}</th>`, rows: ids.map(id => { const c = nodeIn(id, v.id), t = KIND[kindOf(c.kind, true)], d = at[id] || {}, sub = c.sublabel ?? c.sub;
5847
- return `<tr><td>${d.step ? `<i class="num">${esc(d.step)}</i>` : ''}<b>${esc(c.label || id)}</b>${d.kids || d.opens ? ' ⤢' : ''}${sub ? `<small>${esc(sub)}</small>` : ''}</td>`
5848
- + (typed ? `<td>${esc(word(c.type || 'uml.Class'))}</td><td>${arrOf(c.members).filter(m => m && typeof m === 'object').map(m => `<p>${esc(asText(m))}</p>`).join('')}</td></tr>`
5849
- : `<td><i class="sw" style="border-color:var(--um-${t[0]});background:var(--um-${t[0]}-f)"></i>${t[1]}</td><td>${c.what ? md(c.what) : ''}</td></tr>`); }) },
5850
- { title: `Lines <span>${es.length}</span>`, cls: 'lines', head: `${seq || stepped ? '<th class="n">#</th>' : ''}<th>From → to</th><th>Label</th><th>${typed ? 'Kind' : 'Technology'}</th>`, rows: (stepped && !seq ? [...es].sort((a, b) => stepOrder(a.step, b.step)) : es).map((e, i) => { const w = wk.get(e._id) || 'calls';
5851
- return `<tr>${seq ? `<td>${i + 1}</td>` : stepped ? `<td>${esc(e.step ?? '')}</td>` : ''}<td><i class="w" style="border-color:var(--um-w-${w})"></i>${name(v, e.from)} → ${name(v, e.to)}</td><td>${e.label ? esc(e.label) : typed ? '' : `<span class="up">${w}</span>`}</td><td>${esc(typed ? word(e.type) : e.sublabel || '')}</td></tr>`; }) },
5852
- { title: `Boxes <span>${held.length}</span>`, cls: 'boxes', head: '<th>Box</th><th>Holds</th>', rows: held.map(([c, ins]) => `<tr><td><b>${esc(c.label || c.id)}</b></td><td>${andList(ins.map(i => name(v, i)))}</td></tr>`) },
5853
- ].filter(x => x.rows.length);
5854
5789
  [1, .9, .8, .7].some(f => k * f >= Math.min(k, .6) && lay(f)) || lay(1, true);
5855
5790
  });
5856
5791
 
@@ -5869,3 +5804,22 @@ function printDoc(X, model, Q) {
5869
5804
  all.forEach((p, i) => { p.querySelector('.pn').textContent = `page ${i + 1} of ${all.length}`; });
5870
5805
  doc.querySelectorAll('[data-pg]').forEach(td => { td.textContent = all.findIndex(p => p.dataset.key === td.dataset.pg) + 1; });
5871
5806
  }
5807
+
5808
+ // a view's tables, out of printDoc for Node's tests
5809
+ function tablesOf({ D, N, nodeIn, viewEdges }, v, g) {
5810
+ const cards = v => arrOf(v.nodeIds).filter(i => N[i]), boxes = v => arrOf(v.containers).filter(c => c && c.id);
5811
+ const name = (v, id) => esc(N[id] ? nodeIn(id, v.id).label || id : (boxes(v).find(c => c.id === id) || {}).label || id);
5812
+ const ids = cards(v), es = viewEdges(v).filter(e => e.from !== e.to), seq = v.render === 'sequence', stepped = es.some(e => e.step != null), at = Object.fromEntries(g.nodes.map(n => [n.id, n]));
5813
+ const wk = new Map(g.edges.flatMap(d => edgeRefIds(d).map(id => [id, d.wk]))), held = boxes(v).map(c => [c, arrOf(c.nodeIds).filter(i => ids.includes(i))]).filter(([, ins]) => ins.length);
5814
+ const typed = (v.diagram ?? 'map') !== 'map', word = t => String(t || '').split('.').pop().replace(/([a-z])([A-Z])/g, (_, a, b) => a + ' ' + b.toLowerCase()); // 'uml.DataType': "Data type"
5815
+ const T = diagramOf(v) || {}, asText = (T.member || {}).print || (m => m.label ?? m.id); // a member as its type writes it, else its name
5816
+ return [
5817
+ { title: `${typed ? 'Elements' : 'Cards'} <span>${ids.length}</span>`, cls: 'cards', head: `<th>Card</th><th>Kind</th><th>${typed ? 'Members' : 'What it is'}</th>`, rows: ids.map(id => { const c = nodeIn(id, v.id), t = KIND[kindOf(c.kind, true)], d = at[id] || {}, sub = c.sublabel ?? c.sub, rs = typed && T.rows?.(D, v, id);
5818
+ return `<tr><td>${d.step ? `<i class="num">${esc(d.step)}</i>` : ''}<b>${esc(c.label || id)}</b>${d.kids || d.opens ? ' ⤢' : ''}${sub ? `<small>${esc(sub)}</small>` : ''}</td>`
5819
+ + (typed ? `<td>${esc(T.word?.(D, id) ?? word(c.type || 'uml.Class'))}</td><td>${(Array.isArray(rs) ? rs : arrOf(c.members).filter(m => m && typeof m === 'object').map(m => asText(m))).map(r => `<p>${esc(r)}</p>`).join('')}</td></tr>`
5820
+ : `<td><i class="sw" style="border-color:var(--um-${t[0]});background:var(--um-${t[0]}-f)"></i>${t[1]}</td><td>${c.what ? md(c.what) : ''}</td></tr>`); }) },
5821
+ { title: `Lines <span>${es.length}</span>`, cls: 'lines', head: `${seq || stepped ? '<th class="n">#</th>' : ''}<th>From → to</th><th>Label</th><th>${typed ? 'Kind' : 'Technology'}</th>`, rows: (stepped && !seq ? [...es].sort((a, b) => stepOrder(a.step, b.step)) : es).map((e, i) => { const w = wk.get(e._id) || 'calls';
5822
+ return `<tr>${seq ? `<td>${i + 1}</td>` : stepped ? `<td>${esc(e.step ?? '')}</td>` : ''}<td><i class="w" style="border-color:var(--um-w-${w})"></i>${name(v, e.from)} → ${name(v, e.to)}</td><td>${e.label ? esc(e.label) : typed ? '' : `<span class="up">${w}</span>`}</td><td>${esc(typed ? word(e.type) : e.sublabel || '')}</td></tr>`; }) },
5823
+ { title: `Boxes <span>${held.length}</span>`, cls: 'boxes', head: '<th>Box</th><th>Holds</th>', rows: held.map(([c, ins]) => `<tr><td><b>${esc(c.label || c.id)}</b></td><td>${andList(ins.map(i => name(v, i)))}</td></tr>`) },
5824
+ ];
5825
+ }