/**
* graphMutation — 圖資料之變更核心(無框架、無狀態純函式)
*
* 邊界(三層架構之最底層):
* - 只處理 id 解析/去重、連帶(cascade)計算、deletable 政策、就地套用與 change record 產出;
* - 不碰 Vue instance、$emit、選取、拖曳 ghost、路由 cache——那些屬 WFlowVue 內之 transaction coordinator。
* - graph = { nodes, conns } 為宿主陣列本身(1.x 維持「宿主陣列就地 mutate」模型), 每次呼叫皆重新解析,
* 不快取任何狀態; preview 無副作用, apply 才 splice。
*
* 政策(1.x 定案):
* - id 為 opaque identity(嚴格相等比對), 不強制轉字串; 只要求各集合內唯一。
* - deletable:false 只阻止「被直接指定刪除」; 刪除節點時參照完整性優先, 其相鄰邊不論 deletable 一律連帶移除
* (否則留下端點已消失之孤兒邊)。被連帶刪除者列入 deleted 而非 excluded。
*/
const uniq = (arr) => {
const out = []
const seen = new Set()
for (const v of (arr || [])) {
if (v === undefined || v === null) continue
if (seen.has(v)) continue
seen.add(v)
out.push(v)
}
return out
}
/**
* 預覽刪除(無副作用): 解析目標、套用 deletable 政策、計算連帶邊, 產出可供確認閘門與提交共用之 plan
*
* @param {Object} graph { nodes, conns }
* @param {Object} target { nodeIds=[], connIds=[] }
* @returns {Object} plan
* requested: { nodeIds, connIds } 去重後之直接要求
* nodeIds / connIds 將被刪除之 id(connIds 含連帶, 已去重, 依圖內順序)
* nodes / conns 對應之圖內物件(同上順序; conns = 直接指定者在前、連帶者在後)
* cascades: [{ nodeId, connIds }] 各節點連帶刪除之邊(僅列「非直接指定」者)
* notFound: { nodeIds, connIds } 直接要求但圖上不存在者
* excluded: { nodeIds, connIds } 直接要求但 deletable:false 而最終未刪者
* empty: Boolean requested 為空
*/
export function previewDelete(graph, target) {
const nodes = (graph && graph.nodes) || []
const conns = (graph && graph.conns) || []
const reqNodeIds = uniq(target && target.nodeIds)
const reqConnIds = uniq(target && target.connIds)
const notFound = { nodeIds: [], connIds: [] }
const excluded = { nodeIds: [], connIds: [] }
//nodes: 直接指定者
const delNodes = []
const delNodeIdSet = new Set()
for (const id of reqNodeIds) {
const n = nodes.find(x => x.id === id)
if (!n) {
notFound.nodeIds.push(id)
continue
}
if (n.deletable === false) {
excluded.nodeIds.push(id)
continue
}
delNodes.push(n)
delNodeIdSet.add(id)
}
//conns: 直接指定者
const delConns = []
const delConnIdSet = new Set()
const excludedConnIds = []
for (const id of reqConnIds) {
const c = conns.find(x => x.id === id)
if (!c) {
notFound.connIds.push(id)
continue
}
if (c.deletable === false) {
excludedConnIds.push(id)
continue
}
delConns.push(c)
delConnIdSet.add(id)
}
//cascade: 被刪節點之相鄰邊(完整性優先, 不看邊之 deletable); 依圖內順序, 去重
const cascades = []
if (delNodeIdSet.size > 0) {
const perNode = new Map()
for (const id of delNodeIdSet) perNode.set(id, [])
for (const c of conns) {
const hitFrom = delNodeIdSet.has(c.from)
const hitTo = delNodeIdSet.has(c.to)
if (!hitFrom && !hitTo) continue
if (delConnIdSet.has(c.id)) continue //直接指定者不重列於 cascade
delConns.push(c)
delConnIdSet.add(c.id)
//因果歸屬: 兩端皆被刪時歸於 from 端(單一歸屬, 避免同一邊在 cascades 內出現兩次)
const owner = hitFrom ? c.from : c.to
perNode.get(owner).push(c.id)
}
for (const [nodeId, connIds] of perNode) {
if (connIds.length > 0) cascades.push({ nodeId, connIds })
}
}
//excluded 只放「最終確實未刪」者: 直接指定被擋之邊若同時被連帶命中, 仍會被刪 → 不列 excluded
for (const id of excludedConnIds) {
if (!delConnIdSet.has(id)) excluded.connIds.push(id)
}
return {
requested: { nodeIds: reqNodeIds, connIds: reqConnIds },
nodeIds: delNodes.map(n => n.id),
connIds: delConns.map(c => c.id),
nodes: delNodes,
conns: delConns,
cascades,
notFound,
excluded,
empty: reqNodeIds.length === 0 && reqConnIds.length === 0,
}
}
/**
* 就地套用刪除: 以 id 重新解析(不信任 preview 當下之物件參照), 自宿主陣列 splice 移除
*
* @param {Object} graph { nodes, conns }
* @param {Object} plan { nodeIds, connIds }(connIds 須已含連帶邊, 本函式不再計算 cascade)
* @returns {Object} { nodes, conns } 實際被移除之物件(依 plan 之 id 順序; 不存在者略過)
*/
export function applyDelete(graph, plan) {
const nodes = (graph && graph.nodes) || []
const conns = (graph && graph.conns) || []
const removedNodes = []
const removedConns = []
for (const id of uniq(plan && plan.nodeIds)) {
const idx = nodes.findIndex(n => n.id === id)
if (idx === -1) continue
removedNodes.push(nodes[idx])
nodes.splice(idx, 1)
}
for (const id of uniq(plan && plan.connIds)) {
const idx = conns.findIndex(c => c.id === id)
if (idx === -1) continue
removedConns.push(conns[idx])
conns.splice(idx, 1)
}
return { nodes: removedNodes, conns: removedConns }
}
/**
* 找出集合內重複之 id(宿主資料錯誤之防呆; 重複 id 會使一切以 id 為鍵之查找/對帳/Vue key 失效)
*
* @param {Array} list
* @returns {Array} 重複之 id(每個只列一次, 依首次重複出現順序)
*/
export function findDuplicateIds(list) {
const seen = new Set()
const dup = new Set()
for (const it of (list || [])) {
if (!it) continue
const id = it.id
if (seen.has(id)) dup.add(id)
else seen.add(id)
}
return [...dup]
}
/**
* 深複製快照(與內部狀態脫鉤): 事件為歷史紀錄, 不得因後續畫布變動而失真, 亦防宿主改動事件物件汙染內部。
* 採 JSON 往返(與 getFlowData 同策略): 函式/undefined 欄位不保留。
*/
export function snapshotDeep(v) {
return JSON.parse(JSON.stringify(v))
}