yichus 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -16
- package/dist/cli.js +1328 -259
- package/dist/cli.js.map +1 -1
- 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 +122 -8
- package/dist/studio-page/system-map.js +461 -85
- package/dist/studio-page/what-happens-record.js +217 -0
- package/dist/studio-page/what-happens.js +752 -0
- package/dist/studio-reference/deploy-platforms.md +79 -0
- package/dist/studio-reference/portable-programs.md +58 -0
- package/dist/studio-reference/porting.md +32 -0
- 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/the-page.md +107 -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))));
|
|
@@ -329,7 +323,25 @@ function renderService(map, W, opts) {
|
|
|
329
323
|
const gap = phone ? 3 : 4;
|
|
330
324
|
const lockW = e => (e.authority === 'publishable' || e.signedIn ? (phone ? 12 : 14) : 0);
|
|
331
325
|
const labX = e => x0 + (phone ? 7 : 10) + lockW(e);
|
|
332
|
-
|
|
326
|
+
// a bundle says what it counts: its routes
|
|
327
|
+
const countOf = e => (e.members.length > 1 ? `${e.members.length} routes` : '');
|
|
328
|
+
// A row's checks by name: every member's, not only the first's (a bundle
|
|
329
|
+
// holds routes with the same marks, not the same checks). A check that
|
|
330
|
+
// took another definition's place on the same route since the compared
|
|
331
|
+
// base is named with the one it replaced.
|
|
332
|
+
const replacedAt = new Map(((map.compare && map.compare.replaced) || []).map(r => [`${r.entry} ${r.within} ${r.at}`, r]));
|
|
333
|
+
const checks = (e, within, idsOf, namesOf) => {
|
|
334
|
+
const words = [], replaced = [];
|
|
335
|
+
for (const m of e.members) idsOf(m).forEach((id, i) => {
|
|
336
|
+
const r = replacedAt.get(`${m.key} ${within} ${i}`), nm = namesOf(m)[i];
|
|
337
|
+
const w = r ? `${nm} (in place of ${r.wasLabel})` : nm;
|
|
338
|
+
if (!words.includes(w)) words.push(w);
|
|
339
|
+
if (r) replaced.push(`replaced:${m.key}:${id}`);
|
|
340
|
+
});
|
|
341
|
+
return { text: words.join(', '), names: words, replaced };
|
|
342
|
+
};
|
|
343
|
+
const checkLabel = (c, svg) => c.replaced.length ? tg(c.replaced, shape('replaced', svg)) : svg;
|
|
344
|
+
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
345
|
// the hull band starts past the names of the routes that enter it, as far as half the wall
|
|
334
346
|
const hx0 = Math.min(col(0.5, 0.5), Math.max(col(0.22, 0.34), ...rowsIn.filter(e => e.wrapper)
|
|
335
347
|
.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 +479,7 @@ function renderService(map, W, opts) {
|
|
|
467
479
|
// the left column
|
|
468
480
|
const leftEnd = clear(i, x2, e.wrapper ? hx0 - 6 : (e.guards.length ? gx - 9 : xh - rS - 6), true);
|
|
469
481
|
const left = [];
|
|
470
|
-
const guardLines = !e.wrapper && e.guards.length ? wrapped(e.
|
|
482
|
+
const guardLines = !e.wrapper && e.guards.length ? wrapped(e._chk.h.text, gx - 7 - x2, { size: fsS }) : [];
|
|
471
483
|
guardLines.forEach(() => left.push('guard'));
|
|
472
484
|
const guardFork = !e.wrapper && !dense ? forkLines(keyedTypes(e.members.flatMap(m => m.guardIds)), gx + 6 - 3 - x2) : [];
|
|
473
485
|
guardFork.forEach(() => left.push('guardFork'));
|
|
@@ -487,7 +499,7 @@ function renderService(map, W, opts) {
|
|
|
487
499
|
built = builtAt.get(`h:${i}`);
|
|
488
500
|
if (built) right.push('builder');
|
|
489
501
|
} else {
|
|
490
|
-
const gname = p && p.guards.length && !dense ? p.
|
|
502
|
+
const gname = p && p.guards.length && !dense ? e._chk.p.text : '';
|
|
491
503
|
// the check's name over its diamond when it fits there and the function's name leaves room; else right of its stem
|
|
492
504
|
guardBelow = !!gname && (tw(p.label || '', fsS) > xPG - tw(gname, fsS) / 2 - 8 - xSockText || tw(gname, fsS) > (hx1 - xPG) * 2 - 8);
|
|
493
505
|
const gw = gname ? tw(gname, fsS) / 2 + 8 : 0;
|
|
@@ -523,7 +535,7 @@ function renderService(map, W, opts) {
|
|
|
523
535
|
if (b.guards.length) {
|
|
524
536
|
const free = dense || !(p && (p.label || (p.guards.length && !guardBelow)));
|
|
525
537
|
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(
|
|
538
|
+
afterGuard = wrapped(e._chk.a.text, xr - 8 - from, { size: fsS });
|
|
527
539
|
}
|
|
528
540
|
for (let k = 0; k < Math.max(inner.length, xpg, afterWords.length + afterAt - 1); k++) right.push('inner');
|
|
529
541
|
// a wrapper of this row alone: its own station's forks, tables and builder sit under it too
|
|
@@ -578,9 +590,9 @@ function renderService(map, W, opts) {
|
|
|
578
590
|
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
591
|
};
|
|
580
592
|
// 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) => {
|
|
593
|
+
const guardAt = (keys, names, x, y, room, s = 1, own = 'canvas', row = null, named = svg => svg) => {
|
|
582
594
|
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 }))));
|
|
595
|
+
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
596
|
};
|
|
585
597
|
// a builder hung under a mark on line k, its name to the right from x
|
|
586
598
|
const builder = (b, px, y, k, x, end, own = 'canvas', row = null) => {
|
|
@@ -635,7 +647,7 @@ function renderService(map, W, opts) {
|
|
|
635
647
|
if (e.guards.length) {
|
|
636
648
|
guardAt(each(e, m => m.guardIds.map(g => `guard:${m.key}:${g}`)), [], gx, y, 0, 0.85);
|
|
637
649
|
// 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.
|
|
650
|
+
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
651
|
forkAt(e.members.flatMap(m => m.guardIds), gx + 6, y, left.indexOf('guardFork') + 1, x2, 'canvas', true, e);
|
|
640
652
|
}
|
|
641
653
|
const station = own.length ? mark.station(xh, y, rS, writesIn(own)) : mark.dot(xh, y, 3.2);
|
|
@@ -700,7 +712,7 @@ function renderService(map, W, opts) {
|
|
|
700
712
|
const rowId = `route:${e.key}`;
|
|
701
713
|
over.push(tg(each(e, m => `socket:${m.key}`), mark.socket(xSock, y, rS * 1.05, writesIn(pr), pr.length > 0 && !writesIn(pr))));
|
|
702
714
|
socketPt.set(e.key, [xSock, y]);
|
|
703
|
-
const gw = p && p.guards.length ? tw(p.
|
|
715
|
+
const gw = p && p.guards.length ? tw(e._chk.p.text, fsS) / 2 + 8 : 0;
|
|
704
716
|
// the handed-in function's name above the path; over a check whose name went below, high enough to clear the diamond
|
|
705
717
|
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
718
|
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 +721,8 @@ function renderService(map, W, opts) {
|
|
|
709
721
|
if (p && p.guards.length) {
|
|
710
722
|
out.push(`<path${anchorAttr(rowId)} d="M${f1(xSock + rS)} ${y} H${f1(xPG)}" stroke="${C.ink2}" stroke-width="${f1(cableW(e))}"/>`);
|
|
711
723
|
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.
|
|
724
|
+
guardAt(gkeys, dense || below ? [] : e._chk.p.names, xPG, y, (hx1 - xPG) * 2 - 8, 0.9, hull, e, svg => checkLabel(e._chk.p, svg));
|
|
725
|
+
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
726
|
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
727
|
}
|
|
716
728
|
// past the hull: the after band, then the reply bar
|
|
@@ -724,7 +736,7 @@ function renderService(map, W, opts) {
|
|
|
724
736
|
const step = (xr - hx1) / (items.length + 1);
|
|
725
737
|
items.forEach((draw, k) => draw(hx1 + step * (k + 1)));
|
|
726
738
|
// 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 },
|
|
739
|
+
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
740
|
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
741
|
over.push(mark.reply(xr, y, dense ? 0.75 : S));
|
|
730
742
|
}
|
|
@@ -733,7 +745,8 @@ function renderService(map, W, opts) {
|
|
|
733
745
|
// ---- the quiet line (after rows, so forks placed on rows are left out) ----
|
|
734
746
|
let quietBottom = rowsBottom;
|
|
735
747
|
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
|
|
748
|
+
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,
|
|
749
|
+
[...lanes.values()].map(l => [l.x - l.m, l.x + l.m]));
|
|
737
750
|
if (Q && Q.items) {
|
|
738
751
|
const body = Q.under.join('') + Q.out.join('') + Q.over.join('');
|
|
739
752
|
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 +758,12 @@ function renderService(map, W, opts) {
|
|
|
745
758
|
const ghostLink = map.ghostCallers.some(g => g.kind === 'link');
|
|
746
759
|
// the struck importer sits above the rail's left end, clear of HTTP and of the platform under the rail
|
|
747
760
|
const busY = Math.max(Q && Q.items ? quietBottom + (phone ? 14 : 18) : rowsEnd + (phone ? 40 : 46), ghostLink && anyRail ? httpY + (phone ? 104 : 134) : 0);
|
|
748
|
-
const
|
|
761
|
+
const yBot0 = anyRail ? busY + (phone ? 18 : 22) : (Q && Q.items ? quietBottom + (phone ? 10 : 14) : rowsEnd + (phone ? 34 : 38));
|
|
762
|
+
// ---- the program band: under the rail, at the bottom of the wall ----
|
|
763
|
+
const PB = (map.programs || []).length ? programBand(map.programs, x0, x1, yBot0 + (phone ? 10 : 14), phone,
|
|
764
|
+
{ xr, y: key => { const rk = rowOf.get(key); return rk == null ? null : rowY.get(rk); } }) : null;
|
|
765
|
+
if (PB) { under.push(PB.under); out.push(PB.out); over.push(PB.over); }
|
|
766
|
+
const yBot = PB ? PB.bottom + (phone ? 10 : 14) : yBot0;
|
|
749
767
|
const openGap = e => (phone ? 6 : 7) + (e.members.length > 1 ? Math.min(7, e.members.length * 1.1) : 0);
|
|
750
768
|
const wall = wallPathGaps(x0, x1, yTop, yBot, ents.map(e => [rowY.get(e.key), openGap(e)]));
|
|
751
769
|
|
|
@@ -767,10 +785,18 @@ function renderService(map, W, opts) {
|
|
|
767
785
|
|
|
768
786
|
// ---- the rack, outside on the right, in fixed slots ----
|
|
769
787
|
const rackX = x1 + resW / 2 + (phone ? 2 : 4);
|
|
770
|
-
// a
|
|
771
|
-
|
|
772
|
-
const
|
|
773
|
-
const
|
|
788
|
+
// each resource: how many of the routes drawn reach it, and a glyph sized by that share (its area, like a bar's
|
|
789
|
+
// length, says how much it is used), never so small its name leaves it
|
|
790
|
+
const shown = map.entries.filter(e => rowOf.has(e.key));
|
|
791
|
+
const reaches = (e, k) => !!e[k] || (e.beside?.resources || []).includes(k);
|
|
792
|
+
const nReach = Object.fromEntries(RACK.map(k => [k, shown.filter(e => reaches(e, k)).length]));
|
|
793
|
+
const glyphW = phone ? 38 : 64, nameSz = phone ? 9.5 : 12;
|
|
794
|
+
// (as wide as its name needs, as tall as its share)
|
|
795
|
+
const share = k => Math.max(0.5, Math.sqrt(nReach[k] / shown.length));
|
|
796
|
+
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) }]));
|
|
797
|
+
const countLines = n => tw(`reached by ${n}`, phone ? 9 : 10) <= resW - 4 ? [`reached by ${n}`] : ['reached', `by ${n}`];
|
|
798
|
+
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}`] }]));
|
|
799
|
+
const R = rack(map, rackX, yTop + (phone ? 6 : 10), phone, resW - 4, notes, scale);
|
|
774
800
|
out.push(R.svg);
|
|
775
801
|
|
|
776
802
|
// ---- supply of the db handle ----
|
|
@@ -832,12 +858,302 @@ function renderService(map, W, opts) {
|
|
|
832
858
|
}
|
|
833
859
|
}
|
|
834
860
|
out.push(T(x0 + (phone ? 10 : 13), busY - (phone ? 6 : 7), 'db', { size: phone ? 9 : 10, weight: 700, fill: C.db, mono: false }));
|
|
861
|
+
// once, by the rail's name: what a ? on it means
|
|
862
|
+
const someMixed = dbUses.filter(onRail).some(u => u.how === 'mixed');
|
|
863
|
+
if (someMixed) {
|
|
864
|
+
const kx = x0 + (phone ? 10 : 13) + tw('db', phone ? 9 : 10, false) + (phone ? 10 : 12), ky = busY - (phone ? 9.5 : 10.5);
|
|
865
|
+
over.push(tg(dbUses.filter(u => onRail(u) && u.how === 'mixed').map(u => `unknownKey:${u.i}`), mark.unknown(kx, ky, phone ? 5.5 : 6) +
|
|
866
|
+
T(kx + (phone ? 8 : 9), busY - (phone ? 6 : 7), UNTRACED_WORD, { size: 9, fill: C.muted, mono: false })));
|
|
867
|
+
}
|
|
835
868
|
}
|
|
836
869
|
|
|
837
870
|
const H = Math.max(yBot, R.bottom) + (phone ? 12 : 20);
|
|
838
871
|
return outerFrame(W, H, wall.fill + under.join('') + shape('wall', wall.line) + out.join('') + over.join(''), { ...opts, frameTop }, map.program, opts.crumb);
|
|
839
872
|
}
|
|
840
873
|
|
|
874
|
+
// =====================================================================
|
|
875
|
+
// PROGRAM BAND
|
|
876
|
+
// =====================================================================
|
|
877
|
+
// A pure program as a container: its step, its package, and one small
|
|
878
|
+
// diamond per decision it takes, so its area grows with its decisions. Its
|
|
879
|
+
// request constructors are openings on its right edge; each is wired to the
|
|
880
|
+
// engine lane that performs it, with a dot per resource the arm reaches. A
|
|
881
|
+
// request no engine performs is a struck opening. Each lane carries a fork
|
|
882
|
+
// with the engine's own decision count (none: a rule-free engine), ends in
|
|
883
|
+
// the resources its arms reach, and loops back into the program bottom
|
|
884
|
+
// through one loop node that names what repeats the step.
|
|
885
|
+
// A line broken at its spaces and separators so each piece fits `width`.
|
|
886
|
+
export const wrapAt = (text, width, size, mono = true) => {
|
|
887
|
+
const lines = [];
|
|
888
|
+
let cur = '';
|
|
889
|
+
for (const w of String(text).split(/(?<=, |· | )/)) {
|
|
890
|
+
if (cur && tw((cur + w).trimEnd(), size, mono) > width) { lines.push(cur.trimEnd()); cur = w; } else cur += w;
|
|
891
|
+
}
|
|
892
|
+
if (cur) lines.push(cur.trimEnd());
|
|
893
|
+
return lines;
|
|
894
|
+
};
|
|
895
|
+
const RES_WORD = { db: 'db', model: 'model', net: 'net', compile: 'compile', run: 'run', io: 'io' };
|
|
896
|
+
export function programBand(programs, x0, x1, y0, phone, rows = null) {
|
|
897
|
+
const under = [], out = [], over = [];
|
|
898
|
+
// a label that fits the room it is given: shrunk a little, then cut, with
|
|
899
|
+
// the whole text as its title when it is cut
|
|
900
|
+
const fitT = (x, y, s, width, o = {}) => {
|
|
901
|
+
const t = fitText(s, o.size || 10, width, o.mono !== false);
|
|
902
|
+
const svg = T(x, y, t.s, { ...o, size: t.size });
|
|
903
|
+
return t.s === String(s) ? svg : `<g><title>${esc(s)}</title>${svg}</g>`;
|
|
904
|
+
};
|
|
905
|
+
const cw = x1 - x0, fs = phone ? 9.5 : 11, fsS = phone ? 9 : 10;
|
|
906
|
+
const bw = phone ? 12 : 16, lead = phone ? 11 : 12.5;
|
|
907
|
+
let y = y0, wireN = 0;
|
|
908
|
+
for (const p of programs) {
|
|
909
|
+
// the lanes need room for their widest foot pill each; the program gives way
|
|
910
|
+
const pillW = rs => Math.max(tw(RES_WORD[rs], 9, false) * 1.12 + 12, phone ? 26 : 32);
|
|
911
|
+
const needSp = Math.max(0, ...p.engines.flatMap(e => [...e.resources, ...e.also].map(pillW))) + 6;
|
|
912
|
+
const lanesW = needSp * p.engines.length + (phone ? 24 + 26 : 70 + 56);
|
|
913
|
+
// 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
|
|
914
|
+
const countSp = 12 + Math.max(0, ...p.engines.map(e => tw(String(e.decisions), fsS, true, 700)));
|
|
915
|
+
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))
|
|
916
|
+
: Math.max(px0 + cw * 0.4, Math.min(px0 + cw * 0.5, x1 - lanesW));
|
|
917
|
+
// one scale for every decision bar of this program and its engines
|
|
918
|
+
const maxDec = Math.max(1, p.decisions, ...p.engines.map(e => e.decisions));
|
|
919
|
+
const per = Math.min(phone ? 2 : 2.4, (phone ? 170 : 200) / maxDec);
|
|
920
|
+
const barH = n => Math.max(n > 0 ? 2 : 0, n * per);
|
|
921
|
+
const starts = p.starters.filter(s => !s.advances);
|
|
922
|
+
// 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)
|
|
923
|
+
const startLines = (() => {
|
|
924
|
+
if (!starts.length) return [];
|
|
925
|
+
const room = (px1 - px0) - (phone ? 18 : 22), groups = [[]];
|
|
926
|
+
// the words lead the first line, or stand on their own when the first path would not fit after them
|
|
927
|
+
const lead = 'one step per call of', alone = tw(`${lead} ${starts[0].path}`, fsS, false) > room;
|
|
928
|
+
const text = (g, j) => `${j || alone ? '' : `${lead} `}${g.join(', ')}`;
|
|
929
|
+
for (const st of starts) {
|
|
930
|
+
const g = groups[groups.length - 1];
|
|
931
|
+
if (!g.length || tw(text([...g, st.path], groups.length - 1) + ',', fsS, false) <= room) g.push(st.path);
|
|
932
|
+
else groups.push([st.path]);
|
|
933
|
+
}
|
|
934
|
+
const lines = [...(alone ? [lead] : []), ...groups.map((g, j) => text(g, j) + (j < groups.length - 1 ? ',' : ''))];
|
|
935
|
+
if (lines.length <= 3) return lines;
|
|
936
|
+
const shown = groups[0].length + (alone ? 0 : groups[1].length);
|
|
937
|
+
return [lines[0], lines[1], `and ${starts.length - shown} more`];
|
|
938
|
+
})();
|
|
939
|
+
// the counts on one line, or two when the program box is narrow
|
|
940
|
+
// its size in words a reader needs no key for: the package's own definitions, and how many more it uses from elsewhere
|
|
941
|
+
const own = `${p.definitions} definitions in ${p.pkgLabel}`;
|
|
942
|
+
// which packages they come from, the most used first: three named, the rest counted
|
|
943
|
+
// (each name stays with its count when the line wraps)
|
|
944
|
+
const from = p.elsewhere.slice(0, 3).map(x => `${x.label}\u00a0${x.n}`).join(', ') + (p.elsewhere.length > 3 ? `, ${p.elsewhere.length - 3}\u00a0more` : '');
|
|
945
|
+
const more = p.usesElsewhere ? `uses ${p.usesElsewhere} more from ${p.elsewhere.length} package${p.elsewhere.length === 1 ? '' : 's'}: ${from}` : '';
|
|
946
|
+
const defsTxt = more ? `${own} · ${more}` : own;
|
|
947
|
+
const headRoom = cw * (phone ? 0.56 : 0.5) - (phone ? 26 : 34);
|
|
948
|
+
const defsLines = tw(defsTxt, fsS, false) <= headRoom ? [defsTxt] : [own, more].filter(Boolean).flatMap(l => wrapAt(l, headRoom, fsS, false));
|
|
949
|
+
const headH = (phone ? 40 : 44) + (defsLines.length - 1) * lead + startLines.length * lead;
|
|
950
|
+
const vMax = 8;
|
|
951
|
+
const variantRows = q => q.variants.reduce((n, v) => n + Math.min(v.constructors.length, vMax + 1), 0);
|
|
952
|
+
const osOf = q => (phone ? 30 : 34) + variantRows(q) * lead;
|
|
953
|
+
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),
|
|
954
|
+
...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);
|
|
955
|
+
const openH = p.requests.reduce((h, q) => h + osOf(q), 0);
|
|
956
|
+
const bodyH = Math.max(openH, barH(p.decisions) + (phone ? 34 : 30));
|
|
957
|
+
const top = y + (phone ? 18 : 20), bot = top + headH + bodyH + (phone ? 10 : 14);
|
|
958
|
+
const openY = []; { let yy = top + headH; for (const q of p.requests) { openY.push(yy + (phone ? 15 : 17)); yy += osOf(q); } }
|
|
959
|
+
const gapOf = new Set(p.gaps.map(g => g.constructor));
|
|
960
|
+
const og = phone ? 6 : 7, r = phone ? 9 : 12;
|
|
961
|
+
let edge = `M${f1(px1 - r)} ${f1(top)} Q${f1(px1)} ${f1(top)} ${f1(px1)} ${f1(top + r)}`;
|
|
962
|
+
for (const oy of openY) edge += ` V${f1(oy - og)} M${f1(px1)} ${f1(oy + og)}`;
|
|
963
|
+
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)}`;
|
|
964
|
+
// routes that only replay it to read where it is: named on hover, never drawn as starting it
|
|
965
|
+
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>` : '';
|
|
966
|
+
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}"/>` +
|
|
967
|
+
`<path d="${edge}" fill="none" stroke="${C.program}" stroke-width="2"/>`)));
|
|
968
|
+
const ix = px0 + (phone ? 8 : 12);
|
|
969
|
+
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 })));
|
|
970
|
+
// effects in the program's own package: marked; none: a struck ghost
|
|
971
|
+
const markW = phone ? 50 : 64, mx = px1 - (phone ? 8 : 12) - markW / 2, my = top + (phone ? 12 : 13);
|
|
972
|
+
if (!p.beside.length) out.push(tg([`pure:${p.id}`], shape('struck', `<title>${esc(`no effects in ${p.pkg}`)}</title>` +
|
|
973
|
+
`<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"/>` +
|
|
974
|
+
`<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"/>` +
|
|
975
|
+
T(mx - markW / 2 + 14, my, 'effects', { size: 9, fill: C.muted, mono: false, halo: false }))));
|
|
976
|
+
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>` +
|
|
977
|
+
`<circle cx="${f1(mx - markW / 2 + 6)}" cy="${f1(my - 3)}" r="4.5" fill="${C.guard}"/>` +
|
|
978
|
+
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 }))));
|
|
979
|
+
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 }));
|
|
980
|
+
// definitions: the package's own, as the layer stack counts them, and what the program reaches
|
|
981
|
+
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>` : '';
|
|
982
|
+
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('')));
|
|
983
|
+
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>` +
|
|
984
|
+
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('')));
|
|
985
|
+
// the program's decisions: a bar on the scale the engines' bars share, and its count
|
|
986
|
+
const by0 = top + headH + 4, bh = barH(p.decisions), dx = ix + bw + 6, dRoom = px1 - labW - (phone ? 14 : 20) - dx;
|
|
987
|
+
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"/>`) +
|
|
988
|
+
T(dx, by0 + 10, String(p.decisions), { size: phone ? 11 : 13, weight: 700, fill: C.program }) +
|
|
989
|
+
(dRoom >= tw('decisions', fsS, false) ? T(dx, by0 + 10 + lead, 'decisions', { size: fsS, fill: C.muted, mono: false }) : '')));
|
|
990
|
+
// lanes on the resource side
|
|
991
|
+
const k = p.engines.length;
|
|
992
|
+
const lx0 = px1 + (phone ? 24 : 70), lx1 = x1 - (phone ? 26 : 56);
|
|
993
|
+
const laneX = p.engines.map((_, i) => lx0 + (lx1 - lx0) * (i + 0.5) / Math.max(1, k));
|
|
994
|
+
const spacing = (lx1 - lx0) / Math.max(1, k);
|
|
995
|
+
// an engine's decision bar, and its count beside it, end short of the next lane (the bar no narrower than 3)
|
|
996
|
+
const forkW = Math.max(3, Math.min(bw, spacing - 9 - Math.max(...p.engines.map(e => tw(String(e.decisions), fsS, true, 700)))));
|
|
997
|
+
// 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
|
|
998
|
+
// (the arrow stays with what it gives, so a wrapped line never ends on it)
|
|
999
|
+
const sigOf = e => `${e.type.plainTakes.join(', ') || '()'} →\u00a0${e.type.yields}`;
|
|
1000
|
+
const codeOf = e => `${e.label}: ${e.type.takes.join(', ')} → ${e.type.returns}`;
|
|
1001
|
+
// above each lane: its package, then what it does on at most two lines, when every lane's words fit there
|
|
1002
|
+
const sigLines = e => wrapAt(sigOf(e), spacing - 6, fsS);
|
|
1003
|
+
const wide = p.engines.every(x => tw(x.pkgLabel, fsS) <= spacing - 6 && sigLines(x).length <= 2 && sigLines(x).every(l => tw(l, fsS) <= spacing - 6));
|
|
1004
|
+
const laneTop = top + headH - (phone ? 6 : 8);
|
|
1005
|
+
const resH = phone ? 15 : 18, resGap = phone ? 4 : 5;
|
|
1006
|
+
const laneBot = Math.max(bot, openY.length ? openY[openY.length - 1] + 14 : bot, ...p.engines.map(e => laneTop + barH(e.decisions) + 8));
|
|
1007
|
+
const footOf = e => e.resources.length * (resH + resGap) + (e.also.length ? lead + e.also.length * (resH + resGap) : 0);
|
|
1008
|
+
// lanes closer than a pill: every other lane's pills start below its neighbours'
|
|
1009
|
+
const cramped = spacing < needSp;
|
|
1010
|
+
const evenFoot = Math.max(0, ...p.engines.filter((_, i) => i % 2 === 0).map(footOf));
|
|
1011
|
+
const footY0 = i => laneBot + 6 + (cramped && i % 2 ? evenFoot + resGap : 0);
|
|
1012
|
+
const footBot = Math.max(laneBot + 6, ...p.engines.map((e, i) => footY0(i) + footOf(e)));
|
|
1013
|
+
// each loop's caption, one thought per line: which engine (when the lanes
|
|
1014
|
+
// carry numbers), how it repeats and what it replies, and what runs it
|
|
1015
|
+
const capsOf = (e, i) => {
|
|
1016
|
+
// how the rounds come round within one call, and which calls run them
|
|
1017
|
+
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';
|
|
1018
|
+
const reply = `replies ${e.replies.map(rs => RES_WORD[rs]).join(', ')}`;
|
|
1019
|
+
const lines = [];
|
|
1020
|
+
if (!wide) lines.push([`${i + 1} ${e.pkgLabel}: ${sigOf(e)}`]);
|
|
1021
|
+
lines.push(phone ? [how] : [`${how} · ${reply}`]);
|
|
1022
|
+
if (phone && e.replies.length) lines.push([reply]);
|
|
1023
|
+
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}`)]);
|
|
1024
|
+
return lines;
|
|
1025
|
+
};
|
|
1026
|
+
// a caption never reaches the next lane, whose loop comes down there: it wraps at its separators instead
|
|
1027
|
+
// later lanes come back into the program further left, so no loop crosses another's caption
|
|
1028
|
+
const txOf = i => px0 + (px1 - px0) * (0.3 + 0.5 * (k - i) / (k + 1));
|
|
1029
|
+
// it may start just right of the next lane's way back up
|
|
1030
|
+
const capLeft = i => i < k - 1 ? txOf(i + 1) + 8 : txOf(i);
|
|
1031
|
+
const capRoom = i => (i < k - 1 ? laneX[i + 1] - 6 : x1 - 8) - capLeft(i);
|
|
1032
|
+
const caps = p.engines.map((e, i) => capsOf(e, i).flatMap(([t, keys]) => wrapAt(t, capRoom(i), fsS).map(l => [l, keys])));
|
|
1033
|
+
const loopGap = phone ? 26 : 28;
|
|
1034
|
+
const loopY = []; { let yy = footBot + (phone ? 12 : 14); caps.forEach(c => { loopY.push(yy); yy += loopGap + c.length * lead; }); }
|
|
1035
|
+
p.requests.forEach((q, i) => {
|
|
1036
|
+
const oy = openY[i];
|
|
1037
|
+
const gap = gapOf.has(q.constructor);
|
|
1038
|
+
const col = gap ? C.faint : C.program;
|
|
1039
|
+
const lab = fitText(q.constructor, fs, labW), lw0 = Math.min(labW, tw(lab.s, lab.size));
|
|
1040
|
+
const lx = px1 - (phone ? 8 : 10);
|
|
1041
|
+
out.push(tg([gap ? `gap:${p.id}:${q.constructor}` : `request:${p.id}:${q.constructor}`],
|
|
1042
|
+
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"/>` +
|
|
1043
|
+
// a struck request's name and the lines that strike it are one label
|
|
1044
|
+
(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>' : '') +
|
|
1045
|
+
(q.fields.length ? fitT(lx, oy + fsS + 0.5, q.fields.join(', '), labW, { anchor: 'end', size: fsS, fill: C.muted }) : '') +
|
|
1046
|
+
(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"/>` +
|
|
1047
|
+
`<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>` : ''))));
|
|
1048
|
+
// the kinds of this request, from the sum type its payload carries: sub-openings under it
|
|
1049
|
+
let vy = oy + fsS + 0.5 + lead;
|
|
1050
|
+
for (const v of q.variants) {
|
|
1051
|
+
const shown = v.constructors.length > vMax + 1 ? v.constructors.slice(0, vMax) : v.constructors;
|
|
1052
|
+
const nm = v.namings && v.namings[0];
|
|
1053
|
+
const wordOf = (n, c) => (n.names.find(x => x.constructor === c) || {}).name;
|
|
1054
|
+
// one form for every kind of one type: kind and word when they all fit, else the words
|
|
1055
|
+
// 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
|
|
1056
|
+
const varW = Math.max(labW - 8, px1 - (phone ? 10 : 12) - (ix + bw + 6 + tw('decisions', fsS, false) + 10));
|
|
1057
|
+
const pairs = !!nm && shown.every(c => !wordOf(nm, c) || tw(`${c} → ${wordOf(nm, c)}`, fsS) <= varW);
|
|
1058
|
+
for (const c of shown) {
|
|
1059
|
+
// the kind, and the program's own word for it when a match gives one: both when they fit, else the word
|
|
1060
|
+
const word = nm && wordOf(nm, c);
|
|
1061
|
+
const both = word ? `${c} → ${word}` : c;
|
|
1062
|
+
const text = !word || pairs ? both : word;
|
|
1063
|
+
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>` : '';
|
|
1064
|
+
out.push(tg([`variant:${p.id}:${q.constructor}:${c}`, ...(word ? [`variantName:${p.id}:${q.constructor}:${c}`] : [])],
|
|
1065
|
+
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"/>` +
|
|
1066
|
+
fitT(lx, vy, text, pairs ? varW : labW - 8, { anchor: 'end', size: fsS, fill: word ? C.ink : C.ink2, weight: word ? 600 : 400 }))));
|
|
1067
|
+
vy += lead;
|
|
1068
|
+
}
|
|
1069
|
+
if (shown.length < v.constructors.length) {
|
|
1070
|
+
const rest = v.constructors.slice(shown.length);
|
|
1071
|
+
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 }))));
|
|
1072
|
+
vy += lead;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
for (const h of q.handledBy) {
|
|
1076
|
+
const li = p.engines.findIndex(e => e.id === h.engine);
|
|
1077
|
+
const wx = laneX[li];
|
|
1078
|
+
// 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
|
|
1079
|
+
const words = h.resources.map(rs => RES_WORD[rs]), wgap = 5;
|
|
1080
|
+
const wordsW = words.reduce((w, t) => w + tw(t, 9, false), 0) + wgap * Math.max(0, words.length - 1);
|
|
1081
|
+
const left = Math.max(px1 + 6, ...laneX.filter(x => x < wx).map(x => x + 2.5 + bw + 6));
|
|
1082
|
+
const said = words.length && wordsW <= wx - 6 - left;
|
|
1083
|
+
let wxR = wx - 6;
|
|
1084
|
+
const marks = said ? h.resources.slice().reverse().map(rs => { const t = RES_WORD[rs], w = tw(t, 9, false), x = wxR; wxR -= w + wgap;
|
|
1085
|
+
return T(x, oy - 4, t, { anchor: 'end', size: 9, weight: 700, fill: C[rs] || C.io, mono: false }); }).reverse().join('')
|
|
1086
|
+
: 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('');
|
|
1087
|
+
const engineLabel = p.engines[li].label;
|
|
1088
|
+
under.push(tg([`wire:${p.id}:${q.constructor}:${h.engine}`], shape('wire', `<title>${esc(`${q.constructor} → ${engineLabel}${words.length ? `: reaches ${words.join(', ')}` : ''}`)}</title>` +
|
|
1089
|
+
`<path d="M${f1(px1 + 1)} ${f1(oy)} H${f1(wx)}" stroke="${C.ink2}" stroke-width="1.5"/>`)));
|
|
1090
|
+
if (marks) over.push(tg([`wire:${p.id}:${q.constructor}:${h.engine}`], marks));
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
// the routes that start the program: a wire from their row into its top (wide pictures)
|
|
1094
|
+
const toRow = key => rows && rows.y(key);
|
|
1095
|
+
if (!phone && rows) starts.forEach(s => {
|
|
1096
|
+
const ry = toRow(s.entry);
|
|
1097
|
+
if (ry == null) return;
|
|
1098
|
+
const gx = rows.xr + 10 + (wireN++ % 5) * 5, ex = px1 - 24 - (wireN % 5) * 10, ty = top - (phone ? 16 : 20);
|
|
1099
|
+
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"/>` +
|
|
1100
|
+
`<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"/>`)));
|
|
1101
|
+
});
|
|
1102
|
+
p.engines.forEach((e, i) => {
|
|
1103
|
+
const lx = laneX[i];
|
|
1104
|
+
const title = `<title>${esc(`${e.id}${e.entries.length ? ` · ${e.entries.map(k => k.replace(/^route:/, '')).join(', ')}` : ''}`)}</title>`;
|
|
1105
|
+
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}"/>`)));
|
|
1106
|
+
// its package and name above the lane, or a number when the lanes are close
|
|
1107
|
+
if (wide) {
|
|
1108
|
+
const sl = sigLines(e), ly0 = laneTop - (phone ? 5 : 6) - (sl.length - 1) * lead;
|
|
1109
|
+
out.push(tg([`lane:${e.id}`, `engineType:${e.id}`], `<g><title>${esc(codeOf(e))}</title>` + T(lx, ly0 - lead, e.pkgLabel, { anchor: 'middle', size: fsS, fill: C.muted }) +
|
|
1110
|
+
sl.map((l, j) => T(lx, ly0 + j * lead, l, { anchor: 'middle', size: fsS, weight: 700, fill: C.ink })).join('') + '</g>'));
|
|
1111
|
+
} 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>'));
|
|
1112
|
+
// the engine's own decisions, on the program's scale; its bar gives up width so that the count fits before the next lane
|
|
1113
|
+
const eh = barH(e.decisions);
|
|
1114
|
+
over.push(tg([`laneFork:${e.id}`], shape('laneFork', `<title>${esc(`${e.decisions} decisions of its own`)}</title>` +
|
|
1115
|
+
(eh ? `<rect x="${f1(lx + 2.5)}" y="${f1(laneTop)}" width="${f1(forkW)}" height="${f1(eh)}" fill="${C.ink2}" opacity="0.55"/>` : '') +
|
|
1116
|
+
T(lx + 2.5 + forkW + 3, laneTop + 10, String(e.decisions), { size: fsS, weight: 700, fill: C.ink2 }))));
|
|
1117
|
+
// the routes that run it are named under its loop ("run by …"), not wired: one loop per lane is the only line back
|
|
1118
|
+
// the resources its arms reach, at its foot; then the ones it reaches for no request
|
|
1119
|
+
let yy = footY0(i);
|
|
1120
|
+
if (yy > laneBot + 6) under.push(`<path d="M${f1(lx)} ${f1(laneBot)} V${f1(yy)}" stroke="${C.ink2}" stroke-width="1.2"/>`);
|
|
1121
|
+
const pill = (rs, dashed, key) => {
|
|
1122
|
+
const w = pillW(rs);
|
|
1123
|
+
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"' : ''}/>`);
|
|
1124
|
+
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"' : ''}/>` +
|
|
1125
|
+
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 }))));
|
|
1126
|
+
yy += resH + resGap;
|
|
1127
|
+
};
|
|
1128
|
+
e.resources.forEach(rs => pill(rs, false, `laneResource:${e.id}:${rs}`));
|
|
1129
|
+
if (e.also.length) {
|
|
1130
|
+
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 })));
|
|
1131
|
+
yy += lead;
|
|
1132
|
+
e.also.forEach(rs => pill(rs, true, `laneAlso:${e.id}:${rs}`));
|
|
1133
|
+
}
|
|
1134
|
+
// the reply: one loop from the foot back into the program, one loop node on it
|
|
1135
|
+
const ly = loopY[i];
|
|
1136
|
+
const tx = txOf(i);
|
|
1137
|
+
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)}`;
|
|
1138
|
+
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"/>`;
|
|
1139
|
+
const nx = (tx + lx) / 2, nr = phone ? 5 : 6;
|
|
1140
|
+
const node = `<circle cx="${f1(nx)}" cy="${f1(ly)}" r="${nr + 3}" fill="${C.paper}"/>` + mark.loop(nx, ly, nr, C.program);
|
|
1141
|
+
// centred under the loop when it fits there, else from the loop's left end
|
|
1142
|
+
// every line of one caption set the same way: centred when they all fit there
|
|
1143
|
+
const centred = caps[i].every(([t]) => { const m = fitText(t, fsS, lx - tx - 8); return m.s === t && m.size === fsS; });
|
|
1144
|
+
const caption = (cy, text) => centred ? T(nx, cy, text, { anchor: 'middle', size: fsS, fill: C.program })
|
|
1145
|
+
: fitT(capLeft(i), cy, text, capRoom(i), { size: fsS, fill: C.program });
|
|
1146
|
+
const cy0 = ly + nr + (phone ? 11 : 13);
|
|
1147
|
+
const own = caps[i].filter(l => !l[1]), by = caps[i].filter(l => l[1]);
|
|
1148
|
+
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}` +
|
|
1149
|
+
own.map((l, j) => caption(cy0 + j * lead, l[0])).join(''))) +
|
|
1150
|
+
(by.length ? tg(by[0][1], by.map((l, j) => caption(cy0 + (own.length + j) * lead, l[0])).join('')) : ''));
|
|
1151
|
+
});
|
|
1152
|
+
y = (k ? loopY[k - 1] + loopGap + caps[k - 1].length * lead : footBot + (phone ? 12 : 14)) + (phone ? 6 : 10);
|
|
1153
|
+
}
|
|
1154
|
+
return { under: under.join(''), out: out.join(''), over: over.join(''), bottom: y };
|
|
1155
|
+
}
|
|
1156
|
+
|
|
841
1157
|
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
1158
|
// one caller's bracket: a line from the caller to a spine beside the wall, a tick to each opening
|
|
843
1159
|
function bracket(sx, sy, bx, y0, y1, ys) {
|
|
@@ -898,6 +1214,10 @@ function wallPathGaps(x0, x1, yTop, yBot, openings, r = 14) {
|
|
|
898
1214
|
// line; each package is a stretch starting with a tick and its name; the
|
|
899
1215
|
// parts' labels alternate above and below the line so it never wraps at
|
|
900
1216
|
// phone width. A package with many forks draws one fork with a count.
|
|
1217
|
+
// A gathered mark's count, said in words: what it counts, never a bare ×n.
|
|
1218
|
+
const SET_WORDS = { forkset: ['choice', 'choices'], dbset: ['db use', 'db uses'], genset: ['generic part', 'generic parts'], stepset: ['step', 'steps'] };
|
|
1219
|
+
export const setWord = (kind, n) => `${n} ${SET_WORDS[kind][n === 1 ? 0 : 1]}`;
|
|
1220
|
+
|
|
901
1221
|
export function quietItems(map, placed, { services = false, dense = false, phone = false, drawn = new Set(), socketFns = new Map(), genericById = new Map(), exportsLeft = [], strays = [] } = {}) {
|
|
902
1222
|
const pkgOf = id => String(id).split('::')[0];
|
|
903
1223
|
const bare = id => String(id).split('::').pop();
|
|
@@ -917,7 +1237,7 @@ export function quietItems(map, placed, { services = false, dense = false, phone
|
|
|
917
1237
|
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
1238
|
for (const [p, us] of strayBy) {
|
|
919
1239
|
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]) :
|
|
1240
|
+
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
1241
|
}
|
|
922
1242
|
// forks (and bare generic parts) of one package gathered under one mark with a count when the line would be long
|
|
923
1243
|
const total = [...list.values()].reduce((n, l) => n + l.length, 0);
|
|
@@ -926,10 +1246,10 @@ export function quietItems(map, placed, { services = false, dense = false, phone
|
|
|
926
1246
|
const g = P.get(pkg);
|
|
927
1247
|
const forks = ids2.filter(id => keyed.has(id) && !socketFns.has(id));
|
|
928
1248
|
let rest = ids2.filter(id => !forks.includes(id));
|
|
929
|
-
if (gather && forks.length > 1) g.parts.push({ kind: 'forkset', label:
|
|
1249
|
+
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
1250
|
else rest = rest.concat(forks);
|
|
931
1251
|
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:
|
|
1252
|
+
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
1253
|
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
1254
|
fork: keyed.has(id) ? keyed.get(id).types : null, gen: genericById.has(id) ? genericById.get(id).vars.join('') : null,
|
|
935
1255
|
keys: [...(socketFns.has(id) ? [`applied:${id}`] : []), ...(keyed.has(id) ? [`fork:${id}`] : []), ...(genericById.has(id) ? [`generic:${id}`] : []), `part:${id}`] });
|
|
@@ -942,11 +1262,11 @@ export function quietItems(map, placed, { services = false, dense = false, phone
|
|
|
942
1262
|
const of = k => all.filter(it => it.kind === k || (k === 'forkset' && it.fork && it.kind === 'part'));
|
|
943
1263
|
const parts = [];
|
|
944
1264
|
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:
|
|
1265
|
+
const n = list => list.reduce((c, it) => c + (it.n || 1), 0);
|
|
1266
|
+
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) });
|
|
1267
|
+
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
1268
|
parts.push(...others);
|
|
949
|
-
if (steps.length) parts.push({ kind: 'stepset', label:
|
|
1269
|
+
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
1270
|
return [{ pkg: '', exports: [], steps: [], parts, gathered: groups.length }];
|
|
951
1271
|
}
|
|
952
1272
|
|
|
@@ -956,13 +1276,17 @@ export function quietItems(map, placed, { services = false, dense = false, phone
|
|
|
956
1276
|
// above and below, so a label only has to clear the last one on its side.
|
|
957
1277
|
function placeOn(pkgs, lsx, phone, f, M) {
|
|
958
1278
|
const out = []; let x = lsx, lastUp = null, lastDown = null;
|
|
1279
|
+
// the risers that cross the line: the first x at or after x0 where [x + a, x + b] crosses none
|
|
1280
|
+
const bands = M.bands || [];
|
|
1281
|
+
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
1282
|
for (const g of pkgs) {
|
|
960
1283
|
// headers alternate between two tiers, so a tick clears the header two packages back
|
|
961
1284
|
const hdrBack = out.length >= 2 ? out[out.length - 2].tick + out[out.length - 2].hw : 0;
|
|
962
1285
|
// a tick from the upper tier runs down past the lower tier's header before it
|
|
963
1286
|
const hdrPrev = out.length % 2 === 1 ? out[out.length - 1].tick + out[out.length - 1].hw : 0;
|
|
964
|
-
const
|
|
1287
|
+
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
1288
|
const hw = M.header(g, f), sw = M.steps(g, f);
|
|
1289
|
+
const tick = shun(tick0, -(phone ? 4 : 6), Math.max(hw, sw));
|
|
966
1290
|
lastUp = { x: tick, w: phone ? 10 : 14 }; lastDown = sw ? { x: tick, w: sw } : null;
|
|
967
1291
|
let px = tick + (phone ? 9 : 14), flip = 0;
|
|
968
1292
|
const xs = [], sides = [];
|
|
@@ -973,6 +1297,7 @@ function placeOn(pkgs, lsx, phone, f, M) {
|
|
|
973
1297
|
let xx = px;
|
|
974
1298
|
if (last && !(side === 'up' && last.x === tick)) xx = Math.max(xx, last.x + last.w);
|
|
975
1299
|
if (side === 'down' && last && last.x === tick) xx = Math.max(xx, last.x + last.w + 2);
|
|
1300
|
+
xx = shun(xx, -(phone ? 6 : 7), M.width(it, f) - (phone ? 7 : 10));
|
|
976
1301
|
xs.push(xx); sides.push(side);
|
|
977
1302
|
const rec = { x: xx, w: M.width(it, f) };
|
|
978
1303
|
if (side === 'up') lastUp = rec; else lastDown = rec;
|
|
@@ -988,13 +1313,15 @@ function placeOn(pkgs, lsx, phone, f, M) {
|
|
|
988
1313
|
// The quiet line with its names at an angle: parts' names rise to the right
|
|
989
1314
|
// from their marks; each package's name, with its steps, falls to the right
|
|
990
1315
|
// from its tick. One line, however many parts. null when even this would not fit.
|
|
1316
|
+
// a gathered set of choices says what they choose on in its own words; anything else is its label
|
|
1317
|
+
const setLabel = it => it.kind === 'forkset' ? `${it.label} on ${it.fork.join(', ')}` : it.label;
|
|
991
1318
|
function angledQuiet(pkgs, lsx, lex, y0, phone, fsS) {
|
|
992
1319
|
const ang = 50, sin = Math.sin(ang * Math.PI / 180), cos = Math.cos(ang * Math.PI / 180);
|
|
993
1320
|
const pkgName = p => p.split('/').pop();
|
|
994
1321
|
// each name as its spans: [text, fill, attributes, how it is measured: mono, weight, italic]
|
|
995
1322
|
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]] : [])];
|
|
1323
|
+
[setLabel(it), C.ink, it.kind === 'part' ? '' : ' font-weight="700"', true, it.kind === 'part' ? 400 : 700],
|
|
1324
|
+
...(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
1325
|
const downSpans = g => [[pkgName(g.pkg), C.faint, ` font-family="${SANS}" font-weight="700"`, false, 700],
|
|
999
1326
|
...(g.steps.length ? [[' ' + g.steps.map(st => st.label).join(', '), C.hullInk, ' font-weight="700"', true, 700]] : [])];
|
|
1000
1327
|
const lay = pitch => {
|
|
@@ -1058,15 +1385,18 @@ function angledQuiet(pkgs, lsx, lex, y0, phone, fsS) {
|
|
|
1058
1385
|
return { items: pkgs.length, under, out, over, bottom: ry + downLen * sin + (phone ? 4 : 6) };
|
|
1059
1386
|
}
|
|
1060
1387
|
|
|
1061
|
-
export function layoutQuiet(pkgs, lsx, lex, y0, phone, fsS0) {
|
|
1388
|
+
export function layoutQuiet(pkgs, lsx, lex, y0, phone, fsS0, bands = []) {
|
|
1062
1389
|
pkgs = pkgs.filter(g => g.parts.length || g.steps.length || g.exports.length);
|
|
1063
1390
|
if (!pkgs.length) return { items: 0, under: [], out: [], over: [], bottom: y0 };
|
|
1064
1391
|
const pkgName = p => p.split('/').pop();
|
|
1065
1392
|
let fsS = fsS0;
|
|
1066
|
-
|
|
1393
|
+
// a gathered set of choices says what they choose on in its own line of words, with a wider gap after it
|
|
1394
|
+
// so the next count does not read as part of it; a single part keeps its type under it
|
|
1395
|
+
const sub = it => it.kind === 'forkset' ? '' : it.fork ? it.fork.join(',') : it.fns ? it.fns.join(', ') : '';
|
|
1067
1396
|
const M = {
|
|
1397
|
+
bands,
|
|
1068
1398
|
// 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
|
|
1399
|
+
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
1400
|
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
1401
|
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
1402
|
};
|
|
@@ -1122,15 +1452,16 @@ export function layoutQuiet(pkgs, lsx, lex, y0, phone, fsS0) {
|
|
|
1122
1452
|
const ny = up ? ry - (phone ? 7 : 9) : ry + (phone ? 13 : 16);
|
|
1123
1453
|
// a fork's types a line further out than the name, clear of it
|
|
1124
1454
|
const sy = ny + (up ? -1 : 1) * lineGap(fsS);
|
|
1125
|
-
|
|
1455
|
+
// a step set's count starts past its loop's arrowhead
|
|
1456
|
+
let gg = '', tx = it.kind === 'stepset' ? x + (phone ? 6 : 8) : x - 3;
|
|
1126
1457
|
if (it.kind === 'forkset') gg += mark.fork(x - (phone ? 3 : 4), ry, phone ? 0.75 : 0.9);
|
|
1127
1458
|
else if (it.kind === 'dbset') gg += mark.station(x, ry, phone ? 3.6 : 4.6, it.writes);
|
|
1128
1459
|
else if (it.kind === 'stepset') gg += mark.loop(x, ry - 0.5, phone ? 4.2 : 5.4);
|
|
1129
1460
|
else if (it.socket) gg += mark.socket(x, ry, phone ? 4.6 : 6);
|
|
1130
1461
|
else gg += mark.dot(x, ry, phone ? 3 : 3.8);
|
|
1131
1462
|
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 }); }
|
|
1463
|
+
gg += T(tx, ny, setLabel(it), { size: fsS, fill: it.kind === 'stepset' ? C.hullInk : C.ink, weight: it.kind === 'part' ? null : 700 });
|
|
1464
|
+
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
1465
|
else if (it.fns) gg += T(x - 3, sy, it.fns.join(', '), { size: fsS - 0.5, fill: C.hullInk, italic: true });
|
|
1135
1466
|
over.push(tg(it.keys, gg));
|
|
1136
1467
|
});
|
|
@@ -1229,9 +1560,11 @@ function renderLibrary(map, W, opts) {
|
|
|
1229
1560
|
steps.forEach((s, k) => {
|
|
1230
1561
|
const x = xs[k + 1];
|
|
1231
1562
|
decorate(s.id, x, py);
|
|
1563
|
+
// a step's names are centred on it, within its share of the chain and short of the package's edge
|
|
1564
|
+
const room = 2 * Math.min(dx / 2 - 2, x1 - (phone ? 8 : 12) - x);
|
|
1232
1565
|
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,
|
|
1566
|
+
if (fns && !phone) out.push(T(x, py - (phone ? 9 : 11), clip(fns.join(', '), 9, room - 4), { anchor: 'middle', size: 9, fill: C.hullInk }));
|
|
1567
|
+
{ 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
1568
|
});
|
|
1236
1569
|
drawn.add(c.value);
|
|
1237
1570
|
y = py + (phone ? 26 : 32) + gapY + (phone ? 6 : 8);
|
|
@@ -1510,7 +1843,7 @@ export function partFor(map, id) {
|
|
|
1510
1843
|
export function journeyLabel(map, key) {
|
|
1511
1844
|
if (key === 'quiet') return 'off the entry paths';
|
|
1512
1845
|
const b = map.bundles && map.bundles.find(x => x.id === key);
|
|
1513
|
-
if (b) return b.count > 1 ? `${b.label}
|
|
1846
|
+
if (b) return b.count > 1 ? `${b.label} (${b.count} routes)` : b.label;
|
|
1514
1847
|
return partFor(map, key).label;
|
|
1515
1848
|
}
|
|
1516
1849
|
|
|
@@ -1540,7 +1873,6 @@ function renderPart(map, partId, opts) {
|
|
|
1540
1873
|
const rh = phone ? 56 : 66;
|
|
1541
1874
|
const yTop = frameTop + (phone ? 16 : 20);
|
|
1542
1875
|
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
1876
|
const shared = n > 1;
|
|
1545
1877
|
const atPart = u => part.station ? u.at.node === 'entry' && u.at.entry === part.station : u.at.node === 'wrapper' && u.at.wrapper === part.id;
|
|
1546
1878
|
const uses = map.uses.map((u, i) => ({ ...u, i })).filter(u => u.kind === 'db' && atPart(u));
|
|
@@ -1548,17 +1880,52 @@ function renderPart(map, partId, opts) {
|
|
|
1548
1880
|
const rS = phone ? 5.4 : 6.6;
|
|
1549
1881
|
// columns: the casing holds the part; past it, the after band and the reply bars
|
|
1550
1882
|
// on a phone the casing gives up width to the after band
|
|
1551
|
-
const
|
|
1552
|
-
const
|
|
1553
|
-
const
|
|
1883
|
+
const k = phone ? 0.88 : 1, xr = x0 + cw * 0.96;
|
|
1884
|
+
const keyedById = new Map(map.keyed.map(k => [k.id, k]));
|
|
1885
|
+
const keyed = ids => [...new Set(ids)].filter(id => keyedById.has(id));
|
|
1886
|
+
const typesOf = ids => [...new Set(keyed(ids).flatMap(id => keyedById.get(id).types))].join(',');
|
|
1887
|
+
// The sockets and the checks after them take the room their labels' widest
|
|
1888
|
+
// pieces need, so that wrapping never has to clip one: the columns left of
|
|
1889
|
+
// the sockets give way down to three quarters of their width, then the
|
|
1890
|
+
// casing reaches into the band after it, which keeps room for its marks.
|
|
1891
|
+
const piece = s => (s ? widestPiece(s, fsS, true) : 0);
|
|
1892
|
+
const passed = part.station ? [] : callers.map(cl => cl.passed[0]).filter(Boolean);
|
|
1893
|
+
const checked = passed.filter(p => p.guards.length);
|
|
1894
|
+
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)));
|
|
1895
|
+
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)));
|
|
1896
|
+
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; }));
|
|
1897
|
+
let caseEnd = x0 + cw * 0.8 * k, xSock = x0 + cw * 0.6 * k, xPG = x0 + cw * 0.72 * k;
|
|
1898
|
+
if (!part.station) {
|
|
1899
|
+
const checkSpan = checked.length ? Math.max(caseEnd - xPG, checkNeed + 6) : 0;
|
|
1900
|
+
const sockSpan = Math.max((checked.length ? xPG : caseEnd) - xSock, sockNeed + (checked.length ? 8 : 6));
|
|
1901
|
+
// (only columns with nothing of the part's own to label give way)
|
|
1902
|
+
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);
|
|
1903
|
+
caseEnd = Math.max(caseEnd, Math.min(most, least + sockSpan + checkSpan));
|
|
1904
|
+
xPG = caseEnd - checkSpan;
|
|
1905
|
+
xSock = Math.max(least, Math.min(x0 + cw * 0.6 * k, xPG - sockSpan));
|
|
1906
|
+
}
|
|
1907
|
+
// the columns left of the sockets, in their share of what the sockets leave
|
|
1908
|
+
const c = p => x0 + (xSock - x0) * p / 0.6;
|
|
1909
|
+
const xMerge = c(0.08), xRead = c(0.22), xGuard = c(0.38), xSplit = c(0.5);
|
|
1554
1910
|
const stX = part.station ? xSock : xRead;
|
|
1555
1911
|
// the parameter every caller fixes drops into the check it decides, else the part's own station
|
|
1556
1912
|
const feedX = s => (s.guardsFed.length && part.guards.length ? xGuard : xRead);
|
|
1557
|
-
|
|
1913
|
+
// the casing's head lists them, those dropping furthest left first, each on
|
|
1914
|
+
// lines of its own reaching to the casing's edge: a later one's drop starts
|
|
1915
|
+
// at its own name, under the names that reach across it
|
|
1916
|
+
const lead = phone ? 12 : 13;
|
|
1917
|
+
const feedRows = [];
|
|
1918
|
+
{ let fy = yTop + (phone ? 20 : 23);
|
|
1919
|
+
for (const s of feeds.slice().sort((a, b) => feedX(a) - feedX(b))) {
|
|
1920
|
+
const gx = feedX(s), text = `${s.name}: ${String(s.type).split('::').pop()}`;
|
|
1921
|
+
const lines = wrapped(text, caseEnd - 6 - gx - 6, { size: fsS });
|
|
1922
|
+
feedRows.push({ s, gx, text, lines, y: fy });
|
|
1923
|
+
fy += lines.length * lead;
|
|
1924
|
+
} }
|
|
1925
|
+
const feedLines = feedRows.reduce((a, r) => a + r.lines.length, 0);
|
|
1926
|
+
const rowsTop = yTop + (phone ? 30 : 34) + (feeds.length ? (phone ? 34 : 39) + (feedLines - 1) * lead : 0);
|
|
1558
1927
|
// 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(',');
|
|
1928
|
+
const line = (y, k) => y + (phone ? 14 : 16) + (k - 1) * lead;
|
|
1562
1929
|
const forkLines = (ids, x, end) => (keyed(ids).length ? wrapped(typesOf(ids), end - x - 11, { size: fsS }) : []);
|
|
1563
1930
|
const entryOf = cl => map.entries.find(e => e.handlerId === cl.id || e.key === cl.id);
|
|
1564
1931
|
// Each caller's lines: its name over its path; under it its routes, then its
|
|
@@ -1578,7 +1945,8 @@ function renderPart(map, partId, opts) {
|
|
|
1578
1945
|
const tokX = roles.length === 1 ? lx + tw(roles[0], fsS, false, 700) + 8 : lx;
|
|
1579
1946
|
const tokSame = !!tokTxt && roles.length === 1 && tw(tokTxt, fsS) <= x0 - tokX - 6;
|
|
1580
1947
|
const toks = tokTxt ? (tokSame ? [tokTxt] : wrapped(tokTxt, x0 - lx - 6, { size: fsS })) : [];
|
|
1581
|
-
|
|
1948
|
+
// the roles and tokens start a line below the path's own (left empty without a route, for the lock)
|
|
1949
|
+
const left = (roles.length || toks.length ? Math.max(1, sub.length) : sub.length) + roles.length + (tokSame ? 0 : toks.length);
|
|
1582
1950
|
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
1951
|
if (!part.station) {
|
|
1584
1952
|
const p = cl.passed[0], pe = p ? p.effects : [];
|
|
@@ -1658,12 +2026,15 @@ function renderPart(map, partId, opts) {
|
|
|
1658
2026
|
} 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
2027
|
// under the part's own station, right of its riser: its fork, then its tables; they end short of its check's stem
|
|
1660
2028
|
const gx = part.station ? xRead : xGuard;
|
|
1661
|
-
|
|
1662
|
-
const
|
|
1663
|
-
|
|
2029
|
+
// 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
|
|
2030
|
+
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);
|
|
2031
|
+
const stEnd = part.station ? xr - 6 : part.guards.length && !short ? xGuard - 8 : xSplit - 4;
|
|
2032
|
+
const stLine = short ? 1 + Math.max(1, forkLines(part.guardIds || [], gx + 6, xSplit - 4).length) : 1;
|
|
2033
|
+
const stFork = forkLines([part.id], stX + 6, stEnd).length;
|
|
2034
|
+
forkAt([part.id], stX + 6, ty, stLine, stEnd);
|
|
1664
2035
|
if (part.effects.length) {
|
|
1665
2036
|
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,
|
|
2037
|
+
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
2038
|
} else if (part.station) over.push(mark.dot(stX, ty, 3.4));
|
|
1668
2039
|
if (part.guards.length) {
|
|
1669
2040
|
over.push(tg((part.guardIds || []).map(g => `guard:${part.id}:${g}`), mark.guard(gx, ty, phone ? 0.9 : 1.1)));
|
|
@@ -1674,11 +2045,16 @@ function renderPart(map, partId, opts) {
|
|
|
1674
2045
|
forkAt(part.guardIds || [], gx + 6, ty, 1, part.station ? stX - rS - 6 : xSplit - 4);
|
|
1675
2046
|
}
|
|
1676
2047
|
// the parameter every caller fixes drops in from above into the check it decides
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
2048
|
+
const dropped = new Set();
|
|
2049
|
+
for (const { gx, text, lines, y } of feedRows) {
|
|
2050
|
+
// one drop for each column, from above the casing, or from the first name in it the names to its left leave clear
|
|
2051
|
+
if (!dropped.has(gx)) {
|
|
2052
|
+
dropped.add(gx);
|
|
2053
|
+
const from = feedRows.some(r => r.gx < gx) ? y - fsS : yTop + 2;
|
|
2054
|
+
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"/>`);
|
|
2055
|
+
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}"/>`);
|
|
2056
|
+
}
|
|
2057
|
+
out.push(owned(casing, Tw(gx + 6, y, lines, { size: fsS, fill: C.ink2 }, text, lead)));
|
|
1682
2058
|
}
|
|
1683
2059
|
if (!part.station) callers.forEach((cl, i) => {
|
|
1684
2060
|
const y = ys[i];
|