@peter.prib/renderengine 0.0.0-stage → 0.2.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 (451) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +203 -2
  3. package/REFERENCE.md +2198 -0
  4. package/dist/renderengine-workspace.umd.js +46905 -0
  5. package/dist/renderengine.umd.js +42223 -0
  6. package/examples/archimate.html +66 -0
  7. package/examples/diagram-editor.html +51 -0
  8. package/package.json +65 -4
  9. package/src/BaseRenderer.js +57 -0
  10. package/src/CanvasRenderer.js +194 -0
  11. package/src/Mat4.js +111 -0
  12. package/src/OrbitCamera.js +57 -0
  13. package/src/RendererFactory.js +50 -0
  14. package/src/SVGRenderer.js +439 -0
  15. package/src/archimate.js +723 -0
  16. package/src/archimateArchiFormat.js +1693 -0
  17. package/src/archimateArchiTypes.js +72 -0
  18. package/src/archimateArchive.js +140 -0
  19. package/src/archimateCSV.js +127 -0
  20. package/src/archimateDrawio.js +182 -0
  21. package/src/archimateExchangeFormat.js +546 -0
  22. package/src/archimateRelationshipTable.js +93 -0
  23. package/src/archimateSpecializations.js +161 -0
  24. package/src/chartCatalog.js +589 -0
  25. package/src/charting/ArchimateDiagramEditor.js +1624 -0
  26. package/src/charting/ArchimateSpecializationSelector.js +134 -0
  27. package/src/charting/Axis.js +68 -0
  28. package/src/charting/CameraSeeder.js +30 -0
  29. package/src/charting/CatalogDialog.js +92 -0
  30. package/src/charting/Chart.js +37 -0
  31. package/src/charting/Chart3D.js +99 -0
  32. package/src/charting/ChartBox.js +80 -0
  33. package/src/charting/ChartData.js +27 -0
  34. package/src/charting/ChartRegistry.js +76 -0
  35. package/src/charting/CheckboxTreeSelector.js +304 -0
  36. package/src/charting/ColorField.js +107 -0
  37. package/src/charting/ColorPalette.js +88 -0
  38. package/src/charting/CommandHistory.js +176 -0
  39. package/src/charting/DataFilter.js +52 -0
  40. package/src/charting/Diagram.js +36 -0
  41. package/src/charting/Diagram3D.js +29 -0
  42. package/src/charting/DiagramClipboard.js +31 -0
  43. package/src/charting/DiagramEditor.js +3330 -0
  44. package/src/charting/DiagramEditorCommand.js +110 -0
  45. package/src/charting/DiagramEditorCommands.js +237 -0
  46. package/src/charting/DiagramEditorEdgeCommands.js +142 -0
  47. package/src/charting/DiagramEditorState.js +98 -0
  48. package/src/charting/DiagramRegistry.js +86 -0
  49. package/src/charting/DiagramTypeRegistry.js +326 -0
  50. package/src/charting/DiagramViewPropertiesDialog.js +46 -0
  51. package/src/charting/Edge.js +113 -0
  52. package/src/charting/ErrorBars.js +37 -0
  53. package/src/charting/FilterSystem.js +285 -0
  54. package/src/charting/FindReplaceDialog.js +139 -0
  55. package/src/charting/FormDialog.js +385 -0
  56. package/src/charting/Gauge.js +58 -0
  57. package/src/charting/Hierarchy.js +175 -0
  58. package/src/charting/HintPanel.js +86 -0
  59. package/src/charting/ImageSelector.js +148 -0
  60. package/src/charting/InteractionManager.js +725 -0
  61. package/src/charting/Legend.js +58 -0
  62. package/src/charting/MenuBar.js +62 -0
  63. package/src/charting/ModelFileFormat.js +115 -0
  64. package/src/charting/OrbitControls.js +77 -0
  65. package/src/charting/Palette.js +119 -0
  66. package/src/charting/PopupMenu.js +117 -0
  67. package/src/charting/PropertiesPanel.js +155 -0
  68. package/src/charting/PropertyListEditor.js +96 -0
  69. package/src/charting/PropertyLog.js +92 -0
  70. package/src/charting/SettingsCascade.js +93 -0
  71. package/src/charting/SettingsDialog.js +53 -0
  72. package/src/charting/SettingsManager.js +179 -0
  73. package/src/charting/SettingsViewPropertiesDialog.js +58 -0
  74. package/src/charting/ShapePaths.js +27 -0
  75. package/src/charting/Statistics.js +134 -0
  76. package/src/charting/TemplateCatalog.js +59 -0
  77. package/src/charting/TemplateLibrary.js +103 -0
  78. package/src/charting/TextStyle.js +168 -0
  79. package/src/charting/ViewFit.js +55 -0
  80. package/src/charting/ViewMenu.js +70 -0
  81. package/src/charting/ViewPropertiesDialog.js +77 -0
  82. package/src/charting/ViewTabStrip.js +81 -0
  83. package/src/charting/archimate/ArchiFileFormat.js +32 -0
  84. package/src/charting/archimate/ArchimateCanvasEditor.js +815 -0
  85. package/src/charting/archimate/ArchimateCsvFileFormat.js +35 -0
  86. package/src/charting/archimate/ArchimateEdge.js +137 -0
  87. package/src/charting/archimate/ArchimateElementTemplate.js +195 -0
  88. package/src/charting/archimate/ArchimatePalette.js +102 -0
  89. package/src/charting/archimate/CsvImportDialog.js +105 -0
  90. package/src/charting/archimate/CustomViewpoints.js +74 -0
  91. package/src/charting/archimate/DerivedEdge.js +29 -0
  92. package/src/charting/archimate/DrawioFileFormat.js +24 -0
  93. package/src/charting/archimate/ElementFilterRule.js +21 -0
  94. package/src/charting/archimate/ExchangeFileFormat.js +28 -0
  95. package/src/charting/archimate/ViewGroupTemplate.js +137 -0
  96. package/src/charting/archimate/ViewLineEdge.js +84 -0
  97. package/src/charting/archimate/ViewpointRule.js +45 -0
  98. package/src/charting/archimate/archimateFileFormats.js +12 -0
  99. package/src/charting/archimate/archimateNames.js +5 -0
  100. package/src/charting/archimate/csvMapping.js +122 -0
  101. package/src/charting/archimate/elementMenuItems.js +32 -0
  102. package/src/charting/archimate/elementProperties.js +146 -0
  103. package/src/charting/archimate/elementTypeChoices.js +7 -0
  104. package/src/charting/archimate/elementTypeGroups.js +19 -0
  105. package/src/charting/archimate/index.js +17 -0
  106. package/src/charting/archimate/specializationCatalogDialog.js +64 -0
  107. package/src/charting/archimate/typeGlyph.js +29 -0
  108. package/src/charting/arrangement/Arrangement.js +112 -0
  109. package/src/charting/arrangement/ArrangementCommand.js +29 -0
  110. package/src/charting/arrangement/BackgroundCommand.js +41 -0
  111. package/src/charting/arrangement/BackgroundGroup.js +18 -0
  112. package/src/charting/arrangement/GroupCommand.js +46 -0
  113. package/src/charting/arrangement/ItemGroup.js +62 -0
  114. package/src/charting/arrangement/LockCommand.js +32 -0
  115. package/src/charting/arrangement/UngroupCommand.js +27 -0
  116. package/src/charting/arrangement/alignment.js +88 -0
  117. package/src/charting/arrangement/arrangeMenuItems.js +25 -0
  118. package/src/charting/arrangement/index.js +11 -0
  119. package/src/charting/arrangement/layouts.js +233 -0
  120. package/src/charting/canvas/ArchiHint.js +78 -0
  121. package/src/charting/canvas/CanvasBoxKind.js +39 -0
  122. package/src/charting/canvas/CanvasPalette.js +49 -0
  123. package/src/charting/canvas/CanvasTemplateLibrary.js +61 -0
  124. package/src/charting/canvas/CanvasTemplateMenu.js +151 -0
  125. package/src/charting/canvas/EditHintCommand.js +28 -0
  126. package/src/charting/canvas/canvasBoxKinds.js +27 -0
  127. package/src/charting/canvas/canvasBoxMenuItems.js +40 -0
  128. package/src/charting/canvas/defaultCanvasTemplates.js +186 -0
  129. package/src/charting/canvas/diagramBoxKinds.js +12 -0
  130. package/src/charting/canvas/hintMenuItems.js +49 -0
  131. package/src/charting/charts/ArcGauge.js +55 -0
  132. package/src/charting/charts/AreaChart.js +71 -0
  133. package/src/charting/charts/BarChart.js +80 -0
  134. package/src/charting/charts/BarChart3D.js +56 -0
  135. package/src/charting/charts/BarChart3DOrbit.js +53 -0
  136. package/src/charting/charts/BarChartBase.js +70 -0
  137. package/src/charting/charts/BarGauge.js +42 -0
  138. package/src/charting/charts/BoxPlotChart.js +41 -0
  139. package/src/charting/charts/BubbleChart.js +99 -0
  140. package/src/charting/charts/BubbleChart3D.js +78 -0
  141. package/src/charting/charts/BulletGauge.js +87 -0
  142. package/src/charting/charts/CalendarHeatmapChart.js +204 -0
  143. package/src/charting/charts/CandlestickChart.js +54 -0
  144. package/src/charting/charts/ChordDiagram.js +130 -0
  145. package/src/charting/charts/DensityChart.js +73 -0
  146. package/src/charting/charts/DialGauge.js +65 -0
  147. package/src/charting/charts/DonutChart.js +15 -0
  148. package/src/charting/charts/FunnelChart.js +47 -0
  149. package/src/charting/charts/GanttChart.js +201 -0
  150. package/src/charting/charts/GaugeChart.js +175 -0
  151. package/src/charting/charts/GeoMapChart.js +146 -0
  152. package/src/charting/charts/GroupedBarChart.js +88 -0
  153. package/src/charting/charts/GroupedDistributionChart.js +70 -0
  154. package/src/charting/charts/HeatmapChart.js +80 -0
  155. package/src/charting/charts/HierarchyChart.js +39 -0
  156. package/src/charting/charts/HistogramChart.js +45 -0
  157. package/src/charting/charts/KpiTile.js +72 -0
  158. package/src/charting/charts/LagPlotChart.js +41 -0
  159. package/src/charting/charts/LineChart.js +124 -0
  160. package/src/charting/charts/MultiRingGauge.js +54 -0
  161. package/src/charting/charts/OutlineChart.js +95 -0
  162. package/src/charting/charts/ParallelCoordinatesChart.js +130 -0
  163. package/src/charting/charts/PieChart.js +56 -0
  164. package/src/charting/charts/PipeChart.js +88 -0
  165. package/src/charting/charts/PopulationPyramidChart.js +106 -0
  166. package/src/charting/charts/QQPlotChart.js +64 -0
  167. package/src/charting/charts/RadarChart.js +76 -0
  168. package/src/charting/charts/RadialBarChart.js +96 -0
  169. package/src/charting/charts/RingGauge.js +23 -0
  170. package/src/charting/charts/SankeyDiagram.js +174 -0
  171. package/src/charting/charts/ScatterChart.js +104 -0
  172. package/src/charting/charts/StackedAreaChart.js +15 -0
  173. package/src/charting/charts/StackedBarChart.js +33 -0
  174. package/src/charting/charts/StackedBarChart3DOrbit.js +58 -0
  175. package/src/charting/charts/StreamGraphChart.js +131 -0
  176. package/src/charting/charts/SunburstChart.js +57 -0
  177. package/src/charting/charts/ThermometerGauge.js +43 -0
  178. package/src/charting/charts/TreemapChart.js +54 -0
  179. package/src/charting/charts/ViolinChart.js +37 -0
  180. package/src/charting/charts/WaterfallChart.js +66 -0
  181. package/src/charting/charts/WordCloudChart.js +104 -0
  182. package/src/charting/diagramModel/AddViewCommand.js +33 -0
  183. package/src/charting/diagramModel/DiagramModel.js +39 -0
  184. package/src/charting/diagramModel/DiagramModelSearch.js +22 -0
  185. package/src/charting/diagramModel/DiagramView.js +50 -0
  186. package/src/charting/diagramModel/ModelHistory.js +43 -0
  187. package/src/charting/diagramModel/ModelReport.js +153 -0
  188. package/src/charting/diagramModel/ModelSearch.js +223 -0
  189. package/src/charting/diagramModel/RemoveViewCommand.js +25 -0
  190. package/src/charting/diagramModel/RenameViewCommand.js +21 -0
  191. package/src/charting/diagramModel/ReplaceTextCommand.js +25 -0
  192. package/src/charting/diagramModel/ViewSetChecker.js +34 -0
  193. package/src/charting/diagramModel/ViewSetModel.js +243 -0
  194. package/src/charting/diagramModel/index.js +6 -0
  195. package/src/charting/diagramModel/nameTransforms.js +29 -0
  196. package/src/charting/diagrams/ArchimateCommands.js +141 -0
  197. package/src/charting/diagrams/ArchimateDiagram.js +263 -0
  198. package/src/charting/diagrams/ArchimateModel.js +1155 -0
  199. package/src/charting/diagrams/ArchimateModelChecker.js +125 -0
  200. package/src/charting/diagrams/ArchimateModelReport.js +128 -0
  201. package/src/charting/diagrams/ArchimateModelSearch.js +41 -0
  202. package/src/charting/diagrams/ArchimateValidator.js +357 -0
  203. package/src/charting/diagrams/CanvasView.js +112 -0
  204. package/src/charting/diagrams/ContextMenu.js +37 -0
  205. package/src/charting/diagrams/DataModelDiagram.js +86 -0
  206. package/src/charting/diagrams/DataModelRelationshipStrategy.js +38 -0
  207. package/src/charting/diagrams/DerivedRelationships.js +102 -0
  208. package/src/charting/diagrams/EbnfDiagram.js +39 -0
  209. package/src/charting/diagrams/EdgeGeometry.js +553 -0
  210. package/src/charting/diagrams/EditViewItemCommand.js +128 -0
  211. package/src/charting/diagrams/ElementInstance.js +157 -0
  212. package/src/charting/diagrams/GraphDiagram.js +1340 -0
  213. package/src/charting/diagrams/GraphDiagram3D.js +152 -0
  214. package/src/charting/diagrams/GraphMetrics.js +81 -0
  215. package/src/charting/diagrams/GridOverlay.js +137 -0
  216. package/src/charting/diagrams/ImportRowsCommand.js +62 -0
  217. package/src/charting/diagrams/LabelHelper.js +108 -0
  218. package/src/charting/diagrams/MergeElementsCommand.js +101 -0
  219. package/src/charting/diagrams/NestInstanceCommand.js +34 -0
  220. package/src/charting/diagrams/NodeGeometry.js +158 -0
  221. package/src/charting/diagrams/PlaceItemsCommand.js +77 -0
  222. package/src/charting/diagrams/RadialLayout.js +101 -0
  223. package/src/charting/diagrams/RelationshipInstance.js +93 -0
  224. package/src/charting/diagrams/RemoveGroupFromViewCommand.js +28 -0
  225. package/src/charting/diagrams/RemoveLineCommand.js +27 -0
  226. package/src/charting/diagrams/ShowElementCommand.js +43 -0
  227. package/src/charting/diagrams/SvgCoordinate.js +17 -0
  228. package/src/charting/diagrams/SvgZoom.js +127 -0
  229. package/src/charting/diagrams/Tooltip.js +62 -0
  230. package/src/charting/diagrams/ViewConnectionInstance.js +102 -0
  231. package/src/charting/diagrams/ViewObjectInstance.js +56 -0
  232. package/src/charting/diagrams/commands/AddEdgeCommand.js +34 -0
  233. package/src/charting/diagrams/commands/AddNodeCommand.js +28 -0
  234. package/src/charting/diagrams/commands/AddWaypointCommand.js +27 -0
  235. package/src/charting/diagrams/commands/CreateFolderCommand.js +27 -0
  236. package/src/charting/diagrams/commands/DeleteEdgeCommand.js +30 -0
  237. package/src/charting/diagrams/commands/DeleteFolderCommand.js +47 -0
  238. package/src/charting/diagrams/commands/DeleteNodeCommand.js +35 -0
  239. package/src/charting/diagrams/commands/DeleteWaypointCommand.js +24 -0
  240. package/src/charting/diagrams/commands/GraphCommand.js +65 -0
  241. package/src/charting/diagrams/commands/InvertEdgeCommand.js +24 -0
  242. package/src/charting/diagrams/commands/MoveNodeCommand.js +44 -0
  243. package/src/charting/diagrams/commands/MoveWaypointCommand.js +27 -0
  244. package/src/charting/diagrams/commands/RenameEdgeCommand.js +23 -0
  245. package/src/charting/diagrams/commands/RenameFolderCommand.js +31 -0
  246. package/src/charting/diagrams/commands/RenameNodeCommand.js +27 -0
  247. package/src/charting/diagrams/commands/ResizeNodeCommand.js +46 -0
  248. package/src/charting/diagrams/commands/UpdateEdgeSideCommand.js +40 -0
  249. package/src/charting/diagrams/commands/index.js +17 -0
  250. package/src/charting/diagrams/duplicates.js +49 -0
  251. package/src/charting/diagrams/nesting.js +30 -0
  252. package/src/charting/diagrams/roadmap.js +77 -0
  253. package/src/charting/edgeMarkers/ArrowMarker.js +15 -0
  254. package/src/charting/edgeMarkers/CircleMarker.js +13 -0
  255. package/src/charting/edgeMarkers/CrowsFootMarker.js +16 -0
  256. package/src/charting/edgeMarkers/DiamondMarker.js +15 -0
  257. package/src/charting/edgeMarkers/EdgeMarker.js +34 -0
  258. package/src/charting/edgeMarkers/HalfArrowMarker.js +8 -0
  259. package/src/charting/edgeMarkers/HollowDiamondMarker.js +18 -0
  260. package/src/charting/edgeMarkers/HollowTriangleMarker.js +18 -0
  261. package/src/charting/edgeMarkers/OpenArrowMarker.js +17 -0
  262. package/src/charting/edgeMarkers/index.js +27 -0
  263. package/src/charting/edgeStyle/EdgeStyleCascade.js +22 -0
  264. package/src/charting/edgeStyle/EdgeStyleDialog.js +51 -0
  265. package/src/charting/edgeStyle/EdgeStyleSchema.js +43 -0
  266. package/src/charting/edgeStyle/SetStyleCommand.js +12 -0
  267. package/src/charting/edgeStyle/index.js +4 -0
  268. package/src/charting/emphasis/EmphasisRule.js +42 -0
  269. package/src/charting/emphasis/HighlightRule.js +24 -0
  270. package/src/charting/gauges/ArcTrack.js +51 -0
  271. package/src/charting/gauges/GaugeAnimation.js +51 -0
  272. package/src/charting/gauges/GaugeBands.js +48 -0
  273. package/src/charting/gauges/GaugeScale.js +87 -0
  274. package/src/charting/gauges/GaugeTrack.js +44 -0
  275. package/src/charting/gauges/GaugeValue.js +61 -0
  276. package/src/charting/gauges/HorizontalTrack.js +14 -0
  277. package/src/charting/gauges/LinearTrack.js +29 -0
  278. package/src/charting/gauges/Sparkline.js +42 -0
  279. package/src/charting/gauges/VerticalTrack.js +17 -0
  280. package/src/charting/glyphs/ArrowGlyph.js +21 -0
  281. package/src/charting/glyphs/BatteryGlyph.js +18 -0
  282. package/src/charting/glyphs/ChevronGlyph.js +18 -0
  283. package/src/charting/glyphs/CircleGlyph.js +15 -0
  284. package/src/charting/glyphs/CloudGlyph.js +19 -0
  285. package/src/charting/glyphs/ComponentGlyph.js +18 -0
  286. package/src/charting/glyphs/ConstraintGlyph.js +24 -0
  287. package/src/charting/glyphs/ContractBoxGlyph.js +10 -0
  288. package/src/charting/glyphs/CubeGlyph.js +19 -0
  289. package/src/charting/glyphs/CylinderGlyph.js +19 -0
  290. package/src/charting/glyphs/DashedDoubleArrowGlyph.js +11 -0
  291. package/src/charting/glyphs/DeviceGlyph.js +17 -0
  292. package/src/charting/glyphs/DiamondGlyph.js +17 -0
  293. package/src/charting/glyphs/DocumentGlyph.js +17 -0
  294. package/src/charting/glyphs/DoubleArrowGlyph.js +23 -0
  295. package/src/charting/glyphs/DoubleCircleGlyph.js +24 -0
  296. package/src/charting/glyphs/EllipseGlyph.js +18 -0
  297. package/src/charting/glyphs/FactoryGlyph.js +20 -0
  298. package/src/charting/glyphs/FlagGlyph.js +16 -0
  299. package/src/charting/glyphs/GapCircleGlyph.js +17 -0
  300. package/src/charting/glyphs/GearGlyph.js +37 -0
  301. package/src/charting/glyphs/Glyph.js +98 -0
  302. package/src/charting/glyphs/GroupingBoxGlyph.js +13 -0
  303. package/src/charting/glyphs/HalfCircleGlyph.js +22 -0
  304. package/src/charting/glyphs/HexagonGlyph.js +28 -0
  305. package/src/charting/glyphs/IconGlyph.js +22 -0
  306. package/src/charting/glyphs/ImageGlyph.js +16 -0
  307. package/src/charting/glyphs/JunctionGlyph.js +17 -0
  308. package/src/charting/glyphs/LollipopGlyph.js +22 -0
  309. package/src/charting/glyphs/MagnifierGlyph.js +17 -0
  310. package/src/charting/glyphs/NoteGlyph.js +33 -0
  311. package/src/charting/glyphs/ObjectBoxGlyph.js +21 -0
  312. package/src/charting/glyphs/OrJunctionGlyph.js +17 -0
  313. package/src/charting/glyphs/ParallelogramGlyph.js +20 -0
  314. package/src/charting/glyphs/PersonGlyph.js +23 -0
  315. package/src/charting/glyphs/PillGlyph.js +23 -0
  316. package/src/charting/glyphs/PinGlyph.js +18 -0
  317. package/src/charting/glyphs/PrincipleGlyph.js +23 -0
  318. package/src/charting/glyphs/ProductBoxGlyph.js +16 -0
  319. package/src/charting/glyphs/RectGlyph.js +15 -0
  320. package/src/charting/glyphs/RoundedBoxGlyph.js +13 -0
  321. package/src/charting/glyphs/StackGlyph.js +14 -0
  322. package/src/charting/glyphs/StepsGlyph.js +20 -0
  323. package/src/charting/glyphs/StickFigureGlyph.js +32 -0
  324. package/src/charting/glyphs/SystemSoftwareBoxGlyph.js +16 -0
  325. package/src/charting/glyphs/TabbedBoxGlyph.js +37 -0
  326. package/src/charting/glyphs/TargetGlyph.js +23 -0
  327. package/src/charting/glyphs/WavyBoxGlyph.js +17 -0
  328. package/src/charting/glyphs/WheelGlyph.js +28 -0
  329. package/src/charting/glyphs/drawGlyph.js +28 -0
  330. package/src/charting/glyphs/glyphSvg.js +55 -0
  331. package/src/charting/glyphs/index.js +109 -0
  332. package/src/charting/modelTree/AnalysisPanel.js +159 -0
  333. package/src/charting/modelTree/DiagramTabs.js +217 -0
  334. package/src/charting/modelTree/ModelTree.js +164 -0
  335. package/src/charting/modelTree/ModelTreePanel.js +914 -0
  336. package/src/charting/modelTree/TreeBuilder.js +238 -0
  337. package/src/charting/modelTree/TreeModeRegistry.js +51 -0
  338. package/src/charting/modelTree/TreeViewMode.js +110 -0
  339. package/src/charting/modelTree/buildFilterTrees.js +138 -0
  340. package/src/charting/modelTree/modes/FlatTreeMode.js +46 -0
  341. package/src/charting/modelTree/modes/LayerGroupedTreeMode.js +62 -0
  342. package/src/charting/modelTree/modes/TypeGroupedTreeMode.js +60 -0
  343. package/src/charting/modelTree/treeLayers.js +11 -0
  344. package/src/charting/report/reportHtml.js +82 -0
  345. package/src/charting/report/reportViewer.js +118 -0
  346. package/src/charting/shapes/CircleShape.js +19 -0
  347. package/src/charting/shapes/PathShape.js +17 -0
  348. package/src/charting/shapes/RectShape.js +20 -0
  349. package/src/charting/shapes/Shape.js +20 -0
  350. package/src/charting/shapes/ShapeFactory.js +23 -0
  351. package/src/charting/storage/LocalJsonStore.js +43 -0
  352. package/src/charting/templates/ActorTemplate.js +36 -0
  353. package/src/charting/templates/BoxTemplate.js +27 -0
  354. package/src/charting/templates/CircleTemplate.js +18 -0
  355. package/src/charting/templates/ClassTemplate.js +20 -0
  356. package/src/charting/templates/CloudTemplate.js +43 -0
  357. package/src/charting/templates/ComponentTemplate.js +19 -0
  358. package/src/charting/templates/CylinderTemplate.js +30 -0
  359. package/src/charting/templates/DataStoreTemplate.js +16 -0
  360. package/src/charting/templates/DiamondTemplate.js +10 -0
  361. package/src/charting/templates/DocumentTemplate.js +32 -0
  362. package/src/charting/templates/EllipseTemplate.js +15 -0
  363. package/src/charting/templates/FinalStateTemplate.js +12 -0
  364. package/src/charting/templates/GlyphTemplate.js +47 -0
  365. package/src/charting/templates/HexagonTemplate.js +17 -0
  366. package/src/charting/templates/LifelineTemplate.js +20 -0
  367. package/src/charting/templates/ManualInputTemplate.js +12 -0
  368. package/src/charting/templates/NoteTemplate.js +23 -0
  369. package/src/charting/templates/OctagonTemplate.js +7 -0
  370. package/src/charting/templates/PackageTemplate.js +25 -0
  371. package/src/charting/templates/ParallelogramTemplate.js +11 -0
  372. package/src/charting/templates/PentagonTemplate.js +5 -0
  373. package/src/charting/templates/PolygonTemplate.js +18 -0
  374. package/src/charting/templates/PredefinedProcessTemplate.js +13 -0
  375. package/src/charting/templates/RectangleTemplate.js +28 -0
  376. package/src/charting/templates/RegularPolygonTemplate.js +21 -0
  377. package/src/charting/templates/RoundedRectangleTemplate.js +5 -0
  378. package/src/charting/templates/StadiumTemplate.js +8 -0
  379. package/src/charting/templates/StarTemplate.js +17 -0
  380. package/src/charting/templates/SwimlaneTemplate.js +18 -0
  381. package/src/charting/templates/Template.js +208 -0
  382. package/src/charting/templates/TrapezoidTemplate.js +11 -0
  383. package/src/charting/templates/TriangleTemplate.js +12 -0
  384. package/src/charting/templates/index.js +31 -0
  385. package/src/charting/templates/outline.js +59 -0
  386. package/src/charting/templates/svg.js +19 -0
  387. package/src/charting/textToShapes.js +61 -0
  388. package/src/charting/toolbar/ToolbarButton.js +38 -0
  389. package/src/charting/toolbar/ToolbarCheckbox.js +31 -0
  390. package/src/charting/toolbar/ToolbarItem.js +19 -0
  391. package/src/charting/toolbar/ToolbarMenu.js +36 -0
  392. package/src/charting/toolbar/ToolbarSelect.js +38 -0
  393. package/src/charting/toolbar/ToolbarSeparator.js +7 -0
  394. package/src/charting/toolbar/ToolbarText.js +22 -0
  395. package/src/charting/toolbar/index.js +8 -0
  396. package/src/charting/toolbar/pickFile.js +14 -0
  397. package/src/charting/viewDecorations.js +85 -0
  398. package/src/charting/zOrder.js +57 -0
  399. package/src/csvTable.js +61 -0
  400. package/src/datamodel.js +122 -0
  401. package/src/datawindow/BoundedWindow.js +36 -0
  402. package/src/datawindow/FlatDataWindow.js +34 -0
  403. package/src/datawindow/GraphDataWindow.js +40 -0
  404. package/src/datawindow/GroupedSampleDataWindow.js +16 -0
  405. package/src/datawindow/SeriesDataWindow.js +42 -0
  406. package/src/datawindow/index.js +31 -0
  407. package/src/diagramCatalog.js +30 -0
  408. package/src/ebnfDiagram.js +57 -0
  409. package/src/ebnfParser.js +129 -0
  410. package/src/exportImage.js +233 -0
  411. package/src/index.js +2636 -0
  412. package/src/itIcons.js +84 -0
  413. package/src/kernelExpr.js +243 -0
  414. package/src/propertiesDialog.js +463 -0
  415. package/src/railroadDiagram.js +571 -0
  416. package/src/treeModeCatalog.js +16 -0
  417. package/src/workspace/ContextBus.js +79 -0
  418. package/src/workspace/EditorShell.js +67 -0
  419. package/src/workspace/Part.js +163 -0
  420. package/src/workspace/PartFactory.js +49 -0
  421. package/src/workspace/PartRegistry.js +86 -0
  422. package/src/workspace/Workspace.js +321 -0
  423. package/src/workspace/archimateServices.js +33 -0
  424. package/src/workspace/dom.js +120 -0
  425. package/src/workspace/index.js +75 -0
  426. package/src/workspace/layout/GridNode.js +280 -0
  427. package/src/workspace/layout/LayoutNode.js +89 -0
  428. package/src/workspace/layout/PartNode.js +77 -0
  429. package/src/workspace/layout/SplitNode.js +114 -0
  430. package/src/workspace/layout/TabsNode.js +191 -0
  431. package/src/workspace/layout/index.js +24 -0
  432. package/src/workspace/parts/AdapterPart.js +33 -0
  433. package/src/workspace/parts/ArchimateCanvasPart.js +345 -0
  434. package/src/workspace/parts/ArchimateEditorPart.js +79 -0
  435. package/src/workspace/parts/ChartPart.js +115 -0
  436. package/src/workspace/parts/ContentPart.js +34 -0
  437. package/src/workspace/parts/DetailPart.js +46 -0
  438. package/src/workspace/parts/DiagramEditorPart.js +135 -0
  439. package/src/workspace/parts/EditorPart.js +388 -0
  440. package/src/workspace/parts/HtmlPart.js +25 -0
  441. package/src/workspace/parts/ListPart.js +57 -0
  442. package/src/workspace/parts/ModelFile.js +187 -0
  443. package/src/workspace/parts/ModelsPart.js +135 -0
  444. package/src/workspace/styles.js +258 -0
  445. package/src/workspace/themes.js +96 -0
  446. package/src/workspace/workArea.js +413 -0
  447. package/src/workspace/workAreaModel.js +197 -0
  448. package/src/workspace.js +16 -0
  449. package/src/zip/ZipArchive.js +160 -0
  450. package/src/zip/crc32.js +25 -0
  451. package/src/zip/inflate.js +159 -0
