libpetri 2.3.0 → 2.3.2

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.
@@ -1,3 +1,6 @@
1
+ // src/viewer/render.ts
2
+ import { instance as vizInstance } from "@viz-js/viz";
3
+
1
4
  // src/viewer/layout/preprocess.ts
2
5
  function classifyKind(id) {
3
6
  if (id.startsWith("p_")) return "place";
@@ -238,6 +241,124 @@ var ORCHESTRATOR_OPTIONS = {
238
241
  "elk.aspectRatio": "2.4",
239
242
  "elk.spacing.nodeNode": "10"
240
243
  };
244
+ var FLAT_OPTIONS = {
245
+ "elk.padding": `[top=24,left=24,bottom=24,right=24]`,
246
+ "elk.algorithm": "layered",
247
+ "elk.direction": "DOWN",
248
+ "elk.spacing.nodeNode": "45",
249
+ "elk.layered.spacing.nodeNodeBetweenLayers": "60",
250
+ "elk.edgeRouting": "ORTHOGONAL"
251
+ };
252
+ var RECTPACK_CLUSTER_OPTIONS = {
253
+ "elk.padding": `[top=${FONT_CLUSTER + 16},left=16,bottom=16,right=16]`,
254
+ "elk.algorithm": "org.eclipse.elk.rectpacking",
255
+ "elk.aspectRatio": "1.3",
256
+ "elk.spacing.nodeNode": String(CLUSTER_SPACING)
257
+ };
258
+ var FLOW_SUB_OPTIONS = {
259
+ "elk.algorithm": "layered",
260
+ "elk.direction": "DOWN",
261
+ "elk.spacing.nodeNode": String(CLUSTER_SPACING),
262
+ "elk.layered.spacing.nodeNodeBetweenLayers": String(CLUSTER_SPACING + 6),
263
+ "elk.edgeRouting": "ORTHOGONAL"
264
+ };
265
+ var LEAF_SUB_OPTIONS = {
266
+ "elk.algorithm": "org.eclipse.elk.rectpacking",
267
+ "elk.aspectRatio": "1.4",
268
+ "elk.spacing.nodeNode": String(CLUSTER_SPACING)
269
+ };
270
+ var LEAF_BLOCK_GAP = 40;
271
+ var SUB_PAD = 16;
272
+ var SUB_PAD_TOP = FONT_CLUSTER + 16;
273
+ var DEFAULT_MIN_LEAVES = 10;
274
+ function resolveConfig(cfg) {
275
+ const lp = cfg?.leafPacking ?? true;
276
+ const lpObj = typeof lp === "object" ? lp : {};
277
+ return {
278
+ clusterLayout: cfg?.clusterLayout ?? "layered",
279
+ leafPacking: {
280
+ enabled: lp !== false,
281
+ minLeaves: lpObj.minLeaves ?? DEFAULT_MIN_LEAVES,
282
+ arcs: new Set(lpObj.arcs ?? ["reset", "read"])
283
+ }
284
+ };
285
+ }
286
+ function classifyLeaves(graph, cluster, arcs) {
287
+ const members = new Set(cluster.nodes);
288
+ const intraDegree = /* @__PURE__ */ new Map();
289
+ const intraSideEffect = /* @__PURE__ */ new Map();
290
+ for (const e of graph.edges) {
291
+ if (!members.has(e.src) || !members.has(e.dst)) continue;
292
+ const sideEffect = arcs.has(e.arc) ? 1 : 0;
293
+ for (const id of [e.src, e.dst]) {
294
+ intraDegree.set(id, (intraDegree.get(id) ?? 0) + 1);
295
+ intraSideEffect.set(id, (intraSideEffect.get(id) ?? 0) + sideEffect);
296
+ }
297
+ }
298
+ const flow = [];
299
+ const leaves = [];
300
+ for (const id of cluster.nodes) {
301
+ const node = graph.nodes.get(id);
302
+ if (!node) continue;
303
+ const deg = intraDegree.get(id) ?? 0;
304
+ const isLeaf = node.kind === "place" && deg >= 1 && (intraSideEffect.get(id) ?? 0) === deg;
305
+ (isLeaf ? leaves : flow).push(id);
306
+ }
307
+ return { flow, leaves };
308
+ }
309
+ async function layoutDominatedCluster(elk, graph, flow, leaves) {
310
+ const dimsOf = (id) => nodeDims(graph.nodes.get(id));
311
+ const flowSet = new Set(flow);
312
+ const flowEdges = [];
313
+ graph.edges.forEach((e, i) => {
314
+ if (flowSet.has(e.src) && flowSet.has(e.dst)) {
315
+ flowEdges.push({ id: `fe${i}`, sources: [e.src], targets: [e.dst] });
316
+ }
317
+ });
318
+ const flowLayout = await elk.layout({
319
+ id: "__flow",
320
+ layoutOptions: FLOW_SUB_OPTIONS,
321
+ children: flow.map((id) => ({ id, ...dimsOf(id) })),
322
+ edges: flowEdges
323
+ });
324
+ const leafLayout = await elk.layout({
325
+ id: "__leaves",
326
+ layoutOptions: LEAF_SUB_OPTIONS,
327
+ children: leaves.map((id) => ({ id, ...dimsOf(id) })),
328
+ edges: []
329
+ });
330
+ const flowW = flowLayout.width ?? 0;
331
+ const flowH = flowLayout.height ?? 0;
332
+ const leafW = leafLayout.width ?? 0;
333
+ const leafH = leafLayout.height ?? 0;
334
+ const contentW = Math.max(flowW, leafW);
335
+ const contentH = flowH + (leaves.length > 0 ? LEAF_BLOCK_GAP + leafH : 0);
336
+ const rel = /* @__PURE__ */ new Map();
337
+ const flowOffX = SUB_PAD + (contentW - flowW) / 2;
338
+ for (const ch of flowLayout.children ?? []) {
339
+ rel.set(ch.id, {
340
+ x: flowOffX + (ch.x ?? 0),
341
+ y: SUB_PAD_TOP + (ch.y ?? 0),
342
+ width: ch.width ?? 0,
343
+ height: ch.height ?? 0
344
+ });
345
+ }
346
+ const leafOffX = SUB_PAD + (contentW - leafW) / 2;
347
+ const leafOffY = SUB_PAD_TOP + flowH + LEAF_BLOCK_GAP;
348
+ for (const ch of leafLayout.children ?? []) {
349
+ rel.set(ch.id, {
350
+ x: leafOffX + (ch.x ?? 0),
351
+ y: leafOffY + (ch.y ?? 0),
352
+ width: ch.width ?? 0,
353
+ height: ch.height ?? 0
354
+ });
355
+ }
356
+ return {
357
+ width: contentW + 2 * SUB_PAD,
358
+ height: contentH + SUB_PAD_TOP + SUB_PAD,
359
+ rel
360
+ };
361
+ }
241
362
  function partitionEdges(graph) {
242
363
  const byCluster = /* @__PURE__ */ new Map();
243
364
  const cross = [];
@@ -266,29 +387,50 @@ function partitionEdges(graph) {
266
387
  });
267
388
  return { byCluster, cross, edgeIdToKey };
268
389
  }
