@diagrammo/dgmo 0.94.0 → 0.96.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/README.md +1 -1
- package/dist/advanced.d.ts +97 -570
- package/dist/advanced.js +176 -352
- package/dist/block.d.ts +6 -0
- package/dist/block.js +59 -57
- package/dist/{calculator-BNH3GSJE.js → calculator-GIB7POFX.js} +2 -2
- package/dist/{chart-meta-BbjsRY7o.d.ts → chart-meta-uImk4eTn.d.ts} +1 -1
- package/dist/chart-meta.d.ts +2 -2
- package/dist/chart-meta.js +16 -16
- package/dist/{chart-types-Cppnp31G.d.ts → chart-types-CBVYiLqy.d.ts} +1 -5
- package/dist/{charts-d3-SZV2SFTM.js → charts-d3-IRP5VANE.js} +5 -5
- package/dist/{chunk-MLY5ALOV.js → chunk-2BYFNZVM.js} +2 -2
- package/dist/{chunk-OSSTOSXU.js → chunk-2M3K6GD2.js} +3 -6
- package/dist/{chunk-CDM6XMOT.js → chunk-34JOHSVE.js} +2 -2
- package/dist/chunk-35A3J4Z5.js +14 -0
- package/dist/{chunk-HI6R27YA.js → chunk-3W73ZP6W.js} +9 -4
- package/dist/{chunk-IHN6WDEY.js → chunk-4B45SNZH.js} +5 -5
- package/dist/{chunk-AULZD45V.js → chunk-5ARANBIE.js} +2 -2
- package/dist/{chunk-DDIOWNHS.js → chunk-5BG4OSWJ.js} +6 -6
- package/dist/{chunk-6JCTW74W.js → chunk-5G4DOOX2.js} +1 -1
- package/dist/{chunk-JT2BG7LM.js → chunk-5GU6C5WB.js} +3 -3
- package/dist/{chunk-EBIDNRSR.js → chunk-5H7SA5ZD.js} +1 -1
- package/dist/{chunk-CN4RDJDV.js → chunk-5LG526X7.js} +1 -1
- package/dist/{chunk-VDY2GR6U.js → chunk-67N26CER.js} +2 -2
- package/dist/{chunk-G3HZ4PGE.js → chunk-67RKJEKF.js} +6 -6
- package/dist/{chunk-D3K4U3ZM.js → chunk-6K2W3FH7.js} +8 -5
- package/dist/{chunk-C6G36F5S.js → chunk-7UWVHNPM.js} +4 -4
- package/dist/{chunk-FWVE373X.js → chunk-AQRSEQZV.js} +1 -1
- package/dist/{chunk-BQO24BDM.js → chunk-AUXUNV73.js} +1 -1
- package/dist/{chunk-3PP746YM.js → chunk-B2YVZ3QU.js} +1 -1
- package/dist/{chunk-QPQWY5RU.js → chunk-B5CEV7IR.js} +3 -3
- package/dist/{chunk-J7Q4AB23.js → chunk-B7BSRVNC.js} +2 -2
- package/dist/{chunk-IXHWS2HI.js → chunk-BD4O4D2A.js} +2 -2
- package/dist/{chunk-4TEPDQHJ.js → chunk-BUVLLK3D.js} +7 -7
- package/dist/{chunk-B25IWGDH.js → chunk-CD7VQCDL.js} +8 -8
- package/dist/{chunk-I37S3EZE.js → chunk-CP73WWLN.js} +371 -286
- package/dist/{chunk-GI5R5B3S.js → chunk-D7DGLJ7N.js} +2 -2
- package/dist/{chunk-3TWBMU7K.js → chunk-DKCVAQU3.js} +9 -9
- package/dist/{chunk-KBDMEOLI.js → chunk-DPNSDCR3.js} +1 -1
- package/dist/{chunk-BRGRIZQB.js → chunk-DWMQ24JR.js} +2 -2
- package/dist/{chunk-4XYZJTFO.js → chunk-EM5SA7NC.js} +11 -3
- package/dist/{chunk-AM42TLSZ.js → chunk-EMGNCG5C.js} +1 -1
- package/dist/{chunk-3KVUFHUJ.js → chunk-EPB3UEMD.js} +2 -2
- package/dist/{chunk-NXU4N5WT.js → chunk-EZ6BI5LL.js} +1 -1
- package/dist/{chunk-2ZW4NBYX.js → chunk-FBK32S4D.js} +1 -1
- package/dist/{chunk-2I4UQG7W.js → chunk-FHX373L5.js} +1 -1
- package/dist/{chunk-AQQUJEDN.js → chunk-FKJWZWNU.js} +1 -1
- package/dist/{chunk-TIIZIAGS.js → chunk-FUYEXLAN.js} +6 -13
- package/dist/{chunk-TASRGDYP.js → chunk-GEHK4UD5.js} +3 -3
- package/dist/{chunk-ZD3QCNB7.js → chunk-GLL237AP.js} +2 -2
- package/dist/{chunk-PGSGJLUR.js → chunk-GM2SLMQW.js} +2 -2
- package/dist/{chunk-NOQB4KMV.js → chunk-H7JVKUA5.js} +5 -5
- package/dist/{chunk-DNM3XZCK.js → chunk-H7RQG4PH.js} +2 -2
- package/dist/{chunk-7SXV7NUS.js → chunk-HH3DHNQA.js} +2 -2
- package/dist/{chunk-GWQRTA5W.js → chunk-HZX4QZGR.js} +1 -1
- package/dist/{chunk-KBEWVCK3.js → chunk-I2I2FKAK.js} +1 -1
- package/dist/{chunk-2HSHWPTO.js → chunk-I3ZRIMR7.js} +10 -10
- package/dist/{chunk-7IOZ6JE4.js → chunk-IKXQ6NVR.js} +7 -7
- package/dist/{chunk-XJQ6Z3FO.js → chunk-IQZSKWE5.js} +3 -3
- package/dist/{chunk-3GUEYQGL.js → chunk-ITDF3MWD.js} +3 -3
- package/dist/{chunk-JXGSTBW6.js → chunk-IU7XKZA6.js} +2 -2
- package/dist/{chunk-RITTLIDJ.js → chunk-IZFJZCF6.js} +4 -4
- package/dist/{chunk-DRQSB72S.js → chunk-JBAFAKGX.js} +7 -7
- package/dist/{chunk-VSAKEMDD.js → chunk-JCC3B2GS.js} +8 -2
- package/dist/{chunk-66JTY4AN.js → chunk-JRN2CRXJ.js} +1 -1
- package/dist/{chunk-2FDQEQID.js → chunk-K2SFH3GJ.js} +2 -2
- package/dist/{chunk-NM5V5EKY.js → chunk-K2WXMXI2.js} +1 -1
- package/dist/{chunk-XKCRKD2B.js → chunk-KDWEOT54.js} +24 -12
- package/dist/{chunk-PVWIBKV2.js → chunk-KQAPFIYF.js} +2 -2
- package/dist/{chunk-4SACN2C5.js → chunk-LGUFJ7Y2.js} +1 -1
- package/dist/{chunk-AYUSD4Z6.js → chunk-LRHPBWOY.js} +5 -5
- package/dist/{chunk-DLJJOCIT.js → chunk-M4ZUIW3J.js} +3 -3
- package/dist/{chunk-CIHHTS5B.js → chunk-MAYTYF5S.js} +3 -3
- package/dist/{chunk-5ZUZZDW5.js → chunk-MVUNMJKO.js} +46 -52
- package/dist/{chunk-KBHT5XEY.js → chunk-MYHR3PMH.js} +194 -35
- package/dist/{chunk-NBAAIRFP.js → chunk-NI4L6HEU.js} +4 -1
- package/dist/{chunk-A2A2I4Z4.js → chunk-O2HNVNKO.js} +2 -2
- package/dist/{chunk-2XMTZ5JR.js → chunk-OAZ6BIIB.js} +3 -3
- package/dist/{chunk-IZHGT5HD.js → chunk-OO4GUE4E.js} +3 -3
- package/dist/{chunk-KSZB7TIY.js → chunk-OYGJZA24.js} +8 -8
- package/dist/{chunk-H6BT45SN.js → chunk-P2GNNEMN.js} +1 -1
- package/dist/{chunk-5HAYT43L.js → chunk-P3PSLC2K.js} +22 -6
- package/dist/{chunk-HHVIE45A.js → chunk-P6LKX3IQ.js} +4 -4
- package/dist/{chunk-NMS5XSLX.js → chunk-PGD6CNWO.js} +1 -1
- package/dist/{chunk-NVZ7R4EK.js → chunk-PWZEJNO6.js} +3 -3
- package/dist/{chunk-NPDVWFAQ.js → chunk-QEU6VM4O.js} +1 -1
- package/dist/{chunk-UIEKW4SG.js → chunk-R5L5E3TC.js} +0 -1
- package/dist/{chunk-2WQ3YZMN.js → chunk-RGFVHYDP.js} +2 -2
- package/dist/{chunk-YQMIKX54.js → chunk-RIK6WUMO.js} +62 -29
- package/dist/{chunk-BH2SF4TT.js → chunk-RTRORHSP.js} +1 -1
- package/dist/{chunk-4SP6OUO7.js → chunk-RVOSA4RR.js} +845 -193
- package/dist/{chunk-LKZCUFOW.js → chunk-SCYVGPLI.js} +2 -2
- package/dist/{chunk-XIUGSUE4.js → chunk-SIABEREK.js} +6 -2
- package/dist/{chunk-P7TRFL4P.js → chunk-SQBCNEJB.js} +4 -1
- package/dist/{chunk-3ZP3QC47.js → chunk-THTYCJGF.js} +2 -2
- package/dist/{chunk-4EX4OKGZ.js → chunk-TIPNNOZC.js} +5 -5
- package/dist/{chunk-UZ7EZW3B.js → chunk-TO53KK2P.js} +6 -1
- package/dist/{chunk-2MS623GW.js → chunk-TPCP2UOI.js} +3 -3
- package/dist/{chunk-Y3SIQN4J.js → chunk-TUOZ6AXC.js} +2 -2
- package/dist/{chunk-KRPLP5LP.js → chunk-U6N547QA.js} +1 -1
- package/dist/{chunk-RESEJSCV.js → chunk-VJ7NLYFE.js} +2 -2
- package/dist/{chunk-UXNA2H2V.js → chunk-VLI6AWS3.js} +2 -2
- package/dist/{chunk-TDAGJJPZ.js → chunk-VNAPEZX2.js} +1 -1
- package/dist/{chunk-EBVVMUAA.js → chunk-VVTCUIPN.js} +1 -1
- package/dist/{chunk-FVTYJUXR.js → chunk-W5MMPPGX.js} +2 -2
- package/dist/{chunk-BQMHJ6OW.js → chunk-WGNP5ND2.js} +20 -12
- package/dist/{chunk-5OTA2PUH.js → chunk-WLGOIMTI.js} +2 -2
- package/dist/{chunk-IGJEKO2P.js → chunk-WTPS73RM.js} +4 -4
- package/dist/{chunk-R6EGUTIV.js → chunk-YFIUABSF.js} +2 -2
- package/dist/{chunk-7PJBJ2DU.js → chunk-YSGOXZBB.js} +1 -1
- package/dist/{chunk-ZECJ7Q6C.js → chunk-YVIPJGFG.js} +2 -2
- package/dist/{chunk-5KKZXA44.js → chunk-ZBOIYC2G.js} +4 -2
- package/dist/{chunk-RBVFNUAL.js → chunk-ZOKD5F7M.js} +2 -2
- package/dist/{collapse-BSPH3BFM.js → collapse-6X5S2S3G.js} +2 -1
- package/dist/completion-registry.js +6 -37
- package/dist/copy-image.d.ts +34 -0
- package/dist/copy-image.js +96 -0
- package/dist/countdown.js +1 -1
- package/dist/dgmo-router-EPBQKQW2.js +100 -0
- package/dist/{dimensions-DS6FWLRY.js → dimensions-DBCRBZIU.js} +4 -4
- package/dist/editor.js +2 -7
- package/dist/{flowchart-parser-NZXASOZG.js → flowchart-parser-SM5LHF2D.js} +4 -4
- package/dist/{flowchart-renderer-PTCRIANF.js → flowchart-renderer-PPS2GTTD.js} +10 -10
- package/dist/highlight.js +3 -8
- package/dist/index.d.ts +42 -6
- package/dist/index.js +62 -57
- package/dist/{layout-MXHR3P35.js → layout-3J72UULM.js} +3 -3
- package/dist/{layout-U3AH6M7J.js → layout-AF4KA5B2.js} +4 -4
- package/dist/{layout-IT4ZTV6C.js → layout-FCFUVCRI.js} +7 -7
- package/dist/{layout-KV27UW25.js → layout-H2KP5A6M.js} +1 -1
- package/dist/{layout-ND26WVX2.js → layout-IXQNVRBE.js} +3 -3
- package/dist/{layout-ISSBWBH7.js → layout-KELSNWBD.js} +7 -7
- package/dist/{layout-JTIRCKAW.js → layout-LNVIH45F.js} +3 -3
- package/dist/{layout-VC3MXRB4.js → layout-MPSH3YWE.js} +7 -7
- package/dist/{layout-HFBK5UEG.js → layout-PPIVGJXD.js} +2 -2
- package/dist/{layout-RH3N46HP.js → layout-R6G4MEO3.js} +4 -3
- package/dist/{layout-search-BBX2BMEF.js → layout-search-C2TYUH4L.js} +5 -5
- package/dist/{parser-BT4NI6JY.js → parser-2AXU2MHJ.js} +3 -3
- package/dist/{parser-3QIFY5IN.js → parser-4BOP77CD.js} +2 -2
- package/dist/{parser-TAYRTCPR.js → parser-4GHAYZKZ.js} +5 -5
- package/dist/{parser-LTRQASGB.js → parser-5HQPPVHJ.js} +3 -3
- package/dist/{parser-EAQPZX2B.js → parser-5RUD4A3X.js} +3 -3
- package/dist/{parser-BJ7RJNUM.js → parser-6WURFLOB.js} +3 -3
- package/dist/{parser-R3LPEQIT.js → parser-722KLLAQ.js} +4 -4
- package/dist/{parser-KMKVFET2.js → parser-B7SCN7FG.js} +2 -2
- package/dist/{parser-TUAEX6ML.js → parser-BNRX35NT.js} +3 -3
- package/dist/{parser-NL73FSFI.js → parser-BPYHIG24.js} +3 -3
- package/dist/{parser-3LT3ZUUC.js → parser-CRJYLOHA.js} +2 -2
- package/dist/{parser-NUU3DVW4.js → parser-DGLKH4NT.js} +2 -2
- package/dist/{parser-5UZV32ZI.js → parser-DRUGM3R6.js} +2 -2
- package/dist/{parser-KLCSFQ3J.js → parser-DWV2CYBS.js} +3 -3
- package/dist/{parser-2IKPQVKX.js → parser-F3JJNIFC.js} +3 -3
- package/dist/{parser-EFF22GYO.js → parser-F75WNYMK.js} +3 -3
- package/dist/{parser-RTWPFKXG.js → parser-GBQY3WOZ.js} +3 -3
- package/dist/{parser-F7NDRPMA.js → parser-GM5B2EPW.js} +3 -3
- package/dist/{parser-K2DI26SC.js → parser-H6HT2WFQ.js} +2 -2
- package/dist/{parser-PEZ7RN43.js → parser-HM6FEX5K.js} +3 -3
- package/dist/{parser-YG625J5H.js → parser-L7SI3PW4.js} +4 -4
- package/dist/{parser-GMQJISZ6.js → parser-LUJX7AXO.js} +2 -2
- package/dist/{parser-A3AN76RQ.js → parser-MWINQNQW.js} +3 -3
- package/dist/{parser-KJPPUN4D.js → parser-OHBBDCG4.js} +2 -2
- package/dist/{parser-4RFY7D52.js → parser-QWTECQEC.js} +5 -5
- package/dist/{parser-4TBSWULQ.js → parser-TTGZYLYD.js} +3 -3
- package/dist/{parser-6TDWUKM5.js → parser-UHKROC7Z.js} +3 -3
- package/dist/{parser-H5OODY5N.js → parser-W6NX3AIV.js} +3 -3
- package/dist/{parser-GDLKUBTO.js → parser-WIKXZP4X.js} +2 -2
- package/dist/{parser-4Y6PODW6.js → parser-XEJP37GQ.js} +3 -3
- package/dist/{parser-RCIEKN7Y.js → parser-Y7QXD2UR.js} +2 -2
- package/dist/{parser-7QKQ4P4P.js → parser-ZV756RMP.js} +5 -5
- package/dist/{parser-KKF6Y5MM.js → parser-ZWWQZ4XZ.js} +4 -3
- package/dist/{redraw-AHW7EUUF.js → redraw-BZ4GI6IC.js} +6 -4
- package/dist/{redraw-STZ4WJAI.js → redraw-LEOXS7C6.js} +3 -3
- package/dist/{renderer-J3GPDK73.js → renderer-2MCV75JQ.js} +12 -12
- package/dist/{renderer-TINHO2IO.js → renderer-2V2XY2V7.js} +4 -4
- package/dist/{renderer-PHLCYVSN.js → renderer-5AEDVLAA.js} +8 -7
- package/dist/{renderer-W4MUMXJE.js → renderer-5GP7YUS4.js} +3 -3
- package/dist/{renderer-FMTLSOCX.js → renderer-66NCRXSC.js} +6 -6
- package/dist/{renderer-AM7P4LN7.js → renderer-6HU44IGJ.js} +4 -4
- package/dist/{renderer-JJ4QW3SK.js → renderer-7C76HP5W.js} +6 -6
- package/dist/{renderer-O6RP52CQ.js → renderer-7RAMBMTH.js} +6 -6
- package/dist/{renderer-M5DUMITP.js → renderer-BYLQ37T2.js} +8 -8
- package/dist/{renderer-7OGGV327.js → renderer-CVCCW7UT.js} +4 -4
- package/dist/{renderer-QTPWRRJ7.js → renderer-D6EH5UQ6.js} +6 -6
- package/dist/renderer-E3334XLT.js +84 -0
- package/dist/{renderer-IXBOT6DU.js → renderer-F7YGMEGQ.js} +2 -2
- package/dist/{renderer-XCLARXCK.js → renderer-GKY55M4Z.js} +5 -5
- package/dist/{renderer-KJ3V5BYP.js → renderer-LFZKG2GR.js} +5 -5
- package/dist/{renderer-2LYX6NHN.js → renderer-LJPUCG3U.js} +5 -5
- package/dist/{renderer-DAZWHNLI.js → renderer-MGF2NNGG.js} +3 -3
- package/dist/{renderer-HX6ICPDX.js → renderer-MVBAQP3M.js} +4 -4
- package/dist/{renderer-XOWDERU2.js → renderer-MZIWP55B.js} +5 -5
- package/dist/{renderer-MWZC7TMD.js → renderer-N6PM5CSG.js} +7 -7
- package/dist/{renderer-ISQJ2ELA.js → renderer-N75E3UVY.js} +5 -5
- package/dist/{renderer-53B523PM.js → renderer-ONENGXFQ.js} +7 -7
- package/dist/{renderer-RUSQJFFC.js → renderer-PEJF6R2I.js} +2 -2
- package/dist/{renderer-36446HSH.js → renderer-QZJZIAT3.js} +2 -2
- package/dist/{renderer-UTEEJDGB.js → renderer-SYRH3B62.js} +6 -6
- package/dist/{renderer-LNP23NSM.js → renderer-TS5NHOQD.js} +6 -6
- package/dist/{renderer-J7ZSTJUE.js → renderer-VNJOZPSH.js} +7 -7
- package/dist/{renderer-OUDHH5MV.js → renderer-VNUDN6F5.js} +12 -12
- package/dist/{renderer-JYHA6R2D.js → renderer-VUXAAGUG.js} +5 -5
- package/dist/{renderer-BXD5KVSR.js → renderer-YJVQT7QG.js} +6 -6
- package/dist/{renderer-radial-WFUX3Z3C.js → renderer-radial-4R7AKETK.js} +5 -5
- package/dist/{state-parser-KPVW5CRI.js → state-parser-EQBBEKHR.js} +5 -5
- package/dist/{state-renderer-ZVEI265C.js → state-renderer-SOGZ2CNN.js} +12 -12
- package/dist/{themes-DzIH3Hjv.d.ts → themes-CbJL3w8L.d.ts} +1 -1
- package/docs/ai-integration.md +5 -6
- package/docs/language-reference.md +69 -105
- package/gallery/fixtures/whiteboard.dgmo +1 -0
- package/package.json +5 -1
- package/dist/chunk-6BQSDF77.js +0 -734
- package/dist/chunk-NWSPPGIE.js +0 -60
- package/dist/chunk-VBMRZCMQ.js +0 -1465
- package/dist/chunk-WZBZXZ6T.js +0 -636
- package/dist/dgmo-router-E5O7RJSW.js +0 -101
- package/dist/layout-D4L44NRG.js +0 -11
- package/dist/parser-YHDGKPZI.js +0 -14
- package/dist/renderer-4SE7R22T.js +0 -85
- package/dist/renderer-WPN2SXGT.js +0 -29
- package/gallery/fixtures/sketch.dgmo +0 -22
package/dist/chunk-VBMRZCMQ.js
DELETED
|
@@ -1,1465 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
renderCollapseBar,
|
|
3
|
-
renderNodeCard
|
|
4
|
-
} from "./chunk-R2K5L4UA.js";
|
|
5
|
-
import {
|
|
6
|
-
appendArrowheadMarkers,
|
|
7
|
-
appendReverseArrowheadMarkers
|
|
8
|
-
} from "./chunk-XBJJP2HN.js";
|
|
9
|
-
import {
|
|
10
|
-
ScaleContext
|
|
11
|
-
} from "./chunk-24YPR5EH.js";
|
|
12
|
-
import {
|
|
13
|
-
layoutInlineHeader
|
|
14
|
-
} from "./chunk-UIEKW4SG.js";
|
|
15
|
-
import {
|
|
16
|
-
TITLE_FONT_SIZE,
|
|
17
|
-
TITLE_FONT_WEIGHT,
|
|
18
|
-
TITLE_Y
|
|
19
|
-
} from "./chunk-VLC6WNRD.js";
|
|
20
|
-
import {
|
|
21
|
-
ARROWHEAD_HEIGHT,
|
|
22
|
-
ARROWHEAD_WIDTH,
|
|
23
|
-
CARD_RADIUS,
|
|
24
|
-
COLLAPSE_BAR_HEIGHT,
|
|
25
|
-
CONTAINER_LABEL_FONT_SIZE,
|
|
26
|
-
CONTAINER_RADIUS,
|
|
27
|
-
EDGE_DASH,
|
|
28
|
-
EDGE_LABEL_KNOCKOUT_OPACITY,
|
|
29
|
-
EDGE_STROKE_WIDTH,
|
|
30
|
-
HEADER_HEIGHT,
|
|
31
|
-
LABEL_FONT_SIZE,
|
|
32
|
-
META_FONT_SIZE,
|
|
33
|
-
NODE_STROKE_WIDTH
|
|
34
|
-
} from "./chunk-XCKNFFKM.js";
|
|
35
|
-
import {
|
|
36
|
-
renderIntegratedLegend
|
|
37
|
-
} from "./chunk-NMS5XSLX.js";
|
|
38
|
-
import {
|
|
39
|
-
getLegendExtent,
|
|
40
|
-
getMaxLegendReservedHeight
|
|
41
|
-
} from "./chunk-JILEYZ2V.js";
|
|
42
|
-
import {
|
|
43
|
-
FONT_FAMILY
|
|
44
|
-
} from "./chunk-ICLB27PZ.js";
|
|
45
|
-
import {
|
|
46
|
-
UNTAGGED_TAG_COLOR,
|
|
47
|
-
resolveActiveTagGroup,
|
|
48
|
-
resolveGroupTagColor,
|
|
49
|
-
resolveTagColor,
|
|
50
|
-
tagAttrKey
|
|
51
|
-
} from "./chunk-4SACN2C5.js";
|
|
52
|
-
import {
|
|
53
|
-
fitWrapped,
|
|
54
|
-
measureText
|
|
55
|
-
} from "./chunk-KAO7JL4I.js";
|
|
56
|
-
import {
|
|
57
|
-
contrastText,
|
|
58
|
-
groupFill,
|
|
59
|
-
groupStroke,
|
|
60
|
-
mix,
|
|
61
|
-
shapeFill,
|
|
62
|
-
themeBaseBg
|
|
63
|
-
} from "./chunk-6GXLAKUH.js";
|
|
64
|
-
|
|
65
|
-
// src/sketch/renderer.ts
|
|
66
|
-
import * as d3 from "d3-selection";
|
|
67
|
-
|
|
68
|
-
// src/sketch/markdown-card.ts
|
|
69
|
-
var INDENT_PX = 12;
|
|
70
|
-
var BULLET_HANG_PX = 12;
|
|
71
|
-
var ELLIPSIS = "\u2026";
|
|
72
|
-
var BULLET = "\u2022";
|
|
73
|
-
function segWidth(seg, fontSize) {
|
|
74
|
-
return measureText(seg.text, fontSize, { bold: seg.bold });
|
|
75
|
-
}
|
|
76
|
-
function parseInline(text) {
|
|
77
|
-
const segments = [];
|
|
78
|
-
const regex = /\*\*(.+?)\*\*|\[([^\]]*)\]\(([^)]*)\)|((?:(?!\*\*|\[).)+)/gs;
|
|
79
|
-
let m;
|
|
80
|
-
while ((m = regex.exec(text)) !== null) {
|
|
81
|
-
if (m[1] !== void 0) {
|
|
82
|
-
segments.push({ text: m[1], bold: true });
|
|
83
|
-
} else if (m[2] !== void 0) {
|
|
84
|
-
segments.push({ text: m[2], bold: false, ...m[3] && { href: m[3] } });
|
|
85
|
-
} else if (m[4] !== void 0) {
|
|
86
|
-
segments.push({ text: m[4], bold: false });
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return segments;
|
|
90
|
-
}
|
|
91
|
-
function segmentsToWords(segments) {
|
|
92
|
-
const words = [];
|
|
93
|
-
let current = [];
|
|
94
|
-
for (const seg of segments) {
|
|
95
|
-
const parts = seg.text.split(/(\s+)/);
|
|
96
|
-
for (const part of parts) {
|
|
97
|
-
if (part === "") continue;
|
|
98
|
-
if (/^\s+$/.test(part)) {
|
|
99
|
-
if (current.length > 0) {
|
|
100
|
-
words.push(current);
|
|
101
|
-
current = [];
|
|
102
|
-
}
|
|
103
|
-
} else {
|
|
104
|
-
current.push({
|
|
105
|
-
text: part,
|
|
106
|
-
bold: seg.bold,
|
|
107
|
-
...seg.href !== void 0 && { href: seg.href }
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (current.length > 0) words.push(current);
|
|
113
|
-
return words;
|
|
114
|
-
}
|
|
115
|
-
function wordWidth(word, fontSize) {
|
|
116
|
-
let w = 0;
|
|
117
|
-
for (const piece of word) w += segWidth(piece, fontSize);
|
|
118
|
-
return w;
|
|
119
|
-
}
|
|
120
|
-
function coalesce(pieces) {
|
|
121
|
-
const out = [];
|
|
122
|
-
for (const p of pieces) {
|
|
123
|
-
const last = out[out.length - 1];
|
|
124
|
-
if (last?.bold === p.bold && last.href === p.href) {
|
|
125
|
-
last.text += p.text;
|
|
126
|
-
} else {
|
|
127
|
-
out.push({
|
|
128
|
-
text: p.text,
|
|
129
|
-
bold: p.bold,
|
|
130
|
-
...p.href !== void 0 && { href: p.href }
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return out;
|
|
135
|
-
}
|
|
136
|
-
function wrapSegments(segments, fontSize, avail, textX, bulletX) {
|
|
137
|
-
const words = segmentsToWords(segments);
|
|
138
|
-
if (words.length === 0) {
|
|
139
|
-
return [
|
|
140
|
-
{ segments: [], x: textX, ...bulletX !== void 0 && { bulletX } }
|
|
141
|
-
];
|
|
142
|
-
}
|
|
143
|
-
const spaceW = measureText(" ", fontSize);
|
|
144
|
-
const lines = [];
|
|
145
|
-
let currentPieces = [];
|
|
146
|
-
let currentW = 0;
|
|
147
|
-
let first = true;
|
|
148
|
-
const flush = () => {
|
|
149
|
-
lines.push({
|
|
150
|
-
segments: coalesce(currentPieces),
|
|
151
|
-
x: textX,
|
|
152
|
-
// only the FIRST visual line of a bullet carries the marker
|
|
153
|
-
...first && bulletX !== void 0 && { bulletX }
|
|
154
|
-
});
|
|
155
|
-
first = false;
|
|
156
|
-
currentPieces = [];
|
|
157
|
-
currentW = 0;
|
|
158
|
-
};
|
|
159
|
-
for (const word of words) {
|
|
160
|
-
const ww = wordWidth(word, fontSize);
|
|
161
|
-
const addW = currentPieces.length === 0 ? ww : spaceW + ww;
|
|
162
|
-
if (currentPieces.length > 0 && currentW + addW > avail) {
|
|
163
|
-
flush();
|
|
164
|
-
currentPieces.push(...word);
|
|
165
|
-
currentW = ww;
|
|
166
|
-
} else {
|
|
167
|
-
if (currentPieces.length > 0) {
|
|
168
|
-
currentPieces.push({ text: " ", bold: false });
|
|
169
|
-
currentW += spaceW;
|
|
170
|
-
}
|
|
171
|
-
currentPieces.push(...word);
|
|
172
|
-
currentW += ww;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
flush();
|
|
176
|
-
return lines;
|
|
177
|
-
}
|
|
178
|
-
function parseLogicalLine(raw) {
|
|
179
|
-
let i = 0;
|
|
180
|
-
let level = 0;
|
|
181
|
-
let spaceRun = 0;
|
|
182
|
-
while (i < raw.length) {
|
|
183
|
-
const ch = raw[i];
|
|
184
|
-
if (ch === " ") {
|
|
185
|
-
level += 1;
|
|
186
|
-
i += 1;
|
|
187
|
-
spaceRun = 0;
|
|
188
|
-
} else if (ch === " ") {
|
|
189
|
-
spaceRun += 1;
|
|
190
|
-
i += 1;
|
|
191
|
-
if (spaceRun === 2) {
|
|
192
|
-
level += 1;
|
|
193
|
-
spaceRun = 0;
|
|
194
|
-
}
|
|
195
|
-
} else {
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
let content = raw.slice(i);
|
|
200
|
-
let bullet = false;
|
|
201
|
-
if (content.startsWith("- ") || content.startsWith("* ")) {
|
|
202
|
-
bullet = true;
|
|
203
|
-
content = content.slice(2);
|
|
204
|
-
}
|
|
205
|
-
return { level, bullet, content };
|
|
206
|
-
}
|
|
207
|
-
function drawMarkdownBlock(container, text, opts) {
|
|
208
|
-
const { width, fontSize, lineHeight, color, linkColor, maxLines } = opts;
|
|
209
|
-
const visualLines = [];
|
|
210
|
-
for (const raw of text.split("\n")) {
|
|
211
|
-
const { level, bullet, content } = parseLogicalLine(raw);
|
|
212
|
-
const indentX = level * INDENT_PX;
|
|
213
|
-
const textX = bullet ? indentX + BULLET_HANG_PX : indentX;
|
|
214
|
-
const avail = Math.max(1, width - textX);
|
|
215
|
-
const segments = parseInline(content);
|
|
216
|
-
const wrapped = wrapSegments(
|
|
217
|
-
segments,
|
|
218
|
-
fontSize,
|
|
219
|
-
avail,
|
|
220
|
-
textX,
|
|
221
|
-
bullet ? indentX : void 0
|
|
222
|
-
);
|
|
223
|
-
visualLines.push(...wrapped);
|
|
224
|
-
}
|
|
225
|
-
let shown = visualLines;
|
|
226
|
-
let clamped = false;
|
|
227
|
-
if (maxLines !== void 0 && visualLines.length > maxLines) {
|
|
228
|
-
shown = visualLines.slice(0, maxLines);
|
|
229
|
-
clamped = true;
|
|
230
|
-
}
|
|
231
|
-
for (let li = 0; li < shown.length; li++) {
|
|
232
|
-
const line = shown[li];
|
|
233
|
-
const y = fontSize + li * lineHeight;
|
|
234
|
-
const isLast = li === shown.length - 1;
|
|
235
|
-
if (line.bulletX !== void 0) {
|
|
236
|
-
container.append("text").attr("x", line.bulletX).attr("y", y).attr("fill", color).attr("font-size", fontSize).text(BULLET);
|
|
237
|
-
}
|
|
238
|
-
const textEl = container.append("text").attr("x", line.x).attr("y", y).attr("font-size", fontSize);
|
|
239
|
-
let cursorX = line.x;
|
|
240
|
-
const segs = line.segments.slice();
|
|
241
|
-
if (clamped && isLast && !opts.noEllipsis) {
|
|
242
|
-
segs.push({ text: ELLIPSIS, bold: false });
|
|
243
|
-
}
|
|
244
|
-
for (const seg of segs) {
|
|
245
|
-
const tspan = textEl.append("tspan").attr("x", cursorX).attr("fill", seg.href !== void 0 ? linkColor : color).text(seg.text);
|
|
246
|
-
if (seg.bold) tspan.attr("font-weight", "bold");
|
|
247
|
-
const w = segWidth(seg, fontSize);
|
|
248
|
-
if (seg.href !== void 0) {
|
|
249
|
-
tspan.attr("data-href", seg.href);
|
|
250
|
-
container.append("line").attr("x1", cursorX).attr("y1", y + 1.5).attr("x2", cursorX + w).attr("y2", y + 1.5).attr("stroke", linkColor).attr("stroke-width", 1);
|
|
251
|
-
}
|
|
252
|
-
cursorX += w;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
return {
|
|
256
|
-
height: shown.length * lineHeight,
|
|
257
|
-
shown: shown.length,
|
|
258
|
-
total: visualLines.length
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// src/sketch/colors.ts
|
|
263
|
-
var CONTAINER_FILL_OPACITY = 1;
|
|
264
|
-
var CONTAINER_STROKE_OPACITY = 0.35;
|
|
265
|
-
var CONTAINER_STROKE_WIDTH = 1;
|
|
266
|
-
function sketchContainerFill(palette, isDark) {
|
|
267
|
-
return groupFill(palette, isDark);
|
|
268
|
-
}
|
|
269
|
-
function sketchNeutralFill(palette, isDark, fillMode) {
|
|
270
|
-
return fillMode === "outline" ? themeBaseBg(palette, isDark) : mix(palette.textMuted, palette.bg, 12);
|
|
271
|
-
}
|
|
272
|
-
function sketchColors(opts) {
|
|
273
|
-
const { palette, isDark, fillMode } = opts;
|
|
274
|
-
const neutralFill = sketchNeutralFill(palette, isDark, fillMode);
|
|
275
|
-
const tagGroups = [...opts.tagGroups];
|
|
276
|
-
const activeName = resolveActiveTagGroup(
|
|
277
|
-
tagGroups,
|
|
278
|
-
void 0,
|
|
279
|
-
opts.activeTagGroup ?? void 0
|
|
280
|
-
);
|
|
281
|
-
const activeKey = activeName === null ? null : tagAttrKey(activeName);
|
|
282
|
-
return (metadata, isContainer = false) => {
|
|
283
|
-
if (isContainer) {
|
|
284
|
-
const groupColor = resolveGroupTagColor(metadata, tagGroups, activeName);
|
|
285
|
-
return {
|
|
286
|
-
fill: groupFill(palette, isDark, groupColor),
|
|
287
|
-
stroke: groupStroke(palette, groupColor),
|
|
288
|
-
text: palette.text
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
const tagged = activeKey !== null && metadata[activeKey] !== void 0;
|
|
292
|
-
const tagColor = tagged ? resolveTagColor(metadata, tagGroups, activeName, isContainer) : void 0;
|
|
293
|
-
if (!tagColor) {
|
|
294
|
-
return {
|
|
295
|
-
fill: neutralFill,
|
|
296
|
-
// 🔴 The same expression `boxes-and-lines` gives an untagged node
|
|
297
|
-
// (`renderer.ts`): a softened text colour, not `palette.textMuted`.
|
|
298
|
-
// Muted is a good deal darker, and at the shared stroke width it made
|
|
299
|
-
// an untagged sketch card the most heavily outlined object in the
|
|
300
|
-
// product (2026-08-27).
|
|
301
|
-
stroke: mix(palette.text, palette.bg, isDark ? 60 : 40),
|
|
302
|
-
text: palette.text
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
const fill = shapeFill(palette, tagColor, isDark, { mode: fillMode });
|
|
306
|
-
return {
|
|
307
|
-
fill,
|
|
308
|
-
stroke: tagColor,
|
|
309
|
-
// 🔴 CONTRAST-DERIVED, like every other chart type. This took the tag
|
|
310
|
-
// colour itself in the default and `outline` modes, which made sketch the
|
|
311
|
-
// only chart in the product whose labels are tinted — and on the 25% blend
|
|
312
|
-
// a card is filled with, that is pale-green text on a pale-green card.
|
|
313
|
-
// `boxes-and-lines` and `org` both derive it from the fill and land on one
|
|
314
|
-
// of two neutral tokens; sketch now does the same, in every mode rather
|
|
315
|
-
// than only `solid`.
|
|
316
|
-
text: contrastText(fill, palette.textOnFillLight, palette.textOnFillDark)
|
|
317
|
-
};
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// src/sketch/visuals.ts
|
|
322
|
-
var SKETCH_VISUALS = {
|
|
323
|
-
/** Air round the whole diagram. */
|
|
324
|
-
diagramPadding: 20,
|
|
325
|
-
// 🔴 SHARED, not a second answer. Sketch printed its title at 18/y-32 while
|
|
326
|
-
// every other chart type used 20/y-30 from `utils/title-constants` — so two
|
|
327
|
-
// diagrams of the same content, side by side, did not even agree on how big
|
|
328
|
-
// their own name was (2026-08-27, #518).
|
|
329
|
-
titleY: TITLE_Y,
|
|
330
|
-
titleFontSize: TITLE_FONT_SIZE,
|
|
331
|
-
// 🔴 `bold` AS A NUMBER, and never `Number('bold')` — which is `NaN`, which is
|
|
332
|
-
// what this shipped. Every other consumer of `TITLE_FONT_WEIGHT` writes it
|
|
333
|
-
// straight into an SVG attribute, where the string is valid; this one is read
|
|
334
|
-
// as a VALUE by the desktop canvas, which puts it on an SVG attribute AND on
|
|
335
|
-
// an input's inline style, so it has to be a weight rather than a word.
|
|
336
|
-
//
|
|
337
|
-
// The result was `font-weight="NaN"` on every sketch title — silently
|
|
338
|
-
// rendered at the default weight, so a sketch's name was the one title in the
|
|
339
|
-
// product that was not bold, and nothing failed. Found 2026-08-29 by an app
|
|
340
|
-
// test asserting the drawn weight, which had been red on it.
|
|
341
|
-
//
|
|
342
|
-
// The comparison rather than a bare `700` so that changing the shared
|
|
343
|
-
// constant is a type error here instead of another silent coercion.
|
|
344
|
-
//
|
|
345
|
-
// ⚠️ Widened back to `number`. Without the assertion the literal `700` leaks
|
|
346
|
-
// into the published type surface — `check:api` caught it on the push — and a
|
|
347
|
-
// consumer would then be typed against this exact weight rather than against
|
|
348
|
-
// "a weight", which is the opposite of what a shared constant is for.
|
|
349
|
-
titleFontWeight: TITLE_FONT_WEIGHT === "bold" ? 700 : 400,
|
|
350
|
-
// 🔴 SHARED, not overridden. These read 2 and 2 against the conventions'
|
|
351
|
-
// 1.5, under a comment claiming "a bolder, less-washed look" — and the
|
|
352
|
-
// boldness was the complaint: a sketch beside a boxes-and-lines or an org
|
|
353
|
-
// chart of the same content did not look like the same product (2026-08-27).
|
|
354
|
-
// Sketch is not in the deviation list `visual-conventions.ts` keeps, either,
|
|
355
|
-
// so the override was drift rather than a decision anyone had recorded.
|
|
356
|
-
nodeStrokeWidth: NODE_STROKE_WIDTH,
|
|
357
|
-
edgeStrokeWidth: EDGE_STROKE_WIDTH,
|
|
358
|
-
// 🔴 A FILLED polygon, not an open V — and sized in STROKE-WIDTHS, like every
|
|
359
|
-
// other chart type. These read 18×12 in USER SPACE, which is 2.4× the ink
|
|
360
|
-
// boxes-and-lines puts on the same 1.5px connector, and it was the loudest
|
|
361
|
-
// single difference between the two pictures (#518). `markerUnits` defaults
|
|
362
|
-
// to `strokeWidth`, so 5×4 here renders 7.5×6 — the same triangle
|
|
363
|
-
// boxes-and-lines draws.
|
|
364
|
-
arrowheadW: ARROWHEAD_WIDTH,
|
|
365
|
-
arrowheadH: ARROWHEAD_HEIGHT,
|
|
366
|
-
dash: EDGE_DASH,
|
|
367
|
-
/** A card's name. Shared, and it fits DOWN to `nodeLabelFontSizeMin` before
|
|
368
|
-
* it wraps — it used to be allowed up to 30 on a card with no rows, which is
|
|
369
|
-
* more than twice what any other chart type prints. */
|
|
370
|
-
nodeLabelFontSize: LABEL_FONT_SIZE,
|
|
371
|
-
nodeLabelFontSizeMin: 11,
|
|
372
|
-
/** A card's description rows. */
|
|
373
|
-
cardMetaFontSize: META_FONT_SIZE,
|
|
374
|
-
/** A card's header band. */
|
|
375
|
-
cardHeaderHeight: HEADER_HEIGHT,
|
|
376
|
-
// 🔴 A container's own name: shared size, and `600` is what boxes-and-lines
|
|
377
|
-
// gives the same object (org gives it bold). It was 19 at weight 800 behind
|
|
378
|
-
// 0.55 opacity — bigger than a node's name, and the only container label in
|
|
379
|
-
// the product that was faded.
|
|
380
|
-
// 🔴 SHARED, and it stays shared even though boxes-and-lines prints 14 here.
|
|
381
|
-
// Going to 14 to match it was tried and reverted (#518): `sketch` keeps the
|
|
382
|
-
// rule that a container's name is never LOUDER than the names inside it, and
|
|
383
|
-
// one point is imperceptible next to the differences that actually made the
|
|
384
|
-
// two pictures look like different products (footprint, arrowheads, title).
|
|
385
|
-
// boxes-and-lines' 14 — and infra's — are the undocumented deviations here;
|
|
386
|
-
// the fix is those two coming to 13, not a third chart type leaving.
|
|
387
|
-
bandLabelFontSize: CONTAINER_LABEL_FONT_SIZE,
|
|
388
|
-
// 🔴 A WEIGHT, never the word — the same defect `titleFontWeight` carries
|
|
389
|
-
// the note for, one property along. `'bold'` is valid on the SVG attribute
|
|
390
|
-
// this renderer writes it to, and invalid everywhere a consumer needs a
|
|
391
|
-
// number: the app's live canvas puts a container band's weight on an
|
|
392
|
-
// `<input>`'s inline style and into a `fontWeight: number` slot, so it had
|
|
393
|
-
// to translate the word back to 700 itself and typecheck went red on main
|
|
394
|
-
// when it did not (#537, 2026-08-29). 700 is what `'bold'` already drew —
|
|
395
|
-
// this changes the type, not the picture. `as number` for the same reason
|
|
396
|
-
// `titleFontWeight` carries it: the literal would otherwise leak into the
|
|
397
|
-
// published type surface and `check:api` fails on it.
|
|
398
|
-
bandLabelFontWeight: 700,
|
|
399
|
-
bandLabelOpacity: 1,
|
|
400
|
-
/** 11 — what boxes-and-lines prints. It was 12, the only edge label in the
|
|
401
|
-
* product at that size. */
|
|
402
|
-
edgeLabelFontSize: 11,
|
|
403
|
-
/** The `palette.bg` halo painted under an edge label so the connector cannot
|
|
404
|
-
* cross its glyphs. Stroke, with `paint-order: stroke`. */
|
|
405
|
-
edgeLabelHaloWidth: 3,
|
|
406
|
-
cardRadius: CARD_RADIUS,
|
|
407
|
-
containerRadius: CONTAINER_RADIUS
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
// src/sketch/renderer.ts
|
|
411
|
-
var DIAGRAM_PADDING = SKETCH_VISUALS.diagramPadding;
|
|
412
|
-
var TITLE_Y2 = SKETCH_VISUALS.titleY;
|
|
413
|
-
var TITLE_FONT_SIZE2 = SKETCH_VISUALS.titleFontSize;
|
|
414
|
-
var NODE_STROKE_WIDTH2 = SKETCH_VISUALS.nodeStrokeWidth;
|
|
415
|
-
var EDGE_STROKE_WIDTH2 = SKETCH_VISUALS.edgeStrokeWidth;
|
|
416
|
-
var EDGE_HIT_WIDTH = 18;
|
|
417
|
-
var EDGE_LABEL_H_PAD = 6;
|
|
418
|
-
var EDGE_LABEL_V_PAD = 3;
|
|
419
|
-
var CONTAINER_LABEL_BASELINE = 18;
|
|
420
|
-
var ARROWHEAD_W = SKETCH_VISUALS.arrowheadW;
|
|
421
|
-
var ARROWHEAD_H = SKETCH_VISUALS.arrowheadH;
|
|
422
|
-
var DASH = SKETCH_VISUALS.dash;
|
|
423
|
-
var BAND_LABEL_FONT_SIZE = SKETCH_VISUALS.bandLabelFontSize;
|
|
424
|
-
var BAND_LABEL_OPACITY = SKETCH_VISUALS.bandLabelOpacity;
|
|
425
|
-
var BAND_LABEL_WEIGHT = SKETCH_VISUALS.bandLabelFontWeight;
|
|
426
|
-
var COLLAPSE_BAR_HEIGHT2 = COLLAPSE_BAR_HEIGHT;
|
|
427
|
-
var EDGE_LABEL_FONT_SIZE = SKETCH_VISUALS.edgeLabelFontSize;
|
|
428
|
-
var CURVE_HANDLE_MIN = 24;
|
|
429
|
-
var CURVE_HANDLE_MAX = 90;
|
|
430
|
-
var CARD_HEADER_H = SKETCH_VISUALS.cardHeaderHeight;
|
|
431
|
-
var CARD_LABEL_MAX = SKETCH_VISUALS.nodeLabelFontSize;
|
|
432
|
-
var CARD_LABEL_MIN = SKETCH_VISUALS.nodeLabelFontSizeMin;
|
|
433
|
-
var CARD_META_FONT = SKETCH_VISUALS.cardMetaFontSize;
|
|
434
|
-
var CARD_TITLE_MAX = SKETCH_VISUALS.nodeLabelFontSize;
|
|
435
|
-
function renderSketch(container, parsed, layout, palette, isDark, options = {}) {
|
|
436
|
-
const {
|
|
437
|
-
activeTagGroup,
|
|
438
|
-
exportMode = false,
|
|
439
|
-
hideDescriptions = false,
|
|
440
|
-
splitCardIds
|
|
441
|
-
} = options;
|
|
442
|
-
const hideDesc = hideDescriptions || parsed.options.noDescriptions;
|
|
443
|
-
const sctx = ScaleContext.identity();
|
|
444
|
-
const sTitleFontSize = sctx.text(TITLE_FONT_SIZE2);
|
|
445
|
-
const sEdgeLabelFontSize = sctx.text(EDGE_LABEL_FONT_SIZE);
|
|
446
|
-
const sBandLabelFontSize = sctx.text(BAND_LABEL_FONT_SIZE);
|
|
447
|
-
const sCardLabelMax = sctx.text(CARD_LABEL_MAX);
|
|
448
|
-
const sCardLabelMin = sctx.text(CARD_LABEL_MIN);
|
|
449
|
-
const sCardMetaFont = sctx.text(CARD_META_FONT);
|
|
450
|
-
const sCardTitleMax = sctx.text(CARD_TITLE_MAX);
|
|
451
|
-
const sNodeStrokeWidth = sctx.structural(NODE_STROKE_WIDTH2);
|
|
452
|
-
const sEdgeStrokeWidth = sctx.structural(EDGE_STROKE_WIDTH2);
|
|
453
|
-
const sCardRadius = sctx.structural(CARD_RADIUS);
|
|
454
|
-
const sContainerRadius = sctx.structural(CONTAINER_RADIUS);
|
|
455
|
-
const sCardHeaderH = sctx.structural(CARD_HEADER_H);
|
|
456
|
-
const sCollapseBarHeight = sctx.structural(COLLAPSE_BAR_HEIGHT2);
|
|
457
|
-
const scaled = {
|
|
458
|
-
nodeStrokeWidth: sNodeStrokeWidth,
|
|
459
|
-
cardRadius: sCardRadius,
|
|
460
|
-
containerRadius: sContainerRadius,
|
|
461
|
-
cardHeaderH: sCardHeaderH,
|
|
462
|
-
collapseBarHeight: sCollapseBarHeight,
|
|
463
|
-
cardLabelMax: sCardLabelMax,
|
|
464
|
-
cardLabelMin: sCardLabelMin,
|
|
465
|
-
cardMetaFont: sCardMetaFont,
|
|
466
|
-
cardTitleMax: sCardTitleMax,
|
|
467
|
-
bandLabelFontSize: sBandLabelFontSize
|
|
468
|
-
};
|
|
469
|
-
const tagGroups = [...parsed.tagGroups];
|
|
470
|
-
const activeName = resolveActiveTagGroup(
|
|
471
|
-
tagGroups,
|
|
472
|
-
void 0,
|
|
473
|
-
activeTagGroup
|
|
474
|
-
);
|
|
475
|
-
const activeKey = activeName === null ? null : tagAttrKey(activeName);
|
|
476
|
-
const colorsFor = sketchColors({
|
|
477
|
-
palette,
|
|
478
|
-
isDark,
|
|
479
|
-
tagGroups,
|
|
480
|
-
activeTagGroup,
|
|
481
|
-
fillMode: parsed.options.fillMode
|
|
482
|
-
});
|
|
483
|
-
const showTitle = !!parsed.title && !parsed.options.noTitle;
|
|
484
|
-
const titleOffset = showTitle ? 40 : 0;
|
|
485
|
-
const legendGroups = parsed.options.noLegend ? [] : tagGroups;
|
|
486
|
-
const edgeGeom = sketchEdgeGeometry(layout);
|
|
487
|
-
let minX = 0;
|
|
488
|
-
let minY = 0;
|
|
489
|
-
let maxX = layout.width;
|
|
490
|
-
let maxY = layout.height;
|
|
491
|
-
for (let i = 0; i < edgeGeom.length; i++) {
|
|
492
|
-
const g = edgeGeom[i];
|
|
493
|
-
if (!g) continue;
|
|
494
|
-
const nums = (g.d.match(/-?[\d.]+/g) ?? []).map(Number);
|
|
495
|
-
for (let k = 0; k + 1 < nums.length; k += 2) {
|
|
496
|
-
minX = Math.min(minX, nums[k]);
|
|
497
|
-
maxX = Math.max(maxX, nums[k]);
|
|
498
|
-
minY = Math.min(minY, nums[k + 1]);
|
|
499
|
-
maxY = Math.max(maxY, nums[k + 1]);
|
|
500
|
-
}
|
|
501
|
-
const label = layout.edges[i]?.label;
|
|
502
|
-
if (label) {
|
|
503
|
-
const hw = measureText(label, sEdgeLabelFontSize) / 2 + 6;
|
|
504
|
-
const hh = sEdgeLabelFontSize / 2 + 4;
|
|
505
|
-
minX = Math.min(minX, g.mid.x - hw);
|
|
506
|
-
maxX = Math.max(maxX, g.mid.x + hw);
|
|
507
|
-
minY = Math.min(minY, g.mid.y - hh);
|
|
508
|
-
maxY = Math.max(maxY, g.mid.y + hh);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
const extraLeft = Math.max(0, -minX);
|
|
512
|
-
const extraTop = Math.max(0, -minY);
|
|
513
|
-
const boundW = maxX + extraLeft;
|
|
514
|
-
const contentWidth = boundW + 2 * DIAGRAM_PADDING;
|
|
515
|
-
const width = Math.max(contentWidth, options.exportDims?.width ?? 0);
|
|
516
|
-
const legendHeight = legendGroups.length > 0 ? getMaxLegendReservedHeight(
|
|
517
|
-
{
|
|
518
|
-
groups: legendGroups,
|
|
519
|
-
position: { placement: "top-center", titleRelation: "below-title" },
|
|
520
|
-
mode: exportMode ? "export" : "preview"
|
|
521
|
-
},
|
|
522
|
-
width
|
|
523
|
-
) : 0;
|
|
524
|
-
const hasLegend = legendGroups.length > 0;
|
|
525
|
-
const inlineRequested = parsed.options.legendInline === true;
|
|
526
|
-
const legendExtent = inlineRequested && hasLegend ? getLegendExtent(
|
|
527
|
-
{
|
|
528
|
-
groups: legendGroups,
|
|
529
|
-
position: {
|
|
530
|
-
placement: "top-center",
|
|
531
|
-
titleRelation: "inline-with-title"
|
|
532
|
-
},
|
|
533
|
-
mode: exportMode ? "export" : "preview",
|
|
534
|
-
showInactivePills: true
|
|
535
|
-
},
|
|
536
|
-
{ activeGroup: activeName },
|
|
537
|
-
width
|
|
538
|
-
) : { width: 0, height: 0 };
|
|
539
|
-
const header = layoutInlineHeader({
|
|
540
|
-
requested: inlineRequested,
|
|
541
|
-
title: parsed.title ?? "",
|
|
542
|
-
hasLegend,
|
|
543
|
-
legendWidth: legendExtent.width,
|
|
544
|
-
legendHeight: legendExtent.height,
|
|
545
|
-
containerWidth: width,
|
|
546
|
-
titleBandHeight: titleOffset,
|
|
547
|
-
legendReserve: legendHeight,
|
|
548
|
-
titleBaselineY: TITLE_Y2,
|
|
549
|
-
titleFontSize: sTitleFontSize
|
|
550
|
-
});
|
|
551
|
-
const contentTop = titleOffset + (header.inline ? 0 : legendHeight) + DIAGRAM_PADDING + extraTop;
|
|
552
|
-
const height = Math.max(
|
|
553
|
-
contentTop + maxY + DIAGRAM_PADDING,
|
|
554
|
-
options.exportDims?.height ?? 0
|
|
555
|
-
);
|
|
556
|
-
d3.select(container).selectAll("*").remove();
|
|
557
|
-
const svg = d3.select(container).append("svg").attr("width", width).attr("height", height).attr("viewBox", `0 0 ${width} ${height}`).style("font-family", FONT_FAMILY);
|
|
558
|
-
const defs = svg.append("defs");
|
|
559
|
-
const edgeColorFor = (metadata) => {
|
|
560
|
-
if (activeKey !== null && metadata[activeKey] !== void 0) {
|
|
561
|
-
const c = resolveTagColor(metadata, tagGroups, activeName);
|
|
562
|
-
if (c && c !== UNTAGGED_TAG_COLOR) return c;
|
|
563
|
-
}
|
|
564
|
-
return palette.textMuted;
|
|
565
|
-
};
|
|
566
|
-
const flowColor = (edge) => edgeColorFor(edge.metadata);
|
|
567
|
-
const edgeColors = new Set(layout.edges.map((e) => flowColor(e)));
|
|
568
|
-
if (edgeColors.size > 0) {
|
|
569
|
-
const markerOpts = {
|
|
570
|
-
idPrefix: "sk",
|
|
571
|
-
width: ARROWHEAD_W,
|
|
572
|
-
height: ARROWHEAD_H,
|
|
573
|
-
// Every marker is per-colour here; the base id is never referenced.
|
|
574
|
-
baseFill: [...edgeColors][0],
|
|
575
|
-
colors: edgeColors
|
|
576
|
-
};
|
|
577
|
-
appendArrowheadMarkers(defs, markerOpts);
|
|
578
|
-
appendReverseArrowheadMarkers(defs, markerOpts);
|
|
579
|
-
}
|
|
580
|
-
if (showTitle) {
|
|
581
|
-
svg.append("text").attr("class", "chart-title").attr("x", header.titleX).attr("y", TITLE_Y2).attr("text-anchor", header.titleAnchor).attr("font-size", sTitleFontSize).attr("font-weight", "bold").attr("fill", palette.text).text(parsed.title);
|
|
582
|
-
}
|
|
583
|
-
if (legendGroups.length > 0) {
|
|
584
|
-
const legendG = svg.append("g").attr(
|
|
585
|
-
"transform",
|
|
586
|
-
header.inline ? `translate(${header.legendX}, ${header.legendY})` : `translate(0,${titleOffset + 4})`
|
|
587
|
-
);
|
|
588
|
-
renderIntegratedLegend(legendG, {
|
|
589
|
-
groups: legendGroups,
|
|
590
|
-
...activeName !== null && { activeGroup: activeName },
|
|
591
|
-
mode: exportMode ? "export" : "preview",
|
|
592
|
-
showInactivePills: true,
|
|
593
|
-
position: {
|
|
594
|
-
placement: "top-center",
|
|
595
|
-
titleRelation: header.inline ? "inline-with-title" : "below-title"
|
|
596
|
-
},
|
|
597
|
-
palette,
|
|
598
|
-
isDark,
|
|
599
|
-
width
|
|
600
|
-
});
|
|
601
|
-
legendG.selectAll("[data-legend-group]").classed("sk-legend-group", true);
|
|
602
|
-
}
|
|
603
|
-
const contentX = DIAGRAM_PADDING + extraLeft + Math.max(0, (width - contentWidth) / 2);
|
|
604
|
-
const root = svg.append("g").attr("class", "sk-root").attr("transform", `translate(${contentX},${contentTop})`);
|
|
605
|
-
const boxLayer = root.append("g").attr("class", "sk-boxes");
|
|
606
|
-
for (const box of layout.boxes) {
|
|
607
|
-
drawBoxFrame(boxLayer, box, colorsFor(box.metadata, true), palette, scaled);
|
|
608
|
-
}
|
|
609
|
-
const edgeLayer = root.append("g").attr("class", "sk-edges");
|
|
610
|
-
const labelLayerData = [];
|
|
611
|
-
for (let ei = 0; ei < layout.edges.length; ei++) {
|
|
612
|
-
const edge = layout.edges[ei];
|
|
613
|
-
const geo = edgeGeom[ei];
|
|
614
|
-
if (!geo) continue;
|
|
615
|
-
const color = flowColor(edge);
|
|
616
|
-
const hex = color.replace("#", "");
|
|
617
|
-
const { d, mid } = geo;
|
|
618
|
-
const g = edgeLayer.append("g").attr("class", "sk-edge-group").attr("data-from", edge.sourceId).attr("data-to", edge.targetId).attr("data-line-number", edge.lineNumber);
|
|
619
|
-
const path = g.append("path").attr("d", geo.dRender ?? d).attr("fill", "none").attr("stroke", color).attr("stroke-width", sEdgeStrokeWidth);
|
|
620
|
-
if (edge.dashed) path.attr("stroke-dasharray", DASH);
|
|
621
|
-
if (edge.heads === "one" || edge.heads === "both") {
|
|
622
|
-
path.attr("marker-end", `url(#sk-arrow-${hex})`);
|
|
623
|
-
}
|
|
624
|
-
if (edge.heads === "both") {
|
|
625
|
-
path.attr("marker-start", `url(#sk-arrow-rev-${hex})`);
|
|
626
|
-
}
|
|
627
|
-
g.append("path").attr("class", "sk-edge-hit").attr("d", d).attr("fill", "none").attr("stroke", "transparent").attr("stroke-width", EDGE_HIT_WIDTH).attr("stroke-linecap", "round").attr("pointer-events", "stroke");
|
|
628
|
-
if (edge.label) {
|
|
629
|
-
labelLayerData.push({
|
|
630
|
-
x: mid.x,
|
|
631
|
-
y: mid.y,
|
|
632
|
-
text: edge.label,
|
|
633
|
-
color,
|
|
634
|
-
from: edge.sourceId,
|
|
635
|
-
to: edge.targetId,
|
|
636
|
-
ei,
|
|
637
|
-
cp: (d.match(/-?[\d.]+/g) ?? []).map(Number)
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
{
|
|
642
|
-
const labelW = (t) => t.length * sEdgeLabelFontSize * 0.56;
|
|
643
|
-
const halfH = sEdgeLabelFontSize / 2 + 3;
|
|
644
|
-
const GAP = 4;
|
|
645
|
-
const shapeRects = [
|
|
646
|
-
...layout.nodes.map((n) => ({ x: n.x, y: n.y, w: n.w, h: n.h })),
|
|
647
|
-
...layout.boxes.map((b) => ({
|
|
648
|
-
// The band the name is drawn in, not the frame below it.
|
|
649
|
-
x: b.x,
|
|
650
|
-
y: b.y,
|
|
651
|
-
w: b.w,
|
|
652
|
-
h: b.bandH
|
|
653
|
-
}))
|
|
654
|
-
];
|
|
655
|
-
const cubicPt = (cp, t) => {
|
|
656
|
-
const u = 1 - t;
|
|
657
|
-
const a = u * u * u;
|
|
658
|
-
const b = 3 * u * u * t;
|
|
659
|
-
const c = 3 * u * t * t;
|
|
660
|
-
const dd = t * t * t;
|
|
661
|
-
return {
|
|
662
|
-
x: a * cp[0] + b * cp[2] + c * cp[4] + dd * cp[6],
|
|
663
|
-
y: a * cp[1] + b * cp[3] + c * cp[5] + dd * cp[7]
|
|
664
|
-
};
|
|
665
|
-
};
|
|
666
|
-
const allPolys = edgeGeom.map((g) => {
|
|
667
|
-
if (!g) return null;
|
|
668
|
-
const cp = (g.d.match(/-?[\d.]+/g) ?? []).map(Number);
|
|
669
|
-
if (cp.length < 8) return null;
|
|
670
|
-
const pts = [];
|
|
671
|
-
for (let s = 0; s <= 32; s++) pts.push(cubicPt(cp, s / 32));
|
|
672
|
-
return pts;
|
|
673
|
-
});
|
|
674
|
-
const covers = (cx, cy, hw, ox, oy, ohw, ohh) => Math.abs(cx - ox) < hw + ohw + GAP && Math.abs(cy - oy) < halfH + ohh + GAP;
|
|
675
|
-
const normalAt = (cp, t) => {
|
|
676
|
-
const e = 0.01;
|
|
677
|
-
const a = cubicPt(cp, Math.max(0, t - e));
|
|
678
|
-
const b = cubicPt(cp, Math.min(1, t + e));
|
|
679
|
-
const dx = b.x - a.x;
|
|
680
|
-
const dy = b.y - a.y;
|
|
681
|
-
const len = Math.hypot(dx, dy);
|
|
682
|
-
return len === 0 ? { x: 0, y: -1 } : { x: -dy / len, y: dx / len };
|
|
683
|
-
};
|
|
684
|
-
const placed = [];
|
|
685
|
-
const TS = [0.5, 0.4, 0.6, 0.32, 0.68, 0.25, 0.75, 0.18, 0.82];
|
|
686
|
-
const PERP_STEP = 8;
|
|
687
|
-
const PERP_MAX = 40;
|
|
688
|
-
const PERP_OFFSETS = [0];
|
|
689
|
-
for (let d = PERP_STEP; d <= PERP_MAX; d += PERP_STEP)
|
|
690
|
-
PERP_OFFSETS.push(d, -d);
|
|
691
|
-
for (const l of labelLayerData) {
|
|
692
|
-
const hw = labelW(l.text) / 2 + 3;
|
|
693
|
-
let best = cubicPt(l.cp, 0.5);
|
|
694
|
-
let bestScore = Infinity;
|
|
695
|
-
search: for (const perp of PERP_OFFSETS) {
|
|
696
|
-
const n = perp === 0 ? { x: 0, y: 0 } : normalAt(l.cp, 0.5);
|
|
697
|
-
for (const t of TS) {
|
|
698
|
-
const base = cubicPt(l.cp, t);
|
|
699
|
-
const p = { x: base.x + n.x * perp, y: base.y + n.y * perp };
|
|
700
|
-
let labelHits = 0;
|
|
701
|
-
for (const q of placed)
|
|
702
|
-
if (covers(p.x, p.y, hw, q.x, q.y, q.hw, halfH)) labelHits++;
|
|
703
|
-
let shapeHits = 0;
|
|
704
|
-
for (const r of shapeRects)
|
|
705
|
-
if (covers(
|
|
706
|
-
p.x,
|
|
707
|
-
p.y,
|
|
708
|
-
hw,
|
|
709
|
-
r.x + r.w / 2,
|
|
710
|
-
r.y + r.h / 2,
|
|
711
|
-
r.w / 2,
|
|
712
|
-
r.h / 2
|
|
713
|
-
))
|
|
714
|
-
shapeHits++;
|
|
715
|
-
let lineHits = 0;
|
|
716
|
-
for (let k = 0; k < allPolys.length; k++) {
|
|
717
|
-
if (k === l.ei) continue;
|
|
718
|
-
const poly = allPolys[k];
|
|
719
|
-
if (poly?.some((pt) => covers(p.x, p.y, hw, pt.x, pt.y, 0, 0)))
|
|
720
|
-
lineHits++;
|
|
721
|
-
}
|
|
722
|
-
if (labelHits === 0 && shapeHits === 0 && lineHits === 0) {
|
|
723
|
-
best = p;
|
|
724
|
-
break search;
|
|
725
|
-
}
|
|
726
|
-
const score = labelHits * 10 + shapeHits * 6 + lineHits * 4 + Math.abs(t - 0.5) + Math.abs(perp) / PERP_MAX;
|
|
727
|
-
if (score < bestScore) {
|
|
728
|
-
bestScore = score;
|
|
729
|
-
best = p;
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
l.x = best.x;
|
|
734
|
-
l.y = best.y;
|
|
735
|
-
placed.push({ x: best.x, y: best.y, hw });
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
const nodeLayer = root.append("g").attr("class", "sk-nodes");
|
|
739
|
-
for (const node of layout.nodes) {
|
|
740
|
-
let n = node;
|
|
741
|
-
if (hideDesc && node.description) {
|
|
742
|
-
n = { ...node };
|
|
743
|
-
delete n.description;
|
|
744
|
-
}
|
|
745
|
-
drawNode(
|
|
746
|
-
nodeLayer,
|
|
747
|
-
n,
|
|
748
|
-
colorsFor(node.metadata),
|
|
749
|
-
palette,
|
|
750
|
-
isDark,
|
|
751
|
-
!hideDesc && (splitCardIds?.includes(node.id) ?? false),
|
|
752
|
-
scaled
|
|
753
|
-
);
|
|
754
|
-
}
|
|
755
|
-
const labelLayer = root.append("g").attr("class", "sk-edge-labels");
|
|
756
|
-
for (const l of labelLayerData) {
|
|
757
|
-
const g = labelLayer.append("g").attr("class", "sk-edge-label").attr("data-from", l.from).attr("data-to", l.to);
|
|
758
|
-
const plateW = measureText(l.text, sEdgeLabelFontSize) + EDGE_LABEL_H_PAD * 2;
|
|
759
|
-
const plateH = sEdgeLabelFontSize * 1.3 + EDGE_LABEL_V_PAD * 2;
|
|
760
|
-
g.append("rect").attr("x", l.x - plateW / 2).attr("y", l.y - plateH / 2).attr("width", plateW).attr("height", plateH).attr("rx", 3).attr("fill", palette.bg).attr("opacity", EDGE_LABEL_KNOCKOUT_OPACITY);
|
|
761
|
-
g.append("text").attr("x", l.x).attr("y", l.y).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", sEdgeLabelFontSize).attr("fill", l.color).text(l.text);
|
|
762
|
-
}
|
|
763
|
-
if (options.onClickItem) {
|
|
764
|
-
const handler = options.onClickItem;
|
|
765
|
-
svg.selectAll("[data-line-number]").on("click", function() {
|
|
766
|
-
const ln = Number(this.getAttribute("data-line-number"));
|
|
767
|
-
if (Number.isFinite(ln) && ln > 0) handler(ln);
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
function drawBoxFrame(layer, box, colors, palette, s) {
|
|
772
|
-
const g = layer.append("g").attr("class", "sk-box").attr("data-node-id", box.id).attr("data-group-toggle", box.label).attr("data-line-number", box.lineNumber);
|
|
773
|
-
g.append("rect").attr("x", box.x).attr("y", box.y).attr("width", box.w).attr("height", box.h).attr("rx", s.containerRadius).attr("fill", colors.fill).attr("fill-opacity", CONTAINER_FILL_OPACITY).attr("stroke", colors.stroke).attr("stroke-opacity", CONTAINER_STROKE_OPACITY).attr("stroke-width", CONTAINER_STROKE_WIDTH);
|
|
774
|
-
const fit = fitWrapped(box.label, box.w - 16, s.bandLabelFontSize, 12, 2);
|
|
775
|
-
const lineH = fit.fontSize * 1.2;
|
|
776
|
-
const bandMid = box.y + CONTAINER_LABEL_BASELINE;
|
|
777
|
-
const startY = bandMid - (fit.lines.length - 1) * lineH / 2;
|
|
778
|
-
for (let li = 0; li < fit.lines.length; li++) {
|
|
779
|
-
g.append("text").attr("x", box.x + box.w / 2).attr("y", startY + li * lineH).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", fit.fontSize).attr("font-weight", BAND_LABEL_WEIGHT).attr("fill", palette.text).attr("opacity", BAND_LABEL_OPACITY).text(fit.lines[li]);
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
function drawNode(layer, node, colors, palette, isDark, splitCard, s) {
|
|
783
|
-
void isDark;
|
|
784
|
-
const g = layer.append("g").attr("class", node.isCollapsedBox ? "sk-node sk-box-collapsed" : "sk-node").attr("transform", `translate(${node.x},${node.y})`).attr("data-node-id", node.id).attr("data-line-number", node.lineNumber);
|
|
785
|
-
for (const [k, v] of Object.entries(node.metadata)) {
|
|
786
|
-
g.attr(`data-tag-${tagAttrKey(k)}`, v);
|
|
787
|
-
}
|
|
788
|
-
if (node.isCollapsedBox) {
|
|
789
|
-
g.attr("data-group-toggle", node.label);
|
|
790
|
-
}
|
|
791
|
-
const solidLike = colors.stroke === colors.fill;
|
|
792
|
-
const ruleColor = solidLike ? colors.text : colors.stroke;
|
|
793
|
-
if (!node.isCollapsedBox && (node.description || splitCard)) {
|
|
794
|
-
const fitH = fitWrapped(
|
|
795
|
-
node.label,
|
|
796
|
-
node.w - 24,
|
|
797
|
-
s.cardLabelMax,
|
|
798
|
-
s.cardLabelMin,
|
|
799
|
-
1
|
|
800
|
-
);
|
|
801
|
-
renderNodeCard(g, {
|
|
802
|
-
width: node.w,
|
|
803
|
-
height: node.h,
|
|
804
|
-
rx: s.cardRadius,
|
|
805
|
-
fill: colors.fill,
|
|
806
|
-
stroke: colors.stroke,
|
|
807
|
-
strokeWidth: s.nodeStrokeWidth,
|
|
808
|
-
label: fitH.lines[0] ?? node.label,
|
|
809
|
-
labelColor: colors.text,
|
|
810
|
-
labelFontSize: fitH.fontSize,
|
|
811
|
-
headerHeight: s.cardHeaderH
|
|
812
|
-
});
|
|
813
|
-
g.append("line").attr("x1", 0).attr("y1", s.cardHeaderH).attr("x2", node.w).attr("y2", s.cardHeaderH).attr("stroke", ruleColor).attr("stroke-opacity", 0.3).attr("stroke-width", 1);
|
|
814
|
-
if (node.description) {
|
|
815
|
-
const inset = 12;
|
|
816
|
-
const bodyGap = 8;
|
|
817
|
-
const lh = s.cardMetaFont + 4;
|
|
818
|
-
const avail = node.h - s.cardHeaderH - bodyGap - 8;
|
|
819
|
-
const body = g.append("g").attr("class", "sk-desc").attr("transform", `translate(${inset} ${s.cardHeaderH + bodyGap})`);
|
|
820
|
-
const block = drawMarkdownBlock(body, node.description, {
|
|
821
|
-
width: node.w - inset * 2,
|
|
822
|
-
fontSize: s.cardMetaFont,
|
|
823
|
-
lineHeight: lh,
|
|
824
|
-
color: colors.text,
|
|
825
|
-
// match the header label (contrast-aware in solid)
|
|
826
|
-
linkColor: colors.text,
|
|
827
|
-
maxLines: Math.max(1, Math.floor(avail / lh)),
|
|
828
|
-
noEllipsis: true
|
|
829
|
-
});
|
|
830
|
-
if (block.total > block.shown) {
|
|
831
|
-
g.append("text").attr("class", "sk-desc-more").attr("x", node.w - 8).attr("y", node.h - 5).attr("text-anchor", "end").attr("font-size", 9).attr("fill", palette.textMuted).attr("data-line-number", node.lineNumber).style("cursor", "pointer").text(`+${block.total - block.shown} more in source`);
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
return;
|
|
835
|
-
}
|
|
836
|
-
const fit = fitWrapped(
|
|
837
|
-
node.label,
|
|
838
|
-
node.w - 24,
|
|
839
|
-
s.cardTitleMax,
|
|
840
|
-
s.cardLabelMin,
|
|
841
|
-
3
|
|
842
|
-
);
|
|
843
|
-
renderNodeCard(g, {
|
|
844
|
-
width: node.w,
|
|
845
|
-
height: node.h,
|
|
846
|
-
rx: s.cardRadius,
|
|
847
|
-
fill: colors.fill,
|
|
848
|
-
stroke: colors.stroke,
|
|
849
|
-
strokeWidth: s.nodeStrokeWidth,
|
|
850
|
-
label: fit.lines.join(" "),
|
|
851
|
-
labelLines: fit.lines,
|
|
852
|
-
labelColor: colors.text,
|
|
853
|
-
labelFontSize: fit.fontSize,
|
|
854
|
-
headerHeight: node.h
|
|
855
|
-
});
|
|
856
|
-
if (node.isCollapsedBox) {
|
|
857
|
-
renderCollapseBar(g, {
|
|
858
|
-
width: node.w,
|
|
859
|
-
height: node.h,
|
|
860
|
-
barHeight: s.collapseBarHeight,
|
|
861
|
-
inset: 0,
|
|
862
|
-
rx: s.cardRadius,
|
|
863
|
-
fill: colors.stroke === palette.textMuted ? palette.textMuted : colors.stroke,
|
|
864
|
-
clipId: `sk-clip-${node.id.replace(/[^a-zA-Z0-9_-]/g, "")}-${node.lineNumber}`,
|
|
865
|
-
className: "sk-collapse-bar"
|
|
866
|
-
});
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
function assignEdgeSides(edges, rectById) {
|
|
870
|
-
const cx = (r) => r.x + r.w / 2;
|
|
871
|
-
const cy = (r) => r.y + r.h / 2;
|
|
872
|
-
const facing = (dx, dy) => Math.abs(dx) >= Math.abs(dy) ? dx >= 0 ? "R" : "L" : dy >= 0 ? "B" : "T";
|
|
873
|
-
return edges.map((e) => {
|
|
874
|
-
const s = rectById.get(e.sourceId);
|
|
875
|
-
const t = rectById.get(e.targetId);
|
|
876
|
-
if (!s || !t) return null;
|
|
877
|
-
const dx = cx(t) - cx(s);
|
|
878
|
-
const dy = cy(t) - cy(s);
|
|
879
|
-
const xOverlap = Math.min(s.x + s.w, t.x + t.w) - Math.max(s.x, t.x);
|
|
880
|
-
const yOverlap = Math.min(s.y + s.h, t.y + t.h) - Math.max(s.y, t.y);
|
|
881
|
-
if (xOverlap >= 0 && xOverlap >= yOverlap) {
|
|
882
|
-
const below = dy >= 0;
|
|
883
|
-
return { s: below ? "B" : "T", t: below ? "T" : "B" };
|
|
884
|
-
}
|
|
885
|
-
if (yOverlap >= 0 && yOverlap >= xOverlap) {
|
|
886
|
-
const right = dx >= 0;
|
|
887
|
-
return { s: right ? "R" : "L", t: right ? "L" : "R" };
|
|
888
|
-
}
|
|
889
|
-
return { s: facing(dx, dy), t: facing(-dx, -dy) };
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
|
-
function edgePath(source, target, sSide, tSide, sourcePorts, targetPorts, ends) {
|
|
893
|
-
const acx = source.x + source.w / 2;
|
|
894
|
-
const acy = source.y + source.h / 2;
|
|
895
|
-
const bcx = target.x + target.w / 2;
|
|
896
|
-
const bcy = target.y + target.h / 2;
|
|
897
|
-
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
898
|
-
const nearest = (arr, v) => arr.reduce((a, b) => Math.abs(b - v) < Math.abs(a - v) ? b : a, arr[0]);
|
|
899
|
-
const attach = (side, rect, ports, toward) => {
|
|
900
|
-
if (side === "L" || side === "R") {
|
|
901
|
-
const x2 = side === "L" ? rect.x : rect.x + rect.w;
|
|
902
|
-
const y2 = ports ? clamp(nearest(ports.ys, toward.y), rect.y, rect.y + rect.h) : rect.y + rect.h / 2;
|
|
903
|
-
return { x: x2, y: y2 };
|
|
904
|
-
}
|
|
905
|
-
const y = side === "T" ? rect.y : rect.y + rect.h;
|
|
906
|
-
const x = ports ? clamp(nearest(ports.xs, toward.x), rect.x, rect.x + rect.w) : rect.x + rect.w / 2;
|
|
907
|
-
return { x, y };
|
|
908
|
-
};
|
|
909
|
-
const normal = (side) => side === "L" ? { x: -1, y: 0 } : side === "R" ? { x: 1, y: 0 } : side === "T" ? { x: 0, y: -1 } : { x: 0, y: 1 };
|
|
910
|
-
const p0 = ends?.p0 ?? attach(sSide, source, sourcePorts, { x: bcx, y: bcy });
|
|
911
|
-
const p1 = ends?.p1 ?? attach(tSide, target, targetPorts, { x: acx, y: acy });
|
|
912
|
-
const n0 = normal(sSide);
|
|
913
|
-
const n1 = normal(tSide);
|
|
914
|
-
const k = Math.max(
|
|
915
|
-
CURVE_HANDLE_MIN,
|
|
916
|
-
Math.min(CURVE_HANDLE_MAX, Math.hypot(p1.x - p0.x, p1.y - p0.y) / 2)
|
|
917
|
-
);
|
|
918
|
-
const h0 = { x: p0.x + n0.x * k, y: p0.y + n0.y * k };
|
|
919
|
-
const h1 = { x: p1.x + n1.x * k, y: p1.y + n1.y * k };
|
|
920
|
-
return {
|
|
921
|
-
d: `M ${p0.x} ${p0.y} C ${h0.x} ${h0.y}, ${h1.x} ${h1.y}, ${p1.x} ${p1.y}`,
|
|
922
|
-
// Centered ON the line at its ARC midpoint (t=0.5 on the cubic), not the
|
|
923
|
-
// endpoint average: for a straight edge the two coincide, but for a curve
|
|
924
|
-
// that bulges out to route around a shape the arc midpoint sits at the
|
|
925
|
-
// bulge apex — in open space — while the endpoint average would land on the
|
|
926
|
-
// very shape the edge detoured past. The opaque halo masks the line behind.
|
|
927
|
-
mid: {
|
|
928
|
-
x: 0.125 * p0.x + 0.375 * h0.x + 0.375 * h1.x + 0.125 * p1.x,
|
|
929
|
-
y: 0.125 * p0.y + 0.375 * h0.y + 0.375 * h1.y + 0.125 * p1.y
|
|
930
|
-
},
|
|
931
|
-
p0,
|
|
932
|
-
h0,
|
|
933
|
-
h1,
|
|
934
|
-
p1
|
|
935
|
-
};
|
|
936
|
-
}
|
|
937
|
-
var SIDES = ["T", "B", "L", "R"];
|
|
938
|
-
function cubicAt(g, t) {
|
|
939
|
-
const u = 1 - t;
|
|
940
|
-
const a = u * u * u;
|
|
941
|
-
const b = 3 * u * u * t;
|
|
942
|
-
const c = 3 * u * t * t;
|
|
943
|
-
const dd = t * t * t;
|
|
944
|
-
return {
|
|
945
|
-
x: a * g.p0.x + b * g.h0.x + c * g.h1.x + dd * g.p1.x,
|
|
946
|
-
y: a * g.p0.y + b * g.h0.y + c * g.h1.y + dd * g.p1.y
|
|
947
|
-
};
|
|
948
|
-
}
|
|
949
|
-
var EDGE_OBSTACLE_INSET = 6;
|
|
950
|
-
var EDGE_SAMPLES = 24;
|
|
951
|
-
var POLY_SAMPLES = 19;
|
|
952
|
-
function polyline(g) {
|
|
953
|
-
const pts = [];
|
|
954
|
-
for (let i = 0; i <= POLY_SAMPLES; i++)
|
|
955
|
-
pts.push(cubicAt(g, i / POLY_SAMPLES));
|
|
956
|
-
return pts;
|
|
957
|
-
}
|
|
958
|
-
function segmentsCross(p, p2, q, q2) {
|
|
959
|
-
const o = (a, b, c) => (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
|
|
960
|
-
const d1 = o(p, p2, q);
|
|
961
|
-
const d2 = o(p, p2, q2);
|
|
962
|
-
const d32 = o(q, q2, p);
|
|
963
|
-
const d4 = o(q, q2, p2);
|
|
964
|
-
return (d1 > 0 && d2 < 0 || d1 < 0 && d2 > 0) && (d32 > 0 && d4 < 0 || d32 < 0 && d4 > 0);
|
|
965
|
-
}
|
|
966
|
-
function polyCrossings(a, b) {
|
|
967
|
-
let n = 0;
|
|
968
|
-
for (let i = 0; i + 1 < a.length; i++)
|
|
969
|
-
for (let j = 0; j + 1 < b.length; j++)
|
|
970
|
-
if (segmentsCross(a[i], a[i + 1], b[j], b[j + 1])) n++;
|
|
971
|
-
return n;
|
|
972
|
-
}
|
|
973
|
-
function polyIntersections(a, b) {
|
|
974
|
-
const out = [];
|
|
975
|
-
for (let i = 0; i + 1 < a.length; i++) {
|
|
976
|
-
const p = a[i];
|
|
977
|
-
const p2 = a[i + 1];
|
|
978
|
-
for (let j = 0; j + 1 < b.length; j++) {
|
|
979
|
-
const q = b[j];
|
|
980
|
-
const q2 = b[j + 1];
|
|
981
|
-
if (!segmentsCross(p, p2, q, q2)) continue;
|
|
982
|
-
const rx = p2.x - p.x;
|
|
983
|
-
const ry = p2.y - p.y;
|
|
984
|
-
const sx = q2.x - q.x;
|
|
985
|
-
const sy = q2.y - q.y;
|
|
986
|
-
const denom = rx * sy - ry * sx;
|
|
987
|
-
if (denom === 0) continue;
|
|
988
|
-
const t = ((q.x - p.x) * sy - (q.y - p.y) * sx) / denom;
|
|
989
|
-
out.push({ x: p.x + t * rx, y: p.y + t * ry });
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
return out;
|
|
993
|
-
}
|
|
994
|
-
var HOP_R = 7;
|
|
995
|
-
function hoppedPath(g, hops) {
|
|
996
|
-
if (hops.length === 0) return null;
|
|
997
|
-
const N = 49;
|
|
998
|
-
const pts = [];
|
|
999
|
-
for (let s = 0; s <= N; s++) pts.push(cubicAt(g, s / N));
|
|
1000
|
-
const cum = [0];
|
|
1001
|
-
for (let k2 = 1; k2 <= N; k2++) {
|
|
1002
|
-
const a = pts[k2 - 1];
|
|
1003
|
-
const b = pts[k2];
|
|
1004
|
-
cum.push(cum[k2 - 1] + Math.hypot(b.x - a.x, b.y - a.y));
|
|
1005
|
-
}
|
|
1006
|
-
const total = cum[N];
|
|
1007
|
-
const at = (len) => {
|
|
1008
|
-
let k2 = 1;
|
|
1009
|
-
while (k2 < N && cum[k2] < len) k2++;
|
|
1010
|
-
const a = pts[k2 - 1];
|
|
1011
|
-
const b = pts[k2];
|
|
1012
|
-
const span = cum[k2] - cum[k2 - 1] || 1;
|
|
1013
|
-
const f = Math.max(0, Math.min(1, (len - cum[k2 - 1]) / span));
|
|
1014
|
-
return { x: a.x + f * (b.x - a.x), y: a.y + f * (b.y - a.y) };
|
|
1015
|
-
};
|
|
1016
|
-
const centres = [];
|
|
1017
|
-
for (const hp of hops) {
|
|
1018
|
-
let bestD = Infinity;
|
|
1019
|
-
let bestLen = 0;
|
|
1020
|
-
for (let k2 = 1; k2 <= N; k2++) {
|
|
1021
|
-
const a = pts[k2 - 1];
|
|
1022
|
-
const b = pts[k2];
|
|
1023
|
-
const dx = b.x - a.x;
|
|
1024
|
-
const dy = b.y - a.y;
|
|
1025
|
-
const len2 = dx * dx + dy * dy || 1;
|
|
1026
|
-
let f = ((hp.x - a.x) * dx + (hp.y - a.y) * dy) / len2;
|
|
1027
|
-
f = Math.max(0, Math.min(1, f));
|
|
1028
|
-
const d2 = Math.hypot(hp.x - (a.x + f * dx), hp.y - (a.y + f * dy));
|
|
1029
|
-
if (d2 < bestD) {
|
|
1030
|
-
bestD = d2;
|
|
1031
|
-
bestLen = cum[k2 - 1] + f * (cum[k2] - cum[k2 - 1]);
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
centres.push(bestLen);
|
|
1035
|
-
}
|
|
1036
|
-
const clear = Math.max(2 * HOP_R, 3 * total / N);
|
|
1037
|
-
const kept = [];
|
|
1038
|
-
for (const c of centres.sort((x, y) => x - y)) {
|
|
1039
|
-
if (c < clear || c > total - clear) continue;
|
|
1040
|
-
if (kept.length > 0 && c - kept[kept.length - 1] < 2 * HOP_R) continue;
|
|
1041
|
-
kept.push(c);
|
|
1042
|
-
}
|
|
1043
|
-
if (kept.length === 0) return null;
|
|
1044
|
-
let d = `M ${pts[0].x} ${pts[0].y}`;
|
|
1045
|
-
let k = 1;
|
|
1046
|
-
for (const c of kept) {
|
|
1047
|
-
const s0 = c - HOP_R;
|
|
1048
|
-
const s1 = c + HOP_R;
|
|
1049
|
-
while (k <= N && cum[k] < s0) {
|
|
1050
|
-
d += ` L ${pts[k].x} ${pts[k].y}`;
|
|
1051
|
-
k++;
|
|
1052
|
-
}
|
|
1053
|
-
const A = at(s0);
|
|
1054
|
-
const B = at(s1);
|
|
1055
|
-
const chord = Math.hypot(B.x - A.x, B.y - A.y) || 1;
|
|
1056
|
-
const dir = { x: (B.x - A.x) / chord, y: (B.y - A.y) / chord };
|
|
1057
|
-
let perp = { x: dir.y, y: -dir.x };
|
|
1058
|
-
if (perp.y > 0) perp = { x: -perp.x, y: -perp.y };
|
|
1059
|
-
const lift = chord / 2 * 1.33;
|
|
1060
|
-
d += ` L ${A.x} ${A.y} C ${A.x + perp.x * lift} ${A.y + perp.y * lift}, ${B.x + perp.x * lift} ${B.y + perp.y * lift}, ${B.x} ${B.y}`;
|
|
1061
|
-
while (k <= N && cum[k] <= s1) k++;
|
|
1062
|
-
}
|
|
1063
|
-
for (; k <= N; k++) d += ` L ${pts[k].x} ${pts[k].y}`;
|
|
1064
|
-
return d;
|
|
1065
|
-
}
|
|
1066
|
-
function pathCrossings(g, obstacles) {
|
|
1067
|
-
if (obstacles.length === 0) return 0;
|
|
1068
|
-
let hits = 0;
|
|
1069
|
-
for (let i = 1; i < EDGE_SAMPLES; i++) {
|
|
1070
|
-
const p = cubicAt(g, i / EDGE_SAMPLES);
|
|
1071
|
-
for (const r of obstacles) {
|
|
1072
|
-
const x0 = r.x + EDGE_OBSTACLE_INSET;
|
|
1073
|
-
const y0 = r.y + EDGE_OBSTACLE_INSET;
|
|
1074
|
-
const x1 = r.x + r.w - EDGE_OBSTACLE_INSET;
|
|
1075
|
-
const y1 = r.y + r.h - EDGE_OBSTACLE_INSET;
|
|
1076
|
-
if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1) {
|
|
1077
|
-
hits++;
|
|
1078
|
-
break;
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
return hits;
|
|
1083
|
-
}
|
|
1084
|
-
function sketchEdgeGeometry(layout, offsets) {
|
|
1085
|
-
const off = (id) => offsets?.get(id) ?? { dx: 0, dy: 0 };
|
|
1086
|
-
const rectById = /* @__PURE__ */ new Map();
|
|
1087
|
-
for (const n of layout.nodes) {
|
|
1088
|
-
const o = off(n.id);
|
|
1089
|
-
rectById.set(n.id, { x: n.x + o.dx, y: n.y + o.dy, w: n.w, h: n.h });
|
|
1090
|
-
}
|
|
1091
|
-
for (const b of layout.boxes) {
|
|
1092
|
-
const o = off(b.id);
|
|
1093
|
-
rectById.set(b.id, { x: b.x + o.dx, y: b.y + o.dy, w: b.w, h: b.h });
|
|
1094
|
-
}
|
|
1095
|
-
const portsById = /* @__PURE__ */ new Map();
|
|
1096
|
-
for (const box of layout.boxes) {
|
|
1097
|
-
const o = off(box.id);
|
|
1098
|
-
const kids = layout.nodes.filter((n) => n.boxLabel === box.label);
|
|
1099
|
-
const ys = kids.map((k) => k.y + off(k.id).dy + k.h / 2);
|
|
1100
|
-
const xs = kids.map((k) => k.x + off(k.id).dx + k.w / 2);
|
|
1101
|
-
ys.push(box.y + o.dy + box.h / 2);
|
|
1102
|
-
xs.push(box.x + o.dx + box.w / 2);
|
|
1103
|
-
portsById.set(box.id, { ys, xs });
|
|
1104
|
-
}
|
|
1105
|
-
const sides = assignEdgeSides(layout.edges, rectById);
|
|
1106
|
-
const boxIdByLabel = /* @__PURE__ */ new Map();
|
|
1107
|
-
for (const b of layout.boxes) boxIdByLabel.set(b.label, b.id);
|
|
1108
|
-
const boxOf = (id) => {
|
|
1109
|
-
const n = layout.nodes.find((m) => m.id === id);
|
|
1110
|
-
return n?.boxLabel ? boxIdByLabel.get(n.boxLabel) : void 0;
|
|
1111
|
-
};
|
|
1112
|
-
const obstacles = [];
|
|
1113
|
-
for (const [id, rect] of rectById) obstacles.push({ id, rect });
|
|
1114
|
-
const normalOf = (s) => s === "L" ? { x: -1, y: 0 } : s === "R" ? { x: 1, y: 0 } : s === "T" ? { x: 0, y: -1 } : { x: 0, y: 1 };
|
|
1115
|
-
const facingCost = (from, to, side) => {
|
|
1116
|
-
const dx = to.x + to.w / 2 - (from.x + from.w / 2);
|
|
1117
|
-
const dy = to.y + to.h / 2 - (from.y + from.h / 2);
|
|
1118
|
-
const len = Math.hypot(dx, dy) || 1;
|
|
1119
|
-
const n = normalOf(side);
|
|
1120
|
-
return 1 - (n.x * dx + n.y * dy) / len;
|
|
1121
|
-
};
|
|
1122
|
-
const W_BACK = 2.6;
|
|
1123
|
-
const backFacingCost = (from, to, side) => W_BACK * Math.max(0, facingCost(from, to, side) - 1);
|
|
1124
|
-
const ctx = layout.edges.map((edge) => {
|
|
1125
|
-
const source = rectById.get(edge.sourceId);
|
|
1126
|
-
const target = rectById.get(edge.targetId);
|
|
1127
|
-
if (!source || !target) return null;
|
|
1128
|
-
const exclude = new Set(
|
|
1129
|
-
[
|
|
1130
|
-
edge.sourceId,
|
|
1131
|
-
edge.targetId,
|
|
1132
|
-
boxOf(edge.sourceId),
|
|
1133
|
-
boxOf(edge.targetId)
|
|
1134
|
-
].filter((x) => x !== void 0)
|
|
1135
|
-
);
|
|
1136
|
-
return {
|
|
1137
|
-
source,
|
|
1138
|
-
target,
|
|
1139
|
-
sPorts: portsById.get(edge.sourceId),
|
|
1140
|
-
tPorts: portsById.get(edge.targetId),
|
|
1141
|
-
obstacleRects: obstacles.filter((o) => !exclude.has(o.id)).map((o) => o.rect),
|
|
1142
|
-
adjacent: /* @__PURE__ */ new Set()
|
|
1143
|
-
};
|
|
1144
|
-
});
|
|
1145
|
-
for (let a = 0; a < layout.edges.length; a++) {
|
|
1146
|
-
for (let b = a + 1; b < layout.edges.length; b++) {
|
|
1147
|
-
const ea = layout.edges[a];
|
|
1148
|
-
const eb = layout.edges[b];
|
|
1149
|
-
if (ea.sourceId === eb.sourceId || ea.sourceId === eb.targetId || ea.targetId === eb.sourceId || ea.targetId === eb.targetId) {
|
|
1150
|
-
ctx[a]?.adjacent.add(b);
|
|
1151
|
-
ctx[b]?.adjacent.add(a);
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
const W_NODE = 100;
|
|
1156
|
-
const W_EDGE = 8;
|
|
1157
|
-
const W_PORT = 1.5;
|
|
1158
|
-
const W_PORT_SAME = 0.6;
|
|
1159
|
-
const isBox = (id) => portsById.has(id);
|
|
1160
|
-
const portClashCount = (self, s, t) => {
|
|
1161
|
-
const e = layout.edges[self];
|
|
1162
|
-
let n = 0;
|
|
1163
|
-
for (let j = 0; j < layout.edges.length; j++) {
|
|
1164
|
-
if (j === self) continue;
|
|
1165
|
-
const ej = layout.edges[j];
|
|
1166
|
-
const cj = chosen[j];
|
|
1167
|
-
if (!isBox(e.sourceId) && ej.targetId === e.sourceId && ej.sourceId !== e.targetId && // not reciprocal
|
|
1168
|
-
cj.t === s)
|
|
1169
|
-
n++;
|
|
1170
|
-
if (!isBox(e.targetId) && ej.sourceId === e.targetId && ej.targetId !== e.sourceId && // not reciprocal
|
|
1171
|
-
cj.s === t)
|
|
1172
|
-
n++;
|
|
1173
|
-
}
|
|
1174
|
-
return n;
|
|
1175
|
-
};
|
|
1176
|
-
const portStackCount = (self, _s, t) => {
|
|
1177
|
-
const e = layout.edges[self];
|
|
1178
|
-
if (isBox(e.targetId)) return 0;
|
|
1179
|
-
let n = 0;
|
|
1180
|
-
for (let j = 0; j < layout.edges.length; j++) {
|
|
1181
|
-
if (j === self) continue;
|
|
1182
|
-
const ej = layout.edges[j];
|
|
1183
|
-
if (ej.targetId === e.targetId && ej.sourceId !== e.sourceId && // not reciprocal
|
|
1184
|
-
chosen[j].t === t)
|
|
1185
|
-
n++;
|
|
1186
|
-
}
|
|
1187
|
-
return n;
|
|
1188
|
-
};
|
|
1189
|
-
const W_TURN = 0.55;
|
|
1190
|
-
const W_SCURVE = 1.8;
|
|
1191
|
-
const CURVE_STRAIGHT = 0.35;
|
|
1192
|
-
const curveCostPoly = (pts) => {
|
|
1193
|
-
let totalTurn = 0;
|
|
1194
|
-
let inflections = 0;
|
|
1195
|
-
let prevSign = 0;
|
|
1196
|
-
for (let i = 1; i < pts.length - 1; i++) {
|
|
1197
|
-
const ax = pts[i].x - pts[i - 1].x;
|
|
1198
|
-
const ay = pts[i].y - pts[i - 1].y;
|
|
1199
|
-
const bx = pts[i + 1].x - pts[i].x;
|
|
1200
|
-
const by = pts[i + 1].y - pts[i].y;
|
|
1201
|
-
const cross = ax * by - ay * bx;
|
|
1202
|
-
const dot = ax * bx + ay * by;
|
|
1203
|
-
if (ax === 0 && ay === 0) continue;
|
|
1204
|
-
if (bx === 0 && by === 0) continue;
|
|
1205
|
-
totalTurn += Math.abs(Math.atan2(cross, dot));
|
|
1206
|
-
const sign = cross > 1e-6 ? 1 : cross < -1e-6 ? -1 : 0;
|
|
1207
|
-
if (sign !== 0) {
|
|
1208
|
-
if (prevSign !== 0 && sign !== prevSign) inflections++;
|
|
1209
|
-
prevSign = sign;
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
return totalTurn * W_TURN + inflections * W_SCURVE;
|
|
1213
|
-
};
|
|
1214
|
-
const scoreOf = (c, g, poly, self, polys2, s, t) => {
|
|
1215
|
-
let edgeCross = 0;
|
|
1216
|
-
for (let j = 0; j < polys2.length; j++) {
|
|
1217
|
-
if (j === self || c.adjacent.has(j)) continue;
|
|
1218
|
-
const pj = polys2[j];
|
|
1219
|
-
if (pj) edgeCross += polyCrossings(poly, pj);
|
|
1220
|
-
}
|
|
1221
|
-
return pathCrossings(g, c.obstacleRects) * W_NODE + edgeCross * W_EDGE + portClashCount(self, s, t) * W_PORT + portStackCount(self, s, t) * W_PORT_SAME + curveCostPoly(poly) + facingCost(c.source, c.target, s) + facingCost(c.target, c.source, t) + backFacingCost(c.source, c.target, s) + backFacingCost(c.target, c.source, t);
|
|
1222
|
-
};
|
|
1223
|
-
const chosen = layout.edges.map(
|
|
1224
|
-
(_, i) => sides[i] ?? { s: "R", t: "L" }
|
|
1225
|
-
);
|
|
1226
|
-
const geoms = ctx.map(
|
|
1227
|
-
(c, i) => c ? edgePath(
|
|
1228
|
-
c.source,
|
|
1229
|
-
c.target,
|
|
1230
|
-
chosen[i].s,
|
|
1231
|
-
chosen[i].t,
|
|
1232
|
-
c.sPorts,
|
|
1233
|
-
c.tPorts
|
|
1234
|
-
) : null
|
|
1235
|
-
);
|
|
1236
|
-
const polys = geoms.map((g) => g ? polyline(g) : null);
|
|
1237
|
-
for (let pass = 0; pass < 3; pass++) {
|
|
1238
|
-
let changed = false;
|
|
1239
|
-
for (let i = 0; i < ctx.length; i++) {
|
|
1240
|
-
const c = ctx[i];
|
|
1241
|
-
const g = geoms[i];
|
|
1242
|
-
const p = polys[i];
|
|
1243
|
-
if (!c || !g || !p) continue;
|
|
1244
|
-
const cur = chosen[i];
|
|
1245
|
-
const curScore = scoreOf(c, g, p, i, polys, cur.s, cur.t);
|
|
1246
|
-
if (curScore < W_EDGE && portClashCount(i, cur.s, cur.t) === 0 && portStackCount(i, cur.s, cur.t) === 0 && curveCostPoly(p) < CURVE_STRAIGHT && backFacingCost(c.source, c.target, cur.s) === 0 && backFacingCost(c.target, c.source, cur.t) === 0)
|
|
1247
|
-
continue;
|
|
1248
|
-
let bestScore = curScore;
|
|
1249
|
-
let bestG = g;
|
|
1250
|
-
let bestP = p;
|
|
1251
|
-
let bestSide = cur;
|
|
1252
|
-
for (const s of SIDES) {
|
|
1253
|
-
for (const t of SIDES) {
|
|
1254
|
-
const cg = edgePath(c.source, c.target, s, t, c.sPorts, c.tPorts);
|
|
1255
|
-
const cp = polyline(cg);
|
|
1256
|
-
const sc = scoreOf(c, cg, cp, i, polys, s, t);
|
|
1257
|
-
if (sc < bestScore) {
|
|
1258
|
-
bestScore = sc;
|
|
1259
|
-
bestG = cg;
|
|
1260
|
-
bestP = cp;
|
|
1261
|
-
bestSide = { s, t };
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
if (bestSide !== cur) {
|
|
1266
|
-
chosen[i] = bestSide;
|
|
1267
|
-
geoms[i] = bestG;
|
|
1268
|
-
polys[i] = bestP;
|
|
1269
|
-
changed = true;
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
if (!changed) break;
|
|
1273
|
-
}
|
|
1274
|
-
const FAN_GAP_MAX = 32;
|
|
1275
|
-
const fanSides = /* @__PURE__ */ new Map();
|
|
1276
|
-
for (let i = 0; i < layout.edges.length; i++) {
|
|
1277
|
-
const g = geoms[i];
|
|
1278
|
-
if (!g) continue;
|
|
1279
|
-
const e = layout.edges[i];
|
|
1280
|
-
const ch = chosen[i];
|
|
1281
|
-
const ends = [
|
|
1282
|
-
{ id: e.sourceId, side: ch.s, end: "p0", at: g.p0, far: g.p1 },
|
|
1283
|
-
{ id: e.targetId, side: ch.t, end: "p1", at: g.p1, far: g.p0 }
|
|
1284
|
-
];
|
|
1285
|
-
for (const en of ends) {
|
|
1286
|
-
if (isBox(en.id)) continue;
|
|
1287
|
-
const n = normalOf(en.side);
|
|
1288
|
-
const along = en.side === "T" || en.side === "B";
|
|
1289
|
-
const dx = en.far.x - en.at.x;
|
|
1290
|
-
const dy = en.far.y - en.at.y;
|
|
1291
|
-
const proj = along ? dx : dy;
|
|
1292
|
-
const key = Math.atan2(proj, dx * n.x + dy * n.y);
|
|
1293
|
-
const k = `${en.side}:${en.id}`;
|
|
1294
|
-
const list = fanSides.get(k) ?? [];
|
|
1295
|
-
list.push({ edge: i, end: en.end, at: en.at, key, proj });
|
|
1296
|
-
fanSides.set(k, list);
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
const fanned = layout.edges.map(() => ({}));
|
|
1300
|
-
let anyFanned = false;
|
|
1301
|
-
for (const [k, list] of fanSides) {
|
|
1302
|
-
const count = list.length;
|
|
1303
|
-
if (count < 2) continue;
|
|
1304
|
-
const along = k[0] === "T" || k[0] === "B";
|
|
1305
|
-
const c = ctx[list[0].edge];
|
|
1306
|
-
const rect = list[0].end === "p0" ? c.source : c.target;
|
|
1307
|
-
const len = along ? rect.w : rect.h;
|
|
1308
|
-
const gap = Math.min(len / (count + 1), FAN_GAP_MAX);
|
|
1309
|
-
const span = (count - 1) / 2 * gap;
|
|
1310
|
-
const reach = len / 2 - gap / 2;
|
|
1311
|
-
list.sort(
|
|
1312
|
-
(a, b) => a.key - b.key || a.edge - b.edge || (a.end < b.end ? -1 : 1)
|
|
1313
|
-
);
|
|
1314
|
-
const runs = [];
|
|
1315
|
-
const place = (r) => Math.max(
|
|
1316
|
-
-reach,
|
|
1317
|
-
Math.min(reach - (r.n - 1) * gap, r.an > 0 ? r.as / r.an : r.ls / r.ln)
|
|
1318
|
-
);
|
|
1319
|
-
for (const f of list) {
|
|
1320
|
-
const aligned = Math.abs(f.proj) <= span;
|
|
1321
|
-
const d = Math.max(-span, Math.min(span, f.proj));
|
|
1322
|
-
const run = aligned ? { n: 1, an: 1, as: d, ln: 0, ls: 0, at: 0 } : { n: 1, an: 0, as: 0, ln: 1, ls: d, at: 0 };
|
|
1323
|
-
run.at = place(run);
|
|
1324
|
-
runs.push(run);
|
|
1325
|
-
while (runs.length > 1) {
|
|
1326
|
-
const cur = runs[runs.length - 1];
|
|
1327
|
-
const prev = runs[runs.length - 2];
|
|
1328
|
-
if (prev.at + prev.n * gap <= cur.at) break;
|
|
1329
|
-
runs.pop();
|
|
1330
|
-
prev.as += cur.as - cur.an * prev.n * gap;
|
|
1331
|
-
prev.ls += cur.ls - cur.ln * prev.n * gap;
|
|
1332
|
-
prev.an += cur.an;
|
|
1333
|
-
prev.ln += cur.ln;
|
|
1334
|
-
prev.n += cur.n;
|
|
1335
|
-
prev.at = place(prev);
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
let idx = 0;
|
|
1339
|
-
for (const run of runs) {
|
|
1340
|
-
for (let m = 0; m < run.n; m++, idx++) {
|
|
1341
|
-
const f = list[idx];
|
|
1342
|
-
const off2 = run.at + m * gap;
|
|
1343
|
-
fanned[f.edge][f.end] = along ? { x: f.at.x + off2, y: f.at.y } : { x: f.at.x, y: f.at.y + off2 };
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
anyFanned = true;
|
|
1347
|
-
}
|
|
1348
|
-
if (anyFanned) {
|
|
1349
|
-
for (let i = 0; i < ctx.length; i++) {
|
|
1350
|
-
const c = ctx[i];
|
|
1351
|
-
const f = fanned[i];
|
|
1352
|
-
if (!c || !f.p0 && !f.p1) continue;
|
|
1353
|
-
const g = edgePath(
|
|
1354
|
-
c.source,
|
|
1355
|
-
c.target,
|
|
1356
|
-
chosen[i].s,
|
|
1357
|
-
chosen[i].t,
|
|
1358
|
-
c.sPorts,
|
|
1359
|
-
c.tPorts,
|
|
1360
|
-
f
|
|
1361
|
-
);
|
|
1362
|
-
geoms[i] = g;
|
|
1363
|
-
polys[i] = polyline(g);
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
const SHARED_END_CLEAR = 2 * HOP_R;
|
|
1367
|
-
const endsAt = (i, id) => {
|
|
1368
|
-
const e = layout.edges[i];
|
|
1369
|
-
const g = geoms[i];
|
|
1370
|
-
const out = [];
|
|
1371
|
-
if (e.sourceId === id) out.push(g.p0);
|
|
1372
|
-
if (e.targetId === id) out.push(g.p1);
|
|
1373
|
-
return out;
|
|
1374
|
-
};
|
|
1375
|
-
const hopsFor = layout.edges.map(() => []);
|
|
1376
|
-
const hopsOn = layout.edges.map(() => []);
|
|
1377
|
-
for (let i = 0; i < polys.length; i++) {
|
|
1378
|
-
for (let j = i + 1; j < polys.length; j++) {
|
|
1379
|
-
const a = polys[i];
|
|
1380
|
-
const b = polys[j];
|
|
1381
|
-
if (!a || !b) continue;
|
|
1382
|
-
if (!ctx[i]?.adjacent.has(j)) {
|
|
1383
|
-
const pt2 = polyIntersections(a, b)[0];
|
|
1384
|
-
if (pt2) {
|
|
1385
|
-
hopsFor[j].push(pt2);
|
|
1386
|
-
hopsOn[i].push(pt2);
|
|
1387
|
-
hopsOn[j].push(pt2);
|
|
1388
|
-
}
|
|
1389
|
-
continue;
|
|
1390
|
-
}
|
|
1391
|
-
const ei = layout.edges[i];
|
|
1392
|
-
const ej = layout.edges[j];
|
|
1393
|
-
const shared = [ei.sourceId, ei.targetId].filter(
|
|
1394
|
-
(id) => id === ej.sourceId || id === ej.targetId
|
|
1395
|
-
);
|
|
1396
|
-
const nearShared = shared.flatMap((id) => [
|
|
1397
|
-
...endsAt(i, id),
|
|
1398
|
-
...endsAt(j, id)
|
|
1399
|
-
]);
|
|
1400
|
-
const pt = polyIntersections(a, b).find(
|
|
1401
|
-
(p) => nearShared.every(
|
|
1402
|
-
(q) => Math.hypot(p.x - q.x, p.y - q.y) > SHARED_END_CLEAR
|
|
1403
|
-
)
|
|
1404
|
-
);
|
|
1405
|
-
if (pt) {
|
|
1406
|
-
hopsFor[j].push(pt);
|
|
1407
|
-
hopsOn[i].push(pt);
|
|
1408
|
-
hopsOn[j].push(pt);
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
return layout.edges.map((edge, i) => {
|
|
1413
|
-
const g = geoms[i];
|
|
1414
|
-
if (!g) return null;
|
|
1415
|
-
const dRender = hoppedPath(g, hopsFor[i]);
|
|
1416
|
-
return {
|
|
1417
|
-
sourceId: edge.sourceId,
|
|
1418
|
-
targetId: edge.targetId,
|
|
1419
|
-
d: g.d,
|
|
1420
|
-
mid: g.mid,
|
|
1421
|
-
...dRender && { dRender },
|
|
1422
|
-
...hopsOn[i].length > 0 && { hops: hopsOn[i] },
|
|
1423
|
-
p0: g.p0,
|
|
1424
|
-
h0: g.h0,
|
|
1425
|
-
h1: g.h1,
|
|
1426
|
-
p1: g.p1
|
|
1427
|
-
};
|
|
1428
|
-
});
|
|
1429
|
-
}
|
|
1430
|
-
var HOP_LABEL_CLEAR = HOP_R + 2;
|
|
1431
|
-
function sketchLabelSpot(g, halfW, halfH) {
|
|
1432
|
-
const hops = g.hops ?? [];
|
|
1433
|
-
if (hops.length === 0) return g.mid;
|
|
1434
|
-
const clear = (p) => hops.every(
|
|
1435
|
-
(h) => Math.abs(p.x - h.x) >= halfW + HOP_LABEL_CLEAR || Math.abs(p.y - h.y) >= halfH + HOP_LABEL_CLEAR
|
|
1436
|
-
);
|
|
1437
|
-
if (clear(g.mid)) return g.mid;
|
|
1438
|
-
for (let step = 1; step <= 18; step++) {
|
|
1439
|
-
for (const t of [0.5 - step * 0.02, 0.5 + step * 0.02]) {
|
|
1440
|
-
const p = cubicAt(g, t);
|
|
1441
|
-
if (clear(p)) return p;
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
return g.mid;
|
|
1445
|
-
}
|
|
1446
|
-
function renderSketchForExport(container, parsed, layout, palette, isDark, options = {}) {
|
|
1447
|
-
renderSketch(container, parsed, layout, palette, isDark, {
|
|
1448
|
-
...options,
|
|
1449
|
-
exportMode: options.exportMode ?? true
|
|
1450
|
-
});
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
export {
|
|
1454
|
-
CONTAINER_FILL_OPACITY,
|
|
1455
|
-
CONTAINER_STROKE_OPACITY,
|
|
1456
|
-
CONTAINER_STROKE_WIDTH,
|
|
1457
|
-
sketchContainerFill,
|
|
1458
|
-
sketchNeutralFill,
|
|
1459
|
-
sketchColors,
|
|
1460
|
-
SKETCH_VISUALS,
|
|
1461
|
-
renderSketch,
|
|
1462
|
-
sketchEdgeGeometry,
|
|
1463
|
-
sketchLabelSpot,
|
|
1464
|
-
renderSketchForExport
|
|
1465
|
-
};
|