umlimator 0.20.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/README.md +52 -25
- package/lib/umlimator-all.js +3520 -184
- package/lib/umlimator-element.js +2 -5
- package/lib/umlimator.d.ts +41 -19
- package/lib/umlimator.js +104 -152
- package/lib/umlimator.schema.json +81 -31
- package/package.json +2 -2
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
|
|
527
|
-
const shapeOf = (n,
|
|
528
|
-
const
|
|
529
|
-
|
|
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,
|
|
533
|
-
const sh = shapeOf(n,
|
|
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
|
-
//
|
|
627
|
-
const
|
|
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,
|
|
631
|
-
: nodes.map(n => [n, n.shape != null && cardLook(n,
|
|
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 => {
|
|
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'})"
|
|
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'
|
|
704
|
-
const dash = removed || ext || planned || (map &&
|
|
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
|
-
|
|
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
|
|
746
|
-
const glyph = L && SIGIL[L.sh] ? L.sh :
|
|
747
|
-
parts.push((() => { const I = (opts.icons || {})[n.icon], x = map ? n.x + 14 : n.x + (verb ? 14 :
|
|
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]
|
|
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:
|
|
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]);
|
|
@@ -1772,7 +1773,7 @@ const ALIAS = { client: 'frontend', ui: 'frontend', web: 'frontend', page: 'fron
|
|
|
1772
1773
|
user: 'external', human: 'external', thirdparty: 'external', vendor: 'external',
|
|
1773
1774
|
doc: 'file', config: 'file', skill: 'file', md: 'file', template: 'file', output: 'file',
|
|
1774
1775
|
agent: 'ai', llm: 'ai', model: 'ai' };
|
|
1775
|
-
const hasKey = (o, k) => Object.hasOwn(o, k); // a table's own key: "constructor" or "__proto__" is no kind
|
|
1776
|
+
const hasKey = (o, k) => Object.hasOwn(o, k); // a table's own key: "constructor" or "__proto__" is no kind or shape
|
|
1776
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'; };
|
|
1777
1778
|
const SIGIL = {
|
|
1778
1779
|
frontend: '<rect x="2" y="3" width="12" height="8.5" rx="1.2"/><path d="M5.5 14h5M8 11.5V14"/>',
|
|
@@ -1799,25 +1800,17 @@ const SIGIL = {
|
|
|
1799
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"/>',
|
|
1800
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"/>',
|
|
1801
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"/>',
|
|
1802
|
-
// UML's component icon (§11.6.4): a box's mark in the uml notation
|
|
1803
|
-
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"/>',
|
|
1804
|
-
};
|
|
1805
|
-
/* ================= card shapes (0.15.0) =================
|
|
1806
|
-
A notation draws each card as a shape: box (the default: every card a rounded box, as before), c4 (people, browser apps,
|
|
1807
|
-
phones, databases, queues and files as the C4 model's tools draw them) or uml (UML 2.5's actor, node, artifact, package
|
|
1808
|
-
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
|
|
1809
|
-
inside its card's own box, so layout, routing, widths and the layout check never see it: an outline (for the mask, the
|
|
1810
|
-
body, the stack, rings, the minimap and the glow), a face (the rows its mark and text use), details drawn over the body
|
|
1811
|
-
(one of them its accent, which lights as something arrives) and inset(side, t), how far inside the box's side the
|
|
1812
|
-
outline runs at t, which slides a line's end from the box onto the outline. */
|
|
1813
|
-
const NOTATIONS = ['box', 'c4', 'uml'];
|
|
1814
|
-
// what a notation draws: by the kind word the model wrote, then by the kind it means (docs/module-api.md, 0.15.0)
|
|
1815
|
-
const NOTED = {
|
|
1816
|
-
c4: { person: 'person', user: 'person', human: 'person', mobile: 'mobile', agent: 'robot', cli: 'terminal',
|
|
1817
|
-
frontend: 'browser', database: 'cylinder', queue: 'pipe', file: 'document', 'm-hook': 'hexagon' },
|
|
1818
|
-
uml: { person: 'person', user: 'person', human: 'person', agent: 'robot', module: 'folder', package: 'folder',
|
|
1819
|
-
cloud: 'node', database: 'cylinder', file: 'document', 'm-hook': 'hexagon' },
|
|
1820
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' };
|
|
1821
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
|
|
1822
1815
|
`diagram`, is a map and takes exactly today's code path. */
|
|
1823
1816
|
const MAP = registerDiagramType('map', { title: 'architecture map' });
|
|
@@ -2174,16 +2167,9 @@ function mapContext(model, opts = {}) {
|
|
|
2174
2167
|
return pos;
|
|
2175
2168
|
}
|
|
2176
2169
|
|
|
2177
|
-
function buildView(vid, opts = {}) {
|
|
2178
|
-
const g = buildGeo(vid);
|
|
2179
|
-
return { g,
|
|
2180
|
-
}
|
|
2181
|
-
// a view's notation (0.15.0): its own `shapes`, else the model's, else box; drawView: the picture of a geometry in one,
|
|
2182
|
-
// with no layout (a change of notation draws the cards again and nothing else)
|
|
2183
|
-
const notationOf = vid => noteOf((V[vid] || {}).shapes) || noteOf(D.shapes) || 'box';
|
|
2184
|
-
function drawView(vid, g, opts = {}) {
|
|
2185
|
-
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
|
|
2186
|
-
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 }) }) };
|
|
2187
2173
|
}
|
|
2188
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
|
|
2189
2175
|
let ruled = null;
|
|
@@ -2350,7 +2336,7 @@ function mapContext(model, opts = {}) {
|
|
|
2350
2336
|
const viewKey = vid => {
|
|
2351
2337
|
const v = V[vid], ids = (v.nodeIds || []).filter(i => N[i]), ns = ids.map(i => nodeIn(i, vid));
|
|
2352
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]);
|
|
2353
|
-
return JSON.stringify([
|
|
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
|
|
2354
2340
|
};
|
|
2355
2341
|
/* ---------- unfolding in place (#71): a stacked card opens into a box on the view it is on ----------
|
|
2356
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
|
|
@@ -2361,11 +2347,11 @@ function mapContext(model, opts = {}) {
|
|
|
2361
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
|
|
2362
2348
|
has no columns, is laid out again). The inner view's boxes that hold its own cards come along inside, and a numbered
|
|
2363
2349
|
(sequence) view's run order: its numbers, its loops back and its lanes (#82). Not in a numbered view: its own steps
|
|
2364
|
-
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. */
|
|
2365
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)
|
|
2366
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));
|
|
2367
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;
|
|
2368
|
-
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;
|
|
2369
2355
|
return ownOf(v, V[c], nid).length ? c : null; };
|
|
2370
2356
|
function unfold(vid, nid) {
|
|
2371
2357
|
const v = V[vid], c = unfoldable(nid, vid); if (!c) return false;
|
|
@@ -2472,7 +2458,7 @@ function mapContext(model, opts = {}) {
|
|
|
2472
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); }); } }))();
|
|
2473
2459
|
const geoCache = {};
|
|
2474
2460
|
return { D, views, nodes, N, V, rootId, nodeIn, childOf, opensOf, subjectOf, viewsWith, kidsOf, chain, viewEdges, isState, planOf: withM(planOf), planCache,
|
|
2475
|
-
layoutCache, geoCache, memos, buildGeo: withM(buildGeo), buildView: withM(buildView),
|
|
2461
|
+
layoutCache, geoCache, memos, buildGeo: withM(buildGeo), buildView: withM(buildView), optimize: withM(optimize), flowPos, kidCount, viewKey, brokenIn, unfoldable, unfolded };
|
|
2476
2462
|
}
|
|
2477
2463
|
const edgeRefIds = e => [...new Set((e.all || [e]).map(x => x._id).filter(Boolean))];
|
|
2478
2464
|
|
|
@@ -2518,20 +2504,21 @@ export function validate(model) {
|
|
|
2518
2504
|
if (n.owner != null && typeof n.owner !== 'string') warn(at + '.owner', 'an owner is a name: ignored');
|
|
2519
2505
|
shapeIs(n, at);
|
|
2520
2506
|
};
|
|
2521
|
-
const shapeIs = (n, at) => { if (n && n.shape != null && !SHAPE_NAMES.includes(String(n.shape).toLowerCase())) warn(at + '.shape', `unknown shape "${n.shape}":
|
|
2522
|
-
const
|
|
2523
|
-
|
|
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, '');
|
|
2524
2510
|
arr(m.nodes).forEach((n, i) => card(n, `nodes[${i}]`));
|
|
2525
2511
|
views.forEach((v, i) => arr(v.nodes).forEach((n, j) => card(n, `views[${i}].nodes[${j}]`)));
|
|
2526
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); });
|
|
2527
2513
|
if (m.root != null && !vids.has(m.root)) err('root', `root "${m.root}" is not a view`);
|
|
2528
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`); };
|
|
2529
2515
|
arr(m.nodes).forEach((n, i) => later(n, `nodes[${i}]`));
|
|
2530
|
-
views.forEach((v, i) =>
|
|
2531
|
-
//
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
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`); });
|
|
2535
2522
|
const line = (e, at, ends) => {
|
|
2536
2523
|
if (!e || typeof e !== 'object') return err(at, 'a line is an object with from and to');
|
|
2537
2524
|
['from', 'to'].forEach(k => { if (!ends.has(e[k])) err(`${at}.${k}`, `"${e[k]}" is neither a card nor a box`); });
|
|
@@ -3822,7 +3809,7 @@ export function render(container, model, options = {}) {
|
|
|
3822
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).
|
|
3823
3810
|
The camera is the exception: a pan, zoom, glide or drag is one state event, once it comes to rest; until then the
|
|
3824
3811
|
event's camera is where it last rested (handle.state reads it live) */
|
|
3825
|
-
const KEYS = ['view', 'navigating', 'selection', 'expanded', 'lens', 'overlays', 'story', 'tour', 'replay', 'paused', 'camera', 'flow', '
|
|
3812
|
+
const KEYS = ['view', 'navigating', 'selection', 'expanded', 'lens', 'overlays', 'story', 'tour', 'replay', 'paused', 'camera', 'flow', 'motion', 'gesture', 'destroyed'];
|
|
3826
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
|
|
3827
3814
|
const listensState = () => !!((options.on || {}).state || (subs.state || []).length);
|
|
3828
3815
|
function touch() { if (!stateT && !dead && listensState()) stateT = setTimeout(flushState, 16); }
|
|
@@ -3833,18 +3820,10 @@ export function render(container, model, options = {}) {
|
|
|
3833
3820
|
function camMoved() { clearTimeout(restT); restT = !dead && listensState() ? setTimeout(rested, 150) : 0; }
|
|
3834
3821
|
function rested() { restT = 0; if (dead) return; if (cam && (cam.anim || cam.ptrs.size) || nav) return camMoved(); camRest = camNow(); touch(); }
|
|
3835
3822
|
let cam = null, geo = null, svg = null, cur = null, sel = null, more = [], busy = false, paused = false, dead = false;
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
const
|
|
3840
|
-
const pic = vid => { // a view's picture in its notation: laid out once, drawn again only when the notation changes
|
|
3841
|
-
const want = noteFor(vid), o = { tour: P.tour !== false, openHint: P.openHint !== false, shapes: want };
|
|
3842
|
-
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));
|
|
3843
|
-
return c; };
|
|
3844
|
-
// 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
|
|
3845
|
-
// (in box too: a card's own shape is drawn there)
|
|
3846
|
-
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); }; };
|
|
3847
|
-
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);
|
|
3848
3827
|
const timers = new Set(), later = (f, ms) => { const t = setTimeout(() => { timers.delete(t); if (!dead) f(); }, ms); timers.add(t); return t; };
|
|
3849
3828
|
const sleeping = new Set(); // a sleep resolves on its own, or when the map is destroyed: nothing that awaits one is left pending
|
|
3850
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); });
|
|
@@ -3948,9 +3927,9 @@ export function render(container, model, options = {}) {
|
|
|
3948
3927
|
/* minimap */
|
|
3949
3928
|
const mini = {};
|
|
3950
3929
|
function buildMini() {
|
|
3951
|
-
const b = geo.bounds, el = q('.minimap'), bw = Math.max(2, b.w / 300)
|
|
3952
|
-
// a box's tone, as its region
|
|
3953
|
-
const rg = (geo.families || []).filter(f => f.tone
|
|
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('');
|
|
3954
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('');
|
|
3955
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}"`;
|
|
3956
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('');
|
|
@@ -4168,21 +4147,6 @@ export function render(container, model, options = {}) {
|
|
|
4168
4147
|
return true;
|
|
4169
4148
|
}
|
|
4170
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); }
|
|
4171
|
-
/* 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
|
|
4172
|
-
view's own. The view shown is drawn again from the geometry it has, with its camera, selection, lens and marks, and
|
|
4173
|
-
nothing is laid out. False for a notation that isn't one (with a warning). */
|
|
4174
|
-
function setShapes(x) {
|
|
4175
|
-
if (dead) return false;
|
|
4176
|
-
const want = x == null ? null : noteOf(x);
|
|
4177
|
-
if (x != null && !want) { warn(`shapes = ${JSON.stringify(x)}: the notations are ${NOTATIONS.join(', ')}`); return false; }
|
|
4178
|
-
shapesSet = want;
|
|
4179
|
-
if (cur && ctx.geoCache[cur] && ctx.geoCache[cur].shapes !== noteFor(cur)) {
|
|
4180
|
-
const keep = sel, keepMore = more, touched = cam.touched;
|
|
4181
|
-
mount(cur); cam.set({}); cam.touched = touched;
|
|
4182
|
-
if (keep && exists(keep)) { sel = keep; more = keepMore.filter(exists); paint(); }
|
|
4183
|
-
emitSel(); }
|
|
4184
|
-
touch(); return true;
|
|
4185
|
-
}
|
|
4186
4150
|
function setFlow(on, save = true) { // save: the page keeps it as the reader's choice
|
|
4187
4151
|
stage.classList.toggle('flowing', on);
|
|
4188
4152
|
const b = q('[data-z="flow"]'); if (b) { b.classList.toggle('on', on); b.setAttribute('aria-pressed', on); }
|
|
@@ -4591,7 +4555,7 @@ export function render(container, model, options = {}) {
|
|
|
4591
4555
|
return; }
|
|
4592
4556
|
if (!ctx.V[cur]) { // the view shown is gone: the root, or the empty card of a model with no views
|
|
4593
4557
|
const from = cur; mount(ctx.rootId ?? null); cam.set(startView()); cam.touched = false; emitSel(); if (cur) emit('viewchange', { view: cur, from }); return; }
|
|
4594
|
-
if (ctx.geoCache[cur] && keys[cur] === ctx.viewKey(cur)
|
|
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)
|
|
4595
4559
|
const keep = sel, keepMore = more, touched = cam.touched, prev = [geo, svg];
|
|
4596
4560
|
mount(cur);
|
|
4597
4561
|
if (touched) cam.set({}); else cam.set(startView());
|
|
@@ -4657,7 +4621,7 @@ export function render(container, model, options = {}) {
|
|
|
4657
4621
|
const tone = t => 'var(--um-' + String(t || 'accent').replace(/[^\w-]/g, '') + ')', op = m => m.level < 1 ? ` opacity="${m.level}"` : '';
|
|
4658
4622
|
const cards = Object.fromEntries([...s.querySelectorAll('.node')].map(x => [x.dataset.id, x]));
|
|
4659
4623
|
let under = '', over = '';
|
|
4660
|
-
const look = lookIn(
|
|
4624
|
+
const look = lookIn(g);
|
|
4661
4625
|
g.nodes.forEach(n => { const m = markOf2(L => (L.nodes || {})[n.id]), el = cards[n.id]; if (!m || !el) return;
|
|
4662
4626
|
if (m.level < 1) el.setAttribute('opacity', m.level);
|
|
4663
4627
|
const sh = (m.tone || m.pulse) && look(n); // a shape's ring: its outline, 5 px out (0.15.0)
|
|
@@ -4968,7 +4932,7 @@ export function render(container, model, options = {}) {
|
|
|
4968
4932
|
|
|
4969
4933
|
const inner = { ctx: () => ctx, geo: () => geo, cam: () => cam, svg: () => svg, stage, host, go, open, up, flash, setFlow, pause, key, startView, standalone, play,
|
|
4970
4934
|
select, busy: () => busy, flowing: () => stage.classList.contains('flowing'), view: () => cur, unfold: gesture, lens: setLens,
|
|
4971
|
-
highlight, overlay, focus: focusMap, editable: !!E
|
|
4935
|
+
highlight, overlay, focus: focusMap, editable: !!E }; // a story's pointer is the pieces' own, from the story event (0.14.0)
|
|
4972
4936
|
|
|
4973
4937
|
// the pieces a host asked for (#99): they follow the map's selection and views, and take their keys wherever the focus is
|
|
4974
4938
|
// in the map (Enter and Space press their buttons; the map's other keys reach the canvas, as in the page)
|
|
@@ -4980,13 +4944,12 @@ export function render(container, model, options = {}) {
|
|
|
4980
4944
|
if (options.nav) { q('.um-navbtn').addEventListener('click', () => pc.setTree(!frame.classList.contains('tree-open'))); pc.setTree(ctx.views.length > 1); }
|
|
4981
4945
|
}
|
|
4982
4946
|
const motionOf = () => instant ? 'instant' : reducedMotion() ? 'reduced' : 'full';
|
|
4983
|
-
const shapesNow = () => cur ? noteFor(cur) : shapesSet || noteOf(ctx.D.shapes) || 'box';
|
|
4984
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 }; };
|
|
4985
4948
|
function snapshot(event = false) { // MapState: built fresh on every read, frozen all the way down, plain JSON; event: the camera at rest
|
|
4986
4949
|
const deep = o => { Object.values(o).forEach(v => v && typeof v === 'object' && deep(v)); return Object.freeze(o); }, st = story, t = trip;
|
|
4987
4950
|
return deep({ view: cur, navigating: nav ? nav.to : null, selection: picked(), expanded: [...(expanded[cur] || [])], lens: lensId, overlays: [...layers.keys()],
|
|
4988
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,
|
|
4989
|
-
camera: event && camRest ? { ...camRest } : camNow(), flow: stage.classList.contains('flowing'),
|
|
4952
|
+
camera: event && camRest ? { ...camRest } : camNow(), flow: stage.classList.contains('flowing'), motion: motionOf(), gesture: gestureNow(), destroyed: dead });
|
|
4990
4953
|
}
|
|
4991
4954
|
const first = () => {
|
|
4992
4955
|
if (dead) return;
|
|
@@ -5025,8 +4988,6 @@ export function render(container, model, options = {}) {
|
|
|
5025
4988
|
get destroyed() { return dead; },
|
|
5026
4989
|
get flow() { return stage.classList.contains('flowing'); }, // the animated flow along lines (≋, F)
|
|
5027
4990
|
set flow(on) { if (!dead) setFlow(!!on, false); }, // a host's choice, not the reader's: the page saves only ≋ and F
|
|
5028
|
-
get shapes() { return shapesNow(); }, // the notation the view shown is drawn in: 'box', 'c4' or 'uml' (0.15.0)
|
|
5029
|
-
set shapes(x) { setShapes(x); }, // every view in this one (null: each view's own); the cards are drawn again, nothing is laid out
|
|
5030
4991
|
get motion() { return motionOf(); }, // 'instant' (the page's #check, #still, #shot), 'reduced' (the reader's setting), else 'full'
|
|
5031
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
|
|
5032
4993
|
overlay: (name, layer) => overlay(name, layer),
|
|
@@ -5076,26 +5037,22 @@ function detailBullets(body) {
|
|
|
5076
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'],
|
|
5077
5038
|
cylinder: ['Data store', 'a database or a store'], pipe: ['Queue', 'hands work on, in order'], document: ['File', 'a document or a config'],
|
|
5078
5039
|
hexagon: ['Hexagon', 'a guard or a service'], robot: ['Agent', 'an AI agent'], terminal: ['Command line', 'a CLI or a script'],
|
|
5079
|
-
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']
|
|
5080
|
-
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'] };
|
|
5081
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
|
|
5082
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>`; };
|
|
5083
|
-
function legendItems(gs, opens = 'double-click to open'
|
|
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)
|
|
5084
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
|
|
5085
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))];
|
|
5086
|
-
// each card as its picture
|
|
5087
|
-
const cards = gs.flatMap(
|
|
5088
|
-
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
|
|
5089
|
-
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' })));
|
|
5090
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; };
|
|
5091
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
|
|
5092
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'];
|
|
5093
5051
|
const shape = k => k === 'm-verb' ? 'p' : dashOf(k) ? 'd' : k === 'm-hook' ? 'h' : '';
|
|
5094
5052
|
const kinds = order.filter(k => used.includes(k)), own = new Set(kinds.map(kindShape).filter(Boolean)), tone = k => `var(--um-${KIND[k][0]})`;
|
|
5095
|
-
// a shape the kinds' keys don't show already: in a neutral outline, as colour is the kinds'
|
|
5053
|
+
// a shape the kinds' keys don't show already: in a neutral outline, as colour is the kinds'
|
|
5096
5054
|
const shapes = SHAPE_NAMES.filter(sh => sh !== 'box' && !own.has(sh) && cards.some(d => d.sh === sh && !(sh === 'hexagon' && d.k === 'm-hook')))
|
|
5097
|
-
.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>`)
|
|
5098
|
-
.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>`);
|
|
5099
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>`)
|
|
5100
5057
|
.concat(shapes)
|
|
5101
5058
|
.concat(ns.some(n => n.kids || n.opens) ? [`<span class="lgi"><i class="s"></i><b>⤢ Stacked</b><span>${opens}</span></span>`] : [])
|
|
@@ -5114,7 +5071,7 @@ function pieces(X, el, o) {
|
|
|
5114
5071
|
|
|
5115
5072
|
/* the legend: the kinds of cards and lines the view draws, in a box that folds away */
|
|
5116
5073
|
function legend() {
|
|
5117
|
-
const items = legendItems([X().geo()]
|
|
5074
|
+
const items = legendItems([X().geo()]), back = el.legend.contains(el.legend.getRootNode().activeElement);
|
|
5118
5075
|
el.legend.hidden = !items.length;
|
|
5119
5076
|
el.legend.classList.toggle('folded', folded);
|
|
5120
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('')}`;
|
|
@@ -5378,7 +5335,7 @@ export function mountPage(container, model, options = {}) {
|
|
|
5378
5335
|
<span class="mbrand"><i></i><span>${esc(D.title || 'Map')}</span></span>
|
|
5379
5336
|
<nav class="crumbs" id="m-crumbs" aria-label="Breadcrumb"></nav>
|
|
5380
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>
|
|
5381
|
-
<button class="ibtn" id="m-theme" title="Light / dark" aria-label="Dark theme" aria-pressed="false">◐</button
|
|
5338
|
+
<button class="ibtn" id="m-theme" title="Light / dark" aria-label="Dark theme" aria-pressed="false">◐</button>${(D.stories || []).length ? `
|
|
5382
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>
|
|
5383
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) ? `
|
|
5384
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>
|
|
@@ -5443,7 +5400,6 @@ export function mountPage(container, model, options = {}) {
|
|
|
5443
5400
|
const p = new URLSearchParams(); p.set('view', cur); if (sel) p.set('node', sel);
|
|
5444
5401
|
if (told) { p.set('story', told.id); p.set('step', told.step + 1); } // the story shown, at its step (#69)
|
|
5445
5402
|
if (lensOn) p.set('lens', lensOn); // the lens on the map (#94)
|
|
5446
|
-
if (X.override()) p.set('shapes', X.override()); // the notation a reader or the link picked (0.15.0)
|
|
5447
5403
|
if (Q.get('theme')) p.set('theme', document.documentElement.dataset.theme || Q.get('theme'));
|
|
5448
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)
|
|
5449
5405
|
const h = '#' + p.toString();
|
|
@@ -5456,8 +5412,6 @@ export function mountPage(container, model, options = {}) {
|
|
|
5456
5412
|
const idle = () => new Promise(r => { const t = () => X.busy() ? setTimeout(t, 30) : r(); t(); }); // once no move is under way
|
|
5457
5413
|
async function follow(load = false) {
|
|
5458
5414
|
const p = new URLSearchParams(location.hash.slice(1)), my = ++follows, lacks = [], stories = arrOf(D.stories);
|
|
5459
|
-
if (noteOf(p.get('shapes')) && noteOf(p.get('shapes')) !== handle.shapes) handle.shapes = p.get('shapes'); // the link's card shapes (0.15.0)
|
|
5460
|
-
else if (p.get('shapes') && !noteOf(p.get('shapes'))) lacks.push(`no card shapes “${p.get('shapes')}”`);
|
|
5461
5415
|
const lens = p.get('lens') || null, sid = p.get('story'), at = Math.max(1, Math.floor(+p.get('step')) || 1) - 1;
|
|
5462
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)];
|
|
5463
5417
|
linking = { view: null };
|
|
@@ -5688,10 +5642,8 @@ export function mountPage(container, model, options = {}) {
|
|
|
5688
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
|
|
5689
5643
|
const p0 = new URLSearchParams(location.hash.slice(1));
|
|
5690
5644
|
const v0 = V[p0.get('view')] ? p0.get('view') : rootId;
|
|
5691
|
-
const shapesKey = 'visual-shapes:' + location.pathname; // the reader's notation, kept for this page (0.15.0)
|
|
5692
|
-
const shapes0 = noteOf(p0.get('shapes')) || (() => { try { return noteOf(localStorage.getItem(shapesKey)); } catch (_) { return null; } })() || noteOf(options.shapes);
|
|
5693
5645
|
const handle = render($('m-canvas'), model, {
|
|
5694
|
-
view: v0, minimap: true, flow: flowPref ? flowPref === '1' : undefined,
|
|
5646
|
+
view: v0, minimap: true, flow: flowPref ? flowPref === '1' : undefined,
|
|
5695
5647
|
on: { story: e => { told = e.step == null ? null : { id: e.story, step: e.step }; app.classList.toggle('story', !!told); pc.story(e);
|
|
5696
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)
|
|
5697
5649
|
_page: {
|
|
@@ -5718,13 +5670,6 @@ export function mountPage(container, model, options = {}) {
|
|
|
5718
5670
|
if (linking && linking.view === e.view) return; // a link's own move: its entry, written once the link has settled
|
|
5719
5671
|
linking = null; write(true); }); // a new view, a new entry (a move the reader started while a link was followed, too)
|
|
5720
5672
|
handle.on('lens', e => { lensOn = e.lens; showLens(); write(); });
|
|
5721
|
-
// the header's card shapes (0.15.0): box, c4 and uml in turn, for every view; the page keeps the reader's choice
|
|
5722
|
-
const showShapes = () => { const b = $('m-shapes'), n = handle.shapes, next = NOTATIONS[(NOTATIONS.indexOf(n) + 1) % NOTATIONS.length];
|
|
5723
|
-
b.querySelector('.lbl').textContent = ' ' + n; b.setAttribute('aria-label', `Card shapes: ${n}. Show them as ${next}`); };
|
|
5724
|
-
$('m-shapes').addEventListener('click', () => { const next = NOTATIONS[(NOTATIONS.indexOf(handle.shapes) + 1) % NOTATIONS.length];
|
|
5725
|
-
handle.shapes = next; try { localStorage.setItem(shapesKey, next); } catch (_) {} showShapes(); write(); status(`Card shapes: ${next}`); });
|
|
5726
|
-
handle.on('state', e => { if (e.changed.includes('shapes')) showShapes(); });
|
|
5727
|
-
showShapes();
|
|
5728
5673
|
// #pdf=1 (umlimator.py pdf, #93): the page becomes a document for Chrome to print (printDoc), once the fonts are in,
|
|
5729
5674
|
// after the map's own redraw for them
|
|
5730
5675
|
if (Q.get('pdf')) (document.fonts ? document.fonts.ready : Promise.resolve()).then(() => printDoc(X, model, Q));
|
|
@@ -5736,13 +5681,14 @@ export function mountPage(container, model, options = {}) {
|
|
|
5736
5681
|
- The cover: the title, the date, the model's file, a summary line and the contents, each view with its page.
|
|
5737
5682
|
- Each view, from a page of its own: a header (the view's path and its caption), its picture (toSVG()'s, in vectors)
|
|
5738
5683
|
as large as the page allows and never larger than drawn, the legend of what it draws, then tables of its cards,
|
|
5739
|
-
lines and boxes (on a typed view: each element's type and members,
|
|
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).
|
|
5740
5686
|
They fill two columns below the picture, or one column beside a picture too tall to leave room below. A table that
|
|
5741
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
|
|
5742
5688
|
heading never ends a column.
|
|
5743
5689
|
- The rules, when the model has any: each holding or broken in the views printed, and what breaks it. */
|
|
5744
5690
|
function printDoc(X, model, Q) {
|
|
5745
|
-
const { D, views, V, N, chain,
|
|
5691
|
+
const { D, views, V, N, chain, viewEdges, geoCache } = X.ctx();
|
|
5746
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);
|
|
5747
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));
|
|
5748
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
|
|
@@ -5750,7 +5696,6 @@ function printDoc(X, model, Q) {
|
|
|
5750
5696
|
const [pw, ph] = Q.get('paper') === 'letter' ? [279.4, 215.9] : [297, 210], title = esc(D.title || 'Map'); // the paper across, in mm
|
|
5751
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 });
|
|
5752
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
|
|
5753
|
-
const name = (v, id) => esc(N[id] ? nodeIn(id, v.id).label || id : (boxes(v).find(c => c.id === id) || {}).label || id);
|
|
5754
5699
|
document.title = D.title || 'Map'; document.body.removeAttribute('class');
|
|
5755
5700
|
document.body.innerHTML = `<style>@page { size: ${pw}mm ${ph}mm; margin: 11mm 13mm 9mm; background: var(--um-bg); }
|
|
5756
5701
|
html, body { -webkit-print-color-adjust: exact; print-color-adjust: exact; background: var(--um-bg); } body { margin: 0; background-image: none; }
|
|
@@ -5827,14 +5772,11 @@ function printDoc(X, model, Q) {
|
|
|
5827
5772
|
...(rs.length ? ['<tr><td>Rules</td><td class="pg" data-pg="rules"></td></tr>'] : [])] }]);
|
|
5828
5773
|
|
|
5829
5774
|
shown.forEach(([v, svg], vi) => {
|
|
5830
|
-
const g = geoCache[v.id].g,
|
|
5831
|
-
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);
|
|
5832
|
-
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"
|
|
5833
|
-
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);
|
|
5834
5776
|
const up = chain(v.id).slice(0, -1).map(id => `<span class="up">${T(V[id])} › </span>`).join('');
|
|
5835
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>` : ''}`;
|
|
5836
5778
|
const body = sheet(head(false), vi);
|
|
5837
|
-
body.insertAdjacentHTML('beforeend', `<figure>${svg}</figure><div class="lg"><b>Legend</b>${legendItems([g], 'opens a view of its own'
|
|
5779
|
+
body.insertAdjacentHTML('beforeend', `<figure>${svg}</figure><div class="lg"><b>Legend</b>${legendItems([g], 'opens a view of its own').join('')}</div>`);
|
|
5838
5780
|
// the picture as large as the page allows, never larger than drawn: below it the legend, then two columns of tables;
|
|
5839
5781
|
// one too tall for that keeps the page's height, with the legend and one column beside it. Smaller (down to 70%, and
|
|
5840
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.
|
|
@@ -5844,15 +5786,6 @@ function printDoc(X, model, Q) {
|
|
|
5844
5786
|
body.classList.toggle('side', side);
|
|
5845
5787
|
const lay = (f, more) => { s.setAttribute('width', b.width * k * f); s.setAttribute('height', b.height * k * f); body.querySelector('.cols')?.remove();
|
|
5846
5788
|
const first = cols(body, side ? 1 : 2); if (side) first[0].append(lg); return pour(paging(more && head, 2, first), tables); };
|
|
5847
|
-
const tables = [
|
|
5848
|
-
{ 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;
|
|
5849
|
-
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>`
|
|
5850
|
-
+ (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>`
|
|
5851
|
-
: `<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>`); }) },
|
|
5852
|
-
{ 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';
|
|
5853
|
-
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>`; }) },
|
|
5854
|
-
{ 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>`) },
|
|
5855
|
-
].filter(x => x.rows.length);
|
|
5856
5789
|
[1, .9, .8, .7].some(f => k * f >= Math.min(k, .6) && lay(f)) || lay(1, true);
|
|
5857
5790
|
});
|
|
5858
5791
|
|
|
@@ -5871,3 +5804,22 @@ function printDoc(X, model, Q) {
|
|
|
5871
5804
|
all.forEach((p, i) => { p.querySelector('.pn').textContent = `page ${i + 1} of ${all.length}`; });
|
|
5872
5805
|
doc.querySelectorAll('[data-pg]').forEach(td => { td.textContent = all.findIndex(p => p.dataset.key === td.dataset.pg) + 1; });
|
|
5873
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
|
+
}
|