@diagrammo/dgmo 0.46.0 → 0.48.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.
- package/dist/advanced.d.ts +3 -855
- package/dist/advanced.js +541 -70107
- package/dist/analyzer-UYKBXAKR.js +15 -0
- package/dist/auto.js +1 -1
- package/dist/auto.mjs +1 -1
- package/dist/block.js +83 -69417
- package/dist/calculator-LDYQCDDY.js +11 -0
- package/dist/charts-d3-QOZHKTCT.js +25 -0
- package/dist/chunk-2DXMZRWJ.js +800 -0
- package/dist/chunk-2LACINWN.js +786 -0
- package/dist/chunk-33LCUH4H.js +893 -0
- package/dist/chunk-3CIOJ5YJ.js +35 -0
- package/dist/chunk-3HDBAXF5.js +86 -0
- package/dist/chunk-3JLKYGBQ.js +290 -0
- package/dist/chunk-3MQQNXPD.js +415 -0
- package/dist/chunk-3R5POCTZ.js +967 -0
- package/dist/chunk-3SMGIJDD.js +112 -0
- package/dist/chunk-44CM72SQ.js +409 -0
- package/dist/chunk-4GRI7MEB.js +1604 -0
- package/dist/chunk-4OHACBGX.js +606 -0
- package/dist/chunk-5DH3KYFH.js +21 -0
- package/dist/chunk-5MLYVTYM.js +76 -0
- package/dist/chunk-5QXL4NYS.js +553 -0
- package/dist/chunk-5WRI5ZAA.js +31 -0
- package/dist/chunk-6EK55ART.js +117 -0
- package/dist/chunk-6FZYCKXB.js +2065 -0
- package/dist/chunk-6HO5ZTXK.js +277 -0
- package/dist/chunk-6KFWC4CO.js +7 -0
- package/dist/chunk-6MTWIQVO.js +121 -0
- package/dist/chunk-6NYWYNBI.js +329 -0
- package/dist/chunk-6SM5NJQH.js +620 -0
- package/dist/chunk-725LDASD.js +422 -0
- package/dist/chunk-76OWZ3BA.js +130 -0
- package/{src/block/parser.ts → dist/chunk-7JEGTRLD.js} +100 -189
- package/dist/chunk-A44USUV7.js +923 -0
- package/dist/chunk-A4GOY3PE.js +25 -0
- package/dist/chunk-A4VFQH5G.js +251 -0
- package/dist/chunk-AELOBFW5.js +375 -0
- package/dist/chunk-AN2MOO5T.js +522 -0
- package/dist/chunk-BMR4CIEO.js +41 -0
- package/dist/chunk-BS7SV7C3.js +51 -0
- package/dist/chunk-BUPDIHML.js +613 -0
- package/dist/chunk-BUVJFRPY.js +887 -0
- package/dist/chunk-C3Z4DX3V.js +112 -0
- package/dist/chunk-CI2G3OEB.js +1818 -0
- package/dist/chunk-CKNVO24D.js +54 -0
- package/dist/chunk-CUVHQHZ7.js +391 -0
- package/dist/chunk-CXK4DZH3.js +114 -0
- package/dist/{editor.cjs → chunk-DKEPS5LH.js} +14 -225
- package/dist/chunk-DU7W62N5.js +487 -0
- package/dist/chunk-DZAQBW35.js +751 -0
- package/dist/chunk-EB6FPQUP.js +316 -0
- package/dist/chunk-EMS2SK26.js +376 -0
- package/dist/chunk-ERLBPDGU.js +510 -0
- package/dist/chunk-EVCVDTXE.js +732 -0
- package/dist/chunk-FCIIHT24.js +384 -0
- package/dist/chunk-FFUN5D66.js +97 -0
- package/dist/chunk-FITOLXSX.js +714 -0
- package/dist/chunk-FMDSF3OV.js +68 -0
- package/dist/chunk-FNB3PNGF.js +410 -0
- package/dist/chunk-G2H74XJZ.js +2104 -0
- package/dist/chunk-GMR5MFHJ.js +339 -0
- package/dist/chunk-H6PI3KJT.js +305 -0
- package/dist/chunk-I46X5VUA.js +59 -0
- package/dist/chunk-I5IGB56C.js +255 -0
- package/dist/chunk-IH6ZMVQ5.js +3950 -0
- package/dist/chunk-IJZFN3HM.js +463 -0
- package/dist/chunk-IPWPWM6R.js +3079 -0
- package/dist/chunk-IYULMRBE.js +23 -0
- package/dist/chunk-IZMRON6C.js +167 -0
- package/dist/chunk-J5FIXQRT.js +813 -0
- package/dist/chunk-JED6ZDEE.js +73 -0
- package/dist/chunk-JEUJOX5X.js +136 -0
- package/dist/chunk-JM4LEPY3.js +436 -0
- package/dist/chunk-L3UHP6TD.js +101 -0
- package/dist/chunk-L66GEFB6.js +524 -0
- package/dist/chunk-LIOHW36Z.js +469 -0
- package/dist/chunk-LP7EC4MI.js +16 -0
- package/dist/chunk-MITI6NOL.js +548 -0
- package/dist/chunk-MO5YHHMQ.js +168 -0
- package/dist/chunk-MZSBFAPJ.js +885 -0
- package/dist/chunk-NNAAJVBU.js +9 -0
- package/dist/chunk-NOTA6TEI.js +47 -0
- package/dist/chunk-NURGB5Z4.js +335 -0
- package/dist/chunk-NX4EQS3U.js +1062 -0
- package/dist/chunk-NXMESY4J.js +1 -0
- package/dist/chunk-OJR3MQYV.js +1458 -0
- package/dist/chunk-OURHMODR.js +454 -0
- package/dist/chunk-P235SVAH.js +1057 -0
- package/dist/chunk-P4RH4OSP.js +1038 -0
- package/dist/chunk-P6T66CCZ.js +226 -0
- package/dist/chunk-PBAHEPLQ.js +223 -0
- package/dist/chunk-PBHE5UG6.js +1530 -0
- package/dist/chunk-PDKUANDZ.js +35 -0
- package/dist/chunk-PWIHZ6NJ.js +296 -0
- package/{src/c4/layout.ts → dist/chunk-QEBUY5NY.js} +384 -1140
- package/dist/chunk-QLCCJEJI.js +253 -0
- package/dist/chunk-QOOUC2J5.js +52 -0
- package/dist/chunk-QYTPPDAO.js +402 -0
- package/dist/chunk-R3XI2VC7.js +801 -0
- package/dist/chunk-RCERWOJR.js +154 -0
- package/dist/chunk-RJE5JTVK.js +362 -0
- package/dist/chunk-RTLBSQ3G.js +1155 -0
- package/dist/chunk-RUNNI6VE.js +448 -0
- package/dist/chunk-RVMRZAPB.js +354 -0
- package/dist/chunk-RYEGFMUO.js +955 -0
- package/dist/chunk-RZCF2VVM.js +528 -0
- package/dist/chunk-SML67VPV.js +1025 -0
- package/dist/chunk-SQ7IY53Q.js +44 -0
- package/dist/chunk-SQAG3UXH.js +161 -0
- package/dist/chunk-SRTU6HMQ.js +1851 -0
- package/dist/chunk-T2OE3YQB.js +550 -0
- package/dist/chunk-T3UBLACL.js +299 -0
- package/dist/chunk-TLVIHFRO.js +296 -0
- package/dist/chunk-TYS6CSS5.js +52 -0
- package/dist/chunk-U6EQI3U6.js +130 -0
- package/dist/chunk-UJ4FJLBF.js +86 -0
- package/dist/chunk-ULOEATBZ.js +347 -0
- package/dist/chunk-UNCCZ3NU.js +127 -0
- package/dist/chunk-USZW2HHU.js +197 -0
- package/dist/chunk-UWN33VNX.js +1610 -0
- package/dist/chunk-UZ7EZW3B.js +40 -0
- package/dist/chunk-VOKGH627.js +37 -0
- package/dist/chunk-WBRZJOEC.js +204 -0
- package/dist/chunk-WCSOJFU3.js +380 -0
- package/dist/chunk-WCT3NYCV.js +160 -0
- package/dist/chunk-WVAJFAIA.js +529 -0
- package/dist/chunk-X5LCYCL5.js +394 -0
- package/dist/chunk-XAKEUMEJ.js +1256 -0
- package/dist/chunk-Y2ZE3MSG.js +590 -0
- package/dist/chunk-Y3J7VR52.js +646 -0
- package/dist/chunk-Y5QGVTVC.js +33 -0
- package/dist/chunk-YAA6IID3.js +548 -0
- package/dist/chunk-YGJTDXVR.js +692 -0
- package/{src/utils/time-ticks.ts → dist/chunk-YRYKTYMQ.js} +45 -112
- package/dist/chunk-YYVTBZZC.js +61 -0
- package/dist/chunk-Z5CGSFJB.js +647 -0
- package/{src/gantt/calculator.ts → dist/chunk-Z72XRXD4.js} +236 -428
- package/dist/chunk-ZGR7ULFJ.js +978 -0
- package/dist/chunk-ZJEL4H2R.js +851 -0
- package/dist/chunk-ZO27FGJS.js +554 -0
- package/dist/collapse-CLPJD2RL.js +11 -0
- package/dist/collapse-SLINJQW6.js +9 -0
- package/dist/collapse-ZWUFFCK2.js +9 -0
- package/dist/compute-G55RG53N.js +8 -0
- package/dist/dgmo-router-DZCIDUO5.js +73 -0
- package/dist/dimensions-J3FHIMJ6.js +22 -0
- package/dist/element.js +1 -1
- package/dist/element.mjs +1 -1
- package/dist/flowchart-parser-ZNSKIBVR.js +21 -0
- package/dist/flowchart-renderer-ZAEOL4DX.js +35 -0
- package/dist/index.d.ts +3 -469
- package/dist/index.js +137 -69685
- package/dist/layout-46N4JJYK.js +17 -0
- package/dist/layout-5RA6QWR6.js +26 -0
- package/dist/layout-5T6RH5M6.js +14 -0
- package/dist/layout-7QXCNOVS.js +13 -0
- package/dist/layout-EE6GE46Y.js +17 -0
- package/dist/layout-G23F7LDH.js +9 -0
- package/dist/layout-HRBZPFCN.js +23 -0
- package/dist/layout-HWDSRX5E.js +20 -0
- package/dist/layout-HYZPWAK5.js +18 -0
- package/dist/layout-N76NR4ZO.js +19 -0
- package/dist/layout-OB3RI63S.js +14 -0
- package/dist/layout-S2QBYSK2.js +15 -0
- package/dist/layout-UM5F4DJB.js +15 -0
- package/dist/layout-search-OYV77HHS.js +1655 -0
- package/dist/load-data-EHDDZ5ZS.js +8 -0
- package/dist/palettes-PRJ2RI6D.js +56 -0
- package/dist/parser-2MFP2H2E.js +11 -0
- package/dist/parser-3STE62GB.js +21 -0
- package/dist/parser-4FL2CMDG.js +18 -0
- package/dist/parser-4OGOBJJ4.js +13 -0
- package/dist/parser-6BTRLKRZ.js +15 -0
- package/dist/parser-AQETEVRW.js +21 -0
- package/dist/parser-CFZD5IBL.js +12 -0
- package/dist/parser-FUSKFGZN.js +16 -0
- package/dist/parser-I6KXEYFK.js +13 -0
- package/dist/parser-IXSZZQHY.js +18 -0
- package/dist/parser-JAL2HD4I.js +13 -0
- package/dist/parser-KNSQSKQO.js +20 -0
- package/dist/parser-N5O4IHUN.js +13 -0
- package/dist/parser-NGU3ETO6.js +16 -0
- package/dist/parser-NRAI6O25.js +19 -0
- package/dist/parser-O5J4VBII.js +11 -0
- package/dist/parser-OV7NOBW6.js +21 -0
- package/dist/parser-PQ3POX45.js +23 -0
- package/dist/parser-TFZFBVWN.js +11 -0
- package/dist/parser-TRTHDFPP.js +18 -0
- package/dist/parser-VQP2UJ6W.js +11 -0
- package/dist/parser-XDVELHRK.js +13 -0
- package/dist/parser-XKRP7QKP.js +11 -0
- package/dist/parser-Y6WTBU76.js +14 -0
- package/dist/parser-ZSDPZYQP.js +12 -0
- package/dist/renderer-2XQQQ6QR.js +28 -0
- package/dist/renderer-5NDVNDAZ.js +26 -0
- package/dist/renderer-6EEZHMH4.js +26 -0
- package/dist/renderer-6W5462RB.js +43 -0
- package/dist/renderer-7MVLWWQD.js +22 -0
- package/dist/renderer-AWWAF6XD.js +33 -0
- package/dist/renderer-BYCNMBF7.js +34 -0
- package/dist/renderer-CLJHQPBJ.js +29 -0
- package/dist/renderer-GHPZMFU2.js +15 -0
- package/dist/renderer-JGBKUCZD.js +16 -0
- package/dist/renderer-KQB7UGDS.js +19 -0
- package/dist/renderer-PGX2TKVS.js +24 -0
- package/dist/renderer-QIWVHWZO.js +15 -0
- package/dist/renderer-RPOA5YPZ.js +35 -0
- package/dist/renderer-SMIBCRVB.js +24 -0
- package/dist/renderer-T7YKC5VP.js +28 -0
- package/dist/renderer-TOCOVLHS.js +37 -0
- package/dist/renderer-UWA5XS74.js +39 -0
- package/dist/renderer-VACVQYML.js +25 -0
- package/dist/renderer-W4M6QLIJ.js +37 -0
- package/dist/renderer-WZRREQ2P.js +23 -0
- package/dist/renderer-YY2OSF6E.js +27 -0
- package/dist/renderer-Z7I3W2PP.js +24 -0
- package/dist/renderer-ZGNTUWC4.js +22 -0
- package/dist/renderer-ZTR3DER3.js +19 -0
- package/dist/renderer-radial-C32GPJQ7.js +21 -0
- package/dist/resolver-NOAA7ID7.js +13 -0
- package/dist/state-collapse-HCGD6IVJ.js +8 -0
- package/dist/state-parser-Z2V23ATR.js +18 -0
- package/dist/state-renderer-RFYHSP4K.js +34 -0
- package/dist/themes-1-CuKpeH.d.ts +908 -0
- package/package.json +19 -70
- package/dist/advanced.cjs +0 -71648
- package/dist/advanced.d.cts +0 -6571
- package/dist/auto.cjs +0 -70648
- package/dist/auto.d.cts +0 -54
- package/dist/block.cjs +0 -70208
- package/dist/block.d.cts +0 -115
- package/dist/editor.d.cts +0 -26
- package/dist/element.cjs +0 -70530
- package/dist/element.d.cts +0 -62
- package/dist/highlight.cjs +0 -852
- package/dist/highlight.d.cts +0 -49
- package/dist/index.cjs +0 -70491
- package/dist/index.d.cts +0 -626
- package/dist/pert.cjs +0 -325
- package/dist/pert.d.cts +0 -559
- package/src/advanced.ts +0 -873
- package/src/arc/parser.ts +0 -15
- package/src/arc/renderer.ts +0 -633
- package/src/auto/index.ts +0 -621
- package/src/auto/shared.ts +0 -370
- package/src/auto/styles.ts +0 -73
- package/src/block/layout.ts +0 -198
- package/src/block/renderer.ts +0 -484
- package/src/block/types.ts +0 -67
- package/src/boxes-and-lines/collapse.ts +0 -96
- package/src/boxes-and-lines/focus.ts +0 -257
- package/src/boxes-and-lines/label-placement.ts +0 -339
- package/src/boxes-and-lines/layout-grouped.ts +0 -912
- package/src/boxes-and-lines/layout-layered.ts +0 -722
- package/src/boxes-and-lines/layout-search.ts +0 -1678
- package/src/boxes-and-lines/layout.ts +0 -398
- package/src/boxes-and-lines/parser.ts +0 -1181
- package/src/boxes-and-lines/renderer.ts +0 -1465
- package/src/boxes-and-lines/types.ts +0 -66
- package/src/c4/parser.ts +0 -1158
- package/src/c4/renderer.ts +0 -2008
- package/src/c4/types.ts +0 -89
- package/src/chart-type-registry.ts +0 -529
- package/src/chart-types.ts +0 -225
- package/src/chart.ts +0 -685
- package/src/charts-d3/bar.ts +0 -338
- package/src/charts-d3/chord.ts +0 -140
- package/src/charts-d3/function.ts +0 -147
- package/src/charts-d3/funnel.ts +0 -104
- package/src/charts-d3/heatmap.ts +0 -145
- package/src/charts-d3/index.ts +0 -337
- package/src/charts-d3/interactions.ts +0 -477
- package/src/charts-d3/line.ts +0 -344
- package/src/charts-d3/mount.ts +0 -151
- package/src/charts-d3/pie.ts +0 -117
- package/src/charts-d3/polar.ts +0 -89
- package/src/charts-d3/radar.ts +0 -0
- package/src/charts-d3/sankey.ts +0 -0
- package/src/charts-d3/scatter.ts +0 -185
- package/src/charts-d3/shared.ts +0 -208
- package/src/class/layout.ts +0 -326
- package/src/class/parser.ts +0 -635
- package/src/class/renderer.ts +0 -793
- package/src/class/types.ts +0 -58
- package/src/cli-banner.ts +0 -106
- package/src/cli.ts +0 -1080
- package/src/colors-diagnostics.ts +0 -35
- package/src/colors.ts +0 -379
- package/src/completion-types.ts +0 -27
- package/src/completion.ts +0 -2668
- package/src/cycle/layout.ts +0 -1026
- package/src/cycle/parser.ts +0 -387
- package/src/cycle/renderer.ts +0 -653
- package/src/cycle/types.ts +0 -101
- package/src/d3.ts +0 -1434
- package/src/data-chart-parser.ts +0 -1007
- package/src/dgmo-router.ts +0 -296
- package/src/diagnostics-registry.ts +0 -108
- package/src/diagnostics.ts +0 -0
- package/src/dimensions.ts +0 -20
- package/src/directives-registry.ts +0 -396
- package/src/editor/dgmo.grammar +0 -76
- package/src/editor/dgmo.grammar.d.ts +0 -2
- package/src/editor/dgmo.grammar.js +0 -18
- package/src/editor/dgmo.grammar.terms.d.ts +0 -5
- package/src/editor/dgmo.grammar.terms.js +0 -35
- package/src/editor/highlight-api.ts +0 -596
- package/src/editor/highlight.ts +0 -36
- package/src/editor/index.ts +0 -27
- package/src/editor/keywords.ts +0 -128
- package/src/editor/tokens.ts +0 -30
- package/src/element/index.ts +0 -432
- package/src/embed/css.ts +0 -312
- package/src/embed/escape.ts +0 -15
- package/src/embed/index.ts +0 -280
- package/src/er/classify.ts +0 -228
- package/src/er/layout.ts +0 -530
- package/src/er/parser.ts +0 -750
- package/src/er/renderer.ts +0 -725
- package/src/er/types.ts +0 -52
- package/src/error-card.ts +0 -269
- package/src/event-line/diagnostics.ts +0 -117
- package/src/event-line/parser.ts +0 -643
- package/src/event-line/renderer.ts +0 -1742
- package/src/event-line/types.ts +0 -74
- package/src/fonts.ts +0 -3
- package/src/gantt/parser.ts +0 -1641
- package/src/gantt/renderer.ts +0 -3787
- package/src/gantt/resolver.ts +0 -172
- package/src/gantt/types.ts +0 -206
- package/src/graph/diagnostics.ts +0 -64
- package/src/graph/edge-spline.ts +0 -29
- package/src/graph/flowchart-parser.ts +0 -759
- package/src/graph/flowchart-renderer.ts +0 -817
- package/src/graph/layout.ts +0 -472
- package/src/graph/notes.ts +0 -21
- package/src/graph/state-collapse.ts +0 -78
- package/src/graph/state-parser.ts +0 -574
- package/src/graph/state-renderer.ts +0 -653
- package/src/graph/types.ts +0 -65
- package/src/index.ts +0 -261
- package/src/infra/compute.ts +0 -1385
- package/src/infra/layout.ts +0 -888
- package/src/infra/parser.ts +0 -1234
- package/src/infra/renderer.ts +0 -2534
- package/src/infra/roles.ts +0 -75
- package/src/infra/types.ts +0 -216
- package/src/infra/validation.ts +0 -201
- package/src/journey-map/layout.ts +0 -490
- package/src/journey-map/parser.ts +0 -557
- package/src/journey-map/renderer.ts +0 -1580
- package/src/journey-map/types.ts +0 -47
- package/src/kanban/mutations.ts +0 -175
- package/src/kanban/parser.ts +0 -509
- package/src/kanban/renderer.ts +0 -1449
- package/src/kanban/types.ts +0 -39
- package/src/label-layout.ts +0 -368
- package/src/map/colorize.ts +0 -54
- package/src/map/completion.ts +0 -316
- package/src/map/context-labels.ts +0 -609
- package/src/map/data/PROVENANCE.json +0 -1
- package/src/map/data/README.md +0 -40
- package/src/map/data/airport-collisions.json +0 -1
- package/src/map/data/airports.json +0 -1
- package/src/map/data/gazetteer.json +0 -1
- package/src/map/data/lakes.json +0 -1
- package/src/map/data/mountain-ranges.json +0 -1
- package/src/map/data/na-lakes.json +0 -1
- package/src/map/data/na-land.json +0 -1
- package/src/map/data/region-names.json +0 -1
- package/src/map/data/rivers.json +0 -1
- package/src/map/data/types.ts +0 -121
- package/src/map/data/us-states.json +0 -1
- package/src/map/data/water-bodies.json +0 -1
- package/src/map/data/world-coarse.json +0 -1
- package/src/map/data/world-detail.json +0 -1
- package/src/map/diagnostics.ts +0 -134
- package/src/map/dimensions.ts +0 -150
- package/src/map/geo-query.ts +0 -295
- package/src/map/geo.ts +0 -438
- package/src/map/invert.ts +0 -111
- package/src/map/layout.ts +0 -5069
- package/src/map/legend-band.ts +0 -127
- package/src/map/load-data.ts +0 -186
- package/src/map/parser.ts +0 -783
- package/src/map/renderer.ts +0 -1445
- package/src/map/resolved-types.ts +0 -160
- package/src/map/resolver.ts +0 -1146
- package/src/map/types.ts +0 -194
- package/src/mindmap/collapse.ts +0 -67
- package/src/mindmap/layout.ts +0 -649
- package/src/mindmap/parser.ts +0 -430
- package/src/mindmap/renderer.ts +0 -637
- package/src/mindmap/text-wrap.ts +0 -218
- package/src/mindmap/types.ts +0 -56
- package/src/org/collapse.ts +0 -151
- package/src/org/layout.ts +0 -1262
- package/src/org/parser.ts +0 -519
- package/src/org/renderer.ts +0 -866
- package/src/org/resolver.ts +0 -636
- package/src/palettes/atlas.ts +0 -77
- package/src/palettes/blueprint.ts +0 -73
- package/src/palettes/catppuccin.ts +0 -84
- package/src/palettes/color-utils.ts +0 -443
- package/src/palettes/index.ts +0 -67
- package/src/palettes/nord.ts +0 -67
- package/src/palettes/registry.ts +0 -114
- package/src/palettes/slate.ts +0 -73
- package/src/palettes/tidewater.ts +0 -73
- package/src/palettes/tokyo-night.ts +0 -86
- package/src/palettes/types.ts +0 -88
- package/src/pert/analyzer.ts +0 -1183
- package/src/pert/diagnostics.ts +0 -110
- package/src/pert/index.ts +0 -43
- package/src/pert/internal.ts +0 -252
- package/src/pert/layout.ts +0 -971
- package/src/pert/monte-carlo.ts +0 -446
- package/src/pert/parser.ts +0 -1606
- package/src/pert/renderer.ts +0 -4009
- package/src/pert/share-normalize.ts +0 -62
- package/src/pert/types.ts +0 -604
- package/src/pyramid/parser.ts +0 -185
- package/src/pyramid/renderer.ts +0 -768
- package/src/pyramid/types.ts +0 -28
- package/src/quadrant/parser.ts +0 -18
- package/src/quadrant/renderer.ts +0 -638
- package/src/raci/diagnostics.ts +0 -213
- package/src/raci/index.ts +0 -23
- package/src/raci/mutations.ts +0 -321
- package/src/raci/parser.ts +0 -699
- package/src/raci/renderer.ts +0 -1627
- package/src/raci/types.ts +0 -82
- package/src/raci/variants.ts +0 -320
- package/src/render.ts +0 -268
- package/src/ring/parser.ts +0 -212
- package/src/ring/renderer.ts +0 -490
- package/src/ring/types.ts +0 -27
- package/src/sequence/collapse.ts +0 -174
- package/src/sequence/parser.ts +0 -1596
- package/src/sequence/participant-inference.ts +0 -152
- package/src/sequence/renderer.ts +0 -3168
- package/src/sequence/tag-resolution.ts +0 -164
- package/src/sharing.ts +0 -211
- package/src/sitemap/collapse.ts +0 -100
- package/src/sitemap/layout.ts +0 -925
- package/src/sitemap/parser.ts +0 -755
- package/src/sitemap/renderer.ts +0 -828
- package/src/sitemap/types.ts +0 -42
- package/src/slope/parser.ts +0 -18
- package/src/slope/renderer.ts +0 -378
- package/src/swimlane/diagnostics.ts +0 -94
- package/src/swimlane/layout.ts +0 -597
- package/src/swimlane/parser.ts +0 -613
- package/src/swimlane/renderer.ts +0 -681
- package/src/swimlane/types.ts +0 -151
- package/src/tech-radar/interactive.ts +0 -1129
- package/src/tech-radar/layout.ts +0 -197
- package/src/tech-radar/parser.ts +0 -436
- package/src/tech-radar/renderer.ts +0 -1239
- package/src/tech-radar/shared.ts +0 -151
- package/src/tech-radar/types.ts +0 -86
- package/src/themes.ts +0 -22
- package/src/timeline/parser.ts +0 -472
- package/src/timeline/renderer.ts +0 -3024
- package/src/timeline/types.ts +0 -33
- package/src/timeline/viz-parser.ts +0 -21
- package/src/treemap/diagnostics.ts +0 -62
- package/src/treemap/layout-radial.ts +0 -233
- package/src/treemap/layout.ts +0 -189
- package/src/treemap/parser.ts +0 -463
- package/src/treemap/renderer-radial.ts +0 -438
- package/src/treemap/renderer.ts +0 -572
- package/src/treemap/treemap-shared.ts +0 -309
- package/src/treemap/types.ts +0 -60
- package/src/utils/arrow-markers.ts +0 -51
- package/src/utils/arrows-diagnostics.ts +0 -62
- package/src/utils/arrows.ts +0 -227
- package/src/utils/brand.ts +0 -61
- package/src/utils/card.ts +0 -183
- package/src/utils/collapse-engine/tree-edges.ts +0 -115
- package/src/utils/collapse-engine/tree.ts +0 -117
- package/src/utils/d3-helpers.ts +0 -219
- package/src/utils/d3-types.ts +0 -9
- package/src/utils/description-helpers.ts +0 -38
- package/src/utils/duration.ts +0 -312
- package/src/utils/export-container.ts +0 -41
- package/src/utils/expr-eval.ts +0 -27
- package/src/utils/fit-canvas.ts +0 -64
- package/src/utils/hover-styles.ts +0 -673
- package/src/utils/inline-markdown.ts +0 -96
- package/src/utils/legend-constants.ts +0 -48
- package/src/utils/legend-d3.ts +0 -618
- package/src/utils/legend-integration.ts +0 -107
- package/src/utils/legend-layout.ts +0 -776
- package/src/utils/legend-svg.ts +0 -214
- package/src/utils/legend-types.ts +0 -272
- package/src/utils/name-normalize.ts +0 -163
- package/src/utils/note-box/constants.ts +0 -25
- package/src/utils/note-box/index.ts +0 -11
- package/src/utils/note-box/metrics.ts +0 -90
- package/src/utils/note-box/svg.ts +0 -331
- package/src/utils/notes/bounds.ts +0 -30
- package/src/utils/notes/build.ts +0 -131
- package/src/utils/notes/index.ts +0 -18
- package/src/utils/notes/model.ts +0 -19
- package/src/utils/notes/parse.ts +0 -131
- package/src/utils/notes/place.ts +0 -177
- package/src/utils/notes/resolve.ts +0 -88
- package/src/utils/number-format.ts +0 -36
- package/src/utils/parsing.ts +0 -953
- package/src/utils/reserved-key-registry.ts +0 -239
- package/src/utils/safe-href.ts +0 -38
- package/src/utils/scaling.ts +0 -97
- package/src/utils/svg-embed.ts +0 -251
- package/src/utils/tag-groups.ts +0 -607
- package/src/utils/text-measure.ts +0 -122
- package/src/utils/title-constants.ts +0 -24
- package/src/utils/visual-conventions.ts +0 -61
- package/src/utils/wrapped-desc.ts +0 -87
- package/src/venn/parser.ts +0 -18
- package/src/venn/renderer.ts +0 -1047
- package/src/version-control/diagnostics.ts +0 -67
- package/src/version-control/parser.ts +0 -544
- package/src/version-control/renderer.ts +0 -724
- package/src/version-control/types.ts +0 -100
- package/src/visualizations/parse.ts +0 -1560
- package/src/visualizations/types.ts +0 -262
- package/src/wireframe/layout.ts +0 -479
- package/src/wireframe/parser.ts +0 -1114
- package/src/wireframe/renderer.ts +0 -1318
- package/src/wireframe/types.ts +0 -110
- package/src/wordcloud/parser.ts +0 -18
- package/src/wordcloud/renderer.ts +0 -357
|
@@ -1,1678 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Boxes and Lines — experimental "search" layout (behind a flag)
|
|
3
|
-
// ============================================================
|
|
4
|
-
//
|
|
5
|
-
// dagre placement + spline routing, with a multi-seed search over node
|
|
6
|
-
// orderings, scored on the actual spline geometry (curveBasis, sampled
|
|
7
|
-
// headlessly — no DOM) plus an optional stability term (drift from the
|
|
8
|
-
// previous layout). Picks the lowest combined-score ordering.
|
|
9
|
-
//
|
|
10
|
-
// Rationale: dagre placement reads better than orthogonal routing; crossings
|
|
11
|
-
// come from within-layer ordering, so we search orderings rather than reroute.
|
|
12
|
-
// Sync (dagre) — no ELK, no async.
|
|
13
|
-
|
|
14
|
-
import dagre from '@dagrejs/dagre';
|
|
15
|
-
import { line as d3line, curveBasis } from 'd3-shape';
|
|
16
|
-
import type { ParsedBoxesAndLines, BLGroup } from './types';
|
|
17
|
-
import {
|
|
18
|
-
computeNodeSize,
|
|
19
|
-
NODE_WIDTH,
|
|
20
|
-
NODE_HEIGHT,
|
|
21
|
-
type BLLayoutResult,
|
|
22
|
-
type BLLayoutEdge,
|
|
23
|
-
} from './layout';
|
|
24
|
-
import { layeredCandidates } from './layout-layered';
|
|
25
|
-
import { groupedTierCandidates } from './layout-grouped';
|
|
26
|
-
import { measureEdgeLabel, EDGE_LABEL_FONT_SIZE } from './label-placement';
|
|
27
|
-
|
|
28
|
-
type Pt = { x: number; y: number };
|
|
29
|
-
|
|
30
|
-
// Default stability weight: combined = crossings + lambda · (meanDriftPx / 100).
|
|
31
|
-
// Only applies when previousPositions is supplied (re-layout on edit/collapse).
|
|
32
|
-
const DEFAULT_LAMBDA = 4;
|
|
33
|
-
|
|
34
|
-
// Adaptive escalation thresholds (see layoutBoxesAndLinesSearch). Only graphs
|
|
35
|
-
// whose base-budget badness is at least ESCALATE_THRESHOLD spend an extra seed
|
|
36
|
-
// batch; ESCALATE_MAX_N bounds it so huge graphs don't blow the time budget.
|
|
37
|
-
const ESCALATE_THRESHOLD = 4;
|
|
38
|
-
const ESCALATE_MAX_N = 45;
|
|
39
|
-
const ESCALATE_SEEDS = 18;
|
|
40
|
-
const ESCALATE_REFINE = 10;
|
|
41
|
-
|
|
42
|
-
function rng(s: number) {
|
|
43
|
-
return () => {
|
|
44
|
-
s |= 0;
|
|
45
|
-
s = (s + 0x6d2b79f5) | 0;
|
|
46
|
-
let t = Math.imul(s ^ (s >>> 15), 1 | s);
|
|
47
|
-
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
48
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
function shuffle<T>(a: readonly T[], r: () => number): T[] {
|
|
52
|
-
const x = a.slice();
|
|
53
|
-
for (let i = x.length - 1; i > 0; i--) {
|
|
54
|
-
const j = Math.floor(r() * (i + 1));
|
|
55
|
-
[x[i], x[j]] = [x[j]!, x[i]!];
|
|
56
|
-
}
|
|
57
|
-
return x;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const splineGen = d3line<Pt>()
|
|
61
|
-
.x((d) => d.x)
|
|
62
|
-
.y((d) => d.y)
|
|
63
|
-
.curve(curveBasis);
|
|
64
|
-
|
|
65
|
-
// Path tokenizer for `flatten` — module-scope so it isn't recompiled per call.
|
|
66
|
-
// String.match with a /g regex is stateless (ignores lastIndex), so a shared
|
|
67
|
-
// instance is safe to reuse.
|
|
68
|
-
const PATH_TOKEN_RE = /[MLQC]|-?\d*\.?\d+(?:e-?\d+)?/gi;
|
|
69
|
-
// flatten an SVG path "d" (M/L/Q/C) into a polyline for crossing detection
|
|
70
|
-
function flatten(d: string): Pt[] {
|
|
71
|
-
const toks = d.match(PATH_TOKEN_RE) ?? [];
|
|
72
|
-
const pts: Pt[] = [];
|
|
73
|
-
let i = 0,
|
|
74
|
-
cx = 0,
|
|
75
|
-
cy = 0,
|
|
76
|
-
cmd = '';
|
|
77
|
-
const num = () => parseFloat(toks[i++]!);
|
|
78
|
-
const samp = (p0: Pt, c1: Pt, c2: Pt | null, p1: Pt) => {
|
|
79
|
-
for (let t = 0; t <= 1; t += 0.12) {
|
|
80
|
-
const u = 1 - t;
|
|
81
|
-
if (c2)
|
|
82
|
-
pts.push({
|
|
83
|
-
x:
|
|
84
|
-
u * u * u * p0.x +
|
|
85
|
-
3 * u * u * t * c1.x +
|
|
86
|
-
3 * u * t * t * c2.x +
|
|
87
|
-
t * t * t * p1.x,
|
|
88
|
-
y:
|
|
89
|
-
u * u * u * p0.y +
|
|
90
|
-
3 * u * u * t * c1.y +
|
|
91
|
-
3 * u * t * t * c2.y +
|
|
92
|
-
t * t * t * p1.y,
|
|
93
|
-
});
|
|
94
|
-
else
|
|
95
|
-
pts.push({
|
|
96
|
-
x: u * u * p0.x + 2 * u * t * c1.x + t * t * p1.x,
|
|
97
|
-
y: u * u * p0.y + 2 * u * t * c1.y + t * t * p1.y,
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
while (i < toks.length) {
|
|
102
|
-
const tk = toks[i]!;
|
|
103
|
-
if (/[MLQC]/i.test(tk)) {
|
|
104
|
-
cmd = tk;
|
|
105
|
-
i++;
|
|
106
|
-
}
|
|
107
|
-
if (cmd === 'M' || cmd === 'L') {
|
|
108
|
-
const x = num(),
|
|
109
|
-
y = num();
|
|
110
|
-
pts.push({ x, y });
|
|
111
|
-
cx = x;
|
|
112
|
-
cy = y;
|
|
113
|
-
} else if (cmd === 'Q') {
|
|
114
|
-
const c1 = { x: num(), y: num() },
|
|
115
|
-
p1 = { x: num(), y: num() };
|
|
116
|
-
samp({ x: cx, y: cy }, c1, null, p1);
|
|
117
|
-
cx = p1.x;
|
|
118
|
-
cy = p1.y;
|
|
119
|
-
} else if (cmd === 'C') {
|
|
120
|
-
const c1 = { x: num(), y: num() },
|
|
121
|
-
c2 = { x: num(), y: num() },
|
|
122
|
-
p1 = { x: num(), y: num() };
|
|
123
|
-
samp({ x: cx, y: cy }, c1, c2, p1);
|
|
124
|
-
cx = p1.x;
|
|
125
|
-
cy = p1.y;
|
|
126
|
-
} else i++;
|
|
127
|
-
}
|
|
128
|
-
return pts;
|
|
129
|
-
}
|
|
130
|
-
// Flattened edge polyline + its bbox. Building it requires an SVG-string
|
|
131
|
-
// round-trip (d3 spline → regex parse), so it's memoized per layout: every
|
|
132
|
-
// candidate is scored by countSplineCrossings + countEdgeOverlaps +
|
|
133
|
-
// countEdgeNodePierces, which would otherwise each re-flatten all edges.
|
|
134
|
-
type FlatPoly = { pts: Pt[]; x0: number; y0: number; x1: number; y1: number };
|
|
135
|
-
const FLAT_CACHE = new WeakMap<object, FlatPoly[]>();
|
|
136
|
-
function flatPolys(layout: BLLayoutResult): FlatPoly[] {
|
|
137
|
-
const key = layout.edges as unknown as object;
|
|
138
|
-
const hit = FLAT_CACHE.get(key);
|
|
139
|
-
if (hit) return hit;
|
|
140
|
-
const polys = layout.edges.map((e) => {
|
|
141
|
-
const pts =
|
|
142
|
-
e.points.length >= 2 ? flatten(splineGen(e.points as Pt[]) ?? '') : [];
|
|
143
|
-
let x0 = Infinity,
|
|
144
|
-
y0 = Infinity,
|
|
145
|
-
x1 = -Infinity,
|
|
146
|
-
y1 = -Infinity;
|
|
147
|
-
for (const p of pts) {
|
|
148
|
-
if (p.x < x0) x0 = p.x;
|
|
149
|
-
if (p.x > x1) x1 = p.x;
|
|
150
|
-
if (p.y < y0) y0 = p.y;
|
|
151
|
-
if (p.y > y1) y1 = p.y;
|
|
152
|
-
}
|
|
153
|
-
return { pts, x0, y0, x1, y1 };
|
|
154
|
-
});
|
|
155
|
-
FLAT_CACHE.set(key, polys);
|
|
156
|
-
return polys;
|
|
157
|
-
}
|
|
158
|
-
function segPoint(p1: Pt, p2: Pt, p3: Pt, p4: Pt): Pt | null {
|
|
159
|
-
const den = (p2.x - p1.x) * (p4.y - p3.y) - (p2.y - p1.y) * (p4.x - p3.x);
|
|
160
|
-
if (Math.abs(den) < 1e-9) return null;
|
|
161
|
-
const t =
|
|
162
|
-
((p3.x - p1.x) * (p4.y - p3.y) - (p3.y - p1.y) * (p4.x - p3.x)) / den,
|
|
163
|
-
u = ((p3.x - p1.x) * (p2.y - p1.y) - (p3.y - p1.y) * (p2.x - p1.x)) / den;
|
|
164
|
-
return t > 0 && t < 1 && u > 0 && u < 1
|
|
165
|
-
? { x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y) }
|
|
166
|
-
: null;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// trustworthy crossing count on the spline geometry: exclude intersections
|
|
170
|
-
// near a genuinely shared endpoint node; cluster near-duplicate hits.
|
|
171
|
-
// Exported so the playground + benchmark score with the SAME counter the
|
|
172
|
-
// engine optimizes against.
|
|
173
|
-
export function countSplineCrossings(
|
|
174
|
-
layout: BLLayoutResult,
|
|
175
|
-
/** Abort early once the running total exceeds this (the caller only needs to
|
|
176
|
-
* know X has passed the best-so-far badness — the exact value no longer
|
|
177
|
-
* matters). Returns a value > floor in that case; identical otherwise. */
|
|
178
|
-
floor = Infinity
|
|
179
|
-
): number {
|
|
180
|
-
const center = new Map<string, Pt>();
|
|
181
|
-
for (const n of layout.nodes) center.set(n.label, { x: n.x, y: n.y });
|
|
182
|
-
// collapsed group boxes are edge endpoints too (`__group_<label>`); without
|
|
183
|
-
// them, edges meeting AT a collapsed box are miscounted as crossings.
|
|
184
|
-
for (const g of layout.groups)
|
|
185
|
-
if (g.collapsed) center.set('__group_' + g.label, { x: g.x, y: g.y });
|
|
186
|
-
const polys = flatPolys(layout);
|
|
187
|
-
const edges = layout.edges;
|
|
188
|
-
const R = 34;
|
|
189
|
-
let total = 0;
|
|
190
|
-
for (let a = 0; a < polys.length; a++)
|
|
191
|
-
for (let b = a + 1; b < polys.length; b++) {
|
|
192
|
-
const A = polys[a]!,
|
|
193
|
-
B = polys[b]!;
|
|
194
|
-
if (A.pts.length < 2 || B.pts.length < 2) continue;
|
|
195
|
-
if (A.x1 < B.x0 || B.x1 < A.x0 || A.y1 < B.y0 || B.y1 < A.y0) continue; // bbox disjoint
|
|
196
|
-
const ea = edges[a]!,
|
|
197
|
-
eb = edges[b]!;
|
|
198
|
-
// Shared-endpoint centres (≤2) — inlined to avoid allocating a filter/map
|
|
199
|
-
// array on every one of the O(E²) edge pairs.
|
|
200
|
-
let sh0: Pt | undefined, sh1: Pt | undefined;
|
|
201
|
-
if (ea.source === eb.source || ea.source === eb.target)
|
|
202
|
-
sh0 = center.get(ea.source);
|
|
203
|
-
if (ea.target === eb.source || ea.target === eb.target)
|
|
204
|
-
sh1 = center.get(ea.target);
|
|
205
|
-
const hits: Pt[] = [];
|
|
206
|
-
const ap = A.pts,
|
|
207
|
-
bp = B.pts;
|
|
208
|
-
for (let i = 1; i < ap.length; i++) {
|
|
209
|
-
const a0 = ap[i - 1]!,
|
|
210
|
-
a1 = ap[i]!;
|
|
211
|
-
const axMin = a0.x < a1.x ? a0.x : a1.x,
|
|
212
|
-
axMax = a0.x > a1.x ? a0.x : a1.x,
|
|
213
|
-
ayMin = a0.y < a1.y ? a0.y : a1.y,
|
|
214
|
-
ayMax = a0.y > a1.y ? a0.y : a1.y;
|
|
215
|
-
for (let j = 1; j < bp.length; j++) {
|
|
216
|
-
const b0 = bp[j - 1]!,
|
|
217
|
-
b1 = bp[j]!;
|
|
218
|
-
// per-segment bbox reject — disjoint segments can't cross
|
|
219
|
-
if (axMax < (b0.x < b1.x ? b0.x : b1.x)) continue;
|
|
220
|
-
if ((b0.x > b1.x ? b0.x : b1.x) < axMin) continue;
|
|
221
|
-
if (ayMax < (b0.y < b1.y ? b0.y : b1.y)) continue;
|
|
222
|
-
if ((b0.y > b1.y ? b0.y : b1.y) < ayMin) continue;
|
|
223
|
-
const p = segPoint(a0, a1, b0, b1);
|
|
224
|
-
if (!p) continue;
|
|
225
|
-
if (sh0 && Math.hypot(p.x - sh0.x, p.y - sh0.y) < R) continue;
|
|
226
|
-
if (sh1 && Math.hypot(p.x - sh1.x, p.y - sh1.y) < R) continue;
|
|
227
|
-
if (!hits.some((h) => Math.hypot(h.x - p.x, h.y - p.y) < 6))
|
|
228
|
-
hits.push(p);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
total += hits.length;
|
|
232
|
-
if (total > floor) return total; // can't win — stop counting
|
|
233
|
-
}
|
|
234
|
-
return total;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// distance from point p to segment a–b
|
|
238
|
-
function pointSegDist(p: Pt, a: Pt, b: Pt): number {
|
|
239
|
-
const dx = b.x - a.x,
|
|
240
|
-
dy = b.y - a.y;
|
|
241
|
-
const len2 = dx * dx + dy * dy;
|
|
242
|
-
if (len2 < 1e-9) return Math.hypot(p.x - a.x, p.y - a.y);
|
|
243
|
-
let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2;
|
|
244
|
-
t = Math.max(0, Math.min(1, t));
|
|
245
|
-
return Math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy));
|
|
246
|
-
}
|
|
247
|
-
function distToPoly(p: Pt, poly: readonly Pt[]): number {
|
|
248
|
-
let m = Infinity;
|
|
249
|
-
for (let i = 1; i < poly.length; i++)
|
|
250
|
-
m = Math.min(m, pointSegDist(p, poly[i - 1]!, poly[i]!));
|
|
251
|
-
return m;
|
|
252
|
-
}
|
|
253
|
-
type Rect = { x: number; y: number; w: number; h: number };
|
|
254
|
-
// distance from point p to an axis-aligned rectangle (0 if inside)
|
|
255
|
-
function pointRectDist(p: Pt, r: Rect): number {
|
|
256
|
-
const dx = Math.max(r.x - r.w / 2 - p.x, 0, p.x - (r.x + r.w / 2));
|
|
257
|
-
const dy = Math.max(r.y - r.h / 2 - p.y, 0, p.y - (r.y + r.h / 2));
|
|
258
|
-
return Math.hypot(dx, dy);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/** A stretch where one edge runs ALONG another (within `dist`, for at least
|
|
262
|
-
* `minLen` of length) — i.e. two lines "stepping on" each other. Distinct from
|
|
263
|
-
* a true X-crossing (which is a momentary touch, not a sustained run). */
|
|
264
|
-
export interface OverlapRun {
|
|
265
|
-
mid: Pt;
|
|
266
|
-
length: number;
|
|
267
|
-
pts: Pt[];
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Detect edge-overlap runs on the rendered spline geometry. Two edges sharing
|
|
272
|
-
* an endpoint legitimately CONVERGE at that node's port — runs within `nodeClear`
|
|
273
|
-
* of a shared node centre are excluded; only overlap along the open path counts.
|
|
274
|
-
*/
|
|
275
|
-
export function detectEdgeOverlaps(
|
|
276
|
-
layout: BLLayoutResult,
|
|
277
|
-
opts?: { dist?: number; minLen?: number; nodeClear?: number }
|
|
278
|
-
): OverlapRun[] {
|
|
279
|
-
const dist = opts?.dist ?? 8;
|
|
280
|
-
const minLen = opts?.minLen ?? 16;
|
|
281
|
-
// Margin BEYOND the shared node's box that still counts as "converging to the
|
|
282
|
-
// port" (excluded). Edges legitimately meet at a node — only overlap out in
|
|
283
|
-
// the open, away from any shared node, is a real "stepping on another line".
|
|
284
|
-
const nodeClear = opts?.nodeClear ?? 12;
|
|
285
|
-
|
|
286
|
-
const rect = new Map<string, Rect>();
|
|
287
|
-
for (const n of layout.nodes)
|
|
288
|
-
rect.set(n.label, { x: n.x, y: n.y, w: n.width, h: n.height });
|
|
289
|
-
for (const g of layout.groups)
|
|
290
|
-
if (g.collapsed)
|
|
291
|
-
rect.set('__group_' + g.label, {
|
|
292
|
-
x: g.x,
|
|
293
|
-
y: g.y,
|
|
294
|
-
w: g.width,
|
|
295
|
-
h: g.height,
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
const polys = flatPolys(layout);
|
|
299
|
-
const edges = layout.edges;
|
|
300
|
-
|
|
301
|
-
const runs: OverlapRun[] = [];
|
|
302
|
-
for (let a = 0; a < polys.length; a++)
|
|
303
|
-
for (let b = a + 1; b < polys.length; b++) {
|
|
304
|
-
const A = polys[a]!,
|
|
305
|
-
B = polys[b]!;
|
|
306
|
-
if (A.pts.length < 2 || B.pts.length < 2) continue;
|
|
307
|
-
if (
|
|
308
|
-
A.x1 + dist < B.x0 ||
|
|
309
|
-
B.x1 + dist < A.x0 ||
|
|
310
|
-
A.y1 + dist < B.y0 ||
|
|
311
|
-
B.y1 + dist < A.y0
|
|
312
|
-
)
|
|
313
|
-
continue;
|
|
314
|
-
const ea = edges[a]!,
|
|
315
|
-
eb = edges[b]!;
|
|
316
|
-
// Shared-endpoint rects (≤2) — inlined to avoid a per-pair filter/map array.
|
|
317
|
-
let shr0: Rect | undefined, shr1: Rect | undefined;
|
|
318
|
-
if (ea.source === eb.source || ea.source === eb.target)
|
|
319
|
-
shr0 = rect.get(ea.source);
|
|
320
|
-
if (ea.target === eb.source || ea.target === eb.target)
|
|
321
|
-
shr1 = rect.get(ea.target);
|
|
322
|
-
// Walk A; accumulate contiguous "covered" runs (close to B, off any shared
|
|
323
|
-
// node). A run counts once if it reaches minLen.
|
|
324
|
-
let run: Pt[] = [];
|
|
325
|
-
let runLen = 0;
|
|
326
|
-
const flush = (): void => {
|
|
327
|
-
if (runLen >= minLen && run.length >= 2)
|
|
328
|
-
runs.push({
|
|
329
|
-
mid: run[Math.floor(run.length / 2)]!,
|
|
330
|
-
length: runLen,
|
|
331
|
-
pts: run.slice(),
|
|
332
|
-
});
|
|
333
|
-
run = [];
|
|
334
|
-
runLen = 0;
|
|
335
|
-
};
|
|
336
|
-
for (const p of A.pts) {
|
|
337
|
-
const nearShared =
|
|
338
|
-
(shr0 !== undefined && pointRectDist(p, shr0) < nodeClear) ||
|
|
339
|
-
(shr1 !== undefined && pointRectDist(p, shr1) < nodeClear);
|
|
340
|
-
const covered = !nearShared && distToPoly(p, B.pts) < dist;
|
|
341
|
-
if (covered) {
|
|
342
|
-
if (run.length)
|
|
343
|
-
runLen += Math.hypot(
|
|
344
|
-
p.x - run[run.length - 1]!.x,
|
|
345
|
-
p.y - run[run.length - 1]!.y
|
|
346
|
-
);
|
|
347
|
-
run.push(p);
|
|
348
|
-
} else flush();
|
|
349
|
-
}
|
|
350
|
-
flush();
|
|
351
|
-
}
|
|
352
|
-
return runs;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/** Count of edge-overlap runs — the "stepping on another line" metric. */
|
|
356
|
-
export function countEdgeOverlaps(
|
|
357
|
-
layout: BLLayoutResult,
|
|
358
|
-
opts?: { dist?: number; minLen?: number; nodeClear?: number }
|
|
359
|
-
): number {
|
|
360
|
-
return detectEdgeOverlaps(layout, opts).length;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/** An edge routing THROUGH a node box it doesn't connect to. Counts as a
|
|
364
|
-
* crossing — the line is where it shouldn't be. */
|
|
365
|
-
export interface NodePierce {
|
|
366
|
-
edgeIdx: number;
|
|
367
|
-
node: string;
|
|
368
|
-
pts: Pt[];
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Detect edges that pass through (substantially inside, by `inset`) the box of a
|
|
373
|
-
* node that is NOT one of their endpoints. Endpoints — including collapsed group
|
|
374
|
-
* boxes (`__group_<label>`) — are excluded; an edge legitimately meets those.
|
|
375
|
-
*/
|
|
376
|
-
export function detectEdgeNodePierces(
|
|
377
|
-
layout: BLLayoutResult,
|
|
378
|
-
opts?: { inset?: number; minPts?: number }
|
|
379
|
-
): NodePierce[] {
|
|
380
|
-
const inset = opts?.inset ?? 6;
|
|
381
|
-
const minPts = opts?.minPts ?? 2;
|
|
382
|
-
const rects: (Rect & { key: string })[] = [];
|
|
383
|
-
for (const n of layout.nodes)
|
|
384
|
-
rects.push({ key: n.label, x: n.x, y: n.y, w: n.width, h: n.height });
|
|
385
|
-
for (const g of layout.groups)
|
|
386
|
-
if (g.collapsed)
|
|
387
|
-
rects.push({
|
|
388
|
-
key: '__group_' + g.label,
|
|
389
|
-
x: g.x,
|
|
390
|
-
y: g.y,
|
|
391
|
-
w: g.width,
|
|
392
|
-
h: g.height,
|
|
393
|
-
});
|
|
394
|
-
const inside = (p: Pt, r: Rect): boolean =>
|
|
395
|
-
Math.abs(p.x - r.x) < r.w / 2 - inset &&
|
|
396
|
-
Math.abs(p.y - r.y) < r.h / 2 - inset;
|
|
397
|
-
const out: NodePierce[] = [];
|
|
398
|
-
const polys = flatPolys(layout);
|
|
399
|
-
layout.edges.forEach((e, idx) => {
|
|
400
|
-
if (e.points.length < 2) return;
|
|
401
|
-
const poly = polys[idx]!.pts;
|
|
402
|
-
for (const r of rects) {
|
|
403
|
-
if (
|
|
404
|
-
r.key === e.source ||
|
|
405
|
-
r.key === e.target ||
|
|
406
|
-
'__group_' + r.key === e.source ||
|
|
407
|
-
'__group_' + r.key === e.target
|
|
408
|
-
)
|
|
409
|
-
continue;
|
|
410
|
-
const hits = poly.filter((p) => inside(p, r));
|
|
411
|
-
if (hits.length >= minPts)
|
|
412
|
-
out.push({ edgeIdx: idx, node: r.key, pts: hits });
|
|
413
|
-
}
|
|
414
|
-
});
|
|
415
|
-
return out;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/** Count of edges routing through unrelated node boxes — the "line going through
|
|
419
|
-
* a node" metric. */
|
|
420
|
-
export function countEdgeNodePierces(
|
|
421
|
-
layout: BLLayoutResult,
|
|
422
|
-
opts?: { inset?: number; minPts?: number }
|
|
423
|
-
): number {
|
|
424
|
-
return detectEdgeNodePierces(layout, opts).length;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* Re-routed copy of a layout that bends edges AROUND any node box they pierce:
|
|
429
|
-
* for each pierced (edge, node) it inserts a waypoint pushing the edge out past
|
|
430
|
-
* the node, perpendicular to the edge, on the side it's already leaning. The
|
|
431
|
-
* curveBasis spline then bows around the node instead of through it. Returned as
|
|
432
|
-
* an ALTERNATIVE candidate — the caller keeps it only if total badness drops, so
|
|
433
|
-
* a detour that trades a pierce for a crossing is simply rejected.
|
|
434
|
-
*/
|
|
435
|
-
export function deroutePierces(layout: BLLayoutResult): BLLayoutResult {
|
|
436
|
-
const pierces = detectEdgeNodePierces(layout);
|
|
437
|
-
if (!pierces.length) return layout;
|
|
438
|
-
const rect = new Map<string, Rect>();
|
|
439
|
-
for (const n of layout.nodes)
|
|
440
|
-
rect.set(n.label, { x: n.x, y: n.y, w: n.width, h: n.height });
|
|
441
|
-
for (const g of layout.groups)
|
|
442
|
-
if (g.collapsed)
|
|
443
|
-
rect.set('__group_' + g.label, {
|
|
444
|
-
x: g.x,
|
|
445
|
-
y: g.y,
|
|
446
|
-
w: g.width,
|
|
447
|
-
h: g.height,
|
|
448
|
-
});
|
|
449
|
-
const byEdge = new Map<number, string[]>();
|
|
450
|
-
for (const p of pierces) {
|
|
451
|
-
const arr = byEdge.get(p.edgeIdx);
|
|
452
|
-
if (arr) arr.push(p.node);
|
|
453
|
-
else byEdge.set(p.edgeIdx, [p.node]);
|
|
454
|
-
}
|
|
455
|
-
const edges = layout.edges.map((e, idx) => {
|
|
456
|
-
const nodes = byEdge.get(idx);
|
|
457
|
-
if (!nodes) return e;
|
|
458
|
-
let pts: Pt[] = e.points.map((p) => ({ x: p.x, y: p.y }));
|
|
459
|
-
for (const label of nodes) {
|
|
460
|
-
const r = rect.get(label);
|
|
461
|
-
if (r) pts = detourAround(pts, r);
|
|
462
|
-
}
|
|
463
|
-
return { ...e, points: pts };
|
|
464
|
-
});
|
|
465
|
-
return { ...layout, edges };
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Bend a waypoint polyline around a rectangle it passes through: find the
|
|
469
|
-
// segment whose closest point to the rect centre is nearest, and insert a
|
|
470
|
-
// waypoint there pushed out past the rect corner (+margin) on the leaning side.
|
|
471
|
-
function detourAround(pts: Pt[], r: Rect): Pt[] {
|
|
472
|
-
if (pts.length < 2) return pts;
|
|
473
|
-
const c = { x: r.x, y: r.y };
|
|
474
|
-
let bestSeg = -1;
|
|
475
|
-
let bestT = 0;
|
|
476
|
-
let bestD = Infinity;
|
|
477
|
-
let bestPt: Pt = pts[0]!;
|
|
478
|
-
for (let i = 0; i < pts.length - 1; i++) {
|
|
479
|
-
const a = pts[i]!,
|
|
480
|
-
b = pts[i + 1]!;
|
|
481
|
-
const dx = b.x - a.x,
|
|
482
|
-
dy = b.y - a.y;
|
|
483
|
-
const len2 = dx * dx + dy * dy || 1e-9;
|
|
484
|
-
let t = ((c.x - a.x) * dx + (c.y - a.y) * dy) / len2;
|
|
485
|
-
t = Math.max(0, Math.min(1, t));
|
|
486
|
-
const q = { x: a.x + t * dx, y: a.y + t * dy };
|
|
487
|
-
const d = Math.hypot(q.x - c.x, q.y - c.y);
|
|
488
|
-
if (d < bestD) {
|
|
489
|
-
bestD = d;
|
|
490
|
-
bestSeg = i;
|
|
491
|
-
bestT = t;
|
|
492
|
-
bestPt = q;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
if (bestSeg < 0) return pts;
|
|
496
|
-
// push direction: away from rect centre, on the side the edge already leans;
|
|
497
|
-
// if the closest point is dead-centre, pick the box's shorter axis to exit.
|
|
498
|
-
let nx = bestPt.x - c.x;
|
|
499
|
-
let ny = bestPt.y - c.y;
|
|
500
|
-
if (Math.hypot(nx, ny) < 1) {
|
|
501
|
-
if (r.w <= r.h) {
|
|
502
|
-
nx = 1;
|
|
503
|
-
ny = 0;
|
|
504
|
-
} else {
|
|
505
|
-
nx = 0;
|
|
506
|
-
ny = 1;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
const nlen = Math.hypot(nx, ny) || 1;
|
|
510
|
-
nx /= nlen;
|
|
511
|
-
ny /= nlen;
|
|
512
|
-
// Local edge direction (along the closest segment), for shaping a SMOOTH hump
|
|
513
|
-
// around the node instead of a single spike: ease out → peak → ease back in.
|
|
514
|
-
const a = pts[bestSeg]!,
|
|
515
|
-
b = pts[bestSeg + 1]!;
|
|
516
|
-
let ex = b.x - a.x,
|
|
517
|
-
ey = b.y - a.y;
|
|
518
|
-
const elen = Math.hypot(ex, ey) || 1;
|
|
519
|
-
ex /= elen;
|
|
520
|
-
ey /= elen;
|
|
521
|
-
const clear = Math.hypot(r.w, r.h) / 2 + 18;
|
|
522
|
-
const hw = Math.hypot(r.w, r.h) / 2; // hump half-width along the edge
|
|
523
|
-
void bestT;
|
|
524
|
-
const before = {
|
|
525
|
-
x: bestPt.x - ex * hw + nx * clear * 0.5,
|
|
526
|
-
y: bestPt.y - ey * hw + ny * clear * 0.5,
|
|
527
|
-
};
|
|
528
|
-
const peak = { x: c.x + nx * clear, y: c.y + ny * clear };
|
|
529
|
-
const after = {
|
|
530
|
-
x: bestPt.x + ex * hw + nx * clear * 0.5,
|
|
531
|
-
y: bestPt.y + ey * hw + ny * clear * 0.5,
|
|
532
|
-
};
|
|
533
|
-
const out = pts.slice(0, bestSeg + 1);
|
|
534
|
-
out.push(before, peak, after);
|
|
535
|
-
out.push(...pts.slice(bestSeg + 1));
|
|
536
|
-
return out;
|
|
537
|
-
}
|
|
538
|
-
/** Count of edges that come CLOSE to another edge without overlapping — lines
|
|
539
|
-
* that "almost touch". Runs within `near` px (but not already overlapping within
|
|
540
|
-
* `tight` px). Same shared-node exclusion as overlaps. */
|
|
541
|
-
export function countEdgeNearMiss(
|
|
542
|
-
layout: BLLayoutResult,
|
|
543
|
-
opts?: { near?: number; tight?: number; minLen?: number }
|
|
544
|
-
): number {
|
|
545
|
-
const near = opts?.near ?? 14;
|
|
546
|
-
const tight = opts?.tight ?? 8;
|
|
547
|
-
const minLen = opts?.minLen ?? 18;
|
|
548
|
-
const close = detectEdgeOverlaps(layout, { dist: near, minLen }).length;
|
|
549
|
-
const over = detectEdgeOverlaps(layout, { dist: tight, minLen }).length;
|
|
550
|
-
return Math.max(0, close - over);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Renderer extends a parent group's box UPWARD by this label zone (mirrors
|
|
554
|
-
// GROUP_LABEL_ZONE in renderer.ts) — so the collision metric must use the
|
|
555
|
-
// RENDERED box, or near-touching parents read as fine when they actually overlap.
|
|
556
|
-
const GROUP_LABEL_ZONE = 32;
|
|
557
|
-
|
|
558
|
-
/** Labels of GROUP boxes that touch/overlap a non-nested sibling group. Proper
|
|
559
|
-
* nesting (one box fully inside another) is excluded; only sibling/unrelated
|
|
560
|
-
* collisions count. Uses the RENDERED box (parent groups grow up by the label
|
|
561
|
-
* zone, which is what actually makes near-touching parents collide). */
|
|
562
|
-
export function detectGroupOverlaps(
|
|
563
|
-
layout: BLLayoutResult,
|
|
564
|
-
opts?: { margin?: number }
|
|
565
|
-
): string[] {
|
|
566
|
-
const margin = opts?.margin ?? 4;
|
|
567
|
-
const raw = layout.groups.map((g) => ({
|
|
568
|
-
label: g.label,
|
|
569
|
-
l: g.x - g.width / 2,
|
|
570
|
-
r: g.x + g.width / 2,
|
|
571
|
-
t: g.y - g.height / 2,
|
|
572
|
-
b: g.y + g.height / 2,
|
|
573
|
-
}));
|
|
574
|
-
const contains = (
|
|
575
|
-
outer: (typeof raw)[number],
|
|
576
|
-
inner: (typeof raw)[number]
|
|
577
|
-
): boolean =>
|
|
578
|
-
outer.l <= inner.l + margin &&
|
|
579
|
-
outer.r >= inner.r - margin &&
|
|
580
|
-
outer.t <= inner.t + margin &&
|
|
581
|
-
outer.b >= inner.b - margin;
|
|
582
|
-
const rend = raw.map((a, i) => {
|
|
583
|
-
const parent = raw.some((b, j) => j !== i && contains(a, b));
|
|
584
|
-
return parent ? { ...a, t: a.t - GROUP_LABEL_ZONE } : a;
|
|
585
|
-
});
|
|
586
|
-
const hit = new Set<string>();
|
|
587
|
-
for (let i = 0; i < raw.length; i++)
|
|
588
|
-
for (let j = i + 1; j < raw.length; j++) {
|
|
589
|
-
if (contains(raw[i]!, raw[j]!) || contains(raw[j]!, raw[i]!)) continue; // nesting
|
|
590
|
-
const a = rend[i]!,
|
|
591
|
-
b = rend[j]!;
|
|
592
|
-
const dx = Math.max(a.l - b.r, b.l - a.r);
|
|
593
|
-
const dy = Math.max(a.t - b.b, b.t - a.b);
|
|
594
|
-
if (Math.max(dx, dy) < margin) {
|
|
595
|
-
hit.add(raw[i]!.label);
|
|
596
|
-
hit.add(raw[j]!.label);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
return [...hit];
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
/** Count of group-box collisions (pairs touching/overlapping). */
|
|
603
|
-
export function countGroupOverlaps(
|
|
604
|
-
layout: BLLayoutResult,
|
|
605
|
-
opts?: { margin?: number }
|
|
606
|
-
): number {
|
|
607
|
-
const margin = opts?.margin ?? 4;
|
|
608
|
-
const raw = layout.groups.map((g) => ({
|
|
609
|
-
l: g.x - g.width / 2,
|
|
610
|
-
r: g.x + g.width / 2,
|
|
611
|
-
t: g.y - g.height / 2,
|
|
612
|
-
b: g.y + g.height / 2,
|
|
613
|
-
}));
|
|
614
|
-
const contains = (
|
|
615
|
-
outer: (typeof raw)[number],
|
|
616
|
-
inner: (typeof raw)[number]
|
|
617
|
-
): boolean =>
|
|
618
|
-
outer.l <= inner.l + margin &&
|
|
619
|
-
outer.r >= inner.r - margin &&
|
|
620
|
-
outer.t <= inner.t + margin &&
|
|
621
|
-
outer.b >= inner.b - margin;
|
|
622
|
-
const rend = raw.map((a, i) => {
|
|
623
|
-
const parent = raw.some((b, j) => j !== i && contains(a, b));
|
|
624
|
-
return parent ? { ...a, t: a.t - GROUP_LABEL_ZONE } : a;
|
|
625
|
-
});
|
|
626
|
-
let count = 0;
|
|
627
|
-
for (let i = 0; i < raw.length; i++)
|
|
628
|
-
for (let j = i + 1; j < raw.length; j++) {
|
|
629
|
-
if (contains(raw[i]!, raw[j]!) || contains(raw[j]!, raw[i]!)) continue;
|
|
630
|
-
const a = rend[i]!,
|
|
631
|
-
b = rend[j]!;
|
|
632
|
-
const dx = Math.max(a.l - b.r, b.l - a.r);
|
|
633
|
-
const dy = Math.max(a.t - b.b, b.t - a.b);
|
|
634
|
-
if (Math.max(dx, dy) < margin) count++;
|
|
635
|
-
}
|
|
636
|
-
return count;
|
|
637
|
-
}
|
|
638
|
-
/**
|
|
639
|
-
* Separated copy of a layout that pushes overlapping TOP-LEVEL group bands apart
|
|
640
|
-
* along the cross-axis. dagre's compound layout sometimes wedges a small group
|
|
641
|
-
* into a too-tight channel between two others (the multi-cloud On-Prem box lands
|
|
642
|
-
* in AWS's label zone). Translating each top-level group rigidly along the
|
|
643
|
-
* cross-axis preserves every node's rank/column, so the only thing that changes
|
|
644
|
-
* is the gap between bands. Member nodes + nested sub-group boxes move by their
|
|
645
|
-
* band's delta; edge waypoints blend smoothly between their endpoints' deltas
|
|
646
|
-
* (intra-band edges keep their shape exactly; cross-band edges ease across).
|
|
647
|
-
*
|
|
648
|
-
* Returned as an ALTERNATIVE candidate — the caller keeps it only if total
|
|
649
|
-
* badness drops, so a separation that introduces a crossing/pierce is rejected.
|
|
650
|
-
* Fully-expanded graphs only (skips when any group is collapsed).
|
|
651
|
-
*/
|
|
652
|
-
export function separateGroupBands(
|
|
653
|
-
layout: BLLayoutResult,
|
|
654
|
-
parsed: ParsedBoxesAndLines
|
|
655
|
-
): BLLayoutResult {
|
|
656
|
-
if (layout.groups.some((g) => g.collapsed)) return layout;
|
|
657
|
-
if (countGroupOverlaps(layout) === 0) return layout;
|
|
658
|
-
|
|
659
|
-
// Resolve each group label to its top-level ancestor (walk parentGroup).
|
|
660
|
-
const parentOf = new Map<string, string | undefined>();
|
|
661
|
-
for (const g of parsed.groups) parentOf.set(g.label, g.parentGroup);
|
|
662
|
-
const topOf = (label: string): string => {
|
|
663
|
-
let cur = label;
|
|
664
|
-
const seen = new Set<string>();
|
|
665
|
-
for (;;) {
|
|
666
|
-
const p = parentOf.get(cur);
|
|
667
|
-
if (!p || seen.has(p)) return cur;
|
|
668
|
-
seen.add(cur);
|
|
669
|
-
cur = p;
|
|
670
|
-
}
|
|
671
|
-
};
|
|
672
|
-
const topLabels = parsed.groups
|
|
673
|
-
.filter((g) => !g.parentGroup)
|
|
674
|
-
.map((g) => g.label);
|
|
675
|
-
if (topLabels.length < 2) return layout;
|
|
676
|
-
|
|
677
|
-
// Each node's top-level group (a node sits in exactly one leaf group).
|
|
678
|
-
const nodeTop = new Map<string, string>();
|
|
679
|
-
for (const g of parsed.groups)
|
|
680
|
-
for (const child of g.children)
|
|
681
|
-
if (!parsed.groups.some((gg) => gg.label === child))
|
|
682
|
-
nodeTop.set(child, topOf(g.label));
|
|
683
|
-
|
|
684
|
-
// Cross-axis: LR stacks bands vertically (Y); TB stacks them horizontally (X).
|
|
685
|
-
// The label zone always extends a parent's box upward (Y), so it only lands on
|
|
686
|
-
// the cross-axis for LR — for TB this pass can still separate plain X overlaps.
|
|
687
|
-
const axis: 'x' | 'y' = parsed.direction === 'TB' ? 'x' : 'y';
|
|
688
|
-
const boxByLabel = new Map(layout.groups.map((g) => [g.label, g]));
|
|
689
|
-
|
|
690
|
-
// Rendered cross-interval per top-level group (label zone folded in on Y).
|
|
691
|
-
type Band = { label: string; lo: number; hi: number; c: number };
|
|
692
|
-
const bands: Band[] = topLabels.map((label) => {
|
|
693
|
-
const g = boxByLabel.get(label)!;
|
|
694
|
-
const half = (axis === 'y' ? g.height : g.width) / 2;
|
|
695
|
-
let lo = (axis === 'y' ? g.y : g.x) - half;
|
|
696
|
-
const hi = (axis === 'y' ? g.y : g.x) + half;
|
|
697
|
-
// A top-level group with children is a parent → its rendered box grows up.
|
|
698
|
-
const isParent = parsed.groups.some((c) => c.parentGroup === label);
|
|
699
|
-
if (axis === 'y' && isParent) lo -= GROUP_LABEL_ZONE;
|
|
700
|
-
return { label, lo, hi, c: (lo + hi) / 2 };
|
|
701
|
-
});
|
|
702
|
-
bands.sort((a, b) => a.c - b.c);
|
|
703
|
-
|
|
704
|
-
// 1D block-merge (PAV) separation: keep each band's centre as close to where
|
|
705
|
-
// it is as possible, subject to a minimum gap between consecutive bands.
|
|
706
|
-
const GAP = 16;
|
|
707
|
-
const half = bands.map((b) => (b.hi - b.lo) / 2);
|
|
708
|
-
const off: number[] = [0];
|
|
709
|
-
for (let i = 1; i < bands.length; i++)
|
|
710
|
-
off[i] = off[i - 1]! + half[i - 1]! + GAP + half[i]!;
|
|
711
|
-
const desired = bands.map((b, i) => b.c - off[i]!);
|
|
712
|
-
type Blk = { pos: number; count: number; sum: number; first: number };
|
|
713
|
-
const blocks: Blk[] = [];
|
|
714
|
-
for (let i = 0; i < bands.length; i++) {
|
|
715
|
-
let blk: Blk = { pos: desired[i]!, count: 1, sum: desired[i]!, first: i };
|
|
716
|
-
while (blocks.length && blocks[blocks.length - 1]!.pos >= blk.pos) {
|
|
717
|
-
const prev = blocks.pop()!;
|
|
718
|
-
const count = prev.count + blk.count;
|
|
719
|
-
const sum = prev.sum + blk.sum;
|
|
720
|
-
blk = { pos: sum / count, count, sum, first: prev.first };
|
|
721
|
-
}
|
|
722
|
-
blocks.push(blk);
|
|
723
|
-
}
|
|
724
|
-
const newC = new Array<number>(bands.length);
|
|
725
|
-
for (const blk of blocks)
|
|
726
|
-
for (let k = 0; k < blk.count; k++)
|
|
727
|
-
newC[blk.first + k] = blk.pos + off[blk.first + k]!;
|
|
728
|
-
|
|
729
|
-
// Delta per top-level group; bail if nothing actually moves.
|
|
730
|
-
const delta = new Map<string, number>();
|
|
731
|
-
let moved = false;
|
|
732
|
-
bands.forEach((b, i) => {
|
|
733
|
-
const d = newC[i]! - b.c;
|
|
734
|
-
delta.set(b.label, d);
|
|
735
|
-
if (Math.abs(d) > 0.5) moved = true;
|
|
736
|
-
});
|
|
737
|
-
if (!moved) return layout;
|
|
738
|
-
|
|
739
|
-
const nodeDelta = (label: string): number => {
|
|
740
|
-
// Group-incident edge endpoints carry the group's gid, not a node label.
|
|
741
|
-
if (label.startsWith('__group_'))
|
|
742
|
-
return delta.get(topOf(label.slice('__group_'.length))) ?? 0;
|
|
743
|
-
const top = nodeTop.get(label);
|
|
744
|
-
return top ? (delta.get(top) ?? 0) : 0;
|
|
745
|
-
};
|
|
746
|
-
const shift = (p: Pt, d: number): Pt =>
|
|
747
|
-
axis === 'y' ? { x: p.x, y: p.y + d } : { x: p.x + d, y: p.y };
|
|
748
|
-
|
|
749
|
-
const nodes = layout.nodes.map((n) => {
|
|
750
|
-
const d = nodeDelta(n.label);
|
|
751
|
-
return d
|
|
752
|
-
? { ...n, ...(axis === 'y' ? { y: n.y + d } : { x: n.x + d }) }
|
|
753
|
-
: n;
|
|
754
|
-
});
|
|
755
|
-
const groups = layout.groups.map((g) => {
|
|
756
|
-
const d = delta.get(topOf(g.label)) ?? 0;
|
|
757
|
-
return d
|
|
758
|
-
? { ...g, ...(axis === 'y' ? { y: g.y + d } : { x: g.x + d }) }
|
|
759
|
-
: g;
|
|
760
|
-
});
|
|
761
|
-
const edges = layout.edges.map((e) => {
|
|
762
|
-
const ds = nodeDelta(e.source);
|
|
763
|
-
const dt = nodeDelta(e.target);
|
|
764
|
-
if (!ds && !dt) return e;
|
|
765
|
-
const N = e.points.length;
|
|
766
|
-
const points = e.points.map((p, i) => {
|
|
767
|
-
const f = N > 1 ? i / (N - 1) : 0;
|
|
768
|
-
return shift(p, ds * (1 - f) + dt * f);
|
|
769
|
-
});
|
|
770
|
-
return { ...e, points };
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
// Recompute canvas bbox (+ margin) over the shifted content.
|
|
774
|
-
let minX = Infinity,
|
|
775
|
-
minY = Infinity,
|
|
776
|
-
maxX = -Infinity,
|
|
777
|
-
maxY = -Infinity;
|
|
778
|
-
const acc = (x: number, y: number) => {
|
|
779
|
-
if (x < minX) minX = x;
|
|
780
|
-
if (x > maxX) maxX = x;
|
|
781
|
-
if (y < minY) minY = y;
|
|
782
|
-
if (y > maxY) maxY = y;
|
|
783
|
-
};
|
|
784
|
-
for (const n of nodes) {
|
|
785
|
-
acc(n.x - n.width / 2, n.y - n.height / 2);
|
|
786
|
-
acc(n.x + n.width / 2, n.y + n.height / 2);
|
|
787
|
-
}
|
|
788
|
-
for (const g of groups) {
|
|
789
|
-
acc(g.x - g.width / 2, g.y - g.height / 2 - GROUP_LABEL_ZONE);
|
|
790
|
-
acc(g.x + g.width / 2, g.y + g.height / 2);
|
|
791
|
-
}
|
|
792
|
-
for (const e of edges) for (const p of e.points) acc(p.x, p.y);
|
|
793
|
-
const M = 40;
|
|
794
|
-
const sx = M - minX,
|
|
795
|
-
sy = M - minY;
|
|
796
|
-
const reshift = sx !== 0 || sy !== 0;
|
|
797
|
-
return {
|
|
798
|
-
nodes: reshift
|
|
799
|
-
? nodes.map((n) => ({ ...n, x: n.x + sx, y: n.y + sy }))
|
|
800
|
-
: nodes,
|
|
801
|
-
groups: reshift
|
|
802
|
-
? groups.map((g) => ({ ...g, x: g.x + sx, y: g.y + sy }))
|
|
803
|
-
: groups,
|
|
804
|
-
edges: reshift
|
|
805
|
-
? edges.map((e) => ({
|
|
806
|
-
...e,
|
|
807
|
-
points: e.points.map((p) => ({ x: p.x + sx, y: p.y + sy })),
|
|
808
|
-
}))
|
|
809
|
-
: edges,
|
|
810
|
-
width: maxX - minX + 2 * M,
|
|
811
|
-
height: maxY - minY + 2 * M,
|
|
812
|
-
};
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
// Fast crossing estimate for RANKING candidates: straight segments on raw
|
|
816
|
-
// waypoints (no curveBasis flatten) + bbox pruning + early-out per pair.
|
|
817
|
-
// ~10× cheaper than countSplineCrossings; topology-equivalent for ranking.
|
|
818
|
-
function segCross(p1: Pt, p2: Pt, p3: Pt, p4: Pt): boolean {
|
|
819
|
-
const d1x = p2.x - p1.x,
|
|
820
|
-
d1y = p2.y - p1.y,
|
|
821
|
-
d2x = p4.x - p3.x,
|
|
822
|
-
d2y = p4.y - p3.y;
|
|
823
|
-
const den = d1x * d2y - d1y * d2x;
|
|
824
|
-
if (Math.abs(den) < 1e-9) return false;
|
|
825
|
-
const t = ((p3.x - p1.x) * d2y - (p3.y - p1.y) * d2x) / den;
|
|
826
|
-
const s = ((p3.x - p1.x) * d1y - (p3.y - p1.y) * d1x) / den;
|
|
827
|
-
return t > 0.001 && t < 0.999 && s > 0.001 && s < 0.999;
|
|
828
|
-
}
|
|
829
|
-
function countCrossingsFast(layout: BLLayoutResult): number {
|
|
830
|
-
const E = layout.edges.filter((e) => e.points.length >= 2);
|
|
831
|
-
const bb = E.map((e) => {
|
|
832
|
-
let x0 = Infinity,
|
|
833
|
-
y0 = Infinity,
|
|
834
|
-
x1 = -Infinity,
|
|
835
|
-
y1 = -Infinity;
|
|
836
|
-
for (const p of e.points) {
|
|
837
|
-
if (p.x < x0) x0 = p.x;
|
|
838
|
-
if (p.x > x1) x1 = p.x;
|
|
839
|
-
if (p.y < y0) y0 = p.y;
|
|
840
|
-
if (p.y > y1) y1 = p.y;
|
|
841
|
-
}
|
|
842
|
-
return { x0, y0, x1, y1 };
|
|
843
|
-
});
|
|
844
|
-
let count = 0;
|
|
845
|
-
for (let i = 0; i < E.length; i++)
|
|
846
|
-
for (let j = i + 1; j < E.length; j++) {
|
|
847
|
-
const A = E[i]!,
|
|
848
|
-
B = E[j]!;
|
|
849
|
-
if (
|
|
850
|
-
A.source === B.source ||
|
|
851
|
-
A.source === B.target ||
|
|
852
|
-
A.target === B.source ||
|
|
853
|
-
A.target === B.target
|
|
854
|
-
)
|
|
855
|
-
continue;
|
|
856
|
-
const a = bb[i]!,
|
|
857
|
-
b = bb[j]!;
|
|
858
|
-
if (a.x1 < b.x0 || b.x1 < a.x0 || a.y1 < b.y0 || b.y1 < a.y0) continue;
|
|
859
|
-
const pa = A.points,
|
|
860
|
-
pb = B.points;
|
|
861
|
-
let hit = false;
|
|
862
|
-
for (let ai = 0; ai < pa.length - 1 && !hit; ai++)
|
|
863
|
-
for (let bi = 0; bi < pb.length - 1; bi++) {
|
|
864
|
-
if (segCross(pa[ai]!, pa[ai + 1]!, pb[bi]!, pb[bi + 1]!)) {
|
|
865
|
-
hit = true;
|
|
866
|
-
break;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
if (hit) count++;
|
|
870
|
-
}
|
|
871
|
-
return count;
|
|
872
|
-
}
|
|
873
|
-
function meanDrift(
|
|
874
|
-
layout: BLLayoutResult,
|
|
875
|
-
prev: ReadonlyMap<string, Pt> | undefined
|
|
876
|
-
): number {
|
|
877
|
-
if (!prev?.size) return 0;
|
|
878
|
-
let sum = 0,
|
|
879
|
-
n = 0;
|
|
880
|
-
for (const node of layout.nodes) {
|
|
881
|
-
const p = prev.get(node.label);
|
|
882
|
-
if (p) {
|
|
883
|
-
sum += Math.hypot(node.x - p.x, node.y - p.y);
|
|
884
|
-
n++;
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
return n ? sum / n : 0;
|
|
888
|
-
}
|
|
889
|
-
// total edge length — positioning tiebreaker (shorter/straighter reads better)
|
|
890
|
-
function edgeLength(layout: BLLayoutResult): number {
|
|
891
|
-
let total = 0;
|
|
892
|
-
for (const e of layout.edges)
|
|
893
|
-
for (let i = 1; i < e.points.length; i++)
|
|
894
|
-
total += Math.hypot(
|
|
895
|
-
e.points[i]!.x - e.points[i - 1]!.x,
|
|
896
|
-
e.points[i]!.y - e.points[i - 1]!.y
|
|
897
|
-
);
|
|
898
|
-
return total;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
export async function layoutBoxesAndLinesSearch(
|
|
902
|
-
parsed: ParsedBoxesAndLines,
|
|
903
|
-
collapseInfo?: {
|
|
904
|
-
collapsedChildCounts: Map<string, number>;
|
|
905
|
-
originalGroups: readonly BLGroup[];
|
|
906
|
-
},
|
|
907
|
-
opts?: {
|
|
908
|
-
hideDescriptions?: boolean;
|
|
909
|
-
previousPositions?: ReadonlyMap<string, Pt>;
|
|
910
|
-
/** Number of seed orderings to search (default: adaptive by node count). */
|
|
911
|
-
seeds?: number;
|
|
912
|
-
/** Stability weight (default 4). */
|
|
913
|
-
lambda?: number;
|
|
914
|
-
/** How many top candidates to re-rank with the exact counter (default 6). */
|
|
915
|
-
refineK?: number;
|
|
916
|
-
/** Progress hook for the interactive path. When provided, the search yields
|
|
917
|
-
* to a macrotask after each candidate so the host UI can paint a progress
|
|
918
|
-
* indicator. Omit it (CLI/export) and the search runs straight through with
|
|
919
|
-
* no added latency. */
|
|
920
|
-
onProgress?: (done: number, total: number, phase: string) => void;
|
|
921
|
-
/** Last-resort label legibility: reserve dagre label space for every labeled
|
|
922
|
-
* edge so the layout opens a gap wide enough for the (wrapped) label. Only
|
|
923
|
-
* set by layout.ts when steps 1–2 left a label overlapping a node box. */
|
|
924
|
-
reserveEdgeLabels?: boolean;
|
|
925
|
-
}
|
|
926
|
-
): Promise<BLLayoutResult> {
|
|
927
|
-
const hideDescriptions = opts?.hideDescriptions ?? false;
|
|
928
|
-
const reserveEdgeLabels = opts?.reserveEdgeLabels ?? false;
|
|
929
|
-
const onProgress = opts?.onProgress;
|
|
930
|
-
// Yield to a macrotask (lets the browser repaint between heavy placements);
|
|
931
|
-
// no-op when there's no progress observer so non-interactive callers pay
|
|
932
|
-
// nothing.
|
|
933
|
-
const tick = onProgress
|
|
934
|
-
? (): Promise<void> => new Promise<void>((r) => setTimeout(r))
|
|
935
|
-
: (): undefined => undefined;
|
|
936
|
-
// Yielding is only worth its cost once a search is genuinely slow. A typical
|
|
937
|
-
// diagram finishes its whole candidate pool in tens of ms; yielding (a
|
|
938
|
-
// setTimeout, clamped to ~4ms/turn in browsers) after every one of ~90
|
|
939
|
-
// candidates would turn a 30ms layout into ~1s — all to flash a throbber that
|
|
940
|
-
// never shows (the app only reveals it after 150ms). So we suppress yields
|
|
941
|
-
// until the search has burned through a budget, then throttle them: fast
|
|
942
|
-
// diagrams run flat-out, only truly heavy ones cede the main thread to paint.
|
|
943
|
-
const YIELD_AFTER_MS = 150;
|
|
944
|
-
const YIELD_EVERY_MS = 30;
|
|
945
|
-
const searchStart = performance.now();
|
|
946
|
-
let lastYield = searchStart;
|
|
947
|
-
|
|
948
|
-
// collapsed group labels (shown as plain boxes) — mirrors the ELK path
|
|
949
|
-
const collapsedGroupLabels = new Set<string>();
|
|
950
|
-
if (collapseInfo) {
|
|
951
|
-
const missing = new Set<string>();
|
|
952
|
-
for (const og of collapseInfo.originalGroups)
|
|
953
|
-
if (!parsed.groups.some((g) => g.label === og.label))
|
|
954
|
-
missing.add(og.label);
|
|
955
|
-
for (const label of missing) {
|
|
956
|
-
const og = collapseInfo.originalGroups.find((g) => g.label === label);
|
|
957
|
-
const parent = og?.parentGroup;
|
|
958
|
-
if (!parent || !missing.has(parent)) collapsedGroupLabels.add(label);
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
// node sizes (computeNodeSize + uniform-height pass) — identical to ELK path
|
|
963
|
-
const sizes = new Map<string, { width: number; height: number }>();
|
|
964
|
-
let maxDescH = 0;
|
|
965
|
-
for (const node of parsed.nodes) {
|
|
966
|
-
const s = hideDescriptions
|
|
967
|
-
? { width: NODE_WIDTH, height: NODE_HEIGHT }
|
|
968
|
-
: computeNodeSize(node, parsed.showValues === true);
|
|
969
|
-
sizes.set(node.label, s);
|
|
970
|
-
if (!hideDescriptions && node.description && node.description.length > 0)
|
|
971
|
-
maxDescH = Math.max(maxDescH, s.height);
|
|
972
|
-
}
|
|
973
|
-
if (maxDescH > 0)
|
|
974
|
-
for (const node of parsed.nodes)
|
|
975
|
-
if (node.description && node.description.length > 0) {
|
|
976
|
-
const s = sizes.get(node.label)!;
|
|
977
|
-
sizes.set(node.label, { width: s.width, height: maxDescH });
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
const gid = (label: string) => `__group_${label}`;
|
|
981
|
-
const rankdir = parsed.direction === 'TB' ? 'TB' : 'LR';
|
|
982
|
-
|
|
983
|
-
// Group-incident edges: dagre cannot attach edges to a compound cluster node
|
|
984
|
-
// (it throws while ranking). So for any edge whose endpoint is an EXPANDED
|
|
985
|
-
// group, we route the dagre edge through a representative descendant node and
|
|
986
|
-
// later clip the polyline back to the group's boundary rect. Collapsed groups
|
|
987
|
-
// are already plain (childless) nodes, so their edges need no redirect.
|
|
988
|
-
const nodeLabelSet = new Set(parsed.nodes.map((n) => n.label));
|
|
989
|
-
const directNodeChildren = new Map<string, string[]>();
|
|
990
|
-
const childGroupsOf = new Map<string, string[]>();
|
|
991
|
-
for (const grp of parsed.groups) {
|
|
992
|
-
directNodeChildren.set(
|
|
993
|
-
grp.label,
|
|
994
|
-
grp.children.filter((c) => nodeLabelSet.has(c))
|
|
995
|
-
);
|
|
996
|
-
if (grp.parentGroup) {
|
|
997
|
-
const arr = childGroupsOf.get(grp.parentGroup) ?? [];
|
|
998
|
-
arr.push(grp.label);
|
|
999
|
-
childGroupsOf.set(grp.parentGroup, arr);
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
// All descendant node labels of a group (direct children + nested subgroups).
|
|
1003
|
-
const membersCache = new Map<string, string[]>();
|
|
1004
|
-
const membersOf = (label: string): string[] => {
|
|
1005
|
-
const cached = membersCache.get(label);
|
|
1006
|
-
if (cached) return cached;
|
|
1007
|
-
membersCache.set(label, []); // cycle guard
|
|
1008
|
-
const out = [...(directNodeChildren.get(label) ?? [])];
|
|
1009
|
-
for (const sub of childGroupsOf.get(label) ?? [])
|
|
1010
|
-
out.push(...membersOf(sub));
|
|
1011
|
-
membersCache.set(label, out);
|
|
1012
|
-
return out;
|
|
1013
|
-
};
|
|
1014
|
-
|
|
1015
|
-
// For a group used as an edge SOURCE the connector should leave from the
|
|
1016
|
-
// group's internal exit (the member at the deepest rank); used as a TARGET it
|
|
1017
|
-
// should enter at the group's entry (a member with no internal predecessor).
|
|
1018
|
-
// Picking by internal-DAG depth keeps the group a contiguous block and the
|
|
1019
|
-
// overall flow monotonic, instead of stretching it across the target's rank.
|
|
1020
|
-
const gidSourceRep = new Map<string, string>(); // gid -> deepest member
|
|
1021
|
-
const gidTargetRep = new Map<string, string>(); // gid -> shallowest member
|
|
1022
|
-
for (const grp of parsed.groups) {
|
|
1023
|
-
const members = membersOf(grp.label);
|
|
1024
|
-
if (members.length === 0) continue;
|
|
1025
|
-
const memberSet = new Set(members);
|
|
1026
|
-
// Internal predecessors (node->node edges fully inside the group). A
|
|
1027
|
-
// member's rank = longest predecessor chain, so entry members rank 0 and
|
|
1028
|
-
// exit members rank highest — matching how dagre lays the members out.
|
|
1029
|
-
const preds = new Map<string, string[]>();
|
|
1030
|
-
for (const e of parsed.edges)
|
|
1031
|
-
if (memberSet.has(e.source) && memberSet.has(e.target)) {
|
|
1032
|
-
const arr = preds.get(e.target) ?? [];
|
|
1033
|
-
arr.push(e.source);
|
|
1034
|
-
preds.set(e.target, arr);
|
|
1035
|
-
}
|
|
1036
|
-
const rankCache = new Map<string, number>();
|
|
1037
|
-
const onPath = new Set<string>();
|
|
1038
|
-
const rankOf = (n: string): number => {
|
|
1039
|
-
const c = rankCache.get(n);
|
|
1040
|
-
if (c !== undefined) return c;
|
|
1041
|
-
if (onPath.has(n)) return 0; // cycle: treat as root
|
|
1042
|
-
onPath.add(n);
|
|
1043
|
-
let r = 0;
|
|
1044
|
-
for (const p of preds.get(n) ?? []) r = Math.max(r, rankOf(p) + 1);
|
|
1045
|
-
onPath.delete(n);
|
|
1046
|
-
rankCache.set(n, r);
|
|
1047
|
-
return r;
|
|
1048
|
-
};
|
|
1049
|
-
let exit = members[0]!;
|
|
1050
|
-
let entry = members[0]!;
|
|
1051
|
-
for (const mem of members) {
|
|
1052
|
-
if (rankOf(mem) > rankOf(exit)) exit = mem;
|
|
1053
|
-
if (rankOf(mem) < rankOf(entry)) entry = mem;
|
|
1054
|
-
}
|
|
1055
|
-
gidSourceRep.set(gid(grp.label), exit);
|
|
1056
|
-
gidTargetRep.set(gid(grp.label), entry);
|
|
1057
|
-
}
|
|
1058
|
-
const srcRep = (label: string) => gidSourceRep.get(label) ?? label;
|
|
1059
|
-
const tgtRep = (label: string) => gidTargetRep.get(label) ?? label;
|
|
1060
|
-
const edgeKey = (e: { source: string; target: string; lineNumber: number }) =>
|
|
1061
|
-
`${e.source}->${e.target}#${e.lineNumber}`;
|
|
1062
|
-
|
|
1063
|
-
// Clip a polyline endpoint to a group's boundary rect. `rect` is centre-based.
|
|
1064
|
-
// Returns the point where the ray from the rect centre toward `toward` exits
|
|
1065
|
-
// the rect.
|
|
1066
|
-
const rectBorderPoint = (
|
|
1067
|
-
rect: { x: number; y: number; w: number; h: number },
|
|
1068
|
-
toward: Pt
|
|
1069
|
-
): Pt => {
|
|
1070
|
-
const dx = toward.x - rect.x;
|
|
1071
|
-
const dy = toward.y - rect.y;
|
|
1072
|
-
const sx = dx === 0 ? Infinity : rect.w / 2 / Math.abs(dx);
|
|
1073
|
-
const sy = dy === 0 ? Infinity : rect.h / 2 / Math.abs(dy);
|
|
1074
|
-
const s = Math.min(sx, sy);
|
|
1075
|
-
if (!Number.isFinite(s)) return { x: rect.x, y: rect.y };
|
|
1076
|
-
return { x: rect.x + dx * s, y: rect.y + dy * s };
|
|
1077
|
-
};
|
|
1078
|
-
const isInsideRect = (
|
|
1079
|
-
p: Pt,
|
|
1080
|
-
rect: { x: number; y: number; w: number; h: number }
|
|
1081
|
-
) =>
|
|
1082
|
-
Math.abs(p.x - rect.x) <= rect.w / 2 &&
|
|
1083
|
-
Math.abs(p.y - rect.y) <= rect.h / 2;
|
|
1084
|
-
|
|
1085
|
-
// ── Pinned-layout bypass (Canvas Editor spike, Decisions 3/7) ──
|
|
1086
|
-
// When a `layout` block positions EVERY node, place nodes directly from the
|
|
1087
|
-
// stored coordinates and skip the whole dagre search. Edges become
|
|
1088
|
-
// border-clipped straight connectors (no obstacle avoidance — honest-but-ugly).
|
|
1089
|
-
// FLAT, EXPANDED groups are honored: each group's container rect is computed
|
|
1090
|
-
// from its members' pinned positions (canvas group editing). A FLAT group can
|
|
1091
|
-
// also be COLLAPSED while pinned — it renders as a plain box at its members'
|
|
1092
|
-
// bbox-centre (so collapse no longer forces a full dagre reflow). Nested groups
|
|
1093
|
-
// still fall back to dagre (deferred).
|
|
1094
|
-
const pinned = parsed.nodePositions;
|
|
1095
|
-
const groupLabelSet = new Set(parsed.groups.map((g) => g.label));
|
|
1096
|
-
const groupsAreFlat = parsed.groups.every(
|
|
1097
|
-
(g) => !g.parentGroup && !g.children.some((c) => groupLabelSet.has(c))
|
|
1098
|
-
);
|
|
1099
|
-
// A collapsed group can stay pinned only when it's FLAT (top-level, no
|
|
1100
|
-
// sub-groups) and every one of its members has a pinned coord — otherwise we
|
|
1101
|
-
// can't place its box without a search, so fall back to dagre.
|
|
1102
|
-
const allOriginalGroupLabels = new Set(
|
|
1103
|
-
(collapseInfo?.originalGroups ?? parsed.groups).map((g) => g.label)
|
|
1104
|
-
);
|
|
1105
|
-
const collapsedAreFlatPinned =
|
|
1106
|
-
collapsedGroupLabels.size === 0 ||
|
|
1107
|
-
(pinned !== undefined &&
|
|
1108
|
-
collapseInfo !== undefined &&
|
|
1109
|
-
[...collapsedGroupLabels].every((label) => {
|
|
1110
|
-
const og = collapseInfo.originalGroups.find((g) => g.label === label);
|
|
1111
|
-
if (!og || og.parentGroup) return false;
|
|
1112
|
-
return og.children.every(
|
|
1113
|
-
(c) => pinned.has(c) && !allOriginalGroupLabels.has(c)
|
|
1114
|
-
);
|
|
1115
|
-
}));
|
|
1116
|
-
const allPinned =
|
|
1117
|
-
pinned !== undefined &&
|
|
1118
|
-
(parsed.nodes.length > 0 || collapsedGroupLabels.size > 0) &&
|
|
1119
|
-
parsed.nodes.every((n) => pinned.has(n.label)) &&
|
|
1120
|
-
groupsAreFlat &&
|
|
1121
|
-
collapsedAreFlatPinned;
|
|
1122
|
-
function placePinned(pins: ReadonlyMap<string, Pt>): BLLayoutResult {
|
|
1123
|
-
// Collapsed flat groups → a plain NODE-sized box at the bbox-centre of the
|
|
1124
|
-
// group's (now-hidden) members' pinned coords. The collapse transform
|
|
1125
|
-
// redirected their incident edges to `__group_<label>`, so register that id
|
|
1126
|
-
// in the position/rect lookups too.
|
|
1127
|
-
const collapsedPosByGid = new Map<string, Pt>();
|
|
1128
|
-
const collapsedBoxes: Array<{
|
|
1129
|
-
label: string;
|
|
1130
|
-
lineNumber: number;
|
|
1131
|
-
childCount: number;
|
|
1132
|
-
x: number;
|
|
1133
|
-
y: number;
|
|
1134
|
-
}> = [];
|
|
1135
|
-
if (collapseInfo)
|
|
1136
|
-
for (const label of collapsedGroupLabels) {
|
|
1137
|
-
const og = collapseInfo.originalGroups.find((g) => g.label === label);
|
|
1138
|
-
if (!og) continue;
|
|
1139
|
-
let cx0 = Infinity,
|
|
1140
|
-
cy0 = Infinity,
|
|
1141
|
-
cx1 = -Infinity,
|
|
1142
|
-
cy1 = -Infinity;
|
|
1143
|
-
for (const c of og.children) {
|
|
1144
|
-
const p = pins.get(c);
|
|
1145
|
-
if (!p) continue;
|
|
1146
|
-
cx0 = Math.min(cx0, p.x);
|
|
1147
|
-
cx1 = Math.max(cx1, p.x);
|
|
1148
|
-
cy0 = Math.min(cy0, p.y);
|
|
1149
|
-
cy1 = Math.max(cy1, p.y);
|
|
1150
|
-
}
|
|
1151
|
-
if (!Number.isFinite(cx0)) continue;
|
|
1152
|
-
const cx = (cx0 + cx1) / 2;
|
|
1153
|
-
const cy = (cy0 + cy1) / 2;
|
|
1154
|
-
collapsedPosByGid.set(`__group_${label}`, { x: cx, y: cy });
|
|
1155
|
-
collapsedBoxes.push({
|
|
1156
|
-
label,
|
|
1157
|
-
lineNumber: og.lineNumber,
|
|
1158
|
-
childCount:
|
|
1159
|
-
collapseInfo.collapsedChildCounts.get(label) ?? og.children.length,
|
|
1160
|
-
x: cx,
|
|
1161
|
-
y: cy,
|
|
1162
|
-
});
|
|
1163
|
-
}
|
|
1164
|
-
const posOf = (label: string): Pt | undefined =>
|
|
1165
|
-
pins.get(label) ?? collapsedPosByGid.get(label);
|
|
1166
|
-
const rectOf = (label: string) => {
|
|
1167
|
-
const p = posOf(label)!;
|
|
1168
|
-
const s = sizes.get(label) ?? { width: NODE_WIDTH, height: NODE_HEIGHT };
|
|
1169
|
-
return { x: p.x, y: p.y, w: s.width, h: s.height };
|
|
1170
|
-
};
|
|
1171
|
-
const nodes = parsed.nodes.map((n) => {
|
|
1172
|
-
const r = rectOf(n.label);
|
|
1173
|
-
return { label: n.label, x: r.x, y: r.y, width: r.w, height: r.h };
|
|
1174
|
-
});
|
|
1175
|
-
const edges: BLLayoutEdge[] = parsed.edges.flatMap((e) => {
|
|
1176
|
-
const sp = posOf(e.source);
|
|
1177
|
-
const tp = posOf(e.target);
|
|
1178
|
-
if (!sp || !tp) return [];
|
|
1179
|
-
const srcRect = rectOf(e.source);
|
|
1180
|
-
const tgtRect = rectOf(e.target);
|
|
1181
|
-
const p0 = rectBorderPoint(srcRect, tp);
|
|
1182
|
-
const p1 = rectBorderPoint(tgtRect, sp);
|
|
1183
|
-
return [
|
|
1184
|
-
{
|
|
1185
|
-
source: e.source,
|
|
1186
|
-
target: e.target,
|
|
1187
|
-
...(e.label !== undefined && { label: e.label }),
|
|
1188
|
-
bidirectional: e.bidirectional,
|
|
1189
|
-
lineNumber: e.lineNumber,
|
|
1190
|
-
points: [p0, p1],
|
|
1191
|
-
yOffset: 0,
|
|
1192
|
-
parallelCount: 1,
|
|
1193
|
-
metadata: e.metadata,
|
|
1194
|
-
straight: true,
|
|
1195
|
-
},
|
|
1196
|
-
];
|
|
1197
|
-
});
|
|
1198
|
-
// Fit the canvas around the pinned content with a uniform margin on every
|
|
1199
|
-
// side. Crucially, content must never fall off the TOP/LEFT (the viewBox
|
|
1200
|
-
// origin is 0,0): if a node was dragged past the margin we shift everything
|
|
1201
|
-
// back on-canvas by `max(0, M - min)` — clamped so in-bounds diagrams keep
|
|
1202
|
-
// their exact pinned coords (shift 0) and only off-canvas ones are nudged.
|
|
1203
|
-
// Flat-group container rects: bbox of the group's members + side/bottom
|
|
1204
|
-
// padding, with a label zone reserved at the top (mirrors the renderer).
|
|
1205
|
-
const GROUP_PAD = 16;
|
|
1206
|
-
const nodeByLabel = new Map(nodes.map((n) => [n.label, n]));
|
|
1207
|
-
const groups: BLLayoutResult['groups'][number][] = [];
|
|
1208
|
-
for (const grp of parsed.groups) {
|
|
1209
|
-
let gx0 = Infinity,
|
|
1210
|
-
gy0 = Infinity,
|
|
1211
|
-
gx1 = -Infinity,
|
|
1212
|
-
gy1 = -Infinity;
|
|
1213
|
-
for (const c of grp.children) {
|
|
1214
|
-
const n = nodeByLabel.get(c);
|
|
1215
|
-
if (!n) continue;
|
|
1216
|
-
gx0 = Math.min(gx0, n.x - n.width / 2);
|
|
1217
|
-
gx1 = Math.max(gx1, n.x + n.width / 2);
|
|
1218
|
-
gy0 = Math.min(gy0, n.y - n.height / 2);
|
|
1219
|
-
gy1 = Math.max(gy1, n.y + n.height / 2);
|
|
1220
|
-
}
|
|
1221
|
-
if (!Number.isFinite(gx0)) continue; // members not pinned / empty group
|
|
1222
|
-
const x0 = gx0 - GROUP_PAD;
|
|
1223
|
-
const x1 = gx1 + GROUP_PAD;
|
|
1224
|
-
const y0 = gy0 - GROUP_LABEL_ZONE;
|
|
1225
|
-
const y1 = gy1 + GROUP_PAD;
|
|
1226
|
-
groups.push({
|
|
1227
|
-
label: grp.label,
|
|
1228
|
-
lineNumber: grp.lineNumber,
|
|
1229
|
-
x: (x0 + x1) / 2,
|
|
1230
|
-
y: (y0 + y1) / 2,
|
|
1231
|
-
width: x1 - x0,
|
|
1232
|
-
height: y1 - y0,
|
|
1233
|
-
collapsed: false,
|
|
1234
|
-
childCount: grp.children.length,
|
|
1235
|
-
});
|
|
1236
|
-
}
|
|
1237
|
-
// Collapsed flat groups: a plain box at the members' centre.
|
|
1238
|
-
for (const cb of collapsedBoxes) {
|
|
1239
|
-
groups.push({
|
|
1240
|
-
label: cb.label,
|
|
1241
|
-
lineNumber: cb.lineNumber,
|
|
1242
|
-
x: cb.x,
|
|
1243
|
-
y: cb.y,
|
|
1244
|
-
width: NODE_WIDTH,
|
|
1245
|
-
height: NODE_HEIGHT,
|
|
1246
|
-
collapsed: true,
|
|
1247
|
-
childCount: cb.childCount,
|
|
1248
|
-
});
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
const M = 40;
|
|
1252
|
-
let minX = Infinity,
|
|
1253
|
-
minY = Infinity,
|
|
1254
|
-
maxX = -Infinity,
|
|
1255
|
-
maxY = -Infinity;
|
|
1256
|
-
const acc = (x: number, y: number) => {
|
|
1257
|
-
if (x < minX) minX = x;
|
|
1258
|
-
if (x > maxX) maxX = x;
|
|
1259
|
-
if (y < minY) minY = y;
|
|
1260
|
-
if (y > maxY) maxY = y;
|
|
1261
|
-
};
|
|
1262
|
-
for (const n of nodes) {
|
|
1263
|
-
acc(n.x - n.width / 2, n.y - n.height / 2);
|
|
1264
|
-
acc(n.x + n.width / 2, n.y + n.height / 2);
|
|
1265
|
-
}
|
|
1266
|
-
for (const e of edges) for (const p of e.points) acc(p.x, p.y);
|
|
1267
|
-
for (const gr of groups) {
|
|
1268
|
-
acc(gr.x - gr.width / 2, gr.y - gr.height / 2);
|
|
1269
|
-
acc(gr.x + gr.width / 2, gr.y + gr.height / 2);
|
|
1270
|
-
}
|
|
1271
|
-
// Only correct genuinely off-canvas content — a small tolerance ignores the
|
|
1272
|
-
// sub-pixel jitter from rounding coords near the margin, so an already
|
|
1273
|
-
// on-canvas diagram keeps its exact pinned positions (no creeping drift).
|
|
1274
|
-
const TOL = 2;
|
|
1275
|
-
const sx = minX < M - TOL ? M - minX : 0;
|
|
1276
|
-
const sy = minY < M - TOL ? M - minY : 0;
|
|
1277
|
-
const shifted = sx !== 0 || sy !== 0;
|
|
1278
|
-
return {
|
|
1279
|
-
nodes: shifted
|
|
1280
|
-
? nodes.map((n) => ({ ...n, x: n.x + sx, y: n.y + sy }))
|
|
1281
|
-
: nodes,
|
|
1282
|
-
edges: shifted
|
|
1283
|
-
? edges.map((e) => ({
|
|
1284
|
-
...e,
|
|
1285
|
-
points: e.points.map((p) => ({ x: p.x + sx, y: p.y + sy })),
|
|
1286
|
-
}))
|
|
1287
|
-
: edges,
|
|
1288
|
-
groups: shifted
|
|
1289
|
-
? groups.map((gr) => ({ ...gr, x: gr.x + sx, y: gr.y + sy }))
|
|
1290
|
-
: groups,
|
|
1291
|
-
width: maxX + sx + M,
|
|
1292
|
-
height: maxY + sy + M,
|
|
1293
|
-
};
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
function place(cfg: {
|
|
1297
|
-
ranker: string;
|
|
1298
|
-
nodesep: number;
|
|
1299
|
-
ranksep: number;
|
|
1300
|
-
seed?: number;
|
|
1301
|
-
}): BLLayoutResult {
|
|
1302
|
-
const r = cfg.seed === undefined ? null : rng(cfg.seed + 1);
|
|
1303
|
-
const ord = <T>(a: readonly T[]): T[] => (r ? shuffle(a, r) : a.slice());
|
|
1304
|
-
const g = new dagre.graphlib.Graph({ compound: true, multigraph: true });
|
|
1305
|
-
g.setGraph({
|
|
1306
|
-
rankdir,
|
|
1307
|
-
ranker: cfg.ranker,
|
|
1308
|
-
nodesep: cfg.nodesep,
|
|
1309
|
-
ranksep: cfg.ranksep,
|
|
1310
|
-
edgesep: 20,
|
|
1311
|
-
marginx: 40,
|
|
1312
|
-
marginy: 40,
|
|
1313
|
-
});
|
|
1314
|
-
g.setDefaultEdgeLabel(() => ({}));
|
|
1315
|
-
for (const grp of ord(parsed.groups))
|
|
1316
|
-
g.setNode(gid(grp.label), { label: grp.label });
|
|
1317
|
-
for (const node of ord(parsed.nodes)) {
|
|
1318
|
-
const s = sizes.get(node.label)!;
|
|
1319
|
-
g.setNode(node.label, { width: s.width, height: s.height });
|
|
1320
|
-
}
|
|
1321
|
-
for (const label of collapsedGroupLabels)
|
|
1322
|
-
g.setNode(gid(label), { width: NODE_WIDTH, height: NODE_HEIGHT });
|
|
1323
|
-
for (const grp of parsed.groups) {
|
|
1324
|
-
if (grp.parentGroup && g.hasNode(gid(grp.parentGroup)))
|
|
1325
|
-
g.setParent(gid(grp.label), gid(grp.parentGroup));
|
|
1326
|
-
for (const c of ord(grp.children)) {
|
|
1327
|
-
if (g.hasNode(c)) g.setParent(c, gid(grp.label));
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
if (collapseInfo)
|
|
1331
|
-
for (const label of collapsedGroupLabels) {
|
|
1332
|
-
const og = collapseInfo.originalGroups.find((x) => x.label === label);
|
|
1333
|
-
if (
|
|
1334
|
-
og?.parentGroup &&
|
|
1335
|
-
!collapsedGroupLabels.has(og.parentGroup) &&
|
|
1336
|
-
g.hasNode(gid(og.parentGroup))
|
|
1337
|
-
)
|
|
1338
|
-
g.setParent(gid(label), gid(og.parentGroup));
|
|
1339
|
-
}
|
|
1340
|
-
for (const e of ord(parsed.edges)) {
|
|
1341
|
-
const s = srcRep(e.source);
|
|
1342
|
-
const t = tgtRep(e.target);
|
|
1343
|
-
if (s !== t && g.hasNode(s) && g.hasNode(t)) {
|
|
1344
|
-
// Last-resort label legibility: reserve a virtual label node on the edge
|
|
1345
|
-
// so dagre widens the rank gap to fit the (wrapped) label.
|
|
1346
|
-
let edgeLabel: object = {};
|
|
1347
|
-
if (reserveEdgeLabels && e.label) {
|
|
1348
|
-
const m = measureEdgeLabel(e.label, EDGE_LABEL_FONT_SIZE);
|
|
1349
|
-
edgeLabel = { width: m.width, height: m.height, labelpos: 'c' };
|
|
1350
|
-
}
|
|
1351
|
-
g.setEdge(s, t, edgeLabel, edgeKey(e));
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
dagre.layout(g);
|
|
1355
|
-
|
|
1356
|
-
const nodes = parsed.nodes.map((n) => {
|
|
1357
|
-
const p = g.node(n.label);
|
|
1358
|
-
return {
|
|
1359
|
-
label: n.label,
|
|
1360
|
-
x: p.x,
|
|
1361
|
-
y: p.y,
|
|
1362
|
-
width: p.width,
|
|
1363
|
-
height: p.height,
|
|
1364
|
-
};
|
|
1365
|
-
});
|
|
1366
|
-
const groups: BLLayoutResult['groups'][number][] = parsed.groups.map(
|
|
1367
|
-
(grp) => {
|
|
1368
|
-
const p = g.node(gid(grp.label));
|
|
1369
|
-
return {
|
|
1370
|
-
label: grp.label,
|
|
1371
|
-
lineNumber: grp.lineNumber,
|
|
1372
|
-
x: p.x,
|
|
1373
|
-
y: p.y,
|
|
1374
|
-
width: p.width,
|
|
1375
|
-
height: p.height,
|
|
1376
|
-
collapsed: false,
|
|
1377
|
-
childCount: grp.children.length,
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
);
|
|
1381
|
-
for (const label of collapsedGroupLabels) {
|
|
1382
|
-
const p = g.node(gid(label));
|
|
1383
|
-
groups.push({
|
|
1384
|
-
label,
|
|
1385
|
-
lineNumber: 0,
|
|
1386
|
-
x: p.x,
|
|
1387
|
-
y: p.y,
|
|
1388
|
-
width: p.width,
|
|
1389
|
-
height: p.height,
|
|
1390
|
-
collapsed: true,
|
|
1391
|
-
childCount: collapseInfo?.collapsedChildCounts.get(label) ?? 0,
|
|
1392
|
-
});
|
|
1393
|
-
}
|
|
1394
|
-
// Boundary rects for expanded groups — used to clip group-incident edges.
|
|
1395
|
-
const groupRectByGid = new Map<
|
|
1396
|
-
string,
|
|
1397
|
-
{ x: number; y: number; w: number; h: number }
|
|
1398
|
-
>();
|
|
1399
|
-
for (const grp of parsed.groups) {
|
|
1400
|
-
const p = g.node(gid(grp.label));
|
|
1401
|
-
if (p)
|
|
1402
|
-
groupRectByGid.set(gid(grp.label), {
|
|
1403
|
-
x: p.x,
|
|
1404
|
-
y: p.y,
|
|
1405
|
-
w: p.width,
|
|
1406
|
-
h: p.height,
|
|
1407
|
-
});
|
|
1408
|
-
}
|
|
1409
|
-
const clipPointsToGroups = (
|
|
1410
|
-
pts: Pt[],
|
|
1411
|
-
source: string,
|
|
1412
|
-
target: string
|
|
1413
|
-
): Pt[] => {
|
|
1414
|
-
if (pts.length < 2) return pts;
|
|
1415
|
-
let out = pts;
|
|
1416
|
-
const srcRect = groupRectByGid.get(source);
|
|
1417
|
-
if (srcRect) {
|
|
1418
|
-
let k = 0;
|
|
1419
|
-
while (k < out.length - 1 && isInsideRect(out[k]!, srcRect)) k++;
|
|
1420
|
-
out = [rectBorderPoint(srcRect, out[k]!), ...out.slice(k)];
|
|
1421
|
-
}
|
|
1422
|
-
const tgtRect = groupRectByGid.get(target);
|
|
1423
|
-
if (tgtRect) {
|
|
1424
|
-
let k = out.length - 1;
|
|
1425
|
-
while (k > 0 && isInsideRect(out[k]!, tgtRect)) k--;
|
|
1426
|
-
out = [...out.slice(0, k + 1), rectBorderPoint(tgtRect, out[k]!)];
|
|
1427
|
-
}
|
|
1428
|
-
return out;
|
|
1429
|
-
};
|
|
1430
|
-
const edges: BLLayoutEdge[] = parsed.edges.flatMap((e) => {
|
|
1431
|
-
const s = srcRep(e.source);
|
|
1432
|
-
const t = tgtRep(e.target);
|
|
1433
|
-
const key = edgeKey(e);
|
|
1434
|
-
if (s === t || !g.hasEdge(s, t, key)) return [];
|
|
1435
|
-
const ed = g.edge(s, t, key) as { points?: Pt[] };
|
|
1436
|
-
const points = clipPointsToGroups(ed?.points ?? [], e.source, e.target);
|
|
1437
|
-
return [
|
|
1438
|
-
{
|
|
1439
|
-
source: e.source,
|
|
1440
|
-
target: e.target,
|
|
1441
|
-
...(e.label !== undefined && { label: e.label }),
|
|
1442
|
-
bidirectional: e.bidirectional,
|
|
1443
|
-
lineNumber: e.lineNumber,
|
|
1444
|
-
points,
|
|
1445
|
-
yOffset: 0,
|
|
1446
|
-
parallelCount: 1,
|
|
1447
|
-
metadata: e.metadata,
|
|
1448
|
-
},
|
|
1449
|
-
];
|
|
1450
|
-
});
|
|
1451
|
-
const gg = g.graph() as { width?: number; height?: number };
|
|
1452
|
-
return {
|
|
1453
|
-
nodes,
|
|
1454
|
-
edges,
|
|
1455
|
-
groups,
|
|
1456
|
-
width: gg.width ?? 800,
|
|
1457
|
-
height: gg.height ?? 600,
|
|
1458
|
-
} as BLLayoutResult;
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
// Pinned mode short-circuits the entire search.
|
|
1462
|
-
if (allPinned) return placePinned(pinned!);
|
|
1463
|
-
|
|
1464
|
-
const n = parsed.nodes.length;
|
|
1465
|
-
// ~500ms budget: search a larger pool, then refine the top few exactly.
|
|
1466
|
-
const seedCount =
|
|
1467
|
-
opts?.seeds ?? (n <= 12 ? 80 : n <= 22 ? 40 : n <= 35 ? 22 : 10);
|
|
1468
|
-
const REFINE_K = opts?.refineK ?? 6;
|
|
1469
|
-
const lambda = opts?.lambda ?? DEFAULT_LAMBDA;
|
|
1470
|
-
const prev = opts?.previousPositions;
|
|
1471
|
-
|
|
1472
|
-
// Candidate configs: every (ranker × spacing) combo + seed-shuffles of the
|
|
1473
|
-
// default. Diverse candidates lower the crossing floor; seed-shuffles vary
|
|
1474
|
-
// dagre's within-layer ordering.
|
|
1475
|
-
const RANKERS = ['network-simplex', 'tight-tree', 'longest-path'];
|
|
1476
|
-
const SPACINGS = [
|
|
1477
|
-
{ nodesep: 50, ranksep: 60 },
|
|
1478
|
-
{ nodesep: 34, ranksep: 46 },
|
|
1479
|
-
{ nodesep: 66, ranksep: 82 },
|
|
1480
|
-
];
|
|
1481
|
-
const configs: {
|
|
1482
|
-
ranker: string;
|
|
1483
|
-
nodesep: number;
|
|
1484
|
-
ranksep: number;
|
|
1485
|
-
seed?: number;
|
|
1486
|
-
}[] = [];
|
|
1487
|
-
for (const ranker of RANKERS)
|
|
1488
|
-
for (const sp of SPACINGS) configs.push({ ranker, ...sp });
|
|
1489
|
-
for (let s = 0; s < seedCount; s++)
|
|
1490
|
-
configs.push({
|
|
1491
|
-
ranker: 'network-simplex',
|
|
1492
|
-
nodesep: 50,
|
|
1493
|
-
ranksep: 60,
|
|
1494
|
-
seed: s,
|
|
1495
|
-
});
|
|
1496
|
-
|
|
1497
|
-
// Honest "badness" — every kind of line-in-the-wrong-place counts equally:
|
|
1498
|
-
// X true crossings + O overlap runs (lines stepping on each other)
|
|
1499
|
-
// + P edges piercing unrelated node boxes.
|
|
1500
|
-
// (A line through a node and two lines sharing a path are crossings too.)
|
|
1501
|
-
// `floor` lets callers skip the expensive O/P passes once X alone already
|
|
1502
|
-
// exceeds the best badness found so far (it can't win, return Infinity).
|
|
1503
|
-
const badness = (lay: BLLayoutResult, floor: number): number => {
|
|
1504
|
-
const x = countSplineCrossings(lay, floor);
|
|
1505
|
-
if (x > floor) return Infinity;
|
|
1506
|
-
return (
|
|
1507
|
-
x +
|
|
1508
|
-
countEdgeOverlaps(lay) +
|
|
1509
|
-
countEdgeNodePierces(lay) +
|
|
1510
|
-
countGroupOverlaps(lay)
|
|
1511
|
-
);
|
|
1512
|
-
};
|
|
1513
|
-
|
|
1514
|
-
// Objective: badness dominates (×1e6, strictly fewer wins); ties broken by
|
|
1515
|
-
// total edge length (positioning) + stability drift (only when prev given).
|
|
1516
|
-
const objective = (lay: BLLayoutResult, viol: number) =>
|
|
1517
|
-
viol * 1e6 + edgeLength(lay) + lambda * meanDrift(lay, prev) * 10;
|
|
1518
|
-
|
|
1519
|
-
// Progress is reported over the two dominant phases: building the dagre
|
|
1520
|
-
// candidate pool, then exact-scoring the top few. `refineK` is clamped below,
|
|
1521
|
-
// so estimate the total here for a smooth bar.
|
|
1522
|
-
const progressTotal =
|
|
1523
|
-
configs.length + Math.min(opts?.refineK ?? 6, configs.length);
|
|
1524
|
-
let progressDone = 0;
|
|
1525
|
-
const step = async (phase: string): Promise<void> => {
|
|
1526
|
-
if (!onProgress) return;
|
|
1527
|
-
onProgress(++progressDone, progressTotal, phase);
|
|
1528
|
-
const now = performance.now();
|
|
1529
|
-
if (
|
|
1530
|
-
now - searchStart > YIELD_AFTER_MS &&
|
|
1531
|
-
now - lastYield > YIELD_EVERY_MS
|
|
1532
|
-
) {
|
|
1533
|
-
lastYield = now;
|
|
1534
|
-
await tick();
|
|
1535
|
-
}
|
|
1536
|
-
};
|
|
1537
|
-
|
|
1538
|
-
// Build the candidate pool.
|
|
1539
|
-
const pool: BLLayoutResult[] = [];
|
|
1540
|
-
for (const cfg of configs) {
|
|
1541
|
-
try {
|
|
1542
|
-
pool.push(place(cfg));
|
|
1543
|
-
} catch {
|
|
1544
|
-
/* some rankers choke on odd graphs */
|
|
1545
|
-
}
|
|
1546
|
-
await step('Optimizing layout');
|
|
1547
|
-
}
|
|
1548
|
-
if (!pool.length)
|
|
1549
|
-
return place({ ranker: 'network-simplex', nodesep: 50, ranksep: 60 });
|
|
1550
|
-
|
|
1551
|
-
// Home-grown layered candidates (flat graphs only). These own the
|
|
1552
|
-
// crossing-minimization stage AND route back-edges around the periphery, so
|
|
1553
|
-
// they can reach layouts below dagre's ordering+routing floor (e.g. the
|
|
1554
|
-
// pirate-fleet K2,2). Their peripheral back-edges are curved loops that the
|
|
1555
|
-
// cheap straight-segment ranker mis-scores, so they bypass stage-1 and are
|
|
1556
|
-
// ALWAYS exact-scored in stage 2. Best-effort: never block the dagre pool.
|
|
1557
|
-
let layered: BLLayoutResult[] = [];
|
|
1558
|
-
try {
|
|
1559
|
-
layered = layeredCandidates(parsed, sizes);
|
|
1560
|
-
} catch {
|
|
1561
|
-
/* ignore */
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
// Tier-banded grouped candidates. The dagre path treats `[Group]` containers
|
|
1565
|
-
// as compound clusters with no tier ORDER and no cycle-aware routing, so
|
|
1566
|
-
// tiered architecture diagrams collide bands side-by-side and balloon on
|
|
1567
|
-
// back-edges. This generator orders the groups into disjoint rank bands and
|
|
1568
|
-
// routes against-tier edges around the periphery. Additive — kept only on a
|
|
1569
|
-
// strict badness win. Skipped when any group is collapsed (the generator
|
|
1570
|
-
// models fully-expanded groups only).
|
|
1571
|
-
if (parsed.groups.length > 0 && collapsedGroupLabels.size === 0) {
|
|
1572
|
-
try {
|
|
1573
|
-
layered = layered.concat(groupedTierCandidates(parsed, sizes));
|
|
1574
|
-
} catch {
|
|
1575
|
-
/* ignore */
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
// Stage 1: rank the dagre pool with the cheap straight-segment counter — a
|
|
1580
|
-
// cheap proxy to pick which candidates are worth the expensive exact scoring.
|
|
1581
|
-
// Widen REFINE_K a little since the proxy only sees crossings, not O/P.
|
|
1582
|
-
// Pre-score each candidate ONCE (countCrossingsFast is O(E²)) into a key map,
|
|
1583
|
-
// then sort by the stored key — the comparator otherwise recomputes the score
|
|
1584
|
-
// for both operands on every comparison (O(C log C) calls vs C). The score is a
|
|
1585
|
-
// pure function of the layout, so the resulting order is identical.
|
|
1586
|
-
const fastKey = new Map<BLLayoutResult, number>();
|
|
1587
|
-
for (const lay of pool)
|
|
1588
|
-
fastKey.set(lay, objective(lay, countCrossingsFast(lay)));
|
|
1589
|
-
pool.sort((a, b) => fastKey.get(a)! - fastKey.get(b)!);
|
|
1590
|
-
const refineK = Math.min(REFINE_K, pool.length);
|
|
1591
|
-
|
|
1592
|
-
// Stage 2: exact-score the top-K dagre candidates on the FULL badness (X+O+P)
|
|
1593
|
-
// and pick the best — so the placement search avoids overlaps and node-pierces,
|
|
1594
|
-
// not just crossings.
|
|
1595
|
-
let best = pool[0]!;
|
|
1596
|
-
let bestObj = Infinity;
|
|
1597
|
-
let bestBad = Infinity;
|
|
1598
|
-
const consider = (lay: BLLayoutResult): void => {
|
|
1599
|
-
const bad = badness(lay, bestBad);
|
|
1600
|
-
if (bad === Infinity) return;
|
|
1601
|
-
const sc = objective(lay, bad);
|
|
1602
|
-
if (sc < bestObj) {
|
|
1603
|
-
bestObj = sc;
|
|
1604
|
-
bestBad = bad;
|
|
1605
|
-
best = lay;
|
|
1606
|
-
}
|
|
1607
|
-
};
|
|
1608
|
-
for (const lay of pool.slice(0, refineK)) {
|
|
1609
|
-
consider(lay);
|
|
1610
|
-
await step('Refining layout');
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
// Adaptive escalation: a still-high badness after the base seed budget means
|
|
1614
|
-
// the graph is genuinely hard — dense layouts (e.g. the marketplace) need many
|
|
1615
|
-
// more random restarts to stumble onto a low-crossing layer ordering, and the
|
|
1616
|
-
// good ordering is rare enough that refining the existing pool can't find it
|
|
1617
|
-
// (only generating fresh seeds can). Spend an extra seed batch — but ONLY when
|
|
1618
|
-
// the result is actually bad, so the easy 0-badness majority of the corpus
|
|
1619
|
-
// never pays the latency. Bounded by node count to keep the worst case ~1s.
|
|
1620
|
-
if (bestBad >= ESCALATE_THRESHOLD && n <= ESCALATE_MAX_N) {
|
|
1621
|
-
const extra: BLLayoutResult[] = [];
|
|
1622
|
-
for (let s = seedCount; s < seedCount + ESCALATE_SEEDS; s++) {
|
|
1623
|
-
try {
|
|
1624
|
-
extra.push(
|
|
1625
|
-
place({
|
|
1626
|
-
ranker: 'network-simplex',
|
|
1627
|
-
nodesep: 50,
|
|
1628
|
-
ranksep: 60,
|
|
1629
|
-
seed: s,
|
|
1630
|
-
})
|
|
1631
|
-
);
|
|
1632
|
-
} catch {
|
|
1633
|
-
/* ignore choking rankers */
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
const extraKey = new Map<BLLayoutResult, number>();
|
|
1637
|
-
for (const lay of extra)
|
|
1638
|
-
extraKey.set(lay, objective(lay, countCrossingsFast(lay)));
|
|
1639
|
-
extra.sort((a, b) => extraKey.get(a)! - extraKey.get(b)!);
|
|
1640
|
-
for (const lay of extra.slice(0, ESCALATE_REFINE)) consider(lay);
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
// Layered candidates (and their better-routed, de-pierced variants) replace
|
|
1644
|
-
// the dagre winner ONLY on a STRICT total-badness reduction — never on an
|
|
1645
|
-
// edge-length tiebreak. They're few, so de-piercing each is cheap.
|
|
1646
|
-
for (const lay of layered) {
|
|
1647
|
-
const variants = [lay];
|
|
1648
|
-
const dp = deroutePierces(lay);
|
|
1649
|
-
if (dp !== lay) variants.push(dp);
|
|
1650
|
-
for (const v of variants) {
|
|
1651
|
-
const bad = badness(v, bestBad - 1);
|
|
1652
|
-
if (bad < bestBad) {
|
|
1653
|
-
bestBad = bad;
|
|
1654
|
-
best = v;
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
// Feed the objective one more BETTER-ROUTED alternative: bend the current
|
|
1660
|
-
// winner's edges around any node they still pierce. Kept only if it strictly
|
|
1661
|
-
// lowers total badness (a detour that trades a pierce for a crossing/overlap
|
|
1662
|
-
// is rejected).
|
|
1663
|
-
if (bestBad > 0) {
|
|
1664
|
-
const rerouted = deroutePierces(best);
|
|
1665
|
-
if (rerouted !== best && badness(rerouted, bestBad - 1) < bestBad)
|
|
1666
|
-
best = rerouted;
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
// Better-placed alternative: if the winner still has overlapping group bands
|
|
1670
|
-
// (dagre wedged a small group into a tight channel), push the bands apart along
|
|
1671
|
-
// the cross-axis. Kept only on strict total-badness drop.
|
|
1672
|
-
if (bestBad > 0 && countGroupOverlaps(best) > 0) {
|
|
1673
|
-
const separated = separateGroupBands(best, parsed);
|
|
1674
|
-
if (separated !== best && badness(separated, bestBad - 1) < bestBad)
|
|
1675
|
-
best = separated;
|
|
1676
|
-
}
|
|
1677
|
-
return best;
|
|
1678
|
-
}
|