libpetri 3.0.0 → 4.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.
Files changed (38) hide show
  1. package/README.md +54 -2
  2. package/dist/chunk-KO6TSB47.js +1016 -0
  3. package/dist/chunk-KO6TSB47.js.map +1 -0
  4. package/dist/{chunk-5W6SVYPD.js → chunk-WYCGGQAW.js} +1744 -737
  5. package/dist/chunk-WYCGGQAW.js.map +1 -0
  6. package/dist/debug/index.d.ts +2 -2
  7. package/dist/doclet/index.d.ts +12 -3
  8. package/dist/doclet/index.js +5 -1
  9. package/dist/doclet/index.js.map +1 -1
  10. package/dist/doclet/resources/petrinet-diagrams.css +21 -0
  11. package/dist/doclet/resources/petrinet-diagrams.js +6366 -6360
  12. package/dist/{render-ZGZEZ5RK.js → elk-place-YVNQFGXI.js} +3 -258
  13. package/dist/elk-place-YVNQFGXI.js.map +1 -0
  14. package/dist/{event-store-Df_sAVQ_.d.ts → event-store-2FOAeUyh.d.ts} +1 -1
  15. package/dist/export/index.d.ts +1 -1
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.js +6 -3
  18. package/dist/index.js.map +1 -1
  19. package/dist/pan-zoom-Cp51IkDl.d.ts +33 -0
  20. package/dist/{petri-net-UQBBkvLl.d.ts → petri-net-hduM6tJf.d.ts} +20 -1
  21. package/dist/preprocess-FN3F75JR.js +193 -0
  22. package/dist/preprocess-FN3F75JR.js.map +1 -0
  23. package/dist/render-QOHGDWNE.js +78 -0
  24. package/dist/render-QOHGDWNE.js.map +1 -0
  25. package/dist/render-dom/index.d.ts +28 -29
  26. package/dist/render-dom/index.js +14 -21
  27. package/dist/render-dom/index.js.map +1 -1
  28. package/dist/verification/index.d.ts +440 -71
  29. package/dist/verification/index.js +43 -5
  30. package/dist/verification/index.js.map +1 -1
  31. package/dist/viewer/index.d.ts +24 -32
  32. package/dist/viewer/index.js +9 -1003
  33. package/dist/viewer/index.js.map +1 -1
  34. package/dist/viewer/viewer.css +21 -0
  35. package/dist/viewer/viewer.iife.js +6366 -6360
  36. package/package.json +5 -8
  37. package/dist/chunk-5W6SVYPD.js.map +0 -1
  38. package/dist/render-ZGZEZ5RK.js.map +0 -1
@@ -0,0 +1,33 @@
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
+ export { DEFAULT_PANZOOM_OPTS as D, type PanzoomInstance as P, type PanzoomOptions as a };
@@ -982,6 +982,25 @@ interface SmtVerificationResult {
982
982
  readonly discoveredInvariants: readonly string[];
983
983
  readonly counterexampleTrace: readonly MarkingState[];
984
984
  readonly counterexampleTransitions: readonly string[];
985
+ /**
986
+ * Outcome of the abstract counterexample replay, as a TRI-STATE. `null` means
987
+ * "the replay did not apply"; the two booleans both mean it ran.
988
+ *
989
+ * - `true` — an abstract firing chain from M₀ to a property-violating state
990
+ * was re-executed TS-side; `counterexampleTrace` is that chain in FIRING
991
+ * (replay) order and the verdict is `violated`.
992
+ * - `false` — the replay ran without confirming the trace. Either it could not
993
+ * settle the question (nothing decoded from the Z3 derivation, M₀ absent
994
+ * from the decoded set, or a node/segment budget hit), in which case the
995
+ * `violated` verdict rests on Spacer's SAT answer alone; or the search
996
+ * completed and found NO chain, in which case the verdict was downgraded to
997
+ * `unknown`. The report distinguishes the two ("UNCONFIRMED" vs "FAILED").
998
+ * - `null` — replay did not apply: non-violated verdict, replay disabled via
999
+ * `counterexampleReplay(false)`, the coloured ν-encoding / Route B (whose
1000
+ * state shapes are outside the flat replayer's scope), or a structural
1001
+ * proof.
1002
+ */
1003
+ readonly counterexampleConfirmed: boolean | null;
985
1004
  readonly elapsedMs: number;
986
1005
  readonly statistics: SmtStatistics;
987
1006
  }
