umlimator 0.17.0 → 0.18.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 +32 -6
- package/lib/umlimator-all.js +1244 -253
- package/lib/umlimator.d.ts +33 -4
- package/lib/umlimator.js +138 -253
- package/lib/umlimator.schema.json +198 -7
- package/package.json +1 -1
package/lib/umlimator.js
CHANGED
|
@@ -61,6 +61,7 @@ function typedProblems(model) {
|
|
|
61
61
|
arr(m.edges).forEach((e, i) => { if (typedEdge(e)) { element(e, `edges[${i}]`, 'edge'); typeOf(e, `edges[${i}]`, true); } });
|
|
62
62
|
const typedView = v => v.diagram != null && v.diagram !== 'map'; // a view with diagram 'map' is a map: the map's checks
|
|
63
63
|
views.forEach((v, i) => { if (!typedView(v)) return;
|
|
64
|
+
arr(v.nodes).forEach((n, j) => element(n, `views[${i}].nodes[${j}]`, 'node'));
|
|
64
65
|
arr(v.edges).forEach((e, j) => { if (isObj(e)) { element(e, `views[${i}].edges[${j}]`, 'edge'); typeOf(e, `views[${i}].edges[${j}]`, true); } });
|
|
65
66
|
arr(v.fragments).forEach((f, j) => element(f, `views[${i}].fragments[${j}]`, 'fragment'));
|
|
66
67
|
['changes', 'constraints'].forEach(k => arr((v.timeline || {})[k]).forEach((c, j) => element(c, `views[${i}].timeline.${k}[${j}]`, 'time'))); });
|
|
@@ -84,17 +85,18 @@ function typedProblems(model) {
|
|
|
84
85
|
views.forEach((v, i) => { if (!typedView(v)) return; const at = `views[${i}]`;
|
|
85
86
|
if (typeof v.diagram !== 'string' || !/^[a-z0-9]+(\.[a-z0-9-]+)?$/.test(v.diagram)) warn(at + '.diagram', `diagram ${name(v.diagram)} is not <notation> or <notation>.<kind> (uml.class)`);
|
|
86
87
|
if (v.direction != null && !['right', 'down'].includes(v.direction)) warn(at + '.direction', `direction ${name(v.direction)} is not right or down: drawn right`);
|
|
87
|
-
arr(v.nodeIds).forEach((id, j) => { if (!nodeIds.has(id)) err(`${at}.nodeIds[${j}]`, `${name(id)} is not a node`); });
|
|
88
|
-
const mine = new Set(arr(v.containers).map(c => isObj(c) && c.id));
|
|
89
|
-
|
|
88
|
+
arr(v.nodeIds).forEach((id, j) => { if (!nodeIds.has(id) && !arr(v.nodes).some(n => isObj(n) && n.id === id)) err(`${at}.nodeIds[${j}]`, `${name(id)} is not a node`); });
|
|
89
|
+
const mine = new Set(arr(v.containers).map(c => isObj(c) && c.id)), T = diagramOf(v); // an item (the type's items; else no ends) is no line
|
|
90
|
+
const item = e => (T ? arr(T.items).includes(e.type) : e.from == null && e.to == null);
|
|
91
|
+
arr(v.edges).forEach((e, j) => { if (typeof e === 'string') { if (kindOf.get(e) !== 'edge') err(`${at}.edges[${j}]`, `${name(e)} is not a typed edge`); } else if (isObj(e) && !item(e)) ends(e, `${at}.edges[${j}]`, mine); });
|
|
90
92
|
if (v.subject != null && !has(v.subject)) err(at + '.subject', `subject ${name(v.subject)} is not an element`);
|
|
91
93
|
['layout', 'present'].forEach(k => Object.keys(isObj(v[k]) ? v[k] : {}).forEach(id => { if (!has(id)) warn(`${at}.${k}.${id}`, `${name(id)} is not an element: ignored`); })); });
|
|
92
94
|
return out;
|
|
93
95
|
}
|
|
94
96
|
/* ---------- diagram types (work package 1.3) ----------
|
|
95
97
|
A typed view's `diagram` names its type; a view with none is a map, and the map registers itself as the default
|
|
96
|
-
(src/map/typed.js). A type this release does not know is drawn as plain cards, with a warning.
|
|
97
|
-
|
|
98
|
+
(src/map/typed.js). A type this release does not know is drawn as plain cards, with a warning. Internal until the
|
|
99
|
+
API is documented. */
|
|
98
100
|
const TYPES = new Map();
|
|
99
101
|
|
|
100
102
|
function registerDiagramType(name, def = {}) {
|
|
@@ -105,14 +107,18 @@ function registerDiagramType(name, def = {}) {
|
|
|
105
107
|
return type;
|
|
106
108
|
}
|
|
107
109
|
|
|
108
|
-
/* A definition
|
|
109
|
-
|
|
110
|
-
lines [{ type, label }] the relationship picker: the kinds of line between two elements
|
|
111
|
-
props { [type]: [{ key, label, kind: 'text' | 'bool' | 'enum', options? }] } the properties form, per element type
|
|
112
|
-
Without them the canvas offers one generic node and one generic line (core.Node, core.Line), and shows an element's props to read. */
|
|
110
|
+
/* A definition's fields are optional and provisional; absent, things go as before. notes/spike-sequence.md §1 says what
|
|
111
|
+
each does (validate(model) with no view runs once a type, not once a view); 1.9's: rankFlip, member, rules, describe. */
|
|
113
112
|
|
|
114
113
|
/* A view's type: the map for a view with no diagram, the registered type, or null for one this release does not draw */
|
|
115
114
|
const diagramOf = view => TYPES.get(view && view.diagram != null ? view.diagram : 'map') || null;
|
|
115
|
+
|
|
116
|
+
/* What only a registered type's geometry uses, installed at load time by the files under src/typed/, which only
|
|
117
|
+
lib/umlimator-all.js holds (tools/build.mjs): rows, rooms and routes in layoutPass, a typed line's ends in svgOf. Empty in
|
|
118
|
+
umlimator.js, where no view is laid out but as a map (risks.md C5) */
|
|
119
|
+
const TYPED = {};
|
|
120
|
+
|
|
121
|
+
const propRefs = type => Object.assign({}, ...[...TYPES.values()].map(T => T.refs && Object.hasOwn(T.refs, type) && T.refs[type]));
|
|
116
122
|
const warned = new Set(), warn = m => { if (hush) hush.push(m); else if (!warned.has(m)) { warned.add(m); console.warn('umlimator: ' + m); } }; // each warning once
|
|
117
123
|
let hush = null; // a layout tried and maybe dropped (unfolding, #71) keeps its warnings here: only the one kept says them
|
|
118
124
|
/* f(), with the warnings it gives held back: [its result, what it would have said] */
|
|
@@ -173,7 +179,8 @@ export function history(model, limit = 100) {
|
|
|
173
179
|
value undefined removes it. Through objects only; id, members, parent,
|
|
174
180
|
from and to change by their own commands
|
|
175
181
|
add { element, owner?, index?, view? } a node (no from/to), a line, or with `owner` a member, at `index`
|
|
176
|
-
among the owner's members, else among the view's; `view` shows it there
|
|
182
|
+
among the owner's members, else among the view's; `view` shows it there;
|
|
183
|
+
in 'view' (not 'model'): in its edges, nodes or fragments (registry.js)
|
|
177
184
|
move { id, index, owner? } a member to another place among its owner's members, or to a new owner
|
|
178
185
|
{ id, view, index } an element or line to another place in a view's order (its nodeIds or edges)
|
|
179
186
|
hide { id, view } out of one view, with all the view says of it: its place, pin, present,
|
|
@@ -181,14 +188,16 @@ export function history(model, limit = 100) {
|
|
|
181
188
|
keeps it (risks.md E5). A line that lives only in that view goes with it
|
|
182
189
|
delete { id } out of the model, with its members, what it owns, the lines to them, and
|
|
183
190
|
every place that names them (references()). One already gone in the same
|
|
184
|
-
transaction is passed over, so a selection deletes in one transaction (E2)
|
|
191
|
+
transaction is passed over, so a selection deletes in one transaction (E2).
|
|
192
|
+
What it held and stays goes to its nearest owner left
|
|
185
193
|
reparent { id, parent } a new owner by `parent`, or none with parent null
|
|
186
|
-
An element is a node, a member, a model line or a view's own line with an id, or a fragment (schema version 2's
|
|
187
|
-
space).
|
|
194
|
+
An element is a node, a member, a model line or a view's own line or item with an id, or a fragment (schema version 2's
|
|
195
|
+
id space). */
|
|
188
196
|
|
|
189
197
|
const isObj = x => x !== null && typeof x === 'object' && !Array.isArray(x), arr = x => (Array.isArray(x) ? x : []);
|
|
190
198
|
const UNSAFE = new Set(['__proto__', 'constructor', 'prototype']); // never a key a command writes (risks.md M3)
|
|
191
199
|
const isLine = x => isObj(x) && (x.from != null || x.to != null);
|
|
200
|
+
const dig = (x, path) => path.split('.').reduce((o, k) => (isObj(o) ? o[k] : undefined), x);
|
|
192
201
|
|
|
193
202
|
/* Every element by id: { x, list, kind, owner, view } (owner: the element whose member it is; view: the view a line or
|
|
194
203
|
fragment of its own lives in) */
|
|
@@ -205,7 +214,7 @@ function index(m) {
|
|
|
205
214
|
}
|
|
206
215
|
|
|
207
216
|
/* Where a model names any of `ids` (a Set): [{ path, kind }], path a list of keys from the model down. Kinds: end, parent,
|
|
208
|
-
part, view, pin, present, override, subject, story, lens, rule. Neither the element itself nor its members are listed.
|
|
217
|
+
part, prop, view, pin, present, override, subject, story, lens, rule. Neither the element itself nor its members are listed.
|
|
209
218
|
only(viewIndex): look in those views and their story steps only (hide), else in all of the model. */
|
|
210
219
|
function refs(m, ids, only) {
|
|
211
220
|
const out = [], say = (path, kind) => out.push({ path, kind }), has = id => typeof id === 'string' && ids.has(id);
|
|
@@ -213,11 +222,14 @@ function refs(m, ids, only) {
|
|
|
213
222
|
if (has(x.parent)) say([...at, 'parent'], 'parent');
|
|
214
223
|
if (line) ['from', 'to'].forEach(k => { if (has(x[k])) say([...at, k], 'end');
|
|
215
224
|
if (has((((x.props || {}).ends || {})[k] || {}).part)) say([...at, 'props', 'ends', k, 'part'], 'part'); });
|
|
225
|
+
Object.keys(propRefs(x.type)).forEach(p => { const v = dig(x, p), ks = p.split('.');
|
|
226
|
+
if (has(v)) say([...at, ...ks], 'prop'); else arr(v).forEach((id, j) => { if (has(id)) say([...at, ...ks, j], 'prop'); }); });
|
|
216
227
|
arr(x.members).forEach((y, j) => element(y, [...at, 'members', j], false)); };
|
|
217
228
|
if (!only) { arr(m.nodes).forEach((n, i) => element(n, ['nodes', i], false)); arr(m.edges).forEach((e, i) => element(e, ['edges', i], true)); }
|
|
218
229
|
const vids = new Set();
|
|
219
230
|
arr(m.views).forEach((v, i) => { if (!isObj(v) || (only && !only(i))) return; const at = ['views', i]; vids.add(v.id);
|
|
220
231
|
['nodeIds', 'tour', 'stepIds'].forEach(k => arr(v[k]).forEach((id, j) => { if (has(id)) say([...at, k, j], 'view'); }));
|
|
232
|
+
arr(v.nodes).forEach((n, j) => element(n, [...at, 'nodes', j], false));
|
|
221
233
|
arr(v.edges).forEach((e, j) => { if (has(e)) say([...at, 'edges', j], 'view'); else element(e, [...at, 'edges', j], true); });
|
|
222
234
|
arr(v.fragments).forEach((f, j) => element(f, [...at, 'fragments', j], false));
|
|
223
235
|
[['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); }));
|
|
@@ -295,8 +307,18 @@ function run(m, c, gone) {
|
|
|
295
307
|
}
|
|
296
308
|
case 'add': {
|
|
297
309
|
if ('to' in c) throw new Error('add: the owner of a member is `owner`');
|
|
310
|
+
if (c.in != null && c.in !== 'view' && c.in !== 'model') throw new Error(`add: in is 'view' or 'model', not ${JSON.stringify(c.in)}`);
|
|
298
311
|
checkNew(c.element, at);
|
|
299
312
|
const x = structuredClone(c.element), owner = c.owner != null ? need(c.owner) : null;
|
|
313
|
+
if (c.in === 'view') {
|
|
314
|
+
if (c.view == null || owner) throw new Error('add: in \'view\' needs a view, and no owner');
|
|
315
|
+
const v = viewOf(c.view), T = diagramOf(v) || {}, pal = arr(T.elements);
|
|
316
|
+
const k = isLine(x) || arr(T.items).includes(x.type) ? 'edges' : !pal.length || pal.some(t => isObj(t) && t.type === x.type) ? 'nodes' : 'fragments';
|
|
317
|
+
if (k === 'edges' && !Array.isArray(v.edges) && arr(m.edges).length) throw new Error(`add: view "${c.view}" shows every line between its cards; list its lines first`);
|
|
318
|
+
const list = (v[k] ||= []);
|
|
319
|
+
if (k === 'nodes') { list.push(x); (v.nodeIds ||= []).splice(place(v.nodeIds, c.index), 0, x.id); } else list.splice(place(list, c.index), 0, x);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
300
322
|
if (owner && owner.kind !== 'node' && owner.kind !== 'member' && owner.kind !== 'fragment') throw new Error(`add: "${c.owner}" holds no members`);
|
|
301
323
|
const v = c.view != null ? viewOf(c.view) : null, list = owner ? (owner.x.members ||= []) : isLine(x) ? (m.edges ||= []) : (m.nodes ||= []);
|
|
302
324
|
list.splice(owner ? place(list, c.index) : list.length, 0, x);
|
|
@@ -326,19 +348,24 @@ function run(m, c, gone) {
|
|
|
326
348
|
const ends = l => isObj(l) && (ids.has(l.from) || ids.has(l.to)); // the view's lines to it, listed or its own, leave the view with it
|
|
327
349
|
if (Array.isArray(v.edges)) v.edges = v.edges.filter(y => { const l = typeof y === 'string' ? (at.get(y) || {}).x : y, out = y === c.id || ends(l);
|
|
328
350
|
if (out && isObj(l) && typeof l.id === 'string') ids.add(l.id); return !out; }); // what names those lines in this view goes too
|
|
329
|
-
clear(m, refs(m, ids, i => i === vi).filter(r =>
|
|
351
|
+
clear(m, refs(m, ids, i => i === vi).filter(r => !['end', 'parent', 'part', 'prop'].includes(r.kind)));
|
|
330
352
|
return;
|
|
331
353
|
}
|
|
332
354
|
case 'delete': {
|
|
333
355
|
if (!at.has(c.id) && gone.has(c.id)) return; // gone with an earlier command of this transaction
|
|
334
|
-
need(c.id); const now = new Set();
|
|
356
|
+
need(c.id); const now = new Set(), kids = new Map();
|
|
357
|
+
for (const [k, o] of at) if ((o.kind === 'node' || o.kind === 'member') && o.x.parent != null) { if (!kids.has(o.x.parent)) kids.set(o.x.parent, []); kids.get(o.x.parent).push(k); }
|
|
335
358
|
const take = id => { const r = at.get(id); if (!r || now.has(id)) return; now.add(id);
|
|
336
|
-
arr(r.x.members).forEach(y => isObj(y) && take(y.id));
|
|
337
|
-
for (const [k, o] of at) if ((o.kind === 'node' || o.kind === 'member') && o.x.parent === id) take(k); }; // what it owns goes with it
|
|
359
|
+
arr(r.x.members).forEach(y => isObj(y) && take(y.id)); arr(kids.get(id)).forEach(take); }; // what it owns goes with it
|
|
338
360
|
take(c.id);
|
|
339
|
-
// lines to what is gone go too,
|
|
340
|
-
|
|
341
|
-
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))) {
|
|
361
|
+
// lines to what is gone go too, lines to those lines, and what a type's refs cascade (a reply with its call)
|
|
362
|
+
const cascade = x => Object.entries(propRefs(x.type)).some(([p, how]) => how === 'cascade' && [].concat(dig(x, p)).some(id => now.has(id)));
|
|
363
|
+
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; } }
|
|
364
|
+
// what stays in something gone goes to its nearest owner left; an operand's messages to its fragment's parent
|
|
365
|
+
const above = id => { const r = at.get(id); return r && (r.x.parent ?? (r.owner && above(r.owner.id))); };
|
|
366
|
+
for (const [k, o] of at) if (!now.has(k) && now.has(o.x.parent)) { let p = o.x.parent;
|
|
367
|
+
for (let n = 0; p != null && now.has(p) && n++ < at.size;) p = above(p);
|
|
368
|
+
if (p != null && !now.has(p)) o.x.parent = p; else delete o.x.parent; }
|
|
342
369
|
for (const [k, o] of at) if (now.has(k) && !(o.owner && now.has(o.owner.id))) { const i = o.list.indexOf(o.x); if (i >= 0) o.list.splice(i, 1); }
|
|
343
370
|
for (let found = refs(m, now); found.length; found = refs(m, now)) clear(m, found); // a line without an id to a gone line goes in a second round
|
|
344
371
|
now.forEach(id => gone.add(id));
|
|
@@ -479,60 +506,10 @@ const SHAPE = {
|
|
|
479
506
|
return `<path d="M${x + 1} ${y + p}L${x + p} ${y + 1}H${x + w - 1}L${x + w - p} ${y + p}Z" fill="${deeper(k, 12)}"/><path d="M${x + w - p} ${y + p}L${x + w - 1} ${y + 1}V${y + h - p}L${x + w - p} ${y + h - 1}Z" fill="${deeper(k, 22)}"/>`
|
|
480
507
|
+ `<path d="M${x} ${y + p}H${x + w - p}V${y + h}" class="n-det n-accent" stroke="${c}"/><path d="M${x + w - p} ${y + p}L${x + w} ${y}" class="n-det" stroke="${c}"/>`; },
|
|
481
508
|
inset(n, s, t) { const p = 7; return s === 'L' ? Math.max(0, n.y + p - t) : s === 'R' ? Math.max(0, t - n.y - n.h + p) : s === 'T' ? Math.max(0, n.x + p - t) : Math.max(0, t - n.x - n.w + p); } },
|
|
482
|
-
classifier: { // UML's class box (1.5): a rectangle, its name compartment, then one compartment per member kind (classParts)
|
|
483
|
-
size: n => classParts(n).size, out: boxOut(0), face: n => [n.y, n.y + n.h], inset: () => 0, draw: classSvg },
|
|
484
509
|
};
|
|
485
|
-
// what every shape reports (1.5): size(n), the box layout gives it (a map's card: mapNodeW wide, a row high; the classifier
|
|
486
|
-
// from its text), and ports(n, side, k), k line ends spread evenly along a side ('L', 'R', 'T', 'B'), each on the outline
|
|
510
|
+
// what every shape reports (1.5): size(n), the box layout gives it (a map's card: mapNodeW wide, a row high; the classifier,
|
|
511
|
+
// 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
|
|
487
512
|
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); }
|
|
488
|
-
// ---- the classifier (1.5). Its drawing input, on a typed view's node (not the model: 1.9 fills it from the model and the
|
|
489
|
-
// view's present; a map never draws it): shape 'classifier'; label; type ('uml.Class', named in its accessible name);
|
|
490
|
-
// stereotypes [text], shown «…» over the name; abstract (the name in italics); compartments [{ name, members }] in order
|
|
491
|
-
// ('attributes', 'operations', 'literals'), each member in the model's envelope ({ type, label, props }); collapsed
|
|
492
|
-
// [compartment names], hidden with no divider; rows: at most that many rows in a compartment, then "+k more" (stage 1 note §5)
|
|
493
|
-
const CL = { pad: 8, name: 13, row: 11, st: 14, nl: 18, rl: 15, gap: 5, empty: 10, minW: 96, maxW: 320 };
|
|
494
|
-
const VIS = { public: '+', private: '-', protected: '#', package: '~' };
|
|
495
|
-
// a member's row in UML's form: "+ total: Money [1]", "+ /count: Integer = 0", "+ place(at: Date): Order"; a literal's label
|
|
496
|
-
const obj = x => !!x && typeof x === 'object'; // an entry the classifier reads; null, text or a number is passed over
|
|
497
|
-
function memberText(m) { const p = obj(m.props) ? m.props : {}, has = x => x != null && x !== '', name = (hasKey(VIS, p.visibility) ? VIS[p.visibility] + ' ' : '') + (p.isDerived ? '/' : '') + (m.label ?? '');
|
|
498
|
-
if (/Operation$/.test(m.type || '')) return name + '(' + (Array.isArray(p.parameters) ? p.parameters.filter(obj) : []).map(a => (has(a.direction) && a.direction !== 'in' ? a.direction + ' ' : '') + (a.name ?? '') + (has(a.valueType) ? ': ' + a.valueType : '')).join(', ') + ')' + (has(p.returnType) ? ': ' + p.returnType : '');
|
|
499
|
-
return name + (has(p.valueType) ? ': ' + p.valueType : '') + (has(p.multiplicity) ? ` [${p.multiplicity}]` : '') + (has(p.default) ? ' = ' + p.default : ''); }
|
|
500
|
-
// the classifier as drawn: its stereotypes, its compartments (all: every one, each with every member; comps: those shown,
|
|
501
|
-
// with their rows and how many they leave out), the head's height, and the size its text needs: as wide as its widest text,
|
|
502
|
-
// within minW and maxW (a longer row is cut: classSvg). An abstract name is in italics, which the measure (upright) does
|
|
503
|
-
// not see: it is measured a tenth wider (slant)
|
|
504
|
-
function classParts(n) {
|
|
505
|
-
const hidden = new Set(Array.isArray(n.collapsed) ? n.collapsed : []), cap = Number.isFinite(n.rows) ? Math.max(0, Math.floor(n.rows)) : Infinity, slant = n.abstract ? 1.1 : 1;
|
|
506
|
-
const stereo = (Array.isArray(n.stereotypes) ? n.stereotypes : []).filter(t => t != null && t !== '').map(String), st = stereo.map(t => `«${t}»`);
|
|
507
|
-
const all = (Array.isArray(n.compartments) ? n.compartments : []).filter(obj).map(c => ({ name: String(c.name ?? ''), members: Array.isArray(c.members) ? c.members.filter(obj) : [] }));
|
|
508
|
-
const comps = all.filter(c => !hidden.has(c.name)).map(c => ({ name: c.name, rows: c.members.slice(0, cap).map(m => ({ m, text: memberText(m) })), more: Math.max(0, c.members.length - cap) }));
|
|
509
|
-
const texts = [[n.label ?? '', CL.name, slant], ...st.map(t => [t, CL.row, 1]), ...comps.flatMap(c => [...c.rows.map(r => [r.text, CL.row, 1]), ...(c.more ? [[`+${c.more} more`, CL.row, 1]] : [])])];
|
|
510
|
-
const w = Math.ceil(Math.min(CL.maxW, Math.max(CL.minW, ...texts.map(([t, px, k]) => textW(t, px) * k + 2 * CL.pad))));
|
|
511
|
-
const head = 2 * CL.gap + st.length * CL.st + CL.nl, tall = c => { const k = c.rows.length + (c.more ? 1 : 0); return k ? 2 * CL.gap - 2 + k * CL.rl : CL.empty; };
|
|
512
|
-
return { stereo, st, all, comps, head, tall, slant, size: { w, h: comps.reduce((a, c) => a + tall(c), head) } };
|
|
513
|
-
}
|
|
514
|
-
const words = t => String(t).split('.').pop().replace(/([a-z])([A-Z])/g, (_, a, b) => a + ' ' + b.toLowerCase()); // 'uml.DataType': "Data type"
|
|
515
|
-
function classSvg(n, c, f) { // its outline, its head, then each compartment under a divider, every text cut to the box (fit)
|
|
516
|
-
const { stereo, st, all, comps, head, tall, slant } = classParts(n), { x, y, w, h } = n, max = w - 2 * CL.pad, cx = r2(x + w / 2);
|
|
517
|
-
const font = (px, wt, it) => `style="font:${it ? 'italic ' : ''}${wt} ${px}px var(--um-sans)"`;
|
|
518
|
-
const row = (cls, fill, ty, t, more = '') => `<text x="${r2(x + CL.pad)}" y="${r2(ty)}" class="${cls}" fill="${fill}"${more}>${esc(fit(t, CL.row, max))}</text>`;
|
|
519
|
-
const out = [`<rect x="${x}" y="${y}" width="${w}" height="${h}" class="n-mask"/><rect x="${x}" y="${y}" width="${w}" height="${h}" class="n-body" fill="${f}" stroke="${c}"/>`];
|
|
520
|
-
let ty = y + CL.gap; const hd = [];
|
|
521
|
-
st.forEach(t => { hd.push(`<text x="${cx}" y="${r2(ty + 11)}" text-anchor="middle" class="c-st" fill="var(--um-muted)" ${font(CL.row, 400)}>${esc(fit(t, CL.row, max))}</text>`); ty += CL.st; });
|
|
522
|
-
hd.push(`<text x="${cx}" y="${r2(ty + 14)}" text-anchor="middle" class="c-name" fill="var(--um-text)" ${font(CL.name, 600, n.abstract)} data-o="${esc(n.id)}">${esc(fit(n.label ?? '', CL.name, max / slant))}</text>`);
|
|
523
|
-
out.push(`<g class="c-head" data-band="${r2(y)} ${r2(y + head)}">${hd.join('')}</g>`);
|
|
524
|
-
let top = y + head;
|
|
525
|
-
comps.forEach((C, i) => { const bh = i === comps.length - 1 ? Math.max(y + h - top, 0) : tall(C), at = j => top + CL.gap - 1 + j * CL.rl + 11.5;
|
|
526
|
-
const rows = C.rows.map((r, j) => { const p = obj(r.m.props) ? r.m.props : {};
|
|
527
|
-
return row('c-row', 'var(--um-text)', at(j), r.text, ` ${font(CL.row, 400, /Operation$/.test(r.m.type || '') && p.isAbstract)}${p.isStatic ? ' text-decoration="underline"' : ''}`); });
|
|
528
|
-
if (C.more) rows.push(row('c-more', 'var(--um-muted)', at(C.rows.length), `+${C.more} more`, ` ${font(CL.row, 400)}`));
|
|
529
|
-
out.push(`<g class="c-comp" data-comp="${esc(C.name)}" data-band="${r2(top)} ${r2(top + bh)}"><path d="M${x} ${r2(top)}H${x + w}" class="n-det" stroke="${c}"/>${rows.join('')}</g>`);
|
|
530
|
-
top += bh; });
|
|
531
|
-
// its accessible name (A1): "Class Order (entity, abstract), 2 attributes, 1 operation", every member counted, shown or not
|
|
532
|
-
const counts = all.map(C => `${C.members.length} ${C.members.length === 1 ? C.name.replace(/s$/, '') : C.name}`), why = [...stereo, ...(n.abstract ? ['abstract'] : [])];
|
|
533
|
-
const name = [`${words(n.type || 'uml.Class')} ${n.label ?? ''}${why.length ? ` (${why.join(', ')})` : ''}`, ...counts].join(', ');
|
|
534
|
-
return `<g class="node classifier" data-id="${esc(n.id)}" data-shape="classifier" tabindex="-1" role="img" aria-label="${esc(name)}">${out.join('')}</g>`;
|
|
535
|
-
}
|
|
536
513
|
const SHAPE_NAMES = ['box', 'person', 'browser', 'mobile', 'cylinder', 'pipe', 'document', 'hexagon', 'robot', 'terminal', 'window', 'folder', 'node']; // all 13
|
|
537
514
|
// a card's shape in a notation: its own `shape` first; box draws every card as before (a hook stays a hexagon there too)
|
|
538
515
|
const shapeOf = (n, notation) => { const sh = String(n.shape ?? '').toLowerCase(); if (SHAPE_NAMES.includes(sh)) return sh;
|
|
@@ -610,51 +587,21 @@ function roundPath(pts, r = 9) {
|
|
|
610
587
|
}
|
|
611
588
|
const last = pts[pts.length - 1]; return d + ` L ${last[0]} ${last[1]}`;
|
|
612
589
|
}
|
|
613
|
-
|
|
614
|
-
// a typed line's end labels (1.5): each end's role and multiplicity (e.end.labels: { from: { role, mult }, to: { role, mult } }),
|
|
615
|
-
// each in a box reserved by its end. First choice: 4 px out from the outline along the line, 8 px to one side of it, clear of
|
|
616
|
-
// the end's marker; the role above a line that leaves sideways and left of one that leaves up or down, the multiplicity on
|
|
617
|
-
// the other side. A box that would meet a segment of its own line (7.5 px round it, so markers too), either end's node or
|
|
618
|
-
// a box placed before it moves away from the line, 4 px at a time, on its side and then the other, at the end or centred
|
|
619
|
-
// across it (a short end segment, a loop). pts: the line as drawn; e.s and e.t: its nodes. Layout (1.4) may take the boxes
|
|
620
|
-
// as obstacles; it moves them apart where many lines meet (L5)
|
|
621
|
-
function endLabels(e, pts = e.pts) {
|
|
622
|
-
const T = e.end && e.end.labels, out = []; if (!T || !pts || pts.length < 2) return out;
|
|
623
|
-
const meets = (a, b) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
|
|
624
|
-
const keep = [...pts.slice(1).map((q, i) => { const p = pts[i]; return { x: Math.min(p[0], q[0]) - 7.5, y: Math.min(p[1], q[1]) - 7.5, w: Math.abs(q[0] - p[0]) + 15, h: Math.abs(q[1] - p[1]) + 15 }; }),
|
|
625
|
-
...[e.s, e.t].filter(n => n && Number.isFinite(n.x)).map(n => ({ x: n.x - 2, y: n.y - 2, w: n.w + 4, h: n.h + 4 }))];
|
|
626
|
-
[['from', pts[0], pts[1]], ['to', pts[pts.length - 1], pts[pts.length - 2]]].forEach(([end, p, q]) => {
|
|
627
|
-
const at = T[end] || {}, flat = Math.abs(q[0] - p[0]) >= Math.abs(q[1] - p[1]), dir = Math.sign(flat ? q[0] - p[0] : q[1] - p[1]) || 1;
|
|
628
|
-
[['role', at.role, -1], ['mult', at.mult, 1]].forEach(([kind, t, side]) => { if (t == null || t === '') return;
|
|
629
|
-
const w = r2(textW(t, 10) + 6), h = 14, along = (v, d, mid) => mid ? v - d / 2 : dir > 0 ? v + 4 : v - 4 - d, aside = (v, d, s, o) => s < 0 ? v - o - d : v + o;
|
|
630
|
-
const box = (o, s, mid) => ({ end, kind, text: String(t), x: r2(flat ? along(p[0], w, mid) : aside(p[0], w, s, o)), y: r2(flat ? aside(p[1], h, s, o) : along(p[1], h, mid)), w, h });
|
|
631
|
-
let b = null;
|
|
632
|
-
for (let o = 8; !b && o <= 400; o += 4) for (const s of [side, -side]) for (const mid of [false, true]) { const c = box(o, s, mid); if (!b && ![...keep, ...out].some(k => meets(c, k))) b = c; }
|
|
633
|
-
out.push(b || box(8, side, false)); }); });
|
|
634
|
-
return out;
|
|
635
|
-
}
|
|
636
590
|
let svgSeq = 0;
|
|
637
|
-
|
|
638
|
-
// and to name an end in this table, the line's ends as UML draws them (none, or a name it does not hold, draws no marker);
|
|
639
|
-
// dashed: a dependency's or realization's line; labels: each end's role and multiplicity (endLabels). Each end: [its
|
|
640
|
-
// length, half its height, its path pointing right with the tip at its length, its fill]. Its tip is where the line ends,
|
|
641
|
-
// on the node's outline. Hollow is the stage's background, filled the line's own colour (T2), in either theme
|
|
642
|
-
const ENDS = { none: null, arrow: [10, 4, 'M0 0L10 4L0 8Z', 'line'], open: [10, 5, 'M0 0L10 5L0 10', 'none'], triangle: [13, 7, 'M0 0L13 7L0 14Z', 'hollow'],
|
|
643
|
-
diamond: [16, 6, 'M0 6L8 0L16 6L8 12Z', 'hollow'], 'filled-diamond': [16, 6, 'M0 6L8 0L16 6L8 12Z', 'line'] };
|
|
644
|
-
const endOf = (E, w) => hasKey(ENDS, E[w]) && ENDS[E[w]] ? E[w] : null;
|
|
645
|
-
/* SVG markup. opts: { childCount(id) -> n, subject: id, title } */
|
|
591
|
+
/* SVG markup. opts: { childCount(id) -> n, subject: id, title, under, over } */
|
|
646
592
|
function svgOf(geo, opts = {}) {
|
|
647
593
|
const id = 'g' + (svgSeq++), { nodes, edges, groups, bounds: B, map } = geo;
|
|
594
|
+
const LINE = map ? 'var(--um-arrow)' : 'var(--um-muted)'; // a typed view's lines: 3:1 or more in both themes
|
|
648
595
|
const col = k => `var(--um-${KIND[k][0]})`, fill = k => `var(--um-${KIND[k][0]}-f)`;
|
|
649
596
|
const estyle = e => {
|
|
650
|
-
if (e.style === 'faded') return { ...(map && e.wk ? { c: `var(--um-w-${e.wk})`, m: e.wk, cls: ' w-' + e.wk + ' faded' } : { c:
|
|
597
|
+
if (e.style === 'faded') return { ...(map && e.wk ? { c: `var(--um-w-${e.wk})`, m: e.wk, cls: ' w-' + e.wk + ' faded' } : { c: LINE, m: 'd', cls: ' faded' }), dash: '', op: .35 }; // its own colour, faint, no flow (drift's idle lines)
|
|
651
598
|
if (map && e.wk && !e.style) return { c: `var(--um-w-${e.wk})`, m: e.wk, dash: e.back ? '6 5' : '', cls: ' w-' + e.wk + (e.back ? ' back' : '') };
|
|
652
599
|
const st = e.style || (e.change === 'added' ? 'added' : e.change === 'removed' ? 'removed' : '');
|
|
653
600
|
if (st === 'added' || st === 'emphasis') return { c: 'var(--um-green)', m: 'g', dash: '', flow: st === 'added' || e.animate };
|
|
654
601
|
if (st === 'removed') return { c: 'var(--um-rose)', m: 'r', dash: '5 5', op: .7 };
|
|
655
602
|
if (st === 'security') return { c: 'var(--um-rose)', m: 'r', dash: '5 5' };
|
|
656
603
|
if (st === 'dashed' || st === 'async') return { c: 'var(--um-violet)', m: 'v', dash: '4 4' };
|
|
657
|
-
return { c:
|
|
604
|
+
return { c: LINE, m: 'd', dash: '' };
|
|
658
605
|
};
|
|
659
606
|
// a line that breaks a rule (#70): rose, with a "!" just after its start that names the rule (opts.broken: rule titles by edge id)
|
|
660
607
|
const broke = e => opts.broken ? [...new Set((e.all || [e]).flatMap(x => opts.broken[x._id] || []))] : [];
|
|
@@ -662,11 +609,8 @@ function svgOf(geo, opts = {}) {
|
|
|
662
609
|
const ruleMark = (e, why) => { const [a, b] = e.pts, L = Math.abs(b[0] - a[0]) + Math.abs(b[1] - a[1]) || 1, t = Math.min(18, L / 2) / L, x = a[0] + (b[0] - a[0]) * t, y = a[1] + (b[1] - a[1]) * t;
|
|
663
610
|
return `<g class="rule-mark"><title>Breaks rule: ${esc(why.join('; '))}</title><circle cx="${x}" cy="${y}" r="6.5"/><text x="${x}" y="${y + 3.4}" text-anchor="middle">!</text></g>`; };
|
|
664
611
|
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>`;
|
|
665
|
-
const MC = { d:
|
|
612
|
+
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]})`])) };
|
|
666
613
|
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
|
|
667
|
-
const typed = e => !map && e.end ? e.end : null, ends = [...new Set(edges.flatMap(e => typed(e) ? ['from', 'to'].map(w => endOf(e.end, w)).filter(Boolean).map(k => k + '-' + lstyle(e).m) : []))];
|
|
668
|
-
const endMark = km => { const k = km.slice(0, km.lastIndexOf('-')), m = km.slice(k.length + 1), [L, H, p, fl] = ENDS[k];
|
|
669
|
-
return `<marker id="${id}-${km}" markerWidth="${L}" markerHeight="${2 * H}" refX="${L}" refY="${H}" orient="auto-start-reverse" markerUnits="userSpaceOnUse" overflow="visible"><path d="${p}" fill="${fl === 'line' ? MC[m] : fl === 'hollow' ? 'var(--um-stage-bg)' : 'none'}" stroke="${MC[m]}" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"/></marker>`; };
|
|
670
614
|
// the notation (0.15.0): each card's look, and each line as drawn, its ends on the cards' outlines (a map's cards only)
|
|
671
615
|
const notation = map ? noteOf(opts.shapes) || 'box' : 'box', kidsOf = n => n.kids != null ? n.kids : opts.childCount ? opts.childCount(n.id) : 0;
|
|
672
616
|
// 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
|
|
@@ -674,8 +618,9 @@ function svgOf(geo, opts = {}) {
|
|
|
674
618
|
const ports = map ? sidePorts(geo) : null, looks = new Map(map ? nodes.map(n => [n, cardLook(n, notation, !!(kidsOf(n) || n.opens) && opts.openHint !== false, ports.get(n))])
|
|
675
619
|
: nodes.map(n => [n, n.shape != null && cardLook(n, 'box', false, null)]).filter(x => x[1]));
|
|
676
620
|
const drawn = edges.map(e => map && looks.size ? drawnPts(e, n => looks.get(n) || null) : e.pts);
|
|
621
|
+
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)
|
|
677
622
|
let s = `<svg viewBox="${B.x} ${B.y} ${B.w} ${B.h}" data-w="${B.w}" data-h="${B.h}" role="${map ? 'group' : 'img'}" aria-label="${esc(opts.title || 'Diagram')}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
|
|
678
|
-
<defs>${marker('d',
|
|
623
|
+
<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>`;
|
|
679
624
|
const families = geo.families || [], familyDepth = f => f.parent ? 1 + familyDepth(families.find(p => p.id === f.parent) || {}) : 0;
|
|
680
625
|
families.slice().sort((a,b) => familyDepth(a)-familyDepth(b)).forEach(f => { // in c4 every box is a boundary, dashed as Structurizr draws one (a toned box is dashed in any notation)
|
|
681
626
|
s += `<g class="family${f.tone ? ' toned' : ''}" data-family="${esc(f.id)}"${f.parent ? ` data-parent="${esc(f.parent)}"` : ''}${f.tone ? ` style="--um-tone:${toneOf(f)}"` : ''}><rect class="family-box" x="${f.x}" y="${f.y}" width="${f.w}" height="${f.h}" rx="14" stroke="var(--um-${familyDepth(f) ? 'cyan' : 'border-2'})"${notation === 'c4' ? ' stroke-dasharray="7 5"' : ''}/>
|
|
@@ -711,7 +656,7 @@ function svgOf(geo, opts = {}) {
|
|
|
711
656
|
return { d: roundPath(P), legs, at };
|
|
712
657
|
})() : null;
|
|
713
658
|
const tourAnim = (vals, times) => `<animate attributeName="opacity" values="${vals}" keyTimes="${times.map(f2).join(';')}" dur="${TOUR}s" repeatCount="indefinite"/>`;
|
|
714
|
-
s += '<g class="edges">';
|
|
659
|
+
s += (opts.under || '') + '<g class="edges">'; // a type's layers (registry.js draw)
|
|
715
660
|
(geo.intake || []).forEach(b => {
|
|
716
661
|
const bp = roundPath(looks.size ? drawnPts({ pts: b.pts, t: geo.byId[b.to], ts: 'L' }, n => looks.get(n) || null) : b.pts); // its end on the card's outline, as a line's
|
|
717
662
|
s += `<g class="intake"><title>${esc(b.label)} feeds ${esc((geo.byId[b.to] || {}).label || b.to)}</title><path d="${bp}" class="edge" stroke="var(--um-w-feeds)" marker-end="url(#${id}-feeds)"/>
|
|
@@ -720,10 +665,9 @@ function svgOf(geo, opts = {}) {
|
|
|
720
665
|
${tour ? `<circle r="3.2" class="tour tour-packet" opacity="0"><animateMotion path="${bp}" keyPoints="0;0;1" keyTimes="0;.86;1" calcMode="linear" dur="${TOUR}s" repeatCount="indefinite"/>${tourAnim('0;0;.8;.8;0', [0, .86, .88, .99, 1])}</circle>` : ''}</g>`;
|
|
721
666
|
});
|
|
722
667
|
edges.forEach((e, i) => {
|
|
723
|
-
const E =
|
|
724
|
-
const
|
|
725
|
-
const
|
|
726
|
-
const marks = E ? `${at('to') ? `marker-end="${at('to')}"` : ''} ${at('from') ? `marker-start="${at('from')}"` : ''}` : `marker-end="url(#${id}-${mk})" ${e.both ? `marker-start="url(#${id}-${mk})"` : ''}`;
|
|
668
|
+
const E = T && T.edge(e, i), st = E ? E.st : lstyle(e), why = broke(e), d = roundPath(drawn[i]), mk = st.m + (e.weight > 1 ? '-w' + e.weight : '');
|
|
669
|
+
const tip = 9 * (e.weight > 1 ? 1 + (e.weight - 1) * .35 : 1), fd = roundPath(E ? E.pts : trimLine(drawn[i], e.both ? tip : 0, tip)); // an arrowhead's length (marker refX)
|
|
670
|
+
const marks = E ? E.marks : `marker-end="url(#${id}-${mk})" ${e.both ? `marker-start="url(#${id}-${mk})"` : ''}`;
|
|
727
671
|
if (tour && tour.legs[i]) { const [a, b] = tour.legs[i];
|
|
728
672
|
s += `<path d="${d}" class="tour-glow" stroke="${st.c}" opacity="0">${tourAnim('0;0;.55;.55;0;0', [0, a, Math.min(a + .01, b), b, b + .03, 1])}</path>`; }
|
|
729
673
|
s += `<g class="edge-g${st.cls || ''}${e.weight > 1 ? ' wt-' + e.weight : ''}" data-from="${esc(e.from)}" data-to="${esc(e.to)}" data-i="${i}"><path d="${d}" class="halo"/><path d="${d}" class="hitline"/>
|
|
@@ -731,13 +675,13 @@ function svgOf(geo, opts = {}) {
|
|
|
731
675
|
<path d="${fd}" class="flowline" stroke="${st.c}"/>${(e.all || [e]).some(x => x.edited) && !e.strong ? (() => { let m = [e.pts[0], e.pts[1]];
|
|
732
676
|
e.pts.slice(1).forEach((p, k) => { const a = e.pts[k]; if (Math.abs(p[0] - a[0]) + Math.abs(p[1] - a[1]) > Math.abs(m[1][0] - m[0][0]) + Math.abs(m[1][1] - m[0][1])) m = [a, p]; });
|
|
733
677
|
return `<circle cx="${(m[0][0] + m[1][0]) / 2}" cy="${(m[0][1] + m[1][1]) / 2}" r="4" class="edited-dot"><title>Edited</title></circle>`; })() : ''}${why.length ? ruleMark(e, why) : ''}
|
|
734
|
-
<title>${e.step != null ? 'Step ' + esc(e.step) + ': ' : ''}${esc(e.s.label)} ${e.both ? '⇄' : '→'} ${esc(e.t.label)}${e.label ? ': ' + esc(e.label) : ''}${e.label && e.sublabel ? ' (' + esc(e.sublabel) + ')' : ''}${e.when ? '\n' + esc(e.when) : ''}${why.length ? '\nBreaks rule: ' + esc(why.join('; ')) : ''}</title></g>`;
|
|
678
|
+
<title>${!map && e.aria != null ? esc(e.aria) : `${e.step != null ? 'Step ' + esc(e.step) + ': ' : ''}${esc(e.s.label)} ${e.both ? '⇄' : '→'} ${esc(e.t.label)}${e.label ? ': ' + esc(e.label) : ''}`}${e.label && e.sublabel ? ' (' + esc(e.sublabel) + ')' : ''}${e.when ? '\n' + esc(e.when) : ''}${why.length ? '\nBreaks rule: ' + esc(why.join('; ')) : ''}</title></g>`;
|
|
735
679
|
});
|
|
736
680
|
// the tour's dot rides over the lines and boxes but under the cards: it goes into a card and comes out at its next
|
|
737
681
|
// line, rather than across the card's name
|
|
738
682
|
s += '</g>' + (tour ? `<g class="tour" aria-hidden="true"><circle r="5.5" class="tour-dot"><animateMotion path="${tour.d}" dur="${TOUR}s" repeatCount="indefinite"/></circle></g>` : '') + '<g class="nodes">';
|
|
739
683
|
nodes.forEach(n => {
|
|
740
|
-
if (!map && n.shape
|
|
684
|
+
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)
|
|
741
685
|
const c = col(n.k), f = fill(n.k), removed = n.change === 'removed', planned = n.change === 'planned';
|
|
742
686
|
const L = looks.get(n) || null, S = L && L.S; // a shaped card (0.15.0); null: a box, drawn as before
|
|
743
687
|
const pub = map && n.k === 'm-public', priv = map && n.k === 'm-script', ext = map && n.k === 'm-ext', verb = map && n.k === 'm-verb', hook = map && n.k === 'm-hook' && String(n.shape ?? '').toLowerCase() !== 'box';
|
|
@@ -850,9 +794,9 @@ return I && I.src ? `<rect x="${x - M * 3 / 28}" y="${y - M * 3 / 28}" width="${
|
|
|
850
794
|
const name = `${e.step != null ? `Step ${e.step}: ` : ''}Line ${e.both ? `between ${e.s.label} and ${e.t.label}, both ways` : `from ${e.s.label} to ${e.t.label}`}${said}${broke(e).length ? `, breaks rule ${broke(e).join('; ')}` : ''}`;
|
|
851
795
|
s += `<g class="um-key line-key${e.weight > 1 ? ' wt-' + e.weight : ''}" data-i="${i}" tabindex="0" role="button" aria-pressed="false" aria-label="${esc(name)}"><path d="${roundPath(drawn[i])}" class="key-ring"/></g>`; });
|
|
852
796
|
}
|
|
853
|
-
s += '</g
|
|
797
|
+
s += '</g>' + (opts.over || '') + '<g class="labels depth-read">';
|
|
854
798
|
edges.forEach((e, i) => {
|
|
855
|
-
if (
|
|
799
|
+
if (T) s += T.labels(i);
|
|
856
800
|
if (!e.lbl) return;
|
|
857
801
|
const L = e.lbl;
|
|
858
802
|
if (map) {
|
|
@@ -892,10 +836,10 @@ const labelW = e => textW(fit((e.step != null ? e.step + ' · ' : '') + (e.label
|
|
|
892
836
|
leaves a at row oa inside it and reaches b at row ob, as rowsFor's links do (the middle when not given). Each line into a
|
|
893
837
|
box has its own entry, so two that cross on the way in to its cards count as crossing; they all met it at one point.
|
|
894
838
|
fixed: items at a set row in columns of their own, which stay as they are (a box's far ends, in blockPos). */
|
|
895
|
-
function untangle(cols, links, h = () => 1, fixed =
|
|
896
|
-
const colOf =
|
|
897
|
-
const segs = [], extra = new Set(), want =
|
|
898
|
-
const tops = () => { const at =
|
|
839
|
+
function untangle(cols, links, h = () => 1, fixed = Object.create(null)) { // maps by id: no prototype (M3)
|
|
840
|
+
const colOf = Object.create(null); cols.forEach((c, k) => c.forEach(id => { colOf[id] = k; }));
|
|
841
|
+
const segs = [], extra = new Set(), want = Object.create(null), hh = id => extra.has(id) ? 1 : h(id), mid = (id, o) => o ?? (hh(id) - 1) / 2;
|
|
842
|
+
const tops = () => { const at = Object.create(null); cols.forEach(c => { let r = 0; c.forEach(id => { at[id] = fixed[id] ?? r; r += hh(id); }); }); return at; }; // each item's top row
|
|
899
843
|
const at0 = tops();
|
|
900
844
|
links.forEach(([a, b, oa, ob], n) => {
|
|
901
845
|
const ka = colOf[a], kb = colOf[b]; if (ka == null || kb == null || kb <= ka) return;
|
|
@@ -907,7 +851,7 @@ function untangle(cols, links, h = () => 1, fixed = {}) {
|
|
|
907
851
|
});
|
|
908
852
|
if (extra.size) cols.forEach((c, k) => { const ps = [...extra].filter(p => colOf[p] === k);
|
|
909
853
|
if (ps.length) cols[k] = [...c.map(id => [id, at0[id], 0]), ...ps.map(p => [p, want[p], 1])].sort((x, y) => x[1] - y[1] || x[2] - y[2]).map(x => x[0]); });
|
|
910
|
-
const ins =
|
|
854
|
+
const ins = Object.create(null), outs = Object.create(null); segs.forEach(([a, b, oa, ob]) => { (outs[a] ||= []).push([b, ob, oa]); (ins[b] ||= []).push([a, oa, ob]); });
|
|
911
855
|
const crossings = () => { const at = tops(); let n = 0;
|
|
912
856
|
for (let i = 0; i < segs.length; i++) for (let j = i + 1; j < segs.length; j++) { const [a, b, oa, ob] = segs[i], [c, d, oc, od] = segs[j];
|
|
913
857
|
if (colOf[a] === colOf[c] && (at[a] + oa - at[c] - oc) * (at[b] + ob - at[d] - od) < 0) n++; }
|
|
@@ -937,9 +881,10 @@ function pullSources(ids, links, rank) {
|
|
|
937
881
|
adjacent violators); on half rows from 0. A line between cards on one row runs straight, past fewer cards. h: an item's
|
|
938
882
|
height in rows (a box in a view with boxes); a link [a, b, oa, ob] leaves a at row oa inside it and reaches b at row
|
|
939
883
|
ob (a box's card), else at its middle. fixed: items at a set row, in columns of their own that stay (as untangle's). */
|
|
940
|
-
function rowsFor(cols, links, h = () => 1, fixed =
|
|
941
|
-
const row =
|
|
942
|
-
const
|
|
884
|
+
function rowsFor(cols, links, h = () => 1, fixed = Object.create(null)) {
|
|
885
|
+
const row = Object.create(null); if (!cols.length) return row; // an empty view
|
|
886
|
+
const colOf = Object.create(null); cols.forEach((c, k) => { let r = 0; c.forEach(id => { row[id] = fixed[id] ?? r; r += h(id); colOf[id] = k; }); });
|
|
887
|
+
const ins = Object.create(null), outs = Object.create(null), at = (id, o) => o ?? (h(id) - 1) / 2; // where a link meets an item, in rows from its top
|
|
943
888
|
links.forEach(([a, b, oa, ob]) => { if (colOf[a] == null || colOf[b] == null || colOf[b] <= colOf[a]) return;
|
|
944
889
|
(outs[a] ||= []).push([b, at(b, ob), at(a, oa)]); (ins[b] ||= []).push([a, at(a, oa), at(b, ob)]); });
|
|
945
890
|
const median = xs => { const s = xs.slice().sort((a, b) => a - b), m = s.length >> 1; return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2; };
|
|
@@ -948,7 +893,7 @@ function rowsFor(cols, links, h = () => 1, fixed = {}) {
|
|
|
948
893
|
while (blocks.length > 1 && blocks.at(-2).sum / blocks.at(-2).n > blocks.at(-1).sum / blocks.at(-1).n) { const b = blocks.pop(), a = blocks.at(-1); a.ids.push(...b.ids); a.sum += b.sum; a.n += b.n; } });
|
|
949
894
|
off = 0; blocks.forEach(b => b.ids.forEach(id => { row[id] = b.sum / b.n + off; off += h(id); })); };
|
|
950
895
|
for (const [from, to, step] of [[1, cols.length, 1], [cols.length - 2, -1, -1], [1, cols.length, 1]])
|
|
951
|
-
for (let k = from; k !== to; k += step) { if (cols[k].every(id => id in fixed)) continue; const want =
|
|
896
|
+
for (let k = from; k !== to; k += step) { if (cols[k].every(id => id in fixed)) continue; const want = Object.create(null);
|
|
952
897
|
cols[k].forEach(id => { const nb = (step > 0 ? ins : outs)[id] || []; want[id] = nb.length ? median(nb.map(([x, ox, o]) => row[x] + ox - o)) : row[id]; });
|
|
953
898
|
fit(cols[k], want); }
|
|
954
899
|
const low = Math.min(...Object.keys(row).filter(id => !(id in fixed)).map(id => row[id]));
|
|
@@ -1054,29 +999,12 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
|
|
|
1054
999
|
gapAfter[c] = Math.max(gapAfter[c] || 0, w + 54 + box);
|
|
1055
1000
|
[e.from, e.to].forEach(id => { const k = c + '|' + id, t = sum[k] ||= { w: -6, box: 0 }; t.w += w + 6; t.box = Math.max(t.box, box); gapAfter[c] = Math.max(gapAfter[c], t.w + 54 + t.box); }); }); }
|
|
1056
1001
|
const extraX = { ...(room || {}).x }, extraY = { ...(room || {}).y }; // more room after a column (row): for lines with no route, and where a box would touch another box (below)
|
|
1057
|
-
//
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
// node's top lies left of its right side (0 before ports are known, for the room: above the node's row, right of its column)
|
|
1061
|
-
const loopDims = (list, out) => { let D = 0, R = 0, half = 0;
|
|
1062
|
-
return list.map((e, k) => { const lw = e.label || e.step != null ? labelW(e) : 0, [bw, bh] = dg.turned ? [lw && 18, lw] : [lw, lw && 18];
|
|
1063
|
-
D = k ? D + half + bh / 2 + 10 : STUB + 6 + bh / 2; half = bh / 2;
|
|
1064
|
-
R = Math.max(k ? R + 2 * SEP : STUB + 4, bw + 12 - out[k]); return { D, R, top: D + half }; }); };
|
|
1065
|
-
const loops = Object.create(null); if (dg.typed && !dg.abs) (dg.edges || []).forEach(e => { if (e.from === e.to && byId[e.from]) (loops[e.from] ||= []).push(e); });
|
|
1066
|
-
Object.entries(loops).forEach(([id, list]) => { const n = byId[id], d = loopDims(list, list.map(() => 0)).at(-1);
|
|
1067
|
-
extraY[n.row - .5] = Math.max(extraY[n.row - .5] || 0, d.top + STUB + 7 - RG);
|
|
1068
|
-
extraX[n.col] = Math.max(extraX[n.col] || 0, d.R + STUB + 6 - Math.max(CG, gapAfter[n.col] || 0)); });
|
|
1069
|
-
// a typed view's rows (stage 1 note §4): each as tall as its tallest node, a node on a half row counting toward both rows, and
|
|
1070
|
-
// NH with none. With every node NH tall, the pitch is a map's
|
|
1071
|
-
const rowH = {}, rowTop = {}; if (dg.typed && !dg.abs && nodes.length) {
|
|
1072
|
-
nodes.forEach(n => [Math.floor(n.row), Math.ceil(n.row)].forEach(k => { rowH[k] = Math.max(rowH[k] ?? 0, n.h); }));
|
|
1073
|
-
const r0 = Math.min(...Object.keys(rowH).map(Number)), r1 = Math.max(...Object.keys(rowH).map(Number));
|
|
1074
|
-
for (let k = r0, y = r0 * pitch; k <= r1 + 1; k++) { rowTop[k] = y; y += (rowH[k] ?? NH) + RG; } }
|
|
1075
|
-
const rowAt = r => { const k = Math.floor(r), f = r - k; return rowTop[k] == null || rowTop[k + 1] == null ? r * pitch : rowTop[k] + f * (rowTop[k + 1] - rowTop[k]); };
|
|
1076
|
-
const rowHt = r => Math.max(rowH[Math.floor(r)] ?? NH, rowH[Math.ceil(r)] ?? NH); // a typed row's height, a half row's the taller of its two
|
|
1002
|
+
// a typed view's lines from a node to itself, drawn as loops (risks.md F1): the room they take, their ports and routes (src/typed/layout.js)
|
|
1003
|
+
const loops = Object.create(null), LP = dg.typed && !dg.abs ? TYPED.loops(loops, dg, { byId, extraX, extraY, STUB, RG, CG, SEP, gapAfter }) : null;
|
|
1004
|
+
const rows = dg.typed && TYPED.rows(dg, nodes, pitch, NH, RG); // a typed view's rows (src/typed/layout.js)
|
|
1077
1005
|
const gap = c => Math.max(CG, gapAfter[c] || 0) + (extraX[c] || 0);
|
|
1078
1006
|
const cx = c => Number.isInteger(c) ? colX[c] : colX[Math.floor(c)] + (c % 1) * ((colW[Math.floor(c)] || NW) + gap(Math.floor(c)));
|
|
1079
|
-
const rowY = r => Object.keys(extraY).reduce((y, k) => +k < r ? y + extraY[k] : y,
|
|
1007
|
+
const rowY = r => Object.keys(extraY).reduce((y, k) => +k < r ? y + extraY[k] : y, rows ? rows.at(r) : r * pitch);
|
|
1080
1008
|
const place = () => {
|
|
1081
1009
|
x = 0; for (let c = Math.floor(cols[0] ?? 0); c <= Math.ceil(cols[cols.length - 1] ?? 0); c++) { colX[c] = x; x += (colW[c] || NW) + gap(c); }
|
|
1082
1010
|
nodes.forEach(n => { n.x = cx(n.col) + ((colW[n.col] || n.w) - n.w) / 2; n.y = rowY(n.row); if (map && n.k === 'm-script') { n.h = NH - 22; n.y += 11; } });
|
|
@@ -1191,7 +1119,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
|
|
|
1191
1119
|
if (near && !through([...pts, ...up], e)) { e.fixed = [...pts, ...up]; e.under = k + 1; }
|
|
1192
1120
|
else if (!through([...pts, ...bottom], e)) { e.fixed = [...pts, ...bottom]; e.under = true; } // one that would cut through a card is left to the router
|
|
1193
1121
|
});
|
|
1194
|
-
|
|
1122
|
+
if (LP) LP.mark(edges);
|
|
1195
1123
|
const free = edges.filter(e => !e.fixed);
|
|
1196
1124
|
edges.filter(e => e.fixed && !e[OWN_ROUTE]).forEach(e => { e.pts = e.fixed; e.p1 = e.pts[0]; e.p2 = e.pts[e.pts.length - 1]; e.fs = 'R'; e.ts = e.under > 0 ? 'R' : e.under ? 'B' : 'L'; e.stubs = []; e.how = e.under ? 'under' : 'comb'; });
|
|
1197
1125
|
const sides = {};
|
|
@@ -1226,10 +1154,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
|
|
|
1226
1154
|
});
|
|
1227
1155
|
free.forEach(e => { pushSide(e.s, e.fs, e, e.t); pushSide(e.t, e.ts, e, e.s); });
|
|
1228
1156
|
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
|
|
1229
|
-
|
|
1230
|
-
const loopsOf = n => edges.filter(e => e[OWN_ROUTE] === 'loop' && e.s === n);
|
|
1231
|
-
edges.filter(e => e[OWN_ROUTE] === 'loop').forEach(e => { const n = e.s, k = loopsOf(n).indexOf(e);
|
|
1232
|
-
pushSide(n, 'R', e, { x: n.x + n.w, y: -1e6 + k, w: 0, h: 0 }); pushSide(n, 'T', e, { x: 1e6 - k, y: n.y, w: 0, h: 0 }); });
|
|
1157
|
+
if (LP) LP.ports(pushSide);
|
|
1233
1158
|
const portAt = (it, side, p) => { if (it.n === it.e.s && it.e.fs === side) it.e.p1 = p; else it.e.p2 = p; };
|
|
1234
1159
|
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
|
|
1235
1160
|
const side = key.split('|').pop(), vertical = side === 'L' || side === 'R';
|
|
@@ -1267,31 +1192,8 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
|
|
|
1267
1192
|
});
|
|
1268
1193
|
});
|
|
1269
1194
|
edges.filter(e => e.under > 0).forEach(e => { const P = e.fixed; e.pts = e.fixed = [...P.slice(0, 4), [P[3][0], e.p2[1]], e.p2]; });
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
const corners = []; [...new Set(edges.filter(e => e[OWN_ROUTE] === 'loop').map(e => e.s))].forEach(n => { const list = loopsOf(n);
|
|
1273
|
-
loopDims(list, list.map(e => n.x + n.w - e.p2[0])).forEach(({ D, R }, k) => { const e = list[k], x = n.x + n.w + R, y = n.y - D;
|
|
1274
|
-
e.pts = e.fixed = [e.p1, [x, e.p1[1]], [x, y], [e.p2[0], y], e.p2];
|
|
1275
|
-
e.pts.slice(1, 4).forEach(([px, py]) => corners.push({ x: px - SEP, y: py - SEP, w: 2 * SEP, h: 2 * SEP })); }); });
|
|
1276
|
-
// A view laid out down (turned): the labels of lines between one pair are wider than the 16 px between the lines, so the
|
|
1277
|
-
// lines fan out in the gap between the pair's columns, each to a lane of its own that holds its label. Above the middle, the
|
|
1278
|
-
// outer line turns nearer each node, so none crosses another; a group the gap or another node leaves no room for goes to the router
|
|
1279
|
-
if (dg.typed && dg.turned) { const pairs = new Map(), lr = e => e.fs === 'R' ? [e.p1, e.p2] : [e.p2, e.p1];
|
|
1280
|
-
free.forEach(e => { const [l, r] = e.fs === 'R' ? [e.s, e.t] : [e.t, e.s], k = l.id + '\u0001' + r.id;
|
|
1281
|
-
if (/^(RL|LR)$/.test(e.fs + e.ts) && l.x + l.w < r.x) pairs.set(k, [...(pairs.get(k) || []), e]); });
|
|
1282
|
-
pairs.forEach(list => { if (list.length < 2 || !list.some(e => e.label || e.step != null)) return;
|
|
1283
|
-
list.sort((a, b) => lr(a)[0][1] - lr(b)[0][1]); if (list.some((e, i) => i && lr(e)[1][1] <= lr(list[i - 1])[1][1])) return;
|
|
1284
|
-
const bh = list.map(e => e.label || e.step != null ? labelW(e) : 0), gaps = bh.map((h, i) => i ? Math.max(SEP, (bh[i - 1] + h) / 2 + 6) : 0);
|
|
1285
|
-
const mid = list.reduce((a, e) => a + lr(e)[0][1] + lr(e)[1][1], 0) / (2 * list.length), lanes = [];
|
|
1286
|
-
gaps.reduce((y, g, i) => (lanes[i] = y + g), mid - gaps.reduce((a, g) => a + g, 0) / 2);
|
|
1287
|
-
// each line's turn on each side: the k-th line turning towards its lane from the outside turns k SEP px past the stub
|
|
1288
|
-
const turns = side => { const at = []; const up = list.map((e, i) => i).filter(i => lanes[i] < lr(list[i])[side][1]), down = list.map((e, i) => i).filter(i => lanes[i] >= lr(list[i])[side][1]).reverse();
|
|
1289
|
-
[up, down].forEach(g => g.forEach((i, k) => { at[i] = lr(list[i])[side][0] + (side ? -1 : 1) * (STUB + k * SEP); })); return at; };
|
|
1290
|
-
const xl = turns(0), xr = turns(1);
|
|
1291
|
-
if (Math.min(...xr) - Math.max(...xl) < 18 + 16) return;
|
|
1292
|
-
const pts = list.map((e, i) => { const [a, b] = lr(e), P = simplify([a, [xl[i], a[1]], [xl[i], lanes[i]], [xr[i], lanes[i]], [xr[i], b[1]], b]); return e.fs === 'R' ? P : P.reverse(); });
|
|
1293
|
-
if (list.some((e, i) => through(pts[i], e))) return;
|
|
1294
|
-
list.forEach((e, i) => { Object.assign(e, { [OWN_ROUTE]: 'fan', fixed: pts[i], pts: pts[i], stubs: [], how: 'fan' }); free.splice(free.indexOf(e), 1); }); }); }
|
|
1195
|
+
const corners = LP ? LP.route() : []; // a loop's corners: obstacles to the other routes
|
|
1196
|
+
if (dg.typed && dg.turned) TYPED.fan(free, { simplify, through, STUB, SEP }); // a view laid out down: lines between one pair fan out (src/typed/layout.js)
|
|
1295
1197
|
const BEND = map ? 60 : 26, CROSS = 900; // maps: few bends
|
|
1296
1198
|
const OUT = { R: [1, 0], L: [-1, 0], T: [0, -1], B: [0, 1] };
|
|
1297
1199
|
const nodeRects = nodes.map(n => ({ x: n.x, y: n.y, w: n.w, h: n.h }));
|
|
@@ -1301,7 +1203,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
|
|
|
1301
1203
|
nodes.forEach(n => gapLinesY.push(n.y - RG / 2, n.y + n.h + RG / 2));
|
|
1302
1204
|
} else {
|
|
1303
1205
|
Object.keys(colX).map(Number).forEach(c => gapLinesX.push(colX[c] - CG / 2, colX[c] + (colW[c] || NW) + CG / 2));
|
|
1304
|
-
[...new Set(nodes.map(n => n.y))].forEach(y => gapLinesY.push(y - RG / 2, y + (
|
|
1206
|
+
[...new Set(nodes.map(n => n.y))].forEach(y => gapLinesY.push(y - RG / 2, y + (rows ? rows.ht(nodes.find(n => n.y === y).row) : NH) + RG / 2));
|
|
1305
1207
|
}
|
|
1306
1208
|
const routed = [], reserved = [];
|
|
1307
1209
|
const inRect = (p, r) => p[0] > r.x && p[0] < r.x + r.w && p[1] > r.y && p[1] < r.y + r.h;
|
|
@@ -1675,6 +1577,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
|
|
|
1675
1577
|
const xs = [...families.flatMap(f => [f.x,f.x+f.w]), ...(dg.lanes || []).flatMap(l => [l.x, l.x + l.w]), ...nodes.flatMap(n => [n.x - 14, n.x + n.w + 8]), ...groups.flatMap(g => [g.x, g.x2]), ...edges.flatMap(e => e.pts.map(p => p[0])), ...placed.flatMap(p => [p.x, p.x + p.w])];
|
|
1676
1578
|
const ys = [...families.flatMap(f => [f.y,f.y+f.h]), ...(dg.lanes || []).flatMap(l => [l.y, l.y + l.h]), ...nodes.flatMap(n => [n.y - 14, n.y + n.h + (n.tag ? 18 : 8)]), ...groups.flatMap(g => [g.y, g.y2]), ...edges.flatMap(e => e.pts.map(p => p[1])), ...placed.flatMap(p => [p.y, p.y + p.h])];
|
|
1677
1579
|
intake.forEach(b => { xs.push(b.cx - b.r - 8 - textW(b.label || '', 10.5), b.cx + b.r); ys.push(b.cy - b.r, b.cy + b.r); });
|
|
1580
|
+
if (!xs.length) xs.push(0), ys.push(0); // a view with nothing in it: an empty stage
|
|
1678
1581
|
const M = 36, minX = Math.min(...xs) - M, minY = Math.min(...ys) - M;
|
|
1679
1582
|
return { ms: __ms, ...(labelRoom ? { labelRoom } : {}), stats, nodes, edges, subAndChips: !!dg.subAndChips, groups, families, intake, tour: dg.tour || null, byId, map, hub: dg.hub || null, lanes: dg.lanes || [], blocks: dg.blocks || [], bounds: { x: minX, y: minY, w: Math.max(...xs) - minX + M, h: Math.max(...ys) - minY + M } };
|
|
1680
1583
|
}
|
|
@@ -1817,49 +1720,6 @@ function auditSvg(svg, geo, where) {
|
|
|
1817
1720
|
for (let i = 0; i < els.length; i++) for (let j = i + 1; j < els.length; j++) if (hit(els[i].b, els[j].b, -1)) issues.push(`${where}: edge labels "${els[i].txt}" and "${els[j].txt}" overlap`);
|
|
1818
1721
|
return issues;
|
|
1819
1722
|
}
|
|
1820
|
-
/* A typed view's layout (work package 1.4): layoutGraph's plain layout with what a typed view needs, none of it on a map.
|
|
1821
|
-
Rows as tall as their tallest node (stage 1 note §4); a line from a node to itself drawn as a loop (risks.md F1); lines
|
|
1822
|
-
between one pair each with its own ports and label (F2); `direction: "down"` (note §6); and a memo that lays the view out
|
|
1823
|
-
again only when its geometry changed (F7). Nodes come with their sizes, `w` and `h`, which their shapes give (1.5). */
|
|
1824
|
-
|
|
1825
|
-
// what moves a typed view's geometry: every field of the view layoutGraph reads but VIEW_PAINT (a test holds the two lists to
|
|
1826
|
-
// layered.js), and these of a node and a line. Anything else (a colour, a `props` field) only repaints
|
|
1827
|
-
const VIEW_GEO = ['abs', 'authors', 'blocks', 'containers', 'direction', 'fan', 'flipped', 'gap', 'groups', 'hub', 'intake', 'laneGap', 'lanes', 'mainSteps', 'nodeH', 'quick', 'repair', 'rgap', 'turned', 'typed'];
|
|
1828
|
-
const VIEW_PAINT = ['tour', 'subAndChips'], NODE_GEO = ['id', 'col', 'row', 'w', 'h', 'ax', 'ay'], EDGE_GEO = ['id', 'from', 'to', 'label', 'step', 'fromSide', 'toSide', 'back', 'fixed', 'sublabel'];
|
|
1829
|
-
const geometryKey = dg => JSON.stringify([VIEW_GEO.map(k => dg[k]), (dg.nodes || []).map(n => NODE_GEO.map(k => n[k])), (dg.edges || []).map(e => EDGE_GEO.map(k => e[k]))]);
|
|
1830
|
-
|
|
1831
|
-
// direction "down": laid out left to right with every box turned, then turned back, x for y and the sides L for T, R for B
|
|
1832
|
-
const SIDE_T = { L: 'T', T: 'L', R: 'B', B: 'R' }, swapXY = p => [p[1], p[0]];
|
|
1833
|
-
const flipBox = r => { if (r) [r.x, r.y, r.w, r.h] = [r.y, r.x, r.h, r.w]; };
|
|
1834
|
-
function transpose(g) {
|
|
1835
|
-
[...g.nodes, ...(g.families || []), g.bounds].forEach(flipBox);
|
|
1836
|
-
(g.groups || []).forEach(r => { [r.x, r.y, r.x2, r.y2] = [r.y, r.x, r.y2, r.x2]; });
|
|
1837
|
-
g.edges.forEach(e => { e.pts = e.pts.map(swapXY); if (e.fixed) e.fixed = e.pts; e.p1 = swapXY(e.p1); e.p2 = swapXY(e.p2);
|
|
1838
|
-
e.fs = SIDE_T[e.fs]; e.ts = SIDE_T[e.ts]; e.stubs = (e.stubs || []).map(s => s.map(swapXY)); flipBox(e.lbl); });
|
|
1839
|
-
return g;
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
// the geometry kept, with the paint of the view given in place of the paint it was laid out with: each node's own fields, and
|
|
1843
|
-
// each line's, by its id (typed lines have ids); a field the view no longer gives goes. The geometric fields are the same in both
|
|
1844
|
-
function repaint(g, was, dg) {
|
|
1845
|
-
const by = (list, key) => new Map((list || []).filter(x => x && x[key] != null).map(x => [x[key], x]));
|
|
1846
|
-
[[g.nodes, was.nodes, dg.nodes], [g.edges, was.edges, dg.edges]].forEach(([out, a, b]) => { const A = by(a, 'id'), B = by(b, 'id');
|
|
1847
|
-
out.forEach(x => { const old = A.get(x.id), now = B.get(x.id); if (!now) return; for (const k in old) if (!(k in now)) delete x[k]; Object.assign(x, now); }); });
|
|
1848
|
-
return g;
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
/* dg: layoutGraph's input, with `direction` ("right", the default, or "down"). memo: an object kept for the view between
|
|
1852
|
-
calls; while the geometry key is the same, the geometry it holds is repainted and returned, and nothing is laid out or
|
|
1853
|
-
routed. memo.runs counts the layouts. */
|
|
1854
|
-
function layoutTyped(dg, memo) {
|
|
1855
|
-
const key = memo && geometryKey(dg);
|
|
1856
|
-
if (memo && memo.key === key) { const g = repaint(memo.g, memo.dg, dg); memo.dg = dg; return g; }
|
|
1857
|
-
const t = { ...dg, typed: true }, g = dg.direction === 'down'
|
|
1858
|
-
? transpose(layoutGraph({ ...t, turned: true, nodes: (dg.nodes || []).map(n => ({ ...n, w: n.h || dg.nodeH || 64, h: n.w || NW })) }, false))
|
|
1859
|
-
: layoutGraph(t, false);
|
|
1860
|
-
if (memo) Object.assign(memo, { key, g, dg, runs: (memo.runs || 0) + 1 });
|
|
1861
|
-
return g;
|
|
1862
|
-
}
|
|
1863
1723
|
/* ================= diagram engine (shared by document diagrams and the map) =================
|
|
1864
1724
|
Style adapted from tt-a1i/archify (MIT): semantic palette, opaque mask under a translucent
|
|
1865
1725
|
fill, per-kind sigils, grid canvas, orthogonal rounded routes, arrow markers. */
|
|
@@ -2209,7 +2069,7 @@ function mapContext(model, opts = {}) {
|
|
|
2209
2069
|
// the box's lines to cards outside it (#86): each far end at its row from the box's top, where the pass before put both.
|
|
2210
2070
|
// ponytail: three passes, so a far end that moves in the last one is followed where it was, and a far end in the card's
|
|
2211
2071
|
// own column, or a line to a box, places nothing; passes until nothing moves, if a map needs them
|
|
2212
|
-
const far = [], fixed =
|
|
2072
|
+
const far = [], fixed = Object.create(null), top = P && g != null && was[g] ? Object.keys(was[g].pos).map(c => P[c] && P[c][1] - was[g].pos[c][1]).find(r => r != null) : null;
|
|
2213
2073
|
if (top != null) fwd.forEach(e => { const fin = holds(g, e.from); if (fin === holds(g, e.to)) return;
|
|
2214
2074
|
const [mine, other] = fin ? [e.from, e.to] : [e.to, e.from], x = itemIn(mine, g), p = P[mine], q = P[other]; if (x === undefined || !p || !q || p[0] === q[0]) return;
|
|
2215
2075
|
const id = '\u0002' + far.length, left = q[0] < p[0]; fixed[id] = q[1] - top;
|
|
@@ -2232,7 +2092,10 @@ function mapContext(model, opts = {}) {
|
|
|
2232
2092
|
for (const order of [false, true]) { was = now; now = {}; P = block(null, P, order).pos; } // levelled by the lines from outside, then ordered by them too
|
|
2233
2093
|
return P;
|
|
2234
2094
|
}
|
|
2235
|
-
|
|
2095
|
+
// a typed view whose type lays it out (1.9): the plain flow's ranking, with the lines the type turns round for it
|
|
2096
|
+
const ownLayout = vid => { if (D.schemaVersion !== 2) return null; const T = diagramOf(V[vid]); return T && T.layout && (T.layout.run || typeof T.layout === 'function') ? T : null; }; // version 1: a map
|
|
2097
|
+
const autoPos = vid => ownLayout(vid) ? flowPos(vid, { flip: ownLayout(vid).rankFlip }) : ((V[vid] || {}).containers || []).length ? blockPos(vid) : bestFlow(vid);
|
|
2098
|
+
const memos = opts.memos || Object.create(null); // per view, layoutTyped's memo, kept across update()s: an edit that moves nothing lays out nothing
|
|
2236
2099
|
const level = e => !/^(top|bottom)$/.test(e.fromSide) && !/^(top|bottom)$/.test(e.toSide); // a line into a card's top or bottom asks for no row
|
|
2237
2100
|
/* A small plain view is routed a few ways and keeps the one the layout check scores best: cards level with their
|
|
2238
2101
|
neighbours, or packed from the top as before, with cards no line comes into beside their callee or not. A cheap
|
|
@@ -2252,15 +2115,16 @@ function mapContext(model, opts = {}) {
|
|
|
2252
2115
|
const fanIn = vid => { const k = {}; return viewEdges(V[vid] || {}).some(e => e.from !== e.to && (k[e.to] = (k[e.to] || 0) + 1) >= 6); };
|
|
2253
2116
|
const drawn = {}; // the way bestFlow kept, routed already: the view's next buildGeo takes it once instead of routing it again
|
|
2254
2117
|
function flowPos(vid, o = {}) {
|
|
2255
|
-
const v = V[vid],
|
|
2118
|
+
const v = o.flip ? { ...V[vid], edges: viewEdges(V[vid]).map(e => o.flip(e) ? { ...e, from: e.to, to: e.from } : e) } : V[vid];
|
|
2119
|
+
const lp = loopsOf(v), fwd = lp.edges.filter((e, k) => !lp.back.has(k) && e.from !== e.to);
|
|
2256
2120
|
// cards with no line go in rows of their own under the view (#46), as wide as its columns; they stacked with the sources
|
|
2257
2121
|
const all = (v.nodeIds || []).filter(i => N[i]), lined = new Set(lp.edges.filter(e => e.from !== e.to).flatMap(e => [e.from, e.to]));
|
|
2258
2122
|
const loose = lined.size ? all.filter(i => !lined.has(i)) : [], ids = all.filter(i => !loose.includes(i));
|
|
2259
|
-
const rank =
|
|
2123
|
+
const rank = Object.create(null); ids.forEach(i => { rank[i] = 0; });
|
|
2260
2124
|
for (let it = 0; it <= ids.length; it++) fwd.forEach(e => { if (rank[e.to] < rank[e.from] + 1) rank[e.to] = rank[e.from] + 1; });
|
|
2261
2125
|
const seen = id => { const k = lp.edges.findIndex(e => e.to === id || e.from === id); return k < 0 ? 999 : k; };
|
|
2262
2126
|
const sink = i => isState(i) && !fwd.some(e => e.from === i); // state that leads nowhere sits along the bottom
|
|
2263
|
-
const pos =
|
|
2127
|
+
const pos = Object.create(null), links = fwd.map(e => [e.from, e.to]);
|
|
2264
2128
|
const order = rk => { const cols = {}; ids.forEach(i => (cols[rk[i]] ||= []).push(i));
|
|
2265
2129
|
const ranks = Object.keys(cols).map(Number).sort((a, b) => a - b), mains = ranks.map(c => cols[c].filter(i => !sink(i)).sort((a, b) => seen(a) - seen(b)));
|
|
2266
2130
|
untangle(mains, links); return { cols, ranks, mains }; }; // in reading order, unless another order crosses less (#53)
|
|
@@ -2285,8 +2149,8 @@ function mapContext(model, opts = {}) {
|
|
|
2285
2149
|
// with no layout (a change of notation draws the cards again and nothing else)
|
|
2286
2150
|
const notationOf = vid => noteOf((V[vid] || {}).shapes) || noteOf(D.shapes) || 'box';
|
|
2287
2151
|
function drawView(vid, g, opts = {}) {
|
|
2288
|
-
const v = V[vid], shapes = noteOf(opts.shapes) || notationOf(vid);
|
|
2289
|
-
return { markup: svgOf(g, { title: v.title, subject: subjectOf(v), icons: D.icons, broken: brokenIn(vid), ...opts, shapes }), shapes };
|
|
2152
|
+
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
|
|
2153
|
+
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 };
|
|
2290
2154
|
}
|
|
2291
2155
|
// the rules a view's lines break (#70), by model edge id: svgOf draws such a line rose, with a mark that names them
|
|
2292
2156
|
let ruled = null;
|
|
@@ -2296,7 +2160,7 @@ function mapContext(model, opts = {}) {
|
|
|
2296
2160
|
(ruled ||= rules(D)).forEach(b => { if (b.view === vid) b.targets.forEach(t => { if (t.type === 'edge') t.ids.forEach(i => { const l = by[i] ||= []; if (!l.includes(title(b.rule))) l.push(title(b.rule)); }); }); });
|
|
2297
2161
|
return Object.keys(by).length ? by : null;
|
|
2298
2162
|
};
|
|
2299
|
-
const layoutCache =
|
|
2163
|
+
const layoutCache = Object.create(null); // M3
|
|
2300
2164
|
// the cards a card opens: its inner view's, else the count the host gives for one it has not loaded (opensCount)
|
|
2301
2165
|
const kidCount = (nid, vid) => { const c = childOf(nid, vid), k = c && c !== vid ? (V[c].nodeIds || []).length : 0, n = +nodeIn(nid, vid).opensCount;
|
|
2302
2166
|
return k || (opensOf(nid, vid) && n > 0 ? Math.floor(n) : 0); };
|
|
@@ -2324,7 +2188,15 @@ function mapContext(model, opts = {}) {
|
|
|
2324
2188
|
// quick: a candidate the layout searches below score and may drop, laid out once, as it was before 0.6.0 (no room for lines
|
|
2325
2189
|
// with no route, no way round the cards for them, no warning)
|
|
2326
2190
|
function buildGeo(vid, override, repair = (V[vid] || {}).repair !== false, quick = false) { // a view's repair: false turns port repair off
|
|
2327
|
-
const v = V[vid], ids = (v.nodeIds || []).filter(i => N[i]);
|
|
2191
|
+
const v = V[vid], ids = (v.nodeIds || []).filter(i => N[i]), T = ownLayout(vid);
|
|
2192
|
+
if (T) { // the type's layout (registry.js), in the host's measure (R2)
|
|
2193
|
+
const L = T.layout, memo = memos[vid] ||= {}, k = !override && L.key ? L.key(D, v) : null;
|
|
2194
|
+
if (k != null && memo.runKey === k) return memo.runGeo;
|
|
2195
|
+
const pos = override || (v.layout && L.pins !== false ? fillLayout(vid, v.layout) : (layoutCache[vid] ||= autoPos(vid)));
|
|
2196
|
+
const g = L.run ? L.run(D, v, { measure: M, pos, memo }) : L({ model: D, view: v, pos, memo });
|
|
2197
|
+
if (k != null) Object.assign(memo, { runKey: k, runGeo: g });
|
|
2198
|
+
return g;
|
|
2199
|
+
}
|
|
2328
2200
|
const edges = viewEdges(v).map(e => ({ ...e }));
|
|
2329
2201
|
if (v.render === 'sequence') edges.forEach((e, i) => { e.step = i + 1; });
|
|
2330
2202
|
const P = planCache[vid] || { flow: true, backKeys: loopsOf(v).keys, steps: {} };
|
|
@@ -2567,7 +2439,7 @@ function mapContext(model, opts = {}) {
|
|
|
2567
2439
|
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); }); } }))();
|
|
2568
2440
|
const geoCache = {};
|
|
2569
2441
|
return { D, views, nodes, N, V, rootId, nodeIn, childOf, opensOf, subjectOf, viewsWith, kidsOf, chain, viewEdges, isState, planOf: withM(planOf), planCache,
|
|
2570
|
-
layoutCache, geoCache, buildGeo: withM(buildGeo), buildView: withM(buildView), drawView: withM(drawView), notationOf, optimize: withM(optimize), flowPos, kidCount, viewKey, brokenIn, unfoldable, unfolded };
|
|
2442
|
+
layoutCache, geoCache, memos, buildGeo: withM(buildGeo), buildView: withM(buildView), drawView: withM(drawView), notationOf, optimize: withM(optimize), flowPos, kidCount, viewKey, brokenIn, unfoldable, unfolded };
|
|
2571
2443
|
}
|
|
2572
2444
|
const edgeRefIds = e => [...new Set((e.all || [e]).map(x => x._id).filter(Boolean))];
|
|
2573
2445
|
|
|
@@ -2722,6 +2594,9 @@ export function validate(model) {
|
|
|
2722
2594
|
out.push(...typedProblems(m).filter(p => !(p.level === 'error' && said.has(p.path))));
|
|
2723
2595
|
views.forEach((v, i) => { const at = `views[${i}].diagram`; // a well-formed name of a type this release does not draw (M13)
|
|
2724
2596
|
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`); });
|
|
2597
|
+
const once = new Set(); // each view's type's rules, a problem once; a validate(model) that takes no view, once a type
|
|
2598
|
+
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);
|
|
2599
|
+
T.validate(m, v).forEach(p => { const k = JSON.stringify([p.level, p.path, p.message]); if (!once.has(k)) { once.add(k); out.push(p); } }); });
|
|
2725
2600
|
}
|
|
2726
2601
|
return out;
|
|
2727
2602
|
}
|
|
@@ -2965,6 +2840,7 @@ export function reach(model, viewId, from, options = {}) {
|
|
|
2965
2840
|
/** A view in plain sentences: its cards and their kinds, its boxes, what each card's lines do (in run order on a sequence
|
|
2966
2841
|
* view), and the cards with no line. The same model gives the same words: a text alternative, docs, or an AI's context. */
|
|
2967
2842
|
export function describe(model, viewId) {
|
|
2843
|
+
const T = model && model.schemaVersion === 2 && diagramOf(arrOf(model.views).find(v => v && v.id === viewId)); if (T && T.describe) return T.describe(model, viewId); // a type's own words (1.9)
|
|
2968
2844
|
const { ctx, v, ids, edges } = lineGraph(model, viewId), boxes = (v.containers || []).filter(c => c && c.id);
|
|
2969
2845
|
const name = id => ctx.N[id] ? ctx.nodeIn(id, viewId).label || id : (boxes.find(c => c.id === id) || {}).label || id;
|
|
2970
2846
|
const list = xs => xs.length < 3 ? xs.join(' and ') : xs.slice(0, -1).join(', ') + ' and ' + xs[xs.length - 1];
|
|
@@ -3652,7 +3528,9 @@ function typedEditor(api) {
|
|
|
3652
3528
|
const t = nodeTypes()[i], cam = api.cam(); if (!t || !can('add') || !cam) return;
|
|
3653
3529
|
api.editor({ x: cam.s.cx - 80, y: cam.s.cy - 13, w: 160, h: 26 }, '', value => emit([{ op: 'add', element: { ...typed(t, 'node'), label: value }, view: api.view() }]));
|
|
3654
3530
|
}
|
|
3655
|
-
const
|
|
3531
|
+
const hook = k => { const E = def().edits; return isRec(E) && typeof E[k] === 'function' ? E[k] : null; }; // registry.js edits
|
|
3532
|
+
const addLine = (from, to, t) => { const c = hook('connect'); // at (a slot): stage 3's
|
|
3533
|
+
emit(c ? arrOf(c(model(), view(), from, to, null, t)) : [{ op: 'add', element: { ...typed(t, 'line'), from, to, ...(isRec(t.props) ? { props: structuredClone(t.props) } : {}) }, view: api.view() }]); };
|
|
3656
3534
|
function connect(from, to) { // the relationship picker, when the type has more than one kind of line
|
|
3657
3535
|
if (!can('add') || !from || !to || from === to) return;
|
|
3658
3536
|
const types = lineTypes();
|
|
@@ -3735,15 +3613,17 @@ function typedEditor(api) {
|
|
|
3735
3613
|
const off = can('rename') ? '' : ' disabled'; // the editable flags: a field the host does not offer is shown, to read
|
|
3736
3614
|
const text = (f, v, name, more = '') => `<label>${esc(name)}<input data-f="${esc(f)}" data-was="${esc(v)}" value="${esc(v)}"${more}${off}></label>`;
|
|
3737
3615
|
let h = `<h3>Properties${x.type != null ? ': ' + esc(x.type) : ''}</h3>` + text('label', x.label ?? '', 'Name');
|
|
3738
|
-
schema.forEach(f => { const v = P[f.key], name = f.label || f.key;
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
else
|
|
3616
|
+
schema.forEach(f => { const v = Array.isArray(f.path) ? f.path.reduce((o, k) => isRec(o) ? o[k] : undefined, P) : P[f.key], name = f.label || f.key; // path: a field inside props (ends.from.role)
|
|
3617
|
+
const key = ` data-key="${esc(f.key)}"${Array.isArray(f.path) ? ` data-path="${esc(JSON.stringify(f.path))}"` : ''}`;
|
|
3618
|
+
if (f.kind === 'bool') h += `<label class="um-check"><input type="checkbox" data-f="p:${esc(f.key)}"${key}${v ? ' checked' : ''}${off}>${esc(name)}</label>`;
|
|
3619
|
+
else if (f.kind === 'enum') h += `<label>${esc(name)}<select data-f="p:${esc(f.key)}"${key}${off}>${['', ...arrOf(f.options)].map(o => `<option value="${esc(o)}"${o === v ? ' selected' : ''}>${esc(o)}</option>`).join('')}</select></label>`;
|
|
3620
|
+
else h += text('p:' + f.key, f.kind === 'list' ? arrOf(v).join(', ') : v ?? '', name, key + (f.kind === 'list' ? ' data-list' : '')); });
|
|
3742
3621
|
const rest = schema.length ? [] : Object.entries(P); // no schema: what it holds, to read
|
|
3743
3622
|
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>';
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3623
|
+
const M = def().member;
|
|
3624
|
+
if (e.kind === 'node' && !(M && M.holds && !M.holds(x))) { // a comment holds none
|
|
3625
|
+
const ms = arrOf(x.members).filter(m => isRec(m) && typeof m.id === 'string'), line = m => M ? M.print(m) : m.label ?? ''; // a type's member text: "+ total: Money [1]" (1.9)
|
|
3626
|
+
h += '<h3>Members</h3><ul>' + ms.map((m, i) => `<li><input data-f="m:${esc(m.id)}" data-m="${esc(m.id)}" data-was="${esc(line(m))}" value="${esc(line(m))}" aria-label="Member ${i + 1} of ${ms.length}${can('reorder') ? ', Alt + arrows to move it' : ''}"${off}>`
|
|
3747
3627
|
+ (can('reorder') ? `<button data-f="up:${esc(m.id)}" data-up="${esc(m.id)}" aria-label="Move ${esc(nameOf(m))} up">↑</button><button data-f="down:${esc(m.id)}" data-down="${esc(m.id)}" aria-label="Move ${esc(nameOf(m))} down">↓</button>` : '') + '</li>').join('')
|
|
3748
3628
|
+ '</ul>' + (can('add') ? '<input data-f="new" data-new placeholder="New member, Enter adds it" aria-label="New member">' : '');
|
|
3749
3629
|
}
|
|
@@ -3771,15 +3651,19 @@ function typedEditor(api) {
|
|
|
3771
3651
|
|
|
3772
3652
|
function commit(t) { // a field of the properties panel: one transaction
|
|
3773
3653
|
const d = t.dataset, at = elementsOf(model()), id = selId(at);
|
|
3654
|
+
const M = def().member, bad = why => { t.setAttribute('aria-invalid', 'true'); api.status(why); }; // a line the type cannot read: kept as written, nothing applied (E3)
|
|
3774
3655
|
if (d.new != null) { const value = t.value.trim(), x = id && at.get(id); if (!value || !x || !can('add')) return;
|
|
3775
|
-
const
|
|
3776
|
-
|
|
3656
|
+
const r = M && M.add(value, x.x); if (r && r.error) return bad(r.error);
|
|
3657
|
+
const prev = arrOf(x.x.members).filter(isRec).pop(); t.value = ''; t.removeAttribute('aria-invalid');
|
|
3658
|
+
return emit([{ op: 'add', element: r ? { ...typed(r, 'member'), label: r.label, ...(Object.keys(r.props).length ? { props: r.props } : {}) } : { ...typed(prev || {}, 'member'), label: value }, owner: id }]); }
|
|
3777
3659
|
if (!can('rename') || (!id && d.m == null)) return;
|
|
3778
3660
|
let value = t.type === 'checkbox' ? t.checked : t.value.trim();
|
|
3779
3661
|
if (t.type !== 'checkbox') { if (t.value === d.was) return;
|
|
3780
3662
|
if (value === '' && d.key == null) { t.value = d.was; return; } // an empty name does nothing, as on the map; an empty property is removed
|
|
3781
|
-
d.
|
|
3782
|
-
|
|
3663
|
+
if (d.m != null && M) { const m = (at.get(d.m) || {}).x, r = m && M.edit(m, value); if (!r || r.error) return bad(r ? r.error : 'not a member');
|
|
3664
|
+
t.removeAttribute('aria-invalid'); d.was = t.value; return emit(r.commands); }
|
|
3665
|
+
d.was = t.value; if (value === '') value = undefined; else if (d.list != null) value = value.split(',').map(x => x.trim()).filter(Boolean); }
|
|
3666
|
+
const path = d.path ? ['props', ...JSON.parse(d.path)] : d.key == null ? 'label' : d.key.includes('.') ? ['props', d.key] : 'props.' + d.key;
|
|
3783
3667
|
emit([{ op: 'set', id: d.m ?? id, path, value }]);
|
|
3784
3668
|
}
|
|
3785
3669
|
function moveMember(id, by) {
|
|
@@ -3804,7 +3688,7 @@ function typedEditor(api) {
|
|
|
3804
3688
|
else if (d.orphans != null) { orphans = b.checked; put('.um-els', outline()); } });
|
|
3805
3689
|
panel.addEventListener('change', e => { if (e.target.dataset.orphans == null) commit(e.target); });
|
|
3806
3690
|
panel.addEventListener('keydown', e => { e.stopPropagation(); const t = e.target, K = e.key; // the panel's keys stay in it
|
|
3807
|
-
if (K === 'Escape') { if (t.tagName === 'INPUT' && t.type !== 'checkbox') t.value = t.dataset.was ?? ''; api.focus(); } // what was typed is not kept
|
|
3691
|
+
if (K === 'Escape') { if (t.tagName === 'INPUT' && t.type !== 'checkbox') { t.value = t.dataset.was ?? ''; t.removeAttribute('aria-invalid'); } api.focus(); } // what was typed is not kept
|
|
3808
3692
|
else if (K === 'Enter' && t.tagName === 'BUTTON') { e.preventDefault(); t.click(); }
|
|
3809
3693
|
else if (K === 'Enter' && t.tagName === 'INPUT' && t.type !== 'checkbox') { e.preventDefault(); commit(t); }
|
|
3810
3694
|
else if (e.altKey && t.dataset.m != null && (K === 'ArrowUp' || K === 'ArrowDown')) { e.preventDefault(); moveMember(t.dataset.m, K === 'ArrowUp' ? -1 : 1); } });
|
|
@@ -4625,7 +4509,7 @@ export function render(container, model, options = {}) {
|
|
|
4625
4509
|
const old = ctx, keys = {}, wantNew = selectNew && !own;
|
|
4626
4510
|
old.views.forEach(v => { keys[v.id] = relayout ? null : old.viewKey(v.id); });
|
|
4627
4511
|
current = model2;
|
|
4628
|
-
ctx = mapContext(withPlaces(model2, use), { measure, kinds: !!P.kinds, prev: options.stableLayout === false ? null : old.planCache, expand: expanded });
|
|
4512
|
+
ctx = mapContext(withPlaces(model2, use), { measure, kinds: !!P.kinds, prev: options.stableLayout === false ? null : old.planCache, expand: expanded, memos: relayout ? null : old.memos });
|
|
4629
4513
|
Object.keys(expanded).forEach(v => { expanded[v] = ctx.unfolded[v] || []; }); // a card or inner view the model no longer has folds
|
|
4630
4514
|
if (!own) selectNew = false;
|
|
4631
4515
|
ctx.views.forEach(v => { const id = v.id; if (keys[id] == null || keys[id] !== ctx.viewKey(id)) return;
|
|
@@ -5129,9 +5013,10 @@ const SHAPE_SAYS = { person: ['Person', 'someone who uses it'], browser: ['Brows
|
|
|
5129
5013
|
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
|
|
5130
5014
|
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>`; };
|
|
5131
5015
|
function legendItems(gs, opens = 'double-click to open', notes = [], icons = {}) { // what these pictures draw, each with what it means: the view's legend, and the PDF's (#93). notes: each picture's notation (0.15.0)
|
|
5132
|
-
const
|
|
5016
|
+
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
|
|
5017
|
+
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))];
|
|
5133
5018
|
// each card as its picture's notation draws it: its shape, its dash and its mark (the legend draws what the map draws)
|
|
5134
|
-
const cards = gs.flatMap((g, i) => { const nt = g.map ? noteOf(notes[i]) || 'box' : 'box';
|
|
5019
|
+
const cards = gs.flatMap((g, i) => { if (g.legend) return []; const nt = g.map ? noteOf(notes[i]) || 'box' : 'box';
|
|
5135
5020
|
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
|
|
5136
5021
|
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-') }; }); });
|
|
5137
5022
|
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; };
|
|
@@ -5149,7 +5034,7 @@ function legendItems(gs, opens = 'double-click to open', notes = [], icons = {})
|
|
|
5149
5034
|
.concat(ns.some(n => n.step) ? ['<span class="lgi"><i class="num">1</i><b>Step</b><span>run order</span></span>'] : [])
|
|
5150
5035
|
.concat(ns.some(n => n.tag) ? ['<span class="lgi"><i class="tag"></i><b>@author</b><span>who wrote it</span></span>'] : [])
|
|
5151
5036
|
.concat(WIRES.filter(w => es.some(e => e.wk === w[0])).map(w => `<span class="lgi"><i class="w" style="border-color:var(--um-w-${w[0]})"></i><b>${w[1]}</b><span>${w[2]}</span></span>`))
|
|
5152
|
-
.concat(es.some(e => e.back) ? ['<span class="lgi"><i class="w dash" style="border-color:var(--um-w-calls)"></i><b>loops back</b><span>the one line that runs right to left</span></span>'] : []);
|
|
5037
|
+
.concat(es.some(e => e.back) ? ['<span class="lgi"><i class="w dash" style="border-color:var(--um-w-calls)"></i><b>loops back</b><span>the one line that runs right to left</span></span>'] : []).concat(typedKeys);
|
|
5153
5038
|
}
|
|
5154
5039
|
|
|
5155
5040
|
function pieces(X, el, o) {
|