@milehimikey/em 1.5.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/event-modeling/SKILL.md +62 -17
- package/.claude/skills/event-modeling/reference/conform.md +33 -6
- package/.claude/skills/event-modeling/reference/em-dsl.md +144 -43
- package/.claude/skills/event-modeling/reference/extract.md +12 -2
- package/.claude/skills/event-modeling/reference/methodology.md +34 -14
- package/.claude/skills/event-modeling/templates/conformance-report.md +9 -7
- package/.claude/skills/event-modeling/templates/slice.md +6 -1
- package/.claude/skills/event-modeling/templates/state.md +8 -0
- package/README.md +1 -0
- package/dist/catalog/build.d.ts +33 -0
- package/dist/catalog/build.js +117 -0
- package/dist/catalog/build.js.map +1 -0
- package/dist/catalog/classify.d.ts +14 -0
- package/dist/catalog/classify.js +44 -0
- package/dist/catalog/classify.js.map +1 -0
- package/dist/catalog/pages.d.ts +45 -0
- package/dist/catalog/pages.js +174 -0
- package/dist/catalog/pages.js.map +1 -0
- package/dist/catalog/sliceDoc.d.ts +10 -0
- package/dist/catalog/sliceDoc.js +20 -0
- package/dist/catalog/sliceDoc.js.map +1 -0
- package/dist/cli/catalog-inputs.d.ts +16 -0
- package/dist/cli/catalog-inputs.js +18 -0
- package/dist/cli/catalog-inputs.js.map +1 -0
- package/dist/cli/glossary-inputs.d.ts +15 -0
- package/dist/cli/glossary-inputs.js +17 -0
- package/dist/cli/glossary-inputs.js.map +1 -0
- package/dist/cli/render-inputs.d.ts +27 -0
- package/dist/cli/render-inputs.js +41 -0
- package/dist/cli/render-inputs.js.map +1 -0
- package/dist/cli.js +188 -10
- package/dist/cli.js.map +1 -1
- package/dist/emit/diffJson.d.ts +1 -1
- package/dist/emit/diffJson.js +8 -1
- package/dist/emit/diffJson.js.map +1 -1
- package/dist/emit/dot.d.ts +7 -0
- package/dist/emit/dot.js +10 -4
- package/dist/emit/dot.js.map +1 -1
- package/dist/emit/glossaryJson.d.ts +13 -0
- package/dist/emit/glossaryJson.js +36 -0
- package/dist/emit/glossaryJson.js.map +1 -0
- package/dist/emit/json.d.ts +16 -2
- package/dist/emit/json.js +57 -6
- package/dist/emit/json.js.map +1 -1
- package/dist/emit/theme.d.ts +3 -0
- package/dist/emit/theme.js +19 -0
- package/dist/emit/theme.js.map +1 -1
- package/dist/model/diff.d.ts +36 -1
- package/dist/model/diff.js +198 -35
- package/dist/model/diff.js.map +1 -1
- package/dist/model/edges.d.ts +12 -1
- package/dist/model/edges.js +19 -8
- package/dist/model/edges.js.map +1 -1
- package/dist/model/glossary.d.ts +68 -0
- package/dist/model/glossary.js +138 -0
- package/dist/model/glossary.js.map +1 -0
- package/dist/model/model.d.ts +35 -0
- package/dist/model/model.js +41 -0
- package/dist/model/model.js.map +1 -1
- package/dist/model/validate.js +114 -3
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/ast.d.ts +20 -0
- package/dist/parser/parser.js +170 -31
- package/dist/parser/parser.js.map +1 -1
- package/dist/render/drawNotes.d.ts +7 -3
- package/dist/render/drawNotes.js +75 -28
- package/dist/render/drawNotes.js.map +1 -1
- package/dist/render/render.d.ts +20 -1
- package/dist/render/render.js +52 -14
- package/dist/render/render.js.map +1 -1
- package/dist/render/serve.js +155 -0
- package/dist/render/serve.js.map +1 -1
- package/dist/render/sliceDiagram.d.ts +11 -0
- package/dist/render/sliceDiagram.js +198 -0
- package/dist/render/sliceDiagram.js.map +1 -0
- package/dist/render/sliceOverlay.d.ts +21 -0
- package/dist/render/sliceOverlay.js +111 -0
- package/dist/render/sliceOverlay.js.map +1 -0
- package/dist/render/sliceStatus.d.ts +5 -0
- package/dist/render/sliceStatus.js +26 -0
- package/dist/render/sliceStatus.js.map +1 -0
- package/dist/render/statusOverlay.d.ts +16 -0
- package/dist/render/statusOverlay.js +110 -0
- package/dist/render/statusOverlay.js.map +1 -0
- package/dist/render/svgGeometry.d.ts +30 -0
- package/dist/render/svgGeometry.js +33 -14
- package/dist/render/svgGeometry.js.map +1 -1
- package/dist/render/watch.d.ts +1 -1
- package/dist/render/watch.js +5 -2
- package/dist/render/watch.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Renders a single slice's own diagram, matching the canonical shape of
|
|
3
|
+
// whichever of the 4 Event Modeling patterns it is (State Change, State View,
|
|
4
|
+
// Automation, Translation) — instead of cropping the full multi-slice diagram
|
|
5
|
+
// down to one column (which forces a narrow crop to still show a wide swimlane
|
|
6
|
+
// label, and can pull in most of a neighboring slice; see git history).
|
|
7
|
+
//
|
|
8
|
+
// The key trick: every piece of "immediate context" a slice needs to draw its
|
|
9
|
+
// own shape is already one hop away in the compiled model — a view's upstream
|
|
10
|
+
// event(s) resolve via `from` (resolveFromSource, shared with semanticEdges),
|
|
11
|
+
// and an automation/translation's watched view is just whatever `view` sits in
|
|
12
|
+
// the same slice. So this builds a tiny *synthetic* NormalizedModel — reusing
|
|
13
|
+
// the real, already-normalized Element objects (same ids, same fields, same
|
|
14
|
+
// notes), shallow-cloned only to reassign which of a handful of new columns
|
|
15
|
+
// they land in — and runs it through the exact same layout()/emitDot() the
|
|
16
|
+
// full model uses, unmodified. Edge-drawing comes free too: semanticEdges()
|
|
17
|
+
// resolves `from` via byName, so as long as the synthetic model's byName map
|
|
18
|
+
// is populated, the arrows get drawn by the existing inference with zero new
|
|
19
|
+
// edge-drawing code.
|
|
20
|
+
//
|
|
21
|
+
// This never chases more than one hop away (previous slice for a triggerless
|
|
22
|
+
// State Change; a view's own `from` sources for State View; the next slice for
|
|
23
|
+
// Automation/Translation — semanticEdges itself never looks further than i+1),
|
|
24
|
+
// so a slice diagram can never balloon into showing most of the model, the way
|
|
25
|
+
// a naive crop or a naive "just extract this slice and recompile" both could.
|
|
26
|
+
import { AUTOMATION_KINDS } from "../parser/ast.js";
|
|
27
|
+
import { normalizeName } from "../model/model.js";
|
|
28
|
+
import { resolveFromSource } from "../model/edges.js";
|
|
29
|
+
import { layout } from "../layout/grid.js";
|
|
30
|
+
import { emitDot } from "../emit/dot.js";
|
|
31
|
+
import { classifySlicePattern } from "../catalog/classify.js";
|
|
32
|
+
/** Build a small, standalone diagram for one slice — real Graphviz layout, not a crop.
|
|
33
|
+
* `opts` is forwarded to layout() unchanged, so `--keep-empty-lanes` behaves the same
|
|
34
|
+
* way here as it does for the full model instead of being silently dropped. */
|
|
35
|
+
export function buildSliceDiagram(model, sliceIndex, opts = {}) {
|
|
36
|
+
const slice = model.slices[sliceIndex];
|
|
37
|
+
const pattern = classifySlicePattern(slice);
|
|
38
|
+
const columns = mergeColumns(collectColumns(model, sliceIndex, pattern));
|
|
39
|
+
const synthetic = assembleSyntheticModel(model, columns, slice.name);
|
|
40
|
+
const grid = layout(synthetic, opts);
|
|
41
|
+
const dot = emitDot(synthetic, grid);
|
|
42
|
+
return { model: synthetic, grid, dot };
|
|
43
|
+
}
|
|
44
|
+
function collectColumns(model, sliceIndex, pattern) {
|
|
45
|
+
switch (pattern) {
|
|
46
|
+
case "state-change":
|
|
47
|
+
return columnsForStateChange(model, sliceIndex);
|
|
48
|
+
case "state-view":
|
|
49
|
+
return columnsForStateView(model, sliceIndex);
|
|
50
|
+
case "automation":
|
|
51
|
+
case "translation":
|
|
52
|
+
return columnsForReaction(model, sliceIndex);
|
|
53
|
+
default: // "unclassified"
|
|
54
|
+
return [{ sourceSliceIndex: sliceIndex, elements: model.slices[sliceIndex].elements }];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** State Change: UI -> Command -> Event, all in this slice. If there's no local
|
|
58
|
+
* UI, the command's real trigger is the previous slice's automation/translation
|
|
59
|
+
* reaction — pull in just that one element as earlier context. */
|
|
60
|
+
function columnsForStateChange(model, i) {
|
|
61
|
+
const slice = model.slices[i];
|
|
62
|
+
const hasUi = slice.elements.some((e) => e.kind === "ui");
|
|
63
|
+
const cols = [{ sourceSliceIndex: i, elements: slice.elements }];
|
|
64
|
+
if (!hasUi) {
|
|
65
|
+
const prev = model.slices[i - 1];
|
|
66
|
+
const trigger = prev?.elements.find((e) => AUTOMATION_KINDS.has(e.kind));
|
|
67
|
+
if (trigger)
|
|
68
|
+
cols.push({ sourceSliceIndex: i - 1, elements: [trigger] });
|
|
69
|
+
}
|
|
70
|
+
return cols;
|
|
71
|
+
}
|
|
72
|
+
/** State View: Event(s) -> View -> UI. Each view's `from` names resolve to their
|
|
73
|
+
* real source Elements (same rule semanticEdges uses) — one column per distinct
|
|
74
|
+
* origin slice, so a multi-source view (e.g. a dashboard reading two events from
|
|
75
|
+
* two different, possibly far-apart slices) naturally gets one column per source,
|
|
76
|
+
* not a single blown-out column. */
|
|
77
|
+
function columnsForStateView(model, i) {
|
|
78
|
+
const slice = model.slices[i];
|
|
79
|
+
const cols = [];
|
|
80
|
+
for (const view of slice.elements.filter((e) => e.kind === "view")) {
|
|
81
|
+
for (const name of view.from ?? []) {
|
|
82
|
+
const src = resolveFromSource(model, view, name);
|
|
83
|
+
if (src)
|
|
84
|
+
cols.push({ sourceSliceIndex: src.sliceIndex, elements: [src] });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
cols.push({ sourceSliceIndex: i, elements: slice.elements });
|
|
88
|
+
return cols;
|
|
89
|
+
}
|
|
90
|
+
/** Automation/Translation share one shape: [View ->] Automation (this slice),
|
|
91
|
+
* then Command -> Event (the immediately next slice — never further, mirroring
|
|
92
|
+
* semanticEdges' own i+1-only rule). The watched view is same-slice adjacency,
|
|
93
|
+
* not a `from` hop, so it's already included via this slice's own elements —
|
|
94
|
+
* deliberately not chased any further back. Only command/event are pulled from
|
|
95
|
+
* the next slice; a stray `ui` there belongs to *that* slice's own diagram. */
|
|
96
|
+
function columnsForReaction(model, i) {
|
|
97
|
+
const slice = model.slices[i];
|
|
98
|
+
const cols = [{ sourceSliceIndex: i, elements: slice.elements }];
|
|
99
|
+
const next = model.slices[i + 1];
|
|
100
|
+
const forward = next?.elements.filter((e) => e.kind === "command" || e.kind === "event") ?? [];
|
|
101
|
+
if (forward.length > 0)
|
|
102
|
+
cols.push({ sourceSliceIndex: i + 1, elements: forward });
|
|
103
|
+
return cols;
|
|
104
|
+
}
|
|
105
|
+
/** Merge columns sharing a source slice (two `from` names can resolve to the same
|
|
106
|
+
* origin slice), dedupe elements by id, then sort chronologically. `from`/reaction
|
|
107
|
+
* wiring only ever points backward or to the immediate next slice, so a plain
|
|
108
|
+
* ascending sort by real slice index is sufficient for correct left-to-right
|
|
109
|
+
* placement across every pattern — no pattern-specific ordering code needed. */
|
|
110
|
+
function mergeColumns(columns) {
|
|
111
|
+
const bySource = new Map();
|
|
112
|
+
for (const col of columns) {
|
|
113
|
+
let elements = bySource.get(col.sourceSliceIndex);
|
|
114
|
+
if (!elements) {
|
|
115
|
+
elements = new Map();
|
|
116
|
+
bySource.set(col.sourceSliceIndex, elements);
|
|
117
|
+
}
|
|
118
|
+
for (const el of col.elements)
|
|
119
|
+
elements.set(el.id, el);
|
|
120
|
+
}
|
|
121
|
+
return [...bySource.entries()]
|
|
122
|
+
.map(([sourceSliceIndex, elements]) => ({ sourceSliceIndex, elements: [...elements.values()] }))
|
|
123
|
+
.sort((a, b) => a.sourceSliceIndex - b.sourceSliceIndex);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Assemble a small, valid NormalizedModel from the given columns — real Element
|
|
127
|
+
* objects, shallow-cloned only to reassign `sliceIndex` to their new synthetic
|
|
128
|
+
* column (never mutating the real model). Two details matter for correctness:
|
|
129
|
+
*
|
|
130
|
+
* - `byId` keeps each clone's *original* id — everything downstream (emitDot's
|
|
131
|
+
* node ids, styleFor, note markers, parseNodeRects) keys off Element.id, so
|
|
132
|
+
* preserving it is what keeps the whole rest of the render pipeline working
|
|
133
|
+
* completely unmodified.
|
|
134
|
+
* - `byName` is populated from the clones actually included, using their
|
|
135
|
+
* untouched `name`/`from` strings — this is the entire trick that makes edge
|
|
136
|
+
* drawing free: semanticEdges() (called later, unmodified, inside
|
|
137
|
+
* composeSvg) resolves `from` via byName exactly as it does for the full
|
|
138
|
+
* model, so it finds the clones and draws the arrows automatically.
|
|
139
|
+
*
|
|
140
|
+
* `personas`/`contexts` are derived from only the included elements, not
|
|
141
|
+
* copied from the full model — copying them was the exact class of bug that
|
|
142
|
+
* made the previous crop-based approach barely narrower than the full diagram
|
|
143
|
+
* (an unused row still reserves its own swimlane).
|
|
144
|
+
*/
|
|
145
|
+
function assembleSyntheticModel(model, columns, title) {
|
|
146
|
+
const slices = [];
|
|
147
|
+
const elements = [];
|
|
148
|
+
const byId = new Map();
|
|
149
|
+
const byName = new Map();
|
|
150
|
+
columns.forEach((col, syntheticIndex) => {
|
|
151
|
+
const origin = model.slices[col.sourceSliceIndex];
|
|
152
|
+
const cloned = col.elements.map((el) => ({ ...el, sliceIndex: syntheticIndex }));
|
|
153
|
+
slices.push({ name: origin.name, index: syntheticIndex, source: origin.source, elements: cloned, line: origin.line });
|
|
154
|
+
for (const el of cloned) {
|
|
155
|
+
elements.push(el);
|
|
156
|
+
byId.set(el.id, el);
|
|
157
|
+
const key = normalizeName(el.name);
|
|
158
|
+
const bucket = byName.get(key);
|
|
159
|
+
if (bucket)
|
|
160
|
+
bucket.push(el);
|
|
161
|
+
else
|
|
162
|
+
byName.set(key, [el]);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
return {
|
|
166
|
+
name: title,
|
|
167
|
+
personas: dedupeDefined(elements.filter((e) => e.kind === "ui").map((e) => e.persona)),
|
|
168
|
+
contexts: dedupeDefined(elements.filter((e) => e.kind === "event").map((e) => e.context)),
|
|
169
|
+
hasAutomation: elements.some((e) => AUTOMATION_KINDS.has(e.kind)),
|
|
170
|
+
slices,
|
|
171
|
+
elements,
|
|
172
|
+
byId,
|
|
173
|
+
byName,
|
|
174
|
+
// v1 gap, deliberate: explicit DSL `arrow` statements are rare (they exist for
|
|
175
|
+
// relationships semanticEdges' pattern inference can't derive on its own) and
|
|
176
|
+
// dropping them from a single-slice extraction is narrow, not silent — a
|
|
177
|
+
// follow-up could remap model.arrows entries whose both endpoints resolve
|
|
178
|
+
// through an original-id -> cloned-id map, but it's not needed to ship this.
|
|
179
|
+
arrows: [],
|
|
180
|
+
// Unused by layout()/emitDot() — fieldLabel() prints a field's type as a raw
|
|
181
|
+
// string, never resolves it — so passthrough is free and avoids handing back
|
|
182
|
+
// broken/empty maps for no benefit.
|
|
183
|
+
types: model.types,
|
|
184
|
+
typesByName: model.typesByName,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function dedupeDefined(values) {
|
|
188
|
+
const seen = new Set();
|
|
189
|
+
const out = [];
|
|
190
|
+
for (const v of values) {
|
|
191
|
+
if (v && !seen.has(v)) {
|
|
192
|
+
seen.add(v);
|
|
193
|
+
out.push(v);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=sliceDiagram.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sliceDiagram.js","sourceRoot":"","sources":["../../src/render/sliceDiagram.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NormalizedModel } from "../model/model.js";
|
|
2
|
+
import { Grid } from "../layout/grid.js";
|
|
3
|
+
import { Rect } from "./svgGeometry.js";
|
|
4
|
+
/** Every non-element node id the slice overlay needs a rect for (header cells),
|
|
5
|
+
* to fold into the same parseNodeRects() call as element ids. */
|
|
6
|
+
export declare function sliceOverlayIds(grid: Grid): string[];
|
|
7
|
+
/** Splice `data-slice="<index>"` onto every element's node group. */
|
|
8
|
+
export declare function tagSliceAttrs(svg: string, model: NormalizedModel): string;
|
|
9
|
+
/** `<metadata>` (slice ranges, as JSON) + `<style>` (the dim class) block, spliced
|
|
10
|
+
* just inside the root `<svg>`. `svg` is scanned (post-fitCanvas/tagSliceAttrs is
|
|
11
|
+
* fine — neither touches `<text>` elements) for the row-label column's x-range.
|
|
12
|
+
* Returns "" if no slice has a rect — the model is empty, or the SVG shape wasn't
|
|
13
|
+
* what parseNodeRects expected.
|
|
14
|
+
*
|
|
15
|
+
* rects (from parseNodeRects) and the row-label `<text>` x positions scanned below
|
|
16
|
+
* are both in graph-space — the coordinate system *inside* Graphviz's
|
|
17
|
+
* `<g class="graph" transform="scale(...) translate(...)">` wrapper — not the root
|
|
18
|
+
* SVG/viewBox space the served viewer sets `viewBox` in. Every x0/x1 here goes
|
|
19
|
+
* through readGraphTransform()'s toRoot() before it's embedded, the same
|
|
20
|
+
* conversion fitCanvas() applies to the edge-detour bbox for the same reason. */
|
|
21
|
+
export declare function buildSliceOverlay(svg: string, grid: Grid, rects: Map<string, Rect>): string;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Tags each element's rendered node group with its slice index, and embeds a
|
|
3
|
+
// <metadata> block describing every slice's name and x-range in the SVG.
|
|
4
|
+
//
|
|
5
|
+
// This is what lets the storyboard mode in the live viewer (`em watch --serve`,
|
|
6
|
+
// see src/render/serve.ts) highlight and pan/zoom to one slice at a time
|
|
7
|
+
// entirely client-side, with no separate data fetch — everything the browser
|
|
8
|
+
// needs travels inside the SVG it already loads.
|
|
9
|
+
//
|
|
10
|
+
// A slice's x-range comes from its header-cell rect (headerCellId), not from
|
|
11
|
+
// element rects — a slice can have empty rows, which would give an element-based
|
|
12
|
+
// bbox a wrong or missing range.
|
|
13
|
+
//
|
|
14
|
+
// The row-label (swimlane) column has no rect at all in parseNodeRects' sense:
|
|
15
|
+
// row labels render as bare `shape=plaintext` Graphviz nodes, which emit only a
|
|
16
|
+
// `<text>` element (no `<polygon>`/`<path>`), so they fall outside
|
|
17
|
+
// parseNodeRects' polygon/path-based extraction entirely. Their x-range is
|
|
18
|
+
// instead estimated straight from each `<text>` element: an exact center
|
|
19
|
+
// (Graphviz always emits `text-anchor="middle"` here) and a width approximated
|
|
20
|
+
// from character count and font size. That's a deliberate approximation local
|
|
21
|
+
// to this file — good enough to keep the swimlane labels in frame when the
|
|
22
|
+
// storyboard viewer pans/zooms to a slice, not meant to be pixel-accurate, and
|
|
23
|
+
// not worth teaching the shared parseNodeRects() a text-measuring fallback for
|
|
24
|
+
// its one caller that needs it.
|
|
25
|
+
import { headerCellId } from "../layout/grid.js";
|
|
26
|
+
import { rowLabelId } from "../emit/dot.js";
|
|
27
|
+
import { decode, readGraphTransform, NODE_GROUP, TITLE } from "./svgGeometry.js";
|
|
28
|
+
const TEXT = /<text\b([^>]*)>([^<]*)<\/text>/;
|
|
29
|
+
/** Every non-element node id the slice overlay needs a rect for (header cells),
|
|
30
|
+
* to fold into the same parseNodeRects() call as element ids. */
|
|
31
|
+
export function sliceOverlayIds(grid) {
|
|
32
|
+
const ids = [];
|
|
33
|
+
for (let c = 0; c < grid.cols; c++)
|
|
34
|
+
ids.push(headerCellId(c));
|
|
35
|
+
return ids;
|
|
36
|
+
}
|
|
37
|
+
/** Splice `data-slice="<index>"` onto every element's node group. */
|
|
38
|
+
export function tagSliceAttrs(svg, model) {
|
|
39
|
+
return svg.replace(NODE_GROUP, (block) => {
|
|
40
|
+
const id = decode(TITLE.exec(block)?.[1] ?? "");
|
|
41
|
+
const el = model.byId.get(id);
|
|
42
|
+
if (!el)
|
|
43
|
+
return block; // header cell, row label, or placeholder — leave untagged
|
|
44
|
+
return block.replace(/^<g\b/, `<g data-slice="${el.sliceIndex}"`);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** `<metadata>` (slice ranges, as JSON) + `<style>` (the dim class) block, spliced
|
|
48
|
+
* just inside the root `<svg>`. `svg` is scanned (post-fitCanvas/tagSliceAttrs is
|
|
49
|
+
* fine — neither touches `<text>` elements) for the row-label column's x-range.
|
|
50
|
+
* Returns "" if no slice has a rect — the model is empty, or the SVG shape wasn't
|
|
51
|
+
* what parseNodeRects expected.
|
|
52
|
+
*
|
|
53
|
+
* rects (from parseNodeRects) and the row-label `<text>` x positions scanned below
|
|
54
|
+
* are both in graph-space — the coordinate system *inside* Graphviz's
|
|
55
|
+
* `<g class="graph" transform="scale(...) translate(...)">` wrapper — not the root
|
|
56
|
+
* SVG/viewBox space the served viewer sets `viewBox` in. Every x0/x1 here goes
|
|
57
|
+
* through readGraphTransform()'s toRoot() before it's embedded, the same
|
|
58
|
+
* conversion fitCanvas() applies to the edge-detour bbox for the same reason. */
|
|
59
|
+
export function buildSliceOverlay(svg, grid, rects) {
|
|
60
|
+
const transform = readGraphTransform(svg);
|
|
61
|
+
const toRootX = (x) => transform.toRoot(x, 0).x;
|
|
62
|
+
const slices = [];
|
|
63
|
+
for (let c = 0; c < grid.cols; c++) {
|
|
64
|
+
const r = rects.get(headerCellId(c));
|
|
65
|
+
if (!r)
|
|
66
|
+
continue;
|
|
67
|
+
slices.push({ index: c, name: grid.sliceNames[c], x0: toRootX(r.left), x1: toRootX(r.right) });
|
|
68
|
+
}
|
|
69
|
+
if (slices.length === 0)
|
|
70
|
+
return "";
|
|
71
|
+
const rowLabels = rowLabelXRange(svg, grid.rows.length, toRootX);
|
|
72
|
+
const payload = { slices, rowLabels };
|
|
73
|
+
const metadata = `<metadata id="em-slices">${escXml(JSON.stringify(payload))}</metadata>`;
|
|
74
|
+
const style = `<style>.em-slice-dim{opacity:.15;transition:opacity .15s ease}</style>`;
|
|
75
|
+
return metadata + style;
|
|
76
|
+
}
|
|
77
|
+
/** Estimate the row-label column's x-range from its `<text>` elements — see the
|
|
78
|
+
* file header comment for why this can't go through parseNodeRects. `toRootX`
|
|
79
|
+
* converts the graph-space text position (and the estimated half-width around
|
|
80
|
+
* it) into root/viewBox space — see buildSliceOverlay's comment. Returns null
|
|
81
|
+
* if no row label rendered any text (e.g. an empty model). */
|
|
82
|
+
function rowLabelXRange(svg, rowCount, toRootX) {
|
|
83
|
+
let range = null;
|
|
84
|
+
for (let r = 0; r < rowCount; r++) {
|
|
85
|
+
// Scoped to this row's own node group (up to its closing </g>) rather than a
|
|
86
|
+
// fixed-length lookahead — the header row's label is always "" (no <text> of
|
|
87
|
+
// its own), and a fixed window would otherwise pick up the *next* row's text.
|
|
88
|
+
const body = new RegExp(`<title>${rowLabelId(r)}<\\/title>([\\s\\S]*?)<\\/g>`).exec(svg)?.[1];
|
|
89
|
+
if (!body)
|
|
90
|
+
continue;
|
|
91
|
+
const m = TEXT.exec(body);
|
|
92
|
+
if (!m)
|
|
93
|
+
continue; // the header row's label is "" — plaintext emits no <text> at all
|
|
94
|
+
const x = +(attr(m[1], "x") ?? NaN);
|
|
95
|
+
if (Number.isNaN(x))
|
|
96
|
+
continue;
|
|
97
|
+
const fontSize = +(attr(m[1], "font-size") ?? 10);
|
|
98
|
+
const halfWidth = (decode(m[2]).length * fontSize) / 2 + 4; // + a little breathing room
|
|
99
|
+
const x0 = toRootX(x - halfWidth);
|
|
100
|
+
const x1 = toRootX(x + halfWidth);
|
|
101
|
+
range = range ? { x0: Math.min(range.x0, x0), x1: Math.max(range.x1, x1) } : { x0, x1 };
|
|
102
|
+
}
|
|
103
|
+
return range;
|
|
104
|
+
}
|
|
105
|
+
function attr(attrs, name) {
|
|
106
|
+
return new RegExp(`\\b${name}="(-?[\\d.]+)"`).exec(attrs)?.[1];
|
|
107
|
+
}
|
|
108
|
+
function escXml(s) {
|
|
109
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=sliceOverlay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sliceOverlay.js","sourceRoot":"","sources":["../../src/render/sliceOverlay.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,6EAA6E;AAC7E,yEAAyE;AACzE,EAAE;AACF,gFAAgF;AAChF,yEAAyE;AACzE,6EAA6E;AAC7E,iDAAiD;AACjD,EAAE;AACF,6EAA6E;AAC7E,iFAAiF;AACjF,iCAAiC;AACjC,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,mEAAmE;AACnE,2EAA2E;AAC3E,yEAAyE;AACzE,+EAA+E;AAC/E,8EAA8E;AAC9E,2EAA2E;AAC3E,+EAA+E;AAC/E,+EAA+E;AAC/E,gCAAgC;AAGhC,OAAO,EAAQ,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,UAAU,EAAE,KAAK,EAAQ,MAAM,kBAAkB,CAAC;AAEvF,MAAM,IAAI,GAAG,gCAAgC,CAAC;AAE9C;kEACkE;AAClE,MAAM,UAAU,eAAe,CAAC,IAAU;IACxC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE;QAAE,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9D,OAAO,GAAG,CAAC;AACb,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,KAAsB;IAC/D,OAAO,GAAG,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE;QACvC,MAAM,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAChD,MAAM,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC9B,IAAI,CAAC,EAAE;YAAE,OAAO,KAAK,CAAC,CAAC,0DAA0D;QACjF,OAAO,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,kBAAkB,EAAE,CAAC,UAAU,GAAG,CAAC,CAAC;IACpE,CAAC,CAAC,CAAC;AACL,CAAC;AASD;;;;;;;;;;;kFAWkF;AAClF,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,IAAU,EAAE,KAAwB;IACjF,MAAM,SAAS,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;IAC1C,MAAM,OAAO,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAEhE,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;QACnC,MAAM,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;QACrC,IAAI,CAAC,CAAC;YAAE,SAAS;QACjB,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACjG,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEnC,MAAM,SAAS,GAAG,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjE,MAAM,OAAO,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IACtC,MAAM,QAAQ,GAAG,4BAA4B,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,aAAa,CAAC;IAC1F,MAAM,KAAK,GAAG,wEAAwE,CAAC;IACvF,OAAO,QAAQ,GAAG,KAAK,CAAC;AAC1B,CAAC;AAED;;;;+DAI+D;AAC/D,SAAS,cAAc,CACrB,GAAW,EACX,QAAgB,EAChB,OAA8B;IAE9B,IAAI,KAAK,GAAsC,IAAI,CAAC;IACpD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,6EAA6E;QAC7E,6EAA6E;QAC7E,8EAA8E;QAC9E,MAAM,IAAI,GAAG,IAAI,MAAM,CAAC,UAAU,UAAU,CAAC,CAAC,CAAC,8BAA8B,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC9F,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1B,IAAI,CAAC,CAAC;YAAE,SAAS,CAAC,kEAAkE;QACpF,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC;QACpC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YAAE,SAAS;QAC9B,MAAM,QAAQ,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC;QAClD,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,4BAA4B;QACxF,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;QAClC,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;QAClC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;IAC1F,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,IAAI,CAAC,KAAa,EAAE,IAAY;IACvC,OAAO,IAAI,MAAM,CAAC,MAAM,IAAI,gBAAgB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACjE,CAAC;AAED,SAAS,MAAM,CAAC,CAAS;IACvB,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC9E,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { NormalizedModel } from "../model/model.js";
|
|
2
|
+
/** One status per slice, same order as model.slices (and so the same order as
|
|
3
|
+
* the header row's columns — see layout/grid.ts's sliceNames). null means no
|
|
4
|
+
* doc was found, or the doc has no `- **Status:** ...` line. */
|
|
5
|
+
export declare function readSliceStatuses(model: NormalizedModel, baseDir: string): (string | null)[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Reads each slice's development status from its sibling design doc
|
|
3
|
+
// (slices/<kebab-slug>.md, see .claude/skills/event-modeling/templates/slice.md),
|
|
4
|
+
// for the main render pipeline (composeSvg) to color slice headers with.
|
|
5
|
+
//
|
|
6
|
+
// Same doc-path convention `em catalog` already uses (src/catalog/build.ts):
|
|
7
|
+
// the *deduped* export key from computeRefs(), not a fresh kebabSlug(name) —
|
|
8
|
+
// so two same-named slices resolve statuses the same way `em catalog` resolves
|
|
9
|
+
// docs for them (first one gets the real file, the second honestly gets null).
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { computeRefs } from "../emit/json.js";
|
|
13
|
+
import { parseSliceDoc } from "../catalog/sliceDoc.js";
|
|
14
|
+
/** One status per slice, same order as model.slices (and so the same order as
|
|
15
|
+
* the header row's columns — see layout/grid.ts's sliceNames). null means no
|
|
16
|
+
* doc was found, or the doc has no `- **Status:** ...` line. */
|
|
17
|
+
export function readSliceStatuses(model, baseDir) {
|
|
18
|
+
const { sliceKeys } = computeRefs(model);
|
|
19
|
+
return model.slices.map((_slice, i) => {
|
|
20
|
+
const docPath = join(baseDir, "slices", `${sliceKeys[i]}.md`);
|
|
21
|
+
if (!existsSync(docPath))
|
|
22
|
+
return null;
|
|
23
|
+
return parseSliceDoc(readFileSync(docPath, "utf8")).status;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=sliceStatus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sliceStatus.js","sourceRoot":"","sources":["../../src/render/sliceStatus.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,oEAAoE;AACpE,kFAAkF;AAClF,yEAAyE;AACzE,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,+EAA+E;AAC/E,+EAA+E;AAE/E,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAEvD;;iEAEiE;AACjE,MAAM,UAAU,iBAAiB,CAAC,KAAsB,EAAE,OAAe;IACvE,MAAM,EAAE,SAAS,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IACzC,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QACpC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAC9D,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;QACtC,OAAO,aAAa,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;IAC7D,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Grid } from "../layout/grid.js";
|
|
2
|
+
/** Recolor each header cell whose slice resolved to a known status (see
|
|
3
|
+
* statusStyle()). Slices with no doc, `draft`, or an unrecognized status
|
|
4
|
+
* string are left with the default header colors, untouched. */
|
|
5
|
+
export declare function applyStatusColors(svg: string, grid: Grid, statuses: (string | null)[]): string;
|
|
6
|
+
/**
|
|
7
|
+
* Grow the canvas and append a "Slice Status" legend below the diagram, one
|
|
8
|
+
* swatch+label row per distinct known status actually present among
|
|
9
|
+
* `statuses`. Returns the SVG unchanged if no slice resolved to a known
|
|
10
|
+
* status (nobody's using slice docs yet, or none set a recognized value), or
|
|
11
|
+
* if the SVG's dimensions can't be parsed. Independent of appendNoteLegend
|
|
12
|
+
* (src/render/drawNotes.ts) — safe to call after it since it recomputes the
|
|
13
|
+
* (possibly already-grown) viewBox itself, same as every other
|
|
14
|
+
* append-below-diagram step in the render pipeline.
|
|
15
|
+
*/
|
|
16
|
+
export declare function appendStatusLegend(svg: string, grid: Grid, statuses: (string | null)[]): string;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Recolors each slice header cell's fill/stroke/font in a Graphviz-rendered SVG
|
|
3
|
+
// based on that slice's development status (see src/render/sliceStatus.ts),
|
|
4
|
+
// and appends a small legend explaining the colors actually used.
|
|
5
|
+
//
|
|
6
|
+
// This is a post-layout overlay, the same pattern as drawEdges.ts/drawNotes.ts:
|
|
7
|
+
// Graphviz always lays out every header cell identically (status never affects
|
|
8
|
+
// size, only color), so recoloring is a scoped string replace on the already-
|
|
9
|
+
// rendered header node group rather than anything that needs new geometry.
|
|
10
|
+
import { headerCellId } from "../layout/grid.js";
|
|
11
|
+
import { HEADER_FILL, HEADER_BORDER, HEADER_FONT } from "../emit/dot.js";
|
|
12
|
+
import { statusStyle } from "../emit/theme.js";
|
|
13
|
+
// Graphviz lowercases hex colors in its SVG output (e.g. our "#E3E7EB" comes
|
|
14
|
+
// back as "#e3e7eb"), so matching must be case-insensitive even though we
|
|
15
|
+
// authored the DOT-side constants in uppercase.
|
|
16
|
+
function attrRe(name, value) {
|
|
17
|
+
return new RegExp(`${name}="${value}"`, "gi");
|
|
18
|
+
}
|
|
19
|
+
/** Recolor each header cell whose slice resolved to a known status (see
|
|
20
|
+
* statusStyle()). Slices with no doc, `draft`, or an unrecognized status
|
|
21
|
+
* string are left with the default header colors, untouched. */
|
|
22
|
+
export function applyStatusColors(svg, grid, statuses) {
|
|
23
|
+
let out = svg;
|
|
24
|
+
for (let c = 0; c < grid.cols; c++) {
|
|
25
|
+
const style = statusStyle(statuses[c] ?? null);
|
|
26
|
+
if (!style)
|
|
27
|
+
continue;
|
|
28
|
+
const id = headerCellId(c);
|
|
29
|
+
const re = new RegExp(`(<title>${id}</title>)([\\s\\S]*?)(</g>)`);
|
|
30
|
+
out = out.replace(re, (_whole, title, body, close) => {
|
|
31
|
+
const recolored = body
|
|
32
|
+
.replace(attrRe("fill", HEADER_FILL), `fill="${style.fill}"`)
|
|
33
|
+
.replace(attrRe("stroke", HEADER_BORDER), `stroke="${style.stroke}"`)
|
|
34
|
+
.replace(attrRe("fill", HEADER_FONT), `fill="${style.fontColor}"`);
|
|
35
|
+
return title + recolored + close;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
// ---- legend ----
|
|
41
|
+
const PAD_X = 16;
|
|
42
|
+
const PAD_TOP = 14;
|
|
43
|
+
const HEAD_H = 22;
|
|
44
|
+
const LINE_H = 18;
|
|
45
|
+
const PAD_BOTTOM = 14;
|
|
46
|
+
const SWATCH = 11;
|
|
47
|
+
/** Canonical display order, independent of column order — a model's slices can
|
|
48
|
+
* land in any status in any order, but the legend should read the same way
|
|
49
|
+
* (earliest-to-latest lifecycle stage) every time. */
|
|
50
|
+
const STATUS_ORDER = ["reviewed", "ready-to-implement", "implemented"];
|
|
51
|
+
function humanizeStatus(status) {
|
|
52
|
+
return status.charAt(0).toUpperCase() + status.slice(1).replace(/-/g, " ");
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Grow the canvas and append a "Slice Status" legend below the diagram, one
|
|
56
|
+
* swatch+label row per distinct known status actually present among
|
|
57
|
+
* `statuses`. Returns the SVG unchanged if no slice resolved to a known
|
|
58
|
+
* status (nobody's using slice docs yet, or none set a recognized value), or
|
|
59
|
+
* if the SVG's dimensions can't be parsed. Independent of appendNoteLegend
|
|
60
|
+
* (src/render/drawNotes.ts) — safe to call after it since it recomputes the
|
|
61
|
+
* (possibly already-grown) viewBox itself, same as every other
|
|
62
|
+
* append-below-diagram step in the render pipeline.
|
|
63
|
+
*/
|
|
64
|
+
export function appendStatusLegend(svg, grid, statuses) {
|
|
65
|
+
const present = new Set(statuses.slice(0, grid.cols).map((s) => s?.toLowerCase()).filter((s) => !!s));
|
|
66
|
+
const rows = STATUS_ORDER.filter((s) => present.has(s));
|
|
67
|
+
if (rows.length === 0)
|
|
68
|
+
return svg;
|
|
69
|
+
const vb = /viewBox="([\d.eE+-]+)\s+([\d.eE+-]+)\s+([\d.eE+-]+)\s+([\d.eE+-]+)"/.exec(svg);
|
|
70
|
+
const hpt = /height="([\d.eE+-]+)pt"/.exec(svg);
|
|
71
|
+
if (!vb || !hpt)
|
|
72
|
+
return svg;
|
|
73
|
+
const minX = +vb[1];
|
|
74
|
+
const minY = +vb[2];
|
|
75
|
+
const vw = +vb[3];
|
|
76
|
+
const vh = +vb[4];
|
|
77
|
+
const legendH = PAD_TOP + HEAD_H + rows.length * LINE_H + PAD_BOTTOM;
|
|
78
|
+
const newVh = vh + legendH;
|
|
79
|
+
const newHpt = +hpt[1] * (newVh / vh);
|
|
80
|
+
const top = minY + vh;
|
|
81
|
+
let out = svg;
|
|
82
|
+
out = out.replace(hpt[0], `height="${n(newHpt)}pt"`);
|
|
83
|
+
out = out.replace(vb[0], `viewBox="${vb[1]} ${vb[2]} ${vb[3]} ${n(newVh)}"`);
|
|
84
|
+
const sectionY = top + PAD_TOP;
|
|
85
|
+
let sections = `<text x="${n(minX + PAD_X)}" y="${n(sectionY + 14)}" font-family="Helvetica" ` +
|
|
86
|
+
`font-weight="bold" font-size="12" fill="#202124">Slice Status</text>`;
|
|
87
|
+
sections += rows
|
|
88
|
+
.map((status, i) => {
|
|
89
|
+
const style = statusStyle(status);
|
|
90
|
+
const y = sectionY + HEAD_H + i * LINE_H;
|
|
91
|
+
return (`<rect x="${n(minX + PAD_X)}" y="${n(y)}" width="${SWATCH}" height="${SWATCH}" ` +
|
|
92
|
+
`fill="${style.fill}" stroke="${style.stroke}"/>` +
|
|
93
|
+
`<text x="${n(minX + PAD_X + SWATCH + 6)}" y="${n(y + SWATCH - 1)}" font-family="Helvetica" ` +
|
|
94
|
+
`font-size="11" fill="#3C4043">${esc(humanizeStatus(status))}</text>`);
|
|
95
|
+
})
|
|
96
|
+
.join("");
|
|
97
|
+
const legend = `<g class="em-status-legend">` +
|
|
98
|
+
`<rect x="${n(minX)}" y="${n(top)}" width="${n(vw)}" height="${n(legendH)}" fill="#FFFFFF"/>` +
|
|
99
|
+
`<line x1="${n(minX)}" y1="${n(top)}" x2="${n(minX + vw)}" y2="${n(top)}" stroke="#D0D0D0" stroke-width="1"/>` +
|
|
100
|
+
sections +
|
|
101
|
+
`</g>`;
|
|
102
|
+
return out.replace(/(<\/g>\s*)(<\/svg>)/, `$1${legend}$2`);
|
|
103
|
+
}
|
|
104
|
+
function n(v) {
|
|
105
|
+
return String(Math.round(v * 100) / 100);
|
|
106
|
+
}
|
|
107
|
+
function esc(s) {
|
|
108
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=statusOverlay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"statusOverlay.js","sourceRoot":"","sources":["../../src/render/statusOverlay.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,6 +6,8 @@ export interface Rect {
|
|
|
6
6
|
cx: number;
|
|
7
7
|
cy: number;
|
|
8
8
|
}
|
|
9
|
+
export declare const NODE_GROUP: RegExp;
|
|
10
|
+
export declare const TITLE: RegExp;
|
|
9
11
|
/** Map each requested node id to its rectangle in SVG coordinates. */
|
|
10
12
|
export declare function parseNodeRects(svg: string, ids: Set<string>): Map<string, Rect>;
|
|
11
13
|
/** A bounding box in the graph's own (post-transform) coordinate space. */
|
|
@@ -15,9 +17,37 @@ export interface Box {
|
|
|
15
17
|
maxX: number;
|
|
16
18
|
maxY: number;
|
|
17
19
|
}
|
|
20
|
+
/** scale/translate read from the graph's own transform, and the root-space
|
|
21
|
+
* conversion it implies — see readGraphTransform() for where this comes from. */
|
|
22
|
+
export interface GraphTransform {
|
|
23
|
+
sx: number;
|
|
24
|
+
sy: number;
|
|
25
|
+
tx: number;
|
|
26
|
+
ty: number;
|
|
27
|
+
/** Convert a graph-space (pre-transform) point — e.g. a rect from
|
|
28
|
+
* parseNodeRects(), or a raw `<text>` x/y — into root/viewBox space. */
|
|
29
|
+
toRoot(x: number, y: number): {
|
|
30
|
+
x: number;
|
|
31
|
+
y: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Graphviz wraps the graph in `transform="scale(sx sy) rotate(0) translate(tx ty)"`,
|
|
36
|
+
* applied right to left, so a graph-space point lands at `scale * (p + translate)`.
|
|
37
|
+
* Anything computed from parseNodeRects()'s box coordinates (or a `<text>` element's
|
|
38
|
+
* raw x/y) is in that same pre-transform graph space — it must go through
|
|
39
|
+
* `toRoot()` before it's valid as a viewBox coordinate or compared against one.
|
|
40
|
+
* Defaults to the identity transform (scale 1, translate 0) if the graph group or
|
|
41
|
+
* its transform attribute isn't present, matching what Graphviz emits when neither
|
|
42
|
+
* scaling nor an offset was needed.
|
|
43
|
+
*/
|
|
44
|
+
export declare function readGraphTransform(svg: string): GraphTransform;
|
|
18
45
|
/**
|
|
19
46
|
* Grow the canvas so content drawn in graph space isn't clipped by the root viewBox.
|
|
20
47
|
* Edge detours can legitimately run outside the box grid Graphviz sized the canvas for
|
|
21
48
|
* (an arc under the bottom lane, say), and anything past the viewBox is simply lost.
|
|
22
49
|
*/
|
|
23
50
|
export declare function fitCanvas(svg: string, box: Box | null): string;
|
|
51
|
+
/** Decode the handful of entities Graphviz emits in `<title>` text. Shared with
|
|
52
|
+
* sliceOverlay.ts, which also needs to recover an element id from a title. */
|
|
53
|
+
export declare function decode(s: string): string;
|