@@ -1777,4 +1796,4 @@ declare class PetriNetBuilder {
1777
1796
  private buildWithFusion;
1778
1797
  }
1779
1798
 
1780
- export { type VerificationHarness as $, type Arc as A, type Port as B, type Channel as C, type PortDirection as D, type EnvironmentPlace as E, FusionSet as F, SubnetDefBuilder as G, type SubnetInstance as H, type In as I, type Timing as J, type KeyFn as K, type LogFn as L, MAX_DURATION_MS as M, type NameId as N, type Out as O, PetriNet as P, type TimingDeadline as Q, type TimingDelayed as R, SubnetDef as S, type Token as T, type TimingExact as U, type TimingImmediate as V, type TimingWindow as W, TokenInput as X, TokenOutput as Y, type TransitionAction as Z, TransitionBuilder as _, type Place as a, type Unknown as a$, type VerificationResult as a0, all as a1, allPlaces as a2, and as a3, andPlaces as a4, arcPlace as a5, atLeast as a6, consumptionCount as a7, deadline as a8, delayed as a9, requiredCount as aA, resetArc as aB, timeout as aC, timeoutPlace as aD, tokenAt as aE, tokenOf as aF, transform as aG, transformAsync as aH, transformFrom as aI, unitToken as aJ, window as aK, withTimeout as aL, xor as aM, xorPlaces as aN, MarkingState as aO, type PInvariant as aP, MarkingStateBuilder as aQ, type SmtProperty as aR, type SmtVerificationResult as aS, type BranchPlaceBound as aT, type DeadlockFree as aU, type JoinedOrDeadLettered as aV, type MutualExclusion as aW, type PlaceBound as aX, type Proven as aY, type SmtStatistics as aZ, type TokenSupplier as a_, earliest as aa, enumerateBranches as ab, environmentPlace as ac, exact as ad, exactly as ae, fork as af, forwardInput as ag, hasDeadline as ah, immediate as ai, inhibitorArc as aj, inputArc as ak, isPassthrough as al, isUnit as am, keyForPlace as an, latest as ao, matchCorrelates as ap, matchKey as aq, matchSpec as ar, nameId as as, one as at, outPlace as au, outputArc as av, passthrough as aw, place as ax, produce as ay, readArc as az, Transition as b, type Unreachable as b0, type Verdict as b1, type Violated as b2, branchPlaceBound as b3, deadlockFree as b4, isProven as b5, isViolated as b6, joinedOrDeadLettered as b7, mutualExclusion as b8, pInvariant as b9, pInvariantToString as ba, placeBound as bb, propertyDescription as bc, unreachable as bd, TransitionContext as c, type ArcInhibitor as d, type ArcInput as e, type ArcOutput as f, type ArcRead as g, type ArcReset as h, ComposeBindings as i, FusionSetBuilder as j, type InAll as k, type InAtLeast as l, type InExactly as m, type InOne as n, Instance as o, Interface as p, InterfaceBuilder as q, type MatchKey as r, type MatchSpec as s, type OutAnd as t, type OutForwardInput as u, type OutPlace as v, type OutTimeout as w, type OutXor as x, type OutputEntry as y, PetriNetBuilder as z };
1799
+ export { type VerificationHarness as $, type Arc as A, type Port as B, type Channel as C, type PortDirection as D, type EnvironmentPlace as E, FusionSet as F, SubnetDefBuilder as G, type SubnetInstance as H, type In as I, type Timing as J, type KeyFn as K, type LogFn as L, MAX_DURATION_MS as M, type NameId as N, type Out as O, PetriNet as P, type TimingDeadline as Q, type TimingDelayed as R, SubnetDef as S, type Token as T, type TimingExact as U, type TimingImmediate as V, type TimingWindow as W, TokenInput as X, TokenOutput as Y, type TransitionAction as Z, TransitionBuilder as _, type Place as a, type Unknown as a$, type VerificationResult as a0, all as a1, allPlaces as a2, and as a3, andPlaces as a4, arcPlace as a5, atLeast as a6, consumptionCount as a7, deadline as a8, delayed as a9, requiredCount as aA, resetArc as aB, timeout as aC, timeoutPlace as aD, tokenAt as aE, tokenOf as aF, transform as aG, transformAsync as aH, transformFrom as aI, unitToken as aJ, window as aK, withTimeout as aL, xor as aM, xorPlaces as aN, type PInvariant as aO, MarkingState as aP, type SmtProperty as aQ, MarkingStateBuilder as aR, type SmtVerificationResult as aS, type BranchPlaceBound as aT, type DeadlockFree as aU, type JoinedOrDeadLettered as aV, type MutualExclusion as aW, type PlaceBound as aX, type Proven as aY, type SmtStatistics as aZ, type TokenSupplier as a_, earliest as aa, enumerateBranches as ab, environmentPlace as ac, exact as ad, exactly as ae, fork as af, forwardInput as ag, hasDeadline as ah, immediate as ai, inhibitorArc as aj, inputArc as ak, isPassthrough as al, isUnit as am, keyForPlace as an, latest as ao, matchCorrelates as ap, matchKey as aq, matchSpec as ar, nameId as as, one as at, outPlace as au, outputArc as av, passthrough as aw, place as ax, produce as ay, readArc as az, Transition as b, type Unreachable as b0, type Verdict as b1, type Violated as b2, branchPlaceBound as b3, deadlockFree as b4, isProven as b5, isViolated as b6, joinedOrDeadLettered as b7, mutualExclusion as b8, pInvariant as b9, pInvariantToString as ba, placeBound as bb, propertyDescription as bc, unreachable as bd, TransitionContext as c, type ArcInhibitor as d, type ArcInput as e, type ArcOutput as f, type ArcRead as g, type ArcReset as h, ComposeBindings as i, FusionSetBuilder as j, type InAll as k, type InAtLeast as l, type InExactly as m, type InOne as n, Instance as o, Interface as p, InterfaceBuilder as q, type MatchKey as r, type MatchSpec as s, type OutAnd as t, type OutForwardInput as u, type OutPlace as v, type OutTimeout as w, type OutXor as x, type OutputEntry as y, PetriNetBuilder as z };
@@ -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":";AAkEA,SAAS,aAAa,IAAsB;AAC1C,MAAI,GAAG,WAAW,IAAI,EAAG,QAAO;AAChC,MAAI,GAAG,WAAW,IAAI,EAAG,QAAO;AAChC,SAAO;AACT;AAQA,SAAS,WAAW,MAAsC;AACxD,QAAM,QAAgC,CAAC;AACvC,aAAW,KAAK,KAAK,SAAS,0CAA0C,GAAG;AACzE,UAAM,CAAC,EAAE,KAAK,GAAG,IAAI;AACrB,UAAM,IAAI,IAAI,WAAW,GAAG,KAAK,IAAI,SAAS,GAAG,IAAI,IAAI,MAAM,GAAG,EAAE,IAAI;AACxE,UAAM,GAAG,IAAI;AAAA,EACf;AACA,SAAO;AACT;AAaO,SAAS,iBAAiB,KAAyB;AACxD,QAAM,QAAQ,oBAAI,IAAwB;AAM1C,aAAW,KAAK,IAAI,SAAS,6CAA6C,GAAG;AAC3E,UAAM,CAAC,EAAE,IAAI,IAAI,IAAI;AACrB,UAAM,IAAI,IAAI,EAAE,IAAI,MAAM,aAAa,EAAE,GAAG,OAAO,WAAW,IAAI,EAAE,CAAC;AAAA,EACvE;AAEA,QAAM,WAAW,oBAAI,IAA2B;AAChD,QAAM,gBAAgB,oBAAI,IAAoB;AAC9C,aAAW,MAAM,IAAI,SAAS,4CAA4C,GAAG;AAC3E,UAAM,CAAC,EAAE,WAAW,IAAI,IAAI;AAC5B,UAAM,YAAY,CAAC,GAAG,KAAK,SAAS,kCAAkC,CAAC,EACpE,IAAI,OAAM,EAAkC,CAAC,CAAC;AACjD,aAAS,IAAI,WAAW;AAAA,MACtB,IAAI;AAAA,MACJ,WAAW,UAAU,QAAQ,aAAa,EAAE;AAAA,MAC5C,OAAO;AAAA,IACT,CAAC;AACD,eAAW,KAAK,UAAW,eAAc,IAAI,GAAG,SAAS;AAAA,EAC3D;AAGA,QAAM,WAAW,IAAI,QAAQ,0CAA0C,EAAE;AACzE,aAAW,KAAK,SAAS,SAAS,kCAAkC,GAAG;AACrE,UAAM,KAAM,EAAkC,CAAC;AAC/C,QAAI,CAAC,MAAM,IAAI,EAAE,GAAG;AAElB,YAAM,IAAI,IAAI,EAAE,IAAI,MAAM,aAAa,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC;AAAA,IACzD;AAAA,EACF;AAEA,QAAM,UAAU,CAAC,GAAG,MAAM,KAAK,CAAC,EAAE,OAAO,OAAK,CAAC,cAAc,IAAI,CAAC,CAAC;AAEnE,QAAM,QAAsB,CAAC;AAC7B,aAAW,KAAK,IAAI,SAAS,qEAAqE,GAAG;AACnG,UAAM,CAAC,EAAE,KAAK,KAAK,QAAQ,IAAI;AAC/B,UAAM,QAAQ,YAAY;AAG1B,QAAI,MAAM,SAAS,QAAQ,KAAK,MAAM,SAAS,QAAQ,EAAG;AAC1D,QAAI,MAAe;AACnB,QAAI,MAAM,SAAS,eAAe,KAAK,MAAM,SAAS,SAAS,EAAG,OAAM;AAAA,aAC/D,MAAM,SAAS,kBAAkB,KAAK,MAAM,SAAS,SAAS,EAAG,OAAM;AAAA,aACvE,MAAM,SAAS,cAAc,EAAG,OAAM;AAG/C,UAAMA,YAAW,MAAM,WAAW,GAAG,KAAK,MAAM,SAAS,GAAG,IACxD,MAAM,MAAM,GAAG,EAAE,IACjB;AACJ,UAAM,KAAK,EAAE,KAAK,KAAK,KAAK,UAAUA,UAAS,CAAC;AAAA,EAClD;AAEA,SAAO,EAAE,OAAO,UAAU,eAAe,SAAS,MAAM;AAC1D;AAWO,SAAS,YAAY,OAAmB,YAAY,KAAiB;AAC1E,MAAI,aAAa,EAAG,QAAO;AAE3B,QAAM,gBAAgB,IAAI,IAAI,MAAM,aAAa;AACjD,QAAM,iBAAiB,oBAAI,IAAsB;AACjD,aAAW,CAAC,IAAI,CAAC,KAAK,MAAM,SAAU,gBAAe,IAAI,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC;AAEzE,QAAM,QAAQ,oBAAI,IAAiC;AACnD,QAAM,OAAO,CAAC,QAAgB,UAAwB;AACpD,UAAM,KAAK,cAAc,IAAI,KAAK;AAClC,QAAI,CAAC,GAAI;AACT,QAAI,IAAI,MAAM,IAAI,MAAM;AACxB,QAAI,CAAC,GAAG;AAAE,UAAI,oBAAI,IAAI;AAAG,YAAM,IAAI,QAAQ,CAAC;AAAA,IAAG;AAC/C,MAAE,IAAI,KAAK,EAAE,IAAI,EAAE,KAAK,KAAK,CAAC;AAAA,EAChC;AAEA,aAAW,KAAK,MAAM,OAAO;AAC3B,UAAM,YAAY,CAAC,cAAc,IAAI,EAAE,GAAG;AAC1C,UAAM,YAAY,CAAC,cAAc,IAAI,EAAE,GAAG;AAC1C,QAAI,aAAa,CAAC,UAAW,MAAK,EAAE,KAAK,EAAE,GAAG;AAC9C,QAAI,aAAa,CAAC,UAAW,MAAK,EAAE,KAAK,EAAE,GAAG;AAAA,EAChD;AAEA,aAAW,MAAM,MAAM,SAAS;AAC9B,UAAM,IAAI,MAAM,IAAI,EAAE;AACtB,QAAI,CAAC,EAAG;AACR,QAAI,QAAQ;AACZ,QAAI,cAA6B;AACjC,QAAI,YAAY;AAChB,eAAW,CAAC,GAAG,CAAC,KAAK,GAAG;AACtB,eAAS;AACT,UAAI,IAAI,WAAW;AAAE,oBAAY;AAAG,sBAAc;AAAA,MAAG;AAAA,IACvD;AACA,QAAI,eAAe,QAAQ,KAAK,YAAY,SAAS,WAAW;AAC9D,qBAAe,IAAI,WAAW,EAAG,KAAK,EAAE;AACxC,oBAAc,IAAI,IAAI,WAAW;AAAA,IACnC;AAAA,EACF;AAEA,QAAM,WAAW,oBAAI,IAA2B;AAChD,aAAW,CAAC,IAAI,CAAC,KAAK,MAAM,UAAU;AACpC,aAAS,IAAI,IAAI,EAAE,GAAG,GAAG,OAAO,eAAe,IAAI,EAAE,EAAG,CAAC;AAAA,EAC3D;AACA,QAAM,UAAU,CAAC,GAAG,MAAM,MAAM,KAAK,CAAC,EAAE,OAAO,OAAK,CAAC,cAAc,IAAI,CAAC,CAAC;AACzE,SAAO,EAAE,GAAG,OAAO,UAAU,eAAe,QAAQ;AACtD;AAiCO,SAAS,gBACd,OACA,OAAyB,CAAC,GACgC;AAC1D,QAAM,MAAM,KAAK,OAAO;AACxB,QAAM,uBAAuB,KAAK,wBAAwB;AAE1D,QAAM,QAAQ,IAAI,IAAI,MAAM,KAAK;AACjC,QAAM,gBAAgB,IAAI,IAAI,MAAM,aAAa;AACjD,QAAM,iBAAiB,oBAAI,IAAsB;AACjD,aAAW,CAAC,IAAI,CAAC,KAAK,MAAM,SAAU,gBAAe,IAAI,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC;AACzE,QAAM,QAAQ,MAAM,MAAM,IAAI,QAAM,EAAE,GAAG,EAAE,EAAE;AAG7C,QAAM,gBAAgB,oBAAI,IAAyB;AACnD,QAAM,cAAc,CAAC,QAAgB,YAA0B;AAC7D,QAAI,CAAC,OAAO,WAAW,IAAI,KAAK,EAAE,wBAAwB,OAAO,WAAW,IAAI,GAAI;AACpF,UAAM,OAAO,cAAc,IAAI,MAAM;AACrC,UAAM,eAAe,cAAc,IAAI,OAAO;AAC9C,QAAI,CAAC,aAAc;AACnB,QAAI,iBAAiB,KAAM;AAC3B,QAAI,IAAI,cAAc,IAAI,MAAM;AAChC,QAAI,CAAC,GAAG;AAAE,UAAI,oBAAI,IAAI;AAAG,oBAAc,IAAI,QAAQ,CAAC;AAAA,IAAG;AACvD,MAAE,IAAI,YAAY;AAAA,EACpB;AACA,aAAW,KAAK,OAAO;AACrB,gBAAY,EAAE,KAAK,EAAE,GAAG;AACxB,gBAAY,EAAE,KAAK,EAAE,GAAG;AAAA,EAC1B;AAGA,QAAM,aAAa,oBAAI,IAAiC;AACxD,MAAI,mBAAmB;AACvB,MAAI,cAAc;AAClB,aAAW,CAAC,SAAS,QAAQ,KAAK,eAAe;AAC/C,QAAI,SAAS,SAAS,EAAG;AACzB,QAAI,SAAS,OAAO,IAAK;AACzB,UAAM,aAAa,oBAAI,IAAoB;AAC3C,UAAM,OAAO,MAAM,IAAI,OAAO;AAC9B,QAAI,CAAC,KAAM;AACX,eAAW,aAAa,UAAU;AAChC,YAAM,YAAY,UAAU,QAAQ,aAAa,EAAE;AACnD,YAAM,QAAQ,GAAG,OAAO,UAAU,SAAS;AAC3C,iBAAW,IAAI,WAAW,KAAK;AAC/B,qBAAe,IAAI,SAAS,EAAG,KAAK,KAAK;AACzC,oBAAc,IAAI,OAAO,SAAS;AAClC,YAAM,IAAI,OAAO;AAAA,QACf,IAAI;AAAA,QACJ,MAAM,KAAK;AAAA,QACX,OAAO,EAAE,GAAG,KAAK,MAAM;AAAA,QACvB,SAAS;AAAA,QACT,WAAW;AAAA,MACb,CAAC;AACD;AAAA,IACF;AACA,eAAW,IAAI,SAAS,UAAU;AAClC;AAEA,UAAM,IAAI,SAAS,EAAE,GAAG,MAAM,SAAS,MAAM,WAAW,QAAQ,CAAC;AAAA,EACnE;AAGA,aAAW,KAAK,OAAO;AACrB,UAAM,OAAO,WAAW,IAAI,EAAE,GAAG;AACjC,UAAM,OAAO,WAAW,IAAI,EAAE,GAAG;AACjC,QAAI,MAAM;AACR,YAAM,MAAM,cAAc,IAAI,EAAE,GAAG;AACnC,UAAI,OAAO,KAAK,IAAI,GAAG,EAAG,GAAE,MAAM,KAAK,IAAI,GAAG;AAAA,IAChD;AACA,QAAI,MAAM;AACR,YAAM,MAAM,cAAc,IAAI,EAAE,GAAG;AACnC,UAAI,OAAO,KAAK,IAAI,GAAG,EAAG,GAAE,MAAM,KAAK,IAAI,GAAG;AAAA,IAChD;AAAA,EACF;AAGA,aAAW,CAAC,MAAM,KAAK,YAAY;AACjC,QAAI,cAAc,IAAI,MAAM,EAAG;AAC/B,UAAM,eAAe,MAAM,KAAK,OAAK,EAAE,QAAQ,UAAU,EAAE,QAAQ,MAAM;AACzE,QAAI,CAAC,aAAc,OAAM,OAAO,MAAM;AAAA,EACxC;AAEA,QAAM,WAAW,oBAAI,IAA2B;AAChD,aAAW,CAAC,IAAI,CAAC,KAAK,MAAM,UAAU;AACpC,aAAS,IAAI,IAAI,EAAE,GAAG,GAAG,OAAO,eAAe,IAAI,EAAE,EAAG,CAAC;AAAA,EAC3D;AACA,QAAM,UAAU,CAAC,GAAG,MAAM,KAAK,CAAC,EAAE,OAAO,OAAK,CAAC,cAAc,IAAI,CAAC,CAAC;AAEnE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,gBAAgB,EAAE,kBAAkB,YAAY;AAAA,EAClD;AACF;","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 panzoom from 'panzoom';
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
- * DOM rendering helper for libpetri DOT diagrams.
6
+ * Compatibility wrapper over the canonical viewer.
5
7
  *
