umlimator 0.21.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.
@@ -124,7 +124,7 @@ function registerDiagramType(name, def = {}) {
124
124
  reroute is no registry field: reroute: false goes in the dg the type's own layout passes to layoutTyped (src/typed/layout.js).
125
125
  Stage 4's (notes/stage-4.md §12.1, §15): edits.rename(model, view, id, text); a hook's { pick, then }; name: false and
126
126
  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.
127
- Stage 5's: a line's fromPort, toPort, inner, portRank, a box's pad, round, a view's labelGaps (src/typed/layout.js);
127
+ Stage 5's: a line's fromPort, toPort, inner, portRank, a box's pad, round, head, a view's labelGaps (src/typed/layout.js);
128
128
  edits.member, edits.key (src/typed/edit.js). */
129
129
 
130
130
  /* A view's type: the map for a view with no diagram, the registered type, or null for one this release does not draw */
@@ -132,7 +132,9 @@ const diagramOf = view => TYPES.get(view && view.diagram != null ? view.diagram
132
132
 
133
133
  /* What only a registered type's geometry uses, installed at load time by the files under src/typed/, which only
134
134
  lib/umlimator-all.js holds (tools/build.mjs): rows, rooms and routes in layoutPass, a typed line's ends in svgOf, the
135
- gestures of edit.js it takes over (edit). Empty in umlimator.js, where no view is laid out but as a map (risks.md C5) */
135
+ gestures of edit.js it takes over (edit), diff()'s words for typed models (diff, order: src/uml/diff.js). Empty in
136
+ umlimator.js, where no view is laid out but as a map (risks.md C5).
137
+ Stage 6's: across(model), validate()'s rules on the whole model; fields(model, x), more panel fields (notes/stage-6.md §5) */
136
138
  const TYPED = {};
137
139
 
138
140
  const propRefs = type => Object.assign({}, ...[...TYPES.values()].map(T => T.refs && Object.hasOwn(T.refs, type) && T.refs[type]));
@@ -249,6 +251,7 @@ function refs(m, ids, only) {
249
251
  arr(v.nodes).forEach((n, j) => element(n, [...at, 'nodes', j], false));
250
252
  arr(v.edges).forEach((e, j) => { if (has(e)) say([...at, 'edges', j], 'view'); else element(e, [...at, 'edges', j], true); });
251
253
  arr(v.fragments).forEach((f, j) => element(f, [...at, 'fragments', j], false));
254
+ ['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'); })));
252
255
  [['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); }));
253
256
  if (has(v.subject)) say([...at, 'subject'], 'subject');
254
257
  if (!only) ['parent', 'node'].forEach(k => { if (has(v[k])) say([...at, k], 'parent'); }); // a view opened from a card
@@ -375,9 +378,9 @@ function run(m, c, gone) {
375
378
  const take = id => { const r = at.get(id); if (!r || now.has(id)) return; now.add(id);
376
379
  arr(r.x.members).forEach(y => isObj(y) && take(y.id)); arr(kids.get(id)).forEach(take); }; // what it owns goes with it
377
380
  take(c.id);
378
- // lines to what is gone go too, lines to those lines, and what a type's refs cascade (a reply with its call)
381
+ // 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)
379
382
  const cascade = x => Object.entries(propRefs(x.type)).some(([p, how]) => how === 'cascade' && [].concat(dig(x, p)).some(id => now.has(id)));
380
- 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; } }
383
+ 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; } }
381
384
  // what stays in something gone goes to its nearest owner left; an operand's messages to its fragment's parent
382
385
  const above = id => { const r = at.get(id); return r && (r.x.parent ?? (r.owner && above(r.owner.id))); };
383
386
  for (const [k, o] of at) if (!now.has(k) && now.has(o.x.parent)) { let p = o.x.parent;
@@ -1036,7 +1039,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
1036
1039
  // 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
1037
1040
  // grows to hold it; laid out turned (direction down), the 24 px header room goes left, the side that becomes the top
1038
1041
  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
1039
- const headW = f => textW(!map && f.title || f.label || f.id, 11) + 16; // a box's title, as the audit measures it
1042
+ 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)
1040
1043
  const framesNow = () => { const cache = Object.create(null); // a box may be named 'constructor'
1041
1044
  const frameOf = (f, trail = new Set()) => {
1042
1045
  if (cache[f.id]) return cache[f.id];
@@ -1049,7 +1052,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
1049
1052
  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);
1050
1053
  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};
1051
1054
  if (!map) b[TB ? 'h' : 'w'] = Math.max(b[TB ? 'h' : 'w'], headW(f) + 16);
1052
- return cache[f.id] = dg.typed ? TYPED.frame(b) : b;
1055
+ return cache[f.id] = dg.typed ? TYPED.frame(b, TB) : b;
1053
1056
  };
1054
1057
  const out = familyDefs.map(f => frameOf(f)).filter(Boolean);
1055
1058
  if (map) out.forEach(f => { // the box fits its title and its badges, which are tabs on its top border, at the right
@@ -1451,7 +1454,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
1451
1454
  const runRouting = order => {
1452
1455
  routed.length = 0; reserved.length = 0; placed.length = 0; stats = {};
1453
1456
  (dg.blocks || []).forEach(b => { reserved.push(b); placed.push(b); });
1454
- families.forEach(f => { const h = TB ? { x: f.x + 3, y: f.y + 8, w: 22, h: headOf(f) } : { x: f.x + 8, y: f.y + 3, w: headOf(f), h: 22 }; reserved.push(h); placed.push(h);
1457
+ 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);
1455
1458
  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
1456
1459
  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; });
1457
1460
  order.forEach(e => {
@@ -1688,7 +1691,7 @@ function routeAudit(geo, where, only, refs) { // only: a set of edges; counts ju
1688
1691
  if (f.parent && (!parent || !inside(parent,f))) say(`${where}: family "${f.label}" sticks out of its parent`, f);
1689
1692
  (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); });
1690
1693
  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); });
1691
- 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); });
1694
+ 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); });
1692
1695
  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]];
1693
1696
  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);
1694
1697
  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); });
@@ -1703,7 +1706,7 @@ const box = n => ({ x: n.x, y: n.y, w: n.w, h: n.h });
1703
1706
  function overlapIssues(geo, where, refs) {
1704
1707
  const issues = [], nodes = geo.nodes, say = teller(issues, refs);
1705
1708
  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;
1706
- 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
1709
+ 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
1707
1710
  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]);
1708
1711
  return issues;
1709
1712
  }
