// WFlowchart — 統一產圖入口: 輸入正規化繪圖數據 inp, 產出 PNG 圖
// inp: 正規化繪圖數據 { dir, nodes, edges }(與繪圖庫無關, 結構同 test/data/*.json; cls 見 common/palette.mjs)
// opt.mode: 'p1'~'p10', 各對應一套產線(繪圖引擎與佈局各異, 見各 pN/gen.mjs 檔頭說明), 預設 'p10'
// p1: mermaid(dagre) p2: mermaid(ELK) p3: nomnoml p4: cytoscape(dagre)
// p5: D2 p6: G6(antv-dagre) p7: JointJS(dagre) p8: LogicFlow(dagre)
// p9: dagre 自寫渲染 p10: ELK 自寫渲染
// 回傳: Promise<Buffer>(PNG)
import get from 'lodash-es/get.js'
import cloneDeep from 'lodash-es/cloneDeep.js'
import isestr from 'wsemi/src/isestr.mjs'
import iseobj from 'wsemi/src/iseobj.mjs'
import isearr from 'wsemi/src/isearr.mjs'
import isarr from 'wsemi/src/isarr.mjs'
import { deriveLabels } from './common/derive.mjs'
// 引擎延遲載入: 各產線依 opt.mode 動態 import(), 使用者只為所選產線付出依賴成本
// (各 pN 模組層即讀取其繪圖庫 bundle 檔; p5 另載入 @terrastruct/d2 與 sharp, 其上游缺陷不影響其他產線)
const LOADERS = {
p1: () => import('./p1/gen.mjs'),
p2: () => import('./p2/gen.mjs'),
p3: () => import('./p3/gen.mjs'),
p4: () => import('./p4/gen.mjs'),
p5: () => import('./p5/gen.mjs'),
p6: () => import('./p6/gen.mjs'),
p7: () => import('./p7/gen.mjs'),
p8: () => import('./p8/gen.mjs'),
p9: () => import('./p9/gen.mjs'),
p10: () => import('./p10/gen.mjs'),
}
// svg 輸出: p1/p2/p3/p5/p10 引擎原生輸出 SVG 字串; p7/p8 為 SVG DOM 序列化(樣式已內嵌);
// p4 經 cytoscape-svg 外掛、p6 經 @antv/g-svg renderer 重繪為 SVG(皆經像素級忠實度驗證);
// p9 節點為 React Flow 之 HTML DOM, 無忠實 SVG 可產 → 不支援
const SVG_MODES = ['p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p10']
/**
* 產製流程圖 PNG
*
* @param {Object} inp 輸入正規化繪圖數據物件,格式 { dir, nodes, edges },與繪圖引擎無關
* @param {String} [inp.dir='TB'] 輸入佈局方向字串,可選 'TB'(上而下)或 'LR'(左而右),預設 'TB'
* @param {Array} inp.nodes 輸入節點陣列,每項為一般節點 { id, label, cls, group? } 或富節點 { id, title, items, cls, group? };cls 為色票鍵(見 src/common/palette.mjs),結尾 G/G2 者為群組容器,group 為所屬容器 id(容器可巢狀);label 過寬會自動斷行,亦可用 '\n' 強制換行(p10 支援,含群組/菱形標籤;其他產線依引擎行為)
* @param {Array} inp.edges 輸入邊陣列,每項為 { from, to, label?, kind? },kind 給 'dashed' 為虛線;label 同樣支援 '\n' 強制換行(p10)
* @param {Object} [opt={}] 輸入設定物件,原樣傳遞給產線,預設 {}
* @param {String} [opt.mode='p10'] 輸入產線模式字串,可選 'p1'~'p10',各對應一套繪圖引擎產線(技術概念見各 src/pN/README.md),未指定或非有效字串時預設 'p10'
* @param {String} [opt.output='png'] 輸入輸出格式字串,可選 'png'(回傳 PNG Buffer)或 'svg'(回傳 standalone SVG 字串),未指定或非有效字串時預設 'png';svg 支援 p1~p8 與 p10,p9(React Flow 之 HTML DOM 渲染)不支援,給 svg 會拋錯
* @returns {Promise} 回傳 Promise,resolve 依 opt.output 回傳 PNG 圖之 Buffer 或 SVG 字串,reject 回傳錯誤訊息
* @example
*
* import fs from 'fs'
* import WFlowchart from 'w-flowchart'
*
* let inp = {
* dir: 'TB',
* nodes: [
* { id: 'A', label: '開始', cls: 'blue' },
* { id: 'G', label: '處理群組', cls: 'greenG' },
* { id: 'B', label: '步驟一', cls: 'green', group: 'G' },
* { id: 'C', title: '服務', items: ['項目一', '項目二'], cls: 'green', group: 'G' },
* { id: 'D', label: '判斷?', cls: 'diamond' },
* { id: 'E', label: '結束', cls: 'orange' },
* ],
* edges: [
* { from: 'A', to: 'B' },
* { from: 'B', to: 'C', label: '呼叫' },
* { from: 'C', to: 'D' },
* { from: 'D', to: 'E', label: '通過' },
* { from: 'D', to: 'B', kind: 'dashed', label: '退回' },
* ],
* }
*
* let buf = await WFlowchart(inp) //預設 mode 'p10'; 可用 WFlowchart(inp, { mode: 'p3' }) 指定產線
* fs.writeFileSync('flowchart.png', buf)
* // => 產出 flowchart.png
*
* let svg = await WFlowchart(inp, { output: 'svg' }) //輸出 standalone SVG 字串(p9 不支援)
* fs.writeFileSync('flowchart.svg', svg)
* // => 產出 flowchart.svg
*/
async function WFlowchart(inp, opt = {}) {
let mode = get(opt, 'mode')
if (!isestr(mode)) mode = 'p10' //未指定或非有效字串時預設 p10
const m = mode.trim().toLowerCase()
if (!LOADERS[m]) throw new Error(`invalid opt.mode[${mode}], 須為 ${Object.keys(LOADERS).join(', ')} 之一`)
let output = get(opt, 'output')
if (!isestr(output)) output = 'png' //未指定或非有效字串時預設 png
const o = output.trim().toLowerCase()
if (o !== 'png' && o !== 'svg') throw new Error(`invalid opt.output[${output}], 須為 png 或 svg`)
if (o === 'svg' && !SVG_MODES.includes(m)) throw new Error(`mode[${m}] 不支援 svg 輸出, 支援: ${SVG_MODES.join(', ')}`)
if (!iseobj(inp)) throw new Error('invalid inp, 須為 { dir, nodes, edges } 正規化繪圖數據')
if (!isearr(inp.nodes)) throw new Error('invalid inp.nodes, 須為非空陣列')
if (!isarr(inp.edges)) throw new Error('invalid inp.edges, 須為陣列')
const data = deriveLabels(cloneDeep(inp)) //deriveLabels 會就地改動, 先複製避免污染 caller 之 inp
const gen = await LOADERS[m]() //檢核全過才載入所選產線引擎(延遲載入)
return await (o === 'svg' ? gen.genSvg : gen.genPng)(data, opt)
}
export default WFlowchart