@mafianacho/nacholens 0.0.0-stage → 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/LICENSE +202 -0
- package/NOTICE +13 -0
- package/README.de.md +46 -0
- package/README.fr.md +46 -0
- package/README.ja.md +46 -0
- package/README.ko.md +46 -0
- package/README.md +45 -2
- package/README.zh.md +46 -0
- package/demo/shop/v1/README.md +7 -0
- package/demo/shop/v1/api.js +78 -0
- package/demo/shop/v1/db.js +38 -0
- package/demo/shop/v1/gateway.js +50 -0
- package/demo/shop/v1/mock-external.js +17 -0
- package/demo/shop/v1/public/app.js +32 -0
- package/demo/shop/v1/public/format.js +2 -0
- package/demo/shop/v1/public/index.html +13 -0
- package/demo/shop/v1/public/styles.css +4 -0
- package/demo/shop/v2/README.md +7 -0
- package/demo/shop/v2/api.js +88 -0
- package/demo/shop/v2/db.js +38 -0
- package/demo/shop/v2/gateway.js +50 -0
- package/demo/shop/v2/mock-external.js +17 -0
- package/demo/shop/v2/public/app.js +33 -0
- package/demo/shop/v2/public/format.js +3 -0
- package/demo/shop/v2/public/index.html +13 -0
- package/demo/shop/v2/public/styles.css +5 -0
- package/docs/guide.ko.md +97 -0
- package/docs/guide.md +142 -0
- package/docs/model-schema.md +137 -0
- package/docs/review.md +78 -0
- package/examples/demo-shop.artifact.html +1535 -0
- package/examples/demo-shop.artifact.json +7 -0
- package/examples/demo-shop.html +1535 -0
- package/examples/external/README.md +12 -0
- package/examples/external/full-stack-fastapi-template/LICENSE +21 -0
- package/examples/external/full-stack-fastapi-template/changes.json +56 -0
- package/examples/external/full-stack-fastapi-template/full-stack-fastapi-template-backend.html +1535 -0
- package/models/demo-shop/changes.auto.json +121 -0
- package/models/demo-shop/changes.example.json +16 -0
- package/models/demo-shop/intent.example.json +75 -0
- package/models/demo-shop/model.json +3151 -0
- package/package.json +60 -4
- package/skills/nacholens/SKILL.md +235 -0
- package/src/cli.js +377 -0
- package/src/demo.js +42 -0
- package/src/diff.js +254 -0
- package/src/gitio.js +67 -0
- package/src/install.js +94 -0
- package/src/model.js +434 -0
- package/src/page.js +87 -0
- package/src/player/fonts/FONTLOG.md +30 -0
- package/src/player/fonts/OFL-Anton.txt +93 -0
- package/src/player/fonts/OFL-IBMPlex.txt +94 -0
- package/src/player/fonts/anton-400.woff2 +0 -0
- package/src/player/fonts/nacho-mono-500.woff2 +0 -0
- package/src/player/fonts/nacho-mono-600.woff2 +0 -0
- package/src/player/fonts/nacho-sans-400.woff2 +0 -0
- package/src/player/fonts/nacho-sans-500.woff2 +0 -0
- package/src/player/fonts/nacho-sans-600.woff2 +0 -0
- package/src/player/fonts/nacho-sans-700.woff2 +0 -0
- package/src/player/player.css +318 -0
- package/src/player/player.js +872 -0
- package/src/player/review.css +49 -0
- package/src/player/review.js +230 -0
- package/src/player/template.html +58 -0
- package/src/review.js +321 -0
- package/src/scan.js +255 -0
- package/src/template.js +36 -0
- package/src/verify.js +90 -0
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
/* nacholens player.
|
|
2
|
+
One stage with two scenes (the architecture, or the inside of one component) driven by one motion engine,
|
|
3
|
+
the list of flows with their steps, and the current step. Every piece of information has exactly one home:
|
|
4
|
+
flow/step names + change words + review ticks -> sidebar · what is moving now -> stage caption ·
|
|
5
|
+
narration, change reason, data, evidence -> inspector · planner/test/unmapped review -> review dock (review.js).
|
|
6
|
+
Model text is rendered with textContent only. Public API for review.js and tests: window.__nacholens. */
|
|
7
|
+
(function () {
|
|
8
|
+
"use strict";
|
|
9
|
+
var doc = document;
|
|
10
|
+
function $(id) { return doc.getElementById(id); }
|
|
11
|
+
var model = JSON.parse($("sv-model").textContent);
|
|
12
|
+
var changes = JSON.parse($("sv-changes").textContent);
|
|
13
|
+
var review = null;
|
|
14
|
+
try { review = $("sv-review-data") ? JSON.parse($("sv-review-data").textContent) : null; } catch (e) { review = null; }
|
|
15
|
+
var TARGET = (doc.documentElement && doc.documentElement.getAttribute && doc.documentElement.getAttribute("data-target")) || "html";
|
|
16
|
+
var reduce = !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
|
|
17
|
+
function now() { return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now(); }
|
|
18
|
+
function nextFrame(cb) { return typeof requestAnimationFrame === "function" ? requestAnimationFrame(cb) : setTimeout(function () { cb(now()); }, 16); }
|
|
19
|
+
function cancelFrame(id) { if (typeof cancelAnimationFrame === "function") cancelAnimationFrame(id); else clearTimeout(id); }
|
|
20
|
+
|
|
21
|
+
/* ═════════════════ DOM helpers (text nodes only) ═════════════════ */
|
|
22
|
+
var NS = "http://www.w3.org/2000/svg";
|
|
23
|
+
function h(tag, a) {
|
|
24
|
+
var e = doc.createElement(tag);
|
|
25
|
+
if (a) Object.keys(a).forEach(function (k) {
|
|
26
|
+
var v = a[k];
|
|
27
|
+
if (v == null || v === false) return;
|
|
28
|
+
if (k === "on") Object.keys(v).forEach(function (x) { e.addEventListener(x, v[x]); });
|
|
29
|
+
else if (k === "text") e.textContent = v;
|
|
30
|
+
else e.setAttribute(k, v === true ? "" : v);
|
|
31
|
+
});
|
|
32
|
+
for (var i = 2; i < arguments.length; i++) put(e, arguments[i]);
|
|
33
|
+
return e;
|
|
34
|
+
}
|
|
35
|
+
function put(e, c) {
|
|
36
|
+
if (c == null || c === false) return;
|
|
37
|
+
if (Array.isArray(c)) { c.forEach(function (x) { put(e, x); }); return; }
|
|
38
|
+
e.appendChild(typeof c === "object" ? c : doc.createTextNode(String(c)));
|
|
39
|
+
}
|
|
40
|
+
function s(tag, a, text) { var e = doc.createElementNS(NS, tag); Object.keys(a || {}).forEach(function (k) { e.setAttribute(k, a[k]); }); if (text != null) e.textContent = text; return e; }
|
|
41
|
+
function clear(e) { while (e.firstChild) e.removeChild(e.firstChild); }
|
|
42
|
+
function cls(e, name, on) { if (e && e.classList) e.classList[on ? "add" : "remove"](name); }
|
|
43
|
+
var P = {
|
|
44
|
+
check: "M5 12.5l4.5 4.5L19 7.5", play: "M7 4.5v15l12-7.5z", pause: "M8 5v14M16 5v14", prev: "M15 6l-6 6 6 6", next: "M9 6l6 6-6 6",
|
|
45
|
+
first: "M6 5v14M17 6l-7 6 7 6", replay: "M4 12a8 8 0 1 0 2.3-5.7M4 4v4h4", chevron: "m6 9 6 6 6-6", expand: "M14 4h6v6M10 20H4v-6M20 4l-6 6M4 20l6-6",
|
|
46
|
+
git: "M6 3v12M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM18 9a9 9 0 0 1-9 9",
|
|
47
|
+
client: "M8 3h8a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1ZM11 18h2",
|
|
48
|
+
gateway: "M12 3 4 7v5c0 4.4 3.4 8.3 8 9 4.6-.7 8-4.6 8-9V7l-8-4Z",
|
|
49
|
+
service: "M4 5h16v6H4zM4 13h16v6H4zM8 8h.01M8 16h.01",
|
|
50
|
+
datastore: "M12 3c4.4 0 8 1.3 8 3s-3.6 3-8 3-8-1.3-8-3 3.6-3 8-3ZM4 6v12c0 1.7 3.6 3 8 3s8-1.3 8-3V6M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3",
|
|
51
|
+
external: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM3 12h18M12 3c2.5 2.5 3.8 5.5 3.8 9s-1.3 6.5-3.8 9c-2.5-2.5-3.8-5.5-3.8-9S9.5 5.5 12 3Z"
|
|
52
|
+
};
|
|
53
|
+
function icon(n) { var e = s("svg", { "class": "i", viewBox: "0 0 24 24", "aria-hidden": "true" }); e.appendChild(s("path", { d: P[n] })); return e; }
|
|
54
|
+
|
|
55
|
+
/* ═════════════════ words (English by default; build --lang ko gives Korean) ═════════════════ */
|
|
56
|
+
var LANG = (function () { var l = doc.documentElement && doc.documentElement.getAttribute ? doc.documentElement.getAttribute("lang") : null; return String(l || "en").toLowerCase().indexOf("ko") === 0 ? "ko" : "en"; })();
|
|
57
|
+
var W = LANG === "ko" ? {
|
|
58
|
+
chg: { added: "신규", modified: "변경", removed: "삭제" },
|
|
59
|
+
kind: { client: "클라이언트", gateway: "게이트웨이", service: "서비스", datastore: "데이터 저장소", external: "외부 서비스" },
|
|
60
|
+
step: { request: "요청", response: "응답", call: "왕복 호출", internal: "내부 처리" },
|
|
61
|
+
role: { route: "라우트", handler: "핸들러", middleware: "미들웨어", service: "서비스", port: "포트", adapter: "어댑터", job: "작업", module: "모듈" },
|
|
62
|
+
op: { read: "읽기", insert: "추가", update: "수정", upsert: "추가·수정", delete: "삭제" },
|
|
63
|
+
leg: { req: "요청", res: "응답", int: "내부 호출" },
|
|
64
|
+
callers: "부르는 쪽", callees: "불리는 쪽", colEntry: "진입", colWork: "처리", colAdapter: "어댑터", tables: "테이블", endpoints: "엔드포인트",
|
|
65
|
+
whole: "전체 구조", inside: "내부",
|
|
66
|
+
hidden: function (n, arch) { return (arch ? "이 흐름과 무관한 컴포넌트 " : "이 흐름이 지나지 않는 항목 ") + n + "개는 숨김"; },
|
|
67
|
+
lookInside: function (x) { return x + " 내부 보기"; }, goInside: function (x) { return x + " 내부로 이동"; },
|
|
68
|
+
moreCols: function (n) { return "+" + n + "개 컬럼"; }, sends: "보냄", gets: "받음", details: function (x) { return x + " 자세히"; }, call: "호출",
|
|
69
|
+
idle: "대기", idleText: function (focus, canOpen) { return focus ? "재생하면 " + focus + " 안에서 데이터가 움직입니다" : "재생하면 요청이 컴포넌트 사이를 움직입니다" + (canOpen ? " · 상자를 누르면 내부로" : ""); },
|
|
70
|
+
out: "밖", outText: function (inPath, focus) { return inPath ? "이 단계의 " + focus + " 내부 경로는 모델에 없습니다" : "이 단계는 " + focus + " 밖에서 일어납니다"; },
|
|
71
|
+
work: "내부 처리", workText: function (x) { return x + " 안에서 처리"; },
|
|
72
|
+
first: "처음으로", prev: "이전 단계", next: "다음 단계", play: "재생", pause: "일시정지", replay: "다시 재생", toStep: function (n) { return n + "단계로"; }, speed: "재생 속도",
|
|
73
|
+
reviewed: "이 단계 리뷰함", gChanged: "이번에 바뀐 흐름", gSame: "바뀌지 않은 흐름", gAll: "흐름", reviewedTitle: "바뀐 단계 중 리뷰한 수", reviewBadge: function (a, b) { return "리뷰 " + a + "/" + b; },
|
|
74
|
+
noMatch: "맞는 흐름이 없습니다", overview: "흐름 개요", gone: "이번에 없어진 단계", howTo: "애니메이션 읽는 법", inStep: function (n) { return n + "단계에서 일어나는 일"; },
|
|
75
|
+
field: { label: "단계 이름", kind: "종류", route: "경로", request: "요청", response: "응답" }, before: "변경 전", moved: "내용은 같고 단계 순서만 바뀌었습니다.",
|
|
76
|
+
transforms: "데이터 모양이 바뀌는 곳", sent: "보내는 데이터", received: "받는 데이터", evidence: "근거 코드", changed: "바뀜", table: "테이블", endpoint: "엔드포인트",
|
|
77
|
+
backStep: function (n) { return "← " + n + "단계 설명으로"; }, backOverview: "← 흐름 개요로", insideOf: function (k, x) { return k + " · " + x + " 내부"; }, noDetail: "설명이 모델에 없습니다.",
|
|
78
|
+
newHere: "이번 작업에서 새로 생겼습니다", changedHere: "이번 작업에서 근거 코드가 바뀌었습니다", calledBy: "이 부품을 부르는 곳", calls: "이 부품이 부르는 곳", auth: "인증",
|
|
79
|
+
stepsHere: "이 흐름에서 지나는 단계", stepN: function (n) { return n + "단계"; },
|
|
80
|
+
lgReq: "요청이 이동 (주황 점)", lgRes: "응답이 돌아옴 (먹색 점)", lgWork: "컴포넌트 안에서 처리 중", lgPast: "이미 지나간 길", lgChanged: "이번 작업으로 생기거나 바뀐 것",
|
|
81
|
+
lgOpen: "상자를 누르면 내부로 들어가 같은 흐름을 봅니다", lgInt: "내부: 부품 사이 호출 (초록 점)",
|
|
82
|
+
ui: { overlay: "변경 강조", nav: "흐름과 단계", search: "흐름 검색", stage: "데이터 흐름 애니메이션", canvas: "아키텍처와 요청 흐름", transport: "재생 제어", review: "검토", insp: "현재 단계 설명" }
|
|
83
|
+
} : {
|
|
84
|
+
chg: { added: "New", modified: "Changed", removed: "Removed" },
|
|
85
|
+
kind: { client: "Client", gateway: "Gateway", service: "Service", datastore: "Data store", external: "External service" },
|
|
86
|
+
step: { request: "Request", response: "Response", call: "Round trip", internal: "Internal" },
|
|
87
|
+
role: { route: "Route", handler: "Handler", middleware: "Middleware", service: "Service", port: "Port", adapter: "Adapter", job: "Job", module: "Module" },
|
|
88
|
+
op: { read: "Read", insert: "Insert", update: "Update", upsert: "Upsert", delete: "Delete" },
|
|
89
|
+
leg: { req: "Request", res: "Response", int: "Internal call" },
|
|
90
|
+
callers: "Callers", callees: "Called", colEntry: "Entry", colWork: "Logic", colAdapter: "Adapters", tables: "Tables", endpoints: "Endpoints",
|
|
91
|
+
whole: "Architecture", inside: "inside",
|
|
92
|
+
hidden: function (n, arch) { return n + (arch ? (n === 1 ? " component" : " components") + " not on this flow hidden" : (n === 1 ? " item" : " items") + " this flow does not pass hidden"); },
|
|
93
|
+
lookInside: function (x) { return "Look inside " + x; }, goInside: function (x) { return "Go inside " + x; },
|
|
94
|
+
moreCols: function (n) { return "+" + n + (n === 1 ? " column" : " columns"); }, sends: "Req", gets: "Res", details: function (x) { return x + ": details"; }, call: "Call",
|
|
95
|
+
idle: "Idle", idleText: function (focus, canOpen) { return focus ? "Press play to watch the data move inside " + focus : "Press play to watch the request move between components" + (canOpen ? " · click a box to look inside" : ""); },
|
|
96
|
+
out: "Outside", outText: function (inPath, focus) { return inPath ? "The model has no path inside " + focus + " for this step" : "This step happens outside " + focus; },
|
|
97
|
+
work: "Working", workText: function (x) { return "Working inside " + x; },
|
|
98
|
+
first: "Back to the start", prev: "Previous step", next: "Next step", play: "Play", pause: "Pause", replay: "Replay", toStep: function (n) { return "Go to step " + n; }, speed: "Playback speed",
|
|
99
|
+
reviewed: "I reviewed this step", gChanged: "Changed in this work", gSame: "Not changed", gAll: "Flows", reviewedTitle: "Changed steps reviewed", reviewBadge: function (a, b) { return "Reviewed " + a + "/" + b; },
|
|
100
|
+
noMatch: "No matching flow", overview: "Flow overview", gone: "Steps removed in this work", howTo: "How to read the animation", inStep: function (n) { return "What happens in step " + n; },
|
|
101
|
+
field: { label: "Step name", kind: "Kind", route: "Route", request: "Request", response: "Response" }, before: "Before", moved: "Same content; only the step order changed.",
|
|
102
|
+
transforms: "Where the data changes shape", sent: "Data sent", received: "Data received", evidence: "Evidence", changed: "Changed", table: "Table", endpoint: "Endpoint",
|
|
103
|
+
backStep: function (n) { return "← Back to step " + n; }, backOverview: "← Back to the flow overview", insideOf: function (k, x) { return k + " · inside " + x; }, noDetail: "The model has no description.",
|
|
104
|
+
newHere: "New in this work", changedHere: "Its evidence code changed in this work", calledBy: "Called by", calls: "Calls", auth: "Authentication",
|
|
105
|
+
stepsHere: "Steps of this flow that pass here", stepN: function (n) { return "Step " + n; },
|
|
106
|
+
lgReq: "A request moves (orange dot)", lgRes: "The response comes back (ink dot)", lgWork: "Working inside a component", lgPast: "Path already taken", lgChanged: "Added or changed in this work",
|
|
107
|
+
lgOpen: "Click a box to see the same flow inside it", lgInt: "Inside: a call between parts (green dot)",
|
|
108
|
+
ui: null
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/* ═════════════════ vocabulary (one table for every status) ═════════════════ */
|
|
112
|
+
var ST = { added: ["success", W.chg.added], modified: ["warning", W.chg.modified], removed: ["danger", W.chg.removed] };
|
|
113
|
+
var KIND = W.kind, STEP_KO = W.step, ROLE_KO = W.role, OP_KO = W.op, LEG_KO = W.leg;
|
|
114
|
+
var LANE = { client: 0, gateway: 1, service: 2, datastore: 3, external: 3 };
|
|
115
|
+
|
|
116
|
+
/* ═════════════════ derived data ═════════════════ */
|
|
117
|
+
var comp = {}; model.components.forEach(function (c) { comp[c.id] = c; });
|
|
118
|
+
var ucById = {}; model.useCases.forEach(function (u) { ucById[u.id] = u; });
|
|
119
|
+
var stepChg = {}, compChg = {}, dchg = {};
|
|
120
|
+
if (changes) {
|
|
121
|
+
(changes.steps || []).forEach(function (c) { stepChg[c.useCase + "/" + c.step] = c; });
|
|
122
|
+
(changes.components || []).forEach(function (c) { compChg[c.id] = c; });
|
|
123
|
+
(changes.details || []).forEach(function (d) { dchg[d.component + "|" + d.kind + "|" + d.id] = d.change; });
|
|
124
|
+
}
|
|
125
|
+
var cmp = {}; if (review && review.compare) review.compare.useCases.forEach(function (u) { cmp[u.id] = u; });
|
|
126
|
+
function uc() { return ucById[S.uc]; }
|
|
127
|
+
function first(l) { return String(l).split("\n")[0]; }
|
|
128
|
+
function pathOf(st) { return [st.from].concat(st.via || [], [st.to]); }
|
|
129
|
+
function chgOf(u, st) { return (S.overlay && stepChg[u.id + "/" + st.id]) || null; }
|
|
130
|
+
function changedSteps(u) { return u.steps.filter(function (st) { return stepChg[u.id + "/" + st.id]; }); }
|
|
131
|
+
function hasDetail(c) { return !!(c && ((c.parts && c.parts.length) || (c.tables && c.tables.length) || (c.endpoints && c.endpoints.length))); }
|
|
132
|
+
function compTag(id) { // marked only when changed AND on a changed step of the selected flow
|
|
133
|
+
var c = compChg[id]; if (!c || !S.overlay) return null;
|
|
134
|
+
return changedSteps(uc()).some(function (st) { return pathOf(st).indexOf(id) >= 0; }) ? c.change : null;
|
|
135
|
+
}
|
|
136
|
+
function evRef(e) { return e.file + ":" + e.lines[0] + (e.lines[1] !== e.lines[0] ? "-" + e.lines[1] : ""); }
|
|
137
|
+
|
|
138
|
+
/* ═════════════════ state ═════════════════ */
|
|
139
|
+
function parseHash(raw) { var o = {}; String(raw || "").replace(/^#/, "").split("&").forEach(function (kv) { var i = kv.indexOf("="); if (i > 0) o[kv.slice(0, i)] = decodeURIComponent(kv.slice(i + 1)); }); return o; }
|
|
140
|
+
var H0 = parseHash(typeof location !== "undefined" ? location.hash : "");
|
|
141
|
+
var start = model.useCases.filter(function (u) { return changedSteps(u).length; })[0] || model.useCases[0];
|
|
142
|
+
var S = { uc: H0.uc && ucById[H0.uc] ? H0.uc : start.id, overlay: !!changes, filter: "", collapsed: { same: true }, item: null, zooming: false };
|
|
143
|
+
var KEY = "sv-player:" + ((changes && changes.ref) || model.product.id), ticks = {};
|
|
144
|
+
try { ticks = JSON.parse(localStorage.getItem(KEY) || "{}") || {}; } catch (e) { ticks = {}; }
|
|
145
|
+
function setTick(id, on) { if (on) ticks[id] = 1; else delete ticks[id]; try { localStorage.setItem(KEY, JSON.stringify(ticks)); } catch (e) { /* storage blocked: ticks last for this visit */ } }
|
|
146
|
+
|
|
147
|
+
/* ═════════════════ Stage: scenes ═════════════════
|
|
148
|
+
A scene is a table of boxes { id: {x, y, w, h, kind, ref} }: the architecture, or the inside of one component
|
|
149
|
+
(parts + wires / tables / endpoints) with its neighbours as dashed stubs. Paths inside come only from the model
|
|
150
|
+
(step.trace, step.data, step.endpoints); nothing is inferred. */
|
|
151
|
+
var G = { W: 800, H: 400, NW: 168, NH: 56 };
|
|
152
|
+
var SC = { mode: "arch", focus: null, pos: {}, hidden: 0, heads: [] };
|
|
153
|
+
function measure() { var st = $("sv-stage"); G.W = Math.max(480, st.clientWidth || 0); G.H = Math.max(260, st.clientHeight || 0); }
|
|
154
|
+
function ucComps(u) { var out = []; u.steps.forEach(function (st) { pathOf(st).forEach(function (id) { if (out.indexOf(id) < 0) out.push(id); }); }); return out; }
|
|
155
|
+
function archLayout() {
|
|
156
|
+
var used = ucComps(uc()); SC.pos = {}; SC.heads = [];
|
|
157
|
+
// scale rule: with many components only the ones this flow touches are drawn
|
|
158
|
+
var shown = model.components.length > 8 ? model.components.filter(function (c) { return used.indexOf(c.id) >= 0; }) : model.components.slice();
|
|
159
|
+
SC.hidden = model.components.length - shown.length;
|
|
160
|
+
var lanes = {}; shown.forEach(function (c) { var l = c.lane != null ? c.lane : LANE[c.kind]; (lanes[l] = lanes[l] || []).push(c); });
|
|
161
|
+
var keys = Object.keys(lanes).map(Number).sort(function (a, b) { return a - b; });
|
|
162
|
+
G.NW = Math.round(Math.max(140, Math.min(200, (G.W - 60) / keys.length - 28)));
|
|
163
|
+
var mx = G.NW / 2 + 32, span = keys.length > 1 ? (G.W - 2 * mx) / (keys.length - 1) : 0, top = 76, bottom = 28;
|
|
164
|
+
keys.forEach(function (k, li) {
|
|
165
|
+
var list = lanes[k], rows = {}, used2 = {}, next = 0;
|
|
166
|
+
list.forEach(function (c) { if (c.row != null) { rows[c.id] = c.row; used2[c.row] = 1; } });
|
|
167
|
+
list.forEach(function (c) { if (rows[c.id] == null) { while (used2[next]) next++; rows[c.id] = next; used2[next] = 1; } });
|
|
168
|
+
var order = Object.keys(used2).map(Number).sort(function (a, b) { return a - b; }), n = order.length;
|
|
169
|
+
var gap = n > 1 ? Math.min(108, Math.max(G.NH + 18, (G.H - top - bottom - G.NH) / (n - 1))) : 0;
|
|
170
|
+
list.forEach(function (c) {
|
|
171
|
+
var i = order.indexOf(rows[c.id]);
|
|
172
|
+
SC.pos[c.id] = { x: keys.length > 1 ? mx + li * span : G.W / 2, y: top + (G.H - top - bottom) / 2 + (i - (n - 1) / 2) * gap, w: G.NW, h: G.NH, lane: li, kind: "node", ref: c };
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function traceIn(st, cid) { return (st.trace || []).filter(function (r) { return r.split("/")[0] === cid; }).map(function (r) { return "p:" + r.split("/")[1]; }); }
|
|
177
|
+
function targetsIn(st, c) { // the cards a step reaches inside a datastore / third party
|
|
178
|
+
if (c.tables) return (st.data || []).filter(function (d) { return d.store === c.id; }).map(function (d) { return "t:" + d.table; });
|
|
179
|
+
if (c.endpoints) return (st.endpoints || []).filter(function (e) { return e.split("/")[0] === c.id; }).map(function (e) { return "e:" + e.split("/")[1]; });
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
function detailLayout() {
|
|
183
|
+
var c = comp[SC.focus], u = uc(), top = 98, bottom = 26, pad = 18, stubW = 104, stubH = 44;
|
|
184
|
+
SC.pos = {}; SC.heads = []; SC.hidden = 0;
|
|
185
|
+
// neighbours this flow connects to the focus: earlier lanes on the left (callers), later lanes on the right (callees)
|
|
186
|
+
var nb = [];
|
|
187
|
+
u.steps.forEach(function (st) { var p = pathOf(st); p.forEach(function (id, i) { if (id !== c.id) return; [p[i - 1], p[i + 1]].forEach(function (n) { if (n && n !== c.id && nb.indexOf(n) < 0) nb.push(n); }); }); });
|
|
188
|
+
var me = LANE[c.kind], left = nb.filter(function (id) { return LANE[comp[id].kind] < me; }), right = nb.filter(function (id) { return LANE[comp[id].kind] >= me; });
|
|
189
|
+
function stubs(list, x, head) {
|
|
190
|
+
if (!list.length) return;
|
|
191
|
+
SC.heads.push({ x: x, text: head });
|
|
192
|
+
var gap = Math.min(92, (G.H - top - bottom) / list.length);
|
|
193
|
+
list.forEach(function (id, i) { SC.pos["stub:" + id] = { x: x, y: top + (G.H - top - bottom) / 2 + (i - (list.length - 1) / 2) * gap, w: stubW, h: stubH, kind: "stub", ref: comp[id] }; });
|
|
194
|
+
}
|
|
195
|
+
stubs(left, pad + stubW / 2, W.callers);
|
|
196
|
+
stubs(right, G.W - pad - stubW / 2, W.callees);
|
|
197
|
+
var inL = left.length ? pad + stubW + 28 : pad, inR = right.length ? G.W - pad - stubW - 28 : G.W - pad;
|
|
198
|
+
if (c.parts && c.parts.length) {
|
|
199
|
+
var byId = {}; c.parts.forEach(function (p) { byId[p.id] = p; });
|
|
200
|
+
var order = []; u.steps.forEach(function (st) { traceIn(st, c.id).forEach(function (id) { if (order.indexOf(id) < 0 && byId[id.slice(2)]) order.push(id); }); });
|
|
201
|
+
var list = (order.length ? order.map(function (id) { return byId[id.slice(2)]; }) : c.parts.slice()).slice(0, 14);
|
|
202
|
+
SC.hidden = c.parts.length - list.length; // scale rule: only the parts this flow passes
|
|
203
|
+
var cols = [[W.colEntry, ["route", "handler", "middleware"]], [W.colWork, ["service", "module", "job", "port"]], [W.colAdapter, ["adapter"]]]
|
|
204
|
+
.map(function (g) { return { name: g[0], items: list.filter(function (p) { return g[1].indexOf(p.role) >= 0; }) }; })
|
|
205
|
+
.filter(function (g) { return g.items.length; });
|
|
206
|
+
var colW = (inR - inL) / cols.length, cw = Math.max(124, Math.min(200, colW - 30)), ch = 58;
|
|
207
|
+
cols.forEach(function (col, ci) {
|
|
208
|
+
var x = inL + colW * (ci + 0.5), n = col.items.length, gap = n > 1 ? Math.min(86, Math.max(ch + 12, (G.H - top - bottom - ch) / (n - 1))) : 0; // room for a transform chip when there is space
|
|
209
|
+
SC.heads.push({ x: x, text: col.name });
|
|
210
|
+
col.items.forEach(function (p, i) { SC.pos["p:" + p.id] = { x: x, y: top + (G.H - top - bottom) / 2 + (i - (n - 1) / 2) * gap, w: cw, h: ch, kind: "part", ref: p, col: ci }; });
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
var used = {}; u.steps.forEach(function (st) { targetsIn(st, c).forEach(function (id) { used[id] = 1; }); });
|
|
215
|
+
var cards = c.tables ? c.tables.map(function (t) { return { id: "t:" + t.name, kind: "table", ref: t, lines: t.columns.length }; })
|
|
216
|
+
: c.endpoints.map(function (e) { return { id: "e:" + e.id, kind: "endpoint", ref: e, lines: 2 }; });
|
|
217
|
+
cards.sort(function (a, b) { return (used[b.id] ? 1 : 0) - (used[a.id] ? 1 : 0); }); // the ones this flow reaches first
|
|
218
|
+
var avail = inR - inL, ncol = Math.max(1, Math.min(3, Math.floor(avail / 240))), cw2 = avail / ncol - 18, all = (c.tables || c.endpoints).length;
|
|
219
|
+
cards = cards.slice(0, ncol * 3); SC.hidden = all - cards.length;
|
|
220
|
+
var maxLines = 6, rowsH = [], total = 0;
|
|
221
|
+
for (var pass = 0; pass < 3; pass++) {
|
|
222
|
+
cards.forEach(function (cd) { cd.show = Math.min(cd.lines, maxLines); cd.h = 46 + cd.show * 17 + (cd.lines > cd.show ? 18 : 0); });
|
|
223
|
+
rowsH = []; cards.forEach(function (cd, i) { var r = Math.floor(i / ncol); rowsH[r] = Math.max(rowsH[r] || 0, cd.h); });
|
|
224
|
+
total = rowsH.reduce(function (a, b) { return a + b; }, 0) + 14 * Math.max(0, rowsH.length - 1);
|
|
225
|
+
if (total <= G.H - top - bottom) break;
|
|
226
|
+
maxLines = maxLines > 3 ? 3 : 1;
|
|
227
|
+
}
|
|
228
|
+
var y = top + Math.max(0, (G.H - top - bottom - total) / 2);
|
|
229
|
+
rowsH.forEach(function (rh, r) {
|
|
230
|
+
cards.slice(r * ncol, r * ncol + ncol).forEach(function (cd, k) { SC.pos[cd.id] = { x: inL + (k + 0.5) * (avail / ncol), y: y + cd.h / 2, w: cw2, h: cd.h, kind: cd.kind, ref: cd.ref, show: cd.show }; });
|
|
231
|
+
y += rh + 14;
|
|
232
|
+
});
|
|
233
|
+
SC.heads.push({ x: (inL + inR) / 2, text: c.tables ? W.tables : W.endpoints });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* ═════════════════ Stage: geometry (computed here, so the engine needs no SVG measuring) ═════════════════ */
|
|
237
|
+
function edge(id, tx, ty) {
|
|
238
|
+
var p = SC.pos[id], dx = tx - p.x, dy = ty - p.y, hw = p.w / 2 + 3, hh = p.h / 2 + 3;
|
|
239
|
+
if (!dx && !dy) return { x: p.x, y: p.y };
|
|
240
|
+
var t = Math.min(dx ? hw / Math.abs(dx) : 1e9, dy ? hh / Math.abs(dy) : 1e9);
|
|
241
|
+
return { x: p.x + dx * t, y: p.y + dy * t };
|
|
242
|
+
}
|
|
243
|
+
function bend(a, b) {
|
|
244
|
+
var A0 = SC.pos[a], B0 = SC.pos[b];
|
|
245
|
+
if (SC.mode === "arch" && Math.abs(A0.lane - B0.lane) > 1) return { x: (A0.x + B0.x) / 2, y: (A0.y + B0.y) / 2 > G.H / 2 ? Math.max(A0.y, B0.y) + 110 : Math.min(A0.y, B0.y) - 110 };
|
|
246
|
+
if (SC.mode === "detail" && Math.abs(A0.x - B0.x) < 1 && Math.abs(A0.y - B0.y) > 1.7 * A0.h) return { x: A0.x + A0.w / 2 + 48, y: (A0.y + B0.y) / 2 }; // same column, not adjacent: go around
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
function hopPts(a, b) {
|
|
250
|
+
var A0 = SC.pos[a], B0 = SC.pos[b], c = bend(a, b);
|
|
251
|
+
if (!c) return [edge(a, B0.x, B0.y), edge(b, A0.x, A0.y)];
|
|
252
|
+
var q0 = edge(a, c.x, c.y), q1 = edge(b, c.x, c.y), out = [];
|
|
253
|
+
for (var i = 0; i <= 20; i++) { var t = i / 20, v = 1 - t; out.push({ x: v * v * q0.x + 2 * v * t * c.x + t * t * q1.x, y: v * v * q0.y + 2 * v * t * c.y + t * t * q1.y }); }
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
function routePts(ids) { var pts = []; ids.slice(1).forEach(function (b, i) { pts = pts.concat(hopPts(ids[i], b)); }); return pts; }
|
|
257
|
+
function r1(v) { return Math.round(v * 10) / 10; }
|
|
258
|
+
function dOf(pts) { return pts.map(function (p, i) { return (i ? "L" : "M") + r1(p.x) + " " + r1(p.y); }).join(" "); }
|
|
259
|
+
function lengths(pts) { var acc = [0]; for (var i = 1; i < pts.length; i++) acc.push(acc[i - 1] + Math.hypot(pts[i].x - pts[i - 1].x, pts[i].y - pts[i - 1].y)); return acc; }
|
|
260
|
+
function pointAt(pts, acc, d) {
|
|
261
|
+
if (d <= 0) return pts[0];
|
|
262
|
+
for (var i = 1; i < pts.length; i++) if (acc[i] >= d) { var k = acc[i] === acc[i - 1] ? 0 : (d - acc[i - 1]) / (acc[i] - acc[i - 1]); return { x: pts[i - 1].x + (pts[i].x - pts[i - 1].x) * k, y: pts[i - 1].y + (pts[i].y - pts[i - 1].y) * k }; }
|
|
263
|
+
return pts[pts.length - 1];
|
|
264
|
+
}
|
|
265
|
+
// a tag box around centred text: the rendered width when the browser can measure it (the embedded fonts are wider
|
|
266
|
+
// or narrower than the estimate), the estimate otherwise; `pad` on each side so the text never touches the edge
|
|
267
|
+
// (and, given `max`, shortened with … until the measured text fits)
|
|
268
|
+
function measure(text) { try { return text.getComputedTextLength ? text.getComputedTextLength() || 0 : 0; } catch (e) { return 0; } }
|
|
269
|
+
function fitPad(rect, text, cx, est, pad, max) {
|
|
270
|
+
var tw = measure(text);
|
|
271
|
+
if (max && tw && tw + 2 * pad > max) {
|
|
272
|
+
var str = String(text.textContent).replace(/…$/, "");
|
|
273
|
+
while (str.length > 1 && tw + 2 * pad > max) { str = str.slice(0, -1); text.textContent = str.replace(/\s+$/, "") + "…"; tw = measure(text); }
|
|
274
|
+
est = Math.min(est, tw);
|
|
275
|
+
}
|
|
276
|
+
var w = Math.max(est, tw) + 2 * pad;
|
|
277
|
+
rect.setAttribute("x", cx - w / 2); rect.setAttribute("width", w);
|
|
278
|
+
}
|
|
279
|
+
function textW(str, px) { var w = 0; for (var i = 0; i < str.length; i++) w += str.charCodeAt(i) > 0x2e80 ? px : px * 0.6; return w; }
|
|
280
|
+
function clip(str, maxW, px) { str = String(str); if (textW(str, px) <= maxW) return str; var o = ""; for (var i = 0; i < str.length && textW(o + str[i] + "…", px) <= maxW; i++) o += str[i]; return o + "…"; }
|
|
281
|
+
function wrap2(str, maxW, px) { // one line if it fits, else break at a space, a slash or a camelCase hump, then clip
|
|
282
|
+
str = String(str); if (textW(str, px) <= maxW) return [str];
|
|
283
|
+
var cut = 0;
|
|
284
|
+
for (var i = 1; i < str.length; i++) {
|
|
285
|
+
var brk = str[i] === " " || str[i] === "(" || str[i - 1] === "/" || (/[a-z]/.test(str[i - 1]) && /[A-Z]/.test(str[i]));
|
|
286
|
+
if (brk && textW(str.slice(0, i), px) <= maxW) cut = i;
|
|
287
|
+
}
|
|
288
|
+
if (!cut) while (cut < str.length && textW(str.slice(0, cut + 1), px) <= maxW) cut++;
|
|
289
|
+
return [str.slice(0, cut).trim(), clip(str.slice(cut).trim(), maxW, px)];
|
|
290
|
+
}
|
|
291
|
+
function itemName(id) {
|
|
292
|
+
var p = SC.pos[id]; if (!p) return "";
|
|
293
|
+
if (p.kind === "node" || p.kind === "stub") return first(p.ref.label);
|
|
294
|
+
if (p.kind === "part") return p.ref.label;
|
|
295
|
+
if (p.kind === "table") return p.ref.name;
|
|
296
|
+
return p.ref.method + " " + p.ref.path;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* ═════════════════ Stage: legs (what moves, per scene) ═════════════════ */
|
|
300
|
+
function mv(ids, tone, text) { return { type: "move", ids: ids, tone: tone, text: text || "" }; }
|
|
301
|
+
function legsOf(st) { return SC.mode === "arch" ? archLegs(st) : detailLegs(st); }
|
|
302
|
+
function archLegs(st) {
|
|
303
|
+
var p = pathOf(st).filter(function (id) { return SC.pos[id]; });
|
|
304
|
+
if (st.kind === "internal") return SC.pos[st.from] ? [{ type: "work", at: st.from }] : [];
|
|
305
|
+
if (p.length < 2) return [];
|
|
306
|
+
if (st.kind === "call") return [mv(p, "req", st.request), mv(p.slice().reverse(), "res", st.response)];
|
|
307
|
+
return [mv(p, st.kind === "response" ? "res" : "req", st.kind === "response" ? st.response : st.request)];
|
|
308
|
+
}
|
|
309
|
+
function wireBetween(a, b) { return (comp[SC.focus].wires || []).filter(function (w) { var f = "p:" + w.from, t = "p:" + w.to; return (f === a && t === b) || (f === b && t === a); })[0]; }
|
|
310
|
+
function detailLegs(st) {
|
|
311
|
+
var c = comp[SC.focus]; if (pathOf(st).indexOf(c.id) < 0) return [];
|
|
312
|
+
var stub = function (id) { return SC.pos["stub:" + id] ? "stub:" + id : null; };
|
|
313
|
+
if (c.parts && c.parts.length) {
|
|
314
|
+
var T = traceIn(st, c.id).filter(function (id) { return SC.pos[id]; });
|
|
315
|
+
if (st.kind === "internal") {
|
|
316
|
+
if (T.length > 1) return [mv(T, "int", T.slice(1).map(function (b, i) { var w = wireBetween(T[i], b); return w ? w.label : itemName(b); }).join(" → "))];
|
|
317
|
+
return T.length ? [{ type: "work", at: T[0] }] : [];
|
|
318
|
+
}
|
|
319
|
+
if (!T.length) return [];
|
|
320
|
+
// the neighbour on each side of the focus in this step's path (a gateway in `via` counts)
|
|
321
|
+
var p = pathOf(st), i = p.indexOf(c.id), inS = i > 0 ? stub(p[i - 1]) : null, outS = i < p.length - 1 ? stub(p[i + 1]) : null;
|
|
322
|
+
var ids = (inS ? [inS] : []).concat(T, outS ? [outS] : []);
|
|
323
|
+
if (ids.length < 2) return [];
|
|
324
|
+
if (st.kind === "call") return [mv(ids, "req", st.request), mv(ids.slice().reverse(), "res", st.response)];
|
|
325
|
+
return [mv(ids, st.kind === "response" ? "res" : "req", st.kind === "response" ? st.response : st.request)];
|
|
326
|
+
}
|
|
327
|
+
var p2 = pathOf(st), j = p2.indexOf(c.id), other = j > 0 ? p2[j - 1] : p2[j + 1];
|
|
328
|
+
var tg = targetsIn(st, c).filter(function (id) { return SC.pos[id]; })[0], s0 = other ? stub(other) : null;
|
|
329
|
+
if (!tg || !s0) return [];
|
|
330
|
+
if (st.kind === "call") return [mv([s0, tg], "req", st.request), mv([tg, s0], "res", st.response)];
|
|
331
|
+
return [mv(st.to === c.id ? [s0, tg] : [tg, s0], st.kind === "response" ? "res" : "req", st.kind === "response" ? st.response : st.request)];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* ═════════════════ Stage: drawing ═════════════════ */
|
|
335
|
+
var L = {}, NODE = {}, LINK = {};
|
|
336
|
+
function drawStage() {
|
|
337
|
+
measure();
|
|
338
|
+
var svg = $("sv-canvas"); clear(svg);
|
|
339
|
+
svg.setAttribute("viewBox", "0 0 " + G.W + " " + G.H);
|
|
340
|
+
["links", "trail", "flow", "packet", "nodes", "notes", "fx", "rings"].forEach(function (k) { L[k] = s("g", { "class": "layer-" + k }); svg.appendChild(L[k]); });
|
|
341
|
+
NODE = {}; LINK = {};
|
|
342
|
+
if (SC.mode === "arch") { archLayout(); drawArch(); } else { detailLayout(); drawDetail(); }
|
|
343
|
+
cls($("sv-stage"), "in-detail", SC.mode === "detail");
|
|
344
|
+
var cr = $("sv-crumbs"); clear(cr); cr.hidden = SC.mode !== "detail";
|
|
345
|
+
if (SC.mode === "detail") put(cr, [h("button", { type: "button", "class": "back", on: { click: closeDetail } }, icon("prev"), W.whole), h("span", { "class": "here", text: first(comp[SC.focus].label) }), h("span", { "class": "sub", text: W.inside })]);
|
|
346
|
+
var stage = $("sv-stage"), old = stage.lastHiddenNote; if (old && old.parentNode) old.parentNode.removeChild(old);
|
|
347
|
+
stage.lastHiddenNote = null;
|
|
348
|
+
if (SC.hidden > 0) stage.lastHiddenNote = stage.appendChild(h("div", { "class": "hidden-note", text: W.hidden(SC.hidden, SC.mode === "arch") }));
|
|
349
|
+
}
|
|
350
|
+
function addLink(key, el) { (LINK[key] = LINK[key] || []).push(el); L.links.appendChild(el); }
|
|
351
|
+
function keyOf(a, b) { return [a, b].sort().join("|"); }
|
|
352
|
+
function changeTag(g, x, y, w, change) {
|
|
353
|
+
if (!change) return;
|
|
354
|
+
cls(g, "chg-" + change, true);
|
|
355
|
+
var tw = Math.max(32, textW(ST[change][1], 11) + 12), rc = g.appendChild(s("rect", { "class": "tagbg", y: y - 9, height: 18, rx: 4 }));
|
|
356
|
+
var tx = g.appendChild(s("text", { "class": "tagtx", x: x + w - 8 - tw / 2, y: y + 4 }, ST[change][1]));
|
|
357
|
+
fitPad(rc, tx, x + w - 8 - tw / 2, tw - 12, 6);
|
|
358
|
+
}
|
|
359
|
+
function clickable(g, label, fn) {
|
|
360
|
+
g.setAttribute("tabindex", "0"); g.setAttribute("role", "button"); g.setAttribute("aria-label", label);
|
|
361
|
+
g.addEventListener("click", fn);
|
|
362
|
+
g.addEventListener("keydown", function (e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); e.stopPropagation(); fn(); } });
|
|
363
|
+
}
|
|
364
|
+
function drawArch() {
|
|
365
|
+
model.links.forEach(function (l) { if (SC.pos[l.a] && SC.pos[l.b]) addLink(keyOf(l.a, l.b), s("path", { "class": "lk", d: dOf(hopPts(l.a, l.b)) })); });
|
|
366
|
+
Object.keys(SC.pos).forEach(function (id) {
|
|
367
|
+
var p = SC.pos[id], c = p.ref, x = p.x - p.w / 2, y = p.y - p.h / 2, g = s("g", { "class": "nd", "data-id": id, "data-kind": c.kind }), open = hasDetail(c);
|
|
368
|
+
g.appendChild(s("rect", { "class": "box", x: x, y: y, width: p.w, height: p.h, rx: 10 }));
|
|
369
|
+
g.appendChild(s("rect", { "class": "ibox", x: x + 10, y: p.y - 15, width: 30, height: 30, rx: 8 }));
|
|
370
|
+
g.appendChild(s("path", { "class": "ico", d: P[c.kind], transform: "translate(" + (x + 16) + "," + (p.y - 9) + ") scale(.75)" }));
|
|
371
|
+
var ln = wrap2(first(c.label), p.w - (open ? 72 : 56), 14), y1 = ln.length > 1 ? p.y - 11 : p.y - 2;
|
|
372
|
+
ln.forEach(function (t, i) { g.appendChild(s("text", { "class": "nm", x: x + 48, y: y1 + i * 16 }, t)); });
|
|
373
|
+
g.appendChild(s("text", { "class": "kd", x: x + 48, y: ln.length > 1 ? p.y + 20 : p.y + 14 }, KIND[c.kind]));
|
|
374
|
+
if (open) {
|
|
375
|
+
cls(g, "has-detail", true);
|
|
376
|
+
g.appendChild(s("path", { "class": "open", d: P.expand, transform: "translate(" + (x + p.w - 24) + "," + (p.y - 7) + ") scale(.58)" }));
|
|
377
|
+
g.appendChild(s("title", {}, W.lookInside(first(c.label))));
|
|
378
|
+
clickable(g, W.lookInside(first(c.label)), function () { openDetail(id); });
|
|
379
|
+
}
|
|
380
|
+
changeTag(g, x, y, p.w, compTag(id));
|
|
381
|
+
NODE[id] = g; L.nodes.appendChild(g);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
function drawDetail() {
|
|
385
|
+
var c = comp[SC.focus];
|
|
386
|
+
SC.heads.forEach(function (hd) { L.links.appendChild(s("text", { "class": "colh", x: hd.x, y: 82 }, hd.text)); });
|
|
387
|
+
(c.wires || []).forEach(function (w) {
|
|
388
|
+
var a = "p:" + w.from, b = "p:" + w.to; if (!SC.pos[a] || !SC.pos[b]) return;
|
|
389
|
+
var ch = S.overlay ? dchg[c.id + "|wire|" + w.from + ">" + w.to] : null;
|
|
390
|
+
addLink(keyOf(a, b), s("path", { "class": "wr" + (ch ? " chg-" + ch : ""), d: dOf(hopPts(a, b)) }));
|
|
391
|
+
});
|
|
392
|
+
var seen = {}; // the edges to the neighbours: only those a step of this flow actually crosses
|
|
393
|
+
uc().steps.forEach(function (st) {
|
|
394
|
+
detailLegs(st).forEach(function (lg) {
|
|
395
|
+
if (lg.type !== "move") return;
|
|
396
|
+
lg.ids.slice(1).forEach(function (b, i) {
|
|
397
|
+
var a = lg.ids[i], k = keyOf(a, b);
|
|
398
|
+
if (seen[k] || LINK[k]) return;
|
|
399
|
+
seen[k] = 1; addLink(k, s("path", { "class": "wr stubwire", d: dOf(hopPts(a, b)) }));
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
Object.keys(SC.pos).forEach(function (id) { drawItem(id, SC.pos[id], c); });
|
|
404
|
+
}
|
|
405
|
+
function drawItem(id, p, c) {
|
|
406
|
+
var g = s("g", { "class": "it " + p.kind, "data-id": id }), x = p.x - p.w / 2, y = p.y - p.h / 2, r = p.ref;
|
|
407
|
+
g.appendChild(s("rect", { "class": "box", x: x, y: y, width: p.w, height: p.h, rx: 8 }));
|
|
408
|
+
if (p.kind === "stub") {
|
|
409
|
+
g.appendChild(s("text", { "class": "t1", x: p.x, y: p.y - 2 }, clip(first(r.label), p.w - 16, 14)));
|
|
410
|
+
g.appendChild(s("text", { "class": "t2", x: p.x, y: p.y + 14 }, KIND[r.kind]));
|
|
411
|
+
if (hasDetail(r)) { cls(g, "nav", true); g.appendChild(s("title", {}, W.goInside(first(r.label)))); clickable(g, W.goInside(first(r.label)), function () { switchFocus(r.id); }); }
|
|
412
|
+
} else if (p.kind === "part") {
|
|
413
|
+
var ln = wrap2(r.label, p.w - 22, 14), y1 = ln.length > 1 ? p.y - 11 : p.y - 3;
|
|
414
|
+
ln.forEach(function (t, i) { g.appendChild(s("text", { "class": "t1", x: x + 11, y: y1 + i * 16 }, t)); });
|
|
415
|
+
g.appendChild(s("text", { "class": "t2", x: x + 11, y: ln.length > 1 ? p.y + 20 : p.y + 14 }, clip((ROLE_KO[r.role] || r.role) + (r.connects && comp[r.connects] ? " → " + first(comp[r.connects].label) : ""), p.w - 22, 11)));
|
|
416
|
+
changeTag(g, x, y, p.w, S.overlay ? dchg[c.id + "|part|" + r.id] : null);
|
|
417
|
+
} else if (p.kind === "table") {
|
|
418
|
+
g.appendChild(s("text", { "class": "t1 mono", x: x + 12, y: y + 21 }, clip(r.name, p.w - 70, 14)));
|
|
419
|
+
g.appendChild(s("line", { "class": "hr", x1: x + 1, x2: x + p.w - 1, y1: y + 31, y2: y + 31 }));
|
|
420
|
+
g.colEls = {};
|
|
421
|
+
r.columns.slice(0, p.show).forEach(function (col, i) {
|
|
422
|
+
var cy = y + 31 + 17 * (i + 1);
|
|
423
|
+
g.colEls[col.name] = g.appendChild(s("text", { "class": "colname", "data-col": col.name, x: x + 12, y: cy }, clip(col.name, p.w * 0.6, 11.5)));
|
|
424
|
+
if (col.type) g.appendChild(s("text", { "class": "coltype", x: x + p.w - 12, y: cy }, col.type));
|
|
425
|
+
});
|
|
426
|
+
if (r.columns.length > p.show) g.appendChild(s("text", { "class": "collab", x: x + 12, y: y + 33 + 17 * (p.show + 1) }, W.moreCols(r.columns.length - p.show)));
|
|
427
|
+
changeTag(g, x, y, p.w, S.overlay ? dchg[c.id + "|table|" + r.name] : null);
|
|
428
|
+
} else {
|
|
429
|
+
var mth = s("g", { "class": "mth" });
|
|
430
|
+
mth.appendChild(s("rect", { x: x + 10, y: y + 10, width: 46, height: 18, rx: 4 }));
|
|
431
|
+
mth.appendChild(s("text", { x: x + 33, y: y + 23 }, r.method));
|
|
432
|
+
g.appendChild(mth);
|
|
433
|
+
g.appendChild(s("text", { "class": "t1 mono", x: x + 64, y: y + 23 }, clip(r.path, p.w - 122, 14)));
|
|
434
|
+
g.appendChild(s("line", { "class": "hr", x1: x + 1, x2: x + p.w - 1, y1: y + 36, y2: y + 36 }));
|
|
435
|
+
[[W.sends, r.request], [W.gets, r.response]].forEach(function (row, i) {
|
|
436
|
+
var cy = y + 36 + 17 * (i + 1);
|
|
437
|
+
g.appendChild(s("text", { "class": "collab", x: x + 12, y: cy }, row[0]));
|
|
438
|
+
g.appendChild(s("text", { "class": "colname", x: x + 44, y: cy }, clip(row[1] && row[1].length ? row[1].map(function (f) { return f.name; }).join(", ") : "—", p.w - 56, 11.5)));
|
|
439
|
+
});
|
|
440
|
+
changeTag(g, x, y, p.w, S.overlay ? dchg[c.id + "|endpoint|" + r.id] : null);
|
|
441
|
+
}
|
|
442
|
+
if (p.kind !== "stub") clickable(g, W.details(itemName(id)), function () { selectItem(id); });
|
|
443
|
+
g.opb = g.appendChild(s("g", { "class": "opb" }));
|
|
444
|
+
NODE[id] = g; L.nodes.appendChild(g);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/* ═════════════════ Stage: state of the current step (classes only; no re-draw) ═════════════════ */
|
|
448
|
+
function idsOf(lg) { return lg.type === "move" ? lg.ids : [lg.at]; }
|
|
449
|
+
function paintStepState() {
|
|
450
|
+
var u = uc(), cur = A.step >= 0 ? u.steps[A.step] : null, used = {}, usedK = {}, vis = {}, visK = {}, act = {}, work = {};
|
|
451
|
+
u.steps.forEach(function (st, i) {
|
|
452
|
+
var past = i < A.step || (i === A.step && A.finished);
|
|
453
|
+
legsOf(st).forEach(function (lg) {
|
|
454
|
+
var ids = idsOf(lg);
|
|
455
|
+
ids.forEach(function (id) { used[id] = 1; if (past) vis[id] = 1; });
|
|
456
|
+
ids.slice(1).forEach(function (b, j) { var k = keyOf(ids[j], b); usedK[k] = 1; if (past) visK[k] = 1; });
|
|
457
|
+
if (i !== A.step) return;
|
|
458
|
+
if (lg.type === "work") work[lg.at] = 1;
|
|
459
|
+
else ids.forEach(function (id, j) { if (SC.mode === "detail" || j === 0 || j === ids.length - 1) act[id] = 1; });
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
Object.keys(NODE).forEach(function (id) {
|
|
463
|
+
var g = NODE[id];
|
|
464
|
+
cls(g, "off", !used[id] && id.indexOf("stub:") !== 0);
|
|
465
|
+
cls(g, "visited", !!vis[id]); cls(g, "active", !!act[id]); cls(g, "working", !!work[id]); cls(g, "sel", S.item === id); cls(g, "hitcard", false);
|
|
466
|
+
if (g.colEls) Object.keys(g.colEls).forEach(function (k) { cls(g.colEls[k], "col-hit", false); });
|
|
467
|
+
if (g.opb) clear(g.opb);
|
|
468
|
+
});
|
|
469
|
+
Object.keys(LINK).forEach(function (k) { LINK[k].forEach(function (el) { cls(el, "off", !usedK[k]); cls(el, "visited", !!visK[k]); }); });
|
|
470
|
+
clear(L.notes);
|
|
471
|
+
if (SC.mode !== "detail" || !cur) return;
|
|
472
|
+
var c = comp[SC.focus];
|
|
473
|
+
(cur.data || []).filter(function (d) { return d.store === c.id; }).forEach(function (d) { markCard("t:" + d.table, OP_KO[d.op] || d.op, d.columns); });
|
|
474
|
+
(cur.endpoints || []).filter(function (e) { return e.split("/")[0] === c.id; }).forEach(function (e) { markCard("e:" + e.split("/")[1], W.call); });
|
|
475
|
+
// where the data changes shape on this step (step.transforms), under the part that converts it
|
|
476
|
+
(cur.transforms || []).forEach(function (tf) {
|
|
477
|
+
var at = tf.at.split("/"), p = at[0] === c.id ? SC.pos["p:" + at[1]] : null; if (!p) return;
|
|
478
|
+
// "in → out" when it fits beside the neighbours, else "→ out" (the full pair is in the panel), clipped only as a last resort
|
|
479
|
+
var room = chipRoom(p, p.y + p.h / 2 + 6, 20), full = tf["in"].type + " → " + tf.out.type;
|
|
480
|
+
var text = clip(textW(full, 11) <= room - 18 ? full : "→ " + tf.out.type, room - 18, 11), g = s("g", { "class": "xchip" });
|
|
481
|
+
var rc = g.appendChild(s("rect", { y: p.y + p.h / 2 + 6, height: 20, rx: 4 }));
|
|
482
|
+
var tx = g.appendChild(s("text", { x: p.x, y: p.y + p.h / 2 + 20 }, text));
|
|
483
|
+
L.notes.appendChild(g);
|
|
484
|
+
if (text === full && measure(tx) + 18 > room) { tx.textContent = text = clip("→ " + tf.out.type, room - 18, 11); }
|
|
485
|
+
fitPad(rc, tx, p.x, textW(text, 11), 9, room);
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
// widest centred tag under box p that stays clear of every other box beside it and of the stage edge
|
|
489
|
+
function chipRoom(p, top, hgt) {
|
|
490
|
+
var half = Math.min(p.x, G.W - p.x) - 4;
|
|
491
|
+
Object.keys(SC.pos).forEach(function (id) {
|
|
492
|
+
var q = SC.pos[id]; if (q === p || q.y + q.h / 2 < top - 4 || q.y - q.h / 2 > top + hgt + 4) return;
|
|
493
|
+
var gap = q.x > p.x ? q.x - q.w / 2 - p.x : p.x - (q.x + q.w / 2);
|
|
494
|
+
if (gap > 0) half = Math.min(half, gap - 6);
|
|
495
|
+
});
|
|
496
|
+
return Math.max(p.w, 2 * half);
|
|
497
|
+
}
|
|
498
|
+
function markCard(id, label, cols) {
|
|
499
|
+
var g = NODE[id], p = SC.pos[id]; if (!g || !p) return;
|
|
500
|
+
cls(g, "hitcard", true);
|
|
501
|
+
var w = textW(label, 11) + 16;
|
|
502
|
+
var rc = g.opb.appendChild(s("rect", { y: p.y - p.h / 2 + 9, height: 18, rx: 4 }));
|
|
503
|
+
var tx = g.opb.appendChild(s("text", { x: p.x + p.w / 2 - w / 2 - 10, y: p.y - p.h / 2 + 22 }, label));
|
|
504
|
+
fitPad(rc, tx, p.x + p.w / 2 - w / 2 - 10, textW(label, 11), 8);
|
|
505
|
+
(cols || []).forEach(function (n) { if (g.colEls && g.colEls[n]) cls(g.colEls[n], "col-hit", true); });
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/* ═════════════════ Stage: motion engine ═════════════════
|
|
509
|
+
A step is a list of legs. move = the packet travels a route; work = one box processes (progress bar).
|
|
510
|
+
Duration follows route length and speed; each leg eases in and out; a continuous run keeps a faint trail. */
|
|
511
|
+
var SPEED = 300, DWELL = 240, GAP = 260, WORK = 950, EMPTY = 700, HOLD = 900;
|
|
512
|
+
var A = { step: -1, legs: [], leg: 0, t: 0, dur: 1, playing: false, running: false, raf: 0, timer: 0, last: 0, rate: 1, finished: true };
|
|
513
|
+
var MOVE = null; // current move leg geometry: { pts, acc, len, trail, packet }
|
|
514
|
+
function ease(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }
|
|
515
|
+
function stopClock() { cancelFrame(A.raf); clearTimeout(A.timer); A.running = false; }
|
|
516
|
+
function clearMotion() { [L.flow, L.packet, L.fx].forEach(function (g) { if (g) clear(g); }); MOVE = null; }
|
|
517
|
+
function startLeg(i, t0) {
|
|
518
|
+
var lg = A.legs[i]; A.leg = i; A.t = t0 || 0; clearMotion();
|
|
519
|
+
if (lg.type === "move") {
|
|
520
|
+
var pts = routePts(lg.ids), acc = lengths(pts), d = dOf(pts);
|
|
521
|
+
L.flow.appendChild(s("path", { "class": "flowline " + lg.tone, d: d }));
|
|
522
|
+
var trail = L.trail.appendChild(s("path", { "class": "trail " + lg.tone, d: d, "stroke-dasharray": "0 99999" }));
|
|
523
|
+
var pk = s("g", { "class": "pk " + lg.tone }); pk.appendChild(s("circle", { "class": "halo", r: 7 })); pk.appendChild(s("circle", { "class": "core", r: 6 }));
|
|
524
|
+
L.packet.appendChild(pk);
|
|
525
|
+
MOVE = { pts: pts, acc: acc, len: acc[acc.length - 1], trail: trail, packet: pk, tone: lg.tone, x: pts[0].x, y: pts[0].y };
|
|
526
|
+
A.dur = Math.max(520, Math.min(1700, MOVE.len / SPEED * 1000)) / A.rate;
|
|
527
|
+
} else {
|
|
528
|
+
var p = SC.pos[lg.at], w = p.w - 24;
|
|
529
|
+
L.fx.appendChild(s("rect", { "class": "wbar-bg", x: p.x - w / 2, y: p.y + p.h / 2 + 8, width: w, height: 4, rx: 2 }));
|
|
530
|
+
MOVE = { bar: L.fx.appendChild(s("rect", { "class": "wbar", x: p.x - w / 2, y: p.y + p.h / 2 + 8, width: 0, height: 4, rx: 2 })), w: w };
|
|
531
|
+
A.dur = WORK / A.rate;
|
|
532
|
+
}
|
|
533
|
+
frameDraw(); // place the packet before the first frame
|
|
534
|
+
caption();
|
|
535
|
+
if (reduce) { A.t = 1; frameDraw(); legEnd(); return; }
|
|
536
|
+
A.running = true; A.last = now(); A.raf = nextFrame(frame);
|
|
537
|
+
}
|
|
538
|
+
function frame(ts) {
|
|
539
|
+
if (!A.running) return;
|
|
540
|
+
A.t = Math.min(1, A.t + Math.max(0, ts - A.last) / A.dur); A.last = ts;
|
|
541
|
+
frameDraw();
|
|
542
|
+
if (A.t >= 1) { A.running = false; legEnd(); } else A.raf = nextFrame(frame);
|
|
543
|
+
}
|
|
544
|
+
function frameDraw() {
|
|
545
|
+
if (!MOVE) return;
|
|
546
|
+
var e = ease(A.t);
|
|
547
|
+
if (MOVE.pts) {
|
|
548
|
+
var pt = pointAt(MOVE.pts, MOVE.acc, e * MOVE.len); MOVE.x = pt.x; MOVE.y = pt.y;
|
|
549
|
+
MOVE.packet.setAttribute("transform", "translate(" + r1(pt.x) + "," + r1(pt.y) + ")");
|
|
550
|
+
MOVE.trail.setAttribute("stroke-dasharray", r1(e * MOVE.len) + " 99999");
|
|
551
|
+
} else MOVE.bar.setAttribute("width", r1(MOVE.w * e));
|
|
552
|
+
progress();
|
|
553
|
+
}
|
|
554
|
+
function legEnd() {
|
|
555
|
+
var lg = A.legs[A.leg];
|
|
556
|
+
if (lg.type === "move") {
|
|
557
|
+
var p = SC.pos[lg.ids[lg.ids.length - 1]];
|
|
558
|
+
if (!reduce) {
|
|
559
|
+
var rg = L.rings.appendChild(s("rect", { "class": "ring " + lg.tone, x: p.x - p.w / 2 - 3, y: p.y - p.h / 2 - 3, width: p.w + 6, height: p.h + 6, rx: 11 }));
|
|
560
|
+
rg.addEventListener("animationend", function () { if (rg.parentNode) rg.parentNode.removeChild(rg); });
|
|
561
|
+
}
|
|
562
|
+
if (MOVE && MOVE.trail) cls(MOVE.trail, "past", true);
|
|
563
|
+
}
|
|
564
|
+
if (A.leg < A.legs.length - 1) {
|
|
565
|
+
if (reduce) { startLeg(A.leg + 1); return; }
|
|
566
|
+
A.timer = setTimeout(function () { startLeg(A.leg + 1); }, DWELL / A.rate);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
stepFinished();
|
|
570
|
+
}
|
|
571
|
+
function stepFinished() {
|
|
572
|
+
A.finished = true; paintStepState(); progress(); transport(); renderSide();
|
|
573
|
+
if (A.playing && A.step < uc().steps.length - 1) A.timer = setTimeout(function () { go(A.step + 1, true); }, (reduce ? HOLD : GAP) / A.rate);
|
|
574
|
+
else if (A.playing) { A.playing = false; transport(); }
|
|
575
|
+
}
|
|
576
|
+
function go(i, animate) { // the only way the current step changes
|
|
577
|
+
stopClock(); clearMotion();
|
|
578
|
+
var u = uc(), target = Math.max(-1, Math.min(i, u.steps.length - 1)), cont = target === A.step + 1 && A.finished && animate;
|
|
579
|
+
A.step = target; A.finished = !animate || target < 0; A.legs = A.step >= 0 ? legsOf(u.steps[A.step]) : []; A.leg = 0; A.t = 0; S.item = null;
|
|
580
|
+
if (!cont) { clear(L.trail); clear(L.rings); } // a continuous run keeps the faint trail of earlier legs; any jump starts clean
|
|
581
|
+
paintStepState(); caption(); renderSide(); renderInsp(); transport(); syncHash();
|
|
582
|
+
if (A.step >= 0 && animate) {
|
|
583
|
+
if (A.legs.length) startLeg(0);
|
|
584
|
+
else if (reduce) stepFinished();
|
|
585
|
+
else A.timer = setTimeout(stepFinished, EMPTY / A.rate); // nothing moves in this scene: hold briefly, then go on
|
|
586
|
+
} else progress();
|
|
587
|
+
}
|
|
588
|
+
function play() {
|
|
589
|
+
var n = uc().steps.length;
|
|
590
|
+
if (A.playing) { pause(); return; }
|
|
591
|
+
A.playing = true;
|
|
592
|
+
if ((A.step >= n - 1 && A.finished) || A.step < 0) { go(0, true); return; }
|
|
593
|
+
if (!A.finished && A.legs.length) { transport(); if (MOVE && A.t < 1) { A.running = true; A.last = now(); A.raf = nextFrame(frame); } else legEnd(); return; } // resume mid-leg
|
|
594
|
+
go(A.step + 1, true);
|
|
595
|
+
}
|
|
596
|
+
function pause() { A.playing = false; stopClock(); transport(); }
|
|
597
|
+
function next() { A.playing = false; go(A.step + 1, true); }
|
|
598
|
+
function prev() { A.playing = false; go(A.step - 1, false); }
|
|
599
|
+
function caption() {
|
|
600
|
+
var c = $("sv-caption"), u = uc(), lg = A.legs[A.leg], focus = SC.mode === "detail" ? first(comp[SC.focus].label) : null, kids;
|
|
601
|
+
if (A.step < 0) kids = [h("span", { "class": "leg idle" }, W.idle), h("span", { "class": "txt kr", text: W.idleText(focus, model.components.some(hasDetail)) })];
|
|
602
|
+
else if (!lg) kids = [h("span", { "class": "leg idle" }, W.out), h("span", { "class": "txt kr", text: W.outText(pathOf(u.steps[A.step]).indexOf(SC.focus) >= 0, focus) })];
|
|
603
|
+
else if (lg.type === "work") kids = [h("span", { "class": "leg int" }, W.work), h("span", { "class": "txt kr", text: W.workText(itemName(lg.at)) })];
|
|
604
|
+
else kids = [h("span", { "class": "leg " + lg.tone }, LEG_KO[lg.tone] + " → " + itemName(lg.ids[lg.ids.length - 1])), h("span", { "class": "txt mono ell", text: lg.text })];
|
|
605
|
+
if (reduce) { clear(c); put(c, kids); return; }
|
|
606
|
+
cls(c, "swap", true);
|
|
607
|
+
clearTimeout(c.swapTimer);
|
|
608
|
+
c.swapTimer = setTimeout(function () { clear(c); put(c, kids); cls(c, "swap", false); }, 110);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/* ═════════════════ Stage: entering and leaving a component ═════════════════ */
|
|
612
|
+
function tween(ms, fn, done) {
|
|
613
|
+
if (reduce) { fn(1); if (done) done(); return; }
|
|
614
|
+
var t0 = now();
|
|
615
|
+
(function f(ts) { var k = Math.min(1, Math.max(0, (ts - t0) / ms)); fn(ease(k)); if (k < 1) nextFrame(f); else if (done) done(); })(t0);
|
|
616
|
+
}
|
|
617
|
+
function lerpRect(a, b, k) { return { x: a.x + (b.x - a.x) * k, y: a.y + (b.y - a.y) * k, w: a.w + (b.w - a.w) * k, h: a.h + (b.h - a.h) * k }; }
|
|
618
|
+
function setRect(el, r) { el.setAttribute("x", r1(r.x)); el.setAttribute("y", r1(r.y)); el.setAttribute("width", r1(r.w)); el.setAttribute("height", r1(r.h)); }
|
|
619
|
+
function fadeLayers(v) { [L.links, L.trail, L.nodes, L.notes, L.rings].forEach(function (g) { g.style.opacity = String(v); }); }
|
|
620
|
+
function fadeIn() { var cv = $("sv-canvas"); cls(cv, "fade-in", false); if (cv.getBoundingClientRect) cv.getBoundingClientRect(); cls(cv, "fade-in", true); }
|
|
621
|
+
function boxOf(p) { return { x: p.x - p.w / 2, y: p.y - p.h / 2, w: p.w, h: p.h }; }
|
|
622
|
+
function openDetail(id) {
|
|
623
|
+
if (SC.mode !== "arch" || S.zooming || !hasDetail(comp[id]) || !SC.pos[id]) return;
|
|
624
|
+
var resume = A.playing, from = boxOf(SC.pos[id]), to = { x: 8, y: 8, w: G.W - 16, h: G.H - 16 };
|
|
625
|
+
A.playing = false; stopClock(); clearMotion(); S.zooming = true;
|
|
626
|
+
var z = $("sv-canvas").appendChild(s("rect", { "class": "zoom", rx: 10 }));
|
|
627
|
+
tween(360, function (k) { setRect(z, lerpRect(from, to, k)); fadeLayers(1 - k); }, function () {
|
|
628
|
+
SC.mode = "detail"; SC.focus = id; S.zooming = false; drawStage(); fadeIn();
|
|
629
|
+
A.playing = resume; go(A.step, A.step >= 0); // replay the current step inside
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
function closeDetail() {
|
|
633
|
+
if (SC.mode !== "detail" || S.zooming) return;
|
|
634
|
+
var resume = A.playing, id = SC.focus; A.playing = false; stopClock(); clearMotion(); S.zooming = true;
|
|
635
|
+
tween(160, function (k) { fadeLayers(1 - k); }, function () {
|
|
636
|
+
SC.mode = "arch"; SC.focus = null; drawStage(); fadeLayers(0);
|
|
637
|
+
var full = { x: 8, y: 8, w: G.W - 16, h: G.H - 16 }, to = SC.pos[id] ? boxOf(SC.pos[id]) : full;
|
|
638
|
+
var z = $("sv-canvas").appendChild(s("rect", { "class": "zoom", rx: 10 }));
|
|
639
|
+
tween(340, function (k) { setRect(z, lerpRect(full, to, k)); z.style.opacity = String(1 - k * 0.9); fadeLayers(k); }, function () {
|
|
640
|
+
if (z.parentNode) z.parentNode.removeChild(z);
|
|
641
|
+
S.zooming = false; A.playing = resume; go(A.step, resume && A.step >= 0);
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
function switchFocus(id) { // from a stub straight into the neighbour's inside
|
|
646
|
+
if (!hasDetail(comp[id])) return;
|
|
647
|
+
var resume = A.playing; A.playing = false; stopClock(); clearMotion();
|
|
648
|
+
SC.mode = "detail"; SC.focus = id; drawStage(); fadeIn(); A.playing = resume; go(A.step, A.step >= 0);
|
|
649
|
+
}
|
|
650
|
+
function selectItem(id) { S.item = S.item === id ? null : id; paintStepState(); renderInsp(); }
|
|
651
|
+
function setOverlay(on) { S.overlay = !!on && !!changes; renderTop(); drawStage(); paintStepState(); renderSide(); renderInsp(); }
|
|
652
|
+
|
|
653
|
+
/* ═════════════════ Transport ═════════════════ */
|
|
654
|
+
function transport() {
|
|
655
|
+
var t = $("sv-transport"), u = uc(), n = u.steps.length, atEnd = A.step >= n - 1 && A.finished; clear(t);
|
|
656
|
+
put(t, [
|
|
657
|
+
h("button", { type: "button", "class": "btn icon ghost", title: W.first, "aria-label": W.first, disabled: A.step < 0 ? true : null, on: { click: function () { A.playing = false; go(-1, false); } } }, icon("first")),
|
|
658
|
+
h("button", { type: "button", "class": "btn icon", title: W.prev, "aria-label": W.prev, disabled: A.step < 0 ? true : null, on: { click: prev } }, icon("prev")),
|
|
659
|
+
h("button", { type: "button", "class": "btn primary", id: "sv-play", on: { click: play } }, icon(A.playing ? "pause" : atEnd ? "replay" : "play"), A.playing ? W.pause : atEnd ? W.replay : W.play),
|
|
660
|
+
h("button", { type: "button", "class": "btn icon", title: W.next, "aria-label": W.next, disabled: atEnd ? true : null, on: { click: next } }, icon("next"))
|
|
661
|
+
]);
|
|
662
|
+
var sc = h("div", { "class": "scrub", id: "sv-scrub" });
|
|
663
|
+
u.steps.forEach(function (st, i) {
|
|
664
|
+
put(sc, h("button", { type: "button", "class": "seg-step" + (i < A.step || (i === A.step && A.finished) ? " done" : ""), title: (i + 1) + ". " + st.label, "aria-label": W.toStep(i + 1), on: { click: function () { A.playing = false; go(i, true); } } }, h("i")));
|
|
665
|
+
});
|
|
666
|
+
put(t, [sc, h("span", { "class": "counter", id: "sv-counter" }, (A.step + 1) + " / " + n)]);
|
|
667
|
+
var sp = h("div", { "class": "seg", role: "group", "aria-label": W.speed });
|
|
668
|
+
[[0.5, "0.5×"], [1, "1×"], [2, "2×"]].forEach(function (x) { put(sp, h("button", { type: "button", "aria-pressed": A.rate === x[0] ? "true" : "false", on: { click: function () { A.rate = x[0]; transport(); } } }, x[1])); });
|
|
669
|
+
put(t, sp);
|
|
670
|
+
}
|
|
671
|
+
function progress() {
|
|
672
|
+
var sc = $("sv-scrub"); if (!sc) return;
|
|
673
|
+
var seg = sc.children[A.step]; if (!seg || !seg.firstChild) return;
|
|
674
|
+
var frac = A.finished ? 1 : (A.leg + A.t) / Math.max(1, A.legs.length);
|
|
675
|
+
seg.firstChild.style.width = Math.round(frac * 1000) / 10 + "%";
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/* ═════════════════ Sidebar: flows, their steps, the change word and the review tick ═════════════════ */
|
|
679
|
+
function tickBox(id) {
|
|
680
|
+
var on = !!ticks[id];
|
|
681
|
+
return h("button", { type: "button", "class": "check", role: "checkbox", "aria-checked": on ? "true" : "false", "aria-label": W.reviewed, title: W.reviewed,
|
|
682
|
+
on: { click: function (e) { e.stopPropagation(); setTick(id, !on); renderSide(); } } }, icon("check"));
|
|
683
|
+
}
|
|
684
|
+
function renderSide() {
|
|
685
|
+
var side = $("sv-list"); clear(side);
|
|
686
|
+
var f = S.filter.trim().toLowerCase();
|
|
687
|
+
var groups = changes ? [["changed", W.gChanged], ["same", W.gSame]] : [["all", W.gAll]];
|
|
688
|
+
groups.forEach(function (g) {
|
|
689
|
+
var list = model.useCases.filter(function (u) {
|
|
690
|
+
var grp = !changes ? "all" : changedSteps(u).length ? "changed" : "same";
|
|
691
|
+
return grp === g[0] && (!f || (u.title + " " + u.trigger).toLowerCase().indexOf(f) >= 0);
|
|
692
|
+
});
|
|
693
|
+
if (!list.length) return;
|
|
694
|
+
var open = f || g[0] === "all" ? true : !S.collapsed[g[0]];
|
|
695
|
+
put(side, h("button", { type: "button", "class": "gh", "aria-expanded": open ? "true" : "false", on: { click: function () { S.collapsed[g[0]] = open; renderSide(); } } }, icon("chevron"), g[1], h("span", { "class": "n", text: String(list.length) })));
|
|
696
|
+
if (!open) return;
|
|
697
|
+
list.forEach(function (u) {
|
|
698
|
+
var cur = u.id === S.uc, ch = S.overlay ? changedSteps(u) : [], rv = ch.filter(function (st) { return ticks["rv:" + u.id + "/" + st.id]; }).length;
|
|
699
|
+
put(side, h("button", { type: "button", "class": "uc", "data-uc": u.id, "aria-current": cur ? "true" : null, on: { click: function () { if (!cur) selectUc(u.id); } } },
|
|
700
|
+
h("span", { "class": "main" }, h("span", { "class": "t kr", text: u.title }), h("span", { "class": "m ell", text: u.trigger })),
|
|
701
|
+
ch.length ? h("span", { "class": "badge", "data-tone": rv === ch.length ? "success" : "warning", title: W.reviewedTitle }, W.reviewBadge(rv, ch.length)) : null));
|
|
702
|
+
if (!cur) return;
|
|
703
|
+
var ol = h("ol", { "class": "steps", id: "sv-steps" });
|
|
704
|
+
u.steps.forEach(function (st, i) {
|
|
705
|
+
var c = chgOf(u, st), pick = function () { A.playing = false; go(i, true); };
|
|
706
|
+
put(ol, h("li", { "class": "st" + (i < A.step ? " done" : ""), "data-step": st.id, "aria-current": i === A.step ? "step" : null, tabindex: 0,
|
|
707
|
+
on: { click: pick, keydown: function (e) { if (e.key === "Enter") pick(); } } },
|
|
708
|
+
h("span", { "class": "n" }, String(i + 1)), h("span", { "class": "lbl ell kr", text: st.label }),
|
|
709
|
+
c ? h("span", { "class": "chg", "data-tone": ST[c.change][0] }, ST[c.change][1]) : null,
|
|
710
|
+
c ? tickBox("rv:" + u.id + "/" + st.id) : null));
|
|
711
|
+
});
|
|
712
|
+
put(side, ol);
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
if (!side.firstChild) put(side, h("div", { "class": "empty", text: W.noMatch }));
|
|
716
|
+
}
|
|
717
|
+
function selectUc(id, step) {
|
|
718
|
+
if (!ucById[id]) return;
|
|
719
|
+
S.uc = id; A.playing = false; stopClock(); A.step = -1; A.finished = true; S.item = null;
|
|
720
|
+
drawStage(); fadeIn();
|
|
721
|
+
go(step != null && step >= 0 ? step : -1, step != null && step >= 0);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/* ═════════════════ Inspector: the current step, or the item picked inside a component ═════════════════ */
|
|
725
|
+
function sec(title) { var e = h("div", { "class": "isec" }, title ? h("h2", { text: title }) : null); for (var i = 1; i < arguments.length; i++) put(e, arguments[i]); return e; }
|
|
726
|
+
function noteRefs(note) { // "Evidence code changed: a.js:1-3, b.js:9" -> {lead, refs}
|
|
727
|
+
var cut = note.indexOf(": "), lead = (cut > 0 ? note.slice(0, cut) : note).replace(/\s*\(.*\)\s*$/, "");
|
|
728
|
+
var refs = cut > 0 ? note.slice(cut + 2).replace(/\s*\(.*\)\s*$/, "").split(/,\s*(?=[^\s,]+:\d)/) : [];
|
|
729
|
+
return { lead: lead, refs: refs };
|
|
730
|
+
}
|
|
731
|
+
function renderInsp() {
|
|
732
|
+
var el = $("sv-insp"); clear(el);
|
|
733
|
+
if (S.item && SC.mode === "detail" && SC.pos[S.item]) { itemInsp(el); return; }
|
|
734
|
+
var u = uc();
|
|
735
|
+
if (A.step < 0) {
|
|
736
|
+
put(el, h("div", { "class": "ih" }, h("p", { "class": "eyebrow", text: W.overview }), h("p", { "class": "lead kr", text: u.summary })));
|
|
737
|
+
var row = cmp[u.id], gone = row && review.compare.mode === "two-models" && S.overlay ? row.rows.filter(function (r) { return r.status === "removed" && r.before; }) : [];
|
|
738
|
+
if (gone.length) put(el, sec(W.gone, h("ul", { "class": "gone" }, gone.map(function (r) { return h("li", { "class": "kr", text: r.before.label }); }))));
|
|
739
|
+
put(el, sec(W.howTo, legend()));
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
var st = u.steps[A.step], c = chgOf(u, st), hit = {};
|
|
743
|
+
put(el, h("div", { "class": "ih" }, h("p", { "class": "eyebrow", text: W.inStep(A.step + 1) }), h("p", { "class": "lead kr", text: st.narration })));
|
|
744
|
+
if (c) {
|
|
745
|
+
var nr = noteRefs(c.note || ""); nr.refs.forEach(function (r) { hit[r.trim()] = 1; });
|
|
746
|
+
put(el, sec(null, h("div", { "class": "note", "data-tone": ST[c.change][0] }, icon("git"), h("span", { "class": "kr", text: nr.lead }))));
|
|
747
|
+
}
|
|
748
|
+
var row2 = cmp[u.id] && review.compare.mode === "two-models" && S.overlay ? cmp[u.id].rows.filter(function (r) { return r.id === st.id; })[0] : null;
|
|
749
|
+
if (row2 && row2.before && row2.after) {
|
|
750
|
+
var diff = ["label", "kind", "route", "request", "response"].filter(function (k) { return (row2.before[k] || "") !== (row2.after[k] || ""); });
|
|
751
|
+
var NAME = W.field;
|
|
752
|
+
if (diff.length) put(el, sec(W.before, h("dl", { "class": "kv before" }, diff.map(function (k) { return [h("dt", { text: NAME[k] }), h("dd", { "class": "kr", text: k === "kind" ? STEP_KO[row2.before[k]] || row2.before[k] : row2.before[k] || "—" })]; }))));
|
|
753
|
+
else if (row2.status === "moved") put(el, sec(W.before, h("p", { "class": "kr", text: W.moved })));
|
|
754
|
+
}
|
|
755
|
+
if (SC.mode === "arch" && st.transforms && st.transforms.length) { // inside a component the stage shows these as chips
|
|
756
|
+
put(el, sec(W.transforms, st.transforms.map(function (tf) {
|
|
757
|
+
var at = tf.at.split("/"), part = (comp[at[0]] && comp[at[0]].parts || []).filter(function (p) { return p.id === at[1]; })[0];
|
|
758
|
+
return h("div", { "class": "xf" }, h("div", { "class": "at kr", text: (part ? part.label : at[1]) + " · " + first(comp[at[0]].label) }), h("div", { "class": "io", text: tf["in"].type + " → " + tf.out.type }),
|
|
759
|
+
tf.map && tf.map.length ? h("ul", { "class": "map" }, tf.map.map(function (m) { return h("li", { text: m.from + " → " + m.to + (m.how ? " (" + m.how + ")" : "") }); })) : null);
|
|
760
|
+
})));
|
|
761
|
+
}
|
|
762
|
+
(st.payload || []).forEach(function (p) {
|
|
763
|
+
put(el, sec((p.leg === "request" ? W.sent : W.received) + (p.title ? " · " + p.title : ""),
|
|
764
|
+
p.body ? h("pre", { "class": "code", text: p.body }) : null,
|
|
765
|
+
p.bind ? h("dl", { "class": "kv" }, p.bind.map(function (b) { return [h("dt", { text: b.name }), h("dd", { text: b.value })]; })) : null,
|
|
766
|
+
p.columns ? h("dl", { "class": "kv" }, p.columns.map(function (b) { return [h("dt", { text: b.name }), h("dd", { text: b.value })]; })) : null));
|
|
767
|
+
});
|
|
768
|
+
put(el, sec(W.evidence, h("ul", { "class": "ev" }, st.evidence.map(function (e) {
|
|
769
|
+
var ref = evRef(e), on = !!(c && hit[ref]);
|
|
770
|
+
return h("li", { "class": on ? "hit" : null }, h("span", { text: ref }), on ? h("span", { "class": "badge", "data-tone": ST[c.change][0] }, W.changed) : null);
|
|
771
|
+
}))));
|
|
772
|
+
}
|
|
773
|
+
function itemInsp(el) { // a part / table / endpoint picked inside a component: only what its card does not show
|
|
774
|
+
var p = SC.pos[S.item], r = p.ref, c = comp[SC.focus], u = uc(), id = S.item;
|
|
775
|
+
var kindKo = p.kind === "part" ? ROLE_KO[r.role] || r.role : p.kind === "table" ? W.table : W.endpoint;
|
|
776
|
+
put(el, h("div", { "class": "ih" },
|
|
777
|
+
h("button", { type: "button", "class": "link", style: "font-size:12px;margin-bottom:12px", on: { click: function () { S.item = null; paintStepState(); renderInsp(); } } }, A.step >= 0 ? W.backStep(A.step + 1) : W.backOverview),
|
|
778
|
+
h("p", { "class": "eyebrow", text: W.insideOf(kindKo, first(c.label)) }),
|
|
779
|
+
h("p", { "class": "lead kr", text: r.detail || W.noDetail })));
|
|
780
|
+
var ch = S.overlay ? dchg[c.id + "|" + p.kind + "|" + (p.kind === "table" ? r.name : r.id)] : null;
|
|
781
|
+
if (ch) put(el, sec(null, h("div", { "class": "note", "data-tone": ST[ch][0] }, icon("git"), h("span", { "class": "kr", text: ch === "added" ? W.newHere : W.changedHere }))));
|
|
782
|
+
if (p.kind === "part") {
|
|
783
|
+
var byId = {}; c.parts.forEach(function (x) { byId[x.id] = x; });
|
|
784
|
+
var rel = function (w, dir) { var o = dir === "in" ? byId[w.from] : byId[w.to]; return h("li", null, h("span", { "class": "who kr", text: (dir === "in" ? "← " : "→ ") + (o ? o.label : dir === "in" ? w.from : w.to) }), h("span", { "class": "how mono", text: w.label })); };
|
|
785
|
+
var ins = (c.wires || []).filter(function (w) { return w.to === r.id; }), outs = (c.wires || []).filter(function (w) { return w.from === r.id; });
|
|
786
|
+
if (ins.length) put(el, sec(W.calledBy, h("ul", { "class": "rel" }, ins.map(function (w) { return rel(w, "in"); }))));
|
|
787
|
+
if (outs.length) put(el, sec(W.calls, h("ul", { "class": "rel" }, outs.map(function (w) { return rel(w, "out"); }))));
|
|
788
|
+
}
|
|
789
|
+
if (p.kind === "endpoint" && r.auth) put(el, sec(W.auth, h("p", { "class": "kr", text: r.auth })));
|
|
790
|
+
var hits = u.steps.map(function (st, i) { return { st: st, i: i }; }).filter(function (x) { return p.kind === "part" ? traceIn(x.st, c.id).indexOf(id) >= 0 : targetsIn(x.st, c).indexOf(id) >= 0; });
|
|
791
|
+
if (hits.length) put(el, sec(W.stepsHere, h("div", { style: "display:flex;flex-wrap:wrap;gap:6px" }, hits.map(function (x) {
|
|
792
|
+
return h("button", { type: "button", "class": "steplink", title: x.st.label, on: { click: function () { A.playing = false; go(x.i, true); } } }, W.stepN(x.i + 1));
|
|
793
|
+
}))));
|
|
794
|
+
put(el, sec(W.evidence, h("ul", { "class": "ev" }, (r.evidence || []).map(function (e) { return h("li", null, h("span", { text: evRef(e) })); }))));
|
|
795
|
+
}
|
|
796
|
+
function legend() {
|
|
797
|
+
function sw(draw) { var g = s("svg", { width: 28, height: 16, viewBox: "0 0 28 16", "aria-hidden": "true" }); draw(g); return g; }
|
|
798
|
+
function dotLine(color, cx) { return function (g) { g.appendChild(s("line", { x1: 2, y1: 8, x2: 26, y2: 8, style: "stroke:" + color, "stroke-width": 2.5, "stroke-dasharray": "1 5", "stroke-linecap": "round" })); g.appendChild(s("circle", { cx: cx, cy: 8, r: 5, style: "fill:" + color + ";stroke:var(--surface)", "stroke-width": 2 })); }; }
|
|
799
|
+
var rows = [
|
|
800
|
+
[dotLine("var(--req)", 18), W.lgReq],
|
|
801
|
+
[dotLine("var(--res)", 10), W.lgRes],
|
|
802
|
+
[function (g) { g.appendChild(s("rect", { x: 3, y: 6, width: 22, height: 4, rx: 2, style: "fill:var(--int-soft)" })); g.appendChild(s("rect", { x: 3, y: 6, width: 13, height: 4, rx: 2, style: "fill:var(--int)" })); }, W.lgWork],
|
|
803
|
+
[function (g) { g.appendChild(s("line", { x1: 2, y1: 8, x2: 26, y2: 8, style: "stroke:var(--trail-grey)", "stroke-width": 2 })); }, W.lgPast]
|
|
804
|
+
];
|
|
805
|
+
if (changes) rows.push([function (g) { g.appendChild(s("rect", { x: 4, y: 3, width: 20, height: 10, rx: 3, style: "fill:var(--success-bg);stroke:var(--success-border)" })); }, W.lgChanged]);
|
|
806
|
+
if (model.components.some(hasDetail)) {
|
|
807
|
+
rows.push([function (g) { g.appendChild(s("path", { d: P.expand, transform: "translate(6,0) scale(.66)", style: "fill:none;stroke:var(--text-3)", "stroke-width": 2.2, "stroke-linecap": "round" })); }, W.lgOpen]);
|
|
808
|
+
rows.push([dotLine("var(--int)", 14), W.lgInt]);
|
|
809
|
+
}
|
|
810
|
+
return h("div", { "class": "legend" }, rows.map(function (r) { return [sw(r[0]), h("span", { text: r[1] })]; }));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/* ═════════════════ Top bar, URL, wiring ═════════════════ */
|
|
814
|
+
function renderTop() {
|
|
815
|
+
$("sv-product").textContent = model.product.name;
|
|
816
|
+
var g = changes && changes.generated, ref = $("sv-ref");
|
|
817
|
+
ref.hidden = !g; ref.textContent = g && g.base && g.head ? g.base.slice(0, 7) + " → " + g.head.slice(0, 7) : "";
|
|
818
|
+
$("sv-commit").textContent = changes ? changes.title : model.product.description;
|
|
819
|
+
var t = $("sv-overlay"); t.hidden = !changes; t.setAttribute("aria-checked", S.overlay ? "true" : "false");
|
|
820
|
+
if (doc.title !== undefined) doc.title = model.product.name + " · Nacholens";
|
|
821
|
+
}
|
|
822
|
+
function syncHash() {
|
|
823
|
+
if (TARGET !== "html" || typeof history === "undefined" || !history.replaceState) return;
|
|
824
|
+
var extra = H0.dock ? "&dock=" + encodeURIComponent(H0.dock) : "";
|
|
825
|
+
try { history.replaceState(null, "", "#uc=" + S.uc + "&step=" + (A.step + 1) + (SC.mode === "detail" ? "&focus=" + SC.focus : "") + extra); } catch (e) { /* sandboxed */ }
|
|
826
|
+
}
|
|
827
|
+
function relayout() {
|
|
828
|
+
var keep = { leg: A.leg, t: A.t, running: A.running };
|
|
829
|
+
stopClock(); clearMotion(); drawStage(); paintStepState();
|
|
830
|
+
if (A.step >= 0 && !A.finished) {
|
|
831
|
+
A.legs = legsOf(uc().steps[A.step]);
|
|
832
|
+
if (A.legs.length) { startLeg(Math.min(keep.leg, A.legs.length - 1), keep.t); if (!keep.running) stopClock(); }
|
|
833
|
+
} else progress();
|
|
834
|
+
}
|
|
835
|
+
$("sv-q").addEventListener("input", function (e) { S.filter = e.target.value || ""; renderSide(); });
|
|
836
|
+
$("sv-overlay").addEventListener("click", function () { setOverlay(!S.overlay); });
|
|
837
|
+
doc.addEventListener("keydown", function (e) {
|
|
838
|
+
if (e.key === "Escape") { closeDetail(); return; }
|
|
839
|
+
var a = doc.activeElement; if (a && a !== doc.body && a.tagName !== "svg") return; // keys belong to the focused control
|
|
840
|
+
if (e.key === " ") { e.preventDefault(); play(); } else if (e.key === "ArrowRight") next(); else if (e.key === "ArrowLeft") prev();
|
|
841
|
+
});
|
|
842
|
+
// tag boxes are sized from the rendered text: draw again once the embedded fonts are ready
|
|
843
|
+
if (typeof document !== "undefined" && document.fonts && document.fonts.ready && document.fonts.status !== "loaded") document.fonts.ready.then(function () { relayout(); });
|
|
844
|
+
if (typeof ResizeObserver === "function") {
|
|
845
|
+
var rz = 0, lastW = 0, lastH = 0;
|
|
846
|
+
new ResizeObserver(function () {
|
|
847
|
+
var st = $("sv-stage"); if (st.clientWidth === lastW && st.clientHeight === lastH) return;
|
|
848
|
+
lastW = st.clientWidth; lastH = st.clientHeight; clearTimeout(rz); rz = setTimeout(relayout, 60);
|
|
849
|
+
}).observe($("sv-stage"));
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/* ═════════════════ start ═════════════════ */
|
|
853
|
+
if (H0.focus && hasDetail(comp[H0.focus])) { SC.mode = "detail"; SC.focus = H0.focus; }
|
|
854
|
+
renderTop(); drawStage();
|
|
855
|
+
go(H0.step ? Number(H0.step) - 1 : -1, false);
|
|
856
|
+
if (H0.at != null && A.step >= 0) { // capture aid: #…&at=0.4 freezes the step at that point of its motion
|
|
857
|
+
A.finished = false; A.legs = legsOf(uc().steps[A.step]);
|
|
858
|
+
if (A.legs.length) { var f = Math.max(0, Math.min(0.999, Number(H0.at))) * A.legs.length, li = Math.floor(f); startLeg(li, f - li); stopClock(); A.t = f - li; frameDraw(); paintStepState(); caption(); transport(); progress(); }
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
window.__nacholens = {
|
|
862
|
+
model: model, changes: changes, target: TARGET, hash: H0,
|
|
863
|
+
get state() { return { uc: S.uc, step: A.step, finished: A.finished, playing: A.playing, running: A.running, leg: A.leg, t: A.t, mode: SC.mode, focus: SC.focus, item: S.item, overlay: S.overlay, rate: A.rate }; },
|
|
864
|
+
scene: function () { var b = {}; Object.keys(SC.pos).forEach(function (id) { var p = SC.pos[id]; b[id] = { x: p.x, y: p.y, w: p.w, h: p.h, kind: p.kind }; }); return { mode: SC.mode, focus: SC.focus, w: G.W, h: G.H, boxes: b, hidden: SC.hidden }; },
|
|
865
|
+
legs: function (i) { return legsOf(uc().steps[i]).map(function (l) { return l.type === "move" ? { type: l.type, ids: l.ids.slice(), tone: l.tone, text: l.text } : { type: l.type, at: l.at }; }); },
|
|
866
|
+
packet: function () { return MOVE && MOVE.pts ? { x: MOVE.x, y: MOVE.y, tone: MOVE.tone } : null; },
|
|
867
|
+
tagged: function () { return Object.keys(NODE).filter(function (id) { return NODE[id].getAttribute("class").indexOf("chg-") >= 0; }).sort(); },
|
|
868
|
+
selectUseCase: selectUc, go: go, play: play, pause: pause, next: next, prev: prev,
|
|
869
|
+
openDetail: openDetail, closeDetail: closeDetail, switchFocus: switchFocus, selectItem: selectItem,
|
|
870
|
+
setOverlay: setOverlay, relayout: relayout
|
|
871
|
+
};
|
|
872
|
+
})();
|