yichus 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -16
- package/dist/cli.js +17 -2123
- package/dist/cli.js.map +1 -1
- package/dist/main.js +3762 -0
- package/dist/main.js.map +1 -0
- package/dist/studio-page/dom.js +22 -0
- package/dist/studio-page/label-fit.js +7 -0
- package/dist/studio-page/layer-diagram.js +458 -0
- package/dist/studio-page/layer-groups.js +129 -0
- package/dist/studio-page/source-link.js +17 -0
- package/dist/studio-page/studio-views.js +256 -0
- package/dist/studio-page/system-map-record.js +124 -10
- package/dist/studio-page/system-map.js +476 -87
- package/dist/studio-page/what-happens-record.js +224 -0
- package/dist/studio-page/what-happens.js +771 -0
- package/dist/studio-reference/balance-listed-laws.bosatsu +29 -0
- package/dist/studio-reference/balance.bosatsu +7 -0
- package/dist/studio-reference/calculator.md +20 -0
- package/dist/studio-reference/consent-laws.bosatsu +37 -0
- package/dist/studio-reference/consent.bosatsu +15 -0
- package/dist/studio-reference/deploy-platforms.md +79 -0
- package/dist/studio-reference/laws.md +62 -0
- package/dist/studio-reference/portable-programs.md +58 -0
- package/dist/studio-reference/porting.md +34 -0
- package/dist/studio-reference/protocol-cases.md +11 -0
- package/dist/studio-reference/{protocol-specification.txt → protocol-specification.md} +16 -18
- package/dist/studio-reference/refund-desk-one-round.bosatsu +196 -0
- package/dist/studio-reference/refund-desk.bosatsu +194 -0
- package/dist/studio-reference/refund-policy.bosatsu +97 -0
- package/dist/studio-reference/refund-test.bosatsu +75 -0
- package/dist/studio-reference/render.yaml +42 -0
- package/dist/studio-reference/server.mjs +33 -0
- package/dist/studio-reference/store.mjs +16 -0
- package/dist/studio-reference/the-page.md +130 -0
- package/dist/studio-reference/worker-authority.md +159 -0
- package/package.json +2 -2
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// from the record. Every drawn mark carries data-mark (its kind, explained
|
|
17
17
|
// on web/system-map-blocks.html) and data-f (the facts it draws).
|
|
18
18
|
|
|
19
|
-
import { SANS, MONO, textWidth, clipText, fitLabel, lineGap, boxAttr, fitAttr, owned, labelled, wrapText, anchorAttr, anchored, bound } from './label-fit.js';
|
|
19
|
+
import { SANS, MONO, textWidth, clipText, fitLabel, lineGap, boxAttr, fitAttr, owned, labelled, wrapText, widestPiece, anchorAttr, anchored, bound } from './label-fit.js';
|
|
20
20
|
|
|
21
21
|
const C = {
|
|
22
22
|
ink: '#0f172a', ink2: '#334155', muted: '#64748b', faint: '#94a3b8', hair: '#e2e8f0',
|
|
@@ -26,6 +26,9 @@ const C = {
|
|
|
26
26
|
hull: '#f1ecfb', hullEdge: '#c9b8ee', hullInk: '#5b3fa8',
|
|
27
27
|
guard: '#b4231d', region: '#f4f6fa', regionEdge: '#dfe4ec',
|
|
28
28
|
model: '#8b45d6', modelSoft: '#f3ebfc', net: '#c7700a', netSoft: '#fcf1e3',
|
|
29
|
+
// the program runner (compile, run) and an effect of no outside resource
|
|
30
|
+
compile: '#0e7490', compileSoft: '#e0f2f6', run: '#0e7490', runSoft: '#e0f2f6', io: '#64748b', ioSoft: '#eef2f6',
|
|
31
|
+
program: '#0f5132', programSoft: '#eef7f1', programEdge: '#7fb69a',
|
|
29
32
|
};
|
|
30
33
|
const esc = s => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
31
34
|
export const tw = (s, size, mono = true, weight = 400) => textWidth(s, size, mono, weight);
|
|
@@ -36,33 +39,16 @@ const f1 = n => (+n).toFixed(1);
|
|
|
36
39
|
export const MIN_LABEL = 9;
|
|
37
40
|
// every drawn element names the facts it draws, for tests/site/system-map.test.mjs
|
|
38
41
|
export const tg = (keys, svg) => { const ks = [].concat(keys).filter(Boolean); return ks.length ? `<g data-f="${ks.map(k => encodeURIComponent(k)).join(' ')}">${svg}</g>` : svg; };
|
|
39
|
-
// A name that does not fit on one line at 80% size is broken in two after a
|
|
40
|
-
// separator ('_', ', ', ' in ', '(', ' · ') nearest its middle; 'up' puts the
|
|
41
|
-
// first half above, 'down' below. Returns the svg.
|
|
42
|
-
function wrapLines(s, width, size = 10, mono = true, maxLines = 3) {
|
|
43
|
-
s = String(s);
|
|
44
|
-
const one = fitText(s, size, width, mono, 0.8);
|
|
45
|
-
if (!one.s.endsWith('…') || maxLines < 2) return [one];
|
|
46
|
-
// greedy: as much as fits on each line, cut after a separator
|
|
47
|
-
const sz = Math.max(MIN_LABEL, size * 0.8);
|
|
48
|
-
const lines = []; let rest = s;
|
|
49
|
-
while (rest.length && lines.length < maxLines) {
|
|
50
|
-
if (tw(rest, sz, mono) <= width || lines.length === maxLines - 1) { lines.push(rest); rest = ''; break; }
|
|
51
|
-
let cut = -1;
|
|
52
|
-
for (const sep of ['_', ', ', ' in ', '(', ' · ', '-', '/', ' ']) { let i = rest.indexOf(sep, 1); while (i > 0) { const k = i + (sep === '(' ? 0 : sep.length); if (tw(rest.slice(0, k), sz, mono) <= width && k > cut) cut = k; i = rest.indexOf(sep, i + 1); } }
|
|
53
|
-
if (cut <= 0) { cut = 1; while (cut < rest.length && tw(rest.slice(0, cut + 1), sz, mono) <= width) cut++; }
|
|
54
|
-
lines.push(rest.slice(0, cut)); rest = rest.slice(cut);
|
|
55
|
-
}
|
|
56
|
-
return lines.map(t => fitText(t, sz, width, mono, 0.85));
|
|
57
|
-
}
|
|
58
|
-
const lineLead = size => size * 0.95 + 1.4;
|
|
59
|
-
function T2(x, y, s, width, o = {}, dir = 'up', maxLines = 3) {
|
|
60
|
-
const size = o.size || 10, lines = wrapLines(s, width, size, o.mono !== false, maxLines), n = lines.length;
|
|
61
|
-
return lines.map((f, i) => T(x, dir === 'up' ? y - (n - 1 - i) * lineLead(size) : y + i * lineLead(size), f.s, { ...o, size: f.size, full: f.s.endsWith('…') ? s : null })).join('');
|
|
62
|
-
}
|
|
63
42
|
// a station's verb and tables, on as many lines as they take up to three; more, the verb and the count of tables
|
|
64
43
|
function reachLines(text, width, size) {
|
|
65
|
-
const
|
|
44
|
+
const sz = Math.max(MIN_LABEL, size);
|
|
45
|
+
let ls = wrapText(text, sz, width).map(s => ({ s, size: sz }));
|
|
46
|
+
// too narrow for its words on one line: the verb stays whole on its own line, the tables clip under it
|
|
47
|
+
const [verb, ...rest] = text.split(' ');
|
|
48
|
+
// not even the verb fits: no words (the station's mark still says it reads or writes)
|
|
49
|
+
if (wrapText(verb, sz, width)[0] !== verb) return [];
|
|
50
|
+
if (ls.length === 1 && ls[0].s.endsWith('…') && rest.length)
|
|
51
|
+
ls = [verb, ...wrapText(rest.join(' '), sz, width)].map(s => ({ s, size: sz }));
|
|
66
52
|
if (ls.length <= 3) return ls;
|
|
67
53
|
const short = fitText(`${text.match(/^(read|write|delete)( · (read|write|delete))*/)[0]} ${text.split(', ').length} tables`, size, width);
|
|
68
54
|
return short.s.endsWith('…') ? ls : [short];
|
|
@@ -116,8 +102,12 @@ const Tw = (x, y, lines, o, full, gap, dir = 'down') => {
|
|
|
116
102
|
return n > 1 ? `<g><title>${esc(name)}</title>${body}</g>` : body;
|
|
117
103
|
};
|
|
118
104
|
|
|
105
|
+
|
|
119
106
|
// ---------------------------------------------------------------- marks
|
|
120
107
|
// Each mark is one concept. The same function draws it in both views.
|
|
108
|
+
// what a ? says: on hover, and once in words in the key beside the rail
|
|
109
|
+
const UNTRACED = 'the database handle is traced to where it comes from on some paths only';
|
|
110
|
+
const UNTRACED_WORD = 'partly traced';
|
|
121
111
|
export const mark = {
|
|
122
112
|
dot: (x, y, r = 3.4, fill = C.ink2) => `<circle cx="${f1(x)}" cy="${f1(y)}" r="${r}" fill="${fill}"/>`,
|
|
123
113
|
// station: a ring where a definition touches a resource; filled = writes
|
|
@@ -146,7 +136,7 @@ export const mark = {
|
|
|
146
136
|
handle: (x, y, kind = 'db', r = 5.5) => `<circle cx="${f1(x)}" cy="${f1(y)}" r="${r}" fill="${kind === 'record' ? C.rec : C.db}" stroke="${C.paper}" stroke-width="1.5"/>`,
|
|
147
137
|
// the builder: a plus-disc where the handle is made, at the use
|
|
148
138
|
builder: (x, y, kind = 'db', r = 5) => `<circle cx="${f1(x)}" cy="${f1(y)}" r="${r}" fill="${C.paper}" stroke="${kind === 'record' ? C.rec : C.db}" stroke-width="1.6"/><path d="M${f1(x - r * 0.55)} ${f1(y)} H${f1(x + r * 0.55)} M${f1(x)} ${f1(y - r * 0.55)} V${f1(y + r * 0.55)}" stroke="${kind === 'record' ? C.rec : C.db}" stroke-width="1.6"/>`,
|
|
149
|
-
unknown: (x, y, r = 7) => `<circle cx="${f1(x)}" cy="${f1(y)}" r="${r}" fill="${C.wash}"/>` + T(x, y + r * 0.5, '?', { anchor: 'middle', size: r * 1.4, weight: 700, fill: C.muted, mono: false, halo: false }),
|
|
139
|
+
unknown: (x, y, r = 7) => `<title>${UNTRACED}</title><circle cx="${f1(x)}" cy="${f1(y)}" r="${r}" fill="${C.wash}"/>` + T(x, y + r * 0.5, '?', { anchor: 'middle', size: r * 1.4, weight: 700, fill: C.muted, mono: false, halo: false }),
|
|
150
140
|
loop: (x, y, r, color = C.hullInk) => `<path d="M${f1(x + r)} ${f1(y)} A${r} ${r} 0 1 1 ${f1(x)} ${f1(y - r)}" fill="none" stroke="${color}" stroke-width="1.6"/><path d="M${f1(x - 4)} ${f1(y - r - 3.5)} L${f1(x + 1)} ${f1(y - r)} L${f1(x - 4)} ${f1(y - r + 3.5)}" fill="none" stroke="${color}" stroke-width="1.4"/>`,
|
|
151
141
|
// callers
|
|
152
142
|
client: (x, y, s = 1, ghost = false) => {
|
|
@@ -170,7 +160,9 @@ for (const [k, f] of Object.entries(mark)) mark[k] = (...a) => `<g data-mark="${
|
|
|
170
160
|
// A drawn shape of one kind that is not one of the marks above.
|
|
171
161
|
const shape = (kind, svg) => `<g data-mark="${kind}">${svg}</g>`;
|
|
172
162
|
export const MARK_KINDS = [...new Set([...Object.values(MARK_KIND), 'resource', 'hull', 'rail', 'cable', 'bracket', 'role', 'token', 'afterDot', 'modelRing', 'netRing',
|
|
173
|
-
'
|
|
163
|
+
'reachedBy', 'port', 'plug', 'package', 'chain', 'composes', 'combination', 'generic', 'loop', 'wall',
|
|
164
|
+
// the program band: a pure program, its requests, and the engines that perform them
|
|
165
|
+
'program', 'decision', 'request', 'variant', 'gap', 'wire', 'lane', 'laneFork', 'laneResource', 'replyLoop', 'starter', 'programEffect', 'replaced',
|
|
174
166
|
// the what-happens view's (what-happens.js) one new mark: the end of a path
|
|
175
167
|
'outcome'])].sort();
|
|
176
168
|
|
|
@@ -213,20 +205,22 @@ const tableLines = (tables, room, phone) => tables.flatMap(t => wrapText(t, phon
|
|
|
213
205
|
const RACK = ['db', 'model', 'net'];
|
|
214
206
|
|
|
215
207
|
// notes: lines of computed text under a present slot, e.g. { model: { lines, kind } }
|
|
216
|
-
function rack(map, x, y, phone, room, notes = {}) {
|
|
208
|
+
function rack(map, x, y, phone, room, notes = {}, scale = {}) {
|
|
217
209
|
// fixed slots: db, model, net, top to bottom, present or struck
|
|
218
210
|
const out = [], pos = new Map();
|
|
219
|
-
const
|
|
211
|
+
const w0 = phone ? 38 : 64, h0 = phone ? 46 : 64, gw = w0 * 0.66, gh = h0 * 0.66;
|
|
220
212
|
let yy = y;
|
|
221
213
|
for (const k of RACK) {
|
|
222
214
|
const present = map.resources.find(r => r.kind === k);
|
|
215
|
+
// a present resource drawn at its share of the routes that reach it (1 when nothing is counted)
|
|
216
|
+
const w = w0 * (scale[k] ? scale[k].w : 1), h = h0 * (scale[k] ? scale[k].h : 1);
|
|
223
217
|
if (present) {
|
|
224
218
|
const tl = k === 'db' ? (present.tables || []) : [];
|
|
225
219
|
out.push(tg([`resource:${k}`], shape('resource', resourceGlyph(k, x - w / 2, yy, w, h, phone, false, tl, room))));
|
|
226
220
|
pos.set(k, { x: x - w / 2, cx: x, y0: yy, y1: yy + h, bottom: yy + h });
|
|
227
221
|
const tablesH = tl.length && tl.length < 12 ? tableLines(tl, room, phone).length * (phone ? 11 : 13) + (phone ? 6 : 8) : 0;
|
|
228
222
|
const note = notes[k], ls = phone ? 11 : 12;
|
|
229
|
-
if (note) out.push(shape(note.kind, note.lines.map((l, i) => T(x, yy + h + tablesH + (phone ? 12 : 14) + i * ls, l, { anchor: 'middle', size: phone ? 9 : 10, fill: C[k] })).join('')));
|
|
223
|
+
if (note) out.push(tg(note.keys, shape(note.kind, note.lines.map((l, i) => T(x, yy + h + tablesH + (phone ? 12 : 14) + i * ls, l, { anchor: 'middle', size: phone ? 9 : 10, fill: C[k] })).join(''))));
|
|
230
224
|
yy += h + tablesH + (note ? note.lines.length * ls + 4 : 0) + (phone ? 14 : 20);
|
|
231
225
|
} else {
|
|
232
226
|
out.push(tg([`ghost:${k}`], shape('struck', resourceGlyph(k, x - gw / 2, yy, gw, gh, phone, true))));
|
|
@@ -275,6 +269,11 @@ const afterRoom = (b, from, xr) => {
|
|
|
275
269
|
return b.guards.length ? xr - 2 - (from + (xr - from) / (n + 1)) - 6 : xr - from - 8;
|
|
276
270
|
};
|
|
277
271
|
|
|
272
|
+
// A hull's right wall on each path that leaves it, where the wrapper returns:
|
|
273
|
+
// whatever the path passes after it runs after the wrapper.
|
|
274
|
+
const hullWall = (x, ys, phone) =>
|
|
275
|
+
`<path d="${ys.map(y => `M${f1(x)} ${f1(y - (phone ? 8 : 10))} V${f1(y + (phone ? 8 : 10))}`).join(' ')}" stroke="${C.hullInk}" stroke-width="3"/>`;
|
|
276
|
+
|
|
278
277
|
export function renderSystemMap(map, opts = {}) {
|
|
279
278
|
const W = opts.width || 1100;
|
|
280
279
|
return inFrame(W, () => map.kind === 'service' ? renderService(map, W, opts) : renderLibrary(map, W, opts));
|
|
@@ -329,7 +328,25 @@ function renderService(map, W, opts) {
|
|
|
329
328
|
const gap = phone ? 3 : 4;
|
|
330
329
|
const lockW = e => (e.authority === 'publishable' || e.signedIn ? (phone ? 12 : 14) : 0);
|
|
331
330
|
const labX = e => x0 + (phone ? 7 : 10) + lockW(e);
|
|
332
|
-
|
|
331
|
+
// a bundle says what it counts: its routes
|
|
332
|
+
const countOf = e => (e.members.length > 1 ? `${e.members.length} routes` : '');
|
|
333
|
+
// A row's checks by name: every member's, not only the first's (a bundle
|
|
334
|
+
// holds routes with the same marks, not the same checks). A check that
|
|
335
|
+
// took another definition's place on the same route since the compared
|
|
336
|
+
// base is named with the one it replaced.
|
|
337
|
+
const replacedAt = new Map(((map.compare && map.compare.replaced) || []).map(r => [`${r.entry} ${r.within} ${r.at}`, r]));
|
|
338
|
+
const checks = (e, within, idsOf, namesOf) => {
|
|
339
|
+
const words = [], replaced = [];
|
|
340
|
+
for (const m of e.members) idsOf(m).forEach((id, i) => {
|
|
341
|
+
const r = replacedAt.get(`${m.key} ${within} ${i}`), nm = namesOf(m)[i];
|
|
342
|
+
const w = r ? `${nm} (in place of ${r.wasLabel})` : nm;
|
|
343
|
+
if (!words.includes(w)) words.push(w);
|
|
344
|
+
if (r) replaced.push(`replaced:${m.key}:${id}`);
|
|
345
|
+
});
|
|
346
|
+
return { text: words.join(', '), names: words, replaced };
|
|
347
|
+
};
|
|
348
|
+
const checkLabel = (c, svg) => c.replaced.length ? tg(c.replaced, shape('replaced', svg)) : svg;
|
|
349
|
+
for (const e of ents) e._chk = { h: checks(e, 'handler', m => m.guardIds, m => m.guards), p: checks(e, 'passed', m => m.passed[0]?.guardIds || [], m => m.passed[0]?.guards || []), a: checks(e, 'after', m => m.beside?.guardIds || [], m => m.beside?.guards || []) };
|
|
333
350
|
// the hull band starts past the names of the routes that enter it, as far as half the wall
|
|
334
351
|
const hx0 = Math.min(col(0.5, 0.5), Math.max(col(0.22, 0.34), ...rowsIn.filter(e => e.wrapper)
|
|
335
352
|
.map(e => labX(e) + tw(e.bundle ? e.bundle.label : e.path, fs) + (countOf(e) ? gap + tw(countOf(e), fs, true, 700) : 0) + 6)));
|
|
@@ -467,7 +484,7 @@ function renderService(map, W, opts) {
|
|
|
467
484
|
// the left column
|
|
468
485
|
const leftEnd = clear(i, x2, e.wrapper ? hx0 - 6 : (e.guards.length ? gx - 9 : xh - rS - 6), true);
|
|
469
486
|
const left = [];
|
|
470
|
-
const guardLines = !e.wrapper && e.guards.length ? wrapped(e.
|
|
487
|
+
const guardLines = !e.wrapper && e.guards.length ? wrapped(e._chk.h.text, gx - 7 - x2, { size: fsS }) : [];
|
|
471
488
|
guardLines.forEach(() => left.push('guard'));
|
|
472
489
|
const guardFork = !e.wrapper && !dense ? forkLines(keyedTypes(e.members.flatMap(m => m.guardIds)), gx + 6 - 3 - x2) : [];
|
|
473
490
|
guardFork.forEach(() => left.push('guardFork'));
|
|
@@ -487,7 +504,7 @@ function renderService(map, W, opts) {
|
|
|
487
504
|
built = builtAt.get(`h:${i}`);
|
|
488
505
|
if (built) right.push('builder');
|
|
489
506
|
} else {
|
|
490
|
-
const gname = p && p.guards.length && !dense ? p.
|
|
507
|
+
const gname = p && p.guards.length && !dense ? e._chk.p.text : '';
|
|
491
508
|
// the check's name over its diamond when it fits there and the function's name leaves room; else right of its stem
|
|
492
509
|
guardBelow = !!gname && (tw(p.label || '', fsS) > xPG - tw(gname, fsS) / 2 - 8 - xSockText || tw(gname, fsS) > (hx1 - xPG) * 2 - 8);
|
|
493
510
|
const gw = gname ? tw(gname, fsS) / 2 + 8 : 0;
|
|
@@ -523,7 +540,7 @@ function renderService(map, W, opts) {
|
|
|
523
540
|
if (b.guards.length) {
|
|
524
541
|
const free = dense || !(p && (p.label || (p.guards.length && !guardBelow)));
|
|
525
542
|
const from = free ? [...lanes.values()].filter(l => i > l.top && l.x < xr).reduce((s, l) => (l.x + l.m > s ? l.x + l.m : s), xSock + rS + 4) : hx1;
|
|
526
|
-
afterGuard = wrapped(
|
|
543
|
+
afterGuard = wrapped(e._chk.a.text, xr - 8 - from, { size: fsS });
|
|
527
544
|
}
|
|
528
545
|
for (let k = 0; k < Math.max(inner.length, xpg, afterWords.length + afterAt - 1); k++) right.push('inner');
|
|
529
546
|
// a wrapper of this row alone: its own station's forks, tables and builder sit under it too
|
|
@@ -578,9 +595,9 @@ function renderService(map, W, opts) {
|
|
|
578
595
|
over.push(tg(ks.flatMap(id => [`fork:${id}`, `forkRow:${id}`]), mark.fork(x, fy, phone ? 0.6 : 0.7) + owned(own, rowB(row, label))));
|
|
579
596
|
};
|
|
580
597
|
// a check's diamond with its name above it, inside the room given
|
|
581
|
-
const guardAt = (keys, names, x, y, room, s = 1, own = 'canvas', row = null) => {
|
|
598
|
+
const guardAt = (keys, names, x, y, room, s = 1, own = 'canvas', row = null, named = svg => svg) => {
|
|
582
599
|
over.push(tg(keys, mark.guard(x, y, s * (phone ? 0.8 : 0.95))));
|
|
583
|
-
if (names.length) out.push(owned(own, rowB(row, L(x, y - (phone ? 10 : 12), names.join(', '), room, { anchor: 'middle', size: fsS, fill: C.guard }))));
|
|
600
|
+
if (names.length) out.push(owned(own, rowB(row, named(L(x, y - (phone ? 10 : 12), names.join(', '), room, { anchor: 'middle', size: fsS, fill: C.guard })))));
|
|
584
601
|
};
|
|
585
602
|
// a builder hung under a mark on line k, its name to the right from x
|
|
586
603
|
const builder = (b, px, y, k, x, end, own = 'canvas', row = null) => {
|
|
@@ -635,7 +652,7 @@ function renderService(map, W, opts) {
|
|
|
635
652
|
if (e.guards.length) {
|
|
636
653
|
guardAt(each(e, m => m.guardIds.map(g => `guard:${m.key}:${g}`)), [], gx, y, 0, 0.85);
|
|
637
654
|
// its name under the path, left of the failure stem; its fork's types under that
|
|
638
|
-
out.push(rowB(e, Tw(gx - 7, at('guard'), guardLines, { anchor: 'end', size: fsS, fill: C.guard }, e.
|
|
655
|
+
out.push(rowB(e, checkLabel(e._chk.h, Tw(gx - 7, at('guard'), guardLines, { anchor: 'end', size: fsS, fill: C.guard }, e._chk.h.text, lead))));
|
|
639
656
|
forkAt(e.members.flatMap(m => m.guardIds), gx + 6, y, left.indexOf('guardFork') + 1, x2, 'canvas', true, e);
|
|
640
657
|
}
|
|
641
658
|
const station = own.length ? mark.station(xh, y, rS, writesIn(own)) : mark.dot(xh, y, 3.2);
|
|
@@ -666,7 +683,8 @@ function renderService(map, W, opts) {
|
|
|
666
683
|
const first = members[0], last = members[members.length - 1];
|
|
667
684
|
const hy0 = Math.min(...ys) - (phone ? 20 : 24) - first._lay.lift, hy1 = Math.max(...ys) + (phone ? 18 : 22) + lead * Math.max(0, last._lay.lines - 1);
|
|
668
685
|
const hull = `hull:${w.id}`;
|
|
669
|
-
under.push(tg([`wrapper:${w.id}`], `<g data-mark="hull"${opener(w.id, `Open ${w.label}`)}><rect x="${f1(hx0)}" y="${f1(hy0)}" width="${f1(hx1 - hx0)}" height="${f1(hy1 - hy0)}" rx="${phone ? 9 : 13}" fill="${C.hull}" stroke="${C.hullEdge}" stroke-width="1.2"${boxAttr(hull)}
|
|
686
|
+
under.push(tg([`wrapper:${w.id}`], `<g data-mark="hull"${opener(w.id, `Open ${w.label}`)}><rect x="${f1(hx0)}" y="${f1(hy0)}" width="${f1(hx1 - hx0)}" height="${f1(hy1 - hy0)}" rx="${phone ? 9 : 13}" fill="${C.hull}" stroke="${C.hullEdge}" stroke-width="1.2"${boxAttr(hull)}/>` +
|
|
687
|
+
hullWall(hx1, ys, phone) + '</g>'));
|
|
670
688
|
{ const { ha, lines } = hullTitle.get(w.id);
|
|
671
689
|
out.push(Tw(ha, hy0 - (phone ? 4 : 5), lines, { size: titleSz, weight: 700, fill: C.hullInk }, w.label, lineGap(titleSz), 'up')); }
|
|
672
690
|
for (const e of members) {
|
|
@@ -700,7 +718,7 @@ function renderService(map, W, opts) {
|
|
|
700
718
|
const rowId = `route:${e.key}`;
|
|
701
719
|
over.push(tg(each(e, m => `socket:${m.key}`), mark.socket(xSock, y, rS * 1.05, writesIn(pr), pr.length > 0 && !writesIn(pr))));
|
|
702
720
|
socketPt.set(e.key, [xSock, y]);
|
|
703
|
-
const gw = p && p.guards.length ? tw(p.
|
|
721
|
+
const gw = p && p.guards.length ? tw(e._chk.p.text, fsS) / 2 + 8 : 0;
|
|
704
722
|
// the handed-in function's name above the path; over a check whose name went below, high enough to clear the diamond
|
|
705
723
|
if (handed.length) out.push(owned(hull, rowB(e, Tw(xSockText, y - (below ? (phone ? 9 : 11) : (phone ? 6 : 7)), handed, { size: fsS, fill: C.hullInk }, p.label, lead, 'up'))));
|
|
706
724
|
reach.forEach((f, k) => out.push(owned(hull, rowB(e, T(rightX, line(y, k + 1), f.s, { size: f.size, fill: C.db, full: reachLabel(pr) })))));
|
|
@@ -709,8 +727,8 @@ function renderService(map, W, opts) {
|
|
|
709
727
|
if (p && p.guards.length) {
|
|
710
728
|
out.push(`<path${anchorAttr(rowId)} d="M${f1(xSock + rS)} ${y} H${f1(xPG)}" stroke="${C.ink2}" stroke-width="${f1(cableW(e))}"/>`);
|
|
711
729
|
const gkeys = each(e, m => (m.passed[0]?.guardIds || []).map(g => `guard:${m.key}:passed:${g}`));
|
|
712
|
-
guardAt(gkeys, dense || below ? [] : p.
|
|
713
|
-
if (below) out.push(owned(hull, rowB(e, Tw(xPG + 6, line(y, 1), xpgGuard, { size: fsS, fill: C.guard }, p.
|
|
730
|
+
guardAt(gkeys, dense || below ? [] : e._chk.p.names, xPG, y, (hx1 - xPG) * 2 - 8, 0.9, hull, e, svg => checkLabel(e._chk.p, svg));
|
|
731
|
+
if (below) out.push(owned(hull, rowB(e, checkLabel(e._chk.p, Tw(xPG + 6, line(y, 1), xpgGuard, { size: fsS, fill: C.guard }, e._chk.p.text, lead)))));
|
|
714
732
|
if (xpgFork.length || dense) forkAt(e.members.flatMap(m => m.passed[0]?.guardIds || []), xPG + 4, y, xpgGuard.length + 1, hx1 - 6, hull, false, e);
|
|
715
733
|
}
|
|
716
734
|
// past the hull: the after band, then the reply bar
|
|
@@ -724,7 +742,7 @@ function renderService(map, W, opts) {
|
|
|
724
742
|
const step = (xr - hx1) / (items.length + 1);
|
|
725
743
|
items.forEach((draw, k) => draw(hx1 + step * (k + 1)));
|
|
726
744
|
// the after band's checks by name, above the path at its right end, and its calls and tables under it
|
|
727
|
-
if (b.guards.length) out.push(rowB(e, Tw(xr - 4, y - (phone ? 9 : 10.5), afterGuard, { anchor: 'end', size: fsS, fill: C.guard, mask: true },
|
|
745
|
+
if (b.guards.length) out.push(rowB(e, checkLabel(e._chk.a, Tw(xr - 4, y - (phone ? 9 : 10.5), afterGuard, { anchor: 'end', size: fsS, fill: C.guard, mask: true }, e._chk.a.text, lead, 'up'))));
|
|
728
746
|
if (items.length && afterWords.length) out.push(rowB(e, Tw(xr - 2, line(y, afterAt), afterWords, { anchor: 'end', size: fsS, ...(afterWide ? { mask: true } : {}) }, [...b.calls, ...(b.effects.length ? [reachLabel(b.effects)] : [])].join(', '), lead)));
|
|
729
747
|
over.push(mark.reply(xr, y, dense ? 0.75 : S));
|
|
730
748
|
}
|
|
@@ -733,7 +751,8 @@ function renderService(map, W, opts) {
|
|
|
733
751
|
// ---- the quiet line (after rows, so forks placed on rows are left out) ----
|
|
734
752
|
let quietBottom = rowsBottom;
|
|
735
753
|
const strays = dbUses.filter(u => u.at.node === 'definition' || (u.at.entry && !rowOf.has(u.at.entry)));
|
|
736
|
-
const Q = opts.noQuiet ? null : layoutQuiet(quietItems(map, forkPlaced, { services: true, dense, phone, strays }), x0 + (phone ? 22 : 40), x1 - (phone ? 8 : 20), quietTop, phone, fsS
|
|
754
|
+
const Q = opts.noQuiet ? null : layoutQuiet(quietItems(map, forkPlaced, { services: true, dense, phone, strays }), x0 + (phone ? 22 : 40), x1 - (phone ? 8 : 20), quietTop, phone, fsS,
|
|
755
|
+
[...lanes.values()].map(l => [l.x - l.m, l.x + l.m]));
|
|
737
756
|
if (Q && Q.items) {
|
|
738
757
|
const body = Q.under.join('') + Q.out.join('') + Q.over.join('');
|
|
739
758
|
out.push(dense ? `<g${opener('quiet', 'Open the parts on no entry path')}>${body}<rect x="${x0 + 4}" y="${f1(quietTop)}" width="${f1(cw - 8)}" height="${f1(Q.bottom - quietTop)}" fill="transparent"/></g>` : body);
|
|
@@ -745,7 +764,12 @@ function renderService(map, W, opts) {
|
|
|
745
764
|
const ghostLink = map.ghostCallers.some(g => g.kind === 'link');
|
|
746
765
|
// the struck importer sits above the rail's left end, clear of HTTP and of the platform under the rail
|
|
747
766
|
const busY = Math.max(Q && Q.items ? quietBottom + (phone ? 14 : 18) : rowsEnd + (phone ? 40 : 46), ghostLink && anyRail ? httpY + (phone ? 104 : 134) : 0);
|
|
748
|
-
const
|
|
767
|
+
const yBot0 = anyRail ? busY + (phone ? 18 : 22) : (Q && Q.items ? quietBottom + (phone ? 10 : 14) : rowsEnd + (phone ? 34 : 38));
|
|
768
|
+
// ---- the program band: under the rail, at the bottom of the wall ----
|
|
769
|
+
const PB = (map.programs || []).length ? programBand(map.programs, x0, x1, yBot0 + (phone ? 10 : 14), phone,
|
|
770
|
+
{ xr, y: key => { const rk = rowOf.get(key); return rk == null ? null : rowY.get(rk); } }) : null;
|
|
771
|
+
if (PB) { under.push(PB.under); out.push(PB.out); over.push(PB.over); }
|
|
772
|
+
const yBot = PB ? PB.bottom + (phone ? 10 : 14) : yBot0;
|
|
749
773
|
const openGap = e => (phone ? 6 : 7) + (e.members.length > 1 ? Math.min(7, e.members.length * 1.1) : 0);
|
|
750
774
|
const wall = wallPathGaps(x0, x1, yTop, yBot, ents.map(e => [rowY.get(e.key), openGap(e)]));
|
|
751
775
|
|
|
@@ -767,10 +791,18 @@ function renderService(map, W, opts) {
|
|
|
767
791
|
|
|
768
792
|
// ---- the rack, outside on the right, in fixed slots ----
|
|
769
793
|
const rackX = x1 + resW / 2 + (phone ? 2 : 4);
|
|
770
|
-
// a
|
|
771
|
-
|
|
772
|
-
const
|
|
773
|
-
const
|
|
794
|
+
// each resource: how many of the routes drawn reach it, and a glyph sized by that share (its area, like a bar's
|
|
795
|
+
// length, says how much it is used), never so small its name leaves it
|
|
796
|
+
const shown = map.entries.filter(e => rowOf.has(e.key));
|
|
797
|
+
const reaches = (e, k) => !!e[k] || (e.beside?.resources || []).includes(k);
|
|
798
|
+
const nReach = Object.fromEntries(RACK.map(k => [k, shown.filter(e => reaches(e, k)).length]));
|
|
799
|
+
const glyphW = phone ? 38 : 64, nameSz = phone ? 9.5 : 12;
|
|
800
|
+
// (as wide as its name needs, as tall as its share)
|
|
801
|
+
const share = k => Math.max(0.5, Math.sqrt(nReach[k] / shown.length));
|
|
802
|
+
const scale = Object.fromEntries(RACK.filter(k => nReach[k]).map(k => [k, { w: Math.min(1, Math.max((tw(k, nameSz, false, 700) + 12) / glyphW, share(k))), h: share(k) }]));
|
|
803
|
+
const countLines = n => tw(`reached by ${n}`, phone ? 9 : 10) <= resW - 4 ? [`reached by ${n}`] : ['reached', `by ${n}`];
|
|
804
|
+
const notes = Object.fromEntries(RACK.filter(k => nReach[k] && map.resources.some(r => r.kind === k)).map(k => [k, { lines: countLines(nReach[k]), kind: 'reachedBy', keys: [`reachedBy:${k}`] }]));
|
|
805
|
+
const R = rack(map, rackX, yTop + (phone ? 6 : 10), phone, resW - 4, notes, scale);
|
|
774
806
|
out.push(R.svg);
|
|
775
807
|
|
|
776
808
|
// ---- supply of the db handle ----
|
|
@@ -832,12 +864,309 @@ function renderService(map, W, opts) {
|
|
|
832
864
|
}
|
|
833
865
|
}
|
|
834
866
|
out.push(T(x0 + (phone ? 10 : 13), busY - (phone ? 6 : 7), 'db', { size: phone ? 9 : 10, weight: 700, fill: C.db, mono: false }));
|
|
867
|
+
// once, by the rail's name: what a ? on it means
|
|
868
|
+
const someMixed = dbUses.filter(onRail).some(u => u.how === 'mixed');
|
|
869
|
+
if (someMixed) {
|
|
870
|
+
const kx = x0 + (phone ? 10 : 13) + tw('db', phone ? 9 : 10, false) + (phone ? 10 : 12), ky = busY - (phone ? 9.5 : 10.5);
|
|
871
|
+
over.push(tg(dbUses.filter(u => onRail(u) && u.how === 'mixed').map(u => `unknownKey:${u.i}`), mark.unknown(kx, ky, phone ? 5.5 : 6) +
|
|
872
|
+
T(kx + (phone ? 8 : 9), busY - (phone ? 6 : 7), UNTRACED_WORD, { size: 9, fill: C.muted, mono: false })));
|
|
873
|
+
}
|
|
835
874
|
}
|
|
836
875
|
|
|
837
876
|
const H = Math.max(yBot, R.bottom) + (phone ? 12 : 20);
|
|
838
877
|
return outerFrame(W, H, wall.fill + under.join('') + shape('wall', wall.line) + out.join('') + over.join(''), { ...opts, frameTop }, map.program, opts.crumb);
|
|
839
878
|
}
|
|
840
879
|
|
|
880
|
+
// =====================================================================
|
|
881
|
+
// PROGRAM BAND
|
|
882
|
+
// =====================================================================
|
|
883
|
+
// A pure program as a container: its step, its package, and one small
|
|
884
|
+
// diamond per decision it takes, so its area grows with its decisions. Its
|
|
885
|
+
// request constructors are openings on its right edge; each is wired to the
|
|
886
|
+
// engine lane that performs it, with a dot per resource the arm reaches. A
|
|
887
|
+
// request no engine performs is a struck opening. Each lane carries a fork
|
|
888
|
+
// with the engine's own decision count (none: a rule-free engine), ends in
|
|
889
|
+
// the resources its arms reach, and loops back into the program bottom
|
|
890
|
+
// through one loop node that names what repeats the step.
|
|
891
|
+
// A line broken at its spaces and separators so each piece fits `width`.
|
|
892
|
+
export const wrapAt = (text, width, size, mono = true) => {
|
|
893
|
+
const lines = [];
|
|
894
|
+
let cur = '';
|
|
895
|
+
for (const w of String(text).split(/(?<=, |· | )/)) {
|
|
896
|
+
if (cur && tw((cur + w).trimEnd(), size, mono) > width) { lines.push(cur.trimEnd()); cur = w; } else cur += w;
|
|
897
|
+
}
|
|
898
|
+
if (cur) lines.push(cur.trimEnd());
|
|
899
|
+
return lines;
|
|
900
|
+
};
|
|
901
|
+
const RES_WORD = { db: 'db', model: 'model', net: 'net', compile: 'compile', run: 'run', io: 'io' };
|
|
902
|
+
// A lane is named by its package, and by its own definition where another lane shares that package.
|
|
903
|
+
export const laneName = (program, engine) =>
|
|
904
|
+
(program.engines.some(x => x !== engine && x.pkg === engine.pkg) ? `${engine.pkgLabel}.${engine.label}` : engine.pkgLabel);
|
|
905
|
+
|
|
906
|
+
export function programBand(programs, x0, x1, y0, phone, rows = null) {
|
|
907
|
+
const under = [], out = [], over = [];
|
|
908
|
+
// a label that fits the room it is given: shrunk a little, then cut, with
|
|
909
|
+
// the whole text as its title when it is cut
|
|
910
|
+
const fitT = (x, y, s, width, o = {}) => {
|
|
911
|
+
const t = fitText(s, o.size || 10, width, o.mono !== false);
|
|
912
|
+
const svg = T(x, y, t.s, { ...o, size: t.size });
|
|
913
|
+
return t.s === String(s) ? svg : `<g><title>${esc(s)}</title>${svg}</g>`;
|
|
914
|
+
};
|
|
915
|
+
const cw = x1 - x0, fs = phone ? 9.5 : 11, fsS = phone ? 9 : 10;
|
|
916
|
+
const bw = phone ? 12 : 16, lead = phone ? 11 : 12.5;
|
|
917
|
+
let y = y0, wireN = 0;
|
|
918
|
+
for (const p of programs) {
|
|
919
|
+
// the lanes need room for their widest foot pill each; the program gives way
|
|
920
|
+
const pillW = rs => Math.max(tw(RES_WORD[rs], 9, false) * 1.12 + 12, phone ? 26 : 32);
|
|
921
|
+
const needSp = Math.max(0, ...p.engines.flatMap(e => [...e.resources, ...e.also].map(pillW))) + 6;
|
|
922
|
+
const lanesW = needSp * p.engines.length + (phone ? 24 + 26 : 70 + 56);
|
|
923
|
+
// at phone width close lanes stagger their pills, but each lane still needs room for its decision count beside a bar no narrower than 3
|
|
924
|
+
const countSp = 12 + Math.max(0, ...p.engines.map(e => tw(String(e.decisions), fsS, true, 700)));
|
|
925
|
+
const px0 = x0 + (phone ? 8 : 24), px1 = phone ? Math.max(px0 + cw * 0.4, Math.min(px0 + cw * 0.56, x1 - countSp * p.engines.length - 24 - 26))
|
|
926
|
+
: Math.max(px0 + cw * 0.4, Math.min(px0 + cw * 0.5, x1 - lanesW));
|
|
927
|
+
// one scale for every decision bar of this program and its engines
|
|
928
|
+
const maxDec = Math.max(1, p.decisions, ...p.engines.map(e => e.decisions));
|
|
929
|
+
const per = Math.min(phone ? 2 : 2.4, (phone ? 170 : 200) / maxDec);
|
|
930
|
+
const barH = n => Math.max(n > 0 ? 2 : 0, n * per);
|
|
931
|
+
const starts = p.starters.filter(s => !s.advances);
|
|
932
|
+
// the routes that only take a step, as many lines as they need (at most three, then a count with every path in its title)
|
|
933
|
+
const startLines = (() => {
|
|
934
|
+
if (!starts.length) return [];
|
|
935
|
+
const room = (px1 - px0) - (phone ? 18 : 22), groups = [[]];
|
|
936
|
+
// the words lead the first line, or stand on their own when the first path would not fit after them
|
|
937
|
+
const lead = 'one step per call of', alone = tw(`${lead} ${starts[0].path}`, fsS, false) > room;
|
|
938
|
+
const text = (g, j) => `${j || alone ? '' : `${lead} `}${g.join(', ')}`;
|
|
939
|
+
for (const st of starts) {
|
|
940
|
+
const g = groups[groups.length - 1];
|
|
941
|
+
if (!g.length || tw(text([...g, st.path], groups.length - 1) + ',', fsS, false) <= room) g.push(st.path);
|
|
942
|
+
else groups.push([st.path]);
|
|
943
|
+
}
|
|
944
|
+
const lines = [...(alone ? [lead] : []), ...groups.map((g, j) => text(g, j) + (j < groups.length - 1 ? ',' : ''))];
|
|
945
|
+
if (lines.length <= 3) return lines;
|
|
946
|
+
const shown = groups[0].length + (alone ? 0 : groups[1].length);
|
|
947
|
+
return [lines[0], lines[1], `and ${starts.length - shown} more`];
|
|
948
|
+
})();
|
|
949
|
+
// the counts on one line, or two when the program box is narrow
|
|
950
|
+
// its size in words a reader needs no key for: the package's own definitions, and how many more it uses from elsewhere
|
|
951
|
+
const own = `${p.definitions} definitions in ${p.pkgLabel}`;
|
|
952
|
+
// which packages they come from, the most used first: three named, the rest counted
|
|
953
|
+
// (each name stays with its count when the line wraps)
|
|
954
|
+
const from = p.elsewhere.slice(0, 3).map(x => `${x.label}\u00a0${x.n}`).join(', ') + (p.elsewhere.length > 3 ? `, ${p.elsewhere.length - 3}\u00a0more` : '');
|
|
955
|
+
const more = p.usesElsewhere ? `uses ${p.usesElsewhere} more from ${p.elsewhere.length} package${p.elsewhere.length === 1 ? '' : 's'}: ${from}` : '';
|
|
956
|
+
const defsTxt = more ? `${own} · ${more}` : own;
|
|
957
|
+
const headRoom = cw * (phone ? 0.56 : 0.5) - (phone ? 26 : 34);
|
|
958
|
+
const defsLines = tw(defsTxt, fsS, false) <= headRoom ? [defsTxt] : [own, more].filter(Boolean).flatMap(l => wrapAt(l, headRoom, fsS, false));
|
|
959
|
+
const headH = (phone ? 40 : 44) + (defsLines.length - 1) * lead + startLines.length * lead;
|
|
960
|
+
const vMax = 8;
|
|
961
|
+
const variantRows = q => q.variants.reduce((n, v) => n + Math.min(v.constructors.length, vMax + 1), 0);
|
|
962
|
+
const osOf = q => (phone ? 30 : 34) + variantRows(q) * lead;
|
|
963
|
+
const labW = Math.min((px1 - px0) * (phone ? 0.58 : 0.42), Math.max(...p.requests.map(q => Math.max(tw(q.constructor, fs), tw(q.fields.join(', '), fsS),
|
|
964
|
+
...q.variants.flatMap(v => v.constructors.map(c => tw((n => n ? `${c} → ${n}` : c)(v.namings && v.namings[0] && (v.namings[0].names.find(x => x.constructor === c) || {}).name), fsS) + 8))))) + 4);
|
|
965
|
+
const openH = p.requests.reduce((h, q) => h + osOf(q), 0);
|
|
966
|
+
const bodyH = Math.max(openH, barH(p.decisions) + (phone ? 34 : 30));
|
|
967
|
+
const top = y + (phone ? 18 : 20), bot = top + headH + bodyH + (phone ? 10 : 14);
|
|
968
|
+
const openY = []; { let yy = top + headH; for (const q of p.requests) { openY.push(yy + (phone ? 15 : 17)); yy += osOf(q); } }
|
|
969
|
+
const gapOf = new Set(p.gaps.map(g => g.constructor));
|
|
970
|
+
const og = phone ? 6 : 7, r = phone ? 9 : 12;
|
|
971
|
+
let edge = `M${f1(px1 - r)} ${f1(top)} Q${f1(px1)} ${f1(top)} ${f1(px1)} ${f1(top + r)}`;
|
|
972
|
+
for (const oy of openY) edge += ` V${f1(oy - og)} M${f1(px1)} ${f1(oy + og)}`;
|
|
973
|
+
edge += ` V${f1(bot - r)} Q${f1(px1)} ${f1(bot)} ${f1(px1 - r)} ${f1(bot)} H${f1(px0 + r)} Q${f1(px0)} ${f1(bot)} ${f1(px0)} ${f1(bot - r)} V${f1(top + r)} Q${f1(px0)} ${f1(top)} ${f1(px0 + r)} ${f1(top)} H${f1(px1 - r)}`;
|
|
974
|
+
// routes that only replay it to read where it is: named on hover, never drawn as starting it
|
|
975
|
+
const readBy = p.replays.length ? `<title>${esc(`read, not started, by ${p.replays.map(r => r.path).join(', ')}: they replay it and hand its answer back`)}</title>` : '';
|
|
976
|
+
under.push(tg([`program:${p.id}`, ...p.replays.map(r => `replay:${p.id}:${r.entry}`)], shape('program', `${readBy}<rect x="${f1(px0)}" y="${f1(top)}" width="${f1(px1 - px0)}" height="${f1(bot - top)}" rx="${r}" fill="${C.programSoft}"/>` +
|
|
977
|
+
`<path d="${edge}" fill="none" stroke="${C.program}" stroke-width="2"/>`)));
|
|
978
|
+
const ix = px0 + (phone ? 8 : 12);
|
|
979
|
+
out.push(tg([`program:${p.id}`], fitT(px0 + 2, top - (phone ? 5 : 6), p.pkg, px1 - px0 - 4, { size: phone ? 9.5 : 11, weight: 700, fill: C.program, mono: false, haloColor: C.paper })));
|
|
980
|
+
// effects in the program's own package: marked; none: a struck ghost
|
|
981
|
+
const markW = phone ? 50 : 64, mx = px1 - (phone ? 8 : 12) - markW / 2, my = top + (phone ? 12 : 13);
|
|
982
|
+
if (!p.beside.length) out.push(tg([`pure:${p.id}`], shape('struck', `<title>${esc(`no effects in ${p.pkg}`)}</title>` +
|
|
983
|
+
`<circle cx="${f1(mx - markW / 2 + 6)}" cy="${f1(my - 3)}" r="4.5" fill="none" stroke="${C.faint}" stroke-width="1.2" stroke-dasharray="2.4 2"/>` +
|
|
984
|
+
`<path d="M${f1(mx - markW / 2 + 0.5)} ${f1(my + 2)} L${f1(mx - markW / 2 + 11.5)} ${f1(my - 8)}" stroke="${C.faint}" stroke-width="1.2"/>` +
|
|
985
|
+
T(mx - markW / 2 + 14, my, 'effects', { size: 9, fill: C.muted, mono: false, halo: false }))));
|
|
986
|
+
else out.push(tg(p.beside.map(b => `beside:${p.id}:${b.id}`), shape('programEffect', `<title>${esc(`effects in ${p.pkg}: ${p.beside.map(b => `${b.label} (${(b.resources.length ? b.resources : ['io']).join(', ')})`).join('; ')}`)}</title>` +
|
|
987
|
+
`<circle cx="${f1(mx - markW / 2 + 6)}" cy="${f1(my - 3)}" r="4.5" fill="${C.guard}"/>` +
|
|
988
|
+
T(mx - markW / 2 + 14, my, `${p.beside.length} effect${p.beside.length === 1 ? '' : 's'}`, { size: 9, weight: 700, fill: C.guard, mono: false, halo: false }))));
|
|
989
|
+
out.push(fitT(ix, top + (phone ? 15 : 17), `${p.label} → ${p.result}`, px1 - ix - markW - (phone ? 10 : 16), { size: fs, weight: 700, fill: C.ink }));
|
|
990
|
+
// definitions: the package's own, as the layer stack counts them, and what the program reaches
|
|
991
|
+
const defsTitle = p.elsewhere.length ? `<title>${esc(`${p.usesElsewhere} definition${p.usesElsewhere === 1 ? '' : 's'} from other packages: ${p.elsewhere.map(x => `${x.pkg} ${x.n}`).join(', ')}`)}</title>` : '';
|
|
992
|
+
out.push(tg([`definitions:${p.id}`, ...p.elsewhere.map(x => `elsewhere:${p.id}:${x.pkg}`)], defsTitle + defsLines.map((l, j) => fitT(ix, top + (phone ? 29 : 33) + j * lead, l, px1 - ix - 10, { size: fsS, fill: C.muted, mono: false })).join('')));
|
|
993
|
+
if (starts.length) out.push(tg(starts.map(s => `starter:${p.id}:${s.entry}`), `<title>${esc(`${starts.map(s => s.path).join(', ')}: each call takes one step of ${p.label} and acts on its answer; the rounds are run by the engines on the right`)}</title>` +
|
|
994
|
+
startLines.map((l, j) => fitT(ix, top + (phone ? 29 : 33) + (defsLines.length + j) * lead, l, px1 - ix - 10, { size: fsS, fill: C.ink2, mono: false })).join('')));
|
|
995
|
+
// the program's decisions: a bar on the scale the engines' bars share, and its count
|
|
996
|
+
const by0 = top + headH + 4, bh = barH(p.decisions), dx = ix + bw + 6, dRoom = px1 - labW - (phone ? 14 : 20) - dx;
|
|
997
|
+
over.push(tg([`decisions:${p.id}`], shape('decision', `<title>${esc(`${p.decisions} decisions`)}</title><rect x="${f1(ix)}" y="${f1(by0)}" width="${bw}" height="${f1(bh)}" fill="${C.program}" opacity="0.8"/>`) +
|
|
998
|
+
T(dx, by0 + 10, String(p.decisions), { size: phone ? 11 : 13, weight: 700, fill: C.program }) +
|
|
999
|
+
(dRoom >= tw('decisions', fsS, false) ? T(dx, by0 + 10 + lead, 'decisions', { size: fsS, fill: C.muted, mono: false }) : '')));
|
|
1000
|
+
// lanes on the resource side
|
|
1001
|
+
const k = p.engines.length;
|
|
1002
|
+
const lx0 = px1 + (phone ? 24 : 70), lx1 = x1 - (phone ? 26 : 56);
|
|
1003
|
+
const laneX = p.engines.map((_, i) => lx0 + (lx1 - lx0) * (i + 0.5) / Math.max(1, k));
|
|
1004
|
+
const spacing = (lx1 - lx0) / Math.max(1, k);
|
|
1005
|
+
// an engine's decision bar, and its count beside it, end short of the next lane (the bar no narrower than 3)
|
|
1006
|
+
const forkW = Math.max(3, Math.min(bw, spacing - 9 - Math.max(...p.engines.map(e => tw(String(e.decisions), fsS, true, 700)))));
|
|
1007
|
+
// what an engine does, from its type: what it takes of the program's own and what its effect yields; its code name on hover
|
|
1008
|
+
// (the arrow stays with what it gives, so a wrapped line never ends on it)
|
|
1009
|
+
const sigOf = e => `${e.type.plainTakes.join(', ') || '()'} →\u00a0${e.type.yields}`;
|
|
1010
|
+
const nameOf = e => laneName(p, e);
|
|
1011
|
+
const codeOf = e => `${e.label}: ${e.type.takes.join(', ')} → ${e.type.returns}`;
|
|
1012
|
+
// above each lane: its package, then what it does on at most two lines, when every lane's words fit there
|
|
1013
|
+
const sigLines = e => wrapAt(sigOf(e), spacing - 6, fsS);
|
|
1014
|
+
const wide = p.engines.every(x => tw(nameOf(x), fsS) <= spacing - 6 && sigLines(x).length <= 2 && sigLines(x).every(l => tw(l, fsS) <= spacing - 6));
|
|
1015
|
+
const laneTop = top + headH - (phone ? 6 : 8);
|
|
1016
|
+
const resH = phone ? 15 : 18, resGap = phone ? 4 : 5;
|
|
1017
|
+
const laneBot = Math.max(bot, openY.length ? openY[openY.length - 1] + 14 : bot, ...p.engines.map(e => laneTop + barH(e.decisions) + 8));
|
|
1018
|
+
const footOf = e => e.resources.length * (resH + resGap) + (e.also.length ? lead + e.also.length * (resH + resGap) : 0);
|
|
1019
|
+
// lanes closer than a pill: every other lane's pills start below its neighbours'
|
|
1020
|
+
const cramped = spacing < needSp;
|
|
1021
|
+
const evenFoot = Math.max(0, ...p.engines.filter((_, i) => i % 2 === 0).map(footOf));
|
|
1022
|
+
const footY0 = i => laneBot + 6 + (cramped && i % 2 ? evenFoot + resGap : 0);
|
|
1023
|
+
const footBot = Math.max(laneBot + 6, ...p.engines.map((e, i) => footY0(i) + footOf(e)));
|
|
1024
|
+
// each loop's caption, one thought per line: which engine (when the lanes
|
|
1025
|
+
// carry numbers), how it repeats and what it replies, and what runs it
|
|
1026
|
+
const capsOf = (e, i) => {
|
|
1027
|
+
// how the rounds come round within one call, and which calls run them
|
|
1028
|
+
const how = e.loop.kind === 'repeat' ? `${e.loop.label} repeats the round` : e.loop.kind === 'recursion' ? `${e.loop.label} calls itself, a round each time` : 'rounds written out one after another';
|
|
1029
|
+
const reply = `replies ${e.replies.map(rs => RES_WORD[rs]).join(', ')}`;
|
|
1030
|
+
const lines = [];
|
|
1031
|
+
// a name too long for its room breaks after its package, so the definition that tells lanes apart stays whole
|
|
1032
|
+
if (!wide && nameOf(e) !== e.pkgLabel && tw(`${i + 1} ${nameOf(e)}:`, fsS) > capRoom(i)) lines.push([`${i + 1} ${e.pkgLabel}.`], [`${e.label}: ${sigOf(e)}`]);
|
|
1033
|
+
else if (!wide) lines.push([`${i + 1} ${nameOf(e)}: ${sigOf(e)}`]);
|
|
1034
|
+
lines.push(phone ? [how] : [`${how} · ${reply}`]);
|
|
1035
|
+
if (phone && e.replies.length) lines.push([reply]);
|
|
1036
|
+
if (e.starters.length) lines.push([`each call of ${e.starters.map(k => k.replace(/^route:/, '')).join(', ')} runs ${e.loop.kind === 'sequence' ? 'them' : 'it'}`, e.starters.map(k => `starter:${p.id}:${k}`)]);
|
|
1037
|
+
return lines;
|
|
1038
|
+
};
|
|
1039
|
+
// a caption never reaches the next lane, whose loop comes down there: it wraps at its separators instead
|
|
1040
|
+
// later lanes come back into the program further left, so no loop crosses another's caption
|
|
1041
|
+
const txOf = i => px0 + (px1 - px0) * (0.3 + 0.5 * (k - i) / (k + 1));
|
|
1042
|
+
// it may start just right of the next lane's way back up
|
|
1043
|
+
const capLeft = i => i < k - 1 ? txOf(i + 1) + 8 : txOf(i);
|
|
1044
|
+
const capRoom = i => (i < k - 1 ? laneX[i + 1] - 6 : x1 - 8) - capLeft(i);
|
|
1045
|
+
const caps = p.engines.map((e, i) => capsOf(e, i).flatMap(([t, keys]) => wrapAt(t, capRoom(i), fsS).map(l => [l, keys])));
|
|
1046
|
+
const loopGap = phone ? 26 : 28;
|
|
1047
|
+
const loopY = []; { let yy = footBot + (phone ? 12 : 14); caps.forEach(c => { loopY.push(yy); yy += loopGap + c.length * lead; }); }
|
|
1048
|
+
p.requests.forEach((q, i) => {
|
|
1049
|
+
const oy = openY[i];
|
|
1050
|
+
const gap = gapOf.has(q.constructor);
|
|
1051
|
+
const col = gap ? C.faint : C.program;
|
|
1052
|
+
const lab = fitText(q.constructor, fs, labW), lw0 = Math.min(labW, tw(lab.s, lab.size));
|
|
1053
|
+
const lx = px1 - (phone ? 8 : 10);
|
|
1054
|
+
out.push(tg([gap ? `gap:${p.id}:${q.constructor}` : `request:${p.id}:${q.constructor}`],
|
|
1055
|
+
shape(gap ? 'gap' : 'request', `<path d="M${f1(px1 - 5)} ${f1(oy - og)} H${f1(px1 + 5)} M${f1(px1 - 5)} ${f1(oy + og)} H${f1(px1 + 5)}" stroke="${col}" stroke-width="2" stroke-linecap="round"/>` +
|
|
1056
|
+
// a struck request's name and the lines that strike it are one label
|
|
1057
|
+
(gap ? `<g data-label="${esc(`gap:${p.id}:${q.constructor}`)}">` : '') + fitT(lx, oy - (q.fields.length ? 1 : -3.5), q.constructor, labW, { anchor: 'end', size: fs, weight: 600, fill: gap ? C.muted : C.ink }) + (gap ? '</g>' : '') +
|
|
1058
|
+
(q.fields.length ? fitT(lx, oy + fsS + 0.5, q.fields.join(', '), labW, { anchor: 'end', size: fsS, fill: C.muted }) : '') +
|
|
1059
|
+
(gap ? `<g data-label="${esc(`gap:${p.id}:${q.constructor}`)}"><path d="M${f1(px1 - 9)} ${f1(oy + 10)} L${f1(px1 + 9)} ${f1(oy - 10)}" stroke="${C.guard}" stroke-width="1.6"/>` +
|
|
1060
|
+
`<path d="M${f1(px1 - 10 - lw0)} ${f1(oy - (q.fields.length ? 4.5 : -0.5))} H${f1(px1 - (phone ? 7 : 9))}" stroke="${C.muted}" stroke-width="1"/></g>` : ''))));
|
|
1061
|
+
// the kinds of this request, from the sum type its payload carries: sub-openings under it
|
|
1062
|
+
let vy = oy + fsS + 0.5 + lead;
|
|
1063
|
+
for (const v of q.variants) {
|
|
1064
|
+
const shown = v.constructors.length > vMax + 1 ? v.constructors.slice(0, vMax) : v.constructors;
|
|
1065
|
+
const nm = v.namings && v.namings[0];
|
|
1066
|
+
const wordOf = (n, c) => (n.names.find(x => x.constructor === c) || {}).name;
|
|
1067
|
+
// one form for every kind of one type: kind and word when they all fit, else the words
|
|
1068
|
+
// a kind's row is its own, so it may reach further left than the request's name, up to the decision bar's words
|
|
1069
|
+
const varW = Math.max(labW - 8, px1 - (phone ? 10 : 12) - (ix + bw + 6 + tw('decisions', fsS, false) + 10));
|
|
1070
|
+
const pairs = !!nm && shown.every(c => !wordOf(nm, c) || tw(`${c} → ${wordOf(nm, c)}`, fsS) <= varW);
|
|
1071
|
+
for (const c of shown) {
|
|
1072
|
+
// the kind, and the program's own word for it when a match gives one: both when they fit, else the word
|
|
1073
|
+
const word = nm && wordOf(nm, c);
|
|
1074
|
+
const both = word ? `${c} → ${word}` : c;
|
|
1075
|
+
const text = !word || pairs ? both : word;
|
|
1076
|
+
const title = word ? `<title>${esc(`${c} → ${word}, by ${nm.label}${v.namings.length > 1 ? `; also ${v.namings.slice(1).map(n => `${wordOf(n, c)} by ${n.label}`).join(', ')}` : ''}`)}</title>` : '';
|
|
1077
|
+
out.push(tg([`variant:${p.id}:${q.constructor}:${c}`, ...(word ? [`variantName:${p.id}:${q.constructor}:${c}`] : [])],
|
|
1078
|
+
shape('variant', `${title}<path d="M${f1(px1 - 3)} ${f1(vy - 3.5)} H${f1(px1 + 3)}" stroke="${col}" stroke-width="1.6" stroke-linecap="round"/>` +
|
|
1079
|
+
fitT(lx, vy, text, pairs ? varW : labW - 8, { anchor: 'end', size: fsS, fill: word ? C.ink : C.ink2, weight: word ? 600 : 400 }))));
|
|
1080
|
+
vy += lead;
|
|
1081
|
+
}
|
|
1082
|
+
if (shown.length < v.constructors.length) {
|
|
1083
|
+
const rest = v.constructors.slice(shown.length);
|
|
1084
|
+
out.push(tg(rest.map(c => `variant:${p.id}:${q.constructor}:${c}`), shape('variant', `<title>${esc(rest.join(', '))}</title>` + T(lx, vy, `+${rest.length} more`, { anchor: 'end', size: fsS, fill: C.muted }))));
|
|
1085
|
+
vy += lead;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
for (const h of q.handledBy) {
|
|
1089
|
+
const li = p.engines.findIndex(e => e.id === h.engine);
|
|
1090
|
+
const wx = laneX[li];
|
|
1091
|
+
// what this arm reaches, said in words over the wire's end where they fit (clear of any lane it passes), else a dot each
|
|
1092
|
+
const words = h.resources.map(rs => RES_WORD[rs]), wgap = 5;
|
|
1093
|
+
const wordsW = words.reduce((w, t) => w + tw(t, 9, false), 0) + wgap * Math.max(0, words.length - 1);
|
|
1094
|
+
const left = Math.max(px1 + 6, ...laneX.filter(x => x < wx).map(x => x + 2.5 + bw + 6));
|
|
1095
|
+
const said = words.length && wordsW <= wx - 6 - left;
|
|
1096
|
+
let wxR = wx - 6;
|
|
1097
|
+
const marks = said ? h.resources.slice().reverse().map(rs => { const t = RES_WORD[rs], w = tw(t, 9, false), x = wxR; wxR -= w + wgap;
|
|
1098
|
+
return T(x, oy - 4, t, { anchor: 'end', size: 9, weight: 700, fill: C[rs] || C.io, mono: false }); }).reverse().join('')
|
|
1099
|
+
: h.resources.map((rs, j) => `<circle cx="${f1(wx - (phone ? 7 : 10) - j * (phone ? 6 : 8))}" cy="${f1(oy)}" r="${phone ? 2.6 : 3.2}" fill="${C[rs] || C.io}" stroke="${C.paper}" stroke-width="1"/>`).join('');
|
|
1100
|
+
const engineLabel = p.engines[li].label;
|
|
1101
|
+
under.push(tg([`wire:${p.id}:${q.constructor}:${h.engine}`], shape('wire', `<title>${esc(`${q.constructor} → ${engineLabel}${words.length ? `: reaches ${words.join(', ')}` : ''}`)}</title>` +
|
|
1102
|
+
`<path d="M${f1(px1 + 1)} ${f1(oy)} H${f1(wx)}" stroke="${C.ink2}" stroke-width="1.5"/>`)));
|
|
1103
|
+
if (marks) over.push(tg([`wire:${p.id}:${q.constructor}:${h.engine}`], marks));
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
// the routes that start the program: a wire from their row into its top (wide pictures)
|
|
1107
|
+
const toRow = key => rows && rows.y(key);
|
|
1108
|
+
if (!phone && rows) starts.forEach(s => {
|
|
1109
|
+
const ry = toRow(s.entry);
|
|
1110
|
+
if (ry == null) return;
|
|
1111
|
+
const gx = rows.xr + 10 + (wireN++ % 5) * 5, ex = px1 - 24 - (wireN % 5) * 10, ty = top - (phone ? 16 : 20);
|
|
1112
|
+
under.push(tg([`starter:${p.id}:${s.entry}`], shape('starter', `<path d="M${f1(rows.xr)} ${f1(ry)} H${f1(gx)} V${f1(ty - 6)} H${f1(ex)} V${f1(top - 3)}" fill="none" stroke="${C.program}" stroke-width="1.3" stroke-dasharray="5 3"/>` +
|
|
1113
|
+
`<path d="M${f1(ex - 4)} ${f1(top - 9)} L${f1(ex)} ${f1(top - 2)} L${f1(ex + 4)} ${f1(top - 9)}" fill="none" stroke="${C.program}" stroke-width="1.4"/>`)));
|
|
1114
|
+
});
|
|
1115
|
+
p.engines.forEach((e, i) => {
|
|
1116
|
+
const lx = laneX[i];
|
|
1117
|
+
const title = `<title>${esc(`${e.id}${e.entries.length ? ` · ${e.entries.map(k => k.replace(/^route:/, '')).join(', ')}` : ''}`)}</title>`;
|
|
1118
|
+
under.push(tg([`lane:${e.id}`], shape('lane', `${title}<rect x="${f1(lx - 1.5)}" y="${f1(laneTop)}" width="3" height="${f1(laneBot - laneTop)}" rx="1.5" fill="${C.ink2}"/>`)));
|
|
1119
|
+
// its package and name above the lane, or a number when the lanes are close
|
|
1120
|
+
if (wide) {
|
|
1121
|
+
const sl = sigLines(e), ly0 = laneTop - (phone ? 5 : 6) - (sl.length - 1) * lead;
|
|
1122
|
+
out.push(tg([`lane:${e.id}`, `engineType:${e.id}`], `<g><title>${esc(codeOf(e))}</title>` + T(lx, ly0 - lead, nameOf(e), { anchor: 'middle', size: fsS, fill: C.muted }) +
|
|
1123
|
+
sl.map((l, j) => T(lx, ly0 + j * lead, l, { anchor: 'middle', size: fsS, weight: 700, fill: C.ink })).join('') + '</g>'));
|
|
1124
|
+
} else out.push(tg([`lane:${e.id}`, `engineType:${e.id}`], `<g><title>${esc(codeOf(e))}</title>` + T(lx, laneTop - (phone ? 5 : 6), String(i + 1), { anchor: 'middle', size: fsS, weight: 700, fill: C.ink }) + '</g>'));
|
|
1125
|
+
// the engine's own decisions, on the program's scale; its bar gives up width so that the count fits before the next lane
|
|
1126
|
+
const eh = barH(e.decisions);
|
|
1127
|
+
over.push(tg([`laneFork:${e.id}`], shape('laneFork', `<title>${esc(`${e.decisions} decisions of its own`)}</title>` +
|
|
1128
|
+
(eh ? `<rect x="${f1(lx + 2.5)}" y="${f1(laneTop)}" width="${f1(forkW)}" height="${f1(eh)}" fill="${C.ink2}" opacity="0.55"/>` : '') +
|
|
1129
|
+
T(lx + 2.5 + forkW + 3, laneTop + 10, String(e.decisions), { size: fsS, weight: 700, fill: C.ink2 }))));
|
|
1130
|
+
// the routes that run it are named under its loop ("run by …"), not wired: one loop per lane is the only line back
|
|
1131
|
+
// the resources its arms reach, at its foot; then the ones it reaches for no request
|
|
1132
|
+
let yy = footY0(i);
|
|
1133
|
+
if (yy > laneBot + 6) under.push(`<path d="M${f1(lx)} ${f1(laneBot)} V${f1(yy)}" stroke="${C.ink2}" stroke-width="1.2"/>`);
|
|
1134
|
+
const pill = (rs, dashed, key) => {
|
|
1135
|
+
const w = pillW(rs);
|
|
1136
|
+
under.push(`<path d="M${f1(lx)} ${f1(yy - (dashed ? 2 : 6))} V${f1(yy + resH / 2)}" stroke="${C.ink2}" stroke-width="1.2"${dashed ? ' stroke-dasharray="2 2"' : ''}/>`);
|
|
1137
|
+
over.push(tg([key], shape('laneResource', `<rect x="${f1(lx - w / 2)}" y="${f1(yy)}" width="${f1(w)}" height="${resH}" rx="${f1(resH / 2)}" fill="${dashed ? C.paper : C[rs + 'Soft'] || C.ioSoft}" stroke="${C[rs] || C.io}" stroke-width="1.4"${dashed ? ' stroke-dasharray="3 2"' : ''}/>` +
|
|
1138
|
+
T(lx, yy + resH / 2 + 3.2, RES_WORD[rs], { anchor: 'middle', size: 9, weight: 700, fill: C[rs] || C.io, mono: false, halo: false }))));
|
|
1139
|
+
yy += resH + resGap;
|
|
1140
|
+
};
|
|
1141
|
+
e.resources.forEach(rs => pill(rs, false, `laneResource:${e.id}:${rs}`));
|
|
1142
|
+
if (e.also.length) {
|
|
1143
|
+
out.push(tg(e.also.map(rs => `laneAlso:${e.id}:${rs}`), T(lx, yy + 8, 'also', { anchor: 'middle', size: 9, fill: C.muted, mono: false })));
|
|
1144
|
+
yy += lead;
|
|
1145
|
+
e.also.forEach(rs => pill(rs, true, `laneAlso:${e.id}:${rs}`));
|
|
1146
|
+
}
|
|
1147
|
+
// the reply: one loop from the foot back into the program, one loop node on it
|
|
1148
|
+
const ly = loopY[i];
|
|
1149
|
+
const tx = txOf(i);
|
|
1150
|
+
const d = `M${f1(lx)} ${f1(yy - resGap)} V${f1(ly - 8)} Q${f1(lx)} ${f1(ly)} ${f1(lx - 8)} ${f1(ly)} H${f1(tx + 8)} Q${f1(tx)} ${f1(ly)} ${f1(tx)} ${f1(ly - 8)} V${f1(bot + 6)}`;
|
|
1151
|
+
const arrow = `<path d="M${f1(tx - 4)} ${f1(bot + 8)} L${f1(tx)} ${f1(bot + 1.5)} L${f1(tx + 4)} ${f1(bot + 8)}" fill="none" stroke="${C.program}" stroke-width="1.6" stroke-linejoin="round"/>`;
|
|
1152
|
+
const nx = (tx + lx) / 2, nr = phone ? 5 : 6;
|
|
1153
|
+
const node = `<circle cx="${f1(nx)}" cy="${f1(ly)}" r="${nr + 3}" fill="${C.paper}"/>` + mark.loop(nx, ly, nr, C.program);
|
|
1154
|
+
// centred under the loop when it fits there, else from the loop's left end
|
|
1155
|
+
// every line of one caption set the same way: centred when they all fit there
|
|
1156
|
+
const centred = caps[i].every(([t]) => { const m = fitText(t, fsS, lx - tx - 8); return m.s === t && m.size === fsS; });
|
|
1157
|
+
const caption = (cy, text) => centred ? T(nx, cy, text, { anchor: 'middle', size: fsS, fill: C.program })
|
|
1158
|
+
: fitT(capLeft(i), cy, text, capRoom(i), { size: fsS, fill: C.program });
|
|
1159
|
+
const cy0 = ly + nr + (phone ? 11 : 13);
|
|
1160
|
+
const own = caps[i].filter(l => !l[1]), by = caps[i].filter(l => l[1]);
|
|
1161
|
+
over.push(tg([`reply:${e.id}`, `loop:${e.id}`], shape('replyLoop', `<title>${esc(codeOf(e))}</title><path d="${d}" fill="none" stroke="${C.program}" stroke-width="1.6"/>${arrow}${node}` +
|
|
1162
|
+
own.map((l, j) => caption(cy0 + j * lead, l[0])).join(''))) +
|
|
1163
|
+
(by.length ? tg(by[0][1], by.map((l, j) => caption(cy0 + (own.length + j) * lead, l[0])).join('')) : ''));
|
|
1164
|
+
});
|
|
1165
|
+
y = (k ? loopY[k - 1] + loopGap + caps[k - 1].length * lead : footBot + (phone ? 12 : 14)) + (phone ? 6 : 10);
|
|
1166
|
+
}
|
|
1167
|
+
return { under: under.join(''), out: out.join(''), over: over.join(''), bottom: y };
|
|
1168
|
+
}
|
|
1169
|
+
|
|
841
1170
|
const ring = (x, y, r, color) => `<circle cx="${f1(x)}" cy="${f1(y)}" r="${f1(r)}" fill="${C.paper}" stroke="${color}" stroke-width="1.8"/>`;
|
|
842
1171
|
// one caller's bracket: a line from the caller to a spine beside the wall, a tick to each opening
|
|
843
1172
|
function bracket(sx, sy, bx, y0, y1, ys) {
|
|
@@ -898,6 +1227,10 @@ function wallPathGaps(x0, x1, yTop, yBot, openings, r = 14) {
|
|
|
898
1227
|
// line; each package is a stretch starting with a tick and its name; the
|
|
899
1228
|
// parts' labels alternate above and below the line so it never wraps at
|
|
900
1229
|
// phone width. A package with many forks draws one fork with a count.
|
|
1230
|
+
// A gathered mark's count, said in words: what it counts, never a bare ×n.
|
|
1231
|
+
const SET_WORDS = { forkset: ['choice', 'choices'], dbset: ['db use', 'db uses'], genset: ['generic part', 'generic parts'], stepset: ['step', 'steps'] };
|
|
1232
|
+
export const setWord = (kind, n) => `${n} ${SET_WORDS[kind][n === 1 ? 0 : 1]}`;
|
|
1233
|
+
|
|
901
1234
|
export function quietItems(map, placed, { services = false, dense = false, phone = false, drawn = new Set(), socketFns = new Map(), genericById = new Map(), exportsLeft = [], strays = [] } = {}) {
|
|
902
1235
|
const pkgOf = id => String(id).split('::')[0];
|
|
903
1236
|
const bare = id => String(id).split('::').pop();
|
|
@@ -917,7 +1250,7 @@ export function quietItems(map, placed, { services = false, dense = false, phone
|
|
|
917
1250
|
for (const u of strays) { const p = pkgOf(u.definition); pk(p); if (!strayBy.has(p)) strayBy.set(p, []); strayBy.get(p).push(u); }
|
|
918
1251
|
for (const [p, us] of strayBy) {
|
|
919
1252
|
const defs = [...new Set(us.map(u => u.definition))];
|
|
920
|
-
P.get(p).parts.push({ kind: 'dbset', label: defs.length === 1 && !dense ? bare(defs[0]) :
|
|
1253
|
+
P.get(p).parts.push({ kind: 'dbset', label: defs.length === 1 && !dense ? bare(defs[0]) : setWord('dbset', defs.length), n: defs.length, writes: us.some(u => u.writes), keys: us.map(u => `use:${u.i}`) });
|
|
921
1254
|
}
|
|
922
1255
|
// forks (and bare generic parts) of one package gathered under one mark with a count when the line would be long
|
|
923
1256
|
const total = [...list.values()].reduce((n, l) => n + l.length, 0);
|
|
@@ -926,10 +1259,10 @@ export function quietItems(map, placed, { services = false, dense = false, phone
|
|
|
926
1259
|
const g = P.get(pkg);
|
|
927
1260
|
const forks = ids2.filter(id => keyed.has(id) && !socketFns.has(id));
|
|
928
1261
|
let rest = ids2.filter(id => !forks.includes(id));
|
|
929
|
-
if (gather && forks.length > 1) g.parts.push({ kind: 'forkset', label:
|
|
1262
|
+
if (gather && forks.length > 1) g.parts.push({ kind: 'forkset', label: setWord('forkset', forks.length), n: forks.length, fork: [...new Set(forks.flatMap(id => keyed.get(id).types))], keys: forks.flatMap(id => [`fork:${id}`, `part:${id}`, ...(genericById.has(id) ? [`generic:${id}`] : [])]) });
|
|
930
1263
|
else rest = rest.concat(forks);
|
|
931
1264
|
const gens = rest.filter(id => genericById.has(id) && !socketFns.has(id) && !keyed.has(id));
|
|
932
|
-
if (gather && gens.length > 1) { g.parts.push({ kind: 'genset', label:
|
|
1265
|
+
if (gather && gens.length > 1) { g.parts.push({ kind: 'genset', label: setWord('genset', gens.length), n: gens.length, gen: [...new Set(gens.flatMap(id => genericById.get(id).vars))].join(''), keys: gens.flatMap(id => [`generic:${id}`, `part:${id}`]) }); rest = rest.filter(id => !gens.includes(id)); }
|
|
933
1266
|
for (const id of rest.sort()) g.parts.push({ kind: 'part', id, label: bare(id), socket: socketFns.has(id), fns: socketFns.get(id) || null,
|
|
934
1267
|
fork: keyed.has(id) ? keyed.get(id).types : null, gen: genericById.has(id) ? genericById.get(id).vars.join('') : null,
|
|
935
1268
|
keys: [...(socketFns.has(id) ? [`applied:${id}`] : []), ...(keyed.has(id) ? [`fork:${id}`] : []), ...(genericById.has(id) ? [`generic:${id}`] : []), `part:${id}`] });
|
|
@@ -942,11 +1275,11 @@ export function quietItems(map, placed, { services = false, dense = false, phone
|
|
|
942
1275
|
const of = k => all.filter(it => it.kind === k || (k === 'forkset' && it.fork && it.kind === 'part'));
|
|
943
1276
|
const parts = [];
|
|
944
1277
|
const forks = of('forkset'), dbs = of('dbset'), others = all.filter(it => !forks.includes(it) && !dbs.includes(it));
|
|
945
|
-
const n = list => list.reduce((c, it) => c + (it.
|
|
946
|
-
if (forks.length) parts.push({ kind: 'forkset', label:
|
|
947
|
-
if (dbs.length) parts.push({ kind: 'dbset', label:
|
|
1278
|
+
const n = list => list.reduce((c, it) => c + (it.n || 1), 0);
|
|
1279
|
+
if (forks.length) parts.push({ kind: 'forkset', label: setWord('forkset', n(forks)), n: n(forks), fork: [...new Set(forks.flatMap(it => it.fork))], keys: forks.flatMap(it => it.keys) });
|
|
1280
|
+
if (dbs.length) parts.push({ kind: 'dbset', label: setWord('dbset', n(dbs)), n: n(dbs), writes: dbs.some(it => it.writes), keys: dbs.flatMap(it => it.keys) });
|
|
948
1281
|
parts.push(...others);
|
|
949
|
-
if (steps.length) parts.push({ kind: 'stepset', label:
|
|
1282
|
+
if (steps.length) parts.push({ kind: 'stepset', label: setWord('stepset', steps.length), n: steps.length, keys: steps.map(st => `step:${st.pkg}:${st.type}`) });
|
|
950
1283
|
return [{ pkg: '', exports: [], steps: [], parts, gathered: groups.length }];
|
|
951
1284
|
}
|
|
952
1285
|
|
|
@@ -956,13 +1289,17 @@ export function quietItems(map, placed, { services = false, dense = false, phone
|
|
|
956
1289
|
// above and below, so a label only has to clear the last one on its side.
|
|
957
1290
|
function placeOn(pkgs, lsx, phone, f, M) {
|
|
958
1291
|
const out = []; let x = lsx, lastUp = null, lastDown = null;
|
|
1292
|
+
// the risers that cross the line: the first x at or after x0 where [x + a, x + b] crosses none
|
|
1293
|
+
const bands = M.bands || [];
|
|
1294
|
+
const shun = (x0, a, b) => { let v = x0; for (let k = 0; k < 8; k++) { const r = bands.find(([lo, hi]) => v + b > lo && v + a < hi); if (!r) break; v = r[1] - a + 1; } return v; };
|
|
959
1295
|
for (const g of pkgs) {
|
|
960
1296
|
// headers alternate between two tiers, so a tick clears the header two packages back
|
|
961
1297
|
const hdrBack = out.length >= 2 ? out[out.length - 2].tick + out[out.length - 2].hw : 0;
|
|
962
1298
|
// a tick from the upper tier runs down past the lower tier's header before it
|
|
963
1299
|
const hdrPrev = out.length % 2 === 1 ? out[out.length - 1].tick + out[out.length - 1].hw : 0;
|
|
964
|
-
const
|
|
1300
|
+
const tick0 = out.length ? Math.max(x + (phone ? 10 : 16), hdrBack, hdrPrev, lastUp && lastUp.x !== out[out.length - 1].tick ? lastUp.x + lastUp.w + 4 : out[out.length - 1].tick + (phone ? 16 : 22), lastDown ? lastDown.x + lastDown.w + 4 : 0) : x - (phone ? 9 : 14);
|
|
965
1301
|
const hw = M.header(g, f), sw = M.steps(g, f);
|
|
1302
|
+
const tick = shun(tick0, -(phone ? 4 : 6), Math.max(hw, sw));
|
|
966
1303
|
lastUp = { x: tick, w: phone ? 10 : 14 }; lastDown = sw ? { x: tick, w: sw } : null;
|
|
967
1304
|
let px = tick + (phone ? 9 : 14), flip = 0;
|
|
968
1305
|
const xs = [], sides = [];
|
|
@@ -973,6 +1310,7 @@ function placeOn(pkgs, lsx, phone, f, M) {
|
|
|
973
1310
|
let xx = px;
|
|
974
1311
|
if (last && !(side === 'up' && last.x === tick)) xx = Math.max(xx, last.x + last.w);
|
|
975
1312
|
if (side === 'down' && last && last.x === tick) xx = Math.max(xx, last.x + last.w + 2);
|
|
1313
|
+
xx = shun(xx, -(phone ? 6 : 7), M.width(it, f) - (phone ? 7 : 10));
|
|
976
1314
|
xs.push(xx); sides.push(side);
|
|
977
1315
|
const rec = { x: xx, w: M.width(it, f) };
|
|
978
1316
|
if (side === 'up') lastUp = rec; else lastDown = rec;
|
|
@@ -988,13 +1326,15 @@ function placeOn(pkgs, lsx, phone, f, M) {
|
|
|
988
1326
|
// The quiet line with its names at an angle: parts' names rise to the right
|
|
989
1327
|
// from their marks; each package's name, with its steps, falls to the right
|
|
990
1328
|
// from its tick. One line, however many parts. null when even this would not fit.
|
|
1329
|
+
// a gathered set of choices says what they choose on in its own words; anything else is its label
|
|
1330
|
+
const setLabel = it => it.kind === 'forkset' ? `${it.label} on ${it.fork.join(', ')}` : it.label;
|
|
991
1331
|
function angledQuiet(pkgs, lsx, lex, y0, phone, fsS) {
|
|
992
1332
|
const ang = 50, sin = Math.sin(ang * Math.PI / 180), cos = Math.cos(ang * Math.PI / 180);
|
|
993
1333
|
const pkgName = p => p.split('/').pop();
|
|
994
1334
|
// each name as its spans: [text, fill, attributes, how it is measured: mono, weight, italic]
|
|
995
1335
|
const upSpans = it => [...(it.gen ? [[`∀${it.gen} `, C.hullInk, ` font-family="${SANS}" font-weight="700"`, false, 700]] : []),
|
|
996
|
-
[it
|
|
997
|
-
...(it.fork ? [[' ' + it.fork.join(','), C.muted, '', true, 400]] : []), ...(it.fns ? [[' ' + it.fns.join(', '), C.hullInk, ' font-style="italic"', true, 400, true]] : [])];
|
|
1336
|
+
[setLabel(it), C.ink, it.kind === 'part' ? '' : ' font-weight="700"', true, it.kind === 'part' ? 400 : 700],
|
|
1337
|
+
...(it.fork && it.kind !== 'forkset' ? [[' ' + it.fork.join(','), C.muted, '', true, 400]] : []), ...(it.fns ? [[' ' + it.fns.join(', '), C.hullInk, ' font-style="italic"', true, 400, true]] : [])];
|
|
998
1338
|
const downSpans = g => [[pkgName(g.pkg), C.faint, ` font-family="${SANS}" font-weight="700"`, false, 700],
|
|
999
1339
|
...(g.steps.length ? [[' ' + g.steps.map(st => st.label).join(', '), C.hullInk, ' font-weight="700"', true, 700]] : [])];
|
|
1000
1340
|
const lay = pitch => {
|
|
@@ -1058,15 +1398,18 @@ function angledQuiet(pkgs, lsx, lex, y0, phone, fsS) {
|
|
|
1058
1398
|
return { items: pkgs.length, under, out, over, bottom: ry + downLen * sin + (phone ? 4 : 6) };
|
|
1059
1399
|
}
|
|
1060
1400
|
|
|
1061
|
-
export function layoutQuiet(pkgs, lsx, lex, y0, phone, fsS0) {
|
|
1401
|
+
export function layoutQuiet(pkgs, lsx, lex, y0, phone, fsS0, bands = []) {
|
|
1062
1402
|
pkgs = pkgs.filter(g => g.parts.length || g.steps.length || g.exports.length);
|
|
1063
1403
|
if (!pkgs.length) return { items: 0, under: [], out: [], over: [], bottom: y0 };
|
|
1064
1404
|
const pkgName = p => p.split('/').pop();
|
|
1065
1405
|
let fsS = fsS0;
|
|
1066
|
-
|
|
1406
|
+
// a gathered set of choices says what they choose on in its own line of words, with a wider gap after it
|
|
1407
|
+
// so the next count does not read as part of it; a single part keeps its type under it
|
|
1408
|
+
const sub = it => it.kind === 'forkset' ? '' : it.fork ? it.fork.join(',') : it.fns ? it.fns.join(', ') : '';
|
|
1067
1409
|
const M = {
|
|
1410
|
+
bands,
|
|
1068
1411
|
// as drawn: a gathered count and a step set in bold, ∀ in bold sans
|
|
1069
|
-
width: (it, f) => Math.max((it.gen ? tw(`∀${it.gen} `, f, false, 700) : 0) + tw(it
|
|
1412
|
+
width: (it, f) => Math.max((it.gen ? tw(`∀${it.gen} `, f, false, 700) : 0) + tw(setLabel(it), f, true, it.kind === 'part' ? 400 : 700), tw(sub(it), Math.max(MIN_LABEL, f - 0.5)) + (sub(it) && it.fork ? (phone ? 9 : 12) : 0)) + (it.kind === 'forkset' ? (phone ? 16 : 22) : (phone ? 7 : 10)) + (it.kind === 'stepset' ? (phone ? 9 : 11) : 0),
|
|
1070
1413
|
header: (g, f) => tw(g.pkg ? pkgName(g.pkg) : `${g.gathered} packages`, f, false, 700) + (g.exports.length ? (phone ? 9 : 12) + tw(String(g.exports.length), f, true, 700) : 0) + (phone ? 8 : 12),
|
|
1071
1414
|
steps: (g, f) => g.steps.length ? Math.max(...g.steps.map(s => tw(s.label, f, true, 700))) + (phone ? 10 : 13) + (phone ? 6 : 8) : 0,
|
|
1072
1415
|
};
|
|
@@ -1122,15 +1465,16 @@ export function layoutQuiet(pkgs, lsx, lex, y0, phone, fsS0) {
|
|
|
1122
1465
|
const ny = up ? ry - (phone ? 7 : 9) : ry + (phone ? 13 : 16);
|
|
1123
1466
|
// a fork's types a line further out than the name, clear of it
|
|
1124
1467
|
const sy = ny + (up ? -1 : 1) * lineGap(fsS);
|
|
1125
|
-
|
|
1468
|
+
// a step set's count starts past its loop's arrowhead
|
|
1469
|
+
let gg = '', tx = it.kind === 'stepset' ? x + (phone ? 6 : 8) : x - 3;
|
|
1126
1470
|
if (it.kind === 'forkset') gg += mark.fork(x - (phone ? 3 : 4), ry, phone ? 0.75 : 0.9);
|
|
1127
1471
|
else if (it.kind === 'dbset') gg += mark.station(x, ry, phone ? 3.6 : 4.6, it.writes);
|
|
1128
1472
|
else if (it.kind === 'stepset') gg += mark.loop(x, ry - 0.5, phone ? 4.2 : 5.4);
|
|
1129
1473
|
else if (it.socket) gg += mark.socket(x, ry, phone ? 4.6 : 6);
|
|
1130
1474
|
else gg += mark.dot(x, ry, phone ? 3 : 3.8);
|
|
1131
1475
|
if (it.gen) { gg += T(tx, ny, `∀${it.gen}`, { size: fsS, weight: 700, fill: C.hullInk, mono: false }); tx += tw(`∀${it.gen} `, fsS, false, 700); }
|
|
1132
|
-
gg += T(tx, ny, it
|
|
1133
|
-
if (it.fork) { gg += mark.fork(x - 2, sy - (phone ? 2.5 : 3.2), phone ? 0.55 : 0.7); gg += T(x + (phone ? 6 : 8), sy, it.fork.join(','), { size: fsS - 0.5, fill: C.muted }); }
|
|
1476
|
+
gg += T(tx, ny, setLabel(it), { size: fsS, fill: it.kind === 'stepset' ? C.hullInk : C.ink, weight: it.kind === 'part' ? null : 700 });
|
|
1477
|
+
if (it.fork && it.kind !== 'forkset') { gg += mark.fork(x - 2, sy - (phone ? 2.5 : 3.2), phone ? 0.55 : 0.7); gg += T(x + (phone ? 6 : 8), sy, it.fork.join(','), { size: fsS - 0.5, fill: C.muted }); }
|
|
1134
1478
|
else if (it.fns) gg += T(x - 3, sy, it.fns.join(', '), { size: fsS - 0.5, fill: C.hullInk, italic: true });
|
|
1135
1479
|
over.push(tg(it.keys, gg));
|
|
1136
1480
|
});
|
|
@@ -1229,9 +1573,11 @@ function renderLibrary(map, W, opts) {
|
|
|
1229
1573
|
steps.forEach((s, k) => {
|
|
1230
1574
|
const x = xs[k + 1];
|
|
1231
1575
|
decorate(s.id, x, py);
|
|
1576
|
+
// a step's names are centred on it, within its share of the chain and short of the package's edge
|
|
1577
|
+
const room = 2 * Math.min(dx / 2 - 2, x1 - (phone ? 8 : 12) - x);
|
|
1232
1578
|
const fns = socketFns.get(s.id);
|
|
1233
|
-
if (fns && !phone) out.push(T(x, py - (phone ? 9 : 11), clip(fns.join(', '), 9,
|
|
1234
|
-
{ const t = fitText(s.label + (s.applied ? '(..)' : ''), fs,
|
|
1579
|
+
if (fns && !phone) out.push(T(x, py - (phone ? 9 : 11), clip(fns.join(', '), 9, room - 4), { anchor: 'middle', size: 9, fill: C.hullInk }));
|
|
1580
|
+
{ const t = fitText(s.label + (s.applied ? '(..)' : ''), fs, room); out.push(T(x, py + (phone ? 17 : 22), t.s, { anchor: 'middle', size: t.size, fill: C.ink })); }
|
|
1235
1581
|
});
|
|
1236
1582
|
drawn.add(c.value);
|
|
1237
1583
|
y = py + (phone ? 26 : 32) + gapY + (phone ? 6 : 8);
|
|
@@ -1510,7 +1856,7 @@ export function partFor(map, id) {
|
|
|
1510
1856
|
export function journeyLabel(map, key) {
|
|
1511
1857
|
if (key === 'quiet') return 'off the entry paths';
|
|
1512
1858
|
const b = map.bundles && map.bundles.find(x => x.id === key);
|
|
1513
|
-
if (b) return b.count > 1 ? `${b.label}
|
|
1859
|
+
if (b) return b.count > 1 ? `${b.label} (${b.count} routes)` : b.label;
|
|
1514
1860
|
return partFor(map, key).label;
|
|
1515
1861
|
}
|
|
1516
1862
|
|
|
@@ -1540,7 +1886,6 @@ function renderPart(map, partId, opts) {
|
|
|
1540
1886
|
const rh = phone ? 56 : 66;
|
|
1541
1887
|
const yTop = frameTop + (phone ? 16 : 20);
|
|
1542
1888
|
const feeds = part.groups.flat().filter(s => s.role === 'semantic-object');
|
|
1543
|
-
const rowsTop = yTop + (phone ? 30 : 34) + (feeds.length ? (phone ? 28 : 34) : 0);
|
|
1544
1889
|
const shared = n > 1;
|
|
1545
1890
|
const atPart = u => part.station ? u.at.node === 'entry' && u.at.entry === part.station : u.at.node === 'wrapper' && u.at.wrapper === part.id;
|
|
1546
1891
|
const uses = map.uses.map((u, i) => ({ ...u, i })).filter(u => u.kind === 'db' && atPart(u));
|
|
@@ -1548,17 +1893,52 @@ function renderPart(map, partId, opts) {
|
|
|
1548
1893
|
const rS = phone ? 5.4 : 6.6;
|
|
1549
1894
|
// columns: the casing holds the part; past it, the after band and the reply bars
|
|
1550
1895
|
// on a phone the casing gives up width to the after band
|
|
1551
|
-
const
|
|
1552
|
-
const
|
|
1553
|
-
const
|
|
1896
|
+
const k = phone ? 0.88 : 1, xr = x0 + cw * 0.96;
|
|
1897
|
+
const keyedById = new Map(map.keyed.map(k => [k.id, k]));
|
|
1898
|
+
const keyed = ids => [...new Set(ids)].filter(id => keyedById.has(id));
|
|
1899
|
+
const typesOf = ids => [...new Set(keyed(ids).flatMap(id => keyedById.get(id).types))].join(',');
|
|
1900
|
+
// The sockets and the checks after them take the room their labels' widest
|
|
1901
|
+
// pieces need, so that wrapping never has to clip one: the columns left of
|
|
1902
|
+
// the sockets give way down to three quarters of their width, then the
|
|
1903
|
+
// casing reaches into the band after it, which keeps room for its marks.
|
|
1904
|
+
const piece = s => (s ? widestPiece(s, fsS, true) : 0);
|
|
1905
|
+
const passed = part.station ? [] : callers.map(cl => cl.passed[0]).filter(Boolean);
|
|
1906
|
+
const checked = passed.filter(p => p.guards.length);
|
|
1907
|
+
const sockNeed = Math.max(0, ...passed.map(p => Math.max(rS + 3 + Math.max(piece(p.label), piece(reachLabel(p.effects))), keyed([p.id]).length ? 13 + piece(typesOf([p.id])) : 0)));
|
|
1908
|
+
const checkNeed = Math.max(0, ...checked.map(p => Math.max(6 + piece(p.guards.join(', ')), keyed(p.guardIds || []).length ? 15 + piece(typesOf(p.guardIds || [])) : 0)));
|
|
1909
|
+
const afterMarks = Math.max(0, ...callers.map(cl => { const b = cl.beside; return [b.guards.length, b.effects.length, b.resources.includes('model'), b.calls.length].filter(Boolean).length; }));
|
|
1910
|
+
let caseEnd = x0 + cw * 0.8 * k, xSock = x0 + cw * 0.6 * k, xPG = x0 + cw * 0.72 * k;
|
|
1911
|
+
if (!part.station) {
|
|
1912
|
+
const checkSpan = checked.length ? Math.max(caseEnd - xPG, checkNeed + 6) : 0;
|
|
1913
|
+
const sockSpan = Math.max((checked.length ? xPG : caseEnd) - xSock, sockNeed + (checked.length ? 8 : 6));
|
|
1914
|
+
// (only columns with nothing of the part's own to label give way)
|
|
1915
|
+
const least = x0 + cw * 0.6 * k * (part.effects.length || keyed([part.id]).length || part.guards.length ? 1 : 0.75), most = xr - (phone ? 14 : 18) * (afterMarks + 1);
|
|
1916
|
+
caseEnd = Math.max(caseEnd, Math.min(most, least + sockSpan + checkSpan));
|
|
1917
|
+
xPG = caseEnd - checkSpan;
|
|
1918
|
+
xSock = Math.max(least, Math.min(x0 + cw * 0.6 * k, xPG - sockSpan));
|
|
1919
|
+
}
|
|
1920
|
+
// the columns left of the sockets, in their share of what the sockets leave
|
|
1921
|
+
const c = p => x0 + (xSock - x0) * p / 0.6;
|
|
1922
|
+
const xMerge = c(0.08), xRead = c(0.22), xGuard = c(0.38), xSplit = c(0.5);
|
|
1554
1923
|
const stX = part.station ? xSock : xRead;
|
|
1555
1924
|
// the parameter every caller fixes drops into the check it decides, else the part's own station
|
|
1556
1925
|
const feedX = s => (s.guardsFed.length && part.guards.length ? xGuard : xRead);
|
|
1557
|
-
|
|
1926
|
+
// the casing's head lists them, those dropping furthest left first, each on
|
|
1927
|
+
// lines of its own reaching to the casing's edge: a later one's drop starts
|
|
1928
|
+
// at its own name, under the names that reach across it
|
|
1929
|
+
const lead = phone ? 12 : 13;
|
|
1930
|
+
const feedRows = [];
|
|
1931
|
+
{ let fy = yTop + (phone ? 20 : 23);
|
|
1932
|
+
for (const s of feeds.slice().sort((a, b) => feedX(a) - feedX(b))) {
|
|
1933
|
+
const gx = feedX(s), text = `${s.name}: ${String(s.type).split('::').pop()}`;
|
|
1934
|
+
const lines = wrapped(text, caseEnd - 6 - gx - 6, { size: fsS });
|
|
1935
|
+
feedRows.push({ s, gx, text, lines, y: fy });
|
|
1936
|
+
fy += lines.length * lead;
|
|
1937
|
+
} }
|
|
1938
|
+
const feedLines = feedRows.reduce((a, r) => a + r.lines.length, 0);
|
|
1939
|
+
const rowsTop = yTop + (phone ? 30 : 34) + (feeds.length ? (phone ? 34 : 39) + (feedLines - 1) * lead : 0);
|
|
1558
1940
|
// line k (from 1) under a path at y
|
|
1559
|
-
const
|
|
1560
|
-
const keyed = ids => [...new Set(ids)].filter(id => keyedById.has(id));
|
|
1561
|
-
const typesOf = ids => [...new Set(keyed(ids).flatMap(id => keyedById.get(id).types))].join(',');
|
|
1941
|
+
const line = (y, k) => y + (phone ? 14 : 16) + (k - 1) * lead;
|
|
1562
1942
|
const forkLines = (ids, x, end) => (keyed(ids).length ? wrapped(typesOf(ids), end - x - 11, { size: fsS }) : []);
|
|
1563
1943
|
const entryOf = cl => map.entries.find(e => e.handlerId === cl.id || e.key === cl.id);
|
|
1564
1944
|
// Each caller's lines: its name over its path; under it its routes, then its
|
|
@@ -1578,7 +1958,8 @@ function renderPart(map, partId, opts) {
|
|
|
1578
1958
|
const tokX = roles.length === 1 ? lx + tw(roles[0], fsS, false, 700) + 8 : lx;
|
|
1579
1959
|
const tokSame = !!tokTxt && roles.length === 1 && tw(tokTxt, fsS) <= x0 - tokX - 6;
|
|
1580
1960
|
const toks = tokTxt ? (tokSame ? [tokTxt] : wrapped(tokTxt, x0 - lx - 6, { size: fsS })) : [];
|
|
1581
|
-
|
|
1961
|
+
// the roles and tokens start a line below the path's own (left empty without a route, for the lock)
|
|
1962
|
+
const left = (roles.length || toks.length ? Math.max(1, sub.length) : sub.length) + roles.length + (tokSame ? 0 : toks.length);
|
|
1582
1963
|
const r = { e, lx, name, sx, sub, subTxt, roles, roleTxt, toks, tokTxt, tokSame, tokX, handed: [], reach: [], fork: [], below: false, gLines: [], gFork: [], after: [], afterAt: 1, afterWide: false, afterG: [] };
|
|
1583
1964
|
if (!part.station) {
|
|
1584
1965
|
const p = cl.passed[0], pe = p ? p.effects : [];
|
|
@@ -1618,7 +1999,7 @@ function renderPart(map, partId, opts) {
|
|
|
1618
1999
|
const yBot = railUses.length ? busY + (phone ? 18 : 22) : rowsBottom + (phone ? 46 : 52);
|
|
1619
2000
|
const wall = wallPath(x0, x1, yTop, yBot, ys.concat(railUses.length ? [busY] : []), phone ? 6 : 7);
|
|
1620
2001
|
const casing = part.station ? 'canvas' : 'casing';
|
|
1621
|
-
if (!part.station) under.push(shape('hull', `<rect x="${x0 + 8}" y="${yTop + 8}" width="${f1(caseEnd - x0 - 8)}" height="${yBot - yTop - 16}" rx="12" fill="${C.hull}" stroke="${C.hullEdge}" stroke-width="1"${boxAttr(casing)}/>`));
|
|
2002
|
+
if (!part.station) under.push(shape('hull', `<rect x="${x0 + 8}" y="${yTop + 8}" width="${f1(caseEnd - x0 - 8)}" height="${yBot - yTop - 16}" rx="12" fill="${C.hull}" stroke="${C.hullEdge}" stroke-width="1"${boxAttr(casing)}/>` + hullWall(caseEnd, ys, phone)));
|
|
1622
2003
|
// a fork under a mark, its types from line k ending by `end`
|
|
1623
2004
|
const forkAt = (ids, x, y, k, end) => {
|
|
1624
2005
|
const ks = keyed(ids);
|
|
@@ -1658,12 +2039,15 @@ function renderPart(map, partId, opts) {
|
|
|
1658
2039
|
} else out.push(`<path d="M${x0 + 4} ${f1(ty)} H${f1(part.station ? xr : xSock - rS)}" stroke="${C.ink2}" stroke-width="1.6"/>`);
|
|
1659
2040
|
// under the part's own station, right of its riser: its fork, then its tables; they end short of its check's stem
|
|
1660
2041
|
const gx = part.station ? xRead : xGuard;
|
|
1661
|
-
|
|
1662
|
-
const
|
|
1663
|
-
|
|
2042
|
+
// when a piece of them would not fit before the part's check, they go under its failure bar and its fork, to the split
|
|
2043
|
+
const short = !part.station && part.guards.length > 0 && (piece(typesOf([part.id])) > xGuard - 8 - stX - 17 || piece(reachLabel(part.effects)) > xGuard - 8 - stX - rS - 4);
|
|
2044
|
+
const stEnd = part.station ? xr - 6 : part.guards.length && !short ? xGuard - 8 : xSplit - 4;
|
|
2045
|
+
const stLine = short ? 1 + Math.max(1, forkLines(part.guardIds || [], gx + 6, xSplit - 4).length) : 1;
|
|
2046
|
+
const stFork = forkLines([part.id], stX + 6, stEnd).length;
|
|
2047
|
+
forkAt([part.id], stX + 6, ty, stLine, stEnd);
|
|
1664
2048
|
if (part.effects.length) {
|
|
1665
2049
|
over.push(mark.station(stX, ty, shared ? rS * 1.15 : rS, writesIn(part.effects)));
|
|
1666
|
-
out.push(owned(casing, L(stX + rS + 4, line(ty,
|
|
2050
|
+
out.push(owned(casing, L(stX + rS + 4, line(ty, stLine + stFork), reachLabel(part.effects), stEnd - stX - rS - 4, { size: fsS, fill: C.db })));
|
|
1667
2051
|
} else if (part.station) over.push(mark.dot(stX, ty, 3.4));
|
|
1668
2052
|
if (part.guards.length) {
|
|
1669
2053
|
over.push(tg((part.guardIds || []).map(g => `guard:${part.id}:${g}`), mark.guard(gx, ty, phone ? 0.9 : 1.1)));
|
|
@@ -1674,11 +2058,16 @@ function renderPart(map, partId, opts) {
|
|
|
1674
2058
|
forkAt(part.guardIds || [], gx + 6, ty, 1, part.station ? stX - rS - 6 : xSplit - 4);
|
|
1675
2059
|
}
|
|
1676
2060
|
// the parameter every caller fixes drops in from above into the check it decides
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
2061
|
+
const dropped = new Set();
|
|
2062
|
+
for (const { gx, text, lines, y } of feedRows) {
|
|
2063
|
+
// one drop for each column, from above the casing, or from the first name in it the names to its left leave clear
|
|
2064
|
+
if (!dropped.has(gx)) {
|
|
2065
|
+
dropped.add(gx);
|
|
2066
|
+
const from = feedRows.some(r => r.gx < gx) ? y - fsS : yTop + 2;
|
|
2067
|
+
out.push(`<path d="M${f1(gx)} ${f1(from)} V${f1(ty - (phone ? 16 : 20))}" stroke="${C.ink2}" stroke-width="1.4" stroke-dasharray="1.5 3"/>`);
|
|
2068
|
+
out.push(`<path d="M${f1(gx - 3.5)} ${f1(ty - (phone ? 21 : 25))} L${f1(gx)} ${f1(ty - (phone ? 15 : 19))} L${f1(gx + 3.5)} ${f1(ty - (phone ? 21 : 25))}" fill="${C.ink2}"/>`);
|
|
2069
|
+
}
|
|
2070
|
+
out.push(owned(casing, Tw(gx + 6, y, lines, { size: fsS, fill: C.ink2 }, text, lead)));
|
|
1682
2071
|
}
|
|
1683
2072
|
if (!part.station) callers.forEach((cl, i) => {
|
|
1684
2073
|
const y = ys[i];
|