@diagrammo/dgmo 0.95.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.
Files changed (215) hide show
  1. package/README.md +1 -1
  2. package/dist/advanced.d.ts +27 -569
  3. package/dist/advanced.js +157 -350
  4. package/dist/block.js +54 -55
  5. package/dist/{calculator-BNH3GSJE.js → calculator-GIB7POFX.js} +2 -2
  6. package/dist/{chart-meta-BbjsRY7o.d.ts → chart-meta-uImk4eTn.d.ts} +1 -1
  7. package/dist/chart-meta.d.ts +2 -2
  8. package/dist/chart-meta.js +15 -15
  9. package/dist/{chart-types-Cppnp31G.d.ts → chart-types-CBVYiLqy.d.ts} +1 -5
  10. package/dist/{charts-d3-SZV2SFTM.js → charts-d3-IRP5VANE.js} +5 -5
  11. package/dist/{chunk-MLY5ALOV.js → chunk-2BYFNZVM.js} +2 -2
  12. package/dist/{chunk-OSSTOSXU.js → chunk-2M3K6GD2.js} +3 -6
  13. package/dist/{chunk-CDM6XMOT.js → chunk-34JOHSVE.js} +2 -2
  14. package/dist/chunk-35A3J4Z5.js +14 -0
  15. package/dist/{chunk-7IALGIOI.js → chunk-3W73ZP6W.js} +9 -4
  16. package/dist/{chunk-IHN6WDEY.js → chunk-4B45SNZH.js} +5 -5
  17. package/dist/{chunk-AULZD45V.js → chunk-5ARANBIE.js} +2 -2
  18. package/dist/{chunk-L5S32VZJ.js → chunk-5BG4OSWJ.js} +5 -5
  19. package/dist/{chunk-VB4VMB2I.js → chunk-5G4DOOX2.js} +1 -1
  20. package/dist/{chunk-EXHXTMVR.js → chunk-5GU6C5WB.js} +2 -2
  21. package/dist/{chunk-EBIDNRSR.js → chunk-5H7SA5ZD.js} +1 -1
  22. package/dist/{chunk-CN4RDJDV.js → chunk-5LG526X7.js} +1 -1
  23. package/dist/{chunk-VF7IKIRS.js → chunk-67RKJEKF.js} +5 -5
  24. package/dist/{chunk-D3K4U3ZM.js → chunk-6K2W3FH7.js} +8 -5
  25. package/dist/{chunk-C6G36F5S.js → chunk-7UWVHNPM.js} +4 -4
  26. package/dist/{chunk-FWVE373X.js → chunk-AQRSEQZV.js} +1 -1
  27. package/dist/{chunk-BQO24BDM.js → chunk-AUXUNV73.js} +1 -1
  28. package/dist/{chunk-3PP746YM.js → chunk-B2YVZ3QU.js} +1 -1
  29. package/dist/{chunk-QPQWY5RU.js → chunk-B5CEV7IR.js} +3 -3
  30. package/dist/{chunk-J7Q4AB23.js → chunk-B7BSRVNC.js} +2 -2
  31. package/dist/{chunk-IXHWS2HI.js → chunk-BD4O4D2A.js} +2 -2
  32. package/dist/{chunk-RQTDUZUH.js → chunk-BUVLLK3D.js} +6 -6
  33. package/dist/{chunk-JJ7RGTAU.js → chunk-CD7VQCDL.js} +6 -6
  34. package/dist/{chunk-6EFCOES2.js → chunk-CP73WWLN.js} +5 -5
  35. package/dist/{chunk-GI5R5B3S.js → chunk-D7DGLJ7N.js} +2 -2
  36. package/dist/{chunk-XOMIYMQ3.js → chunk-DKCVAQU3.js} +6 -6
  37. package/dist/{chunk-KBDMEOLI.js → chunk-DPNSDCR3.js} +1 -1
  38. package/dist/{chunk-BRGRIZQB.js → chunk-DWMQ24JR.js} +2 -2
  39. package/dist/{chunk-4XYZJTFO.js → chunk-EM5SA7NC.js} +11 -3
  40. package/dist/{chunk-AM42TLSZ.js → chunk-EMGNCG5C.js} +1 -1
  41. package/dist/{chunk-3KVUFHUJ.js → chunk-EPB3UEMD.js} +2 -2
  42. package/dist/{chunk-NXU4N5WT.js → chunk-EZ6BI5LL.js} +1 -1
  43. package/dist/{chunk-2ZW4NBYX.js → chunk-FBK32S4D.js} +1 -1
  44. package/dist/{chunk-2I4UQG7W.js → chunk-FHX373L5.js} +1 -1
  45. package/dist/{chunk-AQQUJEDN.js → chunk-FKJWZWNU.js} +1 -1
  46. package/dist/{chunk-TIIZIAGS.js → chunk-FUYEXLAN.js} +6 -13
  47. package/dist/{chunk-TASRGDYP.js → chunk-GEHK4UD5.js} +3 -3
  48. package/dist/{chunk-ZD3QCNB7.js → chunk-GLL237AP.js} +2 -2
  49. package/dist/{chunk-PGSGJLUR.js → chunk-GM2SLMQW.js} +2 -2
  50. package/dist/{chunk-V5D6GDNK.js → chunk-H7JVKUA5.js} +4 -4
  51. package/dist/{chunk-IY62DXT2.js → chunk-H7RQG4PH.js} +2 -2
  52. package/dist/{chunk-KLBTJEIK.js → chunk-HH3DHNQA.js} +1 -1
  53. package/dist/{chunk-GWQRTA5W.js → chunk-HZX4QZGR.js} +1 -1
  54. package/dist/{chunk-KBEWVCK3.js → chunk-I2I2FKAK.js} +1 -1
  55. package/dist/{chunk-BTP2BMWN.js → chunk-I3ZRIMR7.js} +10 -10
  56. package/dist/{chunk-IQN2NGEU.js → chunk-IKXQ6NVR.js} +6 -6
  57. package/dist/{chunk-XJQ6Z3FO.js → chunk-IQZSKWE5.js} +3 -3
  58. package/dist/{chunk-3GUEYQGL.js → chunk-ITDF3MWD.js} +3 -3
  59. package/dist/{chunk-JXGSTBW6.js → chunk-IU7XKZA6.js} +2 -2
  60. package/dist/{chunk-RITTLIDJ.js → chunk-IZFJZCF6.js} +4 -4
  61. package/dist/{chunk-PGBTUGDB.js → chunk-JBAFAKGX.js} +5 -5
  62. package/dist/{chunk-VSAKEMDD.js → chunk-JCC3B2GS.js} +8 -2
  63. package/dist/{chunk-66JTY4AN.js → chunk-JRN2CRXJ.js} +1 -1
  64. package/dist/{chunk-USZLHYEQ.js → chunk-K2SFH3GJ.js} +2 -2
  65. package/dist/{chunk-NM5V5EKY.js → chunk-K2WXMXI2.js} +1 -1
  66. package/dist/{chunk-62GLKE6Q.js → chunk-KDWEOT54.js} +21 -9
  67. package/dist/{chunk-PVWIBKV2.js → chunk-KQAPFIYF.js} +2 -2
  68. package/dist/{chunk-4SACN2C5.js → chunk-LGUFJ7Y2.js} +1 -1
  69. package/dist/{chunk-LAXMUSGW.js → chunk-LRHPBWOY.js} +4 -4
  70. package/dist/{chunk-5XBYLYYI.js → chunk-M4ZUIW3J.js} +3 -3
  71. package/dist/{chunk-S4SJ4LLY.js → chunk-MAYTYF5S.js} +3 -3
  72. package/dist/{chunk-P5XA627W.js → chunk-MVUNMJKO.js} +46 -52
  73. package/dist/{chunk-E7F76M3U.js → chunk-MYHR3PMH.js} +57 -5
  74. package/dist/{chunk-NBAAIRFP.js → chunk-NI4L6HEU.js} +4 -1
  75. package/dist/{chunk-A2A2I4Z4.js → chunk-O2HNVNKO.js} +2 -2
  76. package/dist/{chunk-4UVCTLRH.js → chunk-OAZ6BIIB.js} +3 -3
  77. package/dist/{chunk-IZHGT5HD.js → chunk-OO4GUE4E.js} +3 -3
  78. package/dist/{chunk-KSZB7TIY.js → chunk-OYGJZA24.js} +8 -8
  79. package/dist/{chunk-H6BT45SN.js → chunk-P2GNNEMN.js} +1 -1
  80. package/dist/{chunk-U6VS3T2M.js → chunk-P3PSLC2K.js} +4 -4
  81. package/dist/{chunk-HHVIE45A.js → chunk-P6LKX3IQ.js} +4 -4
  82. package/dist/{chunk-NMS5XSLX.js → chunk-PGD6CNWO.js} +1 -1
  83. package/dist/{chunk-NVZ7R4EK.js → chunk-PWZEJNO6.js} +3 -3
  84. package/dist/{chunk-NPDVWFAQ.js → chunk-QEU6VM4O.js} +1 -1
  85. package/dist/{chunk-UIEKW4SG.js → chunk-R5L5E3TC.js} +0 -1
  86. package/dist/{chunk-2WQ3YZMN.js → chunk-RGFVHYDP.js} +2 -2
  87. package/dist/{chunk-YQMIKX54.js → chunk-RIK6WUMO.js} +62 -29
  88. package/dist/{chunk-BH2SF4TT.js → chunk-RTRORHSP.js} +1 -1
  89. package/dist/{chunk-USE6HIOZ.js → chunk-RVOSA4RR.js} +845 -193
  90. package/dist/{chunk-LKZCUFOW.js → chunk-SCYVGPLI.js} +2 -2
  91. package/dist/{chunk-XIUGSUE4.js → chunk-SIABEREK.js} +6 -2
  92. package/dist/{chunk-R46N5KR4.js → chunk-SQBCNEJB.js} +2 -1
  93. package/dist/{chunk-VTESGA5F.js → chunk-THTYCJGF.js} +2 -2
  94. package/dist/{chunk-4EX4OKGZ.js → chunk-TIPNNOZC.js} +5 -5
  95. package/dist/{chunk-UZ7EZW3B.js → chunk-TO53KK2P.js} +6 -1
  96. package/dist/{chunk-LJGE43SG.js → chunk-TPCP2UOI.js} +3 -3
  97. package/dist/{chunk-Y3SIQN4J.js → chunk-TUOZ6AXC.js} +2 -2
  98. package/dist/{chunk-KRPLP5LP.js → chunk-U6N547QA.js} +1 -1
  99. package/dist/{chunk-BCV2VJTM.js → chunk-VJ7NLYFE.js} +2 -2
  100. package/dist/{chunk-UXNA2H2V.js → chunk-VLI6AWS3.js} +2 -2
  101. package/dist/{chunk-2J2CWWWK.js → chunk-VNAPEZX2.js} +1 -1
  102. package/dist/{chunk-EBVVMUAA.js → chunk-VVTCUIPN.js} +1 -1
  103. package/dist/{chunk-FVTYJUXR.js → chunk-W5MMPPGX.js} +2 -2
  104. package/dist/{chunk-BQMHJ6OW.js → chunk-WGNP5ND2.js} +20 -12
  105. package/dist/{chunk-5OTA2PUH.js → chunk-WLGOIMTI.js} +2 -2
  106. package/dist/{chunk-IGJEKO2P.js → chunk-WTPS73RM.js} +4 -4
  107. package/dist/{chunk-R6EGUTIV.js → chunk-YFIUABSF.js} +2 -2
  108. package/dist/{chunk-7PJBJ2DU.js → chunk-YSGOXZBB.js} +1 -1
  109. package/dist/{chunk-ZECJ7Q6C.js → chunk-YVIPJGFG.js} +2 -2
  110. package/dist/{chunk-5KKZXA44.js → chunk-ZBOIYC2G.js} +4 -2
  111. package/dist/{chunk-RBVFNUAL.js → chunk-ZOKD5F7M.js} +2 -2
  112. package/dist/{collapse-BSPH3BFM.js → collapse-6X5S2S3G.js} +2 -1
  113. package/dist/completion-registry.js +4 -38
  114. package/dist/countdown.js +1 -1
  115. package/dist/{dgmo-router-6YLKM6WU.js → dgmo-router-EPBQKQW2.js} +44 -45
  116. package/dist/{dimensions-DS6FWLRY.js → dimensions-DBCRBZIU.js} +4 -4
  117. package/dist/editor.js +2 -7
  118. package/dist/{flowchart-parser-J3VWIFWB.js → flowchart-parser-SM5LHF2D.js} +3 -3
  119. package/dist/{flowchart-renderer-FECH5KK5.js → flowchart-renderer-PPS2GTTD.js} +9 -9
  120. package/dist/highlight.js +3 -8
  121. package/dist/index.d.ts +42 -6
  122. package/dist/index.js +61 -56
  123. package/dist/{layout-MXHR3P35.js → layout-3J72UULM.js} +3 -3
  124. package/dist/{layout-TIBRMWHL.js → layout-AF4KA5B2.js} +3 -3
  125. package/dist/{layout-5SZ5A64I.js → layout-FCFUVCRI.js} +6 -6
  126. package/dist/{layout-KV27UW25.js → layout-H2KP5A6M.js} +1 -1
  127. package/dist/{layout-ND26WVX2.js → layout-IXQNVRBE.js} +3 -3
  128. package/dist/{layout-YBMT74MG.js → layout-KELSNWBD.js} +6 -6
  129. package/dist/{layout-JTIRCKAW.js → layout-LNVIH45F.js} +3 -3
  130. package/dist/{layout-EWXHEXNS.js → layout-MPSH3YWE.js} +6 -6
  131. package/dist/{layout-HFBK5UEG.js → layout-PPIVGJXD.js} +2 -2
  132. package/dist/{layout-RH3N46HP.js → layout-R6G4MEO3.js} +4 -3
  133. package/dist/{layout-search-AWPJWBKT.js → layout-search-C2TYUH4L.js} +4 -4
  134. package/dist/{parser-BT4NI6JY.js → parser-2AXU2MHJ.js} +3 -3
  135. package/dist/{parser-3QIFY5IN.js → parser-4BOP77CD.js} +2 -2
  136. package/dist/{parser-MA7L3XND.js → parser-4GHAYZKZ.js} +4 -4
  137. package/dist/{parser-LTRQASGB.js → parser-5HQPPVHJ.js} +3 -3
  138. package/dist/{parser-EAQPZX2B.js → parser-5RUD4A3X.js} +3 -3
  139. package/dist/{parser-BJ7RJNUM.js → parser-6WURFLOB.js} +3 -3
  140. package/dist/{parser-AR2GON37.js → parser-722KLLAQ.js} +4 -4
  141. package/dist/{parser-KMKVFET2.js → parser-B7SCN7FG.js} +2 -2
  142. package/dist/{parser-TUAEX6ML.js → parser-BNRX35NT.js} +3 -3
  143. package/dist/{parser-NL73FSFI.js → parser-BPYHIG24.js} +3 -3
  144. package/dist/{parser-3LT3ZUUC.js → parser-CRJYLOHA.js} +2 -2
  145. package/dist/{parser-NUU3DVW4.js → parser-DGLKH4NT.js} +2 -2
  146. package/dist/{parser-5UZV32ZI.js → parser-DRUGM3R6.js} +2 -2
  147. package/dist/{parser-KLCSFQ3J.js → parser-DWV2CYBS.js} +3 -3
  148. package/dist/{parser-2IKPQVKX.js → parser-F3JJNIFC.js} +3 -3
  149. package/dist/{parser-EFF22GYO.js → parser-F75WNYMK.js} +3 -3
  150. package/dist/{parser-RTWPFKXG.js → parser-GBQY3WOZ.js} +3 -3
  151. package/dist/{parser-F7NDRPMA.js → parser-GM5B2EPW.js} +3 -3
  152. package/dist/{parser-K2DI26SC.js → parser-H6HT2WFQ.js} +2 -2
  153. package/dist/{parser-PEZ7RN43.js → parser-HM6FEX5K.js} +3 -3
  154. package/dist/{parser-YG625J5H.js → parser-L7SI3PW4.js} +4 -4
  155. package/dist/{parser-GMQJISZ6.js → parser-LUJX7AXO.js} +2 -2
  156. package/dist/{parser-A3AN76RQ.js → parser-MWINQNQW.js} +3 -3
  157. package/dist/{parser-KJPPUN4D.js → parser-OHBBDCG4.js} +2 -2
  158. package/dist/{parser-BTSTKY5G.js → parser-QWTECQEC.js} +4 -4
  159. package/dist/{parser-4TBSWULQ.js → parser-TTGZYLYD.js} +3 -3
  160. package/dist/{parser-6TDWUKM5.js → parser-UHKROC7Z.js} +3 -3
  161. package/dist/{parser-H5OODY5N.js → parser-W6NX3AIV.js} +3 -3
  162. package/dist/{parser-GDLKUBTO.js → parser-WIKXZP4X.js} +2 -2
  163. package/dist/{parser-4Y6PODW6.js → parser-XEJP37GQ.js} +3 -3
  164. package/dist/{parser-RCIEKN7Y.js → parser-Y7QXD2UR.js} +2 -2
  165. package/dist/{parser-ZRIZFDZY.js → parser-ZV756RMP.js} +4 -4
  166. package/dist/{parser-KKF6Y5MM.js → parser-ZWWQZ4XZ.js} +4 -3
  167. package/dist/{redraw-AHW7EUUF.js → redraw-BZ4GI6IC.js} +6 -4
  168. package/dist/{redraw-STZ4WJAI.js → redraw-LEOXS7C6.js} +3 -3
  169. package/dist/{renderer-4VGO27EE.js → renderer-2MCV75JQ.js} +11 -11
  170. package/dist/{renderer-TINHO2IO.js → renderer-2V2XY2V7.js} +4 -4
  171. package/dist/{renderer-XDX6S7OV.js → renderer-5AEDVLAA.js} +8 -7
  172. package/dist/{renderer-W4MUMXJE.js → renderer-5GP7YUS4.js} +3 -3
  173. package/dist/{renderer-ACQQ2JXA.js → renderer-66NCRXSC.js} +6 -6
  174. package/dist/{renderer-AM7P4LN7.js → renderer-6HU44IGJ.js} +4 -4
  175. package/dist/{renderer-6BDK47V6.js → renderer-7C76HP5W.js} +6 -6
  176. package/dist/{renderer-O6RP52CQ.js → renderer-7RAMBMTH.js} +6 -6
  177. package/dist/{renderer-IGSHD3OT.js → renderer-BYLQ37T2.js} +8 -8
  178. package/dist/{renderer-7OGGV327.js → renderer-CVCCW7UT.js} +4 -4
  179. package/dist/{renderer-QTPWRRJ7.js → renderer-D6EH5UQ6.js} +6 -6
  180. package/dist/renderer-E3334XLT.js +84 -0
  181. package/dist/{renderer-IXBOT6DU.js → renderer-F7YGMEGQ.js} +2 -2
  182. package/dist/{renderer-XCLARXCK.js → renderer-GKY55M4Z.js} +5 -5
  183. package/dist/{renderer-KJ3V5BYP.js → renderer-LFZKG2GR.js} +5 -5
  184. package/dist/{renderer-2LYX6NHN.js → renderer-LJPUCG3U.js} +5 -5
  185. package/dist/{renderer-DAZWHNLI.js → renderer-MGF2NNGG.js} +3 -3
  186. package/dist/{renderer-HX6ICPDX.js → renderer-MVBAQP3M.js} +4 -4
  187. package/dist/{renderer-UGW4N2W5.js → renderer-MZIWP55B.js} +5 -5
  188. package/dist/{renderer-MWZC7TMD.js → renderer-N6PM5CSG.js} +7 -7
  189. package/dist/{renderer-ISQJ2ELA.js → renderer-N75E3UVY.js} +5 -5
  190. package/dist/{renderer-IVHD7ENY.js → renderer-ONENGXFQ.js} +6 -6
  191. package/dist/{renderer-RUSQJFFC.js → renderer-PEJF6R2I.js} +2 -2
  192. package/dist/{renderer-36446HSH.js → renderer-QZJZIAT3.js} +2 -2
  193. package/dist/{renderer-UTEEJDGB.js → renderer-SYRH3B62.js} +6 -6
  194. package/dist/{renderer-2QPHDBZY.js → renderer-TS5NHOQD.js} +5 -5
  195. package/dist/{renderer-Y47EM337.js → renderer-VNJOZPSH.js} +7 -7
  196. package/dist/{renderer-P3WR7TKD.js → renderer-VNUDN6F5.js} +11 -11
  197. package/dist/{renderer-JYHA6R2D.js → renderer-VUXAAGUG.js} +5 -5
  198. package/dist/{renderer-BXD5KVSR.js → renderer-YJVQT7QG.js} +6 -6
  199. package/dist/{renderer-radial-WFUX3Z3C.js → renderer-radial-4R7AKETK.js} +5 -5
  200. package/dist/{state-parser-PMNHLHLE.js → state-parser-EQBBEKHR.js} +4 -4
  201. package/dist/{state-renderer-JQ5DS66J.js → state-renderer-SOGZ2CNN.js} +11 -11
  202. package/dist/{themes-DzIH3Hjv.d.ts → themes-CbJL3w8L.d.ts} +1 -1
  203. package/docs/ai-integration.md +5 -6
  204. package/docs/language-reference.md +68 -104
  205. package/gallery/fixtures/whiteboard.dgmo +1 -0
  206. package/package.json +1 -1
  207. package/dist/chunk-6BQSDF77.js +0 -734
  208. package/dist/chunk-NWSPPGIE.js +0 -60
  209. package/dist/chunk-VBMRZCMQ.js +0 -1465
  210. package/dist/chunk-WZBZXZ6T.js +0 -636
  211. package/dist/layout-D4L44NRG.js +0 -11
  212. package/dist/parser-YHDGKPZI.js +0 -14
  213. package/dist/renderer-WPN2SXGT.js +0 -29
  214. package/dist/renderer-ZV6KCLCL.js +0 -85
  215. package/gallery/fixtures/sketch.dgmo +0 -22
@@ -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
- };