@@ -2615,9 +2618,10 @@ export function validate(model) {
2615
2618
  out.push(...typedProblems(m).filter(p => !(p.level === 'error' && said.has(p.path))));
2616
2619
  views.forEach((v, i) => { const at = `views[${i}].diagram`; // a well-formed name of a type this release does not draw (M13)
2617
2620
  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`); });
2618
- const once = new Set(); // each view's type's rules, a problem once; a validate(model) that takes no view, once a type
2619
- 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);
2620
- 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); } }); });
2621
+ 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); } };
2622
+ // each view's type's rules, a problem once; a validate(model) that takes no view, once a type
2623
+ 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); });
2624
+ if (TYPED.across) arrOf(TYPED.across(m)).forEach(say);
2621
2625
  }
2622
2626
  return out;
2623
2627
  }
@@ -2774,14 +2778,16 @@ export function diff(before, after) {
2774
2778
  if (h === 'removed') said.push(`removes ${name(n.id)}`);
2775
2779
  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;
2776
2780
  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)
2781
+ 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)
2777
2782
  said.push((keys.includes('label') ? `renames ${o.label || o.id} to ${x.label || x.id}` : `changes ${name(n.id)}`) + (rest.length ? ` (${rest.join(', ')})` : '')); } });
2778
2783
  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;
2779
2784
  const ends = `${name(e.from)} to ${name(e.to)}`, lab = e.label ? ` (${e.label})` : '';
2780
2785
  if (h === 'added') said.push(`adds a line from ${ends}${lab}`);
2781
2786
  if (h === 'removed') said.push(`removes the line from ${ends}${lab}`);
2782
- if (h === 'changed') { const [o, x] = was.get(e);
2787
+ 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);
2783
2788
  said.push(`changes the line from ${ends} (${diffKeys([o, x]).map(k => k === 'label' ? `label ${o.label ?? '-'} → ${x.label ?? '-'}` : k).join(', ')})`); } });
2784
2789
  moved.forEach(([verb, id, v]) => said.push(`${verb} ${name(id)} ${verb === 'puts' ? 'in' : 'out of'} the view ${v.title || v.id}`));
2790
+ if (TYPED.order) TYPED.order(A, out, views, V0, said, touched, name);
2785
2791
 
2786
2792
  // the views that show a change: a card or line the diff marks, or a card it put in or took out
2787
2793
  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);
@@ -2835,9 +2841,11 @@ const walkFrom = (edges, start, back) => { const seen = new Set([start]); // bre
2835
2841
  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); } }); }
2836
2842
  return seen; };
2837
2843
 
2844
+ const viewType = (model, viewId) => model && model.schemaVersion === 2 && diagramOf(arrOf(model.views).find(v => v && v.id === viewId));
2838
2845
  /** Every card and line on a path from one card to another, following lines forward: the cards both reachable from `from`
2839
2846
  * and leading to `to`, and the lines between them. [] when no path joins them. */
2840
2847
  export function paths(model, viewId, from, to) {
2848
+ 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)
2841
2849
  const { ids, edges } = lineGraph(model, viewId);
2842
2850
  if (!ids.includes(from) || !ids.includes(to)) return [];
2843
2851
  const F = walkFrom(edges, from), B = walkFrom(edges, to, true), on = id => F.has(id) && B.has(id);
@@ -2862,7 +2870,7 @@ export function reach(model, viewId, from, options = {}) {
2862
2870
  /** A view in plain sentences: its cards and their kinds, its boxes, what each card's lines do (in run order on a sequence
2863
2871
  * view), and the cards with no line. The same model gives the same words: a text alternative, docs, or an AI's context. */
2864
2872
  export function describe(model, viewId) {
2865
- 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)
2873
+ const T = viewType(model, viewId); if (T && T.describe) return T.describe(model, viewId); // a type's own words (1.9)
2866
2874
  const { ctx, v, ids, edges } = lineGraph(model, viewId), boxes = (v.containers || []).filter(c => c && c.id);
2867
2875
  const name = id => ctx.N[id] ? ctx.nodeIn(id, viewId).label || id : (boxes.find(c => c.id === id) || {}).label || id;
2868
2876
  const list = xs => xs.length < 3 ? xs.join(' and ') : xs.slice(0, -1).join(', ') + ' and ' + xs[xs.length - 1];
@@ -3643,7 +3651,7 @@ function typedEditor(api) {
3643
3651
  function props() {
3644
3652
  const at = elementsOf(model()), id = selId(at), e = id && at.get(id);
3645
3653
  if (!e) return '<h3>Properties</h3><p class="um-note">Select a card or a line</p>';
3646
- const x = e.x, P = isRec(x.props) ? x.props : {}, schema = arrOf(isRec(def().props) ? def().props[x.type] : null).filter(f => isRec(f) && typeof f.key === 'string');
3654
+ 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))];
3647
3655
  const off = can('rename') ? '' : ' disabled'; // the editable flags: a field the host does not offer is shown, to read
3648
3656
  const text = (f, v, name, more = '') => `<label>${esc(name)}<input data-f="${esc(f)}" data-was="${esc(v)}" value="${esc(v)}"${more}${off}></label>`;
3649
3657
  let h = `<h3>Properties${x.type != null ? ': ' + esc(x.type) : ''}</h3>` + text('label', x.label ?? '', 'Name');
@@ -3658,7 +3666,7 @@ function typedEditor(api) {
3658
3666
  const offer = Array.isArray(os) && [{ id: '', label: 'none' }, ...os.filter(o => isRec(o) && o.id != null && !under(o.id))];
3659
3667
  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 });
3660
3668
  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>`;
3661
- const rest = schema.length ? [] : Object.entries(P); // no schema: what it holds, to read
3669
+ const rest = own.length ? [] : Object.entries(P); // no schema: what it holds, to read
3662
3670
  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>';
3663
3671
  const M = def().member;
3664
3672
  if (e.kind === 'node' && !(M && M.holds && !M.holds(x))) { // a comment holds none
@@ -3844,7 +3852,7 @@ export function render(container, model, options = {}) {
3844
3852
  const emitSel = (again = false) => {
3845
3853
  const now = JSON.stringify(picked()); if (now === said && !again) return; said = now;
3846
3854
  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)])
3847
- : `Box ${(svg.querySelector(`.family[data-family="${CSS.escape(sel.id)}"] .family-title`) || {}).textContent || sel.id}`) + (more.length ? `, ${more.length + 1} ${picked().every(r => r.type === 'node') ? 'cards ' : ''}selected` : ', selected'));
3855
+ : `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'));
3848
3856
  emit('select', { target: copyRef(sel), targets: picked() });
3849
3857
  };
3850
3858
  const edgeRef = e => ({ type: 'edge', ids: edgeRefIds(e), view: cur });
@@ -4289,7 +4297,7 @@ export function render(container, model, options = {}) {
4289
4297
  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; }
4290
4298
  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)];
4291
4299
  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 }; }
4292
- 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; };
4300
+ 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; };
4293
4301
  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
4294
4302
  if (naming) naming.close(false);
4295
4303
  const inp = document.createElement('input');
@@ -5883,6 +5891,9 @@ function layoutTyped(dg, memo) {
5883
5891
  or [from, to] for a line with a port at each end
5884
5892
  - a box's pad: its padding in px where it is more than stage 2's 12 (48 where it or a box it holds has ports with lines, §7);
5885
5893
  a box's round: true grows its frame by √2 about its centre, so the ellipse drawn in it holds its contents (§9.3)
5894
+ - a box's head: px of header room over stage 2's 24, on the side its title is on (the top; laid out down, the left, which
5895
+ becomes the top): a second line of title, a keyword above the name (the deployment type's, decision of 13 Nov 2026);
5896
+ its two lines go in the box's kw and name, and its title (both lines) is measured at their 12 px (layered.js headW)
5886
5897
  - a view's labelGaps: true lays the view out once more where a line's label found no spot, was cut short or sits on a box's
5887
5898
  border, with the column gap it needs widened by the label's width (a map's labelRoom, layered.js; column gaps only, as a
5888
5899
  typed view's rows are of mixed height; laid out down, the picture's row gaps, by the label's height, so fewer labels
@@ -5927,9 +5938,12 @@ function spreadPorts({ n, side, ends }, turned) {
5927
5938
  x.e[x.from ? 'p1' : 'p2'] = v ? [B, c] : [c, B]; if (g.port != null) x.e[x.from ? 'fpb' : 'tpb'] = { ...box }; }));
5928
5939
  s += g.L + gap; });
5929
5940
  }
5930
- // A typed view's box as frameOf measured it with stage 2's 12 px of padding: its pad widens that on every side, and round
5931
- // grows it by √2 about its centre (above). Neither: the box as it was
5932
- TYPED.frame = b => { const d = b.pad > 12 ? b.pad - 12 : 0, k = b.round ? Math.SQRT2 : 1; if (!d && k === 1) return b;
5941
+ // A typed view's box as frameOf measured it with stage 2's 12 px of padding and 24 px of header room: its head grows the
5942
+ // header room (turned: laid out down), its pad widens the padding on every side, and round grows it by √2 about its centre
5943
+ // (above). None of them: the box as it was
5944
+ TYPED.frame = (b, turned) => { const d = b.pad > 12 ? b.pad - 12 : 0, k = b.round ? Math.SQRT2 : 1, t = b.head > 0 ? b.head : 0;
5945
+ if (t) Object.assign(b, turned ? { x: b.x - t, w: b.w + t } : { y: b.y - t, h: b.h + t });
5946
+ if (!d && k === 1) return b;
5933
5947
  const w = (b.w + 2 * d) * k, h = (b.h + 2 * d) * k; return Object.assign(b, { x: b.x + (b.w - w) / 2, y: b.y + (b.h - h) / 2, w, h }); };
5934
5948
 
5935
5949
  // A typed view's lines from a node to itself (risks.md F1) are loops round its top right corner, nested, the first innermost:
@@ -6069,7 +6083,8 @@ SHAPE.classifier = CLASSIFIER;
6069
6083
  // (notes/stage-5.md §9.2) is a usage's end on an interface drawn as a 16 px ball: a half circle of radius 11 open towards the
6070
6084
  // ball, its middle on the line 3 px before the end (the ball's outline), its centre 8 px past it (the ball's)
6071
6085
  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'],
6072
- diamond: [16, 6, 'M0 6L8 0L16 6L8 12Z', 'hollow'], 'filled-diamond': [16, 6, 'M0 6L8 0L16 6L8 12Z', 'line'], socket: [3, 11, 'M11 0A11 11 0 0 0 11 22', 'none'] };
6086
+ diamond: [16, 6, 'M0 6L8 0L16 6L8 12Z', 'hollow'], 'filled-diamond': [16, 6, 'M0 6L8 0L16 6L8 12Z', 'line'], socket: [3, 11, 'M11 0A11 11 0 0 0 11 22', 'none'],
6087
+ 'filled-triangle': [13, 7, 'M0 0L13 7L0 14Z', 'line'] }; // an extension's, at the metaclass (notes/stage-6.md §4.1)
6073
6088
  const endOf = (E, w) => hasKey(ENDS, E[w]) && ENDS[E[w]] ? E[w] : null;
6074
6089
 
6075
6090
  // a typed line's end labels (1.5): each end's role and multiplicity (e.end.labels: { from: { role, mult }, to: { role, mult } }),
@@ -6301,7 +6316,7 @@ TYPED.edit = (I, c) => {
6301
6316
  boxes (§7, §10.3). Only lib/umlimator-all.js holds this file. */
6302
6317
 
6303
6318
  const OWN_OBJ = x => x !== null && typeof x === 'object' && !Array.isArray(x), OWN_ARR = x => (Array.isArray(x) ? x : []);
6304
- const OWN_BOXES = ['uml.Package']; // the node types that may be a box (§4.1, 1)
6319
+ const OWN_BOXES = ['uml.Package', 'uml.Profile']; // the node types that may be a box (§4.1, 1); a profile is a package (notes/stage-6.md §4.1)
6305
6320
  // the types of rows 1 to 3 whose owner is a package (rule owner-kinds); comments are not checked
6306
6321
  const OWNED = ['uml.Class', 'uml.Interface', 'uml.Enumeration', 'uml.DataType', 'uml.PrimitiveType', 'uml.Package', 'uml.InstanceSpecification'];
6307
6322
  const ownName = x => String(x.label ?? x.id);
@@ -6331,7 +6346,7 @@ function ownIndex(model, members = []) {
6331
6346
  its node is and it holds something shown, and is a box when its node has two or more in the model (with one, what it
6332
6347
  holds sits in the node's box); shape(x) and word(x), a box's shape and the word its accessible name starts with ('package', 'Package') */
6333
6348
  function ownersOn(model, view, look = {}) {
6334
- const { boxTypes = OWN_BOXES, qualify = true, members = [], shape = () => 'package', word = () => 'Package' } = look;
6349
+ const { boxTypes = OWN_BOXES, qualify = true, members = [], shape = () => 'package', word = x => (x.type === 'uml.Profile' ? 'Profile' : 'Package') } = look;
6335
6350
  const { at, chain, up, mems } = ownIndex(model, members), ids = OWN_ARR(OWN_OBJ(view) ? view.nodeIds : null).filter(id => at.has(id) && !mems.has(id)), seen = new Set(ids);
6336
6351
  const live = new Set(ids.flatMap(id => chain(id).filter(p => mems.has(p) && seen.has(up.get(p))))), place = m => OWN_ARR(at.get(up.get(m)).members).indexOf(at.get(m)); // members holding something shown
6337
6352
  const many = m => OWN_ARR(at.get(up.get(m)).members).filter(x => OWN_OBJ(x) && members.includes(x.type)).length > 1; // §6.1, 2: two or more in the model
@@ -6343,7 +6358,7 @@ function ownersOn(model, view, look = {}) {
6343
6358
  const by = new Map(); all.forEach(id => { const o = owner(id); if (!by.has(o)) by.set(o, []); by.get(o).push(id); }); // grouped once: a view of 10,000 elements
6344
6359
  const holds = o => by.get(o) || [], open = new Set([...by.keys()].filter(x => x != null));
6345
6360
  const boxes = all.filter(id => open.has(id)).map(id => { const p = at.get(id), st = stereoOf(p), held = holds(id), o = owner(id), nm = name(id);
6346
- return { id, label: ownName(p), title: [...st.map(t => `«${t}»`), nm].join(' '), nodeIds: held.filter(x => !open.has(x)), ...(o != null ? { parent: o } : {}), shape: shape(p),
6361
+ return { id, label: ownName(p), title: [...(p.type === 'uml.Profile' ? ['«profile»'] : []), ...st.map(t => `«${t}»`), nm].join(' '), nodeIds: held.filter(x => !open.has(x)), ...(o != null ? { parent: o } : {}), shape: shape(p),
6347
6362
  aria: `${word(p)} ${nm}${st.length ? ` (${st.join(', ')})` : ''}, holding ${held.length} element${held.length === 1 ? '' : 's'}` }; });
6348
6363
  return { ids, owner, holds, boxes, name, full, open, chain, at };
6349
6364
  }
@@ -6356,13 +6371,15 @@ function readingOrder(pic) {
6356
6371
  }
6357
6372
 
6358
6373
  // a line between a package and something it holds, at any depth, or from a box to itself (§7): a, b node ids
6359
- const holdsLine = (pic, a, b) => (a === b && pic.open.has(a)) || [[a, b], [b, a]].some(([p, x]) => (pic.at.get(p) || {}).type === 'uml.Package' && pic.chain(x).includes(p));
6374
+ const holdsLine = (pic, a, b) => (a === b && pic.open.has(a)) || [[a, b], [b, a]].some(([p, x]) => OWN_BOXES.includes((pic.at.get(p) || {}).type) && pic.chain(x).includes(p));
6360
6375
 
6361
- // the Owner field (registry.js owners): every package of the model by its name from the top, for an element a package may own
6376
+ // the Owner field (registry.js owners): every package and profile of the model by its name from the top, for an element a
6377
+ // package may own; a stereotype's and a metaclass's: every profile (notes/stage-6.md §4.2)
6362
6378
  function ownerChoices(model, x) {
6363
- if (!OWN_OBJ(x) || !(OWNED.includes(x.type) || x.type === 'uml.Comment') || x.from != null) return null;
6379
+ const def = OWN_OBJ(x) && (x.type === 'uml.Stereotype' || x.type === 'uml.MetaclassReference');
6380
+ if (!OWN_OBJ(x) || !(OWNED.includes(x.type) || x.type === 'uml.Comment' || def) || x.from != null) return null;
6364
6381
  const { at, chain } = ownIndex(model), full = id => [...chain(id).reverse(), id].map(k => ownName(at.get(k))).join('::');
6365
- return [...at.values()].filter(n => n.type === 'uml.Package').map(n => ({ id: n.id, label: full(n.id) }));
6382
+ return [...at.values()].filter(n => (def ? n.type === 'uml.Profile' : OWN_BOXES.includes(n.type))).map(n => ({ id: n.id, label: full(n.id) }));
6366
6383
  }
6367
6384
 
6368
6385
  /* ---- the rules about owners and boxes (§9.3, §10.3). Each problem: { level, rule, path, message, ids } ---- */
@@ -6378,7 +6395,7 @@ function ownerProblems(model, view, lines) {
6378
6395
  const all = new Map(), put = (x, kind) => { if (OWN_OBJ(x) && typeof x.id === 'string' && !all.has(x.id)) { all.set(x.id, { x, kind }); OWN_ARR(x.members).forEach(m => put(m, 'member')); } };
6379
6396
  OWN_ARR(model.nodes).forEach(n => put(n, 'node')); const q = id => JSON.stringify(ownName((all.get(id) || { x: { id } }).x));
6380
6397
  OWN_ARR(model.nodes).forEach((n, i) => { if (!OWN_OBJ(n) || !OWNED.includes(n.type) || n.parent == null || !all.has(n.parent)) return;
6381
- const p = all.get(n.parent).x; if (p.type !== 'uml.Package') say('owner-kinds', `nodes[${i}].parent`, [n.id, p.id], `${q(n.id)} is owned by ${q(p.id)}, a ${String(p.type).split('.').pop()}, not a package`); });
6398
+ const p = all.get(n.parent).x; if (!OWN_BOXES.includes(p.type)) say('owner-kinds', `nodes[${i}].parent`, [n.id, p.id], `${q(n.id)} is owned by ${q(p.id)}, a ${String(p.type).split('.').pop()}, not a package`); });
6382
6399
  if (!view) return out;
6383
6400
  const vi = OWN_ARR(model.views).indexOf(view), pic = ownersOn(model, view);
6384
6401
  lines.forEach(({ e, from, to }) => { if (holdsLine(pic, from, to)) say('line-inside', `views[${vi}]`, from === to ? [e.id, from] : [e.id, from, to],
@@ -6431,6 +6448,59 @@ function placeTabs(g) {
6431
6448
  });
6432
6449
  return g;
6433
6450
  }
6451
+ /* ---------- diff()'s words for typed models (C10, risks.md M11) ----------
6452
+ Only lib/umlimator-all.js holds this file: it fills TYPED.diff and TYPED.order (registry.js), which diff() (src/map/diff.js)
6453
+ asks. Where order means something, a reorder reads "moves": a member of an element (an attribute, a slot, a port), a
6454
+ message or an interaction use among a sequence view's lines, a message among a communication view's `edges` (its ids, which
6455
+ number the messages; the model's `edges` order means nothing). A line that
6456
+ only changes its operand reads "moves … into …" (or "out of …"), as a re-own does. A sequence view's combined fragments
6457
+ read added, removed and changed. Maps have none of these, and are diffed as before. */
6458
+ const UD_ARR = x => (Array.isArray(x) ? x : []), UD_OBJ = x => x !== null && typeof x === 'object' && !Array.isArray(x);
6459
+ const UD_ORDERED = new Set(['uml.Message', 'uml.InteractionUse']);
6460
+ const UD_KEY = (x, i) => x.id != null ? String(x.id) : '#' + i;
6461
+ // what moved from `old` to `now`, both keyed lists: the kept items not on a longest run in their old order, each with the
6462
+ // kept item now before it (null: first). O(n²), fine for a view's messages or an element's members
6463
+ function udMoved(old, now) {
6464
+ const at = new Map(old.map((x, i) => [UD_KEY(x, i), i])), kept = now.map((x, i) => [x, at.get(UD_KEY(x, i))]).filter(k => k[1] != null);
6465
+ const len = kept.map(() => 1), prev = kept.map(() => -1);
6466
+ kept.forEach((k, i) => { for (let j = 0; j < i; j++) if (kept[j][1] < k[1] && len[j] + 1 > len[i]) { len[i] = len[j] + 1; prev[i] = j; } });
6467
+ const run = new Set(); for (let i = len.indexOf(Math.max(0, ...len)); i >= 0; i = prev[i]) run.add(i);
6468
+ return kept.flatMap((k, i) => run.has(i) ? [] : [[k[0], i ? kept[i - 1][0] : null]]);
6469
+ }
6470
+ const UD_WHAT = (x, name) => String(x.label ?? (x.props && (x.props.trigger ?? x.props.name)) ?? (x.from != null ? `the message from ${name(x.from)} to ${name(x.to)}` : x.id));
6471
+
6472
+ // a changed card or line (o before, x after): its own words, or nothing for diff()'s
6473
+ TYPED.diff = (o, x, name, models) => {
6474
+ if (!x.type) return;
6475
+ const keys = [...new Set([...Object.keys(o), ...Object.keys(x)])].filter(k => canon(o[k]) !== canon(x[k]));
6476
+ if (keys.join() === 'members') { const a = UD_ARR(o.members).filter(UD_OBJ), b = UD_ARR(x.members).filter(UD_OBJ);
6477
+ const same = a.length === b.length && canon([...a].map(canon).sort()) === canon([...b].map(canon).sort());
6478
+ const r = same ? udMoved(a, b).map(([m, p]) => `moves ${UD_WHAT(m, name)} ${p ? 'after ' + UD_WHAT(p, name) : 'to the top'} in ${name(x.id)}`) : [];
6479
+ if (r.length) return r; } // no move found (members with no ids, or the same id twice): "changes X (members)" as before
6480
+ if (keys.join() === 'parent' && UD_ORDERED.has(x.type) && x.from != null) { // a message into or out of an operand
6481
+ const ops = new Map(models.flatMap(m => UD_ARR(m && m.views)).flatMap(v => UD_ARR(v && v.fragments)).flatMap(f => UD_ARR(f && f.members)).filter(UD_OBJ).map(op => [op.id, op]));
6482
+ const op = id => String((ops.get(id) || {}).label ?? id);
6483
+ return [`moves ${UD_WHAT(x, name)} ${x.parent != null ? 'into ' + op(x.parent) : 'out of ' + op(o.parent)}`]; }
6484
+ };
6485
+
6486
+ // the order of lines, and a sequence view's fragments: each move or fragment said, a moved line marked changed (its
6487
+ // edited dot), and the views that show it touched
6488
+ TYPED.order = (A, out, views, V0, said, touched, name) => {
6489
+ const lines = (old, now, v) => { const f = l => UD_ARR(l).filter(e => UD_OBJ(e) && UD_ORDERED.has(e.type) && e.change !== 'removed');
6490
+ udMoved(f(old), f(now)).forEach(([e, p]) => { said.push(`moves ${UD_WHAT(e, name)} ${p ? 'after ' + UD_WHAT(p, name) : 'to the start'}`);
6491
+ Object.assign(e, { change: 'changed', edited: true });
6492
+ touched.add(v.id); }); };
6493
+ const E0 = new Map(UD_ARR(A.edges).filter(UD_OBJ).map(e => [e.id, e])), E1 = new Map(UD_ARR(out.edges).filter(UD_OBJ).map(e => [e.id, e]));
6494
+ const own = (l, E) => UD_ARR(l).map(e => (typeof e === 'string' ? E.get(e) : e)); // a view's model lines by id (a communication view's messages)
6495
+ views.forEach(v => { const o = v && V0.get(v.id); if (!o) return;
6496
+ if (Array.isArray(v.edges)) lines(own(o.edges, E0), own(v.edges, E1), v);
6497
+ const F0 = new Map(UD_ARR(o.fragments).filter(UD_OBJ).map(f => [f.id, f])), F1 = new Map(UD_ARR(v.fragments).filter(UD_OBJ).map(f => [f.id, f]));
6498
+ const op = f => `the ${(f.props && f.props.operator) || 'combined'} fragment`;
6499
+ F1.forEach((f, id) => { const g = F0.get(id); if (g && canon(g) === canon(f)) return;
6500
+ said.push(g ? `changes ${op(g)} (${[...new Set([...Object.keys(g), ...Object.keys(f)])].filter(k => canon(g[k]) !== canon(f[k])).join(', ')})` : `adds ${op(f)}`); touched.add(v.id); });
6501
+ F0.forEach((f, id) => { if (!F1.has(id)) { said.push(`removes ${op(f)}`); touched.add(v.id); } });
6502
+ });
6503
+ };
6434
6504
  /* ---------- the class diagram, uml.class (work package 1.9) ----------
6435
6505
  The subset of proposal.md §8.2: class, interface, enumeration with literals, data type, primitive type; attributes and
6436
6506
  operations in UML's text form; stereotypes as text; packages as owners, a package holding what it owns that the view
@@ -6547,8 +6617,8 @@ function memberAdd(text, owner) {
6547
6617
  }
6548
6618
 
6549
6619
  /* ---- the vocabulary (model.md, uml.class) ---- */
6550
- const CLASSIFIERS = ['uml.Class', 'uml.Interface', 'uml.Enumeration', 'uml.DataType', 'uml.PrimitiveType'];
6551
- const IMPLIED = { 'uml.Interface': 'interface', 'uml.Enumeration': 'enumeration', 'uml.DataType': 'dataType', 'uml.PrimitiveType': 'primitive' }; // shown «…» over the name
6620
+ const CLASSIFIERS = ['uml.Class', 'uml.Interface', 'uml.Enumeration', 'uml.DataType', 'uml.PrimitiveType', 'uml.Stereotype']; // a stereotype specializes a stereotype (notes/stage-6.md §4.2)
6621
+ const IMPLIED = { 'uml.Interface': 'interface', 'uml.Enumeration': 'enumeration', 'uml.DataType': 'dataType', 'uml.PrimitiveType': 'primitive', 'uml.Stereotype': 'stereotype', 'uml.MetaclassReference': 'metaclass', 'uml.Profile': 'profile' }; // shown «…» over the name
6552
6622
  const STEREO = { key: 'stereotypes', label: 'Stereotypes', kind: 'list' };
6553
6623
  const endFields = w => [['role', 'text'], ['multiplicity', 'text'], ['aggregation', 'enum', ['none', 'shared', 'composite']], ['navigable', 'bool']]
6554
6624
  .map(([k, kind, options]) => ({ key: `ends.${w}.${k}`, path: ['ends', w, k], label: `${w === 'from' ? 'From' : 'To'} ${k}`, kind, ...(options ? { options } : {}) }));
@@ -6570,7 +6640,7 @@ const CLASS_DEF = {
6570
6640
  const COMPARTMENTS = { attributes: 'uml.Property', operations: 'uml.Operation', literals: 'uml.EnumerationLiteral' };
6571
6641
  function compartmentsOf(n) { // a class's attributes and operations; an enumeration's literals, then the others it has
6572
6642
  const c = name => ({ name, members: UML_ARR(n.members).filter(m => UML_OBJ(m) && m.type === COMPARTMENTS[name]) });
6573
- if (n.type === 'uml.Package' || n.type === 'uml.Comment') return [];
6643
+ if (OWN_BOXES.includes(n.type) || n.type === 'uml.Comment' || n.type === 'uml.MetaclassReference') return []; // a package or a profile; a metaclass has no compartment (notes/stage-6.md §4.1)
6574
6644
  return n.type === 'uml.Enumeration' ? [c('literals'), ...[c('attributes'), c('operations')].filter(x => x.members.length)] : [c('attributes'), c('operations')];
6575
6645
  }
6576
6646
 
@@ -6605,7 +6675,8 @@ function lineLook(e) {
6605
6675
  case 'uml.InterfaceRealization': return { from: 'none', to: 'triangle', dashed: true };
6606
6676
  case 'uml.Dependency': return { from: 'none', to: 'open', dashed: true };
6607
6677
  case 'uml.CommentLink': return { from: 'none', to: 'none', dashed: true };
6608
- case 'uml.PackageImport': case 'uml.PackageMerge': return { from: 'none', to: 'open', dashed: true };
6678
+ case 'uml.PackageImport': case 'uml.PackageMerge': case 'uml.ProfileApplication': return { from: 'none', to: 'open', dashed: true };
6679
+ case 'uml.Extension': return { from: 'none', to: 'filled-triangle' }; // to the metaclass (notes/stage-6.md §4.1)
6609
6680
  case 'uml.Association': { const E = endsOf(e), lab = w => ({ role: (E[w] || {}).role, mult: (E[w] || {}).multiplicity });
6610
6681
  return { from: endOfLine(e, 'from'), to: endOfLine(e, 'to'), labels: { from: lab('from'), to: lab('to') } }; }
6611
6682
  default: return { from: 'none', to: 'arrow' };
@@ -6613,15 +6684,19 @@ function lineLook(e) {
6613
6684
  }
6614
6685
  const stereoText = x => UML_ARR(UML_OBJ(x.props) ? x.props.stereotypes : null).filter(UML_HAS).map(t => `«${t}»`).join(' ');
6615
6686
  // a package import says «import», or «access» when private; a merge «merge» (notes/stage-2.md §10.2)
6616
- const keyword = e => e.type === 'uml.PackageImport' ? ((UML_OBJ(e.props) ? e.props.visibility : null) === 'private' ? '«access»' : '«import»') : e.type === 'uml.PackageMerge' ? '«merge»' : '';
6687
+ // a profile application «apply», a required extension {required} (notes/stage-6.md §4.1)
6688
+ const keyword = e => e.type === 'uml.PackageImport' ? ((UML_OBJ(e.props) ? e.props.visibility : null) === 'private' ? '«access»' : '«import»') : e.type === 'uml.PackageMerge' ? '«merge»'
6689
+ : e.type === 'uml.ProfileApplication' ? '«apply»' : e.type === 'uml.Extension' && UML_OBJ(e.props) && e.props.isRequired === true ? '{required}' : '';
6617
6690
  const lineLabel = e => [keyword(e), stereoText(e), e.label].filter(UML_HAS).join(' ');
6618
6691
  const LINE_WORDS = { 'uml.Association': ['Association', 'a link between instances'], 'uml.Generalization': ['Generalization', 'is a kind of the one it points to'],
6619
6692
  'uml.InterfaceRealization': ['Realization', 'implements the interface it points to'], 'uml.Dependency': ['Dependency', 'uses the one it points to'],
6620
6693
  'uml.CommentLink': ['Comment link', 'what a comment is about'], 'uml.PackageImport': ['Package import', 'sees the names of the one it points to: «import», or «access» kept private'],
6621
- 'uml.PackageMerge': ['Package merge', 'takes in what the one it points to holds'] };
6694
+ 'uml.PackageMerge': ['Package merge', 'takes in what the one it points to holds'], 'uml.Extension': ['Extension', 'the stereotype extends the metaclass it points to'],
6695
+ 'uml.ProfileApplication': ['Profile application', '«apply»: the package may use the profile\'s stereotypes'] };
6622
6696
  const LEGEND_ENDS = { // small copies of src/typed/draw.js's ends, pointing right with the tip at x 26
6623
6697
  none: '', open: '<path d="M17 4L26 8L17 12" fill="none"/>', triangle: '<path d="M16 3L26 8L16 13Z" fill="var(--um-panel)"/>',
6624
- diamond: '<path d="M12 8L19 4L26 8L19 12Z" fill="var(--um-panel)"/>', 'filled-diamond': '<path d="M12 8L19 4L26 8L19 12Z" fill="var(--um-muted)"/>' };
6698
+ diamond: '<path d="M12 8L19 4L26 8L19 12Z" fill="var(--um-panel)"/>', 'filled-diamond': '<path d="M12 8L19 4L26 8L19 12Z" fill="var(--um-muted)"/>',
6699
+ 'filled-triangle': '<path d="M16 3L26 8L16 13Z" fill="var(--um-muted)"/>' };
6625
6700
  const legendItem = (end, dashed, name, says) => `<span class="lgi" title="${esc(name + ' — ' + says)}"><svg class="lgs" viewBox="0 0 28 16" aria-hidden="true"><g stroke="var(--um-muted)" stroke-width="1.4" stroke-linejoin="round"><path d="M2 8H${end === 'none' ? 26 : end.includes('diamond') ? 12 : 17}"${dashed ? ' stroke-dasharray="3 2"' : ''}/>${LEGEND_ENDS[end]}</g></svg><b>${esc(name)}</b><span>${esc(says)}</span></span>`;
6626
6701
  // the legend of a view's lines (pieces.js shows it): each kind drawn, then the association's ends it shows
6627
6702
  function legendOf(edges) {
@@ -6653,7 +6728,7 @@ const CLASS_LOOK = { members: true, own: null, word: 'class diagram' };
6653
6728
  function nodeName(n, pic, look = CLASS_LOOK) {
6654
6729
  if (n.type === 'uml.Comment') return 'Comment: ' + nameOfEl(n);
6655
6730
  const p = UML_OBJ(n.props) ? n.props : {}, why = [...UML_ARR(p.stereotypes).filter(UML_HAS), ...(p.isAbstract ? ['abstract'] : [])], nm = pic ? pic.name(n.id) : nameOfEl(n);
6656
- if (n.type === 'uml.Package') { const b = pic && pic.boxes.find(x => x.id === n.id); return b ? b.aria : `Package ${nm} (${[...why, 'closed'].join(', ')})`; }
6731
+ if (OWN_BOXES.includes(n.type)) { const b = pic && pic.boxes.find(x => x.id === n.id); return b ? b.aria : `${typeWord(n.type)} ${nm} (${[...why, 'closed'].join(', ')})`; }
6657
6732
  const counts = look.members ? compartmentsOf(n).map(c => plural(c.members.length, c.name.replace(/s$/, ''))) : [];
6658
6733
  return [`${typeWord(n.type)} ${nm}${why.length ? ` (${why.join(', ')})` : ''}`, ...counts].join(', ');
6659
6734
  }
@@ -6678,23 +6753,27 @@ function drawnLines(model, view, own) {
6678
6753
 
6679
6754
  /* the view's geometry, for a type's look: { model, view, pos (col, row by id, the layered layout's ranking or the view's
6680
6755
  pins), memo, direction }. A line that ends on a member is drawn to the member's owner; a line between two nodes the view
6681
- does not both show is not drawn. An open package is a box (its id takes no slot), passed to layoutTyped as a container */
6756
+ does not both show is not drawn. An open package is a box (its id takes no slot), passed to layoutTyped as a container.
6757
+ A look may also give node(d, n, model, pic), a node's drawing input before it is sized (the profile's tag compartments), and
6758
+ dg, more of layoutTyped's input (repair, reroute: notes/stage-6.md §4.1), and box(b, model, pic), a box as drawn (its title
6759
+ and accessible name) */
6682
6760
  const typeGeo = look => ({ model, view, pos, memo }) => {
6683
- const { at, pic, lines } = drawnLines(model, view, look.own), present = UML_OBJ(view.present) ? view.present : {};
6761
+ const { at, pic, lines } = drawnLines(model, view, look.own), present = UML_OBJ(view.present) ? view.present : {}, boxes = look.box ? pic.boxes.map(b => look.box(b, model, pic)) : pic.boxes;
6684
6762
  const nodes = pic.ids.filter(id => !pic.open.has(id)).map(id => { const n = at.get(id).x, pr = UML_OBJ(UML_OWN(present, id)) ? present[id] : {}, p = UML_OBJ(n.props) ? n.props : {};
6685
6763
  const st = UML_ARR(p.stereotypes).filter(UML_HAS);
6686
- const shape = n.type === 'uml.Comment' ? { shape: 'note' } : n.type === 'uml.Package' ? { shape: 'package', stereotypes: st }
6764
+ const shape = n.type === 'uml.Comment' ? { shape: 'note' } : OWN_BOXES.includes(n.type) ? { shape: 'package', stereotypes: [...(UML_OWN(IMPLIED, n.type) ? [IMPLIED[n.type]] : []), ...st] }
6687
6765
  : { shape: 'classifier', stereotypes: [...(UML_OWN(IMPLIED, n.type) ? [IMPLIED[n.type]] : []), ...st], abstract: !!p.isAbstract, compartments: look.members ? compartmentsOf(n) : [],
6688
6766
  collapsed: UML_ARR(pr.collapsed), ...(Number.isFinite(pr.rows) ? { rows: pr.rows } : {}) };
6689
- const d = { id, label: n.type === 'uml.Comment' ? nameOfEl(n) : pic.name(id), type: n.type || 'uml.Class', kind: 'file', ...shape, aria: nodeName(n, pic, look), col: (UML_OWN(pos, id) || [0, 0])[0], row: (UML_OWN(pos, id) || [0, 0])[1] };
6767
+ const d0 = { id, label: n.type === 'uml.Comment' ? nameOfEl(n) : pic.name(id), type: n.type || 'uml.Class', kind: 'file', ...shape, aria: nodeName(n, pic, look), col: (UML_OWN(pos, id) || [0, 0])[0], row: (UML_OWN(pos, id) || [0, 0])[1] };
6768
+ const d = look.node ? look.node(d0, n, model, pic) : d0;
6690
6769
  return { ...d, ...SHAPE[d.shape].size(d) }; });
6691
6770
  const edges = lines.filter(({ from, to }) => !holdsLine(pic, from, to))
6692
6771
  .map(({ e, from, to }) => ({ id: e.id, _id: e.id, type: e.type, from, to, label: lineLabel(e) || undefined, end: lineLook(e), aria: lineName(e, at, pic), ...(UML_OBJ(e.style) || typeof e.style === 'string' ? { style: e.style } : {}) }));
6693
6772
  // a box's border and padding take 48 px of each side of the gap between columns, so a line from box to box has none left for
6694
6773
  // its label, which is then not drawn: laid out left to right, a view with boxes and labelled lines spreads its columns by its
6695
6774
  // widest label and 40 px for its stubs (laid out down, a label sits across its line, in the gap between rows)
6696
- const down = view.direction === 'down', wide = !down && pic.boxes.length ? Math.max(0, ...edges.filter(e => e.label).map(labelW)) : 0;
6697
- const g = layoutTyped({ nodes, edges, ...(pic.boxes.length ? { containers: pic.boxes } : {}), direction: down ? 'down' : 'right', gap: 96 + (wide ? Math.ceil(wide) + 40 : 0), rgap: 56 }, memo);
6775
+ const down = view.direction === 'down', wide = !down && boxes.length ? Math.max(0, ...edges.filter(e => e.label).map(labelW)) : 0;
6776
+ const g = layoutTyped({ nodes, edges, ...(boxes.length ? { containers: boxes } : {}), direction: down ? 'down' : 'right', gap: 96 + (wide ? Math.ceil(wide) + 40 : 0), rgap: 56, ...look.dg }, memo);
6698
6777
  g.legend = legendOf(g.edges);
6699
6778
  return placeTabs(g);
6700
6779
  };
@@ -6779,6 +6858,7 @@ const describeAs = look => (model, viewId) => {
6779
6858
  else if (e.type === 'uml.Dependency') said.push(`depends on ${nm(t)}${stereoText(e) ? ' ' + stereoText(e) : ''}`);
6780
6859
  else if (e.type === 'uml.PackageImport') said.push(`${keyword(e) === '«access»' ? 'accesses' : 'imports'} ${nm(t)}`);
6781
6860
  else if (e.type === 'uml.PackageMerge') said.push(`merges ${nm(t)}`);
6861
+ else if (e.type === 'uml.ProfileApplication') said.push(`applies ${nm(t)}`);
6782
6862
  else if (e.type === 'uml.CommentLink') said.push(`about ${nm(t)}`); });
6783
6863
  out.push(nodeName(at.get(id).x, pic, look) + (said.length ? '; ' + said.join('; ') : '') + '.'); });
6784
6864
  return out.join('\n');
@@ -6806,7 +6886,8 @@ const CLASS_TYPE = registerDiagramType('uml.class', {
6806
6886
  merges («merge») and dependencies, dashed with an open arrow; comments. A view with no `edges` list draws these line
6807
6887
  types only. Ships in lib/umlimator-all.js only. */
6808
6888
 
6809
- const PACKAGE_LINES = ['uml.PackageImport', 'uml.PackageMerge', 'uml.Dependency', 'uml.CommentLink'];
6889
+ // a profile application too, drawn where a view shows a package and a profile (notes/stage-6.md §14); its palette is the profile view's
6890
+ const PACKAGE_LINES = ['uml.PackageImport', 'uml.PackageMerge', 'uml.Dependency', 'uml.CommentLink', 'uml.ProfileApplication'];
6810
6891
  const PACKAGE_LOOK = { members: false, own: PACKAGE_LINES, word: 'package diagram' };
6811
6892
  const PACKAGE_RULES = {
6812
6893
  'import-ends': 'a package import or a package merge runs from a package to another package',
@@ -8165,6 +8246,21 @@ function sqDescribe(model, viewId) {
8165
8246
  return out.join('\n');
8166
8247
  }
8167
8248
 
8249
+ /* ---- paths(): in time order (C10). A path from one lifeline to another is a run of messages, each later than the one
8250
+ before and sent from the lifeline it reached; a line is on one when `from` reaches its sender before it and its receiver
8251
+ reaches `to` after it. Self-messages are left out, as paths() leaves them ---- */
8252
+ function sqPaths(model, viewId, from, to) {
8253
+ const V = SQ_ARR(model && model.views).find(x => SQ_OBJ(x) && x.id === viewId); if (!V) throw new Error(`umlimator: no view ${JSON.stringify(viewId)}`);
8254
+ const ids = sqHeads(model, V).map(h => h.id), ms = sqIndex(model, V).occ.filter(o => o.message && o.from !== o.to);
8255
+ if (!ids.includes(from) || !ids.includes(to)) return [];
8256
+ if (from === to) return [{ type: 'node', id: from, view: viewId }]; // the card alone, as paths() on a map gives it
8257
+ const got = new Map([[from, -1]]), left = new Map([[to, Infinity]]);
8258
+ ms.forEach((m, i) => { if (got.has(m.from) && !got.has(m.to)) got.set(m.to, i); }); // when each lifeline is first reached
8259
+ for (let i = ms.length - 1; i >= 0; i--) { const m = ms[i]; if (left.has(m.to) && !left.has(m.from)) left.set(m.from, i); } // its last chance to reach `to`
8260
+ const on = ms.filter((m, i) => got.get(m.from) < i && left.get(m.to) > i), at = new Set(on.flatMap(m => [m.from, m.to]));
8261
+ return on.length ? [...ids.filter(id => at.has(id)).map(id => ({ type: 'node', id, view: viewId })), ...on.map(m => ({ type: 'edge', ids: [m.id], view: viewId }))] : [];
8262
+ }
8263
+
8168
8264
  /* ---- the gesture hooks (notes/stage-3.md §5.1, §5.2). Each takes the model and the view (or its id) and returns the
8169
8265
  commands of one transaction, null to leave the gesture to the canvas's default, or { error } for the hint line. A
8170
8266
  transaction that would add an error the model did not have is never returned: { error } names the rule (S5) ---- */
@@ -8362,6 +8458,7 @@ const SEQ_TYPE = registerDiagramType('uml.sequence', {
8362
8458
  validate: sqProblems,
8363
8459
  rules: SQ_RULES,
8364
8460
  describe: sqDescribe,
8461
+ paths: sqPaths,
8365
8462
  member: SQ_MEMBER,
8366
8463
  edits: { connect: sqConnect, moveTo: sqMoveTo, wrap: sqWrap, add: sqAdd, remove: sqRemove },
8367
8464
  });
@@ -11506,6 +11603,9 @@ function portsDrawn(g, ports) {
11506
11603
  const at = new Map(), onSide = new Map(), boxesOn = (o, s) => onSide.get(o + '|' + s) || onSide.set(o + '|' + s, []).get(o + '|' + s); // a port's box; each owner's side's
11507
11604
  const seen = new Set(SN_OBJ(g.byId) ? Object.keys(g.byId) : []), list = SN_ARR(ports).filter(p => SN_OBJ(p) && owner(p.owner) && Object.hasOwn(SN_OUT, p.side) && !seen.has(p.id) && seen.add(p.id));
11508
11605
  list.forEach(p => { const r = lined(p); if (r) { at.set(p.id, r); boxesOn(p.owner, p.side).push(r); } });
11606
+ // a line that names no port ends on its owner's side too: a port with no line keeps off its end (0.22.0, 5.8's fixtures)
11607
+ SN_ARR(g.edges).forEach(e => e.pts && [[e.fromPort, e.from, e.fs, e.pts[0]], [e.toPort, e.to, e.ts, e.pts[e.pts.length - 1]]].forEach(([q, id, s, p]) => {
11608
+ if (q == null && s && p) boxesOn(id, s).push({ x: p[0] - 1, y: p[1] - 1, w: 2, h: 2 }); }));
11509
11609
  // the ports with no line, side by side: evenly over the span in members order, then pushed forwards and, at the span's end,
11510
11610
  // back, 20 px centre to centre (12 + 8), each past a port with lines it would come within 8 px of; a side too short for them
11511
11611
  // narrows the gaps down to 4 px (§4.4, step 3), and what still does not fit is held at the span's start, marked over
@@ -11567,9 +11667,8 @@ function structProblems(model, view) {
11567
11667
  comments; realizations, component realizations, usages (a socket at a ball, else «use»), assembly and delegation
11568
11668
  connectors (a delegation from a box's port inward is an inner line, §5), dependencies, manifestations and comment links.
11569
11669
  Laid out by layoutTyped from the type's own ranking (cpPlace), a usage of a ball turned so the ball comes before its requirers: gap 48, labelGaps
11570
- and cut 280 (§8), a box with ports 48 px of padding (§7), repair and reroute off (§14). Ports on a box are nodes of the
11571
- picture; ports on a node are drawn in the node's group (a node 'ported'), so the layout check, which measures every
11572
- node against every other, does not take a port for an overlap of its own owner (§4.5, as built). Ships in
11670
+ and cut 280 (§8), a box with ports 48 px of padding (§7), repair and reroute off (§14). Every port is a node of the
11671
+ picture, with its owner's id (owner), which the layout check passes half inside it (decision 22). Ships in
11573
11672
  lib/umlimator-all.js only. */
11574
11673
 
11575
11674
  const CP_OBJ = x => x !== null && typeof x === 'object' && !Array.isArray(x), CP_ARR = x => (Array.isArray(x) ? x : []);
@@ -11620,6 +11719,11 @@ const CP_BOX = { boxTypes: ['uml.Component', 'uml.Package'], shape: x => (x.type
11620
11719
  const CP_WORD = { 'uml.Component': 'component', 'uml.Interface': 'interface', 'uml.Artifact': 'artifact', 'uml.Package': 'package', 'uml.Comment': 'comment', 'uml.Port': 'port' };
11621
11720
  function cpPic(model, view) {
11622
11721
  const I = cpIndex(model), pic = ownersOn(model, view, CP_BOX), shown = new Set(pic.ids);
11722
+ // a name is qualified by its components and packages only: an artifact nested in a node (deployed on it, as the
11723
+ // deployment view draws it) is not named by the node here (0.22.0, the Bookshop atlas)
11724
+ const qual = pic.name; pic.name = id => { const c = pic.chain(id), k = c.findIndex(p => !CP_BOX.boxTypes.includes((pic.at.get(p) || {}).type));
11725
+ if (k < 0) return qual(id); const up = c.slice(0, k), o = up.indexOf(pic.owner(id));
11726
+ return [...up.slice(0, o < 0 ? k : o).reverse(), id].map(x => String(pic.at.get(x).label ?? x)).join('::'); };
11623
11727
  // each line the view lists (or with no list each of this type's kinds) whose ends, a port taken to its owner, it shows
11624
11728
  const E = new Map(); CP_ARR(model && model.edges).forEach(e => { if (CP_OBJ(e) && typeof e.id === 'string' && !E.has(e.id)) E.set(e.id, e); });
11625
11729
  const list = Array.isArray(view.edges) ? view.edges.map(x => (typeof x === 'string' ? E.get(x) : x)) : [...E.values()].filter(e => CP_LINES.includes(e.type));
@@ -11661,10 +11765,6 @@ const CP_ENDS = { none: '', open: '<path d="M17 4L26 8L17 12" fill="none"/>', tr
11661
11765
  const cpLegend = kinds => Object.keys(CP_LOOK).filter(k => kinds.has(k)).map(k => { const [L, , name, says] = CP_LOOK[k], end = k === 'stem' ? 'ball' : L.to;
11662
11766
  return `<span class="lgi" title="${esc(name + ' — ' + says)}"><svg class="lgs" viewBox="0 0 28 16" aria-hidden="true"><g stroke="var(--um-muted)" stroke-width="1.4" stroke-linejoin="round"><path d="M2 8H${end === 'none' ? 26 : end === 'ball' ? 16 : end === 'socket' ? 23 : 17}"${L.dashed ? ' stroke-dasharray="3 2"' : ''}/>${CP_ENDS[end]}</g></svg><b>${esc(name)}</b><span>${esc(says)}</span></span>`; });
11663
11767
 
11664
- /* ---- a node with ports drawn in its own group: its shape's, with each port's node after it ---- */
11665
- SHAPE.ported = { size: n => SHAPE[n.base].size(n), out: (n, d) => SHAPE[n.base].out(n, d), inset: (n, s, t) => SHAPE[n.base].inset(n, s, t), face: n => SHAPE[n.base].face(n),
11666
- ports: (n, s, k) => SHAPE[n.base].ports(n, s, k), draw: (n, c, f) => SHAPE[n.base].draw({ ...n, shape: n.base }, c, f).replace(/<\/g>$/, CP_ARR(n.held).map(p => SHAPE.port.draw(p)).join('') + '</g>') };
11667
-
11668
11768
  /* ---- the view laid out (§10.1) ----
11669
11769
  Ranked here, not by the host, whose ranking reads only lines between the view's nodes and so none from a port (Q11): as
11670
11770
  the host ranks a view with boxes (blockPos, src/map/context.js), in one pass. Each box's contents, then the top, are
@@ -11746,13 +11846,11 @@ function cpGeo({ model, view, pos: pins, memo }) {
11746
11846
  ...(l.f.port || l.t.port ? { portRank: l.f.port && l.t.port ? r : r[l.f.port ? 0 : 1] } : {}), label: label || undefined, ...(label ? { cut: 280 } : {}),
11747
11847
  end: CP_LOOK[l.kind][0], aria: cpLineName(pic, l), ...(CP_OBJ(l.e.style) || typeof l.e.style === 'string' ? { style: l.e.style } : {}) }; });
11748
11848
  const g = placeTabs(layoutTyped({ nodes, edges, ...(boxes.length ? { containers: boxes } : {}), direction: down ? 'down' : 'right', gap: 48, rgap: 56, labelGaps: true, repair: false, reroute: false }, memo));
11749
- // on a copy (stage 4 §2.4): every port, its name, and each port on a node put in that node's group
11849
+ // on a copy (stage 4 §2.4): every port as a node, and its name
11750
11850
  const d = portsDrawn(g, shownPorts.map(({ p, owner }) => ({ id: p.id, owner, side: side[p.id], label: cpName(p), valueType: cpProps(p).valueType, aria: cpNodeName(model, view, pic, p.id) })));
11751
- const held = new Map(); d.nodes.forEach(n => { if (n.shape === 'port' && Object.hasOwn(g.byId, n.owner)) held.set(n.owner, [...(held.get(n.owner) || []), n]); });
11752
- const out = d.nodes.filter(n => !(n.shape === 'port' && held.has(n.owner))).map(n => (held.has(n.id) && n.shape !== 'port' ? { ...n, shape: 'ported', base: n.shape, held: held.get(n.id) } : n));
11753
11851
  // a slot after each element (src/typed/edit.js): the palette's add hears the selection through it (cpAdd, at.sel)
11754
- const byId = { ...d.byId, ...Object.fromEntries(out.map(n => [n.id, n])) }, slots = Object.values(byId).map((n, index) => ({ index, x: n.x, y: n.y, after: n.id }));
11755
- return { ...d, nodes: out, byId, slots, legend: cpLegend(new Set(lines.map(l => l.kind))), order: cpOrder(pic) };
11852
+ const slots = Object.values(d.byId).map((n, index) => ({ index, x: n.x, y: n.y, after: n.id }));
11853
+ return { ...d, slots, legend: cpLegend(new Set(lines.map(l => l.kind))), order: cpOrder(pic) };
11756
11854
  }
11757
11855
 
11758
11856
  /* ---- validators (§10.1). Each problem: { level, rule, path, message, ids }. With a view, the lines and elements it shows ---- */
@@ -11888,7 +11986,9 @@ const CP_TYPE = registerDiagramType('uml.component', {
11888
11986
  // the PDF's tables (§11): a component's ports, each with what it provides and requires; an interface's operations; what an artifact manifests
11889
11987
  word: (model, id) => { const x = cpIndex(model).N.get(id); return x ? ({ 'uml.Component': 'Component', 'uml.Interface': 'Interface', 'uml.Artifact': 'Artifact', 'uml.Package': 'Package', 'uml.Comment': 'Comment' }[x.type] || null) : null; },
11890
11988
  rows: (model, view, id) => { const pic = cpPic(model, view), x = pic.I.N.get(id); if (!x) return null;
11891
- if (x.type === 'uml.Component') return cpPortsOf(x).map(p => cpNodeName(model, view, pic, p.id).replace(/^Port /, '').replace(` of ${pic.name(id)}`, ''));
11989
+ // then the connectors from its ports (0.22.0), which the Lines table leaves out: a port is no card
11990
+ if (x.type === 'uml.Component') return [...cpPortsOf(x).map(p => cpNodeName(model, view, pic, p.id).replace(/^Port /, '').replace(` of ${pic.name(id)}`, '')),
11991
+ ...pic.lines.filter(l => l.e.type === 'uml.Connector' && (pic.I.P.get(l.e.from) || {}).owner === id).map(l => cpLineName(pic, l))];
11892
11992
  if (x.type === 'uml.Interface') return CP_ARR(x.members).filter(CP_OBJ).map(printMember);
11893
11993
  if (x.type === 'uml.Artifact') return pic.lines.filter(l => l.e.type === 'uml.Manifestation' && l.e.from === id).map(l => `manifests ${pic.name(l.e.to)}`);
11894
11994
  return null; },
@@ -11915,7 +12015,7 @@ const dpWord = x => (Object.hasOwn(DP_WORD, x.type) ? DP_WORD[x.type] : String(x
11915
12015
  const DP_KW = { 'uml.Device': 'device', 'uml.ExecutionEnvironment': 'executionEnvironment' };
11916
12016
 
11917
12017
  /* ---- the cube (§9.1): SHAPE.node's outline and inset, 10 px deep, in the typed look's paint; closed a node, open a frame ---- */
11918
- const DP = { d: 10, pad: 10, name: 12, nlh: 16, st: 11, sth: 14, minW: 120, maxW: 320, minH: 60 };
12018
+ const DP = { d: 10, head: 15, pad: 10, name: 12, nlh: 16, st: 11, sth: 14, minW: 120, maxW: 320, minH: 60 };
11919
12019
  const dpParts = n => { const st = [...(DP_KW[n.type] ? [DP_KW[n.type]] : []), ...DP_ARR(n.stereotypes)].map(t => `«${t}»`), name = String(n.label ?? '');
11920
12020
  return { st, name, w: Math.ceil(Math.min(DP.maxW, Math.max(DP.minW, ...st.map(t => textW(t, DP.st) + 2 * DP.pad), textW(name, DP.name) + 2 * DP.pad)) + DP.d),
11921
12021
  h: Math.max(DP.minH, DP.d + 16 + st.length * DP.sth + DP.nlh) }; };
@@ -11937,11 +12037,12 @@ SHAPE.cube = {
11937
12037
  + `<g class="c-head" data-band="${r2(y + p)} ${r2(y + h)}">${tx.join('')}</g></g>`;
11938
12038
  },
11939
12039
  // open: the frame holds the cube as a closed one does (its pad is 10 px more, dpPic), the front face its rect, the top and
11940
- // right faces inside the frame; the keyword, stereotypes and name (the box's title) at the top left of its 24 px header room
11941
- frame: f => { const p = DP.d, { x, y, w, h } = f;
12040
+ // right faces inside the frame; the box's title at the top left of its header room: the keyword and stereotypes (f.kw) on
12041
+ // a line above the name, as UML 2.5.1 draws them, in 24 px and the box's head more (dpPic); with neither, the name alone
12042
+ frame: f => { const p = DP.d, { x, y, w, h } = f, line = (t, dy) => `<text class="family-title" x="${r2(x + 12)}" y="${r2(y + p + dy)}" fill="var(--um-text)" style="font:600 12px var(--um-sans)" data-t="ft" data-o="${esc(f.id)}">${esc(fit(t, 12, w - p - 24))}</text>`;
11942
12043
  return `<rect class="family-box" x="${x}" y="${r2(y + p)}" width="${r2(w - p)}" height="${r2(h - p)}" stroke="var(--um-muted)"/>`
11943
12044
  + `<path d="M${r2(x)} ${r2(y + p)}L${r2(x + p)} ${r2(y)}H${r2(x + w)}V${r2(y + h - p)}L${r2(x + w - p)} ${r2(y + h)}M${r2(x + w - p)} ${r2(y + p)}L${r2(x + w)} ${r2(y)}" class="dp-face" fill="none" stroke="var(--um-muted)"/>`
11944
- + `<text class="family-title" x="${r2(x + 12)}" y="${r2(y + p + 17)}" fill="var(--um-text)" style="font:600 12px var(--um-sans)" data-t="ft" data-o="${esc(f.id)}">${esc(fit(f.title ?? f.label ?? f.id, 12, w - p - 24))}</text>`; },
12045
+ + (f.kw ? line(f.kw, 17) + line(f.name, 17 + DP.head) : line(f.title ?? f.label ?? f.id, 17)); },
11945
12046
  };
11946
12047
 
11947
12048
  /* ---- the view read: its boxes (§7), its lines, and every element's words ---- */
@@ -11959,7 +12060,9 @@ function dpIndex(model) {
11959
12060
  // the view's boxes, each titled with its keyword and stereotypes and named as §10.3 says
11960
12061
  function dpPic(model, view) {
11961
12062
  const pic = ownersOn(model, view, DP_LOOK), I = dpIndex(model);
11962
- const boxes = pic.boxes.map(b => { const x = pic.at.get(b.id); return { ...b, title: [...[...(DP_KW[x.type] ? [DP_KW[x.type]] : []), ...dpStereo(x)].map(t => `«${t}»`), dpName(x)].join(' '), aria: dpNodeName(x, pic, I), pad: 12 + DP.d }; });
12063
+ // a box with a keyword or stereotypes: they go on a line above its name (the frame's f.kw, f.name), in DP.head more header room
12064
+ const boxes = pic.boxes.map(b => { const x = pic.at.get(b.id), kw = [...(DP_KW[x.type] ? [DP_KW[x.type]] : []), ...dpStereo(x)].map(t => `«${t}»`).join(' ');
12065
+ return { ...b, title: kw ? kw + ' ' + dpName(x) : dpName(x), ...(kw ? { kw, name: dpName(x), head: DP.head } : {}), aria: dpNodeName(x, pic, I), pad: 12 + DP.d }; });
11963
12066
  return { ...pic, I, boxes };
11964
12067
  }
11965
12068
  // the lines drawn: those the view lists, or with no list the model's of this type's kinds, between two elements it shows; a
@@ -12259,6 +12362,8 @@ const csEndName = (P, x) => (x.part != null ? `${csName(P.mem.get(x.id).m)} of $
12259
12362
  const CS_LINE_WORD = { 'uml.Connector': 'Connector', 'uml.RoleBinding': 'Role binding', 'uml.CommentLink': 'Comment link' };
12260
12363
  const csLineName = (P, { e, f, t }) => e.type === 'uml.RoleBinding' ? `Role binding from ${csEndName(P, f)} to ${csEndName(P, t)}${CS_HAS(e.label) ? ', as ' + e.label : ''}`
12261
12364
  : `${CS_LINE_WORD[e.type] || 'Line'} from ${csEndName(P, f)} to ${csEndName(P, t)}${CS_HAS(e.label) ? ': ' + e.label : ''}`;
12365
+ // the structure a connector is drawn in: its from end's part's or role's, else its from end's owner (a port of the subject)
12366
+ const csStructOf = (P, e) => { const p = (CS_PROPS(e).ends || {}).from, k = CS_OBJ(p) && p.part != null ? p.part : e.from; return P.partOf.get(k) || (P.mem.get(k) || {}).owner; };
12262
12367
  // the subject and its ports, each part and its ports, the collaboration uses, each open collaboration and its roles, the rest; then the lines
12263
12368
  function csOrder(P) {
12264
12369
  const out = [], part = m => { out.push(m.id); P.partPorts.get(m.id).forEach(q => out.push(csPortId(m.id, q.id))); };
@@ -12531,8 +12636,1015 @@ const CS_TYPE = registerDiagramType('uml.composite-structure', {
12531
12636
  return x.type === 'uml.Property' ? (q.owner.type === 'uml.Collaboration' ? 'Role' : 'Part') : x.type === 'uml.Port' ? 'Port' : x.type === 'uml.CollaborationUse' ? 'Collaboration use' : null; },
12532
12637
  rows: (model, view, id) => { const P = csPic(model, view), q = P.mem.get(id);
12533
12638
  if (q && q.m.type === 'uml.Property') return [csPartText(q.m), CS_PROPS(q.m).aggregation === 'shared' ? 'shared' : 'composite'];
12534
- const u = P.at.get(id); if (!u || u.type !== 'uml.CollaborationUse') return null;
12639
+ const u = P.at.get(id); if (!u) return null;
12640
+ // the subject or an open collaboration (0.22.0): its members, a part or role with its aggregation, then the connectors
12641
+ // among them, which the Lines table leaves out (their ends are members, not cards)
12642
+ if (id === P.sid && P.S || P.collabs.some(c => c.x.id === id)) return [...CS_ARR(u.members).filter(CS_OBJ).map(m => CS_MEMBER.print(m) + (m.type === 'uml.Property' && id === P.sid ? `, ${CS_PROPS(m).aggregation === 'shared' ? 'shared' : 'composite'}` : '')),
12643
+ ...P.lines.filter(l => l.e.type === 'uml.Connector' && csStructOf(P, l.e) === u).map(l => csLineName(P, l))];
12644
+ if (u.type !== 'uml.CollaborationUse') return null;
12535
12645
  return [...(CS_HAS(CS_PROPS(u).collaboration) ? [String(CS_PROPS(u).collaboration)] : []),
12536
12646
  ...CS_ARR(model.edges).filter(e => CS_OBJ(e) && e.type === 'uml.RoleBinding' && e.to === id).map(e => `${e.label ?? '?'} ← ${csName(P.el(e.from) || { id: e.from })}`)]; },
12537
12647
  edits: { connect: csConnect, moveTo: csMoveTo, add: csAdd, remove: csRemove, rename: csRename, member: csMember },
12538
12648
  });
12649
+ /* ---------- stereotypes applied across the model (work package 6.2, notes/stage-6.md §5) ----------
12650
+ A profile (uml.Profile) defines stereotypes (uml.Stereotype, their tag definitions uml.Property members) that extend
12651
+ metaclasses (uml.Extension to a uml.MetaclassReference); a uml.ProfileApplication from a package applies it to everything
12652
+ the package owns, at any depth (§5.2). Every element of the model that names a stereotype in props.stereotypes is checked
12653
+ here, whatever view draws it: the name (§5.3), the metaclass it fits (§5.4), required extensions (§5.5) and its tags,
12654
+ props.tags keyed by stereotype then tag (§5.6). Fills two slots of the core (src/core/registry.js's TYPED):
12655
+ - TYPED.across(model) -> problems, which validate() runs once on a version-2 model: [] at once when it holds no profile;
12656
+ - TYPED.fields(model, element) -> the panel's fields for the element's tags, after its type's own (§5.7).
12657
+ appliedOf(model, id) is what the profile type and the PDF read. Only lib/umlimator-all.js holds this file. */
12658
+
12659
+ const STY_OBJ = x => x !== null && typeof x === 'object' && !Array.isArray(x), STY_ARR = x => (Array.isArray(x) ? x : []);
12660
+ const styKey = s => s.charAt(0).toLowerCase() + s.slice(1); // names meet with the first letter in either case (§5.3): Entity is «entity»
12661
+ const styName = x => String(x.label ?? x.id), styQ = x => JSON.stringify(styName(x));
12662
+
12663
+ /* UML 2.5.1's metaclasses (§5.4), each with its generals as OMG's UML.xmi (20161101) lists them: a row "A B: C D" says A and B specialize C, then D.
12664
+ A row for every uml.* type a registered type uses (tests/stereotypes.test.mjs holds it), and the abstract ones between */
12665
+ const UML_META = {};
12666
+ `Element:
12667
+ NamedElement MultiplicityElement TemplateableElement ParameterableElement Comment Slot Relationship: Element
12668
+ Namespace RedefinableElement TypedElement DeploymentTarget DeployedArtifact ActivityGroup InteractionFragment Lifeline Message CollaborationUse: NamedElement
12669
+ PackageableElement: NamedElement ParameterableElement
12670
+ Vertex: NamedElement RedefinableElement
12671
+ Type Constraint: PackageableElement
12672
+ IntervalConstraint: Constraint
12673
+ DurationConstraint TimeConstraint: IntervalConstraint
12674
+ ConnectableElement: TypedElement ParameterableElement
12675
+ Classifier: Namespace Type RedefinableElement TemplateableElement
12676
+ StructuredClassifier BehavioredClassifier Interface DataType InformationItem Signal: Classifier
12677
+ Artifact: Classifier DeployedArtifact
12678
+ EncapsulatedClassifier: StructuredClassifier
12679
+ Class: EncapsulatedClassifier BehavioredClassifier
12680
+ Component Stereotype Behavior: Class
12681
+ Node: Class DeploymentTarget
12682
+ Activity StateMachine: Behavior
12683
+ Interaction: Behavior InteractionFragment
12684
+ Device ExecutionEnvironment: Node
12685
+ Actor UseCase: BehavioredClassifier
12686
+ Collaboration: StructuredClassifier BehavioredClassifier
12687
+ Association: Relationship Classifier
12688
+ Extension CommunicationPath: Association
12689
+ Enumeration PrimitiveType: DataType
12690
+ DeploymentSpecification: Artifact
12691
+ Package: Namespace PackageableElement TemplateableElement
12692
+ Profile Model: Package
12693
+ Feature: RedefinableElement
12694
+ StructuralFeature: TypedElement MultiplicityElement Feature
12695
+ Property: StructuralFeature ConnectableElement DeploymentTarget
12696
+ Port: Property
12697
+ BehavioralFeature: Feature Namespace
12698
+ Operation: BehavioralFeature TemplateableElement ParameterableElement
12699
+ Connector: Feature
12700
+ InstanceSpecification: PackageableElement DeploymentTarget DeployedArtifact
12701
+ EnumerationLiteral: InstanceSpecification
12702
+ DirectedRelationship: Relationship
12703
+ Dependency: DirectedRelationship PackageableElement
12704
+ Usage Abstraction Deployment: Dependency
12705
+ Realization Manifestation: Abstraction
12706
+ InterfaceRealization ComponentRealization: Realization
12707
+ Generalization PackageImport PackageMerge ProfileApplication ElementImport: DirectedRelationship
12708
+ InformationFlow: DirectedRelationship PackageableElement
12709
+ Include Extend: DirectedRelationship NamedElement
12710
+ MetaclassReference: ElementImport
12711
+ ActivityNode ActivityEdge Region Transition ExtensionPoint: RedefinableElement
12712
+ ExecutableNode ControlNode: ActivityNode
12713
+ ObjectNode: ActivityNode TypedElement
12714
+ Action: ExecutableNode
12715
+ InvocationAction AcceptEventAction: Action
12716
+ CallAction SendSignalAction: InvocationAction
12717
+ CallBehaviorAction: CallAction
12718
+ InitialNode DecisionNode MergeNode ForkNode JoinNode FinalNode: ControlNode
12719
+ ActivityFinalNode FlowFinalNode: FinalNode
12720
+ CentralBufferNode: ObjectNode
12721
+ DataStoreNode: CentralBufferNode
12722
+ ControlFlow ObjectFlow: ActivityEdge
12723
+ ActivityPartition: ActivityGroup
12724
+ State: Namespace Vertex
12725
+ Pseudostate: Vertex
12726
+ FinalState: State
12727
+ CombinedFragment InteractionUse StateInvariant: InteractionFragment
12728
+ InteractionOperand: InteractionFragment Namespace`.split('\n').forEach(row => { const [a, b] = row.split(':'), g = b.trim() ? b.trim().split(' ') : []; a.split(' ').forEach(n => { UML_META[n] = g; }); });
12729
+ Object.assign(UML_META, { Region: ['Namespace', 'RedefinableElement'], Transition: ['Namespace', 'RedefinableElement'] }); // through Namespace too
12730
+ // the registry's types that are no metaclass, and what they are (§5.4); null: no stereotype fits it
12731
+ const UML_AS = { InternalTransition: 'Transition', Subject: 'Classifier', Link: 'InstanceSpecification', RoleBinding: 'Dependency', CommentLink: null };
12732
+ // a uml.* element's metaclass, null for none, undefined for an element of no UML type (a sysml. one is not checked, §5.4)
12733
+ const metaOf = x => typeof x.type !== 'string' || !x.type.startsWith('uml.') ? undefined : Object.hasOwn(UML_AS, x.type.slice(4)) ? UML_AS[x.type.slice(4)] : x.type.slice(4);
12734
+ const META_UP = new Map();
12735
+ // a metaclass and every metaclass it specializes; a name in no row is only itself
12736
+ function metaUp(name) {
12737
+ if (!META_UP.has(name)) { const out = new Set([name]); META_UP.set(name, out); (Object.hasOwn(UML_META, name) ? UML_META[name] : []).forEach(g => metaUp(g).forEach(x => out.add(x))); }
12738
+ return META_UP.get(name);
12739
+ }
12740
+ const metaKnown = name => Object.hasOwn(UML_META, name);
12741
+
12742
+ // UML 2.5.1's standard profile (Table 22.1) and keywords (Table C.1, three of them two words), and earlier UML's that
12743
+ // models still store (after, all, at, dataStore, deploymentSpec, else, from, when): free keywords inside an applied profile (§5.3, 3)
12744
+ const STY_FREE = new Set(('auxiliary buildComponent call create derive destroy document entity executable file focus framework implement implementationClass instantiate library metaclass '
12745
+ + 'metamodel modelLibrary process realization refine responsibility script send service source specification subsystem systemModel trace type utility '
12746
+ + 'abstraction access activity actor apply artifact centralBuffer bind collaboration component datastore dataType decisionInput decisionInputFlow deploy device '
12747
+ + 'enumeration executionEnvironment extend extended external final flow import include information interface iterative localPostcondition localPrecondition '
12748
+ + 'manifest merge model multicast multireceive occurrence parallel postcondition protocol precondition primitive profile reference representation selection signal '
12749
+ + 'singleExecution statemachine stream stereotype strict structured substitute transformation use '
12750
+ + 'after all at dataStore deploymentSpec else from when').split(' ').concat('deployment spec', 'element access', 'element import').map(styKey));
12751
+
12752
+ const STY_RULES = {
12753
+ 'stereotype-unknown': 'a stereotype applied inside an applied profile\'s scope is one of its stereotypes, a standard one or a UML keyword',
12754
+ 'stereotype-applied': 'a stereotype of a profile is applied only where that profile is applied',
12755
+ 'stereotype-ambiguous': 'a name two applied profiles define is written Profile::name',
12756
+ 'stereotype-extends': 'an applied stereotype extends the element\'s metaclass or one it specializes',
12757
+ 'stereotype-abstract': 'an abstract stereotype is not applied',
12758
+ 'stereotype-required': 'an abstract required stereotype is met by a specialization the element applies',
12759
+ 'tag-defined': 'each tag is under a stereotype the element has, and that stereotype or a general defines it; a redefined tag keeps its type',
12760
+ 'tag-value': 'a tag\'s value fits its type',
12761
+ 'tag-required': 'a tag of lower bound 1 with no default has a value',
12762
+ };
12763
+
12764
+ /* The model read once (§12): its elements with their paths and the id whose scope they take, profiles, stereotypes by name,
12765
+ applications, generals, extensions; scopes memoised per owner. Kept per model object for appliedOf and fields */
12766
+ // ponytail: kept per model object, so a model edited in place keeps its first reading until TYPED.across reads it again;
12767
+ // apply() returns a new model, so the canvas never does. A version stamp per model is the upgrade if a host edits in place
12768
+ const STY_SEEN = new WeakMap();
12769
+ function styIndex(model) {
12770
+ const at = new Map(), up = new Map(), rec = new Map(), els = []; // up: an element's owner (§5.2); a view's own elements have none
12771
+ const put = (x, path, own, scoped) => { if (!STY_OBJ(x) || typeof x.id !== 'string') return; const r = { x, path, own: scoped ? own : undefined, line: scoped && (x.from != null || x.to != null) };
12772
+ if (!at.has(x.id)) { at.set(x.id, x); rec.set(x.id, r); if (scoped) up.set(x.id, own); }
12773
+ els.push(r); STY_ARR(x.members).forEach((m, j) => put(m, `${path}.members[${j}]`, x.id, scoped)); };
12774
+ STY_ARR(model.nodes).forEach((n, i) => put(n, `nodes[${i}]`, n && n.parent, true));
12775
+ // a line's owner: its parent, else its from end's owner (§5.2), read once every node and member is in
12776
+ STY_ARR(model.edges).forEach((e, i) => put(e, `edges[${i}]`, undefined, true));
12777
+ els.forEach(r => { if (r.line) { r.own = r.x.parent != null ? r.x.parent : up.get(r.x.from); if (rec.get(r.x.id) === r) up.set(r.x.id, r.own); } });
12778
+ STY_ARR(model.views).forEach((v, k) => { if (STY_OBJ(v)) ['nodes', 'edges', 'fragments'].forEach(f => STY_ARR(v[f]).forEach((x, j) => put(x, `views[${k}].${f}[${j}]`, undefined, false))); }); // a view's own: no scope
12779
+ const isT = (id, t) => (at.get(id) || {}).type === t, profiles = [...at.values()].filter(x => x.type === 'uml.Profile');
12780
+ const I = { at, rec, els, profiles, byProfile: new Map(), byName: new Map(), byLabel: new Map(), enums: new Map(), applied: new Map(), gens: new Map(), ext: new Map(), req: new Map(), memo: new Map(), cache: new Map(), defsOf: new Map(), upOf: new Map(), probs: [] };
12781
+ if (!profiles.length) return I;
12782
+ profiles.forEach(p => { I.byProfile.set(p.id, new Map()); I.enums.set(p.id, new Map()); if (!I.byLabel.has(styName(p))) I.byLabel.set(styName(p), []); I.byLabel.get(styName(p)).push(p); });
12783
+ [...at.values()].forEach(x => { if (!I.byProfile.has(x.parent) || up.get(x.id) !== x.parent) return; const k = styKey(styName(x));
12784
+ if (x.type === 'uml.Stereotype' && !I.byProfile.get(x.parent).has(k)) { I.byProfile.get(x.parent).set(k, x); if (!I.byName.has(k)) I.byName.set(k, []); I.byName.get(k).push(at.get(x.parent)); }
12785
+ if (x.type === 'uml.Enumeration') I.enums.get(x.parent).set(styName(x), STY_ARR(x.members).filter(m => STY_OBJ(m) && m.type === 'uml.EnumerationLiteral').map(styName)); });
12786
+ const add = (map, k, v) => { if (!map.has(k)) map.set(k, []); map.get(k).push(v); };
12787
+ STY_ARR(model.edges).forEach(e => { if (!STY_OBJ(e)) return;
12788
+ if (e.type === 'uml.ProfileApplication' && isT(e.to, 'uml.Profile') && at.has(e.from)) add(I.applied, e.from, e.to);
12789
+ if (e.type === 'uml.Generalization' && isT(e.from, 'uml.Stereotype') && isT(e.to, 'uml.Stereotype')) add(I.gens, e.from, e.to);
12790
+ if (e.type === 'uml.Extension' && isT(e.from, 'uml.Stereotype') && isT(e.to, 'uml.MetaclassReference')) { const meta = styName(at.get(e.to)); add(I.ext, e.from, meta);
12791
+ const p = at.get(e.from).parent; if (STY_OBJ(e.props) && e.props.isRequired === true && I.byProfile.has(p)) add(I.req, p, { st: at.get(e.from), meta }); } });
12792
+ // the profiles applied to an owner and its owners: memoised per owner, so no element walks its chain on its own
12793
+ I.scope = id => { if (id == null || !at.has(id)) return []; if (I.memo.has(id)) return I.memo.get(id); I.memo.set(id, []); // a cycle (the core's error) ends here
12794
+ const above = I.scope(up.get(id)), mine = (I.applied.get(id) || []).filter(p => !above.includes(p)), s = mine.length ? [...above, ...mine] : above; I.memo.set(id, s); return s; };
12795
+ // an owner inside a profile, or the profile: its own definitions, which its required stereotypes do not reach (§5.5)
12796
+ const inP = new Map(); I.inProf = id => { if (id == null || !at.has(id)) return false; if (!inP.has(id)) { inP.set(id, false); inP.set(id, at.get(id).type === 'uml.Profile' || I.inProf(up.get(id))); } return inP.get(id); };
12797
+ return I;
12798
+ }
12799
+ const styOf = model => { if (!STY_SEEN.has(model)) STY_SEEN.set(model, styIndex(model)); return STY_SEEN.get(model); };
12800
+
12801
+ // a stereotype's generals, nearest first, each once
12802
+ function styGenerals(I, st) {
12803
+ if (I.upOf.has(st.id)) return I.upOf.get(st.id);
12804
+ const out = [], seen = new Set([st.id]);
12805
+ for (let i = 0, q = [st.id]; i < q.length; i++) STY_ARR(I.gens.get(q[i])).forEach(g => { if (!seen.has(g)) { seen.add(g); q.push(g); out.push(I.at.get(g)); } });
12806
+ I.upOf.set(st.id, out); return out;
12807
+ }
12808
+ /* its tag definitions, its generals' first, a specialization's redefining a general's of one name (§5.6): name -> { def, st }.
12809
+ Each stereotype says only its own: a redefinition of another type, or two unrelated generals' tags of one name and two types */
12810
+ function styDefs(I, st) {
12811
+ if (I.defsOf.has(st.id)) return I.defsOf.get(st.id);
12812
+ const out = new Map(), clash = new Map(), t = x => STY_OBJ(x.props) ? x.props.valueType ?? 'String' : 'String'; I.defsOf.set(st.id, out); // a cycle (6.3's error) ends here
12813
+ STY_ARR(I.gens.get(st.id)).forEach(g => styDefs(I, I.at.get(g)).forEach((e, n) => { const was = out.get(n);
12814
+ if (!was || styGenerals(I, e.st).includes(was.st)) return out.set(n, e); // e redefines it
12815
+ if (was.def !== e.def && !styGenerals(I, was.st).includes(e.st) && t(was.def) !== t(e.def)) clash.set(n, [was, e]); }));
12816
+ STY_ARR(st.members).forEach(d => { if (!STY_OBJ(d) || d.type !== 'uml.Property') return; const n = styName(d), was = out.get(n); clash.delete(n);
12817
+ if (was && t(was.def) !== t(d)) I.probs.push(stySay('tag-defined', 'warning', styPath(I, d), [d.id, was.def.id], `«${styName(st)}» redefines tag "${n}" of «${styName(was.st)}» as ${t(d)}, not ${t(was.def)}`));
12818
+ out.set(n, { def: d, st }); });
12819
+ clash.forEach(([a, b], n) => I.probs.push(stySay('tag-defined', 'warning', styPath(I, st), [st.id, a.def.id, b.def.id], `«${styName(st)}» inherits tag "${n}" from «${styName(a.st)}» as ${t(a.def)} and from «${styName(b.st)}» as ${t(b.def)}: redefine it`)));
12820
+ return out;
12821
+ }
12822
+ const styFits = (I, st, meta) => [st, ...styGenerals(I, st)].some(s => STY_ARR(I.ext.get(s.id)).some(m => metaUp(meta).has(m)));
12823
+ const stySay = (rule, level, path, ids, msg) => ({ level, rule, path, ids, message: `${rule}: ${msg}` });
12824
+ const styPath = (I, x) => I.rec.get(x.id).path;
12825
+
12826
+ // a tag's value against its type (§5.6): Boolean, Integer, Real, UnlimitedNatural, an enumeration of the profile, else any text
12827
+ function styTagFits(I, def, prof, v) {
12828
+ const t = STY_OBJ(def.props) ? def.props.valueType : null, s = typeof v === 'object' ? null : String(v), re = (x, say) => (s !== null && x.test(s)) || say; // a list or a map is no value
12829
+ if (t === 'Boolean') return re(/^(true|false)$/, 'true or false');
12830
+ if (t === 'Integer') return re(/^-?\d+$/, 'a whole number');
12831
+ if (t === 'Real') return re(/^-?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/, 'a number');
12832
+ if (t === 'UnlimitedNatural') return re(/^(\d+|\*)$/, 'a whole number or *');
12833
+ const lits = prof && I.enums.get(prof.id).get(t);
12834
+ return lits ? (s !== null && lits.includes(s)) || `one of ${lits.join(', ')}` : s !== null || 'text';
12835
+ }
12836
+
12837
+ /* One element's stereotypes, resolved by §5.3, with the required ones its scope supplies (§5.5) and their tags (§5.6):
12838
+ [{ stereotype, profile, key, required, tags: [{ name, def, value }] }], key the name props.tags keys it by. say: problems */
12839
+ function styResolve(I, r, say) {
12840
+ const { x, path } = r, P = STY_OBJ(x.props) ? x.props : {}, scope = I.scope(I.applied.has(x.id) && I.rec.get(x.id) === r ? x.id : r.own), meta = metaOf(x); // a package applying a profile is in its scope
12841
+ const names = p => p.map(styName).join(' and '), owner = I.at.get(r.own), out = [], again = new Map();
12842
+ STY_ARR(P.stereotypes).forEach((s, k) => { if (typeof s !== 'string' || !s) return; const at = `${path}.props.stereotypes[${k}]`, q = s.indexOf('::');
12843
+ let hit = null;
12844
+ if (q > 0) { const ps = I.byLabel.get(s.slice(0, q)) || [], k2 = styKey(s.slice(q + 2)), has = p => I.byProfile.get(p.id).has(k2); // two of one label: the one applied here
12845
+ const p = ps.find(p => has(p) && scope.includes(p.id)) || ps.find(has) || ps[0], st = p && I.byProfile.get(p.id).get(k2);
12846
+ if (!st) return say(stySay('stereotype-unknown', 'warning', at, [x.id], `«${s}» on ${styQ(x)}: ${p ? `profile ${styName(p)} defines no stereotype ${s.slice(q + 2)}` : `no profile is named ${s.slice(0, q)}`}`));
12847
+ hit = { st, p }; }
12848
+ else { const k2 = styKey(s), here = scope.map(id => I.at.get(id)).filter(p => I.byProfile.get(p.id).has(k2));
12849
+ if (here.length > 1) return say(stySay('stereotype-ambiguous', 'warning', at, [x.id, ...here.map(p => p.id)], `«${s}» on ${styQ(x)} is a stereotype of ${names(here)}, both applied: write ${here.map(p => `${styName(p)}::${s}`).join(' or ')}`));
12850
+ if (here.length) hit = { st: I.byProfile.get(here[0].id).get(k2), p: here[0] };
12851
+ else if (STY_FREE.has(k2)) return; // a standard stereotype or a keyword
12852
+ else if (scope.length) return say(stySay('stereotype-unknown', 'warning', at, [x.id], `«${s}» on ${styQ(x)} is no stereotype of ${names(scope.map(id => I.at.get(id)))}, applied here, and no standard one`));
12853
+ else if (I.byName.has(k2)) { const ps = I.byName.get(k2); return say(stySay('stereotype-applied', 'warning', at, [x.id, ...ps.map(p => p.id)], `«${s}» is a stereotype of ${names(ps)}, which is not applied to ${owner ? `${styName(owner)} or its owners` : `any owner of ${styQ(x)}`}`)); }
12854
+ else return; }
12855
+ const twice = out.find(e => e.stereotype === hit.st); if (twice) return void again.set(s, twice.key);
12856
+ if (!scope.includes(hit.p.id)) return say(stySay('stereotype-applied', 'warning', at, [x.id, hit.p.id], `«${s}» is a stereotype of ${styName(hit.p)}, which is not applied to ${owner ? `${styName(owner)} or its owners` : `any owner of ${styQ(x)}`}`));
12857
+ if (meta !== undefined && !(meta && styFits(I, hit.st, meta))) say(stySay('stereotype-extends', 'warning', at, [x.id, hit.st.id], `«${s}» on ${styQ(x)} extends ${[...new Set([hit.st, ...styGenerals(I, hit.st)].flatMap(t => STY_ARR(I.ext.get(t.id))))].join(', ') || 'no metaclass'}, which a ${meta || String(x.type).slice(4)} is not`));
12858
+ if (STY_OBJ(hit.st.props) && hit.st.props.isAbstract === true) say(stySay('stereotype-abstract', 'error', at, [x.id, hit.st.id], `«${s}» on ${styQ(x)} is abstract: apply one of its specializations`));
12859
+ out.push({ stereotype: hit.st, profile: hit.p, key: s, required: false }); });
12860
+ // required extensions of the profiles in scope, on an element of their metaclass: met by the stereotype or a specialization the
12861
+ // element applies; else supplied by the tool, or warned when abstract (decision 22)
12862
+ if (meta && !(x.type === 'uml.Profile' || I.inProf(r.own))) scope.forEach(pid => STY_ARR(I.req.get(pid)).forEach(({ st, meta: m }) => { if (!metaUp(meta).has(m)) return;
12863
+ const met = out.filter(e => e.stereotype === st || styGenerals(I, e.stereotype).includes(st)); if (met.length) return met.forEach(e => { e.required = true; });
12864
+ if (STY_OBJ(st.props) && st.props.isAbstract === true) return say(stySay('stereotype-required', 'warning', path, [x.id, st.id], `${styQ(x)} applies no specialization of «${styName(st)}», abstract and required on ${m} by ${styName(I.at.get(pid))}`));
12865
+ if (!out.some(e => e.stereotype === st)) { const two = scope.filter(id => I.byProfile.get(id).has(styKey(styName(st)))).length > 1; // the key as §5.3 would write it
12866
+ out.push({ stereotype: st, profile: I.at.get(pid), key: two ? `${styName(I.at.get(pid))}::${styName(st)}` : styName(st), required: true }); } }));
12867
+ // tags, keyed by stereotype then tag
12868
+ const T = P.tags, tp = `${path}.props.tags`;
12869
+ if (T != null && !STY_OBJ(T)) say(stySay('tag-defined', 'warning', tp, [x.id], `the tags of ${styQ(x)} are a map from each stereotype to its tags`));
12870
+ else if (T) Object.keys(T).forEach(k => { const e = out.find(o => o.key === k), inner = T[k];
12871
+ if (!e) return say(stySay('tag-defined', 'warning', tp, [x.id], `${styQ(x)} keeps tags under "${k}", which is no stereotype it has${again.has(k) ? `: it names «${again.get(k)}» again, whose tags are under "${again.get(k)}"` : ''}`));
12872
+ if (!STY_OBJ(inner)) return say(stySay('tag-defined', 'warning', tp, [x.id], `the tags of ${styQ(x)} under «${k}» are a map from each tag to its value`));
12873
+ const D = styDefs(I, e.stereotype);
12874
+ Object.keys(inner).forEach(n => { const d = D.get(n), v = inner[n];
12875
+ if (!d) return say(stySay('tag-defined', 'warning', tp, [x.id, e.stereotype.id], `«${k}» on ${styQ(x)} has no tag "${n}"`));
12876
+ const ok = v == null || styTagFits(I, d.def, I.at.get(d.st.parent), v);
12877
+ if (ok !== true) say(stySay('tag-value', 'warning', tp, [x.id, d.def.id], `«${k}» ${n} = ${JSON.stringify(v)} on ${styQ(x)} is not ${ok}`)); }); });
12878
+ out.forEach(e => { const inner = STY_OBJ(T) && Object.hasOwn(T, e.key) && STY_OBJ(T[e.key]) ? T[e.key] : {};
12879
+ e.tags = [...styDefs(I, e.stereotype)].map(([name, { def }]) => ({ name, def, value: Object.hasOwn(inner, name) ? inner[name] : undefined }));
12880
+ e.tags.forEach(({ name, def, value }) => { const dp = STY_OBJ(def.props) ? def.props : {};
12881
+ if ((value == null || value === '') && dp.default == null && (multBounds(dp.multiplicity ?? '1') || { lower: 1 }).lower >= 1) say(stySay('tag-required', 'warning', tp, [x.id, def.id], `«${e.key}» ${name} on ${styQ(x)} has no value and no default`)); }); });
12882
+ return out;
12883
+ }
12884
+
12885
+ // every element's problems (§5.3 to §5.6), and the profiles' redefined tags: [] at once for a model with no profile
12886
+ function stereoProblems(model) {
12887
+ const I = styIndex(STY_OBJ(model) ? model : {}); if (STY_OBJ(model)) STY_SEEN.set(model, I);
12888
+ if (!I.profiles.length) return [];
12889
+ const out = [];
12890
+ I.byProfile.forEach(sts => sts.forEach(st => styDefs(I, st)));
12891
+ I.els.forEach(r => { const e = styResolve(I, r, p => out.push(p)); if (!I.cache.has(r.x.id)) I.cache.set(r.x.id, e); });
12892
+ return [...I.probs, ...out];
12893
+ }
12894
+
12895
+ // the stereotypes an element has, stored and required, with their tags (§5.5, §5.6): what 6.3 draws and the PDF lists
12896
+ function appliedOf(model, id) {
12897
+ const I = styOf(model); if (!I.profiles.length) return [];
12898
+ if (!I.cache.has(id)) { const r = I.rec.get(id); I.cache.set(id, r ? styResolve(I, r, () => {}) : []); }
12899
+ return I.cache.get(id);
12900
+ }
12901
+
12902
+ /* The panel's fields for an element's tags (§5.7): one per tag of each stereotype it has, its key carrying the stereotype so two
12903
+ stereotypes' `name` keep their own focus; a Boolean or an enumeration of the profile a choice, else text */
12904
+ function tagFields(model, x) {
12905
+ if (!STY_OBJ(x) || typeof x.id !== 'string') return [];
12906
+ const I = styOf(model);
12907
+ return appliedOf(model, x.id).flatMap(e => e.tags.map(({ name, def }) => { const t = STY_OBJ(def.props) ? def.props.valueType : null, p = I.at.get(e.stereotype.parent);
12908
+ const lits = t === 'Boolean' ? ['true', 'false'] : p && I.enums.get(p.id).get(t);
12909
+ return { key: `p:${e.key}:${name}`, path: ['tags', e.key, name], label: `«${styName(e.stereotype)}» ${name}`, ...(lits ? { kind: 'enum', options: lits } : { kind: 'text' }) }; }));
12910
+ }
12911
+
12912
+ TYPED.across = stereoProblems;
12913
+ TYPED.fields = tagFields;
12914
+ /* ---------- the profile diagram, uml.profile (work package 6.3, notes/stage-6.md §4) ----------
12915
+ A profile (a package with «profile») holding its stereotypes («stereotype», their tag definitions as attribute rows),
12916
+ metaclass references («metaclass») and enumerations; extensions (a solid line with a filled triangle at the metaclass,
12917
+ {required} when required), profile applications (dashed, «apply»), generalizations between stereotypes, comments. Drawn by
12918
+ the class type's look (typeGeo, src/uml/class.js), down by default, with repair and reroute off (§12). Any other element
12919
+ shown is its class or package look with a compartment per stereotype it has (src/uml/stereotypes.js's appliedOf, the
12920
+ required ones too), headed «name», each row `tag = value`, and its members that apply a stereotype, `«column» - placedAt:
12921
+ Instant {name = placed_at}`. Only lib/umlimator-all.js holds this file. */
12922
+
12923
+ const PR_OBJ = x => x !== null && typeof x === 'object' && !Array.isArray(x), PR_ARR = x => (Array.isArray(x) ? x : []);
12924
+ const prName = x => String(x.label ?? x.id);
12925
+ const prWord = t => t === 'uml.MetaclassReference' ? 'Metaclass' : String(t || 'uml.Class').split('.').pop().replace(/([a-z])([A-Z])/g, (_, a, b) => a + ' ' + b.toLowerCase()); // 'uml.DataType': "Data type"
12926
+ const PR_DEFS = ['uml.Stereotype', 'uml.MetaclassReference', 'uml.Enumeration']; // what goes into a profile (§4.3)
12927
+
12928
+ // the model read once per call: nodes by id, every member by id, lines by kind
12929
+ function prIndex(model) {
12930
+ const at = new Map(), mem = new Map();
12931
+ PR_ARR(model && model.nodes).forEach(n => { if (PR_OBJ(n) && typeof n.id === 'string' && !at.has(n.id)) { at.set(n.id, n); PR_ARR(n.members).forEach(m => { if (PR_OBJ(m) && typeof m.id === 'string') mem.set(m.id, m); }); } });
12932
+ const E = PR_ARR(model && model.edges).filter(e => PR_OBJ(e) && typeof e.id === 'string'), of = t => E.filter(e => e.type === t);
12933
+ return { at, mem, E, of, ty: id => (at.get(id) || {}).type };
12934
+ }
12935
+
12936
+ // a stereotype as an element has it: «key», required, and its tags `name = value` (a default when unset; `?` for a value a
12937
+ // tag of lower bound 1 needs; an optional one unset is left out)
12938
+ function prTags(e) {
12939
+ return e.tags.flatMap(({ name, def, value }) => { const d = PR_OBJ(def.props) ? def.props : {}, need = (multBounds(d.multiplicity ?? '1') || { lower: 1 }).lower >= 1;
12940
+ const v = value != null && value !== '' ? value : d.default != null ? d.default : need ? '?' : null;
12941
+ return v == null ? [] : [`${name} = ${typeof v === 'object' ? JSON.stringify(v) : v}`]; });
12942
+ }
12943
+ const prSay = e => { const t = prTags(e); return `«${e.key}»${e.required ? ' required' : ''}${t.length ? ' ' + t.join(', ') : ''}`; };
12944
+
12945
+ /* ---- what each element says (§9): its PDF rows, and its accessible name after its kind and name ---- */
12946
+ function prRows(model, x, I = prIndex(model)) {
12947
+ const nm = id => prName(I.at.get(id)), from = (t, id) => I.of(t).filter(e => e.from === id && I.at.has(e.to)), to = (t, id) => I.of(t).filter(e => e.to === id && I.at.has(e.from));
12948
+ if (x.type === 'uml.Profile') return [...[...I.at.values()].filter(n => n.parent === x.id && (n.type === 'uml.Stereotype' || n.type === 'uml.MetaclassReference')).map(n => (n.type === 'uml.Stereotype' ? `«${prName(n)}»` : `metaclass ${prName(n)}`)),
12949
+ ...to('uml.ProfileApplication', x.id).map(e => `applied to ${nm(e.from)}`)];
12950
+ if (x.type === 'uml.Stereotype') return [...from('uml.Extension', x.id).map(e => `extends ${nm(e.to)}${PR_OBJ(e.props) && e.props.isRequired === true ? ' {required}' : ''}`),
12951
+ ...from('uml.Generalization', x.id).map(e => `a kind of «${nm(e.to)}»`), ...PR_ARR(x.members).filter(m => PR_OBJ(m) && m.type === 'uml.Property').map(printMember)];
12952
+ if (x.type === 'uml.MetaclassReference') return to('uml.Extension', x.id).map(e => `extended by «${nm(e.from)}»`);
12953
+ return [...appliedOf(model, x.id).map(prSay), ...PR_ARR(x.members).filter(m => PR_OBJ(m) && appliedOf(model, m.id).length).map(m => `${printMember(m)}: ${appliedOf(model, m.id).map(prSay).join('; ')}`)];
12954
+ }
12955
+ // "Stereotype owned, extends Class {required}, team: String"; "Class Order: «entity» table = orders; «owned» required"
12956
+ function prAria(model, x, drawn, I) {
12957
+ const why = [...(x.type === 'uml.Stereotype' && PR_OBJ(x.props) && x.props.isAbstract === true ? ['abstract'] : []), ...(OWN_BOXES.includes(x.type) ? ['closed'] : [])];
12958
+ const head = `${prWord(x.type)} ${drawn}${why.length ? ` (${why.join(', ')})` : ''}`, rows = prRows(model, x, I);
12959
+ return !rows.length ? head : PR_DEFS.slice(0, 2).includes(x.type) || x.type === 'uml.Profile' ? [head, ...rows].join(', ') : `${head}: ${rows.join('; ')}`;
12960
+ }
12961
+
12962
+ /* ---- the look: the class type's, the classifiers drawn by SHAPE.profiled, which is the classifier with the node's own
12963
+ accessible name (the classifier writes one from its compartments, which here are a stereotype's tags) ---- */
12964
+ SHAPE.profiled = { ...SHAPE.classifier, draw: (n, c, f) => SHAPE.classifier.draw(n, c, f).replace(/aria-label="[^"]*"/, () => `aria-label="${esc(n.aria)}"`) };
12965
+ const row = text => ({ type: 'uml.Property', label: text }); // a compartment row as the classifier prints it: its text alone
12966
+ function prNode(d, n, model, pic) {
12967
+ if (d.shape === 'note') return d;
12968
+ const I = prIndex(model), aria = prAria(model, n, pic.name(n.id), I);
12969
+ if (d.shape !== 'classifier') return { ...d, aria };
12970
+ if (n.type === 'uml.Stereotype') return { ...d, shape: 'profiled', aria, compartments: d.compartments.filter(c => c.name === 'attributes') };
12971
+ const tags = appliedOf(model, n.id).map(e => ({ name: `«${e.key}»`, members: [row(`«${e.key}»`), ...prTags(e).map(row)] })), has = m => PR_OBJ(m) && appliedOf(model, m.id).length > 0;
12972
+ const mrow = m => { const A = appliedOf(model, m.id), t = A.flatMap(prTags); return row(`${A.map(e => `«${e.key}»`).join(' ')} ${printMember(m)}${t.length ? ` {${t.join(', ')}}` : ''}`); };
12973
+ // a metaclass and an enumeration keep their own compartments (none; the literals), the tag compartments over them
12974
+ if (n.type === 'uml.MetaclassReference' || n.type === 'uml.Enumeration') return { ...d, shape: 'profiled', aria, compartments: [...tags, ...d.compartments.map(c => ({ ...c, members: c.members.map(m => (has(m) ? mrow(m) : m)) }))] };
12975
+ const ms = PR_ARR(n.members).filter(has).map(mrow);
12976
+ return { ...d, shape: 'profiled', aria, compartments: [...tags, ...(ms.length ? [{ name: 'members', members: ms }] : [])] };
12977
+ }
12978
+ // an open package's or profile's box: over its name the stereotypes it has, the required ones too; its tags in its accessible name
12979
+ function prBox(b, model, pic) {
12980
+ const x = pic.at.get(b.id), A = appliedOf(model, b.id); if (!A.length) return b;
12981
+ const st = PR_ARR(PR_OBJ(x.props) ? x.props.stereotypes : null).filter(t => t != null && t !== '').map(String), all = [...st, ...A.map(e => e.key).filter(k => !st.includes(k))];
12982
+ return { ...b, title: [...(x.type === 'uml.Profile' ? ['«profile»'] : []), ...all.map(t => `«${t}»`), pic.name(b.id)].join(' '), aria: `${b.aria}: ${A.map(prSay).join('; ')}` };
12983
+ }
12984
+ const PR_LOOK = { members: true, own: null, word: 'profile diagram', node: prNode, box: prBox, dg: { repair: false, reroute: false } };
12985
+ const prGeo0 = typeGeo(PR_LOOK);
12986
+ // down unless the view says right; slots at each node and box, so the palette's add hears what is selected (composite.js's)
12987
+ function prGeo(a) {
12988
+ const view = a.view.direction ? a.view : { ...a.view, direction: 'down' }, g = prGeo0({ ...a, view }), pic = ownersOn(a.model, a.view);
12989
+ const slots = [...g.nodes, ...PR_ARR(g.families)].map(n => ({ after: n.id, parent: null, x: n.x + n.w / 2, y: n.y + n.h / 2 }));
12990
+ return { ...g, slots, order: [...readingOrder(pic), ...g.edges.map(e => e.id)] }; // a new object: what typeGeo returned is not edited (§2.4)
12991
+ }
12992
+
12993
+ /* ---- validators (§4.2): definitions are errors, a metaclass name a warning. Each problem: { level, rule, path, message, ids } ---- */
12994
+ const PR_RULES = {
12995
+ 'profile-owns': 'a stereotype\'s and a metaclass reference\'s owner is a profile',
12996
+ 'stereotype-names': 'stereotypes in one profile have different names, the first letter in either case; profiles have different names',
12997
+ 'extension-ends': 'an extension runs from a stereotype to a metaclass reference',
12998
+ 'application-ends': 'a profile application runs from a package to a profile, not from a profile to itself',
12999
+ 'metaclass-known': 'a metaclass reference names a UML metaclass',
13000
+ };
13001
+ function prProblems(model) {
13002
+ const out = [], I = prIndex(model), q = id => JSON.stringify(I.at.has(id) ? prName(I.at.get(id)) : String(id)), say = (rule, path, ids, msg, level = 'error') => out.push({ level, rule, path, ids: ids.filter(x => x != null), message: `${rule}: ${msg}` });
13003
+ const seen = new Map(), names = (k, id, path, what) => { const was = seen.get(k); if (was) say('stereotype-names', path, [was, id], `${q(id)} and ${q(was)} are one name ${what} (${was}, ${id})`); else seen.set(k, id); };
13004
+ PR_ARR(model && model.nodes).forEach((n, i) => { if (!PR_OBJ(n) || I.at.get(n.id) !== n) return; const path = `nodes[${i}]`;
13005
+ if ((n.type === 'uml.Stereotype' || n.type === 'uml.MetaclassReference') && I.ty(n.parent) !== 'uml.Profile') say('profile-owns', path, [n.id, n.parent], `${prWord(n.type).toLowerCase()} ${q(n.id)} is owned by ${n.parent != null && I.at.has(n.parent) ? q(n.parent) + ', not a profile' : 'no profile'}`);
13006
+ if (n.type === 'uml.Stereotype' && I.ty(n.parent) === 'uml.Profile') names(`s\u0000${n.parent}\u0000${styKey(prName(n))}`, n.id, path, `in ${q(n.parent)}`);
13007
+ if (n.type === 'uml.Profile') names(`p\u0000${prName(n)}`, n.id, path, 'for two profiles');
13008
+ if (n.type === 'uml.MetaclassReference' && !metaKnown(prName(n))) say('metaclass-known', path, [n.id], `${q(n.id)} is no UML metaclass`, 'warning'); });
13009
+ PR_ARR(model && model.edges).forEach((e, i) => { if (!PR_OBJ(e)) return; const path = `edges[${i}]`, ends = [...new Set([e.id, e.from, e.to])], k = id => (I.at.has(id) ? prWord(I.ty(id)).toLowerCase() : 'nothing');
13010
+ if (e.type === 'uml.Extension' && (I.ty(e.from) !== 'uml.Stereotype' || I.ty(e.to) !== 'uml.MetaclassReference')) say('extension-ends', path, ends, `${e.id} runs from ${q(e.from)} (${k(e.from)}) to ${q(e.to)} (${k(e.to)}): from a stereotype to a metaclass`);
13011
+ if (e.type === 'uml.ProfileApplication' && (!OWN_BOXES.includes(I.ty(e.from)) || I.ty(e.to) !== 'uml.Profile' || e.from === e.to)) say('application-ends', path, ends, `${e.id} runs from ${q(e.from)} (${k(e.from)}) to ${q(e.to)} (${k(e.to)}): from a package to a profile${e.from === e.to ? ' other than itself' : ''}`); });
13012
+ return out;
13013
+ }
13014
+
13015
+ /* ---- describe(): the view in sentences, each element by its accessible name in reading order, then each line ---- */
13016
+ function prDescribe(model, viewId) {
13017
+ const v = PR_ARR(model && model.views).find(x => PR_OBJ(x) && x.id === viewId); if (!v) throw new Error(`umlimator: no view ${JSON.stringify(viewId)}`);
13018
+ const { at, pic, lines: all } = drawnLines(model, v, null), lines = all.filter(({ from, to }) => !holdsLine(pic, from, to)), I = prIndex(model), ids = readingOrder(pic);
13019
+ const pl = (k, w) => `${k} ${k === 1 ? w : w + 's'}`, box = new Map(pic.boxes.map(b => [b.id, prBox(b, model, pic).aria]));
13020
+ const said = id => { const x = at.get(id).x; return box.get(id) ?? (x.type === 'uml.Comment' ? 'Comment: ' + prName(x) : prAria(model, x, pic.name(id), I)); };
13021
+ return [`${v.title || v.id}: a profile diagram of ${pl(ids.length, 'element')} and ${pl(lines.length, 'line')}.`, ...ids.map(id => said(id) + '.'), ...lines.map(({ e }) => lineName(e, at, pic) + '.')].join('\n');
13022
+ }
13023
+
13024
+ /* ---- editing (§4.3) ---- */
13025
+ const prNewId = (model, base) => { const used = new Set(); JSON.stringify(model, (k, v) => { if (k === 'id' && typeof v === 'string') used.add(v); return v; });
13026
+ let k = 1; while (used.has(base + k)) k++; return base + k; };
13027
+ // edit distance, for the metaclass names nearest one typed. ponytail: quadratic in the names' lengths, over about 90 names
13028
+ function prDist(a, b) {
13029
+ let row = [...Array(b.length + 1).keys()];
13030
+ for (let i = 1; i <= a.length; i++) { const next = [i]; for (let j = 1; j <= b.length; j++) next[j] = Math.min(row[j] + 1, next[j - 1] + 1, row[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)); row = next; }
13031
+ return row[b.length];
13032
+ }
13033
+ // add: a stereotype, a metaclass or an enumeration goes into the profile selected (or the one holding what is selected), else
13034
+ // the view's first; a metaclass not in the table offers the nearest names; anything else is the default
13035
+ function prAdd(model, view, entry, label, at) {
13036
+ if (!PR_DEFS.includes(entry.type)) return null;
13037
+ const I = prIndex(model), prof = id => (I.ty(id) === 'uml.Profile' ? id : I.ty((I.at.get(id) || {}).parent) === 'uml.Profile' ? I.at.get(id).parent : null);
13038
+ const p = PR_ARR(at && at.sel).map(prof).find(x => x) ?? PR_ARR(view.nodeIds).map(id => (I.ty(id) === 'uml.Profile' ? id : null)).find(x => x);
13039
+ if (!p) return { error: 'Add a profile first' };
13040
+ const add = name => [{ op: 'add', element: { id: prNewId(model, { 'uml.Stereotype': 'stereotype', 'uml.MetaclassReference': 'metaclass', 'uml.Enumeration': 'enumeration' }[entry.type]), type: entry.type, ...(name ? { label: name } : {}), parent: p }, view: view.id }];
13041
+ const nm = String(label ?? ''), lo = nm.toLowerCase(); // a metaclass with no name is added unnamed (metaclass-known then warns)
13042
+ if (entry.type !== 'uml.MetaclassReference' || !nm || metaKnown(nm)) return add(nm);
13043
+ const near = Object.keys(UML_META).sort((a, b) => prDist(lo, a.toLowerCase()) - prDist(lo, b.toLowerCase()) || a.localeCompare(b)).slice(0, 6);
13044
+ return { pick: near, then: i => add(near[i]) };
13045
+ }
13046
+ // connect: an extension from a stereotype to a metaclass, an application from a package to a profile, a generalization
13047
+ // between stereotypes; else said, the rule's words
13048
+ function prConnect(model, view, from, to, at, line) {
13049
+ const I = prIndex(model), t = PR_OBJ(line) ? line.type : null;
13050
+ if (t === 'uml.Extension' && (I.ty(from) !== 'uml.Stereotype' || I.ty(to) !== 'uml.MetaclassReference')) return { error: 'An extension runs from a stereotype to a metaclass' };
13051
+ if (t === 'uml.ProfileApplication' && (!OWN_BOXES.includes(I.ty(from)) || I.ty(to) !== 'uml.Profile' || from === to)) return { error: `A profile application runs from a package to a profile${from === to ? ' other than itself' : ''}` };
13052
+ if (t === 'uml.Generalization' && (I.ty(from) !== 'uml.Stereotype' || I.ty(to) !== 'uml.Stereotype')) return { error: 'A generalization here joins two stereotypes' };
13053
+ return null;
13054
+ }
13055
+
13056
+ /* ---- the type ---- */
13057
+ const PR_STEREO = { key: 'stereotypes', label: 'Stereotypes', kind: 'list' };
13058
+ const PR_TYPE = registerDiagramType('uml.profile', {
13059
+ title: 'profile diagram',
13060
+ elements: [['Profile', 'Profile'], ['Stereotype', 'Stereotype'], ['MetaclassReference', 'Metaclass'], ['Enumeration', 'Enumeration'], ['Package', 'Package'], ['Comment', 'Comment']].map(([t, label]) => ({ type: 'uml.' + t, label })),
13061
+ lines: [['Extension', 'Extension'], ['Generalization', 'Generalization'], ['ProfileApplication', 'Profile application'], ['CommentLink', 'Comment link']].map(([t, label]) => ({ type: 'uml.' + t, label })),
13062
+ props: { ...CLASS_DEF.props, 'uml.Profile': [PR_STEREO], 'uml.Stereotype': [{ key: 'isAbstract', label: 'Abstract', kind: 'bool' }], 'uml.MetaclassReference': [],
13063
+ 'uml.Extension': [{ key: 'isRequired', label: 'Required', kind: 'bool' }], 'uml.ProfileApplication': [] },
13064
+ // the metaclass ranks first, above the stereotypes that extend it, as a general is above its specializations (§4.1)
13065
+ rankFlip: e => e.type === 'uml.Generalization' || e.type === 'uml.InterfaceRealization' || e.type === 'uml.Extension',
13066
+ layout: prGeo,
13067
+ boxes: (model, view) => { const pic = ownersOn(model, view); return pic.boxes.map(b => prBox(b, model, pic)); },
13068
+ owners: ownerChoices,
13069
+ // a stereotype's rows are its tag definitions, in the class type's member syntax; an enumeration's its literals
13070
+ member: { ...CLASS_TYPE.member, holds: n => n.type === 'uml.Stereotype' || n.type === 'uml.Enumeration' },
13071
+ validate: (model, view) => [...classProblems(model), ...prProblems(model), ...ownerProblems(model, view, drawnLines(model, view, null).lines)],
13072
+ rules: { ...RULES, ...OWNER_RULES, ...PR_RULES, ...STY_RULES },
13073
+ describe: prDescribe,
13074
+ // the PDF's tables (§9): a profile's, a stereotype's and a metaclass's kind; each element's stereotypes and tags
13075
+ word: (model, id) => { const x = prIndex(model).at.get(id);
13076
+ return !x ? null : x.type === 'uml.Stereotype' ? (PR_OBJ(x.props) && x.props.isAbstract === true ? 'Stereotype (abstract)' : 'Stereotype') : x.type === 'uml.MetaclassReference' ? 'Metaclass' : x.type === 'uml.Profile' ? 'Profile' : null; },
13077
+ rows: (model, view, id) => { const I = prIndex(model), x = I.at.get(id); if (!x) return null; const r = prRows(model, x, I); return r.length || PR_DEFS.slice(0, 2).includes(x.type) ? r : null; },
13078
+ edits: { add: prAdd, connect: prConnect },
13079
+ });
13080
+ /* ---------- the time ruler (work package 6.4, notes/stage-6.md §6) ----------
13081
+ layoutTimeRuler(input) -> out, the geometry of a timing view: no DOM, no model, in layoutTimeAxis's contract (sizes come
13082
+ in measured by the type, problems go out, never warn(); slots for the gestures). Time here is a quantity, not an order:
13083
+ x is linear in it, one base for every lane (§6.3). Only lib/umlimator-all.js holds this file.
13084
+
13085
+ input = {
13086
+ lanes: [{ id, name: { w, h }, form: 'states' | 'values', rows: [{ state, w }], lifelines, top to bottom; rows: the
13087
+ changes: [{ id, at, state, w }] }], states, top first (values: []); a
13088
+ change's w: its text's width
13089
+ messages: [{ id, from, to, sentAt, receivedAt, w, h }], from, to: lane ids; w, h: the label
13090
+ constraints: [{ id, kind: 'duration' | 'time', from, to, w, h }], from, to: change ids (time: from only)
13091
+ unit, text or null
13092
+ }
13093
+ out = { lanes: [{ id, x, y, w, h, name: { x, y }, rows: [{ state, y }], strip: { y, h } }],
13094
+ steps: [{ lane, pts }], bands: [{ lane, segs: [{ x0, x1, text, cut }] }], changes: [{ id, x, y, w, h }],
13095
+ lines: [{ id, pts, lbl }], marks: [{ id, kind, pts, lbl }],
13096
+ ruler: { y, x0, x1, ticks: [{ t, x, major, label }], unit }, slots: [{ index, x, y, parent, after, t }],
13097
+ scale: { t0, t1, k, step }, bounds, problems }
13098
+ A lane's rows: its states', then one per state its changes name that the lane does not list (drawn at the bottom; the
13099
+ type's change-state says it). Changes are drawn by `at`; two on one lane at one instant: the one whose id sorts last
13100
+ (the type's change-instant says it); the other gets no hit box, and a constraint on it is on the one drawn. A step
13101
+ line starts at a lane's first change; a band segment's text is null under 16 px, and `cut` when the text (cut at
13102
+ 160 px) and 12 px do not fit it: the type fits it to x1 - x0 - 12. A duration mark's pts run up the extension line at
13103
+ its earlier change, along the dimension line, up the other: [a, b, c, d], the arrows on b and c, its label on the
13104
+ line; a time mark's are its 6 px tick, its label to the right, bottoms level. A strip's level is 20 px, or its tallest
13105
+ label and 4 px. Ticks start at the first minor tick at or after t0. Slots: on each lane one at every minor tick (a
13106
+ fifth of the step); index: how many of the lane's drawn changes are at or before t, after: the last of them.
13107
+ Problems: { level, rule, message, ids }: timing-dense (the plot at its 4,800 px cap and changes still under 12 px
13108
+ apart, or values cut), timing-label (a message label that meets a step, a value or a label within its sender's lane's
13109
+ height), and an item not drawn (a time past ±1e300, an end that is not here, a constraint kind not known). */
13110
+
13111
+ const TR = {
13112
+ edge: 16, pad: 8, cut: 160, // the margin; a column's padding each side; the widest name and value text
13113
+ row: 24, above: 12, band: 24, // a state row's pitch; above the first row and below the last; a value band's height
13114
+ gap: 12, room: 12, bare: 16, // two changes' least distance; a value's room in its segment; a segment too short for text
13115
+ plot: 480, max: 4800, tick: 64, font: 10, // the plot's least and greatest width; majors' least distance; their labels' size
13116
+ lift: 4, level: 20, hit: 12, mark: 6, ruler: 28, // a label over its line, and each move; a strip's level; a change's hit width; a tick; the ruler's height
13117
+ };
13118
+
13119
+ const trArr = x => (Array.isArray(x) ? x : []);
13120
+ const trNum = x => (Number.isFinite(x) ? x : 0);
13121
+ const trTime = t => Number.isFinite(t) && Math.abs(t) <= 1e300; // past it the span, a step or t1 + a step is no number
13122
+ const trNice = x => Number(x.toPrecision(12));
13123
+ const trFmt = (t, s) => (Math.round(t / s) * s).toFixed(Math.max(0, -Math.floor(Math.log10(s) + 1e-9)));
13124
+ const trMeets = (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;
13125
+
13126
+ function layoutTimeRuler(input = {}) {
13127
+ const problems = [], say = (level, rule, message, ids) => problems.push({ level, rule, message, ids });
13128
+ const unit = input.unit == null || input.unit === '' ? null : String(input.unit);
13129
+
13130
+ // ---- each lane's changes in time order, one per instant (§7.2's change-instant): the id that sorts last
13131
+ const byId = new Map(), lanes = trArr(input.lanes).map((ln, j) => {
13132
+ const all = trArr(ln.changes).filter(c => trTime(c.at) || (say('error', 'change-time', `change ${c.id}: its time is not a number within ±1e300: not drawn`, [c.id]), false))
13133
+ .sort((a, b) => a.at - b.at || (String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0));
13134
+ const cs = all.filter((c, i) => !(all[i + 1] && all[i + 1].at === c.at)), L = { ln, j, form: ln.form === 'values' ? 'values' : 'states', cs };
13135
+ for (let i = all.length - 1, d; i >= 0; i--) { if (!all[i + 1] || all[i + 1].at !== all[i].at) d = all[i]; byId.set(all[i].id, { L, c: d }); } // a skipped id: the change drawn at its instant
13136
+ return L; });
13137
+ const laneOf = new Map(lanes.map(L => [L.ln.id, L]));
13138
+ const msgs = trArr(input.messages).filter(m => {
13139
+ if (!laneOf.has(m.from) || !laneOf.has(m.to)) say('error', 'message-ends', `message ${m.id}: ${laneOf.has(m.from) ? m.to : m.from} is not a lifeline here: not drawn`, [m.id]);
13140
+ else if (!trTime(m.sentAt) || !trTime(m.receivedAt)) say('error', 'change-time', `message ${m.id}: its times are not numbers within ±1e300: not drawn`, [m.id]);
13141
+ else return true;
13142
+ return false; });
13143
+
13144
+ // ---- the scale (§6.3): k the least px per unit that keeps changes 12 px apart, values in their segments and the plot
13145
+ // 480 px wide, capped at 4,800; t1 the latest time and half a step; the step the least 1, 2 or 5 × 10ⁿ 64 px apart
13146
+ let t0 = 0, tMax = 0;
13147
+ const time = t => { if (t < t0) t0 = t; if (t > tMax) tMax = t; };
13148
+ lanes.forEach(L => L.cs.forEach(c => time(c.at))); msgs.forEach(m => { time(m.sentAt); time(m.receivedAt); });
13149
+ const vw = c => Math.min(TR.cut, trNum(c.w)) + TR.room, top = tMax - t0 >= 1e-90 ? tMax : t0 + 1; // all at one instant (or within 1e-90, past toFixed's digits): one unit
13150
+ let need = 0;
13151
+ lanes.forEach(L => L.cs.forEach((c, i) => { const n = L.cs[i + 1]; if (n) need = Math.max(need, TR.gap / (n.at - c.at), L.form === 'values' ? vw(c) / (n.at - c.at) : 0); }));
13152
+ const stepOf = k => { const wide = s => Math.max(textW(trFmt(t0, s), TR.font), textW(trFmt(tMax + s, s), TR.font)) + TR.gap;
13153
+ for (let n = Math.floor(Math.log10(TR.tick / k)) - 1; ; n++) for (const m of [1, 2, 5]) { const s = trNice(m * 10 ** n); if (s * k >= TR.tick && s * k >= wide(s)) return s; } };
13154
+ const endOf = (k, s) => Math.max(top, tMax + s / 2, ...lanes.filter(L => L.form === 'values' && L.cs.length).map(L => { const c = L.cs[L.cs.length - 1]; return c.at + vw(c) / k; }));
13155
+ const kOf = t1 => Math.min(Math.max(need, TR.plot / (t1 - t0)), TR.max / (t1 - t0));
13156
+ let k = kOf(top), step, t1;
13157
+ for (let n = 0; n < 40; n++) { step = stepOf(k); t1 = endOf(k, step); const kk = kOf(t1); if (kk === k) break; k = kk; }
13158
+ step = stepOf(k); t1 = endOf(k, step);
13159
+ if ((t1 - t0) * k > TR.max) { k = TR.max / (t1 - t0); step = stepOf(k); } // not settled in 40 rounds: the cap holds
13160
+ const name = Math.min(TR.cut, Math.max(0, ...lanes.map(L => trNum((L.ln.name || {}).w)))) + 2 * TR.pad;
13161
+
13162
+ // ---- each lane's rows, height, and the strip its constraints need
13163
+ lanes.forEach(L => {
13164
+ L.rows = L.form === 'values' ? [] : trArr(L.ln.rows).map(r => ({ state: r.state, w: trNum(r.w) }));
13165
+ const listed = new Set(L.rows.map(r => r.state));
13166
+ L.cs.forEach(c => { if (L.form === 'states' && !listed.has(c.state)) { listed.add(c.state); L.rows.push({ state: c.state, w: trNum(c.w) }); } });
13167
+ const nh = trNum((L.ln.name || {}).h) + 2 * TR.pad;
13168
+ L.h = Math.max(nh, L.form === 'values' ? TR.band + 2 * TR.above : Math.max(1, L.rows.length) * TR.row);
13169
+ L.levels = []; L.lh = []; L.marks = []; });
13170
+ const states = Math.max(0, ...lanes.flatMap(L => L.rows.map(r => r.w)));
13171
+ const X0 = TR.edge + name + (states ? states + 2 * TR.pad : 0), xOf = t => X0 + (t - t0) * k, X1 = xOf(t1);
13172
+
13173
+ const marks = trArr(input.constraints).filter(c => {
13174
+ if (c.kind !== 'time' && c.kind !== 'duration') { say('error', 'constraint-kind', `constraint ${c.id}: its kind ${c.kind} is not duration or time: not drawn`, [c.id]); return false; }
13175
+ const a = byId.get(c.from), b = c.kind === 'time' ? a : byId.get(c.to);
13176
+ if (!a || !b) { say('error', 'constraint-ends', `constraint ${c.id}: ${a ? c.to : c.from} is not a change here: not drawn`, [c.id]); return false; }
13177
+ const lo = a.c.at <= b.c.at ? a : b, hi = lo === a ? b : a, L = a.L.j >= b.L.j ? a.L : b.L, w = trNum(c.w), h = trNum(c.h);
13178
+ const xa = xOf(lo.c.at), xb = xOf(hi.c.at), cx = (xa + xb) / 2;
13179
+ const it = c.kind === 'time' ? { c, a, x0: xa - TR.mark / 2, x1: xa + TR.mark / 2 + TR.lift + w, w, h } : { c, a: lo, b: hi, x0: Math.min(xa, cx - w / 2), x1: Math.max(xb, cx + w / 2), w, h };
13180
+ L.marks.push(it); return true; });
13181
+ lanes.forEach(L => L.marks.slice().sort((p, q) => p.x0 - q.x0 || (String(p.c.id) < String(q.c.id) ? -1 : 1)).forEach(it => { // greedy, by start
13182
+ let v = L.levels.findIndex(end => end + TR.mark <= it.x0); if (v < 0) { v = L.levels.length; L.levels.push(0); }
13183
+ L.levels[v] = it.x1; L.lh[v] = Math.max(L.lh[v] || TR.level, trNum(it.h) + TR.lift); it.v = v; })); // a level as tall as its labels, 20 px at least
13184
+
13185
+ // ---- down the page: lanes, their strips, the ruler
13186
+ let y = TR.edge;
13187
+ lanes.forEach(L => { L.y = y; L.mid = y + L.h / 2; y += L.h; L.sy = y;
13188
+ L.top = L.lh.map((h, v) => L.lh.slice(0, v).reduce((s, x) => s + x, 0)); L.sh = L.lh.length ? L.lh.reduce((s, x) => s + x, 0) + TR.lift : 0; y += L.sh; });
13189
+ const rulerY = y + TR.pad;
13190
+ const rowY = (L, s) => L.y + TR.above + L.rows.findIndex(r => r.state === s) * TR.row;
13191
+ const lvl = (L, c) => (L.form === 'values' ? L.mid : rowY(L, c.state));
13192
+ const levelAt = (L, t) => { let c = null; for (const d of L.cs) { if (d.at > t) break; c = d; } return c ? lvl(L, c) : L.mid; };
13193
+
13194
+ // ---- steps, bands, the changes' hit boxes
13195
+ const steps = [], bands = [], changes = [], obst = [], dense = new Set();
13196
+ lanes.forEach(L => {
13197
+ const pts = [], segs = [], cs = L.cs;
13198
+ cs.forEach((c, i) => { const x = xOf(c.at), yc = lvl(L, c), yp = i ? lvl(L, cs[i - 1]) : yc, n = cs[i + 1];
13199
+ if (n && (n.at - c.at) * k < TR.gap - 1e-6) dense.add(n.id);
13200
+ if (L.form === 'values') { const x1 = n ? xOf(n.at) : X1, cut = x1 - x < vw(c) - 1e-6;
13201
+ if (cut) dense.add(c.id);
13202
+ segs.push({ x0: x, x1, text: x1 - x < TR.bare ? null : c.state ?? '', cut: cut || trNum(c.w) > TR.cut });
13203
+ if (x1 - x >= TR.bare) { const tw = Math.min(TR.cut, trNum(c.w), x1 - x - TR.room); obst.push({ x: (x + x1 - tw) / 2, y: L.mid - 7, w: tw, h: 14 }); }
13204
+ changes.push({ id: c.id, x: x - TR.hit / 2, y: L.mid - TR.band / 2, w: TR.hit, h: TR.band }); return; }
13205
+ if (i) pts.push([x, yp]); pts.push([x, yc]);
13206
+ const h = Math.max(TR.hit, Math.abs(yc - yp));
13207
+ changes.push({ id: c.id, x: x - TR.hit / 2, y: (yc + yp) / 2 - h / 2, w: TR.hit, h }); });
13208
+ if (L.form === 'values') { if (segs.length) bands.push({ lane: L.ln.id, segs }); return; }
13209
+ if (!cs.length) return;
13210
+ pts.push([X1, lvl(L, cs[cs.length - 1])]);
13211
+ steps.push({ lane: L.ln.id, pts });
13212
+ pts.slice(1).forEach((q, i) => { const p = pts[i]; obst.push({ x: Math.min(p[0], q[0]), y: Math.min(p[1], q[1]), w: Math.abs(q[0] - p[0]), h: Math.abs(q[1] - p[1]) }); }); });
13213
+ // a duplicate at one instant is not dense: it is skipped, and the type's change-instant says so
13214
+ if (dense.size) say('warning', 'timing-dense', `the ruler is at its ${TR.max} px cap: ${dense.size} change${dense.size > 1 ? 's are' : ' is'} closer than ${TR.gap} px to the one before, or ${dense.size > 1 ? 'have their values' : 'has its value'} cut`, [...dense].sort());
13215
+
13216
+ // ---- constraints in their strips (§6.6), placed before the messages, whose labels then keep off theirs
13217
+ const outMarks = lanes.flatMap(L => L.marks.map(it => {
13218
+ const yl = L.sy + L.top[it.v] + L.lh[it.v] - TR.lift; // the dimension line, or the tick's middle; its label on it
13219
+ if (it.c.kind === 'time') { const x = xOf(it.a.c.at);
13220
+ return { id: it.c.id, kind: 'time', pts: [[x, yl - TR.mark / 2], [x, yl + TR.mark / 2]], lbl: { x: x + TR.mark / 2 + TR.lift, y: yl + TR.mark / 2 - it.h, w: it.w, h: it.h } }; }
13221
+ const xa = xOf(it.a.c.at), xb = xOf(it.b.c.at);
13222
+ return { id: it.c.id, kind: 'duration', pts: [[xa, lvl(it.a.L, it.a.c)], [xa, yl], [xb, yl], [xb, lvl(it.b.L, it.b.c)]], lbl: { x: (xa + xb) / 2 - it.w / 2, y: yl - it.h, w: it.w, h: it.h } }; }))
13223
+ .map(o => ({ ...o, pts: o.pts.map(p => p.map(r2)), lbl: { x: r2(o.lbl.x), y: r2(o.lbl.y), w: r2(o.lbl.w), h: r2(o.lbl.h) } }));
13224
+ outMarks.forEach(o => obst.push(o.lbl));
13225
+
13226
+ // ---- messages: straight from the sender's level at sentAt to the receiver's at receivedAt; each label 4 px over its
13227
+ // middle, moved up 4 px at a time, at most its lane's height, until it meets nothing placed before it (§6.5).
13228
+ // ponytail: quadratic in labels, with the obstacles filtered by x once per message; a grid of buckets if views grow
13229
+ const lines = msgs.map(m => {
13230
+ const A = laneOf.get(m.from), B = laneOf.get(m.to), p = [xOf(m.sentAt), levelAt(A, m.sentAt)], q = [xOf(m.receivedAt), levelAt(B, m.receivedAt)];
13231
+ const w = trNum(m.w), h = trNum(m.h), lbl = { x: (p[0] + q[0]) / 2 - w / 2, y: (p[1] + q[1]) / 2 - TR.lift - h, w, h }, y0 = lbl.y;
13232
+ const near = obst.filter(o => o.x < lbl.x + w && lbl.x < o.x + o.w), hits = () => near.some(o => trMeets(lbl, o));
13233
+ while (hits() && y0 - lbl.y + TR.lift <= A.h) lbl.y -= TR.lift;
13234
+ if (hits()) { lbl.y = y0; say('warning', 'timing-label', `message ${m.id}: its label meets a step, a value or a label within ${A.h} px above it`, [m.id]); }
13235
+ obst.push(lbl);
13236
+ return { id: m.id, pts: [p, q].map(pt => pt.map(r2)), lbl: { x: r2(lbl.x), y: r2(lbl.y), w: r2(w), h: r2(h) } }; });
13237
+
13238
+ // ---- the ruler and the slots: a tick at every fifth of the step from the first at or after t0
13239
+ const minor = step / 5, ticks = [], slots = [];
13240
+ for (let i = Math.ceil(t0 / minor - 1e-9), e = Math.floor(t1 / minor + 1e-9); i <= e; i++) { const t = trNice(i * minor), major = i % 5 === 0;
13241
+ ticks.push({ t, x: r2(xOf(t)), major, label: major ? trFmt(t, step) : null }); }
13242
+ lanes.forEach(L => { let n = 0;
13243
+ ticks.forEach(tk => { while (n < L.cs.length && L.cs[n].at <= tk.t) n++;
13244
+ slots.push({ index: n, x: tk.x, y: r2(L.mid), parent: L.ln.id, after: n ? L.cs[n - 1].id : null, t: tk.t }); }); });
13245
+
13246
+ // ---- out
13247
+ const outLanes = lanes.map(L => ({ id: L.ln.id, x: TR.edge, y: r2(L.y), w: r2(X1 - TR.edge), h: r2(L.h),
13248
+ name: { x: TR.edge + TR.pad, y: r2(L.y + (L.h - trNum((L.ln.name || {}).h)) / 2) }, rows: L.rows.map(r => ({ state: r.state, y: r2(rowY(L, r.state)) })),
13249
+ strip: { y: r2(L.sy), h: r2(L.sh) } }));
13250
+ const last = ticks.filter(t => t.major).pop(), right = X1 + (last ? textW(last.label, TR.font) / 2 : 0) + (unit ? TR.lift + textW(unit, TR.font) : 0);
13251
+ const boxes = [...lines.map(l => l.lbl), ...outMarks.map(o => o.lbl)];
13252
+ const xs = [right, X1, ...boxes.flatMap(b => [b.x, b.x + b.w])], ys = [rulerY + TR.ruler, ...boxes.flatMap(b => [b.y, b.y + b.h])];
13253
+ const bx0 = Math.min(0, ...xs.map(v => v - TR.edge)), by0 = Math.min(0, ...ys.map(v => v - TR.edge));
13254
+ return {
13255
+ lanes: outLanes, steps: steps.map(s => ({ lane: s.lane, pts: s.pts.map(p => p.map(r2)) })),
13256
+ bands: bands.map(b => ({ lane: b.lane, segs: b.segs.map(s => ({ ...s, x0: r2(s.x0), x1: r2(s.x1) })) })),
13257
+ changes: changes.map(c => ({ id: c.id, x: r2(c.x), y: r2(c.y), w: c.w, h: r2(c.h) })), lines, marks: outMarks,
13258
+ ruler: { y: r2(rulerY), x0: r2(X0), x1: r2(X1), ticks, unit }, slots, scale: { t0, t1: trNice(t1), k, step },
13259
+ bounds: { x: r2(bx0), y: r2(by0), w: r2(Math.max(...xs) + TR.edge - bx0), h: r2(Math.max(...ys) + TR.edge - by0) }, problems,
13260
+ };
13261
+ }
13262
+ /* ---------- the timing diagram, uml.timing (work package 6.5, notes/stage-6.md §7 and §8) ----------
13263
+ Lifelines (nodeIds) are lanes stacked down the page, each drawn in the states form (a step line over its `states`, top to
13264
+ bottom) or, with present.<id>.as "values", the values form (a band crossed at each change, its value inside). The view's
13265
+ `timeline` holds its changes ({ id, lifeline, at, state }, drawn by `at`) and its time and duration constraints; its
13266
+ inline uml.Message lines are drawn from the sender's level at sentAt to the receiver's at receivedAt. The geometry is
13267
+ src/layout/timeruler.js's; this file measures the view into its input and turns its output into svgOf's geometry: lanes,
13268
+ state rows, changes, constraints and the ruler as nodes (a change, a constraint: their own ids; a state row
13269
+ `<lifeline>-><state>` and the ruler `->ruler`: geometry ids no element has, notes/stage-5.md §6), messages as lines, steps
13270
+ and bands in the type's `under` layer. Then the validators, describe(), the legend, the PDF's rows and the gesture hooks
13271
+ of §8, which write a time item by `set` of the view's timeline path. Ships in lib/umlimator-all.js only. */
13272
+
13273
+ const TM_ARR = x => (Array.isArray(x) ? x : []), TM_OBJ = x => x !== null && typeof x === 'object' && !Array.isArray(x);
13274
+ const TM_HAS = x => x != null && x !== '';
13275
+ const TM = { name: 12, text: 10.5, lbl: 16, mark: 14 }; // a lane's name, a state's or a value's text; a message's label's and a constraint's height
13276
+ const TM_SORTS = ['call', 'async', 'reply'];
13277
+ // the sequence type's ends for these sorts (sequence.js's SQ_END): a call filled, async open, a reply dashed and open
13278
+ const TM_END = { call: { from: 'none', to: 'arrow' }, async: { from: 'none', to: 'open' }, reply: { from: 'none', to: 'open', dashed: true } };
13279
+ const TM_KIND = { 'uml.DurationConstraint': 'duration', 'uml.TimeConstraint': 'time' };
13280
+ const TM_RULER = '->ruler', tmRowId = (lane, state) => `${lane}->${state}`; // geometry ids: no element id holds '->' (model.md rule 5)
13281
+
13282
+ const tmView = (model, view) => (TM_OBJ(view) ? view : TM_ARR(model && model.views).find(v => TM_OBJ(v) && v.id === view)) || null;
13283
+ const tmProps = x => (TM_OBJ(x && x.props) ? x.props : {});
13284
+ const tmName = n => String(n.label ?? n.id);
13285
+ const tmHead = n => tmName(n) + (TM_HAS(tmProps(n).represents) ? ' : ' + tmProps(n).represents : ''); // "worker : OrderWorker"
13286
+ const tmStates = n => [...new Set(TM_ARR(tmProps(n).states).filter(TM_HAS).map(String))]; // a state listed twice is one row
13287
+ const tmNameType = s => { const [name, ...rest] = s.split(/\s*:\s*/); return [name, rest.join(':').trim()]; }; // `a : B : C` is a, represents B:C
13288
+ const tmLabel = x => (TM_HAS(x.label) ? String(x.label) : '');
13289
+ const tmPlural = (k, w) => `${k} ${k === 1 ? w : w + 's'}`;
13290
+
13291
+ /* The view read once: its lanes (nodeIds that are nodes), every node by id, the timeline's changes and constraints, its
13292
+ messages, the unit, and each lane's form; shown: the constraints drawn (one on a hidden lifeline's change is not, §7.2) */
13293
+ function tmIndex(model, V) {
13294
+ const N = new Map(); [...TM_ARR(V.nodes), ...TM_ARR(model && model.nodes)].forEach(n => { if (TM_OBJ(n) && typeof n.id === 'string' && !N.has(n.id)) N.set(n.id, n); });
13295
+ const TL = TM_OBJ(V.timeline) ? V.timeline : {}, item = x => TM_OBJ(x) && typeof x.id === 'string';
13296
+ const changes = TM_ARR(TL.changes).filter(item), cons = TM_ARR(TL.constraints).filter(item), msgs = TM_ARR(V.edges).filter(e => item(e) && e.type === 'uml.Message');
13297
+ const heads = TM_ARR(V.nodeIds).filter(id => N.has(id) && N.get(id).type === 'uml.Lifeline').map(id => N.get(id)), P = TM_OBJ(V.present) ? V.present : {};
13298
+ const unit = TM_HAS(TL.unit) ? String(TL.unit) : null, t = x => `${x}${unit ? ' ' + unit : ''}`;
13299
+ const name = id => (N.has(id) ? tmName(N.get(id)) : String(id)), C = new Map(changes.map(c => [c.id, c]));
13300
+ const H = new Map(heads.map(n => [n.id, n])), hid = id => C.has(id) && !H.has(C.get(id).lifeline); // a change of a hidden lifeline
13301
+ return { N, H, heads, changes, C, cons, shown: cons.filter(c => !hid(c.from) && !hid(c.to)), msgs, unit, t, name, values: id => TM_OBJ(P[id]) && P[id].as === 'values',
13302
+ mine: id => changes.filter(c => c.lifeline === id).sort((a, b) => a.at - b.at || (a.id < b.id ? -1 : 1)) };
13303
+ }
13304
+
13305
+ /* ---- names (§9): a change's, a message's, a constraint's, said the same in the picture, describe() and the PDF ---- */
13306
+ const tmSort = m => (TM_SORTS.includes(tmProps(m).sort) ? tmProps(m).sort : 'call');
13307
+ const tmChangeSaid = (X, c) => `${X.name(c.lifeline)}: ${c.state ?? ''} from ${X.t(c.at)}`;
13308
+ const tmMsgSaid = (X, m) => { const p = tmProps(m), t = tmLabel(m), s = tmSort(m);
13309
+ return `${s[0].toUpperCase() + s.slice(1)}${t ? ' ' + t : ''} from ${X.name(m.from)} at ${X.t(p.sentAt)} to ${X.name(m.to)} at ${X.t(p.receivedAt)}`; };
13310
+ const tmConSaid = (X, c) => { const a = X.C.get(c.from), b = X.C.get(c.to), t = tmLabel(c);
13311
+ if (TM_KIND[c.type] === 'time') return `Time constraint${t ? ' ' + t : ''}${a ? ` on ${X.name(a.lifeline)} at ${X.t(a.at)}` : ''}`;
13312
+ const [p, q] = a && b && b.at < a.at ? [b, a] : [a, b];
13313
+ return `Duration constraint${t ? ' ' + t : ''}${p && q ? ` on ${X.name(p.lifeline)}${q.lifeline !== p.lifeline ? ` and ${X.name(q.lifeline)}` : ''} from ${X.t(p.at)} to ${X.t(q.at)}` : ''}`; };
13314
+
13315
+ /* ---- F2's micro-syntax (§8.1): `text`, `text @ t`, `text @ t1 → t2` (or ->); times are numbers in the view's unit. A
13316
+ text whose part after its last @ is not one or two numbers is all text (an address with @ is a name) ---- */
13317
+ function tmParse(text) {
13318
+ const s = String(text ?? '').trim(), m = /^(.*?)\s*@\s*([^@]*)$/.exec(s);
13319
+ const ts = m ? m[2].split(/\s*(?:→|->)\s*/).map(x => x.trim()) : [];
13320
+ if (!m || ts.length > 2 || ts.some(x => !/^[-+]?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?$/i.test(x) || !Number.isFinite(Number(x)))) return { text: s, times: [] };
13321
+ return { text: m[1].trim(), times: ts.map(Number) };
13322
+ }
13323
+ const tmPrint = (text, ...times) => `${text ?? ''}${times.length ? ' @ ' + times.join(' → ') : ''}`;
13324
+
13325
+ /* ---- layout: the view as layoutTimeRuler's input (§6.2), measured with textW in the host's measure ---- */
13326
+ function tmInput(model, V, X = tmIndex(model, V)) {
13327
+ const w = (t, px = TM.text) => Math.ceil(textW(String(t ?? ''), px));
13328
+ return {
13329
+ lanes: X.heads.map(n => { const vals = X.values(n.id);
13330
+ return { id: n.id, name: { w: w(tmHead(n), TM.name), h: TM.lbl }, form: vals ? 'values' : 'states', rows: vals ? [] : tmStates(n).map(s => ({ state: s, w: w(s) })),
13331
+ changes: X.changes.filter(c => c.lifeline === n.id).map(c => ({ id: c.id, at: c.at, state: String(c.state ?? ''), w: w(c.state) })) }; }),
13332
+ messages: X.msgs.map(m => { const t = tmLabel(m), p = tmProps(m);
13333
+ return { id: m.id, from: m.from, to: m.to, sentAt: p.sentAt, receivedAt: p.receivedAt, ...(t ? { w: w(t) + 12, h: TM.lbl } : { w: 0, h: 0 }) }; }),
13334
+ constraints: X.shown.map(c => { const t = tmLabel(c); return { id: c.id, kind: TM_KIND[c.type] ?? c.type, from: c.from, to: c.to, ...(t ? { w: w(t) + 6, h: TM.mark } : { w: 0, h: 0 }) }; }),
13335
+ unit: X.unit,
13336
+ };
13337
+ }
13338
+
13339
+ /* run(model, view): svgOf's geometry. Nodes: each lane (the lifeline, its name in the left column), its state rows, its
13340
+ changes' hit boxes, the constraints (their marks and labels) and the ruler; edges: the messages; tr: the ruler's output,
13341
+ for draw(); slots and problems from it; order (§9): lanes top to bottom, each with its state rows and its changes in time
13342
+ order, then messages by sentAt, then constraints, then the ruler */
13343
+ function tmRun(model, view) {
13344
+ const V = tmView(model, view), X = tmIndex(model, V), o = layoutTimeRuler(tmInput(model, V, X)), x0 = o.ruler.x0;
13345
+ const nodes = [], order = [];
13346
+ o.lanes.forEach(l => { const n = X.N.get(l.id), vals = X.values(l.id), cs = X.mine(l.id);
13347
+ nodes.push({ id: l.id, shape: 'tm-lane', type: 'uml.Lifeline', label: tmHead(n), k: 'file', x: l.x, y: l.y, w: l.w, h: l.h, nx: l.name.x, ny: l.name.y,
13348
+ aria: `${vals ? 'Value lifeline' : 'Lifeline'} ${tmHead(n)}${vals ? '' : `, ${tmPlural(l.rows.length, 'state')}`}, ${tmPlural(cs.length, 'change')}` });
13349
+ order.push(l.id);
13350
+ l.rows.forEach(r => { const w = Math.ceil(textW(r.state, TM.text)), id = tmRowId(l.id, r.state);
13351
+ nodes.push({ id, shape: 'tm-row', label: r.state, k: 'file', x: r2(x0 - TR.pad - w), y: r2(r.y - 8), w, h: 16, aria: `State ${r.state} of ${tmName(n)}` }); order.push(id); }); });
13352
+ o.changes.forEach(h => { const c = X.C.get(h.id); nodes.push({ ...h, shape: 'tm-change', label: tmPrint(c.state, c.at), k: 'file', aria: tmChangeSaid(X, c) }); });
13353
+ o.lanes.forEach(l => { const at = o.changes.filter(h => X.C.get(h.id).lifeline === l.id).sort((a, b) => a.x - b.x).map(h => h.id); order.splice(order.indexOf(l.id) + 1 + l.rows.length, 0, ...at); });
13354
+ const byCon = new Map(X.cons.map(c => [c.id, c]));
13355
+ o.marks.forEach(mk => { const c = byCon.get(mk.id), b = mk.lbl.w ? mk.lbl : { x: mk.pts[1][0] - 6, y: mk.pts[1][1] - 6, w: 12, h: 12 }; // a label-less one: round its line's middle
13356
+ nodes.push({ id: c.id, shape: 'tm-mark', kind: mk.kind, pts: mk.pts, lbl: mk.lbl, label: tmLabel(c), k: 'file', x: b.x, y: b.y, w: b.w, h: b.h, aria: tmConSaid(X, c) }); });
13357
+ const R = o.ruler, rw = Math.max(R.x1 - x0, 1), s = o.scale, M = new Map(X.msgs.map(m => [m.id, m]));
13358
+ const last = [...o.changes.map(h => X.C.get(h.id).at), ...o.lines.flatMap(l => [tmProps(M.get(l.id)).sentAt, tmProps(M.get(l.id)).receivedAt])].reduce((a, t) => (tmNum(t) && t > a ? t : a), s.t0); // the span of the times, not the room after
13359
+ nodes.push({ id: TM_RULER, shape: 'tm-ruler', label: X.unit ?? '', k: 'file', x: x0, y: R.y, w: rw, h: TR.ruler, ticks: R.ticks, unit: R.unit,
13360
+ aria: `Time ruler, ${s.t0} to ${X.t(last)}, a tick every ${X.t(s.step)}` });
13361
+ const byId = Object.fromEntries(nodes.map(n => [n.id, n]));
13362
+ const edges = o.lines.map(l => { const m = M.get(l.id), p = tmProps(m), t = tmLabel(m), sort = tmSort(m);
13363
+ return { id: m.id, _id: m.id, type: 'uml.Message', from: m.from, to: m.to, s: byId[m.from], t: byId[m.to], pts: l.pts, end: TM_END[sort], sort,
13364
+ label: tmPrint(t, p.sentAt, p.receivedAt), ...(t ? { lbl: { ...l.lbl, text: t } } : {}), aria: tmMsgSaid(X, m) }; });
13365
+ order.push(...[...edges].sort((a, b) => tmProps(M.get(a.id)).sentAt - tmProps(M.get(b.id)).sentAt).map(e => e.id), ...o.marks.map(mk => mk.id), TM_RULER);
13366
+ return { nodes, edges, groups: [], byId, bounds: o.bounds, map: false, order, slots: o.slots, problems: o.problems, tr: o, legend: tmLegend(X, o) };
13367
+ }
13368
+
13369
+ /* draw, under the lines (§7.1): the rules between lanes, then each step line and value band, --um-text at 1.5 px */
13370
+ function tmDraw(g) {
13371
+ const o = g.tr; if (!o) return { under: '', over: '' };
13372
+ const L = new Map(o.lanes.map(l => [l.id, l])), path = pts => 'M' + pts.map(p => `${p[0]} ${p[1]}`).join('L');
13373
+ const rules = o.lanes.slice(1).map(l => `<path d="M${l.x} ${l.y}H${r2(l.x + l.w)}" stroke="var(--um-muted)" stroke-width="1"/>`).join('');
13374
+ const steps = o.steps.map(s => `<path class="tm-step" d="${path(s.pts)}" fill="none" stroke="var(--um-text)" stroke-width="1.5" stroke-linejoin="round"/>`).join('');
13375
+ const bands = o.bands.map(b => { const l = L.get(b.lane), m = r2(l.y + l.h / 2), up = r2(m - TR.band / 2), dn = r2(m + TR.band / 2);
13376
+ return b.segs.map(s => { const c = Math.min(4, (s.x1 - s.x0) / 2), a = r2(s.x0 + c), z = r2(s.x1 - c); // the edges cross over 8 px at each change
13377
+ return `<path class="tm-band" d="M${s.x0} ${m}L${a} ${up}H${z}L${s.x1} ${m}L${z} ${dn}H${a}Z" fill="none" stroke="var(--um-text)" stroke-width="1.5" stroke-linejoin="round"/>`
13378
+ + (s.text != null ? `<text x="${r2((s.x0 + s.x1) / 2)}" y="${r2(m + 4)}" text-anchor="middle" class="tm-value" fill="var(--um-text)" style="font:500 ${TM.text}px var(--um-sans)">${esc(fit(s.text, TM.text, Math.min(TR.cut, s.x1 - s.x0 - TR.room)))}</text>` : ''); }).join(''); }).join('');
13379
+ return { under: `<g class="tm-under">${rules}${steps}${bands}</g>`, over: '' };
13380
+ }
13381
+
13382
+ /* the shapes, each a node the canvas selects, names and focuses (§6.7) */
13383
+ const tmNode = (n, shape, inner) => `<g class="node ${shape}" data-id="${esc(n.id)}" data-shape="${shape}" tabindex="-1" role="img" aria-label="${esc(n.aria || n.label || n.id)}">${inner}</g>`;
13384
+ const tmText = (x, y, t, px, w = 500, a = 'start', cls = '') => `<text x="${r2(x)}" y="${r2(y)}" text-anchor="${a}"${cls ? ` class="${cls}"` : ''} fill="var(--um-text)" style="font:${w} ${px}px var(--um-sans)">${esc(t)}</text>`;
13385
+ SHAPE['tm-lane'] = { draw: n => tmNode(n, 'tm-lane', `<rect x="${n.x}" y="${n.y}" width="${n.w}" height="${n.h}" class="n-body" fill="transparent"/>${tmText(n.nx, n.ny + 12, fit(n.label, TM.name, TR.cut), TM.name, 600, 'start', 'c-name')}`) };
13386
+ SHAPE['tm-row'] = { draw: n => tmNode(n, 'tm-row', `<rect x="${n.x}" y="${n.y}" width="${n.w}" height="${n.h}" fill="transparent"/>${tmText(n.x + n.w, n.y + 12, n.label, TM.text, 500, 'end')}`) };
13387
+ SHAPE['tm-change'] = { draw: n => tmNode(n, 'tm-change', `<rect x="${n.x}" y="${n.y}" width="${n.w}" height="${n.h}" class="n-body" fill="transparent"/>`) };
13388
+ SHAPE['tm-mark'] = {
13389
+ draw(n) { const P = n.pts, d = n.kind === 'time' ? `M${P[0][0]} ${P[0][1]}V${P[1][1]}` : `M${P[0][0]} ${P[0][1]}V${P[1][1]}M${P[2][0]} ${P[2][1]}V${P[3][1]}`;
13390
+ const arrows = n.kind === 'time' ? '' : (() => { const [b, c] = [P[1], P[2]], k = Math.sign(c[0] - b[0]) || 1, y = b[1], tip = (x, s) => `M${r2(x + 7 * s)} ${r2(y - 3.5)}L${x} ${y}L${r2(x + 7 * s)} ${r2(y + 3.5)}`;
13391
+ return `<path d="M${b[0]} ${y}H${c[0]}${tip(b[0], k)}${tip(c[0], -k)}" fill="none" stroke="var(--um-muted)" stroke-width="1.2"/>`; })();
13392
+ return tmNode(n, 'tm-mark', `<rect x="${n.x}" y="${n.y}" width="${n.w}" height="${n.h}" fill="transparent"/><path d="${d}" stroke="var(--um-muted)" stroke-width="${n.kind === 'time' ? 1.5 : 0.8}"/>${arrows}`
13393
+ + (n.label ? tmText(n.lbl.x + (n.kind === 'time' ? 0 : n.lbl.w / 2), n.lbl.y + 11, n.label, TM.text, 500, n.kind === 'time' ? 'start' : 'middle') : '')); },
13394
+ };
13395
+ SHAPE['tm-ruler'] = {
13396
+ draw(n) { const y = n.y, ticks = TM_ARR(n.ticks), last = ticks.filter(t => t.major).pop();
13397
+ const d = `M${n.x} ${y}H${r2(n.x + n.w)}` + ticks.map(t => `M${t.x} ${y}v${t.major ? TR.mark : 3}`).join('');
13398
+ const labels = ticks.filter(t => t.major).map(t => tmText(t.x, y + 18, t.label, TR.font, 500, 'middle')).join('');
13399
+ const unit = n.unit && last ? tmText(last.x + textW(last.label, TR.font) / 2 + 4, y + 18, n.unit, TR.font, 500) : '';
13400
+ return tmNode(n, 'tm-ruler', `<rect x="${n.x}" y="${y}" width="${n.w}" height="${n.h}" fill="transparent"/><path d="${d}" stroke="var(--um-muted)" stroke-width="1"/>${labels}${unit}`); },
13401
+ };
13402
+
13403
+ /* the legend (pieces.js shows it): each sort of message, each kind of constraint and each lane form the view draws */
13404
+ const TM_WORDS = { call: ['Call', 'the sender waits for it'], async: ['Async', 'the sender goes on'], reply: ['Reply', 'the answer to a call'] };
13405
+ const tmKey = (name, says, svg) => `<span class="lgi" title="${esc(name + ' — ' + says)}"><svg class="lgs" viewBox="0 0 28 16" aria-hidden="true"><g stroke="var(--um-muted)" stroke-width="1.4" fill="none" stroke-linejoin="round">${svg}</g></svg><b>${esc(name)}</b><span>${esc(says)}</span></span>`;
13406
+ function tmLegend(X, o) {
13407
+ const sorts = new Set(o.lines.map(l => tmSort(X.msgs.find(m => m.id === l.id)))), kinds = new Set(o.marks.map(m => m.kind)), forms = new Set(o.lanes.map(l => (X.values(l.id) ? 'values' : 'states')));
13408
+ return [...TM_SORTS.filter(s => sorts.has(s)).map(s => tmKey(...TM_WORDS[s], `<path d="M2 8H17"${TM_END[s].dashed ? ' stroke-dasharray="3 2"' : ''}/>${s === 'call' ? '<path d="M17 4L26 8L17 12Z" fill="var(--um-muted)"/>' : '<path d="M17 4L26 8L17 12"/>'}`)),
13409
+ ...(kinds.has('duration') ? [tmKey('Duration constraint', 'how long may pass between two changes', '<path d="M2 4V12M26 4V12M2 8H26M8 5L2 8L8 11M20 5L26 8L20 11"/>')] : []),
13410
+ ...(kinds.has('time') ? [tmKey('Time constraint', 'when a change may happen', '<path d="M8 4V12"/>')] : []),
13411
+ ...(forms.has('states') ? [tmKey('States', 'the lifeline steps between its states', '<path d="M2 12H10V4H18V12H26" stroke="var(--um-text)"/>')] : []),
13412
+ ...(forms.has('values') ? [tmKey('Values', 'the lifeline\'s value between changes', '<path d="M2 8L5 3H12L15 8L18 3H26M2 8L5 13H12L15 8L18 13H26" stroke="var(--um-text)"/>')] : [])];
13413
+ }
13414
+
13415
+ /* ---- validators (§7.2). Each problem: { level, rule, path, message, ids } ---- */
13416
+ const TM_RULES = {
13417
+ 'change-lifeline': 'a change names a lifeline of the model',
13418
+ 'change-time': 'a change\'s at, and a message\'s sentAt and receivedAt, are numbers',
13419
+ 'change-state': 'in the states form, a change\'s state is one of its lifeline\'s states (else it is drawn on an extra row)',
13420
+ 'change-instant': 'one lifeline has one change at an instant (of two, the one whose id sorts last is drawn)',
13421
+ 'message-ends': 'a message joins one or two lifelines of the view',
13422
+ 'message-times': 'a message is received no earlier than it is sent',
13423
+ sort: 'a timing message\'s sort is call, async or reply; any other is drawn as a call',
13424
+ 'reply-call': 'a reply names in replyTo a call of the view going the other way, sent no later',
13425
+ 'constraint-ends': 'a duration constraint\'s from and to are changes of the view; a time constraint has a from and no to',
13426
+ 'constraint-order': 'a duration constraint\'s to is no earlier than its from',
13427
+ };
13428
+ const tmNum = x => typeof x === 'number' && Number.isFinite(x);
13429
+ function tmProblems(model, view) {
13430
+ const out = [], V = tmView(model, view); if (!TM_OBJ(model) || !V) return out;
13431
+ const vi = TM_ARR(model.views).indexOf(V), at = `views[${vi}]`, X = tmIndex(model, V), TL = TM_OBJ(V.timeline) ? V.timeline : {};
13432
+ const say = (rule, path, who, msg, level = 'error') => out.push({ level, rule, path, ids: who.filter(x => x != null), message: `${rule}: ${msg}` });
13433
+ const cPath = c => `${at}.timeline.changes[${TM_ARR(TL.changes).indexOf(c)}]`, kPath = c => `${at}.timeline.constraints[${TM_ARR(TL.constraints).indexOf(c)}]`, ePath = m => `${at}.edges[${TM_ARR(V.edges).indexOf(m)}]`;
13434
+ const seen = new Map();
13435
+ X.changes.forEach(c => { const n = X.N.get(c.lifeline);
13436
+ if (!n || n.type !== 'uml.Lifeline') return say('change-lifeline', cPath(c) + '.lifeline', [c.id], `change ${c.id} names ${JSON.stringify(c.lifeline)}, which is not a lifeline of the model`);
13437
+ if (!tmNum(c.at)) return say('change-time', cPath(c) + '.at', [c.id], `change ${c.id}'s time ${JSON.stringify(c.at)} is not a number`);
13438
+ if (!X.H.has(c.lifeline)) return; // a hidden lifeline keeps its changes, not drawn and not warned (§7.2)
13439
+ if (!X.values(c.lifeline) && !tmStates(n).includes(String(c.state ?? ''))) say('change-state', cPath(c) + '.state', [c.id, c.lifeline], `change ${c.id}'s state ${JSON.stringify(c.state)} is not one of ${tmName(n)}'s states: drawn on an extra row`, 'warning');
13440
+ const k = `${c.lifeline} ${c.at}`; if (seen.has(k)) say('change-instant', cPath(c) + '.at', [seen.get(k), c.id], `${tmName(n)} changes twice at ${X.t(c.at)} (${seen.get(k)} and ${c.id}): only ${[seen.get(k), c.id].sort().pop()} is drawn`, 'warning'); else seen.set(k, c.id); });
13441
+ X.msgs.forEach(m => { const p = tmProps(m), off = [m.from, m.to].filter(id => !X.H.has(id));
13442
+ if (off.length) say('message-ends', ePath(m), [m.id, ...off], `${m.id} runs from ${m.from} to ${m.to}, and ${[...new Set(off)].join(' and ')} ${new Set(off).size > 1 ? 'are not lifelines' : 'is not a lifeline'} of view ${V.id}`);
13443
+ if (!tmNum(p.sentAt) || !tmNum(p.receivedAt)) say('change-time', ePath(m) + '.props', [m.id], `message ${m.id}'s sentAt and receivedAt are not both numbers`);
13444
+ else if (p.receivedAt < p.sentAt) say('message-times', ePath(m) + '.props.receivedAt', [m.id], `${m.id} is received at ${X.t(p.receivedAt)}, before it is sent at ${X.t(p.sentAt)}`);
13445
+ if (!TM_SORTS.includes(p.sort)) say('sort', ePath(m) + '.props.sort', [m.id], `${m.id} has ${p.sort === undefined ? 'no sort' : `sort ${JSON.stringify(p.sort)}`}, not one of ${TM_SORTS.join(', ')}: it is drawn as a call`, 'warning');
13446
+ else if (p.sort === 'reply') { const c = X.msgs.find(x => x.id === p.replyTo);
13447
+ if (!c || tmSort(c) !== 'call' || c.from !== m.to || c.to !== m.from || !(tmProps(c).sentAt <= p.sentAt)) say('reply-call', ePath(m) + '.props.replyTo', [m.id, c && c.id], `${m.id} answers ${JSON.stringify(p.replyTo)}, which is not a call of this view from ${m.to} to ${m.from} sent no later`); } });
13448
+ X.cons.forEach(c => { const kind = TM_KIND[c.type], a = X.C.get(c.from), b = X.C.get(c.to);
13449
+ if (kind === 'time' ? !a || c.to != null : !a || !b) return say('constraint-ends', kPath(c), [c.id], kind === 'time' ? `time constraint ${c.id} needs a change of the view in from, and no to` : kind ? `duration constraint ${c.id} needs two changes of the view in from and to` : `${c.id} is a ${JSON.stringify(c.type)}, not a duration or time constraint`);
13450
+ if (kind === 'duration' && tmNum(a.at) && tmNum(b.at) && b.at < a.at) say('constraint-order', kPath(c) + '.to', [c.id, c.from, c.to], `${c.id} runs from ${X.t(a.at)} back to ${X.t(b.at)}`, 'warning'); });
13451
+ return out;
13452
+ }
13453
+
13454
+ /* ---- describe(): the view in words, lanes top to bottom, then messages in time order, then constraints ---- */
13455
+ function tmDescribe(model, viewId) {
13456
+ const V = TM_ARR(model && model.views).find(x => TM_OBJ(x) && x.id === viewId); if (!V) throw new Error(`umlimator: no view ${JSON.stringify(viewId)}`);
13457
+ const X = tmIndex(model, V), shown = X.changes.filter(c => X.H.has(c.lifeline));
13458
+ const out = [`${V.title || V.id}: a timing diagram of ${tmPlural(X.heads.length, 'lifeline')}, ${tmPlural(shown.length, 'change')}, ${tmPlural(X.msgs.length, 'message')} and ${tmPlural(X.shown.length, 'constraint')}${X.unit ? `, in ${X.unit}` : ''}.`];
13459
+ X.heads.forEach(n => { const cs = X.mine(n.id); out.push(`${tmHead(n)}${X.values(n.id) ? ' (values)' : ''}: ${cs.length ? cs.map(c => `${c.state ?? ''} from ${X.t(c.at)}`).join('; ') : 'no change'}.`); });
13460
+ [...X.msgs].sort((a, b) => tmProps(a).sentAt - tmProps(b).sentAt).forEach(m => out.push(tmMsgSaid(X, m) + '.'));
13461
+ X.shown.forEach(c => out.push(tmConSaid(X, c) + '.'));
13462
+ return out.join('\n');
13463
+ }
13464
+
13465
+ /* the PDF's rows (4.9, §9): a lifeline's changes in time order, the messages it sends, the constraints on its changes */
13466
+ function tmRows(model, view, id) {
13467
+ const V = tmView(model, view); if (!V) return null; const X = tmIndex(model, V); if (!X.H.has(id)) return null;
13468
+ const mine = X.mine(id), ids = new Set(mine.map(c => c.id));
13469
+ return [...mine.map(c => `${X.t(c.at)}: ${c.state ?? ''}`),
13470
+ ...X.msgs.filter(m => m.from === id).map(m => { const p = tmProps(m); return `${tmLabel(m) || tmSort(m)} to ${X.name(m.to)} at ${X.t(p.sentAt)} → ${X.t(p.receivedAt)}`; }),
13471
+ ...X.shown.filter(c => ids.has(c.from) || ids.has(c.to)).map(c => tmConSaid(X, c))];
13472
+ }
13473
+
13474
+ /* ---- the gesture hooks (§8.1). Each takes the model and the view (or its id) and returns the commands of one transaction,
13475
+ { commands, say }, null to leave the gesture to the canvas's default, or { error } for the hint line. A transaction that
13476
+ would add an error the model did not have, or a change on another's instant, is never returned: { error } names the rule ---- */
13477
+ function tmNewId(model, base) { // the base and the first number free in the model, time items and members included
13478
+ const used = new Set(); JSON.stringify(model, (k, v) => { if (k === 'id' && typeof v === 'string') used.add(v); return v; });
13479
+ const b = String(base).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'el'; let k = 1; while (used.has(b + k)) k++; return b + k;
13480
+ }
13481
+ const tmErr = error => ({ error });
13482
+ const tmKeyOf = p => `${p.rule}|${[...p.ids].sort().join()}`, tmBad = p => p.level === 'error' || p.rule === 'change-instant'; // a change put on another's instant would not be drawn
13483
+ function tmGuard(model, V, commands) {
13484
+ if (!Array.isArray(commands) || !commands.length) return commands;
13485
+ let next; try { next = apply(model, commands); } catch (x) { return tmErr(String(x && x.message || x)); }
13486
+ const was = new Set(tmProblems(model, V).filter(tmBad).map(tmKeyOf));
13487
+ const bad = tmProblems(next, tmView(next, V.id)).find(p => tmBad(p) && !was.has(tmKeyOf(p)));
13488
+ return bad ? tmErr(bad.message) : commands;
13489
+ }
13490
+ // a new time item, by set of the whole list (the core's add would put it in nodes or edges, §8.1), its id unused in the model
13491
+ function tmPut(model, V, list, item) {
13492
+ const used = new Set(); JSON.stringify(model, (k, v) => { if (k === 'id' && typeof v === 'string') used.add(v); return v; });
13493
+ if (used.has(item.id)) return tmErr(`id ${item.id} is used already`);
13494
+ return { op: 'set', view: V.id, path: ['timeline', list], value: [...TM_ARR(TM_OBJ(V.timeline) && V.timeline[list]), item] };
13495
+ }
13496
+ // the minor tick (a fifth of the ruler's step) and the next one before or after t
13497
+ const tmMinor = (model, V) => layoutTimeRuler(tmInput(model, V)).scale.step / 5;
13498
+ const tmSnap = (t, minor, dir) => Number(((dir > 0 ? Math.floor(t / minor + 1e-9) + 1 : Math.ceil(t / minor - 1e-9) - 1) * minor).toPrecision(12));
13499
+ // a state to put a change in: in the states form, one not listed is added to the lifeline's states in the same transaction
13500
+ function tmState(X, lane, state) {
13501
+ const n = X.N.get(lane), have = tmStates(n);
13502
+ if (X.values(lane) || have.includes(state)) return { cmds: [], said: null };
13503
+ return { cmds: [{ op: 'set', id: lane, path: 'props.states', value: [...have, state] }], said: `added state ${state} to ${tmName(n)}` };
13504
+ }
13505
+ const tmSaid = (r, say) => (Array.isArray(r) && say ? { commands: r, say } : r);
13506
+
13507
+ /* add(model, view, entry, label, at): a lifeline (name, or `name : Type`) at the end of nodeIds; a change (its state or
13508
+ value typed) after the change selected, a minor tick on, else on the lifeline selected at the ruler's start, else at the
13509
+ slot `at` (its lane and t), moved on a minor tick at a time past its lifeline's changes; a time constraint on the change
13510
+ selected */
13511
+ function tmAdd(model, view, entry, label, at) {
13512
+ const V = tmView(model, view), type = TM_OBJ(entry) ? entry.type : null; if (!V) return null;
13513
+ const X = tmIndex(model, V), text = String(label ?? '').trim(), sel = TM_ARR(TM_OBJ(at) && at.sel);
13514
+ const change = [...sel].reverse().map(id => X.C.get(id)).find(Boolean), lane = [...sel].reverse().find(id => X.H.has(id));
13515
+ if (type === 'uml.Lifeline') { const [name, rep] = tmNameType(text);
13516
+ const element = { id: tmNewId(model, name || 'lifeline'), type, ...(name ? { label: name } : {}), ...(TM_HAS(rep) ? { props: { represents: rep } } : {}) };
13517
+ return tmGuard(model, V, [{ op: 'add', element, view: V.id }]); }
13518
+ if (type === 'uml.StateInvariant') {
13519
+ if (!text) return tmErr('a change needs its state: type it');
13520
+ const minor = tmMinor(model, V), where = change && X.H.has(change.lifeline) ? { lane: change.lifeline, t: tmSnap(change.at, minor, 1) }
13521
+ : lane ? { lane, t: layoutTimeRuler(tmInput(model, V, X)).scale.t0 } : TM_OBJ(at) && X.H.has(at.parent) && Number.isFinite(at.t) ? { lane: at.parent, t: at.t } : null;
13522
+ if (!where) return tmErr('select a lifeline or a change first');
13523
+ const taken = new Set(X.changes.filter(c => c.lifeline === where.lane).map(c => c.at)); while (taken.has(where.t)) where.t = tmSnap(where.t, minor, 1); // the first free minor tick
13524
+ const s = tmState(X, where.lane, text), put = tmPut(model, V, 'changes', { id: tmNewId(model, 'change'), lifeline: where.lane, at: where.t, state: text }); if (put.error) return put;
13525
+ return tmSaid(tmGuard(model, V, [...s.cmds, put]), s.said && `Added the change; ${s.said}`); }
13526
+ if (type === 'uml.TimeConstraint') {
13527
+ if (!change) return tmErr('select the change a time constraint is on first');
13528
+ const put = tmPut(model, V, 'constraints', { id: tmNewId(model, 'time'), type, from: change.id, ...(text ? { label: text } : {}) }); if (put.error) return put;
13529
+ return tmGuard(model, V, [put]); }
13530
+ return null;
13531
+ }
13532
+
13533
+ /* connect(model, view, from, to, at, line): a message between two lifelines at the slot's t (sent and received then), from
13534
+ one change's time to another's, or from a change to a lifeline at the change's time; a duration constraint between two
13535
+ changes, in time order. A reply answers the latest call back along it sent no later */
13536
+ function tmConnect(model, view, from, to, at, line) {
13537
+ const V = tmView(model, view); if (!V) return null;
13538
+ const X = tmIndex(model, V), type = (line && line.type) || 'uml.Message', t0 = TM_OBJ(at) && Number.isFinite(at.t) ? at.t : 0;
13539
+ const end = id => (X.C.has(id) ? { lane: X.C.get(id).lifeline, t: X.C.get(id).at, c: X.C.get(id) } : X.H.has(id) ? { lane: id, t: t0 } : null), a = end(from), b = end(to);
13540
+ if (type === 'uml.DurationConstraint') {
13541
+ if (!a || !b || !a.c || !b.c || a.c === b.c) return tmErr('a duration constraint joins two changes: draw it from one change to another');
13542
+ const [p, q] = b.c.at < a.c.at ? [b.c, a.c] : [a.c, b.c], put = tmPut(model, V, 'constraints', { id: tmNewId(model, 'duration'), type, from: p.id, to: q.id }); if (put.error) return put;
13543
+ return tmGuard(model, V, [put]); }
13544
+ if (!a || !b) return tmErr('a message joins two lifelines of the view, or two changes on them');
13545
+ const props = { ...(TM_OBJ(line && line.props) ? structuredClone(line.props) : {}) }, sort = TM_SORTS.includes(props.sort) ? props.sort : (props.sort = 'call');
13546
+ Object.assign(props, { sentAt: a.t, receivedAt: b.t });
13547
+ if (sort === 'reply') { const c = X.msgs.filter(m => tmSort(m) === 'call' && m.from === b.lane && m.to === a.lane && tmProps(m).sentAt <= a.t).sort((p, q) => tmProps(q).sentAt - tmProps(p).sentAt)[0];
13548
+ if (c) props.replyTo = c.id; }
13549
+ return tmGuard(model, V, [{ op: 'add', element: { id: tmNewId(model, 'm'), type: 'uml.Message', from: a.lane, to: b.lane, props }, view: V.id, in: 'view' }]);
13550
+ }
13551
+
13552
+ /* moveTo(model, view, id, at): a change dropped (at a slot: its t) or Alt + Left/Right (one minor tick), Alt + Up/Down on a
13553
+ states-form change (the state above or below); a message dropped (both times by the drop's t less its sentAt) or Alt +
13554
+ Left/Right (both by a minor tick); a lifeline Alt + Up/Down (its place in nodeIds). Anything else: null */
13555
+ function tmMoveTo(model, view, id, at) {
13556
+ const V = tmView(model, view); if (!V || !TM_OBJ(at)) return null;
13557
+ const X = tmIndex(model, V), by = Array.isArray(at.by) ? at.by : null, c = X.C.get(id), m = X.msgs.find(x => x.id === id);
13558
+ if (X.H.has(id)) { if (!by || by[0] || !by[1]) return null; const ids = TM_ARR(V.nodeIds), i = ids.indexOf(id), j = i + Math.sign(by[1]);
13559
+ return j < 0 || j >= ids.length ? [] : [{ op: 'move', id, view: V.id, index: j }]; }
13560
+ if (c) {
13561
+ if (by && by[1] && !by[0]) { const n = X.N.get(c.lifeline); if (!n || X.values(c.lifeline)) return [];
13562
+ const ss = tmStates(n), i = ss.indexOf(String(c.state)), k = (i < 0 ? ss.length : i) + Math.sign(by[1]); // an unlisted state: the extra row below
13563
+ return k < 0 || k >= ss.length ? [] : tmGuard(model, V, [{ op: 'set', id, path: 'state', value: ss[k] }]); }
13564
+ const t = by ? (by[0] ? tmSnap(c.at, tmMinor(model, V), by[0]) : null) : Number.isFinite(at.t) ? at.t : null;
13565
+ return t == null ? null : t === c.at ? [] : tmGuard(model, V, [{ op: 'set', id, path: 'at', value: t }]); }
13566
+ if (m) { const p = tmProps(m), d = by ? (by[0] && !by[1] ? Math.sign(by[0]) * tmMinor(model, V) : 0) : Number.isFinite(at.t) ? at.t - p.sentAt : null;
13567
+ if (d == null) return null; if (!d) return [];
13568
+ const nice = x => Number((x + d).toPrecision(12));
13569
+ return tmGuard(model, V, [{ op: 'set', id, path: 'props.sentAt', value: nice(p.sentAt) }, { op: 'set', id, path: 'props.receivedAt', value: nice(p.receivedAt) }]); }
13570
+ return null;
13571
+ }
13572
+
13573
+ /* rename(model, view, id, text): F2. A change: `state` or `state @ t`; a message: `label`, `label @ t` or `label @ t1 → t2`;
13574
+ a constraint: its label; a lifeline: `name` or `name : Type`; a state's row: the state's name, in states and in every
13575
+ change of that lifeline in it; the ruler: the timeline's unit (empty removes it) */
13576
+ function tmRename(model, view, id, text) {
13577
+ const V = tmView(model, view); if (!V) return null;
13578
+ const X = tmIndex(model, V), s = String(text ?? '').trim();
13579
+ if (id === TM_RULER) return [{ op: 'set', view: V.id, path: ['timeline', 'unit'], value: s || undefined }];
13580
+ const c = X.C.get(id);
13581
+ if (c) { const r = tmParse(s); if (r.times.length > 1) return tmErr('a change has one time: state @ t'); if (!r.text) return tmErr('a change needs its state');
13582
+ const st = X.H.has(c.lifeline) ? tmState(X, c.lifeline, r.text) : { cmds: [], said: null };
13583
+ return tmSaid(tmGuard(model, V, [...st.cmds, { op: 'set', id, path: 'state', value: r.text }, ...(r.times.length ? [{ op: 'set', id, path: 'at', value: r.times[0] }] : [])]), st.said && `Renamed the change; ${st.said}`); }
13584
+ const m = X.msgs.find(x => x.id === id);
13585
+ if (m) { const r = tmParse(s), [t1, t2] = r.times.length === 1 ? [r.times[0], r.times[0]] : r.times;
13586
+ return tmGuard(model, V, [{ op: 'set', id, path: 'label', value: r.text || undefined }, ...(r.times.length ? [{ op: 'set', id, path: 'props.sentAt', value: t1 }, { op: 'set', id, path: 'props.receivedAt', value: t2 }] : [])]); }
13587
+ if (X.cons.some(k => k.id === id)) return [{ op: 'set', id, path: 'label', value: s || undefined }];
13588
+ if (X.H.has(id)) { const [name, rep] = tmNameType(s);
13589
+ if (!name) return tmErr('a lifeline needs a name');
13590
+ return [{ op: 'set', id, path: 'label', value: name }, ...(s.includes(':') ? [{ op: 'set', id, path: 'props.represents', value: rep || undefined }] : [])]; }
13591
+ const k = String(id).indexOf('->'), lane = k > 0 ? String(id).slice(0, k) : null, was = k > 0 ? String(id).slice(k + 2) : null;
13592
+ if (lane && X.H.has(lane)) { const ss = tmStates(X.N.get(lane));
13593
+ if (!ss.includes(was)) return tmErr(`${was} is not one of ${X.name(lane)}'s listed states: add it to its states first`);
13594
+ if (!s) return tmErr('a state needs a name'); if (s !== was && ss.includes(s)) return tmErr(`${X.name(lane)} has a state ${s} already`);
13595
+ return [{ op: 'set', id: lane, path: 'props.states', value: ss.map(x => (x === was ? s : x)) }, ...X.changes.filter(x => x.lifeline === lane && String(x.state) === was).map(x => ({ op: 'set', id: x.id, path: 'state', value: s }))]; }
13596
+ return null;
13597
+ }
13598
+
13599
+ /* remove(model, view, ids, fromModel): a change, a message or a constraint cannot be hidden (S17): Delete deletes it, and says
13600
+ so. Delete on a lifeline hides it (its changes stay in the timeline, not drawn; its messages go with it, said with their
13601
+ count, decision 23); Shift + Delete deletes it, its changes and their constraints (the core's cascade). A state's row or
13602
+ the ruler is not deleted on its own */
13603
+ function tmRemove(model, view, ids, fromModel) {
13604
+ const V = tmView(model, view); if (!V) return null;
13605
+ const X = tmIndex(model, V), out = [], gone = { lane: [] };
13606
+ for (const id of new Set(TM_ARR(ids))) {
13607
+ if (id === TM_RULER || String(id).includes('->')) return tmErr('a state\'s row and the ruler go with their lifeline or view: rename a state with F2, or edit the lifeline\'s states');
13608
+ if (X.H.has(id)) { gone.lane.push(id); out.push(fromModel || TM_ARR(V.nodes).some(n => TM_OBJ(n) && n.id === id) ? { op: 'delete', id } : { op: 'hide', id, view: V.id }); continue; }
13609
+ if (X.C.has(id) || X.msgs.some(m => m.id === id) || X.cons.some(c => c.id === id)) { out.push({ op: 'delete', id }); continue; }
13610
+ return null;
13611
+ }
13612
+ const r = tmGuard(model, V, out); if (!Array.isArray(r) || !r.length) return r;
13613
+ // what goes, read off the model after: the core's cascade takes a call's reply and a change's constraints too
13614
+ const next = apply(model, r), Y = tmIndex(next, tmView(next, V.id)), left = new Set([...Y.changes, ...Y.msgs, ...Y.cons].map(x => x.id));
13615
+ const lanes = new Set(gone.lane), theirs = new Set(X.msgs.filter(m => lanes.has(m.from) || lanes.has(m.to)).map(m => m.id)), n = theirs.size;
13616
+ const k = X.changes.filter(c => lanes.has(c.lifeline) && left.has(c.id)).length;
13617
+ const list = (skip = new Set()) => [[X.changes, 'change'], [X.msgs, 'message'], [X.cons, 'constraint']].map(([xs, w]) => [xs.filter(x => !left.has(x.id) && !skip.has(x.id)).length, w])
13618
+ .filter(([c]) => c).map(([c, w]) => tmPlural(c, w)).join(', ').replace(/, ([^,]*)$/, ' and $1'); // "1 change, 2 messages and 1 constraint"
13619
+ const who = gone.lane.map(X.name).join(', '), one = gone.lane.length === 1, msgs = n ? tmPlural(n, 'message') : 'no message', more = list(theirs), all = list();
13620
+ const say = gone.lane.length ? (fromModel ? `Deleted ${who}${all ? ', ' + all : ''}` : `Hid ${who}; deleted ${one ? 'its' : 'their'} ${msgs}${more ? `, and ${more}` : ''}; ${one ? 'its' : 'their'} ${k === 1 ? 'change stays' : 'changes stay'} in the timeline`)
13621
+ : `Deleted ${all}: a change, a message or a constraint is deleted, not hidden`;
13622
+ return { commands: r, say };
13623
+ }
13624
+
13625
+ /* key(model, view, ids, key): S on a lifeline turns its values form on or off (present.<id>.as); any other key: null */
13626
+ function tmKeyHook(model, view, ids, key) {
13627
+ const V = tmView(model, view); if (!V || (key !== 's' && key !== 'S')) return null;
13628
+ const X = tmIndex(model, V), ls = TM_ARR(ids).filter(id => X.H.has(id)); if (!ls.length) return null;
13629
+ return ls.map(id => ({ op: 'set', view: V.id, path: ['present', id, 'as'], value: X.values(id) ? undefined : 'values' }));
13630
+ }
13631
+
13632
+ /* ---- the type ---- */
13633
+ const TM_STEREO = { key: 'stereotypes', label: 'Stereotypes', kind: 'list' };
13634
+ const TM_TYPE = registerDiagramType('uml.timing', {
13635
+ title: 'timing diagram',
13636
+ elements: [{ type: 'uml.Lifeline', label: 'Lifeline' }, { type: 'uml.StateInvariant', label: 'Change' }, { type: 'uml.TimeConstraint', label: 'Time constraint' }],
13637
+ lines: [...TM_SORTS.map(sort => ({ type: 'uml.Message', label: TM_WORDS[sort][0], props: { sort } })), { type: 'uml.DurationConstraint', label: 'Duration constraint' }],
13638
+ refs: { 'uml.Message': { 'props.replyTo': 'cascade' } },
13639
+ props: {
13640
+ 'uml.Lifeline': [{ key: 'represents', label: 'Represents', kind: 'text' }, { key: 'states', label: 'States', kind: 'list' }, TM_STEREO],
13641
+ 'uml.Message': [{ key: 'sort', label: 'Sort', kind: 'enum', options: TM_SORTS }, { key: 'replyTo', label: 'Reply to', kind: 'text' }],
13642
+ },
13643
+ layout: { run: tmRun, pins: false },
13644
+ draw: tmDraw,
13645
+ validate: tmProblems,
13646
+ rules: TM_RULES,
13647
+ describe: tmDescribe,
13648
+ rows: tmRows,
13649
+ edits: { connect: tmConnect, moveTo: tmMoveTo, add: tmAdd, remove: tmRemove, rename: tmRename, key: tmKeyHook },
13650
+ });