@jimka/typescript-ui 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/dist/lib/{AbstractBooleanInput-DSwKBAic.js → AbstractBooleanInput-CbW7ZrDT.js} +2 -2
  2. package/dist/lib/{AbstractBooleanInput-DSwKBAic.js.map → AbstractBooleanInput-CbW7ZrDT.js.map} +1 -1
  3. package/dist/lib/{AbstractInput-BmWGJTs1.js → AbstractInput-B10DfLOf.js} +2 -2
  4. package/dist/lib/{AbstractInput-BmWGJTs1.js.map → AbstractInput-B10DfLOf.js.map} +1 -1
  5. package/dist/lib/Anchor-BsblS5it.js +2 -0
  6. package/dist/lib/Anchor-BsblS5it.js.map +1 -0
  7. package/dist/lib/AnchorConstraints-Bfkt8019.js +2 -0
  8. package/dist/lib/AnchorConstraints-Bfkt8019.js.map +1 -0
  9. package/dist/lib/{AnimatedDropdown-DKas40B-.js → AnimatedDropdown-kL47HPGP.js} +2 -2
  10. package/dist/lib/{AnimatedDropdown-DKas40B-.js.map → AnimatedDropdown-kL47HPGP.js.map} +1 -1
  11. package/dist/lib/{Border-Bz29z-_7.js → Border-CNS6aEXB.js} +2 -2
  12. package/dist/lib/Border-CNS6aEXB.js.map +1 -0
  13. package/dist/lib/Button-T16zSZip.js +2 -0
  14. package/dist/lib/Button-T16zSZip.js.map +1 -0
  15. package/dist/lib/{Card-BdfbvOD_.js → Card-CD3jxshU.js} +2 -2
  16. package/dist/lib/{Card-BdfbvOD_.js.map → Card-CD3jxshU.js.map} +1 -1
  17. package/dist/lib/CodeEditor-BGlB232h.js +2 -0
  18. package/dist/lib/CodeEditor-BGlB232h.js.map +1 -0
  19. package/dist/lib/ComboBox-C9FYMuLv.js +2 -0
  20. package/dist/lib/ComboBox-C9FYMuLv.js.map +1 -0
  21. package/dist/lib/Component-w65rPK75.js +2 -0
  22. package/dist/lib/Component-w65rPK75.js.map +1 -0
  23. package/dist/lib/Container-C7RPLQEc.js +2 -0
  24. package/dist/lib/{Container-CAQpj4yb.js.map → Container-C7RPLQEc.js.map} +1 -1
  25. package/dist/lib/DOM-Ba8YRVel.js +2 -0
  26. package/dist/lib/{DOM-67StIm5G.js.map → DOM-Ba8YRVel.js.map} +1 -1
  27. package/dist/lib/DragManager-UvPHSoRm.js +2 -0
  28. package/dist/lib/{DragManager-CHWCe3-8.js.map → DragManager-UvPHSoRm.js.map} +1 -1
  29. package/dist/lib/{FieldDecorator-CgAGcqCk.js → FieldDecorator-HG65hEtk.js} +2 -2
  30. package/dist/lib/{FieldDecorator-CgAGcqCk.js.map → FieldDecorator-HG65hEtk.js.map} +1 -1
  31. package/dist/lib/{Fit-DIYt2aQB.js → Fit-Cg3dPyZg.js} +2 -2
  32. package/dist/lib/{Fit-DIYt2aQB.js.map → Fit-Cg3dPyZg.js.map} +1 -1
  33. package/dist/lib/FloatingPanel-D5buuVrQ.js +2 -0
  34. package/dist/lib/FloatingPanel-D5buuVrQ.js.map +1 -0
  35. package/dist/lib/{Glyph-CeAIWyPB.js → Glyph-DeJF_zsR.js} +2 -2
  36. package/dist/lib/{Glyph-CeAIWyPB.js.map → Glyph-DeJF_zsR.js.map} +1 -1
  37. package/dist/lib/{Grid-ViA0joow.js → Grid-BYBohJIA.js} +2 -2
  38. package/dist/lib/{Grid-ViA0joow.js.map → Grid-BYBohJIA.js.map} +1 -1
  39. package/dist/lib/{GridConstraints-B1rxCw9B.js → GridConstraints-Bts284FI.js} +2 -2
  40. package/dist/lib/{GridConstraints-B1rxCw9B.js.map → GridConstraints-Bts284FI.js.map} +1 -1
  41. package/dist/lib/{Header-mKaqgfat.js → Header-BIFJK9nR.js} +2 -2
  42. package/dist/lib/{Header-mKaqgfat.js.map → Header-BIFJK9nR.js.map} +1 -1
  43. package/dist/lib/{IconText-BqwXA82-.js → IconText-Dsb1xS-E.js} +2 -2
  44. package/dist/lib/{IconText-BqwXA82-.js.map → IconText-Dsb1xS-E.js.map} +1 -1
  45. package/dist/lib/LanguageRegistry-CcVnbKuh.js +2 -0
  46. package/dist/lib/LanguageRegistry-CcVnbKuh.js.map +1 -0
  47. package/dist/lib/LayoutConstraints-LIHYYd2F.js +2 -0
  48. package/dist/lib/LayoutConstraints-LIHYYd2F.js.map +1 -0
  49. package/dist/lib/LayoutSerialization-ySzY1h2l.js +2 -0
  50. package/dist/lib/{LayoutSerialization-D82UtnDD.js.map → LayoutSerialization-ySzY1h2l.js.map} +1 -1
  51. package/dist/lib/LayoutSizes-DKDK1Cle.js +2 -0
  52. package/dist/lib/LayoutSizes-DKDK1Cle.js.map +1 -0
  53. package/dist/lib/List-BPd-IXZR.js +2 -0
  54. package/dist/lib/{List-CUntS8ux.js.map → List-BPd-IXZR.js.map} +1 -1
  55. package/dist/lib/MemoryStore-BgP7-Uzn.js +2 -0
  56. package/dist/lib/{MemoryStore-DT6iWWco.js.map → MemoryStore-BgP7-Uzn.js.map} +1 -1
  57. package/dist/lib/Menu-C0roOCY7.js +2 -0
  58. package/dist/lib/Menu-C0roOCY7.js.map +1 -0
  59. package/dist/lib/MenuButton-BR4KOFld.js +2 -0
  60. package/dist/lib/MenuButton-BR4KOFld.js.map +1 -0
  61. package/dist/lib/Panel-CpNQTLNF.js +2 -0
  62. package/dist/lib/{Panel-D2X33qR6.js.map → Panel-CpNQTLNF.js.map} +1 -1
  63. package/dist/lib/{Position-Ck_KD4cR.js → Position-PBVBW1JS.js} +2 -2
  64. package/dist/lib/{Position-Ck_KD4cR.js.map → Position-PBVBW1JS.js.map} +1 -1
  65. package/dist/lib/{ProgressSpinner-DCyq02pd.js → ProgressSpinner-lAsK26C-.js} +2 -2
  66. package/dist/lib/{ProgressSpinner-DCyq02pd.js.map → ProgressSpinner-lAsK26C-.js.map} +1 -1
  67. package/dist/lib/RadioButton-CCcIaL54.js +2 -0
  68. package/dist/lib/RadioButton-CCcIaL54.js.map +1 -0
  69. package/dist/lib/{RovingTabIndex-DFJyoveq.js → RovingTabIndex-C2Vzq0IX.js} +2 -2
  70. package/dist/lib/{RovingTabIndex-DFJyoveq.js.map → RovingTabIndex-C2Vzq0IX.js.map} +1 -1
  71. package/dist/lib/Scrollbar-C3ZZyZ1e.js +2 -0
  72. package/dist/lib/Scrollbar-C3ZZyZ1e.js.map +1 -0
  73. package/dist/lib/Slider-DZ9drUxn.js +2 -0
  74. package/dist/lib/Slider-DZ9drUxn.js.map +1 -0
  75. package/dist/lib/Spacer-SDhOMp2g.js +2 -0
  76. package/dist/lib/{Spacer-D159iJy9.js.map → Spacer-SDhOMp2g.js.map} +1 -1
  77. package/dist/lib/TabButton-DNo4ZgV8.js +2 -0
  78. package/dist/lib/TabButton-DNo4ZgV8.js.map +1 -0
  79. package/dist/lib/Text-CdHf3B-1.js +2 -0
  80. package/dist/lib/Text-CdHf3B-1.js.map +1 -0
  81. package/dist/lib/{Tooltip-DzK6LxUt.js → Tooltip-B_UF5bXD.js} +2 -2
  82. package/dist/lib/{Tooltip-DzK6LxUt.js.map → Tooltip-B_UF5bXD.js.map} +1 -1
  83. package/dist/lib/Tree-C7qLbIYH.js +2 -0
  84. package/dist/lib/Tree-C7qLbIYH.js.map +1 -0
  85. package/dist/lib/VBox-32fxpkDl.js +2 -0
  86. package/dist/lib/VBox-32fxpkDl.js.map +1 -0
  87. package/dist/lib/{Validator-Du8PqS5i.js → Validator-Cjs1EU5Q.js} +2 -2
  88. package/dist/lib/{Validator-Du8PqS5i.js.map → Validator-Cjs1EU5Q.js.map} +1 -1
  89. package/dist/lib/{VirtualScroller-zQr-vKrO.js → VirtualScroller-cvFNMS7U.js} +2 -2
  90. package/dist/lib/{VirtualScroller-zQr-vKrO.js.map → VirtualScroller-cvFNMS7U.js.map} +1 -1
  91. package/dist/lib/component/button.es.js +1 -1
  92. package/dist/lib/component/button.es.js.map +1 -1
  93. package/dist/lib/component/chart.es.js +1 -1
  94. package/dist/lib/component/chart.es.js.map +1 -1
  95. package/dist/lib/component/container.es.js +1 -1
  96. package/dist/lib/component/container.es.js.map +1 -1
  97. package/dist/lib/component/diagram.es.js +1 -1
  98. package/dist/lib/component/diagram.es.js.map +1 -1
  99. package/dist/lib/component/display.es.js +1 -1
  100. package/dist/lib/component/display.es.js.map +1 -1
  101. package/dist/lib/component/editor.es.js +3 -3
  102. package/dist/lib/component/editor.es.js.map +1 -1
  103. package/dist/lib/component/input.es.js +1 -1
  104. package/dist/lib/component/input.es.js.map +1 -1
  105. package/dist/lib/component/list.es.js +1 -1
  106. package/dist/lib/component/list.es.js.map +1 -1
  107. package/dist/lib/component/menubar.es.js +1 -1
  108. package/dist/lib/component/menubar.es.js.map +1 -1
  109. package/dist/lib/component/table.es.js +2 -2
  110. package/dist/lib/component/table.es.js.map +1 -1
  111. package/dist/lib/component/tree.es.js +1 -1
  112. package/dist/lib/component/tree.es.js.map +1 -1
  113. package/dist/lib/core.es.js +1 -1
  114. package/dist/lib/core.es.js.map +1 -1
  115. package/dist/lib/data.es.js +1 -1
  116. package/dist/lib/glyphs/solid/index.es.js +1 -1
  117. package/dist/lib/languages-Dl8qdL8a.js +2 -0
  118. package/dist/lib/languages-Dl8qdL8a.js.map +1 -0
  119. package/dist/lib/layout.es.js +1 -1
  120. package/dist/lib/layout.es.js.map +1 -1
  121. package/dist/lib/overlay.es.js +1 -1
  122. package/dist/lib/overlay.es.js.map +1 -1
  123. package/dist/lib/primitive.es.js +1 -1
  124. package/dist/lib/router.es.js +1 -1
  125. package/dist/lib/router.es.js.map +1 -1
  126. package/dist/lib/selectionsEqual-BrTxPMmK.js +2 -0
  127. package/dist/lib/{selectionsEqual-4r5R7pTK.js.map → selectionsEqual-BrTxPMmK.js.map} +1 -1
  128. package/dist/lib/types/component/button/MenuButton.d.ts +1 -0
  129. package/dist/lib/types/component/button/SplitButton.d.ts +1 -0
  130. package/dist/lib/types/component/button/TabButton.d.ts +2 -1
  131. package/dist/lib/types/component/button/ToggleButton.d.ts +1 -1
  132. package/dist/lib/types/component/chart/ChartLegend.d.ts +1 -2
  133. package/dist/lib/types/component/container/CollapseButton.d.ts +0 -1
  134. package/dist/lib/types/component/container/FloatingPanel.d.ts +23 -0
  135. package/dist/lib/types/component/container/ScrollStrip.d.ts +1 -1
  136. package/dist/lib/types/component/container/Scrollbar.d.ts +1 -3
  137. package/dist/lib/types/component/container/TabBar.d.ts +1 -1
  138. package/dist/lib/types/component/container/TabPanel.d.ts +3 -0
  139. package/dist/lib/types/component/container/index.d.ts +2 -0
  140. package/dist/lib/types/component/diagram/DiagramGroupNode.d.ts +1 -1
  141. package/dist/lib/types/component/display/Markdown.d.ts +42 -1
  142. package/dist/lib/types/component/display/MarkdownMinimap.d.ts +38 -0
  143. package/dist/lib/types/component/display/MarkdownViewer.d.ts +60 -0
  144. package/dist/lib/types/component/display/index.d.ts +6 -0
  145. package/dist/lib/types/component/editor/CodeEditor.d.ts +13 -0
  146. package/dist/lib/types/component/editor/index.d.ts +1 -1
  147. package/dist/lib/types/component/input/Checkbox.d.ts +1 -1
  148. package/dist/lib/types/component/input/Link.d.ts +0 -1
  149. package/dist/lib/types/component/input/RadioButton.d.ts +1 -1
  150. package/dist/lib/types/component/input/Slider.d.ts +1 -1
  151. package/dist/lib/types/component/input/Text.d.ts +0 -2
  152. package/dist/lib/types/component/input/Toggle.d.ts +1 -1
  153. package/dist/lib/types/component/menubar/MenuBar.d.ts +2 -1
  154. package/dist/lib/types/component/menubar/MenuBarButton.d.ts +0 -1
  155. package/dist/lib/types/component/menubar/ToolBar.d.ts +2 -0
  156. package/dist/lib/types/component/table/Body.d.ts +1 -0
  157. package/dist/lib/types/component/table/Table.d.ts +1 -0
  158. package/dist/lib/types/component/table/cell/SortPriorityBadge.d.ts +1 -1
  159. package/dist/lib/types/component/table/cell/editor/CellEditorPool.d.ts +1 -0
  160. package/dist/lib/types/component/table/cell/editor/Date.d.ts +1 -0
  161. package/dist/lib/types/component/table/cell/editor/DateTime.d.ts +1 -0
  162. package/dist/lib/types/component/table/cell/editor/Time.d.ts +1 -0
  163. package/dist/lib/types/component/tree/Tree.d.ts +9 -1
  164. package/dist/lib/types/component/tree/index.d.ts +1 -1
  165. package/dist/lib/types/component/tree/renderer/Label.d.ts +1 -1
  166. package/dist/lib/types/core/BorderWidths.d.ts +12 -0
  167. package/dist/lib/types/core/Component.d.ts +6 -0
  168. package/dist/lib/types/core/Event.d.ts +2 -0
  169. package/dist/lib/types/core/Theme.d.ts +4 -2
  170. package/dist/lib/types/layout/LayoutConstraints.d.ts +1 -0
  171. package/dist/lib/types/layout/LayoutManager.d.ts +8 -0
  172. package/dist/lib/types/layout/LayoutSerialization.d.ts +3 -0
  173. package/dist/lib/types/overlay/Dock.d.ts +1 -0
  174. package/dist/lib/validation.es.js +1 -1
  175. package/llms.txt +3 -0
  176. package/package.json +15 -15
  177. package/dist/lib/AnchorConstraints-CVrMP9rz.js +0 -2
  178. package/dist/lib/AnchorConstraints-CVrMP9rz.js.map +0 -1
  179. package/dist/lib/Border-Bz29z-_7.js.map +0 -1
  180. package/dist/lib/Button-bjpWruNc.js +0 -2
  181. package/dist/lib/Button-bjpWruNc.js.map +0 -1
  182. package/dist/lib/ComboBox-CBw2VLLN.js +0 -2
  183. package/dist/lib/ComboBox-CBw2VLLN.js.map +0 -1
  184. package/dist/lib/Component-DHqrPwAw.js +0 -2
  185. package/dist/lib/Component-DHqrPwAw.js.map +0 -1
  186. package/dist/lib/Container-CAQpj4yb.js +0 -2
  187. package/dist/lib/DOM-67StIm5G.js +0 -2
  188. package/dist/lib/DragManager-CHWCe3-8.js +0 -2
  189. package/dist/lib/LayoutConstraints-DkctJZXB.js +0 -2
  190. package/dist/lib/LayoutConstraints-DkctJZXB.js.map +0 -1
  191. package/dist/lib/LayoutSerialization-D82UtnDD.js +0 -2
  192. package/dist/lib/LayoutSizes-DG9aW9k6.js +0 -2
  193. package/dist/lib/LayoutSizes-DG9aW9k6.js.map +0 -1
  194. package/dist/lib/List-CUntS8ux.js +0 -2
  195. package/dist/lib/MemoryStore-DT6iWWco.js +0 -2
  196. package/dist/lib/Menu-B_-SQj72.js +0 -2
  197. package/dist/lib/Menu-B_-SQj72.js.map +0 -1
  198. package/dist/lib/MenuButton-C79l8Ysf.js +0 -2
  199. package/dist/lib/MenuButton-C79l8Ysf.js.map +0 -1
  200. package/dist/lib/Panel-D2X33qR6.js +0 -2
  201. package/dist/lib/RadioButton-BJIAQkII.js +0 -2
  202. package/dist/lib/RadioButton-BJIAQkII.js.map +0 -1
  203. package/dist/lib/Scrollbar-CSFK-Y57.js +0 -2
  204. package/dist/lib/Scrollbar-CSFK-Y57.js.map +0 -1
  205. package/dist/lib/Slider-C-kQQYCj.js +0 -2
  206. package/dist/lib/Slider-C-kQQYCj.js.map +0 -1
  207. package/dist/lib/Spacer-D159iJy9.js +0 -2
  208. package/dist/lib/TabButton-CHkXBgCj.js +0 -2
  209. package/dist/lib/TabButton-CHkXBgCj.js.map +0 -1
  210. package/dist/lib/Text-D5pg7taZ.js +0 -2
  211. package/dist/lib/Text-D5pg7taZ.js.map +0 -1
  212. package/dist/lib/VBox-BNegDPs_.js +0 -2
  213. package/dist/lib/VBox-BNegDPs_.js.map +0 -1
  214. package/dist/lib/selectionsEqual-4r5R7pTK.js +0 -2
