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
package/test/cases.mjs ADDED
@@ -0,0 +1,349 @@
1
+ // p10 合成測試案例 — 每案隔離「一個 p10 機制」, 失敗時可快速定位是哪個機制被改壞。
2
+ // data 為標準正規化數據 {dir,nodes,edges}(與 9 張報告圖同格式; cls 見 common/palette.mjs)。
3
+ // expect: 選用之比例界線(A4 折排案)。 check(spec,geom,h): 選用之案例專屬斷言(回 [{name,pass,detail}])。
4
+ // 通用不變量(存在性/包含/標題不溢出/不重疊/items框/邊)由 lib.runInvariants 對「所有」案例(含 9 真圖)套用。
5
+
6
+ export const CASES = [
7
+
8
+ // 1) 最基本: 純 label 矩形節點 + 實線箭頭, 直式
9
+ {
10
+ name: 'flat-tb',
11
+ desc: '基本 — 純 label 矩形 + 實線箭頭 (TB)',
12
+ data: {
13
+ dir: 'TB',
14
+ nodes: [
15
+ { id: 'A', label: '節點 A', cls: 'blue' },
16
+ { id: 'B', label: '節點 B', cls: 'green' },
17
+ { id: 'C', label: '節點 C', cls: 'orange' },
18
+ ],
19
+ edges: [{ from: 'A', to: 'B' }, { from: 'B', to: 'C' }],
20
+ },
21
+ },
22
+
23
+ // 2) 標題+items 富節點(6 層框: 框背景/標題背景/框線/分隔線/標題/items); 不同 item 數驗框高算法
24
+ {
25
+ name: 'items-node',
26
+ desc: '富節點 — 標題+items 分層框 (含框高算法)',
27
+ data: {
28
+ dir: 'TB',
29
+ nodes: [
30
+ { id: 'X', title: '服務 X', items: ['項目一', '項目二', '項目三', '項目四', '項目五'], cls: 'orange' },
31
+ { id: 'Y', title: '服務 Y', items: ['只有一項'], cls: 'orange' },
32
+ ],
33
+ edges: [{ from: 'X', to: 'Y', label: '呼叫' }],
34
+ },
35
+ check(spec, geom, h) {
36
+ const X = h.node('X'); const Y = h.node('Y')
37
+ return [
38
+ { name: 'X 為 items 框', pass: !!X && X.kind === 'items' && X.headH > 0, detail: X && `kind=${X.kind} headH=${X.headH}` },
39
+ { name: 'Y 為 items 框', pass: !!Y && Y.kind === 'items' && Y.headH > 0, detail: Y && `kind=${Y.kind} headH=${Y.headH}` },
40
+ // 多 item 框應高於少 item 框(框高隨內容, 非固定)
41
+ { name: '框高隨 item 數', pass: !!X && !!Y && X.h > Y.h, detail: X && Y && `X.h=${X.h} Y.h=${Y.h}` },
42
+ ]
43
+ },
44
+ },
45
+
46
+ // 3) 二層巢狀容器 + 跨容器邊(LCA 邊路由: a→c 應掛在最近共同祖先 OUT)
47
+ {
48
+ name: 'nested-group',
49
+ desc: '巢狀 — 二層容器 + 跨內層邊 (LCA 路由)',
50
+ data: {
51
+ dir: 'TB',
52
+ nodes: [
53
+ { id: 'OUT', label: '外層容器', cls: 'blueG' },
54
+ { id: 'IN1', label: '內層 A', cls: 'greenG', group: 'OUT' },
55
+ { id: 'IN2', label: '內層 B', cls: 'greenG', group: 'OUT' },
56
+ { id: 'a', label: 'a', cls: 'green', group: 'IN1' },
57
+ { id: 'b', label: 'b', cls: 'green', group: 'IN1' },
58
+ { id: 'c', label: 'c', cls: 'green', group: 'IN2' },
59
+ ],
60
+ edges: [{ from: 'a', to: 'b' }, { from: 'a', to: 'c', label: '跨層' }],
61
+ },
62
+ check(spec, geom, h) {
63
+ const OUT = h.group('OUT'); const IN1 = h.group('IN1'); const IN2 = h.group('IN2')
64
+ return [
65
+ { name: '三層深度齊備', pass: !!OUT && !!IN1 && !!IN2 && OUT.depth === 0 && IN1.depth === 1 && IN2.depth === 1, detail: `OUT=${OUT && OUT.depth} IN1=${IN1 && IN1.depth} IN2=${IN2 && IN2.depth}` },
66
+ ]
67
+ },
68
+ },
69
+
70
+ // 4) 容器標題比成員寬 → 2-pass padding 撐寬, 標題不溢出(此前修過的 bug)
71
+ {
72
+ name: 'title-overflow',
73
+ desc: '標題撐寬 — 容器標題比成員寬, 2-pass 不溢出',
74
+ data: {
75
+ dir: 'TB',
76
+ nodes: [
77
+ { id: 'WIDE', label: '這是一個相當長的容器標題用於測試自動撐寬', cls: 'purpleG' },
78
+ { id: 's1', label: '短', cls: 'purple', group: 'WIDE' },
79
+ { id: 's2', label: 'A', cls: 'purple', group: 'WIDE' },
80
+ ],
81
+ edges: [{ from: 's1', to: 's2' }],
82
+ },
83
+ check(spec, geom, h) {
84
+ const W = h.group('WIDE')
85
+ return [
86
+ { name: '標題寬 ≤ 容器寬', pass: !!W && W.labelW <= W.w + 2, detail: W && `labelW=${W.labelW} w=${W.w}` },
87
+ ]
88
+ },
89
+ },
90
+
91
+ // 5) 決策菱形 + 通過/退回分支
92
+ {
93
+ name: 'diamond',
94
+ desc: '菱形 — 決策節點 + 是/否分支',
95
+ data: {
96
+ dir: 'TB',
97
+ nodes: [
98
+ { id: 'P', label: '輸入', cls: 'blue' },
99
+ { id: 'D', label: '判斷?', cls: 'diamond' },
100
+ { id: 'Y', label: '通過', cls: 'green' },
101
+ { id: 'N', label: '退回', cls: 'red' },
102
+ ],
103
+ edges: [{ from: 'P', to: 'D' }, { from: 'D', to: 'Y', label: '是' }, { from: 'D', to: 'N', label: '否' }],
104
+ },
105
+ check(spec, geom, h) {
106
+ const D = h.node('D')
107
+ return [{ name: 'D 為菱形', pass: !!D && D.kind === 'diamond', detail: D && `kind=${D.kind}` }]
108
+ },
109
+ },
110
+
111
+ // 6) 邊樣式: 實線 / 虛線(dashed) / 帶標籤(白光暈) / 短邊(ensureStub)
112
+ {
113
+ name: 'edges-mix',
114
+ desc: '邊樣式 — 實線/虛線/帶標籤/短邊 stub',
115
+ data: {
116
+ dir: 'TB',
117
+ nodes: [
118
+ { id: 'A', label: '來源', cls: 'blue' },
119
+ { id: 'B', label: '目標一', cls: 'green' },
120
+ { id: 'C', label: '目標二', cls: 'orange' },
121
+ ],
122
+ edges: [
123
+ { from: 'A', to: 'B', label: '實線標籤' },
124
+ { from: 'A', to: 'C', kind: 'dashed', label: '虛線標籤' },
125
+ { from: 'B', to: 'C' },
126
+ ],
127
+ },
128
+ },
129
+
130
+ // 7) A4 過高 → 折排: 最高之多子容器(垂直鏈)折成多欄縮高
131
+ {
132
+ name: 'fold-tall',
133
+ desc: 'A4 折排 — 過高, 多子容器折成多欄',
134
+ data: {
135
+ dir: 'TB',
136
+ nodes: [
137
+ { id: 'G', label: '垂直長鏈容器', cls: 'blueG' },
138
+ ...Array.from({ length: 7 }, (_, i) => ({ id: 'n' + i, label: '步驟 ' + (i + 1), cls: 'blue', group: 'G' })),
139
+ ],
140
+ edges: Array.from({ length: 6 }, (_, i) => ({ from: 'n' + i, to: 'n' + (i + 1) })),
141
+ },
142
+ expect: { ratioMax: 1.45 },
143
+ check(spec, geom, h) {
144
+ const xs = new Set(geom.nodes.filter(n => n.parent === 'G').map(n => Math.round(n.x / 20)))
145
+ return [{ name: '已折成 ≥2 欄', pass: xs.size >= 2, detail: '欄數(x 帶)=' + xs.size }]
146
+ },
147
+ },
148
+
149
+ // 8) A4 過寬 → 折排: 最寬之多子容器(水平鏈)折成多列增高
150
+ {
151
+ name: 'fold-wide',
152
+ desc: 'A4 折排 — 過寬, 多子容器折成多列',
153
+ data: {
154
+ dir: 'LR',
155
+ nodes: [
156
+ { id: 'G', label: '水平長鏈容器', cls: 'orangeG' },
157
+ ...Array.from({ length: 7 }, (_, i) => ({ id: 'n' + i, label: '階段 ' + (i + 1), cls: 'orange', group: 'G' })),
158
+ ],
159
+ edges: Array.from({ length: 6 }, (_, i) => ({ from: 'n' + i, to: 'n' + (i + 1) })),
160
+ },
161
+ expect: { ratioMin: 0.70 },
162
+ check(spec, geom, h) {
163
+ const ys = new Set(geom.nodes.filter(n => n.parent === 'G').map(n => Math.round(n.y / 20)))
164
+ return [{ name: '已折成 ≥2 列', pass: ys.size >= 2, detail: '列數(y 帶)=' + ys.size }]
165
+ },
166
+ },
167
+
168
+ // 9) 容器內矩形框垂直對齊: top / center / bottom(各容器一高框一矮框, 驗矮框位置)
169
+ {
170
+ name: 'valign',
171
+ desc: '垂直對齊 — 容器內框 top/center/bottom',
172
+ data: {
173
+ dir: 'TB',
174
+ nodes: [
175
+ { id: 'gT', label: 'top 對齊', cls: 'blueG', align: 'top' },
176
+ { id: 'tT', title: '高框', items: ['一', '二', '三', '四', '五'], cls: 'blue', group: 'gT' },
177
+ { id: 'sT', label: '矮', cls: 'green', group: 'gT' },
178
+ { id: 'gC', label: 'center 對齊', cls: 'blueG', align: 'center' },
179
+ { id: 'tC', title: '高框', items: ['一', '二', '三', '四', '五'], cls: 'blue', group: 'gC' },
180
+ { id: 'sC', label: '矮', cls: 'green', group: 'gC' },
181
+ { id: 'gB', label: 'bottom 對齊', cls: 'blueG', align: 'bottom' },
182
+ { id: 'tB', title: '高框', items: ['一', '二', '三', '四', '五'], cls: 'blue', group: 'gB' },
183
+ { id: 'sB', label: '矮', cls: 'green', group: 'gB' },
184
+ ],
185
+ edges: [],
186
+ },
187
+ check(spec, geom, h) {
188
+ const pair = (t, s) => {
189
+ const T = h.node(t); const S = h.node(s); return { T, S }
190
+ }
191
+ const top = pair('tT', 'sT'); const cen = pair('tC', 'sC'); const bot = pair('tB', 'sB')
192
+ const okTop = top.T && top.S && Math.abs(top.S.y - top.T.y) <= 3
193
+ const okCen = cen.T && cen.S && Math.abs((cen.S.y + cen.S.h / 2) - (cen.T.y + cen.T.h / 2)) <= 4
194
+ const okBot = bot.T && bot.S && Math.abs((bot.S.y + bot.S.h) - (bot.T.y + bot.T.h)) <= 3
195
+ return [
196
+ { name: 'top: 矮框頂端對齊', pass: okTop, detail: top.T && top.S && `Δtop=${(top.S.y - top.T.y).toFixed(1)}` },
197
+ { name: 'center: 矮框置中對齊', pass: okCen, detail: cen.T && cen.S && `Δmid=${((cen.S.y + cen.S.h / 2) - (cen.T.y + cen.T.h / 2)).toFixed(1)}` },
198
+ { name: 'bottom: 矮框底端對齊', pass: okBot, detail: bot.T && bot.S && `Δbot=${((bot.S.y + bot.S.h) - (bot.T.y + bot.T.h)).toFixed(1)}` },
199
+ ]
200
+ },
201
+ },
202
+
203
+ // 10) 水平佈局 dir:LR(含一個包覆容器)
204
+ {
205
+ name: 'lr-flat',
206
+ desc: '水平佈局 — dir:LR + 包覆容器',
207
+ data: {
208
+ dir: 'LR',
209
+ nodes: [
210
+ { id: 'A', label: '入口', cls: 'blue' },
211
+ { id: 'GP', label: '處理群組', cls: 'greenG' },
212
+ { id: 'B', label: '處理一', cls: 'green', group: 'GP' },
213
+ { id: 'C', label: '處理二', cls: 'green', group: 'GP' },
214
+ { id: 'D', label: '出口', cls: 'orange' },
215
+ ],
216
+ edges: [{ from: 'A', to: 'B' }, { from: 'B', to: 'C' }, { from: 'C', to: 'D' }],
217
+ },
218
+ },
219
+
220
+ // 11) 循環退回邊(hasCycle 路徑: cycleBreaking MODEL_ORDER, 根層不加位置提示; 入口節點應仍在頂端)
221
+ {
222
+ name: 'cycle',
223
+ desc: '循環 — 決策退回邊 (cycleBreaking)',
224
+ data: {
225
+ dir: 'TB',
226
+ nodes: [
227
+ { id: 'S', label: '開始', cls: 'blue' },
228
+ { id: 'W', label: '處理', cls: 'green' },
229
+ { id: 'D', label: '審核?', cls: 'diamond' },
230
+ { id: 'E', label: '完成', cls: 'done' },
231
+ ],
232
+ edges: [
233
+ { from: 'S', to: 'W' },
234
+ { from: 'W', to: 'D' },
235
+ { from: 'D', to: 'E', label: '通過' },
236
+ { from: 'D', to: 'W', kind: 'dashed', label: '退回' },
237
+ ],
238
+ },
239
+ check(spec, geom, h) {
240
+ const S = h.node('S')
241
+ const minY = Math.min(...geom.nodes.map(n => n.y))
242
+ return [
243
+ { name: '入口節點居頂(不因循環偏移)', pass: !!S && S.y <= minY + 2, detail: S && `S.y=${S.y} minY=${minY}` },
244
+ ]
245
+ },
246
+ },
247
+
248
+ // 12) 三層巢狀容器 + 由最深葉節點連出至頂層節點(LCA=root 之跨三層邊)
249
+ {
250
+ name: 'deep-nest',
251
+ desc: '深巢狀 — 三層容器 + 跨三層邊',
252
+ data: {
253
+ dir: 'TB',
254
+ nodes: [
255
+ { id: 'L1', label: '第一層', cls: 'blueG' },
256
+ { id: 'L2', label: '第二層', cls: 'greenG', group: 'L1' },
257
+ { id: 'L3', label: '第三層', cls: 'orangeG', group: 'L2' },
258
+ { id: 'deep', label: '最深節點', cls: 'orange', group: 'L3' },
259
+ { id: 'top', label: '頂層節點', cls: 'blue' },
260
+ ],
261
+ edges: [{ from: 'deep', to: 'top', label: '跨三層' }],
262
+ },
263
+ check(spec, geom, h) {
264
+ const L1 = h.group('L1'); const L2 = h.group('L2'); const L3 = h.group('L3')
265
+ return [
266
+ { name: '容器深度 0/1/2', pass: !!L1 && !!L2 && !!L3 && L1.depth === 0 && L2.depth === 1 && L3.depth === 2, detail: `L1=${L1 && L1.depth} L2=${L2 && L2.depth} L3=${L3 && L3.depth}` },
267
+ ]
268
+ },
269
+ },
270
+
271
+ // 13) 英文數據(半形括號/斜線之衍生 label 情境 + 長英文識別字折行不攔腰斷)
272
+ {
273
+ name: 'english',
274
+ desc: '英文 — 半形標籤 + items 富節點 + 長識別字',
275
+ data: {
276
+ dir: 'TB',
277
+ nodes: [
278
+ { id: 'api', label: 'API Gateway', cls: 'blue' },
279
+ { id: 'svc', title: 'Auth Service', items: ['login/logout', 'token refresh (JWT)', 'session mgmt'], cls: 'green' },
280
+ { id: 'cfg', label: 'InternationalizationConfigurationManager', cls: 'orange' },
281
+ ],
282
+ edges: [{ from: 'api', to: 'svc', label: 'HTTPS' }, { from: 'svc', to: 'cfg', kind: 'dashed' }],
283
+ },
284
+ check(spec, geom, h) {
285
+ const svc = h.node('svc')
286
+ return [
287
+ { name: 'svc 為 items 框', pass: !!svc && svc.kind === 'items' && svc.headH > 0, detail: svc && `kind=${svc.kind} headH=${svc.headH}` },
288
+ ]
289
+ },
290
+ },
291
+
292
+ // 14) 長中文標籤折行(measure maxW=200 → 框寬受限, 不無限撐寬)
293
+ {
294
+ name: 'long-label',
295
+ desc: '長標籤 — 超長中文自動折行, 框寬受 MAXW 限制',
296
+ data: {
297
+ dir: 'TB',
298
+ nodes: [
299
+ { id: 'L', label: '這是一段非常非常長的節點標籤文字用於驗證自動折行機制與框寬上限行為', cls: 'purple' },
300
+ { id: 'B', label: '下游', cls: 'blue' },
301
+ ],
302
+ edges: [{ from: 'L', to: 'B' }],
303
+ },
304
+ check(spec, geom, h) {
305
+ const L = h.node('L')
306
+ return [
307
+ { name: '框寬受 MAXW 限制(≤230)', pass: !!L && L.w <= 230, detail: L && `w=${L.w}` },
308
+ { name: '折行後高於單行(≥2行)', pass: !!L && L.h >= 50, detail: L && `h=${L.h}` },
309
+ ]
310
+ },
311
+ },
312
+
313
+ // 15) 無邊圖(純節點+容器; 邊相關機制全跳過仍須正確佈局)
314
+ {
315
+ name: 'no-edges',
316
+ desc: '無邊 — 純節點與容器, 無任何連線',
317
+ data: {
318
+ dir: 'TB',
319
+ nodes: [
320
+ { id: 'G', label: '群組', cls: 'greenG' },
321
+ { id: 'a', label: '甲', cls: 'green', group: 'G' },
322
+ { id: 'b', label: '乙', cls: 'green', group: 'G' },
323
+ { id: 'solo', label: '獨立節點', cls: 'blue' },
324
+ ],
325
+ edges: [],
326
+ },
327
+ },
328
+
329
+ // 16) 群組對群組邊 + 葉節點對群組邊(端點為複合容器之階層邊)
330
+ {
331
+ name: 'group-edge',
332
+ desc: '群組邊 — 群組↔群組 / 葉節點↔群組',
333
+ data: {
334
+ dir: 'TB',
335
+ nodes: [
336
+ { id: 'G1', label: '來源群組', cls: 'blueG' },
337
+ { id: 'x', label: 'x', cls: 'blue', group: 'G1' },
338
+ { id: 'G2', label: '目標群組', cls: 'orangeG' },
339
+ { id: 'y', label: 'y', cls: 'orange', group: 'G2' },
340
+ { id: 'ext', label: '外部節點', cls: 'green' },
341
+ ],
342
+ edges: [
343
+ { from: 'G1', to: 'G2', label: '群組相依' },
344
+ { from: 'ext', to: 'G2', kind: 'dashed' },
345
+ ],
346
+ },
347
+ },
348
+
349
+ ]
@@ -0,0 +1,92 @@
1
+ {
2
+ "dir": "TB",
3
+ "nodes": [
4
+ {
5
+ "id": "s1",
6
+ "title": "執行單位產製詮釋資料",
7
+ "items": [
8
+ "Excel 模板+附件"
9
+ ],
10
+ "cls": "blue"
11
+ },
12
+ {
13
+ "id": "s2",
14
+ "label": "上傳(分片)",
15
+ "cls": "blue"
16
+ },
17
+ {
18
+ "id": "s3",
19
+ "label": "移至批次區",
20
+ "cls": "blue"
21
+ },
22
+ {
23
+ "id": "s4",
24
+ "title": "格式檢測",
25
+ "items": [
26
+ "欄位",
27
+ "必填",
28
+ "重複編號",
29
+ "重複檔案 SHA",
30
+ "引用關聯"
31
+ ],
32
+ "cls": "blue"
33
+ },
34
+ {
35
+ "id": "s5",
36
+ "label": "檢測摘要報告",
37
+ "cls": "blue"
38
+ },
39
+ {
40
+ "id": "s6",
41
+ "label": "管理員確認",
42
+ "cls": "diamond"
43
+ },
44
+ {
45
+ "id": "s7",
46
+ "label": "寫入資料庫",
47
+ "cls": "done"
48
+ },
49
+ {
50
+ "id": "s8",
51
+ "label": "查詢/展示/報表輸出",
52
+ "cls": "done"
53
+ }
54
+ ],
55
+ "edges": [
56
+ {
57
+ "from": "s1",
58
+ "to": "s2"
59
+ },
60
+ {
61
+ "from": "s2",
62
+ "to": "s3"
63
+ },
64
+ {
65
+ "from": "s3",
66
+ "to": "s4"
67
+ },
68
+ {
69
+ "from": "s4",
70
+ "to": "s5"
71
+ },
72
+ {
73
+ "from": "s5",
74
+ "to": "s6"
75
+ },
76
+ {
77
+ "from": "s6",
78
+ "to": "s7",
79
+ "label": "通過"
80
+ },
81
+ {
82
+ "from": "s7",
83
+ "to": "s8"
84
+ },
85
+ {
86
+ "from": "s6",
87
+ "to": "s1",
88
+ "label": "退回",
89
+ "kind": "dashed"
90
+ }
91
+ ]
92
+ }
@@ -0,0 +1,112 @@
1
+ {
2
+ "dir": "TB",
3
+ "nodes": [
4
+ {
5
+ "id": "APP",
6
+ "title": "App/AppMod",
7
+ "items": [
8
+ "前端入口"
9
+ ],
10
+ "cls": "blue"
11
+ },
12
+ {
13
+ "id": "LOGIN",
14
+ "title": "登入",
15
+ "items": [
16
+ "SSO 驗證+取權限"
17
+ ],
18
+ "cls": "red"
19
+ },
20
+ {
21
+ "id": "LAYOUT",
22
+ "title": "Layout",
23
+ "items": [
24
+ "主畫面選單+各頁面元件"
25
+ ],
26
+ "cls": "blue"
27
+ },
28
+ {
29
+ "id": "CH",
30
+ "label": "$fapi 兩通道",
31
+ "cls": "greenG"
32
+ },
33
+ {
34
+ "id": "WS",
35
+ "title": "即時資料介接",
36
+ "items": [
37
+ "全表推播 → Vuex Store"
38
+ ],
39
+ "cls": "green",
40
+ "group": "CH"
41
+ },
42
+ {
43
+ "id": "HTTP",
44
+ "title": "HTTP 檔案/批次",
45
+ "items": [
46
+ "upload",
47
+ "download",
48
+ "batchUploadCntr"
49
+ ],
50
+ "cls": "green",
51
+ "group": "CH"
52
+ },
53
+ {
54
+ "id": "STORE",
55
+ "title": "Vuex Store",
56
+ "items": [
57
+ "keyTable",
58
+ "syncState",
59
+ "loading"
60
+ ],
61
+ "cls": "purple"
62
+ },
63
+ {
64
+ "id": "SRV",
65
+ "label": "srv 主服務",
66
+ "cls": "orange"
67
+ },
68
+ {
69
+ "id": "BATSRV",
70
+ "label": "srvBatch 批次服務",
71
+ "cls": "orange"
72
+ }
73
+ ],
74
+ "edges": [
75
+ {
76
+ "from": "APP",
77
+ "to": "LOGIN"
78
+ },
79
+ {
80
+ "from": "LOGIN",
81
+ "to": "LAYOUT"
82
+ },
83
+ {
84
+ "from": "LAYOUT",
85
+ "to": "WS",
86
+ "label": "通道一"
87
+ },
88
+ {
89
+ "from": "LAYOUT",
90
+ "to": "HTTP",
91
+ "label": "通道二"
92
+ },
93
+ {
94
+ "from": "WS",
95
+ "to": "STORE",
96
+ "label": "推播"
97
+ },
98
+ {
99
+ "from": "STORE",
100
+ "to": "LAYOUT",
101
+ "label": "讀取"
102
+ },
103
+ {
104
+ "from": "WS",
105
+ "to": "SRV"
106
+ },
107
+ {
108
+ "from": "HTTP",
109
+ "to": "BATSRV"
110
+ }
111
+ ]
112
+ }
@@ -0,0 +1,116 @@
1
+ {
2
+ "dir": "LR",
3
+ "nodes": [
4
+ {
5
+ "id": "SRVS",
6
+ "label": "後端服務程式",
7
+ "cls": "orangeG"
8
+ },
9
+ {
10
+ "id": "srv",
11
+ "title": "srv 主服務",
12
+ "items": [
13
+ "ORM CRUD",
14
+ "全表推播+擴充函數"
15
+ ],
16
+ "cls": "orange",
17
+ "group": "SRVS"
18
+ },
19
+ {
20
+ "id": "api",
21
+ "title": "srvApi 對外服務",
22
+ "items": [
23
+ "REST 每表 GET /api/表名"
24
+ ],
25
+ "cls": "orange",
26
+ "group": "SRVS"
27
+ },
28
+ {
29
+ "id": "bat",
30
+ "title": "srvBatch 批次服務",
31
+ "items": [
32
+ "批次任務",
33
+ "大檔分片"
34
+ ],
35
+ "cls": "orange",
36
+ "group": "SRVS"
37
+ },
38
+ {
39
+ "id": "prv",
40
+ "title": "srvProvide 推送服務",
41
+ "items": [
42
+ "排程每 30 秒推送"
43
+ ],
44
+ "cls": "orange",
45
+ "group": "SRVS"
46
+ },
47
+ {
48
+ "id": "FE",
49
+ "label": "前端應用",
50
+ "cls": "blue"
51
+ },
52
+ {
53
+ "id": "EXT1",
54
+ "label": "外部應用系統",
55
+ "cls": "purple"
56
+ },
57
+ {
58
+ "id": "DB",
59
+ "label": "資料庫(LMDB)",
60
+ "cls": "green"
61
+ },
62
+ {
63
+ "id": "EXT2",
64
+ "label": "權限管理系統",
65
+ "cls": "purple"
66
+ },
67
+ {
68
+ "id": "EXT3",
69
+ "label": "API 管理系統",
70
+ "cls": "purple"
71
+ }
72
+ ],
73
+ "edges": [
74
+ {
75
+ "from": "FE",
76
+ "to": "srv",
77
+ "label": "即時資料介接"
78
+ },
79
+ {
80
+ "from": "FE",
81
+ "to": "bat",
82
+ "label": "HTTP 分片"
83
+ },
84
+ {
85
+ "from": "EXT1",
86
+ "to": "api",
87
+ "label": "REST 取用詮釋資料"
88
+ },
89
+ {
90
+ "from": "srv",
91
+ "to": "DB"
92
+ },
93
+ {
94
+ "from": "api",
95
+ "to": "DB"
96
+ },
97
+ {
98
+ "from": "bat",
99
+ "to": "DB"
100
+ },
101
+ {
102
+ "from": "prv",
103
+ "to": "DB"
104
+ },
105
+ {
106
+ "from": "prv",
107
+ "to": "EXT2",
108
+ "label": "providePerm 推權限目標"
109
+ },
110
+ {
111
+ "from": "prv",
112
+ "to": "EXT3",
113
+ "label": "provideApi 推 API 說明"
114
+ }
115
+ ]
116
+ }