umlimator 0.20.0 → 0.22.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 +93 -34
- package/lib/umlimator-all.js +4669 -221
- package/lib/umlimator-element.js +2 -5
- package/lib/umlimator.d.ts +78 -19
- package/lib/umlimator.js +128 -168
- package/lib/umlimator.schema.json +143 -35
- package/package.json +2 -2
package/lib/umlimator.js
CHANGED
|
@@ -120,14 +120,20 @@ 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, head, 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;
|
|
127
131
|
|
|
128
132
|
/* What only a registered type's geometry uses, installed at load time by the files under src/typed/, which only
|
|
129
133
|
lib/umlimator-all.js holds (tools/build.mjs): rows, rooms and routes in layoutPass, a typed line's ends in svgOf, the
|
|
130
|
-
gestures of edit.js it takes over (edit)
|
|
134
|
+
gestures of edit.js it takes over (edit), diff()'s words for typed models (diff, order: src/uml/diff.js). Empty in
|
|
135
|
+
umlimator.js, where no view is laid out but as a map (risks.md C5).
|
|
136
|
+
Stage 6's: across(model), validate()'s rules on the whole model; fields(model, x), more panel fields (notes/stage-6.md §5) */
|
|
131
137
|
const TYPED = {};
|
|
132
138
|
|
|
133
139
|
const propRefs = type => Object.assign({}, ...[...TYPES.values()].map(T => T.refs && Object.hasOwn(T.refs, type) && T.refs[type]));
|
|
@@ -244,6 +250,7 @@ function refs(m, ids, only) {
|
|
|
244
250
|
arr(v.nodes).forEach((n, j) => element(n, [...at, 'nodes', j], false));
|
|
245
251
|
arr(v.edges).forEach((e, j) => { if (has(e)) say([...at, 'edges', j], 'view'); else element(e, [...at, 'edges', j], true); });
|
|
246
252
|
arr(v.fragments).forEach((f, j) => element(f, [...at, 'fragments', j], false));
|
|
253
|
+
['changes', 'constraints'].forEach(k => arr(isObj(v.timeline) && v.timeline[k]).forEach((c, j) => isObj(c) && ['lifeline', 'from', 'to'].forEach(f => { if (has(c[f])) say([...at, 'timeline', k, j, f], 'end'); })));
|
|
247
254
|
[['layout', 'pin'], ['present', 'present'], ['nodeOverrides', 'override']].forEach(([k, kind]) => Object.keys(isObj(v[k]) ? v[k] : {}).forEach(id => { if (has(id)) say([...at, k, id], kind); }));
|
|
248
255
|
if (has(v.subject)) say([...at, 'subject'], 'subject');
|
|
249
256
|
if (!only) ['parent', 'node'].forEach(k => { if (has(v[k])) say([...at, k], 'parent'); }); // a view opened from a card
|
|
@@ -370,9 +377,9 @@ function run(m, c, gone) {
|
|
|
370
377
|
const take = id => { const r = at.get(id); if (!r || now.has(id)) return; now.add(id);
|
|
371
378
|
arr(r.x.members).forEach(y => isObj(y) && take(y.id)); arr(kids.get(id)).forEach(take); }; // what it owns goes with it
|
|
372
379
|
take(c.id);
|
|
373
|
-
// lines to what is gone go too, lines to those lines, and what a type's refs cascade (a reply with its call)
|
|
380
|
+
// lines to what is gone go too, lines to those lines, a timeline's items, and what a type's refs cascade (a reply with its call)
|
|
374
381
|
const cascade = x => Object.entries(propRefs(x.type)).some(([p, how]) => how === 'cascade' && [].concat(dig(x, p)).some(id => now.has(id)));
|
|
375
|
-
for (let more = true; more;) { more = false; for (const [k, o] of at) if (!now.has(k) && (o.kind === 'edge' && (now.has(o.x.from) || now.has(o.x.to)) || cascade(o.x))) { take(k); more = true; } }
|
|
382
|
+
for (let more = true; more;) { more = false; for (const [k, o] of at) if (!now.has(k) && (o.kind === 'edge' && (now.has(o.x.from) || now.has(o.x.to)) || o.kind === 'time' && [o.x.lifeline, o.x.from, o.x.to].some(i => now.has(i)) || cascade(o.x))) { take(k); more = true; } }
|
|
376
383
|
// what stays in something gone goes to its nearest owner left; an operand's messages to its fragment's parent
|
|
377
384
|
const above = id => { const r = at.get(id); return r && (r.x.parent ?? (r.owner && above(r.owner.id))); };
|
|
378
385
|
for (const [k, o] of at) if (!now.has(k) && now.has(o.x.parent)) { let p = o.x.parent;
|
|
@@ -523,14 +530,13 @@ const SHAPE = {
|
|
|
523
530
|
// 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
531
|
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
532
|
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
|
|
533
|
+
// a card's shape: its own `shape` first, then (a map's card: byKind) the one its kind draws, else a box
|
|
534
|
+
const shapeOf = (n, byKind = true) => { const sh = String(n.shape ?? '').toLowerCase(); if (SHAPE_NAMES.includes(sh)) return sh;
|
|
535
|
+
const k = String(n.kind ?? '').toLowerCase(); return byKind && ((hasKey(NOTED, k) && NOTED[k]) || (hasKey(NOTED, n.k) && NOTED[n.k])) || 'box'; };
|
|
536
|
+
// 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
537
|
// 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,
|
|
538
|
+
function cardLook(n, byKind, hint, ports) {
|
|
539
|
+
const sh = shapeOf(n, byKind); if (sh === 'box') return null;
|
|
534
540
|
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
541
|
// 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
542
|
// the shoulder comes up under it, 5 px below its middle (it floated 4 px above the shoulder)
|
|
@@ -623,23 +629,23 @@ function svgOf(geo, opts = {}) {
|
|
|
623
629
|
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
630
|
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
631
|
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
|
|
632
|
+
// each card's look (0.15.0), and each line as drawn, its ends on the cards' outlines (a map's cards only)
|
|
633
|
+
const kidsOf = n => n.kids != null ? n.kids : opts.childCount ? opts.childCount(n.id) : 0;
|
|
628
634
|
// 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
635
|
// 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,
|
|
636
|
+
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))])
|
|
637
|
+
: nodes.map(n => [n, n.shape != null && cardLook(n, false, false, null)]).filter(x => x[1]));
|
|
632
638
|
const drawn = edges.map(e => map && looks.size ? drawnPts(e, n => looks.get(n) || null) : e.pts);
|
|
633
639
|
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
640
|
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
641
|
<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
642
|
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 => {
|
|
643
|
+
families.slice().sort((a,b) => familyDepth(a)-familyDepth(b)).forEach(f => {
|
|
638
644
|
// 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
645
|
// .family-title (the audit, the canvas's "Box …, selected") and draws its border with class family-box: the canvas outlines a
|
|
640
646
|
// selected box and the box under a drag by .family-box alone (src/canvas/styles.js)
|
|
641
647
|
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'})"
|
|
648
|
+
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
649
|
<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
650
|
<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
651
|
<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 +706,8 @@ function svgOf(geo, opts = {}) {
|
|
|
700
706
|
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
707
|
const c = col(n.k), f = fill(n.k), removed = n.change === 'removed', planned = n.change === 'planned';
|
|
702
708
|
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 &&
|
|
709
|
+
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';
|
|
710
|
+
const dash = removed || ext || planned || (map && n.k === 'external'); // an external card is dashed (the legend says so)
|
|
705
711
|
const rx = verb ? n.h / 2 : map ? 10 : 9;
|
|
706
712
|
const kids = kidsOf(n), stacked = !!(kids || n.opens); // opens a view, loaded or not (#47)
|
|
707
713
|
const subject = opts.subject === n.id;
|
|
@@ -738,13 +744,11 @@ function svgOf(geo, opts = {}) {
|
|
|
738
744
|
else {
|
|
739
745
|
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
746
|
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"/>
|
|
747
|
+
parts.push(`<rect x="${n.x}" y="${n.y}" width="${n.w}" height="${n.h}" rx="${rx}" class="n-mask"/>
|
|
744
748
|
<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 :
|
|
749
|
+
// the mark, centred on the face
|
|
750
|
+
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
|
|
751
|
+
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
752
|
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
753
|
: 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
754
|
: `<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 +843,7 @@ const hit = (a, b, pad = 0) => a.x < b.x + b.w + pad && b.x < a.x + a.w + pad &&
|
|
|
839
843
|
// 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
844
|
// model's line can set it
|
|
841
845
|
const OWN_ROUTE = Symbol('own route');
|
|
842
|
-
const labelW = e => textW(fit((e.step != null ? e.step + ' · ' : '') + (e.label || ''), 10, 150), 10) + 12;
|
|
846
|
+
const labelW = e => textW(fit((e.step != null ? e.step + ' · ' : '') + (e.label || ''), 10, e.cut || 150), 10) + 12;
|
|
843
847
|
|
|
844
848
|
/* Layered auto-layout for views that carry no col/row: longest-path layers (cycles broken by DFS),
|
|
845
849
|
barycentre ordering sweeps, integer rows so row gaps stay free as routing channels. */
|
|
@@ -1034,7 +1038,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
|
|
|
1034
1038
|
// A typed view's boxes (notes/stage-2.md §3.2) take the same code as a map's: their title is the type's (`title`), and a box
|
|
1035
1039
|
// grows to hold it; laid out turned (direction down), the 24 px header room goes left, the side that becomes the top
|
|
1036
1040
|
const familyDefs = dg.containers || [], tagged = n => map && (n.author || n.k === 'm-ext'), TB = !map && dg.turned; // tagged: a name tag hangs under the card
|
|
1037
|
-
const headW = f => textW(!map && f.title || f.label || f.id, 11) + 16; // a box's title, as the audit measures it
|
|
1041
|
+
const headW = f => textW(!map && f.title || f.label || f.id, !map && f.head ? 12 : 11) + 16; // a box's title, as the audit measures it (two lines: at 12 px)
|
|
1038
1042
|
const framesNow = () => { const cache = Object.create(null); // a box may be named 'constructor'
|
|
1039
1043
|
const frameOf = (f, trail = new Set()) => {
|
|
1040
1044
|
if (cache[f.id]) return cache[f.id];
|
|
@@ -1047,7 +1051,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
|
|
|
1047
1051
|
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
1052
|
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
1053
|
if (!map) b[TB ? 'h' : 'w'] = Math.max(b[TB ? 'h' : 'w'], headW(f) + 16);
|
|
1050
|
-
return cache[f.id] = b;
|
|
1054
|
+
return cache[f.id] = dg.typed ? TYPED.frame(b, TB) : b;
|
|
1051
1055
|
};
|
|
1052
1056
|
const out = familyDefs.map(f => frameOf(f)).filter(Boolean);
|
|
1053
1057
|
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 +1118,13 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
|
|
|
1114
1118
|
bend penalty. Earlier routes may never be overlapped and are crossed only when no clean
|
|
1115
1119
|
route exists; earlier labels are obstacles, so later routes keep clear of them. */
|
|
1116
1120
|
// 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)
|
|
1121
|
+
// something inside it: that has no line, but a typed view's inner line)
|
|
1118
1122
|
const headOf = f => Math.min((TB ? f.h : f.w) - 16, headW(f)); // the length of a box's title along its header
|
|
1119
1123
|
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
1124
|
decoT: TB ? [] : [[f.x + 8, headOf(f)], ...(f.tabs || []).map(t => [t.x - 6, t.w + 12])], decoB: [] }]));
|
|
1121
1125
|
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
1126
|
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; }
|
|
1127
|
+
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
1128
|
return true; }).map(e => ({ ...e, s: end(e.from), t: end(e.to) }));
|
|
1125
1129
|
const segsOf = pts => pts.slice(0, -1).map((a, i) => [a, pts[i + 1]]);
|
|
1126
1130
|
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 +1182,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
|
|
|
1178
1182
|
});
|
|
1179
1183
|
free.forEach(e => { pushSide(e.s, e.fs, e, e.t); pushSide(e.t, e.ts, e, e.s); });
|
|
1180
1184
|
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);
|
|
1185
|
+
if (LP) LP.ports(pushSide, sides);
|
|
1182
1186
|
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
1187
|
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
1188
|
const side = key.split('|').pop(), vertical = side === 'L' || side === 'R';
|
|
@@ -1424,7 +1428,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
|
|
|
1424
1428
|
const put = (e, s, full) => { placed.push(s.box); reserved.push(s.grow); e.lbl = { ...s.lbl, short: s.lbl.text !== full }; };
|
|
1425
1429
|
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
1430
|
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);
|
|
1431
|
+
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
1432
|
function placeLabel(e) {
|
|
1429
1433
|
if (!e.label && e.step == null) return;
|
|
1430
1434
|
const full = fullOf(e);
|
|
@@ -1449,7 +1453,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
|
|
|
1449
1453
|
const runRouting = order => {
|
|
1450
1454
|
routed.length = 0; reserved.length = 0; placed.length = 0; stats = {};
|
|
1451
1455
|
(dg.blocks || []).forEach(b => { reserved.push(b); placed.push(b); });
|
|
1452
|
-
families.forEach(f => { const h = TB ? { x: f.x + 3, y: f.y + 8, w:
|
|
1456
|
+
families.forEach(f => { const t = 22 + (!map && f.head || 0), h = TB ? { x: f.x + 3, y: f.y + 8, w: t, h: headOf(f) } : { x: f.x + 8, y: f.y + 3, w: headOf(f), h: t }; reserved.push(h); placed.push(h);
|
|
1453
1457
|
placed.push(...[{ x: f.x, y: f.y - 3, w: f.w, h: 6 }, { x: f.x, y: f.y + f.h - 3, w: f.w, h: 6 }, { x: f.x - 3, y: f.y, w: 6, h: f.h }, { x: f.x + f.w - 3, y: f.y, w: 6, h: f.h }].map(r => ({ ...r, band: true }))); }); // lane headers and arrows stay clear
|
|
1454
1458
|
edges.filter(e => e.fixed).forEach(e => { routed.push(e); if (e.under || e[OWN_ROUTE]) { placeLabel(e); stats[e.how] = (stats[e.how] || 0) + 1; return; } if (e.lbl) { placed.push(e.lbl); reserved.push({ x: e.lbl.x - 3, y: e.lbl.y - 3, w: e.lbl.w + 6, h: e.lbl.h + 6 }); } stats.comb = (stats.comb || 0) + 1; });
|
|
1455
1459
|
order.forEach(e => {
|
|
@@ -1563,7 +1567,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
|
|
|
1563
1567
|
if ((map || dg.typed) && dg.repair !== false) free.forEach(e => {
|
|
1564
1568
|
const P = e.pts, ax = e.fs === 'L' || e.fs === 'R' ? 1 : 0, d = P[P.length - 1][ax] - P[0][ax];
|
|
1565
1569
|
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]
|
|
1570
|
+
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
1571
|
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
1572
|
if (!k || v < sp[0] || v > sp[1]) return;
|
|
1569
1573
|
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 +1585,11 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
|
|
|
1581
1585
|
// 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
1586
|
// run crosses, a column's first. layoutGraph lays the view out again with it, and keeps that layout if it is better
|
|
1583
1587
|
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)))) : [];
|
|
1588
|
+
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
1589
|
if (tight.length) {
|
|
1586
1590
|
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
1591
|
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:
|
|
1592
|
+
...Array.from({ length: map && r1 - r0 }, (_, k) => ['y', r0 + k, rowY(r0 + k) + NH, rowY(r0 + k + 1)])];
|
|
1589
1593
|
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
1594
|
let best = c && ['x', 'y'].map(k => gaps.find(([gk, g, p, q]) => gk === k && c[k] >= p && c[k] <= q)).find(Boolean);
|
|
1591
1595
|
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]);
|
|
@@ -1686,7 +1690,7 @@ function routeAudit(geo, where, only, refs) { // only: a set of edges; counts ju
|
|
|
1686
1690
|
if (f.parent && (!parent || !inside(parent,f))) say(`${where}: family "${f.label}" sticks out of its parent`, f);
|
|
1687
1691
|
(f.nodeIds || []).forEach(id => { const n = geo.byId[id]; if (n && !inside(f,n)) say(`${where}: family "${f.label}" does not contain "${n.label}"`, f, n); });
|
|
1688
1692
|
families.filter(g => g !== f && g.parent === f.parent && g.id > f.id).forEach(g => { if (hit(f,g,-1)) say(`${where}: families "${f.label}" and "${g.label}" overlap`, f, g); });
|
|
1689
|
-
geo.edges.forEach((e,i) => { if (segHit({x:f.x+8,y:f.y+3,w:Math.min(f.w-16,textW(f.label || f.id,11)+16),h:22},i)) say(`${where}: family "${f.label}": header crossed by edge ${name(e)}`, f, e); });
|
|
1693
|
+
geo.edges.forEach((e,i) => { if (segHit({x:f.x+8,y:f.y+3,w:Math.min(f.w-16,textW(f.label || f.id,11)+16),h:22+(!geo.map&&f.head||0)},i)) say(`${where}: family "${f.label}": header crossed by edge ${name(e)}`, f, e); });
|
|
1690
1694
|
geo.edges.forEach((e,i) => { if (segs[i].some(([a,b]) => { const v = a[0] === b[0], c = v ? a[0] : a[1], p = v ? [a[1], b[1]] : [a[0], b[0]];
|
|
1691
1695
|
const lo = Math.max(Math.min(...p), v ? f.y : f.x), hi = Math.min(Math.max(...p), v ? f.y + f.h : f.x + f.w);
|
|
1692
1696
|
return hi - lo > 40 && (v ? [f.x, f.x + f.w] : [f.y, f.y + f.h]).some(u => Math.abs(c - u) < 8); })) say(`${where}: edge ${name(e)} runs along the border of family "${f.label}"`, e, f); });
|
|
@@ -1701,7 +1705,7 @@ const box = n => ({ x: n.x, y: n.y, w: n.w, h: n.h });
|
|
|
1701
1705
|
function overlapIssues(geo, where, refs) {
|
|
1702
1706
|
const issues = [], nodes = geo.nodes, say = teller(issues, refs);
|
|
1703
1707
|
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;
|
|
1704
|
-
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
|
|
1708
|
+
const holds = (a, b) => (a.holder && inside(b, a)) || (b.holder && inside(a, b)) || !geo.map && ((a.owner || 0) === b.id || (b.owner || 0) === a.id); // a holder (a sequence's frame) may hold what it meets, not cross it; a port its owner
|
|
1705
1709
|
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]);
|
|
1706
1710
|
return issues;
|
|
1707
1711
|
}
|
|
@@ -1772,7 +1776,7 @@ const ALIAS = { client: 'frontend', ui: 'frontend', web: 'frontend', page: 'fron
|
|
|
1772
1776
|
user: 'external', human: 'external', thirdparty: 'external', vendor: 'external',
|
|
1773
1777
|
doc: 'file', config: 'file', skill: 'file', md: 'file', template: 'file', output: 'file',
|
|
1774
1778
|
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
|
|
1779
|
+
const hasKey = (o, k) => Object.hasOwn(o, k); // a table's own key: "constructor" or "__proto__" is no kind or shape
|
|
1776
1780
|
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
1781
|
const SIGIL = {
|
|
1778
1782
|
frontend: '<rect x="2" y="3" width="12" height="8.5" rx="1.2"/><path d="M5.5 14h5M8 11.5V14"/>',
|
|
@@ -1799,25 +1803,17 @@ const SIGIL = {
|
|
|
1799
1803
|
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
1804
|
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
1805
|
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
1806
|
};
|
|
1807
|
+
/* ================= card shapes (0.15.0; one style since 0.21.0) =================
|
|
1808
|
+
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,
|
|
1809
|
+
terminals, queues, packages, hosts, robots, data stores, files and hooks, and a rounded box where no shape says more. A
|
|
1810
|
+
card's own `shape` wins. Each shape is drawn inside its card's own box, so layout, routing, widths and the layout check
|
|
1811
|
+
never see it: an outline (for the mask, the body, the stack, rings, the minimap and the glow), a face (the rows its mark
|
|
1812
|
+
and text use), details drawn over the body (one of them its accent, which lights as something arrives) and
|
|
1813
|
+
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. */
|
|
1814
|
+
// the shape a card is drawn as: by the kind word the model wrote, then by the kind it means (docs/module-api.md)
|
|
1815
|
+
const NOTED = { person: 'person', user: 'person', human: 'person', agent: 'robot', mobile: 'mobile', cli: 'terminal', frontend: 'browser',
|
|
1816
|
+
queue: 'pipe', module: 'folder', package: 'folder', cloud: 'node', database: 'cylinder', file: 'document', 'm-hook': 'hexagon' };
|
|
1821
1817
|
/* 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
1818
|
`diagram`, is a map and takes exactly today's code path. */
|
|
1823
1819
|
const MAP = registerDiagramType('map', { title: 'architecture map' });
|
|
@@ -2174,16 +2170,9 @@ function mapContext(model, opts = {}) {
|
|
|
2174
2170
|
return pos;
|
|
2175
2171
|
}
|
|
2176
2172
|
|
|
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 };
|
|
2173
|
+
function buildView(vid, opts = {}) { // a view's geometry and its picture
|
|
2174
|
+
const g = buildGeo(vid), v = V[vid], T = ownLayout(vid), L = T && T.draw && T.draw(g, { esc, r2, textW }); // its two layers only
|
|
2175
|
+
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
2176
|
}
|
|
2188
2177
|
// 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
2178
|
let ruled = null;
|
|
@@ -2350,7 +2339,7 @@ function mapContext(model, opts = {}) {
|
|
|
2350
2339
|
const viewKey = vid => {
|
|
2351
2340
|
const v = V[vid], ids = (v.nodeIds || []).filter(i => N[i]), ns = ids.map(i => nodeIn(i, vid));
|
|
2352
2341
|
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([
|
|
2342
|
+
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
2343
|
};
|
|
2355
2344
|
/* ---------- unfolding in place (#71): a stacked card opens into a box on the view it is on ----------
|
|
2356
2345
|
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 +2350,11 @@ function mapContext(model, opts = {}) {
|
|
|
2361
2350
|
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
2351
|
has no columns, is laid out again). The inner view's boxes that hold its own cards come along inside, and a numbered
|
|
2363
2352
|
(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. */
|
|
2353
|
+
would be numbered again; that card opens. Nor into another kind of diagram: that card opens too. */
|
|
2365
2354
|
// 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
2355
|
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
2356
|
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;
|
|
2357
|
+
if (!c || c === vid || chain(vid).includes(c) || (v.containers || []).some(b => b && b.id === nid) || V[c].diagram != v.diagram) return null;
|
|
2369
2358
|
return ownOf(v, V[c], nid).length ? c : null; };
|
|
2370
2359
|
function unfold(vid, nid) {
|
|
2371
2360
|
const v = V[vid], c = unfoldable(nid, vid); if (!c) return false;
|
|
@@ -2472,7 +2461,7 @@ function mapContext(model, opts = {}) {
|
|
|
2472
2461
|
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
2462
|
const geoCache = {};
|
|
2474
2463
|
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),
|
|
2464
|
+
layoutCache, geoCache, memos, buildGeo: withM(buildGeo), buildView: withM(buildView), optimize: withM(optimize), flowPos, kidCount, viewKey, brokenIn, unfoldable, unfolded };
|
|
2476
2465
|
}
|
|
2477
2466
|
const edgeRefIds = e => [...new Set((e.all || [e]).map(x => x._id).filter(Boolean))];
|
|
2478
2467
|
|
|
@@ -2518,20 +2507,21 @@ export function validate(model) {
|
|
|
2518
2507
|
if (n.owner != null && typeof n.owner !== 'string') warn(at + '.owner', 'an owner is a name: ignored');
|
|
2519
2508
|
shapeIs(n, at);
|
|
2520
2509
|
};
|
|
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
|
-
|
|
2510
|
+
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(', ')})`); };
|
|
2511
|
+
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
|
|
2512
|
+
gone(m, '');
|
|
2524
2513
|
arr(m.nodes).forEach((n, i) => card(n, `nodes[${i}]`));
|
|
2525
2514
|
views.forEach((v, i) => arr(v.nodes).forEach((n, j) => card(n, `views[${i}].nodes[${j}]`)));
|
|
2526
2515
|
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
2516
|
if (m.root != null && !vids.has(m.root)) err('root', `root "${m.root}" is not a view`);
|
|
2528
2517
|
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
2518
|
arr(m.nodes).forEach((n, i) => later(n, `nodes[${i}]`));
|
|
2530
|
-
views.forEach((v, i) =>
|
|
2531
|
-
//
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2519
|
+
views.forEach((v, i) => gone(v, `views[${i}].`));
|
|
2520
|
+
// 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
|
|
2521
|
+
// only typed views hold, which keep their own notation (0.21.0)
|
|
2522
|
+
const typedOnly = id => { const vs = views.filter(v => arr(v.nodeIds).includes(id)); return vs.length && vs.every(v => typedView(v) && diagramOf(v)); };
|
|
2523
|
+
arr(m.nodes).forEach((n, i) => { if (!n || n.shape != null || !['user', 'users', 'usercheck'].includes(n.icon) || typedOnly(n.id)) return;
|
|
2524
|
+
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
2525
|
const line = (e, at, ends) => {
|
|
2536
2526
|
if (!e || typeof e !== 'object') return err(at, 'a line is an object with from and to');
|
|
2537
2527
|
['from', 'to'].forEach(k => { if (!ends.has(e[k])) err(`${at}.${k}`, `"${e[k]}" is neither a card nor a box`); });
|
|
@@ -2627,9 +2617,10 @@ export function validate(model) {
|
|
|
2627
2617
|
out.push(...typedProblems(m).filter(p => !(p.level === 'error' && said.has(p.path))));
|
|
2628
2618
|
views.forEach((v, i) => { const at = `views[${i}].diagram`; // a well-formed name of a type this release does not draw (M13)
|
|
2629
2619
|
if (typedView(v) && !diagramOf(v) && !out.some(p => p.path === at)) warn(at, `diagram "${v.diagram}" is not drawn by this release: its nodes are drawn as plain cards`); });
|
|
2630
|
-
const once = new Set()
|
|
2631
|
-
|
|
2632
|
-
|
|
2620
|
+
const once = new Set(), say = p => { const k = JSON.stringify([p.level, p.path, p.message]); if (!once.has(k)) { once.add(k); out.push(p); } };
|
|
2621
|
+
// each view's type's rules, a problem once; a validate(model) that takes no view, once a type
|
|
2622
|
+
views.filter(typedView).forEach(v => { const T = diagramOf(v); if (!T || !T.validate || once.has(T)) return; if (T.validate.length < 2) once.add(T); T.validate(m, v).forEach(say); });
|
|
2623
|
+
if (TYPED.across) arrOf(TYPED.across(m)).forEach(say);
|
|
2633
2624
|
}
|
|
2634
2625
|
return out;
|
|
2635
2626
|
}
|
|
@@ -2786,14 +2777,16 @@ export function diff(before, after) {
|
|
|
2786
2777
|
if (h === 'removed') said.push(`removes ${name(n.id)}`);
|
|
2787
2778
|
if (h === 'changed') { const [o, x] = was.get(n), keys = diffKeys([o, x]), rest = keys.filter(k => k !== 'label'), p0 = o.parent ?? null, p1 = x.parent ?? null;
|
|
2788
2779
|
if (keys.join() === 'parent' && p0 !== p1 && A.schemaVersion === 2 && out.schemaVersion === 2) return said.push(`moves ${name(n.id)} ${p1 != null ? 'into ' + name(p1) : 'out of ' + name(p0)}`); // a re-own (schema version 2; absent or null is no owner)
|
|
2780
|
+
const t = TYPED.diff && TYPED.diff(o, x, name, [A, out]); if (t) return said.push(...t); // a typed element's own words (src/uml/diff.js)
|
|
2789
2781
|
said.push((keys.includes('label') ? `renames ${o.label || o.id} to ${x.label || x.id}` : `changes ${name(n.id)}`) + (rest.length ? ` (${rest.join(', ')})` : '')); } });
|
|
2790
2782
|
lines.forEach(e => { const h = how.get(e); if (!h || cardHow.get(e.from) === h && h !== 'changed' || cardHow.get(e.to) === h && h !== 'changed') return;
|
|
2791
2783
|
const ends = `${name(e.from)} to ${name(e.to)}`, lab = e.label ? ` (${e.label})` : '';
|
|
2792
2784
|
if (h === 'added') said.push(`adds a line from ${ends}${lab}`);
|
|
2793
2785
|
if (h === 'removed') said.push(`removes the line from ${ends}${lab}`);
|
|
2794
|
-
if (h === 'changed') { const [o, x] = was.get(e);
|
|
2786
|
+
if (h === 'changed') { const [o, x] = was.get(e), t = TYPED.diff && TYPED.diff(o, x, name, [A, out]); if (t) return said.push(...t);
|
|
2795
2787
|
said.push(`changes the line from ${ends} (${diffKeys([o, x]).map(k => k === 'label' ? `label ${o.label ?? '-'} → ${x.label ?? '-'}` : k).join(', ')})`); } });
|
|
2796
2788
|
moved.forEach(([verb, id, v]) => said.push(`${verb} ${name(id)} ${verb === 'puts' ? 'in' : 'out of'} the view ${v.title || v.id}`));
|
|
2789
|
+
if (TYPED.order) TYPED.order(A, out, views, V0, said, touched, name);
|
|
2797
2790
|
|
|
2798
2791
|
// the views that show a change: a card or line the diff marks, or a card it put in or took out
|
|
2799
2792
|
const changed = views.filter(v => v && (touched.has(v.id) || arr(v.nodeIds).some(id => cardHow.has(id)) || linesOf(out, v).some(e => how.has(e)))).map(v => v.id);
|
|
@@ -2847,9 +2840,11 @@ const walkFrom = (edges, start, back) => { const seen = new Set([start]); // bre
|
|
|
2847
2840
|
for (let q = [start]; q.length;) { const u = q.shift(); edges.forEach(e => { const [a, b] = back ? [e.to, e.from] : [e.from, e.to]; if (a === u && !seen.has(b)) { seen.add(b); q.push(b); } }); }
|
|
2848
2841
|
return seen; };
|
|
2849
2842
|
|
|
2843
|
+
const viewType = (model, viewId) => model && model.schemaVersion === 2 && diagramOf(arrOf(model.views).find(v => v && v.id === viewId));
|
|
2850
2844
|
/** Every card and line on a path from one card to another, following lines forward: the cards both reachable from `from`
|
|
2851
2845
|
* and leading to `to`, and the lines between them. [] when no path joins them. */
|
|
2852
2846
|
export function paths(model, viewId, from, to) {
|
|
2847
|
+
const T = viewType(model, viewId); if (T && T.paths) return T.paths(model, viewId, from, to); // a type's own walk (a sequence's, in time)
|
|
2853
2848
|
const { ids, edges } = lineGraph(model, viewId);
|
|
2854
2849
|
if (!ids.includes(from) || !ids.includes(to)) return [];
|
|
2855
2850
|
const F = walkFrom(edges, from), B = walkFrom(edges, to, true), on = id => F.has(id) && B.has(id);
|
|
@@ -2874,7 +2869,7 @@ export function reach(model, viewId, from, options = {}) {
|
|
|
2874
2869
|
/** A view in plain sentences: its cards and their kinds, its boxes, what each card's lines do (in run order on a sequence
|
|
2875
2870
|
* view), and the cards with no line. The same model gives the same words: a text alternative, docs, or an AI's context. */
|
|
2876
2871
|
export function describe(model, viewId) {
|
|
2877
|
-
const T =
|
|
2872
|
+
const T = viewType(model, viewId); if (T && T.describe) return T.describe(model, viewId); // a type's own words (1.9)
|
|
2878
2873
|
const { ctx, v, ids, edges } = lineGraph(model, viewId), boxes = (v.containers || []).filter(c => c && c.id);
|
|
2879
2874
|
const name = id => ctx.N[id] ? ctx.nodeIn(id, viewId).label || id : (boxes.find(c => c.id === id) || {}).label || id;
|
|
2880
2875
|
const list = xs => xs.length < 3 ? xs.join(' and ') : xs.slice(0, -1).join(', ') + ' and ' + xs[xs.length - 1];
|
|
@@ -3655,7 +3650,7 @@ function typedEditor(api) {
|
|
|
3655
3650
|
function props() {
|
|
3656
3651
|
const at = elementsOf(model()), id = selId(at), e = id && at.get(id);
|
|
3657
3652
|
if (!e) return '<h3>Properties</h3><p class="um-note">Select a card or a line</p>';
|
|
3658
|
-
const x = e.x, P = isRec(x.props) ? x.props : {},
|
|
3653
|
+
const x = e.x, P = isRec(x.props) ? x.props : {}, F = a => arrOf(a).filter(f => isRec(f) && typeof f.key === 'string'), own = F(isRec(def().props) ? def().props[x.type] : null), schema = [...own, ...F(TYPED.fields && TYPED.fields(model(), x))];
|
|
3659
3654
|
const off = can('rename') ? '' : ' disabled'; // the editable flags: a field the host does not offer is shown, to read
|
|
3660
3655
|
const text = (f, v, name, more = '') => `<label>${esc(name)}<input data-f="${esc(f)}" data-was="${esc(v)}" value="${esc(v)}"${more}${off}></label>`;
|
|
3661
3656
|
let h = `<h3>Properties${x.type != null ? ': ' + esc(x.type) : ''}</h3>` + text('label', x.label ?? '', 'Name');
|
|
@@ -3670,7 +3665,7 @@ function typedEditor(api) {
|
|
|
3670
3665
|
const offer = Array.isArray(os) && [{ id: '', label: 'none' }, ...os.filter(o => isRec(o) && o.id != null && !under(o.id))];
|
|
3671
3666
|
if (offer && x.parent != null && !offer.some(o => o.id === x.parent)) offer.push({ id: x.parent, label: (at.get(x.parent) || { x: {} }).x.label });
|
|
3672
3667
|
if (offer) h += `<label>Owner<select data-f="owner" data-owner${can('move') ? '' : ' disabled'}>${offer.map(o => `<option value="${esc(o.id)}"${o.id === (x.parent ?? '') ? ' selected' : ''}>${esc(o.label ?? o.id)}</option>`).join('')}</select></label>`;
|
|
3673
|
-
const rest =
|
|
3668
|
+
const rest = own.length ? [] : Object.entries(P); // no schema: what it holds, to read
|
|
3674
3669
|
if (rest.length) h += '<dl>' + rest.map(([k, v]) => `<dt>${esc(k)}</dt><dd>${esc(typeof v === 'string' ? v : JSON.stringify(v))}</dd>`).join('') + '</dl>';
|
|
3675
3670
|
const M = def().member;
|
|
3676
3671
|
if (e.kind === 'node' && !(M && M.holds && !M.holds(x))) { // a comment holds none
|
|
@@ -3822,7 +3817,7 @@ export function render(container, model, options = {}) {
|
|
|
3822
3817
|
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
3818
|
The camera is the exception: a pan, zoom, glide or drag is one state event, once it comes to rest; until then the
|
|
3824
3819
|
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', '
|
|
3820
|
+
const KEYS = ['view', 'navigating', 'selection', 'expanded', 'lens', 'overlays', 'story', 'tour', 'replay', 'paused', 'camera', 'flow', 'motion', 'gesture', 'destroyed'];
|
|
3826
3821
|
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
3822
|
const listensState = () => !!((options.on || {}).state || (subs.state || []).length);
|
|
3828
3823
|
function touch() { if (!stateT && !dead && listensState()) stateT = setTimeout(flushState, 16); }
|
|
@@ -3833,18 +3828,10 @@ export function render(container, model, options = {}) {
|
|
|
3833
3828
|
function camMoved() { clearTimeout(restT); restT = !dead && listensState() ? setTimeout(rested, 150) : 0; }
|
|
3834
3829
|
function rested() { restT = 0; if (dead) return; if (cam && (cam.anim || cam.ptrs.size) || nav) return camMoved(); camRest = camNow(); touch(); }
|
|
3835
3830
|
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);
|
|
3831
|
+
const pic = vid => ctx.geoCache[vid] ||= ctx.buildView(vid, { tour: P.tour !== false, openHint: P.openHint !== false }); // a view's picture: laid out once
|
|
3832
|
+
// each card's look, for what is drawn over the picture: rings, the minimap, a tour's or replay's dot
|
|
3833
|
+
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); }; };
|
|
3834
|
+
const looker = () => lookIn(geo);
|
|
3848
3835
|
const timers = new Set(), later = (f, ms) => { const t = setTimeout(() => { timers.delete(t); if (!dead) f(); }, ms); timers.add(t); return t; };
|
|
3849
3836
|
const sleeping = new Set(); // a sleep resolves on its own, or when the map is destroyed: nothing that awaits one is left pending
|
|
3850
3837
|
const sleep = ms => new Promise(r => { if (dead) return r(); const wake = () => { sleeping.delete(wake); r(); }; sleeping.add(wake); later(wake, still() ? 0 : ms); });
|
|
@@ -3864,7 +3851,7 @@ export function render(container, model, options = {}) {
|
|
|
3864
3851
|
const emitSel = (again = false) => {
|
|
3865
3852
|
const now = JSON.stringify(picked()); if (now === said && !again) return; said = now;
|
|
3866
3853
|
say(!sel ? 'Nothing selected' : (sel.type === 'node' ? nameOf(sel.id) : sel.type === 'edge' ? (e => e ? `Line from ${nameOf(e.from)} to ${nameOf(e.to)}` : 'Line')(geo.edges[edgeIndex(sel)])
|
|
3867
|
-
: `Box ${
|
|
3854
|
+
: `Box ${[...svg.querySelectorAll(`.family[data-family="${CSS.escape(sel.id)}"] .family-title`)].map(t => t.textContent).join(' ') || sel.id}`) + (more.length ? `, ${more.length + 1} ${picked().every(r => r.type === 'node') ? 'cards ' : ''}selected` : ', selected'));
|
|
3868
3855
|
emit('select', { target: copyRef(sel), targets: picked() });
|
|
3869
3856
|
};
|
|
3870
3857
|
const edgeRef = e => ({ type: 'edge', ids: edgeRefIds(e), view: cur });
|
|
@@ -3948,9 +3935,9 @@ export function render(container, model, options = {}) {
|
|
|
3948
3935
|
/* minimap */
|
|
3949
3936
|
const mini = {};
|
|
3950
3937
|
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
|
|
3938
|
+
const b = geo.bounds, el = q('.minimap'), bw = Math.max(2, b.w / 300);
|
|
3939
|
+
// a box's tone, as its region, dashed as in the picture
|
|
3940
|
+
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
3941
|
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
3942
|
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
3943
|
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 +4155,6 @@ export function render(container, model, options = {}) {
|
|
|
4168
4155
|
return true;
|
|
4169
4156
|
}
|
|
4170
4157
|
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
4158
|
function setFlow(on, save = true) { // save: the page keeps it as the reader's choice
|
|
4187
4159
|
stage.classList.toggle('flowing', on);
|
|
4188
4160
|
const b = q('[data-z="flow"]'); if (b) { b.classList.toggle('on', on); b.setAttribute('aria-pressed', on); }
|
|
@@ -4324,7 +4296,7 @@ export function render(container, model, options = {}) {
|
|
|
4324
4296
|
if (ref.type === 'node') { const n = geo.byId[ref.id]; return n ? { x: n.x + 50, y: n.y + n.h / 2 - 13, w: n.w - 58, h: 26 } : null; }
|
|
4325
4297
|
if (ref.type === 'edge') { const e = geo.edges[edgeIndex(ref)]; if (!e) return null; const L = e.lbl, m = e.pts[Math.floor(e.pts.length / 2)];
|
|
4326
4298
|
return L ? { x: L.x - 8, y: L.y - 2, w: L.w + 16, h: L.h + 4 } : { x: m[0] - 60, y: m[1] - 12, w: 120, h: 24 }; }
|
|
4327
|
-
const f = ref.type === 'container' && (geo.families || []).find(x => x.id === ref.id); return f ? { x: f.x + 8, y: f.y + 2, w: Math.min(f.w - 16, 220), h: 24 } : null; };
|
|
4299
|
+
const f = ref.type === 'container' && (geo.families || []).find(x => x.id === ref.id); return f ? { x: f.x + 8, y: f.y + 2 + (f.head || 0), w: Math.min(f.w - 16, 220), h: 24 } : null; };
|
|
4328
4300
|
function editor(where, text, done, ref = null) { // where: a box in map units (a new card's), or ref names what the field sits over
|
|
4329
4301
|
if (naming) naming.close(false);
|
|
4330
4302
|
const inp = document.createElement('input');
|
|
@@ -4591,7 +4563,7 @@ export function render(container, model, options = {}) {
|
|
|
4591
4563
|
return; }
|
|
4592
4564
|
if (!ctx.V[cur]) { // the view shown is gone: the root, or the empty card of a model with no views
|
|
4593
4565
|
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)
|
|
4566
|
+
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
4567
|
const keep = sel, keepMore = more, touched = cam.touched, prev = [geo, svg];
|
|
4596
4568
|
mount(cur);
|
|
4597
4569
|
if (touched) cam.set({}); else cam.set(startView());
|
|
@@ -4657,7 +4629,7 @@ export function render(container, model, options = {}) {
|
|
|
4657
4629
|
const tone = t => 'var(--um-' + String(t || 'accent').replace(/[^\w-]/g, '') + ')', op = m => m.level < 1 ? ` opacity="${m.level}"` : '';
|
|
4658
4630
|
const cards = Object.fromEntries([...s.querySelectorAll('.node')].map(x => [x.dataset.id, x]));
|
|
4659
4631
|
let under = '', over = '';
|
|
4660
|
-
const look = lookIn(
|
|
4632
|
+
const look = lookIn(g);
|
|
4661
4633
|
g.nodes.forEach(n => { const m = markOf2(L => (L.nodes || {})[n.id]), el = cards[n.id]; if (!m || !el) return;
|
|
4662
4634
|
if (m.level < 1) el.setAttribute('opacity', m.level);
|
|
4663
4635
|
const sh = (m.tone || m.pulse) && look(n); // a shape's ring: its outline, 5 px out (0.15.0)
|
|
@@ -4968,7 +4940,7 @@ export function render(container, model, options = {}) {
|
|
|
4968
4940
|
|
|
4969
4941
|
const inner = { ctx: () => ctx, geo: () => geo, cam: () => cam, svg: () => svg, stage, host, go, open, up, flash, setFlow, pause, key, startView, standalone, play,
|
|
4970
4942
|
select, busy: () => busy, flowing: () => stage.classList.contains('flowing'), view: () => cur, unfold: gesture, lens: setLens,
|
|
4971
|
-
highlight, overlay, focus: focusMap, editable: !!E
|
|
4943
|
+
highlight, overlay, focus: focusMap, editable: !!E }; // a story's pointer is the pieces' own, from the story event (0.14.0)
|
|
4972
4944
|
|
|
4973
4945
|
// the pieces a host asked for (#99): they follow the map's selection and views, and take their keys wherever the focus is
|
|
4974
4946
|
// in the map (Enter and Space press their buttons; the map's other keys reach the canvas, as in the page)
|
|
@@ -4980,13 +4952,12 @@ export function render(container, model, options = {}) {
|
|
|
4980
4952
|
if (options.nav) { q('.um-navbtn').addEventListener('click', () => pc.setTree(!frame.classList.contains('tree-open'))); pc.setTree(ctx.views.length > 1); }
|
|
4981
4953
|
}
|
|
4982
4954
|
const motionOf = () => instant ? 'instant' : reducedMotion() ? 'reduced' : 'full';
|
|
4983
|
-
const shapesNow = () => cur ? noteFor(cur) : shapesSet || noteOf(ctx.D.shapes) || 'box';
|
|
4984
4955
|
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
4956
|
function snapshot(event = false) { // MapState: built fresh on every read, frozen all the way down, plain JSON; event: the camera at rest
|
|
4986
4957
|
const deep = o => { Object.values(o).forEach(v => v && typeof v === 'object' && deep(v)); return Object.freeze(o); }, st = story, t = trip;
|
|
4987
4958
|
return deep({ view: cur, navigating: nav ? nav.to : null, selection: picked(), expanded: [...(expanded[cur] || [])], lens: lensId, overlays: [...layers.keys()],
|
|
4988
4959
|
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'),
|
|
4960
|
+
camera: event && camRest ? { ...camRest } : camNow(), flow: stage.classList.contains('flowing'), motion: motionOf(), gesture: gestureNow(), destroyed: dead });
|
|
4990
4961
|
}
|
|
4991
4962
|
const first = () => {
|
|
4992
4963
|
if (dead) return;
|
|
@@ -5025,8 +4996,6 @@ export function render(container, model, options = {}) {
|
|
|
5025
4996
|
get destroyed() { return dead; },
|
|
5026
4997
|
get flow() { return stage.classList.contains('flowing'); }, // the animated flow along lines (≋, F)
|
|
5027
4998
|
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
4999
|
get motion() { return motionOf(); }, // 'instant' (the page's #check, #still, #shot), 'reduced' (the reader's setting), else 'full'
|
|
5031
5000
|
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
5001
|
overlay: (name, layer) => overlay(name, layer),
|
|
@@ -5076,26 +5045,22 @@ function detailBullets(body) {
|
|
|
5076
5045
|
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
5046
|
cylinder: ['Data store', 'a database or a store'], pipe: ['Queue', 'hands work on, in order'], document: ['File', 'a document or a config'],
|
|
5078
5047
|
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)'] };
|
|
5048
|
+
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
5049
|
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
5050
|
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'
|
|
5051
|
+
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
5052
|
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
5053
|
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-') }; }); });
|
|
5054
|
+
// each card as its picture draws it: its shape and its dash (the legend draws what the map draws)
|
|
5055
|
+
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
5056
|
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
5057
|
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
5058
|
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
5059
|
const shape = k => k === 'm-verb' ? 'p' : dashOf(k) ? 'd' : k === 'm-hook' ? 'h' : '';
|
|
5094
5060
|
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'
|
|
5061
|
+
// a shape the kinds' keys don't show already: in a neutral outline, as colour is the kinds'
|
|
5096
5062
|
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>`] : []);
|
|
5063
|
+
.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
5064
|
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
5065
|
.concat(shapes)
|
|
5101
5066
|
.concat(ns.some(n => n.kids || n.opens) ? [`<span class="lgi"><i class="s"></i><b>⤢ Stacked</b><span>${opens}</span></span>`] : [])
|
|
@@ -5114,7 +5079,7 @@ function pieces(X, el, o) {
|
|
|
5114
5079
|
|
|
5115
5080
|
/* the legend: the kinds of cards and lines the view draws, in a box that folds away */
|
|
5116
5081
|
function legend() {
|
|
5117
|
-
const items = legendItems([X().geo()]
|
|
5082
|
+
const items = legendItems([X().geo()]), back = el.legend.contains(el.legend.getRootNode().activeElement);
|
|
5118
5083
|
el.legend.hidden = !items.length;
|
|
5119
5084
|
el.legend.classList.toggle('folded', folded);
|
|
5120
5085
|
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 +5343,7 @@ export function mountPage(container, model, options = {}) {
|
|
|
5378
5343
|
<span class="mbrand"><i></i><span>${esc(D.title || 'Map')}</span></span>
|
|
5379
5344
|
<nav class="crumbs" id="m-crumbs" aria-label="Breadcrumb"></nav>
|
|
5380
5345
|
<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
|
|
5346
|
+
<button class="ibtn" id="m-theme" title="Light / dark" aria-label="Dark theme" aria-pressed="false">◐</button>${(D.stories || []).length ? `
|
|
5382
5347
|
<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
5348
|
<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
5349
|
<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 +5408,6 @@ export function mountPage(container, model, options = {}) {
|
|
|
5443
5408
|
const p = new URLSearchParams(); p.set('view', cur); if (sel) p.set('node', sel);
|
|
5444
5409
|
if (told) { p.set('story', told.id); p.set('step', told.step + 1); } // the story shown, at its step (#69)
|
|
5445
5410
|
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
5411
|
if (Q.get('theme')) p.set('theme', document.documentElement.dataset.theme || Q.get('theme'));
|
|
5448
5412
|
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
5413
|
const h = '#' + p.toString();
|
|
@@ -5456,8 +5420,6 @@ export function mountPage(container, model, options = {}) {
|
|
|
5456
5420
|
const idle = () => new Promise(r => { const t = () => X.busy() ? setTimeout(t, 30) : r(); t(); }); // once no move is under way
|
|
5457
5421
|
async function follow(load = false) {
|
|
5458
5422
|
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
5423
|
const lens = p.get('lens') || null, sid = p.get('story'), at = Math.max(1, Math.floor(+p.get('step')) || 1) - 1;
|
|
5462
5424
|
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
5425
|
linking = { view: null };
|
|
@@ -5688,10 +5650,8 @@ export function mountPage(container, model, options = {}) {
|
|
|
5688
5650
|
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
5651
|
const p0 = new URLSearchParams(location.hash.slice(1));
|
|
5690
5652
|
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
5653
|
const handle = render($('m-canvas'), model, {
|
|
5694
|
-
view: v0, minimap: true, flow: flowPref ? flowPref === '1' : undefined,
|
|
5654
|
+
view: v0, minimap: true, flow: flowPref ? flowPref === '1' : undefined,
|
|
5695
5655
|
on: { story: e => { told = e.step == null ? null : { id: e.story, step: e.step }; app.classList.toggle('story', !!told); pc.story(e);
|
|
5696
5656
|
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
5657
|
_page: {
|
|
@@ -5718,13 +5678,6 @@ export function mountPage(container, model, options = {}) {
|
|
|
5718
5678
|
if (linking && linking.view === e.view) return; // a link's own move: its entry, written once the link has settled
|
|
5719
5679
|
linking = null; write(true); }); // a new view, a new entry (a move the reader started while a link was followed, too)
|
|
5720
5680
|
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
5681
|
// #pdf=1 (umlimator.py pdf, #93): the page becomes a document for Chrome to print (printDoc), once the fonts are in,
|
|
5729
5682
|
// after the map's own redraw for them
|
|
5730
5683
|
if (Q.get('pdf')) (document.fonts ? document.fonts.ready : Promise.resolve()).then(() => printDoc(X, model, Q));
|
|
@@ -5736,13 +5689,14 @@ export function mountPage(container, model, options = {}) {
|
|
|
5736
5689
|
- The cover: the title, the date, the model's file, a summary line and the contents, each view with its page.
|
|
5737
5690
|
- Each view, from a page of its own: a header (the view's path and its caption), its picture (toSVG()'s, in vectors)
|
|
5738
5691
|
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,
|
|
5692
|
+
lines and boxes (on a typed view, "Elements": each element's type and members, or its type's word and rows, and each
|
|
5693
|
+
line's kind).
|
|
5740
5694
|
They fill two columns below the picture, or one column beside a picture too tall to leave room below. A table that
|
|
5741
5695
|
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
5696
|
heading never ends a column.
|
|
5743
5697
|
- The rules, when the model has any: each holding or broken in the views printed, and what breaks it. */
|
|
5744
5698
|
function printDoc(X, model, Q) {
|
|
5745
|
-
const { D, views, V, N, chain,
|
|
5699
|
+
const { D, views, V, N, chain, viewEdges, geoCache } = X.ctx();
|
|
5746
5700
|
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
5701
|
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
5702
|
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 +5704,6 @@ function printDoc(X, model, Q) {
|
|
|
5750
5704
|
const [pw, ph] = Q.get('paper') === 'letter' ? [279.4, 215.9] : [297, 210], title = esc(D.title || 'Map'); // the paper across, in mm
|
|
5751
5705
|
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
5706
|
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
5707
|
document.title = D.title || 'Map'; document.body.removeAttribute('class');
|
|
5755
5708
|
document.body.innerHTML = `<style>@page { size: ${pw}mm ${ph}mm; margin: 11mm 13mm 9mm; background: var(--um-bg); }
|
|
5756
5709
|
html, body { -webkit-print-color-adjust: exact; print-color-adjust: exact; background: var(--um-bg); } body { margin: 0; background-image: none; }
|
|
@@ -5827,14 +5780,11 @@ function printDoc(X, model, Q) {
|
|
|
5827
5780
|
...(rs.length ? ['<tr><td>Rules</td><td class="pg" data-pg="rules"></td></tr>'] : [])] }]);
|
|
5828
5781
|
|
|
5829
5782
|
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
|
|
5783
|
+
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
5784
|
const up = chain(v.id).slice(0, -1).map(id => `<span class="up">${T(V[id])} › </span>`).join('');
|
|
5835
5785
|
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
5786
|
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'
|
|
5787
|
+
body.insertAdjacentHTML('beforeend', `<figure>${svg}</figure><div class="lg"><b>Legend</b>${legendItems([g], 'opens a view of its own').join('')}</div>`);
|
|
5838
5788
|
// the picture as large as the page allows, never larger than drawn: below it the legend, then two columns of tables;
|
|
5839
5789
|
// one too tall for that keeps the page's height, with the legend and one column beside it. Smaller (down to 70%, and
|
|
5840
5790
|
// 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 +5794,6 @@ function printDoc(X, model, Q) {
|
|
|
5844
5794
|
body.classList.toggle('side', side);
|
|
5845
5795
|
const lay = (f, more) => { s.setAttribute('width', b.width * k * f); s.setAttribute('height', b.height * k * f); body.querySelector('.cols')?.remove();
|
|
5846
5796
|
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
5797
|
[1, .9, .8, .7].some(f => k * f >= Math.min(k, .6) && lay(f)) || lay(1, true);
|
|
5857
5798
|
});
|
|
5858
5799
|
|
|
@@ -5871,3 +5812,22 @@ function printDoc(X, model, Q) {
|
|
|
5871
5812
|
all.forEach((p, i) => { p.querySelector('.pn').textContent = `page ${i + 1} of ${all.length}`; });
|
|
5872
5813
|
doc.querySelectorAll('[data-pg]').forEach(td => { td.textContent = all.findIndex(p => p.dataset.key === td.dataset.pg) + 1; });
|
|
5873
5814
|
}
|
|
5815
|
+
|
|
5816
|
+
// a view's tables, out of printDoc for Node's tests
|
|
5817
|
+
function tablesOf({ D, N, nodeIn, viewEdges }, v, g) {
|
|
5818
|
+
const cards = v => arrOf(v.nodeIds).filter(i => N[i]), boxes = v => arrOf(v.containers).filter(c => c && c.id);
|
|
5819
|
+
const name = (v, id) => esc(N[id] ? nodeIn(id, v.id).label || id : (boxes(v).find(c => c.id === id) || {}).label || id);
|
|
5820
|
+
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]));
|
|
5821
|
+
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);
|
|
5822
|
+
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"
|
|
5823
|
+
const T = diagramOf(v) || {}, asText = (T.member || {}).print || (m => m.label ?? m.id); // a member as its type writes it, else its name
|
|
5824
|
+
return [
|
|
5825
|
+
{ 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);
|
|
5826
|
+
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>`
|
|
5827
|
+
+ (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>`
|
|
5828
|
+
: `<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>`); }) },
|
|
5829
|
+
{ 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';
|
|
5830
|
+
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>`; }) },
|
|
5831
|
+
{ 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>`) },
|
|
5832
|
+
];
|
|
5833
|
+
}
|