@@ -0,0 +1,1340 @@
1
+ import { TextStyle } from '../TextStyle.js';
2
+ import { Diagram } from '../Diagram.js';
3
+ import { CommandHistory } from '../CommandHistory.js';
4
+ import { GraphMetrics } from './GraphMetrics.js';
5
+ import { LabelHelper } from './LabelHelper.js';
6
+ import { NodeGeometry } from './NodeGeometry.js';
7
+ import { SvgCoordinate } from './SvgCoordinate.js';
8
+ import { EdgeGeometry } from './EdgeGeometry.js';
9
+ import { ContextMenu } from './ContextMenu.js';
10
+ import { GridOverlay } from './GridOverlay.js';
11
+ import { LINE_STYLE_DASH } from '../edgeStyle/EdgeStyleSchema.js';
12
+ import {
13
+ AddNodeCommand, DeleteNodeCommand,
14
+ AddEdgeCommand, DeleteEdgeCommand, InvertEdgeCommand,
15
+ RenameNodeCommand, RenameEdgeCommand,
16
+ AddWaypointCommand,
17
+ } from './commands/index.js';
18
+
19
+ /**
20
+ * GraphDiagram — renders an interactive 2D node/edge graph diagram.
21
+ */
22
+
23
+ /** Marks a selected line's end and bend-point handles (removed when the line is deselected). */
24
+ const EDGE_HANDLE_ATTR = 'data-edge-handle';
25
+
26
+ export class GraphDiagram extends Diagram {
27
+ static diagramType = 'graph';
28
+
29
+ constructor(renderer, nodes, edges, options = {}, engine = null, folders = []) {
30
+ super(renderer, { nodes, edges, folders }, options);
31
+ this.engine = engine;
32
+ this._chartState = { type: 'graph', nodes, edges, folders, options };
33
+ this._graph = null;
34
+ this._graphCollapsedNodes = new Set();
35
+ this._graphFilterKeyword = '';
36
+ this._graphFixedPos = new Set();
37
+ this._hiddenGraphNodes = new Set();
38
+ this._hiddenGraphGroups = new Set();
39
+ this._collapsedFolders = new Set(); // Track which folders are collapsed
40
+ // A host that re-renders the diagram hands in its own history so undo
41
+ // outlives this instance; that host then owns the undo/redo keys too.
42
+ this.hostOwnsHistory = !!options.history;
43
+ this.history = options.history ?? new CommandHistory({
44
+ maxSize: options.historySize ?? 200,
45
+ onChange: (canUndo, canRedo) => {
46
+ options?.onHistoryChange?.(canUndo, canRedo);
47
+ },
48
+ });
49
+ }
50
+
51
+ /**
52
+ * Fields that are not caller "extra data" for tooltips, search and the
53
+ * properties dialog: the engine's own, plus the host's `internalKeys`
54
+ * (bookkeeping a host adds to the items it hands in).
55
+ * @param {'node'|'table'|'edge'} kind
56
+ * @returns {Set<string>}
57
+ */
58
+ structuralKeys(kind) {
59
+ const E = this.engine?.constructor ?? {};
60
+ const own = { node: E.NODE_STRUCTURAL_KEYS, table: E.TABLE_NODE_STRUCTURAL_KEYS, edge: E.EDGE_STRUCTURAL_KEYS }[kind];
61
+ return new Set([...(own ?? []), ...(this.options?.internalKeys ?? [])]);
62
+ }
63
+
64
+ _render() {
65
+ const { nodes, edges } = this.data;
66
+ const options = this.options;
67
+ this._chartState = { type: 'graph', nodes, edges, options };
68
+ if (!this.renderer || !this.renderer.supportsSceneGraph) {
69
+ console.warn('Advanced graph diagram is only supported in SVG renderer.');
70
+ return;
71
+ }
72
+ this.renderer.clear();
73
+
74
+ const { nodeRadius = 14, onNodeClick, onEdgeClick, legend: showLegend = false, colors = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452'], edgeColor: defaultEdgeColor = '#bdc3c7', contextMenuItems, edgeContextMenuItems, canvasContextMenuItems, editable = false } = options;
75
+ const { compactness = 1.0 } = options;
76
+ // Default auto-routing for edges that don't set their own `edge.routing`
77
+ // — 'straight' (default), 'curved', 'orthogonal', or 'polyline' (basic
78
+ // node-avoidance). See _computeEdgePath. Editable diagrams also let
79
+ // the user drag a manual bend point onto any edge (double-click to
80
+ // add/remove, drag to reposition — see _addDragListeners), which
81
+ // always wins over whatever routing style is set.
82
+ const { edgeRouting: defaultEdgeRouting = 'straight' } = options;
83
+ // Resize handle (see _addResizeListeners): off unless `resizable` is
84
+ // true (a node can still opt out/in individually via its own
85
+ // `node.resizable`), corner defaults to 'top-right', overridable per
86
+ // shape (`resizeCornerByShape: {shape: corner}`) or per node
87
+ // (`node.resizeCorner`) — see _nodeResizeCorner.
88
+ const { resizable = false, resizeCorner = 'top-right', resizeCornerByShape } = options;
89
+ // Where a node's icon (icon/iconUrl, see the nodes param doc above)
90
+ // sits: 'center' (default — replaces the initials) or 'corner' (a
91
+ // small badge, positioned like the resize handle via the same
92
+ // node > per-shape > diagram-wide precedence — see _nodeIconCorner).
93
+ // A node's own `node.iconPosition` overrides this diagram default.
94
+ const { iconPosition: defaultIconPosition = 'center', iconCorner = 'top-right', iconCornerByShape } = options;
95
+ // 'below' (default, the original caption-under-the-shape convention)
96
+ // or 'inside' (label left-aligned inside the box — see the render
97
+ // loop below). A node's own `node.labelPosition` overrides this.
98
+ const { labelPosition: defaultLabelPosition = 'below' } = options;
99
+ // Horizontal alignment of the label: 'left' | 'center' | 'right'.
100
+ // Default keeps the existing behaviour: centred for captions below the
101
+ // shape, left-aligned for wrapped labels inside a box. Per-node values
102
+ // override the diagram default.
103
+ const { textAlign: defaultTextAlign = null } = options;
104
+ // Lines over boxes (Archi's connection layer) instead of under them.
105
+ const { edgesOnTop = false } = options;
106
+ // Vertical alignment, only meaningful for `labelPosition: 'inside'`:
107
+ // 'top' | 'middle' | 'bottom'. Default 'top' preserves existing layout.
108
+ const { verticalAlign: defaultVerticalAlign = null } = options;
109
+ // `sizeBy: 'degree'` sizes nodes by connection count instead of the
110
+ // caller pre-populating `node.size` — see _computeNodeDegrees.
111
+ const { minRadius = nodeRadius, maxRadius = nodeRadius * 2.5, sizeScale = 'linear', sizeBy } = options;
112
+ // Same pattern, for edge thickness — `edge.weight` drives stroke width.
113
+ const { minEdgeWidth = 1.4, maxEdgeWidth = minEdgeWidth * 4, edgeWidthScale = 'linear' } = options;
114
+ // Grid: `gridSize` > 0 snaps user-driven positions (drag, add node) to
115
+ // this many SVG units; `showGrid` draws the grid behind the diagram.
116
+ const { gridSize = 0, showGrid = false } = options;
117
+
118
+ // Initialize GridOverlay for responsive grid rendering
119
+ if (showGrid && gridSize > 0) {
120
+ if (!this._gridOverlay) {
121
+ this._gridOverlay = new GridOverlay(this.renderer.container, { gridSize, color: '#e5e5e5', width: 0.5 });
122
+ } else {
123
+ this._gridOverlay.setGridSize(gridSize);
124
+ }
125
+ } else if (this._gridOverlay) {
126
+ this._gridOverlay.destroy();
127
+ this._gridOverlay = null;
128
+ }
129
+
130
+ const W = this.renderer.width, H = this.renderer.height;
131
+ const R = nodeRadius;
132
+ const PAD = R + 6;
133
+ // Degree is computed from the caller's own `edges`, not whatever
134
+ // `effectiveEdges` layout inference below may substitute — "number of
135
+ // connections" means the graph the caller described, not an internal
136
+ // tree-layout artifact.
137
+ const degreeMap = sizeBy === 'degree' ? GraphMetrics.computeNodeDegrees(nodes, edges) : null;
138
+ const sizeDomain = GraphMetrics.computeSizeDomain(nodes, { sizeBy, degreeMap });
139
+ const sizeOptions = { minRadius, maxRadius, sizeScale, sizeBy, degreeMap };
140
+
141
+ // A hierarchical-key node set (see _inferHierarchyEdges) replaces the
142
+ // caller's edges with the inferred parent/child structure (plus any
143
+ // "quasi-leaf" edges it chose to keep) and switches layout from
144
+ // force-directed to a top-down tree — the two layouts don't blend.
145
+ const hierarchyEdges = this._inferHierarchyEdges(nodes, edges);
146
+ const useTreeLayout = hierarchyEdges !== null;
147
+ const effectiveEdges = useTreeLayout ? hierarchyEdges : edges;
148
+ const childrenMap = useTreeLayout ? this._hierarchyChildrenMap(effectiveEdges) : null;
149
+ // Over effectiveEdges, not the caller's raw edges — this is the domain
150
+ // for what actually gets drawn below, and in tree-layout mode that's a
151
+ // synthesized edge set the caller's `.weight` values don't apply to.
152
+ const edgeWidthDomain = GraphMetrics.computeEdgeWidthDomain(effectiveEdges);
153
+ const edgeWidthOptions = { minEdgeWidth, maxEdgeWidth, widthScale: edgeWidthScale };
154
+ // Collapsed subtrees (toggled via each node's +/− badge, see nodeEls
155
+ // below) hide their descendants — not the collapsed node itself,
156
+ // whose badge must stay visible/clickable to re-expand it.
157
+ const hiddenByCollapse = useTreeLayout
158
+ ? this._collapsedDescendantIds(this._graphCollapsedNodes, childrenMap)
159
+ : new Set();
160
+ // Live keyword filter (see filterGraphNodes) — recomputed every render
161
+ // so typing in a search box just calls filterGraphNodes and re-renders.
162
+ const filterKeyword = (this._graphFilterKeyword || '').trim();
163
+ const hiddenByFilter = this._hiddenByKeywordFilter(nodes, filterKeyword, childrenMap);
164
+
165
+ // Initialize positions, respecting any fixed positions from the input
166
+ this._graphFixedPos = new Set(nodes.filter(n => n.x != null || n.y != null).map(n => n.id));
167
+ let pos;
168
+ if (useTreeLayout) {
169
+ pos = this._computeTreeLayout(nodes, effectiveEdges, W, H, PAD);
170
+ nodes.forEach(n => { // an explicit x/y still overrides the computed tree slot
171
+ if (this._graphFixedPos.has(n.id)) {
172
+ const p = pos.get(n.id);
173
+ if (n.x != null) p.x = n.x;
174
+ if (n.y != null) p.y = n.y;
175
+ }
176
+ });
177
+ } else {
178
+ pos = new Map(nodes.map(n => [n.id, {
179
+ x: n.x ?? (Math.random() * (W - 2 * PAD) + PAD),
180
+ y: n.y ?? (Math.random() * (H - 2 * PAD) + PAD),
181
+ vx: 0, vy: 0 }]));
182
+ const K_FR = Math.sqrt((W * H) / Math.max(nodes.length, 1)) * 0.9 * compactness;
183
+ const frStep = (temp) => {
184
+ nodes.forEach(n => { if (!this._graphFixedPos.has(n.id)) { const p = pos.get(n.id); p.vx = 0; p.vy = 0; } });
185
+ for (let i = 0; i < nodes.length; i++) {
186
+ for (let j = i + 1; j < nodes.length; j++) {
187
+ const a = pos.get(nodes[i].id), b = pos.get(nodes[j].id);
188
+ const dx = b.x - a.x, dy = b.y - a.y;
189
+ const dist = Math.sqrt(dx * dx + dy * dy) || 0.01;
190
+ const f = K_FR * K_FR / dist;
191
+ a.vx -= dx / dist * f; a.vy -= dy / dist * f;
192
+ b.vx += dx / dist * f; b.vy += dy / dist * f;
193
+ }
194
+ }
195
+ effectiveEdges.forEach(e => {
196
+ const a = pos.get(e.source), b = pos.get(e.target);
197
+ if (!a || !b) return;
198
+ const dx = b.x - a.x, dy = b.y - a.y;
199
+ const dist = Math.sqrt(dx * dx + dy * dy) || 0.01;
200
+ const f = (dist * dist) / (K_FR * 1.5);
201
+ a.vx += dx / dist * f; a.vy += dy / dist * f;
202
+ b.vx -= dx / dist * f; b.vy -= dy / dist * f;
203
+ });
204
+ nodes.forEach(n => {
205
+ if (this._graphFixedPos.has(n.id)) return;
206
+ const p = pos.get(n.id);
207
+ p.vx += (W / 2 - p.x) * 0.006;
208
+ p.vy += (H / 2 - p.y) * 0.006;
209
+ const mag = Math.sqrt(p.vx * p.vx + p.vy * p.vy) || 1;
210
+ const dt = Math.min(mag, temp) / mag;
211
+ p.x = Math.max(PAD, Math.min(W - PAD, p.x + p.vx * dt));
212
+ p.y = Math.max(PAD, Math.min(H - PAD, p.y + p.vy * dt));
213
+ });
214
+ };
215
+
216
+ if (!this._graph) { // Only run full simulation on initial draw
217
+ // Run simulation synchronously before drawing
218
+ for (let iter = 0; iter < 180; iter++) {
219
+ frStep(K_FR * (1 - iter / 180) * 0.25);
220
+ }
221
+ }
222
+ }
223
+
224
+ // Other nodes as circular obstacles for 'polyline' routing (see
225
+ // _polylineEdgePath) — computed once per render, source/target of
226
+ // each edge excluded per-edge below rather than filtered out here.
227
+ const routingObstacles = nodes.map(n => {
228
+ const p = pos.get(n.id);
229
+ if (!p) return null;
230
+ const size = NodeGeometry.nodeSize(n, R, sizeDomain, sizeOptions);
231
+ return { id: n.id, x: p.x, y: p.y, r: Math.max(size.w, size.h) / 2 };
232
+ }).filter(Boolean);
233
+
234
+ // The selected line's handles are drawn after the nodes: an end handle
235
+ // sits on a node's rim, and a node drawn over it would hide half of it.
236
+ const selectionHandles = [];
237
+ const svgRoot = this.renderer.svg;
238
+ const edgesFrom = svgRoot?.childNodes?.length ?? 0;
239
+ const edgeEls = effectiveEdges.map(edge => {
240
+ const sourceNode = pos.get(edge.source);
241
+ const targetNode = pos.get(edge.target);
242
+ if (!sourceNode || !targetNode) return null;
243
+ const sourceInfo = nodes.find(n => n.id === edge.source);
244
+ const targetInfo = nodes.find(n => n.id === edge.target);
245
+ const sourceSize = this.engine._nodeSize(sourceInfo, R, sizeDomain, sizeOptions);
246
+ const targetSize = this.engine._nodeSize(targetInfo, R, sizeDomain, sizeOptions);
247
+ const edgeObstacles = routingObstacles.filter(o => o.id !== edge.source && o.id !== edge.target);
248
+
249
+ const { x1, y1, x2, y2, d } = EdgeGeometry.computeEdgePath(
250
+ sourceInfo, sourceNode, sourceSize, targetInfo, targetNode, targetSize, edge, defaultEdgeRouting, edgeObstacles
251
+ );
252
+
253
+ const edgeColor = edge.color || defaultEdgeColor;
254
+ const arrowhead = edge.arrowhead || 'arrow'; // default to 'arrow'
255
+ const markerId = arrowhead === 'none' ? '' : this.renderer.markerUrl(arrowhead);
256
+ const edgeWidth = GraphMetrics.effectiveEdgeWidth(edge, 1.4, edgeWidthDomain, edgeWidthOptions);
257
+ const path = this.renderer.path(d, 'none', { color: edgeColor, width: edgeWidth }, { isEdge: true, edge, tooltip: this._edgeTooltipText(edge) });
258
+ if (edge.id) path.setAttribute('edgeId', edge.id);
259
+ if (edge.opacity != null && edge.opacity !== 1) {
260
+ path.setAttribute('opacity', String(edge.opacity));
261
+ }
262
+ if (markerId) path.setAttribute('marker-end', markerId);
263
+ // marker-start — a second, independent arrowhead/glyph at the
264
+ // SOURCE end (e.g. ArchiMate's composition/aggregation diamond,
265
+ // or assignment's filled ball, both drawn at the "whole"/source
266
+ // end while marker-end carries a different glyph or none at all).
267
+ // Existing edges that only ever set `arrowhead` are unaffected —
268
+ // this is purely additive.
269
+ if (edge.arrowheadStart && edge.arrowheadStart !== 'none') path.setAttribute('marker-start', this.renderer.markerUrl(edge.arrowheadStart));
270
+ // `lineStyle` ('solid'|'dashed'|'dotted') is the general form of
271
+ // the older boolean `edge.dashed` (kept working, still meaning
272
+ // 'dashed', for callers who never adopted lineStyle) — 'dotted'
273
+ // is the extra case neither `dashed` nor the old fixed pattern
274
+ // could express (ArchiMate's realization/access relationships).
275
+ const dashArray = LINE_STYLE_DASH[edge.lineStyle ?? (edge.dashed ? 'dashed' : 'solid')] ?? null;
276
+ if (dashArray) path.setAttribute('stroke-dasharray', dashArray);
277
+ // onEdgeClick was accepted as an option but never actually wired
278
+ // up to anything — direct DOM listener, same pattern node click
279
+ // already uses (bubbles fine regardless of hitTest).
280
+ if (onEdgeClick) path.style.cursor = 'pointer';
281
+ if (onEdgeClick) path.addEventListener('click', () => onEdgeClick(edge));
282
+
283
+ // Track edge selection for waypoint visibility (only show waypoints on selected edge)
284
+ path.addEventListener('click', (e) => {
285
+ e.stopPropagation(); // Prevent canvas deselection
286
+ this._selectedEdge = edge;
287
+ this._rerenderGraph(); // Redraw to update waypoint visibility
288
+ });
289
+
290
+ // A collapsed ancestor or the keyword filter hides some nodes
291
+ // (see nodeEls below) — hide their connecting edges too, or
292
+ // they'd dangle toward an invisible node instead of disappearing
293
+ // with it.
294
+ const edgeHidden = hiddenByCollapse.has(edge.source) || hiddenByCollapse.has(edge.target)
295
+ || hiddenByFilter.has(edge.source) || hiddenByFilter.has(edge.target);
296
+ if (edgeHidden) path.setAttribute('visibility', 'hidden');
297
+
298
+ if (edge.label) {
299
+ const midX = (x1 + x2) / 2;
300
+ const midY = (y1 + y2) / 2;
301
+ const defaultTooltip = `${sourceInfo.label} → ${targetInfo.label}: ${edge.label}`;
302
+
303
+ const labelEl = this.renderer.text(edge.label, midX, midY, { textAnchor: 'middle', fontSize: '9px', fill: '#636e72', paintOrder: 'stroke', stroke: '#fafbfc', 'stroke-width': '2px', 'stroke-linecap': 'butt', 'stroke-linejoin': 'miter', tooltip: edge.tooltip || defaultTooltip });
304
+ if (edgeHidden) labelEl.setAttribute('visibility', 'hidden');
305
+ }
306
+
307
+ // Independent end labels — a UML association's multiplicity
308
+ // ('0..1', '1..*', ...) shown near EACH end, distinct from the
309
+ // one midpoint `edge.label` above (e.g. the relationship's own
310
+ // name) — see dataModelDiagram's 'uml' notation, though this is
311
+ // a plain graphDiagram field, not data-model-specific. Placed
312
+ // 18% of the way in from each anchor, not right at it, so the
313
+ // text doesn't sit on top of an arrowhead/cardinality glyph.
314
+ if (edge.sourceLabel) {
315
+ const lx = x1 + (x2 - x1) * 0.18, ly = y1 + (y2 - y1) * 0.18;
316
+ const labelEl = this.renderer.text(edge.sourceLabel, lx, ly, { textAnchor: 'middle', fontSize: '9px', fill: '#636e72', paintOrder: 'stroke', stroke: '#fafbfc', 'stroke-width': '2px', 'stroke-linecap': 'butt', 'stroke-linejoin': 'miter' });
317
+ if (edgeHidden) labelEl.setAttribute('visibility', 'hidden');
318
+ }
319
+ if (edge.targetLabel) {
320
+ const lx = x2 + (x1 - x2) * 0.18, ly = y2 + (y1 - y2) * 0.18;
321
+ const labelEl = this.renderer.text(edge.targetLabel, lx, ly, { textAnchor: 'middle', fontSize: '9px', fill: '#636e72', paintOrder: 'stroke', stroke: '#fafbfc', 'stroke-width': '2px', 'stroke-linecap': 'butt', 'stroke-linejoin': 'miter' });
322
+ if (edgeHidden) labelEl.setAttribute('visibility', 'hidden');
323
+ }
324
+
325
+ // Manual bend points (see _computeEdgePath's waypoints branch and
326
+ // the drag/double-click handlers in _addDragListeners) only need a
327
+ // visible/draggable handle in editable mode — a non-editable
328
+ // diagram still routes through them, it just can't gain new ones.
329
+ // Only show waypoints when this edge is selected (reduces clutter).
330
+ if (editable && this._selectedEdge === edge) selectionHandles.push(() => {
331
+ // Endpoint handles (source and target) — red circles, draggable to reposition on node rim
332
+ const endpointHandleColor = '#ff6b6b';
333
+ {
334
+ const sourceHandle = this.renderer.circle(x1, y1, 5, '#ffffff', { color: endpointHandleColor, width: 2 }, { draggable: true, isEndpoint: true, edge, endpointType: 'source' });
335
+ sourceHandle.style.cursor = 'grab';
336
+ sourceHandle.setAttribute(EDGE_HANDLE_ATTR, '');
337
+ if (edgeHidden) sourceHandle.setAttribute('visibility', 'hidden');
338
+
339
+ const targetHandle = this.renderer.circle(x2, y2, 5, '#ffffff', { color: endpointHandleColor, width: 2 }, { draggable: true, isEndpoint: true, edge, endpointType: 'target' });
340
+ targetHandle.style.cursor = 'grab';
341
+ targetHandle.setAttribute(EDGE_HANDLE_ATTR, '');
342
+ if (edgeHidden) targetHandle.setAttribute('visibility', 'hidden');
343
+ }
344
+
345
+ // Waypoint handles (blue circles for manual bends)
346
+ if (edge.waypoints?.length) {
347
+ edge.waypoints.forEach((wp, waypointIndex) => {
348
+ const handle = this.renderer.circle(wp.x, wp.y, 4, '#ffffff', { color: '#4f8ef7', width: 2 }, { draggable: true, isWaypoint: true, edge, waypointIndex });
349
+ handle.style.cursor = 'move';
350
+ handle.setAttribute(EDGE_HANDLE_ATTR, '');
351
+ if (edgeHidden) handle.setAttribute('visibility', 'hidden');
352
+ });
353
+ }
354
+ });
355
+ return path;
356
+ }).filter(Boolean);
357
+
358
+ // Draw edge-crossing bridges (small arcs where edges cross)
359
+ const edgePaths = edgeEls.map(el => el ? el.getAttribute('d') : null).filter(Boolean);
360
+ if (edgePaths.length > 1) {
361
+ // Detect all pairwise crossings
362
+ const allCrossings = [];
363
+ for (let i = 0; i < edgePaths.length; i++) {
364
+ for (let j = i + 1; j < edgePaths.length; j++) {
365
+ const crossings = EdgeGeometry.findEdgeCrossings(edgePaths[i], [edgePaths[j]]);
366
+ allCrossings.push(...crossings);
367
+ }
368
+ }
369
+
370
+ // Draw bridges at each crossing (small semicircle arcs)
371
+ for (const crossing of allCrossings) {
372
+ const bridgeRadius = 6;
373
+ const dx = Math.cos(crossing.angle);
374
+ const dy = Math.sin(crossing.angle);
375
+ const px = crossing.x - dy * bridgeRadius;
376
+ const py = crossing.y + dx * bridgeRadius;
377
+ const qx = crossing.x + dy * bridgeRadius;
378
+ const qy = crossing.y - dx * bridgeRadius;
379
+
380
+ const bridgePath = `M${px},${py} A${bridgeRadius},${bridgeRadius} 0 0,1 ${qx},${qy}`;
381
+ this.renderer.path(bridgePath, 'none', {
382
+ color: '#ffffff',
383
+ width: 8,
384
+ opacity: 0.8
385
+ });
386
+ // Draw a thin line on top for visibility
387
+ this.renderer.path(bridgePath, 'none', {
388
+ color: '#bdc3c7',
389
+ width: 2,
390
+ opacity: 1
391
+ });
392
+ }
393
+ }
394
+
395
+ const groups = new Map();
396
+ nodes.forEach(node => {
397
+ if (node.group && !groups.has(node.group)) {
398
+ groups.set(node.group, colors[groups.size % colors.length]);
399
+ }
400
+ });
401
+
402
+ // Get nodes hidden by collapsed folders
403
+ const { folders = [] } = this.data;
404
+ const hiddenByCollapsedFolders = this._getHiddenByCollapsedFolders(folders);
405
+
406
+ // Filter out nodes from hidden groups, collapsed subtrees, keyword filter, and collapsed folders
407
+ const visibleNodes = nodes.filter(node =>
408
+ (!node.group || !this._hiddenGraphGroups.has(node.group)) && !this._hiddenGraphNodes.has(node.id)
409
+ && !hiddenByCollapse.has(node.id) && !hiddenByFilter.has(node.id)
410
+ && !hiddenByCollapsedFolders.has(node.id)
411
+ );
412
+ const visibleNodeIds = new Set(visibleNodes.map(n => n.id));
413
+ const visibleEdges = effectiveEdges.filter(edge => visibleNodeIds.has(edge.source) && visibleNodeIds.has(edge.target));
414
+
415
+ const edgesTo = svgRoot?.childNodes?.length ?? 0;
416
+ // Render folders (visual containers) before nodes so they appear behind
417
+ this._renderFolders(this.renderer.svg, folders);
418
+
419
+ const nodeEls = nodes.map(node => {
420
+ const isHidden = this._hiddenGraphGroups.has(node.group) || this._hiddenGraphNodes.has(node.id)
421
+ || hiddenByCollapse.has(node.id) || hiddenByFilter.has(node.id)
422
+ || hiddenByCollapsedFolders.has(node.id);
423
+ const p = pos.get(node.id);
424
+
425
+ // Table nodes previously got just the bare label as a tooltip —
426
+ // real detail (column/row counts) already exists on the node
427
+ // object at this point but wasn't being surfaced on hover. Either
428
+ // way, any extra caller-supplied field not in the structural set
429
+ // (e.g. {owner:'Alice'}) is appended as its own "key: value" line.
430
+ const resolvedLabel = LabelHelper.resolveNodeLabel(node);
431
+ const tooltip = node.shape === 'table'
432
+ ? [
433
+ `${resolvedLabel} — ${node.columns?.length ?? 0} column${node.columns?.length === 1 ? '' : 's'}` +
434
+ (Number.isFinite(node.rowCount) ? ` · ${node.rowCount.toLocaleString()} row${node.rowCount === 1 ? '' : 's'}` : ''),
435
+ ...LabelHelper.nodeExtraDataLines(node, this.structuralKeys('table')),
436
+ ].join('\n')
437
+ : [`Node ${resolvedLabel}`, ...LabelHelper.nodeExtraDataLines(node, this.structuralKeys('node'))].join('\n');
438
+ const g = this.renderer.group({ transform: `translate(${p.x},${p.y})`, draggable: true, nodeId: node.id, tooltip });
439
+ if (node.opacity != null && node.opacity !== 1) {
440
+ g.setAttribute('opacity', String(node.opacity));
441
+ }
442
+
443
+ if (isHidden) {
444
+ g.setAttribute('visibility', 'hidden');
445
+ }
446
+
447
+ if (node.shape === 'table') {
448
+ NodeGeometry.buildTableNodeContent(this.renderer, g, node);
449
+ } else {
450
+ const nodeColor = node.color || (node.group ? groups.get(node.group) : (options.nodeColor || '#2ecc71'));
451
+ // A 'rect' node's own width/height (like a table node's, see
452
+ // _nodeSize) override the plain square-of-radius box every
453
+ // other shape still uses — needed for ArchiMate-style wider-
454
+ // than-tall boxes (archimateDiagram). nodeR (still used below
455
+ // for icon sizing/badge placement on non-rect shapes) is the
456
+ // radius that SAME square would have had, so nothing changes
457
+ // for a 'rect' node that never sets width/height.
458
+ const nodeSize = NodeGeometry.nodeSize(node, R, sizeDomain, sizeOptions);
459
+ const nodeR = Math.max(nodeSize.w, nodeSize.h) / 2;
460
+ const { el: shapeEl, suppressInner } = NodeGeometry.nodeShapeEl(this.renderer, node.shape, nodeR, nodeColor, nodeSize, node.cornerStyle, node.strokeColor, node.strokeWidth, node.iconUrl, node.icon);
461
+ if (node.strokeDasharray) shapeEl.setAttribute?.('stroke-dasharray', node.strokeDasharray);
462
+ g.appendChild(shapeEl);
463
+
464
+ // `iconShape` — a shape NAME (any _nodeShapeEl one) drawn
465
+ // small in the corner as line art, instead of an emoji/image
466
+ // badge. This is how the real ArchiMate notation works: the
467
+ // corner badge is the element's own standalone glyph at badge
468
+ // size (see archimateDiagram/_archimateBuildNode), so one
469
+ // vector glyph serves both. Drawn unfilled with a dark
470
+ // outline so it reads on top of the node's own fill.
471
+ // The icon is drawn in the outline colour; with no outline ('none') it stays black, as in Archi.
472
+ const markColor = node.strokeColor === 'none' ? '#000000' : node.strokeColor;
473
+ if (node.iconShape) {
474
+ const badge = Math.max(10, Math.round(Math.min(nodeSize.w, nodeSize.h) * 0.3));
475
+ const corner = NodeGeometry.nodeIconCorner(node, iconCornerByShape, iconCorner);
476
+ const cp = NodeGeometry.cornerPoint({ x: 0, y: 0 }, nodeSize, corner);
477
+ const inset = badge / 2 + 3;
478
+ const bx = cp.x + (corner.includes('right') ? -inset : inset);
479
+ const by = cp.y + (corner.includes('bottom') ? -inset : inset);
480
+ const { el: badgeEl } = NodeGeometry.nodeShapeEl(
481
+ this.renderer, node.iconShape, badge / 2, 'none', { w: badge, h: badge * 0.78 }, undefined,
482
+ markColor || '#000000', 1,
483
+ );
484
+ badgeEl.setAttribute('transform', `translate(${bx},${by})`);
485
+ badgeEl.setAttribute('pointer-events', 'none');
486
+ g.appendChild(badgeEl);
487
+ }
488
+
489
+ const iconPosition = node.iconPosition || defaultIconPosition;
490
+ // A custom icon (an emoji/glyph string, or an image/SVG file via
491
+ // iconUrl) — centred on the shape (replacing the plain initials,
492
+ // 'center', the default) or a small corner badge ('corner', see
493
+ // _nodeIconCorner) alongside whatever else is in the box.
494
+ // suppressInner still gates 'center' mode only — a 'person'
495
+ // shape (which draws its own head/body glyph) stays uncluttered
496
+ // there, but a corner badge sits well clear of that glyph.
497
+ if (iconPosition === 'corner' && (node.icon || node.iconUrl)) {
498
+ // Sized off the box's SMALLER dimension, not nodeR (which
499
+ // is max(w,h)/2 — for a wide-but-short ArchiMate-style
500
+ // box that would badly oversize the corner badge).
501
+ const badgeSize = Math.max(12, Math.round(Math.min(nodeSize.w, nodeSize.h) * 0.3));
502
+ const corner = NodeGeometry.nodeIconCorner(node, iconCornerByShape, iconCorner);
503
+ const cp = NodeGeometry.cornerPoint({ x: 0, y: 0 }, nodeSize, corner);
504
+ const inset = badgeSize / 2 + 2; // keep the badge inside the box, not straddling its edge
505
+ const bx = cp.x + (corner.includes('right') ? -inset : inset);
506
+ const by = cp.y + (corner.includes('bottom') ? -inset : inset);
507
+ if (node.iconUrl) {
508
+ g.appendChild(this.renderer.image(node.iconUrl, bx - badgeSize / 2, by - badgeSize / 2, badgeSize, badgeSize, { 'pointer-events': 'none' }));
509
+ } else {
510
+ // Badge colour follows the node's own outline colour —
511
+ // white reads on plain graphDiagram's saturated node
512
+ // fills, but vanishes on an ArchiMate box's pastel one
513
+ // (which sets strokeColor: '#000').
514
+ g.appendChild(this.renderer.text(node.icon, bx, by, { fill: markColor || '#fff', textAnchor: 'middle', verticalAlign: 'middle', fontSize: `${badgeSize}px`, 'pointer-events': 'none' }));
515
+ }
516
+ // A node with a real glyph badge (iconShape, drawn above)
517
+ // must not ALSO get the centred initials fallback below —
518
+ // that's what put a stray "RE"/"CA" in the middle of every
519
+ // ArchiMate box once emoji badges gave way to glyphs.
520
+ } else if (!suppressInner && !node.iconShape && (!node.viewObject || node.iconUrl)) {
521
+ // (A view-only box shows its own text: no initials, but its picture if it has one.)
522
+ if (node.iconUrl) {
523
+ const iconSize = nodeR * 1.3;
524
+ g.appendChild(this.renderer.image(node.iconUrl, -iconSize / 2, -iconSize / 2, iconSize, iconSize, { 'pointer-events': 'none' }));
525
+ } else {
526
+ const centerText = node.icon || resolvedLabel.slice(0, 2).toUpperCase() || (node.id ?? '').slice(0, 2).toUpperCase();
527
+ const fontSize = node.icon ? Math.max(12, Math.round(nodeR * 1.1)) : 10;
528
+ g.appendChild(this.renderer.text(centerText, 0, 0, { fill: '#fff', textAnchor: 'middle', verticalAlign: 'middle', fontSize: `${fontSize}px`, fontWeight: node.icon ? 'normal' : 'bold', 'pointer-events': 'none' }));
529
+ }
530
+ }
531
+
532
+ // 'below' (default) keeps the original single-line caption-
533
+ // under-the-shape convention; 'inside' (ArchiMate-style
534
+ // boxes) left-aligns the label near the box's own top-left
535
+ // instead, WORD-WRAPPED (_wrapLabelLines) to as many lines as
536
+ // fit inside the real box (nodeSize, not the icon-sizing
537
+ // nodeR above — a wide-but-short rect must not push it far
538
+ // below) rather than a single line overflowing the edges.
539
+ // Image objects draw their own visual, so they skip labels.
540
+ if (node.shape !== 'image') {
541
+ const labelPosition = node.labelPosition || (node.shape === 'note' ? 'inside' : defaultLabelPosition);
542
+ const inside = labelPosition === 'inside';
543
+ const textAlign = node.textAlign || defaultTextAlign || (inside ? 'left' : 'center');
544
+ const verticalAlign = node.verticalAlign || defaultVerticalAlign || 'top';
545
+ const anchorByAlign = { left: 'start', center: 'middle', right: 'end' };
546
+ const textAnchor = anchorByAlign[textAlign] || 'start';
547
+ if (inside) {
548
+ // The node's own font when it has one (an Archi import: points, as Archi sizes them).
549
+ const FONT_SIZE = GraphDiagram.fontPixels(node.fontSize) ?? 10;
550
+ const PAD = 6, LINE_HEIGHT = Math.round(FONT_SIZE * 1.2);
551
+ const font = {
552
+ fill: node.textColor || '#222', fontSize: `${FONT_SIZE}px`,
553
+ ...(node.fontFamily && { fontFamily: node.fontFamily }),
554
+ ...(node.fontWeight && { fontWeight: node.fontWeight }),
555
+ ...(node.fontStyle && { fontStyle: node.fontStyle }),
556
+ };
557
+ // A badge in a top corner takes its width off that side of the text, as in Archi.
558
+ const hasBadge = node.iconShape || (iconPosition === 'corner' && (node.icon || node.iconUrl));
559
+ const badgeCorner = hasBadge ? NodeGeometry.nodeIconCorner(node, iconCornerByShape, iconCorner) : '';
560
+ const badgeWidth = hasBadge && badgeCorner.includes('top') ? Math.max(12, Math.round(Math.min(nodeSize.w, nodeSize.h) * 0.3)) + 4 : 0;
561
+ const insetLeft = badgeCorner.includes('left') ? badgeWidth : 0;
562
+ const insetRight = badgeCorner.includes('right') ? badgeWidth : 0;
563
+ const lines = LabelHelper.wrapLabelLines(resolvedLabel, nodeSize.w - PAD * 2 - insetLeft - insetRight, FONT_SIZE, nodeSize.h - PAD * 2, LINE_HEIGHT,
564
+ this.renderer.measureText ? (s) => this.renderer.measureText(s, font) : null);
565
+ const { x, baseline: y0 } = TextStyle.block({ textAlign, textPosition: verticalAlign },
566
+ { left: -nodeSize.w / 2 + insetLeft, right: nodeSize.w / 2 - insetRight, top: -nodeSize.h / 2, bottom: nodeSize.h / 2 },
567
+ lines.length, { pixels: FONT_SIZE, lineHeight: LINE_HEIGHT, pad: PAD });
568
+ lines.forEach((line, i) => {
569
+ g.appendChild(this.renderer.text(line, x, y0 + i * LINE_HEIGHT, { ...font, textAnchor, 'pointer-events': 'none' }));
570
+ });
571
+ } else {
572
+ // nodeSize.h/2 (the shape's own real bottom edge), not
573
+ // nodeR (max(w,h)/2 — for a wide-but-short shape, e.g. any
574
+ // archimateDiagram distinct notation shape at its usual
575
+ // 120x55-ish box footprint, nodeR badly overshoots the
576
+ // shape's actual visual height, sitting the label so far
577
+ // down it lands on top of the NEXT row in a dense grid
578
+ // instead of just under this node). For a shape that
579
+ // ignores non-square `size` entirely (person/diamond/the
580
+ // plain default circle — always rendered from R alone),
581
+ // nodeSize.h still equals 2*R when no explicit
582
+ // width/height override is set, so this is a no-op there.
583
+ let x;
584
+ if (textAlign === 'left') x = -nodeSize.w / 2;
585
+ else if (textAlign === 'right') x = nodeSize.w / 2;
586
+ else x = 0;
587
+ g.appendChild(this.renderer.text(resolvedLabel, x, nodeSize.h / 2 + 9, { fill: '#333', textAnchor, fontSize: '9px', 'pointer-events': 'none' }));
588
+ }
589
+ }
590
+
591
+ // Hierarchical-key mode only: a +/− badge on any node with
592
+ // children toggles its subtree's visibility (_graphCollapsedNodes,
593
+ // read back into hiddenByCollapse above on the next render).
594
+ // stopPropagation keeps this click from also bubbling to the
595
+ // node's own onNodeClick listener below.
596
+ if (childrenMap?.get(node.id)?.length) {
597
+ const collapsed = this._graphCollapsedNodes.has(node.id);
598
+ const badgeY = nodeR + 20; // below the label (at nodeR+9) so it doesn't overlap the node circle or its text
599
+ const badge = this.renderer.circle(0, badgeY, 6, '#ffffff', { color: '#888', width: 1.2 });
600
+ const badgeGlyph = this.renderer.text(collapsed ? '+' : '−', 0, badgeY, {
601
+ textAnchor: 'middle', verticalAlign: 'middle', fontSize: '9px', fontWeight: 'bold', fill: '#555', 'pointer-events': 'none',
602
+ });
603
+ badge.style.cursor = 'pointer';
604
+ badge.addEventListener('click', (event) => {
605
+ event.stopPropagation();
606
+ if (collapsed) this._graphCollapsedNodes.delete(node.id);
607
+ else this._graphCollapsedNodes.add(node.id);
608
+ this._rerenderGraph();
609
+ });
610
+ g.appendChild(badge);
611
+ g.appendChild(badgeGlyph);
612
+ }
613
+ }
614
+
615
+ if (onNodeClick) {
616
+ g.addEventListener('click', () => onNodeClick(node));
617
+ }
618
+ return g;
619
+ });
620
+ if (edgesOnTop && svgRoot?.childNodes) {
621
+ // Re-append the lines (and their labels and bridges), in order, above every box.
622
+ for (const el of Array.from(svgRoot.childNodes).slice(edgesFrom, edgesTo)) svgRoot.appendChild(el);
623
+ }
624
+ for (const draw of selectionHandles) draw();
625
+
626
+ if (showLegend && groups.size > 0) {
627
+ const legendX = W - 120;
628
+ let legendY = 40;
629
+ const legendItemHeight = 20;
630
+ const legendMarkerRadius = 6;
631
+
632
+ const legendGroup = this.renderer.group({ class: 'legend' });
633
+
634
+ groups.forEach((color, groupName) => {
635
+ const isHidden = this._hiddenGraphGroups.has(groupName);
636
+ const legendItemAttrs = { legendItem: true, graphLegend: true, groupName };
637
+ const itemColor = isHidden ? '#ccc' : color;
638
+ const textColor = isHidden ? '#ccc' : '#333';
639
+
640
+ const marker = this.renderer.circle(legendX, legendY, legendMarkerRadius, itemColor, { color: 'white', width: 1.5 }, legendItemAttrs);
641
+ const label = this.renderer.text(groupName, legendX + legendMarkerRadius + 8, legendY, {
642
+ textAnchor: 'start',
643
+ verticalAlign: 'middle',
644
+ fontSize: '12px',
645
+ fill: textColor,
646
+ ...legendItemAttrs
647
+ });
648
+ marker.addEventListener('click', () => this._onLegendClick(groupName));
649
+ label.addEventListener('click', () => this._onLegendClick(groupName));
650
+ legendY += legendItemHeight;
651
+ });
652
+ }
653
+
654
+ // A click anywhere but on the selected line's handles deselects it: empty canvas, a shape, the
655
+ // grid, a group. Listened in the capture phase (a shape stops its own click from bubbling); a
656
+ // click on a line selects that one (its own listener). The handles are removed in place rather
657
+ // than by redrawing, which would replace a shape under a double-click.
658
+ if (this.renderer.svg) {
659
+ this.renderer.svg.removeEventListener('click', this._handleCanvasClick, true);
660
+ this._handleCanvasClick = (e) => this.deselectEdgeOnClick(e.target);
661
+ this.renderer.svg.addEventListener('click', this._handleCanvasClick, true);
662
+ }
663
+
664
+ // The engine keeps the selection (several nodes), so it outlives a redraw.
665
+ this._showNodeSelection(nodeEls);
666
+
667
+ this._graph = {
668
+ nodes, edges: visibleEdges, pos, nodeEls, edgeEls, nodeRadius: R, groups,
669
+ contextMenuItems, edgeContextMenuItems, canvasContextMenuItems, editable,
670
+ sizeDomain, sizeOptions,
671
+ resizableDefault: resizable, resizeCorner, resizeCornerByShape,
672
+ };
673
+ }
674
+
675
+ static inferHierarchyEdges(nodes, explicitEdges = []) {
676
+ const HIER_KEY_RE = /^\d+(?:[./-]\d+)+$/;
677
+ if (!nodes.some(n => HIER_KEY_RE.test(String(n.id)))) return null;
678
+ const isHierId = (id) => /^\d+(?:[./-]\d+)*$/.test(String(id));
679
+ const nodeIds = new Set(nodes.map(n => n.id));
680
+ const edges = [];
681
+ const seen = new Set();
682
+ nodes.forEach(n => {
683
+ const m = String(n.id).match(/^(.+)[./-]\d+$/);
684
+ const parentId = m ? m[1] : null;
685
+ if (parentId && nodeIds.has(parentId)) {
686
+ edges.push({ source: parentId, target: n.id });
687
+ seen.add(`${parentId}\u0000${n.id}`);
688
+ }
689
+ });
690
+ explicitEdges.forEach(e => {
691
+ if (isHierId(e.source) && isHierId(e.target)) return; // superseded by the inferred parent/child above
692
+ const key = `${e.source}\u0000${e.target}`;
693
+ if (seen.has(key)) return;
694
+ seen.add(key);
695
+ edges.push(e);
696
+ });
697
+ return edges;
698
+ }
699
+ _inferHierarchyEdges(nodes, explicitEdges = []) {
700
+ return GraphDiagram.inferHierarchyEdges(nodes, explicitEdges);
701
+ }
702
+
703
+ static hierarchyChildrenMap(edges) {
704
+ const map = new Map();
705
+ edges.forEach(e => {
706
+ if (!map.has(e.source)) map.set(e.source, []);
707
+ map.get(e.source).push(e.target);
708
+ });
709
+ return map;
710
+ }
711
+ _hierarchyChildrenMap(edges) {
712
+ return GraphDiagram.hierarchyChildrenMap(edges);
713
+ }
714
+
715
+ static collapsedDescendantIds(collapsedIds, childrenMap) {
716
+ const hidden = new Set();
717
+ const visit = (id) => {
718
+ for (const child of (childrenMap.get(id) || [])) {
719
+ if (!hidden.has(child)) { hidden.add(child); visit(child); }
720
+ }
721
+ };
722
+ collapsedIds.forEach(visit);
723
+ return hidden;
724
+ }
725
+ _collapsedDescendantIds(collapsedIds, childrenMap) {
726
+ return GraphDiagram.collapsedDescendantIds(collapsedIds, childrenMap);
727
+ }
728
+
729
+ static computeTreeLayout(nodes, edges, W, H, PAD) {
730
+ const childrenMap = GraphDiagram.hierarchyChildrenMap(edges);
731
+ const hasParent = new Set(edges.map(e => e.target));
732
+ const roots = nodes.filter(n => !hasParent.has(n.id)).map(n => n.id);
733
+
734
+ const depth = new Map();
735
+ const leafOrder = [];
736
+ const visited = new Set();
737
+ const visit = (id, d) => {
738
+ if (visited.has(id)) return;
739
+ visited.add(id);
740
+ depth.set(id, d);
741
+ const kids = childrenMap.get(id) || [];
742
+ if (kids.length === 0) leafOrder.push(id);
743
+ else kids.forEach(k => visit(k, d + 1));
744
+ };
745
+ roots.forEach(id => visit(id, 0));
746
+ nodes.forEach(n => { if (!visited.has(n.id)) visit(n.id, 0); }); // any node an edge cycle left unreached becomes its own root
747
+
748
+ const slot = new Map();
749
+ leafOrder.forEach((id, i) => slot.set(id, i));
750
+ const computeSlot = (id, seen) => {
751
+ if (slot.has(id)) return slot.get(id);
752
+ if (seen.has(id)) return 0; // guards a malformed/cyclic edge set
753
+ seen.add(id);
754
+ const kids = childrenMap.get(id) || [];
755
+ const s = kids.reduce((sum, k) => sum + computeSlot(k, seen), 0) / kids.length;
756
+ slot.set(id, s);
757
+ return s;
758
+ };
759
+ nodes.forEach(n => computeSlot(n.id, new Set()));
760
+
761
+ const maxDepth = Math.max(0, ...Array.from(depth.values()));
762
+ const numLeaves = Math.max(1, leafOrder.length);
763
+ const pos = new Map();
764
+ nodes.forEach(n => {
765
+ const d = depth.get(n.id) ?? 0;
766
+ const s = slot.get(n.id) ?? 0;
767
+ const x = numLeaves === 1 ? W / 2 : PAD + (s / (numLeaves - 1)) * (W - 2 * PAD);
768
+ const y = maxDepth === 0 ? H / 2 : PAD + (d / maxDepth) * (H - 2 * PAD);
769
+ pos.set(n.id, { x, y, vx: 0, vy: 0 });
770
+ });
771
+ return pos;
772
+ }
773
+ _computeTreeLayout(nodes, edges, W, H, PAD) {
774
+ return GraphDiagram.computeTreeLayout(nodes, edges, W, H, PAD);
775
+ }
776
+
777
+ static nodeMatchesKeyword(node, keyword, { nodeStructuralKeys, tableNodeStructuralKeys }) {
778
+ const structuralKeys = node.shape === 'table' ? tableNodeStructuralKeys : nodeStructuralKeys;
779
+ const haystack = [LabelHelper.resolveNodeLabel(node), ...LabelHelper.nodeExtraDataLines(node, structuralKeys)].join(' ').toLowerCase();
780
+ return haystack.includes(String(keyword).toLowerCase());
781
+ }
782
+ _nodeMatchesKeyword(node, keyword) {
783
+ return GraphDiagram.nodeMatchesKeyword(node, keyword, {
784
+ nodeStructuralKeys: this.structuralKeys('node'),
785
+ tableNodeStructuralKeys: this.structuralKeys('table'),
786
+ });
787
+ }
788
+
789
+ static hiddenByKeywordFilter(nodes, keyword, childrenMap = null, { nodeStructuralKeys, tableNodeStructuralKeys }) {
790
+ const hidden = new Set();
791
+ if (!keyword) return hidden;
792
+ const matchedIds = nodes.filter(n => GraphDiagram.nodeMatchesKeyword(n, keyword, { nodeStructuralKeys, tableNodeStructuralKeys })).map(n => n.id);
793
+ let keepVisible;
794
+ if (childrenMap) {
795
+ const parentOf = new Map();
796
+ childrenMap.forEach((kids, parent) => kids.forEach(kid => parentOf.set(kid, parent)));
797
+ keepVisible = new Set();
798
+ matchedIds.forEach(id => {
799
+ let cur = id;
800
+ while (cur != null && !keepVisible.has(cur)) {
801
+ keepVisible.add(cur);
802
+ cur = parentOf.get(cur);
803
+ }
804
+ });
805
+ } else {
806
+ keepVisible = new Set(matchedIds);
807
+ }
808
+ nodes.forEach(n => { if (!keepVisible.has(n.id)) hidden.add(n.id); });
809
+ return hidden;
810
+ }
811
+ _hiddenByKeywordFilter(nodes, keyword, childrenMap = null) {
812
+ return GraphDiagram.hiddenByKeywordFilter(nodes, keyword, childrenMap, {
813
+ nodeStructuralKeys: this.structuralKeys('node'),
814
+ tableNodeStructuralKeys: this.structuralKeys('table'),
815
+ });
816
+ }
817
+
818
+ static edgeTooltipText(edge, { edgeStructuralKeys }) {
819
+ const lines = LabelHelper.nodeExtraDataLines(edge, edgeStructuralKeys);
820
+ if (!lines.length) return undefined;
821
+ return [`Edge ${edge.source} → ${edge.target}`, ...lines].join('\n');
822
+ }
823
+ _edgeTooltipText(edge) {
824
+ return GraphDiagram.edgeTooltipText(edge, {
825
+ edgeStructuralKeys: this.structuralKeys('edge'),
826
+ });
827
+ }
828
+
829
+ static generateNodeId(label, existingIds = []) {
830
+ const base = (label || 'node').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-+|-+$)/g, '') || 'node';
831
+ const existing = new Set(existingIds);
832
+ if (!existing.has(base)) return base;
833
+ let i = 2;
834
+ while (existing.has(`${base}-${i}`)) i++;
835
+ return `${base}-${i}`;
836
+ }
837
+ _generateNodeId(label) {
838
+ const existingIds = (this._chartState?.nodes ?? []).map(n => n.id);
839
+ return GraphDiagram.generateNodeId(label, existingIds);
840
+ }
841
+ _insertEdgeWaypoint(edge, point) {
842
+ const sourcePos = this._graph?.pos?.get(edge.source), targetPos = this._graph?.pos?.get(edge.target);
843
+ if (!sourcePos || !targetPos) return;
844
+ const oldWaypoints = edge.waypoints ? edge.waypoints.slice() : [];
845
+ const waypoints = EdgeGeometry.edgeWaypointsWithInsertion(edge, sourcePos, targetPos, point);
846
+ // Find where the new point landed in the recomputed array.
847
+ let insertedIndex = -1;
848
+ for (let i = 0; i < waypoints.length; i++) {
849
+ if (i >= oldWaypoints.length || waypoints[i] !== oldWaypoints[i]) {
850
+ insertedIndex = i;
851
+ break;
852
+ }
853
+ }
854
+ if (insertedIndex < 0) insertedIndex = waypoints.length - 1;
855
+ // Keep edge selected after adding waypoint so endpoints remain visible
856
+ this._selectedEdge = edge;
857
+ this.history.execute(new AddWaypointCommand(edge, waypoints[insertedIndex], { diagram: this, index: insertedIndex }));
858
+ }
859
+ /** The nodes selected together (a selection box, Ctrl-click): their ids. */
860
+ get selectedNodeIds() {
861
+ return this.engine?._selectedNodeIds ?? new Set();
862
+ }
863
+
864
+ /** Select these nodes (or add them, with `extend`), and show it. */
865
+ selectNodes(ids, { extend = false } = {}) {
866
+ if (!this.engine) return;
867
+ const next = extend ? new Set([...this.selectedNodeIds, ...ids]) : new Set(ids);
868
+ this.engine._selectedNodeIds = next;
869
+ this._showNodeSelection(this._graph?.nodeEls ?? []);
870
+ }
871
+
872
+ /** Mark the selected nodes' elements (an outline glow), and unmark the rest. @private */
873
+ _showNodeSelection(nodeEls) {
874
+ const selected = this.selectedNodeIds;
875
+ if (!selected.size && !this._showingSelection) return; // nothing to mark or unmark
876
+ this._showingSelection = selected.size > 0;
877
+ for (const el of nodeEls) {
878
+ if (typeof el?.getAttribute !== 'function') continue;
879
+ const on = selected.has(el.getAttribute('nodeId'));
880
+ el.classList?.toggle('multi-selected', on);
881
+ el.style.filter = on ? 'drop-shadow(0 0 3px #0066cc) drop-shadow(0 0 1px #0066cc)' : '';
882
+ }
883
+ }
884
+
885
+ /**
886
+ * After a click on `target`: unless it is one of the selected line's
887
+ * handles, the line is no longer selected and its handles go.
888
+ * @returns {boolean} whether a line was deselected
889
+ */
890
+ deselectEdgeOnClick(target) {
891
+ if (!this._selectedEdge || target?.closest?.(`[${EDGE_HANDLE_ATTR}]`)) return false;
892
+ this._selectedEdge = null;
893
+ this.renderer.svg?.querySelectorAll(`[${EDGE_HANDLE_ATTR}]`).forEach(n => n.remove());
894
+ return true;
895
+ }
896
+
897
+ _updateGraphLayout() {
898
+ if (!this._graph) return;
899
+
900
+ const { nodes, edges, pos, nodeEls, edgeEls, nodeRadius: R, sizeDomain, sizeOptions } = this._graph;
901
+ const defaultEdgeRouting = this._chartState?.options?.edgeRouting || 'straight';
902
+
903
+ // Update node positions
904
+ nodeEls.forEach(g => {
905
+ const nodeId = g.getAttribute('nodeId');
906
+ const p = pos.get(nodeId);
907
+ g.setAttribute('transform', `translate(${p.x},${p.y})`);
908
+ });
909
+
910
+ const routingObstacles = nodes.map(n => {
911
+ const p = pos.get(n.id);
912
+ if (!p) return null;
913
+ const size = NodeGeometry.nodeSize(n, R, sizeDomain, sizeOptions);
914
+ return { id: n.id, x: p.x, y: p.y, r: Math.max(size.w, size.h) / 2 };
915
+ }).filter(Boolean);
916
+
917
+ // Update edge positions — routes through the same _computeEdgePath the
918
+ // initial draw in graphDiagram() uses, so a live drag frame doesn't
919
+ // visibly snap between two different geometry calculations.
920
+ edges.forEach((edge, i) => {
921
+ const edgeEl = edgeEls[i];
922
+ if (!edgeEl) return;
923
+ const sourceNode = pos.get(edge.source);
924
+ const targetNode = pos.get(edge.target);
925
+ if (!sourceNode || !targetNode) return;
926
+ const sourceInfo = nodes.find(n => n.id === edge.source);
927
+ const targetInfo = nodes.find(n => n.id === edge.target);
928
+ const sourceSize = this.engine._nodeSize(sourceInfo, R, sizeDomain, sizeOptions);
929
+ const targetSize = this.engine._nodeSize(targetInfo, R, sizeDomain, sizeOptions);
930
+ const edgeObstacles = routingObstacles.filter(o => o.id !== edge.source && o.id !== edge.target);
931
+ const { d } = EdgeGeometry.computeEdgePath(
932
+ sourceInfo, sourceNode, sourceSize, targetInfo, targetNode, targetSize, edge, defaultEdgeRouting, edgeObstacles
933
+ );
934
+ edgeEl.setAttribute('d', d);
935
+ });
936
+ }
937
+
938
+ /** Find the edge whose rendered path passes closest to (x,y), within
939
+ * `tolerance` SVG units. Returns the edge object or null. Used to make
940
+ * thin or angled lines easier to select/hover when the renderer's own
941
+ * pixel-exact hitTest misses the actual stroke. */
942
+ findNearestEdge(x, y, tolerance = 14) {
943
+ if (!this._graph) return null;
944
+ const { nodes, edges, pos, nodeRadius: R, sizeDomain, sizeOptions } = this._graph;
945
+ const defaultEdgeRouting = this._chartState?.options?.edgeRouting || 'straight';
946
+ const tol2 = tolerance * tolerance;
947
+
948
+ const routingObstacles = nodes.map(n => {
949
+ const p = pos.get(n.id);
950
+ if (!p) return null;
951
+ const size = NodeGeometry.nodeSize(n, R, sizeDomain, sizeOptions);
952
+ return { id: n.id, x: p.x, y: p.y, r: Math.max(size.w, size.h) / 2 };
953
+ }).filter(Boolean);
954
+
955
+ let bestEdge = null;
956
+ let bestDist2 = Infinity;
957
+
958
+ for (const edge of edges) {
959
+ const sourcePos = pos.get(edge.source);
960
+ const targetPos = pos.get(edge.target);
961
+ if (!sourcePos || !targetPos) continue;
962
+ const sourceInfo = nodes.find(n => n.id === edge.source);
963
+ const targetInfo = nodes.find(n => n.id === edge.target);
964
+ const sourceSize = this.engine._nodeSize(sourceInfo, R, sizeDomain, sizeOptions);
965
+ const targetSize = this.engine._nodeSize(targetInfo, R, sizeDomain, sizeOptions);
966
+ const edgeObstacles = routingObstacles.filter(o => o.id !== edge.source && o.id !== edge.target);
967
+ const { d } = EdgeGeometry.computeEdgePath(
968
+ sourceInfo, sourcePos, sourceSize, targetInfo, targetPos, targetSize, edge, defaultEdgeRouting, edgeObstacles
969
+ );
970
+ const dist2 = EdgeGeometry.pathDistance2(d, x, y);
971
+ if (dist2 < tol2 && dist2 < bestDist2) {
972
+ bestDist2 = dist2;
973
+ bestEdge = edge;
974
+ }
975
+ }
976
+ return bestEdge;
977
+ }
978
+
979
+ _getContextMenu() {
980
+ if (!this._contextMenu && this.engine?.container) {
981
+ this._contextMenu = new ContextMenu(this.engine.container);
982
+ }
983
+ return this._contextMenu;
984
+ }
985
+
986
+ _showNodeContextMenu(event, nodeId) {
987
+ const node = this._graph.nodes.find(n => n.id === nodeId);
988
+ if (!node) return;
989
+
990
+ const userItems = this._graph.contextMenuItems;
991
+ let menuItems;
992
+ if (userItems) {
993
+ menuItems = userItems;
994
+ } else {
995
+ menuItems = [
996
+ {
997
+ label: (node) => this._graphFixedPos.has(node.id) ? 'Unpin Node' : 'Pin Node',
998
+ action: (node) => {
999
+ this._graphFixedPos.has(node.id)
1000
+ ? this._graphFixedPos.delete(node.id)
1001
+ : this._graphFixedPos.add(node.id);
1002
+ }
1003
+ },
1004
+ ...(this._graph.editable ? [{
1005
+ label: 'Rename Node',
1006
+ action: (node) => {
1007
+ const next = window.prompt('Rename node:', node.label ?? '');
1008
+ if (next == null) return;
1009
+ const trimmed = next.trim();
1010
+ if (trimmed === (node.label ?? '')) return;
1011
+ this.history.execute(new RenameNodeCommand(
1012
+ node.id, node.label ?? '', trimmed,
1013
+ { diagram: this, onRename: this._chartState?.options?.onNodeRename }
1014
+ ));
1015
+ }
1016
+ }] : []),
1017
+ {
1018
+ label: 'Show Details in Console',
1019
+ action: (node) => console.log('Node Details:', node)
1020
+ },
1021
+ {
1022
+ label: 'Hide Node',
1023
+ action: (node) => {
1024
+ this._hiddenGraphNodes.add(node.id);
1025
+ this._rerenderGraph();
1026
+ }
1027
+ },
1028
+ {
1029
+ label: 'Delete Node',
1030
+ action: (node) => {
1031
+ const dangling = this._chartState.edges.filter(e => e.source === node.id || e.target === node.id);
1032
+ this.history.execute(new DeleteNodeCommand(node, dangling, { diagram: this }));
1033
+ }
1034
+ }
1035
+ ];
1036
+
1037
+ if (this._hiddenGraphNodes.size > 0) {
1038
+ menuItems.push({ separator: true });
1039
+ menuItems.push({
1040
+ label: 'Show All Hidden Nodes',
1041
+ action: () => {
1042
+ this._hiddenGraphNodes.clear();
1043
+ this._rerenderGraph();
1044
+ }
1045
+ });
1046
+ }
1047
+ }
1048
+
1049
+ this._getContextMenu()?.show(event, menuItems, node, (clientX, clientY) => SvgCoordinate.clientToSvgPoint(this.renderer?.svg, clientX, clientY), this.engine);
1050
+ }
1051
+ _showEdgeContextMenu(event, edge) {
1052
+ const menuItems = this._graph.edgeContextMenuItems || [
1053
+ {
1054
+ label: 'Rename Edge',
1055
+ action: (edge) => {
1056
+ const next = window.prompt('Rename edge:', edge.label ?? '');
1057
+ if (next == null) return;
1058
+ const trimmed = next.trim();
1059
+ if (trimmed === (edge.label ?? '')) return;
1060
+ this.history.execute(new RenameEdgeCommand(
1061
+ edge, edge.label ?? '', trimmed,
1062
+ { diagram: this, onRename: this._chartState?.options?.onEdgeRename }
1063
+ ));
1064
+ }
1065
+ },
1066
+ {
1067
+ label: 'Invert Direction',
1068
+ action: (edge) => {
1069
+ this.history.execute(new InvertEdgeCommand(
1070
+ edge,
1071
+ { diagram: this, onInvert: this._chartState?.options?.onEdgeInvert }
1072
+ ));
1073
+ }
1074
+ },
1075
+ // Where adding a bend point moved to once double-clicking the
1076
+ // line itself was repurposed for onEdgeDoubleClick (see
1077
+ // graphDiagram's own doc comment) — `point` is the right-click
1078
+ // location _buildAndShowContextMenu now passes to every action.
1079
+ { label: 'Add Waypoint Here', action: (edge, engine, point) => this._insertEdgeWaypoint(edge, point) },
1080
+ {
1081
+ label: 'Delete Edge',
1082
+ action: (edge) => {
1083
+ this.history.execute(new DeleteEdgeCommand(
1084
+ edge,
1085
+ { diagram: this, onDelete: this._chartState?.options?.onEdgeDelete }
1086
+ ));
1087
+ }
1088
+ }
1089
+ ];
1090
+ this._getContextMenu()?.show(event, menuItems, edge, (clientX, clientY) => SvgCoordinate.clientToSvgPoint(this.renderer?.svg, clientX, clientY), this.engine);
1091
+ }
1092
+ _showCanvasContextMenu(event) {
1093
+ const point = SvgCoordinate.clientToSvgPoint(this.renderer?.svg, event.clientX, event.clientY);
1094
+ const menuItems = this._graph.canvasContextMenuItems || [
1095
+ {
1096
+ label: 'Add Node',
1097
+ action: (point) => {
1098
+ const label = window.prompt('New node label:', '');
1099
+ if (label == null) return;
1100
+ const trimmed = label.trim();
1101
+ if (!trimmed) return;
1102
+ const gridSize = this._chartState?.options?.gridSize ?? 0;
1103
+ const id = this._generateNodeId(trimmed);
1104
+ const newNode = {
1105
+ id,
1106
+ label: trimmed,
1107
+ x: GraphDiagram.snapToGrid(point.x, gridSize),
1108
+ y: GraphDiagram.snapToGrid(point.y, gridSize),
1109
+ };
1110
+ this.history.execute(new AddNodeCommand(
1111
+ newNode,
1112
+ { diagram: this, onAdd: this._chartState?.options?.onNodeAdd }
1113
+ ));
1114
+ }
1115
+ },
1116
+ {
1117
+ label: 'Add Folder',
1118
+ action: (point) => {
1119
+ const name = window.prompt('New folder name:', 'New Folder');
1120
+ if (name == null) return;
1121
+ const trimmed = name.trim();
1122
+ if (!trimmed) return;
1123
+ const gridSize = this._chartState?.options?.gridSize ?? 0;
1124
+ const folderId = `folder-${Math.random().toString(36).slice(2, 9)}`;
1125
+ const newFolder = {
1126
+ id: folderId,
1127
+ name: trimmed,
1128
+ x: GraphDiagram.snapToGrid(point.x, gridSize),
1129
+ y: GraphDiagram.snapToGrid(point.y, gridSize),
1130
+ width: 200,
1131
+ height: 150,
1132
+ collapsed: false,
1133
+ };
1134
+ // Add to folders in data
1135
+ if (!this.data.folders) this.data.folders = [];
1136
+ this.data.folders.push(newFolder);
1137
+ this._rerenderGraph();
1138
+ }
1139
+ }
1140
+ ];
1141
+ this._getContextMenu()?.show(event, menuItems, point, (clientX, clientY) => SvgCoordinate.clientToSvgPoint(this.renderer?.svg, clientX, clientY), this.engine);
1142
+ }
1143
+ _onLegendClick(groupName) {
1144
+ if (this._hiddenGraphGroups.has(groupName)) {
1145
+ this._hiddenGraphGroups.delete(groupName);
1146
+ } else {
1147
+ this._hiddenGraphGroups.add(groupName);
1148
+ }
1149
+ this._rerenderGraph();
1150
+ }
1151
+
1152
+ /**
1153
+ * Handle clicks on diagram chrome (e.g. the group legend).
1154
+ * @param {MouseEvent} event
1155
+ * @param {object} hitShape
1156
+ */
1157
+ onClick(event, hitShape) {
1158
+ if (hitShape && hitShape.graphLegend) {
1159
+ this._onLegendClick(hitShape.groupName);
1160
+ }
1161
+ }
1162
+ filterGraphNodes(keyword) {
1163
+ this._graphFilterKeyword = keyword ? String(keyword).trim() : '';
1164
+ this._rerenderGraph();
1165
+ }
1166
+
1167
+ /** Snap a coordinate to the nearest grid multiple. `gridSize <= 0` means no
1168
+ * snapping. Exposed as a static helper so drag/add-node code outside this
1169
+ * class can reuse the same rounding rule. */
1170
+ static snapToGrid(value, gridSize) {
1171
+ if (!gridSize || gridSize <= 0) return value;
1172
+ return Math.round(value / gridSize) * gridSize;
1173
+ }
1174
+
1175
+ /** A single path string covering a W×H area with a grid of `gridSize`
1176
+ * spacing — rendered as one cheap background path. */
1177
+ static _gridPath(W, H, gridSize) {
1178
+ const parts = [];
1179
+ for (let x = gridSize; x < W; x += gridSize) parts.push(`M${x},0 L${x},${H}`);
1180
+ for (let y = gridSize; y < H; y += gridSize) parts.push(`M0,${y} L${W},${y}`);
1181
+ return parts.join(' ');
1182
+ }
1183
+
1184
+ _rerenderGraph() {
1185
+ // graphDiagram() reseeds every node's position from `n.x ?? Math.random()`
1186
+ // on each call — nothing else ever writes the live/simulated/dragged
1187
+ // position back onto the node object itself. Without carrying it
1188
+ // forward here, a node being actively dragged would snap back to its
1189
+ // pre-drag spot on the very next re-render (this is called on every
1190
+ // drag mousemove), and any node without an explicit x/y would jump to
1191
+ // a brand-new random position on every hide/show, delete, or legend
1192
+ // toggle. Copying this._graph.pos back onto the node objects before
1193
+ // re-invoking graphDiagram makes it a true "redraw with current state"
1194
+ // instead of "reroll positions and redraw".
1195
+ if (this._graph?.pos) {
1196
+ for (const node of this._chartState.nodes) {
1197
+ const p = this._graph.pos.get(node.id);
1198
+ if (p) { node.x = p.x; node.y = p.y; }
1199
+ }
1200
+ }
1201
+ this.data.nodes = this._chartState.nodes;
1202
+ this.data.edges = this._chartState.edges;
1203
+ this.options = this._chartState.options;
1204
+ this.render();
1205
+ }
1206
+
1207
+ /** Render folder containers (before nodes so they appear behind) */
1208
+ _renderFolders(svg, folders = []) {
1209
+ if (!folders.length || !svg) return;
1210
+ for (const folder of folders) {
1211
+ this._renderFolderContainer(svg, folder);
1212
+ }
1213
+ }
1214
+
1215
+ /** Render a single folder container with border, header, and collapse toggle */
1216
+ _renderFolderContainer(svg, folder) {
1217
+ // A group needs a place; 0 is one.
1218
+ if (!Number.isFinite(folder.x) || !Number.isFinite(folder.y)) return;
1219
+ const HEADER_HEIGHT = 24;
1220
+ const BORDER_COLOR = this._dragOverFolderId === folder.id ? '#44aa44' : '#999';
1221
+ const FILL_COLOR = this._dragOverFolderId === folder.id ? 'rgba(68, 170, 68, 0.15)' : 'rgba(150, 150, 200, 0.08)';
1222
+ const isCollapsed = this._collapsedFolders.has(folder.id);
1223
+
1224
+ // Folder background rectangle
1225
+ const bgRect = this.renderer.rect(
1226
+ folder.x, folder.y, folder.width || 150, folder.height || 100,
1227
+ FILL_COLOR, { color: BORDER_COLOR, width: 1.5 }
1228
+ );
1229
+ svg.appendChild(bgRect);
1230
+
1231
+ // Folder header bar
1232
+ const headerRect = this.renderer.rect(
1233
+ folder.x, folder.y, folder.width || 150, HEADER_HEIGHT,
1234
+ 'rgba(150, 150, 200, 0.3)', { color: BORDER_COLOR, width: 1.5 }
1235
+ );
1236
+ headerRect.setAttribute('folderId', folder.id);
1237
+ headerRect.setAttribute('class', 'folder-header');
1238
+ headerRect.style.cursor = 'pointer';
1239
+ svg.appendChild(headerRect);
1240
+
1241
+ // Collapse/expand indicator
1242
+ const indicator = isCollapsed ? '▶' : '▼';
1243
+ const indicatorText = this.renderer.text(indicator, folder.x + 4, folder.y + 16,
1244
+ { fontSize: '12px', fill: '#333', cursor: 'pointer' }
1245
+ );
1246
+ indicatorText.setAttribute('folderId', folder.id);
1247
+ indicatorText.setAttribute('class', 'folder-toggle');
1248
+ svg.appendChild(indicatorText);
1249
+
1250
+ // Folder name label
1251
+ const label = this.renderer.text(
1252
+ LabelHelper.resolveNodeLabel({ label: folder.name }) || 'Folder',
1253
+ folder.x + 22, folder.y + 16,
1254
+ { fontSize: '12px', fontWeight: 'bold', fill: '#333', cursor: 'pointer' }
1255
+ );
1256
+ label.setAttribute('folderId', folder.id);
1257
+ label.setAttribute('class', 'folder-label');
1258
+ svg.appendChild(label);
1259
+
1260
+ // Add click handlers
1261
+ headerRect.addEventListener('click', (e) => this._onFolderHeaderClick(e, folder.id));
1262
+ headerRect.addEventListener('dblclick', (e) => this._onFolderRename(e, folder));
1263
+ indicatorText.addEventListener('click', (e) => this._onFolderHeaderClick(e, folder.id));
1264
+ label.addEventListener('click', (e) => this._onFolderHeaderClick(e, folder.id));
1265
+ label.addEventListener('dblclick', (e) => this._onFolderRename(e, folder));
1266
+ }
1267
+
1268
+ /** Handle folder header click — toggle collapse state */
1269
+ _onFolderHeaderClick(event, folderId) {
1270
+ event.stopPropagation();
1271
+ if (this._collapsedFolders.has(folderId)) {
1272
+ this._collapsedFolders.delete(folderId);
1273
+ } else {
1274
+ this._collapsedFolders.add(folderId);
1275
+ }
1276
+ this._rerenderGraph();
1277
+ }
1278
+
1279
+ /** Handle folder rename (double-click) */
1280
+ _onFolderRename(event, folder) {
1281
+ event.stopPropagation();
1282
+ const newName = window.prompt('Rename folder:', folder.name || 'Folder');
1283
+ if (newName == null) return;
1284
+ const trimmed = newName.trim();
1285
+ if (!trimmed || trimmed === folder.name) return;
1286
+
1287
+ // Update folder name
1288
+ folder.name = trimmed;
1289
+ this._rerenderGraph();
1290
+ }
1291
+
1292
+ /** Return IDs of all nodes within a given folder (recursively including nested folders) */
1293
+ _getNodesInFolder(folderId) {
1294
+ const nodeIds = new Set();
1295
+ const elementFolderMap = this._chartState?.options?.elementFolderMap;
1296
+ if (!elementFolderMap) return nodeIds;
1297
+
1298
+ const folders = this._chartState?.folders || [];
1299
+
1300
+ // Find all elements in this folder directly
1301
+ for (const [elementId, folderIdForElement] of elementFolderMap.entries()) {
1302
+ if (folderIdForElement === folderId) {
1303
+ nodeIds.add(elementId);
1304
+ }
1305
+ }
1306
+
1307
+ // Recursively add elements in nested folders
1308
+ for (const folder of folders) {
1309
+ if (folder.parentId === folderId) {
1310
+ for (const nodeId of this._getNodesInFolder(folder.id)) {
1311
+ nodeIds.add(nodeId);
1312
+ }
1313
+ }
1314
+ }
1315
+
1316
+ return nodeIds;
1317
+ }
1318
+
1319
+ /** Return IDs of all nodes hidden due to collapsed folders */
1320
+ _getHiddenByCollapsedFolders(folders) {
1321
+ if (!folders.length && !this._collapsedFolders.size) return new Set();
1322
+ const hidden = new Set();
1323
+ for (const folderId of this._collapsedFolders) {
1324
+ const nodesInFolder = this._getNodesInFolder(folderId);
1325
+ for (const nodeId of nodesInFolder) {
1326
+ hidden.add(nodeId);
1327
+ }
1328
+ }
1329
+ return hidden;
1330
+ }
1331
+
1332
+ }
1333
+
1334
+ /**
1335
+ * A font size in pixels: a number, "12px" or "9pt" (1pt = 4/3 px). Undefined when there is none.
1336
+ * @param {number|string} [size]
1337
+ * @returns {number|undefined}
1338
+ */
1339
+ /** A font size in pixels (TextStyle.pixels: points are 4/3 of a pixel). */
1340
+ GraphDiagram.fontPixels = (size) => TextStyle.pixels(size);