libpetri 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -1
- package/dist/chunk-6NH64RCU.js +1016 -0
- package/dist/chunk-6NH64RCU.js.map +1 -0
- package/dist/{chunk-5W6SVYPD.js → chunk-JZIEWVAV.js} +839 -44
- package/dist/chunk-JZIEWVAV.js.map +1 -0
- package/dist/debug/index.d.ts +2 -2
- package/dist/doclet/index.d.ts +12 -3
- package/dist/doclet/index.js +5 -1
- package/dist/doclet/index.js.map +1 -1
- package/dist/doclet/resources/petrinet-diagrams.css +21 -0
- package/dist/doclet/resources/petrinet-diagrams.js +3575 -3573
- package/dist/{render-ZGZEZ5RK.js → elk-place-YVNQFGXI.js} +3 -258
- package/dist/elk-place-YVNQFGXI.js.map +1 -0
- package/dist/{event-store-Df_sAVQ_.d.ts → event-store-BFX_yJ8I.d.ts} +1 -1
- package/dist/export/index.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/pan-zoom-Cp51IkDl.d.ts +33 -0
- package/dist/{petri-net-UQBBkvLl.d.ts → petri-net-WSScMyDL.d.ts} +20 -1
- package/dist/preprocess-FN3F75JR.js +193 -0
- package/dist/preprocess-FN3F75JR.js.map +1 -0
- package/dist/render-QOHGDWNE.js +78 -0
- package/dist/render-QOHGDWNE.js.map +1 -0
- package/dist/render-dom/index.d.ts +28 -29
- package/dist/render-dom/index.js +14 -21
- package/dist/render-dom/index.js.map +1 -1
- package/dist/verification/index.d.ts +269 -4
- package/dist/verification/index.js +7 -1
- package/dist/verification/index.js.map +1 -1
- package/dist/viewer/index.d.ts +24 -32
- package/dist/viewer/index.js +9 -1003
- package/dist/viewer/index.js.map +1 -1
- package/dist/viewer/viewer.css +21 -0
- package/dist/viewer/viewer.iife.js +3575 -3573
- package/package.json +2 -2
- package/dist/chunk-5W6SVYPD.js.map +0 -1
- package/dist/render-ZGZEZ5RK.js.map +0 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// src/viewer/layout/preprocess.ts
|
|
2
|
+
function classifyKind(id) {
|
|
3
|
+
if (id.startsWith("p_")) return "place";
|
|
4
|
+
if (id.startsWith("t_")) return "transition";
|
|
5
|
+
return "junction";
|
|
6
|
+
}
|
|
7
|
+
function parseAttrs(body) {
|
|
8
|
+
const attrs = {};
|
|
9
|
+
for (const m of body.matchAll(/(\w+)=("(?:[^"]|\\.)*"|\S+?)(?=[\s,]|$)/g)) {
|
|
10
|
+
const [, key, raw] = m;
|
|
11
|
+
const v = raw.startsWith('"') && raw.endsWith('"') ? raw.slice(1, -1) : raw;
|
|
12
|
+
attrs[key] = v;
|
|
13
|
+
}
|
|
14
|
+
return attrs;
|
|
15
|
+
}
|
|
16
|
+
function parseLibpetriDot(dot) {
|
|
17
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
18
|
+
for (const m of dot.matchAll(/^\s*([pjt]_[A-Za-z0-9_]+)\s*\[(.*)\];\s*$/gm)) {
|
|
19
|
+
const [, id, body] = m;
|
|
20
|
+
nodes.set(id, { id, kind: classifyKind(id), attrs: parseAttrs(body) });
|
|
21
|
+
}
|
|
22
|
+
const clusters = /* @__PURE__ */ new Map();
|
|
23
|
+
const nodeToCluster = /* @__PURE__ */ new Map();
|
|
24
|
+
for (const cm of dot.matchAll(/subgraph (cluster_\w+)\s*\{([^]*?)^\s*\}/gm)) {
|
|
25
|
+
const [, clusterId, body] = cm;
|
|
26
|
+
const memberIds = [...body.matchAll(/^\s*([pjt]_[A-Za-z0-9_]+)\s*\[/gm)].map((m) => m[1]);
|
|
27
|
+
clusters.set(clusterId, {
|
|
28
|
+
id: clusterId,
|
|
29
|
+
shortName: clusterId.replace(/^cluster_/, ""),
|
|
30
|
+
nodes: memberIds
|
|
31
|
+
});
|
|
32
|
+
for (const n of memberIds) nodeToCluster.set(n, clusterId);
|
|
33
|
+
}
|
|
34
|
+
const stripped = dot.replace(/subgraph cluster_\w+\s*\{[^]*?^\s*\}/gm, "");
|
|
35
|
+
for (const m of stripped.matchAll(/^\s*([pjt]_[A-Za-z0-9_]+)\s*\[/gm)) {
|
|
36
|
+
const id = m[1];
|
|
37
|
+
if (!nodes.has(id)) {
|
|
38
|
+
nodes.set(id, { id, kind: classifyKind(id), attrs: {} });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const orphans = [...nodes.keys()].filter((n) => !nodeToCluster.has(n));
|
|
42
|
+
const edges = [];
|
|
43
|
+
for (const m of dot.matchAll(/([pjt]_[A-Za-z0-9_]+)\s*->\s*([pjt]_[A-Za-z0-9_]+)\s*(\[[^\]]*\])?/g)) {
|
|
44
|
+
const [, src, dst, rawAttrs] = m;
|
|
45
|
+
const attrs = rawAttrs ?? "";
|
|
46
|
+
if (attrs.includes("lhead=") || attrs.includes("ltail=")) continue;
|
|
47
|
+
let arc = "normal";
|
|
48
|
+
if (attrs.includes('label="reset"') || attrs.includes("#fd7e14")) arc = "reset";
|
|
49
|
+
else if (attrs.includes('arrowhead="odot"') || attrs.includes("#dc3545")) arc = "inhibitor";
|
|
50
|
+
else if (attrs.includes('label="read"')) arc = "read";
|
|
51
|
+
const stripped2 = attrs.startsWith("[") && attrs.endsWith("]") ? attrs.slice(1, -1) : "";
|
|
52
|
+
edges.push({ src, dst, arc, rawAttrs: stripped2 });
|
|
53
|
+
}
|
|
54
|
+
return { nodes, clusters, nodeToCluster, orphans, edges };
|
|
55
|
+
}
|
|
56
|
+
function foldOrphans(graph, threshold = 0.7) {
|
|
57
|
+
if (threshold <= 0) return graph;
|
|
58
|
+
const nodeToCluster = new Map(graph.nodeToCluster);
|
|
59
|
+
const clusterMembers = /* @__PURE__ */ new Map();
|
|
60
|
+
for (const [id, c] of graph.clusters) clusterMembers.set(id, [...c.nodes]);
|
|
61
|
+
const tally = /* @__PURE__ */ new Map();
|
|
62
|
+
const bump = (orphan, other) => {
|
|
63
|
+
const oc = nodeToCluster.get(other);
|
|
64
|
+
if (!oc) return;
|
|
65
|
+
let t = tally.get(orphan);
|
|
66
|
+
if (!t) {
|
|
67
|
+
t = /* @__PURE__ */ new Map();
|
|
68
|
+
tally.set(orphan, t);
|
|
69
|
+
}
|
|
70
|
+
t.set(oc, (t.get(oc) ?? 0) + 1);
|
|
71
|
+
};
|
|
72
|
+
for (const e of graph.edges) {
|
|
73
|
+
const sIsOrphan = !nodeToCluster.has(e.src);
|
|
74
|
+
const dIsOrphan = !nodeToCluster.has(e.dst);
|
|
75
|
+
if (sIsOrphan && !dIsOrphan) bump(e.src, e.dst);
|
|
76
|
+
if (dIsOrphan && !sIsOrphan) bump(e.dst, e.src);
|
|
77
|
+
}
|
|
78
|
+
for (const id of graph.orphans) {
|
|
79
|
+
const t = tally.get(id);
|
|
80
|
+
if (!t) continue;
|
|
81
|
+
let total = 0;
|
|
82
|
+
let bestCluster = null;
|
|
83
|
+
let bestCount = 0;
|
|
84
|
+
for (const [c, n] of t) {
|
|
85
|
+
total += n;
|
|
86
|
+
if (n > bestCount) {
|
|
87
|
+
bestCount = n;
|
|
88
|
+
bestCluster = c;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (bestCluster && total > 0 && bestCount / total >= threshold) {
|
|
92
|
+
clusterMembers.get(bestCluster).push(id);
|
|
93
|
+
nodeToCluster.set(id, bestCluster);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const clusters = /* @__PURE__ */ new Map();
|
|
97
|
+
for (const [id, c] of graph.clusters) {
|
|
98
|
+
clusters.set(id, { ...c, nodes: clusterMembers.get(id) });
|
|
99
|
+
}
|
|
100
|
+
const orphans = [...graph.nodes.keys()].filter((n) => !nodeToCluster.has(n));
|
|
101
|
+
return { ...graph, clusters, nodeToCluster, orphans };
|
|
102
|
+
}
|
|
103
|
+
function replicateShared(graph, opts = {}) {
|
|
104
|
+
const max = opts.max ?? Infinity;
|
|
105
|
+
const replicateTransitions = opts.replicateTransitions ?? false;
|
|
106
|
+
const nodes = new Map(graph.nodes);
|
|
107
|
+
const nodeToCluster = new Map(graph.nodeToCluster);
|
|
108
|
+
const clusterMembers = /* @__PURE__ */ new Map();
|
|
109
|
+
for (const [id, c] of graph.clusters) clusterMembers.set(id, [...c.nodes]);
|
|
110
|
+
const edges = graph.edges.map((e) => ({ ...e }));
|
|
111
|
+
const foreignByNode = /* @__PURE__ */ new Map();
|
|
112
|
+
const noteForeign = (nodeId, otherId) => {
|
|
113
|
+
if (!nodeId.startsWith("p_") && !(replicateTransitions && nodeId.startsWith("t_"))) return;
|
|
114
|
+
const home = nodeToCluster.get(nodeId);
|
|
115
|
+
const otherCluster = nodeToCluster.get(otherId);
|
|
116
|
+
if (!otherCluster) return;
|
|
117
|
+
if (otherCluster === home) return;
|
|
118
|
+
let s = foreignByNode.get(nodeId);
|
|
119
|
+
if (!s) {
|
|
120
|
+
s = /* @__PURE__ */ new Set();
|
|
121
|
+
foreignByNode.set(nodeId, s);
|
|
122
|
+
}
|
|
123
|
+
s.add(otherCluster);
|
|
124
|
+
};
|
|
125
|
+
for (const e of edges) {
|
|
126
|
+
noteForeign(e.src, e.dst);
|
|
127
|
+
noteForeign(e.dst, e.src);
|
|
128
|
+
}
|
|
129
|
+
const replicaMap = /* @__PURE__ */ new Map();
|
|
130
|
+
let replicatedPlaces = 0;
|
|
131
|
+
let totalCopies = 0;
|
|
132
|
+
for (const [placeId, foreigns] of foreignByNode) {
|
|
133
|
+
if (foreigns.size === 0) continue;
|
|
134
|
+
if (foreigns.size > max) continue;
|
|
135
|
+
const perCluster = /* @__PURE__ */ new Map();
|
|
136
|
+
const orig = nodes.get(placeId);
|
|
137
|
+
if (!orig) continue;
|
|
138
|
+
for (const clusterId of foreigns) {
|
|
139
|
+
const shortName = clusterId.replace(/^cluster_/, "");
|
|
140
|
+
const repId = `${placeId}__rep__${shortName}`;
|
|
141
|
+
perCluster.set(clusterId, repId);
|
|
142
|
+
clusterMembers.get(clusterId).push(repId);
|
|
143
|
+
nodeToCluster.set(repId, clusterId);
|
|
144
|
+
nodes.set(repId, {
|
|
145
|
+
id: repId,
|
|
146
|
+
kind: orig.kind,
|
|
147
|
+
attrs: { ...orig.attrs },
|
|
148
|
+
replica: true,
|
|
149
|
+
replicaOf: placeId
|
|
150
|
+
});
|
|
151
|
+
totalCopies++;
|
|
152
|
+
}
|
|
153
|
+
replicaMap.set(placeId, perCluster);
|
|
154
|
+
replicatedPlaces++;
|
|
155
|
+
nodes.set(placeId, { ...orig, replica: true, replicaOf: placeId });
|
|
156
|
+
}
|
|
157
|
+
for (const e of edges) {
|
|
158
|
+
const sMap = replicaMap.get(e.src);
|
|
159
|
+
const dMap = replicaMap.get(e.dst);
|
|
160
|
+
if (sMap) {
|
|
161
|
+
const dCl = nodeToCluster.get(e.dst);
|
|
162
|
+
if (dCl && sMap.has(dCl)) e.src = sMap.get(dCl);
|
|
163
|
+
}
|
|
164
|
+
if (dMap) {
|
|
165
|
+
const sCl = nodeToCluster.get(e.src);
|
|
166
|
+
if (sCl && dMap.has(sCl)) e.dst = dMap.get(sCl);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const [origId] of replicaMap) {
|
|
170
|
+
if (nodeToCluster.has(origId)) continue;
|
|
171
|
+
const stillHasEdge = edges.some((e) => e.src === origId || e.dst === origId);
|
|
172
|
+
if (!stillHasEdge) nodes.delete(origId);
|
|
173
|
+
}
|
|
174
|
+
const clusters = /* @__PURE__ */ new Map();
|
|
175
|
+
for (const [id, c] of graph.clusters) {
|
|
176
|
+
clusters.set(id, { ...c, nodes: clusterMembers.get(id) });
|
|
177
|
+
}
|
|
178
|
+
const orphans = [...nodes.keys()].filter((n) => !nodeToCluster.has(n));
|
|
179
|
+
return {
|
|
180
|
+
nodes,
|
|
181
|
+
clusters,
|
|
182
|
+
nodeToCluster,
|
|
183
|
+
orphans,
|
|
184
|
+
edges,
|
|
185
|
+
replicateStats: { replicatedPlaces, totalCopies }
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
export {
|
|
189
|
+
foldOrphans,
|
|
190
|
+
parseLibpetriDot,
|
|
191
|
+
replicateShared
|
|
192
|
+
};
|
|
193
|
+
//# sourceMappingURL=preprocess-FN3F75JR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/viewer/layout/preprocess.ts"],"sourcesContent":["/**\n * Graph pre-processing for the C0-replicate-default layout pipeline.\n *\n * Three pure-ish transforms operate on a {@link GraphModel} parsed from a\n * libpetri-exported DOT string:\n *\n * parseLibpetriDot(dot) → GraphModel\n * foldOrphans(graph, threshold) → GraphModel (REASSIGN_ORPHANS)\n * replicateShared(graph, opts) → GraphModel (REPLICATE_SHARED)\n *\n * The pipeline turns a flat cluster soup into a cluster-internal one by\n * (a) folding orphan nodes whose edges concentrate in one cluster into that\n * cluster, and (b) cloning shared places into every foreign cluster they\n * touch so cross-cluster spaghetti disappears. The resulting GraphModel is\n * what ELK lays out (Stage 2) and Graphviz then renders in pin-mode.\n *\n * Algorithm lineage: ported from\n * `/home/db/otto-repos/nucleus_marvin/.scratch/javadoc-layout-exp/CHECKPOINTS/run16_GOOD_v3_skip-junctions.mjs`\n * lines 14–220. The arc-type detection, replica naming\n * (`${placeId}__rep__${clusterShortName}`), and the `__replica` /\n * `replicaOf` markers are load-bearing — downstream stages key off them.\n *\n * @module viewer/layout/preprocess\n */\n\nexport type ArcType = 'normal' | 'reset' | 'inhibitor' | 'read';\nexport type NodeKind = 'place' | 'transition' | 'junction';\n\nexport interface ParsedNode {\n readonly id: string;\n readonly kind: NodeKind;\n readonly attrs: Record<string, string>;\n /** True iff this node was emitted by replicateShared. */\n readonly replica?: boolean;\n /** For a replica, the id of the original logical place. */\n readonly replicaOf?: string;\n}\n\nexport interface ParsedEdge {\n readonly src: string;\n readonly dst: string;\n readonly arc: ArcType;\n /**\n * Raw attribute body from the libpetri DOT (between the `[…]` brackets,\n * brackets stripped). Carries style/penwidth/label that we want to\n * round-trip verbatim so the rendered output matches `DotExporter`'s\n * native look.\n */\n readonly rawAttrs: string;\n}\n\nexport interface ParsedCluster {\n readonly id: string; // \"cluster_productSearch\"\n readonly shortName: string; // \"productSearch\"\n readonly nodes: readonly string[];\n}\n\nexport interface GraphModel {\n readonly nodes: ReadonlyMap<string, ParsedNode>;\n readonly clusters: ReadonlyMap<string, ParsedCluster>;\n readonly nodeToCluster: ReadonlyMap<string, string>;\n /** Node ids not in any cluster (rendered at the orchestrator root). */\n readonly orphans: readonly string[];\n readonly edges: readonly ParsedEdge[];\n}\n\nfunction classifyKind(id: string): NodeKind {\n if (id.startsWith('p_')) return 'place';\n if (id.startsWith('t_')) return 'transition';\n return 'junction';\n}\n\n/**\n * Recover key=value attributes from inside a DOT `[...]` block.\n *\n * Mirrors v3 line 29. The regex is intentionally permissive about quoted\n * values containing escaped quotes; it does not validate DOT syntax.\n */\nfunction parseAttrs(body: string): Record<string, string> {\n const attrs: Record<string, string> = {};\n for (const m of body.matchAll(/(\\w+)=(\"(?:[^\"]|\\\\.)*\"|\\S+?)(?=[\\s,]|$)/g)) {\n const [, key, raw] = m as unknown as [string, string, string];\n const v = raw.startsWith('\"') && raw.endsWith('\"') ? raw.slice(1, -1) : raw;\n attrs[key] = v;\n }\n return attrs;\n}\n\n/**\n * Parse a libpetri-exported DOT string into a {@link GraphModel}.\n *\n * Tolerates the current exporter shape (see\n * `java/src/main/java/org/libpetri/export/StyleConstants.java`):\n * - inhibitor arcs use `arrowhead=\"odot\"` + color `#dc3545`\n * - reset arcs use `label=\"reset\"` + color `#fd7e14`\n * - read arcs use `label=\"read\"` + color `#6c757d`\n *\n * Mirrors v3 lines 24–103.\n */\nexport function parseLibpetriDot(dot: string): GraphModel {\n const nodes = new Map<string, ParsedNode>();\n // Greedy `.*` (no /s flag) consumes the full attrs body up to the line's\n // trailing `];`. The v3 reference used `[^\\]]*` which silently dropped\n // transitions whose label contained `[…]` text (e.g., `\"[0, ∞]ms\"`); we\n // can't tolerate that here because the typed model wants every cluster\n // member to have a Node entry.\n for (const m of dot.matchAll(/^\\s*([pjt]_[A-Za-z0-9_]+)\\s*\\[(.*)\\];\\s*$/gm)) {\n const [, id, body] = m as unknown as [string, string, string];\n nodes.set(id, { id, kind: classifyKind(id), attrs: parseAttrs(body) });\n }\n\n const clusters = new Map<string, ParsedCluster>();\n const nodeToCluster = new Map<string, string>();\n for (const cm of dot.matchAll(/subgraph (cluster_\\w+)\\s*\\{([^]*?)^\\s*\\}/gm)) {\n const [, clusterId, body] = cm as unknown as [string, string, string];\n const memberIds = [...body.matchAll(/^\\s*([pjt]_[A-Za-z0-9_]+)\\s*\\[/gm)]\n .map(m => (m as unknown as [string, string])[1]);\n clusters.set(clusterId, {\n id: clusterId,\n shortName: clusterId.replace(/^cluster_/, ''),\n nodes: memberIds,\n });\n for (const n of memberIds) nodeToCluster.set(n, clusterId);\n }\n\n // Catch nodes declared at root (orphans) — strip cluster bodies, then scan\n const stripped = dot.replace(/subgraph cluster_\\w+\\s*\\{[^]*?^\\s*\\}/gm, '');\n for (const m of stripped.matchAll(/^\\s*([pjt]_[A-Za-z0-9_]+)\\s*\\[/gm)) {\n const id = (m as unknown as [string, string])[1];\n if (!nodes.has(id)) {\n // Root-declared node without full attrs (rare); seed with empty attrs.\n nodes.set(id, { id, kind: classifyKind(id), attrs: {} });\n }\n }\n\n const orphans = [...nodes.keys()].filter(n => !nodeToCluster.has(n));\n\n const edges: ParsedEdge[] = [];\n for (const m of dot.matchAll(/([pjt]_[A-Za-z0-9_]+)\\s*->\\s*([pjt]_[A-Za-z0-9_]+)\\s*(\\[[^\\]]*\\])?/g)) {\n const [, src, dst, rawAttrs] = m as unknown as [string, string, string, string | undefined];\n const attrs = rawAttrs ?? '';\n // Cluster-anchor edges (lhead/ltail) are exporter-internal routing hints,\n // not real arcs in the Petri net — skip per v3 line 97.\n if (attrs.includes('lhead=') || attrs.includes('ltail=')) continue;\n let arc: ArcType = 'normal';\n if (attrs.includes('label=\"reset\"') || attrs.includes('#fd7e14')) arc = 'reset';\n else if (attrs.includes('arrowhead=\"odot\"') || attrs.includes('#dc3545')) arc = 'inhibitor';\n else if (attrs.includes('label=\"read\"')) arc = 'read';\n // Strip the `[…]` brackets so writeBack can splice the body into a new\n // attr list. Empty if libpetri emitted a bare `src -> dst;`.\n const stripped = attrs.startsWith('[') && attrs.endsWith(']')\n ? attrs.slice(1, -1)\n : '';\n edges.push({ src, dst, arc, rawAttrs: stripped });\n }\n\n return { nodes, clusters, nodeToCluster, orphans, edges };\n}\n\n/**\n * Adopt orphan nodes into the cluster their edges most prefer.\n *\n * For each orphan, tally how many of its edges touch each cluster. If the\n * dominant cluster gets at least `threshold` of the orphan's cluster-touching\n * edges, the orphan is reassigned to that cluster.\n *\n * Mirrors v3 lines 105–136. Default threshold 0.7 matches REASSIGN_ORPHANS.\n */\nexport function foldOrphans(graph: GraphModel, threshold = 0.7): GraphModel {\n if (threshold <= 0) return graph;\n\n const nodeToCluster = new Map(graph.nodeToCluster);\n const clusterMembers = new Map<string, string[]>();\n for (const [id, c] of graph.clusters) clusterMembers.set(id, [...c.nodes]);\n\n const tally = new Map<string, Map<string, number>>();\n const bump = (orphan: string, other: string): void => {\n const oc = nodeToCluster.get(other);\n if (!oc) return;\n let t = tally.get(orphan);\n if (!t) { t = new Map(); tally.set(orphan, t); }\n t.set(oc, (t.get(oc) ?? 0) + 1);\n };\n\n for (const e of graph.edges) {\n const sIsOrphan = !nodeToCluster.has(e.src);\n const dIsOrphan = !nodeToCluster.has(e.dst);\n if (sIsOrphan && !dIsOrphan) bump(e.src, e.dst);\n if (dIsOrphan && !sIsOrphan) bump(e.dst, e.src);\n }\n\n for (const id of graph.orphans) {\n const t = tally.get(id);\n if (!t) continue;\n let total = 0;\n let bestCluster: string | null = null;\n let bestCount = 0;\n for (const [c, n] of t) {\n total += n;\n if (n > bestCount) { bestCount = n; bestCluster = c; }\n }\n if (bestCluster && total > 0 && bestCount / total >= threshold) {\n clusterMembers.get(bestCluster)!.push(id);\n nodeToCluster.set(id, bestCluster);\n }\n }\n\n const clusters = new Map<string, ParsedCluster>();\n for (const [id, c] of graph.clusters) {\n clusters.set(id, { ...c, nodes: clusterMembers.get(id)! });\n }\n const orphans = [...graph.nodes.keys()].filter(n => !nodeToCluster.has(n));\n return { ...graph, clusters, nodeToCluster, orphans };\n}\n\nexport interface ReplicateOptions {\n /**\n * Skip places that touch more than this many foreign clusters. Per the\n * plan's \"no cap\" decision, default is `Infinity`. Set to a finite value\n * to suppress replication of hub places that would otherwise produce many\n * copies.\n */\n readonly max?: number;\n /** If true, replicate transitions as well as places. Default false. */\n readonly replicateTransitions?: boolean;\n}\n\nexport interface ReplicateStats {\n readonly replicatedPlaces: number;\n readonly totalCopies: number;\n}\n\n/**\n * For each place P, replicate it into every foreign cluster touched by its\n * edges, and redirect those cross-cluster edges to the local copy.\n *\n * Replica id convention is load-bearing: `${placeId}__rep__${shortClusterName}`.\n * Downstream stages (mount-time tagging, overlay highlight, click-all-copies)\n * key off this naming and the `replica: true` / `replicaOf` markers.\n *\n * If P has a home cluster (folded earlier), the original stays as the primary\n * copy and is also marked as a replica so the ⇄ glyph renders consistently.\n * If P is an orphan with all its edges redirected, the original is dropped.\n *\n * Mirrors v3 lines 138–221.\n */\nexport function replicateShared(\n graph: GraphModel,\n opts: ReplicateOptions = {},\n): GraphModel & { readonly replicateStats: ReplicateStats } {\n const max = opts.max ?? Infinity;\n const replicateTransitions = opts.replicateTransitions ?? false;\n\n const nodes = new Map(graph.nodes);\n const nodeToCluster = new Map(graph.nodeToCluster);\n const clusterMembers = new Map<string, string[]>();\n for (const [id, c] of graph.clusters) clusterMembers.set(id, [...c.nodes]);\n const edges = graph.edges.map(e => ({ ...e }));\n\n // 1. Compute foreign-cluster set per replicable node.\n const foreignByNode = new Map<string, Set<string>>();\n const noteForeign = (nodeId: string, otherId: string): void => {\n if (!nodeId.startsWith('p_') && !(replicateTransitions && nodeId.startsWith('t_'))) return;\n const home = nodeToCluster.get(nodeId);\n const otherCluster = nodeToCluster.get(otherId);\n if (!otherCluster) return;\n if (otherCluster === home) return;\n let s = foreignByNode.get(nodeId);\n if (!s) { s = new Set(); foreignByNode.set(nodeId, s); }\n s.add(otherCluster);\n };\n for (const e of edges) {\n noteForeign(e.src, e.dst);\n noteForeign(e.dst, e.src);\n }\n\n // 2. Emit replicas for nodes within the cap.\n const replicaMap = new Map<string, Map<string, string>>(); // orig → cluster → replicaId\n let replicatedPlaces = 0;\n let totalCopies = 0;\n for (const [placeId, foreigns] of foreignByNode) {\n if (foreigns.size === 0) continue;\n if (foreigns.size > max) continue;\n const perCluster = new Map<string, string>();\n const orig = nodes.get(placeId);\n if (!orig) continue;\n for (const clusterId of foreigns) {\n const shortName = clusterId.replace(/^cluster_/, '');\n const repId = `${placeId}__rep__${shortName}`;\n perCluster.set(clusterId, repId);\n clusterMembers.get(clusterId)!.push(repId);\n nodeToCluster.set(repId, clusterId);\n nodes.set(repId, {\n id: repId,\n kind: orig.kind,\n attrs: { ...orig.attrs },\n replica: true,\n replicaOf: placeId,\n });\n totalCopies++;\n }\n replicaMap.set(placeId, perCluster);\n replicatedPlaces++;\n // Mark the original as a shared place so the ⇄ glyph renders uniformly.\n nodes.set(placeId, { ...orig, replica: true, replicaOf: placeId });\n }\n\n // 3. Redirect cross-cluster edges to the local replica on each end.\n for (const e of edges) {\n const sMap = replicaMap.get(e.src);\n const dMap = replicaMap.get(e.dst);\n if (sMap) {\n const dCl = nodeToCluster.get(e.dst);\n if (dCl && sMap.has(dCl)) e.src = sMap.get(dCl)!;\n }\n if (dMap) {\n const sCl = nodeToCluster.get(e.src);\n if (sCl && dMap.has(sCl)) e.dst = dMap.get(sCl)!;\n }\n }\n\n // 4. Drop orphan originals whose edges all got redirected away.\n for (const [origId] of replicaMap) {\n if (nodeToCluster.has(origId)) continue; // folded — keep as primary\n const stillHasEdge = edges.some(e => e.src === origId || e.dst === origId);\n if (!stillHasEdge) nodes.delete(origId);\n }\n\n const clusters = new Map<string, ParsedCluster>();\n for (const [id, c] of graph.clusters) {\n clusters.set(id, { ...c, nodes: clusterMembers.get(id)! });\n }\n const orphans = [...nodes.keys()].filter(n => !nodeToCluster.has(n));\n\n return {\n nodes,\n clusters,\n nodeToCluster,\n orphans,\n edges,\n replicateStats: { replicatedPlaces, totalCopies },\n };\n}\n"],"mappings":"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","names":["stripped"]}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// src/viewer/render.ts
|
|
2
|
+
import { instance as vizInstance } from "@viz-js/viz";
|
|
3
|
+
var vizPromise = null;
|
|
4
|
+
function getViz() {
|
|
5
|
+
if (!vizPromise) vizPromise = vizInstance();
|
|
6
|
+
return vizPromise;
|
|
7
|
+
}
|
|
8
|
+
async function renderDotToSvg(dotSource) {
|
|
9
|
+
const viz = await getViz();
|
|
10
|
+
return viz.renderSVGElement(dotSource, { engine: "dot" });
|
|
11
|
+
}
|
|
12
|
+
function fnv1a(input) {
|
|
13
|
+
let h = 2166136261;
|
|
14
|
+
for (let i = 0; i < input.length; i++) {
|
|
15
|
+
h ^= input.charCodeAt(i);
|
|
16
|
+
h = Math.imul(h, 16777619);
|
|
17
|
+
}
|
|
18
|
+
return (h >>> 0).toString(16);
|
|
19
|
+
}
|
|
20
|
+
var PINNED_DOT_CACHE_CAP = 16;
|
|
21
|
+
var pinnedDotCache = /* @__PURE__ */ new Map();
|
|
22
|
+
function getCachedPinnedDot(key) {
|
|
23
|
+
const hit = pinnedDotCache.get(key);
|
|
24
|
+
if (hit !== void 0) {
|
|
25
|
+
pinnedDotCache.delete(key);
|
|
26
|
+
pinnedDotCache.set(key, hit);
|
|
27
|
+
}
|
|
28
|
+
return hit;
|
|
29
|
+
}
|
|
30
|
+
function setCachedPinnedDot(key, value) {
|
|
31
|
+
pinnedDotCache.set(key, value);
|
|
32
|
+
while (pinnedDotCache.size > PINNED_DOT_CACHE_CAP) {
|
|
33
|
+
const oldest = pinnedDotCache.keys().next().value;
|
|
34
|
+
if (oldest === void 0) break;
|
|
35
|
+
pinnedDotCache.delete(oldest);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function _clearElkLayoutCache() {
|
|
39
|
+
pinnedDotCache.clear();
|
|
40
|
+
}
|
|
41
|
+
async function renderDotToSvgWithElkLayout(dotSource, cfg) {
|
|
42
|
+
const key = fnv1a(dotSource + "\0" + JSON.stringify(cfg ?? {}));
|
|
43
|
+
let pinnedDot = getCachedPinnedDot(key);
|
|
44
|
+
if (pinnedDot === void 0) {
|
|
45
|
+
const { foldOrphans, parseLibpetriDot, replicateShared } = await import("./preprocess-FN3F75JR.js");
|
|
46
|
+
const { elkLayout, writeBack } = await import("./elk-place-YVNQFGXI.js").catch(
|
|
47
|
+
(cause) => {
|
|
48
|
+
throw new Error(
|
|
49
|
+
"the viewer's default 'elk' layout requires the optional peer dependency 'elkjs'. Install it, or pass { layout: 'graphviz' } to mount() for stock Graphviz layout with spline edges.",
|
|
50
|
+
{ cause }
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
const graph = replicateShared(
|
|
55
|
+
foldOrphans(parseLibpetriDot(dotSource), 0.7),
|
|
56
|
+
{ max: Infinity }
|
|
57
|
+
);
|
|
58
|
+
const layout = await elkLayout(graph, cfg);
|
|
59
|
+
pinnedDot = writeBack(graph, layout);
|
|
60
|
+
setCachedPinnedDot(key, pinnedDot);
|
|
61
|
+
}
|
|
62
|
+
const viz = await getViz();
|
|
63
|
+
return viz.renderSVGElement(pinnedDot, {
|
|
64
|
+
// nop2 draws the pinned node positions AND our ELK-computed orthogonal
|
|
65
|
+
// edge routes verbatim. See writeBack/edgePosSpline: we route edges
|
|
66
|
+
// ourselves rather than let Graphviz's ortho router run, which crashes
|
|
67
|
+
// the wasm on large nets.
|
|
68
|
+
engine: "nop2",
|
|
69
|
+
yInvert: true
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
_clearElkLayoutCache,
|
|
74
|
+
getViz,
|
|
75
|
+
renderDotToSvg,
|
|
76
|
+
renderDotToSvgWithElkLayout
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=render-QOHGDWNE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/viewer/render.ts"],"sourcesContent":["/**\n * DOT → SVG rendering for the canonical libpetri viewer.\n *\n * Two render paths:\n *\n * renderDotToSvg(dot) — plain Graphviz `dot` engine.\n * renderDotToSvgWithElkLayout(dot) — C0 pipeline: parse → fold →\n * replicate → ELK → writeBack →\n * Graphviz `nop2` (pinned nodes AND\n * edge routes). Cached by DOT hash.\n *\n * The ELK path is the default for {@link mount}; the plain-Graphviz path\n * remains as a fallback for callers that want stock layout (or for\n * environments where elkjs isn't installed).\n *\n * The ELK stage is pulled in by dynamic import rather than at module load, so\n * the plain-Graphviz path genuinely works without `elkjs` present. Under a\n * static import the whole module failed to load and `layout: 'graphviz'` was\n * an escape hatch that could never be reached.\n *\n * @module viewer/render\n */\n\nimport { instance as vizInstance } from '@viz-js/viz';\nimport type { ElkLayoutConfig } from './layout/elk-place.js';\n\ntype Viz = Awaited<ReturnType<typeof vizInstance>>;\n\nlet vizPromise: Promise<Viz> | null = null;\n\n/** Memoize the viz.js instance so the wasm loads only once per page. */\nexport function getViz(): Promise<Viz> {\n if (!vizPromise) vizPromise = vizInstance();\n return vizPromise;\n}\n\n/**\n * Render a DOT source string to an SVGSVGElement using the plain `dot`\n * engine. Deterministic across runs; libpetri exporters emit byte-stable\n * DOT per spec EXP-014 so the same DOT yields the same SVG.\n */\nexport async function renderDotToSvg(dotSource: string): Promise<SVGSVGElement> {\n const viz = await getViz();\n return viz.renderSVGElement(dotSource, { engine: 'dot' });\n}\n\n// ---- C0 / ELK pin-mode cache ---------------------------------------------\n\n/** FNV-1a 32-bit hash. Fast enough that a SHA isn't worth the import. */\nfunction fnv1a(input: string): string {\n let h = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0).toString(16);\n}\n\nconst PINNED_DOT_CACHE_CAP = 16;\nconst pinnedDotCache = new Map<string, string>();\n\nfunction getCachedPinnedDot(key: string): string | undefined {\n const hit = pinnedDotCache.get(key);\n if (hit !== undefined) {\n // LRU touch: re-insert moves to most-recent position in Map ordering.\n pinnedDotCache.delete(key);\n pinnedDotCache.set(key, hit);\n }\n return hit;\n}\n\nfunction setCachedPinnedDot(key: string, value: string): void {\n pinnedDotCache.set(key, value);\n while (pinnedDotCache.size > PINNED_DOT_CACHE_CAP) {\n const oldest = pinnedDotCache.keys().next().value;\n if (oldest === undefined) break;\n pinnedDotCache.delete(oldest);\n }\n}\n\n/** Test helper — clears the pinned-DOT LRU cache between mounts. */\nexport function _clearElkLayoutCache(): void {\n pinnedDotCache.clear();\n}\n\n/**\n * Run the C0 layout pipeline against a libpetri DOT source and return the\n * rendered SVG. The expensive steps (preprocess + ELK layout + DOT rewrite)\n * are cached keyed on the input DOT's hash plus the layout config, so\n * re-mounts on the same net structure and config skip everything except the\n * Graphviz pin-mode draw.\n *\n * Per-tick marking updates do NOT call this — they toggle classes on the\n * already-mounted SVG. ELK only runs when the net structure changes.\n *\n * `cfg` tunes the ELK stage — see {@link ElkLayoutConfig}.\n */\nexport async function renderDotToSvgWithElkLayout(\n dotSource: string,\n cfg?: ElkLayoutConfig,\n): Promise<SVGSVGElement> {\n const key = fnv1a(dotSource + '\\0' + JSON.stringify(cfg ?? {}));\n let pinnedDot = getCachedPinnedDot(key);\n if (pinnedDot === undefined) {\n const { foldOrphans, parseLibpetriDot, replicateShared } =\n await import('./layout/preprocess.js');\n // `elkjs` is an optional peer dependency, in line with the viewer's other\n // browser-only peers. Name it explicitly here: the raw resolution failure\n // mentions a deep path inside elkjs and reads as a libpetri bug.\n const { elkLayout, writeBack } = await import('./layout/elk-place.js').catch(\n (cause: unknown) => {\n throw new Error(\n \"the viewer's default 'elk' layout requires the optional peer \"\n + \"dependency 'elkjs'. Install it, or pass { layout: 'graphviz' } \"\n + 'to mount() for stock Graphviz layout with spline edges.',\n { cause },\n );\n },\n );\n const graph = replicateShared(\n foldOrphans(parseLibpetriDot(dotSource), 0.7),\n { max: Infinity },\n );\n const layout = await elkLayout(graph, cfg);\n pinnedDot = writeBack(graph, layout);\n setCachedPinnedDot(key, pinnedDot);\n }\n const viz = await getViz();\n return viz.renderSVGElement(pinnedDot, {\n // nop2 draws the pinned node positions AND our ELK-computed orthogonal\n // edge routes verbatim. See writeBack/edgePosSpline: we route edges\n // ourselves rather than let Graphviz's ortho router run, which crashes\n // the wasm on large nets.\n engine: 'nop2',\n yInvert: true,\n });\n}\n"],"mappings":";AAuBA,SAAS,YAAY,mBAAmB;AAKxC,IAAI,aAAkC;AAG/B,SAAS,SAAuB;AACrC,MAAI,CAAC,WAAY,cAAa,YAAY;AAC1C,SAAO;AACT;AAOA,eAAsB,eAAe,WAA2C;AAC9E,QAAM,MAAM,MAAM,OAAO;AACzB,SAAO,IAAI,iBAAiB,WAAW,EAAE,QAAQ,MAAM,CAAC;AAC1D;AAKA,SAAS,MAAM,OAAuB;AACpC,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,SAAK,MAAM,WAAW,CAAC;AACvB,QAAI,KAAK,KAAK,GAAG,QAAU;AAAA,EAC7B;AACA,UAAQ,MAAM,GAAG,SAAS,EAAE;AAC9B;AAEA,IAAM,uBAAuB;AAC7B,IAAM,iBAAiB,oBAAI,IAAoB;AAE/C,SAAS,mBAAmB,KAAiC;AAC3D,QAAM,MAAM,eAAe,IAAI,GAAG;AAClC,MAAI,QAAQ,QAAW;AAErB,mBAAe,OAAO,GAAG;AACzB,mBAAe,IAAI,KAAK,GAAG;AAAA,EAC7B;AACA,SAAO;AACT;AAEA,SAAS,mBAAmB,KAAa,OAAqB;AAC5D,iBAAe,IAAI,KAAK,KAAK;AAC7B,SAAO,eAAe,OAAO,sBAAsB;AACjD,UAAM,SAAS,eAAe,KAAK,EAAE,KAAK,EAAE;AAC5C,QAAI,WAAW,OAAW;AAC1B,mBAAe,OAAO,MAAM;AAAA,EAC9B;AACF;AAGO,SAAS,uBAA6B;AAC3C,iBAAe,MAAM;AACvB;AAcA,eAAsB,4BACpB,WACA,KACwB;AACxB,QAAM,MAAM,MAAM,YAAY,OAAO,KAAK,UAAU,OAAO,CAAC,CAAC,CAAC;AAC9D,MAAI,YAAY,mBAAmB,GAAG;AACtC,MAAI,cAAc,QAAW;AAC3B,UAAM,EAAE,aAAa,kBAAkB,gBAAgB,IACrD,MAAM,OAAO,0BAAwB;AAIvC,UAAM,EAAE,WAAW,UAAU,IAAI,MAAM,OAAO,yBAAuB,EAAE;AAAA,MACrE,CAAC,UAAmB;AAClB,cAAM,IAAI;AAAA,UACR;AAAA,UAGA,EAAE,MAAM;AAAA,QACV;AAAA,MACF;AAAA,IACF;AACA,UAAM,QAAQ;AAAA,MACZ,YAAY,iBAAiB,SAAS,GAAG,GAAG;AAAA,MAC5C,EAAE,KAAK,SAAS;AAAA,IAClB;AACA,UAAM,SAAS,MAAM,UAAU,OAAO,GAAG;AACzC,gBAAY,UAAU,OAAO,MAAM;AACnC,uBAAmB,KAAK,SAAS;AAAA,EACnC;AACA,QAAM,MAAM,MAAM,OAAO;AACzB,SAAO,IAAI,iBAAiB,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,IAKrC,QAAQ;AAAA,IACR,SAAS;AAAA,EACX,CAAC;AACH;","names":[]}
|
|
@@ -1,20 +1,33 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { P as PanzoomInstance, a as PanzoomOptions } from '../pan-zoom-Cp51IkDl.js';
|
|
2
|
+
export { D as DEFAULT_PANZOOM_OPTS } from '../pan-zoom-Cp51IkDl.js';
|
|
3
|
+
import 'panzoom';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
|
-
*
|
|
6
|
+
* Compatibility wrapper over the canonical viewer.
|
|
5
7
|
*
|
|
6
|
-
*
|
|
7
|
-
* debug-ui
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* This module predates `libpetri/viewer` and used to be the shared viz.js +
|
|
9
|
+
* panzoom pipeline behind debug-ui and the dev-preview app. Both moved to the
|
|
10
|
+
* canonical viewer at `274cb41`, and the doc generators (Javadoc taglet,
|
|
11
|
+
* `libpetri-docgen`, TypeDoc plugin) never shelled out to a `dot` binary
|
|
12
|
+
* again either: they embed the viewer IIFE and mount client-side.
|
|
11
13
|
*
|
|
12
|
-
*
|
|
14
|
+
* What was left behind was a public export that still pinned Graphviz
|
|
15
|
+
* `engine: 'dot'`, so anything reaching for it got stock spline layout with
|
|
16
|
+
* diagonal edges while every first-party surface rendered ELK-placed nodes
|
|
17
|
+
* with orthogonal routes. `renderDotToContainer` now delegates to
|
|
18
|
+
* {@link mount}, so all render paths agree.
|
|
19
|
+
*
|
|
20
|
+
* Prefer importing `libpetri/viewer` directly in new code: `mount()` returns a
|
|
21
|
+
* {@link ViewerHandle} with cluster collapse, subnet toggling, filtering and
|
|
22
|
+
* highlight control, of which this wrapper surfaces only the SVG and the
|
|
23
|
+
* panzoom instance.
|
|
24
|
+
*
|
|
25
|
+
* Browser-only. Requires `@viz-js/viz`, `panzoom`, and `elkjs` (the viewer's
|
|
26
|
+
* default layout) as peer dependencies.
|
|
13
27
|
*
|
|
14
28
|
* @module render-dom
|
|
15
29
|
*/
|
|
16
30
|
|
|
17
|
-
type PanzoomInstance = ReturnType<typeof panzoom>;
|
|
18
31
|
interface RenderDotOptions {
|
|
19
32
|
/**
|
|
20
33
|
* Existing panzoom instance to dispose before initializing a new one.
|
|
@@ -27,33 +40,19 @@ interface RenderDotOptions {
|
|
|
27
40
|
* so partial overrides keep the unspecified defaults (e.g. passing only
|
|
28
41
|
* `smoothScroll: true` retains `maxZoom: 1000` and `minZoom: 0.02`).
|
|
29
42
|
*/
|
|
30
|
-
readonly panzoom?:
|
|
43
|
+
readonly panzoom?: PanzoomOptions;
|
|
31
44
|
}
|
|
32
45
|
interface RenderDotResult {
|
|
33
46
|
readonly svg: SVGSVGElement;
|
|
34
47
|
readonly panzoom: PanzoomInstance;
|
|
35
48
|
}
|
|
36
|
-
/**
|
|
37
|
-
* Default panzoom configuration shared by debug-ui and the dev preview.
|
|
38
|
-
*
|
|
39
|
-
* - `maxZoom: 1000` — effectively unlimited; lets users dive into ~150-node
|
|
40
|
-
* diagrams without hitting an artificial ceiling.
|
|
41
|
-
* - `minZoom: 0.02` — allows fitting very large nets in a small viewport.
|
|
42
|
-
* - `smoothScroll: false` — sharper interactive feel.
|
|
43
|
-
* - `zoomDoubleClickSpeed: 1` — single-click double-zoom toggle.
|
|
44
|
-
*/
|
|
45
|
-
declare const DEFAULT_PANZOOM_OPTS: {
|
|
46
|
-
readonly smoothScroll: false;
|
|
47
|
-
readonly zoomDoubleClickSpeed: 1;
|
|
48
|
-
readonly maxZoom: 1000;
|
|
49
|
-
readonly minZoom: 0.02;
|
|
50
|
-
};
|
|
51
49
|
/**
|
|
52
50
|
* Render a DOT string into a container element and wrap it with panzoom.
|
|
53
51
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
52
|
+
* Layout is the viewer default: ELK node placement plus ELK-computed
|
|
53
|
+
* orthogonal edge routes, drawn by Graphviz `nop2`. Deterministic across
|
|
54
|
+
* reloads, since libpetri mappers emit byte-stable DOT (spec EXP-014) and
|
|
55
|
+
* neither stage introduces randomness.
|
|
57
56
|
*
|
|
58
57
|
* The container's existing children are removed before the new SVG is
|
|
59
58
|
* appended.
|
|
@@ -64,4 +63,4 @@ declare const DEFAULT_PANZOOM_OPTS: {
|
|
|
64
63
|
*/
|
|
65
64
|
declare function renderDotToContainer(dotSource: string, container: HTMLElement, opts?: RenderDotOptions): Promise<RenderDotResult>;
|
|
66
65
|
|
|
67
|
-
export {
|
|
66
|
+
export { PanzoomInstance, PanzoomOptions, type RenderDotOptions, type RenderDotResult, renderDotToContainer };
|
package/dist/render-dom/index.js
CHANGED
|
@@ -1,28 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_PANZOOM_OPTS,
|
|
3
|
+
mount
|
|
4
|
+
} from "../chunk-6NH64RCU.js";
|
|
5
|
+
|
|
1
6
|
// src/render-dom/index.ts
|
|
2
|
-
import { instance as vizInstance } from "@viz-js/viz";
|
|
3
|
-
import panzoom from "panzoom";
|
|
4
|
-
var vizPromise = null;
|
|
5
|
-
function getViz() {
|
|
6
|
-
if (!vizPromise) vizPromise = vizInstance();
|
|
7
|
-
return vizPromise;
|
|
8
|
-
}
|
|
9
|
-
var DEFAULT_PANZOOM_OPTS = {
|
|
10
|
-
smoothScroll: false,
|
|
11
|
-
zoomDoubleClickSpeed: 1,
|
|
12
|
-
maxZoom: 1e3,
|
|
13
|
-
minZoom: 0.02
|
|
14
|
-
};
|
|
15
7
|
async function renderDotToContainer(dotSource, container, opts = {}) {
|
|
16
|
-
const viz = await getViz();
|
|
17
|
-
const svg = viz.renderSVGElement(dotSource, { engine: "dot" });
|
|
18
|
-
container.innerHTML = "";
|
|
19
|
-
container.appendChild(svg);
|
|
20
8
|
if (opts.previousPanzoom) {
|
|
21
|
-
|
|
9
|
+
try {
|
|
10
|
+
opts.previousPanzoom.dispose();
|
|
11
|
+
} catch {
|
|
12
|
+
}
|
|
22
13
|
}
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
const handle = await mount(dotSource, container, {
|
|
15
|
+
chrome: false,
|
|
16
|
+
panzoom: opts.panzoom
|
|
17
|
+
});
|
|
18
|
+
return { svg: handle.svg, panzoom: handle.panzoom };
|
|
26
19
|
}
|
|
27
20
|
export {
|
|
28
21
|
DEFAULT_PANZOOM_OPTS,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/render-dom/index.ts"],"sourcesContent":["/**\n *
|
|
1
|
+
{"version":3,"sources":["../../src/render-dom/index.ts"],"sourcesContent":["/**\n * Compatibility wrapper over the canonical viewer.\n *\n * This module predates `libpetri/viewer` and used to be the shared viz.js +\n * panzoom pipeline behind debug-ui and the dev-preview app. Both moved to the\n * canonical viewer at `274cb41`, and the doc generators (Javadoc taglet,\n * `libpetri-docgen`, TypeDoc plugin) never shelled out to a `dot` binary\n * again either: they embed the viewer IIFE and mount client-side.\n *\n * What was left behind was a public export that still pinned Graphviz\n * `engine: 'dot'`, so anything reaching for it got stock spline layout with\n * diagonal edges while every first-party surface rendered ELK-placed nodes\n * with orthogonal routes. `renderDotToContainer` now delegates to\n * {@link mount}, so all render paths agree.\n *\n * Prefer importing `libpetri/viewer` directly in new code: `mount()` returns a\n * {@link ViewerHandle} with cluster collapse, subnet toggling, filtering and\n * highlight control, of which this wrapper surfaces only the SVG and the\n * panzoom instance.\n *\n * Browser-only. Requires `@viz-js/viz`, `panzoom`, and `elkjs` (the viewer's\n * default layout) as peer dependencies.\n *\n * @module render-dom\n */\n\nimport {\n DEFAULT_PANZOOM_OPTS,\n mount,\n type PanzoomInstance,\n type PanzoomOptions,\n type ViewerHandle,\n} from '../viewer/index.js';\n\nexport type { PanzoomInstance, PanzoomOptions };\n\n/**\n * Default panzoom configuration.\n *\n * Re-exported from the viewer rather than redefined here; a second copy of\n * these numbers is exactly how the two render paths drifted apart in the first\n * place.\n */\nexport { DEFAULT_PANZOOM_OPTS };\n\nexport interface RenderDotOptions {\n /**\n * Existing panzoom instance to dispose before initializing a new one.\n * Pass the value previously returned from {@link renderDotToContainer}\n * so callers don't need to track it themselves.\n */\n readonly previousPanzoom?: PanzoomInstance | null;\n /**\n * Per-call panzoom overrides. Merged on top of {@link DEFAULT_PANZOOM_OPTS},\n * so partial overrides keep the unspecified defaults (e.g. passing only\n * `smoothScroll: true` retains `maxZoom: 1000` and `minZoom: 0.02`).\n */\n readonly panzoom?: PanzoomOptions;\n}\n\nexport interface RenderDotResult {\n readonly svg: SVGSVGElement;\n readonly panzoom: PanzoomInstance;\n}\n\n/**\n * Render a DOT string into a container element and wrap it with panzoom.\n *\n * Layout is the viewer default: ELK node placement plus ELK-computed\n * orthogonal edge routes, drawn by Graphviz `nop2`. Deterministic across\n * reloads, since libpetri mappers emit byte-stable DOT (spec EXP-014) and\n * neither stage introduces randomness.\n *\n * The container's existing children are removed before the new SVG is\n * appended.\n *\n * @returns the rendered SVG element and the panzoom instance, so callers\n * can build secondary indexes (node caches, highlighting state) on the SVG\n * and dispose the panzoom on the next render.\n */\nexport async function renderDotToContainer(\n dotSource: string,\n container: HTMLElement,\n opts: RenderDotOptions = {},\n): Promise<RenderDotResult> {\n // Disposed up front rather than after the swap (the pre-viewer ordering):\n // the old instance is bound to nodes `mount` is about to discard, and a\n // panzoom still listening on a detached subtree has nothing useful to do.\n if (opts.previousPanzoom) {\n try {\n opts.previousPanzoom.dispose();\n } catch {\n // Already detached, or panzoom raised on a missing root. Either way the\n // only goal was to stop the old instance.\n }\n }\n\n const handle: ViewerHandle = await mount(dotSource, container, {\n chrome: false,\n panzoom: opts.panzoom,\n });\n\n return { svg: handle.svg, panzoom: handle.panzoom };\n}\n"],"mappings":";;;;;;AAgFA,eAAsB,qBACpB,WACA,WACA,OAAyB,CAAC,GACA;AAI1B,MAAI,KAAK,iBAAiB;AACxB,QAAI;AACF,WAAK,gBAAgB,QAAQ;AAAA,IAC/B,QAAQ;AAAA,IAGR;AAAA,EACF;AAEA,QAAM,SAAuB,MAAM,MAAM,WAAW,WAAW;AAAA,IAC7D,QAAQ;AAAA,IACR,SAAS,KAAK;AAAA,EAChB,CAAC;AAED,SAAO,EAAE,KAAK,OAAO,KAAK,SAAS,OAAO,QAAQ;AACpD;","names":[]}
|