6
- * Single source of truth for the viz.js + panzoom pipeline used by both
7
- * debug-ui (live sessions) and the internal dev-preview Vite app
8
- * (sample.dot iteration loop). Doc generators (Javadoc taglet, Rustdoc
9
- * build.rs, TypeDoc plugin) take a different path they shell out to the
10
- * Graphviz `dot` binary at build time and do not consume this module.
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
- * Browser-only: requires `@viz-js/viz` and `panzoom` as peer dependencies.
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?: Parameters<typeof panzoom>[1];
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
- * Engine is pinned to `'dot'` so layout is deterministic across reloads;
55
- * libpetri mappers emit byte-stable DOT (per spec EXP-014) and the `dot`
56
- * engine produces identical SVG for identical input.
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 { DEFAULT_PANZOOM_OPTS, type PanzoomInstance, type RenderDotOptions, type RenderDotResult, renderDotToContainer };
66
+ export { PanzoomInstance, PanzoomOptions, type RenderDotOptions, type RenderDotResult, renderDotToContainer };
@@ -1,28 +1,21 @@
1
+ import {
2
+ DEFAULT_PANZOOM_OPTS,
3
+ mount
4
+ } from "../chunk-KO6TSB47.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
- opts.previousPanzoom.dispose();
9
+ try {
10
+ opts.previousPanzoom.dispose();
11
+ } catch {
12
+ }
22
13
  }