269
- async function elkLayout(graph) {
390
+ async function elkLayout(graph, cfg) {
391
+ const config = resolveConfig(cfg);
270
392
  const { byCluster, cross, edgeIdToKey } = partitionEdges(graph);
393
+ const elk = new ELK();
394
+ const clusterOptions = config.clusterLayout === "rectpacking" ? RECTPACK_CLUSTER_OPTIONS : CLUSTER_OPTIONS;
395
+ const prebuilt = /* @__PURE__ */ new Map();
396
+ if (config.clusterLayout === "layered" && config.leafPacking.enabled) {
397
+ for (const [clusterId, cluster] of graph.clusters) {
398
+ const { flow, leaves } = classifyLeaves(graph, cluster, config.leafPacking.arcs);
399
+ if (leaves.length >= config.leafPacking.minLeaves && flow.length > 0) {
400
+ prebuilt.set(clusterId, await layoutDominatedCluster(elk, graph, flow, leaves));
401
+ }
402
+ }
403
+ }
271
404
  const elkChildren = [];
272
405
  for (const [clusterId, cluster] of graph.clusters) {
406
+ const pre = prebuilt.get(clusterId);
407
+ if (pre) {
408
+ elkChildren.push({ id: clusterId, width: pre.width, height: pre.height });
409
+ continue;
410
+ }
273
411
  elkChildren.push({
274
412
  id: clusterId,
275
- layoutOptions: CLUSTER_OPTIONS,
413
+ layoutOptions: clusterOptions,
276
414
  children: cluster.nodes.map((id) => graph.nodes.get(id)).filter((n) => n !== void 0).map((n) => ({ id: n.id, ...nodeDims(n) })),
277
415
  edges: byCluster.get(clusterId) ?? []
278
416
  });
279
417
  }
418
+ const orchestratorOptions = graph.clusters.size === 0 ? FLAT_OPTIONS : config.clusterLayout === "rectpacking" ? RECTPACK_CLUSTER_OPTIONS : ORCHESTRATOR_OPTIONS;
280
419
  elkChildren.push({
281
420
  id: ORCHESTRATOR_CLUSTER_ID,
282
- layoutOptions: ORCHESTRATOR_OPTIONS,
421
+ layoutOptions: orchestratorOptions,
283
422
  children: graph.orphans.map((id) => graph.nodes.get(id)).filter((n) => n !== void 0).map((n) => ({ id: n.id, ...nodeDims(n) })),
284
423
  edges: byCluster.get(ORCHESTRATOR_CLUSTER_ID) ?? []
285
424
  });
286
- const elk = new ELK();
287
425
  const rootGraph = {
288
426
  id: "root",
289
427
  layoutOptions: ROOT_OPTIONS,
290
428
  children: elkChildren,
291
- edges: cross
429
+ // Cross-cluster edges only ever fed `edgePaths` (an unused nop2
430
+ // affordance); `rectpacking` ignores them for placement. Drop them when
431
+ // any cluster is a childless prebuilt box — the edges reference member
432
+ // nodes ELK can no longer resolve in the hierarchy.
433
+ edges: prebuilt.size > 0 ? [] : cross
292
434
  };
293
435
  const layout = await elk.layout(rootGraph);
294
436
  const nodePositions = /* @__PURE__ */ new Map();
@@ -297,7 +439,8 @@ async function elkLayout(graph) {
297
439
  const walk = (node, parentX, parentY) => {
298
440
  const ax = parentX + (node.x ?? 0);
299
441
  const ay = parentY + (node.y ?? 0);
300
- const isCluster = node.id !== "root" && (node.children?.length ?? 0) > 0;
442
+ const pre = node.id !== "root" ? prebuilt.get(node.id) : void 0;
443
+ const isCluster = node.id !== "root" && ((node.children?.length ?? 0) > 0 || pre !== void 0);
301
444
  if (isCluster) {
302
445
  clusterBoxes.set(node.id, {
303
446
  x: ax,
@@ -305,6 +448,16 @@ async function elkLayout(graph) {
305
448
  width: node.width ?? 0,
306
449
  height: node.height ?? 0
307
450
  });
451
+ if (pre) {
452
+ for (const [memberId, r] of pre.rel) {
453
+ nodePositions.set(memberId, {
454
+ x: ax + r.x,
455
+ y: ay + r.y,
456
+ width: r.width,
457
+ height: r.height
458
+ });
459
+ }
460
+ }
308
461
  }
309
462
  if (node.id !== "root" && !isCluster) {
310
463
  nodePositions.set(node.id, {
@@ -409,11 +562,67 @@ function writeBack(graph, layout) {
409
562
  return lines.join("\n");
410
563
  }
411
564
 
565
+ // src/viewer/render.ts
566
+ var vizPromise = null;
567
+ function getViz() {
568
+ if (!vizPromise) vizPromise = vizInstance();
569
+ return vizPromise;
570
+ }
571
+ async function renderDotToSvg(dotSource) {
572
+ const viz = await getViz();
573
+ return viz.renderSVGElement(dotSource, { engine: "dot" });
574
+ }
575
+ function fnv1a(input) {
576
+ let h = 2166136261;
577
+ for (let i = 0; i < input.length; i++) {
578
+ h ^= input.charCodeAt(i);
579
+ h = Math.imul(h, 16777619);
580
+ }
581
+ return (h >>> 0).toString(16);
582
+ }
583
+ var PINNED_DOT_CACHE_CAP = 16;
584
+ var pinnedDotCache = /* @__PURE__ */ new Map();
585
+ function getCachedPinnedDot(key) {
586
+ const hit = pinnedDotCache.get(key);
587
+ if (hit !== void 0) {
588
+ pinnedDotCache.delete(key);
589
+ pinnedDotCache.set(key, hit);
590
+ }
591
+ return hit;
592
+ }
593
+ function setCachedPinnedDot(key, value) {
594
+ pinnedDotCache.set(key, value);
595
+ while (pinnedDotCache.size > PINNED_DOT_CACHE_CAP) {
596
+ const oldest = pinnedDotCache.keys().next().value;
597
+ if (oldest === void 0) break;
598
+ pinnedDotCache.delete(oldest);
599
+ }
600
+ }
601
+ function _clearElkLayoutCache() {
602
+ pinnedDotCache.clear();
603
+ }
604
+ async function renderDotToSvgWithElkLayout(dotSource, cfg) {
605
+ const key = fnv1a(dotSource + "\0" + JSON.stringify(cfg ?? {}));
606
+ let pinnedDot = getCachedPinnedDot(key);
607
+ if (pinnedDot === void 0) {
608
+ const graph = replicateShared(
609
+ foldOrphans(parseLibpetriDot(dotSource), 0.7),
610
+ { max: Infinity }
611
+ );
612
+ const layout = await elkLayout(graph, cfg);
613
+ pinnedDot = writeBack(graph, layout);
614
+ setCachedPinnedDot(key, pinnedDot);
615
+ }
616
+ const viz = await getViz();
617
+ return viz.renderSVGElement(pinnedDot, {
618
+ engine: "nop",
619
+ yInvert: true
620
+ });
621
+ }
412
622
  export {
413
- parseLibpetriDot,
414
- foldOrphans,
415
- replicateShared,
416
- elkLayout,
417
- writeBack
623
+ _clearElkLayoutCache,
624
+ getViz,
625
+ renderDotToSvg,
626
+ renderDotToSvgWithElkLayout
418
627
  };
419
- //# sourceMappingURL=chunk-RNWTYK5B.js.map
628
+ //# sourceMappingURL=render-UFO543NR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/viewer/render.ts","../src/viewer/layout/preprocess.ts","../src/viewer/layout/elk-place.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 `neato` with `nop=1`\n * (pin mode). 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 * @module viewer/render\n */\n\nimport { instance as vizInstance } from '@viz-js/viz';\nimport {\n foldOrphans,\n parseLibpetriDot,\n replicateShared,\n} from './layout/preprocess.js';\nimport { elkLayout, writeBack } from './layout/elk-place.js';\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 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 engine: 'nop',\n yInvert: true,\n });\n}\n","/**\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","/**\n * ELK placement + DOT pin-mode rewrite.\n *\n * Stage 2 of the C0 pipeline. Takes the {@link GraphModel} produced by\n * {@link parseLibpetriDot} → {@link foldOrphans} → {@link replicateShared},\n * runs ELKjs to compute absolute `(x, y)` per node and bounding boxes per\n * cluster, then emits a fresh DOT string with positions pinned via\n * `pos=\"x,y!\"` and `bb=\"…\"`. Downstream callers render the result through\n * `@viz-js/viz` with `engine: 'neato'` + `graphAttributes: { nop: '1' }` —\n * Graphviz uses the pinned positions verbatim and routes edges around the\n * pinned cluster boundaries.\n *\n * `elkLayout` takes (graph, cfg?); `writeBack` takes (graph, layout).\n * Neither touches the original DOT text — the model after replication has\n * nodes the original DOT doesn't, so patching the original would be\n * incorrect.\n *\n * Layout is configurable via {@link ElkLayoutConfig}: the per-subnet\n * algorithm (`clusterLayout`) and whether clusters dominated by side-effect\n * leaf places pack those leaves into a grid sub-block (`leafPacking`).\n *\n * @module viewer/layout/elk-place\n */\n\nimport ELK from 'elkjs/lib/elk.bundled.js';\nimport type {\n ElkExtendedEdge,\n ElkNode,\n} from 'elkjs/lib/elk-api.js';\nimport type { ArcType, GraphModel, ParsedCluster, ParsedNode } from './preprocess.js';\n\nconst ORCHESTRATOR_CLUSTER_ID = 'cluster_orchestrator';\n\n// Visual sizing — kept proportional to the v3 reference renderer so the\n// resulting bounding box is comparable to C0-GOOD_v3.svg.\nconst PLACE_RADIUS = 22;\nconst PLACE_CHARW = 7.4;\nconst PLACE_PAD_BELOW = 16;\nconst FONT_PLACE = 13;\nconst TRANSITION_MIN_W = 180;\nconst TRANSITION_H = 40;\nconst TRANSITION_CHARW = 8.5;\nconst JUNCTION_SIZE = 28;\nconst FONT_CLUSTER = 17;\nconst ROOT_SPACING = 90;\nconst CLUSTER_SPACING = 16;\n\n/**\n * Width/height in points that ELK should reserve for a node. Mirrors\n * `nodeDims` in v3 lines 66–78 — places get extra height to leave room for\n * the xlabel underneath the circle.\n */\nfunction nodeDims(node: ParsedNode): { width: number; height: number } {\n if (node.kind === 'place') {\n const label = (node.attrs.xlabel ?? node.attrs.label ?? node.id).replace(/\\\\n/g, ' ');\n const labelWidth = label.length * PLACE_CHARW;\n const circleD = PLACE_RADIUS * 2 + 4;\n return {\n width: Math.max(circleD, labelWidth),\n height: circleD + PLACE_PAD_BELOW + FONT_PLACE,\n };\n }\n if (node.kind === 'junction') {\n return { width: JUNCTION_SIZE, height: JUNCTION_SIZE };\n }\n const label = (node.attrs.label ?? node.id).replace(/\\\\n/g, ' ');\n return {\n width: Math.max(TRANSITION_MIN_W, label.length * TRANSITION_CHARW),\n height: TRANSITION_H,\n };\n}\n\nexport interface NodePosition {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\nexport interface ClusterBox {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\nexport interface EdgePath {\n readonly start: { x: number; y: number };\n readonly end: { x: number; y: number };\n readonly bends: ReadonlyArray<{ x: number; y: number }>;\n}\n\nexport interface LayoutResult {\n readonly nodePositions: ReadonlyMap<string, NodePosition>;\n readonly clusterBoxes: ReadonlyMap<string, ClusterBox>;\n /** Keyed on `${src}->${dst}` — ELK-computed edge routes for `nop=2`. */\n readonly edgePaths: ReadonlyMap<string, EdgePath>;\n readonly totalWidth: number;\n readonly totalHeight: number;\n}\n\nconst CLUSTER_OPTIONS: Record<string, string> = {\n 'elk.padding': `[top=${FONT_CLUSTER + 16},left=16,bottom=16,right=16]`,\n 'elk.algorithm': 'layered',\n 'elk.direction': 'DOWN',\n 'elk.spacing.nodeNode': String(CLUSTER_SPACING),\n 'elk.layered.spacing.nodeNodeBetweenLayers': String(CLUSTER_SPACING + 6),\n 'elk.edgeRouting': 'ORTHOGONAL',\n};\n\nconst ROOT_OPTIONS: Record<string, string> = {\n 'elk.algorithm': 'org.eclipse.elk.rectpacking',\n 'elk.aspectRatio': '1.4',\n 'elk.padding': '[top=24,left=24,bottom=24,right=24]',\n 'elk.spacing.nodeNode': String(ROOT_SPACING),\n};\n\nconst ORCHESTRATOR_OPTIONS: Record<string, string> = {\n ...CLUSTER_OPTIONS,\n 'elk.aspectRatio': '2.4',\n 'elk.spacing.nodeNode': '10',\n};\n\n/**\n * Layout options for the synthetic orchestrator when it IS the whole\n * diagram — the flat \"one-net\" view (no real subnet clusters).\n *\n * `ORCHESTRATOR_OPTIONS` deliberately packs the orphan block tight\n * (`nodeNode: 10`, `aspectRatio: 2.4`) so it stays compact next to the\n * real subnet blocks the `rectpacking` root arranges. When the graph is\n * flat there are no other blocks: that compaction just bunches the whole\n * net up. Flat mode instead gets a generous layered layout — a clean\n * place→transition rank flow — with no forced aspect ratio.\n */\nconst FLAT_OPTIONS: Record<string, string> = {\n 'elk.padding': `[top=24,left=24,bottom=24,right=24]`,\n 'elk.algorithm': 'layered',\n 'elk.direction': 'DOWN',\n 'elk.spacing.nodeNode': '45',\n 'elk.layered.spacing.nodeNodeBetweenLayers': '60',\n 'elk.edgeRouting': 'ORTHOGONAL',\n};\n\n/**\n * Per-subnet ELK algorithm used when {@link ElkLayoutConfig.clusterLayout}\n * is `'rectpacking'` — packs a cluster's nodes as a 2-D grid, ignoring\n * intra-cluster edges. Maximally compact, but the place→transition flow\n * reading order is lost.\n */\nconst RECTPACK_CLUSTER_OPTIONS: Record<string, string> = {\n 'elk.padding': `[top=${FONT_CLUSTER + 16},left=16,bottom=16,right=16]`,\n 'elk.algorithm': 'org.eclipse.elk.rectpacking',\n 'elk.aspectRatio': '1.3',\n 'elk.spacing.nodeNode': String(CLUSTER_SPACING),\n};\n\n// Sub-layout options for a leaf-packed cluster: the flow part keeps the\n// layered place→transition rank flow; the leaf part is a compact grid.\nconst FLOW_SUB_OPTIONS: Record<string, string> = {\n 'elk.algorithm': 'layered',\n 'elk.direction': 'DOWN',\n 'elk.spacing.nodeNode': String(CLUSTER_SPACING),\n 'elk.layered.spacing.nodeNodeBetweenLayers': String(CLUSTER_SPACING + 6),\n 'elk.edgeRouting': 'ORTHOGONAL',\n};\nconst LEAF_SUB_OPTIONS: Record<string, string> = {\n 'elk.algorithm': 'org.eclipse.elk.rectpacking',\n 'elk.aspectRatio': '1.4',\n 'elk.spacing.nodeNode': String(CLUSTER_SPACING),\n};\n/** Gap between the layered flow and the packed leaf block, in points. */\nconst LEAF_BLOCK_GAP = 40;\n/** Padding inside a leaf-packed cluster box (left/right/bottom). */\nconst SUB_PAD = 16;\n/** Top padding inside a cluster box — room for the cluster label. */\nconst SUB_PAD_TOP = FONT_CLUSTER + 16;\n\n// ======================== Layout configuration ========================\n\n/** Per-subnet ELK algorithm. */\nexport type ClusterLayout = 'layered' | 'rectpacking';\n\n/** Tuning for side-effect-leaf packing — see {@link ElkLayoutConfig}. */\nexport interface LeafPackingOptions {\n /**\n * A cluster is \"dominated\" — and its side-effect leaf places get packed\n * into a grid sub-block — when it has at least this many of them.\n * Default 10.\n */\n readonly minLeaves?: number;\n /**\n * Arc types that mark a place as a side-effect leaf (a place whose every\n * intra-cluster arc is one of these carries no data-flow ordering).\n * Default `['reset', 'read']`.\n */\n readonly arcs?: readonly ArcType[];\n}\n\n/**\n * Layout configuration for {@link elkLayout}, surfaced to viewer callers\n * via `MountOptions`.\n */\nexport interface ElkLayoutConfig {\n /** Per-subnet ELK algorithm. Default `'layered'`. */\n readonly clusterLayout?: ClusterLayout;\n /**\n * Pack side-effect leaf places into a rectpacking sub-block, in clusters\n * where they dominate. `true` (default) enables it with defaults; `false`\n * disables it; an object tunes the threshold / arc types. Only takes\n * effect when `clusterLayout` is `'layered'`.\n */\n readonly leafPacking?: boolean | LeafPackingOptions;\n}\n\ninterface ResolvedConfig {\n readonly clusterLayout: ClusterLayout;\n readonly leafPacking: {\n readonly enabled: boolean;\n readonly minLeaves: number;\n readonly arcs: ReadonlySet<ArcType>;\n };\n}\n\nconst DEFAULT_MIN_LEAVES = 10;\n\nfunction resolveConfig(cfg?: ElkLayoutConfig): ResolvedConfig {\n const lp = cfg?.leafPacking ?? true;\n const lpObj: LeafPackingOptions = typeof lp === 'object' ? lp : {};\n return {\n clusterLayout: cfg?.clusterLayout ?? 'layered',\n leafPacking: {\n enabled: lp !== false,\n minLeaves: lpObj.minLeaves ?? DEFAULT_MIN_LEAVES,\n arcs: new Set(lpObj.arcs ?? ['reset', 'read']),\n },\n };\n}\n\n/** A cluster pre-laid-out as [layered flow] stacked over [packed leaves]. */\ninterface PrebuiltCluster {\n readonly width: number;\n readonly height: number;\n /** member node id → position relative to the cluster's top-left. */\n readonly rel: ReadonlyMap<string, NodePosition>;\n}\n\n/**\n * Split a cluster's members into flow nodes and \"side-effect leaf places\".\n *\n * A side-effect leaf is a place whose every intra-cluster arc is a\n * side-effect arc (per `arcs` — reset/read). Such a place is not on any\n * data-flow path, so packing it as a grid cell loses no reading order.\n */\nfunction classifyLeaves(\n graph: GraphModel,\n cluster: ParsedCluster,\n arcs: ReadonlySet<ArcType>,\n): { flow: string[]; leaves: string[] } {\n const members = new Set(cluster.nodes);\n const intraDegree = new Map<string, number>();\n const intraSideEffect = new Map<string, number>();\n for (const e of graph.edges) {\n if (!members.has(e.src) || !members.has(e.dst)) continue;\n const sideEffect = arcs.has(e.arc) ? 1 : 0;\n for (const id of [e.src, e.dst]) {\n intraDegree.set(id, (intraDegree.get(id) ?? 0) + 1);\n intraSideEffect.set(id, (intraSideEffect.get(id) ?? 0) + sideEffect);\n }\n }\n const flow: string[] = [];\n const leaves: string[] = [];\n for (const id of cluster.nodes) {\n const node = graph.nodes.get(id);\n if (!node) continue;\n const deg = intraDegree.get(id) ?? 0;\n const isLeaf =\n node.kind === 'place' &&\n deg >= 1 &&\n (intraSideEffect.get(id) ?? 0) === deg;\n (isLeaf ? leaves : flow).push(id);\n }\n return { flow, leaves };\n}\n\n/**\n * Lay a dominated cluster out as two stacked blocks: the flow nodes via\n * `layered` (place→transition rank flow), the side-effect leaves via\n * `rectpacking` (a compact grid). Returns the cluster's overall size and\n * every member node's position relative to the cluster's top-left.\n */\nasync function layoutDominatedCluster(\n elk: InstanceType<typeof ELK>,\n graph: GraphModel,\n flow: string[],\n leaves: string[],\n): Promise<PrebuiltCluster> {\n const dimsOf = (id: string): { width: number; height: number } =>\n nodeDims(graph.nodes.get(id)!);\n\n const flowSet = new Set(flow);\n const flowEdges: ElkExtendedEdge[] = [];\n graph.edges.forEach((e, i) => {\n if (flowSet.has(e.src) && flowSet.has(e.dst)) {\n flowEdges.push({ id: `fe${i}`, sources: [e.src], targets: [e.dst] });\n }\n });\n\n const flowLayout: ElkNode = await elk.layout({\n id: '__flow',\n layoutOptions: FLOW_SUB_OPTIONS,\n children: flow.map(id => ({ id, ...dimsOf(id) })),\n edges: flowEdges,\n });\n const leafLayout: ElkNode = await elk.layout({\n id: '__leaves',\n layoutOptions: LEAF_SUB_OPTIONS,\n children: leaves.map(id => ({ id, ...dimsOf(id) })),\n edges: [],\n });\n\n const flowW = flowLayout.width ?? 0;\n const flowH = flowLayout.height ?? 0;\n const leafW = leafLayout.width ?? 0;\n const leafH = leafLayout.height ?? 0;\n const contentW = Math.max(flowW, leafW);\n const contentH = flowH + (leaves.length > 0 ? LEAF_BLOCK_GAP + leafH : 0);\n\n const rel = new Map<string, NodePosition>();\n const flowOffX = SUB_PAD + (contentW - flowW) / 2;\n for (const ch of flowLayout.children ?? []) {\n rel.set(ch.id, {\n x: flowOffX + (ch.x ?? 0),\n y: SUB_PAD_TOP + (ch.y ?? 0),\n width: ch.width ?? 0,\n height: ch.height ?? 0,\n });\n }\n const leafOffX = SUB_PAD + (contentW - leafW) / 2;\n const leafOffY = SUB_PAD_TOP + flowH + LEAF_BLOCK_GAP;\n for (const ch of leafLayout.children ?? []) {\n rel.set(ch.id, {\n x: leafOffX + (ch.x ?? 0),\n y: leafOffY + (ch.y ?? 0),\n width: ch.width ?? 0,\n height: ch.height ?? 0,\n });\n }\n\n return {\n width: contentW + 2 * SUB_PAD,\n height: contentH + SUB_PAD_TOP + SUB_PAD,\n rel,\n };\n}\n\n/**\n * Group edges by the cluster that contains both endpoints (or 'root' if\n * the edge crosses cluster boundaries).\n *\n * Orphans count as living in `cluster_orchestrator` for routing purposes,\n * matching v3's `ownerOf` (line 223).\n */\nfunction partitionEdges(graph: GraphModel): {\n byCluster: Map<string, ElkExtendedEdge[]>;\n cross: ElkExtendedEdge[];\n /** edgeId → \"src->dst\" so the layout walk can recover the original key. */\n edgeIdToKey: Map<string, string>;\n} {\n const byCluster = new Map<string, ElkExtendedEdge[]>();\n const cross: ElkExtendedEdge[] = [];\n const edgeIdToKey = new Map<string, string>();\n const ownerOf = (id: string): string =>\n graph.nodeToCluster.get(id) ?? ORCHESTRATOR_CLUSTER_ID;\n\n graph.edges.forEach((e, i) => {\n const so = ownerOf(e.src);\n const dt = ownerOf(e.dst);\n const id = `e${i}`;\n edgeIdToKey.set(id, `${e.src}->${e.dst}`);\n const elkEdge: ElkExtendedEdge = {\n id,\n sources: [e.src],\n targets: [e.dst],\n };\n if (so === dt) {\n let list = byCluster.get(so);\n if (!list) { list = []; byCluster.set(so, list); }\n list.push(elkEdge);\n } else {\n cross.push(elkEdge);\n }\n });\n return { byCluster, cross, edgeIdToKey };\n}\n\n/**\n * Run ELK layout against the C0 graph and return absolute positions.\n *\n * Orphan nodes are wrapped in a synthetic `cluster_orchestrator` so ELK\n * lays them out as a single block alongside the real subnets, which the\n * `rectpacking` root algorithm then packs.\n *\n * With the default config a cluster dominated by side-effect leaf places\n * (≥ `leafPacking.minLeaves` of them) is pre-laid-out as a layered flow\n * stacked over a packed grid of those leaves — so a transition with many\n * reset arcs no longer strings its leaves into one wide row. See\n * {@link ElkLayoutConfig}.\n */\nexport async function elkLayout(\n graph: GraphModel,\n cfg?: ElkLayoutConfig,\n): Promise<LayoutResult> {\n const config = resolveConfig(cfg);\n const { byCluster, cross, edgeIdToKey } = partitionEdges(graph);\n const elk = new ELK();\n const clusterOptions =\n config.clusterLayout === 'rectpacking' ? RECTPACK_CLUSTER_OPTIONS : CLUSTER_OPTIONS;\n\n // Pre-lay-out clusters dominated by side-effect leaf places. Only in\n // 'layered' mode — 'rectpacking' already packs every cluster's nodes.\n const prebuilt = new Map<string, PrebuiltCluster>();\n if (config.clusterLayout === 'layered' && config.leafPacking.enabled) {\n for (const [clusterId, cluster] of graph.clusters) {\n const { flow, leaves } = classifyLeaves(graph, cluster, config.leafPacking.arcs);\n if (leaves.length >= config.leafPacking.minLeaves && flow.length > 0) {\n prebuilt.set(clusterId, await layoutDominatedCluster(elk, graph, flow, leaves));\n }\n }\n }\n\n const elkChildren: ElkNode[] = [];\n for (const [clusterId, cluster] of graph.clusters) {\n const pre = prebuilt.get(clusterId);\n if (pre) {\n // Childless fixed-size box — ELK's root packer places it; the member\n // node positions are spliced back in during the walk.\n elkChildren.push({ id: clusterId, width: pre.width, height: pre.height });\n continue;\n }\n elkChildren.push({\n id: clusterId,\n layoutOptions: clusterOptions,\n children: cluster.nodes\n .map(id => graph.nodes.get(id))\n .filter((n): n is ParsedNode => n !== undefined)\n .map(n => ({ id: n.id, ...nodeDims(n) })),\n edges: byCluster.get(clusterId) ?? [],\n });\n }\n // Synthetic orchestrator cluster holds the remaining orphans. When there\n // are no real clusters the orchestrator IS the whole diagram (flat view),\n // so it gets a generous full layered layout instead of the compact block\n // packing tuned for sitting beside real subnets.\n const orchestratorOptions =\n graph.clusters.size === 0\n ? FLAT_OPTIONS\n : config.clusterLayout === 'rectpacking'\n ? RECTPACK_CLUSTER_OPTIONS\n : ORCHESTRATOR_OPTIONS;\n elkChildren.push({\n id: ORCHESTRATOR_CLUSTER_ID,\n layoutOptions: orchestratorOptions,\n children: graph.orphans\n .map(id => graph.nodes.get(id))\n .filter((n): n is ParsedNode => n !== undefined)\n .map(n => ({ id: n.id, ...nodeDims(n) })),\n edges: byCluster.get(ORCHESTRATOR_CLUSTER_ID) ?? [],\n });\n\n const rootGraph: ElkNode = {\n id: 'root',\n layoutOptions: ROOT_OPTIONS,\n children: elkChildren,\n // Cross-cluster edges only ever fed `edgePaths` (an unused nop2\n // affordance); `rectpacking` ignores them for placement. Drop them when\n // any cluster is a childless prebuilt box — the edges reference member\n // nodes ELK can no longer resolve in the hierarchy.\n edges: prebuilt.size > 0 ? [] : cross,\n };\n const layout: ElkNode = await elk.layout(rootGraph);\n\n const nodePositions = new Map<string, NodePosition>();\n const clusterBoxes = new Map<string, ClusterBox>();\n const edgePaths = new Map<string, EdgePath>();\n\n const walk = (node: ElkNode, parentX: number, parentY: number): void => {\n const ax = parentX + (node.x ?? 0);\n const ay = parentY + (node.y ?? 0);\n const pre = node.id !== 'root' ? prebuilt.get(node.id) : undefined;\n const isCluster =\n node.id !== 'root' && ((node.children?.length ?? 0) > 0 || pre !== undefined);\n if (isCluster) {\n clusterBoxes.set(node.id, {\n x: ax, y: ay,\n width: node.width ?? 0,\n height: node.height ?? 0,\n });\n // Prebuilt cluster: splice in the pre-computed member positions,\n // offset by the cluster's ELK-assigned absolute origin.\n if (pre) {\n for (const [memberId, r] of pre.rel) {\n nodePositions.set(memberId, {\n x: ax + r.x, y: ay + r.y, width: r.width, height: r.height,\n });\n }\n }\n }\n if (node.id !== 'root' && !isCluster) {\n nodePositions.set(node.id, {\n x: ax, y: ay,\n width: node.width ?? 0,\n height: node.height ?? 0,\n });\n }\n // Edges inside this node — their section coords are in the parent's\n // coordinate frame, so the offset to absolutize is (parentX, parentY)\n // for root edges, or (ax, ay) for cluster-internal edges.\n const edgeOffsetX = node.id === 'root' ? parentX : ax;\n const edgeOffsetY = node.id === 'root' ? parentY : ay;\n for (const edge of node.edges ?? []) {\n const key = edge.id ? edgeIdToKey.get(edge.id) : undefined;\n const section = edge.sections?.[0];\n if (!key || !section) continue;\n edgePaths.set(key, {\n start: {\n x: edgeOffsetX + section.startPoint.x,\n y: edgeOffsetY + section.startPoint.y,\n },\n end: {\n x: edgeOffsetX + section.endPoint.x,\n y: edgeOffsetY + section.endPoint.y,\n },\n bends: (section.bendPoints ?? []).map(p => ({\n x: edgeOffsetX + p.x,\n y: edgeOffsetY + p.y,\n })),\n });\n }\n for (const child of node.children ?? []) walk(child, ax, ay);\n };\n walk(layout, 0, 0);\n\n return {\n nodePositions,\n clusterBoxes,\n edgePaths,\n totalWidth: layout.width ?? 0,\n totalHeight: layout.height ?? 0,\n };\n}\n\n/**\n * Escape an attribute value for inclusion in a DOT string literal.\n *\n * Quotes are doubled (per the DOT escape convention `\\\"`).\n */\nfunction quote(value: string): string {\n return `\"${value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`;\n}\n\n/**\n * Render a node's attribute list — preserving the parsed attrs (including\n * libpetri's `width`/`height`, which are the *visual* shape dimensions)\n * and adding the ELK-computed `pos` so Graphviz `nop` can pin it.\n *\n * The `pos=\"x,y!\"` form (with trailing `!`) is the neato pin-mode marker.\n * Do NOT emit ELK's layout-reserved width/height to Graphviz — those are\n * the bounding boxes needed for ELK packing (which account for xlabel\n * spillover), not the visible shape sizes. libpetri's standard styles use\n * `width=0.35` for places (25pt circles); overriding with ELK's\n * ~3 inch label-padded box made Graphviz render 114pt-radius circles.\n */\nfunction nodeAttrLine(node: ParsedNode, pos: NodePosition): string {\n const attrs: string[] = [];\n // Center the pinned position on the node's bbox (ELK gives the top-left).\n const cx = pos.x + pos.width / 2;\n const cy = pos.y + pos.height / 2;\n attrs.push(`pos=${quote(`${cx.toFixed(2)},${cy.toFixed(2)}!`)}`);\n for (const [k, v] of Object.entries(node.attrs)) {\n if (k === 'pos') continue;\n attrs.push(`${k}=${quote(v)}`);\n }\n return `${node.id} [${attrs.join(', ')}];`;\n}\n\nfunction edgeAttrLine(src: string, dst: string, rawAttrs: string): string {\n // Splice libpetri's original attrs verbatim so style/penwidth/label/color\n // match `DotExporter` 1:1. We do NOT emit `pos=` for edges — under engine\n // `nop` Graphviz routes edges itself around the pinned node positions,\n // which gives proper boundary clipping (arrowheads land at the visual\n // node edge) and avoids needing a custom polyline-to-spline converter.\n // The only \"custom\" arrowhead is `arrowhead=\"odot\"` for inhibitor edges,\n // which libpetri's exporter already writes into rawAttrs.\n const body = rawAttrs.trim();\n return `${src} -> ${dst}` + (body ? ` [${body}];` : ';');\n}\n\n/**\n * Emit a Graphviz DOT string with positions pinned. Render the result with\n * `@viz-js/viz` using:\n *\n * ```ts\n * viz.renderSVGElement(dot, {\n * engine: 'nop',\n * yInvert: true,\n * });\n * ```\n *\n * `engine: 'nop'` (a.k.a. `nop1`) honours the pinned `pos=` on every node\n * and routes edges around them. We use this rather than `nop2` because\n * `nop2` would force us to provide a spline for every edge ourselves (no\n * routing) — and Graphviz's own routing produces correctly-clipped\n * arrowheads at the visual node boundary, which our ELK polylines did not\n * (ELK uses the layout-reserved bbox, our visual circle is much smaller).\n *\n * `yInvert: true` matches ELK's Y-down convention to Graphviz's Y-up.\n */\nexport function writeBack(graph: GraphModel, layout: LayoutResult): string {\n const lines: string[] = [];\n lines.push('digraph LibpetriPinned {');\n lines.push(' rankdir=TB;');\n lines.push(' overlap=\"false\";');\n lines.push(' compound=\"true\";');\n lines.push(' fontname=\"Helvetica,Arial,sans-serif\";');\n lines.push(' outputorder=\"edgesfirst\";');\n lines.push(' node [fontname=\"Helvetica,Arial,sans-serif\", fontsize=10];');\n lines.push(' edge [fontname=\"Helvetica,Arial,sans-serif\", fontsize=9];');\n\n // Clusters with their pinned bounding boxes\n for (const [clusterId, cluster] of graph.clusters) {\n const box = layout.clusterBoxes.get(clusterId);\n lines.push(` subgraph ${clusterId} {`);\n lines.push(` label=${quote(cluster.shortName)};`);\n lines.push(` style=\"rounded,dashed\";`);\n lines.push(` bgcolor=\"#FAFAFA\";`);\n lines.push(` penwidth=1.5;`);\n if (box) {\n const x1 = box.x.toFixed(2);\n const y1 = box.y.toFixed(2);\n const x2 = (box.x + box.width).toFixed(2);\n const y2 = (box.y + box.height).toFixed(2);\n lines.push(` bb=${quote(`${x1},${y1},${x2},${y2}`)};`);\n }\n for (const nodeId of cluster.nodes) {\n const node = graph.nodes.get(nodeId);\n const pos = layout.nodePositions.get(nodeId);\n if (!node || !pos) continue;\n lines.push(' ' + nodeAttrLine(node, pos));\n }\n lines.push(' }');\n }\n\n // Orphans (root-level nodes) — laid out inside the synthetic\n // orchestrator cluster but emitted at root so they sit alongside the\n // real subnets without their own labelled box.\n for (const orphanId of graph.orphans) {\n const node = graph.nodes.get(orphanId);\n const pos = layout.nodePositions.get(orphanId);\n if (!node || !pos) continue;\n lines.push(' ' + nodeAttrLine(node, pos));\n }\n\n // Edges — emit just the source -> dest pair plus libpetri's original\n // styling attrs. Graphviz `nop` routes the line itself and clips arrow\n // heads to the visual node boundary.\n for (const edge of graph.edges) {\n lines.push(' ' + edgeAttrLine(edge.src, edge.dst, edge.rawAttrs));\n }\n\n lines.push('}');\n return lines.join('\\n');\n}\n"],"mappings":"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","names":["stripped","label"]}
@@ -109,6 +109,89 @@ interface VisibilityState {
109
109
  /** CSS custom property names that the canonical stylesheet honours. */
110
110
  declare const VIEWER_CSS_VARIABLES: readonly ["--lpv-bg", "--lpv-header-bg", "--lpv-border", "--lpv-text", "--lpv-muted", "--lpv-cluster-bg", "--lpv-cluster-bg-collapsed", "--lpv-cluster-stroke-width", "--lpv-cluster-stroke-dash", "--lpv-cluster-fill-tint", "--lpv-dim-opacity", "--lpv-active-filter-outline", "--lpv-legend-bg", "--lpv-chip-bg", "--lpv-chip-active-bg", "--lpv-sidebar-bg", "--lpv-sidebar-border", "--lpv-sidebar-text", "--lpv-sidebar-muted", "--lpv-sidebar-chip-bg", "--lpv-sidebar-chip-hover-bg", "--lpv-sidebar-chip-off-opacity", "--lpv-shared-glyph-color", "--lpv-replica-fill", "--lpv-replica-stroke", "--lpv-highlight-stroke", "--lpv-highlight-glow", "--lpv-neighbor-stroke", "--lpv-faded-node-opacity", "--lpv-faded-edge-opacity", "--lpv-faded-cluster-opacity"];
111
111
 
112
+ /**
113
+ * Graph pre-processing for the C0-replicate-default layout pipeline.
114
+ *
115
+ * Three pure-ish transforms operate on a {@link GraphModel} parsed from a
116
+ * libpetri-exported DOT string:
117
+ *
118
+ * parseLibpetriDot(dot) → GraphModel
119
+ * foldOrphans(graph, threshold) → GraphModel (REASSIGN_ORPHANS)
120
+ * replicateShared(graph, opts) → GraphModel (REPLICATE_SHARED)
121
+ *
122
+ * The pipeline turns a flat cluster soup into a cluster-internal one by
123
+ * (a) folding orphan nodes whose edges concentrate in one cluster into that
124
+ * cluster, and (b) cloning shared places into every foreign cluster they
125
+ * touch so cross-cluster spaghetti disappears. The resulting GraphModel is
126
+ * what ELK lays out (Stage 2) and Graphviz then renders in pin-mode.
127
+ *
128
+ * Algorithm lineage: ported from
129
+ * `/home/db/otto-repos/nucleus_marvin/.scratch/javadoc-layout-exp/CHECKPOINTS/run16_GOOD_v3_skip-junctions.mjs`
130
+ * lines 14–220. The arc-type detection, replica naming
131
+ * (`${placeId}__rep__${clusterShortName}`), and the `__replica` /
132
+ * `replicaOf` markers are load-bearing — downstream stages key off them.
133
+ *
134
+ * @module viewer/layout/preprocess
135
+ */
136
+ type ArcType = 'normal' | 'reset' | 'inhibitor' | 'read';
137
+
138
+ /**
139
+ * ELK placement + DOT pin-mode rewrite.
140
+ *
141
+ * Stage 2 of the C0 pipeline. Takes the {@link GraphModel} produced by
142
+ * {@link parseLibpetriDot} → {@link foldOrphans} → {@link replicateShared},
143
+ * runs ELKjs to compute absolute `(x, y)` per node and bounding boxes per
144
+ * cluster, then emits a fresh DOT string with positions pinned via
145
+ * `pos="x,y!"` and `bb="…"`. Downstream callers render the result through
146
+ * `@viz-js/viz` with `engine: 'neato'` + `graphAttributes: { nop: '1' }` —
147
+ * Graphviz uses the pinned positions verbatim and routes edges around the
148
+ * pinned cluster boundaries.
149
+ *
150
+ * `elkLayout` takes (graph, cfg?); `writeBack` takes (graph, layout).
151
+ * Neither touches the original DOT text — the model after replication has
152
+ * nodes the original DOT doesn't, so patching the original would be
153
+ * incorrect.
154
+ *
155
+ * Layout is configurable via {@link ElkLayoutConfig}: the per-subnet
156
+ * algorithm (`clusterLayout`) and whether clusters dominated by side-effect
157
+ * leaf places pack those leaves into a grid sub-block (`leafPacking`).
158
+ *
159
+ * @module viewer/layout/elk-place
160
+ */
161
+
162
+ /** Per-subnet ELK algorithm. */
163
+ type ClusterLayout = 'layered' | 'rectpacking';
164
+ /** Tuning for side-effect-leaf packing — see {@link ElkLayoutConfig}. */
165
+ interface LeafPackingOptions {
166
+ /**
167
+ * A cluster is "dominated" — and its side-effect leaf places get packed
168
+ * into a grid sub-block — when it has at least this many of them.
169
+ * Default 10.
170
+ */
171
+ readonly minLeaves?: number;
172
+ /**
173
+ * Arc types that mark a place as a side-effect leaf (a place whose every
174
+ * intra-cluster arc is one of these carries no data-flow ordering).
175
+ * Default `['reset', 'read']`.
176
+ */
177
+ readonly arcs?: readonly ArcType[];
178
+ }
179
+ /**
180
+ * Layout configuration for {@link elkLayout}, surfaced to viewer callers
181
+ * via `MountOptions`.
182
+ */
183
+ interface ElkLayoutConfig {
184
+ /** Per-subnet ELK algorithm. Default `'layered'`. */
185
+ readonly clusterLayout?: ClusterLayout;
186
+ /**
187
+ * Pack side-effect leaf places into a rectpacking sub-block, in clusters
188
+ * where they dominate. `true` (default) enables it with defaults; `false`
189
+ * disables it; an object tunes the threshold / arc types. Only takes
190
+ * effect when `clusterLayout` is `'layered'`.
191
+ */
192
+ readonly leafPacking?: boolean | LeafPackingOptions;
193
+ }
194
+
112
195
  /**
113
196
  * Canonical libpetri Petri-net diagram viewer.
114
197
  *
@@ -229,6 +312,23 @@ interface MountOptions {
229
312
  * cached on the DOT hash; re-mounts on identical DOT skip the pipeline.
230
313
  */
231
314
  readonly layout?: 'elk' | 'graphviz';
315
+ /**
316
+ * Per-subnet ELK algorithm, used within the `'elk'` pipeline. `'layered'`
317
+ * (default) gives each subnet a place→transition rank flow; `'rectpacking'`
318
+ * packs every subnet's nodes as a compact 2-D grid (maximally compact, but
319
+ * intra-cluster flow order is lost). Ignored when {@link layout} is
320
+ * `'graphviz'`.
321
+ */
322
+ readonly clusterLayout?: ClusterLayout;
323
+ /**
324
+ * Within the `'elk'` + `'layered'` pipeline, pack "side-effect leaf"
325
+ * places (places whose only arcs are reset/read) into a grid sub-block,
326
+ * in clusters where they dominate — so a transition with many reset arcs
327
+ * doesn't string its leaves into one wide row. `true` (default) enables
328
+ * it; `false` disables it; an object tunes the threshold / arc types.
329
+ * See {@link LeafPackingOptions}.
330
+ */
331
+ readonly leafPacking?: boolean | LeafPackingOptions;
232
332
  /**
233
333
  * Initial subnet visibility mode. Defaults to `'show'` (clustered view).
234
334
  * Pass `'hide'` to mount with the flat view from the start. The chrome
@@ -244,19 +344,8 @@ interface MountOptions {
244
344
  * and surface cluster overlay controls.
245
345
  *
246
346
  * The container's existing children are removed before the new SVG is
247
- * appended (the same teardown behaviour `renderDotToContainer` used to provide).
248
- *
249
- * Pass `dotSource === null` to adopt an SVG already present as a direct child
250
- * of `container` (pre-rendered SVG path — e.g. the Java javadoc taglet emits
251
- * `dot -Tsvg` at build time). In that mode the viewer never imports the
252
- * runtime DOT renderer, which lets the static IIFE bundle drop `@viz-js/viz`
253
- * entirely.
254
- *
255
- * NOTE: when shipping a pre-rendered SVG (`dotSource === null`), do NOT also
256
- * pass `previousHandle` — the handle's `dispose()` runs before SVG detection
257
- * and would orphan the host. The Java taglet mounts once per page so this is
258
- * a non-issue in practice.
347
+ * appended.
259
348
  */
260
- declare function mount(dotSource: string | null, container: HTMLElement, opts?: MountOptions): Promise<ViewerHandle>;
349
+ declare function mount(dotSource: string, container: HTMLElement, opts?: MountOptions): Promise<ViewerHandle>;
261
350
 
262
- export { type ClusterDescriptor, DEFAULT_PANZOOM_OPTS, type MountOptions, type PanzoomInstance, type PanzoomOptions, type SubnetVisibility, VIEWER_CSS_VARIABLES, type ViewerHandle, colorForPrefix, discoverClusters, mount };
351
+ export { type ClusterDescriptor, type ClusterLayout, DEFAULT_PANZOOM_OPTS, type ElkLayoutConfig, type LeafPackingOptions, type MountOptions, type PanzoomInstance, type PanzoomOptions, type SubnetVisibility, VIEWER_CSS_VARIABLES, type ViewerHandle, colorForPrefix, discoverClusters, mount };
@@ -698,21 +698,16 @@ async function mount(dotSource, container, opts = {}) {
698
698
  if (previousHandle) {
699
699
  previousHandle.dispose();
700
700
  }
701
- let svg;
702
- const existing = container.querySelector(":scope > svg");
703
- if (dotSource == null) {
704
- if (!(existing instanceof SVGSVGElement)) {
705
- throw new Error("mount: no DOT source and no pre-rendered SVG");
706
- }
707
- svg = existing;
708
- } else {
709
- const renderModule = await import("../render-QK57X4TP.js");
710
- const useElk = (opts.layout ?? "elk") === "elk";
711
- const renderedDot = subnetsMode === "hide" ? flattenClusters(dotSource) : dotSource;
712
- svg = useElk ? await renderModule.renderDotToSvgWithElkLayout(renderedDot) : await renderModule.renderDotToSvg(renderedDot);
713
- container.innerHTML = "";
714
- container.appendChild(svg);
715
- }
701
+ const renderModule = await import("../render-UFO543NR.js");
702
+ const useElk = (opts.layout ?? "elk") === "elk";
703
+ const renderedDot = subnetsMode === "hide" ? flattenClusters(dotSource) : dotSource;
704
+ const elkConfig = {
705
+ clusterLayout: opts.clusterLayout,
706
+ leafPacking: opts.leafPacking
707
+ };
708
+ const svg = useElk ? await renderModule.renderDotToSvgWithElkLayout(renderedDot, elkConfig) : await renderModule.renderDotToSvg(renderedDot);
709
+ container.innerHTML = "";
710
+ container.appendChild(svg);
716
711
  container.classList.add("libpetri-viewer");
717
712
  const panzoomInstance = attachPanzoom(svg, opts.panzoom);
718
713
  const clusters = discoverClusters(svg);
@@ -757,10 +752,6 @@ async function mount(dotSource, container, opts = {}) {
757
752
  subnetsBtn.className = "diagram-btn btn-subnets";
758
753
  subnetsBtn.title = subnetsMode === "show" ? "Hide subnet groupings" : "Show subnet groupings";
759
754
  subnetsBtn.textContent = subnetsMode === "show" ? "Flat view" : "Subnets view";
760
- if (dotSource == null) {
761
- subnetsBtn.disabled = true;
762
- subnetsBtn.title = "Subnet toggle unavailable for pre-rendered SVG";
763
- }
764
755
  subnetsBtn.addEventListener("click", () => {
765
756
  void handle.toggleSubnets();
766
757
  });
@@ -801,7 +792,6 @@ async function mount(dotSource, container, opts = {}) {
801
792
  return subnetsMode;
802
793
  },
803
794
  setSubnets(mode) {
804
- if (dotSource == null) return Promise.resolve(handle);
805
795
  if (mode === subnetsMode) return Promise.resolve(handle);
806
796
  return mount(dotSource, container, {
807
797
  ...opts,