/* 层级变化后一致性清理(用户拍板:同级不应有线;方向规则:上级层级必须 > 下级): committed 手动边与 pending 新增中,两端层级相同或父层级 <= 子层级的 → 移除并记日志(修订⑧:edges 全为手动边)。 */ function sanitizeManualEdges() { const bad = e => { const p = byId(e.parent), c = byId(e.child); return p && c && p.level != null && c.level != null && p.level <= c.level; }; G.pendingAdds = G.pendingAdds.filter(a => { if (bad({ parent: a.parent, child: a.child })) { addLog('关系', '清除未保存连线:' + cname(a.parent) + ' → ' + cname(a.child) + '(层级变化后不再满足上下级关系)'); return false; } return true; }); const removed = G.edges.filter(bad); if (removed.length) { G.edges = G.edges.filter(e => !bad(e)); removed.forEach(e => addLog('关系', '删除连线:' + cname(e.parent) + ' → ' + cname(e.child) + '(两端层级变化后不再满足上下级关系,已清除)')); } } /* 保存后有效边集 = edges(去 pendingDeletes) + pendingAdds(全部手动边,无 auto) */ function effectiveEdges() { const del = new Set(G.pendingDeletes); const list = G.edges.filter(e => !del.has(e.id)).map(e => ({ id: e.id, parent: e.parent, child: e.child })); G.pendingAdds.forEach(a => list.push({ id: 'p_' + a.parent + '_' + a.child, parent: a.parent, child: a.child, pending: true })); return list; } /* 修订⑧ D8:公司边 = 等级 10 → 公司 的派生视图(不入 edges/不入 save 快照,不可删;摘线 = 降级) */ function derivedCompanyEdges() { return G.contacts.filter(c => c.level === 10).map(c => ({ id: 'C>' + c.id, parent: c.id, child: COMPANY_ID, company: true })); } function hasPair(list, p, c) { return list.some(e => e.parent === p && e.child === c); } /* DFS 环检测:返回环节点序列([a,b,c,a])或 null;保存前兜底(连线双闸已挡大部分) */ function findCycle(list) { const adj = {}; list.forEach(e => (adj[e.parent] = adj[e.parent] || []).push(e.child)); const color = {}, stack = []; let cycle = null; function dfs(u) { color[u] = 1; stack.push(u); for (const v of (adj[u] || [])) { if (color[v] === 1) { cycle = stack.slice(stack.indexOf(v)).concat([v]); return true; } if (!color[v] && dfs(v)) return true; } color[u] = 2; stack.pop(); return false; } for (const id of Object.keys(adj)) { if (!color[id] && dfs(id)) break; } return cycle; } /* 画布坐标 → 落点区域(unrec 优先于 y 判定) */ function rowAt(x, y) { if (x >= UNREC_X) return { kind: 'unrec' }; for (const r of ROWS) if (y >= r.y - 10 && y <= r.y + CARD_H + 10) return { kind: 'lv', lv: r.lv, y: r.y }; if (y >= COMPANY_POS.y - 10 && y <= COMPANY_POS.y + CARD_H + 10) return { kind: 'company' }; return { kind: 'none' }; }