libpetri 2.13.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 +89 -130
- package/dist/chunk-6NH64RCU.js +1016 -0
- package/dist/chunk-6NH64RCU.js.map +1 -0
- package/dist/{chunk-7VJ5CYUU.js → chunk-JZIEWVAV.js} +905 -62
- package/dist/chunk-JZIEWVAV.js.map +1 -0
- package/dist/{chunk-JVI5HFRX.js → chunk-VCDOKWVU.js} +2 -2
- package/dist/{chunk-E3ZWB645.js → chunk-YVIPJ6KM.js} +1 -1
- package/dist/chunk-YVIPJ6KM.js.map +1 -0
- package/dist/debug/index.d.ts +2 -2
- package/dist/debug/index.js +2 -2
- package/dist/doclet/index.d.ts +12 -3
- package/dist/doclet/index.js +8 -4
- 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/dot-exporter-3STXYK74.js +9 -0
- package/dist/{render-ZGZEZ5RK.js → elk-place-YVNQFGXI.js} +3 -258
- package/dist/elk-place-YVNQFGXI.js.map +1 -0
- package/dist/{event-store-DKTenPbC.d.ts → event-store-BFX_yJ8I.d.ts} +1 -1
- package/dist/export/index.d.ts +1 -1
- package/dist/export/index.js +2 -2
- package/dist/index.d.ts +91 -38
- package/dist/index.js +368 -294
- package/dist/index.js.map +1 -1
- package/dist/pan-zoom-Cp51IkDl.d.ts +33 -0
- package/dist/{petri-net-C3LSY-vm.d.ts → petri-net-WSScMyDL.d.ts} +43 -29
- 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 +273 -7
- 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-7VJ5CYUU.js.map +0 -1
- package/dist/chunk-E3ZWB645.js.map +0 -1
- package/dist/dot-exporter-SHBYMMJ3.js +0 -9
- package/dist/render-ZGZEZ5RK.js.map +0 -1
- /package/dist/{chunk-JVI5HFRX.js.map → chunk-VCDOKWVU.js.map} +0 -0
- /package/dist/{dot-exporter-SHBYMMJ3.js.map → dot-exporter-3STXYK74.js.map} +0 -0
|
@@ -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 };
|
|
@@ -72,7 +72,6 @@ type Arc = ArcInput | ArcOutput | ArcInhibitor | ArcRead | ArcReset;
|
|
|
72
72
|
interface ArcInput<T = any> {
|
|
73
73
|
readonly type: 'input';
|
|
74
74
|
readonly place: Place<T>;
|
|
75
|
-
readonly guard?: (value: T) => boolean;
|
|
76
75
|
}
|
|
77
76
|
interface ArcOutput<T = any> {
|
|
78
77
|
readonly type: 'output';
|
|
@@ -91,7 +90,7 @@ interface ArcReset<T = any> {
|
|
|
91
90
|
readonly place: Place<T>;
|
|
92
91
|
}
|
|
93
92
|
/** Input arc: consumes token from place when transition fires. */
|
|
94
|
-
declare function inputArc<T>(place: Place<T
|
|
93
|
+
declare function inputArc<T>(place: Place<T>): ArcInput<T>;
|
|
95
94
|
/** Output arc: produces token to place when transition fires. */
|
|
96
95
|
declare function outputArc<T>(place: Place<T>): ArcOutput<T>;
|
|
97
96
|
/** Inhibitor arc: blocks transition if place has tokens. */
|
|
@@ -102,15 +101,14 @@ declare function readArc<T>(place: Place<T>): ArcRead<T>;
|
|
|
102
101
|
declare function resetArc<T>(place: Place<T>): ArcReset<T>;
|
|
103
102
|
/** Returns the place this arc connects to. */
|
|
104
103
|
declare function arcPlace(arc: Arc): Place<any>;
|
|
105
|
-
/** Checks if an input arc has a guard predicate. */
|
|
106
|
-
declare function hasGuard(arc: ArcInput): boolean;
|
|
107
|
-
/** Checks if a token value matches an input arc's guard. */
|
|
108
|
-
declare function matchesGuard<T>(arc: ArcInput<T>, value: T): boolean;
|
|
109
104
|
|
|
110
105
|
/**
|
|
111
|
-
* Input specification with cardinality
|
|
112
|
-
*
|
|
113
|
-
*
|
|
106
|
+
* Input specification with cardinality. Purely structural (IO-006): cardinality
|
|
107
|
+
* determines how many tokens to consume; there is no per-token predicate.
|
|
108
|
+
*
|
|
109
|
+
* Conditional token selection is modeled with multiple conflicting transitions
|
|
110
|
+
* and XOR-on-input semantics rather than a predicate coupled to the enablement
|
|
111
|
+
* check.
|
|
114
112
|
*
|
|
115
113
|
* Inputs are always AND-joined (all must be satisfied to enable transition).
|
|
116
114
|
* XOR on inputs is modeled via multiple transitions (conflict).
|
|
@@ -119,33 +117,29 @@ type In = InOne | InExactly | InAll | InAtLeast;
|
|
|
119
117
|
interface InOne<T = any> {
|
|
120
118
|
readonly type: 'one';
|
|
121
119
|
readonly place: Place<T>;
|
|
122
|
-
readonly guard?: (value: T) => boolean;
|
|
123
120
|
}
|
|
124
121
|
interface InExactly<T = any> {
|
|
125
122
|
readonly type: 'exactly';
|
|
126
123
|
readonly place: Place<T>;
|
|
127
124
|
readonly count: number;
|
|
128
|
-
readonly guard?: (value: T) => boolean;
|
|
129
125
|
}
|
|
130
126
|
interface InAll<T = any> {
|
|
131
127
|
readonly type: 'all';
|
|
132
128
|
readonly place: Place<T>;
|
|
133
|
-
readonly guard?: (value: T) => boolean;
|
|
134
129
|
}
|
|
135
130
|
interface InAtLeast<T = any> {
|
|
136
131
|
readonly type: 'at-least';
|
|
137
132
|
readonly place: Place<T>;
|
|
138
133
|
readonly minimum: number;
|
|
139
|
-
readonly guard?: (value: T) => boolean;
|
|
140
134
|
}
|
|
141
|
-
/** Consume exactly 1 token (standard CPN semantics).
|
|
142
|
-
declare function one<T>(place: Place<T
|
|
143
|
-
/** Consume exactly N tokens (batching).
|
|
144
|
-
declare function exactly<T>(count: number, place: Place<T
|
|
145
|
-
/** Consume all available tokens (must be 1+).
|
|
146
|
-
declare function all<T>(place: Place<T
|
|
147
|
-
/** Wait for N+ tokens, consume all when enabled.
|
|
148
|
-
declare function atLeast<T>(minimum: number, place: Place<T
|
|
135
|
+
/** Consume exactly 1 token (standard CPN semantics). */
|
|
136
|
+
declare function one<T>(place: Place<T>): InOne<T>;
|
|
137
|
+
/** Consume exactly N tokens (batching). */
|
|
138
|
+
declare function exactly<T>(count: number, place: Place<T>): InExactly<T>;
|
|
139
|
+
/** Consume all available tokens (must be 1+). */
|
|
140
|
+
declare function all<T>(place: Place<T>): InAll<T>;
|
|
141
|
+
/** Wait for N+ tokens, consume all when enabled. */
|
|
142
|
+
declare function atLeast<T>(minimum: number, place: Place<T>): InAtLeast<T>;
|
|
149
143
|
/** Returns the minimum number of tokens required to enable. */
|
|
150
144
|
declare function requiredCount(spec: In): number;
|
|
151
145
|
/**
|
|
@@ -191,11 +185,11 @@ declare function nameId(value: string): NameId;
|
|
|
191
185
|
* On firing, exactly those name-matched tokens are consumed (spec NU-020).
|
|
192
186
|
*
|
|
193
187
|
* This is the single *decidable* predicate — equality of opaque names — and is
|
|
194
|
-
* deliberately NOT a general
|
|
195
|
-
* places (composition-structural, like cardinality) rather than
|
|
196
|
-
* arbitrary boolean per token.
|
|
197
|
-
*
|
|
198
|
-
*
|
|
188
|
+
* deliberately NOT a general input predicate: it correlates the name dimension
|
|
189
|
+
* *across* places (composition-structural, like cardinality) rather than
|
|
190
|
+
* evaluating an arbitrary boolean per token. Input specifications themselves
|
|
191
|
+
* remain purely structural (IO-006); name correlation is the only per-token
|
|
192
|
+
* filter the enablement check ever applies (NU-021).
|
|
199
193
|
*/
|
|
200
194
|
|
|
201
195
|
/**
|
|
@@ -228,7 +222,7 @@ declare function matchKey<T>(place: Place<T>, key: KeyFn<T>): MatchKey<T>;
|
|
|
228
222
|
* ))
|
|
229
223
|
*
|
|
230
224
|
* @throws if fewer than two inputs are correlated (a match over a single place
|
|
231
|
-
*
|
|
225
|
+
* correlates nothing).
|
|
232
226
|
*/
|
|
233
227
|
declare function matchSpec(...keys: MatchKey[]): MatchSpec;
|
|
234
228
|
/** Returns the name projection for `placeName`, or `undefined` if not correlated. */
|
|
@@ -988,6 +982,25 @@ interface SmtVerificationResult {
|
|
|
988
982
|
readonly discoveredInvariants: readonly string[];
|
|
989
983
|
readonly counterexampleTrace: readonly MarkingState[];
|
|
990
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;
|
|
991
1004
|
readonly elapsedMs: number;
|
|
992
1005
|
readonly statistics: SmtStatistics;
|
|
993
1006
|
}
|
|
@@ -1746,7 +1759,8 @@ declare class PetriNetBuilder {
|
|
|
1746
1759
|
* Map<string, Place<unknown>> convention — TypeScript Place identity is
|
|
1747
1760
|
* name-based per `runtime/compiled-net.ts`).
|
|
1748
1761
|
* 3. Walk every transition through {@link applyFusion} to rewrite arc place
|
|
1749
|
-
* references
|
|
1762
|
+
* references; input arcs colliding on a canonical place merge per
|
|
1763
|
+
* [MOD-021] (additive where summable, rejected otherwise).
|
|
1750
1764
|
* 4. Re-derive the place set from the rewritten transitions plus any
|
|
1751
1765
|
* caller-declared standalone places, dropping non-canonical members.
|
|
1752
1766
|
* Caller-declared standalone places that happen to be non-canonical
|
|
@@ -1782,4 +1796,4 @@ declare class PetriNetBuilder {
|
|
|
1782
1796
|
private buildWithFusion;
|
|
1783
1797
|
}
|
|
1784
1798
|
|
|
1785
|
-
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
|
|
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, MarkingState as aO, type PInvariant 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":"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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 };
|