libpetri 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/viewer/pan-zoom.ts","../../src/viewer/cluster-overlay.ts","../../src/viewer/styles.ts","../../src/viewer/index.ts"],"sourcesContent":["/**\n * Pan/zoom wrapper for the canonical libpetri viewer.\n *\n * Delegates to the `panzoom` library (no hand-rolled wheel handlers — we\n * intentionally drop the IIFE/javadoc version's bespoke math in favour of\n * a single battle-tested implementation). Defaults match the old debug-ui\n * and dev-preview values so the canonical viewer feels identical to what\n * users had before.\n *\n * @module viewer/pan-zoom\n */\n\nimport panzoom from 'panzoom';\n\nexport type PanzoomInstance = ReturnType<typeof panzoom>;\nexport type PanzoomOptions = Parameters<typeof panzoom>[1];\n\n/**\n * Default panzoom configuration shared across all viewer surfaces.\n *\n * - `maxZoom: 1000` — effectively unlimited; lets users dive into ~150-node\n * diagrams without hitting an artificial ceiling.\n * - `minZoom: 0.02` — allows fitting very large nets in a small viewport.\n * - `smoothScroll: false` — sharper interactive feel.\n * - `zoomDoubleClickSpeed: 1` — single-click double-zoom toggle.\n */\nexport const DEFAULT_PANZOOM_OPTS = {\n smoothScroll: false,\n zoomDoubleClickSpeed: 1,\n maxZoom: 1000,\n minZoom: 0.02,\n} as const satisfies PanzoomOptions;\n\n/**\n * Attach panzoom to a freshly rendered SVG element. The caller is\n * responsible for disposing any previous instance (or pass it as\n * `previous` to dispose it here).\n */\nexport function attachPanzoom(\n svg: SVGSVGElement,\n options?: PanzoomOptions,\n previous?: PanzoomInstance | null,\n): PanzoomInstance {\n if (previous) {\n try {\n previous.dispose();\n } catch {\n // Swallow disposal errors — the previous instance may already be\n // detached or the panzoom internals may have raised on a missing\n // root node. Either way we just want a fresh instance.\n }\n }\n const opts = { ...DEFAULT_PANZOOM_OPTS, ...options };\n return panzoom(svg, opts);\n}\n","/**\n * Cluster overlay for the canonical libpetri viewer.\n *\n * Post-processes a rendered SVG to surface subnet structure visually:\n * 1. Discover `<g class=\"cluster\">` subgraphs, derive each prefix from the\n * contained `<title>` (Graphviz emits `cluster_<sanitizedPrefix>`).\n * 2. Tag every contained `<g class=\"node\">` with `data-instance=\"<prefix>\"`\n * so filtering can target it via attribute selectors.\n * 3. Assign each cluster a deterministic HSL colour (FNV-1a hash + golden\n * ratio hue increment) and paint its border.\n * 4. Provide collapse/expand and \"show only <prefix>\" filtering.\n *\n * Unlike the previous debug-ui cluster-overlay, this module is instance-\n * based: every `mount()` call gets its own `ClusterOverlay` so multiple\n * viewers on the same page don't share collapse/isolate state.\n *\n * @module viewer/cluster-overlay\n */\n\n/** A discovered cluster with its DOM group, node count, and palette colour. */\nexport interface ClusterDescriptor {\n readonly prefix: string;\n readonly group: SVGGElement;\n readonly nodeCount: number;\n readonly color: string;\n}\n\n/**\n * Augmentation slots stashed on the cluster `<g>` element across collapse\n * cycles. We don't subclass SVGElement; we tag arbitrary properties on the\n * DOM node, matching the doclet's `petrinet-diagrams.js` so the cross-surface\n * mental model is identical.\n *\n * `_petriHiddenSiblings` holds the sibling `<g class=\"node\">` and\n * `<g class=\"edge\">` elements (sibling to the cluster, not its DOM children\n * — Graphviz never nests nodes inside cluster groups) tagged as belonging\n * to this prefix at the moment of collapse. They retain their place in the\n * SVG tree; we hide them via the `petri-collapsed-inside` class so the\n * underlying layout is preserved for a cheap re-expand.\n */\ninterface ClusterAugment {\n _petriHiddenSiblings?: Element[];\n _petriCollapsedBadge?: SVGTextElement | null;\n}\ntype AugmentedGroup = SVGGElement & ClusterAugment;\n\n/**\n * Walk the SVG, find every `<g class=\"cluster\">`, derive the prefix from\n * each cluster's `<title>` (`cluster_<sanitized>`), and tag every cluster-\n * interior `<g class=\"node\">` with `data-instance=\"<prefix>\"`. Returns one\n * descriptor per cluster.\n *\n * Graphviz emits cluster nodes as SIBLINGS of the cluster `<g>`, not as\n * children — `<g class=\"cluster\">` only carries title, polygon border, and\n * the cluster's text label. To recover cluster membership we match each\n * node's `<title>` (the original graph id) against libpetri's prefixing\n * convention: interior places are emitted as `p_<prefix>_<name>`,\n * transitions as `t_<prefix>_<name>`, and junctions as `j_<prefix>_<name>`.\n *\n * Does not split nested-cluster prefixes (`outer_inner` could mean either\n * `outer/inner` or a flat `outer_inner` prefix — Graphviz sanitisation is\n * lossy). We treat every cluster as its own top-level entry, matching the\n * doclet's pragmatism.\n */\nexport function discoverClusters(svg: SVGSVGElement): Map<string, ClusterDescriptor> {\n const out = new Map<string, ClusterDescriptor>();\n const clusterGroups = svg.querySelectorAll<SVGGElement>('g.cluster');\n if (!clusterGroups.length) return out;\n\n const allNodes = Array.from(svg.querySelectorAll<SVGGElement>('g.node'));\n\n for (const g of Array.from(clusterGroups)) {\n const titleEl = directChild(g, 'title');\n if (!titleEl) continue;\n const raw = titleEl.textContent ?? '';\n if (!raw.startsWith('cluster_')) continue;\n const prefix = raw.slice('cluster_'.length);\n if (out.has(prefix)) continue;\n\n let nodeCount = 0;\n for (const node of allNodes) {\n // Don't re-tag a node already claimed by an inner (more specific)\n // cluster — outer clusters can still match by string-prefix at filter\n // time, but the data-instance is the leaf.\n if (node.getAttribute('data-instance')) continue;\n const nodeTitle = directChild(node, 'title');\n if (!nodeTitle) continue;\n const id = (nodeTitle.textContent ?? '').trim();\n if (nodeBelongsToPrefix(id, prefix)) {\n node.setAttribute('data-instance', prefix);\n nodeCount++;\n }\n }\n\n out.set(prefix, {\n prefix,\n group: g,\n nodeCount,\n color: colorForPrefix(prefix),\n });\n }\n return out;\n}\n\n/**\n * Does this node id belong to the cluster with the given prefix?\n *\n * libpetri's DOT renderer prefixes interior names: places as `p_<prefix>_…`,\n * transitions as `t_<prefix>_…`, junctions as `j_<prefix>_…`. Generic DOT\n * may also use slash-separated names (`<prefix>/…`). We accept both shapes\n * so non-libpetri DOT graphs and bare-Graphviz fixtures still match.\n */\nfunction nodeBelongsToPrefix(nodeId: string, prefix: string): boolean {\n return (\n nodeId === prefix ||\n nodeId.startsWith(`${prefix}/`) ||\n nodeId.startsWith(`p_${prefix}_`) ||\n nodeId.startsWith(`t_${prefix}_`) ||\n nodeId.startsWith(`j_${prefix}_`)\n );\n}\n\n/**\n * Deterministic HSL palette. FNV-1a-ish hash of the prefix, multiplied by\n * the golden-ratio fraction (0.61803...) to spread adjacent prefixes\n * visually. Saturation/lightness are fixed (60% / 45%) so the palette\n * stays readable on both the doc background and the dark debug UI.\n */\nexport function colorForPrefix(prefix: string): string {\n let h = 2166136261;\n for (let i = 0; i < prefix.length; i++) {\n h ^= prefix.charCodeAt(i);\n h = (h * 16777619) >>> 0;\n }\n let hue = Math.floor(((h / 4294967296) * 360 + (h % 360) * 0.61803) % 360);\n if (hue < 0) hue += 360;\n return `hsl(${hue}, 60%, 45%)`;\n}\n\n/** Paint the cluster border with the deterministic prefix colour. */\nexport function paintClusterBorders(clusters: Map<string, ClusterDescriptor>): void {\n for (const c of clusters.values()) {\n const border = directChild(c.group, 'polygon') ?? directChild(c.group, 'path');\n if (border) {\n border.setAttribute('stroke', c.color);\n // Slightly thicker than the old 2.0 — the user asked for clusters to\n // be visually obvious. Backed off from 3 to keep it readable at the\n // tiniest zoom.\n border.setAttribute('stroke-width', '2.2');\n }\n }\n}\n\n/**\n * Collapse / expand a cluster.\n *\n * Graphviz emits nodes and edges as siblings of the cluster `<g>`, not as\n * its DOM children. So \"collapsing\" cannot work by detaching the cluster's\n * children — that only removes title/border/label. Instead, we identify the\n * cluster-interior nodes (`data-instance=\"<prefix>\"`) and edges with both\n * endpoints inside the cluster, then add the `petri-collapsed-inside` class\n * so the canonical viewer.css hides them. Stash the list on the cluster\n * group so re-expand is a constant-time class removal.\n *\n * Edges that cross the cluster boundary (one endpoint inside, one outside)\n * are intentionally left visible — they show the cluster's external wiring\n * even when the interior is hidden.\n *\n * A small `<text>` badge with the actual interior node count is appended\n * inside the cluster `<g>` (which keeps its layout box).\n */\nexport function setClusterCollapsed(cluster: ClusterDescriptor, collapsed: boolean): void {\n const g = cluster.group as AugmentedGroup;\n const isCollapsed = g.classList.contains('cluster-collapsed');\n const root = g.ownerSVGElement;\n if (!root) return;\n\n if (collapsed && !isCollapsed) {\n g.classList.add('cluster-collapsed');\n const hidden: Element[] = [];\n\n // Hide interior nodes (sibling elements tagged with this prefix).\n root\n .querySelectorAll<SVGGElement>(`g.node[data-instance=\"${cssEscape(cluster.prefix)}\"]`)\n .forEach((node) => {\n node.classList.add('petri-collapsed-inside');\n hidden.push(node);\n });\n\n // Hide interior-interior edges. Edges' titles are `from->to`; we hide\n // an edge only when BOTH endpoints belong to this cluster.\n root.querySelectorAll<SVGGElement>('g.edge').forEach((edge) => {\n const titleEl = directChild(edge, 'title');\n if (!titleEl) return;\n const t = (titleEl.textContent ?? '').trim();\n const arrow = t.indexOf('->');\n if (arrow < 0) return;\n const from = t.slice(0, arrow);\n const to = t.slice(arrow + 2);\n if (nodeBelongsToPrefix(from, cluster.prefix) && nodeBelongsToPrefix(to, cluster.prefix)) {\n edge.classList.add('petri-collapsed-inside');\n hidden.push(edge);\n }\n });\n\n g._petriHiddenSiblings = hidden;\n\n if (!g._petriCollapsedBadge) {\n const label = directChild(g, 'text') as SVGTextElement | null;\n if (label) {\n const badge = document.createElementNS('http://www.w3.org/2000/svg', 'text');\n badge.setAttribute('x', label.getAttribute('x') ?? '0');\n const labelY = parseFloat(label.getAttribute('y') ?? '0');\n badge.setAttribute('y', String(labelY + 16));\n badge.setAttribute('text-anchor', label.getAttribute('text-anchor') ?? 'middle');\n badge.setAttribute('font-size', '11');\n badge.setAttribute('fill', '#9ca3af');\n badge.setAttribute('class', 'cluster-collapsed-badge');\n badge.textContent = `(${cluster.nodeCount} internal node${cluster.nodeCount === 1 ? '' : 's'})`;\n g.appendChild(badge);\n g._petriCollapsedBadge = badge;\n }\n }\n } else if (!collapsed && isCollapsed) {\n g.classList.remove('cluster-collapsed');\n if (g._petriHiddenSiblings) {\n g._petriHiddenSiblings.forEach((el) => el.classList.remove('petri-collapsed-inside'));\n g._petriHiddenSiblings = undefined;\n }\n if (g._petriCollapsedBadge && g._petriCollapsedBadge.parentNode === g) {\n g.removeChild(g._petriCollapsedBadge);\n g._petriCollapsedBadge = null;\n }\n }\n}\n\n/**\n * Toggle the \"show only <prefix>\" filter. Passing `null` (or the same\n * prefix already active) clears the filter. Mirrors the doclet's\n * `applyFilter`, including the edge-dimming heuristic that parses each\n * edge's `<title>` (`from->to`) and keeps an edge visible when either\n * endpoint matches the active prefix.\n */\nexport function applyFilter(svg: SVGSVGElement, prefix: string | null): void {\n if (prefix == null) {\n svg.removeAttribute('data-active-filter');\n svg.classList.remove('has-active-filter');\n svg.querySelectorAll('g.node, g.edge').forEach((el) => el.classList.remove('petri-dimmed'));\n return;\n }\n svg.setAttribute('data-active-filter', prefix);\n svg.classList.add('has-active-filter');\n svg.querySelectorAll('g.node').forEach((node) => {\n const di = node.getAttribute('data-instance');\n const match =\n !!di &&\n (di === prefix || di.indexOf(prefix + '_') === 0 || di.indexOf(prefix + '/') === 0);\n if (match) node.classList.remove('petri-dimmed');\n else node.classList.add('petri-dimmed');\n });\n svg.querySelectorAll('g.edge').forEach((edge) => {\n const titleEl = directChild(edge, 'title');\n if (!titleEl) return;\n const titleText = titleEl.textContent ?? '';\n const arrow = titleText.indexOf('->');\n if (arrow < 0) {\n edge.classList.remove('petri-dimmed');\n return;\n }\n const from = titleText.slice(0, arrow);\n const to = titleText.slice(arrow + 2);\n const matches = (name: string): boolean =>\n name.indexOf(prefix + '/') >= 0 || name.indexOf(prefix + '_') >= 0 || name === prefix;\n if (matches(from) || matches(to)) edge.classList.remove('petri-dimmed');\n else edge.classList.add('petri-dimmed');\n });\n}\n\n/**\n * Returns true when the place/transition node identified by `graphId` is\n * currently inside a collapsed cluster (i.e. detached from the live SVG).\n * Live consumers (e.g. highlight code in debug-ui) use this to skip lookups\n * that would otherwise hit a stale cache entry pointing at a detached node.\n */\nexport function isInsideCollapsedCluster(\n svg: SVGSVGElement | null,\n clusters: Map<string, ClusterDescriptor>,\n collapsedPrefixes: ReadonlySet<string>,\n graphId: string,\n): boolean {\n if (collapsedPrefixes.size === 0) return false;\n if (!svg) return false;\n for (const prefix of collapsedPrefixes) {\n if (!clusters.has(prefix)) continue;\n if (nodeBelongsToPrefix(graphId, prefix)) return true;\n }\n return false;\n}\n\n/** Minimal CSS.escape polyfill — happy-dom may not implement CSS globals. */\nfunction cssEscape(s: string): string {\n if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') return CSS.escape(s);\n return s.replace(/[^a-zA-Z0-9_-]/g, (c) => `\\\\${c}`);\n}\n\n/**\n * `:scope > <tag>` polyfill — happy-dom doesn't reliably resolve `:scope`\n * against namespaced SVG elements (returns null even when the child exists).\n * Walk `children` directly instead, matching by lower-cased tag name.\n */\nfunction directChild(parent: Element, tagName: string): Element | null {\n const lc = tagName.toLowerCase();\n for (const child of Array.from(parent.children)) {\n if (child.tagName.toLowerCase() === lc) return child;\n }\n return null;\n}\n","/**\n * CSS custom properties exposed by the canonical viewer stylesheet.\n *\n * Each property has a default in `viewer.css`. Consumers can override any of\n * them at any scope (the viewer container, the document root, etc.) to\n * theme the viewer without touching the canonical CSS.\n *\n * Names are listed here as a reference + so the IIFE bundle can expose them\n * on `window.LibpetriViewer.cssVariables`. The list is informational — the\n * stylesheet is still the source of truth.\n *\n * @module viewer/styles\n */\n\n/** CSS custom property names that the canonical stylesheet honours. */\nexport const VIEWER_CSS_VARIABLES = [\n '--lpv-bg',\n '--lpv-header-bg',\n '--lpv-border',\n '--lpv-text',\n '--lpv-muted',\n '--lpv-cluster-bg',\n '--lpv-cluster-bg-collapsed',\n '--lpv-cluster-stroke-width',\n '--lpv-cluster-stroke-dash',\n '--lpv-cluster-fill-tint',\n '--lpv-dim-opacity',\n '--lpv-active-filter-outline',\n '--lpv-legend-bg',\n '--lpv-chip-bg',\n '--lpv-chip-active-bg',\n] as const;\n\nexport type ViewerCssVariable = (typeof VIEWER_CSS_VARIABLES)[number];\n","/**\n * Canonical libpetri Petri-net diagram viewer.\n *\n * Single source of truth for DOT → SVG rendering + cluster overlay across\n * all four surfaces:\n * - debug-ui (live debugger)\n * - dev-preview (Vite app)\n * - Javadoc taglet (Java doclet)\n * - Rustdoc embed (libpetri-docgen)\n * - TypeDoc / typescript doclet\n *\n * Build outputs:\n * - ESM: `dist/viewer/index.js` — consumed by Vite/Webpack/Node bundlers.\n * - IIFE: `dist/viewer/viewer.iife.js` — self-contained, exposes\n * `window.LibpetriViewer = { mount, ... }`. Used by the doc taglets\n * that ship pre-rendered HTML pages without a bundler.\n * - CSS: `dist/viewer/viewer.css` — canonical stylesheet (copied from\n * `src/viewer/resources/viewer.css`).\n *\n * Edits to viewer behaviour MUST happen here. The three resource directories\n * under `java/`, `rust/`, and `typescript/src/doclet/` are **build outputs**\n * populated by `scripts/build-viewer.sh` and must not be hand-edited.\n *\n * @module viewer\n */\n\nimport {\n attachPanzoom,\n DEFAULT_PANZOOM_OPTS,\n type PanzoomInstance,\n type PanzoomOptions,\n} from './pan-zoom.js';\nimport {\n applyFilter as overlayApplyFilter,\n colorForPrefix,\n discoverClusters,\n isInsideCollapsedCluster as overlayIsInsideCollapsedCluster,\n paintClusterBorders,\n setClusterCollapsed,\n type ClusterDescriptor,\n} from './cluster-overlay.js';\nimport { VIEWER_CSS_VARIABLES } from './styles.js';\n\nexport {\n colorForPrefix,\n discoverClusters,\n VIEWER_CSS_VARIABLES,\n DEFAULT_PANZOOM_OPTS,\n};\nexport type { ClusterDescriptor, PanzoomInstance, PanzoomOptions };\n\n/** Handle returned by {@link mount}. */\nexport interface ViewerHandle {\n /** The rendered root `<svg>` element. */\n readonly svg: SVGSVGElement;\n /** The panzoom instance attached to the SVG. */\n readonly panzoom: PanzoomInstance;\n /** Map of cluster prefix → descriptor for the current SVG. */\n readonly clusters: ReadonlyMap<string, ClusterDescriptor>;\n /** Prefixes currently marked collapsed on this handle. */\n readonly collapsedPrefixes: ReadonlySet<string>;\n /** Currently active \"show only <prefix>\" filter, or null. */\n readonly activeFilter: string | null;\n /** Collapse a single cluster by prefix. No-op if unknown. */\n collapse(prefix: string): void;\n /** Expand a single cluster by prefix. No-op if unknown. */\n expand(prefix: string): void;\n /** Collapse every discovered cluster. */\n collapseAll(): void;\n /** Expand every collapsed cluster. */\n expandAll(): void;\n /** Apply the \"show only <prefix>\" filter; pass `null` to clear. */\n filter(prefix: string | null): void;\n /** Reset pan/zoom to the identity transform. */\n resetZoom(): void;\n /** Scale and translate so the full diagram fits the host viewport. */\n fit(): void;\n /** Reports whether `graphId` is inside a currently-collapsed cluster. */\n isInsideCollapsedCluster(graphId: string): boolean;\n /** Tear down: dispose panzoom and detach listeners. */\n dispose(): void;\n}\n\n/** Options accepted by {@link mount}. */\nexport interface MountOptions {\n /**\n * Previous handle to dispose before mounting. Pass the handle returned\n * by a prior `mount()` call so callers don't need to track it separately.\n * Cluster collapse / filter state from `previousHandle` is preserved\n * across the re-render so the user's intent survives a live re-draw.\n */\n readonly previousHandle?: ViewerHandle | null;\n /** Per-call panzoom overrides; merged on top of {@link DEFAULT_PANZOOM_OPTS}. */\n readonly panzoom?: PanzoomOptions;\n /** Fired after a cluster's collapsed state changes via the handle API. */\n readonly onClusterCollapse?: (prefix: string, collapsed: boolean) => void;\n /** Fired after `filter()` runs, with the new filter prefix or null. */\n readonly onClusterFilter?: (prefix: string | null) => void;\n /** When true, append a legend sidebar + filter chip strip inside `container`. */\n readonly chrome?: boolean;\n}\n\n/**\n * Render a DOT source, mount the resulting SVG into `container`, wire pan/zoom,\n * and surface cluster overlay controls.\n *\n * The container's existing children are removed before the new SVG is\n * appended (the same teardown behaviour `renderDotToContainer` used to provide).\n *\n * Pass `dotSource === null` to adopt an SVG already present as a direct child\n * of `container` (pre-rendered SVG path — e.g. the Java javadoc taglet emits\n * `dot -Tsvg` at build time). In that mode the viewer never imports the\n * runtime DOT renderer, which lets the static IIFE bundle drop `@viz-js/viz`\n * entirely.\n *\n * NOTE: when shipping a pre-rendered SVG (`dotSource === null`), do NOT also\n * pass `previousHandle` — the handle's `dispose()` runs before SVG detection\n * and would orphan the host. The Java taglet mounts once per page so this is\n * a non-issue in practice.\n */\nexport async function mount(\n dotSource: string | null,\n container: HTMLElement,\n opts: MountOptions = {},\n): Promise<ViewerHandle> {\n const previousHandle = opts.previousHandle ?? null;\n const preservedCollapsed = previousHandle\n ? new Set(previousHandle.collapsedPrefixes)\n : new Set<string>();\n const preservedFilter = previousHandle?.activeFilter ?? null;\n\n if (previousHandle) {\n previousHandle.dispose();\n }\n\n // Gate on dotSource (not on \"SVG present\") — debug-ui re-renders by\n // passing DOT with `previousHandle` and the leftover SVG must NOT be\n // misread as pre-rendered. Java passes literal `null` for the\n // pre-rendered path.\n let svg: SVGSVGElement;\n const existing = container.querySelector(':scope > svg');\n if (dotSource == null) {\n if (!(existing instanceof SVGSVGElement)) {\n throw new Error('mount: no DOT source and no pre-rendered SVG');\n }\n svg = existing;\n } else {\n // Dynamic import so the static IIFE entry can alias './render.js' to a\n // throwing stub and drop the @viz-js/viz dependency.\n const { renderDotToSvg } = await import('./render.js');\n svg = await renderDotToSvg(dotSource);\n container.innerHTML = '';\n container.appendChild(svg);\n }\n // Tag the host so the canonical CSS (which scopes to .libpetri-viewer)\n // applies even when the consumer didn't wrap us in a .petrinet-diagram.\n container.classList.add('libpetri-viewer');\n\n const panzoomInstance = attachPanzoom(svg, opts.panzoom);\n const clusters = discoverClusters(svg);\n paintClusterBorders(clusters);\n\n const collapsedPrefixes = new Set<string>();\n let activeFilter: string | null = null;\n let disposed = false;\n\n // Chrome elements (created lazily; only when chrome:true is requested).\n // Re-rendering through the same handle does not duplicate them.\n let chromeRoot: HTMLElement | null = null;\n\n function ensureChrome(): void {\n if (!opts.chrome) return;\n if (chromeRoot && chromeRoot.parentNode === container) return;\n chromeRoot = document.createElement('div');\n chromeRoot.className = 'libpetri-viewer-chrome';\n chromeRoot.style.position = 'absolute';\n chromeRoot.style.inset = '0';\n chromeRoot.style.pointerEvents = 'none';\n\n const legend = document.createElement('div');\n legend.className = 'diagram-legend';\n legend.style.pointerEvents = 'auto';\n legend.innerHTML = '<div class=\"legend-title\">Clusters</div>';\n chromeRoot.appendChild(legend);\n\n const strip = document.createElement('div');\n strip.className = 'diagram-filter-strip';\n strip.style.pointerEvents = 'auto';\n strip.innerHTML = '<span class=\"filter-strip-label\">Show only:</span>';\n chromeRoot.appendChild(strip);\n\n const controls = document.createElement('div');\n controls.className = 'diagram-controls';\n controls.style.pointerEvents = 'auto';\n const resetBtn = document.createElement('button');\n resetBtn.type = 'button';\n resetBtn.className = 'diagram-btn btn-reset';\n resetBtn.title = 'Reset view';\n resetBtn.textContent = 'Reset';\n // Reset = return to the meaningful default (fit-to-host), not the\n // mathematical identity transform. `resetZoom()` stays on the handle\n // API for callers that want raw identity.\n resetBtn.addEventListener('click', () => handle.fit());\n const fsBtn = document.createElement('button');\n fsBtn.type = 'button';\n fsBtn.className = 'diagram-btn btn-fullscreen';\n fsBtn.title = 'Toggle fullscreen';\n fsBtn.textContent = 'Fullscreen';\n fsBtn.addEventListener('click', () => toggleFullscreen(container, fsBtn));\n controls.appendChild(resetBtn);\n controls.appendChild(fsBtn);\n chromeRoot.appendChild(controls);\n\n // Position parent must be relative for the absolute chrome to anchor.\n if (getComputedStyle(container).position === 'static') {\n container.style.position = 'relative';\n }\n container.appendChild(chromeRoot);\n\n renderLegend(legend);\n renderFilterStrip(strip);\n }\n\n // In-page lightbox fullscreen: toggle a CSS class on the host and\n // re-fit. We deliberately don't invoke the native Fullscreen API —\n // users want the diagram to expand inside the page, not yank the\n // browser into OS-level fullscreen.\n function toggleFullscreen(host: HTMLElement, btn: HTMLButtonElement): void {\n const isFs = host.classList.toggle('diagram-fullscreen');\n btn.textContent = isFs ? 'Exit fullscreen' : 'Fullscreen';\n // Re-fit on both enter and exit — the available area changed, so the\n // previous transform is no longer the right size. Defer a frame so\n // flex layout settles before getBBox + clientWidth/Height reads.\n requestAnimationFrame(() => handle.fit());\n }\n\n function renderLegend(legend: HTMLElement): void {\n legend.innerHTML = '<div class=\"legend-title\">Clusters</div>';\n for (const cluster of clusters.values()) {\n const row = document.createElement('div');\n row.className = 'legend-row';\n row.innerHTML =\n '<span class=\"legend-ribbon\" style=\"background:' + cluster.color + '\"></span>' +\n '<span class=\"legend-label\"></span>' +\n '<span class=\"legend-count\"></span>';\n (row.querySelector('.legend-label') as HTMLElement).textContent = cluster.prefix;\n (row.querySelector('.legend-count') as HTMLElement).textContent = String(cluster.nodeCount);\n row.addEventListener('click', () => {\n if (collapsedPrefixes.has(cluster.prefix)) {\n handle.expand(cluster.prefix);\n } else {\n handle.collapse(cluster.prefix);\n }\n });\n legend.appendChild(row);\n }\n }\n\n function renderFilterStrip(strip: HTMLElement): void {\n strip.innerHTML = '<span class=\"filter-strip-label\">Show only:</span>';\n const allChip = document.createElement('button');\n allChip.type = 'button';\n allChip.className = 'filter-chip filter-chip-all filter-chip-active';\n allChip.textContent = 'all';\n allChip.addEventListener('click', () => {\n handle.filter(null);\n });\n strip.appendChild(allChip);\n for (const cluster of clusters.values()) {\n const chip = document.createElement('button');\n chip.type = 'button';\n chip.className = 'filter-chip';\n chip.style.borderColor = cluster.color;\n chip.dataset.prefix = cluster.prefix;\n chip.innerHTML =\n '<span class=\"chip-dot\" style=\"background:' + cluster.color + '\"></span>';\n chip.append(document.createTextNode(cluster.prefix));\n chip.addEventListener('click', () => {\n if (activeFilter === cluster.prefix) {\n handle.filter(null);\n } else {\n handle.filter(cluster.prefix);\n }\n });\n strip.appendChild(chip);\n }\n }\n\n function refreshChromeActiveStates(): void {\n if (!chromeRoot) return;\n const strip = chromeRoot.querySelector('.diagram-filter-strip');\n if (!strip) return;\n strip.querySelectorAll<HTMLElement>('.filter-chip').forEach((chip) => {\n chip.classList.remove('filter-chip-active');\n });\n if (activeFilter === null) {\n strip\n .querySelector<HTMLElement>('.filter-chip-all')\n ?.classList.add('filter-chip-active');\n } else {\n strip\n .querySelector<HTMLElement>(`.filter-chip[data-prefix=\"${activeFilter}\"]`)\n ?.classList.add('filter-chip-active');\n }\n }\n\n const handle: ViewerHandle = {\n svg,\n panzoom: panzoomInstance,\n clusters,\n get collapsedPrefixes() {\n return collapsedPrefixes;\n },\n get activeFilter() {\n return activeFilter;\n },\n collapse(prefix: string): void {\n if (disposed) return;\n const cluster = clusters.get(prefix);\n if (!cluster) return;\n if (!collapsedPrefixes.has(prefix)) {\n setClusterCollapsed(cluster, true);\n collapsedPrefixes.add(prefix);\n opts.onClusterCollapse?.(prefix, true);\n }\n },\n expand(prefix: string): void {\n if (disposed) return;\n const cluster = clusters.get(prefix);\n if (!cluster) return;\n if (collapsedPrefixes.has(prefix)) {\n setClusterCollapsed(cluster, false);\n collapsedPrefixes.delete(prefix);\n opts.onClusterCollapse?.(prefix, false);\n }\n },\n collapseAll(): void {\n if (disposed) return;\n for (const cluster of clusters.values()) {\n if (!collapsedPrefixes.has(cluster.prefix)) {\n setClusterCollapsed(cluster, true);\n collapsedPrefixes.add(cluster.prefix);\n opts.onClusterCollapse?.(cluster.prefix, true);\n }\n }\n },\n expandAll(): void {\n if (disposed) return;\n for (const prefix of Array.from(collapsedPrefixes)) {\n const cluster = clusters.get(prefix);\n if (!cluster) continue;\n setClusterCollapsed(cluster, false);\n collapsedPrefixes.delete(prefix);\n opts.onClusterCollapse?.(prefix, false);\n }\n },\n filter(prefix: string | null): void {\n if (disposed) return;\n activeFilter = prefix;\n overlayApplyFilter(svg, prefix);\n opts.onClusterFilter?.(prefix);\n refreshChromeActiveStates();\n },\n resetZoom(): void {\n if (disposed) return;\n try {\n panzoomInstance.moveTo(0, 0);\n panzoomInstance.zoomAbs(0, 0, 1);\n } catch {\n // panzoom internals can throw if the element was detached; ignore.\n }\n },\n fit(): void {\n if (disposed) return;\n try {\n // With the host CSS-sized and the SVG forced to width/height: 100%\n // (see .petrinet-diagram-viewer rules in viewer.css), the browser\n // auto-fits viewBox content via preserveAspectRatio=\"xMidYMid meet\".\n // \"Fit\" is therefore the panzoom identity transform — no math needed.\n panzoomInstance.zoomAbs(0, 0, 1);\n panzoomInstance.moveTo(0, 0);\n } catch {\n // panzoom internals can throw if the element was detached; ignore.\n }\n },\n isInsideCollapsedCluster(graphId: string): boolean {\n return overlayIsInsideCollapsedCluster(svg, clusters, collapsedPrefixes, graphId);\n },\n dispose(): void {\n if (disposed) return;\n disposed = true;\n try {\n panzoomInstance.dispose();\n } catch {\n // ignore\n }\n if (chromeRoot && chromeRoot.parentNode === container) {\n container.removeChild(chromeRoot);\n }\n chromeRoot = null;\n },\n };\n\n // Re-apply preserved collapse/filter state from the previous handle so\n // a live re-render (e.g. on marking-snapshot) keeps the user's selection.\n for (const prefix of preservedCollapsed) {\n handle.collapse(prefix);\n }\n if (preservedFilter !== null) {\n handle.filter(preservedFilter);\n }\n\n ensureChrome();\n\n // Default state: fit-to-host. Huge diagrams shouldn't mount at 1:1\n // (the user only sees the top-left corner). Deferred a frame so the\n // SVG has been laid out before getBBox + clientWidth/Height reads.\n requestAnimationFrame(() => handle.fit());\n\n return handle;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/viewer/pan-zoom.ts","../../src/viewer/cluster-overlay.ts","../../src/viewer/c0-annotations.ts","../../src/viewer/chrome/sidebar.ts","../../src/viewer/highlight.ts","../../src/viewer/replica-tagging.ts","../../src/viewer/visibility.ts","../../src/viewer/styles.ts","../../src/viewer/index.ts"],"sourcesContent":["/**\n * Pan/zoom wrapper for the canonical libpetri viewer.\n *\n * Delegates to the `panzoom` library (no hand-rolled wheel handlers — we\n * intentionally drop the IIFE/javadoc version's bespoke math in favour of\n * a single battle-tested implementation). Defaults match the old debug-ui\n * and dev-preview values so the canonical viewer feels identical to what\n * users had before.\n *\n * @module viewer/pan-zoom\n */\n\nimport panzoom from 'panzoom';\n\nexport type PanzoomInstance = ReturnType<typeof panzoom>;\nexport type PanzoomOptions = Parameters<typeof panzoom>[1];\n\n/**\n * Default panzoom configuration shared across all viewer surfaces.\n *\n * - `maxZoom: 1000` — effectively unlimited; lets users dive into ~150-node\n * diagrams without hitting an artificial ceiling.\n * - `minZoom: 0.02` — allows fitting very large nets in a small viewport.\n * - `smoothScroll: false` — sharper interactive feel.\n * - `zoomDoubleClickSpeed: 1` — single-click double-zoom toggle.\n */\nexport const DEFAULT_PANZOOM_OPTS = {\n smoothScroll: false,\n zoomDoubleClickSpeed: 1,\n maxZoom: 1000,\n minZoom: 0.02,\n} as const satisfies PanzoomOptions;\n\n/**\n * Attach panzoom to a freshly rendered SVG element. The caller is\n * responsible for disposing any previous instance (or pass it as\n * `previous` to dispose it here).\n */\nexport function attachPanzoom(\n svg: SVGSVGElement,\n options?: PanzoomOptions,\n previous?: PanzoomInstance | null,\n): PanzoomInstance {\n if (previous) {\n try {\n previous.dispose();\n } catch {\n // Swallow disposal errors — the previous instance may already be\n // detached or the panzoom internals may have raised on a missing\n // root node. Either way we just want a fresh instance.\n }\n }\n const opts = { ...DEFAULT_PANZOOM_OPTS, ...options };\n return panzoom(svg, opts);\n}\n","/**\n * Cluster overlay for the canonical libpetri viewer.\n *\n * Post-processes a rendered SVG to surface subnet structure visually:\n * 1. Discover `<g class=\"cluster\">` subgraphs, derive each prefix from the\n * contained `<title>` (Graphviz emits `cluster_<sanitizedPrefix>`).\n * 2. Tag every contained `<g class=\"node\">` with `data-instance=\"<prefix>\"`\n * so filtering can target it via attribute selectors.\n * 3. Assign each cluster a deterministic HSL colour (FNV-1a hash + golden\n * ratio hue increment) and paint its border.\n * 4. Provide collapse/expand and \"show only <prefix>\" filtering.\n *\n * Unlike the previous debug-ui cluster-overlay, this module is instance-\n * based: every `mount()` call gets its own `ClusterOverlay` so multiple\n * viewers on the same page don't share collapse/isolate state.\n *\n * @module viewer/cluster-overlay\n */\n\n/** A discovered cluster with its DOM group, node count, and palette colour. */\nexport interface ClusterDescriptor {\n readonly prefix: string;\n readonly group: SVGGElement;\n readonly nodeCount: number;\n readonly color: string;\n}\n\n/**\n * Augmentation slots stashed on the cluster `<g>` element across collapse\n * cycles. We don't subclass SVGElement; we tag arbitrary properties on the\n * DOM node, matching the doclet's `petrinet-diagrams.js` so the cross-surface\n * mental model is identical.\n *\n * `_petriHiddenSiblings` holds the sibling `<g class=\"node\">` and\n * `<g class=\"edge\">` elements (sibling to the cluster, not its DOM children\n * — Graphviz never nests nodes inside cluster groups) tagged as belonging\n * to this prefix at the moment of collapse. They retain their place in the\n * SVG tree; we hide them via the `petri-collapsed-inside` class so the\n * underlying layout is preserved for a cheap re-expand.\n */\ninterface ClusterAugment {\n _petriHiddenSiblings?: Element[];\n _petriCollapsedBadge?: SVGTextElement | null;\n}\ntype AugmentedGroup = SVGGElement & ClusterAugment;\n\n/**\n * Walk the SVG, find every `<g class=\"cluster\">`, derive the prefix from\n * each cluster's `<title>` (`cluster_<sanitized>`), and tag every cluster-\n * interior `<g class=\"node\">` with `data-instance=\"<prefix>\"`. Returns one\n * descriptor per cluster.\n *\n * Graphviz emits cluster nodes as SIBLINGS of the cluster `<g>`, not as\n * children — `<g class=\"cluster\">` only carries title, polygon border, and\n * the cluster's text label. To recover cluster membership we match each\n * node's `<title>` (the original graph id) against libpetri's prefixing\n * convention: interior places are emitted as `p_<prefix>_<name>`,\n * transitions as `t_<prefix>_<name>`, and junctions as `j_<prefix>_<name>`.\n *\n * Does not split nested-cluster prefixes (`outer_inner` could mean either\n * `outer/inner` or a flat `outer_inner` prefix — Graphviz sanitisation is\n * lossy). We treat every cluster as its own top-level entry, matching the\n * doclet's pragmatism.\n */\nexport function discoverClusters(svg: SVGSVGElement): Map<string, ClusterDescriptor> {\n const out = new Map<string, ClusterDescriptor>();\n const clusterGroups = svg.querySelectorAll<SVGGElement>('g.cluster');\n if (!clusterGroups.length) return out;\n\n const allNodes = Array.from(svg.querySelectorAll<SVGGElement>('g.node'));\n\n for (const g of Array.from(clusterGroups)) {\n const titleEl = directChild(g, 'title');\n if (!titleEl) continue;\n const raw = titleEl.textContent ?? '';\n if (!raw.startsWith('cluster_')) continue;\n const prefix = raw.slice('cluster_'.length);\n if (out.has(prefix)) continue;\n\n let nodeCount = 0;\n for (const node of allNodes) {\n // Don't re-tag a node already claimed by an inner (more specific)\n // cluster — outer clusters can still match by string-prefix at filter\n // time, but the data-instance is the leaf.\n if (node.getAttribute('data-instance')) continue;\n const nodeTitle = directChild(node, 'title');\n if (!nodeTitle) continue;\n const id = (nodeTitle.textContent ?? '').trim();\n if (nodeBelongsToPrefix(id, prefix)) {\n node.setAttribute('data-instance', prefix);\n nodeCount++;\n }\n }\n\n out.set(prefix, {\n prefix,\n group: g,\n nodeCount,\n color: colorForPrefix(prefix),\n });\n }\n return out;\n}\n\n/**\n * Does this node id belong to the cluster with the given prefix?\n *\n * libpetri's DOT renderer prefixes interior names: places as `p_<prefix>_…`,\n * transitions as `t_<prefix>_…`, junctions as `j_<prefix>_…`. Generic DOT\n * may also use slash-separated names (`<prefix>/…`). We accept both shapes\n * so non-libpetri DOT graphs and bare-Graphviz fixtures still match.\n */\nfunction nodeBelongsToPrefix(nodeId: string, prefix: string): boolean {\n return (\n nodeId === prefix ||\n nodeId.startsWith(`${prefix}/`) ||\n nodeId.startsWith(`p_${prefix}_`) ||\n nodeId.startsWith(`t_${prefix}_`) ||\n nodeId.startsWith(`j_${prefix}_`)\n );\n}\n\n/**\n * Deterministic HSL palette. FNV-1a-ish hash of the prefix, multiplied by\n * the golden-ratio fraction (0.61803...) to spread adjacent prefixes\n * visually. Saturation/lightness are fixed (60% / 45%) so the palette\n * stays readable on both the doc background and the dark debug UI.\n */\nexport function colorForPrefix(prefix: string): string {\n let h = 2166136261;\n for (let i = 0; i < prefix.length; i++) {\n h ^= prefix.charCodeAt(i);\n h = (h * 16777619) >>> 0;\n }\n let hue = Math.floor(((h / 4294967296) * 360 + (h % 360) * 0.61803) % 360);\n if (hue < 0) hue += 360;\n return `hsl(${hue}, 60%, 45%)`;\n}\n\n/** Paint the cluster border with the deterministic prefix colour. */\nexport function paintClusterBorders(clusters: Map<string, ClusterDescriptor>): void {\n for (const c of clusters.values()) {\n const border = directChild(c.group, 'polygon') ?? directChild(c.group, 'path');\n if (border) {\n border.setAttribute('stroke', c.color);\n // Slightly thicker than the old 2.0 — the user asked for clusters to\n // be visually obvious. Backed off from 3 to keep it readable at the\n // tiniest zoom.\n border.setAttribute('stroke-width', '2.2');\n }\n }\n}\n\n/**\n * Collapse / expand a cluster.\n *\n * Graphviz emits nodes and edges as siblings of the cluster `<g>`, not as\n * its DOM children. So \"collapsing\" cannot work by detaching the cluster's\n * children — that only removes title/border/label. Instead, we identify the\n * cluster-interior nodes (`data-instance=\"<prefix>\"`) and edges with both\n * endpoints inside the cluster, then add the `petri-collapsed-inside` class\n * so the canonical viewer.css hides them. Stash the list on the cluster\n * group so re-expand is a constant-time class removal.\n *\n * Edges that cross the cluster boundary (one endpoint inside, one outside)\n * are intentionally left visible — they show the cluster's external wiring\n * even when the interior is hidden.\n *\n * A small `<text>` badge with the actual interior node count is appended\n * inside the cluster `<g>` (which keeps its layout box).\n */\nexport function setClusterCollapsed(cluster: ClusterDescriptor, collapsed: boolean): void {\n const g = cluster.group as AugmentedGroup;\n const isCollapsed = g.classList.contains('cluster-collapsed');\n const root = g.ownerSVGElement;\n if (!root) return;\n\n if (collapsed && !isCollapsed) {\n g.classList.add('cluster-collapsed');\n const hidden: Element[] = [];\n\n // Hide interior nodes (sibling elements tagged with this prefix).\n root\n .querySelectorAll<SVGGElement>(`g.node[data-instance=\"${cssEscape(cluster.prefix)}\"]`)\n .forEach((node) => {\n node.classList.add('petri-collapsed-inside');\n hidden.push(node);\n });\n\n // Hide interior-interior edges. Edges' titles are `from->to`; we hide\n // an edge only when BOTH endpoints belong to this cluster.\n root.querySelectorAll<SVGGElement>('g.edge').forEach((edge) => {\n const titleEl = directChild(edge, 'title');\n if (!titleEl) return;\n const t = (titleEl.textContent ?? '').trim();\n const arrow = t.indexOf('->');\n if (arrow < 0) return;\n const from = t.slice(0, arrow);\n const to = t.slice(arrow + 2);\n if (nodeBelongsToPrefix(from, cluster.prefix) && nodeBelongsToPrefix(to, cluster.prefix)) {\n edge.classList.add('petri-collapsed-inside');\n hidden.push(edge);\n }\n });\n\n g._petriHiddenSiblings = hidden;\n\n if (!g._petriCollapsedBadge) {\n const label = directChild(g, 'text') as SVGTextElement | null;\n if (label) {\n const badge = document.createElementNS('http://www.w3.org/2000/svg', 'text');\n badge.setAttribute('x', label.getAttribute('x') ?? '0');\n const labelY = parseFloat(label.getAttribute('y') ?? '0');\n badge.setAttribute('y', String(labelY + 16));\n badge.setAttribute('text-anchor', label.getAttribute('text-anchor') ?? 'middle');\n badge.setAttribute('font-size', '11');\n badge.setAttribute('fill', '#9ca3af');\n badge.setAttribute('class', 'cluster-collapsed-badge');\n badge.textContent = `(${cluster.nodeCount} internal node${cluster.nodeCount === 1 ? '' : 's'})`;\n g.appendChild(badge);\n g._petriCollapsedBadge = badge;\n }\n }\n } else if (!collapsed && isCollapsed) {\n g.classList.remove('cluster-collapsed');\n if (g._petriHiddenSiblings) {\n g._petriHiddenSiblings.forEach((el) => el.classList.remove('petri-collapsed-inside'));\n g._petriHiddenSiblings = undefined;\n }\n if (g._petriCollapsedBadge && g._petriCollapsedBadge.parentNode === g) {\n g.removeChild(g._petriCollapsedBadge);\n g._petriCollapsedBadge = null;\n }\n }\n}\n\n/**\n * Toggle the \"show only <prefix>\" filter. Passing `null` (or the same\n * prefix already active) clears the filter. Mirrors the doclet's\n * `applyFilter`, including the edge-dimming heuristic that parses each\n * edge's `<title>` (`from->to`) and keeps an edge visible when either\n * endpoint matches the active prefix.\n */\nexport function applyFilter(svg: SVGSVGElement, prefix: string | null): void {\n if (prefix == null) {\n svg.removeAttribute('data-active-filter');\n svg.classList.remove('has-active-filter');\n svg.querySelectorAll('g.node, g.edge').forEach((el) => el.classList.remove('petri-dimmed'));\n return;\n }\n svg.setAttribute('data-active-filter', prefix);\n svg.classList.add('has-active-filter');\n svg.querySelectorAll('g.node').forEach((node) => {\n const di = node.getAttribute('data-instance');\n const match =\n !!di &&\n (di === prefix || di.indexOf(prefix + '_') === 0 || di.indexOf(prefix + '/') === 0);\n if (match) node.classList.remove('petri-dimmed');\n else node.classList.add('petri-dimmed');\n });\n svg.querySelectorAll('g.edge').forEach((edge) => {\n const titleEl = directChild(edge, 'title');\n if (!titleEl) return;\n const titleText = titleEl.textContent ?? '';\n const arrow = titleText.indexOf('->');\n if (arrow < 0) {\n edge.classList.remove('petri-dimmed');\n return;\n }\n const from = titleText.slice(0, arrow);\n const to = titleText.slice(arrow + 2);\n const matches = (name: string): boolean =>\n name.indexOf(prefix + '/') >= 0 || name.indexOf(prefix + '_') >= 0 || name === prefix;\n if (matches(from) || matches(to)) edge.classList.remove('petri-dimmed');\n else edge.classList.add('petri-dimmed');\n });\n}\n\n/**\n * Returns true when the place/transition node identified by `graphId` is\n * currently inside a collapsed cluster (i.e. detached from the live SVG).\n * Live consumers (e.g. highlight code in debug-ui) use this to skip lookups\n * that would otherwise hit a stale cache entry pointing at a detached node.\n */\nexport function isInsideCollapsedCluster(\n svg: SVGSVGElement | null,\n clusters: Map<string, ClusterDescriptor>,\n collapsedPrefixes: ReadonlySet<string>,\n graphId: string,\n): boolean {\n if (collapsedPrefixes.size === 0) return false;\n if (!svg) return false;\n for (const prefix of collapsedPrefixes) {\n if (!clusters.has(prefix)) continue;\n if (nodeBelongsToPrefix(graphId, prefix)) return true;\n }\n return false;\n}\n\n/** Minimal CSS.escape polyfill — happy-dom may not implement CSS globals. */\nfunction cssEscape(s: string): string {\n if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') return CSS.escape(s);\n return s.replace(/[^a-zA-Z0-9_-]/g, (c) => `\\\\${c}`);\n}\n\n/**\n * `:scope > <tag>` polyfill — happy-dom doesn't reliably resolve `:scope`\n * against namespaced SVG elements (returns null even when the child exists).\n * Walk `children` directly instead, matching by lower-cased tag name.\n */\nfunction directChild(parent: Element, tagName: string): Element | null {\n const lc = tagName.toLowerCase();\n for (const child of Array.from(parent.children)) {\n if (child.tagName.toLowerCase() === lc) return child;\n }\n return null;\n}\n","/**\n * Post-render SVG annotation for the C0 layout pipeline.\n *\n * Graphviz emits a clean SVG but without the `data-id`, `data-src`,\n * `data-dst`, `data-cluster`, `data-instance` attributes the click-all-\n * copies, highlight-through-junctions, and directed-reachability overlays\n * key off. {@link annotateSvgForC0} walks the rendered SVG once and adds\n * those attributes, plus `intra-cluster` / `cross-cluster` classes on\n * `<g class=\"edge\">` so the visibility layer can toggle edges as their\n * containing cluster's state changes.\n *\n * Mirrors the SVG-side conventions used by v3\n * (`run16_GOOD_v3_skip-junctions.mjs` overlay block, lines 876–905) but\n * derived from Graphviz's emitted DOM shape (`<g class=\"node\"><title>`,\n * `<g class=\"edge\"><title>src-&gt;dst</title>`) rather than the manual\n * emission v3 did.\n *\n * @module viewer/c0-annotations\n */\n\n/** Read the immediate `<title>` child's text content (Graphviz convention). */\nfunction ownTitle(g: Element): string {\n for (const child of Array.from(g.children)) {\n if (child.tagName === 'title') return child.textContent ?? '';\n }\n return '';\n}\n\n/**\n * Walk to the nearest ancestor `<g class=\"cluster\">` and return its\n * short name (with the leading `cluster_` stripped), or `''` if the node\n * lives at root (i.e. it's an orphan inside the synthetic orchestrator).\n *\n * Graphviz emits clusters as DOM **siblings** of their member nodes (not\n * parents), so `closest('g.cluster')` is null in our pipeline — this is\n * effectively a no-op fallback. Cluster membership is established up\n * front by {@link discoverClusters} via `<title>` prefix matching.\n */\nfunction enclosingClusterShortName(node: Element): string {\n const cluster = node.parentElement?.closest('g.cluster');\n if (!cluster) return '';\n return ownTitle(cluster).replace(/^cluster_/, '');\n}\n\n/**\n * Annotate a Graphviz-rendered SVG with the data attributes + edge classes\n * the C0 overlay layer relies on. Idempotent.\n *\n * Must run AFTER {@link discoverClusters}, which seeds `data-instance` via\n * title prefix matching (cluster → its member nodes by `t_${prefix}_…` /\n * `p_${prefix}_…` etc.). This function never clears `data-instance` —\n * Graphviz nests neither nodes inside clusters nor anything else, so DOM\n * ancestry is not a reliable source of cluster membership here.\n */\nexport function annotateSvgForC0(svg: SVGSVGElement): void {\n // Nodes — data-id (from title). Prefer the cluster tag discoverClusters\n // already wrote. Replicas (id ends `__rep__<shortName>`) override that\n // tag with their target cluster so the visibility layer can hide them\n // alongside the cluster they belong to — discoverClusters keys off the\n // libpetri prefix convention (`p_<short>_`) which replica ids don't match.\n const nodes = Array.from(svg.querySelectorAll<SVGGElement>('g.node'));\n for (const node of nodes) {\n const id = ownTitle(node);\n if (!id) continue;\n node.setAttribute('data-id', id);\n const replicaM = /__rep__([A-Za-z0-9_]+)$/.exec(id);\n if (replicaM) {\n node.setAttribute('data-instance', replicaM[1]!);\n } else if (!node.hasAttribute('data-instance')) {\n const inst = enclosingClusterShortName(node);\n if (inst) node.setAttribute('data-instance', inst);\n }\n }\n\n // Index nodes by data-id for fast cluster lookup on edge endpoints\n const nodeIndex = new Map<string, SVGGElement>();\n for (const n of nodes) {\n const id = n.getAttribute('data-id');\n if (id) nodeIndex.set(id, n);\n }\n\n // Edges — parse \"src->dst\" from title; tag with data-src/data-dst,\n // their cluster short names, and intra-/cross-cluster class.\n for (const edge of Array.from(svg.querySelectorAll<SVGGElement>('g.edge'))) {\n const title = ownTitle(edge);\n const arrow = title.indexOf('->');\n if (arrow <= 0) continue;\n const src = title.slice(0, arrow).trim();\n const dst = title.slice(arrow + 2).trim();\n edge.setAttribute('data-src', src);\n edge.setAttribute('data-dst', dst);\n\n const srcInst = nodeIndex.get(src)?.getAttribute('data-instance') ?? '';\n const dstInst = nodeIndex.get(dst)?.getAttribute('data-instance') ?? '';\n edge.setAttribute('data-src-cluster', srcInst);\n edge.setAttribute('data-dst-cluster', dstInst);\n\n edge.classList.remove('intra-cluster', 'cross-cluster');\n if (srcInst !== '' && srcInst === dstInst) {\n edge.classList.add('intra-cluster');\n edge.setAttribute('data-cluster', srcInst);\n } else {\n edge.classList.add('cross-cluster');\n edge.removeAttribute('data-cluster');\n }\n }\n}\n","/**\n * C0 cluster-chip sidebar. Replaces the previous top-left legend +\n * bottom filter strip with a single left-docked panel:\n *\n * - Cluster chips (click = toggle; shift-click = isolate to just this;\n * ctrl/cmd-click = add/remove from selection)\n * - \"show all\" / \"hide all\" buttons\n * - Shared-places toggle (orphan / cross-cluster places on/off)\n * - Highlight-mode toggle (click-node-to-highlight on/off)\n * - Inline legend explaining the ⇄ glyph\n *\n * Mounted from {@link import('../index.js').mount} when `chrome: true`.\n * Lives inside the viewer container as an `aside` so it travels with the\n * container into fullscreen.\n *\n * @module viewer/chrome/sidebar\n */\n\nimport type { ClusterDescriptor } from '../cluster-overlay.js';\nimport type { VisibilityState } from '../visibility.js';\n\nexport interface SidebarCallbacks {\n /**\n * Fired whenever the visibility state changes (chip click, show/hide all,\n * or orphan toggle). Caller forwards to `handle.setVisibility(state)`.\n */\n readonly onVisibilityChange: (state: VisibilityState) => void;\n /**\n * Fired when the user toggles highlight mode off, so the caller can\n * clear any active highlight via `handle.highlight(null)`.\n */\n readonly onHighlightModeChange: (enabled: boolean) => void;\n}\n\nexport interface SidebarHandle {\n /** The root `<aside>` element appended to the container. */\n readonly root: HTMLElement;\n /** Tear down listeners and remove the sidebar from the DOM. */\n dispose(): void;\n}\n\nconst SIDEBAR_CLASS = 'libpetri-sidebar';\nconst CHIP_CLASS = 'libpetri-sidebar-chip';\nconst CHIP_OFF_CLASS = 'is-off';\n\n/**\n * Build + mount the sidebar inside `container`. Returns a handle the\n * caller disposes on re-mount.\n */\nexport function mountSidebar(\n container: HTMLElement,\n clusters: ReadonlyMap<string, ClusterDescriptor>,\n callbacks: SidebarCallbacks,\n): SidebarHandle {\n const visibleClusters = new Set<string>();\n for (const prefix of clusters.keys()) visibleClusters.add(prefix);\n let includeSharedPlaces = true;\n let highlightMode = true;\n\n const emit = (): void => {\n callbacks.onVisibilityChange({\n visibleClusters: new Set(visibleClusters),\n includeSharedPlaces,\n });\n };\n\n const root = document.createElement('aside');\n root.className = SIDEBAR_CLASS;\n\n const title = document.createElement('h4');\n title.className = 'libpetri-sidebar-title';\n title.textContent = `Subnets (${clusters.size})`;\n root.appendChild(title);\n\n // Show all / hide all\n const actions = document.createElement('div');\n actions.className = 'libpetri-sidebar-actions';\n const showAll = document.createElement('button');\n showAll.type = 'button';\n showAll.textContent = 'show all';\n const hideAll = document.createElement('button');\n hideAll.type = 'button';\n hideAll.textContent = 'hide all';\n actions.appendChild(showAll);\n actions.appendChild(hideAll);\n root.appendChild(actions);\n\n // Shared-places toggle — hides every replica + original of a shared\n // place (anything tagged `petri-replica`) when unchecked.\n const sharedLabel = document.createElement('label');\n sharedLabel.className = 'libpetri-sidebar-checkbox';\n const sharedCb = document.createElement('input');\n sharedCb.type = 'checkbox';\n sharedCb.checked = includeSharedPlaces;\n sharedLabel.appendChild(sharedCb);\n sharedLabel.appendChild(document.createTextNode(' Shared places'));\n sharedCb.addEventListener('change', () => {\n includeSharedPlaces = sharedCb.checked;\n emit();\n });\n root.appendChild(sharedLabel);\n\n // Highlight-mode toggle\n const hlLabel = document.createElement('label');\n hlLabel.className = 'libpetri-sidebar-checkbox';\n const hlCb = document.createElement('input');\n hlCb.type = 'checkbox';\n hlCb.checked = highlightMode;\n hlLabel.appendChild(hlCb);\n hlLabel.appendChild(document.createTextNode(' Click node → highlight'));\n hlCb.addEventListener('change', () => {\n highlightMode = hlCb.checked;\n callbacks.onHighlightModeChange(highlightMode);\n });\n root.appendChild(hlLabel);\n\n // Hint\n const hint = document.createElement('div');\n hint.className = 'libpetri-sidebar-hint';\n hint.innerHTML =\n 'Click chip: toggle.<br>' +\n 'Shift-click: isolate.<br>' +\n 'Cmd/Ctrl-click: multi-select.<br>' +\n 'Click empty: clear highlight.';\n root.appendChild(hint);\n\n // Chips\n const chipList = document.createElement('ul');\n chipList.className = 'libpetri-sidebar-chips';\n const chipMap = new Map<string, HTMLLIElement>();\n for (const cluster of clusters.values()) {\n const li = document.createElement('li');\n li.className = CHIP_CLASS;\n li.style.borderLeftColor = cluster.color;\n li.dataset.prefix = cluster.prefix;\n const dot = document.createElement('span');\n dot.className = 'libpetri-sidebar-chip-dot';\n dot.style.background = cluster.color;\n const name = document.createElement('span');\n name.className = 'libpetri-sidebar-chip-name';\n name.textContent = cluster.prefix;\n const count = document.createElement('span');\n count.className = 'libpetri-sidebar-chip-count';\n count.textContent = String(cluster.nodeCount);\n li.appendChild(dot);\n li.appendChild(name);\n li.appendChild(count);\n li.addEventListener('click', (ev) => {\n const prefix = cluster.prefix;\n if (ev.shiftKey) {\n // Isolate to just this one — or restore \"show all\" if already isolated.\n const allOff = [...clusters.keys()].every(k =>\n k === prefix ? visibleClusters.has(k) : !visibleClusters.has(k),\n );\n visibleClusters.clear();\n if (allOff) for (const k of clusters.keys()) visibleClusters.add(k);\n else visibleClusters.add(prefix);\n } else if (ev.ctrlKey || ev.metaKey) {\n // Multi-select: ADD/remove. If currently \"show all\", start fresh.\n const anyHidden = [...clusters.keys()].some(k => !visibleClusters.has(k));\n if (!anyHidden) {\n visibleClusters.clear();\n visibleClusters.add(prefix);\n } else if (visibleClusters.has(prefix)) {\n visibleClusters.delete(prefix);\n } else {\n visibleClusters.add(prefix);\n }\n } else {\n if (visibleClusters.has(prefix)) visibleClusters.delete(prefix);\n else visibleClusters.add(prefix);\n }\n refreshChips();\n emit();\n });\n chipList.appendChild(li);\n chipMap.set(cluster.prefix, li);\n }\n root.appendChild(chipList);\n\n function refreshChips(): void {\n for (const [prefix, li] of chipMap) {\n li.classList.toggle(CHIP_OFF_CLASS, !visibleClusters.has(prefix));\n }\n }\n\n showAll.addEventListener('click', () => {\n visibleClusters.clear();\n for (const k of clusters.keys()) visibleClusters.add(k);\n includeSharedPlaces = true;\n sharedCb.checked = true;\n refreshChips();\n emit();\n });\n hideAll.addEventListener('click', () => {\n visibleClusters.clear();\n includeSharedPlaces = false;\n sharedCb.checked = false;\n refreshChips();\n emit();\n });\n\n container.appendChild(root);\n\n // Emit initial state so the caller's visibility starts in sync.\n emit();\n\n return {\n root,\n dispose(): void {\n if (root.parentNode === container) container.removeChild(root);\n },\n };\n}\n","/**\n * Click-to-highlight overlay for the C0 layout.\n *\n * Two operations:\n *\n * findAllCopies(svg, focusId) — given a node id, returns the set of every\n * DOM node representing the same logical place. For a replica it returns\n * the original + every sibling replica; for an original of a shared place\n * it returns itself + every replica; for an unshared node it returns just\n * `{focusId}`.\n *\n * applyHighlight(svg, focusId) — highlights the focus group, walks\n * junctions (`j_*` routing nodes) transparently to reveal the real\n * downstream place/transition neighbors, and fades everything else.\n * Pass `null` to clear.\n *\n * Ported from v3 `applyHighlight` / `findAllCopies` (lines 971–1047). All\n * SVG queries assume the DOM has been through {@link annotateSvgForC0}.\n *\n * @module viewer/highlight\n */\n\nconst HIGHLIGHT_CLASSES = ['is-highlight', 'is-neighbor', 'is-faded'] as const;\n\nfunction clearHighlightClasses(svg: SVGSVGElement): void {\n for (const cls of HIGHLIGHT_CLASSES) {\n for (const el of Array.from(svg.querySelectorAll<Element>('.' + cls))) {\n el.classList.remove(cls);\n }\n }\n}\n\n/**\n * Resolve every DOM node representing the same logical place as `focusId`.\n *\n * Replicas carry `data-replica-of=\"<semanticName>\"`. The original of a\n * shared place is itself tagged with `data-replica-of=\"<semanticName>\"` so\n * a single attribute query finds the whole family.\n */\nexport function findAllCopies(svg: SVGSVGElement, focusId: string): Set<string> {\n const focus = svg.querySelector<SVGGElement>(`g.node[data-id=\"${cssEscape(focusId)}\"]`);\n if (!focus) return new Set([focusId]);\n const copies = new Set<string>([focusId]);\n const origLabel = focus.getAttribute('data-replica-of')\n ?? (focusId.startsWith('p_') ? focusId.slice(2) : null);\n if (origLabel) {\n const orig = svg.querySelector<SVGGElement>(`g.node[data-id=\"${cssEscape('p_' + origLabel)}\"]`);\n if (orig) {\n const id = orig.getAttribute('data-id');\n if (id) copies.add(id);\n }\n for (const sibling of Array.from(\n svg.querySelectorAll<SVGGElement>(`g.node[data-replica-of=\"${cssEscape(origLabel)}\"]`),\n )) {\n const id = sibling.getAttribute('data-id');\n if (id) copies.add(id);\n }\n }\n return copies;\n}\n\n/**\n * Highlight `focusId` (and every DOM copy of the same logical place),\n * walking through junctions to reveal real downstream/upstream neighbors.\n *\n * Pass `null` to clear. Hidden nodes/edges are skipped so toggling a\n * cluster off doesn't bring them back into the path.\n */\nexport function applyHighlight(svg: SVGSVGElement, focusId: string | null): void {\n clearHighlightClasses(svg);\n if (focusId === null) return;\n\n const copies = findAllCopies(svg, focusId);\n for (const id of copies) {\n const n = svg.querySelector<SVGGElement>(`g.node[data-id=\"${cssEscape(id)}\"]`);\n if (n && !n.classList.contains('is-hidden')) n.classList.add('is-highlight');\n }\n\n // BFS through edges; junctions are transparent — the true neighbour is\n // the place/transition on the other side of the routing node.\n const neighbors = new Set<string>();\n const pathJunctions = new Set<string>();\n const pathEdges: SVGGElement[] = [];\n const queue: string[] = Array.from(copies);\n const visited = new Set<string>(copies);\n\n const allEdges = Array.from(svg.querySelectorAll<SVGGElement>('g.edge'));\n while (queue.length > 0) {\n const nodeId = queue.shift()!;\n for (const e of allEdges) {\n if (e.classList.contains('is-hidden')) continue;\n const s = e.getAttribute('data-src');\n const d = e.getAttribute('data-dst');\n if (s !== nodeId && d !== nodeId) continue;\n const otherId = s === nodeId ? d : s;\n if (!otherId) continue;\n pathEdges.push(e);\n if (visited.has(otherId)) continue;\n visited.add(otherId);\n if (otherId.startsWith('j_')) {\n pathJunctions.add(otherId);\n queue.push(otherId);\n } else {\n neighbors.add(otherId);\n }\n }\n }\n\n for (const e of pathEdges) e.classList.add('is-highlight');\n\n for (const e of allEdges) {\n if (e.classList.contains('is-hidden') || e.classList.contains('is-highlight')) continue;\n e.classList.add('is-faded');\n }\n\n for (const n of Array.from(svg.querySelectorAll<SVGGElement>('g.node'))) {\n if (n.classList.contains('is-hidden')) continue;\n const id = n.getAttribute('data-id') ?? '';\n if (copies.has(id)) continue;\n if (neighbors.has(id)) n.classList.add('is-neighbor');\n else if (pathJunctions.has(id)) continue;\n else n.classList.add('is-faded');\n }\n\n for (const c of Array.from(\n svg.querySelectorAll<SVGGElement>('g.cluster:not(.cluster-orchestrator)'),\n )) {\n if (c.classList.contains('is-hidden')) continue;\n c.classList.add('is-faded');\n }\n}\n\n/**\n * Escape a string for use inside a CSS attribute selector value.\n *\n * Built-in `CSS.escape` covers identifier characters; node ids contain\n * `_` and digits which `CSS.escape` accepts, plus the `__rep__` separator\n * which is also safe. We only need to escape the few syntactic chars\n * (`\"`, `\\`).\n */\nfunction cssEscape(value: string): string {\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n","/**\n * Replica detection + DOM tagging for the C0 layout.\n *\n * `replicateShared` (in `layout/preprocess.ts`) emits clones of shared\n * places with ids of the form `${placeId}__rep__${shortCluster}`. After\n * Graphviz renders the pinned DOT, those replica clones appear as\n * `<g class=\"node\">` siblings whose inner `<title>` carries the replica\n * id. The original `placeId` also gets a sibling for every cluster it\n * touches and stays marked as a shared place.\n *\n * {@link tagReplicas} walks the rendered SVG once and adds:\n * - `class=\"petri-replica\"` on every replica node AND its original (so\n * `data-locations` / ⇄ glyph styling can target both).\n * - `data-replica-of=\"<semantic-place-name>\"` — the original place name\n * stripped of the `p_` prefix. Click-all-copies (Stage 4) looks up\n * every node with the same `data-replica-of` value.\n *\n * The ⇄ glyph + dashed border land in CSS (Stage 5); this module only\n * does the attribute tagging.\n *\n * @module viewer/replica-tagging\n */\n\nconst REPLICA_RE = /^(p_[A-Za-z0-9_]+)__rep__/;\n\n/**\n * Stable semantic id for a logical place — strips the `p_` prefix so the\n * value matches the `name` field on libpetri's Place model. Click-all-\n * copies in the overlay keys off this value, not the rendered DOM id.\n */\nfunction semanticName(placeId: string): string {\n return placeId.replace(/^p_/, '');\n}\n\n/**\n * Walk every `<g class=\"node\">` in `svg`, detect replicas and originals of\n * shared places, and tag them with `petri-replica` + `data-replica-of`.\n *\n * Idempotent: re-running on an already-tagged SVG is a no-op (the attrs\n * already match).\n */\nexport function tagReplicas(svg: SVGSVGElement): void {\n // First pass: collect every original whose id appears as the prefix of\n // at least one replica's title. We can't tag the originals until we\n // know which ones got copies.\n const originalsWithCopies = new Set<string>();\n const allNodes = Array.from(svg.querySelectorAll<SVGGElement>('g.node'));\n for (const g of allNodes) {\n const title = g.querySelector('title')?.textContent ?? '';\n const m = REPLICA_RE.exec(title);\n if (m) originalsWithCopies.add(m[1]!);\n }\n\n // Second pass: tag both replicas and their (matching) originals + append\n // the shared-place ⇄ glyph. SVG has no ::after pseudo-element so the\n // glyph is injected as a <text> child of the node's <g>.\n for (const g of allNodes) {\n const title = g.querySelector('title')?.textContent ?? '';\n const replicaMatch = REPLICA_RE.exec(title);\n let semantic: string | null = null;\n if (replicaMatch) {\n semantic = semanticName(replicaMatch[1]!);\n } else if (originalsWithCopies.has(title)) {\n semantic = semanticName(title);\n }\n if (semantic === null) continue;\n g.classList.add('petri-replica');\n g.setAttribute('data-replica-of', semantic);\n appendSharedGlyph(g);\n }\n}\n\nconst SVG_NS = 'http://www.w3.org/2000/svg';\nconst GLYPH_CLASS = 'libpetri-replica-glyph';\n\n/**\n * Inject a ⇄ marker adjacent to the upper-right of the node's place shape.\n * Positioned OUTSIDE the circle so it doesn't overlap the boundary stroke\n * or the inner fill (libpetri places are 25pt circles — anything drawn\n * inside is unreadable). Anchored top-left at `(cx + rx + 2, cy - ry + 2)`\n * with a 9pt sans-serif glyph; the styling is finished by the canonical\n * CSS via the `libpetri-replica-glyph` class.\n *\n * Idempotent.\n */\nfunction appendSharedGlyph(g: SVGGElement): void {\n if (g.querySelector(`text.${GLYPH_CLASS}`)) return;\n const shape = g.querySelector('ellipse, circle');\n if (!shape) return;\n let cx = 0, cy = 0, rx = 0, ry = 0;\n if (shape.tagName === 'ellipse') {\n cx = parseFloat(shape.getAttribute('cx') ?? '0');\n cy = parseFloat(shape.getAttribute('cy') ?? '0');\n rx = parseFloat(shape.getAttribute('rx') ?? '0');\n ry = parseFloat(shape.getAttribute('ry') ?? '0');\n } else {\n cx = parseFloat(shape.getAttribute('cx') ?? '0');\n cy = parseFloat(shape.getAttribute('cy') ?? '0');\n rx = parseFloat(shape.getAttribute('r') ?? '0');\n ry = rx;\n }\n const text = document.createElementNS(SVG_NS, 'text');\n text.setAttribute('class', GLYPH_CLASS);\n // Anchored at the upper-right OUTSIDE the circle (top-left of the glyph\n // sits at +2 / -2 from the bounding-box corner so it doesn't kiss the\n // stroke). text-anchor=start so the glyph extends to the right.\n text.setAttribute('x', (cx + rx + 2).toFixed(2));\n text.setAttribute('y', (cy - ry + 2).toFixed(2));\n text.setAttribute('text-anchor', 'start');\n text.setAttribute('font-size', '9');\n text.textContent = '⇄';\n g.appendChild(text);\n}\n","/**\n * Directed-reachability orphan visibility for the C0 layout.\n *\n * When only a subset of clusters is visible, orphan transitions (and\n * places) should appear iff they participate in a chain that ends at a\n * visible cluster — the chain direction matters. v3's `computeVisibleOrphans`\n * (lines 892–918) walks orphan-orphan edges backwards from \"seed upstream\"\n * orphans (those that feed INTO a visible cluster) and forwards from\n * \"seed downstream\" orphans (those a visible cluster feeds INTO). This\n * keeps `ResponseAwaited` hidden when only `productSearch` is visible\n * while exposing legitimate upstream chains like\n * `SearchProductsToolRequest → ForkSearchProductsToolRequest → … → productSearch`.\n *\n * @module viewer/visibility\n */\n\nimport { annotateSvgForC0 } from './c0-annotations.js';\n\n/**\n * Set of orphan ids whose visibility is justified by the current cluster\n * selection. Caller toggles `is-hidden` on the corresponding `<g class=\"node\">`s.\n */\nexport function computeVisibleOrphans(\n svg: SVGSVGElement,\n visibleClusters: ReadonlySet<string>,\n): Set<string> {\n if (visibleClusters.size === 0) return new Set();\n\n const orphanNodes = Array.from(\n svg.querySelectorAll<SVGGElement>('g.node:not([data-instance])'),\n );\n const orphanIds = new Set<string>();\n for (const n of orphanNodes) {\n const id = n.getAttribute('data-id');\n if (id) orphanIds.add(id);\n }\n\n const fwdAdj = new Map<string, Set<string>>();\n const bwdAdj = new Map<string, Set<string>>();\n const link = (m: Map<string, Set<string>>, k: string, v: string): void => {\n let s = m.get(k);\n if (!s) { s = new Set(); m.set(k, s); }\n s.add(v);\n };\n\n // Orphan-orphan edges feed the BFS walk\n for (const e of Array.from(svg.querySelectorAll<SVGGElement>('g.edge'))) {\n const s = e.getAttribute('data-src') ?? '';\n const d = e.getAttribute('data-dst') ?? '';\n if (orphanIds.has(s) && orphanIds.has(d)) {\n link(fwdAdj, s, d);\n link(bwdAdj, d, s);\n }\n }\n\n // Seed orphans = orphans on either end of a cross-cluster edge whose\n // other endpoint sits in a visible cluster.\n const seedUpstream = new Set<string>();\n const seedDownstream = new Set<string>();\n for (const e of Array.from(\n svg.querySelectorAll<SVGGElement>('g.edge.cross-cluster'),\n )) {\n const s = e.getAttribute('data-src') ?? '';\n const d = e.getAttribute('data-dst') ?? '';\n const sCl = e.getAttribute('data-src-cluster') ?? '';\n const dCl = e.getAttribute('data-dst-cluster') ?? '';\n if (orphanIds.has(s) && visibleClusters.has(dCl)) seedUpstream.add(s);\n if (orphanIds.has(d) && visibleClusters.has(sCl)) seedDownstream.add(d);\n }\n\n const visible = new Set<string>();\n const bfs = (seeds: Set<string>, adj: Map<string, Set<string>>): void => {\n const q: string[] = [];\n for (const s of seeds) { visible.add(s); q.push(s); }\n while (q.length) {\n const cur = q.shift()!;\n const next = adj.get(cur);\n if (!next) continue;\n for (const x of next) {\n if (!visible.has(x)) { visible.add(x); q.push(x); }\n }\n }\n };\n bfs(seedUpstream, bwdAdj);\n bfs(seedDownstream, fwdAdj);\n return visible;\n}\n\nexport interface VisibilityState {\n /** Cluster short names currently visible. */\n readonly visibleClusters: ReadonlySet<string>;\n /**\n * When false, hide every replica/shared place (`g.node.petri-replica`)\n * regardless of which cluster it belongs to. The corresponding sidebar\n * checkbox is labelled \"Shared places\".\n */\n readonly includeSharedPlaces: boolean;\n}\n\n/**\n * Apply visibility state to an annotated SVG. Idempotent — re-call to\n * reflect a new state.\n *\n * Graphviz emits cluster member nodes as DOM SIBLINGS of the cluster `<g>`,\n * not its children. Toggling `is-hidden` on the cluster `<g>` alone hides\n * only the cluster outline + label — the nodes, junctions, transitions and\n * intra-cluster edges inside stay visible. So this routine hides them\n * explicitly:\n *\n * 1. Cluster outlines via `g.cluster` ↔ `visibleClusters`.\n * 2. Cluster member nodes via `g.node[data-instance]` ↔ `visibleClusters`.\n * 3. Orphan nodes (no `data-instance`) via directed-reachability — when\n * no cluster is visible they all hide, otherwise only the chain\n * reaching a visible cluster shows.\n * 4. Replica + original shared places (`g.node.petri-replica`) hide as a\n * group when `includeSharedPlaces=false`.\n * 5. Edges follow their endpoints — hide if either is hidden.\n */\nexport function applyVisibility(svg: SVGSVGElement, state: VisibilityState): void {\n annotateSvgForC0(svg);\n\n const { visibleClusters, includeSharedPlaces } = state;\n\n // 1. Cluster outlines.\n for (const c of Array.from(\n svg.querySelectorAll<SVGGElement>('g.cluster:not(.cluster-orchestrator)'),\n )) {\n const titleEl = c.querySelector(':scope > title');\n const shortName = (titleEl?.textContent ?? '').replace(/^cluster_/, '');\n c.classList.toggle('is-hidden', !visibleClusters.has(shortName));\n }\n\n // 2. Cluster member nodes follow their cluster's visibility.\n for (const n of Array.from(svg.querySelectorAll<SVGGElement>('g.node[data-instance]'))) {\n const inst = n.getAttribute('data-instance') ?? '';\n n.classList.toggle('is-hidden', !visibleClusters.has(inst));\n }\n\n // 3. Orphan nodes (no data-instance) — orchestrator-level transitions\n // + unclustered places. Reachability emptys to ∅ when no cluster is\n // on, which hides every orphan — the \"Hide all\" case.\n const visOrphans = computeVisibleOrphans(svg, visibleClusters);\n for (const n of Array.from(\n svg.querySelectorAll<SVGGElement>('g.node:not([data-instance])'),\n )) {\n const id = n.getAttribute('data-id') ?? '';\n n.classList.toggle('is-hidden', !visOrphans.has(id));\n }\n\n // 4. Shared places (replicas + originals). The `petri-replica` class is\n // set by `replica-tagging.ts` on BOTH the replica clones and the\n // original whose cross-cluster shape spawned them — toggling them as a\n // group lets the user collapse the shared-place clutter in one click.\n if (!includeSharedPlaces) {\n for (const n of Array.from(svg.querySelectorAll<SVGGElement>('g.node.petri-replica'))) {\n n.classList.add('is-hidden');\n }\n }\n\n // 5. Edges follow their endpoints. Map lookup beats N*M querySelector.\n const nodeById = new Map<string, SVGGElement>();\n for (const n of Array.from(svg.querySelectorAll<SVGGElement>('g.node'))) {\n const id = n.getAttribute('data-id');\n if (id) nodeById.set(id, n);\n }\n for (const e of Array.from(svg.querySelectorAll<SVGGElement>('g.edge'))) {\n const s = e.getAttribute('data-src') ?? '';\n const d = e.getAttribute('data-dst') ?? '';\n const sHidden = nodeById.get(s)?.classList.contains('is-hidden') ?? false;\n const dHidden = nodeById.get(d)?.classList.contains('is-hidden') ?? false;\n e.classList.toggle('is-hidden', sHidden || dHidden);\n }\n}\n","/**\n * CSS custom properties exposed by the canonical viewer stylesheet.\n *\n * Each property has a default in `viewer.css`. Consumers can override any of\n * them at any scope (the viewer container, the document root, etc.) to\n * theme the viewer without touching the canonical CSS.\n *\n * Names are listed here as a reference + so the IIFE bundle can expose them\n * on `window.LibpetriViewer.cssVariables`. The list is informational — the\n * stylesheet is still the source of truth.\n *\n * @module viewer/styles\n */\n\n/** CSS custom property names that the canonical stylesheet honours. */\nexport const VIEWER_CSS_VARIABLES = [\n '--lpv-bg',\n '--lpv-header-bg',\n '--lpv-border',\n '--lpv-text',\n '--lpv-muted',\n '--lpv-cluster-bg',\n '--lpv-cluster-bg-collapsed',\n '--lpv-cluster-stroke-width',\n '--lpv-cluster-stroke-dash',\n '--lpv-cluster-fill-tint',\n '--lpv-dim-opacity',\n '--lpv-active-filter-outline',\n '--lpv-legend-bg',\n '--lpv-chip-bg',\n '--lpv-chip-active-bg',\n // C0 sidebar + replica + highlight (Stage 5)\n '--lpv-sidebar-bg',\n '--lpv-sidebar-border',\n '--lpv-sidebar-text',\n '--lpv-sidebar-muted',\n '--lpv-sidebar-chip-bg',\n '--lpv-sidebar-chip-hover-bg',\n '--lpv-sidebar-chip-off-opacity',\n '--lpv-shared-glyph-color',\n '--lpv-replica-fill',\n '--lpv-replica-stroke',\n '--lpv-highlight-stroke',\n '--lpv-highlight-glow',\n '--lpv-neighbor-stroke',\n '--lpv-faded-node-opacity',\n '--lpv-faded-edge-opacity',\n '--lpv-faded-cluster-opacity',\n] as const;\n\nexport type ViewerCssVariable = (typeof VIEWER_CSS_VARIABLES)[number];\n","/**\n * Canonical libpetri Petri-net diagram viewer.\n *\n * Single source of truth for DOT → SVG rendering + cluster overlay across\n * all four surfaces:\n * - debug-ui (live debugger)\n * - dev-preview (Vite app)\n * - Javadoc taglet (Java doclet)\n * - Rustdoc embed (libpetri-docgen)\n * - TypeDoc / typescript doclet\n *\n * Build outputs:\n * - ESM: `dist/viewer/index.js` — consumed by Vite/Webpack/Node bundlers.\n * - IIFE: `dist/viewer/viewer.iife.js` — self-contained, exposes\n * `window.LibpetriViewer = { mount, ... }`. Used by the doc taglets\n * that ship pre-rendered HTML pages without a bundler.\n * - CSS: `dist/viewer/viewer.css` — canonical stylesheet (copied from\n * `src/viewer/resources/viewer.css`).\n *\n * Edits to viewer behaviour MUST happen here. The three resource directories\n * under `java/`, `rust/`, and `typescript/src/doclet/` are **build outputs**\n * populated by `scripts/build-viewer.sh` and must not be hand-edited.\n *\n * @module viewer\n */\n\nimport {\n attachPanzoom,\n DEFAULT_PANZOOM_OPTS,\n type PanzoomInstance,\n type PanzoomOptions,\n} from './pan-zoom.js';\nimport {\n applyFilter as overlayApplyFilter,\n colorForPrefix,\n discoverClusters,\n isInsideCollapsedCluster as overlayIsInsideCollapsedCluster,\n paintClusterBorders,\n setClusterCollapsed,\n type ClusterDescriptor,\n} from './cluster-overlay.js';\nimport { annotateSvgForC0 } from './c0-annotations.js';\nimport { mountSidebar, type SidebarHandle } from './chrome/sidebar.js';\nimport { applyHighlight } from './highlight.js';\nimport { tagReplicas } from './replica-tagging.js';\nimport { applyVisibility, type VisibilityState } from './visibility.js';\nimport { VIEWER_CSS_VARIABLES } from './styles.js';\n\nexport {\n colorForPrefix,\n discoverClusters,\n VIEWER_CSS_VARIABLES,\n DEFAULT_PANZOOM_OPTS,\n};\nexport type { ClusterDescriptor, PanzoomInstance, PanzoomOptions };\n\n/** Handle returned by {@link mount}. */\nexport interface ViewerHandle {\n /** The rendered root `<svg>` element. */\n readonly svg: SVGSVGElement;\n /** The panzoom instance attached to the SVG. */\n readonly panzoom: PanzoomInstance;\n /** Map of cluster prefix → descriptor for the current SVG. */\n readonly clusters: ReadonlyMap<string, ClusterDescriptor>;\n /** Prefixes currently marked collapsed on this handle. */\n readonly collapsedPrefixes: ReadonlySet<string>;\n /** Currently active \"show only <prefix>\" filter, or null. */\n readonly activeFilter: string | null;\n /** Collapse a single cluster by prefix. No-op if unknown. */\n collapse(prefix: string): void;\n /** Expand a single cluster by prefix. No-op if unknown. */\n expand(prefix: string): void;\n /** Collapse every discovered cluster. */\n collapseAll(): void;\n /** Expand every collapsed cluster. */\n expandAll(): void;\n /** Apply the \"show only <prefix>\" filter; pass `null` to clear. */\n filter(prefix: string | null): void;\n /** Reset pan/zoom to the identity transform. */\n resetZoom(): void;\n /** Scale and translate so the full diagram fits the host viewport. */\n fit(): void;\n /** Reports whether `graphId` is inside a currently-collapsed cluster. */\n isInsideCollapsedCluster(graphId: string): boolean;\n /**\n * Highlight a node and every DOM copy of the same logical place; walk\n * junctions to surface real neighbors. Pass `null` to clear. C0-only.\n */\n highlight(nodeId: string | null): void;\n /** The node id currently highlighted, or `null`. */\n readonly highlightedNodeId: string | null;\n /**\n * Set the cluster-visibility state: which clusters render, and whether\n * directed-reachable orphans render. C0-only.\n */\n setVisibility(state: VisibilityState): void;\n /** Tear down: dispose panzoom and detach listeners. */\n dispose(): void;\n}\n\n/** Options accepted by {@link mount}. */\nexport interface MountOptions {\n /**\n * Previous handle to dispose before mounting. Pass the handle returned\n * by a prior `mount()` call so callers don't need to track it separately.\n * Cluster collapse / filter state from `previousHandle` is preserved\n * across the re-render so the user's intent survives a live re-draw.\n */\n readonly previousHandle?: ViewerHandle | null;\n /** Per-call panzoom overrides; merged on top of {@link DEFAULT_PANZOOM_OPTS}. */\n readonly panzoom?: PanzoomOptions;\n /** Fired after a cluster's collapsed state changes via the handle API. */\n readonly onClusterCollapse?: (prefix: string, collapsed: boolean) => void;\n /** Fired after `filter()` runs, with the new filter prefix or null. */\n readonly onClusterFilter?: (prefix: string | null) => void;\n /** When true, append a legend sidebar + filter chip strip inside `container`. */\n readonly chrome?: boolean;\n /**\n * Layout strategy. `'elk'` (default) runs the C0 pipeline — parse → fold\n * → replicate → ELK → Graphviz `neato` pin mode — and tags replica\n * nodes for the click-all-copies + ⇄ overlay. `'graphviz'` runs the\n * plain Graphviz `dot` engine and skips replica tagging. The result is\n * cached on the DOT hash; re-mounts on identical DOT skip the pipeline.\n */\n readonly layout?: 'elk' | 'graphviz';\n}\n\n/**\n * Render a DOT source, mount the resulting SVG into `container`, wire pan/zoom,\n * and surface cluster overlay controls.\n *\n * The container's existing children are removed before the new SVG is\n * appended (the same teardown behaviour `renderDotToContainer` used to provide).\n *\n * Pass `dotSource === null` to adopt an SVG already present as a direct child\n * of `container` (pre-rendered SVG path — e.g. the Java javadoc taglet emits\n * `dot -Tsvg` at build time). In that mode the viewer never imports the\n * runtime DOT renderer, which lets the static IIFE bundle drop `@viz-js/viz`\n * entirely.\n *\n * NOTE: when shipping a pre-rendered SVG (`dotSource === null`), do NOT also\n * pass `previousHandle` — the handle's `dispose()` runs before SVG detection\n * and would orphan the host. The Java taglet mounts once per page so this is\n * a non-issue in practice.\n */\nexport async function mount(\n dotSource: string | null,\n container: HTMLElement,\n opts: MountOptions = {},\n): Promise<ViewerHandle> {\n const previousHandle = opts.previousHandle ?? null;\n const preservedCollapsed = previousHandle\n ? new Set(previousHandle.collapsedPrefixes)\n : new Set<string>();\n const preservedFilter = previousHandle?.activeFilter ?? null;\n\n if (previousHandle) {\n previousHandle.dispose();\n }\n\n // Gate on dotSource (not on \"SVG present\") — debug-ui re-renders by\n // passing DOT with `previousHandle` and the leftover SVG must NOT be\n // misread as pre-rendered. Java passes literal `null` for the\n // pre-rendered path.\n let svg: SVGSVGElement;\n const existing = container.querySelector(':scope > svg');\n if (dotSource == null) {\n if (!(existing instanceof SVGSVGElement)) {\n throw new Error('mount: no DOT source and no pre-rendered SVG');\n }\n svg = existing;\n } else {\n // Dynamic import so the static IIFE entry can alias './render.js' to a\n // throwing stub and drop the @viz-js/viz + elkjs dependencies.\n const renderModule = await import('./render.js');\n const useElk = (opts.layout ?? 'elk') === 'elk';\n svg = useElk\n ? await renderModule.renderDotToSvgWithElkLayout(dotSource)\n : await renderModule.renderDotToSvg(dotSource);\n container.innerHTML = '';\n container.appendChild(svg);\n }\n // Tag the host so the canonical CSS (which scopes to .libpetri-viewer)\n // applies even when the consumer didn't wrap us in a .petrinet-diagram.\n container.classList.add('libpetri-viewer');\n\n const panzoomInstance = attachPanzoom(svg, opts.panzoom);\n const clusters = discoverClusters(svg);\n paintClusterBorders(clusters);\n const layoutMode = opts.layout ?? 'elk';\n if (layoutMode === 'elk') {\n tagReplicas(svg);\n annotateSvgForC0(svg);\n }\n\n const collapsedPrefixes = new Set<string>();\n let activeFilter: string | null = null;\n let highlightedNodeId: string | null = null;\n let disposed = false;\n\n // Chrome elements (created lazily; only when chrome:true is requested).\n // Re-rendering through the same handle does not duplicate them.\n let chromeRoot: HTMLElement | null = null;\n let sidebarHandle: SidebarHandle | null = null;\n let highlightModeEnabled = true;\n\n function ensureChrome(): void {\n if (!opts.chrome) return;\n if (chromeRoot && chromeRoot.parentNode === container) return;\n\n // Reset + Fullscreen controls anchor top-right; the C0 sidebar\n // (top-left) replaces the legacy legend + filter strip.\n chromeRoot = document.createElement('div');\n chromeRoot.className = 'libpetri-viewer-chrome';\n chromeRoot.style.position = 'absolute';\n chromeRoot.style.inset = '0';\n chromeRoot.style.pointerEvents = 'none';\n\n const controls = document.createElement('div');\n controls.className = 'diagram-controls';\n controls.style.pointerEvents = 'auto';\n const resetBtn = document.createElement('button');\n resetBtn.type = 'button';\n resetBtn.className = 'diagram-btn btn-reset';\n resetBtn.title = 'Reset view';\n resetBtn.textContent = 'Reset';\n resetBtn.addEventListener('click', () => handle.fit());\n const fsBtn = document.createElement('button');\n fsBtn.type = 'button';\n fsBtn.className = 'diagram-btn btn-fullscreen';\n fsBtn.title = 'Toggle fullscreen';\n fsBtn.textContent = 'Fullscreen';\n fsBtn.addEventListener('click', () => toggleFullscreen(container, fsBtn));\n controls.appendChild(resetBtn);\n controls.appendChild(fsBtn);\n chromeRoot.appendChild(controls);\n\n if (getComputedStyle(container).position === 'static') {\n container.style.position = 'relative';\n }\n container.appendChild(chromeRoot);\n\n // The cluster-chip sidebar is the C0 chrome — only mount when\n // there's a renderable layout and at least one cluster to show.\n if (layoutMode === 'elk' && clusters.size > 0) {\n sidebarHandle = mountSidebar(container, clusters, {\n onVisibilityChange: (state) => handle.setVisibility(state),\n onHighlightModeChange: (enabled) => {\n highlightModeEnabled = enabled;\n if (!enabled) handle.highlight(null);\n },\n });\n }\n }\n\n // In-page lightbox fullscreen: toggle a CSS class on the host and\n // re-fit. We deliberately don't invoke the native Fullscreen API —\n // users want the diagram to expand inside the page, not yank the\n // browser into OS-level fullscreen.\n function toggleFullscreen(host: HTMLElement, btn: HTMLButtonElement): void {\n const isFs = host.classList.toggle('diagram-fullscreen');\n btn.textContent = isFs ? 'Exit fullscreen' : 'Fullscreen';\n requestAnimationFrame(() => handle.fit());\n }\n\n const handle: ViewerHandle = {\n svg,\n panzoom: panzoomInstance,\n clusters,\n get collapsedPrefixes() {\n return collapsedPrefixes;\n },\n get activeFilter() {\n return activeFilter;\n },\n collapse(prefix: string): void {\n if (disposed) return;\n const cluster = clusters.get(prefix);\n if (!cluster) return;\n if (!collapsedPrefixes.has(prefix)) {\n setClusterCollapsed(cluster, true);\n collapsedPrefixes.add(prefix);\n opts.onClusterCollapse?.(prefix, true);\n }\n },\n expand(prefix: string): void {\n if (disposed) return;\n const cluster = clusters.get(prefix);\n if (!cluster) return;\n if (collapsedPrefixes.has(prefix)) {\n setClusterCollapsed(cluster, false);\n collapsedPrefixes.delete(prefix);\n opts.onClusterCollapse?.(prefix, false);\n }\n },\n collapseAll(): void {\n if (disposed) return;\n for (const cluster of clusters.values()) {\n if (!collapsedPrefixes.has(cluster.prefix)) {\n setClusterCollapsed(cluster, true);\n collapsedPrefixes.add(cluster.prefix);\n opts.onClusterCollapse?.(cluster.prefix, true);\n }\n }\n },\n expandAll(): void {\n if (disposed) return;\n for (const prefix of Array.from(collapsedPrefixes)) {\n const cluster = clusters.get(prefix);\n if (!cluster) continue;\n setClusterCollapsed(cluster, false);\n collapsedPrefixes.delete(prefix);\n opts.onClusterCollapse?.(prefix, false);\n }\n },\n filter(prefix: string | null): void {\n if (disposed) return;\n activeFilter = prefix;\n overlayApplyFilter(svg, prefix);\n opts.onClusterFilter?.(prefix);\n },\n resetZoom(): void {\n if (disposed) return;\n try {\n panzoomInstance.moveTo(0, 0);\n panzoomInstance.zoomAbs(0, 0, 1);\n } catch {\n // panzoom internals can throw if the element was detached; ignore.\n }\n },\n fit(): void {\n if (disposed) return;\n try {\n // With the host CSS-sized and the SVG forced to width/height: 100%\n // (see .petrinet-diagram-viewer rules in viewer.css), the browser\n // auto-fits viewBox content via preserveAspectRatio=\"xMidYMid meet\".\n // \"Fit\" is therefore the panzoom identity transform — no math needed.\n panzoomInstance.zoomAbs(0, 0, 1);\n panzoomInstance.moveTo(0, 0);\n } catch {\n // panzoom internals can throw if the element was detached; ignore.\n }\n },\n isInsideCollapsedCluster(graphId: string): boolean {\n return overlayIsInsideCollapsedCluster(svg, clusters, collapsedPrefixes, graphId);\n },\n get highlightedNodeId() {\n return highlightedNodeId;\n },\n highlight(nodeId: string | null): void {\n if (disposed) return;\n if (layoutMode !== 'elk') return;\n highlightedNodeId = nodeId;\n applyHighlight(svg, nodeId);\n },\n setVisibility(state: VisibilityState): void {\n if (disposed) return;\n if (layoutMode !== 'elk') return;\n applyVisibility(svg, state);\n },\n dispose(): void {\n if (disposed) return;\n disposed = true;\n try {\n panzoomInstance.dispose();\n } catch {\n // ignore\n }\n sidebarHandle?.dispose();\n sidebarHandle = null;\n if (chromeRoot && chromeRoot.parentNode === container) {\n container.removeChild(chromeRoot);\n }\n chromeRoot = null;\n },\n };\n\n // Re-apply preserved collapse/filter state from the previous handle so\n // a live re-render (e.g. on marking-snapshot) keeps the user's selection.\n for (const prefix of preservedCollapsed) {\n handle.collapse(prefix);\n }\n if (preservedFilter !== null) {\n handle.filter(preservedFilter);\n }\n\n // C0 click-to-highlight: clicking a node highlights it + every copy of\n // the same logical place. Clicking the SVG background (or the already-\n // highlighted node) clears. The sidebar's \"Click node → highlight\"\n // toggle gates the binding via `highlightModeEnabled`.\n if (layoutMode === 'elk') {\n svg.addEventListener('click', (ev) => {\n if (!highlightModeEnabled) return;\n const target = ev.target;\n if (!(target instanceof Element)) return;\n const nodeG = target.closest('g.node') as SVGGElement | null;\n if (!nodeG) {\n handle.highlight(null);\n return;\n }\n const id = nodeG.getAttribute('data-id');\n if (!id) return;\n handle.highlight(id === highlightedNodeId ? null : id);\n });\n }\n\n ensureChrome();\n\n // Default state: fit-to-host. Huge diagrams shouldn't mount at 1:1\n // (the user only sees the top-left corner). Deferred a frame so the\n // SVG has been laid out before getBBox + clientWidth/Height reads.\n requestAnimationFrame(() => handle.fit());\n\n return handle;\n}\n"],"mappings":"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","names":["cssEscape"]}
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Graph pre-processing for the C0-replicate-default layout pipeline.
3
+ *
4
+ * Three pure-ish transforms operate on a {@link GraphModel} parsed from a
5
+ * libpetri-exported DOT string:
6
+ *
7
+ * parseLibpetriDot(dot) → GraphModel
8
+ * foldOrphans(graph, threshold) → GraphModel (REASSIGN_ORPHANS)
9
+ * replicateShared(graph, opts) → GraphModel (REPLICATE_SHARED)
10
+ *
11
+ * The pipeline turns a flat cluster soup into a cluster-internal one by
12
+ * (a) folding orphan nodes whose edges concentrate in one cluster into that
13
+ * cluster, and (b) cloning shared places into every foreign cluster they
14
+ * touch so cross-cluster spaghetti disappears. The resulting GraphModel is
15
+ * what ELK lays out (Stage 2) and Graphviz then renders in pin-mode.
16
+ *
17
+ * Algorithm lineage: ported from
18
+ * `/home/db/otto-repos/nucleus_marvin/.scratch/javadoc-layout-exp/CHECKPOINTS/run16_GOOD_v3_skip-junctions.mjs`
19
+ * lines 14–220. The arc-type detection, replica naming
20
+ * (`${placeId}__rep__${clusterShortName}`), and the `__replica` /
21
+ * `replicaOf` markers are load-bearing — downstream stages key off them.
22
+ *
23
+ * @module viewer/layout/preprocess
24
+ */
25
+ type ArcType = 'normal' | 'reset' | 'inhibitor' | 'read';
26
+ type NodeKind = 'place' | 'transition' | 'junction';
27
+ interface ParsedNode {
28
+ readonly id: string;
29
+ readonly kind: NodeKind;
30
+ readonly attrs: Record<string, string>;
31
+ /** True iff this node was emitted by replicateShared. */
32
+ readonly replica?: boolean;
33
+ /** For a replica, the id of the original logical place. */
34
+ readonly replicaOf?: string;
35
+ }
36
+ interface ParsedEdge {
37
+ readonly src: string;
38
+ readonly dst: string;
39
+ readonly arc: ArcType;
40
+ /**
41
+ * Raw attribute body from the libpetri DOT (between the `[…]` brackets,
42
+ * brackets stripped). Carries style/penwidth/label that we want to
43
+ * round-trip verbatim so the rendered output matches `DotExporter`'s
44
+ * native look.
45
+ */
46
+ readonly rawAttrs: string;
47
+ }
48
+ interface ParsedCluster {
49
+ readonly id: string;
50
+ readonly shortName: string;
51
+ readonly nodes: readonly string[];
52
+ }
53
+ interface GraphModel {
54
+ readonly nodes: ReadonlyMap<string, ParsedNode>;
55
+ readonly clusters: ReadonlyMap<string, ParsedCluster>;
56
+ readonly nodeToCluster: ReadonlyMap<string, string>;
57
+ /** Node ids not in any cluster (rendered at the orchestrator root). */
58
+ readonly orphans: readonly string[];
59
+ readonly edges: readonly ParsedEdge[];
60
+ }
61
+ /**
62
+ * Parse a libpetri-exported DOT string into a {@link GraphModel}.
63
+ *
64
+ * Tolerates the current exporter shape (see
65
+ * `java/src/main/java/org/libpetri/export/StyleConstants.java`):
66
+ * - inhibitor arcs use `arrowhead="odot"` + color `#dc3545`
67
+ * - reset arcs use `label="reset"` + color `#fd7e14`
68
+ * - read arcs use `label="read"` + color `#6c757d`
69
+ *
70
+ * Mirrors v3 lines 24–103.
71
+ */
72
+ declare function parseLibpetriDot(dot: string): GraphModel;
73
+ /**
74
+ * Adopt orphan nodes into the cluster their edges most prefer.
75
+ *
76
+ * For each orphan, tally how many of its edges touch each cluster. If the
77
+ * dominant cluster gets at least `threshold` of the orphan's cluster-touching
78
+ * edges, the orphan is reassigned to that cluster.
79
+ *
80
+ * Mirrors v3 lines 105–136. Default threshold 0.7 matches REASSIGN_ORPHANS.
81
+ */
82
+ declare function foldOrphans(graph: GraphModel, threshold?: number): GraphModel;
83
+ interface ReplicateOptions {
84
+ /**
85
+ * Skip places that touch more than this many foreign clusters. Per the
86
+ * plan's "no cap" decision, default is `Infinity`. Set to a finite value
87
+ * to suppress replication of hub places that would otherwise produce many
88
+ * copies.
89
+ */
90
+ readonly max?: number;
91
+ /** If true, replicate transitions as well as places. Default false. */
92
+ readonly replicateTransitions?: boolean;
93
+ }
94
+ interface ReplicateStats {
95
+ readonly replicatedPlaces: number;
96
+ readonly totalCopies: number;
97
+ }
98
+ /**
99
+ * For each place P, replicate it into every foreign cluster touched by its
100
+ * edges, and redirect those cross-cluster edges to the local copy.
101
+ *
102
+ * Replica id convention is load-bearing: `${placeId}__rep__${shortClusterName}`.
103
+ * Downstream stages (mount-time tagging, overlay highlight, click-all-copies)
104
+ * key off this naming and the `replica: true` / `replicaOf` markers.
105
+ *
106
+ * If P has a home cluster (folded earlier), the original stays as the primary
107
+ * copy and is also marked as a replica so the ⇄ glyph renders consistently.
108
+ * If P is an orphan with all its edges redirected, the original is dropped.
109
+ *
110
+ * Mirrors v3 lines 138–221.
111
+ */
112
+ declare function replicateShared(graph: GraphModel, opts?: ReplicateOptions): GraphModel & {
113
+ readonly replicateStats: ReplicateStats;
114
+ };
115
+
116
+ /**
117
+ * ELK placement + DOT pin-mode rewrite.
118
+ *
119
+ * Stage 2 of the C0 pipeline. Takes the {@link GraphModel} produced by
120
+ * {@link parseLibpetriDot} → {@link foldOrphans} → {@link replicateShared},
121
+ * runs ELKjs to compute absolute `(x, y)` per node and bounding boxes per
122
+ * cluster, then emits a fresh DOT string with positions pinned via
123
+ * `pos="x,y!"` and `bb="…"`. Downstream callers render the result through
124
+ * `@viz-js/viz` with `engine: 'neato'` + `graphAttributes: { nop: '1' }` —
125
+ * Graphviz uses the pinned positions verbatim and routes edges around the
126
+ * pinned cluster boundaries.
127
+ *
128
+ * The two functions are pure: `elkLayout` takes only the graph; `writeBack`
129
+ * takes (graph, layout). Neither touches the original DOT text — the model
130
+ * after replication has nodes the original DOT doesn't, so patching the
131
+ * original would be incorrect.
132
+ *
133
+ * @module viewer/layout/elk-place
134
+ */
135
+
136
+ interface NodePosition {
137
+ readonly x: number;
138
+ readonly y: number;
139
+ readonly width: number;
140
+ readonly height: number;
141
+ }
142
+ interface ClusterBox {
143
+ readonly x: number;
144
+ readonly y: number;
145
+ readonly width: number;
146
+ readonly height: number;
147
+ }
148
+ interface EdgePath {
149
+ readonly start: {
150
+ x: number;
151
+ y: number;
152
+ };
153
+ readonly end: {
154
+ x: number;
155
+ y: number;
156
+ };
157
+ readonly bends: ReadonlyArray<{
158
+ x: number;
159
+ y: number;
160
+ }>;
161
+ }
162
+ interface LayoutResult {
163
+ readonly nodePositions: ReadonlyMap<string, NodePosition>;
164
+ readonly clusterBoxes: ReadonlyMap<string, ClusterBox>;
165
+ /** Keyed on `${src}->${dst}` — ELK-computed edge routes for `nop=2`. */
166
+ readonly edgePaths: ReadonlyMap<string, EdgePath>;
167
+ readonly totalWidth: number;
168
+ readonly totalHeight: number;
169
+ }
170
+ /**
171
+ * Run ELK layout against the C0 graph and return absolute positions.
172
+ *
173
+ * Orphan nodes are wrapped in a synthetic `cluster_orchestrator` so ELK
174
+ * lays them out as a single block alongside the real subnets, which the
175
+ * `rectpacking` root algorithm then packs.
176
+ */
177
+ declare function elkLayout(graph: GraphModel): Promise<LayoutResult>;
178
+ /**
179
+ * Emit a Graphviz DOT string with positions pinned. Render the result with
180
+ * `@viz-js/viz` using:
181
+ *
182
+ * ```ts
183
+ * viz.renderSVGElement(dot, {
184
+ * engine: 'nop',
185
+ * yInvert: true,
186
+ * });
187
+ * ```
188
+ *
189
+ * `engine: 'nop'` (a.k.a. `nop1`) honours the pinned `pos=` on every node
190
+ * and routes edges around them. We use this rather than `nop2` because
191
+ * `nop2` would force us to provide a spline for every edge ourselves (no
192
+ * routing) — and Graphviz's own routing produces correctly-clipped
193
+ * arrowheads at the visual node boundary, which our ELK polylines did not
194
+ * (ELK uses the layout-reserved bbox, our visual circle is much smaller).
195
+ *
196
+ * `yInvert: true` matches ELK's Y-down convention to Graphviz's Y-up.
197
+ */
198
+ declare function writeBack(graph: GraphModel, layout: LayoutResult): string;
199
+
200
+ export { type ArcType, type ClusterBox, type GraphModel, type LayoutResult, type NodeKind, type NodePosition, type ParsedCluster, type ParsedEdge, type ParsedNode, type ReplicateOptions, type ReplicateStats, elkLayout, foldOrphans, parseLibpetriDot, replicateShared, writeBack };
@@ -0,0 +1,15 @@
1
+ import {
2
+ elkLayout,
3
+ foldOrphans,
4
+ parseLibpetriDot,
5
+ replicateShared,
6
+ writeBack
7
+ } from "../../chunk-RNWTYK5B.js";
8
+ export {
9
+ elkLayout,
10
+ foldOrphans,
11
+ parseLibpetriDot,
12
+ replicateShared,
13
+ writeBack
14
+ };
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}