libpetri 1.8.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/dist/{chunk-B2D5DMTO.js → chunk-4L6JVKH4.js} +165 -8
- package/dist/chunk-4L6JVKH4.js.map +1 -0
- package/dist/chunk-H62Z76FY.js +1346 -0
- package/dist/chunk-H62Z76FY.js.map +1 -0
- package/dist/chunk-SXK2Z45Z.js +50 -0
- package/dist/chunk-SXK2Z45Z.js.map +1 -0
- package/dist/debug/index.d.ts +50 -3
- package/dist/debug/index.js +64 -6
- package/dist/debug/index.js.map +1 -1
- package/dist/doclet/index.d.ts +152 -31
- package/dist/doclet/index.js +458 -57
- package/dist/doclet/index.js.map +1 -1
- package/dist/doclet/resources/petrinet-diagrams.css +384 -7
- package/dist/doclet/resources/petrinet-diagrams.js +7214 -106
- package/dist/dot-exporter-PMHOQHZ3.js +8 -0
- package/dist/{event-store-BnyHh3TF.d.ts → event-store-2zkXeQkd.d.ts} +1 -1
- package/dist/export/index.d.ts +16 -2
- package/dist/export/index.js +1 -1
- package/dist/index.d.ts +41 -5
- package/dist/index.js +1221 -35
- package/dist/index.js.map +1 -1
- package/dist/petri-net-BDrj4XZE.d.ts +1461 -0
- package/dist/render-P6GROU7J.js +16 -0
- package/dist/render-P6GROU7J.js.map +1 -0
- package/dist/verification/index.d.ts +3 -144
- package/dist/verification/index.js +30 -1214
- package/dist/verification/index.js.map +1 -1
- package/dist/viewer/index.d.ts +196 -0
- package/dist/viewer/index.js +442 -0
- package/dist/viewer/index.js.map +1 -0
- package/dist/viewer/viewer-static.iife.js +3 -0
- package/dist/viewer/viewer.css +503 -0
- package/dist/viewer/viewer.iife.js +7219 -0
- package/package.json +17 -8
- package/dist/chunk-B2D5DMTO.js.map +0 -1
- package/dist/chunk-VQ4XMJTD.js +0 -107
- package/dist/chunk-VQ4XMJTD.js.map +0 -1
- package/dist/dot-exporter-3CVCH6J4.js +0 -8
- package/dist/petri-net-D-GN9g_D.d.ts +0 -570
- /package/dist/{dot-exporter-3CVCH6J4.js.map → dot-exporter-PMHOQHZ3.js.map} +0 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import panzoom from 'panzoom';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pan/zoom wrapper for the canonical libpetri viewer.
|
|
5
|
+
*
|
|
6
|
+
* Delegates to the `panzoom` library (no hand-rolled wheel handlers — we
|
|
7
|
+
* intentionally drop the IIFE/javadoc version's bespoke math in favour of
|
|
8
|
+
* a single battle-tested implementation). Defaults match the old debug-ui
|
|
9
|
+
* and dev-preview values so the canonical viewer feels identical to what
|
|
10
|
+
* users had before.
|
|
11
|
+
*
|
|
12
|
+
* @module viewer/pan-zoom
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
type PanzoomInstance = ReturnType<typeof panzoom>;
|
|
16
|
+
type PanzoomOptions = Parameters<typeof panzoom>[1];
|
|
17
|
+
/**
|
|
18
|
+
* Default panzoom configuration shared across all viewer surfaces.
|
|
19
|
+
*
|
|
20
|
+
* - `maxZoom: 1000` — effectively unlimited; lets users dive into ~150-node
|
|
21
|
+
* diagrams without hitting an artificial ceiling.
|
|
22
|
+
* - `minZoom: 0.02` — allows fitting very large nets in a small viewport.
|
|
23
|
+
* - `smoothScroll: false` — sharper interactive feel.
|
|
24
|
+
* - `zoomDoubleClickSpeed: 1` — single-click double-zoom toggle.
|
|
25
|
+
*/
|
|
26
|
+
declare const DEFAULT_PANZOOM_OPTS: {
|
|
27
|
+
readonly smoothScroll: false;
|
|
28
|
+
readonly zoomDoubleClickSpeed: 1;
|
|
29
|
+
readonly maxZoom: 1000;
|
|
30
|
+
readonly minZoom: 0.02;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Cluster overlay for the canonical libpetri viewer.
|
|
35
|
+
*
|
|
36
|
+
* Post-processes a rendered SVG to surface subnet structure visually:
|
|
37
|
+
* 1. Discover `<g class="cluster">` subgraphs, derive each prefix from the
|
|
38
|
+
* contained `<title>` (Graphviz emits `cluster_<sanitizedPrefix>`).
|
|
39
|
+
* 2. Tag every contained `<g class="node">` with `data-instance="<prefix>"`
|
|
40
|
+
* so filtering can target it via attribute selectors.
|
|
41
|
+
* 3. Assign each cluster a deterministic HSL colour (FNV-1a hash + golden
|
|
42
|
+
* ratio hue increment) and paint its border.
|
|
43
|
+
* 4. Provide collapse/expand and "show only <prefix>" filtering.
|
|
44
|
+
*
|
|
45
|
+
* Unlike the previous debug-ui cluster-overlay, this module is instance-
|
|
46
|
+
* based: every `mount()` call gets its own `ClusterOverlay` so multiple
|
|
47
|
+
* viewers on the same page don't share collapse/isolate state.
|
|
48
|
+
*
|
|
49
|
+
* @module viewer/cluster-overlay
|
|
50
|
+
*/
|
|
51
|
+
/** A discovered cluster with its DOM group, node count, and palette colour. */
|
|
52
|
+
interface ClusterDescriptor {
|
|
53
|
+
readonly prefix: string;
|
|
54
|
+
readonly group: SVGGElement;
|
|
55
|
+
readonly nodeCount: number;
|
|
56
|
+
readonly color: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Walk the SVG, find every `<g class="cluster">`, derive the prefix from
|
|
60
|
+
* each cluster's `<title>` (`cluster_<sanitized>`), and tag every cluster-
|
|
61
|
+
* interior `<g class="node">` with `data-instance="<prefix>"`. Returns one
|
|
62
|
+
* descriptor per cluster.
|
|
63
|
+
*
|
|
64
|
+
* Graphviz emits cluster nodes as SIBLINGS of the cluster `<g>`, not as
|
|
65
|
+
* children — `<g class="cluster">` only carries title, polygon border, and
|
|
66
|
+
* the cluster's text label. To recover cluster membership we match each
|
|
67
|
+
* node's `<title>` (the original graph id) against libpetri's prefixing
|
|
68
|
+
* convention: interior places are emitted as `p_<prefix>_<name>`,
|
|
69
|
+
* transitions as `t_<prefix>_<name>`, and junctions as `j_<prefix>_<name>`.
|
|
70
|
+
*
|
|
71
|
+
* Does not split nested-cluster prefixes (`outer_inner` could mean either
|
|
72
|
+
* `outer/inner` or a flat `outer_inner` prefix — Graphviz sanitisation is
|
|
73
|
+
* lossy). We treat every cluster as its own top-level entry, matching the
|
|
74
|
+
* doclet's pragmatism.
|
|
75
|
+
*/
|
|
76
|
+
declare function discoverClusters(svg: SVGSVGElement): Map<string, ClusterDescriptor>;
|
|
77
|
+
/**
|
|
78
|
+
* Deterministic HSL palette. FNV-1a-ish hash of the prefix, multiplied by
|
|
79
|
+
* the golden-ratio fraction (0.61803...) to spread adjacent prefixes
|
|
80
|
+
* visually. Saturation/lightness are fixed (60% / 45%) so the palette
|
|
81
|
+
* stays readable on both the doc background and the dark debug UI.
|
|
82
|
+
*/
|
|
83
|
+
declare function colorForPrefix(prefix: string): string;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* CSS custom properties exposed by the canonical viewer stylesheet.
|
|
87
|
+
*
|
|
88
|
+
* Each property has a default in `viewer.css`. Consumers can override any of
|
|
89
|
+
* them at any scope (the viewer container, the document root, etc.) to
|
|
90
|
+
* theme the viewer without touching the canonical CSS.
|
|
91
|
+
*
|
|
92
|
+
* Names are listed here as a reference + so the IIFE bundle can expose them
|
|
93
|
+
* on `window.LibpetriViewer.cssVariables`. The list is informational — the
|
|
94
|
+
* stylesheet is still the source of truth.
|
|
95
|
+
*
|
|
96
|
+
* @module viewer/styles
|
|
97
|
+
*/
|
|
98
|
+
/** CSS custom property names that the canonical stylesheet honours. */
|
|
99
|
+
declare const VIEWER_CSS_VARIABLES: readonly ["--lpv-bg", "--lpv-header-bg", "--lpv-border", "--lpv-text", "--lpv-muted", "--lpv-cluster-bg", "--lpv-cluster-bg-collapsed", "--lpv-cluster-stroke-width", "--lpv-cluster-stroke-dash", "--lpv-cluster-fill-tint", "--lpv-dim-opacity", "--lpv-active-filter-outline", "--lpv-legend-bg", "--lpv-chip-bg", "--lpv-chip-active-bg"];
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Canonical libpetri Petri-net diagram viewer.
|
|
103
|
+
*
|
|
104
|
+
* Single source of truth for DOT → SVG rendering + cluster overlay across
|
|
105
|
+
* all four surfaces:
|
|
106
|
+
* - debug-ui (live debugger)
|
|
107
|
+
* - dev-preview (Vite app)
|
|
108
|
+
* - Javadoc taglet (Java doclet)
|
|
109
|
+
* - Rustdoc embed (libpetri-docgen)
|
|
110
|
+
* - TypeDoc / typescript doclet
|
|
111
|
+
*
|
|
112
|
+
* Build outputs:
|
|
113
|
+
* - ESM: `dist/viewer/index.js` — consumed by Vite/Webpack/Node bundlers.
|
|
114
|
+
* - IIFE: `dist/viewer/viewer.iife.js` — self-contained, exposes
|
|
115
|
+
* `window.LibpetriViewer = { mount, ... }`. Used by the doc taglets
|
|
116
|
+
* that ship pre-rendered HTML pages without a bundler.
|
|
117
|
+
* - CSS: `dist/viewer/viewer.css` — canonical stylesheet (copied from
|
|
118
|
+
* `src/viewer/resources/viewer.css`).
|
|
119
|
+
*
|
|
120
|
+
* Edits to viewer behaviour MUST happen here. The three resource directories
|
|
121
|
+
* under `java/`, `rust/`, and `typescript/src/doclet/` are **build outputs**
|
|
122
|
+
* populated by `scripts/build-viewer.sh` and must not be hand-edited.
|
|
123
|
+
*
|
|
124
|
+
* @module viewer
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
/** Handle returned by {@link mount}. */
|
|
128
|
+
interface ViewerHandle {
|
|
129
|
+
/** The rendered root `<svg>` element. */
|
|
130
|
+
readonly svg: SVGSVGElement;
|
|
131
|
+
/** The panzoom instance attached to the SVG. */
|
|
132
|
+
readonly panzoom: PanzoomInstance;
|
|
133
|
+
/** Map of cluster prefix → descriptor for the current SVG. */
|
|
134
|
+
readonly clusters: ReadonlyMap<string, ClusterDescriptor>;
|
|
135
|
+
/** Prefixes currently marked collapsed on this handle. */
|
|
136
|
+
readonly collapsedPrefixes: ReadonlySet<string>;
|
|
137
|
+
/** Currently active "show only <prefix>" filter, or null. */
|
|
138
|
+
readonly activeFilter: string | null;
|
|
139
|
+
/** Collapse a single cluster by prefix. No-op if unknown. */
|
|
140
|
+
collapse(prefix: string): void;
|
|
141
|
+
/** Expand a single cluster by prefix. No-op if unknown. */
|
|
142
|
+
expand(prefix: string): void;
|
|
143
|
+
/** Collapse every discovered cluster. */
|
|
144
|
+
collapseAll(): void;
|
|
145
|
+
/** Expand every collapsed cluster. */
|
|
146
|
+
expandAll(): void;
|
|
147
|
+
/** Apply the "show only <prefix>" filter; pass `null` to clear. */
|
|
148
|
+
filter(prefix: string | null): void;
|
|
149
|
+
/** Reset pan/zoom to the identity transform. */
|
|
150
|
+
resetZoom(): void;
|
|
151
|
+
/** Scale and translate so the full diagram fits the host viewport. */
|
|
152
|
+
fit(): void;
|
|
153
|
+
/** Reports whether `graphId` is inside a currently-collapsed cluster. */
|
|
154
|
+
isInsideCollapsedCluster(graphId: string): boolean;
|
|
155
|
+
/** Tear down: dispose panzoom and detach listeners. */
|
|
156
|
+
dispose(): void;
|
|
157
|
+
}
|
|
158
|
+
/** Options accepted by {@link mount}. */
|
|
159
|
+
interface MountOptions {
|
|
160
|
+
/**
|
|
161
|
+
* Previous handle to dispose before mounting. Pass the handle returned
|
|
162
|
+
* by a prior `mount()` call so callers don't need to track it separately.
|
|
163
|
+
* Cluster collapse / filter state from `previousHandle` is preserved
|
|
164
|
+
* across the re-render so the user's intent survives a live re-draw.
|
|
165
|
+
*/
|
|
166
|
+
readonly previousHandle?: ViewerHandle | null;
|
|
167
|
+
/** Per-call panzoom overrides; merged on top of {@link DEFAULT_PANZOOM_OPTS}. */
|
|
168
|
+
readonly panzoom?: PanzoomOptions;
|
|
169
|
+
/** Fired after a cluster's collapsed state changes via the handle API. */
|
|
170
|
+
readonly onClusterCollapse?: (prefix: string, collapsed: boolean) => void;
|
|
171
|
+
/** Fired after `filter()` runs, with the new filter prefix or null. */
|
|
172
|
+
readonly onClusterFilter?: (prefix: string | null) => void;
|
|
173
|
+
/** When true, append a legend sidebar + filter chip strip inside `container`. */
|
|
174
|
+
readonly chrome?: boolean;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Render a DOT source, mount the resulting SVG into `container`, wire pan/zoom,
|
|
178
|
+
* and surface cluster overlay controls.
|
|
179
|
+
*
|
|
180
|
+
* The container's existing children are removed before the new SVG is
|
|
181
|
+
* appended (the same teardown behaviour `renderDotToContainer` used to provide).
|
|
182
|
+
*
|
|
183
|
+
* Pass `dotSource === null` to adopt an SVG already present as a direct child
|
|
184
|
+
* of `container` (pre-rendered SVG path — e.g. the Java javadoc taglet emits
|
|
185
|
+
* `dot -Tsvg` at build time). In that mode the viewer never imports the
|
|
186
|
+
* runtime DOT renderer, which lets the static IIFE bundle drop `@viz-js/viz`
|
|
187
|
+
* entirely.
|
|
188
|
+
*
|
|
189
|
+
* NOTE: when shipping a pre-rendered SVG (`dotSource === null`), do NOT also
|
|
190
|
+
* pass `previousHandle` — the handle's `dispose()` runs before SVG detection
|
|
191
|
+
* and would orphan the host. The Java taglet mounts once per page so this is
|
|
192
|
+
* a non-issue in practice.
|
|
193
|
+
*/
|
|
194
|
+
declare function mount(dotSource: string | null, container: HTMLElement, opts?: MountOptions): Promise<ViewerHandle>;
|
|
195
|
+
|
|
196
|
+
export { type ClusterDescriptor, DEFAULT_PANZOOM_OPTS, type MountOptions, type PanzoomInstance, type PanzoomOptions, VIEWER_CSS_VARIABLES, type ViewerHandle, colorForPrefix, discoverClusters, mount };
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
// src/viewer/pan-zoom.ts
|
|
2
|
+
import panzoom from "panzoom";
|
|
3
|
+
var DEFAULT_PANZOOM_OPTS = {
|
|
4
|
+
smoothScroll: false,
|
|
5
|
+
zoomDoubleClickSpeed: 1,
|
|
6
|
+
maxZoom: 1e3,
|
|
7
|
+
minZoom: 0.02
|
|
8
|
+
};
|
|
9
|
+
function attachPanzoom(svg, options, previous) {
|
|
10
|
+
if (previous) {
|
|
11
|
+
try {
|
|
12
|
+
previous.dispose();
|
|
13
|
+
} catch {
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const opts = { ...DEFAULT_PANZOOM_OPTS, ...options };
|
|
17
|
+
return panzoom(svg, opts);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/viewer/cluster-overlay.ts
|
|
21
|
+
function discoverClusters(svg) {
|
|
22
|
+
const out = /* @__PURE__ */ new Map();
|
|
23
|
+
const clusterGroups = svg.querySelectorAll("g.cluster");
|
|
24
|
+
if (!clusterGroups.length) return out;
|
|
25
|
+
const allNodes = Array.from(svg.querySelectorAll("g.node"));
|
|
26
|
+
for (const g of Array.from(clusterGroups)) {
|
|
27
|
+
const titleEl = directChild(g, "title");
|
|
28
|
+
if (!titleEl) continue;
|
|
29
|
+
const raw = titleEl.textContent ?? "";
|
|
30
|
+
if (!raw.startsWith("cluster_")) continue;
|
|
31
|
+
const prefix = raw.slice("cluster_".length);
|
|
32
|
+
if (out.has(prefix)) continue;
|
|
33
|
+
let nodeCount = 0;
|
|
34
|
+
for (const node of allNodes) {
|
|
35
|
+
if (node.getAttribute("data-instance")) continue;
|
|
36
|
+
const nodeTitle = directChild(node, "title");
|
|
37
|
+
if (!nodeTitle) continue;
|
|
38
|
+
const id = (nodeTitle.textContent ?? "").trim();
|
|
39
|
+
if (nodeBelongsToPrefix(id, prefix)) {
|
|
40
|
+
node.setAttribute("data-instance", prefix);
|
|
41
|
+
nodeCount++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
out.set(prefix, {
|
|
45
|
+
prefix,
|
|
46
|
+
group: g,
|
|
47
|
+
nodeCount,
|
|
48
|
+
color: colorForPrefix(prefix)
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function nodeBelongsToPrefix(nodeId, prefix) {
|
|
54
|
+
return nodeId === prefix || nodeId.startsWith(`${prefix}/`) || nodeId.startsWith(`p_${prefix}_`) || nodeId.startsWith(`t_${prefix}_`) || nodeId.startsWith(`j_${prefix}_`);
|
|
55
|
+
}
|
|
56
|
+
function colorForPrefix(prefix) {
|
|
57
|
+
let h = 2166136261;
|
|
58
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
59
|
+
h ^= prefix.charCodeAt(i);
|
|
60
|
+
h = h * 16777619 >>> 0;
|
|
61
|
+
}
|
|
62
|
+
let hue = Math.floor((h / 4294967296 * 360 + h % 360 * 0.61803) % 360);
|
|
63
|
+
if (hue < 0) hue += 360;
|
|
64
|
+
return `hsl(${hue}, 60%, 45%)`;
|
|
65
|
+
}
|
|
66
|
+
function paintClusterBorders(clusters) {
|
|
67
|
+
for (const c of clusters.values()) {
|
|
68
|
+
const border = directChild(c.group, "polygon") ?? directChild(c.group, "path");
|
|
69
|
+
if (border) {
|
|
70
|
+
border.setAttribute("stroke", c.color);
|
|
71
|
+
border.setAttribute("stroke-width", "2.2");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function setClusterCollapsed(cluster, collapsed) {
|
|
76
|
+
const g = cluster.group;
|
|
77
|
+
const isCollapsed = g.classList.contains("cluster-collapsed");
|
|
78
|
+
const root = g.ownerSVGElement;
|
|
79
|
+
if (!root) return;
|
|
80
|
+
if (collapsed && !isCollapsed) {
|
|
81
|
+
g.classList.add("cluster-collapsed");
|
|
82
|
+
const hidden = [];
|
|
83
|
+
root.querySelectorAll(`g.node[data-instance="${cssEscape(cluster.prefix)}"]`).forEach((node) => {
|
|
84
|
+
node.classList.add("petri-collapsed-inside");
|
|
85
|
+
hidden.push(node);
|
|
86
|
+
});
|
|
87
|
+
root.querySelectorAll("g.edge").forEach((edge) => {
|
|
88
|
+
const titleEl = directChild(edge, "title");
|
|
89
|
+
if (!titleEl) return;
|
|
90
|
+
const t = (titleEl.textContent ?? "").trim();
|
|
91
|
+
const arrow = t.indexOf("->");
|
|
92
|
+
if (arrow < 0) return;
|
|
93
|
+
const from = t.slice(0, arrow);
|
|
94
|
+
const to = t.slice(arrow + 2);
|
|
95
|
+
if (nodeBelongsToPrefix(from, cluster.prefix) && nodeBelongsToPrefix(to, cluster.prefix)) {
|
|
96
|
+
edge.classList.add("petri-collapsed-inside");
|
|
97
|
+
hidden.push(edge);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
g._petriHiddenSiblings = hidden;
|
|
101
|
+
if (!g._petriCollapsedBadge) {
|
|
102
|
+
const label = directChild(g, "text");
|
|
103
|
+
if (label) {
|
|
104
|
+
const badge = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
105
|
+
badge.setAttribute("x", label.getAttribute("x") ?? "0");
|
|
106
|
+
const labelY = parseFloat(label.getAttribute("y") ?? "0");
|
|
107
|
+
badge.setAttribute("y", String(labelY + 16));
|
|
108
|
+
badge.setAttribute("text-anchor", label.getAttribute("text-anchor") ?? "middle");
|
|
109
|
+
badge.setAttribute("font-size", "11");
|
|
110
|
+
badge.setAttribute("fill", "#9ca3af");
|
|
111
|
+
badge.setAttribute("class", "cluster-collapsed-badge");
|
|
112
|
+
badge.textContent = `(${cluster.nodeCount} internal node${cluster.nodeCount === 1 ? "" : "s"})`;
|
|
113
|
+
g.appendChild(badge);
|
|
114
|
+
g._petriCollapsedBadge = badge;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} else if (!collapsed && isCollapsed) {
|
|
118
|
+
g.classList.remove("cluster-collapsed");
|
|
119
|
+
if (g._petriHiddenSiblings) {
|
|
120
|
+
g._petriHiddenSiblings.forEach((el) => el.classList.remove("petri-collapsed-inside"));
|
|
121
|
+
g._petriHiddenSiblings = void 0;
|
|
122
|
+
}
|
|
123
|
+
if (g._petriCollapsedBadge && g._petriCollapsedBadge.parentNode === g) {
|
|
124
|
+
g.removeChild(g._petriCollapsedBadge);
|
|
125
|
+
g._petriCollapsedBadge = null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function applyFilter(svg, prefix) {
|
|
130
|
+
if (prefix == null) {
|
|
131
|
+
svg.removeAttribute("data-active-filter");
|
|
132
|
+
svg.classList.remove("has-active-filter");
|
|
133
|
+
svg.querySelectorAll("g.node, g.edge").forEach((el) => el.classList.remove("petri-dimmed"));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
svg.setAttribute("data-active-filter", prefix);
|
|
137
|
+
svg.classList.add("has-active-filter");
|
|
138
|
+
svg.querySelectorAll("g.node").forEach((node) => {
|
|
139
|
+
const di = node.getAttribute("data-instance");
|
|
140
|
+
const match = !!di && (di === prefix || di.indexOf(prefix + "_") === 0 || di.indexOf(prefix + "/") === 0);
|
|
141
|
+
if (match) node.classList.remove("petri-dimmed");
|
|
142
|
+
else node.classList.add("petri-dimmed");
|
|
143
|
+
});
|
|
144
|
+
svg.querySelectorAll("g.edge").forEach((edge) => {
|
|
145
|
+
const titleEl = directChild(edge, "title");
|
|
146
|
+
if (!titleEl) return;
|
|
147
|
+
const titleText = titleEl.textContent ?? "";
|
|
148
|
+
const arrow = titleText.indexOf("->");
|
|
149
|
+
if (arrow < 0) {
|
|
150
|
+
edge.classList.remove("petri-dimmed");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const from = titleText.slice(0, arrow);
|
|
154
|
+
const to = titleText.slice(arrow + 2);
|
|
155
|
+
const matches = (name) => name.indexOf(prefix + "/") >= 0 || name.indexOf(prefix + "_") >= 0 || name === prefix;
|
|
156
|
+
if (matches(from) || matches(to)) edge.classList.remove("petri-dimmed");
|
|
157
|
+
else edge.classList.add("petri-dimmed");
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function isInsideCollapsedCluster(svg, clusters, collapsedPrefixes, graphId) {
|
|
161
|
+
if (collapsedPrefixes.size === 0) return false;
|
|
162
|
+
if (!svg) return false;
|
|
163
|
+
for (const prefix of collapsedPrefixes) {
|
|
164
|
+
if (!clusters.has(prefix)) continue;
|
|
165
|
+
if (nodeBelongsToPrefix(graphId, prefix)) return true;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
function cssEscape(s) {
|
|
170
|
+
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return CSS.escape(s);
|
|
171
|
+
return s.replace(/[^a-zA-Z0-9_-]/g, (c) => `\\${c}`);
|
|
172
|
+
}
|
|
173
|
+
function directChild(parent, tagName) {
|
|
174
|
+
const lc = tagName.toLowerCase();
|
|
175
|
+
for (const child of Array.from(parent.children)) {
|
|
176
|
+
if (child.tagName.toLowerCase() === lc) return child;
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/viewer/styles.ts
|
|
182
|
+
var VIEWER_CSS_VARIABLES = [
|
|
183
|
+
"--lpv-bg",
|
|
184
|
+
"--lpv-header-bg",
|
|
185
|
+
"--lpv-border",
|
|
186
|
+
"--lpv-text",
|
|
187
|
+
"--lpv-muted",
|
|
188
|
+
"--lpv-cluster-bg",
|
|
189
|
+
"--lpv-cluster-bg-collapsed",
|
|
190
|
+
"--lpv-cluster-stroke-width",
|
|
191
|
+
"--lpv-cluster-stroke-dash",
|
|
192
|
+
"--lpv-cluster-fill-tint",
|
|
193
|
+
"--lpv-dim-opacity",
|
|
194
|
+
"--lpv-active-filter-outline",
|
|
195
|
+
"--lpv-legend-bg",
|
|
196
|
+
"--lpv-chip-bg",
|
|
197
|
+
"--lpv-chip-active-bg"
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
// src/viewer/index.ts
|
|
201
|
+
async function mount(dotSource, container, opts = {}) {
|
|
202
|
+
const previousHandle = opts.previousHandle ?? null;
|
|
203
|
+
const preservedCollapsed = previousHandle ? new Set(previousHandle.collapsedPrefixes) : /* @__PURE__ */ new Set();
|
|
204
|
+
const preservedFilter = previousHandle?.activeFilter ?? null;
|
|
205
|
+
if (previousHandle) {
|
|
206
|
+
previousHandle.dispose();
|
|
207
|
+
}
|
|
208
|
+
let svg;
|
|
209
|
+
const existing = container.querySelector(":scope > svg");
|
|
210
|
+
if (dotSource == null) {
|
|
211
|
+
if (!(existing instanceof SVGSVGElement)) {
|
|
212
|
+
throw new Error("mount: no DOT source and no pre-rendered SVG");
|
|
213
|
+
}
|
|
214
|
+
svg = existing;
|
|
215
|
+
} else {
|
|
216
|
+
const { renderDotToSvg } = await import("../render-P6GROU7J.js");
|
|
217
|
+
svg = await renderDotToSvg(dotSource);
|
|
218
|
+
container.innerHTML = "";
|
|
219
|
+
container.appendChild(svg);
|
|
220
|
+
}
|
|
221
|
+
container.classList.add("libpetri-viewer");
|
|
222
|
+
const panzoomInstance = attachPanzoom(svg, opts.panzoom);
|
|
223
|
+
const clusters = discoverClusters(svg);
|
|
224
|
+
paintClusterBorders(clusters);
|
|
225
|
+
const collapsedPrefixes = /* @__PURE__ */ new Set();
|
|
226
|
+
let activeFilter = null;
|
|
227
|
+
let disposed = false;
|
|
228
|
+
let chromeRoot = null;
|
|
229
|
+
function ensureChrome() {
|
|
230
|
+
if (!opts.chrome) return;
|
|
231
|
+
if (chromeRoot && chromeRoot.parentNode === container) return;
|
|
232
|
+
chromeRoot = document.createElement("div");
|
|
233
|
+
chromeRoot.className = "libpetri-viewer-chrome";
|
|
234
|
+
chromeRoot.style.position = "absolute";
|
|
235
|
+
chromeRoot.style.inset = "0";
|
|
236
|
+
chromeRoot.style.pointerEvents = "none";
|
|
237
|
+
const legend = document.createElement("div");
|
|
238
|
+
legend.className = "diagram-legend";
|
|
239
|
+
legend.style.pointerEvents = "auto";
|
|
240
|
+
legend.innerHTML = '<div class="legend-title">Clusters</div>';
|
|
241
|
+
chromeRoot.appendChild(legend);
|
|
242
|
+
const strip = document.createElement("div");
|
|
243
|
+
strip.className = "diagram-filter-strip";
|
|
244
|
+
strip.style.pointerEvents = "auto";
|
|
245
|
+
strip.innerHTML = '<span class="filter-strip-label">Show only:</span>';
|
|
246
|
+
chromeRoot.appendChild(strip);
|
|
247
|
+
const controls = document.createElement("div");
|
|
248
|
+
controls.className = "diagram-controls";
|
|
249
|
+
controls.style.pointerEvents = "auto";
|
|
250
|
+
const resetBtn = document.createElement("button");
|
|
251
|
+
resetBtn.type = "button";
|
|
252
|
+
resetBtn.className = "diagram-btn btn-reset";
|
|
253
|
+
resetBtn.title = "Reset view";
|
|
254
|
+
resetBtn.textContent = "Reset";
|
|
255
|
+
resetBtn.addEventListener("click", () => handle.fit());
|
|
256
|
+
const fsBtn = document.createElement("button");
|
|
257
|
+
fsBtn.type = "button";
|
|
258
|
+
fsBtn.className = "diagram-btn btn-fullscreen";
|
|
259
|
+
fsBtn.title = "Toggle fullscreen";
|
|
260
|
+
fsBtn.textContent = "Fullscreen";
|
|
261
|
+
fsBtn.addEventListener("click", () => toggleFullscreen(container, fsBtn));
|
|
262
|
+
controls.appendChild(resetBtn);
|
|
263
|
+
controls.appendChild(fsBtn);
|
|
264
|
+
chromeRoot.appendChild(controls);
|
|
265
|
+
if (getComputedStyle(container).position === "static") {
|
|
266
|
+
container.style.position = "relative";
|
|
267
|
+
}
|
|
268
|
+
container.appendChild(chromeRoot);
|
|
269
|
+
renderLegend(legend);
|
|
270
|
+
renderFilterStrip(strip);
|
|
271
|
+
}
|
|
272
|
+
function toggleFullscreen(host, btn) {
|
|
273
|
+
const isFs = host.classList.toggle("diagram-fullscreen");
|
|
274
|
+
btn.textContent = isFs ? "Exit fullscreen" : "Fullscreen";
|
|
275
|
+
requestAnimationFrame(() => handle.fit());
|
|
276
|
+
}
|
|
277
|
+
function renderLegend(legend) {
|
|
278
|
+
legend.innerHTML = '<div class="legend-title">Clusters</div>';
|
|
279
|
+
for (const cluster of clusters.values()) {
|
|
280
|
+
const row = document.createElement("div");
|
|
281
|
+
row.className = "legend-row";
|
|
282
|
+
row.innerHTML = '<span class="legend-ribbon" style="background:' + cluster.color + '"></span><span class="legend-label"></span><span class="legend-count"></span>';
|
|
283
|
+
row.querySelector(".legend-label").textContent = cluster.prefix;
|
|
284
|
+
row.querySelector(".legend-count").textContent = String(cluster.nodeCount);
|
|
285
|
+
row.addEventListener("click", () => {
|
|
286
|
+
if (collapsedPrefixes.has(cluster.prefix)) {
|
|
287
|
+
handle.expand(cluster.prefix);
|
|
288
|
+
} else {
|
|
289
|
+
handle.collapse(cluster.prefix);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
legend.appendChild(row);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function renderFilterStrip(strip) {
|
|
296
|
+
strip.innerHTML = '<span class="filter-strip-label">Show only:</span>';
|
|
297
|
+
const allChip = document.createElement("button");
|
|
298
|
+
allChip.type = "button";
|
|
299
|
+
allChip.className = "filter-chip filter-chip-all filter-chip-active";
|
|
300
|
+
allChip.textContent = "all";
|
|
301
|
+
allChip.addEventListener("click", () => {
|
|
302
|
+
handle.filter(null);
|
|
303
|
+
});
|
|
304
|
+
strip.appendChild(allChip);
|
|
305
|
+
for (const cluster of clusters.values()) {
|
|
306
|
+
const chip = document.createElement("button");
|
|
307
|
+
chip.type = "button";
|
|
308
|
+
chip.className = "filter-chip";
|
|
309
|
+
chip.style.borderColor = cluster.color;
|
|
310
|
+
chip.dataset.prefix = cluster.prefix;
|
|
311
|
+
chip.innerHTML = '<span class="chip-dot" style="background:' + cluster.color + '"></span>';
|
|
312
|
+
chip.append(document.createTextNode(cluster.prefix));
|
|
313
|
+
chip.addEventListener("click", () => {
|
|
314
|
+
if (activeFilter === cluster.prefix) {
|
|
315
|
+
handle.filter(null);
|
|
316
|
+
} else {
|
|
317
|
+
handle.filter(cluster.prefix);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
strip.appendChild(chip);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function refreshChromeActiveStates() {
|
|
324
|
+
if (!chromeRoot) return;
|
|
325
|
+
const strip = chromeRoot.querySelector(".diagram-filter-strip");
|
|
326
|
+
if (!strip) return;
|
|
327
|
+
strip.querySelectorAll(".filter-chip").forEach((chip) => {
|
|
328
|
+
chip.classList.remove("filter-chip-active");
|
|
329
|
+
});
|
|
330
|
+
if (activeFilter === null) {
|
|
331
|
+
strip.querySelector(".filter-chip-all")?.classList.add("filter-chip-active");
|
|
332
|
+
} else {
|
|
333
|
+
strip.querySelector(`.filter-chip[data-prefix="${activeFilter}"]`)?.classList.add("filter-chip-active");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const handle = {
|
|
337
|
+
svg,
|
|
338
|
+
panzoom: panzoomInstance,
|
|
339
|
+
clusters,
|
|
340
|
+
get collapsedPrefixes() {
|
|
341
|
+
return collapsedPrefixes;
|
|
342
|
+
},
|
|
343
|
+
get activeFilter() {
|
|
344
|
+
return activeFilter;
|
|
345
|
+
},
|
|
346
|
+
collapse(prefix) {
|
|
347
|
+
if (disposed) return;
|
|
348
|
+
const cluster = clusters.get(prefix);
|
|
349
|
+
if (!cluster) return;
|
|
350
|
+
if (!collapsedPrefixes.has(prefix)) {
|
|
351
|
+
setClusterCollapsed(cluster, true);
|
|
352
|
+
collapsedPrefixes.add(prefix);
|
|
353
|
+
opts.onClusterCollapse?.(prefix, true);
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
expand(prefix) {
|
|
357
|
+
if (disposed) return;
|
|
358
|
+
const cluster = clusters.get(prefix);
|
|
359
|
+
if (!cluster) return;
|
|
360
|
+
if (collapsedPrefixes.has(prefix)) {
|
|
361
|
+
setClusterCollapsed(cluster, false);
|
|
362
|
+
collapsedPrefixes.delete(prefix);
|
|
363
|
+
opts.onClusterCollapse?.(prefix, false);
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
collapseAll() {
|
|
367
|
+
if (disposed) return;
|
|
368
|
+
for (const cluster of clusters.values()) {
|
|
369
|
+
if (!collapsedPrefixes.has(cluster.prefix)) {
|
|
370
|
+
setClusterCollapsed(cluster, true);
|
|
371
|
+
collapsedPrefixes.add(cluster.prefix);
|
|
372
|
+
opts.onClusterCollapse?.(cluster.prefix, true);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
expandAll() {
|
|
377
|
+
if (disposed) return;
|
|
378
|
+
for (const prefix of Array.from(collapsedPrefixes)) {
|
|
379
|
+
const cluster = clusters.get(prefix);
|
|
380
|
+
if (!cluster) continue;
|
|
381
|
+
setClusterCollapsed(cluster, false);
|
|
382
|
+
collapsedPrefixes.delete(prefix);
|
|
383
|
+
opts.onClusterCollapse?.(prefix, false);
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
filter(prefix) {
|
|
387
|
+
if (disposed) return;
|
|
388
|
+
activeFilter = prefix;
|
|
389
|
+
applyFilter(svg, prefix);
|
|
390
|
+
opts.onClusterFilter?.(prefix);
|
|
391
|
+
refreshChromeActiveStates();
|
|
392
|
+
},
|
|
393
|
+
resetZoom() {
|
|
394
|
+
if (disposed) return;
|
|
395
|
+
try {
|
|
396
|
+
panzoomInstance.moveTo(0, 0);
|
|
397
|
+
panzoomInstance.zoomAbs(0, 0, 1);
|
|
398
|
+
} catch {
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
fit() {
|
|
402
|
+
if (disposed) return;
|
|
403
|
+
try {
|
|
404
|
+
panzoomInstance.zoomAbs(0, 0, 1);
|
|
405
|
+
panzoomInstance.moveTo(0, 0);
|
|
406
|
+
} catch {
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
isInsideCollapsedCluster(graphId) {
|
|
410
|
+
return isInsideCollapsedCluster(svg, clusters, collapsedPrefixes, graphId);
|
|
411
|
+
},
|
|
412
|
+
dispose() {
|
|
413
|
+
if (disposed) return;
|
|
414
|
+
disposed = true;
|
|
415
|
+
try {
|
|
416
|
+
panzoomInstance.dispose();
|
|
417
|
+
} catch {
|
|
418
|
+
}
|
|
419
|
+
if (chromeRoot && chromeRoot.parentNode === container) {
|
|
420
|
+
container.removeChild(chromeRoot);
|
|
421
|
+
}
|
|
422
|
+
chromeRoot = null;
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
for (const prefix of preservedCollapsed) {
|
|
426
|
+
handle.collapse(prefix);
|
|
427
|
+
}
|
|
428
|
+
if (preservedFilter !== null) {
|
|
429
|
+
handle.filter(preservedFilter);
|
|
430
|
+
}
|
|
431
|
+
ensureChrome();
|
|
432
|
+
requestAnimationFrame(() => handle.fit());
|
|
433
|
+
return handle;
|
|
434
|
+
}
|
|
435
|
+
export {
|
|
436
|
+
DEFAULT_PANZOOM_OPTS,
|
|
437
|
+
VIEWER_CSS_VARIABLES,
|
|
438
|
+
colorForPrefix,
|
|
439
|
+
discoverClusters,
|
|
440
|
+
mount
|
|
441
|
+
};
|
|
442
|
+
//# sourceMappingURL=index.js.map
|