23
- const panzoomOpts = { ...DEFAULT_PANZOOM_OPTS, ...opts.panzoom };
24
- const instance = panzoom(svg, panzoomOpts);
25
- return { svg, panzoom: instance };
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 * DOM rendering helper for libpetri DOT diagrams.\n *\n * Single source of truth for the viz.js + panzoom pipeline used by both\n * debug-ui (live sessions) and the internal dev-preview Vite app\n * (sample.dot iteration loop). Doc generators (Javadoc taglet, Rustdoc\n * build.rs, TypeDoc plugin) take a different path they shell out to the\n * Graphviz `dot` binary at build time and do not consume this module.\n *\n * Browser-only: requires `@viz-js/viz` and `panzoom` as peer dependencies.\n *\n * @module render-dom\n */\n\nimport { instance as vizInstance } from '@viz-js/viz';\nimport panzoom from 'panzoom';\n\ntype Viz = Awaited<ReturnType<typeof vizInstance>>;\nexport type PanzoomInstance = ReturnType<typeof panzoom>;\n\nlet vizPromise: Promise<Viz> | null = null;\n\n/** Memoizes the viz.js instance so loading happens only once per page. */\nfunction getViz(): Promise<Viz> {\n if (!vizPromise) vizPromise = vizInstance();\n return vizPromise;\n}\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?: Parameters<typeof panzoom>[1];\n}\n\nexport interface RenderDotResult {\n readonly svg: SVGSVGElement;\n readonly panzoom: PanzoomInstance;\n}\n\n/**\n * Default panzoom configuration shared by debug-ui and the dev preview.\n *\n * - `maxZoom: 1000` effectively unlimited; lets users dive into ~150-node\n * diagrams without hitting an artificial ceiling.\n * - `minZoom: 0.02` — allows fitting very large nets in a small viewport.\n * - `smoothScroll: false` — sharper interactive feel.\n * - `zoomDoubleClickSpeed: 1` — single-click double-zoom toggle.\n */\nexport const DEFAULT_PANZOOM_OPTS = {\n smoothScroll: false,\n zoomDoubleClickSpeed: 1,\n maxZoom: 1000,\n minZoom: 0.02,\n} as const satisfies Parameters<typeof panzoom>[1];\n\n/**\n * Render a DOT string into a container element and wrap it with panzoom.\n *\n * Engine is pinned to `'dot'` so layout is deterministic across reloads;\n * libpetri mappers emit byte-stable DOT (per spec EXP-014) and the `dot`\n * engine produces identical SVG for identical input.\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 const viz = await getViz();\n const svg = viz.renderSVGElement(dotSource, { engine: 'dot' });\n\n container.innerHTML = '';\n container.appendChild(svg);\n\n if (opts.previousPanzoom) {\n opts.previousPanzoom.dispose();\n }\n\n const panzoomOpts = { ...DEFAULT_PANZOOM_OPTS, ...opts.panzoom };\n const instance = panzoom(svg, panzoomOpts);\n\n return { svg, panzoom: instance };\n}\n"],"mappings":";AAcA,SAAS,YAAY,mBAAmB;AACxC,OAAO,aAAa;AAKpB,IAAI,aAAkC;AAGtC,SAAS,SAAuB;AAC9B,MAAI,CAAC,WAAY,cAAa,YAAY;AAC1C,SAAO;AACT;AA+BO,IAAM,uBAAuB;AAAA,EAClC,cAAc;AAAA,EACd,sBAAsB;AAAA,EACtB,SAAS;AAAA,EACT,SAAS;AACX;AAgBA,eAAsB,qBACpB,WACA,WACA,OAAyB,CAAC,GACA;AAC1B,QAAM,MAAM,MAAM,OAAO;AACzB,QAAM,MAAM,IAAI,iBAAiB,WAAW,EAAE,QAAQ,MAAM,CAAC;AAE7D,YAAU,YAAY;AACtB,YAAU,YAAY,GAAG;AAEzB,MAAI,KAAK,iBAAiB;AACxB,SAAK,gBAAgB,QAAQ;AAAA,EAC/B;AAEA,QAAM,cAAc,EAAE,GAAG,sBAAsB,GAAG,KAAK,QAAQ;AAC/D,QAAM,WAAW,QAAQ,KAAK,WAAW;AAEzC,SAAO,EAAE,KAAK,SAAS,SAAS;AAClC;","names":[]}
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":[]}