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,117 @@
1
+ {
2
+ "dir": "LR",
3
+ "nodes": [
4
+ {
5
+ "id": "APP",
6
+ "label": "應用系統",
7
+ "cls": "purpleG"
8
+ },
9
+ {
10
+ "id": "a1",
11
+ "label": "GIS2D",
12
+ "cls": "purple",
13
+ "group": "APP"
14
+ },
15
+ {
16
+ "id": "a2",
17
+ "label": "GIS3D",
18
+ "cls": "purple",
19
+ "group": "APP"
20
+ },
21
+ {
22
+ "id": "a3",
23
+ "label": "需求管理",
24
+ "cls": "purple",
25
+ "group": "APP"
26
+ },
27
+ {
28
+ "id": "a4",
29
+ "label": "知識管理",
30
+ "cls": "purple",
31
+ "group": "APP"
32
+ },
33
+ {
34
+ "id": "a5",
35
+ "label": "特徵事件作用",
36
+ "cls": "purple",
37
+ "group": "APP"
38
+ },
39
+ {
40
+ "id": "a6",
41
+ "label": "評估模式流程",
42
+ "cls": "purple",
43
+ "group": "APP"
44
+ },
45
+ {
46
+ "id": "a7",
47
+ "label": "源項",
48
+ "cls": "purple",
49
+ "group": "APP"
50
+ },
51
+ {
52
+ "id": "API",
53
+ "label": "API 管理系統",
54
+ "cls": "blue"
55
+ },
56
+ {
57
+ "id": "PDM",
58
+ "label": "計畫資料管理系統",
59
+ "cls": "blue"
60
+ },
61
+ {
62
+ "id": "BAT",
63
+ "label": "批次上傳系統",
64
+ "cls": "orange"
65
+ },
66
+ {
67
+ "id": "AUTH",
68
+ "label": "單一登入+權限管理",
69
+ "cls": "red"
70
+ },
71
+ {
72
+ "id": "URLC",
73
+ "label": "輔助網址控制系統",
74
+ "cls": "orange"
75
+ }
76
+ ],
77
+ "edges": [
78
+ {
79
+ "from": "APP",
80
+ "to": "API",
81
+ "label": "取用詮釋資料"
82
+ },
83
+ {
84
+ "from": "API",
85
+ "to": "PDM"
86
+ },
87
+ {
88
+ "from": "BAT",
89
+ "to": "PDM",
90
+ "label": "匯入"
91
+ },
92
+ {
93
+ "from": "AUTH",
94
+ "to": "APP",
95
+ "label": "共用驗證",
96
+ "kind": "dashed"
97
+ },
98
+ {
99
+ "from": "AUTH",
100
+ "to": "PDM",
101
+ "label": "共用驗證",
102
+ "kind": "dashed"
103
+ },
104
+ {
105
+ "from": "AUTH",
106
+ "to": "API",
107
+ "label": "共用驗證",
108
+ "kind": "dashed"
109
+ },
110
+ {
111
+ "from": "URLC",
112
+ "to": "APP",
113
+ "label": "跨系統調閱",
114
+ "kind": "dashed"
115
+ }
116
+ ]
117
+ }
@@ -0,0 +1,162 @@
1
+ {
2
+ "dir": "LR",
3
+ "nodes": [
4
+ {
5
+ "id": "L",
6
+ "title": "登入",
7
+ "items": [
8
+ "SSO"
9
+ ],
10
+ "cls": "red"
11
+ },
12
+ {
13
+ "id": "M",
14
+ "title": "主畫面",
15
+ "items": [
16
+ "選單"
17
+ ],
18
+ "cls": "blue"
19
+ },
20
+ {
21
+ "id": "p0",
22
+ "label": "主要資訊",
23
+ "cls": "purple"
24
+ },
25
+ {
26
+ "id": "p1",
27
+ "label": "計畫資料管理",
28
+ "cls": "purple"
29
+ },
30
+ {
31
+ "id": "p2",
32
+ "label": "輔助參考文獻",
33
+ "cls": "purple"
34
+ },
35
+ {
36
+ "id": "p3",
37
+ "label": "需求管理",
38
+ "cls": "purple"
39
+ },
40
+ {
41
+ "id": "p4",
42
+ "label": "知識管理",
43
+ "cls": "purple"
44
+ },
45
+ {
46
+ "id": "p5",
47
+ "label": "特徵事件作用",
48
+ "cls": "purple"
49
+ },
50
+ {
51
+ "id": "p6",
52
+ "label": "評估模式流程",
53
+ "cls": "purple"
54
+ },
55
+ {
56
+ "id": "p7",
57
+ "label": "源項",
58
+ "cls": "purple"
59
+ },
60
+ {
61
+ "id": "p8",
62
+ "label": "GIS2D",
63
+ "cls": "purple"
64
+ },
65
+ {
66
+ "id": "p9",
67
+ "label": "GIS3D",
68
+ "cls": "purple"
69
+ },
70
+ {
71
+ "id": "p10",
72
+ "label": "模板下載",
73
+ "cls": "purple"
74
+ },
75
+ {
76
+ "id": "p11",
77
+ "label": "輔助工具",
78
+ "cls": "purple"
79
+ },
80
+ {
81
+ "id": "p12",
82
+ "label": "批次上傳",
83
+ "cls": "purple"
84
+ },
85
+ {
86
+ "id": "p13",
87
+ "label": "API 管理",
88
+ "cls": "purple"
89
+ },
90
+ {
91
+ "id": "p14",
92
+ "label": "後台管理",
93
+ "cls": "purple"
94
+ }
95
+ ],
96
+ "edges": [
97
+ {
98
+ "from": "L",
99
+ "to": "M"
100
+ },
101
+ {
102
+ "from": "M",
103
+ "to": "p0"
104
+ },
105
+ {
106
+ "from": "M",
107
+ "to": "p1"
108
+ },
109
+ {
110
+ "from": "M",
111
+ "to": "p2"
112
+ },
113
+ {
114
+ "from": "M",
115
+ "to": "p3"
116
+ },
117
+ {
118
+ "from": "M",
119
+ "to": "p4"
120
+ },
121
+ {
122
+ "from": "M",
123
+ "to": "p5"
124
+ },
125
+ {
126
+ "from": "M",
127
+ "to": "p6"
128
+ },
129
+ {
130
+ "from": "M",
131
+ "to": "p7"
132
+ },
133
+ {
134
+ "from": "M",
135
+ "to": "p8"
136
+ },
137
+ {
138
+ "from": "M",
139
+ "to": "p9"
140
+ },
141
+ {
142
+ "from": "M",
143
+ "to": "p10"
144
+ },
145
+ {
146
+ "from": "M",
147
+ "to": "p11"
148
+ },
149
+ {
150
+ "from": "M",
151
+ "to": "p12"
152
+ },
153
+ {
154
+ "from": "M",
155
+ "to": "p13"
156
+ },
157
+ {
158
+ "from": "M",
159
+ "to": "p14"
160
+ }
161
+ ]
162
+ }
@@ -0,0 +1,10 @@
1
+ // 9 張真圖正規化數據索引(供回歸測試): 讀 test/data/*.json, 衍生 rich 節點 label
2
+ import fs from 'fs'
3
+ import { fileURLToPath } from 'url'
4
+ import { dirname, resolve } from 'path'
5
+ import { deriveLabels } from '../src/common/derive.mjs'
6
+
7
+ const __dir = dirname(fileURLToPath(import.meta.url))
8
+ const KEYS = ['系統架構圖', '系統關聯圖', '作業流程圖', '線上畫面關聯圖', '程式分層架構圖', '程式元件組成圖', '後端服務組成圖', '批次上傳處理流程圖', '前端架構與資料流圖']
9
+
10
+ export const FIGURES = KEYS.map(key => ({ key, data: deriveLabels(JSON.parse(fs.readFileSync(resolve(__dir, 'data', key + '.json'), 'utf8'))) }))
package/test/lib.mjs ADDED
@@ -0,0 +1,116 @@
1
+ // p10 測試共用 — 重用 gen.mjs 之「真實」管線(translate + PAGE_HTML + window.renderFig), 不複製渲染器(複製會與 production 分叉, 失去測試意義)。
2
+ // openStage(): 啟一個 headless 舞台頁(elkjs + renderFig 就緒)
3
+ // renderCase(page, data): 跑單案真實管線 → { spec, res(含 geom 幾何), svg(字串), png(Buffer) }
4
+ // runInvariants(spec, geom): 對「任何正確 p10 輸出皆須成立」之結構不變量做斷言(spec 翻譯, 非現狀指紋)
5
+ import { chromium } from 'playwright'
6
+ import { translate, PAGE_HTML } from '../src/p10/gen.mjs'
7
+
8
+ export { translate }
9
+
10
+ export async function openStage() {
11
+ const browser = await chromium.launch()
12
+ const page = await browser.newPage({ deviceScaleFactor: 2 })
13
+ const errs = []
14
+ page.on('pageerror', e => errs.push(e.message))
15
+ page.on('console', m => {
16
+ if (m.type() === 'error') errs.push('c:' + m.text().slice(0, 200))
17
+ })
18
+ await page.setContent(PAGE_HTML, { waitUntil: 'load' })
19
+ await page.waitForFunction(() => window.__ready, { timeout: 60000 }).catch(() => {})
20
+ await page.evaluate(() => document.fonts && document.fonts.ready)
21
+ return { browser, page, errs }
22
+ }
23
+
24
+ // 跑單一案例之真實管線 → { spec, res, svg, png }
25
+ export async function renderCase(page, data) {
26
+ const spec = translate(data)
27
+ const res = await page.evaluate(s => window.renderFig(s), spec)
28
+ if (!res.ok) return { spec, res, svg: null, png: null }
29
+ await page.evaluate(() => document.fonts && document.fonts.ready)
30
+ await page.waitForTimeout(120)
31
+ const svg = await page.evaluate(() => document.getElementById('stage').innerHTML)
32
+ const png = await page.locator('#stage svg').screenshot()
33
+ return { spec, res, svg, png }
34
+ }
35
+
36
+ // 幾何便捷查詢
37
+ export function helpers(geom) {
38
+ return {
39
+ node: id => geom.nodes.find(n => n.id === id),
40
+ group: id => geom.groups.find(g => g.id === id),
41
+ }
42
+ }
43
+
44
+ const within = (c, p, tol) => c.x >= p.x - tol && c.y >= p.y - tol && c.x + c.w <= p.x + p.w + tol && c.y + c.h <= p.y + p.h + tol
45
+ const overlap = (a, b, tol) => a.x < b.x + b.w - tol && b.x < a.x + a.w - tol && a.y < b.y + b.h - tol && b.y < a.y + a.h - tol
46
+ const near = (p, box, tol) => p.x >= box.x - tol && p.x <= box.x + box.w + tol && p.y >= box.y - tol && p.y <= box.y + box.h + tol
47
+
48
+ // 通用結構不變量(對所有案例 + 9 真圖皆套用)。每項對應「正確 p10 輸出必成立」之一句 spec, 非「跟上次一樣」之指紋。
49
+ // 回 [{ name, pass, detail }]
50
+ export function runInvariants(spec, geom) {
51
+ const out = []
52
+ const bbox = {}; geom.groups.forEach(g => bbox[g.id] = g); geom.nodes.forEach(n => bbox[n.id] = n)
53
+
54
+ // 1) 存在性: 每個邏輯節點/群組都被畫出(重構不可漏掉元素)
55
+ {
56
+ const gNodes = new Set(geom.nodes.map(n => n.id)); const gGroups = new Set(geom.groups.map(g => g.id))
57
+ const missN = spec.nodes.filter(n => !gNodes.has(n.id)).map(n => n.id)
58
+ const missG = spec.groups.filter(g => !gGroups.has(g.id)).map(g => g.id)
59
+ out.push({ name: '存在性: 節點/群組無遺漏', pass: !missN.length && !missG.length, detail: (missN.length || missG.length) ? `缺節點[${missN}] 缺群組[${missG}]` : `${geom.nodes.length} 節點 / ${geom.groups.length} 群組` })
60
+ }
61
+
62
+ // 2) 包含: 有父者其外框 ⊆ 父外框(容器須包住成員, ELK INCLUDE_CHILDREN 合約)
63
+ {
64
+ const bad = []
65
+ ;[...geom.groups, ...geom.nodes].forEach(c => {
66
+ if (c.parent && bbox[c.parent] && !within(c, bbox[c.parent], 1.5)) bad.push(c.id + '⊄' + c.parent)
67
+ })
68
+ out.push({ name: '包含: 成員不逃出容器', pass: !bad.length, detail: bad.length ? bad.slice(0, 4).join(', ') : '全部成員在容器內' })
69
+ }
70
+
71
+ // 3) 標題不溢出: 每個容器標題寬 ≤ 容器寬(2-pass padding 撐寬機制之合約)
72
+ {
73
+ const bad = geom.groups.filter(g => g.labelW > g.w + 2).map(g => `${g.id}(標題${g.labelW}>框${g.w})`)
74
+ out.push({ name: '標題不溢出容器', pass: !bad.length, detail: bad.length ? bad.slice(0, 4).join(', ') : '全部標題在框內' })
75
+ }
76
+
77
+ // 4) 不重疊: 同一父之葉節點互不重疊(佈局健全性)
78
+ {
79
+ const byParent = {}; geom.nodes.forEach(n => {
80
+ (byParent[n.parent || '_root'] = byParent[n.parent || '_root'] || []).push(n)
81
+ })
82
+ const bad = []
83
+ Object.values(byParent).forEach(arr => {
84
+ for (let i = 0; i < arr.length; i++) for (let j = i + 1; j < arr.length; j++) if (overlap(arr[i], arr[j], 1.5)) bad.push(arr[i].id + '∩' + arr[j].id)
85
+ })
86
+ out.push({ name: '不重疊: 同層節點分離', pass: !bad.length, detail: bad.length ? bad.slice(0, 4).join(', ') : '節點無重疊' })
87
+ }
88
+
89
+ // 5) items 框: 標題+items 節點須有標題列(headH>0)且本體高於標題列
90
+ {
91
+ const its = geom.nodes.filter(n => n.kind === 'items')
92
+ const bad = its.filter(n => !(n.headH > 0 && n.h > n.headH + 4)).map(n => `${n.id}(h=${n.h} headH=${n.headH})`)
93
+ out.push({ name: 'items 框: 標題列+本體完整', pass: !bad.length, detail: its.length ? (bad.length ? bad.join(', ') : `${its.length} 個 items 框正常`) : '(無 items 框)' })
94
+ }
95
+
96
+ // 6) 邊: 每條邏輯邊都有路由(≥1 段, ≥2 點), 且兩端點落在來源/目標節點邊界(LCA 容器座標正確 → 不會跑到左上角)
97
+ {
98
+ const idset = new Set(geom.edges.map(e => e.id))
99
+ const missing = spec.edges.filter(e => !idset.has(e.id)).map(e => e.id)
100
+ const noRoute = []; const offTarget = []
101
+ spec.edges.forEach(se => {
102
+ const ge = geom.edges.find(e => e.id === se.id); if (!ge) return
103
+ const seg = ge.segs && ge.segs.find(s => s.length >= 2)
104
+ if (!seg) {
105
+ noRoute.push(se.id); return
106
+ }
107
+ const s0 = seg[0]; const s1 = seg[seg.length - 1]
108
+ const src = bbox[se.from]; const tgt = bbox[se.to]
109
+ if (src && tgt && !(near(s0, src, 6) && near(s1, tgt, 6))) offTarget.push(`${se.id}(${se.from}→${se.to})`)
110
+ })
111
+ const pass = !missing.length && !noRoute.length && !offTarget.length
112
+ out.push({ name: '邊: 皆有路由且兩端接節點', pass, detail: pass ? `${geom.edges.length} 條邊正常` : `缺[${missing}] 無路由[${noRoute}] 端點脫離[${offTarget.slice(0, 4)}]` })
113
+ }
114
+
115
+ return out
116
+ }
@@ -0,0 +1,89 @@
1
+ // p10 渲染回歸(mocha) — 10 合成案 + 9 真圖, 兩層把關:
2
+ // A. 結構不變量(spec 翻譯, 硬失敗): 存在性/包含/標題不溢出/不重疊/items框/邊路由
3
+ // B. 快照基準(回歸偵測): SVG 逐字比對 + PNG 像素(pixelmatch includeAA:false, 容差 MAXDIFF)
4
+ // 用法:
5
+ // npm test 全部測試
6
+ // npx mocha "test/p10.test.mjs" --timeout 60000 只跑本回歸
7
+ // npx mocha "test/p10.test.mjs" --timeout 60000 --grep fold 只跑名稱含 fold 之案
8
+ // 重建基準(確認變動為預期後才做): 設環境變數 UPDATE_BASELINE=1 再跑
9
+ import assert from 'assert'
10
+ import fs from 'fs'
11
+ import { fileURLToPath } from 'url'
12
+ import { dirname, resolve } from 'path'
13
+ import pixelmatch from 'pixelmatch'
14
+ import { PNG } from 'pngjs'
15
+ import { openStage, renderCase, runInvariants, helpers } from './lib.mjs'
16
+ import { CASES } from './cases.mjs'
17
+ import { FIGURES } from './figures.mjs'
18
+
19
+ const __dir = dirname(fileURLToPath(import.meta.url))
20
+ const SNAP = resolve(__dir, 'pics')
21
+ const MAXDIFF = 60 // PNG 容許之反鋸齒雜訊像素上限(真 regression 量級遠超此)
22
+ const UPDATE = process.env.UPDATE_BASELINE === '1' || process.env.UPDATE_BASELINE === 'true'
23
+
24
+ // 待測清單: 合成案 + 9 真圖(真圖為最重要之回歸案)
25
+ const items = []
26
+ for (const c of CASES) items.push({ name: c.name, data: c.data, check: c.check, expect: c.expect, kind: '合成' })
27
+ for (const f of FIGURES) items.push({ name: f.key, data: f.data, kind: '真圖' })
28
+
29
+ // PNG 像素比對(反鋸齒容差): 回 { same, detail }
30
+ function pngCompare(baseBuf, curBuf, diffPath) {
31
+ const a = PNG.sync.read(baseBuf), b = PNG.sync.read(curBuf)
32
+ if (a.width !== b.width || a.height !== b.height) return { same: false, detail: `尺寸 ${a.width}x${a.height} → ${b.width}x${b.height}` }
33
+ const diff = new PNG({ width: a.width, height: a.height })
34
+ const n = pixelmatch(a.data, b.data, diff.data, a.width, a.height, { includeAA: false, threshold: 0.1 })
35
+ if (n > MAXDIFF) { fs.writeFileSync(diffPath, PNG.sync.write(diff)); return { same: false, detail: `${n} 像素差 (>${MAXDIFF}) → ${diffPath}` } }
36
+ return { same: true, detail: `${n} 像素差` }
37
+ }
38
+
39
+ describe('p10 渲染回歸(結構不變量 + 快照基準)', function() {
40
+ let browser = null
41
+ let page = null
42
+
43
+ before(async function() {
44
+ this.timeout(120000)
45
+ fs.mkdirSync(SNAP, { recursive: true })
46
+ const s = await openStage()
47
+ browser = s.browser
48
+ page = s.page
49
+ })
50
+
51
+ after(async function() {
52
+ if (browser) await browser.close()
53
+ })
54
+
55
+ for (const tc of items) {
56
+ it(`[${tc.kind}] ${tc.name}`, async function() {
57
+ const r = await renderCase(page, tc.data)
58
+ assert.ok(r.res && r.res.ok, '渲染失敗: ' + (r.res && r.res.err))
59
+ const geom = r.res.geom
60
+ const h = helpers(geom)
61
+
62
+ // A. 不變量 + 案例專屬斷言 + 比例期望
63
+ const checks = runInvariants(r.spec, geom)
64
+ if (tc.check) checks.push(...tc.check(r.spec, geom, h).filter(Boolean))
65
+ if (tc.expect) {
66
+ const R = geom.H / geom.W
67
+ if (tc.expect.ratioMax != null) checks.push({ name: `比例 H/W ≤ ${tc.expect.ratioMax}`, pass: R <= tc.expect.ratioMax + 0.02, detail: `R=${R.toFixed(3)}` })
68
+ if (tc.expect.ratioMin != null) checks.push({ name: `比例 H/W ≥ ${tc.expect.ratioMin}`, pass: R >= tc.expect.ratioMin - 0.02, detail: `R=${R.toFixed(3)}` })
69
+ }
70
+ const failed = checks.filter(c => !c.pass)
71
+ assert.ok(!failed.length, '不變量失敗: ' + failed.map(c => `${c.name}(${c.detail || ''})`).join('; '))
72
+
73
+ // B. 快照基準(無基準或 UPDATE_BASELINE=1 時建立/重建)
74
+ const svgP = resolve(SNAP, tc.name + '.svg')
75
+ const pngP = resolve(SNAP, tc.name + '.png')
76
+ if (UPDATE || !fs.existsSync(svgP)) fs.writeFileSync(svgP, r.svg)
77
+ else {
78
+ const base = fs.readFileSync(svgP, 'utf8')
79
+ if (base !== r.svg) fs.writeFileSync(resolve(SNAP, tc.name + '.new.svg'), r.svg)
80
+ assert.ok(base === r.svg, `SVG 與基準不同(長 ${base.length}→${r.svg.length}; 已輸出 ${tc.name}.new.svg, 確認為預期後以 UPDATE_BASELINE=1 重建)`)
81
+ }
82
+ if (UPDATE || !fs.existsSync(pngP)) fs.writeFileSync(pngP, r.png)
83
+ else {
84
+ const c = pngCompare(fs.readFileSync(pngP), r.png, resolve(SNAP, tc.name + '.diff.png'))
85
+ assert.ok(c.same, `PNG 與基準不同: ${c.detail}(確認為預期後以 UPDATE_BASELINE=1 重建)`)
86
+ }
87
+ })
88
+ }
89
+ })
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="147" height="497" viewBox="-18 -18 147 497" style="background:#fff"><path d="M 49.66666666666667 48 L 49.66666666666667 82" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 49.66666666666667 82 L 53.66666666666667 73 L 45.66666666666667 73 Z" fill="#44505a"></path><path d="M 58.33333333333333 118 L 58.33333333333333 144 L 66 144 L 66 214 L 70 214 L 70 240" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 70 240 L 74 231 L 66 231 Z" fill="#44505a"></path><path d="M 55.5 327 L 55.5 413" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 55.5 413 L 59.5 404 L 51.5 404 Z" fill="#44505a"></path><foreignObject x="28.5" y="361" width="31" height="22"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:12.5px;color:#4a5560;text-shadow:0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff;white-space:nowrap">通過</div></foreignObject><path d="M 41 240 L 41 118" fill="none" stroke="#44505a" stroke-width="2" stroke-dasharray="6 4"></path><path d="M 41 118 L 37 127 L 45 127 Z" fill="#44505a"></path><foreignObject x="14" y="170" width="31" height="22"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:12.5px;color:#4a5560;text-shadow:0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff;white-space:nowrap">退回</div></foreignObject><rect x="23.666666666666668" y="12" width="52" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="23.666666666666668" y="12" width="52" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">開始</div></foreignObject><rect x="23.666666666666668" y="82" width="52" height="36" rx="7" fill="#eaf4ef" stroke="#348a5c" stroke-width="1.6"></rect><foreignObject x="23.666666666666668" y="82" width="52" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">處理</div></foreignObject><polygon points="55.5,240 99,283.5 55.5,327 12,283.5" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.8"></polygon><foreignObject x="12" y="240" width="87" height="87"><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:14px;color:#23303a;line-height:1.4;padding:0 4px;box-sizing:border-box">審核?</div></foreignObject><rect x="29.5" y="413" width="52" height="36" rx="7" fill="#eaf4ef" stroke="#256046" stroke-width="1.6"></rect><foreignObject x="29.5" y="413" width="52" height="36"><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:14px;color:#256046;line-height:1.4;padding:0 4px;box-sizing:border-box">完成</div></foreignObject></svg>
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="224" height="419" viewBox="-18 -18 224 419" style="background:#fff"><rect x="12" y="12" width="164" height="226" rx="8" fill="#f5f9fe" fill-opacity="0.38" stroke="#3f6fb0" stroke-width="2"></rect><foreignObject x="59.5" y="17" width="69" height="33"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="text-align:center;font-weight:bold;font-size:15px;color:#3f6fb0;line-height:1.3">第一層</div></foreignObject><rect x="26" y="54" width="136" height="159" rx="8" fill="#f0f7f3" fill-opacity="0.38" stroke="#348a5c" stroke-width="2"></rect><foreignObject x="59.5" y="59" width="69" height="33"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="text-align:center;font-weight:bold;font-size:15px;color:#348a5c;line-height:1.3">第二層</div></foreignObject><rect x="40" y="96" width="108" height="92" rx="8" fill="#fdf6ec" fill-opacity="0.38" stroke="#c46e1a" stroke-width="2"></rect><foreignObject x="59.5" y="101" width="69" height="33"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="text-align:center;font-weight:bold;font-size:15px;color:#c46e1a;line-height:1.3">第三層</div></foreignObject><path d="M 94 174 L 94 335" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 94 335 L 98 326 L 90 326 Z" fill="#44505a"></path><foreignObject x="54" y="283" width="44" height="22"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:12.5px;color:#4a5560;text-shadow:0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff;white-space:nowrap">跨三層</div></foreignObject><rect x="54" y="138" width="80" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="54" y="138" width="80" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">最深節點</div></foreignObject><rect x="54" y="335" width="80" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="54" y="335" width="80" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">頂層節點</div></foreignObject></svg>
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="197" height="357" viewBox="-18 -18 197 357" style="background:#fff"><path d="M 55.5 48 L 55.5 82" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 55.5 82 L 59.5 73 L 51.5 73 Z" fill="#44505a"></path><path d="M 41 169 L 41 273" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 41 273 L 45 264 L 37 264 Z" fill="#44505a"></path><foreignObject x="26" y="221" width="19" height="22"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:12.5px;color:#4a5560;text-shadow:0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff;white-space:nowrap">是</div></foreignObject><path d="M 70 169 L 70 195 L 123 195 L 123 273" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 123 273 L 127 264 L 119 264 Z" fill="#44505a"></path><foreignObject x="126" y="221" width="19" height="22"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:12.5px;color:#4a5560;text-shadow:0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff;white-space:nowrap">否</div></foreignObject><rect x="29.5" y="12" width="52" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="29.5" y="12" width="52" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">輸入</div></foreignObject><polygon points="55.5,82 99,125.5 55.5,169 12,125.5" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.8"></polygon><foreignObject x="12" y="82" width="87" height="87"><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:14px;color:#23303a;line-height:1.4;padding:0 4px;box-sizing:border-box">判斷?</div></foreignObject><rect x="15" y="273" width="52" height="36" rx="7" fill="#eaf4ef" stroke="#348a5c" stroke-width="1.6"></rect><foreignObject x="15" y="273" width="52" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">通過</div></foreignObject><rect x="97" y="273" width="52" height="36" rx="7" fill="#fbecea" stroke="#c0392b" stroke-width="1.6"></rect><foreignObject x="97" y="273" width="52" height="36"><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:14px;color:#23303a;line-height:1.4;padding:0 4px;box-sizing:border-box">退回</div></foreignObject></svg>
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="222" height="324" viewBox="-18 -18 222 324" style="background:#fff"><path d="M 64 48 L 64 152" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 64 152 L 68 143 L 60 143 Z" fill="#44505a"></path><foreignObject x="12" y="100" width="56" height="22"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:12.5px;color:#4a5560;text-shadow:0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff;white-space:nowrap">實線標籤</div></foreignObject><path d="M 81.33333333333334 48 L 81.33333333333334 74 L 121 74 L 121 214 L 86 214 L 86 240" fill="none" stroke="#44505a" stroke-width="2" stroke-dasharray="6 4"></path><path d="M 86 240 L 90 231 L 82 231 Z" fill="#44505a"></path><foreignObject x="124" y="100" width="56" height="22"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:12.5px;color:#4a5560;text-shadow:0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff;white-space:nowrap">虛線標籤</div></foreignObject><path d="M 64 188 L 64 240" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 64 240 L 68 231 L 60 231 Z" fill="#44505a"></path><rect x="46.66666666666667" y="12" width="52" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="46.66666666666667" y="12" width="52" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">來源</div></foreignObject><rect x="31" y="152" width="66" height="36" rx="7" fill="#eaf4ef" stroke="#348a5c" stroke-width="1.6"></rect><foreignObject x="31" y="152" width="66" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">目標一</div></foreignObject><rect x="42" y="240" width="66" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="42" y="240" width="66" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">目標二</div></foreignObject></svg>
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="284" height="375" viewBox="-18 -18 284 375" style="background:#fff"><path d="M 124 48 L 124 134" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 124 134 L 128 125 L 120 125 Z" fill="#44505a"></path><foreignObject x="83" y="82" width="45" height="22"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:12.5px;color:#4a5560;text-shadow:0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff;white-space:nowrap">HTTPS</div></foreignObject><path d="M 124 237 L 124 271" fill="none" stroke="#44505a" stroke-width="2" stroke-dasharray="6 4"></path><path d="M 124 271 L 128 262 L 120 262 Z" fill="#44505a"></path><rect x="70.5" y="12" width="107" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="70.5" y="12" width="107" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">API Gateway</div></foreignObject><rect x="45.5" y="134" width="157" height="103" rx="7" fill="#eaf4ef"></rect><path d="M 52.5 134 H 195.5 A 7 7 0 0 1 202.5 141 V 166 H 45.5 V 141 A 7 7 0 0 1 52.5 134 Z" fill="#cde3d7"></path><rect x="45.5" y="134" width="157" height="103" rx="7" fill="none" stroke="#348a5c" stroke-width="1.6"></rect><line x1="45.5" y1="166" x2="202.5" y2="166" stroke="#348a5c" stroke-width="1"></line><foreignObject x="45.5" y="134" width="157" height="32"><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:14px;color:#1c2b36">Auth Service</div></foreignObject><foreignObject x="45.5" y="166" width="157" height="71"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:13px;line-height:1.55;padding:6px 9px 4px;text-align:left;color:#1c2b36"><div style="white-space:nowrap">•&nbsp;login/logout</div><div style="white-space:nowrap">•&nbsp;token refresh (JWT)</div><div style="white-space:nowrap">•&nbsp;session mgmt</div></div></foreignObject><rect x="12" y="271" width="224" height="56" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="12" y="271" width="224" height="56"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">InternationalizationConfigurationManager</div></foreignObject></svg>
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="126" height="236" viewBox="-18 -18 126 236" style="background:#fff"><path d="M 45 48 L 45 82" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 45 82 L 49 73 L 41 73 Z" fill="#44505a"></path><path d="M 45 118 L 45 152" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 45 152 L 49 143 L 41 143 Z" fill="#44505a"></path><rect x="12" y="12" width="66" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="12" y="12" width="66" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">節點 A</div></foreignObject><rect x="12.5" y="82" width="65" height="36" rx="7" fill="#eaf4ef" stroke="#348a5c" stroke-width="1.6"></rect><foreignObject x="12.5" y="82" width="65" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">節點 B</div></foreignObject><rect x="12.5" y="152" width="65" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="12.5" y="152" width="65" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">節點 C</div></foreignObject></svg>
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="384" height="304" viewBox="-18 -18 384 304" style="background:#fff"><rect x="12" y="12" width="324" height="244" rx="8" fill="#f5f9fe" fill-opacity="0.38" stroke="#3f6fb0" stroke-width="2"></rect><foreignObject x="117" y="17" width="114" height="33"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="text-align:center;font-weight:bold;font-size:15px;color:#3f6fb0;line-height:1.3">垂直長鏈容器</div></foreignObject><path d="M 69 100 L 69 130" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 69 130 L 73 121 L 65 121 Z" fill="#44505a"></path><path d="M 69 166 L 69 196" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 69 196 L 73 187 L 65 187 Z" fill="#44505a"></path><path d="M 69 232 L 69 250 L 121 250 L 121 46 L 174 46 L 174 64" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 174 64 L 178 55 L 170 55 Z" fill="#44505a"></path><path d="M 174 100 L 174 130" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 174 130 L 178 121 L 170 121 Z" fill="#44505a"></path><path d="M 174 166 L 174 196" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 174 196 L 178 187 L 170 187 Z" fill="#44505a"></path><path d="M 174 232 L 174 250 L 226 250 L 226 46 L 279 46 L 279 64" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 279 64 L 283 55 L 275 55 Z" fill="#44505a"></path><rect x="37" y="64" width="64" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="37" y="64" width="64" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">步驟 1</div></foreignObject><rect x="37" y="130" width="64" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="37" y="130" width="64" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">步驟 2</div></foreignObject><rect x="37" y="196" width="64" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="37" y="196" width="64" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">步驟 3</div></foreignObject><rect x="142" y="64" width="64" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="142" y="64" width="64" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">步驟 4</div></foreignObject><rect x="142" y="130" width="64" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="142" y="130" width="64" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">步驟 5</div></foreignObject><rect x="142" y="196" width="64" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="142" y="196" width="64" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">步驟 6</div></foreignObject><rect x="247" y="64" width="64" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="247" y="64" width="64" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">步驟 7</div></foreignObject></svg>
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="360" height="306" viewBox="-18 -18 360 306" style="background:#fff"><rect x="12" y="12" width="300" height="246" rx="8" fill="#fdf6ec" fill-opacity="0.38" stroke="#c46e1a" stroke-width="2"></rect><foreignObject x="105" y="17" width="114" height="33"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="text-align:center;font-weight:bold;font-size:15px;color:#c46e1a;line-height:1.3">水平長鏈容器</div></foreignObject><path d="M 100 72 L 130 72" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 130 72 L 121 68 L 121 76 Z" fill="#44505a"></path><path d="M 194 72 L 224 72" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 224 72 L 215 68 L 215 76 Z" fill="#44505a"></path><path d="M 288 72 L 306 72 L 306 110 L 18 110 L 18 149 L 36 149" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 36 149 L 27 145 L 27 153 Z" fill="#44505a"></path><path d="M 100 149 L 130 149" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 130 149 L 121 145 L 121 153 Z" fill="#44505a"></path><path d="M 194 149 L 224 149" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 224 149 L 215 145 L 215 153 Z" fill="#44505a"></path><path d="M 288 149 L 306 149 L 306 187 L 18 187 L 18 226 L 36 226" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 36 226 L 27 222 L 27 230 Z" fill="#44505a"></path><rect x="36" y="54" width="64" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="36" y="54" width="64" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">階段 1</div></foreignObject><rect x="130" y="54" width="64" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="130" y="54" width="64" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">階段 2</div></foreignObject><rect x="224" y="54" width="64" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="224" y="54" width="64" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">階段 3</div></foreignObject><rect x="36" y="131" width="64" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="36" y="131" width="64" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">階段 4</div></foreignObject><rect x="130" y="131" width="64" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="130" y="131" width="64" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">階段 5</div></foreignObject><rect x="224" y="131" width="64" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="224" y="131" width="64" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">階段 6</div></foreignObject><rect x="36" y="208" width="64" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="36" y="208" width="64" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">階段 7</div></foreignObject></svg>
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="223" height="366" viewBox="-18 -18 223 366" style="background:#fff"><rect x="79.5" y="12" width="85" height="92" rx="8" fill="#f5f9fe" fill-opacity="0.38" stroke="#3f6fb0" stroke-width="2"></rect><foreignObject x="80" y="17" width="84" height="33"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="text-align:center;font-weight:bold;font-size:15px;color:#3f6fb0;line-height:1.3">來源群組</div></foreignObject><rect x="24" y="226" width="84" height="92" rx="8" fill="#fdf6ec" fill-opacity="0.38" stroke="#c46e1a" stroke-width="2"></rect><foreignObject x="24" y="231" width="84" height="33"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="text-align:center;font-weight:bold;font-size:15px;color:#c46e1a;line-height:1.3">目標群組</div></foreignObject><path d="M 122 104 L 122 200 L 80 200 L 80 226" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 80 226 L 84 217 L 76 217 Z" fill="#44505a"></path><foreignObject x="125" y="147" width="56" height="22"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:12.5px;color:#4a5560;text-shadow:0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff;white-space:nowrap">群組相依</div></foreignObject><path d="M 52 174 L 52 226" fill="none" stroke="#44505a" stroke-width="2" stroke-dasharray="6 4"></path><path d="M 52 226 L 56 217 L 48 217 Z" fill="#44505a"></path><rect x="106.5" y="54" width="31" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="106.5" y="54" width="31" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">x</div></foreignObject><rect x="50" y="268" width="32" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="50" y="268" width="32" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">y</div></foreignObject><rect x="12" y="138" width="80" height="36" rx="7" fill="#eaf4ef" stroke="#348a5c" stroke-width="1.6"></rect><foreignObject x="12" y="138" width="80" height="36"><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:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">外部節點</div></foreignObject></svg>
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="152" height="352" viewBox="-18 -18 152 352" style="background:#fff"><path d="M 58 155 L 58 241" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 58 241 L 62 232 L 54 232 Z" fill="#44505a"></path><foreignObject x="31" y="189" width="31" height="22"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:12.5px;color:#4a5560;text-shadow:0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff;white-space:nowrap">呼叫</div></foreignObject><rect x="18.5" y="12" width="79" height="143" rx="7" fill="#fcf0e2"></rect><path d="M 25.5 12 H 90.5 A 7 7 0 0 1 97.5 19 V 44 H 18.5 V 19 A 7 7 0 0 1 25.5 12 Z" fill="#f3dbc2"></path><rect x="18.5" y="12" width="79" height="143" rx="7" fill="none" stroke="#c46e1a" stroke-width="1.6"></rect><line x1="18.5" y1="44" x2="97.5" y2="44" stroke="#c46e1a" stroke-width="1"></line><foreignObject x="18.5" y="12" width="79" height="32"><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:14px;color:#1c2b36">服務 X</div></foreignObject><foreignObject x="18.5" y="44" width="79" height="111"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:13px;line-height:1.55;padding:6px 9px 4px;text-align:left;color:#1c2b36"><div style="white-space:nowrap">•&nbsp;項目一</div><div style="white-space:nowrap">•&nbsp;項目二</div><div style="white-space:nowrap">•&nbsp;項目三</div><div style="white-space:nowrap">•&nbsp;項目四</div><div style="white-space:nowrap">•&nbsp;項目五</div></div></foreignObject><rect x="12" y="241" width="92" height="63" rx="7" fill="#fcf0e2"></rect><path d="M 19 241 H 97 A 7 7 0 0 1 104 248 V 273 H 12 V 248 A 7 7 0 0 1 19 241 Z" fill="#f3dbc2"></path><rect x="12" y="241" width="92" height="63" rx="7" fill="none" stroke="#c46e1a" stroke-width="1.6"></rect><line x1="12" y1="273" x2="104" y2="273" stroke="#c46e1a" stroke-width="1"></line><foreignObject x="12" y="241" width="92" height="32"><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:14px;color:#1c2b36">服務 Y</div></foreignObject><foreignObject x="12" y="273" width="92" height="31"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:13px;line-height:1.55;padding:6px 9px 4px;text-align:left;color:#1c2b36"><div style="white-space:nowrap">•&nbsp;只有一項</div></div></foreignObject></svg>
Binary file