w-flowchart 1.0.0 → 1.0.2

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 (435) hide show
  1. package/README.md +5 -1
  2. package/dist/w-flowchart.umd.js +2 -2
  3. package/dist/w-flowchart.umd.js.map +1 -1
  4. package/docs/WFlowchart.mjs.html +169 -0
  5. package/docs/global.html +657 -0
  6. package/docs/index.html +2 -2
  7. package/package.json +52 -49
  8. package/src/WFlowchart.mjs +97 -0
  9. package/src/p1/README.md +2 -2
  10. package/src/p1/gen.mjs +54 -20
  11. package/src/p10/README.md +6 -2
  12. package/src/p10/gen.mjs +235 -15
  13. package/src/p2/README.md +1 -1
  14. package/src/p2/gen.mjs +52 -7
  15. package/src/p3/README.md +1 -1
  16. package/src/p3/gen.mjs +24 -2
  17. package/src/p4/README.md +1 -1
  18. package/src/p4/gen.mjs +36 -10
  19. package/src/p5/README.md +1 -1
  20. package/src/p5/gen.mjs +45 -18
  21. package/src/p6/README.md +1 -1
  22. package/src/p6/gen.mjs +108 -5
  23. package/src/p7/README.md +1 -1
  24. package/src/p7/gen.mjs +42 -3
  25. package/src/p8/README.md +1 -1
  26. package/src/p8/gen.mjs +41 -2
  27. package/src/p9/README.md +1 -1
  28. package/src/p9/gen.mjs +14 -7
  29. package/src/p9/page9.html +13 -5
  30. package/test/cases-diamond.mjs +221 -0
  31. package/test/cases.mjs +17 -1
  32. package/test/data//345/267/245/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.json +43 -0
  33. package/test/data//345/276/256/346/234/215/345/213/231/345/217/242/351/233/206/346/236/266/346/247/213/345/234/226.json +132 -0
  34. package/test/data//346/234/203/345/223/241/350/250/273/345/206/212/346/265/201/347/250/213/345/234/226.json +27 -0
  35. package/test/data//346/250/241/347/265/204/347/265/204/346/210/220/345/234/226.json +111 -0
  36. package/test/data//346/254/212/351/231/220/350/247/222/350/211/262/351/227/234/350/201/257/345/234/226.json +117 -0
  37. package/test/data//347/211/251/350/201/257/347/266/262/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.json +203 -0
  38. package/test/data//347/263/273/347/265/261/345/210/206/345/261/244/346/236/266/346/247/213/345/234/226.json +156 -0
  39. package/test/data//350/250/202/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.json +46 -0
  40. package/test/data//350/263/207/346/226/231/346/265/201/345/220/221/345/234/226.json +137 -0
  41. package/test/data//350/263/207/346/226/231/346/270/205/346/264/227/346/265/201/347/250/213/345/234/226.json +28 -0
  42. package/test/data//351/233/273/345/225/206/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.json +134 -0
  43. package/test/data//351/240/201/351/235/242/345/260/216/350/246/275/351/227/234/350/201/257/345/234/226.json +178 -0
  44. package/test/figures.mjs +3 -2
  45. package/test/lib.mjs +3 -1
  46. package/test/p1.test.mjs +5 -0
  47. package/test/p10.test.mjs +54 -2
  48. package/test/p2.test.mjs +5 -0
  49. package/test/p3.test.mjs +5 -0
  50. package/test/p4.test.mjs +5 -0
  51. package/test/p5.test.mjs +6 -0
  52. package/test/p6.test.mjs +5 -0
  53. package/test/p7.test.mjs +5 -0
  54. package/test/p8.test.mjs +5 -0
  55. package/test/p9.test.mjs +5 -0
  56. package/test/pics/p1/cycle.png +0 -0
  57. package/test/pics/p1/deep-nest.png +0 -0
  58. package/test/pics/p1/diamond.png +0 -0
  59. package/test/pics/p1/edges-mix.png +0 -0
  60. package/test/pics/p1/english.png +0 -0
  61. package/test/pics/p1/flat-tb.png +0 -0
  62. package/test/pics/p1/fold-tall.png +0 -0
  63. package/test/pics/p1/fold-wide.png +0 -0
  64. package/test/pics/p1/group-edge.png +0 -0
  65. package/test/pics/p1/items-node.png +0 -0
  66. package/test/pics/p1/long-label.png +0 -0
  67. package/test/pics/p1/lr-flat.png +0 -0
  68. package/test/pics/p1/nested-group.png +0 -0
  69. package/test/pics/p1/no-edges.png +0 -0
  70. package/test/pics/p1/title-overflow.png +0 -0
  71. package/test/pics/p1/valign.png +0 -0
  72. package/test/pics/p1//345/267/245/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  73. package/test/pics/p1//345/276/256/346/234/215/345/213/231/345/217/242/351/233/206/346/236/266/346/247/213/345/234/226.png +0 -0
  74. package/test/pics/p1//346/234/203/345/223/241/350/250/273/345/206/212/346/265/201/347/250/213/345/234/226.png +0 -0
  75. package/test/pics/p1//346/250/241/347/265/204/347/265/204/346/210/220/345/234/226.png +0 -0
  76. package/test/pics/p1//346/254/212/351/231/220/350/247/222/350/211/262/351/227/234/350/201/257/345/234/226.png +0 -0
  77. package/test/pics/p1//347/211/251/350/201/257/347/266/262/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.png +0 -0
  78. package/test/pics/p1//347/263/273/347/265/261/345/210/206/345/261/244/346/236/266/346/247/213/345/234/226.png +0 -0
  79. package/test/pics/p1//350/250/202/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  80. package/test/pics/p1//350/263/207/346/226/231/346/265/201/345/220/221/345/234/226.png +0 -0
  81. package/test/pics/p1//350/263/207/346/226/231/346/270/205/346/264/227/346/265/201/347/250/213/345/234/226.png +0 -0
  82. package/test/pics/p1//351/233/273/345/225/206/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.png +0 -0
  83. package/test/pics/p1//351/240/201/351/235/242/345/260/216/350/246/275/351/227/234/350/201/257/345/234/226.png +0 -0
  84. package/test/pics/p10/cycle.png +0 -0
  85. package/test/pics/p10/cycle.svg +1 -0
  86. package/test/pics/p10/deep-nest.svg +1 -0
  87. package/test/pics/p10/dia-1in-1out.png +0 -0
  88. package/test/pics/p10/dia-1in-1out.svg +1 -0
  89. package/test/pics/p10/dia-1in-2out.png +0 -0
  90. package/test/pics/p10/dia-1in-2out.svg +1 -0
  91. package/test/pics/p10/dia-1in.png +0 -0
  92. package/test/pics/p10/dia-1in.svg +1 -0
  93. package/test/pics/p10/dia-1out.png +0 -0
  94. package/test/pics/p10/dia-1out.svg +1 -0
  95. package/test/pics/p10/dia-2in-1out.png +0 -0
  96. package/test/pics/p10/dia-2in-1out.svg +1 -0
  97. package/test/pics/p10/dia-2in-2out-dd.png +0 -0
  98. package/test/pics/p10/dia-2in-2out-dd.svg +1 -0
  99. package/test/pics/p10/dia-2in-2out-ss.png +0 -0
  100. package/test/pics/p10/dia-2in-2out-ss.svg +1 -0
  101. package/test/pics/p10/dia-2in.png +0 -0
  102. package/test/pics/p10/dia-2in.svg +1 -0
  103. package/test/pics/p10/dia-2out.png +0 -0
  104. package/test/pics/p10/dia-2out.svg +1 -0
  105. package/test/pics/p10/diamond.png +0 -0
  106. package/test/pics/p10/diamond.svg +1 -0
  107. package/test/pics/p10/edges-mix.png +0 -0
  108. package/test/pics/p10/edges-mix.svg +1 -0
  109. package/test/pics/p10/english.png +0 -0
  110. package/test/pics/p10/english.svg +1 -0
  111. package/test/pics/p10/flat-tb.svg +1 -0
  112. package/test/pics/p10/fold-tall.svg +1 -0
  113. package/test/pics/p10/fold-wide.svg +1 -0
  114. package/test/pics/p10/group-edge.png +0 -0
  115. package/test/pics/p10/group-edge.svg +1 -0
  116. package/test/pics/p10/items-node.svg +1 -0
  117. package/test/pics/p10/long-label.svg +1 -0
  118. package/test/pics/p10/lr-flat.svg +1 -0
  119. package/test/pics/p10/nested-group.png +0 -0
  120. package/test/pics/p10/nested-group.svg +1 -0
  121. package/test/pics/p10/no-edges.svg +1 -0
  122. package/test/pics/p10/title-overflow.svg +1 -0
  123. package/test/pics/p10/valign.svg +1 -0
  124. package/test/pics/p10//345/267/245/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  125. package/test/pics/p10//345/267/245/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.svg +1 -0
  126. package/test/pics/p10//345/276/256/346/234/215/345/213/231/345/217/242/351/233/206/346/236/266/346/247/213/345/234/226.png +0 -0
  127. package/test/pics/p10//345/276/256/346/234/215/345/213/231/345/217/242/351/233/206/346/236/266/346/247/213/345/234/226.svg +1 -0
  128. package/test/pics/p10//346/234/203/345/223/241/350/250/273/345/206/212/346/265/201/347/250/213/345/234/226.png +0 -0
  129. package/test/pics/p10//346/234/203/345/223/241/350/250/273/345/206/212/346/265/201/347/250/213/345/234/226.svg +1 -0
  130. package/test/pics/p10//346/250/241/347/265/204/347/265/204/346/210/220/345/234/226.png +0 -0
  131. package/test/pics/p10//346/250/241/347/265/204/347/265/204/346/210/220/345/234/226.svg +1 -0
  132. package/test/pics/p10//346/254/212/351/231/220/350/247/222/350/211/262/351/227/234/350/201/257/345/234/226.png +0 -0
  133. package/test/pics/p10//346/254/212/351/231/220/350/247/222/350/211/262/351/227/234/350/201/257/345/234/226.svg +1 -0
  134. package/test/pics/p10//347/211/251/350/201/257/347/266/262/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.png +0 -0
  135. package/test/pics/p10//347/211/251/350/201/257/347/266/262/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.svg +1 -0
  136. package/test/pics/p10//347/263/273/347/265/261/345/210/206/345/261/244/346/236/266/346/247/213/345/234/226.png +0 -0
  137. package/test/pics/p10//347/263/273/347/265/261/345/210/206/345/261/244/346/236/266/346/247/213/345/234/226.svg +1 -0
  138. package/test/pics/p10//350/250/202/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  139. package/test/pics/p10//350/250/202/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.svg +1 -0
  140. package/test/pics/p10//350/263/207/346/226/231/346/265/201/345/220/221/345/234/226.png +0 -0
  141. package/test/pics/p10//350/263/207/346/226/231/346/265/201/345/220/221/345/234/226.svg +1 -0
  142. package/test/pics/p10//350/263/207/346/226/231/346/270/205/346/264/227/346/265/201/347/250/213/345/234/226.png +0 -0
  143. package/test/pics/p10//350/263/207/346/226/231/346/270/205/346/264/227/346/265/201/347/250/213/345/234/226.svg +1 -0
  144. package/test/pics/p10//351/233/273/345/225/206/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.png +0 -0
  145. package/test/pics/p10//351/233/273/345/225/206/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.svg +1 -0
  146. package/test/pics/p10//351/240/201/351/235/242/345/260/216/350/246/275/351/227/234/350/201/257/345/234/226.png +0 -0
  147. package/test/pics/p10//351/240/201/351/235/242/345/260/216/350/246/275/351/227/234/350/201/257/345/234/226.svg +1 -0
  148. package/test/pics/p2/cycle.png +0 -0
  149. package/test/pics/p2/deep-nest.png +0 -0
  150. package/test/pics/p2/diamond.png +0 -0
  151. package/test/pics/p2/edges-mix.png +0 -0
  152. package/test/pics/p2/english.png +0 -0
  153. package/test/pics/p2/flat-tb.png +0 -0
  154. package/test/pics/p2/fold-tall.png +0 -0
  155. package/test/pics/p2/fold-wide.png +0 -0
  156. package/test/pics/p2/group-edge.png +0 -0
  157. package/test/pics/p2/items-node.png +0 -0
  158. package/test/pics/p2/long-label.png +0 -0
  159. package/test/pics/p2/lr-flat.png +0 -0
  160. package/test/pics/p2/nested-group.png +0 -0
  161. package/test/pics/p2/no-edges.png +0 -0
  162. package/test/pics/p2/title-overflow.png +0 -0
  163. package/test/pics/p2/valign.png +0 -0
  164. package/test/pics/p2//345/267/245/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  165. package/test/pics/p2//345/276/256/346/234/215/345/213/231/345/217/242/351/233/206/346/236/266/346/247/213/345/234/226.png +0 -0
  166. package/test/pics/p2//346/234/203/345/223/241/350/250/273/345/206/212/346/265/201/347/250/213/345/234/226.png +0 -0
  167. package/test/pics/p2//346/250/241/347/265/204/347/265/204/346/210/220/345/234/226.png +0 -0
  168. package/test/pics/p2//346/254/212/351/231/220/350/247/222/350/211/262/351/227/234/350/201/257/345/234/226.png +0 -0
  169. package/test/pics/p2//347/211/251/350/201/257/347/266/262/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.png +0 -0
  170. package/test/pics/p2//347/263/273/347/265/261/345/210/206/345/261/244/346/236/266/346/247/213/345/234/226.png +0 -0
  171. package/test/pics/p2//350/250/202/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  172. package/test/pics/p2//350/263/207/346/226/231/346/265/201/345/220/221/345/234/226.png +0 -0
  173. package/test/pics/p2//350/263/207/346/226/231/346/270/205/346/264/227/346/265/201/347/250/213/345/234/226.png +0 -0
  174. package/test/pics/p2//351/233/273/345/225/206/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.png +0 -0
  175. package/test/pics/p2//351/240/201/351/235/242/345/260/216/350/246/275/351/227/234/350/201/257/345/234/226.png +0 -0
  176. package/test/pics/p3/cycle.png +0 -0
  177. package/test/pics/p3/deep-nest.png +0 -0
  178. package/test/pics/p3/diamond.png +0 -0
  179. package/test/pics/p3/edges-mix.png +0 -0
  180. package/test/pics/p3/english.png +0 -0
  181. package/test/pics/p3/flat-tb.png +0 -0
  182. package/test/pics/p3/fold-tall.png +0 -0
  183. package/test/pics/p3/fold-wide.png +0 -0
  184. package/test/pics/p3/group-edge.png +0 -0
  185. package/test/pics/p3/items-node.png +0 -0
  186. package/test/pics/p3/long-label.png +0 -0
  187. package/test/pics/p3/lr-flat.png +0 -0
  188. package/test/pics/p3/nested-group.png +0 -0
  189. package/test/pics/p3/no-edges.png +0 -0
  190. package/test/pics/p3/title-overflow.png +0 -0
  191. package/test/pics/p3/valign.png +0 -0
  192. package/test/pics/p3//345/267/245/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  193. package/test/pics/p3//345/276/256/346/234/215/345/213/231/345/217/242/351/233/206/346/236/266/346/247/213/345/234/226.png +0 -0
  194. package/test/pics/p3//346/234/203/345/223/241/350/250/273/345/206/212/346/265/201/347/250/213/345/234/226.png +0 -0
  195. package/test/pics/p3//346/250/241/347/265/204/347/265/204/346/210/220/345/234/226.png +0 -0
  196. package/test/pics/p3//346/254/212/351/231/220/350/247/222/350/211/262/351/227/234/350/201/257/345/234/226.png +0 -0
  197. package/test/pics/p3//347/211/251/350/201/257/347/266/262/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.png +0 -0
  198. package/test/pics/p3//347/263/273/347/265/261/345/210/206/345/261/244/346/236/266/346/247/213/345/234/226.png +0 -0
  199. package/test/pics/p3//350/250/202/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  200. package/test/pics/p3//350/263/207/346/226/231/346/265/201/345/220/221/345/234/226.png +0 -0
  201. package/test/pics/p3//350/263/207/346/226/231/346/270/205/346/264/227/346/265/201/347/250/213/345/234/226.png +0 -0
  202. package/test/pics/p3//351/233/273/345/225/206/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.png +0 -0
  203. package/test/pics/p3//351/240/201/351/235/242/345/260/216/350/246/275/351/227/234/350/201/257/345/234/226.png +0 -0
  204. package/test/pics/p4/cycle.png +0 -0
  205. package/test/pics/p4/deep-nest.png +0 -0
  206. package/test/pics/p4/diamond.png +0 -0
  207. package/test/pics/p4/edges-mix.png +0 -0
  208. package/test/pics/p4/english.png +0 -0
  209. package/test/pics/p4/flat-tb.png +0 -0
  210. package/test/pics/p4/fold-tall.png +0 -0
  211. package/test/pics/p4/fold-wide.png +0 -0
  212. package/test/pics/p4/group-edge.png +0 -0
  213. package/test/pics/p4/items-node.png +0 -0
  214. package/test/pics/p4/long-label.png +0 -0
  215. package/test/pics/p4/lr-flat.png +0 -0
  216. package/test/pics/p4/nested-group.png +0 -0
  217. package/test/pics/p4/no-edges.png +0 -0
  218. package/test/pics/p4/title-overflow.png +0 -0
  219. package/test/pics/p4/valign.png +0 -0
  220. package/test/pics/p4//345/267/245/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  221. package/test/pics/p4//345/276/256/346/234/215/345/213/231/345/217/242/351/233/206/346/236/266/346/247/213/345/234/226.png +0 -0
  222. package/test/pics/p4//346/234/203/345/223/241/350/250/273/345/206/212/346/265/201/347/250/213/345/234/226.png +0 -0
  223. package/test/pics/p4//346/250/241/347/265/204/347/265/204/346/210/220/345/234/226.png +0 -0
  224. package/test/pics/p4//346/254/212/351/231/220/350/247/222/350/211/262/351/227/234/350/201/257/345/234/226.png +0 -0
  225. package/test/pics/p4//347/211/251/350/201/257/347/266/262/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.png +0 -0
  226. package/test/pics/p4//347/263/273/347/265/261/345/210/206/345/261/244/346/236/266/346/247/213/345/234/226.png +0 -0
  227. package/test/pics/p4//350/250/202/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  228. package/test/pics/p4//350/263/207/346/226/231/346/265/201/345/220/221/345/234/226.png +0 -0
  229. package/test/pics/p4//350/263/207/346/226/231/346/270/205/346/264/227/346/265/201/347/250/213/345/234/226.png +0 -0
  230. package/test/pics/p4//351/233/273/345/225/206/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.png +0 -0
  231. package/test/pics/p4//351/240/201/351/235/242/345/260/216/350/246/275/351/227/234/350/201/257/345/234/226.png +0 -0
  232. package/test/pics/p5/cycle.png +0 -0
  233. package/test/pics/p5/deep-nest.png +0 -0
  234. package/test/pics/p5/diamond.png +0 -0
  235. package/test/pics/p5/edges-mix.png +0 -0
  236. package/test/pics/p5/english.png +0 -0
  237. package/test/pics/p5/flat-tb.png +0 -0
  238. package/test/pics/p5/fold-tall.png +0 -0
  239. package/test/pics/p5/fold-wide.png +0 -0
  240. package/test/pics/p5/group-edge.png +0 -0
  241. package/test/pics/p5/items-node.png +0 -0
  242. package/test/pics/p5/long-label.png +0 -0
  243. package/test/pics/p5/lr-flat.png +0 -0
  244. package/test/pics/p5/nested-group.png +0 -0
  245. package/test/pics/p5/no-edges.png +0 -0
  246. package/test/pics/p5/title-overflow.png +0 -0
  247. package/test/pics/p5/valign.png +0 -0
  248. package/test/pics/p5//345/267/245/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  249. package/test/pics/p5//345/276/256/346/234/215/345/213/231/345/217/242/351/233/206/346/236/266/346/247/213/345/234/226.png +0 -0
  250. package/test/pics/p5//346/234/203/345/223/241/350/250/273/345/206/212/346/265/201/347/250/213/345/234/226.png +0 -0
  251. package/test/pics/p5//346/250/241/347/265/204/347/265/204/346/210/220/345/234/226.png +0 -0
  252. package/test/pics/p5//346/254/212/351/231/220/350/247/222/350/211/262/351/227/234/350/201/257/345/234/226.png +0 -0
  253. package/test/pics/p5//347/211/251/350/201/257/347/266/262/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.png +0 -0
  254. package/test/pics/p5//347/263/273/347/265/261/345/210/206/345/261/244/346/236/266/346/247/213/345/234/226.png +0 -0
  255. package/test/pics/p5//350/250/202/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  256. package/test/pics/p5//350/263/207/346/226/231/346/265/201/345/220/221/345/234/226.png +0 -0
  257. package/test/pics/p5//350/263/207/346/226/231/346/270/205/346/264/227/346/265/201/347/250/213/345/234/226.png +0 -0
  258. package/test/pics/p5//351/233/273/345/225/206/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.png +0 -0
  259. package/test/pics/p5//351/240/201/351/235/242/345/260/216/350/246/275/351/227/234/350/201/257/345/234/226.png +0 -0
  260. package/test/pics/p6/cycle.png +0 -0
  261. package/test/pics/p6/deep-nest.png +0 -0
  262. package/test/pics/p6/diamond.png +0 -0
  263. package/test/pics/p6/edges-mix.png +0 -0
  264. package/test/pics/p6/english.png +0 -0
  265. package/test/pics/p6/flat-tb.png +0 -0
  266. package/test/pics/p6/fold-tall.png +0 -0
  267. package/test/pics/p6/fold-wide.png +0 -0
  268. package/test/pics/p6/group-edge.png +0 -0
  269. package/test/pics/p6/items-node.png +0 -0
  270. package/test/pics/p6/long-label.png +0 -0
  271. package/test/pics/p6/lr-flat.png +0 -0
  272. package/test/pics/p6/nested-group.png +0 -0
  273. package/test/pics/p6/no-edges.png +0 -0
  274. package/test/pics/p6/title-overflow.png +0 -0
  275. package/test/pics/p6/valign.png +0 -0
  276. package/test/pics/p6//345/267/245/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  277. package/test/pics/p6//345/276/256/346/234/215/345/213/231/345/217/242/351/233/206/346/236/266/346/247/213/345/234/226.png +0 -0
  278. package/test/pics/p6//346/234/203/345/223/241/350/250/273/345/206/212/346/265/201/347/250/213/345/234/226.png +0 -0
  279. package/test/pics/p6//346/250/241/347/265/204/347/265/204/346/210/220/345/234/226.png +0 -0
  280. package/test/pics/p6//346/254/212/351/231/220/350/247/222/350/211/262/351/227/234/350/201/257/345/234/226.png +0 -0
  281. package/test/pics/p6//347/211/251/350/201/257/347/266/262/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.alt.png +0 -0
  282. package/test/pics/p6//347/211/251/350/201/257/347/266/262/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.new.png +0 -0
  283. package/test/pics/p6//347/211/251/350/201/257/347/266/262/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.png +0 -0
  284. package/test/pics/p6//347/263/273/347/265/261/345/210/206/345/261/244/346/236/266/346/247/213/345/234/226.png +0 -0
  285. package/test/pics/p6//350/250/202/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  286. package/test/pics/p6//350/263/207/346/226/231/346/265/201/345/220/221/345/234/226.png +0 -0
  287. package/test/pics/p6//350/263/207/346/226/231/346/270/205/346/264/227/346/265/201/347/250/213/345/234/226.png +0 -0
  288. package/test/pics/p6//351/233/273/345/225/206/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.png +0 -0
  289. package/test/pics/p6//351/240/201/351/235/242/345/260/216/350/246/275/351/227/234/350/201/257/345/234/226.png +0 -0
  290. package/test/pics/p7/cycle.png +0 -0
  291. package/test/pics/p7/deep-nest.png +0 -0
  292. package/test/pics/p7/diamond.png +0 -0
  293. package/test/pics/p7/edges-mix.png +0 -0
  294. package/test/pics/p7/english.png +0 -0
  295. package/test/pics/p7/flat-tb.png +0 -0
  296. package/test/pics/p7/fold-tall.png +0 -0
  297. package/test/pics/p7/fold-wide.png +0 -0
  298. package/test/pics/p7/group-edge.png +0 -0
  299. package/test/pics/p7/items-node.png +0 -0
  300. package/test/pics/p7/long-label.png +0 -0
  301. package/test/pics/p7/lr-flat.png +0 -0
  302. package/test/pics/p7/nested-group.png +0 -0
  303. package/test/pics/p7/no-edges.png +0 -0
  304. package/test/pics/p7/title-overflow.png +0 -0
  305. package/test/pics/p7/valign.png +0 -0
  306. package/test/pics/p7//345/267/245/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  307. package/test/pics/p7//345/276/256/346/234/215/345/213/231/345/217/242/351/233/206/346/236/266/346/247/213/345/234/226.png +0 -0
  308. package/test/pics/p7//346/234/203/345/223/241/350/250/273/345/206/212/346/265/201/347/250/213/345/234/226.png +0 -0
  309. package/test/pics/p7//346/250/241/347/265/204/347/265/204/346/210/220/345/234/226.png +0 -0
  310. package/test/pics/p7//346/254/212/351/231/220/350/247/222/350/211/262/351/227/234/350/201/257/345/234/226.png +0 -0
  311. package/test/pics/p7//347/211/251/350/201/257/347/266/262/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.png +0 -0
  312. package/test/pics/p7//347/263/273/347/265/261/345/210/206/345/261/244/346/236/266/346/247/213/345/234/226.png +0 -0
  313. package/test/pics/p7//350/250/202/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  314. package/test/pics/p7//350/263/207/346/226/231/346/265/201/345/220/221/345/234/226.png +0 -0
  315. package/test/pics/p7//350/263/207/346/226/231/346/270/205/346/264/227/346/265/201/347/250/213/345/234/226.png +0 -0
  316. package/test/pics/p7//351/233/273/345/225/206/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.png +0 -0
  317. package/test/pics/p7//351/240/201/351/235/242/345/260/216/350/246/275/351/227/234/350/201/257/345/234/226.png +0 -0
  318. package/test/pics/p8/cycle.png +0 -0
  319. package/test/pics/p8/deep-nest.png +0 -0
  320. package/test/pics/p8/diamond.png +0 -0
  321. package/test/pics/p8/edges-mix.png +0 -0
  322. package/test/pics/p8/english.png +0 -0
  323. package/test/pics/p8/flat-tb.png +0 -0
  324. package/test/pics/p8/fold-tall.png +0 -0
  325. package/test/pics/p8/fold-wide.png +0 -0
  326. package/test/pics/p8/group-edge.png +0 -0
  327. package/test/pics/p8/items-node.png +0 -0
  328. package/test/pics/p8/long-label.png +0 -0
  329. package/test/pics/p8/lr-flat.png +0 -0
  330. package/test/pics/p8/nested-group.png +0 -0
  331. package/test/pics/p8/no-edges.png +0 -0
  332. package/test/pics/p8/title-overflow.png +0 -0
  333. package/test/pics/p8/valign.png +0 -0
  334. package/test/pics/p8//345/267/245/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  335. package/test/pics/p8//345/276/256/346/234/215/345/213/231/345/217/242/351/233/206/346/236/266/346/247/213/345/234/226.png +0 -0
  336. package/test/pics/p8//346/234/203/345/223/241/350/250/273/345/206/212/346/265/201/347/250/213/345/234/226.png +0 -0
  337. package/test/pics/p8//346/250/241/347/265/204/347/265/204/346/210/220/345/234/226.png +0 -0
  338. package/test/pics/p8//346/254/212/351/231/220/350/247/222/350/211/262/351/227/234/350/201/257/345/234/226.png +0 -0
  339. package/test/pics/p8//347/211/251/350/201/257/347/266/262/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.png +0 -0
  340. package/test/pics/p8//347/263/273/347/265/261/345/210/206/345/261/244/346/236/266/346/247/213/345/234/226.png +0 -0
  341. package/test/pics/p8//350/250/202/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  342. package/test/pics/p8//350/263/207/346/226/231/346/265/201/345/220/221/345/234/226.png +0 -0
  343. package/test/pics/p8//350/263/207/346/226/231/346/270/205/346/264/227/346/265/201/347/250/213/345/234/226.png +0 -0
  344. package/test/pics/p8//351/233/273/345/225/206/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.png +0 -0
  345. package/test/pics/p8//351/240/201/351/235/242/345/260/216/350/246/275/351/227/234/350/201/257/345/234/226.png +0 -0
  346. package/test/pics/p9/cycle.png +0 -0
  347. package/test/pics/p9/deep-nest.png +0 -0
  348. package/test/pics/p9/diamond.png +0 -0
  349. package/test/pics/p9/edges-mix.png +0 -0
  350. package/test/pics/p9/english.png +0 -0
  351. package/test/pics/p9/flat-tb.png +0 -0
  352. package/test/pics/p9/fold-tall.png +0 -0
  353. package/test/pics/p9/fold-wide.png +0 -0
  354. package/test/pics/p9/group-edge.png +0 -0
  355. package/test/pics/p9/items-node.png +0 -0
  356. package/test/pics/p9/long-label.png +0 -0
  357. package/test/pics/p9/lr-flat.png +0 -0
  358. package/test/pics/p9/nested-group.png +0 -0
  359. package/test/pics/p9/no-edges.png +0 -0
  360. package/test/pics/p9/title-overflow.png +0 -0
  361. package/test/pics/p9/valign.png +0 -0
  362. package/test/pics/p9//345/267/245/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  363. package/test/pics/p9//345/276/256/346/234/215/345/213/231/345/217/242/351/233/206/346/236/266/346/247/213/345/234/226.png +0 -0
  364. package/test/pics/p9//346/234/203/345/223/241/350/250/273/345/206/212/346/265/201/347/250/213/345/234/226.png +0 -0
  365. package/test/pics/p9//346/250/241/347/265/204/347/265/204/346/210/220/345/234/226.png +0 -0
  366. package/test/pics/p9//346/254/212/351/231/220/350/247/222/350/211/262/351/227/234/350/201/257/345/234/226.png +0 -0
  367. package/test/pics/p9//347/211/251/350/201/257/347/266/262/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.png +0 -0
  368. package/test/pics/p9//347/263/273/347/265/261/345/210/206/345/261/244/346/236/266/346/247/213/345/234/226.png +0 -0
  369. package/test/pics/p9//350/250/202/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  370. package/test/pics/p9//350/263/207/346/226/231/346/265/201/345/220/221/345/234/226.png +0 -0
  371. package/test/pics/p9//350/263/207/346/226/231/346/270/205/346/264/227/346/265/201/347/250/213/345/234/226.png +0 -0
  372. package/test/pics/p9//351/233/273/345/225/206/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.png +0 -0
  373. package/test/pics/p9//351/240/201/351/235/242/345/260/216/350/246/275/351/227/234/350/201/257/345/234/226.png +0 -0
  374. package/test/snapshot.mjs +67 -0
  375. package/test/wflowchart.test.mjs +57 -11
  376. package/toolg/gDistRollup.mjs +3 -0
  377. package/test/data//344/275/234/346/245/255/346/265/201/347/250/213/345/234/226.json +0 -92
  378. package/test/data//345/211/215/347/253/257/346/236/266/346/247/213/350/210/207/350/263/207/346/226/231/346/265/201/345/234/226.json +0 -112
  379. package/test/data//345/276/214/347/253/257/346/234/215/345/213/231/347/265/204/346/210/220/345/234/226.json +0 -116
  380. package/test/data//346/211/271/346/254/241/344/270/212/345/202/263/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.json +0 -251
  381. package/test/data//347/250/213/345/274/217/345/205/203/344/273/266/347/265/204/346/210/220/345/234/226.json +0 -126
  382. package/test/data//347/250/213/345/274/217/345/210/206/345/261/244/346/236/266/346/247/213/345/234/226.json +0 -133
  383. package/test/data//347/263/273/347/265/261/346/236/266/346/247/213/345/234/226.json +0 -102
  384. package/test/data//347/263/273/347/265/261/351/227/234/350/201/257/345/234/226.json +0 -117
  385. package/test/data//347/267/232/344/270/212/347/225/253/351/235/242/351/227/234/350/201/257/345/234/226.json +0 -162
  386. package/test/pics/cycle.png +0 -0
  387. package/test/pics/cycle.svg +0 -1
  388. package/test/pics/deep-nest.svg +0 -1
  389. package/test/pics/diamond.png +0 -0
  390. package/test/pics/diamond.svg +0 -1
  391. package/test/pics/edges-mix.png +0 -0
  392. package/test/pics/edges-mix.svg +0 -1
  393. package/test/pics/english.png +0 -0
  394. package/test/pics/english.svg +0 -1
  395. package/test/pics/flat-tb.svg +0 -1
  396. package/test/pics/fold-tall.svg +0 -1
  397. package/test/pics/fold-wide.svg +0 -1
  398. package/test/pics/group-edge.png +0 -0
  399. package/test/pics/group-edge.svg +0 -1
  400. package/test/pics/items-node.svg +0 -1
  401. package/test/pics/long-label.svg +0 -1
  402. package/test/pics/lr-flat.svg +0 -1
  403. package/test/pics/nested-group.png +0 -0
  404. package/test/pics/nested-group.svg +0 -1
  405. package/test/pics/no-edges.svg +0 -1
  406. package/test/pics/title-overflow.svg +0 -1
  407. package/test/pics/valign.svg +0 -1
  408. package/test/pics//344/275/234/346/245/255/346/265/201/347/250/213/345/234/226.png +0 -0
  409. package/test/pics//344/275/234/346/245/255/346/265/201/347/250/213/345/234/226.svg +0 -1
  410. package/test/pics//345/211/215/347/253/257/346/236/266/346/247/213/350/210/207/350/263/207/346/226/231/346/265/201/345/234/226.png +0 -0
  411. package/test/pics//345/211/215/347/253/257/346/236/266/346/247/213/350/210/207/350/263/207/346/226/231/346/265/201/345/234/226.svg +0 -1
  412. package/test/pics//345/276/214/347/253/257/346/234/215/345/213/231/347/265/204/346/210/220/345/234/226.png +0 -0
  413. package/test/pics//345/276/214/347/253/257/346/234/215/345/213/231/347/265/204/346/210/220/345/234/226.svg +0 -1
  414. package/test/pics//346/211/271/346/254/241/344/270/212/345/202/263/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
  415. package/test/pics//346/211/271/346/254/241/344/270/212/345/202/263/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.svg +0 -1
  416. package/test/pics//347/250/213/345/274/217/345/205/203/344/273/266/347/265/204/346/210/220/345/234/226.png +0 -0
  417. package/test/pics//347/250/213/345/274/217/345/205/203/344/273/266/347/265/204/346/210/220/345/234/226.svg +0 -1
  418. package/test/pics//347/250/213/345/274/217/345/210/206/345/261/244/346/236/266/346/247/213/345/234/226.png +0 -0
  419. package/test/pics//347/250/213/345/274/217/345/210/206/345/261/244/346/236/266/346/247/213/345/234/226.svg +0 -1
  420. package/test/pics//347/263/273/347/265/261/346/236/266/346/247/213/345/234/226.png +0 -0
  421. package/test/pics//347/263/273/347/265/261/346/236/266/346/247/213/345/234/226.svg +0 -1
  422. package/test/pics//347/263/273/347/265/261/351/227/234/350/201/257/345/234/226.png +0 -0
  423. package/test/pics//347/263/273/347/265/261/351/227/234/350/201/257/345/234/226.svg +0 -1
  424. package/test/pics//347/267/232/344/270/212/347/225/253/351/235/242/351/227/234/350/201/257/345/234/226.png +0 -0
  425. package/test/pics//347/267/232/344/270/212/347/225/253/351/235/242/351/227/234/350/201/257/345/234/226.svg +0 -1
  426. /package/test/pics/{deep-nest.png → p10/deep-nest.png} +0 -0
  427. /package/test/pics/{flat-tb.png → p10/flat-tb.png} +0 -0
  428. /package/test/pics/{fold-tall.png → p10/fold-tall.png} +0 -0
  429. /package/test/pics/{fold-wide.png → p10/fold-wide.png} +0 -0
  430. /package/test/pics/{items-node.png → p10/items-node.png} +0 -0
  431. /package/test/pics/{long-label.png → p10/long-label.png} +0 -0
  432. /package/test/pics/{lr-flat.png → p10/lr-flat.png} +0 -0
  433. /package/test/pics/{no-edges.png → p10/no-edges.png} +0 -0
  434. /package/test/pics/{title-overflow.png → p10/title-overflow.png} +0 -0
  435. /package/test/pics/{valign.png → p10/valign.png} +0 -0
