package com.yunhai.flowcenter.service; import com.yunhai.flowcenter.engine.FlowDefModel; import com.yunhai.flowcenter.model.FlowModels; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * 流程图布局单源(2026-10-03 三视图统一;2026-10-09 竖排改造):拓扑最长路径分层 * (层=行,自上向下),层内按定义顺序横排、整层块对竖直中线水平居中;并行分支组以 * SPLIT 竖直中线为轴左右对称展开(SPLIT↔JOIN 配对按各出边可达集求交、取 BFS 序最早的 * JOIN);输出内容区坐标(minX/minY 已归零),PNG 渲染与 DiagramVo.positions 共同消费—— * 改布局只改这里。 */ @Service public class DiagramLayoutService { /** 逻辑坐标常量(盒型在槽内居中;层间距留给竖直走线与多出口文案,分支列间无需走线空间) */ static final int SLOT_H = 100; // 层槽高(盒型在槽内垂直居中) static final int GAP_Y = 56; // 层间距(竖直段 + 多出口文案空间) static final int SLOT_W = 280; // 分支槽宽(盒型在槽内水平居中) static final int GAP_X = 44; // 分支列间距 /** 各类型盒型(w,h):开始/结束胶囊、审批圆角卡(名单 2 行)、并行/条件菱形(取外接矩形) */ static int[] boxOf(String type) { switch (type == null ? "" : type) { case FlowDefModel.TYPE_START: case FlowDefModel.TYPE_END: return new int[]{116, 44}; case FlowDefModel.TYPE_PARALLEL_SPLIT: case FlowDefModel.TYPE_PARALLEL_JOIN: case FlowDefModel.TYPE_CONDITION: return new int[]{132, 78}; default: return new int[]{264, 76}; } } /** nodes+edges → nodeId→{x,y,w,h}(内容区坐标,minX/minY 已归零)。PNG 与 DiagramVo.positions 共同消费;改布局只改这里。 */ public Map layout(List nodes, List edges) { Map byId = new HashMap<>(); for (FlowModels.NodeVo n : nodes) { byId.put(n.getId(), n); } Map> out = new HashMap<>(); Map inDeg = new LinkedHashMap<>(); for (FlowModels.NodeVo n : nodes) { inDeg.put(n.getId(), 0); } for (FlowModels.EdgeVo e : edges) { if (byId.containsKey(e.getFrom()) && byId.containsKey(e.getTo())) { out.computeIfAbsent(e.getFrom(), k -> new ArrayList<>()).add(e.getTo()); inDeg.merge(e.getTo(), 1, Integer::sum); } } // 拓扑最长路径分层(层=行,自上向下);环兜底:无入度的从层 0 起算,成环残留节点强制层 0(仍必出图) Map level = new HashMap<>(); List queue = new ArrayList<>(); for (Map.Entry en : inDeg.entrySet()) { if (en.getValue() == 0) { level.put(en.getKey(), 0); queue.add(en.getKey()); } } while (!queue.isEmpty()) { String id = queue.remove(queue.size() - 1); int parent = level.get(id); for (String to : out.getOrDefault(id, new ArrayList<>())) { level.merge(to, parent + 1, Math::max); int d = inDeg.merge(to, -1, Integer::sum); if (d == 0) { queue.add(to); } } } for (FlowModels.NodeVo n : nodes) { level.putIfAbsent(n.getId(), 0); } // 初始网格:行 y 由层号决定,层内按定义顺序横排、整层块以内容区竖直中线居中 Map> layers = new LinkedHashMap<>(); int maxCols = 1; for (FlowModels.NodeVo n : nodes) { int l = level.get(n.getId()); layers.computeIfAbsent(l, k -> new ArrayList<>()).add(n); } for (List layer : layers.values()) { maxCols = Math.max(maxCols, layer.size()); } int axisW = (maxCols - 1) * (SLOT_W + GAP_X) + SLOT_W; Map box = new HashMap<>(); for (Map.Entry> en : layers.entrySet()) { List layer = en.getValue(); int blockW = (layer.size() - 1) * (SLOT_W + GAP_X) + SLOT_W; int left0 = (axisW - blockW) / 2; for (int i = 0; i < layer.size(); i++) { FlowModels.NodeVo n = layer.get(i); int[] wh = boxOf(n.getType()); box.put(n.getId(), new int[]{ left0 + i * (SLOT_W + GAP_X) + (SLOT_W - wh[0]) / 2, en.getKey() * (SLOT_H + GAP_Y) + (SLOT_H - wh[1]) / 2, wh[0], wh[1]}); } } // 并行分支组以 SPLIT 竖直中线为轴左右居中(内层组先处理,最后整体推回左边界 0) centerParallelGroups(nodes, out, box); // 归零(2026-10-03 三视图统一新增):全体平移使 minX=minY=0,positions 消费端无需再算包围盒 if (!box.isEmpty()) { int minX = Integer.MAX_VALUE; int minY = Integer.MAX_VALUE; for (int[] b : box.values()) { minX = Math.min(minX, b[0]); minY = Math.min(minY, b[1]); } if (minX != 0 || minY != 0) { for (int[] b : box.values()) { b[0] -= minX; b[1] -= minY; } } } return box; } /** * 并行分支组水平居中:分支不再从 SPLIT 行向右堆叠,而是以 SPLIT 竖直中线为轴左右对称展开。 * SPLIT↔JOIN 配对沿用编辑器口径(各出边可达集求交,取 BFS 序最早的 PARALLEL_JOIN); * 组员 = SPLIT 到 JOIN 之间可达的全部节点;JOIN 水平中点吸附回 SPLIT 中线。 * 内层组先于外层组处理(嵌套并行整体平移不破坏内层居中);结束统一把最左列推回 0。 */ private void centerParallelGroups(List nodes, Map> out, Map box) { class Group { final String split; final String join; final List members; final int maxY; Group(String split, String join, List members, int maxY) { this.split = split; this.join = join; this.members = members; this.maxY = maxY; } } List groups = new ArrayList<>(); for (FlowModels.NodeVo split : nodes) { if (!FlowDefModel.TYPE_PARALLEL_SPLIT.equals(split.getType()) || !box.containsKey(split.getId())) { continue; } String sid = split.getId(); // BFS 序 + 各分支可达集,用于定位配对 JOIN Map order = new HashMap<>(); order.put(sid, 0); List bfs = new ArrayList<>(); bfs.add(sid); while (!bfs.isEmpty()) { String cur = bfs.remove(0); for (String to : out.getOrDefault(cur, new ArrayList<>())) { if (!order.containsKey(to)) { order.put(to, order.size()); bfs.add(to); } } } List heads = out.getOrDefault(sid, new ArrayList<>()); List> reachSets = new ArrayList<>(); for (String head : heads) { java.util.Set set = new java.util.HashSet<>(); List q = new ArrayList<>(); q.add(head); while (!q.isEmpty()) { String cur = q.remove(0); if (set.contains(cur)) { continue; } set.add(cur); q.addAll(out.getOrDefault(cur, new ArrayList<>())); } reachSets.add(set); } java.util.Set common = reachSets.isEmpty() ? new java.util.HashSet<>() : reachSets.get(0); for (java.util.Set set : reachSets) { common = new java.util.HashSet<>(common); common.retainAll(set); } String join = null; int joinOrder = Integer.MAX_VALUE; for (FlowModels.NodeVo n : nodes) { if (FlowDefModel.TYPE_PARALLEL_JOIN.equals(n.getType()) && box.containsKey(n.getId()) && common.contains(n.getId())) { int o = order.getOrDefault(n.getId(), Integer.MAX_VALUE); if (o < joinOrder) { joinOrder = o; join = n.getId(); } } } if (join == null) { continue; } // 组员:SPLIT 起步、不穿过 JOIN 的可达集(含嵌套 SPLIT/JOIN) List members = new ArrayList<>(); java.util.Set seen = new java.util.HashSet<>(); seen.add(join); List q = new ArrayList<>(); q.add(sid); while (!q.isEmpty()) { String cur = q.remove(0); for (String to : out.getOrDefault(cur, new ArrayList<>())) { if (seen.contains(to) || !box.containsKey(to)) { continue; } seen.add(to); members.add(to); q.add(to); } } int maxLevel = 0; for (String m : members) { maxLevel = Math.max(maxLevel, box.get(m)[1]); } groups.add(new Group(sid, join, members, maxLevel)); } // maxY 降序 = 内层组先处理 groups.sort((a, b) -> Integer.compare(b.maxY, a.maxY)); for (Group gp : groups) { int[] s = box.get(gp.split); int axisX = s[0] + s[2] / 2; int left = Integer.MAX_VALUE; int right = Integer.MIN_VALUE; for (String m : gp.members) { int[] b = box.get(m); left = Math.min(left, b[0]); right = Math.max(right, b[0] + b[2]); } int delta = axisX - (left + right) / 2; for (String m : gp.members) { box.get(m)[0] += delta; } int[] j = box.get(gp.join); j[0] += axisX - (j[0] + j[2] / 2); } // 左移出界(x<0)的组把整图平移回 0(同 delta 平移不破坏各组居中) int minX = Integer.MAX_VALUE; for (int[] b : box.values()) { minX = Math.min(minX, b[0]); } if (minX < 0) { for (int[] b : box.values()) { b[0] -= minX; } } } }