w-flowchart 1.0.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 (135) hide show
  1. package/.editorconfig +9 -0
  2. package/.eslintignore +3 -0
  3. package/.eslintrc.js +55 -0
  4. package/.jsdoc +25 -0
  5. package/LICENSE +21 -0
  6. package/README.md +49 -0
  7. package/SECURITY.md +5 -0
  8. package/babel.config.js +16 -0
  9. package/dist/w-flowchart.umd.js +7 -0
  10. package/dist/w-flowchart.umd.js.map +1 -0
  11. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  12. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  13. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  14. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  15. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  16. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  17. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  18. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  19. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  20. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
  21. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  22. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  23. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  24. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  25. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
  26. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  27. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  28. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  29. package/docs/index.html +84 -0
  30. package/docs/scripts/collapse.js +39 -0
  31. package/docs/scripts/commonNav.js +28 -0
  32. package/docs/scripts/linenumber.js +25 -0
  33. package/docs/scripts/nav.js +12 -0
  34. package/docs/scripts/polyfill.js +4 -0
  35. package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
  36. package/docs/scripts/prettify/lang-css.js +2 -0
  37. package/docs/scripts/prettify/prettify.js +28 -0
  38. package/docs/scripts/search.js +99 -0
  39. package/docs/styles/jsdoc.css +776 -0
  40. package/docs/styles/prettify.css +80 -0
  41. package/package.json +49 -0
  42. package/script.txt +17 -0
  43. package/src/WFlowchart.mjs +0 -0
  44. package/src/common/derive.mjs +7 -0
  45. package/src/common/palette.mjs +34 -0
  46. package/src/common/pkg.mjs +31 -0
  47. package/src/p1/README.md +65 -0
  48. package/src/p1/gen.mjs +203 -0
  49. package/src/p10/README.md +31 -0
  50. package/src/p10/gen.mjs +228 -0
  51. package/src/p2/README.md +87 -0
  52. package/src/p2/gen.mjs +210 -0
  53. package/src/p3/README.md +99 -0
  54. package/src/p3/gen.mjs +188 -0
  55. package/src/p4/README.md +84 -0
  56. package/src/p4/gen.mjs +82 -0
  57. package/src/p5/README.md +90 -0
  58. package/src/p5/gen.mjs +182 -0
  59. package/src/p6/README.md +87 -0
  60. package/src/p6/gen.mjs +186 -0
  61. package/src/p7/README.md +84 -0
  62. package/src/p7/gen.mjs +386 -0
  63. package/src/p8/README.md +92 -0
  64. package/src/p8/gen.mjs +416 -0
  65. package/src/p9/README.md +90 -0
  66. package/src/p9/gen.mjs +63 -0
  67. package/src/p9/page9.html +420 -0
  68. package/test/cases.mjs +349 -0
  69. package/test/data//344/275/234/346/245/255/346/265/201/347/250/213/345/234/226.json +92 -0
  70. 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 +112 -0
  71. 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 +116 -0
  72. 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 +251 -0
  73. 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 +126 -0
  74. 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 +133 -0
  75. package/test/data//347/263/273/347/265/261/346/236/266/346/247/213/345/234/226.json +102 -0
  76. package/test/data//347/263/273/347/265/261/351/227/234/350/201/257/345/234/226.json +117 -0
  77. 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 +162 -0
  78. package/test/figures.mjs +10 -0
  79. package/test/lib.mjs +116 -0
  80. package/test/p10.test.mjs +89 -0
  81. package/test/pics/cycle.png +0 -0
  82. package/test/pics/cycle.svg +1 -0
  83. package/test/pics/deep-nest.png +0 -0
  84. package/test/pics/deep-nest.svg +1 -0
  85. package/test/pics/diamond.png +0 -0
  86. package/test/pics/diamond.svg +1 -0
  87. package/test/pics/edges-mix.png +0 -0
  88. package/test/pics/edges-mix.svg +1 -0
  89. package/test/pics/english.png +0 -0
  90. package/test/pics/english.svg +1 -0
  91. package/test/pics/flat-tb.png +0 -0
  92. package/test/pics/flat-tb.svg +1 -0
  93. package/test/pics/fold-tall.png +0 -0
  94. package/test/pics/fold-tall.svg +1 -0
  95. package/test/pics/fold-wide.png +0 -0
  96. package/test/pics/fold-wide.svg +1 -0
  97. package/test/pics/group-edge.png +0 -0
  98. package/test/pics/group-edge.svg +1 -0
  99. package/test/pics/items-node.png +0 -0
  100. package/test/pics/items-node.svg +1 -0
  101. package/test/pics/long-label.png +0 -0
  102. package/test/pics/long-label.svg +1 -0
  103. package/test/pics/lr-flat.png +0 -0
  104. package/test/pics/lr-flat.svg +1 -0
  105. package/test/pics/nested-group.png +0 -0
  106. package/test/pics/nested-group.svg +1 -0
  107. package/test/pics/no-edges.png +0 -0
  108. package/test/pics/no-edges.svg +1 -0
  109. package/test/pics/title-overflow.png +0 -0
  110. package/test/pics/title-overflow.svg +1 -0
  111. package/test/pics/valign.png +0 -0
  112. package/test/pics/valign.svg +1 -0
  113. package/test/pics//344/275/234/346/245/255/346/265/201/347/250/213/345/234/226.png +0 -0
  114. package/test/pics//344/275/234/346/245/255/346/265/201/347/250/213/345/234/226.svg +1 -0
  115. 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
  116. 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 +1 -0
  117. 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
  118. 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 +1 -0
  119. 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
  120. 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 +1 -0
  121. 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
  122. 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 +1 -0
  123. 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
  124. 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 +1 -0
  125. package/test/pics//347/263/273/347/265/261/346/236/266/346/247/213/345/234/226.png +0 -0
  126. package/test/pics//347/263/273/347/265/261/346/236/266/346/247/213/345/234/226.svg +1 -0
  127. package/test/pics//347/263/273/347/265/261/351/227/234/350/201/257/345/234/226.png +0 -0
  128. package/test/pics//347/263/273/347/265/261/351/227/234/350/201/257/345/234/226.svg +1 -0
  129. 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
  130. 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 +1 -0
  131. package/test/wflowchart.test.mjs +41 -0
  132. package/toolg/addVersion.mjs +4 -0
  133. package/toolg/cleanFolder.mjs +4 -0
  134. package/toolg/gDistRollup.mjs +51 -0
  135. package/toolg/modifyReadme.mjs +4 -0