package/src/p10/gen.mjs CHANGED
@@ -54,7 +54,91 @@ const esc = s => String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>
54
54
  const mix = (a,b,t) => { const pa=[1,3,5].map(i=>parseInt(a.slice(i,i+2),16)), pb=[1,3,5].map(i=>parseInt(b.slice(i,i+2),16)); return '#'+pa.map((x,i)=>Math.round(x*(1-t)+pb[i]*t).toString(16).padStart(2,'0')).join('') }
55
55
  // 確保「起點後第一段」與「終點前最後一段」之直線長度 >= MIN(轉折不貼端點, 維持箭頭指向性)。
56
56
  // 做法: 末段過短時, 沿趨近軸向把「最後轉角」與「其前一轉角」一起後移(兩者同移=保正交); 需 >=4 點才不動端點。
57
- function ensureStub(pts, MIN){
57
+ // ── 菱形端點貼齊(ELK 以外接矩形算端點, 落在矩形邊上非頂點處會與菱形斜邊「空接」) ──
58
+ // 規則(共進共出, 每側獨立; 詳 p10/README):
59
+ // R1 共出: 該側所有「出」一律由該側「頂點」出發, 多條共用首段後於各自高度 90 度轉出(分岔)。
60
+ // R2 共進: 該側「無出」→ 全部匯入「頂點」(各自橫移至中軸, 共用末段, 箭頭重合);
61
+ // 該側「有出」→ 依「半側」匯入該半側「斜邊中點」(頂點讓給出, 進出不同點)。
62
+ // 正交線交叉允許存在(正交路由常態), 一致性優先。
63
+
64
+ // 判定端點落在外接矩形哪一側; 回 { ax, sg, v }(離開軸/正負/該側頂點), 不在邊上回 null
65
+ function diamondSide(p, box){
66
+ const cx = box.x + box.w/2, cy = box.y + box.h/2, EPS = 1.5
67
+ if (Math.abs(p.y - (box.y + box.h)) < EPS) return { ax: 'y', sg: 1, v: { x: cx, y: box.y + box.h } }
68
+ if (Math.abs(p.y - box.y) < EPS) return { ax: 'y', sg: -1, v: { x: cx, y: box.y } }
69
+ if (Math.abs(p.x - box.x) < EPS) return { ax: 'x', sg: -1, v: { x: box.x, y: cy } }
70
+ if (Math.abs(p.x - (box.x + box.w)) < EPS) return { ax: 'x', sg: 1, v: { x: box.x + box.w, y: cy } }
71
+ return null
72
+ }
73
+
74
+ // 該側某「半側」之斜邊中點(混合側的進匯點); half=+1 取座標較大半側, -1 反之
75
+ function diamondSlantMid(box, side, half){
76
+ const cx = box.x + box.w/2, cy = box.y + box.h/2, hw = box.w/2, hh = box.h/2
77
+ if (side.ax === 'y') return { x: cx + half * hw / 2, y: cy + side.sg * hh / 2 }
78
+ return { x: cx + side.sg * hw / 2, y: cy + half * hh / 2 }
79
+ }
80
+
81
+ // R1 共出: 出邊起點改自「頂點」出發
82
+ function routeOutFromVertex(pts, side){
83
+ const p0 = pts[0], p1 = pts[1]
84
+ const EPS = 1.5, J = 18
85
+ const v = side.v
86
+ const off = side.ax === 'y' ? Math.abs(p0.x - v.x) : Math.abs(p0.y - v.y)
87
+ if (off < EPS) return // 已在頂點(置中), 不動
88
+ const alongAxis = side.ax === 'y' ? Math.abs(p1.x - p0.x) < EPS : Math.abs(p1.y - p0.y) < EPS
89
+ if (!alongAxis) { pts.unshift(v); return } // 原首段即沿側邊橫走: 自頂點先橫走接回
90
+ if (pts.length > 2) {
91
+ // 有後續轉折: 頂點 → 沿軸行至原首個轉折高度 → 橫向接回(單一 L 型; 轉折可貼近頂點)
92
+ const q1 = side.ax === 'y' ? { x: v.x, y: p1.y } : { x: p1.x, y: v.y }
93
+ pts.splice(0, 1, v, q1)
94
+ return
95
+ }
96
+ // 無轉折之直線邊: 於頂點外側作雙直角接回各自路線(距離取 J 與可用長度之小者)
97
+ const segLen = side.ax === 'y' ? (p1.y - p0.y) * side.sg : (p1.x - p0.x) * side.sg
98
+ const j = Math.min(J, Math.max(4, segLen / 2))
99
+ const q1 = side.ax === 'y' ? { x: v.x, y: v.y + side.sg * j } : { x: v.x + side.sg * j, y: v.y }
100
+ const q2 = side.ax === 'y' ? { x: p0.x, y: v.y + side.sg * j } : { x: v.x + side.sg * j, y: p0.y }
101
+ pts.splice(0, 1, v, q1, q2)
102
+ }
103
+
104
+ // R2 決策: 進匯點 = 該側無出 → 頂點; 有出 → 依端點所在半側之斜邊中點
105
+ function diamondInAnchor(box, side, hasOutOnSide, pE){
106
+ if(!hasOutOnSide) return { x: side.v.x, y: side.v.y }
107
+ const half = (side.ax === 'y' ? (pE.x - (box.x + box.w/2)) : (pE.y - (box.y + box.h/2))) >= 0 ? 1 : -1
108
+ return diamondSlantMid(box, side, half)
109
+ }
110
+
111
+ // R2 共進: 進邊終點改匯入 anchor(頂點或半側斜邊中點)
112
+ // 橫向差距小且末段前為可伸縮橫向段 → 整段末端直線「平移」對齊 anchor 軸線(零多餘轉折);
113
+ // 否則於側外 J 處橫移 bus 再沿軸進入 anchor。
114
+ function routeInToAnchor(pts, side, anchor){
115
+ const pE = pts[pts.length - 1], pP = pts[pts.length - 2]
116
+ const EPS = 1.5, J = 18, SLIDE = 40 // SLIDE: 可平移之最大橫向差距(過大恐撞鄰近元素, 改走 bus)
117
+ const alongAxis = side.ax === 'y' ? Math.abs(pP.x - pE.x) < EPS : Math.abs(pP.y - pE.y) < EPS
118
+ if (!alongAxis) return // 末段非沿進入軸(罕見), 保留原路由
119
+ const lateral = side.ax === 'y' ? Math.abs(pE.x - anchor.x) : Math.abs(pE.y - anchor.y)
120
+ if (lateral < EPS) {
121
+ pts[pts.length - 1] = { x: anchor.x, y: anchor.y } // 已在 anchor 軸線上: 直接沿軸進入
122
+ return
123
+ }
124
+ if (lateral <= SLIDE && pts.length >= 3) {
125
+ const pP2 = pts[pts.length - 3]
126
+ const prevHorizontal = side.ax === 'y' ? Math.abs(pP2.y - pP.y) < EPS : Math.abs(pP2.x - pP.x) < EPS
127
+ if (prevHorizontal) {
128
+ if (side.ax === 'y') pP.x = anchor.x; else pP.y = anchor.y
129
+ pts[pts.length - 1] = { x: anchor.x, y: anchor.y }
130
+ return
131
+ }
132
+ }
133
+ const Lv = side.ax === 'y' ? side.v.y + side.sg * J : side.v.x + side.sg * J // 側外橫移層
134
+ const segOK = side.ax === 'y' ? (pP.y - Lv) * side.sg > 2 : (pP.x - Lv) * side.sg > 2 // 原末段須延伸到橫移層之外
135
+ if (!segOK) { pts[pts.length - 1] = { x: anchor.x, y: anchor.y }; return } // 空間不足: 直接斜線前最短接入(極罕見)
136
+ const b1 = side.ax === 'y' ? { x: pE.x, y: Lv } : { x: Lv, y: pE.y }
137
+ const b2 = side.ax === 'y' ? { x: anchor.x, y: Lv } : { x: Lv, y: anchor.y }
138
+ pts.splice(pts.length - 1, 1, b1, b2, { x: anchor.x, y: anchor.y })
139
+ }
140
+
141
+ function ensureStub(pts, MIN, skipStart){
58
142
  function fixEnd(p){
59
143
  const n=p.length; if(n<4) return
60
144
  const C=p[n-1], B=p[n-2]
@@ -64,11 +148,16 @@ function ensureStub(pts, MIN){
64
148
  p[n-2]={x:B.x-ux*need, y:B.y-uy*need}
65
149
  p[n-3]={x:p[n-3].x-ux*need, y:p[n-3].y-uy*need}
66
150
  }
151
+ // 終點端(箭頭側)一律套最低門檻——含菱形貼齊後之路徑(bus 支線本給滿 J; 平移/保留支線之轉角平移不破壞正交)
67
152
  fixEnd(pts)
68
- pts.reverse(); fixEnd(pts); pts.reverse()
153
+ // 起點端僅於「菱形頂點分岔」時跳過(分岔轉折貼近頂點屬預期; 後移轉角會與其後路徑段脫勾扭出斜線)
154
+ if(!skipStart){ pts.reverse(); fixEnd(pts); pts.reverse() }
69
155
  return pts
70
156
  }
71
157
 
158
+ // 供測試直接驗證菱形端點規則之純函式(不影響渲染)
159
+ window.__dia = { diamondSide, diamondSlantMid, diamondInAnchor, routeOutFromVertex, routeInToAnchor }
160
+
72
161
  window.renderFig = async function(spec){
73
162
  try {
74
163
  const dir = spec.dir==='LR' ? 'RIGHT' : 'DOWN'
@@ -92,7 +181,7 @@ window.renderFig = async function(spec){
92
181
  // 例外: 含循環(如流程之退回邊)的圖, 位置提示會干擾分層使入口節點偏離頂端 → 根層不加提示(容器內仍保序)。
93
182
  const ORDER_OPTS = { 'elk.layered.crossingMinimization.semiInteractive':'true' }
94
183
  const hasCycle = (() => { const adj={}; spec.edges.forEach(e=>{ (adj[e.from]=adj[e.from]||[]).push(e.to) }); const st={}; const dfs=(u)=>{ st[u]=1; for(const v of (adj[u]||[])){ if(st[v]===1) return true; if(!st[v] && dfs(v)) return true } st[u]=2; return false }; return Object.keys(adj).some(u=>!st[u]&&dfs(u)) })()
95
- const gOpts = { 'elk.padding':'[top=10,left=14,bottom=14,right=14]', 'elk.nodeLabels.placement':'[H_CENTER, V_TOP, INSIDE]', 'elk.algorithm':'layered', 'elk.direction':dir, 'elk.edgeRouting':'ORTHOGONAL', 'elk.hierarchyHandling':'INCLUDE_CHILDREN', 'elk.spacing.nodeNode':'28', 'elk.layered.spacing.nodeNodeBetweenLayers':'30', 'elk.spacing.edgeEdge':'22', 'elk.layered.spacing.edgeEdgeBetweenLayers':'24', ...ORDER_OPTS }
184
+ const gOpts = { 'elk.layered.nodePlacement.bk.fixedAlignment':'BALANCED', 'elk.padding':'[top=10,left=14,bottom=14,right=14]', 'elk.nodeLabels.placement':'[H_CENTER, V_TOP, INSIDE]', 'elk.algorithm':'layered', 'elk.direction':dir, 'elk.edgeRouting':'ORTHOGONAL', 'elk.hierarchyHandling':'INCLUDE_CHILDREN', 'elk.spacing.nodeNode':'28', 'elk.layered.spacing.nodeNodeBetweenLayers':'30', 'elk.spacing.edgeEdge':'22', 'elk.layered.spacing.edgeEdgeBetweenLayers':'24', ...ORDER_OPTS }
96
185
  function elkEdge(e){ const lab = e.label ? [{ text:e.label, width: measure(e.label,EFS,300).w, height:18 }] : []; return { id:e.id, sources:[e.from], targets:[e.to], labels:lab } }
97
186
  function build(parentId, padMap, wrapAR){
98
187
  const children=[], edges=[]
@@ -108,7 +197,7 @@ window.renderFig = async function(spec){
108
197
  spec.edges.filter(e=>e.container===parentId).forEach(e=>{ edges.push(elkEdge(e)) })
109
198
  return { children, edges }
110
199
  }
111
- const rootOpts = { 'elk.algorithm':'layered', 'elk.direction':dir, 'elk.edgeRouting':'ORTHOGONAL', 'elk.hierarchyHandling':'INCLUDE_CHILDREN', 'elk.spacing.nodeNode':'30', 'elk.layered.spacing.nodeNodeBetweenLayers':'34', 'elk.spacing.edgeNode':'24', 'elk.spacing.edgeEdge':'22', 'elk.layered.spacing.edgeEdgeBetweenLayers':'24', 'elk.layered.spacing.edgeNodeBetweenLayers':'26', ...(hasCycle ? { 'elk.layered.cycleBreaking.strategy':'MODEL_ORDER' } : ORDER_OPTS) }
200
+ const rootOpts = { 'elk.layered.nodePlacement.bk.fixedAlignment':'BALANCED', 'elk.algorithm':'layered', 'elk.direction':dir, 'elk.edgeRouting':'ORTHOGONAL', 'elk.hierarchyHandling':'INCLUDE_CHILDREN', 'elk.spacing.nodeNode':'30', 'elk.layered.spacing.nodeNodeBetweenLayers':'34', 'elk.spacing.edgeNode':'24', 'elk.spacing.edgeEdge':'22', 'elk.layered.spacing.edgeEdgeBetweenLayers':'24', 'elk.layered.spacing.edgeNodeBetweenLayers':'26', ...(hasCycle ? { 'elk.layered.cycleBreaking.strategy':'MODEL_ORDER' } : ORDER_OPTS) }
112
201
  const elk = new ELK()
113
202
  const mkGraph = (pm, war) => { const rb = build(null, pm, war); return { id:'root', layoutOptions:rootOpts, children:rb.children, edges:rb.edges } }
114
203
  let res = await elk.layout(mkGraph({}, {}))
@@ -151,14 +240,121 @@ window.renderFig = async function(spec){
151
240
  groupsOut.sort((a,b)=>a.depth-b.depth).forEach(o=>{
152
241
  svg+='<rect x="'+(o.x)+'" y="'+(o.y)+'" width="'+o.w+'" height="'+o.h+'" rx="8" fill="'+o.g.fill+'" fill-opacity="0.38" stroke="'+o.g.stroke+'" stroke-width="2"/>'
153
242
  const tw=o.lbl?o.lbl.width+12:o.w, th=o.lbl?o.lbl.height+6:24, tx=o.x+Math.max(0,(o.w-tw)/2), ty=o.y+(o.lbl?o.lbl.y:5)
154
- svg+='<foreignObject x="'+tx+'" y="'+ty+'" width="'+tw+'" height="'+th+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="text-align:center;font-weight:bold;font-size:'+GFS+'px;color:'+o.g.stroke+';line-height:1.3">'+esc(o.g.label)+'</div></foreignObject>'
243
+ svg+='<foreignObject x="'+tx+'" y="'+ty+'" width="'+tw+'" height="'+th+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="text-align:center;font-weight:bold;font-size:'+GFS+'px;color:'+o.g.stroke+';line-height:1.3;overflow-wrap:break-word;word-break:normal">'+esc(o.g.label)+'</div></foreignObject>'
155
244
  })
156
245
  // 2) 邊(在節點之下、容器之上)
246
+ const nodeBoxById={}; nodesOut.forEach(o=>{ nodeBoxById[o.n.id]={ x:o.x, y:o.y, w:o.w, h:o.h, diamond:!!o.n.diamond } })
247
+ const specEdgeById={}; spec.edges.forEach(se=>{ specEdgeById[se.id]=se })
248
+ // 2a) 蒐集各 section 絕對路徑點 + 統計各菱形「各側」進出數(側=上/下/左/右, key: nodeId|軸|正負)。
249
+ // 規則: 純出側→全部由頂點分岔; 純入側→各自貼斜邊; 混合側→全部貼斜邊
250
+ // (混合側若連出走頂點+橫向接回, 橫向段必與夾在中間的連入直線交叉; 全貼斜邊則零共用點、零新增交叉)
251
+ const drawList=[], sideMix={}
157
252
  edgesOut.forEach(o=>{
158
- const e=o.e
159
- ;(e.sections||[]).forEach(sec=>{
253
+ const e=o.e, se=specEdgeById[e.id]
254
+ const srcBox=se&&nodeBoxById[se.from], tgtBox=se&&nodeBoxById[se.to]
255
+ const secs=(e.sections||[]).map(sec=>{
160
256
  const pts=[sec.startPoint].concat(sec.bendPoints||[]).concat([sec.endPoint]).map(p=>({x:o.ox+p.x, y:o.oy+p.y}))
161
- ensureStub(pts, 18)
257
+ const item={ pts }
258
+ if(srcBox&&srcBox.diamond){ item.srcSide=diamondSide(pts[0], srcBox); if(item.srcSide){ const k=se.from+'|'+item.srcSide.ax+item.srcSide.sg; (sideMix[k]=sideMix[k]||{in:0,out:0}).out++ } }
259
+ if(tgtBox&&tgtBox.diamond){ item.tgtSide=diamondSide(pts[pts.length-1], tgtBox); if(item.tgtSide){ const k=se.to+'|'+item.tgtSide.ax+item.tgtSide.sg; (sideMix[k]=sideMix[k]||{in:0,out:0}).in++ } }
260
+ return item
261
+ })
262
+ drawList.push({ o, e, se, srcBox, tgtBox, secs })
263
+ })
264
+ // 2a-2) 通用路徑化簡: ELK 階層邊(INCLUDE_CHILDREN)常見「先繞反方向再折返」之 S/U 形繞行,
265
+ // 反覆對「三段式窗」(平行-垂直-平行)嘗試收斂成兩段, 僅在碰撞偵測全過才接受:
266
+ // 不撞節點框/容器標題帶/邊標籤、不與他邊平行段貼齊併線; 端點段軸向與方向不可翻轉(維持節點進出方向,
267
+ // 化簡先於菱形貼齊故不干擾貼齊規則); 本邊標籤若因化簡脫離路徑則整段還原(標籤座標由 ELK 依原路徑決定)。
268
+ const obstacles=[]
269
+ nodesOut.forEach(o=>{ obstacles.push({ x:o.x-8, y:o.y-8, w:o.w+16, h:o.h+16 }) })
270
+ groupsOut.forEach(o=>{ if(!o.lbl) return; const tw=o.lbl.width+12, th=o.lbl.height+6, tx=o.x+Math.max(0,(o.w-tw)/2), ty=o.y+o.lbl.y; obstacles.push({ x:tx-4, y:ty-4, w:tw+8, h:th+8 }) })
271
+ const labelBoxes=[]
272
+ drawList.forEach(dd=>{ (dd.e.labels||[]).forEach(l=>{ if(!l.text) return; labelBoxes.push({ x:dd.o.ox+(l.x||0)-2, y:dd.o.oy+(l.y||0)-2, w:(l.width||40)+10, h:(l.height||18)+8 }) }) })
273
+ function segHitsBox(a,b,box){ // 軸對齊線段 vs 矩形(線段必水平或垂直)
274
+ const x1=Math.min(a.x,b.x), x2=Math.max(a.x,b.x), y1=Math.min(a.y,b.y), y2=Math.max(a.y,b.y)
275
+ return x2>box.x && x1<box.x+box.w && y2>box.y && y1<box.y+box.h
276
+ }
277
+ function segOverlapsOther(a,b,selfItem){ // 與他邊平行段貼齊重疊(側距<6 且共線範圍>10)→ 視覺併線, 拒絕
278
+ const horiz = Math.abs(a.y-b.y)<0.5
279
+ for(const dd of drawList) for(const it of dd.secs){ if(it===selfItem) continue
280
+ const q=it.pts
281
+ for(let k=0;k+1<q.length;k++){ const c=q[k], d=q[k+1]
282
+ if(horiz && Math.abs(c.y-d.y)<0.5 && Math.abs(c.y-a.y)<6){ const lo=Math.max(Math.min(a.x,b.x),Math.min(c.x,d.x)), hi=Math.min(Math.max(a.x,b.x),Math.max(c.x,d.x)); if(hi-lo>10) return true }
283
+ if(!horiz && Math.abs(c.x-d.x)<0.5 && Math.abs(c.x-a.x)<6){ const lo=Math.max(Math.min(a.y,b.y),Math.min(c.y,d.y)), hi=Math.min(Math.max(a.y,b.y),Math.max(c.y,d.y)); if(hi-lo>10) return true }
284
+ }
285
+ }
286
+ return false
287
+ }
288
+ // 標籤一律為硬障礙(含本邊自己的): ELK 邊標籤是「先佔位、路徑繞標籤走」, 原路的 jog 即標籤讓位,
289
+ // 拉直會從自家標籤正中穿過(且「脫離才還原」守門對「穿過=距離變近」無感), 故不可豁免自家標籤
290
+ function segClear(a,b,selfItem){
291
+ for(const ob of obstacles) if(segHitsBox(a,b,ob)) return false
292
+ for(const lb of labelBoxes) if(segHitsBox(a,b,lb)) return false
293
+ return !segOverlapsOther(a,b,selfItem)
294
+ }
295
+ function mergeColl(p){ // 去重合點 + 併共線(含同線折返, 折返僅由化簡產生)
296
+ for(let i=p.length-2;i>=0;i--) if(Math.abs(p[i].x-p[i+1].x)<0.5 && Math.abs(p[i].y-p[i+1].y)<0.5) p.splice(i,1)
297
+ for(let i=p.length-2;i>=1;i--){ const A=p[i-1],B=p[i],C=p[i+1]
298
+ if((Math.abs(A.x-B.x)<0.5 && Math.abs(B.x-C.x)<0.5) || (Math.abs(A.y-B.y)<0.5 && Math.abs(B.y-C.y)<0.5)) p.splice(i,1) }
299
+ }
300
+ function distToPoly(pt,p){ let d=Infinity
301
+ for(let i=0;i+1<p.length;i++){ const a=p[i], b=p[i+1]
302
+ const len2=Math.max(1,(b.x-a.x)*(b.x-a.x)+(b.y-a.y)*(b.y-a.y))
303
+ const t=Math.max(0,Math.min(1, ((pt.x-a.x)*(b.x-a.x)+(pt.y-a.y)*(b.y-a.y))/len2 ))
304
+ const qx=a.x+t*(b.x-a.x), qy=a.y+t*(b.y-a.y); d=Math.min(d, Math.hypot(pt.x-qx, pt.y-qy)) }
305
+ return d
306
+ }
307
+ function simplifyOrtho(item, labels){
308
+ const p=item.pts
309
+ const before=p.map(q=>({x:q.x,y:q.y}))
310
+ mergeColl(p)
311
+ for(let pass=0; pass<20; pass++){
312
+ let changed=false
313
+ for(let i=0;i+3<p.length;i++){
314
+ const isStart=(i===0), isEnd=(i+3===p.length-1)
315
+ if(isStart && isEnd) continue // 4 點路徑已極簡, 兩端軸向無法同時保持
316
+ const A=p[i], B=p[i+1], C=p[i+2], D=p[i+3]
317
+ const s1h=Math.abs(A.y-B.y)<0.5
318
+ // 取代點取窗之一角: 一般保持首段軸向; 末端窗改保持末段軸向(箭頭進入方向不可變)
319
+ const P = isEnd ? (s1h ? {x:A.x,y:D.y} : {x:D.x,y:A.y}) : (s1h ? {x:D.x,y:A.y} : {x:A.x,y:D.y})
320
+ if(isStart){ const sg0=s1h?Math.sign(B.x-A.x):Math.sign(B.y-A.y), sgN=s1h?Math.sign(P.x-A.x):Math.sign(P.y-A.y); if(sgN!==sg0) continue }
321
+ if(isEnd){ const s3h=Math.abs(C.y-D.y)<0.5, sg0=s3h?Math.sign(D.x-C.x):Math.sign(D.y-C.y), sgN=s3h?Math.sign(D.x-P.x):Math.sign(D.y-P.y); if(sgN!==sg0) continue }
322
+ if(!segClear(A,P,item) || !segClear(P,D,item)) continue
323
+ p.splice(i+1, 2, P)
324
+ mergeColl(p)
325
+ changed=true
326
+ break
327
+ }
328
+ if(!changed) break
329
+ }
330
+ // 標籤守門: 化簡若令本邊任一標籤脫離路徑(距離顯著變大), 整段還原
331
+ if(labels.length && p.length !== before.length){
332
+ for(const l of labels){
333
+ if(distToPoly(l,p) > distToPoly(l,before)+2){ p.length=0; before.forEach(q=>p.push(q)); return }
334
+ }
335
+ }
336
+ }
337
+ drawList.forEach(dd=>{
338
+ const labs=(dd.e.labels||[]).filter(l=>l.text).map(l=>({ x:dd.o.ox+(l.x||0)+(l.width||40)/2, y:dd.o.oy+(l.y||0)+(l.height||18)/2 }))
339
+ dd.secs.forEach(item=>{ if(item.pts.length>=5) simplifyOrtho(item, labs) })
340
+ })
341
+ // 2b) 依側別規則貼齊並繪製
342
+ const procSegsById={} // 實際繪製之路徑點(含菱形貼齊與 ensureStub), 供 geom 輸出一致
343
+ drawList.forEach(dd=>{
344
+ const o=dd.o, e=dd.e
345
+ dd.secs.forEach(item=>{
346
+ const pts=item.pts
347
+ if(item.srcSide && pts.length >= 2) routeOutFromVertex(pts, item.srcSide) // R1: 出永遠自該側頂點
348
+ if(item.tgtSide && pts.length >= 2){
349
+ // R2: 純進側匯「頂點」; 混合側依「半側」匯「斜邊中點」(進出不同點)
350
+ const m=sideMix[dd.se.to+'|'+item.tgtSide.ax+item.tgtSide.sg]
351
+ const anchor=diamondInAnchor(dd.tgtBox, item.tgtSide, !!(m && m.out > 0), pts[pts.length-1])
352
+ routeInToAnchor(pts, item.tgtSide, anchor)
353
+ }
354
+ // 分岔/匯入重排後清除共線折返: 頂點分岔之「接回原路第一轉折層」若跨過下一轉點, 會多走再折回疊出短勾, 併共線即塌縮為直接連線
355
+ if(item.srcSide || item.tgtSide) mergeColl(pts)
356
+ ensureStub(pts, 18, !!item.srcSide)
357
+ ;(procSegsById[e.id]=procSegsById[e.id]||[]).push(pts)
162
358
  let d='M '+pts[0].x+' '+pts[0].y; for(let i=1;i<pts.length;i++) d+=' L '+pts[i].x+' '+pts[i].y
163
359
  const dash = (e.id && spec.edges.find(x=>x.id===e.id) && spec.edges.find(x=>x.id===e.id).dashed) ? ' stroke-dasharray="6 4"' : ''
164
360
  svg+='<path d="'+d+'" fill="none" stroke="'+EDGE_LINE+'" stroke-width="2"'+dash+'/>'
@@ -172,7 +368,8 @@ window.renderFig = async function(spec){
172
368
  ;(e.labels||[]).forEach(l=>{ if(!l.text) return; const lx=o.ox+(l.x||0), ly=o.oy+(l.y||0), lw=l.width||40, lh=l.height||18
173
369
  svg+='<foreignObject x="'+lx+'" y="'+ly+'" width="'+(lw+6)+'" height="'+(lh+4)+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:'+EFS+'px;color:'+EDGE_TEXT+';text-shadow:0 0 3px '+HALO+',0 0 3px '+HALO+',0 0 3px '+HALO+',0 0 3px '+HALO+';white-space:nowrap">'+esc(l.text)+'</div></foreignObject>' })
174
370
  })
175
- // 3) 節點(最上)
371
+ // 3) 節點(最上)。各文字 div 之斷行 CSS(overflow-wrap:break-word + word-break:normal)須與 measure()/measureItems() 一致,
372
+ // 否則長英文識別字量測時折行、渲染時不折 → 單行溢出 foreignObject 被裁切
176
373
  nodesOut.forEach(o=>{
177
374
  const n=o.n
178
375
  if(n.items && n.items.length){ // 標題+項目框: 依序分層(框背景→標題背景→框線→分隔線→標題→items), 使框線置頂不被標題背景遮蔽、線寬完整
@@ -182,14 +379,14 @@ window.renderFig = async function(spec){
182
379
  svg+='<path d="M '+(o.x+r)+' '+o.y+' H '+(o.x+o.w-r)+' A '+r+' '+r+' 0 0 1 '+(o.x+o.w)+' '+(o.y+r)+' V '+sep+' H '+o.x+' V '+(o.y+r)+' A '+r+' '+r+' 0 0 1 '+(o.x+r)+' '+o.y+' Z" fill="'+mix(n.fill,n.stroke,0.16)+'"/>' // 2 標題背景(上圓角下平)
183
380
  svg+='<rect x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'" rx="'+r+'" fill="none" stroke="'+n.stroke+'" stroke-width="1.6"/>' // 3 框線(頂層)
184
381
  svg+='<line x1="'+o.x+'" y1="'+sep+'" x2="'+(o.x+o.w)+'" y2="'+sep+'" stroke="'+n.stroke+'" stroke-width="1"/>' // 4 分隔線
185
- svg+='<foreignObject x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+headH+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-weight:bold;font-size:'+FS+'px;color:'+n.font+'">'+esc(n.title||n.label)+'</div></foreignObject>' // 5 標題文字
382
+ svg+='<foreignObject x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+headH+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-weight:bold;font-size:'+FS+'px;color:'+n.font+'"><div style="width:100%;min-width:0;overflow-wrap:break-word;word-break:normal">'+esc(n.title||n.label)+'</div></div></foreignObject>' // 5 標題文字
186
383
  svg+='<foreignObject x="'+o.x+'" y="'+sep+'" width="'+o.w+'" height="'+(o.h-headH)+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:'+(FS-1)+'px;line-height:1.55;padding:6px 9px 4px;text-align:left;color:'+n.font+'">'+lis+'</div></foreignObject>' // 6 items文字
187
384
  } else if(n.diamond){ const cx=o.x+o.w/2, cy=o.y+o.h/2
188
385
  svg+='<polygon points="'+cx+','+o.y+' '+(o.x+o.w)+','+cy+' '+cx+','+(o.y+o.h)+' '+o.x+','+cy+'" fill="'+n.fill+'" stroke="'+n.stroke+'" stroke-width="1.8"/>'
189
- svg+='<foreignObject x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:'+FS+'px;color:'+n.font+';line-height:1.4;padding:0 4px;box-sizing:border-box">'+esc(n.label)+'</div></foreignObject>'
386
+ svg+='<foreignObject x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:'+FS+'px;color:'+n.font+';line-height:1.4;padding:0 4px;box-sizing:border-box"><div style="width:100%;min-width:0;overflow-wrap:break-word;word-break:normal">'+esc(n.label)+'</div></div></foreignObject>'
190
387
  } else {
191
388
  svg+='<rect x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'" rx="7" fill="'+n.fill+'" stroke="'+n.stroke+'" stroke-width="1.6"/>'
192
- svg+='<foreignObject x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:'+FS+'px;color:'+n.font+';line-height:1.4;padding:0 4px;box-sizing:border-box">'+esc(n.label)+'</div></foreignObject>'
389
+ svg+='<foreignObject x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:'+FS+'px;color:'+n.font+';line-height:1.4;padding:0 4px;box-sizing:border-box"><div style="width:100%;min-width:0;overflow-wrap:break-word;word-break:normal">'+esc(n.label)+'</div></div></foreignObject>'
193
390
  }
194
391
  })
195
392
 
@@ -200,7 +397,7 @@ window.renderFig = async function(spec){
200
397
  W:W+PAD*2, H:H+PAD*2,
201
398
  nodes: nodesOut.map(o=>({ id:o.n.id, x:o.x, y:o.y, w:o.w, h:o.h, parent:o.n.parent||null, kind:(o.n.items&&o.n.items.length)?'items':(o.n.diamond?'diamond':'box'), headH:(size[o.n.id]&&size[o.n.id].headH)||null })),
202
399
  groups: groupsOut.map(o=>({ id:o.g.id, x:o.x, y:o.y, w:o.w, h:o.h, depth:o.depth, parent:o.g.parent||null, labelW:(o.lbl&&o.lbl.width)||0 })),
203
- edges: edgesOut.map(o=>({ id:o.e.id, segs:(o.e.sections||[]).map(sec=>[sec.startPoint].concat(sec.bendPoints||[]).concat([sec.endPoint]).map(p=>({x:o.ox+p.x,y:o.oy+p.y}))) }))
400
+ edges: edgesOut.map(o=>({ id:o.e.id, segs: procSegsById[o.e.id]||[] }))
204
401
  }
205
402
  return { ok:true, w:W+PAD*2, h:H+PAD*2, geom }
206
403
  } catch(e){ return { ok:false, err:String(e&&(e.stack||e.message)||e).slice(0,400) } }
@@ -208,8 +405,8 @@ window.renderFig = async function(spec){
208
405
  window.__ready = true
209
406
  </script></body></html>`
210
407
 
211
- // 單張渲染 PNG Buffer(供 WFlowchart 統一入口呼叫; 自帶瀏覽器生命週期)
212
- export async function genPng(data, opt = {}) {
408
+ // 渲染單張至就緒頁面(renderFig 完成 + 字型就緒); caller 負責 close browser
409
+ async function renderFigPage(data) {
213
410
  const browser = await chromium.launch()
214
411
  try {
215
412
  const page = await browser.newPage({ deviceScaleFactor: 2 })
@@ -220,9 +417,32 @@ export async function genPng(data, opt = {}) {
220
417
  if (!res.ok) throw new Error('p10 render failed: ' + res.err)
221
418
  await page.evaluate(() => document.fonts && document.fonts.ready)
222
419
  await page.waitForTimeout(150)
420
+ return { browser, page }
421
+ }
422
+ catch (err) {
423
+ await browser.close()
424
+ throw err
425
+ }
426
+ }
427
+
428
+ // 單張渲染 → PNG Buffer(供 WFlowchart 統一入口呼叫; 自帶瀏覽器生命週期)
429
+ export async function genPng(data, opt = {}) {
430
+ const { browser, page } = await renderFigPage(data)
431
+ try {
223
432
  return await page.locator('#stage svg').screenshot()
224
433
  }
225
434
  finally {
226
435
  await browser.close()
227
436
  }
228
437
  }
438
+
439
+ // 單張渲染 → SVG 字串(本產線自組之 SVG; &nbsp; 正規化為 &#160; 使其為合法 standalone SVG)
440
+ export async function genSvg(data, opt = {}) {
441
+ const { browser, page } = await renderFigPage(data)
442
+ try {
443
+ return (await page.evaluate(() => document.getElementById('stage').innerHTML)).replace(/&nbsp;/g, '&#160;').trim()
444
+ }
445
+ finally {
446
+ await browser.close()
447
+ }
448
+ }
package/src/p2/README.md CHANGED
@@ -52,7 +52,7 @@
52
52
 
53
53
  ### 對外介面
54
54
 
55
- - 產線唯一輸出入口為 `genPng(data, opt)`,`data` 為正規化繪圖數據 `{ dir, nodes, edges }`,回傳 `Promise<Buffer>`(PNG);`genPng` 自帶瀏覽器生命週期(`chromium.launch()` → 渲染 → `browser.close()`),呼叫端不需自行管理 Playwright。
55
+ - 產線對外介面為 `genPng(data, opt)` 與 `genSvg(data, opt)`,`data` 為正規化繪圖數據 `{ dir, nodes, edges }`;`genPng` 回傳 `Promise<Buffer>`(PNG),`genSvg` 回傳 standalone SVG 字串(引擎原生輸出,另將頁面 `<style>` 之視覺修正 CSS 內嵌進 SVG 根元素使其脫離頁面仍忠實、`&nbsp;` 正規化為 `&#160;`);兩者皆自帶瀏覽器生命週期(`chromium.launch()` → 渲染 → `browser.close()`),呼叫端不需自行管理 Playwright。
56
56
  - 統一由 `src/WFlowchart.mjs` 依 `mode === 'p2'` 呼叫,不提供批次流程或落地寫檔行為,是否寫檔、寫至何處由呼叫端決定。
57
57
 
58
58
  ## 自動化機制
package/src/p2/gen.mjs CHANGED
@@ -37,6 +37,20 @@ export function translate(data) {
37
37
  const dir = data.dir || 'TB'
38
38
  const innerDir = dir === 'TB' ? 'LR' : 'TB'
39
39
 
40
+ // mermaid 保留字(end/click/style/class/subgraph/direction 等)不可作節點 id → 一律加前綴消歧
41
+ // 僅影響 DSL 內部識別(顯示文字仍用 label); 不同原 id 淨化後若同名, 以序號區隔避免節點被合併
42
+ const used = new Set()
43
+ const qidMap = {}
44
+ const qid = (id) => {
45
+ if (qidMap[id]) return qidMap[id]
46
+ const base = 'n_' + String(id).replace(/[^A-Za-z0-9_]/g, '_')
47
+ let q = base, k = 2
48
+ while (used.has(q)) q = base + '_' + (k++)
49
+ used.add(q)
50
+ qidMap[id] = q
51
+ return q
52
+ }
53
+
40
54
  // 建立 group → children 映射(children 依 nodes 原序)
41
55
  const childrenOf = {} // groupId → [nodeId]
42
56
  const nodeById = {}
@@ -57,7 +71,7 @@ export function translate(data) {
57
71
  function renderNode(nd, indent) {
58
72
  const sp = ' '.repeat(indent)
59
73
  if (isGroupCls(nd.cls)) {
60
- lines.push(`${sp}subgraph ${nd.id}["${nd.label}"]`)
74
+ lines.push(`${sp}subgraph ${qid(nd.id)}["${nd.label}"]`)
61
75
  lines.push(`${sp} direction ${innerDir}`)
62
76
  const kids = childrenOf[nd.id] || []
63
77
  for (const cid of kids) renderNode(nodeById[cid], indent + 1)
@@ -66,7 +80,7 @@ export function translate(data) {
66
80
  const shape = nd.cls === 'diamond'
67
81
  ? `{${nd.label}}`
68
82
  : `["${nd.label}"]`
69
- lines.push(`${sp}${nd.id}${shape}`)
83
+ lines.push(`${sp}${qid(nd.id)}${shape}`)
70
84
  }
71
85
  }
72
86
 
@@ -77,7 +91,7 @@ export function translate(data) {
77
91
  for (const ed of data.edges) {
78
92
  const arrow = ed.kind === 'dashed' ? '-.->' : '-->'
79
93
  const lbl = ed.label ? `|"${ed.label}"|` : ''
80
- lines.push(` ${ed.from} ${arrow}${lbl} ${ed.to}`)
94
+ lines.push(` ${qid(ed.from)} ${arrow}${lbl} ${qid(ed.to)}`)
81
95
  }
82
96
 
83
97
  // classDef: 蒐集所有出現的 cls, 從 palette 取色
@@ -96,7 +110,7 @@ export function translate(data) {
96
110
  lines.push(` classDef ${cls} fill:${fillColor},stroke:${strokeColor},stroke-width:${sw},color:${textColor};`)
97
111
  }
98
112
  for (const [cls, ids] of Object.entries(clsByNodes)) {
99
- lines.push(` class ${ids.join(',')} ${cls};`)
113
+ lines.push(` class ${ids.map(qid).join(',')} ${cls};`)
100
114
  }
101
115
 
102
116
  return lines.join('\n')
@@ -187,10 +201,9 @@ try {
187
201
  } catch(e){ window.__setupErr = (e&&(e.message||e.stack))||String(e) }
188
202
  </script></body></html>`
189
203
 
190
- // 單張渲染 供其他模組 import 使用
204
+ // 渲染單張至就緒頁面(renderFig 完成); caller 負責 close browser
191
205
  // data: 正規化繪圖數據 { dir, nodes, edges }(caller 已先做 label 衍生)
192
- // 回傳: PNG 圖片的 Node Buffer
193
- export async function genPng(data, opt = {}) {
206
+ async function renderFigPage(data) {
194
207
  const code = FM + translate(data)
195
208
  const browser = await chromium.launch()
196
209
  try {
@@ -203,8 +216,40 @@ export async function genPng(data, opt = {}) {
203
216
  const r = await page.evaluate(([c, idx]) => window.renderFig(c, idx), [code, 0])
204
217
  if (!r.ok) throw new Error(r.err)
205
218
  await page.waitForTimeout(250)
219
+ return { browser, page }
220
+ }
221
+ catch (err) {
222
+ await browser.close()
223
+ throw err
224
+ }
225
+ }
226
+
227
+ // 單張渲染 → PNG Buffer
228
+ export async function genPng(data, opt = {}) {
229
+ const { browser, page } = await renderFigPage(data)
230
+ try {
206
231
  return await page.locator('#box svg').screenshot()
207
232
  } finally {
208
233
  await browser.close()
209
234
  }
210
235
  }
236
+
237
+ // 單張渲染 → SVG 字串(#box 內後處理完成之 SVG; &nbsp; 正規化為 &#160; 使其為合法 standalone SVG)
238
+ // 頁面 <head><style> 之視覺修正 CSS(邊標籤透明底/光暈/字型覆蓋)序列化後會遺失 → 內嵌進 SVG 根使其 standalone
239
+ export async function genSvg(data, opt = {}) {
240
+ const { browser, page } = await renderFigPage(data)
241
+ try {
242
+ return (await page.evaluate(() => {
243
+ const el = document.querySelector('#box svg')
244
+ const css = Array.from(document.querySelectorAll('head style')).map(s => s.textContent).join('\n')
245
+ const st = document.createElementNS('http://www.w3.org/2000/svg', 'style')
246
+ st.textContent = css
247
+ el.insertBefore(st, el.firstChild)
248
+ const out = document.getElementById('box').innerHTML
249
+ st.remove()
250
+ return out
251
+ })).replace(/&nbsp;/g, '&#160;').trim()
252
+ } finally {
253
+ await browser.close()
254
+ }
255
+ }
package/src/p3/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  - **渲染方式**:於頁面注入 `window.renderAndDetect(src)` 函式,呼叫 `window.nomnoml.renderSvg(src)` 將 DSL 字串渲染為 SVG,並將結果寫入 `#box` div 的 innerHTML。
7
7
  - **截圖目標**:以 Playwright locator `#box svg` 精確擷取 SVG 元素本身,排除頁面邊距干擾。
8
8
  - **解析度**:啟動 `browser.newPage({ deviceScaleFactor: 2 })`,輸出為 2× 實體像素的高解析度 PNG。
9
- - **呼叫方式**:`genPng(data, opt)` 為單張渲染函式,接受正規化繪圖數據 `{ dir, nodes, edges }`,內部自建 browser/page 並於結束時關閉,回傳 PNG 的 Node Buffer(不落地檔案),由 `src/WFlowchart.mjs` 統一調用(`mode: 'p3'`)。
9
+ - **呼叫方式**:`genPng(data, opt)` 為單張渲染函式,接受正規化繪圖數據 `{ dir, nodes, edges }`,內部自建 browser/page 並於結束時關閉,回傳 PNG 的 Node Buffer(不落地檔案),由 `src/WFlowchart.mjs` 統一調用(`mode: 'p3'`);同結構之 `genSvg(data, opt)` 回傳 `#box` 內引擎原生輸出之 SVG 字串(`&nbsp;` 正規化為 `&#160;`)。
10
10
 
11
11
  ## 產製原理(資料驅動)
12
12
 
package/src/p3/gen.mjs CHANGED
@@ -168,7 +168,7 @@ async function autofix(page, src) {
168
168
  // 單張渲染: 供其他模組 import 呼叫。輸入單份正規化繪圖數據(結構同 FIGURES[n].data,
169
169
  // caller 已先做 label 衍生), 內部自建 browser/page(避免與批次流程共用 state), 沿用批次流程
170
170
  // 同一套 deviceScaleFactor、等待邏輯、碰撞偵測自動調 spacing, 回傳 PNG 之 Node Buffer(不落地檔案)。
171
- export async function genPng(data, opt = {}) {
171
+ async function renderFigPage(data) {
172
172
  const browser = await chromium.launch()
173
173
  try {
174
174
  const page = await browser.newPage({ deviceScaleFactor: 2 })
@@ -177,12 +177,34 @@ export async function genPng(data, opt = {}) {
177
177
  const src = translate(data)
178
178
  const fx = await autofix(page, src)
179
179
  if (!fx.ok) throw new Error(fx.err)
180
- // 以最佳間距重繪後截圖
180
+ // 以最佳間距重繪
181
181
  await page.evaluate((x) => window.renderAndDetect(x), '#spacing: ' + fx.spacing + '\n' + src)
182
182
  await page.evaluate(() => document.fonts && document.fonts.ready)
183
183
  await page.waitForTimeout(180)
184
+ return { browser, page }
185
+ }
186
+ catch (err) {
187
+ await browser.close()
188
+ throw err
189
+ }
190
+ }
191
+
192
+ // 單張渲染 → PNG Buffer
193
+ export async function genPng(data, opt = {}) {
194
+ const { browser, page } = await renderFigPage(data)
195
+ try {
184
196
  return await page.locator('#box svg').screenshot()
185
197
  } finally {
186
198
  await browser.close()
187
199
  }
188
200
  }
201
+
202
+ // 單張渲染 → SVG 字串(#box 內後處理完成之 SVG; &nbsp; 正規化為 &#160; 使其為合法 standalone SVG)
203
+ export async function genSvg(data, opt = {}) {
204
+ const { browser, page } = await renderFigPage(data)
205
+ try {
206
+ return (await page.evaluate(() => document.getElementById('box').innerHTML)).replace(/&nbsp;/g, '&#160;').trim()
207
+ } finally {
208
+ await browser.close()
209
+ }
210
+ }
package/src/p4/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  - **繪圖庫**:Cytoscape.js(`cytoscape/dist/cytoscape.min.js`)搭配 dagre 排版引擎(`dagre/dist/dagre.min.js`)與橋接外掛(`cytoscape-dagre/dist/cytoscape-dagre.js`),三者由本機 `node_modules` 讀出後內聯注入頁面 `<script>`(`common/pkg.mjs` 的 `pkgScript()`),非透過 CDN,斷網環境亦可渲染。
6
6
  - **渲染環境**:由 Playwright `chromium.launch()` 開啟無頭瀏覽器,`page.setContent()` 注入含三段內聯 script 的靜態 HTML,頁面掛載一個 `1400×1400px` 的 `<div id="cy">` 作為 Cytoscape 容器。
7
7
  - **截圖與匯出**:排版完成後呼叫 `cy.png({ full:true, scale:2, bg:'#ffffff', output:'base64' })` 取得 base64 字串,存入全域變數 `window.__png`,再由 `page.evaluate()` 讀回 Node 端,以 `Buffer.from(b64, 'base64')` 轉為 PNG Buffer 回傳。`full:true` 使輸出範圍自動裁切至實際元素範圍,`scale:2` 使最終 PNG 解析度為實際佈局尺寸的 2 倍;`deviceScaleFactor` 設為 `1`(由 `browser.newPage({ deviceScaleFactor:1 })` 指定),縮放完全由 `cy.png` 的 `scale:2` 控制。
8
- - **呼叫方式**:`genPng(data, opt)` 為單張渲染函式,接受正規化繪圖數據 `{ dir, nodes, edges }`,內部自建 browser/page 並於結束時關閉,回傳 PNG 的 Node Buffer(不落地檔案),由 `src/WFlowchart.mjs` 統一調用(`mode: 'p4'`)。
8
+ - **呼叫方式**:`genPng(data, opt)` 為單張渲染函式,接受正規化繪圖數據 `{ dir, nodes, edges }`,內部自建 browser/page 並於結束時關閉,回傳 PNG 的 Node Buffer(不落地檔案),由 `src/WFlowchart.mjs` 統一調用(`mode: 'p4'`);同結構之 `genSvg(data, opt)` 經 `cytoscape-svg` 外掛將同一份佈局/樣式匯出為 SVG 字串(canvas 引擎重繪為 SVG,經像素級忠實度驗證)。
9
9
 
10
10
  ## 產製原理(資料驅動)
11
11
 
package/src/p4/gen.mjs CHANGED
@@ -4,10 +4,11 @@
4
4
  import { chromium } from 'playwright'
5
5
  import { pkgScript } from '../common/pkg.mjs'
6
6
 
7
- // cytoscape + dagre + cytoscape-dagre 由本機 node_modules 內聯注入(取代 CDN, 斷網環境可用)
7
+ // cytoscape + dagre + cytoscape-dagre + cytoscape-svg 由本機 node_modules 內聯注入(取代 CDN, 斷網環境可用)
8
8
  const CYTOSCAPE_JS = pkgScript('cytoscape/dist/cytoscape.min.js')
9
9
  const DAGRE_JS = pkgScript('dagre/dist/dagre.min.js')
10
10
  const CYTOSCAPE_DAGRE_JS = pkgScript('cytoscape-dagre/dist/cytoscape-dagre.js')
11
+ const CYTOSCAPE_SVG_JS = pkgScript('cytoscape-svg/cytoscape-svg.js')
11
12
 
12
13
  // 正規化數據 → cytoscape elements(節點帶 parent=group; 邊 dashed→classes)
13
14
  function toEls(data) {
@@ -41,18 +42,26 @@ const html = `<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">
41
42
  <script>${CYTOSCAPE_JS}</script>
42
43
  <script>${DAGRE_JS}</script>
43
44
  <script>${CYTOSCAPE_DAGRE_JS}</script>
45
+ <script>${CYTOSCAPE_SVG_JS}</script>
44
46
  </head><body style="margin:0;background:#fff">
45
47
  <div id="cy" style="width:1400px;height:1400px;background:#fff"></div>
46
48
  <script>
47
49
  if (window.cytoscapeDagre) { try { cytoscape.use(window.cytoscapeDagre) } catch(e){} }
48
- window.renderFig = function(els, styleJson, rankDir){ return new Promise(function(resolve){
50
+ if (window.cytoscapeSvg) { try { cytoscape.use(window.cytoscapeSvg) } catch(e){} }
51
+ // mode: 'png'(預設) 或 'svg' — 版面/元素/樣式建置流程共用, 僅末端輸出格式不同(genPng 行為不變)
52
+ window.renderFig = function(els, styleJson, rankDir, mode){ return new Promise(function(resolve){
49
53
  try {
50
54
  var cy = cytoscape({ container: document.getElementById('cy'), elements: els, style: JSON.parse(styleJson) })
51
55
  els.forEach(function(x){ if (x.classes && x.data && x.data.id) cy.getElementById(x.data.id).addClass(x.classes) })
52
56
  var l = cy.layout({ name:'dagre', rankDir:rankDir, nodeSep:42, rankSep:55, edgeSep:18 })
53
57
  l.promiseOn('layoutstop').then(function(){
54
58
  setTimeout(function(){
55
- try { window.__png = cy.png({ full:true, scale:2, bg:'#ffffff', output:'base64' }); var bb=cy.elements().boundingBox(); resolve({ ok:true, w:Math.round(bb.w), h:Math.round(bb.h) }) }
59
+ try {
60
+ var bb=cy.elements().boundingBox()
61
+ if (mode === 'svg') { window.__svg = cy.svg({ full:true, scale:2, bg:'#ffffff' }) }
62
+ else { window.__png = cy.png({ full:true, scale:2, bg:'#ffffff', output:'base64' }) }
63
+ resolve({ ok:true, w:Math.round(bb.w), h:Math.round(bb.h) })
64
+ }
56
65
  catch(e){ resolve({ ok:false, err:String(e.message||e) }) }
57
66
  }, 250)
58
67
  })
@@ -62,21 +71,38 @@ window.renderFig = function(els, styleJson, rankDir){ return new Promise(functio
62
71
  window.__ready = true
63
72
  </script></body></html>`
64
73
 
65
- // 單張渲染供其他模組 import 使用
66
- // data: 正規化繪圖數據 { dir, nodes, edges }(caller 已先做 label 衍生)
67
- // 回傳: PNG 圖片的 Node Buffer
68
- export async function genPng(data, opt = {}) {
74
+ // 共用渲染流程啟動 headless 頁面, 建置 cytoscape + dagre 版面, 回傳指定 mode 之末端輸出(base64 png 或 svg 字串)
75
+ // genPng/genSvg 共用(genPng 行為不變: mode 'png' 與原本 3 參數呼叫等價)
76
+ async function renderPage(data, mode) {
69
77
  const browser = await chromium.launch()
70
78
  try {
71
79
  const page = await browser.newPage({ deviceScaleFactor: 1 })
72
80
  await page.setContent(html, { waitUntil: 'load' })
73
81
  await page.waitForFunction(() => window.__ready, { timeout: 60000 }).catch(() => {})
74
82
  await page.evaluate(() => document.fonts && document.fonts.ready)
75
- const res = await page.evaluate(([els, st, rd]) => window.renderFig(els, st, rd), [toEls(data), style, data.dir])
83
+ const res = await page.evaluate(([els, st, rd, md]) => window.renderFig(els, st, rd, md), [toEls(data), style, data.dir, mode])
76
84
  if (!res.ok) throw new Error(res.err)
77
- const b64 = await page.evaluate(() => window.__png)
78
- return Buffer.from(b64, 'base64')
85
+ return await page.evaluate((m) => (m === 'svg' ? window.__svg : window.__png), mode)
79
86
  } finally {
80
87
  await browser.close()
81
88
  }
82
89
  }
90
+
91
+ // 單張渲染 — 供其他模組 import 使用
92
+ // data: 正規化繪圖數據 { dir, nodes, edges }(caller 已先做 label 衍生)
93
+ // 回傳: PNG 圖片的 Node Buffer
94
+ export async function genPng(data, opt = {}) {
95
+ const b64 = await renderPage(data, 'png')
96
+ return Buffer.from(b64, 'base64')
97
+ }
98
+
99
+ // 單張渲染 — 供其他模組 import 使用(cytoscape-svg 外掛匯出, 與 genPng 共用相同版面/樣式/scale)
100
+ // data: 正規化繪圖數據 { dir, nodes, edges }(caller 已先做 label 衍生)
101
+ // 回傳: SVG 字串
102
+ export async function genSvg(data, opt = {}) {
103
+ let svg = await renderPage(data, 'svg')
104
+ svg = svg.replace(/&nbsp;/g, '&#160;')
105
+ if (!/\sxmlns=/.test(svg)) svg = svg.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"')
106
+ if (!/font-family/.test(svg)) svg = svg.replace('>', '><style>text{font-family:\'Microsoft JhengHei\',sans-serif}</style>')
107
+ return svg
108
+ }
package/src/p5/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
  - **解析度處理**:不使用固定 `deviceScaleFactor`,改為通用公式依 SVG `viewBox` 寬動態推算 `density`(見自動化機制節),讓輸出像素寬落在 1600–2200 px 區間。
15
15
  - **字型問題**:本專案所用之 D2 傳入自訂 font buffer 會穩定回傳 `"invalid JSON input"`(實測),故不傳字型給 D2,改在產生的 SVG 插入 `<style>text{font-family:"Microsoft JhengHei","Microsoft YaHei",sans-serif !important;}</style>` 讓 librsvg 以系統安裝的 Microsoft JhengHei 描繪中文。D2 引擎本身已內建 CJK 全形寬度量測,故即使長中文標籤,box 寬度也不溢出。
16
16
  - **邊標籤光暈**:D2 對有標籤的邊會用 `mask` + 黑色 `rect` 遮斷連線偽造白底效果,且標籤文字疊在連線正上方;`injectEdgeLabelHalo(svg)` 移除 mask 內黑色 rect 讓連線完整穿過標籤區,並注入 CSS 為 `.text-italic` 加 `paint-order:stroke` + 白色描邊,使標籤文字在線段上仍清晰可讀。
17
- - **介面**:`genPng(data, opt)` 為單張渲染函式,回傳值為 PNG Buffer(不寫檔),由 `src/WFlowchart.mjs` 統一調用;`data` 為正規化繪圖數據 `{ dir, nodes, edges }`(caller 已完成 label 衍生)。
17
+ - **介面**:`genPng(data, opt)` 為單張渲染函式,回傳值為 PNG Buffer(不寫檔),由 `src/WFlowchart.mjs` 統一調用;`data` 為正規化繪圖數據 `{ dir, nodes, edges }`(caller 已完成 label 衍生);同結構之 `genSvg(data, opt)` 回傳 D2 引擎原生輸出之 SVG 字串(已套用邊標籤光暈與字型注入之後處理)。
18
18
 
19
19
  ## 產製原理(資料驅動)
20
20