@@ -1 +1 @@
1
- {"version":3,"file":"diagram.es.js","names":[],"sources":["../../../src/typescript/lib/component/diagram/ElkLayoutEngine.ts","../../../src/typescript/lib/component/diagram/DiagramNode.ts","../../../src/typescript/lib/component/diagram/DiagramGroupNode.ts","../../../src/typescript/lib/component/diagram/DiagramEdgeLayer.ts","../../../src/typescript/lib/component/diagram/DiagramView.ts"],"sourcesContent":["// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n//\n// The sole adapter between the framework-native diagram model and ElkJS. It is\n// the only module that names ELK types or imports `elkjs`; everything else in\n// the family speaks `DiagramData` / `DiagramLayoutResult`, keeping the layout\n// engine swappable. ELK takes JSON and returns JSON — it never touches the DOM,\n// so it needs no `DOM.sink` / `DOM.source` seam. `elkjs` is an optional peer\n// dependency imported lazily the first time a diagram lays out (mirroring\n// `StoreWorkerClient.ensureWorker`) and externalised from the library bundle so\n// its GWT blob never lands in the core chunk.\n\nimport type { DiagramData, DiagramNodeData } from \"~/component/diagram/DiagramModel.js\";\n\n/** A point in ELK's layout coordinate space. */\nexport interface ElkPoint {\n x: number;\n y: number;\n}\n\n/**\n * One routed section of an ELK edge: a straight run from `startPoint` to\n * `endPoint`, optionally threaded through `bendPoints`.\n */\nexport interface ElkEdgeSection {\n startPoint: ElkPoint;\n endPoint: ElkPoint;\n bendPoints?: ElkPoint[];\n}\n\n/** The engine-agnostic layout result consumed by `DiagramView`. */\nexport interface DiagramLayoutResult {\n /** Each node's absolute position and size in graph space. */\n nodes: Array<{ id: string; x: number; y: number; width: number; height: number }>;\n /** Each edge's routed sections. */\n edges: Array<{ id: string; sections: ElkEdgeSection[] }>;\n /** Graph bounding-box width. */\n width: number;\n /** Graph bounding-box height. */\n height: number;\n}\n\n/** ELK port shape: a fixed anchor on a node an edge can attach to. */\ninterface ElkPort {\n id: string;\n x?: number;\n y?: number;\n width?: number;\n height?: number;\n layoutOptions?: Record<string, string>;\n}\n\n/** ELK graph node shape (input carries sizes/options; output adds x/y). */\ninterface ElkNode {\n id: string;\n width?: number;\n height?: number;\n x?: number;\n y?: number;\n layoutOptions?: Record<string, string>;\n children?: ElkNode[];\n edges?: ElkExtendedEdge[];\n ports?: ElkPort[];\n}\n\n/** ELK edge shape with explicit source/target endpoint lists. */\ninterface ElkExtendedEdge {\n id: string;\n sources: string[];\n targets: string[];\n sections?: ElkEdgeSection[];\n /**\n * The node whose origin the routed `sections` coordinates are relative to,\n * as reported by ELK. For an edge between two nodes nested inside the same\n * container, ELK routes it in that container and reports container-relative\n * coordinates here (see {@link mapElkResult}, which shifts them to absolute).\n */\n container?: string;\n}\n\n/** Minimal structural type for the lazily-imported ELK instance. */\ninterface ElkInstance {\n layout(graph: unknown): Promise<unknown>;\n terminateWorker(): void;\n}\n\n/**\n * Fallback node width in pixels, used when a node carries neither an explicit\n * `width` nor a measurable preferred width. Sized to comfortably hold a short\n * label so a graph still lays out sensibly rather than collapsing to zero-area\n * nodes ELK would overlap.\n */\nconst DEFAULT_NODE_WIDTH = 120;\n\n/**\n * Fallback node height in pixels, mirroring {@link DEFAULT_NODE_WIDTH}. Tracks a\n * single label line plus vertical breathing room.\n */\nconst DEFAULT_NODE_HEIGHT = 40;\n\n/** Message carried by the rejection a disposed engine's `layout` produces. */\nconst DISPOSED_MESSAGE = \"ElkLayoutEngine has been disposed\";\n\n/**\n * Merges layout-option maps left-to-right, so later arguments win. `undefined`\n * maps are skipped. Used to layer graph options over the view-level defaults.\n *\n * @param maps - Option maps in ascending precedence order.\n * @returns The merged option map.\n */\nfunction mergeLayoutOptions(...maps: Array<Record<string, string> | undefined>): Record<string, string> {\n const merged: Record<string, string> = {};\n\n for (const map of maps) {\n if (map) {\n Object.assign(merged, map);\n }\n }\n\n return merged;\n}\n\n/**\n * Default padding reserved inside every container's ELK box, keyed by side.\n * The default renderer (`DiagramGroupNode`) paints its header label a few\n * pixels from the top-left corner, so without a reserved top inset ELK could\n * place a child flush against — or under — the title. The top inset reserves\n * the header row (its inset + one text/glyph line) plus a ~10px gap so the\n * first child sits clearly below the title, not tight against it. The\n * left/bottom/right values match ELK's own built-in default (12px); a consumer\n * supplying a custom `groupRenderer` with a taller or shorter header overrides\n * this via the container's own `layoutOptions`.\n */\nconst CONTAINER_PADDING_DEFAULT: Record<string, string> = { \"elk.padding\": \"[top=34,left=12,bottom=12,right=12]\" };\n\n/**\n * Maps one framework-native node to its ELK counterpart, recursing into\n * `children` for a compound container. A container (non-empty `children`)\n * carries no explicit `width`/`height` — ELK computes its box from its\n * contents — and its children are mapped the same way, so nesting is\n * unbounded. A container's `layoutOptions` resolve as\n * `CONTAINER_PADDING_DEFAULT` < `node.layoutOptions` (the node's own option\n * wins), reserving header clearance by default while staying overridable. A\n * leaf (no children) maps exactly as before.\n *\n * @param node - The framework-native node.\n * @param sizes - Per-leaf resolved sizes (explicit size, else preferred size).\n * @returns The mapped ELK node.\n */\nfunction mapDiagramNode(\n node: DiagramNodeData,\n sizes: Map<string, { width: number; height: number }>,\n): ElkNode {\n if (node.children && node.children.length > 0) {\n return {\n id: node.id,\n layoutOptions: mergeLayoutOptions(CONTAINER_PADDING_DEFAULT, node.layoutOptions),\n children: node.children.map((child) => mapDiagramNode(child, sizes)),\n };\n }\n\n const size = sizes.get(node.id);\n\n return {\n id: node.id,\n width: node.width ?? size?.width ?? DEFAULT_NODE_WIDTH,\n height: node.height ?? size?.height ?? DEFAULT_NODE_HEIGHT,\n layoutOptions: node.layoutOptions,\n ports: node.ports?.map((p) => ({\n id: p.id,\n x: p.x,\n y: p.y,\n width: p.width,\n height: p.height,\n layoutOptions: p.side !== undefined ? { \"elk.port.side\": p.side } : undefined,\n })),\n };\n}\n\n/**\n * Root-level ELK options enabling cross-container edge routing. Without it,\n * an edge declared on the root (as every {@link DiagramEdgeData} is) between\n * two nodes nested under different containers may be dropped or mis-routed by\n * ELK's hierarchical layout. Merged in as the lowest-precedence tier, so a\n * view-level default or a graph's own `layoutOptions` can still override it.\n */\nconst HIERARCHY_HANDLING_DEFAULT: Record<string, string> = { \"elk.hierarchyHandling\": \"INCLUDE_CHILDREN\" };\n\n/**\n * Maps the framework-native graph to an ELK graph JSON. Pure and synchronous —\n * no `elkjs` import — so it is unit-testable directly.\n *\n * Graph-level options resolve as `HIERARCHY_HANDLING_DEFAULT` < `defaults` <\n * `data.layoutOptions` (graph wins over view defaults, which win over the\n * hierarchy-handling default) on the root; each node carries its own\n * `layoutOptions`, which ELK resolves over the inherited root options so a\n * per-node option wins over all three.\n *\n * @param data - The framework-native graph.\n * @param sizes - Per-node resolved sizes (explicit size, else preferred size).\n * @param defaults - View-level default ELK options applied to every layout.\n * @returns The ELK graph JSON ready for `elk.layout`.\n */\nexport function buildElkGraph(\n data: DiagramData,\n sizes: Map<string, { width: number; height: number }>,\n defaults?: Record<string, string>,\n): ElkNode {\n const children: ElkNode[] = data.nodes.map((node) => mapDiagramNode(node, sizes));\n\n const edges: ElkExtendedEdge[] = data.edges.map((edge) => ({\n id: edge.id,\n sources: [edge.sourcePort ?? edge.source],\n targets: [edge.targetPort ?? edge.target],\n }));\n\n return {\n id: \"root\",\n layoutOptions: mergeLayoutOptions(HIERARCHY_HANDLING_DEFAULT, defaults, data.layoutOptions),\n children,\n edges,\n };\n}\n\n/**\n * Recursively flattens an ELK node (and its `children`, if any) into\n * `out`, threading an `(offsetX, offsetY)` accumulator. ELK reports each\n * child's `x`/`y` relative to its own parent, so a node's absolute position is\n * its parent's absolute origin plus its own relative `x`/`y`; a container then\n * recurses with that absolute origin as the new offset for its own children.\n *\n * @param node - The ELK node to flatten (a container or a leaf).\n * @param offsetX - The accumulated absolute x of `node`'s parent (0 at the root).\n * @param offsetY - The accumulated absolute y of `node`'s parent (0 at the root).\n * @param out - The flat output array nodes are appended to, in traversal order.\n */\nfunction flattenElkNode(\n node: ElkNode,\n offsetX: number,\n offsetY: number,\n out: Array<{ id: string; x: number; y: number; width: number; height: number }>,\n): void {\n const x = offsetX + (node.x ?? 0);\n const y = offsetY + (node.y ?? 0);\n\n out.push({ id: node.id, x, y, width: node.width ?? 0, height: node.height ?? 0 });\n\n for (const child of node.children ?? []) {\n flattenElkNode(child, x, y, out);\n }\n}\n\n/**\n * Collects every ELK edge in the tree — the root's own `edges` plus any nested\n * in a container's `edges` — resolving each edge's container id (the node its\n * routed coordinates are relative to). ELK tags a routed edge with its own\n * `container`; absent that, the edge is relative to the node whose `edges`\n * array holds it (the root for a flat graph).\n *\n * @param node - The ELK node whose `edges` (and descendants') to collect.\n * @param out - The flat output array edges are appended to, each paired with\n * its resolved container id.\n */\nfunction collectElkEdges(node: ElkNode, out: Array<{ edge: ElkExtendedEdge; container: string }>): void {\n for (const edge of node.edges ?? []) {\n out.push({ edge, container: edge.container ?? node.id });\n }\n\n for (const child of node.children ?? []) {\n collectElkEdges(child, out);\n }\n}\n\n/**\n * Shifts every point of an edge's routed sections by `origin`, translating\n * container-relative ELK coordinates into absolute graph space. Returns the\n * sections unchanged when `origin` is the graph origin, so a flat graph's\n * root-relative edges pass through untouched.\n *\n * @param sections - The edge's routed sections, in their container's frame.\n * @param origin - The absolute origin of the edge's container.\n * @returns The sections with every point translated into absolute coordinates.\n */\nfunction offsetSections(sections: ElkEdgeSection[], origin: ElkPoint): ElkEdgeSection[] {\n if (origin.x === 0 && origin.y === 0) {\n return sections;\n }\n\n const shift = (point: ElkPoint): ElkPoint => ({ x: point.x + origin.x, y: point.y + origin.y });\n\n return sections.map((section) => ({\n startPoint: shift(section.startPoint),\n endPoint: shift(section.endPoint),\n bendPoints: section.bendPoints?.map(shift),\n }));\n}\n\n/**\n * Maps an ELK layout result back to the engine-agnostic\n * {@link DiagramLayoutResult}. Pure and synchronous, so it is unit-testable\n * directly. A container's descendants are flattened into the same output list\n * as its siblings, each carrying absolute (not parent-relative) coordinates —\n * see {@link flattenElkNode}. Edge sections get the same treatment: ELK reports\n * an intra-container edge's route relative to that container's origin, so each\n * edge is shifted by its container's absolute position (a flat graph's\n * root-relative edges are shifted by the zero origin, i.e. left unchanged).\n *\n * @param result - The root ELK node returned by `elk.layout`.\n * @returns The mapped layout result.\n */\nexport function mapElkResult(result: ElkNode): DiagramLayoutResult {\n const nodes: Array<{ id: string; x: number; y: number; width: number; height: number }> = [];\n\n for (const child of result.children ?? []) {\n flattenElkNode(child, 0, 0, nodes);\n }\n\n // Absolute origin of every node (containers included), so an edge routed in\n // a container's frame can be lifted into absolute graph space. The root maps\n // to the zero origin.\n const origins = new Map<string, ElkPoint>([[result.id, { x: 0, y: 0 }]]);\n\n for (const node of nodes) {\n origins.set(node.id, { x: node.x, y: node.y });\n }\n\n const collected: Array<{ edge: ElkExtendedEdge; container: string }> = [];\n collectElkEdges(result, collected);\n\n const edges = collected.map(({ edge, container }) => ({\n id: edge.id,\n sections: offsetSections(edge.sections ?? [], origins.get(container) ?? { x: 0, y: 0 }),\n }));\n\n return {\n nodes,\n edges,\n width: result.width ?? 0,\n height: result.height ?? 0,\n };\n}\n\n/** Construction options for {@link ElkLayoutEngine}. */\nexport interface ElkLayoutEngineOptions {\n /**\n * Consumer-provided factory returning a Web Worker for off-thread ELK\n * layout. Takes precedence over {@link ElkLayoutEngineOptions.workerUrl}\n * when both are set.\n */\n workerFactory?: () => Worker;\n /**\n * URL of a consumer-hosted `elk-worker.js`, requesting off-thread layout.\n * With the `elk.bundled.js` module this engine imports, elkjs's own\n * worker-availability check (`require.resolve('web-worker')`) always\n * fails — the browserify module system that check runs against has no\n * `.resolve` — so `workerUrl` alone never actually constructs a Worker\n * here; elkjs logs its own console warning and runs on the main thread via\n * its in-process fallback. Pass\n * {@link ElkLayoutEngineOptions.workerFactory} for real off-thread\n * execution; `workerUrl` is kept only for parity with elkjs's own API.\n */\n workerUrl?: string;\n}\n\n/**\n * Lazily-loaded ELK layout adapter. One instance owns one ELK engine, built on\n * the first {@link ElkLayoutEngine.layout} call and reused afterward. With\n * neither `workerFactory` nor `workerUrl` set, ELK runs on the main thread.\n * Setting `workerFactory` runs layout in the Worker it returns. Setting\n * `workerUrl` does not: see {@link ElkLayoutEngineOptions.workerUrl} — it\n * always ends up on the main thread with this engine's `elk.bundled.js`\n * import. Either mode falls back transparently to the main thread — within\n * the same failing `layout` call, which rebuilds the engine and retries once\n * — the first time its engine fails to construct or a layout rejects; that\n * fallback is permanent for the instance's lifetime, so later layouts run on\n * the main thread even if the worker would have recovered.\n *\n * In `workerFactory` mode the engine owns a real Web Worker for as long as it\n * lives. Its owner is expected to call {@link ElkLayoutEngine.dispose} when\n * discarding it, or that worker thread outlives the engine.\n *\n * @category Components\n */\nexport class ElkLayoutEngine {\n\n private _elk: ElkInstance | null = null;\n private readonly _workerFactory?: () => Worker;\n private readonly _workerUrl?: string;\n /**\n * True while `_elk` is worker-backed. Set before constructing a\n * worker-backed engine and never cleared on a successful layout, so the\n * *next* layout failure — whichever one it is — still triggers the\n * one-time main-thread rebuild in {@link layout}; after that rebuild it is\n * `false` for good and a further failure propagates.\n */\n private _workerBacked = false;\n /** True once `dispose` has run. A disposed engine never builds another ELK. */\n private _disposed = false;\n /**\n * True while the ELK instance this engine holds — or is in the middle of\n * building — drives a real Web Worker, which is the consumer-factory mode\n * and only that mode. Cleared when the main-thread fallback replaces the\n * instance. Deliberately NOT cleared by `dispose`: a construction still in\n * flight sets this just before it builds, and the adopt-time check needs\n * it afterward to terminate an instance `dispose` could not yet see.\n */\n private _ownsWorker = false;\n /**\n * The in-flight construction, shared by every `layout` that arrives while\n * ELK is still being imported and built. Without it each of those builds\n * its own ELK — and its own Worker — because `_elk` is still `null` for\n * all of them, and `dispose` can only terminate the one instance the\n * engine ends up keeping. Cleared whenever what it memoised stops being\n * this engine's instance: a failed construction (so a later `layout`\n * retries the import instead of replaying the failure), the main-thread\n * fallback replacing the instance, and `dispose`. Keeping those in step\n * is what stops the memo retaining an ELK — and its `Worker` — the engine\n * has already abandoned, so any future code that assigns or nulls `_elk`\n * must clear this too.\n */\n private _building: Promise<ElkInstance> | null = null;\n\n /**\n * @param options - Optional worker configuration. With neither field set,\n * ELK runs on the main thread via the zero-config bundle.\n */\n constructor(options?: ElkLayoutEngineOptions) {\n this._workerFactory = options?.workerFactory;\n this._workerUrl = options?.workerUrl;\n }\n\n /**\n * Lazily imports ELK (if needed), maps the model to ELK JSON, runs the\n * layout, and maps the result back. The first time a worker-backed engine\n * fails to construct or a layout rejects, rebuilds on the main thread and\n * retries once, so a worker problem never surfaces as a diagram error;\n * that switch to the main thread is permanent for this instance's\n * lifetime, even if a later failure is unrelated to the worker itself.\n *\n * @param data - The framework-native graph.\n * @param sizes - Per-node resolved sizes.\n * @param defaults - View-level default ELK options.\n * @returns The mapped layout result.\n * @throws Error - If `elkjs` is not installed / cannot be imported, if a\n * main-thread layout fails (no worker fallback remains at that point),\n * or if the engine was already disposed when the call started or is\n * disposed while it is building or rebuilding its ELK. Disposal landing\n * during the layout compute itself does not reject — that call can still\n * resolve, which is why `DiagramView` drops late results by generation\n * rather than relying on this.\n */\n async layout(\n data: DiagramData,\n sizes: Map<string, { width: number; height: number }>,\n defaults?: Record<string, string>,\n ): Promise<DiagramLayoutResult> {\n if (this._disposed) {\n throw new Error(DISPOSED_MESSAGE);\n }\n\n const graph = buildElkGraph(data, sizes, defaults);\n\n try {\n const elk = await this.ensureElk();\n\n return mapElkResult(await elk.layout(graph) as ElkNode);\n } catch (error) {\n // Disposed mid-flight: the engine is gone for good, so there is\n // nothing to fall back onto. Main-thread failure: elkjs itself is\n // absent/broken. Either way, propagate.\n if (this._disposed || !this._workerBacked) {\n throw error;\n }\n\n // Worker construction failed (factory threw, `Worker` undefined,\n // CSP block) or its first compute failed. Terminate the worker\n // being abandoned before the reference is dropped, then rebuild on\n // the main thread and retry once so the diagram still renders.\n this.terminateOwnedWorker(this._elk);\n this._workerBacked = false;\n this._ownsWorker = false;\n\n const rebuilt = await this.createMainThreadElk();\n\n // `dispose` can land while that rebuild is awaited. Adopting the\n // instance then would leave a disposed engine holding a live ELK\n // and laying out through it, so drop it. Unlike `ensureElk`'s\n // equivalent check this does NOT terminate what it drops: the\n // rebuild is main-thread, and terminating a non-factory instance\n // throws a `TypeError` (see `terminateOwnedWorker`).\n if (this._disposed) {\n throw new Error(DISPOSED_MESSAGE);\n }\n\n this._elk = rebuilt;\n // The memo still resolves to the worker-backed instance just\n // abandoned; drop it so it does not outlive what it built.\n this._building = null;\n\n return mapElkResult(await rebuilt.layout(graph) as ElkNode);\n }\n }\n\n /**\n * Terminates this engine's ELK Web Worker, if it owns one, and drops the\n * ELK instance. A later {@link ElkLayoutEngine.layout} rejects rather than\n * rebuilding, so a disposed engine never resurrects a worker. Idempotent,\n * and a no-op on an engine that never laid out or never ran off-thread.\n *\n * A layout request outstanding in a real worker at this moment never\n * settles — elkjs does not reject pending requests on termination — so a\n * caller awaiting one must not depend on it resolving or rejecting.\n */\n dispose(): void {\n this._disposed = true;\n\n this.terminateOwnedWorker(this._elk);\n\n this._elk = null;\n // Dropped too, or the engine keeps the terminated instance reachable\n // through the memoised construction for as long as the engine lives.\n this._building = null;\n }\n\n /**\n * Returns the ELK engine, importing and constructing it on first use. The\n * dynamic `import(\"elkjs/...\")` is left external in the library build so the\n * GWT bundle resolves from the consumer's install rather than being inlined.\n *\n * @returns The ELK engine instance.\n * @throws Error - If the engine was disposed while ELK was still being\n * imported and constructed.\n */\n private async ensureElk(): Promise<ElkInstance> {\n if (this._elk) {\n return this._elk;\n }\n\n // Memoised so overlapping layouts share one construction rather than\n // each building an ELK of its own — see `_building`. The adopt-or-\n // terminate decision therefore runs exactly once however many callers\n // are waiting, and they all settle on its outcome.\n this._building ??= this.createElk()\n .then((elk) => {\n if (this._disposed) {\n // `dispose` ran while ELK was still being imported and\n // constructed, so it saw no instance to terminate.\n // Terminate this one rather than adopting it, or the\n // worker outlives the disposed engine.\n this.terminateOwnedWorker(elk);\n\n throw new Error(DISPOSED_MESSAGE);\n }\n\n this._elk = elk;\n\n return elk;\n })\n .catch((error: unknown) => {\n this._building = null;\n\n throw error;\n });\n\n return this._building;\n }\n\n /**\n * Terminates `elk`'s Web Worker, but only when this engine built it from a\n * consumer factory. In every other mode elkjs drives an in-process stand-in\n * worker with no `terminate` method, and asking elkjs to terminate that\n * throws a `TypeError` — so the guard is load-bearing, not defensive.\n *\n * @param elk - The instance to terminate, or `null` if none was built.\n */\n private terminateOwnedWorker(elk: ElkInstance | null): void {\n if (elk && this._ownsWorker) {\n elk.terminateWorker();\n }\n }\n\n /**\n * Builds the ELK engine per the configured mode: a consumer worker\n * factory, a consumer-hosted worker URL, or the main-thread default —\n * checked in that precedence order. `_workerBacked` is set *before*\n * constructing a worker-backed engine, so a synchronous construction\n * throw is still caught by {@link layout}'s retry.\n *\n * @returns The constructed ELK engine.\n */\n private async createElk(): Promise<ElkInstance> {\n // `elkjs` is an optional peer dep, typed by the local ambient shim in\n // `elkjs.d.ts` and resolved by the consumer's bundler at runtime.\n const { default: ELK } = await import(\"elkjs/lib/elk.bundled.js\");\n\n if (this._workerFactory) {\n this._workerBacked = true;\n this._ownsWorker = true;\n\n return new ELK({ workerFactory: this._workerFactory });\n }\n\n if (this._workerUrl) {\n this._workerBacked = true;\n\n return new ELK({ workerUrl: this._workerUrl });\n }\n\n return new ELK();\n }\n\n /**\n * Builds a plain main-thread ELK engine, used both as the default mode\n * and as the fallback after a worker-backed engine fails.\n *\n * @returns The constructed main-thread ELK engine.\n */\n private async createMainThreadElk(): Promise<ElkInstance> {\n const { default: ELK } = await import(\"elkjs/lib/elk.bundled.js\");\n\n return new ELK();\n }\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n//\n// The default node renderer for a DiagramView: a themed rounded box wrapping a\n// glyph + label (or a bare label). Its content-derived preferred size is what\n// the view feeds to ELK when a node carries no explicit width/height.\n\nimport { Panel, PanelOptions } from \"~/core/Panel.js\";\nimport { Component } from \"~/core/Component.js\";\nimport { StyleRule } from \"~/core/StyleTarget.js\";\nimport { DOM } from \"~/core/DOM.js\";\nimport type { Handle } from \"~/core/DOM.js\";\nimport { Fit } from \"~/layout/Fit.js\";\nimport { HBox } from \"~/layout/HBox.js\";\nimport { Insets } from \"~/primitive/Insets.js\";\nimport { IconText } from \"~/component/display/IconText.js\";\nimport { Text } from \"~/component/input/Text.js\";\nimport { callable } from \"~/core/Callable.js\";\n\n/**\n * Construction-time options for {@link DiagramNode}.\n *\n * @category Components\n */\nexport interface DiagramNodeOptions extends PanelOptions {\n /** Label text shown inside the node. */\n label?: string;\n /** Optional registered glyph name shown before the label. */\n glyph?: string;\n /** Short marker text drawn after the label, in the same row. */\n badge?: string;\n /** Whether the node starts selected. */\n selected?: boolean;\n}\n\n/**\n * User-overridable defaults. The node paints a themed rounded box; its interior\n * inset gives the label structural breathing room away from the border.\n */\nconst _defaultDiagramNodeOptions: Partial<DiagramNodeOptions> = {\n // Vertical 4px / horizontal 8px so a short label never sits flush against\n // the rounded border — structural interior spacing, not cosmetic nudging.\n insets: new Insets(4, 8, 4, 8),\n};\n\n/** Corner radius in pixels for the node's rounded box. */\nconst NODE_BORDER_RADIUS = \"4px\";\n\n// The badge's opacity: present but secondary to the label it trails. Matches\n// the \"dim the supporting value\" weight the framework already uses for a\n// receded label, so the badge reads as an annotation rather than a second\n// name.\nconst BADGE_OPACITY = 0.6;\n\n/**\n * The default themed node renderer for a\n * [`DiagramView`](/api/component/diagram/classes/DiagramView). Composes a glyph\n * and label inside a rounded, theme-aware box and toggles a `.selected` state\n * rule when selected.\n *\n * @category Components\n */\nclass DiagramNode extends Panel<DiagramNodeOptions> {\n\n /** The glyph+label (or bare label) component. */\n private _label!: IconText | Text;\n\n /** The child added to the node: `_label` alone, or a row of `_label` + `_badge`. */\n private _content!: Component;\n\n /** The trailing badge chip, when the node carries one. */\n private _badge?: Text;\n\n // Lazy `.selected` state rule. The slot caches the wrapper returned by\n // Component's `createStyleRule` builder, which dedupes by selector suffix.\n private declare _selectedStyleRule?: StyleRule;\n private get selectedStyleRule(): StyleRule {\n return this._selectedStyleRule ??= this.createStyleRule(\".selected\");\n }\n\n /**\n * @param options - Optional construction-time options.\n * @param subclassDefaults - Per-subclass default bag layered over this\n * class's defaults; subclasses forward their `_defaultXxxOptions`\n * constant here.\n */\n constructor(options?: DiagramNodeOptions, subclassDefaults?: Partial<DiagramNodeOptions>) {\n super(options, {\n ..._defaultDiagramNodeOptions,\n layoutManager: new Fit(),\n ...(subclassDefaults ?? {}),\n });\n\n this.setBackgroundColor(\"var(--ts-ui-diagram-node-bg, var(--ts-ui-button-bg, rgb(245, 245, 245)))\");\n this.setBorder(\"1px solid var(--ts-ui-border-color, rgb(180, 180, 180))\");\n this.setBorderRadius(NODE_BORDER_RADIUS);\n this.setCursor(\"pointer\");\n\n this.selectedStyleRule.set(\"borderColor\", \"var(--ts-ui-accent-color, rgb(30, 100, 200))\");\n this.selectedStyleRule.set(\"backgroundColor\", \"var(--ts-ui-diagram-node-selected-bg, var(--ts-ui-table-row-selected, rgba(30, 100, 200, 0.15)))\");\n\n // Content children are built here (not during super's cascade), so the\n // label/glyph/badge/selected values cached pure in `applyOptions` are\n // dispatched now that the row exists.\n this.buildContent(this._options.glyph, this._options.label ?? \"\", this._options.badge);\n\n if (this._options.selected !== undefined) {\n this.setSelected(this._options.selected);\n }\n }\n\n /**\n * Caches the label/glyph/badge/selected fields pure to `_options`; they are\n * dispatched from the constructor body once the content child exists.\n *\n * @param options - The options bag carrying the values to apply.\n */\n protected applyOptions(options: DiagramNodeOptions): this {\n super.applyOptions(options);\n\n if (options.label !== undefined) this._options.label = options.label;\n if (options.glyph !== undefined) this._options.glyph = options.glyph;\n if (options.badge !== undefined) this._options.badge = options.badge;\n if (options.selected !== undefined) this._options.selected = options.selected;\n\n return this;\n }\n\n /**\n * Builds (or rebuilds) the inner content child: `_label` alone (an\n * `IconText` when a glyph is present, else a bare `Text`), or — when a\n * badge is given — a row of `_label` followed by the badge `Text`.\n *\n * @param glyph - Optional glyph name.\n * @param label - The label text.\n * @param badge - Optional trailing badge text.\n */\n private buildContent(glyph: string | undefined, label: string, badge: string | undefined): void {\n if (this._content) {\n this.removeComponent(this._content);\n }\n\n this._label = glyph !== undefined ? new IconText(glyph, label) : new Text(label);\n\n if (badge === undefined) {\n this._badge = undefined;\n this._content = this._label;\n } else {\n this._badge = new Text(badge);\n this._badge.setOpacity(BADGE_OPACITY);\n this._content = new Component({ layoutManager: new HBox(), components: [this._label, this._badge] });\n }\n\n // Every Component stamps its own `cursor` (defaulting to `default`) onto\n // its CSS rule, so the label/glyph/badge would override the node's\n // `pointer` wherever they sit under the cursor — the hover cursor would\n // flicker between pointer (over padding) and arrow (over the text).\n // Make the content transparent to pointer events so hover + clicks land\n // on the node itself; `pointer-events: none` inherits, so this one call\n // also covers everything nested inside (the glyph and text inside an\n // `IconText`, or the label + badge inside the row). Mirrors how `Button`\n // frees its label row so the button's cursor governs.\n this._content.setPointerEvents(\"none\");\n\n this.addComponent(this._content);\n }\n\n /**\n * Updates the node's label text.\n *\n * @param value - The new label.\n *\n * @returns This node, for method chaining.\n */\n setLabel(value: string): this {\n this._options.label = value;\n this._label.setText(value);\n\n return this;\n }\n\n /**\n * Returns the node's label, or `null` when none was set.\n *\n * @returns The label text, or `null`.\n */\n getLabel(): string | null {\n return this._options.label ?? null;\n }\n\n /**\n * Returns the node's badge text, or `null` when none was set.\n *\n * @returns The badge text, or `null`.\n */\n getBadge(): string | null {\n return this._options.badge ?? null;\n }\n\n /**\n * Sets the selected state and toggles the `.selected` CSS class.\n *\n * @param value - True to select, false to deselect.\n *\n * @returns This node, for method chaining.\n */\n setSelected(value: boolean): this {\n this._options.selected = value;\n\n const element = this.getElement();\n\n if (element) {\n DOM.sink.apply(element, { toggleClass: { selected: value } });\n }\n\n return this;\n }\n\n /**\n * Returns whether the node is currently selected.\n *\n * @returns True when selected.\n */\n isSelected(): boolean {\n return this._options.selected ?? false;\n }\n\n /**\n * Re-applies the cached selected class after (re-)render, since a fresh\n * element starts without it.\n *\n * @param element - Optional element from the render pipeline.\n *\n * @returns This node, for method chaining.\n */\n protected init(element?: Handle): this {\n super.init(element);\n\n const el = element || this.getElement();\n\n if (el && this.isSelected()) {\n DOM.sink.apply(el, { toggleClass: { selected: true } });\n }\n\n return this;\n }\n}\n\nconst DiagramNodeCallable = callable(DiagramNode);\ntype DiagramNodeCallable = DiagramNode;\nexport {\n DiagramNode as _DiagramNode,\n DiagramNodeCallable as DiagramNode,\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n//\n// The default container renderer for a DiagramView compound node: a titled,\n// translucent box painted behind its (flat-sibling, not DOM-child) leaf/child\n// components. `DiagramView` positions this box at ELK's computed absolute\n// bounds and paints it at the lowest z-index among a compound graph's nodes,\n// so it never intercepts a click meant for a leaf sitting visually inside it.\n\nimport { Panel, PanelOptions } from \"~/core/Panel.js\";\nimport { Absolute } from \"~/layout/Absolute.js\";\nimport { IconText } from \"~/component/display/IconText.js\";\nimport { Text } from \"~/component/input/Text.js\";\nimport { callable } from \"~/core/Callable.js\";\n\n/**\n * Construction-time options for {@link DiagramGroupNode}.\n *\n * @category Components\n */\nexport interface DiagramGroupNodeOptions extends PanelOptions {\n /** The container's header label (e.g. the schema name). */\n label?: string;\n /** Optional registered glyph name shown before the header label. */\n glyph?: string;\n}\n\n/** Corner radius in pixels for the container's box, matching `DiagramNode`. */\nconst GROUP_BORDER_RADIUS = \"4px\";\n\n/**\n * Distance in pixels from the box's top-left corner to the header label's\n * origin — enough breathing room that the label never sits flush against the\n * rounded border, mirroring `DiagramNode`'s interior inset.\n */\nconst HEADER_INSET = 6;\n\n/**\n * The default themed container renderer for a\n * [`DiagramView`](/api/component/diagram/classes/DiagramView) compound node.\n * Paints a translucent, rounded box with a header label pinned to the\n * top-left corner; the rest of the box stays open so the container's\n * children — rendered as separate flat siblings positioned by ELK, not as DOM\n * children of this component — visually read as sitting inside it.\n *\n * @category Components\n */\nclass DiagramGroupNode extends Panel<DiagramGroupNodeOptions> {\n\n /** The header shown at the box's top-left corner: a bare label, or a glyph + label. */\n private _header!: IconText | Text;\n\n constructor(options?: DiagramGroupNodeOptions) {\n super(options, { layoutManager: new Absolute() });\n\n // A container is a selectable node like any leaf, so it carries the same\n // pointer cursor `DiagramNode` does — left at the Component default it\n // would read as an arrow and promise a pan its own box does not perform.\n this.setCursor(\"pointer\");\n\n this.setBackgroundColor(\"var(--ts-ui-diagram-group-bg, rgba(120, 120, 120, 0.08))\");\n this.setBorder(\"1px solid var(--ts-ui-diagram-group-border, var(--ts-ui-border-color, rgb(180, 180, 180)))\");\n this.setBorderRadius(GROUP_BORDER_RADIUS);\n\n // The header is built here (not during super's cascade), so the values\n // cached pure in `applyOptions` are dispatched now that the box exists.\n // An IconText when a glyph is present (glyph before the label, mirroring\n // DiagramNode), else a bare Text.\n const label = this._options.label ?? \"\";\n this._header = this._options.glyph !== undefined\n ? new IconText(this._options.glyph, label)\n : new Text(label);\n this._header.setPointerEvents(\"none\");\n this._header.setX(HEADER_INSET);\n this._header.setY(HEADER_INSET);\n this.addComponent(this._header);\n }\n\n /**\n * Caches the label/glyph fields pure to `_options`; they are dispatched from\n * the constructor body once the header child exists.\n *\n * @param options - The options bag carrying the values to apply.\n */\n protected applyOptions(options: DiagramGroupNodeOptions): this {\n super.applyOptions(options);\n\n if (options.label !== undefined) this._options.label = options.label;\n if (options.glyph !== undefined) this._options.glyph = options.glyph;\n\n return this;\n }\n\n /**\n * Updates the container's header label.\n *\n * @param value - The new label.\n *\n * @returns This node, for method chaining.\n */\n setLabel(value: string): this {\n this._options.label = value;\n this._header.setText(value);\n\n return this;\n }\n\n /**\n * Returns the container's header label, or `null` when none was set.\n *\n * @returns The label text, or `null`.\n */\n getLabel(): string | null {\n return this._options.label ?? null;\n }\n}\n\nconst DiagramGroupNodeCallable = callable(DiagramGroupNode);\ntype DiagramGroupNodeCallable = DiagramGroupNode;\nexport {\n DiagramGroupNode as _DiagramGroupNode,\n DiagramGroupNodeCallable as DiagramGroupNode,\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n//\n// One `<svg>` Component drawing the diagram's edges as `<path>` elements with a\n// shared arrowhead `<marker>`. Follows the Glyph SVG-through-the-seam pattern:\n// the root `<svg>` and every leaf child are created via `DOM.sink.createElementNS`\n// and tracked with `trackHandle`, never raw DOM. The layer's root stays\n// non-interactive (`pointer-events: none`) so node clicks fall through to the\n// node components; each drawn edge additionally gets an invisible wide hit\n// path that opts itself back into pointer events, so hovering/pressing an edge\n// is possible without making the layer as a whole interactive.\n\nimport { Component, ComponentOptions } from \"~/core/Component.js\";\nimport { DOM } from \"~/core/DOM.js\";\nimport type { Handle } from \"~/core/DOM.js\";\nimport type { ElkEdgeSection, ElkPoint } from \"~/component/diagram/ElkLayoutEngine.js\";\nimport type { DiagramEdgeMarker, DiagramEdgeStyle } from \"~/component/diagram/DiagramModel.js\";\nimport { callable } from \"~/core/Callable.js\";\n\n/** SVG namespace URI. */\nconst SVG_NS = \"http://www.w3.org/2000/svg\";\n\n/** Themed stroke for edge paths and the arrowhead. */\nconst EDGE_STROKE = \"var(--ts-ui-diagram-edge, var(--ts-ui-border-color, rgb(120, 120, 120)))\";\n\n/** Edge path stroke width in pixels — a hairline that reads at any zoom. */\nconst EDGE_STROKE_WIDTH = \"1.5\";\n\n/** Font size (px) of an edge's mid-route label. */\nconst LABEL_FONT_SIZE = \"12\";\n\n/**\n * Background colour and width (px) of the halo drawn behind an edge label. The\n * label text is painted `stroke`-then-`fill` (`paint-order`), so this\n * background-coloured stroke masks the edge line running under the glyphs — the\n * label reads clearly instead of being crossed out by the edge.\n */\nconst LABEL_HALO = \"var(--ts-ui-diagram-bg, var(--ts-ui-panel-bg, rgb(255, 255, 255)))\";\nconst LABEL_HALO_WIDTH = \"4\";\n\n/** Arrowhead marker box size in user units (matches the `refX`/`refY` below). */\nconst ARROW_SIZE = 8;\n\n/** Dash pattern applied when an edge's `style.dashed` is set. */\nconst DASH_ARRAY = \"6 4\";\n\n/**\n * Stroke width (px, unscaled graph units) of an edge's invisible hit path —\n * ±6px either side of the 1.5px visible hairline. A hairline is far too thin\n * to aim at, and this is deliberately wider than ELK's default 10px\n * `elk.spacing.edgeEdge`, so a bundle of parallel routes reports as a bundle\n * rather than forcing the user to land on exactly one of them.\n */\nconst EDGE_HIT_WIDTH = 12;\n\n/** Half the hit width: the distance from a route within which it answers `edgesNear`. */\nconst EDGE_HIT_TOLERANCE = EDGE_HIT_WIDTH / 2;\n\n/**\n * Opacity of the group holding every edge outside a non-empty emphasis set. A\n * 1.5px hairline has almost no area, so it still reads as a full line at\n * ChartLegend's `HIDDEN_OPACITY` (`0.4`), a strength tuned for a filled legend\n * swatch — dimmed enough there is not dimmed enough for a stroke. `0.15` over\n * the default light canvas resolves to a pale, still-traceable grey while\n * leaving the emphasised edges clearly what the eye lands on.\n *\n * Carried by the dimmed *group* rather than each dimmed edge, because routes\n * overlap by design: fan-in and fan-out bundles share a junction stub, so two\n * or more dimmed paths coincide there. Per-element alpha composites at each\n * overlap — two paths at `0.15` resolve to `0.28`, three to `0.39` — so a\n * bundle read as emphasised precisely where it was densest. Group opacity\n * composites the group's whole rendering once, so an overlap looks the same as\n * a single line.\n */\nconst DIMMED_EDGE_OPACITY = \"0.15\";\n\n/**\n * Non-`\"arrow\"` crow's-foot marker kinds this layer pre-defines in `<defs>`.\n * Kept as a literal array (rather than deriving from {@link DiagramEdgeMarker})\n * so `createRootElement` can iterate it directly.\n */\nconst CROWS_FOOT_MARKERS: readonly Exclude<DiagramEdgeMarker, \"arrow\">[] =\n [\"one\", \"zeroOrOne\", \"oneOrMany\", \"zeroOrMany\"];\n\n/**\n * One crow's-foot marker's geometry: its `<marker>` box (`width`/`height`),\n * the point within that box the edge vertex anchors to (`refX`/`refY`), and\n * its child shapes as bare SVG element specs. Every marker uses\n * `orient=\"auto-start-reverse\"`, so the same definition auto-flips when used\n * at `marker-start` and reads unreversed at `marker-end`.\n */\ninterface MarkerGeometry {\n width: number;\n height: number;\n refX: number;\n refY: number;\n /** Child shapes, each a bare tag name plus its SVG attributes. */\n parts: Array<{ tag: \"path\" | \"circle\"; attrs: Record<string, string> }>;\n}\n\n/**\n * Geometry table for the four crow's-foot markers. Coordinates are chosen so\n * every marker's \"attach\" edge sits at `x = width` (the vertex, via `refX`)\n * and its \"open\" edge sits toward `x = 0` (away from the node) — exact pixels\n * are a manual visual-tuning step, not a correctness concern (see the\n * fk-diagram-cardinality-and-index-coverage plan).\n */\nconst MARKER_GEOMETRY: Record<Exclude<DiagramEdgeMarker, \"arrow\">, MarkerGeometry> = {\n // \"one and only one\": two perpendicular bars across the line.\n one: {\n width: 12, height: 12, refX: 12, refY: 6,\n parts: [{ tag: \"path\", attrs: { d: \"M 4 0 L 4 12 M 8 0 L 8 12\" } }],\n },\n // \"zero or one\": one perpendicular bar plus a small inboard circle.\n zeroOrOne: {\n width: 16, height: 12, refX: 16, refY: 6,\n parts: [\n { tag: \"path\", attrs: { d: \"M 12 0 L 12 12\" } },\n { tag: \"circle\", attrs: { cx: \"6\", cy: \"6\", r: \"4\" } },\n ],\n },\n // \"one or many\": a three-prong crow's foot plus one perpendicular bar just inboard of it.\n oneOrMany: {\n width: 16, height: 12, refX: 16, refY: 6,\n parts: [\n { tag: \"path\", attrs: { d: \"M 0 6 L 12 0 M 0 6 L 12 6 M 0 6 L 12 12\" } },\n { tag: \"path\", attrs: { d: \"M 14 0 L 14 12\" } },\n ],\n },\n // \"zero or many\": a three-prong crow's foot plus a small inboard circle.\n zeroOrMany: {\n width: 18, height: 12, refX: 18, refY: 6,\n parts: [\n { tag: \"path\", attrs: { d: \"M 6 6 L 18 0 M 6 6 L 18 6 M 6 6 L 18 12\" } },\n { tag: \"circle\", attrs: { cx: \"3\", cy: \"6\", r: \"3\" } },\n ],\n },\n};\n\n/**\n * How far, in unscaled graph units, the longest end marker reaches back along\n * an edge from the point it attaches to. Every marker anchors its vertex at\n * `x = width` (via `refX`) and opens toward `x = 0`, so a marker's `width` *is*\n * its reach, and this is the widest of them all.\n *\n * Exported because a consumer that rewrites edge routes needs it: anything\n * placed on the route within this distance of an endpoint lands underneath the\n * marker glyph rather than beside it. SQLAdmin's junction stubs use it as the\n * floor for how far from a node a bundle may branch.\n *\n * @category Components\n */\nexport const EDGE_MARKER_EXTENT: number =\n Math.max(ARROW_SIZE, ...Object.values(MARKER_GEOMETRY).map(geometry => geometry.width));\n\n/**\n * A single routed edge: its identity, the ELK sections describing its\n * polyline route (start point, optional bend points, end point), and its\n * optional visual style (joined in from the model by\n * [`DiagramView.applyLayout`](/api/component/diagram/classes/DiagramView)).\n */\nexport interface DiagramEdgeRoute {\n id: string;\n sections: ElkEdgeSection[];\n /** Optional cardinality/dependency style. Absent = today's plain arrow-ended edge. */\n style?: DiagramEdgeStyle;\n}\n\n/**\n * The point used to place an edge's optional label: the halfway point *along the\n * routed polyline* (start → bend points → end), measured by arc length. This\n * stays visually centred on the edge even when it bends — a bend point can sit\n * far off centre and drag the label toward one endpoint.\n *\n * @param sections - The edge's routed sections.\n * @returns The label anchor point.\n */\nfunction labelPoint(sections: ElkEdgeSection[]): ElkPoint {\n const section = sections[0];\n\n if (!section) {\n return { x: 0, y: 0 };\n }\n\n const points = [section.startPoint, ...(section.bendPoints ?? []), section.endPoint];\n\n return midpointAlong(points);\n}\n\n/**\n * The point half the total length along a polyline, interpolated within the\n * segment that straddles the midpoint.\n *\n * @param points - The polyline vertices, in order (length >= 1).\n * @returns The arc-length midpoint.\n */\nfunction midpointAlong(points: ElkPoint[]): ElkPoint {\n const segmentLength = (a: ElkPoint, b: ElkPoint): number => Math.hypot(b.x - a.x, b.y - a.y);\n\n let total = 0;\n\n for (let i = 1; i < points.length; i++) {\n total += segmentLength(points[i - 1], points[i]);\n }\n\n let remaining = total / 2;\n\n for (let i = 1; i < points.length; i++) {\n const length = segmentLength(points[i - 1], points[i]);\n\n if (length >= remaining) {\n const t = length === 0 ? 0 : remaining / length;\n\n return {\n x: points[i - 1].x + (points[i].x - points[i - 1].x) * t,\n y: points[i - 1].y + (points[i].y - points[i - 1].y) * t,\n };\n }\n\n remaining -= length;\n }\n\n return points[points.length - 1];\n}\n\n/**\n * Serialises an edge's ELK sections into an SVG path `d` string. Each section\n * starts with a move to its `startPoint`, threads any `bendPoints`, and lines to\n * its `endPoint`.\n *\n * @param sections - The edge's routed sections.\n * @returns The SVG path data, or an empty string when there is nothing to draw.\n */\nfunction buildPathData(sections: ElkEdgeSection[]): string {\n const parts: string[] = [];\n\n for (const section of sections) {\n parts.push(`M ${section.startPoint.x} ${section.startPoint.y}`);\n\n for (const bend of section.bendPoints ?? []) {\n parts.push(`L ${bend.x} ${bend.y}`);\n }\n\n parts.push(`L ${section.endPoint.x} ${section.endPoint.y}`);\n }\n\n return parts.join(\" \");\n}\n\n/**\n * The shortest distance from point `(px, py)` to the line segment `(ax, ay)`–\n * `(bx, by)`, via the standard clamped projection onto the segment.\n *\n * @param px - Point x.\n * @param py - Point y.\n * @param ax - Segment start x.\n * @param ay - Segment start y.\n * @param bx - Segment end x.\n * @param by - Segment end y.\n * @returns The shortest distance from the point to the segment.\n */\nfunction distanceToSegment(px: number, py: number, ax: number, ay: number, bx: number, by: number): number {\n const dx = bx - ax;\n const dy = by - ay;\n const lengthSquared = dx * dx + dy * dy;\n\n if (lengthSquared === 0) {\n return Math.hypot(px - ax, py - ay);\n }\n\n const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / lengthSquared));\n\n return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));\n}\n\n/**\n * The shortest distance from point `(x, y)` to a routed edge's polyline —\n * the minimum over every segment of every section, walking the same\n * start/bend/end points {@link labelPoint} does.\n *\n * @param sections - The edge's routed sections.\n * @param x - Point x in unscaled graph coordinates.\n * @param y - Point y in unscaled graph coordinates.\n * @returns The shortest distance to the route, or `Infinity` when it has no drawable segments.\n */\nfunction distanceToRoute(sections: ElkEdgeSection[], x: number, y: number): number {\n let min = Infinity;\n\n for (const section of sections) {\n const points = [section.startPoint, ...(section.bendPoints ?? []), section.endPoint];\n\n for (let i = 1; i < points.length; i++) {\n min = Math.min(min, distanceToSegment(x, y, points[i - 1].x, points[i - 1].y, points[i].x, points[i].y));\n }\n }\n\n return min;\n}\n\n/** The elements drawn for one edge, so it can be hit-tested and released. */\ninterface DrawnEdge {\n id: string;\n route: DiagramEdgeRoute;\n /** The visible stroked path. */\n path: Handle;\n /** The invisible wide path that takes pointer events. */\n hit: Handle;\n /** The mid-route label, when the edge carries one. */\n label: Handle | null;\n /** The group the three elements above were appended into, so they can be removed from it. */\n group: Handle;\n}\n\n/**\n * The SVG edge layer for a [`DiagramView`](/api/component/diagram/classes/DiagramView).\n * Owns exactly one `<svg>` element and rebuilds its `<path>` children from ELK\n * edge routes whenever the diagram re-lays-out.\n *\n * @category Components\n */\nclass DiagramEdgeLayer extends Component<ComponentOptions> {\n\n /** Cached edge routes, redrawn at render time and on every `setEdges`. */\n private _edges: DiagramEdgeRoute[] = [];\n\n /** Everything currently drawn, released and rebuilt by `rebuildPaths`. */\n private _drawn: DrawnEdge[] = [];\n\n /**\n * The currently emphasised edge ids. Runtime interaction state (not a\n * `ComponentOptions` field — see ARCHITECTURE.md's rule that transient,\n * framework-managed state stays off the options bag). Cleared by `setEdges`.\n */\n private _edgeEmphasis: Set<string> = new Set();\n\n /**\n * The `<g>` holding every edge outside a non-empty emphasis set, carrying\n * {@link DIMMED_EDGE_OPACITY}. Painted before `_normalLayer`, so an\n * emphasised edge always draws over a dimmed one it crosses. Created by\n * `createRootElement`, so it exists before any draw.\n */\n private _dimLayer!: Handle;\n\n /** The `<g>` holding every edge drawn at full strength. Painted over `_dimLayer`. */\n private _normalLayer!: Handle;\n\n /** Per-instance arrowhead marker id, referenced by each plain edge's `marker-end`. */\n private readonly _markerId: string;\n\n /** Per-instance crow's-foot marker ids, keyed by marker kind. */\n private readonly _crowsFootMarkerIds: Record<Exclude<DiagramEdgeMarker, \"arrow\">, string>;\n\n constructor(options?: ComponentOptions) {\n super(options);\n\n this._markerId = `${this.getId()}-arrow`;\n this._crowsFootMarkerIds = Object.fromEntries(\n CROWS_FOOT_MARKERS.map(kind => [kind, `${this.getId()}-${kind}`]),\n ) as Record<Exclude<DiagramEdgeMarker, \"arrow\">, string>;\n\n // Non-interactive overlay: clicks must reach the node components beneath.\n this.setPointerEvents(\"none\");\n\n // Every Component stamps ComponentDefaults' \"default\" cursor into its\n // own rule; left at that, a hit path inheriting from this `<svg>`\n // would resolve to an arrow regardless of what it declares itself.\n // Inheriting through to the view root's live grab/grabbing write is\n // what lets an edge press pan the canvas honestly.\n this.setCursor(\"inherit\");\n }\n\n /**\n * Resolves a marker kind to its namespaced `url(#…)` reference, or\n * `undefined` when the kind is absent (no marker on that end).\n *\n * @param kind - The requested marker kind, if any.\n * @returns The `url(#id)` reference, or `undefined`.\n */\n private markerUrl(kind: DiagramEdgeMarker | undefined): string | undefined {\n if (!kind) {\n return undefined;\n }\n\n const id = kind === \"arrow\" ? this._markerId : this._crowsFootMarkerIds[kind];\n\n return `url(#${id})`;\n }\n\n /**\n * Replaces the drawn edges and rebuilds the path children. Clears any\n * active edge emphasis first, so the freshly-drawn set always starts\n * undimmed — a re-layout that swaps in a new graph is not the graph the\n * emphasis was computed against.\n *\n * Routes routinely arrive before this layer has an element: a diagram built\n * inside a dock tab runs its whole ELK layout while the tab is still\n * detached (the app awaits\n * [`DiagramView.whenLaidOut`](/api/component/diagram/classes/DiagramView)\n * before mounting, so the layout *always* lands first), and `rebuildPaths`\n * cannot draw without one. `render` only performs the first draw when it is\n * what creates the element, which is not the case here — so without the\n * deferral below the sole draw for those routes is silently lost and the\n * diagram shows nodes with no edges until some later `setEdges` happens to\n * find an element. {@link Component.onFirstLayout} exists for exactly this\n * \"content built before the host attaches it\" case.\n *\n * @param edges - The routed edges to draw.\n *\n * @returns This layer, for method chaining.\n */\n setEdges(edges: DiagramEdgeRoute[]): this {\n this._edges = edges;\n this._edgeEmphasis = new Set();\n\n if (this.getElement()) {\n this.rebuildPaths();\n } else {\n this.onFirstLayout(() => this.rebuildPaths());\n }\n\n return this;\n }\n\n /**\n * Sets the emphasised edge ids. While the set is non-empty every edge NOT\n * in it is drawn at a reduced opacity; the emphasised edges keep their\n * normal weight. `null` or an empty array clears the emphasis. Ids naming\n * no drawn edge are kept but have no effect.\n *\n * @param ids - The edge ids to emphasise, or null to clear.\n *\n * @returns This layer, for method chaining.\n */\n setEdgeEmphasis(ids: readonly string[] | null): this {\n this._edgeEmphasis = new Set(ids ?? []);\n\n // A full redraw rather than a restyle in place: which of the two groups\n // an edge belongs to is decided at draw time, and redrawing is what\n // moves it. Cheap enough to do on a click — a redraw of a 1000-edge\n // graph's paths measures around 20ms, against the seconds ELK itself\n // takes — and it reuses the release/rebuild path already covered by\n // tests instead of relying on appendChild's move semantics through the\n // DOM seam.\n this.rebuildPaths();\n\n return this;\n }\n\n /**\n * The currently emphasised edge ids.\n *\n * @returns A copy of the emphasised id array; empty when nothing is emphasised.\n */\n getEdgeEmphasis(): string[] {\n return [...this._edgeEmphasis];\n }\n\n /**\n * Resolves a raw DOM event target to the edge whose invisible hit path it is.\n *\n * @param target - The raw DOM event target.\n * @returns The edge id, or null when the target is not an edge hit path.\n */\n edgeIdAt(target: EventTarget | null): string | null {\n if (target === null) {\n return null;\n }\n\n const handle = DOM.source.intern(target);\n\n return this._drawn.find(d => d.hit === handle)?.id ?? null;\n }\n\n /**\n * Every drawn edge whose route passes within the hit tolerance of a point,\n * in draw order. Several edges answer here wherever their routes overlap —\n * which is what makes a merged trunk answerable.\n *\n * @param x - Point x in unscaled graph coordinates.\n * @param y - Point y in unscaled graph coordinates.\n * @returns The routes within tolerance; empty when none is.\n */\n edgesNear(x: number, y: number): DiagramEdgeRoute[] {\n return this._drawn\n .filter(drawn => distanceToRoute(drawn.route.sections, x, y) <= EDGE_HIT_TOLERANCE)\n .map(drawn => drawn.route);\n }\n\n /**\n * Which group one edge draws into: the dimmed group when an emphasis set is\n * active and this edge is not in it, the full-strength group otherwise.\n *\n * @param id - The edge id.\n * @returns The group handle to append the edge's elements into.\n */\n private groupFor(id: string): Handle {\n if (this._edgeEmphasis.size === 0 || this._edgeEmphasis.has(id)) {\n return this._normalLayer;\n }\n\n return this._dimLayer;\n }\n\n /**\n * Creates the root `<svg>` with the arrowhead marker plus one `<marker>` per\n * crow's-foot kind in a shared `<defs>`. Every marker and its child shapes\n * are seam-created leaf children tracked for release.\n *\n * @returns The root `<svg>` handle.\n */\n protected createRootElement(): Handle {\n const svg = DOM.sink.createElementNS(SVG_NS, \"svg\");\n const defs = DOM.sink.createElementNS(SVG_NS, \"defs\");\n\n this.createArrowMarker(defs);\n\n for (const kind of CROWS_FOOT_MARKERS) {\n this.createCrowsFootMarker(defs, kind);\n }\n\n DOM.sink.appendChild(svg, defs);\n this.trackHandle(defs);\n\n this._dimLayer = this.createEdgeGroup(svg, DIMMED_EDGE_OPACITY);\n this._normalLayer = this.createEdgeGroup(svg, null);\n\n return svg;\n }\n\n /**\n * Creates one of the two persistent edge groups as a child of the root\n * `<svg>`. Append order is paint order, so the caller creates the dimmed\n * group first.\n *\n * @param svg - The root `<svg>` handle to append the group into.\n * @param opacity - The group's opacity, or null to leave it at full strength.\n *\n * @returns The `<g>` handle.\n */\n private createEdgeGroup(svg: Handle, opacity: string | null): Handle {\n const group = DOM.sink.createElementNS(SVG_NS, \"g\");\n\n if (opacity !== null) {\n DOM.sink.apply(group, { setAttr: { opacity } });\n }\n\n DOM.sink.appendChild(svg, group);\n this.trackHandle(group);\n\n return group;\n }\n\n /**\n * Defines the default arrowhead `<marker>` into `defs`. `orient=\"auto-start-\n * reverse\"` so the one definition serves both `marker-end` (unreversed, the\n * common case) and `marker-start` (auto-flipped to point back out of the\n * source node) — e.g. a data-flow edge that draws its arrow at the source end.\n *\n * @param defs - The `<defs>` element to append into.\n */\n private createArrowMarker(defs: Handle): void {\n const marker = DOM.sink.createElementNS(SVG_NS, \"marker\");\n\n DOM.sink.apply(marker, { setAttr: {\n id: this._markerId,\n markerWidth: String(ARROW_SIZE),\n markerHeight: String(ARROW_SIZE),\n refX: String(ARROW_SIZE),\n refY: String(ARROW_SIZE / 2),\n orient: \"auto-start-reverse\",\n markerUnits: \"userSpaceOnUse\",\n } });\n\n const arrow = DOM.sink.createElementNS(SVG_NS, \"path\");\n DOM.sink.apply(arrow, { setAttr: {\n d: `M 0 0 L ${ARROW_SIZE} ${ARROW_SIZE / 2} L 0 ${ARROW_SIZE} z`,\n fill: EDGE_STROKE,\n } });\n\n DOM.sink.appendChild(marker, arrow);\n DOM.sink.appendChild(defs, marker);\n\n this.trackHandle(marker);\n this.trackHandle(arrow);\n }\n\n /**\n * Defines one crow's-foot `<marker>` into `defs` from its\n * {@link MARKER_GEOMETRY} entry. `orient=\"auto-start-reverse\"` lets the same\n * definition serve both `marker-start` (auto-flipped) and `marker-end`\n * (unreversed).\n *\n * @param defs - The `<defs>` element to append into.\n * @param kind - Which crow's-foot marker to define.\n */\n private createCrowsFootMarker(defs: Handle, kind: Exclude<DiagramEdgeMarker, \"arrow\">): void {\n const geometry = MARKER_GEOMETRY[kind];\n const marker = DOM.sink.createElementNS(SVG_NS, \"marker\");\n\n DOM.sink.apply(marker, { setAttr: {\n id: this._crowsFootMarkerIds[kind],\n markerWidth: String(geometry.width),\n markerHeight: String(geometry.height),\n refX: String(geometry.refX),\n refY: String(geometry.refY),\n orient: \"auto-start-reverse\",\n markerUnits: \"userSpaceOnUse\",\n } });\n\n DOM.sink.appendChild(defs, marker);\n this.trackHandle(marker);\n\n for (const part of geometry.parts) {\n const shape = DOM.sink.createElementNS(SVG_NS, part.tag);\n\n DOM.sink.apply(shape, { setAttr: {\n ...part.attrs,\n fill: \"none\",\n stroke: EDGE_STROKE,\n } });\n\n DOM.sink.appendChild(marker, shape);\n this.trackHandle(shape);\n }\n }\n\n /**\n * Renders the root element and draws the cached edges.\n *\n * @returns The rendered root element.\n */\n protected render(): Handle {\n const element = super.render();\n\n this.rebuildPaths();\n\n return element;\n }\n\n /**\n * Clears the previously-drawn paths and rebuilds them from the cached edge\n * routes. No-op before the element exists — `render` performs the first draw.\n */\n private rebuildPaths(): void {\n const svg = this.getElement();\n\n if (!svg) {\n return;\n }\n\n for (const drawn of this._drawn) {\n this.releaseDrawnEdge(drawn);\n }\n\n this._drawn = [];\n\n for (const edge of this._edges) {\n const d = buildPathData(edge.sections);\n\n if (!d) {\n continue;\n }\n\n const group = this.groupFor(edge.id);\n const hit = this.drawHitPath(group, d);\n const path = this.drawVisiblePath(group, edge, d);\n const style = edge.style;\n let label: Handle | null = null;\n\n if (style?.label) {\n label = this.drawLabel(group, edge, style.label);\n }\n\n this._drawn.push({ id: edge.id, route: edge, path, hit, label, group });\n }\n }\n\n /**\n * Draws one edge's invisible wide hit path: same `d` as the visible path,\n * transparent stroke, and the only element in the layer that opts back\n * into pointer events (`pointer-events: stroke`) — the root `<svg>` stays\n * inert. Appended before the visible path.\n *\n * @param group - The edge group handle to append into.\n * @param d - The edge's path data (shared with the visible path).\n * @returns The hit path's handle.\n */\n private drawHitPath(group: Handle, d: string): Handle {\n const hit = DOM.sink.createElementNS(SVG_NS, \"path\");\n\n DOM.sink.apply(hit, { setAttr: {\n d,\n fill: \"none\",\n stroke: \"transparent\",\n \"stroke-width\": String(EDGE_HIT_WIDTH),\n \"pointer-events\": \"stroke\",\n // Dragging an edge pans the canvas like empty canvas does, so the\n // hit path takes the viewport's own live grab/grabbing cursor by\n // inheriting rather than promising a cursor of its own.\n cursor: \"inherit\",\n } });\n\n DOM.sink.appendChild(group, hit);\n this.trackHandle(hit);\n\n return hit;\n }\n\n /**\n * Draws one edge's visible stroked path, carrying its markers and dash. The\n * edge's emphasis state is not written here — it is the group the path is\n * appended into (see {@link DIMMED_EDGE_OPACITY}).\n *\n * @param group - The edge group handle to append into.\n * @param edge - The routed edge to draw.\n * @param d - The edge's path data.\n * @returns The visible path's handle.\n */\n private drawVisiblePath(group: Handle, edge: DiagramEdgeRoute, d: string): Handle {\n const path = DOM.sink.createElementNS(SVG_NS, \"path\");\n const style = edge.style;\n\n // No style: today's back-compat behaviour — a plain arrow at the end,\n // no start marker. With a style, each end draws only the marker (if\n // any) the style names; an edge can carry cardinality with no\n // marker-end at all.\n const markerEnd = style ? this.markerUrl(style.endMarker) : this.markerUrl(\"arrow\");\n const markerStart = style ? this.markerUrl(style.startMarker) : undefined;\n\n const attrs: Record<string, string> = {\n d,\n fill: \"none\",\n stroke: style?.stroke ?? EDGE_STROKE,\n \"stroke-width\": EDGE_STROKE_WIDTH,\n };\n\n if (markerEnd) {\n attrs[\"marker-end\"] = markerEnd;\n }\n\n if (markerStart) {\n attrs[\"marker-start\"] = markerStart;\n }\n\n if (style?.dashed) {\n attrs[\"stroke-dasharray\"] = DASH_ARRAY;\n }\n\n DOM.sink.apply(path, { setAttr: attrs });\n\n DOM.sink.appendChild(group, path);\n this.trackHandle(path);\n\n return path;\n }\n\n /**\n * Releases one previously-drawn edge's elements (hit path, visible path,\n * and optional label) from the group they were appended into and from the\n * tracked-handle set.\n *\n * @param drawn - The drawn edge record to release.\n */\n private releaseDrawnEdge(drawn: DrawnEdge): void {\n const handles = [drawn.hit, drawn.path, ...(drawn.label ? [drawn.label] : [])];\n\n for (const handle of handles) {\n DOM.sink.removeChild(drawn.group, handle);\n this.untrackHandle(handle);\n DOM.sink.release(handle);\n }\n }\n\n /**\n * Draws one edge's optional mid-route label as a `<text>` element.\n *\n * @param group - The edge group handle to append into, which also carries\n * the label's emphasis state (see {@link DIMMED_EDGE_OPACITY}).\n * @param edge - The edge route the label belongs to (for its anchor point).\n * @param label - The label text.\n * @returns The label's handle.\n */\n private drawLabel(group: Handle, edge: DiagramEdgeRoute, label: string): Handle {\n const point = labelPoint(edge.sections);\n const text = DOM.sink.createElementNS(SVG_NS, \"text\");\n\n DOM.sink.apply(text, {\n setAttr: {\n x: String(point.x),\n y: String(point.y),\n fill: EDGE_STROKE,\n // Background-coloured halo painted first, so the edge line under the\n // glyphs is masked and the label reads clearly (not crossed out).\n stroke: LABEL_HALO,\n \"stroke-width\": LABEL_HALO_WIDTH,\n \"stroke-linejoin\": \"round\",\n \"paint-order\": \"stroke\",\n \"font-size\": LABEL_FONT_SIZE,\n \"text-anchor\": \"middle\",\n \"dominant-baseline\": \"central\",\n },\n text: label,\n });\n\n DOM.sink.appendChild(group, text);\n this.trackHandle(text);\n\n return text;\n }\n}\n\nconst DiagramEdgeLayerCallable = callable(DiagramEdgeLayer);\ntype DiagramEdgeLayerCallable = DiagramEdgeLayer;\nexport {\n DiagramEdgeLayer as _DiagramEdgeLayer,\n DiagramEdgeLayerCallable as DiagramEdgeLayer,\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n//\n// A read-only graph/diagram viewer. Takes a framework-native node/edge model,\n// runs it through ELK for automatic layout (off-seam pure compute, lazily\n// imported), and renders themed node components plus an SVG edge layer with pan,\n// zoom, and node selection.\n//\n// Structure: DiagramView (an Anchor-managed Panel viewport) owns a single\n// content host Container (Absolute layout) that carries the pan/zoom transform\n// and holds the node components + the edge layer, plus a corner-pinned control\n// cluster (zoom in/out, fit, reset). Pan is an unbounded `translate()` on the\n// content host's transform, not native scroll — the viewport has no scrollbars\n// and simply clips (`overflow: hidden`) whatever pans outside it, giving an\n// infinite-canvas feel. Zoom is the transform's `scale()` factor.\n\nimport { Panel, PanelOptions } from \"~/core/Panel.js\";\nimport { Container } from \"~/core/Container.js\";\nimport { Component } from \"~/core/Component.js\";\nimport { Absolute } from \"~/layout/Absolute.js\";\nimport { Anchor } from \"~/layout/Anchor.js\";\nimport { AnchorConstraints } from \"~/layout/AnchorConstraints.js\";\nimport { VBox } from \"~/layout/VBox.js\";\nimport { Button } from \"~/component/button/Button.js\";\nimport { Glyph } from \"~/component/display/Glyph.js\";\nimport { ProgressSpinner } from \"~/component/display/ProgressSpinner.js\";\nimport { plus } from \"~/glyphs/solid/plus.js\";\nimport { minus } from \"~/glyphs/solid/minus.js\";\nimport { expand } from \"~/glyphs/solid/expand.js\";\nimport { crosshairs } from \"~/glyphs/solid/crosshairs.js\";\nimport { Event } from \"~/core/Event.js\";\nimport { DOM } from \"~/core/DOM.js\";\nimport type { Handle } from \"~/core/DOM.js\";\nimport { ListenerBag } from \"~/core/ListenerBag.js\";\nimport { DiagramData, DiagramEdgeData, DiagramNodeData } from \"~/component/diagram/DiagramModel.js\";\nimport { ElkLayoutEngine, DiagramLayoutResult } from \"~/component/diagram/ElkLayoutEngine.js\";\nimport { DiagramNode } from \"~/component/diagram/DiagramNode.js\";\nimport { DiagramGroupNode } from \"~/component/diagram/DiagramGroupNode.js\";\nimport { DiagramEdgeLayer } from \"~/component/diagram/DiagramEdgeLayer.js\";\nimport type { DiagramEdgeRoute } from \"~/component/diagram/DiagramEdgeLayer.js\";\nimport { callable } from \"~/core/Callable.js\";\n\nGlyph.register(plus, minus, expand, crosshairs);\n\n/** Factory producing a node component from a node's model data. */\nexport type DiagramNodeRenderer = (data: DiagramNodeData) => Component;\n\n/** String-literal union of the events emitted by {@link DiagramView}. */\nexport type DiagramViewEvent = \"selection\" | \"activate\" | \"layout\" | \"contextmenu\" | \"edgehover\" | \"edgeleave\";\n\n/** Default initial zoom factor. */\nconst DEFAULT_ZOOM = 1;\n\n/** Default minimum zoom factor (a quarter scale). */\nconst DEFAULT_MIN_ZOOM = 0.25;\n\n/** Default maximum zoom factor (4× scale). */\nconst DEFAULT_MAX_ZOOM = 4;\n\n/** Multiplicative zoom step per wheel notch. */\nconst WHEEL_ZOOM_STEP = 1.1;\n\n/** Multiplicative zoom step per control-cluster button press. */\nconst ZOOM_BUTTON_STEP = 1.5;\n\n/**\n * Pixel distance the pointer must travel between `pointerdown` and a later\n * `pointermove` before the press-and-release it ends in counts as a drag\n * rather than a click. Matches `DragManager.DRAG_THRESHOLD`, the library's\n * only other click-versus-drag disambiguation.\n */\nconst CLICK_SLOP = 4;\n\n/**\n * Opacity of a node component outside a non-empty node-emphasis set. Higher\n * than `DiagramEdgeLayer`'s `DIMMED_EDGE_OPACITY` (`0.15`) because perceived\n * presence scales with area, not just alpha: a `TableCardNode`-sized box with\n * a border and text rows still reads at `0.15`, whereas a 1.5px hairline\n * needs the lower number to recede. `0.35` keeps a dimmed card's shape and\n * label legible while clearly receding behind the emphasised ones.\n */\nconst DIMMED_NODE_OPACITY = 0.35;\n\n// Diameter in pixels of the busy overlay's arc. Matches `TablePanel`'s\n// store-loading spinner and `createSpinnerWrap`'s lazy-tab placeholder, so a\n// slow diagram update and a slow data load read as the same kind of wait.\nconst BUSY_SPINNER_DIAMETER = 24;\n\n// Structural breathing room between the control cluster and the viewport\n// corner it is pinned to — not a cosmetic choice, but the inset the Anchor\n// constraint needs so the cluster does not paint flush against the edge.\nconst CONTROLS_MARGIN = 12;\n\n// Paint order for a compound graph (one with at least one container): the\n// container boxes sit behind the edges, which sit behind the leaves, so a\n// leaf's click is never intercepted by its own container box. A leaf/container\n// component is always freshly built by rebuildNodes, so on a flat graph it\n// simply starts at DEFAULT_Z_INDEX and is never touched. The edge layer is the\n// one exception — it is a persistent child of the content host (built once in\n// the constructor, never torn down/rebuilt — see the constructor comment), so\n// a flat pass must explicitly restore DEFAULT_Z_INDEX in case an earlier\n// compound pass on the same view left EDGE_LAYER_Z_INDEX behind.\nconst DEFAULT_Z_INDEX = 0;\nconst CONTAINER_Z_INDEX = 0;\nconst EDGE_LAYER_Z_INDEX = 1;\nconst LEAF_Z_INDEX = 2;\n\n/**\n * Construction-time options for {@link DiagramView}.\n *\n * @category Components\n */\nexport interface DiagramViewOptions extends PanelOptions {\n /** The initial graph to lay out and render. */\n data?: DiagramData;\n /** Factory for node components; defaults to building a `DiagramNode`. */\n nodeRenderer?: DiagramNodeRenderer;\n /** Factory for compound container components; defaults to `DiagramGroupNode`. */\n groupRenderer?: DiagramNodeRenderer;\n /** Default ELK layout options applied to every layout pass. */\n layoutOptions?: Record<string, string>;\n /**\n * URL of a consumer-hosted `elk-worker.js`, requesting off-thread layout.\n * With the `elk.bundled.js` module this view's engine imports, elkjs's\n * own worker-availability check always fails, so `workerUrl` alone never\n * actually constructs a Worker; layout still runs on the main thread, via\n * elkjs's own fallback. Pass {@link DiagramViewOptions.elkWorkerFactory}\n * for real off-thread execution.\n */\n elkWorkerUrl?: string;\n /**\n * Factory returning a Web Worker for off-thread ELK layout. When set,\n * ELK's compute runs in the returned worker. Construct it in your app so\n * your bundler emits the worker, e.g.\n * `() => new Worker(new URL(\"elkjs/lib/elk-worker.min.js\", import.meta.url), { type: \"classic\" })`.\n * Takes precedence over {@link DiagramViewOptions.elkWorkerUrl} when both\n * are set.\n */\n elkWorkerFactory?: () => Worker;\n /** Minimum zoom factor (default 0.25). */\n minZoom?: number;\n /** Maximum zoom factor (default 4). */\n maxZoom?: number;\n /** Initial zoom factor (default 1). */\n zoom?: number;\n /** Show the built-in zoom / fit / reset control cluster (default true). */\n controls?: boolean;\n /**\n * Id of the node the one-shot initial view centres on, instead of the\n * graph's bounds. An id naming no node in the graph falls back to\n * centring the bounds. The configured `zoom` is honoured, except that a\n * focus node too large to fit the viewport lowers it until the node fits.\n */\n initialFocusNode?: string;\n /** Construction-time listener bag dispatched to {@link DiagramView.on}. */\n listeners?: {\n selection?: (nodes: DiagramNodeData[]) => void;\n activate?: (node: DiagramNodeData) => void;\n layout?: () => void;\n contextmenu?: (node: DiagramNodeData, event: MouseEvent) => void;\n edgehover?: (edges: DiagramEdgeData[], event: MouseEvent) => void;\n edgeleave?: () => void;\n };\n}\n\n/**\n * A read-only automatic-layout graph viewer with pan, zoom, and node selection.\n *\n * Pass a graph via the `data` option or {@link DiagramView.setData}. Layout runs\n * asynchronously through ELK; a `\"layout\"` event fires after each successful\n * pass, a `\"selection\"` event fires when the selected node changes, and an\n * `\"activate\"` event fires when a node is double-clicked.\n *\n * @example\n * ```typescript\n * const view = new DiagramView({\n * data: {\n * nodes: [{ id: \"a\", label: \"A\" }, { id: \"b\", label: \"B\" }],\n * edges: [{ id: \"e\", source: \"a\", target: \"b\" }],\n * },\n * });\n * view.on(\"selection\", (nodes) => console.log(nodes));\n * ```\n *\n * @category Components\n */\nclass DiagramView extends Panel<DiagramViewOptions> {\n\n /** The transform-carrying content host holding nodes + the edge layer. */\n private _contentHost!: Container;\n\n /** The SVG edge layer, a persistent child of the content host. */\n private _edgeLayer!: DiagramEdgeLayer;\n\n /** The ELK layout adapter. Runtime state, off the options bag. */\n private _engine!: ElkLayoutEngine;\n\n /** Node components keyed by node id — the graph currently on screen. */\n private _nodeComponents: Map<string, Component> = new Map();\n\n /** Node model data keyed by node id (selection payload source) — the graph currently on screen. */\n private _nodeData: Map<string, DiagramNodeData> = new Map();\n\n /** Ids of the compound container nodes in the graph currently on screen. */\n private _containerIds: Set<string> = new Set();\n\n /**\n * The graph built by the latest `setData`, mounted hidden and awaiting a\n * layout. Promoted into the shown set above by `applyLayout`, discarded by\n * `handleLayoutFailure` or by the next `rebuildNodes`. Kept separate from\n * the shown set so a re-layout leaves the previous graph on screen for the\n * whole ELK round-trip instead of blanking the canvas.\n */\n private _incomingComponents: Map<string, Component> = new Map();\n private _incomingData: Map<string, DiagramNodeData> = new Map();\n private _incomingContainerIds: Set<string> = new Set();\n\n /** Currently selected node data (single-select). */\n private _selection: DiagramNodeData[] = [];\n\n /**\n * Ids of the emphasised nodes; every other node component is dimmed.\n * Runtime interaction state, off the options bag — the view's own mirror\n * of `DiagramEdgeLayer`'s `_edgeEmphasis`, except there is no equivalent\n * layer for nodes, so it lives here instead. Cleared in\n * `promoteIncomingNodes` beside `_selection`.\n */\n private _nodeEmphasis: Set<string> = new Set();\n\n /** Monotonic layout token; guards against a stale in-flight layout landing. */\n private _layoutGeneration: number = 0;\n\n /** Resolver for the promise `whenLaidOut` hands out, or `null` when idle. */\n private _layoutSettled: { promise: Promise<void>; resolve: () => void } | null = null;\n\n /**\n * The overlay busy indicator, built the first time a layout pass runs on a\n * view that has a size. Runtime state, deliberately off the options bag.\n */\n private _busySpinner: ProgressSpinner | null = null;\n\n /** Cached graph bounding box from the last successful layout. */\n private _graphWidth: number = 0;\n private _graphHeight: number = 0;\n\n /** Custom-event fan-out for `\"selection\"` / `\"layout\"` / `\"contextmenu\"`. */\n private _listeners: ListenerBag<DiagramViewEvent> = new ListenerBag<DiagramViewEvent>();\n\n /** Edge ids of the last emitted \"edgehover\", joined, or null when not hovering. */\n private _hoveredEdgeKey: string | null = null;\n\n /** Pan drag state. */\n private _panning: boolean = false;\n private _panStartX: number = 0;\n private _panStartY: number = 0;\n\n /**\n * Pointer position at the last `pointerdown`, and whether the pointer has\n * since travelled past `CLICK_SLOP`. Runtime gesture state, off the\n * options bag: `_handleClick` reads `_pointerMoved` to tell a click from\n * the tail end of a drag — a pan that starts and ends on empty canvas\n * still fires a native `click`, and it must not clear the selection (or\n * the edge emphasis the app keys off it).\n */\n private _pressX: number = 0;\n private _pressY: number = 0;\n private _pointerMoved: boolean = false;\n\n /** Pan offset (viewport pixels) captured when a drag begins. */\n private _panOriginX: number = 0;\n private _panOriginY: number = 0;\n\n /** Current, unbounded pan offset (viewport pixels) driving the content host's transform. */\n private _panX: number = 0;\n private _panY: number = 0;\n\n /**\n * Whether the graph still owes its one-time initial centring. Cleared by\n * the first layout that manages to centre, so a later `setData` re-layout\n * never yanks a pan the user has since dragged to.\n */\n private _needsInitialCentre: boolean = true;\n\n /**\n * Id of the node the one-shot initial view centres on, instead of the\n * graph's bounds; also the target of the most recent `focusNode` call.\n * Seeded from `initialFocusNode` in the constructor body (mirroring how\n * `data`/`controls` are cached during `applyOptions` and dispatched once\n * the constructor body can act on them) and re-pointed by `focusNode`.\n */\n private _focusNodeId: string | null = null;\n\n /**\n * Viewport size at the last layout, so a resize can be measured as a delta\n * and the graph point under the viewport centre held in place. `NaN` until\n * the view is first sized (matching `getWidth`/`getHeight`).\n */\n private _lastViewportWidth: number = NaN;\n private _lastViewportHeight: number = NaN;\n\n /** The corner-pinned zoom / fit / reset control cluster. */\n private _controls!: Component;\n private _zoomInBtn!: Button;\n private _zoomOutBtn!: Button;\n private _fitBtn!: Button;\n private _resetBtn!: Button;\n\n private readonly _onZoomIn: () => void = () => this.zoomIn();\n private readonly _onZoomOut: () => void = () => this.zoomOut();\n private readonly _onFit: () => void = () => this.zoomToFit();\n private readonly _onReset: () => void = () => this.resetView();\n\n constructor(options?: DiagramViewOptions) {\n super(options, { zoom: DEFAULT_ZOOM, minZoom: DEFAULT_MIN_ZOOM, maxZoom: DEFAULT_MAX_ZOOM, controls: true });\n\n this.setLayoutManager(new Anchor());\n this.setCursor(\"grab\");\n\n this._engine = this.createEngine();\n\n // Nodes are laid out at unscaled graph coordinates under the host's\n // `translate(panX,panY) scale(zoom)` transform, and the host's own box\n // is likewise sized to the unscaled graph bounds (see applyLayout), so\n // node coordinates never exceed the box regardless of zoom — unlike the\n // old scaled-box model this stays visible for consistency with that\n // history rather than out of present necessity.\n // `cursor: \"inherit\"` rather than the Component default: the host is an\n // invisible box spanning the whole graph bounds, so the default\n // `cursor: default` every Component stamps would paint an arrow across\n // the entire canvas and mask the viewport's own grab/grabbing. Inheriting\n // lets the single write on the view root govern the whole canvas.\n this._contentHost = new Container({ layoutManager: new Absolute(), overflow: \"visible\", cursor: \"inherit\" });\n this._contentHost.setTransformOrigin(\"0 0\");\n this.addComponent(this._contentHost);\n\n this._edgeLayer = new DiagramEdgeLayer();\n this._contentHost.addComponent(this._edgeLayer);\n\n this.buildControls();\n this.wireControlListeners();\n\n const controlsConstraints = new AnchorConstraints();\n controlsConstraints.right = CONTROLS_MARGIN;\n controlsConstraints.bottom = CONTROLS_MARGIN;\n this.addComponent(this._controls, controlsConstraints);\n\n this.applyListeners(options?.listeners);\n\n this.setControlsVisible(this._options.controls ?? this._defaultOptions.controls ?? true);\n\n if (this._options.zoom !== undefined) {\n this.setZoom(this._options.zoom);\n }\n\n this._focusNodeId = this._options.initialFocusNode ?? null;\n\n if (this._options.data) {\n this.setData(this._options.data);\n }\n }\n\n /**\n * Builds the layout engine. Isolated as a factory so the swappable-engine\n * seam can be exercised (a test substitutes a stub engine that returns a\n * fixed result without importing ELK).\n *\n * @returns A fresh {@link ElkLayoutEngine}.\n */\n protected createEngine(): ElkLayoutEngine {\n return new ElkLayoutEngine({\n workerFactory: this._options.elkWorkerFactory,\n workerUrl: this._options.elkWorkerUrl,\n });\n }\n\n /**\n * Disposes the layout engine — releasing its ELK Web Worker, if it had one\n * — before the inherited destructor detaches the element.\n */\n protected destructor(): void {\n // Invalidate any layout still in flight before the engine goes away. A\n // result landing afterwards would write into a torn-down view, and a\n // failure landing afterwards would strip nodes off it; both guards\n // compare against this token and drop a stale one.\n this._layoutGeneration += 1;\n\n // Settle rather than leave dangling: a caller awaiting `whenLaidOut()`\n // (e.g. the app holding a lazy tab's spinner) must not hang forever\n // just because the view was disposed on some other path mid-pass.\n this.settleLayout();\n\n // The spinner is mounted by a raw DOM append rather than as a child\n // component, so the inherited destructor's child pass never reaches it.\n this._busySpinner?.dispose();\n this._busySpinner = null;\n\n this._engine.dispose();\n\n super.destructor();\n }\n\n /**\n * Caches consumer-configurable fields pure to `_options`; effects that need\n * the content host (built in the constructor body) are dispatched there.\n *\n * @param options - The options bag carrying the values to apply.\n */\n protected applyOptions(options: DiagramViewOptions): this {\n super.applyOptions(options);\n\n if (options.data !== undefined) this._options.data = options.data;\n if (options.nodeRenderer !== undefined) this._options.nodeRenderer = options.nodeRenderer;\n if (options.groupRenderer !== undefined) this._options.groupRenderer = options.groupRenderer;\n if (options.layoutOptions !== undefined) this._options.layoutOptions = options.layoutOptions;\n if (options.elkWorkerUrl !== undefined) this._options.elkWorkerUrl = options.elkWorkerUrl;\n if (options.elkWorkerFactory !== undefined) this._options.elkWorkerFactory = options.elkWorkerFactory;\n if (options.minZoom !== undefined) this._options.minZoom = options.minZoom;\n if (options.maxZoom !== undefined) this._options.maxZoom = options.maxZoom;\n if (options.zoom !== undefined) this._options.zoom = options.zoom;\n // Cached only: the control cluster does not exist yet during the\n // `super()` cascade. The constructor dispatches `setControlsVisible`\n // itself once the cluster is built.\n if (options.controls !== undefined) this._options.controls = options.controls;\n // Cached only: dispatched into the runtime `_focusNodeId` field from\n // the constructor body, mirroring `data` below.\n if (options.initialFocusNode !== undefined) this._options.initialFocusNode = options.initialFocusNode;\n\n return this;\n }\n\n /**\n * Replaces the graph, rebuilds node components, and triggers an async layout.\n *\n * @param data - The new graph.\n *\n * @returns This view, for method chaining.\n */\n setData(data: DiagramData): this {\n this._options.data = data;\n this.rebuildNodes(data);\n this.relayout(data);\n\n return this;\n }\n\n /**\n * Returns the current graph, or `null` when none has been set.\n *\n * @returns The graph data, or `null`.\n */\n getData(): DiagramData | null {\n return this._options.data ?? null;\n }\n\n /**\n * Builds fresh node components via the node renderer (default:\n * `DiagramNode`) into the *incoming* set, built and measured off the\n * component tree and left `setVisible(false)` — not `display: none`, so\n * the flag still reads false the instant a component mounts, before the\n * promote step's reveal. `applyLayout` positions them, then\n * `promoteIncomingNodes` swaps them in for the shown set, mounts them, and\n * reveals them together, so a diagram never paints an unplaced graph and a\n * re-layout keeps the previous graph on screen for the whole ELK\n * round-trip. A graph superseded by a newer `setData` before its layout\n * lands is therefore never mounted at all. Leads with\n * `discardIncomingNodes()` so a second `setData` arriving before the\n * first's layout lands does not leak the first attempt's components.\n * Recurses into `children`: a container node (non-empty `children`) is\n * built via the group renderer (default: `DiagramGroupNode`) and its\n * children are built the same way, so container + leaf components all\n * land as flat siblings in `_incomingComponents`/`_incomingData`, keyed by\n * id — a container is never a DOM parent of its children (that would\n * perturb the single-content-host model `nodeIdAt` hit-testing relies on).\n *\n * @param data - The graph whose nodes to build.\n */\n private rebuildNodes(data: DiagramData): void {\n this.discardIncomingNodes();\n\n const renderer = this._options.nodeRenderer ?? ((node: DiagramNodeData): Component =>\n new DiagramNode({ label: node.label, glyph: node.glyph, badge: node.badge }));\n const groupRenderer = this._options.groupRenderer ?? ((node: DiagramNodeData): Component =>\n new DiagramGroupNode({ label: node.label, glyph: node.glyph }));\n\n const build = (nodes: DiagramNodeData[]): void => {\n for (const node of nodes) {\n const isContainer = (node.children?.length ?? 0) > 0;\n const component = isContainer ? groupRenderer(node) : renderer(node);\n\n component.setVisible(false);\n\n this._incomingComponents.set(node.id, component);\n this._incomingData.set(node.id, node);\n\n if (isContainer) {\n this._incomingContainerIds.add(node.id);\n build(node.children!);\n }\n }\n };\n\n build(data.nodes);\n }\n\n /** Forgets the un-promoted incoming components; never mounted, so nothing to detach. */\n private discardIncomingNodes(): void {\n this._incomingComponents.clear();\n this._incomingData.clear();\n this._incomingContainerIds.clear();\n }\n\n /**\n * Swaps the incoming set in for the shown one: removes the shown\n * components from the content host, promotes the incoming maps, clears\n * the selection and the node emphasis, then mounts and reveals every\n * promoted component together — this is the first time an incoming\n * component is added to the content host at all.\n */\n private promoteIncomingNodes(): void {\n for (const component of this._nodeComponents.values()) {\n this._contentHost.removeComponent(component);\n }\n\n this._nodeComponents = this._incomingComponents;\n this._nodeData = this._incomingData;\n this._containerIds = this._incomingContainerIds;\n\n this._incomingComponents = new Map();\n this._incomingData = new Map();\n this._incomingContainerIds = new Set();\n\n this._selection = [];\n this._nodeEmphasis = new Set();\n\n for (const component of this._nodeComponents.values()) {\n this._contentHost.addComponent(component);\n component.setVisible(true);\n }\n }\n\n /**\n * Collects node sizes, bumps the generation token, and runs the async ELK\n * layout. A stale result (older token) is dropped; a failed layout tears the\n * nodes down so the view stays empty.\n *\n * @param data - The graph to lay out.\n */\n private relayout(data: DiagramData): void {\n this.armLayoutSettled();\n this.syncBusyIndicator();\n\n const sizes = this.collectNodeSizes(data);\n\n this._layoutGeneration += 1;\n const generation = this._layoutGeneration;\n\n this._engine\n .layout(data, sizes, this._options.layoutOptions)\n .then((result) => this.applyLayout(result, generation))\n .catch(() => this.handleLayoutFailure(generation));\n }\n\n /**\n * Arms the awaitable `whenLaidOut` hands out, so one deferred spans\n * however many layout passes run before one of them finishes. A no-op\n * when a pass is already in flight, so two rapid `setData` calls share\n * one promise, resolved by whichever pass settles it.\n */\n private armLayoutSettled(): void {\n if (this._layoutSettled !== null) {\n return;\n }\n\n let resolve!: () => void;\n const promise = new Promise<void>((r) => { resolve = r; });\n\n this._layoutSettled = { promise, resolve };\n }\n\n /** Settles the armed `whenLaidOut` awaitable, if there is one. */\n private settleLayout(): void {\n const settled = this._layoutSettled;\n\n this._layoutSettled = null;\n settled?.resolve();\n\n this.syncBusyIndicator();\n }\n\n /**\n * Matches the overlay busy indicator to whether a layout pass is in flight,\n * and re-sizes a showing overlay to the current viewport. Called when a pass\n * starts, when one settles, and from every layout pass — which is where a\n * view that had no size when its pass started finally gets one.\n *\n * A view with no committed size has nothing to cover: `getWidth()` /\n * `getHeight()` are `NaN` (not 0) until the first `setSize`, and `> 0` rejects\n * both (see `effectiveMinZoom`). That also keeps the indicator off the first\n * pass every diagram runs from its own constructor, before its host has sized\n * it — whatever opened the view owns that first wait.\n */\n private syncBusyIndicator(): void {\n if (this._layoutSettled === null || !(this.getWidth() > 0) || !(this.getHeight() > 0)) {\n this._busySpinner?.hideOverlay();\n\n return;\n }\n\n if (this._busySpinner === null) {\n this._busySpinner = new ProgressSpinner(BUSY_SPINNER_DIAMETER);\n }\n\n // `showOverlay` is a no-op once shown, so the explicit `doLayout` is what\n // re-sizes a showing overlay after a viewport resize: the spinner is\n // mounted by a raw DOM append, so it is not in this view's laid-out set\n // and nothing else ever lays it out.\n this._busySpinner.showOverlay(this);\n this._busySpinner.doLayout();\n }\n\n /**\n * Resolves once the layout pass currently in flight has finished placing\n * nodes. Resolves immediately when no pass is in flight, and resolves\n * (never rejects) when a pass fails or the view is disposed mid-pass —\n * a rejecting promise would force every caller into a `try`/`catch`\n * whose only sensible branch is \"carry on anyway\".\n *\n * @returns A promise settling on the next finished layout pass.\n */\n whenLaidOut(): Promise<void> {\n return this._layoutSettled?.promise ?? Promise.resolve();\n }\n\n /**\n * Resolves each node's size fed to ELK: the explicit `width`/`height` from\n * the model when present, else the node component's preferred size — read\n * before the component is mounted, which is safe because measurement goes\n * through the DOM seam's font metrics rather than the live document (a\n * custom `nodeRenderer` whose preferred size needs a live element in the\n * document is not supported). Recurses into `children` so every container\n * and leaf is represented; a container's entry is harmless-but-unused\n * since `buildElkGraph` computes a container's box from its contents\n * rather than consulting this map.\n *\n * @param data - The graph whose node sizes to collect.\n * @returns A map of node id to resolved size.\n */\n private collectNodeSizes(data: DiagramData): Map<string, { width: number; height: number }> {\n const sizes = new Map<string, { width: number; height: number }>();\n\n const collect = (nodes: DiagramNodeData[]): void => {\n for (const node of nodes) {\n const component = this._incomingComponents.get(node.id);\n const preferred = component?.getPreferredSize();\n\n sizes.set(node.id, {\n width: node.width ?? preferred?.width ?? 0,\n height: node.height ?? preferred?.height ?? 0,\n });\n\n if (node.children && node.children.length > 0) {\n collect(node.children);\n }\n }\n };\n\n collect(data.nodes);\n\n return sizes;\n }\n\n /**\n * Applies a completed layout: positions each incoming node, promotes the\n * incoming set to shown (revealing it), sizes + scales the content host,\n * redraws the edges, and emits `\"layout\"`. A stale result (superseded by\n * a newer `setData`) is ignored.\n *\n * @param result - The ELK layout result.\n * @param generation - The generation token captured when the layout started.\n */\n private applyLayout(result: DiagramLayoutResult, generation: number): void {\n if (generation !== this._layoutGeneration) {\n return;\n }\n\n for (const node of result.nodes) {\n const component = this._incomingComponents.get(node.id);\n\n if (component) {\n component.setPreferredSize({ width: node.width, height: node.height });\n component.setX(node.x);\n component.setY(node.y);\n }\n }\n\n this._graphWidth = result.width;\n this._graphHeight = result.height;\n\n this.promoteIncomingNodes();\n\n this._edgeLayer.setX(0);\n this._edgeLayer.setY(0);\n this._edgeLayer.setPreferredSize({ width: result.width, height: result.height });\n this._edgeLayer.setEdges(this.joinEdgeStyles(result.edges));\n\n this.applyContainerZIndex();\n\n // The host is no longer resized per zoom (see applyTransformToHost) — its\n // box always matches the unscaled graph bounds the nodes are laid out at.\n this._contentHost.setPreferredSize({ width: result.width, height: result.height });\n this.applyTransformToHost();\n\n // Before `emit`, so a consumer's own `\"layout\"` listener (the sanctioned\n // auto-fit hook, `view.on(\"layout\", () => view.zoomToFit())`) still runs\n // afterwards and wins. Only succeeds if the view is already sized; the\n // `doLayout` override retries otherwise.\n this.tryInitialCentre();\n\n this.scheduleLayout();\n\n this.emit(\"layout\");\n\n this.settleLayout();\n }\n\n /**\n * Applies the compound paint order — containers behind the edge layer,\n * leaves in front of it — when `rebuildNodes` built at least one\n * container. A flat graph's own (freshly-built) node components are never\n * touched here — they start at `DEFAULT_Z_INDEX` — but the persistent edge\n * layer is explicitly restored to `DEFAULT_Z_INDEX`, since an earlier\n * compound `setData` on this same view could have left it elevated (see\n * the module-level z-index constants).\n */\n private applyContainerZIndex(): void {\n if (this._containerIds.size === 0) {\n this._edgeLayer.setZIndex(DEFAULT_Z_INDEX);\n\n return;\n }\n\n this._edgeLayer.setZIndex(EDGE_LAYER_Z_INDEX);\n\n for (const [id, component] of this._nodeComponents) {\n component.setZIndex(this._containerIds.has(id) ? CONTAINER_Z_INDEX : LEAF_Z_INDEX);\n }\n }\n\n /**\n * Re-attaches each model edge's `style` to its layout-routed counterpart.\n * ELK's result carries only `{ id, sections }` — `style` never survives the\n * round trip — so this joins by edge id back to `this._options.data.edges`,\n * the model the layout was computed from.\n *\n * @param edges - The ELK-routed edges (id + sections only).\n * @returns The same routes, each carrying its model edge's `style` if any.\n */\n private joinEdgeStyles(edges: DiagramEdgeRoute[]): DiagramEdgeRoute[] {\n const modelById = new Map(this._options.data?.edges.map(e => [e.id, e]) ?? []);\n\n return edges.map(edge => ({ ...edge, style: modelById.get(edge.id)?.style }));\n }\n\n /**\n * Discards the just-built incoming nodes when a layout fails (e.g. `elkjs`\n * absent), so a first, never-shown graph leaves the view empty, and a\n * failed *re*-layout leaves whatever graph was already on screen — rather\n * than showing stacked, unpositioned nodes either way.\n *\n * @param generation - The generation token captured when the layout started.\n */\n private handleLayoutFailure(generation: number): void {\n if (generation !== this._layoutGeneration) {\n return;\n }\n\n this.discardIncomingNodes();\n this.settleLayout();\n }\n\n /**\n * Writes the content host's `translate(panX,panY) scale(zoom)` transform\n * from the current pan offset and zoom factor. The host's box is set once\n * per layout (see `applyLayout`) to the unscaled graph bounds and is not\n * touched here — pan and zoom live entirely on the transform.\n */\n private applyTransformToHost(): void {\n const zoom = this.getZoom();\n\n this._contentHost.setTransform(`translate(${this._panX}px, ${this._panY}px) scale(${zoom})`);\n }\n\n /**\n * Returns the current zoom factor.\n *\n * @returns The zoom factor.\n */\n getZoom(): number {\n return this._options.zoom ?? this._defaultOptions.zoom ?? DEFAULT_ZOOM;\n }\n\n /**\n * Sets the zoom factor, clamped to `[minZoom, maxZoom]` (the lower bound\n * adaptively lowered so a huge graph can still reach its fit zoom — see\n * `effectiveMinZoom`), and re-applies the content host's transform. A\n * non-finite request (e.g. `zoomToFit`'s `graphWidth / 0` on an unsized\n * view) is rejected outright rather than clamped, since `Math.max`/`min`\n * propagate `NaN` instead of resolving it — this is the one guard every\n * zoom-changing entry point (`zoomToFit`, `resetView`, `zoomIn`/`zoomOut`)\n * relies on rather than each re-checking its own inputs.\n *\n * @param zoom - The desired zoom factor.\n *\n * @returns This view, for method chaining.\n */\n setZoom(zoom: number): this {\n if (!Number.isFinite(zoom)) {\n return this;\n }\n\n this._options.zoom = this.clampZoom(zoom);\n this.applyTransformToHost();\n\n return this;\n }\n\n /**\n * Fits the whole graph into the viewport by choosing the largest zoom at\n * which the graph bounds fit both axes, then centres the graph. Requires a\n * completed layout.\n *\n * @returns This view, for method chaining.\n */\n zoomToFit(): this {\n if (this._graphWidth <= 0 || this._graphHeight <= 0) {\n return this;\n }\n\n const zoomX = this.getWidth() / this._graphWidth;\n const zoomY = this.getHeight() / this._graphHeight;\n\n this.setZoom(Math.min(zoomX, zoomY));\n this.centreGraph();\n\n return this;\n }\n\n /**\n * Resets to the default zoom, then re-centres: on the focus node when the\n * view has one that is in the shown graph, else on the graph bounds. The\n * focus node is `initialFocusNode`, or the target of the most recent\n * `focusNode` call. Centring a node also lowers the zoom, if needed, until\n * the node's whole box fits. Retried after the next layout pass when the\n * view has no committed size yet.\n *\n * @returns This view, for method chaining.\n */\n resetView(): this {\n this.setZoom(this._defaultOptions.zoom ?? DEFAULT_ZOOM);\n\n this._needsInitialCentre = true;\n this.tryInitialCentre();\n\n return this;\n }\n\n /**\n * Steps the zoom up by the configured multiplicative factor, keeping the\n * graph point currently at the viewport centre fixed.\n *\n * @returns This view, for method chaining.\n */\n zoomIn(): this {\n this.zoomAboutViewportPoint(ZOOM_BUTTON_STEP, this.getWidth() / 2, this.getHeight() / 2);\n\n return this;\n }\n\n /**\n * Steps the zoom down by the configured multiplicative factor, keeping the\n * graph point currently at the viewport centre fixed.\n *\n * @returns This view, for method chaining.\n */\n zoomOut(): this {\n this.zoomAboutViewportPoint(1 / ZOOM_BUTTON_STEP, this.getWidth() / 2, this.getHeight() / 2);\n\n return this;\n }\n\n /**\n * Attempts the one-time initial centring, so the first render shows the\n * graph where `resetView` would put it rather than at the viewport's\n * top-left corner. The configured `zoom` deliberately stands (unlike\n * `resetView`, which also restores the default zoom), so a consumer's\n * explicit `zoom` option still decides the initial scale — except that a\n * focus node too large to fit the viewport lowers it until the node fits.\n *\n * Both inputs arrive asynchronously and in either order: the graph bounds\n * come from an ELK layout, the viewport size from the host's layout pass.\n * So this is *retried* — from `applyLayout`, from every `doLayout`, and\n * from `resetView` — until it actually succeeds, and the pending flag is\n * cleared only on a confirmed centring. Clearing it on an attempt that\n * silently no-opped (the view not yet sized, `centreGraph` returning\n * `false`) is what used to leave the diagram stuck in the corner whenever\n * the layout landed first.\n *\n * Picks between two targets: `_focusNodeId`, when it names a node in the\n * graph just promoted, or the graph's bounds otherwise — the same\n * generalisation `focusNode` reuses at runtime instead of a separate\n * one-shot mechanism.\n */\n private tryInitialCentre(): void {\n if (!this._needsInitialCentre || !(this._graphWidth > 0) || !(this._graphHeight > 0)) {\n return;\n }\n\n const focus = this._focusNodeId !== null && this._nodeComponents.has(this._focusNodeId)\n ? this._focusNodeId\n : null;\n\n if (focus !== null ? this.centreNode(focus) : this.centreGraph()) {\n this._needsInitialCentre = false;\n }\n }\n\n /**\n * Centres the graph bounds in the viewport at the current zoom, without\n * changing the zoom factor. Shared by `zoomToFit`, `resetView`, and the\n * initial centring. Declines before the view is sized —\n * `getWidth()`/`getHeight()` are `NaN` (not 0) until then (see\n * `effectiveMinZoom`), and writing a `NaN` pan would blank the diagram\n * until another call recomputes it.\n *\n * @returns `true` when the pan was written, `false` when the view has no\n * committed size yet and the caller should try again later.\n */\n private centreGraph(): boolean {\n const vw = this.getWidth();\n const vh = this.getHeight();\n\n if (!(vw > 0) || !(vh > 0)) {\n return false;\n }\n\n const zoom = this.getZoom();\n\n this._panX = (vw - this._graphWidth * zoom) / 2;\n this._panY = (vh - this._graphHeight * zoom) / 2;\n\n this.applyTransformToHost();\n\n return true;\n }\n\n /**\n * Scales by `factor` about a fixed viewport point: the graph point under\n * `(vx, vy)` maps to the same viewport point before and after the zoom.\n * Shared by the zoom in/out control buttons and (about the pointer instead\n * of the viewport centre) by wheel-zoom. A no-op when `vx`/`vy` is not\n * finite — `zoomIn`/`zoomOut` pass the viewport centre, which is `NaN`\n * before the view is sized (see `centreGraph`).\n *\n * @param factor - The multiplicative zoom change (`> 1` zooms in).\n * @param vx - The viewport-relative x coordinate to keep fixed.\n * @param vy - The viewport-relative y coordinate to keep fixed.\n */\n private zoomAboutViewportPoint(factor: number, vx: number, vy: number): void {\n if (!Number.isFinite(vx) || !Number.isFinite(vy)) {\n return;\n }\n\n const oldZoom = this.getZoom();\n const newZoom = this.clampZoom(oldZoom * factor);\n\n if (newZoom === oldZoom) {\n return;\n }\n\n const graphX = (vx - this._panX) / oldZoom;\n const graphY = (vy - this._panY) / oldZoom;\n\n this._panX = vx - graphX * newZoom;\n this._panY = vy - graphY * newZoom;\n\n this.setZoom(newZoom);\n }\n\n /**\n * Resolves the effective minimum zoom: the configured `minZoom`, floored\n * further down to the zoom that fits the whole graph in the current\n * viewport when the graph is too large for that configured floor to reach.\n * A small graph is unaffected (its fit zoom already exceeds the configured\n * minimum); an unsized or unlaid-out view falls back to the configured\n * minimum untouched.\n *\n * @returns The effective minimum zoom factor.\n */\n private effectiveMinZoom(): number {\n const configuredMin = this._options.minZoom ?? this._defaultOptions.minZoom ?? DEFAULT_MIN_ZOOM;\n const vw = this.getWidth();\n const vh = this.getHeight();\n\n // `getWidth()` / `getHeight()` are `NaN` (not 0) before the first\n // `setSize` — `> 0` (rather than `<= 0`) also rejects NaN, since every\n // NaN comparison is false.\n if (!(this._graphWidth > 0) || !(this._graphHeight > 0) || !(vw > 0) || !(vh > 0)) {\n return configuredMin;\n }\n\n const fitZoom = Math.min(vw / this._graphWidth, vh / this._graphHeight);\n\n return Math.min(configuredMin, fitZoom);\n }\n\n /**\n * Clamps a zoom request to `[effectiveMinZoom(), maxZoom]`.\n *\n * @param zoom - The requested zoom factor.\n * @returns The clamped zoom factor.\n */\n private clampZoom(zoom: number): number {\n const min = this.effectiveMinZoom();\n const max = this._options.maxZoom ?? this._defaultOptions.maxZoom ?? DEFAULT_MAX_ZOOM;\n\n return Math.max(min, Math.min(max, zoom));\n }\n\n /**\n * Resolves the zoom that fits `size` whole in the viewport, never raising\n * the current zoom — only lowering it when the size does not already fit.\n * Shared by every node-centring entry point (`centreNode`, and so\n * `revealNode`, `focusNode`, and the focus-node branch of\n * `tryInitialCentre`) so a card larger than the viewport is never\n * centred-and-clipped.\n *\n * @param size - The node box to fit, in unscaled graph coordinates.\n * @returns The clamped zoom to use for the centring.\n */\n private zoomFittingNode(size: { width: number; height: number }): number {\n const current = this.getZoom();\n const vw = this.getWidth();\n const vh = this.getHeight();\n\n // `getWidth()` / `getHeight()` are `NaN` before the first `setSize`, and a\n // zero-sized node box has no fit zoom — `> 0` rejects both, since every\n // NaN comparison is false (see `effectiveMinZoom`).\n if (!(vw > 0) || !(vh > 0) || !(size.width > 0) || !(size.height > 0)) {\n return this.clampZoom(current);\n }\n\n const fitZoom = Math.min(vw / size.width, vh / size.height);\n\n return this.clampZoom(Math.min(current, fitZoom));\n }\n\n /**\n * Returns the current selection (single-select).\n *\n * @returns A copy of the selected node data array.\n */\n getSelection(): DiagramNodeData[] {\n return [...this._selection];\n }\n\n /**\n * Selects a node programmatically without emitting `\"selection\"` (mirroring\n * the Tree precedent). Passing `null` clears the selection.\n *\n * @param id - The node id to select, or `null` to clear.\n *\n * @returns This view, for method chaining.\n */\n selectNode(id: string | null): this {\n this.setSelection(id);\n\n return this;\n }\n\n /**\n * Emphasises a subset of the drawn edges: every edge outside the set\n * recedes to a lower opacity while the named ones keep their normal\n * weight. Cleared by `null`, by an empty array, and by the next layout\n * that replaces the drawn edges. Emits nothing. Forwards straight to the\n * edge layer, which owns the emphasis state — this view holds no copy of\n * its own.\n *\n * @param ids - The edge ids to emphasise, or null to clear.\n *\n * @returns This view, for method chaining.\n */\n setEdgeEmphasis(ids: readonly string[] | null): this {\n this._edgeLayer.setEdgeEmphasis(ids);\n\n return this;\n }\n\n /**\n * The currently emphasised edge ids.\n *\n * @returns A copy of the emphasised id array; empty when nothing is emphasised.\n */\n getEdgeEmphasis(): string[] {\n return this._edgeLayer.getEdgeEmphasis();\n }\n\n /**\n * Emphasises a subset of the drawn nodes: every node component outside\n * the set dims to a reduced opacity while the named ones keep full\n * opacity. Cleared by `null`, by an empty array, and by the next layout\n * that rebuilds the node components (`promoteIncomingNodes`). Emits\n * nothing. Dims through `Component.setOpacity` / `clearOpacity` directly\n * on each node's own root, so a custom `nodeRenderer` needs no\n * cooperation — unlike `applySelectedVisual`, which duck-types a\n * `setSelected?.()` because \"selected\" has no single generic rendering.\n * An unknown id is kept in the set (and reported back by\n * `getNodeEmphasis`) but dims nothing, since no node component answers to\n * it.\n *\n * @param ids - The node ids to emphasise, or null to clear.\n *\n * @returns This view, for method chaining.\n */\n setNodeEmphasis(ids: readonly string[] | null): this {\n this._nodeEmphasis = new Set(ids ?? []);\n this.applyNodeEmphasis();\n\n return this;\n }\n\n /**\n * The currently emphasised node ids.\n *\n * @returns A copy of the emphasised id array; empty when nothing is emphasised.\n */\n getNodeEmphasis(): string[] {\n return [...this._nodeEmphasis];\n }\n\n /** Rewrites every node component's opacity from the current emphasis set. */\n private applyNodeEmphasis(): void {\n for (const [id, component] of this._nodeComponents) {\n if (this._nodeEmphasis.size === 0 || this._nodeEmphasis.has(id)) {\n component.clearOpacity();\n } else {\n component.setOpacity(DIMMED_NODE_OPACITY);\n }\n }\n }\n\n /**\n * Pans so the given node is centred in the viewport, without changing the\n * selection or emitting any event. Also lowers the zoom, if needed, until\n * the node's whole box fits the viewport — never raises it. No-op for an\n * unknown id, before the first layout has positioned the node, or before\n * the view has a real committed size — `getWidth()`/`getHeight()` are\n * `NaN` (not `0`) until then (see `effectiveMinZoom`). Unlike a\n * non-finite zoom request, which `setZoom` rejects outright, a `NaN` pan\n * has no such gate here, so it is guarded directly: a `NaN` pan is\n * sticky, silently blanking the diagram until another call recomputes\n * it. Pair with {@link selectNode} to both highlight and reveal. Unlike\n * {@link focusNode}, this does not retry — a no-op call must be repeated\n * by the caller once the view is sized.\n *\n * @param id - The node id to centre, or a no-op when not found.\n *\n * @returns This view, for method chaining.\n */\n revealNode(id: string): this {\n this.centreNode(id);\n\n return this;\n }\n\n /**\n * Centres a node in the viewport by writing the pan, without changing the\n * selection or emitting any event. Also lowers the zoom, via\n * `zoomFittingNode`, when the node's whole box does not already fit the\n * viewport — never raises it.\n *\n * @param id - The node id to centre.\n *\n * @returns `true` when the pan and zoom were written, `false` when the\n * node is unknown or the view has no committed size yet.\n */\n private centreNode(id: string): boolean {\n const component = this._nodeComponents.get(id);\n const size = component?.getPreferredSize();\n\n if (!component || !size) {\n return false;\n }\n\n const zoom = this.zoomFittingNode(size);\n\n // Node centre in unscaled graph coordinates.\n const centreX = component.getX() + size.width / 2;\n const centreY = component.getY() + size.height / 2;\n\n // Pan so the node centre maps to the viewport centre: viewport = pan + graph·zoom.\n const panX = this.getWidth() / 2 - centreX * zoom;\n const panY = this.getHeight() / 2 - centreY * zoom;\n\n if (!Number.isFinite(panX) || !Number.isFinite(panY)) {\n return false;\n }\n\n this._panX = panX;\n this._panY = panY;\n\n // Writes the pan fields first, then lets `setZoom` apply the transform\n // once — the shape `zoomAboutViewportPoint` already uses.\n this.setZoom(zoom);\n\n return true;\n }\n\n /**\n * Centres the given node in the viewport, retrying after each layout pass\n * until it succeeds — unlike `revealNode`, which centres only when the\n * graph and the viewport are both already measured. Also lowers the\n * zoom, if needed, until the node's whole box fits the viewport; never\n * raises it.\n *\n * @param id - The node id to centre on.\n *\n * @returns This view, for method chaining.\n */\n focusNode(id: string): this {\n this._focusNodeId = id;\n this._needsInitialCentre = true;\n\n this.tryInitialCentre();\n\n return this;\n }\n\n /**\n * Updates the selection state and toggles each node's selected visual.\n *\n * @param id - The node id to select, or `null` to clear.\n */\n private setSelection(id: string | null): void {\n for (const component of this._nodeComponents.values()) {\n this.applySelectedVisual(component, false);\n }\n\n const data = id === null ? undefined : this._nodeData.get(id);\n\n if (data) {\n this._selection = [data];\n this.applySelectedVisual(this._nodeComponents.get(id!)!, true);\n } else {\n this._selection = [];\n }\n }\n\n /**\n * Toggles a node component's selected visual when it supports one (the\n * default `DiagramNode` does; a custom renderer may not).\n *\n * @param component - The node component.\n * @param selected - The selected state to apply.\n */\n private applySelectedVisual(component: Component, selected: boolean): void {\n const node = component as unknown as { setSelected?: (value: boolean) => void };\n\n node.setSelected?.(selected);\n }\n\n /**\n * Registers a listener for a diagram event.\n *\n * @param event - `\"selection\"` fires when the selected node changes;\n * `\"activate\"` fires when a node is double-clicked; `\"layout\"` fires\n * after each successful ELK layout pass; `\"contextmenu\"` fires when a\n * node is right-clicked; `\"edgehover\"` fires with every model edge\n * within the pointer's hit tolerance (several where routes overlap) and\n * the originating event; `\"edgeleave\"` fires when the pointer leaves\n * whatever edge(s) it was hovering.\n * @param listener - The callback to invoke.\n *\n * @returns This view, for method chaining.\n */\n on(event: \"selection\", listener: (nodes: DiagramNodeData[]) => void): this;\n on(event: \"activate\", listener: (node: DiagramNodeData) => void): this;\n on(event: \"layout\", listener: () => void): this;\n on(event: \"contextmenu\", listener: (node: DiagramNodeData, event: MouseEvent) => void): this;\n on(event: \"edgehover\", listener: (edges: DiagramEdgeData[], event: MouseEvent) => void): this;\n on(event: \"edgeleave\", listener: () => void): this;\n on(event: DiagramViewEvent, listener: Function): this {\n this._listeners.add(event, listener);\n\n return this;\n }\n\n /**\n * Removes a previously registered listener; the exact reference must match.\n *\n * @param event - The event the listener was registered for.\n * @param listener - The callback to remove.\n *\n * @returns This view, for method chaining.\n */\n off(event: DiagramViewEvent, listener: Function): this {\n this._listeners.remove(event, listener);\n\n return this;\n }\n\n /**\n * Fires every listener registered for `event`, in registration order.\n *\n * @param event - The event to emit.\n * @param nodes - The selected node data (for `\"selection\"`), or the\n * activated / right-clicked node data (for `\"activate\"` / `\"contextmenu\"`).\n */\n protected emit(event: \"selection\", nodes: DiagramNodeData[]): void;\n protected emit(event: \"activate\", node: DiagramNodeData): void;\n protected emit(event: \"layout\"): void;\n protected emit(event: \"contextmenu\", node: DiagramNodeData, mouseEvent: MouseEvent): void;\n protected emit(event: \"edgehover\", edges: DiagramEdgeData[], mouseEvent: MouseEvent): void;\n protected emit(event: \"edgeleave\"): void;\n protected emit(event: DiagramViewEvent, ...payload: unknown[]): void {\n this._listeners.fire(event, ...payload);\n }\n\n /**\n * Lays the viewport out, then retries the pending initial centring. This is\n * where the viewport size becomes known, and an ELK layout that landed\n * before the host sized this view has nothing to centre against until now\n * (see `tryInitialCentre`). Also re-syncs the busy overlay, which is where a\n * view that had no size when its layout pass started finally picks the\n * overlay up. Writes only the content host's transform and the (unmanaged)\n * overlay, never a child's rect, so neither can feed back into the layout it\n * runs inside.\n *\n * @returns This view, for method chaining.\n */\n doLayout(): this {\n super.doLayout();\n\n this.tryInitialCentre();\n this.anchorCentreAcrossResize();\n this.syncBusyIndicator();\n\n return this;\n }\n\n /**\n * Holds the graph point under the viewport centre still across a viewport\n * resize, so whatever the user was looking at stays in the middle instead\n * of drifting toward a corner as the window grows or shrinks.\n *\n * With `viewport = pan + graph·zoom`, keeping the centre's graph point\n * fixed means `pan += (newExtent − oldExtent) / 2` on each axis — the zoom\n * cancels out, so this is correct at any zoom and never changes it. The\n * first sizing has no previous centre to preserve, so it only records the\n * extent (the initial centring owns that pass).\n */\n private anchorCentreAcrossResize(): void {\n const vw = this.getWidth();\n const vh = this.getHeight();\n\n if (!(vw > 0) || !(vh > 0)) {\n return;\n }\n\n const previousWidth = this._lastViewportWidth;\n const previousHeight = this._lastViewportHeight;\n\n this._lastViewportWidth = vw;\n this._lastViewportHeight = vh;\n\n if (!(previousWidth > 0) || !(previousHeight > 0)) {\n return;\n }\n\n if (previousWidth === vw && previousHeight === vh) {\n return;\n }\n\n this._panX += (vw - previousWidth) / 2;\n this._panY += (vh - previousHeight) / 2;\n\n this.applyTransformToHost();\n }\n\n /**\n * Wires the DOM-routed pan / zoom / selection listeners once the element\n * exists.\n *\n * @param element - Optional element from the render pipeline.\n *\n * @returns This view, for method chaining.\n */\n protected init(element?: Handle): this {\n super.init(element);\n\n // All seven use the SUBTREE variant: the content (nodes, the SVG edge\n // layer, and the Panel's own overlay-scroll element) are descendants of\n // this view's root, so a real wheel/pointer event's target is never the\n // root itself. An exact-target `addListener` would therefore never fire\n // for pan/zoom — only `addSubtreeListener` sees the descendant events\n // (mirrors click/dblclick, which is why selection worked but pan did not).\n Event.addSubtreeListener(this, \"click\", this._handleClick);\n Event.addSubtreeListener(this, \"dblclick\", this._handleDoubleClick);\n Event.addSubtreeListener(this, \"contextmenu\", this._handleContextMenu);\n // Non-passive: `_handleWheel` calls `preventDefault()` to suppress the\n // page's native scroll/zoom, which a passive listener silently ignores\n // (mirrors `Component.attachWheelScrolling` / `WheelTrap`).\n Event.addSubtreeListener(this, \"wheel\", this._handleWheel, { passive: false });\n Event.addSubtreeListener(this, \"pointerdown\", this._handlePointerDown);\n Event.addSubtreeListener(this, \"pointermove\", this._handlePointerMove);\n Event.addSubtreeListener(this, \"pointerup\", this._handlePointerUp);\n // `mousemove`/`mouseout` rather than `mouseenter`/`mouseleave`: per\n // ARCHITECTURE.md, the non-bubbling enter/leave pair never reaches the\n // framework's window-level capture handler.\n Event.addSubtreeListener(this, \"mousemove\", this._handleEdgeMouseMove);\n Event.addSubtreeListener(this, \"mouseout\", this._handleEdgeMouseOut);\n\n return this;\n }\n\n /**\n * Resolves the node under a click and updates the selection, emitting\n * `\"selection\"` when it changes. A click on empty space clears the\n * selection.\n *\n * @param event - The click event whose target is inside the view's subtree.\n */\n private _handleClick(event: MouseEvent): void {\n // A drag is not a click: a pan that starts and ends on empty canvas\n // (or starts on a node and ends on empty canvas) still fires a native\n // \"click\" on their nearest common ancestor, and it must not clear the\n // selection or the edge emphasis the user was studying.\n if (this._pointerMoved) {\n return;\n }\n\n if (this.isControlsTarget(event.target)) {\n return;\n }\n\n // A press on an edge is neither a node click nor a canvas click: it\n // must not clear the selection the user is looking at.\n if (this._edgeLayer.edgeIdAt(event.target) !== null) {\n return;\n }\n\n const id = this.nodeIdAt(event.target);\n\n if (id !== null) {\n if (id === (this._selection[0]?.id ?? null)) {\n return;\n }\n\n this.setSelection(id);\n this.emit(\"selection\", this.getSelection());\n } else if (this._selection.length > 0) {\n this.setSelection(null);\n this.emit(\"selection\", this.getSelection());\n }\n }\n\n /**\n * Resolves the node under a double-click and emits `\"activate\"` with its\n * data. A double-click is preceded by the single click that already\n * selected the node, so this only signals activation. A double-click on\n * empty space resolves to no node and emits nothing.\n *\n * @param event - The dblclick event whose target is inside the view's\n * subtree.\n */\n private _handleDoubleClick(event: MouseEvent): void {\n const id = this.nodeIdAt(event.target);\n\n if (id === null) {\n return;\n }\n\n const data = this._nodeData.get(id);\n\n if (data !== undefined) {\n this.emit(\"activate\", data);\n }\n }\n\n /**\n * Resolves the node id whose component owns the given event target.\n *\n * @param target - The raw DOM event target.\n * @returns The owning node id, or `null` when the target is not on a node.\n */\n private nodeIdAt(target: EventTarget | null): string | null {\n if (target === null) {\n return null;\n }\n\n const handle = DOM.source.intern(target);\n\n for (const [id, component] of this._nodeComponents) {\n const element = component.getElement();\n\n if (element && (element === handle || DOM.source.contains(element, handle))) {\n return id;\n }\n }\n\n return null;\n }\n\n /**\n * Reports hover over the diagram's edges: resolves the pointer's graph\n * coordinate (inverting the pan/zoom transform `zoomAboutViewportPoint`\n * applies) and asks the edge layer which routes are within hit tolerance\n * there, emitting `\"edgehover\"` with the joined model edges whenever the\n * reported set changes. A drag in progress is not a hover (`_panning`), and\n * a move whose target is not an edge hit path — or whose resolved point\n * lands on no route — instead ends any hover in progress.\n *\n * @param event - The mousemove event, dispatched to the whole subtree.\n */\n private _handleEdgeMouseMove(event: MouseEvent): void {\n if (this._panning) {\n return;\n }\n\n if (this._edgeLayer.edgeIdAt(event.target) === null) {\n this.leaveEdges();\n\n return;\n }\n\n const rect = DOM.source.getViewportRect(this);\n const zoom = this.getZoom();\n const gx = (event.clientX - rect.left - this._panX) / zoom;\n const gy = (event.clientY - rect.top - this._panY) / zoom;\n\n const routes = this._edgeLayer.edgesNear(gx, gy);\n\n if (routes.length === 0) {\n this.leaveEdges();\n\n return;\n }\n\n const key = routes.map(r => r.id).join(\" \");\n\n if (key === this._hoveredEdgeKey) {\n return;\n }\n\n const modelById = new Map(this._options.data?.edges.map(e => [e.id, e]) ?? []);\n const edges = routes.map(r => modelById.get(r.id)).filter((e): e is DiagramEdgeData => e !== undefined);\n\n this._hoveredEdgeKey = key;\n this.emit(\"edgehover\", edges, event);\n }\n\n /**\n * Ends an edge hover when the pointer's `mouseout` leaves an edge hit\n * path. A `mouseout` from anywhere else is not a hover boundary and is\n * ignored — `_handleEdgeMouseMove`'s own \"target is not an edge\" branch\n * already covers a move that lands on empty canvas or a node.\n *\n * @param event - The mouseout event, dispatched to the whole subtree.\n */\n private _handleEdgeMouseOut(event: MouseEvent): void {\n if (this._edgeLayer.edgeIdAt(event.target) !== null) {\n this.leaveEdges();\n }\n }\n\n /** Emits `\"edgeleave\"` and clears the hover key, unless already idle. */\n private leaveEdges(): void {\n if (this._hoveredEdgeKey === null) {\n return;\n }\n\n this._hoveredEdgeKey = null;\n this.emit(\"edgeleave\");\n }\n\n /**\n * Resolves the node under a right-click and emits `\"contextmenu\"` with its\n * data, mirroring `Tree`'s contextmenu handling: a node hit suppresses the\n * browser's native menu via `preventDefault` and emits; a right-click on\n * empty canvas is left to the browser.\n *\n * @param event - The contextmenu event whose target is inside the view's\n * subtree.\n */\n private _handleContextMenu(event: MouseEvent): void {\n const id = this.nodeIdAt(event.target);\n\n if (id === null) {\n return;\n }\n\n const data = this._nodeData.get(id);\n\n if (data !== undefined) {\n event.preventDefault();\n this.emit(\"contextmenu\", data, event);\n }\n }\n\n /**\n * Whether the given raw DOM event target lands inside the control cluster\n * — used to keep the cluster's own clicks/drags from also being\n * interpreted as a canvas click (clearing selection) or the start of a pan.\n *\n * @param target - The raw DOM event target.\n * @returns `true` when the target is the cluster or one of its descendants.\n */\n private isControlsTarget(target: EventTarget | null): boolean {\n if (target === null || this._controls === undefined) {\n return false;\n }\n\n const el = this._controls.getElement();\n const handle = DOM.source.intern(target);\n\n return el !== undefined && (el === handle || DOM.source.contains(el, handle));\n }\n\n /**\n * Wheel-zoom about the pointer: scales toward/away, keeping the graph\n * point under the cursor fixed in the viewport.\n *\n * @param event - The wheel event.\n */\n private _handleWheel(event: WheelEvent): void {\n event.preventDefault();\n\n const rect = DOM.source.getViewportRect(this);\n\n this.zoomAboutViewportPoint(event.deltaY < 0 ? WHEEL_ZOOM_STEP : 1 / WHEEL_ZOOM_STEP,\n event.clientX - rect.left, event.clientY - rect.top);\n }\n\n /**\n * Begins a pan drag on a primary-button press over the canvas — empty\n * space or an edge alike; only a node or the control cluster refuses —\n * recording the start pointer and pan offset, and switches the cursor to\n * `\"grabbing\"`. Also arms the click-versus-drag guard `_handleClick`\n * reads, regardless of whether this press ends up panning.\n *\n * @param event - The pointerdown event.\n */\n private _handlePointerDown(event: PointerEvent): void {\n // Recorded above every guard below, so a press that does not pan (a\n // node, the control cluster) still arms the click-versus-drag guard\n // `_handleClick` reads.\n this._pressX = event.clientX;\n this._pressY = event.clientY;\n this._pointerMoved = false;\n\n // A press on a node (leaf or container) or the control cluster is not\n // a pan: both show `pointer`, and the cursor has to promise what the\n // drag will actually do. Everything else — empty canvas and edges\n // alike — pans, since dragging an edge is this component's way of\n // panning from under the graph's own lines.\n if (event.button !== 0 || this.isControlsTarget(event.target) || this.nodeIdAt(event.target) !== null) {\n return;\n }\n\n this._panning = true;\n this._panStartX = event.clientX;\n this._panStartY = event.clientY;\n this._panOriginX = this._panX;\n this._panOriginY = this._panY;\n\n this.setCursor(\"grabbing\");\n }\n\n /**\n * Pans the content host during a drag by writing the pan offset from the\n * pointer delta — unbounded, so the graph can be dragged into empty space\n * in any direction.\n *\n * @param event - The pointermove event.\n */\n private _handlePointerMove(event: PointerEvent): void {\n // Gated on the primary button, like DragManager.onMouseMove gates on\n // an open session (DragManager.ts:492): this handler fires on every\n // pointer move over the subtree, including ordinary ambient hover\n // with no button held, and `_pressX`/`_pressY` default to `(0, 0)`\n // until the first real `pointerdown` — without this gate, the very\n // first hover of a session would latch `_pointerMoved` permanently.\n if (!this._pointerMoved && (event.buttons & 1) !== 0) {\n const dx = event.clientX - this._pressX;\n const dy = event.clientY - this._pressY;\n\n // Squared-distance comparison avoids a Math.hypot call, matching\n // DragManager's own threshold test.\n this._pointerMoved = dx * dx + dy * dy >= CLICK_SLOP * CLICK_SLOP;\n }\n\n if (!this._panning) {\n return;\n }\n\n // The primary button is no longer held — the pointer was released\n // outside this view's subtree, so its `pointerup` never reached us. End\n // the pan here rather than keep dragging on a button-up move.\n if ((event.buttons & 1) === 0) {\n this._panning = false;\n this.setCursor(\"grab\");\n\n return;\n }\n\n this._panX = this._panOriginX + (event.clientX - this._panStartX);\n this._panY = this._panOriginY + (event.clientY - this._panStartY);\n\n this.applyTransformToHost();\n }\n\n /**\n * Ends a pan drag and restores the `\"grab\"` cursor.\n */\n private _handlePointerUp(): void {\n this._panning = false;\n this.setCursor(\"grab\");\n }\n\n /** Builds the corner-pinned zoom / fit / reset control cluster. */\n private buildControls(): void {\n this._zoomInBtn = this.makeControlButton(\"plus\", \"Zoom in\");\n this._zoomOutBtn = this.makeControlButton(\"minus\", \"Zoom out\");\n this._fitBtn = this.makeControlButton(\"expand\", \"Fit to view\");\n this._resetBtn = this.makeControlButton(\"crosshairs\", \"Reset view\");\n\n this._controls = new Component();\n this._controls.setLayoutManager(new VBox());\n this._controls.addComponent(this._zoomInBtn);\n this._controls.addComponent(this._zoomOutBtn);\n this._controls.addComponent(this._fitBtn);\n this._controls.addComponent(this._resetBtn);\n }\n\n /**\n * Builds a glyph-only control button with an accessible label, mirroring\n * `VideoPlayer`'s control-bar buttons.\n *\n * @param glyph - The glyph name to show.\n * @param label - The accessible name (drives `aria-label` and the tooltip).\n * @returns The configured button.\n */\n private makeControlButton(glyph: string, label: string): Button {\n return new Button({ glyph, text: label, showText: false });\n }\n\n /** Wires the control cluster's buttons to their viewport-motion methods. */\n private wireControlListeners(): void {\n this._zoomInBtn.on(\"action\", this._onZoomIn);\n this._zoomOutBtn.on(\"action\", this._onZoomOut);\n this._fitBtn.on(\"action\", this._onFit);\n this._resetBtn.on(\"action\", this._onReset);\n }\n\n /**\n * Whether the built-in zoom / fit / reset control cluster is visible.\n *\n * @returns `true` when the control cluster shows.\n */\n isControlsVisible(): boolean {\n return this._options.controls ?? this._defaultOptions.controls ?? true;\n }\n\n /**\n * Shows or hides the built-in control cluster.\n *\n * @param value - Whether the control cluster is visible.\n *\n * @returns This view, for method chaining.\n */\n setControlsVisible(value: boolean): this {\n this._options.controls = value;\n this._controls.setVisible(value);\n\n return this;\n }\n}\n\nconst DiagramViewCallable = callable(DiagramView);\ntype DiagramViewCallable = DiagramView;\nexport {\n DiagramView as _DiagramView,\n DiagramViewCallable as DiagramView,\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"diagram.es.js","names":[],"sources":["../../../src/typescript/lib/component/diagram/ElkLayoutEngine.ts","../../../src/typescript/lib/component/diagram/DiagramNode.ts","../../../src/typescript/lib/component/diagram/DiagramGroupNode.ts","../../../src/typescript/lib/component/diagram/DiagramEdgeLayer.ts","../../../src/typescript/lib/component/diagram/DiagramView.ts"],"sourcesContent":["// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n//\n// The sole adapter between the framework-native diagram model and ElkJS. It is\n// the only module that names ELK types or imports `elkjs`; everything else in\n// the family speaks `DiagramData` / `DiagramLayoutResult`, keeping the layout\n// engine swappable. ELK takes JSON and returns JSON — it never touches the DOM,\n// so it needs no `DOM.sink` / `DOM.source` seam. `elkjs` is an optional peer\n// dependency imported lazily the first time a diagram lays out (mirroring\n// `StoreWorkerClient.ensureWorker`) and externalised from the library bundle so\n// its GWT blob never lands in the core chunk.\n\nimport type { DiagramData, DiagramNodeData } from \"~/component/diagram/DiagramModel.js\";\n\n/** A point in ELK's layout coordinate space. */\nexport interface ElkPoint {\n x: number;\n y: number;\n}\n\n/**\n * One routed section of an ELK edge: a straight run from `startPoint` to\n * `endPoint`, optionally threaded through `bendPoints`.\n */\nexport interface ElkEdgeSection {\n startPoint: ElkPoint;\n endPoint: ElkPoint;\n bendPoints?: ElkPoint[];\n}\n\n/** The engine-agnostic layout result consumed by `DiagramView`. */\nexport interface DiagramLayoutResult {\n /** Each node's absolute position and size in graph space. */\n nodes: Array<{ id: string; x: number; y: number; width: number; height: number }>;\n /** Each edge's routed sections. */\n edges: Array<{ id: string; sections: ElkEdgeSection[] }>;\n /** Graph bounding-box width. */\n width: number;\n /** Graph bounding-box height. */\n height: number;\n}\n\n/** ELK port shape: a fixed anchor on a node an edge can attach to. */\ninterface ElkPort {\n id: string;\n x?: number;\n y?: number;\n width?: number;\n height?: number;\n layoutOptions?: Record<string, string>;\n}\n\n/** ELK graph node shape (input carries sizes/options; output adds x/y). */\ninterface ElkNode {\n id: string;\n width?: number;\n height?: number;\n x?: number;\n y?: number;\n layoutOptions?: Record<string, string>;\n children?: ElkNode[];\n edges?: ElkExtendedEdge[];\n ports?: ElkPort[];\n}\n\n/** ELK edge shape with explicit source/target endpoint lists. */\ninterface ElkExtendedEdge {\n id: string;\n sources: string[];\n targets: string[];\n sections?: ElkEdgeSection[];\n /**\n * The node whose origin the routed `sections` coordinates are relative to,\n * as reported by ELK. For an edge between two nodes nested inside the same\n * container, ELK routes it in that container and reports container-relative\n * coordinates here (see {@link mapElkResult}, which shifts them to absolute).\n */\n container?: string;\n}\n\n/** Minimal structural type for the lazily-imported ELK instance. */\ninterface ElkInstance {\n layout(graph: unknown): Promise<unknown>;\n terminateWorker(): void;\n}\n\n/**\n * Fallback node width in pixels, used when a node carries neither an explicit\n * `width` nor a measurable preferred width. Sized to comfortably hold a short\n * label so a graph still lays out sensibly rather than collapsing to zero-area\n * nodes ELK would overlap.\n */\nconst DEFAULT_NODE_WIDTH = 120;\n\n/**\n * Fallback node height in pixels, mirroring {@link DEFAULT_NODE_WIDTH}. Tracks a\n * single label line plus vertical breathing room.\n */\nconst DEFAULT_NODE_HEIGHT = 40;\n\n/** Message carried by the rejection a disposed engine's `layout` produces. */\nconst DISPOSED_MESSAGE = \"ElkLayoutEngine has been disposed\";\n\n/**\n * Merges layout-option maps left-to-right, so later arguments win. `undefined`\n * maps are skipped. Used to layer graph options over the view-level defaults.\n *\n * @param maps - Option maps in ascending precedence order.\n * @returns The merged option map.\n */\nfunction mergeLayoutOptions(...maps: Array<Record<string, string> | undefined>): Record<string, string> {\n const merged: Record<string, string> = {};\n\n for (const map of maps) {\n if (map) {\n Object.assign(merged, map);\n }\n }\n\n return merged;\n}\n\n/**\n * Default padding reserved inside every container's ELK box, keyed by side.\n * The default renderer (`DiagramGroupNode`) paints its header label a few\n * pixels from the top-left corner, so without a reserved top inset ELK could\n * place a child flush against — or under — the title. The top inset reserves\n * the header row (its inset + one text/glyph line) plus a ~10px gap so the\n * first child sits clearly below the title, not tight against it. The\n * left/bottom/right values match ELK's own built-in default (12px); a consumer\n * supplying a custom `groupRenderer` with a taller or shorter header overrides\n * this via the container's own `layoutOptions`.\n */\nconst CONTAINER_PADDING_DEFAULT: Record<string, string> = { \"elk.padding\": \"[top=34,left=12,bottom=12,right=12]\" };\n\n/**\n * Maps one framework-native node to its ELK counterpart, recursing into\n * `children` for a compound container. A container (non-empty `children`)\n * carries no explicit `width`/`height` — ELK computes its box from its\n * contents — and its children are mapped the same way, so nesting is\n * unbounded. A container's `layoutOptions` resolve as\n * `CONTAINER_PADDING_DEFAULT` < `node.layoutOptions` (the node's own option\n * wins), reserving header clearance by default while staying overridable. A\n * leaf (no children) maps exactly as before.\n *\n * @param node - The framework-native node.\n * @param sizes - Per-leaf resolved sizes (explicit size, else preferred size).\n * @returns The mapped ELK node.\n */\nfunction mapDiagramNode(\n node: DiagramNodeData,\n sizes: Map<string, { width: number; height: number }>,\n): ElkNode {\n if (node.children && node.children.length > 0) {\n return {\n id: node.id,\n layoutOptions: mergeLayoutOptions(CONTAINER_PADDING_DEFAULT, node.layoutOptions),\n children: node.children.map((child) => mapDiagramNode(child, sizes)),\n };\n }\n\n const size = sizes.get(node.id);\n\n return {\n id: node.id,\n width: node.width ?? size?.width ?? DEFAULT_NODE_WIDTH,\n height: node.height ?? size?.height ?? DEFAULT_NODE_HEIGHT,\n layoutOptions: node.layoutOptions,\n ports: node.ports?.map((p) => ({\n id: p.id,\n x: p.x,\n y: p.y,\n width: p.width,\n height: p.height,\n layoutOptions: p.side !== undefined ? { \"elk.port.side\": p.side } : undefined,\n })),\n };\n}\n\n/**\n * Root-level ELK options enabling cross-container edge routing. Without it,\n * an edge declared on the root (as every {@link DiagramEdgeData} is) between\n * two nodes nested under different containers may be dropped or mis-routed by\n * ELK's hierarchical layout. Merged in as the lowest-precedence tier, so a\n * view-level default or a graph's own `layoutOptions` can still override it.\n */\nconst HIERARCHY_HANDLING_DEFAULT: Record<string, string> = { \"elk.hierarchyHandling\": \"INCLUDE_CHILDREN\" };\n\n/**\n * Maps the framework-native graph to an ELK graph JSON. Pure and synchronous —\n * no `elkjs` import — so it is unit-testable directly.\n *\n * Graph-level options resolve as `HIERARCHY_HANDLING_DEFAULT` < `defaults` <\n * `data.layoutOptions` (graph wins over view defaults, which win over the\n * hierarchy-handling default) on the root; each node carries its own\n * `layoutOptions`, which ELK resolves over the inherited root options so a\n * per-node option wins over all three.\n *\n * @param data - The framework-native graph.\n * @param sizes - Per-node resolved sizes (explicit size, else preferred size).\n * @param defaults - View-level default ELK options applied to every layout.\n * @returns The ELK graph JSON ready for `elk.layout`.\n */\nexport function buildElkGraph(\n data: DiagramData,\n sizes: Map<string, { width: number; height: number }>,\n defaults?: Record<string, string>,\n): ElkNode {\n const children: ElkNode[] = data.nodes.map((node) => mapDiagramNode(node, sizes));\n\n const edges: ElkExtendedEdge[] = data.edges.map((edge) => ({\n id: edge.id,\n sources: [edge.sourcePort ?? edge.source],\n targets: [edge.targetPort ?? edge.target],\n }));\n\n return {\n id: \"root\",\n layoutOptions: mergeLayoutOptions(HIERARCHY_HANDLING_DEFAULT, defaults, data.layoutOptions),\n children,\n edges,\n };\n}\n\n/**\n * Recursively flattens an ELK node (and its `children`, if any) into\n * `out`, threading an `(offsetX, offsetY)` accumulator. ELK reports each\n * child's `x`/`y` relative to its own parent, so a node's absolute position is\n * its parent's absolute origin plus its own relative `x`/`y`; a container then\n * recurses with that absolute origin as the new offset for its own children.\n *\n * @param node - The ELK node to flatten (a container or a leaf).\n * @param offsetX - The accumulated absolute x of `node`'s parent (0 at the root).\n * @param offsetY - The accumulated absolute y of `node`'s parent (0 at the root).\n * @param out - The flat output array nodes are appended to, in traversal order.\n */\nfunction flattenElkNode(\n node: ElkNode,\n offsetX: number,\n offsetY: number,\n out: Array<{ id: string; x: number; y: number; width: number; height: number }>,\n): void {\n const x = offsetX + (node.x ?? 0);\n const y = offsetY + (node.y ?? 0);\n\n out.push({ id: node.id, x, y, width: node.width ?? 0, height: node.height ?? 0 });\n\n for (const child of node.children ?? []) {\n flattenElkNode(child, x, y, out);\n }\n}\n\n/**\n * Collects every ELK edge in the tree — the root's own `edges` plus any nested\n * in a container's `edges` — resolving each edge's container id (the node its\n * routed coordinates are relative to). ELK tags a routed edge with its own\n * `container`; absent that, the edge is relative to the node whose `edges`\n * array holds it (the root for a flat graph).\n *\n * @param node - The ELK node whose `edges` (and descendants') to collect.\n * @param out - The flat output array edges are appended to, each paired with\n * its resolved container id.\n */\nfunction collectElkEdges(node: ElkNode, out: Array<{ edge: ElkExtendedEdge; container: string }>): void {\n for (const edge of node.edges ?? []) {\n out.push({ edge, container: edge.container ?? node.id });\n }\n\n for (const child of node.children ?? []) {\n collectElkEdges(child, out);\n }\n}\n\n/**\n * Shifts every point of an edge's routed sections by `origin`, translating\n * container-relative ELK coordinates into absolute graph space. Returns the\n * sections unchanged when `origin` is the graph origin, so a flat graph's\n * root-relative edges pass through untouched.\n *\n * @param sections - The edge's routed sections, in their container's frame.\n * @param origin - The absolute origin of the edge's container.\n * @returns The sections with every point translated into absolute coordinates.\n */\nfunction offsetSections(sections: ElkEdgeSection[], origin: ElkPoint): ElkEdgeSection[] {\n if (origin.x === 0 && origin.y === 0) {\n return sections;\n }\n\n const shift = (point: ElkPoint): ElkPoint => ({ x: point.x + origin.x, y: point.y + origin.y });\n\n return sections.map((section) => ({\n startPoint: shift(section.startPoint),\n endPoint: shift(section.endPoint),\n bendPoints: section.bendPoints?.map(shift),\n }));\n}\n\n/**\n * Maps an ELK layout result back to the engine-agnostic\n * {@link DiagramLayoutResult}. Pure and synchronous, so it is unit-testable\n * directly. A container's descendants are flattened into the same output list\n * as its siblings, each carrying absolute (not parent-relative) coordinates —\n * see {@link flattenElkNode}. Edge sections get the same treatment: ELK reports\n * an intra-container edge's route relative to that container's origin, so each\n * edge is shifted by its container's absolute position (a flat graph's\n * root-relative edges are shifted by the zero origin, i.e. left unchanged).\n *\n * @param result - The root ELK node returned by `elk.layout`.\n * @returns The mapped layout result.\n */\nexport function mapElkResult(result: ElkNode): DiagramLayoutResult {\n const nodes: Array<{ id: string; x: number; y: number; width: number; height: number }> = [];\n\n for (const child of result.children ?? []) {\n flattenElkNode(child, 0, 0, nodes);\n }\n\n // Absolute origin of every node (containers included), so an edge routed in\n // a container's frame can be lifted into absolute graph space. The root maps\n // to the zero origin.\n const origins = new Map<string, ElkPoint>([[result.id, { x: 0, y: 0 }]]);\n\n for (const node of nodes) {\n origins.set(node.id, { x: node.x, y: node.y });\n }\n\n const collected: Array<{ edge: ElkExtendedEdge; container: string }> = [];\n collectElkEdges(result, collected);\n\n const edges = collected.map(({ edge, container }) => ({\n id: edge.id,\n sections: offsetSections(edge.sections ?? [], origins.get(container) ?? { x: 0, y: 0 }),\n }));\n\n return {\n nodes,\n edges,\n width: result.width ?? 0,\n height: result.height ?? 0,\n };\n}\n\n/** Construction options for {@link ElkLayoutEngine}. */\nexport interface ElkLayoutEngineOptions {\n /**\n * Consumer-provided factory returning a Web Worker for off-thread ELK\n * layout. Takes precedence over {@link ElkLayoutEngineOptions.workerUrl}\n * when both are set.\n */\n workerFactory?: () => Worker;\n /**\n * URL of a consumer-hosted `elk-worker.js`, requesting off-thread layout.\n * With the `elk.bundled.js` module this engine imports, elkjs's own\n * worker-availability check (`require.resolve('web-worker')`) always\n * fails — the browserify module system that check runs against has no\n * `.resolve` — so `workerUrl` alone never actually constructs a Worker\n * here; elkjs logs its own console warning and runs on the main thread via\n * its in-process fallback. Pass\n * {@link ElkLayoutEngineOptions.workerFactory} for real off-thread\n * execution; `workerUrl` is kept only for parity with elkjs's own API.\n */\n workerUrl?: string;\n}\n\n/**\n * Lazily-loaded ELK layout adapter. One instance owns one ELK engine, built on\n * the first {@link ElkLayoutEngine.layout} call and reused afterward. With\n * neither `workerFactory` nor `workerUrl` set, ELK runs on the main thread.\n * Setting `workerFactory` runs layout in the Worker it returns. Setting\n * `workerUrl` does not: see {@link ElkLayoutEngineOptions.workerUrl} — it\n * always ends up on the main thread with this engine's `elk.bundled.js`\n * import. Either mode falls back transparently to the main thread — within\n * the same failing `layout` call, which rebuilds the engine and retries once\n * — the first time its engine fails to construct or a layout rejects; that\n * fallback is permanent for the instance's lifetime, so later layouts run on\n * the main thread even if the worker would have recovered.\n *\n * In `workerFactory` mode the engine owns a real Web Worker for as long as it\n * lives. Its owner is expected to call {@link ElkLayoutEngine.dispose} when\n * discarding it, or that worker thread outlives the engine.\n *\n * @category Components\n */\nexport class ElkLayoutEngine {\n\n private _elk: ElkInstance | null = null;\n private readonly _workerFactory?: () => Worker;\n private readonly _workerUrl?: string;\n /**\n * True while `_elk` is worker-backed. Set before constructing a\n * worker-backed engine and never cleared on a successful layout, so the\n * *next* layout failure — whichever one it is — still triggers the\n * one-time main-thread rebuild in {@link layout}; after that rebuild it is\n * `false` for good and a further failure propagates.\n */\n private _workerBacked = false;\n /** True once `dispose` has run. A disposed engine never builds another ELK. */\n private _disposed = false;\n /**\n * True while the ELK instance this engine holds — or is in the middle of\n * building — drives a real Web Worker, which is the consumer-factory mode\n * and only that mode. Cleared when the main-thread fallback replaces the\n * instance. Deliberately NOT cleared by `dispose`: a construction still in\n * flight sets this just before it builds, and the adopt-time check needs\n * it afterward to terminate an instance `dispose` could not yet see.\n */\n private _ownsWorker = false;\n /**\n * The in-flight construction, shared by every `layout` that arrives while\n * ELK is still being imported and built. Without it each of those builds\n * its own ELK — and its own Worker — because `_elk` is still `null` for\n * all of them, and `dispose` can only terminate the one instance the\n * engine ends up keeping. Cleared whenever what it memoised stops being\n * this engine's instance: a failed construction (so a later `layout`\n * retries the import instead of replaying the failure), the main-thread\n * fallback replacing the instance, and `dispose`. Keeping those in step\n * is what stops the memo retaining an ELK — and its `Worker` — the engine\n * has already abandoned, so any future code that assigns or nulls `_elk`\n * must clear this too.\n */\n private _building: Promise<ElkInstance> | null = null;\n\n /**\n * @param options - Optional worker configuration. With neither field set,\n * ELK runs on the main thread via the zero-config bundle.\n */\n constructor(options?: ElkLayoutEngineOptions) {\n this._workerFactory = options?.workerFactory;\n this._workerUrl = options?.workerUrl;\n }\n\n /**\n * Lazily imports ELK (if needed), maps the model to ELK JSON, runs the\n * layout, and maps the result back. The first time a worker-backed engine\n * fails to construct or a layout rejects, rebuilds on the main thread and\n * retries once, so a worker problem never surfaces as a diagram error;\n * that switch to the main thread is permanent for this instance's\n * lifetime, even if a later failure is unrelated to the worker itself.\n *\n * @param data - The framework-native graph.\n * @param sizes - Per-node resolved sizes.\n * @param defaults - View-level default ELK options.\n * @returns The mapped layout result.\n * @throws Error - If `elkjs` is not installed / cannot be imported, if a\n * main-thread layout fails (no worker fallback remains at that point),\n * or if the engine was already disposed when the call started or is\n * disposed while it is building or rebuilding its ELK. Disposal landing\n * during the layout compute itself does not reject — that call can still\n * resolve, which is why `DiagramView` drops late results by generation\n * rather than relying on this.\n */\n async layout(\n data: DiagramData,\n sizes: Map<string, { width: number; height: number }>,\n defaults?: Record<string, string>,\n ): Promise<DiagramLayoutResult> {\n if (this._disposed) {\n throw new Error(DISPOSED_MESSAGE);\n }\n\n const graph = buildElkGraph(data, sizes, defaults);\n\n try {\n const elk = await this.ensureElk();\n\n return mapElkResult(await elk.layout(graph) as ElkNode);\n } catch (error) {\n // Disposed mid-flight: the engine is gone for good, so there is\n // nothing to fall back onto. Main-thread failure: elkjs itself is\n // absent/broken. Either way, propagate.\n if (this._disposed || !this._workerBacked) {\n throw error;\n }\n\n // Worker construction failed (factory threw, `Worker` undefined,\n // CSP block) or its first compute failed. Terminate the worker\n // being abandoned before the reference is dropped, then rebuild on\n // the main thread and retry once so the diagram still renders.\n this.terminateOwnedWorker(this._elk);\n this._workerBacked = false;\n this._ownsWorker = false;\n\n const rebuilt = await this.createMainThreadElk();\n\n // `dispose` can land while that rebuild is awaited. Adopting the\n // instance then would leave a disposed engine holding a live ELK\n // and laying out through it, so drop it. Unlike `ensureElk`'s\n // equivalent check this does NOT terminate what it drops: the\n // rebuild is main-thread, and terminating a non-factory instance\n // throws a `TypeError` (see `terminateOwnedWorker`).\n if (this._disposed) {\n throw new Error(DISPOSED_MESSAGE);\n }\n\n this._elk = rebuilt;\n // The memo still resolves to the worker-backed instance just\n // abandoned; drop it so it does not outlive what it built.\n this._building = null;\n\n return mapElkResult(await rebuilt.layout(graph) as ElkNode);\n }\n }\n\n /**\n * Terminates this engine's ELK Web Worker, if it owns one, and drops the\n * ELK instance. A later {@link ElkLayoutEngine.layout} rejects rather than\n * rebuilding, so a disposed engine never resurrects a worker. Idempotent,\n * and a no-op on an engine that never laid out or never ran off-thread.\n *\n * A layout request outstanding in a real worker at this moment never\n * settles — elkjs does not reject pending requests on termination — so a\n * caller awaiting one must not depend on it resolving or rejecting.\n */\n dispose(): void {\n this._disposed = true;\n\n this.terminateOwnedWorker(this._elk);\n\n this._elk = null;\n // Dropped too, or the engine keeps the terminated instance reachable\n // through the memoised construction for as long as the engine lives.\n this._building = null;\n }\n\n /**\n * Returns the ELK engine, importing and constructing it on first use. The\n * dynamic `import(\"elkjs/...\")` is left external in the library build so the\n * GWT bundle resolves from the consumer's install rather than being inlined.\n *\n * @returns The ELK engine instance.\n * @throws Error - If the engine was disposed while ELK was still being\n * imported and constructed.\n */\n private async ensureElk(): Promise<ElkInstance> {\n if (this._elk) {\n return this._elk;\n }\n\n // Memoised so overlapping layouts share one construction rather than\n // each building an ELK of its own — see `_building`. The adopt-or-\n // terminate decision therefore runs exactly once however many callers\n // are waiting, and they all settle on its outcome.\n this._building ??= this.createElk()\n .then((elk) => {\n if (this._disposed) {\n // `dispose` ran while ELK was still being imported and\n // constructed, so it saw no instance to terminate.\n // Terminate this one rather than adopting it, or the\n // worker outlives the disposed engine.\n this.terminateOwnedWorker(elk);\n\n throw new Error(DISPOSED_MESSAGE);\n }\n\n this._elk = elk;\n\n return elk;\n })\n .catch((error: unknown) => {\n this._building = null;\n\n throw error;\n });\n\n return this._building;\n }\n\n /**\n * Terminates `elk`'s Web Worker, but only when this engine built it from a\n * consumer factory. In every other mode elkjs drives an in-process stand-in\n * worker with no `terminate` method, and asking elkjs to terminate that\n * throws a `TypeError` — so the guard is load-bearing, not defensive.\n *\n * @param elk - The instance to terminate, or `null` if none was built.\n */\n private terminateOwnedWorker(elk: ElkInstance | null): void {\n if (elk && this._ownsWorker) {\n elk.terminateWorker();\n }\n }\n\n /**\n * Builds the ELK engine per the configured mode: a consumer worker\n * factory, a consumer-hosted worker URL, or the main-thread default —\n * checked in that precedence order. `_workerBacked` is set *before*\n * constructing a worker-backed engine, so a synchronous construction\n * throw is still caught by {@link layout}'s retry.\n *\n * @returns The constructed ELK engine.\n */\n private async createElk(): Promise<ElkInstance> {\n // `elkjs` is an optional peer dep, typed by the local ambient shim in\n // `elkjs.d.ts` and resolved by the consumer's bundler at runtime.\n const { default: ELK } = await import(\"elkjs/lib/elk.bundled.js\");\n\n if (this._workerFactory) {\n this._workerBacked = true;\n this._ownsWorker = true;\n\n return new ELK({ workerFactory: this._workerFactory });\n }\n\n if (this._workerUrl) {\n this._workerBacked = true;\n\n return new ELK({ workerUrl: this._workerUrl });\n }\n\n return new ELK();\n }\n\n /**\n * Builds a plain main-thread ELK engine, used both as the default mode\n * and as the fallback after a worker-backed engine fails.\n *\n * @returns The constructed main-thread ELK engine.\n */\n private async createMainThreadElk(): Promise<ElkInstance> {\n const { default: ELK } = await import(\"elkjs/lib/elk.bundled.js\");\n\n return new ELK();\n }\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n//\n// The default node renderer for a DiagramView: a themed rounded box wrapping a\n// glyph + label (or a bare label). Its content-derived preferred size is what\n// the view feeds to ELK when a node carries no explicit width/height.\n\nimport { Panel, PanelOptions } from \"~/core/Panel.js\";\nimport { Component } from \"~/core/Component.js\";\nimport { StyleRule } from \"~/core/StyleTarget.js\";\nimport { DOM } from \"~/core/DOM.js\";\nimport type { Handle } from \"~/core/DOM.js\";\nimport { Fit } from \"~/layout/Fit.js\";\nimport { HBox } from \"~/layout/HBox.js\";\nimport { Insets } from \"~/primitive/Insets.js\";\nimport { IconText } from \"~/component/display/IconText.js\";\nimport { Text } from \"~/component/input/Text.js\";\nimport { callable } from \"~/core/Callable.js\";\n\n/**\n * Construction-time options for {@link DiagramNode}.\n *\n * @category Components\n */\nexport interface DiagramNodeOptions extends PanelOptions {\n /** Label text shown inside the node. */\n label?: string;\n /** Optional registered glyph name shown before the label. */\n glyph?: string;\n /** Short marker text drawn after the label, in the same row. */\n badge?: string;\n /** Whether the node starts selected. */\n selected?: boolean;\n}\n\n/**\n * User-overridable defaults. The node paints a themed rounded box; its interior\n * inset gives the label structural breathing room away from the border.\n */\nconst _defaultDiagramNodeOptions: Partial<DiagramNodeOptions> = {\n // Vertical 4px / horizontal 8px so a short label never sits flush against\n // the rounded border — structural interior spacing, not cosmetic nudging.\n insets: new Insets(4, 8, 4, 8),\n backgroundColor: \"var(--ts-ui-diagram-node-bg, var(--ts-ui-button-bg, rgb(245, 245, 245)))\",\n border: \"1px solid var(--ts-ui-border-color, rgb(180, 180, 180))\",\n borderRadius: \"4px\",\n cursor: \"pointer\",\n};\n\n// The badge's opacity: present but secondary to the label it trails. Matches\n// the \"dim the supporting value\" weight the framework already uses for a\n// receded label, so the badge reads as an annotation rather than a second\n// name.\nconst BADGE_OPACITY = 0.6;\n\n/**\n * The default themed node renderer for a\n * [`DiagramView`](/api/component/diagram/classes/DiagramView). Composes a glyph\n * and label inside a rounded, theme-aware box and toggles a `.selected` state\n * rule when selected.\n *\n * @category Components\n */\nclass DiagramNode extends Panel<DiagramNodeOptions> {\n\n /** The glyph+label (or bare label) component. */\n private _label!: IconText | Text;\n\n /** The child added to the node: `_label` alone, or a row of `_label` + `_badge`. */\n private _content!: Component;\n\n /** The trailing badge chip, when the node carries one. */\n private _badge?: Text;\n\n // Lazy `.selected` state rule. The slot caches the wrapper returned by\n // Component's `createStyleRule` builder, which dedupes by selector suffix.\n private declare _selectedStyleRule?: StyleRule;\n private get selectedStyleRule(): StyleRule {\n return this._selectedStyleRule ??= this.createStyleRule(\".selected\");\n }\n\n /**\n * @param options - Optional construction-time options.\n * @param subclassDefaults - Per-subclass default bag layered over this\n * class's defaults; subclasses forward their `_defaultXxxOptions`\n * constant here.\n */\n constructor(options?: DiagramNodeOptions, subclassDefaults?: Partial<DiagramNodeOptions>) {\n super(options, {\n ..._defaultDiagramNodeOptions,\n layoutManager: new Fit(),\n ...(subclassDefaults ?? {}),\n });\n\n this.selectedStyleRule.set(\"borderColor\", \"var(--ts-ui-accent-color, rgb(30, 100, 200))\");\n this.selectedStyleRule.set(\"backgroundColor\", \"var(--ts-ui-diagram-node-selected-bg, var(--ts-ui-table-row-selected, rgba(30, 100, 200, 0.15)))\");\n\n // Content children are built here (not during super's cascade), so the\n // label/glyph/badge/selected values cached pure in `applyOptions` are\n // dispatched now that the row exists.\n this.buildContent(this._options.glyph, this._options.label ?? \"\", this._options.badge);\n\n if (this._options.selected !== undefined) {\n this.setSelected(this._options.selected);\n }\n }\n\n /**\n * Caches the label/glyph/badge/selected fields pure to `_options`; they are\n * dispatched from the constructor body once the content child exists.\n *\n * @param options - The options bag carrying the values to apply.\n */\n protected applyOptions(options: DiagramNodeOptions): this {\n super.applyOptions(options);\n\n if (options.label !== undefined) this._options.label = options.label;\n if (options.glyph !== undefined) this._options.glyph = options.glyph;\n if (options.badge !== undefined) this._options.badge = options.badge;\n if (options.selected !== undefined) this._options.selected = options.selected;\n\n return this;\n }\n\n /**\n * Builds (or rebuilds) the inner content child: `_label` alone (an\n * `IconText` when a glyph is present, else a bare `Text`), or — when a\n * badge is given — a row of `_label` followed by the badge `Text`.\n *\n * @param glyph - Optional glyph name.\n * @param label - The label text.\n * @param badge - Optional trailing badge text.\n */\n private buildContent(glyph: string | undefined, label: string, badge: string | undefined): void {\n if (this._content) {\n this.removeComponent(this._content);\n }\n\n this._label = glyph !== undefined ? new IconText(glyph, label) : new Text(label);\n\n if (badge === undefined) {\n this._badge = undefined;\n this._content = this._label;\n } else {\n this._badge = new Text(badge);\n this._badge.setOpacity(BADGE_OPACITY);\n this._content = new Component({ layoutManager: new HBox(), components: [this._label, this._badge] });\n }\n\n // Every Component stamps its own `cursor` (defaulting to `default`) onto\n // its CSS rule, so the label/glyph/badge would override the node's\n // `pointer` wherever they sit under the cursor — the hover cursor would\n // flicker between pointer (over padding) and arrow (over the text).\n // Make the content transparent to pointer events so hover + clicks land\n // on the node itself; `pointer-events: none` inherits, so this one call\n // also covers everything nested inside (the glyph and text inside an\n // `IconText`, or the label + badge inside the row). Mirrors how `Button`\n // frees its label row so the button's cursor governs.\n this._content.setPointerEvents(\"none\");\n\n this.addComponent(this._content);\n }\n\n /**\n * Updates the node's label text.\n *\n * @param value - The new label.\n *\n * @returns This node, for method chaining.\n */\n setLabel(value: string): this {\n this._options.label = value;\n this._label.setText(value);\n\n return this;\n }\n\n /**\n * Returns the node's label, or `null` when none was set.\n *\n * @returns The label text, or `null`.\n */\n getLabel(): string | null {\n return this._options.label ?? null;\n }\n\n /**\n * Returns the node's badge text, or `null` when none was set.\n *\n * @returns The badge text, or `null`.\n */\n getBadge(): string | null {\n return this._options.badge ?? null;\n }\n\n /**\n * Sets the selected state and toggles the `.selected` CSS class.\n *\n * @param value - True to select, false to deselect.\n *\n * @returns This node, for method chaining.\n */\n setSelected(value: boolean): this {\n this._options.selected = value;\n\n const element = this.getElement();\n\n if (element) {\n DOM.sink.apply(element, { toggleClass: { selected: value } });\n }\n\n return this;\n }\n\n /**\n * Returns whether the node is currently selected.\n *\n * @returns True when selected.\n */\n isSelected(): boolean {\n return this._options.selected ?? false;\n }\n\n /**\n * Re-applies the cached selected class after (re-)render, since a fresh\n * element starts without it.\n *\n * @param element - Optional element from the render pipeline.\n *\n * @returns This node, for method chaining.\n */\n protected init(element?: Handle): this {\n super.init(element);\n\n const el = element || this.getElement();\n\n if (el && this.isSelected()) {\n DOM.sink.apply(el, { toggleClass: { selected: true } });\n }\n\n return this;\n }\n}\n\nconst DiagramNodeCallable = callable(DiagramNode);\ntype DiagramNodeCallable = DiagramNode;\nexport {\n DiagramNode as _DiagramNode,\n DiagramNodeCallable as DiagramNode,\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n//\n// The default container renderer for a DiagramView compound node: a titled,\n// translucent box painted behind its (flat-sibling, not DOM-child) leaf/child\n// components. `DiagramView` positions this box at ELK's computed absolute\n// bounds and paints it at the lowest z-index among a compound graph's nodes,\n// so it never intercepts a click meant for a leaf sitting visually inside it.\n\nimport { Panel, PanelOptions } from \"~/core/Panel.js\";\nimport { Absolute } from \"~/layout/Absolute.js\";\nimport { IconText } from \"~/component/display/IconText.js\";\nimport { Text } from \"~/component/input/Text.js\";\nimport { callable } from \"~/core/Callable.js\";\n\n/**\n * Construction-time options for {@link DiagramGroupNode}.\n *\n * @category Components\n */\nexport interface DiagramGroupNodeOptions extends PanelOptions {\n /** The container's header label (e.g. the schema name). */\n label?: string;\n /** Optional registered glyph name shown before the header label. */\n glyph?: string;\n}\n\n/**\n * Distance in pixels from the box's top-left corner to the header label's\n * origin — enough breathing room that the label never sits flush against the\n * rounded border, mirroring `DiagramNode`'s interior inset.\n */\nconst HEADER_INSET = 6;\n\nconst _defaultDiagramGroupNodeOptions: Partial<DiagramGroupNodeOptions> = {\n backgroundColor: \"var(--ts-ui-diagram-group-bg, rgba(120, 120, 120, 0.08))\",\n // A container is a selectable node like any leaf, so it carries the same\n // pointer cursor `DiagramNode` does — left at the Component default it\n // would read as an arrow and promise a pan its own box does not perform.\n cursor: \"pointer\",\n border: \"1px solid var(--ts-ui-diagram-group-border, var(--ts-ui-border-color, rgb(180, 180, 180)))\",\n borderRadius: \"4px\",\n};\n\n/**\n * The default themed container renderer for a\n * [`DiagramView`](/api/component/diagram/classes/DiagramView) compound node.\n * Paints a translucent, rounded box with a header label pinned to the\n * top-left corner; the rest of the box stays open so the container's\n * children — rendered as separate flat siblings positioned by ELK, not as DOM\n * children of this component — visually read as sitting inside it.\n *\n * @category Components\n */\nclass DiagramGroupNode extends Panel<DiagramGroupNodeOptions> {\n\n /** The header shown at the box's top-left corner: a bare label, or a glyph + label. */\n private _header!: IconText | Text;\n\n constructor(options?: DiagramGroupNodeOptions, subclassDefaults?: Partial<DiagramGroupNodeOptions>) {\n super(options, {\n layoutManager: new Absolute(),\n ..._defaultDiagramGroupNodeOptions,\n ...(subclassDefaults ?? {}),\n });\n\n // The header is built here (not during super's cascade), so the values\n // cached pure in `applyOptions` are dispatched now that the box exists.\n // An IconText when a glyph is present (glyph before the label, mirroring\n // DiagramNode), else a bare Text.\n const label = this._options.label ?? \"\";\n this._header = this._options.glyph !== undefined\n ? new IconText(this._options.glyph, label)\n : new Text(label);\n this._header.setPointerEvents(\"none\");\n this._header.setX(HEADER_INSET);\n this._header.setY(HEADER_INSET);\n this.addComponent(this._header);\n }\n\n /**\n * Caches the label/glyph fields pure to `_options`; they are dispatched from\n * the constructor body once the header child exists.\n *\n * @param options - The options bag carrying the values to apply.\n */\n protected applyOptions(options: DiagramGroupNodeOptions): this {\n super.applyOptions(options);\n\n if (options.label !== undefined) this._options.label = options.label;\n if (options.glyph !== undefined) this._options.glyph = options.glyph;\n\n return this;\n }\n\n /**\n * Updates the container's header label.\n *\n * @param value - The new label.\n *\n * @returns This node, for method chaining.\n */\n setLabel(value: string): this {\n this._options.label = value;\n this._header.setText(value);\n\n return this;\n }\n\n /**\n * Returns the container's header label, or `null` when none was set.\n *\n * @returns The label text, or `null`.\n */\n getLabel(): string | null {\n return this._options.label ?? null;\n }\n}\n\nconst DiagramGroupNodeCallable = callable(DiagramGroupNode);\ntype DiagramGroupNodeCallable = DiagramGroupNode;\nexport {\n DiagramGroupNode as _DiagramGroupNode,\n DiagramGroupNodeCallable as DiagramGroupNode,\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n//\n// One `<svg>` Component drawing the diagram's edges as `<path>` elements with a\n// shared arrowhead `<marker>`. Follows the Glyph SVG-through-the-seam pattern:\n// the root `<svg>` and every leaf child are created via `DOM.sink.createElementNS`\n// and tracked with `trackHandle`, never raw DOM. The layer's root stays\n// non-interactive (`pointer-events: none`) so node clicks fall through to the\n// node components; each drawn edge additionally gets an invisible wide hit\n// path that opts itself back into pointer events, so hovering/pressing an edge\n// is possible without making the layer as a whole interactive.\n\nimport { Component, ComponentOptions } from \"~/core/Component.js\";\nimport { DOM } from \"~/core/DOM.js\";\nimport type { Handle } from \"~/core/DOM.js\";\nimport type { ElkEdgeSection, ElkPoint } from \"~/component/diagram/ElkLayoutEngine.js\";\nimport type { DiagramEdgeMarker, DiagramEdgeStyle } from \"~/component/diagram/DiagramModel.js\";\nimport { callable } from \"~/core/Callable.js\";\n\n/** SVG namespace URI. */\nconst SVG_NS = \"http://www.w3.org/2000/svg\";\n\n/** Themed stroke for edge paths and the arrowhead. */\nconst EDGE_STROKE = \"var(--ts-ui-diagram-edge, var(--ts-ui-border-color, rgb(120, 120, 120)))\";\n\n/** Edge path stroke width in pixels — a hairline that reads at any zoom. */\nconst EDGE_STROKE_WIDTH = \"1.5\";\n\n/** Font size (px) of an edge's mid-route label. */\nconst LABEL_FONT_SIZE = \"12\";\n\n/**\n * Background colour and width (px) of the halo drawn behind an edge label. The\n * label text is painted `stroke`-then-`fill` (`paint-order`), so this\n * background-coloured stroke masks the edge line running under the glyphs — the\n * label reads clearly instead of being crossed out by the edge.\n */\nconst LABEL_HALO = \"var(--ts-ui-diagram-bg, var(--ts-ui-panel-bg, rgb(255, 255, 255)))\";\nconst LABEL_HALO_WIDTH = \"4\";\n\n/** Arrowhead marker box size in user units (matches the `refX`/`refY` below). */\nconst ARROW_SIZE = 8;\n\n/** Dash pattern applied when an edge's `style.dashed` is set. */\nconst DASH_ARRAY = \"6 4\";\n\n/**\n * Stroke width (px, unscaled graph units) of an edge's invisible hit path —\n * ±6px either side of the 1.5px visible hairline. A hairline is far too thin\n * to aim at, and this is deliberately wider than ELK's default 10px\n * `elk.spacing.edgeEdge`, so a bundle of parallel routes reports as a bundle\n * rather than forcing the user to land on exactly one of them.\n */\nconst EDGE_HIT_WIDTH = 12;\n\n/** Half the hit width: the distance from a route within which it answers `edgesNear`. */\nconst EDGE_HIT_TOLERANCE = EDGE_HIT_WIDTH / 2;\n\n/**\n * Opacity of the group holding every edge outside a non-empty emphasis set. A\n * 1.5px hairline has almost no area, so it still reads as a full line at\n * ChartLegend's `HIDDEN_OPACITY` (`0.4`), a strength tuned for a filled legend\n * swatch — dimmed enough there is not dimmed enough for a stroke. `0.15` over\n * the default light canvas resolves to a pale, still-traceable grey while\n * leaving the emphasised edges clearly what the eye lands on.\n *\n * Carried by the dimmed *group* rather than each dimmed edge, because routes\n * overlap by design: fan-in and fan-out bundles share a junction stub, so two\n * or more dimmed paths coincide there. Per-element alpha composites at each\n * overlap — two paths at `0.15` resolve to `0.28`, three to `0.39` — so a\n * bundle read as emphasised precisely where it was densest. Group opacity\n * composites the group's whole rendering once, so an overlap looks the same as\n * a single line.\n */\nconst DIMMED_EDGE_OPACITY = \"0.15\";\n\n/**\n * Non-`\"arrow\"` crow's-foot marker kinds this layer pre-defines in `<defs>`.\n * Kept as a literal array (rather than deriving from {@link DiagramEdgeMarker})\n * so `createRootElement` can iterate it directly.\n */\nconst CROWS_FOOT_MARKERS: readonly Exclude<DiagramEdgeMarker, \"arrow\">[] =\n [\"one\", \"zeroOrOne\", \"oneOrMany\", \"zeroOrMany\"];\n\n/**\n * One crow's-foot marker's geometry: its `<marker>` box (`width`/`height`),\n * the point within that box the edge vertex anchors to (`refX`/`refY`), and\n * its child shapes as bare SVG element specs. Every marker uses\n * `orient=\"auto-start-reverse\"`, so the same definition auto-flips when used\n * at `marker-start` and reads unreversed at `marker-end`.\n */\ninterface MarkerGeometry {\n width: number;\n height: number;\n refX: number;\n refY: number;\n /** Child shapes, each a bare tag name plus its SVG attributes. */\n parts: Array<{ tag: \"path\" | \"circle\"; attrs: Record<string, string> }>;\n}\n\n/**\n * Geometry table for the four crow's-foot markers. Coordinates are chosen so\n * every marker's \"attach\" edge sits at `x = width` (the vertex, via `refX`)\n * and its \"open\" edge sits toward `x = 0` (away from the node) — exact pixels\n * are a manual visual-tuning step, not a correctness concern (see the\n * fk-diagram-cardinality-and-index-coverage plan).\n */\nconst MARKER_GEOMETRY: Record<Exclude<DiagramEdgeMarker, \"arrow\">, MarkerGeometry> = {\n // \"one and only one\": two perpendicular bars across the line.\n one: {\n width: 12, height: 12, refX: 12, refY: 6,\n parts: [{ tag: \"path\", attrs: { d: \"M 4 0 L 4 12 M 8 0 L 8 12\" } }],\n },\n // \"zero or one\": one perpendicular bar plus a small inboard circle.\n zeroOrOne: {\n width: 16, height: 12, refX: 16, refY: 6,\n parts: [\n { tag: \"path\", attrs: { d: \"M 12 0 L 12 12\" } },\n { tag: \"circle\", attrs: { cx: \"6\", cy: \"6\", r: \"4\" } },\n ],\n },\n // \"one or many\": a three-prong crow's foot plus one perpendicular bar just inboard of it.\n oneOrMany: {\n width: 16, height: 12, refX: 16, refY: 6,\n parts: [\n { tag: \"path\", attrs: { d: \"M 0 6 L 12 0 M 0 6 L 12 6 M 0 6 L 12 12\" } },\n { tag: \"path\", attrs: { d: \"M 14 0 L 14 12\" } },\n ],\n },\n // \"zero or many\": a three-prong crow's foot plus a small inboard circle.\n zeroOrMany: {\n width: 18, height: 12, refX: 18, refY: 6,\n parts: [\n { tag: \"path\", attrs: { d: \"M 6 6 L 18 0 M 6 6 L 18 6 M 6 6 L 18 12\" } },\n { tag: \"circle\", attrs: { cx: \"3\", cy: \"6\", r: \"3\" } },\n ],\n },\n};\n\n/**\n * How far, in unscaled graph units, the longest end marker reaches back along\n * an edge from the point it attaches to. Every marker anchors its vertex at\n * `x = width` (via `refX`) and opens toward `x = 0`, so a marker's `width` *is*\n * its reach, and this is the widest of them all.\n *\n * Exported because a consumer that rewrites edge routes needs it: anything\n * placed on the route within this distance of an endpoint lands underneath the\n * marker glyph rather than beside it. SQLAdmin's junction stubs use it as the\n * floor for how far from a node a bundle may branch.\n *\n * @category Components\n */\nexport const EDGE_MARKER_EXTENT: number =\n Math.max(ARROW_SIZE, ...Object.values(MARKER_GEOMETRY).map(geometry => geometry.width));\n\n/**\n * A single routed edge: its identity, the ELK sections describing its\n * polyline route (start point, optional bend points, end point), and its\n * optional visual style (joined in from the model by\n * [`DiagramView.applyLayout`](/api/component/diagram/classes/DiagramView)).\n */\nexport interface DiagramEdgeRoute {\n id: string;\n sections: ElkEdgeSection[];\n /** Optional cardinality/dependency style. Absent = today's plain arrow-ended edge. */\n style?: DiagramEdgeStyle;\n}\n\n/**\n * The point used to place an edge's optional label: the halfway point *along the\n * routed polyline* (start → bend points → end), measured by arc length. This\n * stays visually centred on the edge even when it bends — a bend point can sit\n * far off centre and drag the label toward one endpoint.\n *\n * @param sections - The edge's routed sections.\n * @returns The label anchor point.\n */\nfunction labelPoint(sections: ElkEdgeSection[]): ElkPoint {\n const section = sections[0];\n\n if (!section) {\n return { x: 0, y: 0 };\n }\n\n const points = [section.startPoint, ...(section.bendPoints ?? []), section.endPoint];\n\n return midpointAlong(points);\n}\n\n/**\n * The point half the total length along a polyline, interpolated within the\n * segment that straddles the midpoint.\n *\n * @param points - The polyline vertices, in order (length >= 1).\n * @returns The arc-length midpoint.\n */\nfunction midpointAlong(points: ElkPoint[]): ElkPoint {\n const segmentLength = (a: ElkPoint, b: ElkPoint): number => Math.hypot(b.x - a.x, b.y - a.y);\n\n let total = 0;\n\n for (let i = 1; i < points.length; i++) {\n total += segmentLength(points[i - 1], points[i]);\n }\n\n let remaining = total / 2;\n\n for (let i = 1; i < points.length; i++) {\n const length = segmentLength(points[i - 1], points[i]);\n\n if (length >= remaining) {\n const t = length === 0 ? 0 : remaining / length;\n\n return {\n x: points[i - 1].x + (points[i].x - points[i - 1].x) * t,\n y: points[i - 1].y + (points[i].y - points[i - 1].y) * t,\n };\n }\n\n remaining -= length;\n }\n\n return points[points.length - 1];\n}\n\n/**\n * Serialises an edge's ELK sections into an SVG path `d` string. Each section\n * starts with a move to its `startPoint`, threads any `bendPoints`, and lines to\n * its `endPoint`.\n *\n * @param sections - The edge's routed sections.\n * @returns The SVG path data, or an empty string when there is nothing to draw.\n */\nfunction buildPathData(sections: ElkEdgeSection[]): string {\n const parts: string[] = [];\n\n for (const section of sections) {\n parts.push(`M ${section.startPoint.x} ${section.startPoint.y}`);\n\n for (const bend of section.bendPoints ?? []) {\n parts.push(`L ${bend.x} ${bend.y}`);\n }\n\n parts.push(`L ${section.endPoint.x} ${section.endPoint.y}`);\n }\n\n return parts.join(\" \");\n}\n\n/**\n * The shortest distance from point `(px, py)` to the line segment `(ax, ay)`–\n * `(bx, by)`, via the standard clamped projection onto the segment.\n *\n * @param px - Point x.\n * @param py - Point y.\n * @param ax - Segment start x.\n * @param ay - Segment start y.\n * @param bx - Segment end x.\n * @param by - Segment end y.\n * @returns The shortest distance from the point to the segment.\n */\nfunction distanceToSegment(px: number, py: number, ax: number, ay: number, bx: number, by: number): number {\n const dx = bx - ax;\n const dy = by - ay;\n const lengthSquared = dx * dx + dy * dy;\n\n if (lengthSquared === 0) {\n return Math.hypot(px - ax, py - ay);\n }\n\n const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / lengthSquared));\n\n return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));\n}\n\n/**\n * The shortest distance from point `(x, y)` to a routed edge's polyline —\n * the minimum over every segment of every section, walking the same\n * start/bend/end points {@link labelPoint} does.\n *\n * @param sections - The edge's routed sections.\n * @param x - Point x in unscaled graph coordinates.\n * @param y - Point y in unscaled graph coordinates.\n * @returns The shortest distance to the route, or `Infinity` when it has no drawable segments.\n */\nfunction distanceToRoute(sections: ElkEdgeSection[], x: number, y: number): number {\n let min = Infinity;\n\n for (const section of sections) {\n const points = [section.startPoint, ...(section.bendPoints ?? []), section.endPoint];\n\n for (let i = 1; i < points.length; i++) {\n min = Math.min(min, distanceToSegment(x, y, points[i - 1].x, points[i - 1].y, points[i].x, points[i].y));\n }\n }\n\n return min;\n}\n\n/** The elements drawn for one edge, so it can be hit-tested and released. */\ninterface DrawnEdge {\n id: string;\n route: DiagramEdgeRoute;\n /** The visible stroked path. */\n path: Handle;\n /** The invisible wide path that takes pointer events. */\n hit: Handle;\n /** The mid-route label, when the edge carries one. */\n label: Handle | null;\n /** The group the three elements above were appended into, so they can be removed from it. */\n group: Handle;\n}\n\n/**\n * The SVG edge layer for a [`DiagramView`](/api/component/diagram/classes/DiagramView).\n * Owns exactly one `<svg>` element and rebuilds its `<path>` children from ELK\n * edge routes whenever the diagram re-lays-out.\n *\n * @category Components\n */\nclass DiagramEdgeLayer extends Component<ComponentOptions> {\n\n /** Cached edge routes, redrawn at render time and on every `setEdges`. */\n private _edges: DiagramEdgeRoute[] = [];\n\n /** Everything currently drawn, released and rebuilt by `rebuildPaths`. */\n private _drawn: DrawnEdge[] = [];\n\n /**\n * The currently emphasised edge ids. Runtime interaction state (not a\n * `ComponentOptions` field — see ARCHITECTURE.md's rule that transient,\n * framework-managed state stays off the options bag). Cleared by `setEdges`.\n */\n private _edgeEmphasis: Set<string> = new Set();\n\n /**\n * The `<g>` holding every edge outside a non-empty emphasis set, carrying\n * {@link DIMMED_EDGE_OPACITY}. Painted before `_normalLayer`, so an\n * emphasised edge always draws over a dimmed one it crosses. Created by\n * `createRootElement`, so it exists before any draw.\n */\n private _dimLayer!: Handle;\n\n /** The `<g>` holding every edge drawn at full strength. Painted over `_dimLayer`. */\n private _normalLayer!: Handle;\n\n /** Per-instance arrowhead marker id, referenced by each plain edge's `marker-end`. */\n private readonly _markerId: string;\n\n /** Per-instance crow's-foot marker ids, keyed by marker kind. */\n private readonly _crowsFootMarkerIds: Record<Exclude<DiagramEdgeMarker, \"arrow\">, string>;\n\n constructor(options?: ComponentOptions) {\n super(options);\n\n this._markerId = `${this.getId()}-arrow`;\n this._crowsFootMarkerIds = Object.fromEntries(\n CROWS_FOOT_MARKERS.map(kind => [kind, `${this.getId()}-${kind}`]),\n ) as Record<Exclude<DiagramEdgeMarker, \"arrow\">, string>;\n\n // Non-interactive overlay: clicks must reach the node components beneath.\n this.setPointerEvents(\"none\");\n\n // Every Component stamps ComponentDefaults' \"default\" cursor into its\n // own rule; left at that, a hit path inheriting from this `<svg>`\n // would resolve to an arrow regardless of what it declares itself.\n // Inheriting through to the view root's live grab/grabbing write is\n // what lets an edge press pan the canvas honestly.\n this.setCursor(\"inherit\");\n }\n\n /**\n * Resolves a marker kind to its namespaced `url(#…)` reference, or\n * `undefined` when the kind is absent (no marker on that end).\n *\n * @param kind - The requested marker kind, if any.\n * @returns The `url(#id)` reference, or `undefined`.\n */\n private markerUrl(kind: DiagramEdgeMarker | undefined): string | undefined {\n if (!kind) {\n return undefined;\n }\n\n const id = kind === \"arrow\" ? this._markerId : this._crowsFootMarkerIds[kind];\n\n return `url(#${id})`;\n }\n\n /**\n * Replaces the drawn edges and rebuilds the path children. Clears any\n * active edge emphasis first, so the freshly-drawn set always starts\n * undimmed — a re-layout that swaps in a new graph is not the graph the\n * emphasis was computed against.\n *\n * Routes routinely arrive before this layer has an element: a diagram built\n * inside a dock tab runs its whole ELK layout while the tab is still\n * detached (the app awaits\n * [`DiagramView.whenLaidOut`](/api/component/diagram/classes/DiagramView)\n * before mounting, so the layout *always* lands first), and `rebuildPaths`\n * cannot draw without one. `render` only performs the first draw when it is\n * what creates the element, which is not the case here — so without the\n * deferral below the sole draw for those routes is silently lost and the\n * diagram shows nodes with no edges until some later `setEdges` happens to\n * find an element. {@link Component.onFirstLayout} exists for exactly this\n * \"content built before the host attaches it\" case.\n *\n * @param edges - The routed edges to draw.\n *\n * @returns This layer, for method chaining.\n */\n setEdges(edges: DiagramEdgeRoute[]): this {\n this._edges = edges;\n this._edgeEmphasis = new Set();\n\n if (this.getElement()) {\n this.rebuildPaths();\n } else {\n this.onFirstLayout(() => this.rebuildPaths());\n }\n\n return this;\n }\n\n /**\n * Sets the emphasised edge ids. While the set is non-empty every edge NOT\n * in it is drawn at a reduced opacity; the emphasised edges keep their\n * normal weight. `null` or an empty array clears the emphasis. Ids naming\n * no drawn edge are kept but have no effect.\n *\n * @param ids - The edge ids to emphasise, or null to clear.\n *\n * @returns This layer, for method chaining.\n */\n setEdgeEmphasis(ids: readonly string[] | null): this {\n this._edgeEmphasis = new Set(ids ?? []);\n\n // A full redraw rather than a restyle in place: which of the two groups\n // an edge belongs to is decided at draw time, and redrawing is what\n // moves it. Cheap enough to do on a click — a redraw of a 1000-edge\n // graph's paths measures around 20ms, against the seconds ELK itself\n // takes — and it reuses the release/rebuild path already covered by\n // tests instead of relying on appendChild's move semantics through the\n // DOM seam.\n this.rebuildPaths();\n\n return this;\n }\n\n /**\n * The currently emphasised edge ids.\n *\n * @returns A copy of the emphasised id array; empty when nothing is emphasised.\n */\n getEdgeEmphasis(): string[] {\n return [...this._edgeEmphasis];\n }\n\n /**\n * Resolves a raw DOM event target to the edge whose invisible hit path it is.\n *\n * @param target - The raw DOM event target.\n * @returns The edge id, or null when the target is not an edge hit path.\n */\n edgeIdAt(target: EventTarget | null): string | null {\n if (target === null) {\n return null;\n }\n\n const handle = DOM.source.intern(target);\n\n return this._drawn.find(d => d.hit === handle)?.id ?? null;\n }\n\n /**\n * Every drawn edge whose route passes within the hit tolerance of a point,\n * in draw order. Several edges answer here wherever their routes overlap —\n * which is what makes a merged trunk answerable.\n *\n * @param x - Point x in unscaled graph coordinates.\n * @param y - Point y in unscaled graph coordinates.\n * @returns The routes within tolerance; empty when none is.\n */\n edgesNear(x: number, y: number): DiagramEdgeRoute[] {\n return this._drawn\n .filter(drawn => distanceToRoute(drawn.route.sections, x, y) <= EDGE_HIT_TOLERANCE)\n .map(drawn => drawn.route);\n }\n\n /**\n * Which group one edge draws into: the dimmed group when an emphasis set is\n * active and this edge is not in it, the full-strength group otherwise.\n *\n * @param id - The edge id.\n * @returns The group handle to append the edge's elements into.\n */\n private groupFor(id: string): Handle {\n if (this._edgeEmphasis.size === 0 || this._edgeEmphasis.has(id)) {\n return this._normalLayer;\n }\n\n return this._dimLayer;\n }\n\n /**\n * Creates the root `<svg>` with the arrowhead marker plus one `<marker>` per\n * crow's-foot kind in a shared `<defs>`. Every marker and its child shapes\n * are seam-created leaf children tracked for release.\n *\n * @returns The root `<svg>` handle.\n */\n protected createRootElement(): Handle {\n const svg = DOM.sink.createElementNS(SVG_NS, \"svg\");\n const defs = DOM.sink.createElementNS(SVG_NS, \"defs\");\n\n this.createArrowMarker(defs);\n\n for (const kind of CROWS_FOOT_MARKERS) {\n this.createCrowsFootMarker(defs, kind);\n }\n\n DOM.sink.appendChild(svg, defs);\n this.trackHandle(defs);\n\n this._dimLayer = this.createEdgeGroup(svg, DIMMED_EDGE_OPACITY);\n this._normalLayer = this.createEdgeGroup(svg, null);\n\n return svg;\n }\n\n /**\n * Creates one of the two persistent edge groups as a child of the root\n * `<svg>`. Append order is paint order, so the caller creates the dimmed\n * group first.\n *\n * @param svg - The root `<svg>` handle to append the group into.\n * @param opacity - The group's opacity, or null to leave it at full strength.\n *\n * @returns The `<g>` handle.\n */\n private createEdgeGroup(svg: Handle, opacity: string | null): Handle {\n const group = DOM.sink.createElementNS(SVG_NS, \"g\");\n\n if (opacity !== null) {\n DOM.sink.apply(group, { setAttr: { opacity } });\n }\n\n DOM.sink.appendChild(svg, group);\n this.trackHandle(group);\n\n return group;\n }\n\n /**\n * Defines the default arrowhead `<marker>` into `defs`. `orient=\"auto-start-\n * reverse\"` so the one definition serves both `marker-end` (unreversed, the\n * common case) and `marker-start` (auto-flipped to point back out of the\n * source node) — e.g. a data-flow edge that draws its arrow at the source end.\n *\n * @param defs - The `<defs>` element to append into.\n */\n private createArrowMarker(defs: Handle): void {\n const marker = DOM.sink.createElementNS(SVG_NS, \"marker\");\n\n DOM.sink.apply(marker, { setAttr: {\n id: this._markerId,\n markerWidth: String(ARROW_SIZE),\n markerHeight: String(ARROW_SIZE),\n refX: String(ARROW_SIZE),\n refY: String(ARROW_SIZE / 2),\n orient: \"auto-start-reverse\",\n markerUnits: \"userSpaceOnUse\",\n } });\n\n const arrow = DOM.sink.createElementNS(SVG_NS, \"path\");\n DOM.sink.apply(arrow, { setAttr: {\n d: `M 0 0 L ${ARROW_SIZE} ${ARROW_SIZE / 2} L 0 ${ARROW_SIZE} z`,\n fill: EDGE_STROKE,\n } });\n\n DOM.sink.appendChild(marker, arrow);\n DOM.sink.appendChild(defs, marker);\n\n this.trackHandle(marker);\n this.trackHandle(arrow);\n }\n\n /**\n * Defines one crow's-foot `<marker>` into `defs` from its\n * {@link MARKER_GEOMETRY} entry. `orient=\"auto-start-reverse\"` lets the same\n * definition serve both `marker-start` (auto-flipped) and `marker-end`\n * (unreversed).\n *\n * @param defs - The `<defs>` element to append into.\n * @param kind - Which crow's-foot marker to define.\n */\n private createCrowsFootMarker(defs: Handle, kind: Exclude<DiagramEdgeMarker, \"arrow\">): void {\n const geometry = MARKER_GEOMETRY[kind];\n const marker = DOM.sink.createElementNS(SVG_NS, \"marker\");\n\n DOM.sink.apply(marker, { setAttr: {\n id: this._crowsFootMarkerIds[kind],\n markerWidth: String(geometry.width),\n markerHeight: String(geometry.height),\n refX: String(geometry.refX),\n refY: String(geometry.refY),\n orient: \"auto-start-reverse\",\n markerUnits: \"userSpaceOnUse\",\n } });\n\n DOM.sink.appendChild(defs, marker);\n this.trackHandle(marker);\n\n for (const part of geometry.parts) {\n const shape = DOM.sink.createElementNS(SVG_NS, part.tag);\n\n DOM.sink.apply(shape, { setAttr: {\n ...part.attrs,\n fill: \"none\",\n stroke: EDGE_STROKE,\n } });\n\n DOM.sink.appendChild(marker, shape);\n this.trackHandle(shape);\n }\n }\n\n /**\n * Renders the root element and draws the cached edges.\n *\n * @returns The rendered root element.\n */\n protected render(): Handle {\n const element = super.render();\n\n this.rebuildPaths();\n\n return element;\n }\n\n /**\n * Clears the previously-drawn paths and rebuilds them from the cached edge\n * routes. No-op before the element exists — `render` performs the first draw.\n */\n private rebuildPaths(): void {\n const svg = this.getElement();\n\n if (!svg) {\n return;\n }\n\n for (const drawn of this._drawn) {\n this.releaseDrawnEdge(drawn);\n }\n\n this._drawn = [];\n\n for (const edge of this._edges) {\n const d = buildPathData(edge.sections);\n\n if (!d) {\n continue;\n }\n\n const group = this.groupFor(edge.id);\n const hit = this.drawHitPath(group, d);\n const path = this.drawVisiblePath(group, edge, d);\n const style = edge.style;\n let label: Handle | null = null;\n\n if (style?.label) {\n label = this.drawLabel(group, edge, style.label);\n }\n\n this._drawn.push({ id: edge.id, route: edge, path, hit, label, group });\n }\n }\n\n /**\n * Draws one edge's invisible wide hit path: same `d` as the visible path,\n * transparent stroke, and the only element in the layer that opts back\n * into pointer events (`pointer-events: stroke`) — the root `<svg>` stays\n * inert. Appended before the visible path.\n *\n * @param group - The edge group handle to append into.\n * @param d - The edge's path data (shared with the visible path).\n * @returns The hit path's handle.\n */\n private drawHitPath(group: Handle, d: string): Handle {\n const hit = DOM.sink.createElementNS(SVG_NS, \"path\");\n\n DOM.sink.apply(hit, { setAttr: {\n d,\n fill: \"none\",\n stroke: \"transparent\",\n \"stroke-width\": String(EDGE_HIT_WIDTH),\n \"pointer-events\": \"stroke\",\n // Dragging an edge pans the canvas like empty canvas does, so the\n // hit path takes the viewport's own live grab/grabbing cursor by\n // inheriting rather than promising a cursor of its own.\n cursor: \"inherit\",\n } });\n\n DOM.sink.appendChild(group, hit);\n this.trackHandle(hit);\n\n return hit;\n }\n\n /**\n * Draws one edge's visible stroked path, carrying its markers and dash. The\n * edge's emphasis state is not written here — it is the group the path is\n * appended into (see {@link DIMMED_EDGE_OPACITY}).\n *\n * @param group - The edge group handle to append into.\n * @param edge - The routed edge to draw.\n * @param d - The edge's path data.\n * @returns The visible path's handle.\n */\n private drawVisiblePath(group: Handle, edge: DiagramEdgeRoute, d: string): Handle {\n const path = DOM.sink.createElementNS(SVG_NS, \"path\");\n const style = edge.style;\n\n // No style: today's back-compat behaviour — a plain arrow at the end,\n // no start marker. With a style, each end draws only the marker (if\n // any) the style names; an edge can carry cardinality with no\n // marker-end at all.\n const markerEnd = style ? this.markerUrl(style.endMarker) : this.markerUrl(\"arrow\");\n const markerStart = style ? this.markerUrl(style.startMarker) : undefined;\n\n const attrs: Record<string, string> = {\n d,\n fill: \"none\",\n stroke: style?.stroke ?? EDGE_STROKE,\n \"stroke-width\": EDGE_STROKE_WIDTH,\n };\n\n if (markerEnd) {\n attrs[\"marker-end\"] = markerEnd;\n }\n\n if (markerStart) {\n attrs[\"marker-start\"] = markerStart;\n }\n\n if (style?.dashed) {\n attrs[\"stroke-dasharray\"] = DASH_ARRAY;\n }\n\n DOM.sink.apply(path, { setAttr: attrs });\n\n DOM.sink.appendChild(group, path);\n this.trackHandle(path);\n\n return path;\n }\n\n /**\n * Releases one previously-drawn edge's elements (hit path, visible path,\n * and optional label) from the group they were appended into and from the\n * tracked-handle set.\n *\n * @param drawn - The drawn edge record to release.\n */\n private releaseDrawnEdge(drawn: DrawnEdge): void {\n const handles = [drawn.hit, drawn.path, ...(drawn.label ? [drawn.label] : [])];\n\n for (const handle of handles) {\n DOM.sink.removeChild(drawn.group, handle);\n this.untrackHandle(handle);\n DOM.sink.release(handle);\n }\n }\n\n /**\n * Draws one edge's optional mid-route label as a `<text>` element.\n *\n * @param group - The edge group handle to append into, which also carries\n * the label's emphasis state (see {@link DIMMED_EDGE_OPACITY}).\n * @param edge - The edge route the label belongs to (for its anchor point).\n * @param label - The label text.\n * @returns The label's handle.\n */\n private drawLabel(group: Handle, edge: DiagramEdgeRoute, label: string): Handle {\n const point = labelPoint(edge.sections);\n const text = DOM.sink.createElementNS(SVG_NS, \"text\");\n\n DOM.sink.apply(text, {\n setAttr: {\n x: String(point.x),\n y: String(point.y),\n fill: EDGE_STROKE,\n // Background-coloured halo painted first, so the edge line under the\n // glyphs is masked and the label reads clearly (not crossed out).\n stroke: LABEL_HALO,\n \"stroke-width\": LABEL_HALO_WIDTH,\n \"stroke-linejoin\": \"round\",\n \"paint-order\": \"stroke\",\n \"font-size\": LABEL_FONT_SIZE,\n \"text-anchor\": \"middle\",\n \"dominant-baseline\": \"central\",\n },\n text: label,\n });\n\n DOM.sink.appendChild(group, text);\n this.trackHandle(text);\n\n return text;\n }\n}\n\nconst DiagramEdgeLayerCallable = callable(DiagramEdgeLayer);\ntype DiagramEdgeLayerCallable = DiagramEdgeLayer;\nexport {\n DiagramEdgeLayer as _DiagramEdgeLayer,\n DiagramEdgeLayerCallable as DiagramEdgeLayer,\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n//\n// A read-only graph/diagram viewer. Takes a framework-native node/edge model,\n// runs it through ELK for automatic layout (off-seam pure compute, lazily\n// imported), and renders themed node components plus an SVG edge layer with pan,\n// zoom, and node selection.\n//\n// Structure: DiagramView (an Anchor-managed Panel viewport) owns a single\n// content host Container (Absolute layout) that carries the pan/zoom transform\n// and holds the node components + the edge layer, plus a corner-pinned control\n// cluster (zoom in/out, fit, reset). Pan is an unbounded `translate()` on the\n// content host's transform, not native scroll — the viewport has no scrollbars\n// and simply clips (`overflow: hidden`) whatever pans outside it, giving an\n// infinite-canvas feel. Zoom is the transform's `scale()` factor.\n\nimport { Panel, PanelOptions } from \"~/core/Panel.js\";\nimport { Container } from \"~/core/Container.js\";\nimport { Component } from \"~/core/Component.js\";\nimport { Absolute } from \"~/layout/Absolute.js\";\nimport { Anchor } from \"~/layout/Anchor.js\";\nimport { VBox } from \"~/layout/VBox.js\";\nimport { FloatingPanel } from \"~/component/container/FloatingPanel.js\";\nimport { Button } from \"~/component/button/Button.js\";\nimport { Glyph } from \"~/component/display/Glyph.js\";\nimport { ProgressSpinner } from \"~/component/display/ProgressSpinner.js\";\nimport { plus } from \"~/glyphs/solid/plus.js\";\nimport { minus } from \"~/glyphs/solid/minus.js\";\nimport { expand } from \"~/glyphs/solid/expand.js\";\nimport { crosshairs } from \"~/glyphs/solid/crosshairs.js\";\nimport { Event } from \"~/core/Event.js\";\nimport { DOM } from \"~/core/DOM.js\";\nimport type { Handle } from \"~/core/DOM.js\";\nimport { ListenerBag } from \"~/core/ListenerBag.js\";\nimport { DiagramData, DiagramEdgeData, DiagramNodeData } from \"~/component/diagram/DiagramModel.js\";\nimport { ElkLayoutEngine, DiagramLayoutResult } from \"~/component/diagram/ElkLayoutEngine.js\";\nimport { DiagramNode } from \"~/component/diagram/DiagramNode.js\";\nimport { DiagramGroupNode } from \"~/component/diagram/DiagramGroupNode.js\";\nimport { DiagramEdgeLayer } from \"~/component/diagram/DiagramEdgeLayer.js\";\nimport type { DiagramEdgeRoute } from \"~/component/diagram/DiagramEdgeLayer.js\";\nimport { callable } from \"~/core/Callable.js\";\n\nGlyph.register(plus, minus, expand, crosshairs);\n\n/** Factory producing a node component from a node's model data. */\nexport type DiagramNodeRenderer = (data: DiagramNodeData) => Component;\n\n/** String-literal union of the events emitted by {@link DiagramView}. */\nexport type DiagramViewEvent = \"selection\" | \"activate\" | \"layout\" | \"contextmenu\" | \"edgehover\" | \"edgeleave\";\n\n/** Default initial zoom factor. */\nconst DEFAULT_ZOOM = 1;\n\n/** Default minimum zoom factor (a quarter scale). */\nconst DEFAULT_MIN_ZOOM = 0.25;\n\n/** Default maximum zoom factor (4× scale). */\nconst DEFAULT_MAX_ZOOM = 4;\n\n/** Multiplicative zoom step per wheel notch. */\nconst WHEEL_ZOOM_STEP = 1.1;\n\n/** Multiplicative zoom step per control-cluster button press. */\nconst ZOOM_BUTTON_STEP = 1.5;\n\n/**\n * Pixel distance the pointer must travel between `pointerdown` and a later\n * `pointermove` before the press-and-release it ends in counts as a drag\n * rather than a click. Matches `DragManager.DRAG_THRESHOLD`, the library's\n * only other click-versus-drag disambiguation.\n */\nconst CLICK_SLOP = 4;\n\n/**\n * Opacity of a node component outside a non-empty node-emphasis set. Higher\n * than `DiagramEdgeLayer`'s `DIMMED_EDGE_OPACITY` (`0.15`) because perceived\n * presence scales with area, not just alpha: a `TableCardNode`-sized box with\n * a border and text rows still reads at `0.15`, whereas a 1.5px hairline\n * needs the lower number to recede. `0.35` keeps a dimmed card's shape and\n * label legible while clearly receding behind the emphasised ones.\n */\nconst DIMMED_NODE_OPACITY = 0.35;\n\n// Diameter in pixels of the busy overlay's arc. Matches `TablePanel`'s\n// store-loading spinner and `createSpinnerWrap`'s lazy-tab placeholder, so a\n// slow diagram update and a slow data load read as the same kind of wait.\nconst BUSY_SPINNER_DIAMETER = 24;\n\n// Structural breathing room between the control cluster and the viewport\n// corner it is pinned to — not a cosmetic choice, but the inset the Anchor\n// constraint needs so the cluster does not paint flush against the edge.\nconst CONTROLS_MARGIN = 12;\n\n// Paint order for a compound graph (one with at least one container): the\n// container boxes sit behind the edges, which sit behind the leaves, so a\n// leaf's click is never intercepted by its own container box. A leaf/container\n// component is always freshly built by rebuildNodes, so on a flat graph it\n// simply starts at DEFAULT_Z_INDEX and is never touched. The edge layer is the\n// one exception — it is a persistent child of the content host (built once in\n// the constructor, never torn down/rebuilt — see the constructor comment), so\n// a flat pass must explicitly restore DEFAULT_Z_INDEX in case an earlier\n// compound pass on the same view left EDGE_LAYER_Z_INDEX behind.\nconst DEFAULT_Z_INDEX = 0;\nconst CONTAINER_Z_INDEX = 0;\nconst EDGE_LAYER_Z_INDEX = 1;\nconst LEAF_Z_INDEX = 2;\n\n/**\n * Construction-time options for {@link DiagramView}.\n *\n * @category Components\n */\nexport interface DiagramViewOptions extends PanelOptions {\n /** The initial graph to lay out and render. */\n data?: DiagramData;\n /** Factory for node components; defaults to building a `DiagramNode`. */\n nodeRenderer?: DiagramNodeRenderer;\n /** Factory for compound container components; defaults to `DiagramGroupNode`. */\n groupRenderer?: DiagramNodeRenderer;\n /** Default ELK layout options applied to every layout pass. */\n layoutOptions?: Record<string, string>;\n /**\n * URL of a consumer-hosted `elk-worker.js`, requesting off-thread layout.\n * With the `elk.bundled.js` module this view's engine imports, elkjs's\n * own worker-availability check always fails, so `workerUrl` alone never\n * actually constructs a Worker; layout still runs on the main thread, via\n * elkjs's own fallback. Pass {@link DiagramViewOptions.elkWorkerFactory}\n * for real off-thread execution.\n */\n elkWorkerUrl?: string;\n /**\n * Factory returning a Web Worker for off-thread ELK layout. When set,\n * ELK's compute runs in the returned worker. Construct it in your app so\n * your bundler emits the worker, e.g.\n * `() => new Worker(new URL(\"elkjs/lib/elk-worker.min.js\", import.meta.url), { type: \"classic\" })`.\n * Takes precedence over {@link DiagramViewOptions.elkWorkerUrl} when both\n * are set.\n */\n elkWorkerFactory?: () => Worker;\n /** Minimum zoom factor (default 0.25). */\n minZoom?: number;\n /** Maximum zoom factor (default 4). */\n maxZoom?: number;\n /** Initial zoom factor (default 1). */\n zoom?: number;\n /** Show the built-in zoom / fit / reset control cluster (default true). */\n controls?: boolean;\n /**\n * Id of the node the one-shot initial view centres on, instead of the\n * graph's bounds. An id naming no node in the graph falls back to\n * centring the bounds. The configured `zoom` is honoured, except that a\n * focus node too large to fit the viewport lowers it until the node fits.\n */\n initialFocusNode?: string;\n /** Construction-time listener bag dispatched to {@link DiagramView.on}. */\n listeners?: {\n selection?: (nodes: DiagramNodeData[]) => void;\n activate?: (node: DiagramNodeData) => void;\n layout?: () => void;\n contextmenu?: (node: DiagramNodeData, event: MouseEvent) => void;\n edgehover?: (edges: DiagramEdgeData[], event: MouseEvent) => void;\n edgeleave?: () => void;\n };\n}\n\n/**\n * A read-only automatic-layout graph viewer with pan, zoom, and node selection.\n *\n * Pass a graph via the `data` option or {@link DiagramView.setData}. Layout runs\n * asynchronously through ELK; a `\"layout\"` event fires after each successful\n * pass, a `\"selection\"` event fires when the selected node changes, and an\n * `\"activate\"` event fires when a node is double-clicked.\n *\n * @example\n * ```typescript\n * const view = new DiagramView({\n * data: {\n * nodes: [{ id: \"a\", label: \"A\" }, { id: \"b\", label: \"B\" }],\n * edges: [{ id: \"e\", source: \"a\", target: \"b\" }],\n * },\n * });\n * view.on(\"selection\", (nodes) => console.log(nodes));\n * ```\n *\n * @category Components\n */\nclass DiagramView extends Panel<DiagramViewOptions> {\n\n /** The transform-carrying content host holding nodes + the edge layer. */\n private _contentHost!: Container;\n\n /** The SVG edge layer, a persistent child of the content host. */\n private _edgeLayer!: DiagramEdgeLayer;\n\n /** The ELK layout adapter. Runtime state, off the options bag. */\n private _engine!: ElkLayoutEngine;\n\n /** Node components keyed by node id — the graph currently on screen. */\n private _nodeComponents: Map<string, Component> = new Map();\n\n /** Node model data keyed by node id (selection payload source) — the graph currently on screen. */\n private _nodeData: Map<string, DiagramNodeData> = new Map();\n\n /** Ids of the compound container nodes in the graph currently on screen. */\n private _containerIds: Set<string> = new Set();\n\n /**\n * The graph built by the latest `setData`, mounted hidden and awaiting a\n * layout. Promoted into the shown set above by `applyLayout`, discarded by\n * `handleLayoutFailure` or by the next `rebuildNodes`. Kept separate from\n * the shown set so a re-layout leaves the previous graph on screen for the\n * whole ELK round-trip instead of blanking the canvas.\n */\n private _incomingComponents: Map<string, Component> = new Map();\n private _incomingData: Map<string, DiagramNodeData> = new Map();\n private _incomingContainerIds: Set<string> = new Set();\n\n /** Currently selected node data (single-select). */\n private _selection: DiagramNodeData[] = [];\n\n /**\n * Ids of the emphasised nodes; every other node component is dimmed.\n * Runtime interaction state, off the options bag — the view's own mirror\n * of `DiagramEdgeLayer`'s `_edgeEmphasis`, except there is no equivalent\n * layer for nodes, so it lives here instead. Cleared in\n * `promoteIncomingNodes` beside `_selection`.\n */\n private _nodeEmphasis: Set<string> = new Set();\n\n /** Monotonic layout token; guards against a stale in-flight layout landing. */\n private _layoutGeneration: number = 0;\n\n /** Resolver for the promise `whenLaidOut` hands out, or `null` when idle. */\n private _layoutSettled: { promise: Promise<void>; resolve: () => void } | null = null;\n\n /**\n * The overlay busy indicator, built the first time a layout pass runs on a\n * view that has a size. Runtime state, deliberately off the options bag.\n */\n private _busySpinner: ProgressSpinner | null = null;\n\n /** Cached graph bounding box from the last successful layout. */\n private _graphWidth: number = 0;\n private _graphHeight: number = 0;\n\n /** Custom-event fan-out for `\"selection\"` / `\"layout\"` / `\"contextmenu\"`. */\n private _listeners: ListenerBag<DiagramViewEvent> = new ListenerBag<DiagramViewEvent>();\n\n /** Edge ids of the last emitted \"edgehover\", joined, or null when not hovering. */\n private _hoveredEdgeKey: string | null = null;\n\n /** Pan drag state. */\n private _panning: boolean = false;\n private _panStartX: number = 0;\n private _panStartY: number = 0;\n\n /**\n * Pointer position at the last `pointerdown`, and whether the pointer has\n * since travelled past `CLICK_SLOP`. Runtime gesture state, off the\n * options bag: `_handleClick` reads `_pointerMoved` to tell a click from\n * the tail end of a drag — a pan that starts and ends on empty canvas\n * still fires a native `click`, and it must not clear the selection (or\n * the edge emphasis the app keys off it).\n */\n private _pressX: number = 0;\n private _pressY: number = 0;\n private _pointerMoved: boolean = false;\n\n /** Pan offset (viewport pixels) captured when a drag begins. */\n private _panOriginX: number = 0;\n private _panOriginY: number = 0;\n\n /** Current, unbounded pan offset (viewport pixels) driving the content host's transform. */\n private _panX: number = 0;\n private _panY: number = 0;\n\n /**\n * Whether the graph still owes its one-time initial centring. Cleared by\n * the first layout that manages to centre, so a later `setData` re-layout\n * never yanks a pan the user has since dragged to.\n */\n private _needsInitialCentre: boolean = true;\n\n /**\n * Id of the node the one-shot initial view centres on, instead of the\n * graph's bounds; also the target of the most recent `focusNode` call.\n * Seeded from `initialFocusNode` in the constructor body (mirroring how\n * `data`/`controls` are cached during `applyOptions` and dispatched once\n * the constructor body can act on them) and re-pointed by `focusNode`.\n */\n private _focusNodeId: string | null = null;\n\n /**\n * Viewport size at the last layout, so a resize can be measured as a delta\n * and the graph point under the viewport centre held in place. `NaN` until\n * the view is first sized (matching `getWidth`/`getHeight`).\n */\n private _lastViewportWidth: number = NaN;\n private _lastViewportHeight: number = NaN;\n\n /** The corner-pinned zoom / fit / reset control cluster. */\n private _controls!: FloatingPanel;\n private _zoomInBtn!: Button;\n private _zoomOutBtn!: Button;\n private _fitBtn!: Button;\n private _resetBtn!: Button;\n\n private readonly _onZoomIn: () => void = () => this.zoomIn();\n private readonly _onZoomOut: () => void = () => this.zoomOut();\n private readonly _onFit: () => void = () => this.zoomToFit();\n private readonly _onReset: () => void = () => this.resetView();\n\n constructor(options?: DiagramViewOptions) {\n super(options, { zoom: DEFAULT_ZOOM, minZoom: DEFAULT_MIN_ZOOM, maxZoom: DEFAULT_MAX_ZOOM, controls: true });\n\n this.setLayoutManager(new Anchor());\n this.setCursor(\"grab\");\n\n this._engine = this.createEngine();\n\n // Nodes are laid out at unscaled graph coordinates under the host's\n // `translate(panX,panY) scale(zoom)` transform, and the host's own box\n // is likewise sized to the unscaled graph bounds (see applyLayout), so\n // node coordinates never exceed the box regardless of zoom — unlike the\n // old scaled-box model this stays visible for consistency with that\n // history rather than out of present necessity.\n // `cursor: \"inherit\"` rather than the Component default: the host is an\n // invisible box spanning the whole graph bounds, so the default\n // `cursor: default` every Component stamps would paint an arrow across\n // the entire canvas and mask the viewport's own grab/grabbing. Inheriting\n // lets the single write on the view root govern the whole canvas.\n this._contentHost = new Container({ layoutManager: new Absolute(), overflow: \"visible\", cursor: \"inherit\" });\n this._contentHost.setTransformOrigin(\"0 0\");\n this.addComponent(this._contentHost);\n\n this._edgeLayer = new DiagramEdgeLayer();\n this._contentHost.addComponent(this._edgeLayer);\n\n this.buildControls();\n this.wireControlListeners();\n\n this.addComponent(this._controls, this._controls.getAnchorConstraints());\n\n this.applyListeners(options?.listeners);\n\n this.setControlsVisible(this._options.controls ?? this._defaultOptions.controls ?? true);\n\n if (this._options.zoom !== undefined) {\n this.setZoom(this._options.zoom);\n }\n\n this._focusNodeId = this._options.initialFocusNode ?? null;\n\n if (this._options.data) {\n this.setData(this._options.data);\n }\n }\n\n /**\n * Builds the layout engine. Isolated as a factory so the swappable-engine\n * seam can be exercised (a test substitutes a stub engine that returns a\n * fixed result without importing ELK).\n *\n * @returns A fresh {@link ElkLayoutEngine}.\n */\n protected createEngine(): ElkLayoutEngine {\n return new ElkLayoutEngine({\n workerFactory: this._options.elkWorkerFactory,\n workerUrl: this._options.elkWorkerUrl,\n });\n }\n\n /**\n * Disposes the layout engine — releasing its ELK Web Worker, if it had one\n * — before the inherited destructor detaches the element.\n */\n protected destructor(): void {\n // Invalidate any layout still in flight before the engine goes away. A\n // result landing afterwards would write into a torn-down view, and a\n // failure landing afterwards would strip nodes off it; both guards\n // compare against this token and drop a stale one.\n this._layoutGeneration += 1;\n\n // Settle rather than leave dangling: a caller awaiting `whenLaidOut()`\n // (e.g. the app holding a lazy tab's spinner) must not hang forever\n // just because the view was disposed on some other path mid-pass.\n this.settleLayout();\n\n // The spinner is mounted by a raw DOM append rather than as a child\n // component, so the inherited destructor's child pass never reaches it.\n this._busySpinner?.dispose();\n this._busySpinner = null;\n\n this._engine.dispose();\n\n super.destructor();\n }\n\n /**\n * Caches consumer-configurable fields pure to `_options`; effects that need\n * the content host (built in the constructor body) are dispatched there.\n *\n * @param options - The options bag carrying the values to apply.\n */\n protected applyOptions(options: DiagramViewOptions): this {\n super.applyOptions(options);\n\n if (options.data !== undefined) this._options.data = options.data;\n if (options.nodeRenderer !== undefined) this._options.nodeRenderer = options.nodeRenderer;\n if (options.groupRenderer !== undefined) this._options.groupRenderer = options.groupRenderer;\n if (options.layoutOptions !== undefined) this._options.layoutOptions = options.layoutOptions;\n if (options.elkWorkerUrl !== undefined) this._options.elkWorkerUrl = options.elkWorkerUrl;\n if (options.elkWorkerFactory !== undefined) this._options.elkWorkerFactory = options.elkWorkerFactory;\n if (options.minZoom !== undefined) this._options.minZoom = options.minZoom;\n if (options.maxZoom !== undefined) this._options.maxZoom = options.maxZoom;\n if (options.zoom !== undefined) this._options.zoom = options.zoom;\n // Cached only: the control cluster does not exist yet during the\n // `super()` cascade. The constructor dispatches `setControlsVisible`\n // itself once the cluster is built.\n if (options.controls !== undefined) this._options.controls = options.controls;\n // Cached only: dispatched into the runtime `_focusNodeId` field from\n // the constructor body, mirroring `data` below.\n if (options.initialFocusNode !== undefined) this._options.initialFocusNode = options.initialFocusNode;\n\n return this;\n }\n\n /**\n * Replaces the graph, rebuilds node components, and triggers an async layout.\n *\n * @param data - The new graph.\n *\n * @returns This view, for method chaining.\n */\n setData(data: DiagramData): this {\n this._options.data = data;\n this.rebuildNodes(data);\n this.relayout(data);\n\n return this;\n }\n\n /**\n * Returns the current graph, or `null` when none has been set.\n *\n * @returns The graph data, or `null`.\n */\n getData(): DiagramData | null {\n return this._options.data ?? null;\n }\n\n /**\n * Builds fresh node components via the node renderer (default:\n * `DiagramNode`) into the *incoming* set, built and measured off the\n * component tree and left `setVisible(false)` — not `display: none`, so\n * the flag still reads false the instant a component mounts, before the\n * promote step's reveal. `applyLayout` positions them, then\n * `promoteIncomingNodes` swaps them in for the shown set, mounts them, and\n * reveals them together, so a diagram never paints an unplaced graph and a\n * re-layout keeps the previous graph on screen for the whole ELK\n * round-trip. A graph superseded by a newer `setData` before its layout\n * lands is therefore never mounted at all. Leads with\n * `discardIncomingNodes()` so a second `setData` arriving before the\n * first's layout lands does not leak the first attempt's components.\n * Recurses into `children`: a container node (non-empty `children`) is\n * built via the group renderer (default: `DiagramGroupNode`) and its\n * children are built the same way, so container + leaf components all\n * land as flat siblings in `_incomingComponents`/`_incomingData`, keyed by\n * id — a container is never a DOM parent of its children (that would\n * perturb the single-content-host model `nodeIdAt` hit-testing relies on).\n *\n * @param data - The graph whose nodes to build.\n */\n private rebuildNodes(data: DiagramData): void {\n this.discardIncomingNodes();\n\n const renderer = this._options.nodeRenderer ?? ((node: DiagramNodeData): Component =>\n new DiagramNode({ label: node.label, glyph: node.glyph, badge: node.badge }));\n const groupRenderer = this._options.groupRenderer ?? ((node: DiagramNodeData): Component =>\n new DiagramGroupNode({ label: node.label, glyph: node.glyph }));\n\n const build = (nodes: DiagramNodeData[]): void => {\n for (const node of nodes) {\n const isContainer = (node.children?.length ?? 0) > 0;\n const component = isContainer ? groupRenderer(node) : renderer(node);\n\n component.setVisible(false);\n\n this._incomingComponents.set(node.id, component);\n this._incomingData.set(node.id, node);\n\n if (isContainer) {\n this._incomingContainerIds.add(node.id);\n build(node.children!);\n }\n }\n };\n\n build(data.nodes);\n }\n\n /** Forgets the un-promoted incoming components; never mounted, so nothing to detach. */\n private discardIncomingNodes(): void {\n this._incomingComponents.clear();\n this._incomingData.clear();\n this._incomingContainerIds.clear();\n }\n\n /**\n * Swaps the incoming set in for the shown one: removes the shown\n * components from the content host, promotes the incoming maps, clears\n * the selection and the node emphasis, then mounts and reveals every\n * promoted component together — this is the first time an incoming\n * component is added to the content host at all.\n */\n private promoteIncomingNodes(): void {\n for (const component of this._nodeComponents.values()) {\n this._contentHost.removeComponent(component);\n }\n\n this._nodeComponents = this._incomingComponents;\n this._nodeData = this._incomingData;\n this._containerIds = this._incomingContainerIds;\n\n this._incomingComponents = new Map();\n this._incomingData = new Map();\n this._incomingContainerIds = new Set();\n\n this._selection = [];\n this._nodeEmphasis = new Set();\n\n for (const component of this._nodeComponents.values()) {\n this._contentHost.addComponent(component);\n component.setVisible(true);\n }\n }\n\n /**\n * Collects node sizes, bumps the generation token, and runs the async ELK\n * layout. A stale result (older token) is dropped; a failed layout tears the\n * nodes down so the view stays empty.\n *\n * @param data - The graph to lay out.\n */\n private relayout(data: DiagramData): void {\n this.armLayoutSettled();\n this.syncBusyIndicator();\n\n const sizes = this.collectNodeSizes(data);\n\n this._layoutGeneration += 1;\n const generation = this._layoutGeneration;\n\n this._engine\n .layout(data, sizes, this._options.layoutOptions)\n .then((result) => this.applyLayout(result, generation))\n .catch(() => this.handleLayoutFailure(generation));\n }\n\n /**\n * Arms the awaitable `whenLaidOut` hands out, so one deferred spans\n * however many layout passes run before one of them finishes. A no-op\n * when a pass is already in flight, so two rapid `setData` calls share\n * one promise, resolved by whichever pass settles it.\n */\n private armLayoutSettled(): void {\n if (this._layoutSettled !== null) {\n return;\n }\n\n let resolve!: () => void;\n const promise = new Promise<void>((r) => { resolve = r; });\n\n this._layoutSettled = { promise, resolve };\n }\n\n /** Settles the armed `whenLaidOut` awaitable, if there is one. */\n private settleLayout(): void {\n const settled = this._layoutSettled;\n\n this._layoutSettled = null;\n settled?.resolve();\n\n this.syncBusyIndicator();\n }\n\n /**\n * Matches the overlay busy indicator to whether a layout pass is in flight,\n * and re-sizes a showing overlay to the current viewport. Called when a pass\n * starts, when one settles, and from every layout pass — which is where a\n * view that had no size when its pass started finally gets one.\n *\n * A view with no committed size has nothing to cover: `getWidth()` /\n * `getHeight()` are `NaN` (not 0) until the first `setSize`, and `> 0` rejects\n * both (see `effectiveMinZoom`). That also keeps the indicator off the first\n * pass every diagram runs from its own constructor, before its host has sized\n * it — whatever opened the view owns that first wait.\n */\n private syncBusyIndicator(): void {\n if (this._layoutSettled === null || !(this.getWidth() > 0) || !(this.getHeight() > 0)) {\n this._busySpinner?.hideOverlay();\n\n return;\n }\n\n if (this._busySpinner === null) {\n this._busySpinner = new ProgressSpinner(BUSY_SPINNER_DIAMETER);\n }\n\n // `showOverlay` is a no-op once shown, so the explicit `doLayout` is what\n // re-sizes a showing overlay after a viewport resize: the spinner is\n // mounted by a raw DOM append, so it is not in this view's laid-out set\n // and nothing else ever lays it out.\n this._busySpinner.showOverlay(this);\n this._busySpinner.doLayout();\n }\n\n /**\n * Resolves once the layout pass currently in flight has finished placing\n * nodes. Resolves immediately when no pass is in flight, and resolves\n * (never rejects) when a pass fails or the view is disposed mid-pass —\n * a rejecting promise would force every caller into a `try`/`catch`\n * whose only sensible branch is \"carry on anyway\".\n *\n * @returns A promise settling on the next finished layout pass.\n */\n whenLaidOut(): Promise<void> {\n return this._layoutSettled?.promise ?? Promise.resolve();\n }\n\n /**\n * Resolves each node's size fed to ELK: the explicit `width`/`height` from\n * the model when present, else the node component's preferred size — read\n * before the component is mounted, which is safe because measurement goes\n * through the DOM seam's font metrics rather than the live document (a\n * custom `nodeRenderer` whose preferred size needs a live element in the\n * document is not supported). Recurses into `children` so every container\n * and leaf is represented; a container's entry is harmless-but-unused\n * since `buildElkGraph` computes a container's box from its contents\n * rather than consulting this map.\n *\n * @param data - The graph whose node sizes to collect.\n * @returns A map of node id to resolved size.\n */\n private collectNodeSizes(data: DiagramData): Map<string, { width: number; height: number }> {\n const sizes = new Map<string, { width: number; height: number }>();\n\n const collect = (nodes: DiagramNodeData[]): void => {\n for (const node of nodes) {\n const component = this._incomingComponents.get(node.id);\n const preferred = component?.getPreferredSize();\n\n sizes.set(node.id, {\n width: node.width ?? preferred?.width ?? 0,\n height: node.height ?? preferred?.height ?? 0,\n });\n\n if (node.children && node.children.length > 0) {\n collect(node.children);\n }\n }\n };\n\n collect(data.nodes);\n\n return sizes;\n }\n\n /**\n * Applies a completed layout: positions each incoming node, promotes the\n * incoming set to shown (revealing it), sizes + scales the content host,\n * redraws the edges, and emits `\"layout\"`. A stale result (superseded by\n * a newer `setData`) is ignored.\n *\n * @param result - The ELK layout result.\n * @param generation - The generation token captured when the layout started.\n */\n private applyLayout(result: DiagramLayoutResult, generation: number): void {\n if (generation !== this._layoutGeneration) {\n return;\n }\n\n for (const node of result.nodes) {\n const component = this._incomingComponents.get(node.id);\n\n if (component) {\n component.setPreferredSize({ width: node.width, height: node.height });\n component.setX(node.x);\n component.setY(node.y);\n }\n }\n\n this._graphWidth = result.width;\n this._graphHeight = result.height;\n\n this.promoteIncomingNodes();\n\n this._edgeLayer.setX(0);\n this._edgeLayer.setY(0);\n this._edgeLayer.setPreferredSize({ width: result.width, height: result.height });\n this._edgeLayer.setEdges(this.joinEdgeStyles(result.edges));\n\n this.applyContainerZIndex();\n\n // The host is no longer resized per zoom (see applyTransformToHost) — its\n // box always matches the unscaled graph bounds the nodes are laid out at.\n this._contentHost.setPreferredSize({ width: result.width, height: result.height });\n this.applyTransformToHost();\n\n // Before `emit`, so a consumer's own `\"layout\"` listener (the sanctioned\n // auto-fit hook, `view.on(\"layout\", () => view.zoomToFit())`) still runs\n // afterwards and wins. Only succeeds if the view is already sized; the\n // `doLayout` override retries otherwise.\n this.tryInitialCentre();\n\n this.scheduleLayout();\n\n this.emit(\"layout\");\n\n this.settleLayout();\n }\n\n /**\n * Applies the compound paint order — containers behind the edge layer,\n * leaves in front of it — when `rebuildNodes` built at least one\n * container. A flat graph's own (freshly-built) node components are never\n * touched here — they start at `DEFAULT_Z_INDEX` — but the persistent edge\n * layer is explicitly restored to `DEFAULT_Z_INDEX`, since an earlier\n * compound `setData` on this same view could have left it elevated (see\n * the module-level z-index constants).\n */\n private applyContainerZIndex(): void {\n if (this._containerIds.size === 0) {\n this._edgeLayer.setZIndex(DEFAULT_Z_INDEX);\n\n return;\n }\n\n this._edgeLayer.setZIndex(EDGE_LAYER_Z_INDEX);\n\n for (const [id, component] of this._nodeComponents) {\n component.setZIndex(this._containerIds.has(id) ? CONTAINER_Z_INDEX : LEAF_Z_INDEX);\n }\n }\n\n /**\n * Re-attaches each model edge's `style` to its layout-routed counterpart.\n * ELK's result carries only `{ id, sections }` — `style` never survives the\n * round trip — so this joins by edge id back to `this._options.data.edges`,\n * the model the layout was computed from.\n *\n * @param edges - The ELK-routed edges (id + sections only).\n * @returns The same routes, each carrying its model edge's `style` if any.\n */\n private joinEdgeStyles(edges: DiagramEdgeRoute[]): DiagramEdgeRoute[] {\n const modelById = new Map(this._options.data?.edges.map(e => [e.id, e]) ?? []);\n\n return edges.map(edge => ({ ...edge, style: modelById.get(edge.id)?.style }));\n }\n\n /**\n * Discards the just-built incoming nodes when a layout fails (e.g. `elkjs`\n * absent), so a first, never-shown graph leaves the view empty, and a\n * failed *re*-layout leaves whatever graph was already on screen — rather\n * than showing stacked, unpositioned nodes either way.\n *\n * @param generation - The generation token captured when the layout started.\n */\n private handleLayoutFailure(generation: number): void {\n if (generation !== this._layoutGeneration) {\n return;\n }\n\n this.discardIncomingNodes();\n this.settleLayout();\n }\n\n /**\n * Writes the content host's `translate(panX,panY) scale(zoom)` transform\n * from the current pan offset and zoom factor. The host's box is set once\n * per layout (see `applyLayout`) to the unscaled graph bounds and is not\n * touched here — pan and zoom live entirely on the transform.\n */\n private applyTransformToHost(): void {\n const zoom = this.getZoom();\n\n this._contentHost.setTransform(`translate(${this._panX}px, ${this._panY}px) scale(${zoom})`);\n }\n\n /**\n * Returns the current zoom factor.\n *\n * @returns The zoom factor.\n */\n getZoom(): number {\n return this._options.zoom ?? this._defaultOptions.zoom ?? DEFAULT_ZOOM;\n }\n\n /**\n * Sets the zoom factor, clamped to `[minZoom, maxZoom]` (the lower bound\n * adaptively lowered so a huge graph can still reach its fit zoom — see\n * `effectiveMinZoom`), and re-applies the content host's transform. A\n * non-finite request (e.g. `zoomToFit`'s `graphWidth / 0` on an unsized\n * view) is rejected outright rather than clamped, since `Math.max`/`min`\n * propagate `NaN` instead of resolving it — this is the one guard every\n * zoom-changing entry point (`zoomToFit`, `resetView`, `zoomIn`/`zoomOut`)\n * relies on rather than each re-checking its own inputs.\n *\n * @param zoom - The desired zoom factor.\n *\n * @returns This view, for method chaining.\n */\n setZoom(zoom: number): this {\n if (!Number.isFinite(zoom)) {\n return this;\n }\n\n this._options.zoom = this.clampZoom(zoom);\n this.applyTransformToHost();\n\n return this;\n }\n\n /**\n * Fits the whole graph into the viewport by choosing the largest zoom at\n * which the graph bounds fit both axes, then centres the graph. Requires a\n * completed layout.\n *\n * @returns This view, for method chaining.\n */\n zoomToFit(): this {\n if (this._graphWidth <= 0 || this._graphHeight <= 0) {\n return this;\n }\n\n const zoomX = this.getWidth() / this._graphWidth;\n const zoomY = this.getHeight() / this._graphHeight;\n\n this.setZoom(Math.min(zoomX, zoomY));\n this.centreGraph();\n\n return this;\n }\n\n /**\n * Resets to the default zoom, then re-centres: on the focus node when the\n * view has one that is in the shown graph, else on the graph bounds. The\n * focus node is `initialFocusNode`, or the target of the most recent\n * `focusNode` call. Centring a node also lowers the zoom, if needed, until\n * the node's whole box fits. Retried after the next layout pass when the\n * view has no committed size yet.\n *\n * @returns This view, for method chaining.\n */\n resetView(): this {\n this.setZoom(this._defaultOptions.zoom ?? DEFAULT_ZOOM);\n\n this._needsInitialCentre = true;\n this.tryInitialCentre();\n\n return this;\n }\n\n /**\n * Steps the zoom up by the configured multiplicative factor, keeping the\n * graph point currently at the viewport centre fixed.\n *\n * @returns This view, for method chaining.\n */\n zoomIn(): this {\n this.zoomAboutViewportPoint(ZOOM_BUTTON_STEP, this.getWidth() / 2, this.getHeight() / 2);\n\n return this;\n }\n\n /**\n * Steps the zoom down by the configured multiplicative factor, keeping the\n * graph point currently at the viewport centre fixed.\n *\n * @returns This view, for method chaining.\n */\n zoomOut(): this {\n this.zoomAboutViewportPoint(1 / ZOOM_BUTTON_STEP, this.getWidth() / 2, this.getHeight() / 2);\n\n return this;\n }\n\n /**\n * Attempts the one-time initial centring, so the first render shows the\n * graph where `resetView` would put it rather than at the viewport's\n * top-left corner. The configured `zoom` deliberately stands (unlike\n * `resetView`, which also restores the default zoom), so a consumer's\n * explicit `zoom` option still decides the initial scale — except that a\n * focus node too large to fit the viewport lowers it until the node fits.\n *\n * Both inputs arrive asynchronously and in either order: the graph bounds\n * come from an ELK layout, the viewport size from the host's layout pass.\n * So this is *retried* — from `applyLayout`, from every `doLayout`, and\n * from `resetView` — until it actually succeeds, and the pending flag is\n * cleared only on a confirmed centring. Clearing it on an attempt that\n * silently no-opped (the view not yet sized, `centreGraph` returning\n * `false`) is what used to leave the diagram stuck in the corner whenever\n * the layout landed first.\n *\n * Picks between two targets: `_focusNodeId`, when it names a node in the\n * graph just promoted, or the graph's bounds otherwise — the same\n * generalisation `focusNode` reuses at runtime instead of a separate\n * one-shot mechanism.\n */\n private tryInitialCentre(): void {\n if (!this._needsInitialCentre || !(this._graphWidth > 0) || !(this._graphHeight > 0)) {\n return;\n }\n\n const focus = this._focusNodeId !== null && this._nodeComponents.has(this._focusNodeId)\n ? this._focusNodeId\n : null;\n\n if (focus !== null ? this.centreNode(focus) : this.centreGraph()) {\n this._needsInitialCentre = false;\n }\n }\n\n /**\n * Centres the graph bounds in the viewport at the current zoom, without\n * changing the zoom factor. Shared by `zoomToFit`, `resetView`, and the\n * initial centring. Declines before the view is sized —\n * `getWidth()`/`getHeight()` are `NaN` (not 0) until then (see\n * `effectiveMinZoom`), and writing a `NaN` pan would blank the diagram\n * until another call recomputes it.\n *\n * @returns `true` when the pan was written, `false` when the view has no\n * committed size yet and the caller should try again later.\n */\n private centreGraph(): boolean {\n const vw = this.getWidth();\n const vh = this.getHeight();\n\n if (!(vw > 0) || !(vh > 0)) {\n return false;\n }\n\n const zoom = this.getZoom();\n\n this._panX = (vw - this._graphWidth * zoom) / 2;\n this._panY = (vh - this._graphHeight * zoom) / 2;\n\n this.applyTransformToHost();\n\n return true;\n }\n\n /**\n * Scales by `factor` about a fixed viewport point: the graph point under\n * `(vx, vy)` maps to the same viewport point before and after the zoom.\n * Shared by the zoom in/out control buttons and (about the pointer instead\n * of the viewport centre) by wheel-zoom. A no-op when `vx`/`vy` is not\n * finite — `zoomIn`/`zoomOut` pass the viewport centre, which is `NaN`\n * before the view is sized (see `centreGraph`).\n *\n * @param factor - The multiplicative zoom change (`> 1` zooms in).\n * @param vx - The viewport-relative x coordinate to keep fixed.\n * @param vy - The viewport-relative y coordinate to keep fixed.\n */\n private zoomAboutViewportPoint(factor: number, vx: number, vy: number): void {\n if (!Number.isFinite(vx) || !Number.isFinite(vy)) {\n return;\n }\n\n const oldZoom = this.getZoom();\n const newZoom = this.clampZoom(oldZoom * factor);\n\n if (newZoom === oldZoom) {\n return;\n }\n\n const graphX = (vx - this._panX) / oldZoom;\n const graphY = (vy - this._panY) / oldZoom;\n\n this._panX = vx - graphX * newZoom;\n this._panY = vy - graphY * newZoom;\n\n this.setZoom(newZoom);\n }\n\n /**\n * Resolves the effective minimum zoom: the configured `minZoom`, floored\n * further down to the zoom that fits the whole graph in the current\n * viewport when the graph is too large for that configured floor to reach.\n * A small graph is unaffected (its fit zoom already exceeds the configured\n * minimum); an unsized or unlaid-out view falls back to the configured\n * minimum untouched.\n *\n * @returns The effective minimum zoom factor.\n */\n private effectiveMinZoom(): number {\n const configuredMin = this._options.minZoom ?? this._defaultOptions.minZoom ?? DEFAULT_MIN_ZOOM;\n const vw = this.getWidth();\n const vh = this.getHeight();\n\n // `getWidth()` / `getHeight()` are `NaN` (not 0) before the first\n // `setSize` — `> 0` (rather than `<= 0`) also rejects NaN, since every\n // NaN comparison is false.\n if (!(this._graphWidth > 0) || !(this._graphHeight > 0) || !(vw > 0) || !(vh > 0)) {\n return configuredMin;\n }\n\n const fitZoom = Math.min(vw / this._graphWidth, vh / this._graphHeight);\n\n return Math.min(configuredMin, fitZoom);\n }\n\n /**\n * Clamps a zoom request to `[effectiveMinZoom(), maxZoom]`.\n *\n * @param zoom - The requested zoom factor.\n * @returns The clamped zoom factor.\n */\n private clampZoom(zoom: number): number {\n const min = this.effectiveMinZoom();\n const max = this._options.maxZoom ?? this._defaultOptions.maxZoom ?? DEFAULT_MAX_ZOOM;\n\n return Math.max(min, Math.min(max, zoom));\n }\n\n /**\n * Resolves the zoom that fits `size` whole in the viewport, never raising\n * the current zoom — only lowering it when the size does not already fit.\n * Shared by every node-centring entry point (`centreNode`, and so\n * `revealNode`, `focusNode`, and the focus-node branch of\n * `tryInitialCentre`) so a card larger than the viewport is never\n * centred-and-clipped.\n *\n * @param size - The node box to fit, in unscaled graph coordinates.\n * @returns The clamped zoom to use for the centring.\n */\n private zoomFittingNode(size: { width: number; height: number }): number {\n const current = this.getZoom();\n const vw = this.getWidth();\n const vh = this.getHeight();\n\n // `getWidth()` / `getHeight()` are `NaN` before the first `setSize`, and a\n // zero-sized node box has no fit zoom — `> 0` rejects both, since every\n // NaN comparison is false (see `effectiveMinZoom`).\n if (!(vw > 0) || !(vh > 0) || !(size.width > 0) || !(size.height > 0)) {\n return this.clampZoom(current);\n }\n\n const fitZoom = Math.min(vw / size.width, vh / size.height);\n\n return this.clampZoom(Math.min(current, fitZoom));\n }\n\n /**\n * Returns the current selection (single-select).\n *\n * @returns A copy of the selected node data array.\n */\n getSelection(): DiagramNodeData[] {\n return [...this._selection];\n }\n\n /**\n * Selects a node programmatically without emitting `\"selection\"` (mirroring\n * the Tree precedent). Passing `null` clears the selection.\n *\n * @param id - The node id to select, or `null` to clear.\n *\n * @returns This view, for method chaining.\n */\n selectNode(id: string | null): this {\n this.setSelection(id);\n\n return this;\n }\n\n /**\n * Emphasises a subset of the drawn edges: every edge outside the set\n * recedes to a lower opacity while the named ones keep their normal\n * weight. Cleared by `null`, by an empty array, and by the next layout\n * that replaces the drawn edges. Emits nothing. Forwards straight to the\n * edge layer, which owns the emphasis state — this view holds no copy of\n * its own.\n *\n * @param ids - The edge ids to emphasise, or null to clear.\n *\n * @returns This view, for method chaining.\n */\n setEdgeEmphasis(ids: readonly string[] | null): this {\n this._edgeLayer.setEdgeEmphasis(ids);\n\n return this;\n }\n\n /**\n * The currently emphasised edge ids.\n *\n * @returns A copy of the emphasised id array; empty when nothing is emphasised.\n */\n getEdgeEmphasis(): string[] {\n return this._edgeLayer.getEdgeEmphasis();\n }\n\n /**\n * Emphasises a subset of the drawn nodes: every node component outside\n * the set dims to a reduced opacity while the named ones keep full\n * opacity. Cleared by `null`, by an empty array, and by the next layout\n * that rebuilds the node components (`promoteIncomingNodes`). Emits\n * nothing. Dims through `Component.setOpacity` / `clearOpacity` directly\n * on each node's own root, so a custom `nodeRenderer` needs no\n * cooperation — unlike `applySelectedVisual`, which duck-types a\n * `setSelected?.()` because \"selected\" has no single generic rendering.\n * An unknown id is kept in the set (and reported back by\n * `getNodeEmphasis`) but dims nothing, since no node component answers to\n * it.\n *\n * @param ids - The node ids to emphasise, or null to clear.\n *\n * @returns This view, for method chaining.\n */\n setNodeEmphasis(ids: readonly string[] | null): this {\n this._nodeEmphasis = new Set(ids ?? []);\n this.applyNodeEmphasis();\n\n return this;\n }\n\n /**\n * The currently emphasised node ids.\n *\n * @returns A copy of the emphasised id array; empty when nothing is emphasised.\n */\n getNodeEmphasis(): string[] {\n return [...this._nodeEmphasis];\n }\n\n /** Rewrites every node component's opacity from the current emphasis set. */\n private applyNodeEmphasis(): void {\n for (const [id, component] of this._nodeComponents) {\n if (this._nodeEmphasis.size === 0 || this._nodeEmphasis.has(id)) {\n component.clearOpacity();\n } else {\n component.setOpacity(DIMMED_NODE_OPACITY);\n }\n }\n }\n\n /**\n * Pans so the given node is centred in the viewport, without changing the\n * selection or emitting any event. Also lowers the zoom, if needed, until\n * the node's whole box fits the viewport — never raises it. No-op for an\n * unknown id, before the first layout has positioned the node, or before\n * the view has a real committed size — `getWidth()`/`getHeight()` are\n * `NaN` (not `0`) until then (see `effectiveMinZoom`). Unlike a\n * non-finite zoom request, which `setZoom` rejects outright, a `NaN` pan\n * has no such gate here, so it is guarded directly: a `NaN` pan is\n * sticky, silently blanking the diagram until another call recomputes\n * it. Pair with {@link selectNode} to both highlight and reveal. Unlike\n * {@link focusNode}, this does not retry — a no-op call must be repeated\n * by the caller once the view is sized.\n *\n * @param id - The node id to centre, or a no-op when not found.\n *\n * @returns This view, for method chaining.\n */\n revealNode(id: string): this {\n this.centreNode(id);\n\n return this;\n }\n\n /**\n * Centres a node in the viewport by writing the pan, without changing the\n * selection or emitting any event. Also lowers the zoom, via\n * `zoomFittingNode`, when the node's whole box does not already fit the\n * viewport — never raises it.\n *\n * @param id - The node id to centre.\n *\n * @returns `true` when the pan and zoom were written, `false` when the\n * node is unknown or the view has no committed size yet.\n */\n private centreNode(id: string): boolean {\n const component = this._nodeComponents.get(id);\n const size = component?.getPreferredSize();\n\n if (!component || !size) {\n return false;\n }\n\n const zoom = this.zoomFittingNode(size);\n\n // Node centre in unscaled graph coordinates.\n const centreX = component.getX() + size.width / 2;\n const centreY = component.getY() + size.height / 2;\n\n // Pan so the node centre maps to the viewport centre: viewport = pan + graph·zoom.\n const panX = this.getWidth() / 2 - centreX * zoom;\n const panY = this.getHeight() / 2 - centreY * zoom;\n\n if (!Number.isFinite(panX) || !Number.isFinite(panY)) {\n return false;\n }\n\n this._panX = panX;\n this._panY = panY;\n\n // Writes the pan fields first, then lets `setZoom` apply the transform\n // once — the shape `zoomAboutViewportPoint` already uses.\n this.setZoom(zoom);\n\n return true;\n }\n\n /**\n * Centres the given node in the viewport, retrying after each layout pass\n * until it succeeds — unlike `revealNode`, which centres only when the\n * graph and the viewport are both already measured. Also lowers the\n * zoom, if needed, until the node's whole box fits the viewport; never\n * raises it.\n *\n * @param id - The node id to centre on.\n *\n * @returns This view, for method chaining.\n */\n focusNode(id: string): this {\n this._focusNodeId = id;\n this._needsInitialCentre = true;\n\n this.tryInitialCentre();\n\n return this;\n }\n\n /**\n * Updates the selection state and toggles each node's selected visual.\n *\n * @param id - The node id to select, or `null` to clear.\n */\n private setSelection(id: string | null): void {\n for (const component of this._nodeComponents.values()) {\n this.applySelectedVisual(component, false);\n }\n\n const data = id === null ? undefined : this._nodeData.get(id);\n\n if (data) {\n this._selection = [data];\n this.applySelectedVisual(this._nodeComponents.get(id!)!, true);\n } else {\n this._selection = [];\n }\n }\n\n /**\n * Toggles a node component's selected visual when it supports one (the\n * default `DiagramNode` does; a custom renderer may not).\n *\n * @param component - The node component.\n * @param selected - The selected state to apply.\n */\n private applySelectedVisual(component: Component, selected: boolean): void {\n const node = component as unknown as { setSelected?: (value: boolean) => void };\n\n node.setSelected?.(selected);\n }\n\n /**\n * Registers a listener for a diagram event.\n *\n * @param event - `\"selection\"` fires when the selected node changes;\n * `\"activate\"` fires when a node is double-clicked; `\"layout\"` fires\n * after each successful ELK layout pass; `\"contextmenu\"` fires when a\n * node is right-clicked; `\"edgehover\"` fires with every model edge\n * within the pointer's hit tolerance (several where routes overlap) and\n * the originating event; `\"edgeleave\"` fires when the pointer leaves\n * whatever edge(s) it was hovering.\n * @param listener - The callback to invoke.\n *\n * @returns This view, for method chaining.\n */\n on(event: \"selection\", listener: (nodes: DiagramNodeData[]) => void): this;\n on(event: \"activate\", listener: (node: DiagramNodeData) => void): this;\n on(event: \"layout\", listener: () => void): this;\n on(event: \"contextmenu\", listener: (node: DiagramNodeData, event: MouseEvent) => void): this;\n on(event: \"edgehover\", listener: (edges: DiagramEdgeData[], event: MouseEvent) => void): this;\n on(event: \"edgeleave\", listener: () => void): this;\n on(event: DiagramViewEvent, listener: Function): this {\n this._listeners.add(event, listener);\n\n return this;\n }\n\n /**\n * Removes a previously registered listener; the exact reference must match.\n *\n * @param event - The event the listener was registered for.\n * @param listener - The callback to remove.\n *\n * @returns This view, for method chaining.\n */\n off(event: DiagramViewEvent, listener: Function): this {\n this._listeners.remove(event, listener);\n\n return this;\n }\n\n /**\n * Fires every listener registered for `event`, in registration order.\n *\n * @param event - The event to emit.\n * @param nodes - The selected node data (for `\"selection\"`), or the\n * activated / right-clicked node data (for `\"activate\"` / `\"contextmenu\"`).\n */\n protected emit(event: \"selection\", nodes: DiagramNodeData[]): void;\n protected emit(event: \"activate\", node: DiagramNodeData): void;\n protected emit(event: \"layout\"): void;\n protected emit(event: \"contextmenu\", node: DiagramNodeData, mouseEvent: MouseEvent): void;\n protected emit(event: \"edgehover\", edges: DiagramEdgeData[], mouseEvent: MouseEvent): void;\n protected emit(event: \"edgeleave\"): void;\n protected emit(event: DiagramViewEvent, ...payload: unknown[]): void {\n this._listeners.fire(event, ...payload);\n }\n\n /**\n * Lays the viewport out, then retries the pending initial centring. This is\n * where the viewport size becomes known, and an ELK layout that landed\n * before the host sized this view has nothing to centre against until now\n * (see `tryInitialCentre`). Also re-syncs the busy overlay, which is where a\n * view that had no size when its layout pass started finally picks the\n * overlay up. Writes only the content host's transform and the (unmanaged)\n * overlay, never a child's rect, so neither can feed back into the layout it\n * runs inside.\n *\n * @returns This view, for method chaining.\n */\n doLayout(): this {\n super.doLayout();\n\n this.tryInitialCentre();\n this.anchorCentreAcrossResize();\n this.syncBusyIndicator();\n\n return this;\n }\n\n /**\n * Holds the graph point under the viewport centre still across a viewport\n * resize, so whatever the user was looking at stays in the middle instead\n * of drifting toward a corner as the window grows or shrinks.\n *\n * With `viewport = pan + graph·zoom`, keeping the centre's graph point\n * fixed means `pan += (newExtent − oldExtent) / 2` on each axis — the zoom\n * cancels out, so this is correct at any zoom and never changes it. The\n * first sizing has no previous centre to preserve, so it only records the\n * extent (the initial centring owns that pass).\n */\n private anchorCentreAcrossResize(): void {\n const vw = this.getWidth();\n const vh = this.getHeight();\n\n if (!(vw > 0) || !(vh > 0)) {\n return;\n }\n\n const previousWidth = this._lastViewportWidth;\n const previousHeight = this._lastViewportHeight;\n\n this._lastViewportWidth = vw;\n this._lastViewportHeight = vh;\n\n if (!(previousWidth > 0) || !(previousHeight > 0)) {\n return;\n }\n\n if (previousWidth === vw && previousHeight === vh) {\n return;\n }\n\n this._panX += (vw - previousWidth) / 2;\n this._panY += (vh - previousHeight) / 2;\n\n this.applyTransformToHost();\n }\n\n /**\n * Wires the DOM-routed pan / zoom / selection listeners once the element\n * exists.\n *\n * @param element - Optional element from the render pipeline.\n *\n * @returns This view, for method chaining.\n */\n protected init(element?: Handle): this {\n super.init(element);\n\n // All seven use the SUBTREE variant: the content (nodes, the SVG edge\n // layer, and the Panel's own overlay-scroll element) are descendants of\n // this view's root, so a real wheel/pointer event's target is never the\n // root itself. An exact-target `addListener` would therefore never fire\n // for pan/zoom — only `addSubtreeListener` sees the descendant events\n // (mirrors click/dblclick, which is why selection worked but pan did not).\n Event.addSubtreeListener(this, \"click\", this._handleClick);\n Event.addSubtreeListener(this, \"dblclick\", this._handleDoubleClick);\n Event.addSubtreeListener(this, \"contextmenu\", this._handleContextMenu);\n // Non-passive: `_handleWheel` calls `preventDefault()` to suppress the\n // page's native scroll/zoom, which a passive listener silently ignores\n // (mirrors `Component.attachWheelScrolling` / `WheelTrap`).\n Event.addSubtreeListener(this, \"wheel\", this._handleWheel, { passive: false });\n Event.addSubtreeListener(this, \"pointerdown\", this._handlePointerDown);\n Event.addSubtreeListener(this, \"pointermove\", this._handlePointerMove);\n Event.addSubtreeListener(this, \"pointerup\", this._handlePointerUp);\n // `mousemove`/`mouseout` rather than `mouseenter`/`mouseleave`: per\n // ARCHITECTURE.md, the non-bubbling enter/leave pair never reaches the\n // framework's window-level capture handler.\n Event.addSubtreeListener(this, \"mousemove\", this._handleEdgeMouseMove);\n Event.addSubtreeListener(this, \"mouseout\", this._handleEdgeMouseOut);\n\n return this;\n }\n\n /**\n * Resolves the node under a click and updates the selection, emitting\n * `\"selection\"` when it changes. A click on empty space clears the\n * selection.\n *\n * @param event - The click event whose target is inside the view's subtree.\n */\n private _handleClick(event: MouseEvent): void {\n // A drag is not a click: a pan that starts and ends on empty canvas\n // (or starts on a node and ends on empty canvas) still fires a native\n // \"click\" on their nearest common ancestor, and it must not clear the\n // selection or the edge emphasis the user was studying.\n if (this._pointerMoved) {\n return;\n }\n\n if (this.isControlsTarget(event.target)) {\n return;\n }\n\n // A press on an edge is neither a node click nor a canvas click: it\n // must not clear the selection the user is looking at.\n if (this._edgeLayer.edgeIdAt(event.target) !== null) {\n return;\n }\n\n const id = this.nodeIdAt(event.target);\n\n if (id !== null) {\n if (id === (this._selection[0]?.id ?? null)) {\n return;\n }\n\n this.setSelection(id);\n this.emit(\"selection\", this.getSelection());\n } else if (this._selection.length > 0) {\n this.setSelection(null);\n this.emit(\"selection\", this.getSelection());\n }\n }\n\n /**\n * Resolves the node under a double-click and emits `\"activate\"` with its\n * data. A double-click is preceded by the single click that already\n * selected the node, so this only signals activation. A double-click on\n * empty space resolves to no node and emits nothing.\n *\n * @param event - The dblclick event whose target is inside the view's\n * subtree.\n */\n private _handleDoubleClick(event: MouseEvent): void {\n const id = this.nodeIdAt(event.target);\n\n if (id === null) {\n return;\n }\n\n const data = this._nodeData.get(id);\n\n if (data !== undefined) {\n this.emit(\"activate\", data);\n }\n }\n\n /**\n * Resolves the node id whose component owns the given event target.\n *\n * @param target - The raw DOM event target.\n * @returns The owning node id, or `null` when the target is not on a node.\n */\n private nodeIdAt(target: EventTarget | null): string | null {\n if (target === null) {\n return null;\n }\n\n const handle = DOM.source.intern(target);\n\n for (const [id, component] of this._nodeComponents) {\n const element = component.getElement();\n\n if (element && (element === handle || DOM.source.contains(element, handle))) {\n return id;\n }\n }\n\n return null;\n }\n\n /**\n * Reports hover over the diagram's edges: resolves the pointer's graph\n * coordinate (inverting the pan/zoom transform `zoomAboutViewportPoint`\n * applies) and asks the edge layer which routes are within hit tolerance\n * there, emitting `\"edgehover\"` with the joined model edges whenever the\n * reported set changes. A drag in progress is not a hover (`_panning`), and\n * a move whose target is not an edge hit path — or whose resolved point\n * lands on no route — instead ends any hover in progress.\n *\n * @param event - The mousemove event, dispatched to the whole subtree.\n */\n private _handleEdgeMouseMove(event: MouseEvent): void {\n if (this._panning) {\n return;\n }\n\n if (this._edgeLayer.edgeIdAt(event.target) === null) {\n this.leaveEdges();\n\n return;\n }\n\n const rect = DOM.source.getViewportRect(this);\n const zoom = this.getZoom();\n const gx = (event.clientX - rect.left - this._panX) / zoom;\n const gy = (event.clientY - rect.top - this._panY) / zoom;\n\n const routes = this._edgeLayer.edgesNear(gx, gy);\n\n if (routes.length === 0) {\n this.leaveEdges();\n\n return;\n }\n\n const key = routes.map(r => r.id).join(\" \");\n\n if (key === this._hoveredEdgeKey) {\n return;\n }\n\n const modelById = new Map(this._options.data?.edges.map(e => [e.id, e]) ?? []);\n const edges = routes.map(r => modelById.get(r.id)).filter((e): e is DiagramEdgeData => e !== undefined);\n\n this._hoveredEdgeKey = key;\n this.emit(\"edgehover\", edges, event);\n }\n\n /**\n * Ends an edge hover when the pointer's `mouseout` leaves an edge hit\n * path. A `mouseout` from anywhere else is not a hover boundary and is\n * ignored — `_handleEdgeMouseMove`'s own \"target is not an edge\" branch\n * already covers a move that lands on empty canvas or a node.\n *\n * @param event - The mouseout event, dispatched to the whole subtree.\n */\n private _handleEdgeMouseOut(event: MouseEvent): void {\n if (this._edgeLayer.edgeIdAt(event.target) !== null) {\n this.leaveEdges();\n }\n }\n\n /** Emits `\"edgeleave\"` and clears the hover key, unless already idle. */\n private leaveEdges(): void {\n if (this._hoveredEdgeKey === null) {\n return;\n }\n\n this._hoveredEdgeKey = null;\n this.emit(\"edgeleave\");\n }\n\n /**\n * Resolves the node under a right-click and emits `\"contextmenu\"` with its\n * data, mirroring `Tree`'s contextmenu handling: a node hit suppresses the\n * browser's native menu via `preventDefault` and emits; a right-click on\n * empty canvas is left to the browser.\n *\n * @param event - The contextmenu event whose target is inside the view's\n * subtree.\n */\n private _handleContextMenu(event: MouseEvent): void {\n const id = this.nodeIdAt(event.target);\n\n if (id === null) {\n return;\n }\n\n const data = this._nodeData.get(id);\n\n if (data !== undefined) {\n event.preventDefault();\n this.emit(\"contextmenu\", data, event);\n }\n }\n\n /**\n * Whether the given raw DOM event target lands inside the control cluster\n * — used to keep the cluster's own clicks/drags from also being\n * interpreted as a canvas click (clearing selection) or the start of a pan.\n *\n * @param target - The raw DOM event target.\n * @returns `true` when the target is the cluster or one of its descendants.\n */\n private isControlsTarget(target: EventTarget | null): boolean {\n if (target === null || this._controls === undefined) {\n return false;\n }\n\n const el = this._controls.getElement();\n const handle = DOM.source.intern(target);\n\n return el !== undefined && (el === handle || DOM.source.contains(el, handle));\n }\n\n /**\n * Wheel-zoom about the pointer: scales toward/away, keeping the graph\n * point under the cursor fixed in the viewport.\n *\n * @param event - The wheel event.\n */\n private _handleWheel(event: WheelEvent): void {\n event.preventDefault();\n\n const rect = DOM.source.getViewportRect(this);\n\n this.zoomAboutViewportPoint(event.deltaY < 0 ? WHEEL_ZOOM_STEP : 1 / WHEEL_ZOOM_STEP,\n event.clientX - rect.left, event.clientY - rect.top);\n }\n\n /**\n * Begins a pan drag on a primary-button press over the canvas — empty\n * space or an edge alike; only a node or the control cluster refuses —\n * recording the start pointer and pan offset, and switches the cursor to\n * `\"grabbing\"`. Also arms the click-versus-drag guard `_handleClick`\n * reads, regardless of whether this press ends up panning.\n *\n * @param event - The pointerdown event.\n */\n private _handlePointerDown(event: PointerEvent): void {\n // Recorded above every guard below, so a press that does not pan (a\n // node, the control cluster) still arms the click-versus-drag guard\n // `_handleClick` reads.\n this._pressX = event.clientX;\n this._pressY = event.clientY;\n this._pointerMoved = false;\n\n // A press on a node (leaf or container) or the control cluster is not\n // a pan: both show `pointer`, and the cursor has to promise what the\n // drag will actually do. Everything else — empty canvas and edges\n // alike — pans, since dragging an edge is this component's way of\n // panning from under the graph's own lines.\n if (event.button !== 0 || this.isControlsTarget(event.target) || this.nodeIdAt(event.target) !== null) {\n return;\n }\n\n this._panning = true;\n this._panStartX = event.clientX;\n this._panStartY = event.clientY;\n this._panOriginX = this._panX;\n this._panOriginY = this._panY;\n\n this.setCursor(\"grabbing\");\n }\n\n /**\n * Pans the content host during a drag by writing the pan offset from the\n * pointer delta — unbounded, so the graph can be dragged into empty space\n * in any direction.\n *\n * @param event - The pointermove event.\n */\n private _handlePointerMove(event: PointerEvent): void {\n // Gated on the primary button, like DragManager.onMouseMove gates on\n // an open session (DragManager.ts:492): this handler fires on every\n // pointer move over the subtree, including ordinary ambient hover\n // with no button held, and `_pressX`/`_pressY` default to `(0, 0)`\n // until the first real `pointerdown` — without this gate, the very\n // first hover of a session would latch `_pointerMoved` permanently.\n if (!this._pointerMoved && (event.buttons & 1) !== 0) {\n const dx = event.clientX - this._pressX;\n const dy = event.clientY - this._pressY;\n\n // Squared-distance comparison avoids a Math.hypot call, matching\n // DragManager's own threshold test.\n this._pointerMoved = dx * dx + dy * dy >= CLICK_SLOP * CLICK_SLOP;\n }\n\n if (!this._panning) {\n return;\n }\n\n // The primary button is no longer held — the pointer was released\n // outside this view's subtree, so its `pointerup` never reached us. End\n // the pan here rather than keep dragging on a button-up move.\n if ((event.buttons & 1) === 0) {\n this._panning = false;\n this.setCursor(\"grab\");\n\n return;\n }\n\n this._panX = this._panOriginX + (event.clientX - this._panStartX);\n this._panY = this._panOriginY + (event.clientY - this._panStartY);\n\n this.applyTransformToHost();\n }\n\n /**\n * Ends a pan drag and restores the `\"grab\"` cursor.\n */\n private _handlePointerUp(): void {\n this._panning = false;\n this.setCursor(\"grab\");\n }\n\n /** Builds the corner-pinned zoom / fit / reset control cluster. */\n private buildControls(): void {\n this._zoomInBtn = this.makeControlButton(\"plus\", \"Zoom in\");\n this._zoomOutBtn = this.makeControlButton(\"minus\", \"Zoom out\");\n this._fitBtn = this.makeControlButton(\"expand\", \"Fit to view\");\n this._resetBtn = this.makeControlButton(\"crosshairs\", \"Reset view\");\n\n this._controls = new FloatingPanel({ corner: \"bottom-right\", margin: CONTROLS_MARGIN, layoutManager: new VBox() });\n this._controls.addComponent(this._zoomInBtn);\n this._controls.addComponent(this._zoomOutBtn);\n this._controls.addComponent(this._fitBtn);\n this._controls.addComponent(this._resetBtn);\n }\n\n /**\n * Builds a glyph-only control button with an accessible label, mirroring\n * `VideoPlayer`'s control-bar buttons.\n *\n * @param glyph - The glyph name to show.\n * @param label - The accessible name (drives `aria-label` and the tooltip).\n * @returns The configured button.\n */\n private makeControlButton(glyph: string, label: string): Button {\n return new Button({ glyph, text: label, showText: false });\n }\n\n /** Wires the control cluster's buttons to their viewport-motion methods. */\n private wireControlListeners(): void {\n this._zoomInBtn.on(\"action\", this._onZoomIn);\n this._zoomOutBtn.on(\"action\", this._onZoomOut);\n this._fitBtn.on(\"action\", this._onFit);\n this._resetBtn.on(\"action\", this._onReset);\n }\n\n /**\n * Whether the built-in zoom / fit / reset control cluster is visible.\n *\n * @returns `true` when the control cluster shows.\n */\n isControlsVisible(): boolean {\n return this._options.controls ?? this._defaultOptions.controls ?? true;\n }\n\n /**\n * Shows or hides the built-in control cluster.\n *\n * @param value - Whether the control cluster is visible.\n *\n * @returns This view, for method chaining.\n */\n setControlsVisible(value: boolean): this {\n this._options.controls = value;\n this._controls.setVisible(value);\n\n return this;\n }\n}\n\nconst DiagramViewCallable = callable(DiagramView);\ntype DiagramViewCallable = DiagramView;\nexport {\n DiagramView as _DiagramView,\n DiagramViewCallable as DiagramView,\n};\n"],"mappings":"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"}