@@ -0,0 +1,420 @@
1
+ <!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">
2
+ <!-- 繪圖庫由本機 node_modules 內聯注入(gen.mjs 讀本檔後替換各佔位符; 取代 esm.sh/CDN, 斷網環境可用) -->
3
+ <style>/*__XYFLOW_CSS__*/</style>
4
+ <style>
5
+ html,body{margin:0;background:#fff;font-family:'Microsoft JhengHei',sans-serif}
6
+ /* 字型設定層兜底: 強制整個 React Flow 渲染樹(節點/容器標題/粗體/邊標籤)全繼承 JhengHei,
7
+ 即使未來新增文字元素未逐處指定 fontFamily 也被全覆蓋(原則1: 從設定層做到完整覆蓋) */
8
+ .react-flow, .react-flow__node, .react-flow__edge-text, .react-flow__edgelabel-renderer{font-family:'Microsoft JhengHei',sans-serif!important}
9
+ .react-flow__attribution{display:none!important}
10
+ #stage{background:#fff}
11
+ /* 量測用隱形容器 */
12
+ #meas{position:absolute;left:-99999px;top:0;font-family:'Microsoft JhengHei',sans-serif;visibility:hidden}
13
+ </style>
14
+ </head><body>
15
+ <div id="shot" style="display:inline-block;background:#fff;padding:24px">
16
+ <div id="stage" style="width:1200px;height:900px"></div>
17
+ </div>
18
+ <div id="meas"></div>
19
+ <script>/*__REACT_JS__*/</script>
20
+ <script>/*__REACT_DOM_JS__*/</script>
21
+ <script>
22
+ // react/jsx-runtime shim: @xyflow/react UMD 之外部依賴含全域 jsxRuntime(react 18 UMD 未提供),
23
+ // 以 createElement 等價實作 jsx/jsxs(children 由 props 抽出改為位置參數, key 併回 props)
24
+ window.jsxRuntime = (function(){
25
+ var R = window.React
26
+ function jsxShim(type, props, key){
27
+ props = props || {}
28
+ var children = props.children
29
+ var rest = {}
30
+ for (var k in props) if (k !== 'children') rest[k] = props[k]
31
+ if (key !== undefined) rest.key = key
32
+ if (children === undefined) return R.createElement(type, rest)
33
+ return Array.isArray(children) ? R.createElement.apply(null, [type, rest].concat(children)) : R.createElement(type, rest, children)
34
+ }
35
+ return { jsx: jsxShim, jsxs: jsxShim, Fragment: R.Fragment }
36
+ })()
37
+ </script>
38
+ <script>/*__XYFLOW_JS__*/</script>
39
+ <script>/*__DAGRE_JS__*/</script>
40
+ <script type="module">
41
+ try {
42
+ const React = window.React
43
+ const { createRoot } = window.ReactDOM
44
+ const RF = window.ReactFlow
45
+ const dagre = window.dagre
46
+ const ReactFlow = RF.ReactFlow || RF.default
47
+ const h = React.createElement
48
+
49
+ // ---- 色票(沿用 cytoscape 版配色) ----
50
+ const COL = {
51
+ blue: ['#eef4fb','#3f6fb0','#1c2b36'],
52
+ green: ['#eaf4ef','#348a5c','#1c2b36'],
53
+ orange: ['#fcf0e2','#c46e1a','#1c2b36'],
54
+ purple: ['#f4f0fa','#8163ad','#1c2b36'],
55
+ red: ['#fbecea','#c0392b','#1c2b36'],
56
+ done: ['#eaf4ef','#256046','#256046'],
57
+ // 群組(淡底+粗框, 文字用框色)
58
+ blueG: ['#f5f9fe','#3f6fb0','#3f6fb0'],
59
+ blueG2: ['#eef4fb','#3f6fb0','#3f6fb0'],
60
+ greenG: ['#f0f7f3','#348a5c','#348a5c'],
61
+ greenG2: ['#eaf4ef','#348a5c','#348a5c'],
62
+ orangeG: ['#fdf6ec','#c46e1a','#c46e1a'],
63
+ purpleG: ['#faf7fd','#8163ad','#8163ad'],
64
+ }
65
+
66
+ // ---- 量測一段多行文字實際像素(避免爆框) ----
67
+ const meas = document.getElementById('meas')
68
+ function measure(label, fontSize, padX, padY, maxW) {
69
+ const d = document.createElement('div')
70
+ d.style.cssText = `display:inline-block;white-space:pre-line;overflow-wrap:break-word;word-break:normal;text-align:center;line-height:1.45;`
71
+ + `font-size:${fontSize}px;padding:${padY}px ${padX}px;max-width:${maxW}px;box-sizing:border-box;`
72
+ d.textContent = label
73
+ meas.appendChild(d)
74
+ const r = d.getBoundingClientRect()
75
+ meas.removeChild(d)
76
+ return { w: Math.ceil(r.width) + 2, h: Math.ceil(r.height) + 2 }
77
+ }
78
+
79
+ // ---- 單一 div 多行 label (whiteSpace:pre-line 置中) ----
80
+ const Label = (text, fontSize, color) => h('div', {
81
+ style: { whiteSpace: 'pre-line', overflowWrap: 'break-word', wordBreak: 'normal', textAlign: 'center', lineHeight: 1.45, fontSize, color,
82
+ width: '100%', fontFamily: 'Microsoft JhengHei,sans-serif' }
83
+ }, text)
84
+
85
+ // ---- 自訂菱形節點 ----
86
+ const Pos = RF.Position
87
+ function DiamondNode({ data }) {
88
+ const s = data._sz
89
+ return h('div', { style: { width: s.w, height: s.h, position: 'relative' } }, [
90
+ h('div', { key: 'd', style: {
91
+ position: 'absolute', inset: 0, background: COL.orange[0],
92
+ border: '1.8px solid ' + COL.orange[1], transform: 'rotate(45deg)', borderRadius: 4 } }),
93
+ h('div', { key: 't', style: {
94
+ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' } },
95
+ Label(data.label, data.fontSize || 14, COL.orange[2])),
96
+ ...[['t', Pos.Top], ['b', Pos.Bottom], ['l', Pos.Left], ['r', Pos.Right]].flatMap(([id, pos]) => [
97
+ h(RF.Handle, { key: 's' + id, id: 's' + id, type: 'source', position: pos, style: { opacity: 0, pointerEvents: 'none' } }),
98
+ h(RF.Handle, { key: 't' + id, id: 't' + id, type: 'target', position: pos, style: { opacity: 0, pointerEvents: 'none' } }),
99
+ ]),
100
+ ])
101
+ }
102
+ // ---- 顯式佈局用節點: 四邊各一組 source+target handle, 供任意方向連邊 ----
103
+ function BoxNode({ data }) {
104
+ const s = data._sz, c = COL[data.kind] || COL.blue
105
+ const sides = [['t', Pos.Top], ['b', Pos.Bottom], ['l', Pos.Left], ['r', Pos.Right]]
106
+ const hs = []
107
+ for (const [id, pos] of sides) {
108
+ hs.push(h(RF.Handle, { key: 's' + id, id: 's' + id, type: 'source', position: pos, style: { opacity: 0, pointerEvents: 'none' } }))
109
+ hs.push(h(RF.Handle, { key: 't' + id, id: 't' + id, type: 'target', position: pos, style: { opacity: 0, pointerEvents: 'none' } }))
110
+ }
111
+ return h('div', { style: {
112
+ width: s.w, height: s.h, background: c[0], border: '1.8px solid ' + c[1], borderRadius: 8,
113
+ color: c[2], display: 'flex', alignItems: 'center', justifyContent: 'center', boxSizing: 'border-box',
114
+ fontFamily: 'Microsoft JhengHei,sans-serif' } }, [Label(data.label, data.fontSize || 14, c[2]), ...hs])
115
+ }
116
+ // ---- 群組容器(含四邊 handle, 供層級邊連接) ----
117
+ function GroupNode({ data }) {
118
+ const c = COL[data.kind] || COL.blueG, s = data._sz
119
+ const sides = [['t', Pos.Top], ['b', Pos.Bottom], ['l', Pos.Left], ['r', Pos.Right]]
120
+ const hs = []
121
+ for (const [id, pos] of sides) {
122
+ hs.push(h(RF.Handle, { key: 's' + id, id: 's' + id, type: 'source', position: pos, style: { opacity: 0, pointerEvents: 'none' } }))
123
+ hs.push(h(RF.Handle, { key: 't' + id, id: 't' + id, type: 'target', position: pos, style: { opacity: 0, pointerEvents: 'none' } }))
124
+ }
125
+ return h('div', { style: {
126
+ width: s.w, height: s.h, background: c[0], border: '2px solid ' + c[1], borderRadius: 10,
127
+ boxSizing: 'border-box', position: 'relative' } }, [
128
+ h('div', { key: 'ttl', style: {
129
+ position: 'absolute', top: 6, left: 0, width: '100%', textAlign: 'center',
130
+ fontWeight: 'bold', fontSize: (data.titleSize || 15), color: c[1],
131
+ fontFamily: 'Microsoft JhengHei,sans-serif' } }, data.label),
132
+ ...hs,
133
+ ])
134
+ }
135
+ const nodeTypes = { diamond: DiamondNode, box: BoxNode, grp: GroupNode }
136
+
137
+ // ---- 通用節點樣式 ----
138
+ function nodeStyle(kind, w, hgt, isGroup) {
139
+ const c = COL[kind] || COL.blue
140
+ return {
141
+ width: w, height: hgt, background: c[0], border: (isGroup ? '2px' : '1.8px') + ' solid ' + c[1],
142
+ borderRadius: 8, color: c[2], display: 'flex', alignItems: 'center', justifyContent: 'center',
143
+ boxSizing: 'border-box', fontFamily: 'Microsoft JhengHei,sans-serif',
144
+ }
145
+ }
146
+
147
+ // 群組標題(置頂)樣式: 用節點 label 區塊撐開, 子節點顯式定位避開標題列
148
+ function groupNode(spec) {
149
+ const c = COL[spec.kind] || COL.blueG
150
+ return {
151
+ id: spec.id, type: 'group', position: { x: spec.x, y: spec.y },
152
+ data: {},
153
+ style: { width: spec.w, height: spec.h, background: c[0], border: '2px solid ' + c[1],
154
+ borderRadius: 10, boxSizing: 'border-box' },
155
+ draggable: false, selectable: false,
156
+ }
157
+ }
158
+ function groupTitle(spec) {
159
+ // 群組標題以一個獨立子節點(無框)放在群組頂部置中
160
+ const c = COL[spec.kind] || COL.blueG
161
+ return {
162
+ id: spec.id + '__title', parentId: spec.id, extent: 'parent',
163
+ position: { x: 0, y: 6 },
164
+ data: { label: Label(spec.label, spec.titleSize || 15, c[1]) },
165
+ style: { width: spec.w, height: 24, background: 'transparent', border: 'none',
166
+ display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 'bold' },
167
+ draggable: false, selectable: false,
168
+ }
169
+ }
170
+
171
+ // ---- 通用節點尺寸量測(資料驅動共用): 依 label 字數/行數自動算寬高 ----
172
+ // 矩形 padding 18/12; 菱形取對角線方框; 無逐圖魔術數字。
173
+ function sizeOf(n) {
174
+ const fs = n.fontSize || 14
175
+ if (n.kind === 'diamond') { const s = measure(n.label, fs, 4, 4, 160); const d = Math.max(s.w, s.h) + 56; return { w: d, h: d } }
176
+ return measure(n.label, fs, 18, 12, 320)
177
+ }
178
+
179
+ // ---- 資料驅動 compound dagre 佈局(群組以 dagre 原生 setParent 自動排版) ----
180
+ // spec.nodes: [{id,label,kind,parent?}] (parent=容器 id, 容器本身也是一個 node, kind 結尾 G/G2)
181
+ // 群組容器尺寸/位置由 dagre cluster 自動推算; 子節點絕對定位; 容器頂部保留標題列高度。
182
+ function layoutDagreCompound(spec) {
183
+ const { rankDir = 'TB', nodes: rawNodes, edges: rawEdges } = spec
184
+ const isGrp = n => /G2?$/.test(n.kind || '')
185
+ const grpIds = new Set(rawNodes.filter(isGrp).map(n => n.id))
186
+ // 群組標題列高度: 依標題字級通用推算(字級 + 上下留白)
187
+ const titleBand = n => Math.round((n.fontSize || 15) * 1.4) + 12
188
+
189
+ const g = new dagre.graphlib.Graph({ compound: true })
190
+ g.setGraph({ rankdir: rankDir, nodesep: spec.nodesep || 46, ranksep: spec.ranksep || 62, marginx: 12, marginy: 12 })
191
+ g.setDefaultEdgeLabel(() => ({}))
192
+
193
+ const sized = {}
194
+ // 先放葉節點(非群組)
195
+ for (const n of rawNodes) {
196
+ if (grpIds.has(n.id)) continue
197
+ const s = sizeOf(n)
198
+ sized[n.id] = s
199
+ g.setNode(n.id, { width: s.w, height: s.h })
200
+ }
201
+ // 放群組容器(dagre compound: 容器尺寸由其子節點自動撐開, 標題列高度於佈局後再向上擴張)
202
+ for (const n of rawNodes) {
203
+ if (!grpIds.has(n.id)) continue
204
+ g.setNode(n.id, {})
205
+ }
206
+ // 建立歸屬(子→容器), 支援巢狀(容器亦可有 parent)
207
+ for (const n of rawNodes) {
208
+ if (n.parent && grpIds.has(n.parent)) g.setParent(n.id, n.parent)
209
+ }
210
+ // dagre compound 不能對「容器節點」直接連邊(ranking 階段會崩),
211
+ // 故 ranking 用「代表葉節點」替代容器端點(取該容器第一個葉子後代);
212
+ // 實際繪線仍用容器框(後段 pickHandles 以絕對 box 計算)。此為通用處理, 非逐圖。
213
+ const repLeaf = {}
214
+ for (const n of rawNodes) {
215
+ if (n.parent && grpIds.has(n.parent) && !grpIds.has(n.id) && repLeaf[n.parent] === undefined) repLeaf[n.parent] = n.id
216
+ }
217
+ const rankId = id => grpIds.has(id) ? (repLeaf[id] || id) : id
218
+ const seenRank = new Set()
219
+ for (const e of rawEdges) {
220
+ const s = rankId(e.s), t = rankId(e.t)
221
+ if (s === t) continue
222
+ const key = s + '' + t
223
+ if (seenRank.has(key)) continue
224
+ seenRank.add(key)
225
+ g.setEdge(s, t)
226
+ }
227
+ dagre.layout(g)
228
+
229
+ // dagre 給出每個節點/群組之中心 + 尺寸, 全部以絕對座標放置(群組先放→在底層)
230
+ // 群組框向上擴張一個「標題列」高度, 讓置頂標題不壓到第一列子節點(通用, 依字級推算)。
231
+ const nodes = []
232
+ for (const n of rawNodes) {
233
+ if (!grpIds.has(n.id)) continue
234
+ const p = g.node(n.id)
235
+ const band = titleBand(n)
236
+ const w = p.width, hh = p.height + band
237
+ const x = p.x - w / 2, y = (p.y - p.height / 2) - band
238
+ nodes.push({
239
+ id: n.id, type: 'grp', position: { x, y },
240
+ data: { label: n.label, kind: n.kind, titleSize: n.fontSize || 15, _sz: { w, h: hh } },
241
+ zIndex: 0, // z-order 最底層: 容器填色墊底, 不遮節點/邊/標籤(原則2)
242
+ draggable: false, selectable: false,
243
+ })
244
+ }
245
+ for (const n of rawNodes) {
246
+ if (grpIds.has(n.id)) continue
247
+ const p = g.node(n.id), s = sized[n.id], fs = n.fontSize || 14
248
+ const c = COL[n.kind] || COL.blue
249
+ const base = { id: n.id, position: { x: p.x - s.w / 2, y: p.y - s.h / 2 }, zIndex: 1, draggable: false, selectable: false }
250
+ if (n.kind === 'diamond') {
251
+ nodes.push({ ...base, type: 'diamond', data: { label: n.label, fontSize: fs, _sz: s } })
252
+ } else {
253
+ nodes.push({ ...base, type: 'box', data: { label: n.label, fontSize: fs, kind: n.kind, _sz: s } })
254
+ }
255
+ }
256
+ // 邊: 依絕對幾何自動挑 handle(同 explicit 路徑), 虛線/標籤沿用 mkEdge
257
+ const box = {}
258
+ for (const n of nodes) box[n.id] = { x: n.position.x, y: n.position.y, w: n.data._sz.w, h: n.data._sz.h }
259
+ const edges = rawEdges.map((e, i) => {
260
+ const a = box[e.s], b = box[e.t]
261
+ if (!a || !b) return mkEdge(e, i, null, null, 'default')
262
+ const { sh, th } = pickHandles(a, b)
263
+ return mkEdge(e, i, sh, th, 'default')
264
+ })
265
+ // 回傳含畫布實際尺寸(供 stage 自動設定, 無逐圖 W/H)
266
+ const gg = g.graph()
267
+ return { nodes, edges, W: Math.ceil(gg.width) + 48, H: Math.ceil(gg.height) + 48 }
268
+ }
269
+
270
+ // ---- dagre 佈局(流程/關係圖) ----
271
+ function layoutDagre(spec) {
272
+ const { rankDir = 'TB', nodes: rawNodes, edges: rawEdges } = spec
273
+ const g = new dagre.graphlib.Graph()
274
+ g.setGraph({ rankdir: rankDir, nodesep: spec.nodesep || 46, ranksep: spec.ranksep || 62, marginx: 10, marginy: 10 })
275
+ g.setDefaultEdgeLabel(() => ({}))
276
+ const sized = {}
277
+ for (const n of rawNodes) {
278
+ const fs = n.fontSize || 14
279
+ const maxW = n.maxW || (n.kind === 'diamond' ? 160 : 300)
280
+ let s = measure(n.label, fs, n.kind === 'diamond' ? 4 : 18, n.kind === 'diamond' ? 4 : 12, maxW)
281
+ if (n.kind === 'diamond') { const d = Math.max(s.w, s.h) + 56; s = { w: d, h: d } }
282
+ sized[n.id] = s
283
+ g.setNode(n.id, { width: s.w, height: s.h })
284
+ }
285
+ for (const e of rawEdges) g.setEdge(e.s, e.t)
286
+ dagre.layout(g)
287
+
288
+ const nodes = rawNodes.map(n => {
289
+ const p = g.node(n.id), s = sized[n.id], fs = n.fontSize || 14
290
+ const c = COL[n.kind] || COL.blue
291
+ if (n.kind === 'diamond') {
292
+ return { id: n.id, type: 'diamond', position: { x: p.x - s.w / 2, y: p.y - s.h / 2 },
293
+ data: { label: n.label, fontSize: fs, _sz: s }, draggable: false, selectable: false }
294
+ }
295
+ return {
296
+ id: n.id, position: { x: p.x - s.w / 2, y: p.y - s.h / 2 },
297
+ data: { label: Label(n.label, fs, c[2]) }, style: nodeStyle(n.kind, s.w, s.h),
298
+ sourcePosition: rankDir === 'LR' ? 'right' : 'bottom',
299
+ targetPosition: rankDir === 'LR' ? 'left' : 'top',
300
+ draggable: false, selectable: false,
301
+ }
302
+ })
303
+ const edges = rawEdges.map((e, i) => mkEdge(e, i))
304
+ return { nodes, edges }
305
+ }
306
+
307
+ // ---- 邊 ----
308
+ function mkEdge(e, i, sh, th, etype) {
309
+ const dash = e.dashed
310
+ const col = dash ? '#c46e1a' : '#44505a'
311
+ return {
312
+ id: 'e' + i, source: e.s, target: e.t,
313
+ sourceHandle: sh || e.sh, targetHandle: th || e.th,
314
+ zIndex: 2, // z-order 最上層: 設 edge.zIndex 後 React Flow 以獨立 wrapper 渲染, 邊與邊標籤皆浮於容器/節點之上, 標籤不被容器填色遮(原則2)
315
+ label: e.l || '', type: etype || e.type || 'default',
316
+ pathOptions: (etype === 'smoothstep' || e.type === 'smoothstep') ? { borderRadius: 10 } : undefined,
317
+ style: { stroke: col, strokeWidth: 2, strokeDasharray: dash ? '6 4' : undefined },
318
+ labelStyle: { fontFamily: 'Microsoft JhengHei,sans-serif', fontSize: 12.5, fill: '#3a444d',
319
+ stroke: '#ffffff', strokeWidth: 3.5, paintOrder: 'stroke' },
320
+ labelBgStyle: { fill: 'transparent', fillOpacity: 0 },
321
+ labelBgPadding: [0, 0], labelBgBorderRadius: 0,
322
+ markerEnd: { type: 'arrowclosed', color: col, width: 18, height: 18 },
323
+ }
324
+ }
325
+
326
+ // 依兩節點絕對 box 幾何, 自動挑選 source/target 邊上的 handle
327
+ function pickHandles(a, b) {
328
+ const acx = a.x + a.w / 2, acy = a.y + a.h / 2
329
+ const bcx = b.x + b.w / 2, bcy = b.y + b.h / 2
330
+ const dx = bcx - acx, dy = bcy - acy
331
+ let sSide, tSide
332
+ if (Math.abs(dx) >= Math.abs(dy)) { sSide = dx >= 0 ? 'r' : 'l'; tSide = dx >= 0 ? 'l' : 'r' }
333
+ else { sSide = dy >= 0 ? 'b' : 't'; tSide = dy >= 0 ? 't' : 'b' }
334
+ return { sh: 's' + sSide, th: 't' + tSide }
335
+ }
336
+
337
+ // ---- 顯式佈局(群組/分層/巢狀圖) ----
338
+ // spec.groups: [{id,label,kind,x,y,w,h,titleSize}]
339
+ // spec.nodes: [{id,label,kind,parent,x,y,w,h,fontSize}] (x,y 為相對 parent; 無 parent 則為絕對)
340
+ function layoutExplicit(spec) {
341
+ const nodes = []
342
+ // 絕對 box 表(供 pickHandles 計算), 含群組容器自身
343
+ const box = {}
344
+ const grpById = {}
345
+ for (const grp of (spec.groups || [])) {
346
+ grpById[grp.id] = grp
347
+ }
348
+ for (const grp of (spec.groups || [])) {
349
+ // 解析群組絕對座標(支援巢狀群組)
350
+ let gx = grp.x, gy = grp.y, gp = grp.parent
351
+ while (gp && grpById[gp]) { gx += grpById[gp].x; gy += grpById[gp].y; gp = grpById[gp].parent }
352
+ box[grp.id] = { x: gx, y: gy, w: grp.w, h: grp.h }
353
+ const gbase = { id: grp.id, type: 'grp', position: { x: grp.x, y: grp.y },
354
+ data: { label: grp.label, kind: grp.kind, titleSize: grp.titleSize, _sz: { w: grp.w, h: grp.h } },
355
+ draggable: false, selectable: false }
356
+ if (grp.parent) { gbase.parentId = grp.parent; gbase.extent = 'parent' }
357
+ nodes.push(gbase)
358
+ }
359
+ for (const n of (spec.nodes || [])) {
360
+ // 解析絕對座標(可能多層 parent)
361
+ let ax = n.x, ay = n.y, p = n.parent
362
+ while (p && grpById[p]) { ax += grpById[p].x; ay += grpById[p].y; p = grpById[p].parent }
363
+ box[n.id] = { x: ax, y: ay, w: n.w, h: n.h }
364
+
365
+ const fs = n.fontSize || 14
366
+ const base = { id: n.id, position: { x: n.x, y: n.y }, draggable: false, selectable: false }
367
+ if (n.parent) { base.parentId = n.parent; base.extent = 'parent' }
368
+ if (n.kind === 'diamond') {
369
+ nodes.push({ ...base, type: 'diamond', data: { label: n.label, fontSize: fs, _sz: { w: n.w, h: n.h } } })
370
+ } else {
371
+ nodes.push({ ...base, type: 'box', data: { label: n.label, fontSize: fs, kind: n.kind, _sz: { w: n.w, h: n.h } } })
372
+ }
373
+ }
374
+ const etype = spec.edgeType || 'smoothstep'
375
+ const edges = (spec.edges || []).map((e, i) => {
376
+ // 群組→群組的層級邊用直線置中; 其餘自動挑 handle
377
+ if (e.sh || e.th) return mkEdge(e, i, null, null, e.type || etype)
378
+ const a = box[e.s], b = box[e.t]
379
+ if (!a || !b) return mkEdge(e, i, null, null, e.type || etype)
380
+ const { sh, th } = pickHandles(a, b)
381
+ return mkEdge(e, i, sh, th, e.type || etype)
382
+ })
383
+ return { nodes, edges }
384
+ }
385
+
386
+ // ---- 主渲染 ----
387
+ let root = null
388
+ window.renderFig = (spec) => new Promise((resolve) => {
389
+ try {
390
+ const stage = document.getElementById('stage')
391
+ // layout: 'dagrec' = 資料驅動 compound dagre(畫布尺寸由佈局自動算出, 無逐圖 W/H)
392
+ let laid
393
+ if (spec.layout === 'dagrec') laid = layoutDagreCompound(spec)
394
+ else if (spec.layout === 'explicit') laid = layoutExplicit(spec)
395
+ else laid = layoutDagre(spec)
396
+ const { nodes, edges } = laid
397
+ const W = laid.W || spec.W, H = laid.H || spec.H
398
+ stage.style.width = W + 'px'
399
+ stage.style.height = H + 'px'
400
+ const App = () => h(ReactFlow, {
401
+ nodes, edges, nodeTypes,
402
+ fitView: true, fitViewOptions: { padding: 0.04 },
403
+ nodesDraggable: false, nodesConnectable: false, elementsSelectable: false,
404
+ panOnDrag: false, zoomOnScroll: false, preventScrolling: false,
405
+ proOptions: { hideAttribution: true }, minZoom: 0.05, maxZoom: 4,
406
+ defaultEdgeOptions: {},
407
+ })
408
+ if (root) root.unmount()
409
+ root = createRoot(stage)
410
+ root.render(h(App))
411
+ setTimeout(() => resolve({ ok: true, w: W, h: H }), 900)
412
+ } catch (e) { resolve({ ok: false, err: String(e && (e.stack || e.message) || e).slice(0, 400) }) }
413
+ })
414
+
415
+ document.title = 'READY'
416
+ } catch (e) {
417
+ window.__err = String(e && (e.stack || e.message) || e).slice(0, 400)
418
+ document.title = 'FAIL'
419
+ }
420
+ </script></body></html>