feat(diagram): 流程图竖排改造(横→竖)——布局单源坐标转置+名单2行换行+边几何/箭头/标签翻转,golden 用例改写并新增 6 出口用例
build / build-push (push) Successful in 2m44s

This commit is contained in:
chenxing
2026-10-09 19:42:50 +08:00
parent 56a71f813f
commit 7c92e953ec
3 changed files with 227 additions and 103 deletions
@@ -11,20 +11,22 @@ import java.util.List;
import java.util.Map;
/**
* 流程图布局单源(2026-10-03 三视图统一):自 DiagramImageService 原样搬移(算法与常量不变,
* PNG 出图逐像素不变)。拓扑最长路径分层(层=列,自左向右),并行分支组以 SPLIT 中心线为轴
* 垂直居中(SPLIT↔JOIN 配对按各出边可达集求交、取 BFS 序最早的 JOIN);输出内容区坐标
* (minX/minY 已归零),PNG 渲染与 DiagramVo.positions 共同消费——改布局只改这里。
* 流程图布局单源(2026-10-03 三视图统一;2026-10-09 竖排改造):拓扑最长路径分层
* (层=行,自上向下),层内按定义顺序横排、整层块对竖直中线水平居中;并行分支组以
* SPLIT 竖直中线为轴左右对称展开(SPLIT↔JOIN 配对按各出边可达集求交、取 BFS 序最早的
* JOIN);输出内容区坐标(minX/minY 已归零),PNG 渲染与 DiagramVo.positions 共同消费——
* 改布局只改这里。
*/
@Service
public class DiagramLayoutService {
/** 逻辑坐标常量(盒型在槽内居中;列间距留给正交折线走线) */
static final int SLOT_W = 210; // 列槽宽(盒型在槽内水平居中)
static final int GAP_X = 84; // 列间距(留给正交折线走线)
static final int ROW_H = 92; // 行槽高(盒型在槽内垂直居中)
/** 逻辑坐标常量(盒型在槽内居中;层间距留给竖直走线与多出口文案,分支列间无需走线空间) */
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):开始/结束胶囊、审批圆角卡、并行/条件菱形(取外接矩形) */
/** 各类型盒型(w,h):开始/结束胶囊、审批圆角卡(名单 2 行)、并行/条件菱形(取外接矩形) */
static int[] boxOf(String type) {
switch (type == null ? "" : type) {
case FlowDefModel.TYPE_START:
@@ -35,7 +37,7 @@ public class DiagramLayoutService {
case FlowDefModel.TYPE_CONDITION:
return new int[]{132, 78};
default:
return new int[]{200, 64};
return new int[]{264, 76};
}
}
@@ -56,7 +58,7 @@ public class DiagramLayoutService {
inDeg.merge(e.getTo(), 1, Integer::sum);
}
}
// 拓扑最长路径分层(层=列,自左向右);环兜底:无入度的从层 0 起算,成环残留节点强制层 0(仍必出图)
// 拓扑最长路径分层(层=行,自上向下);环兜底:无入度的从层 0 起算,成环残留节点强制层 0(仍必出图)
Map<String, Integer> level = new HashMap<>();
List<String> queue = new ArrayList<>();
for (Map.Entry<String, Integer> en : inDeg.entrySet()) {
@@ -80,35 +82,33 @@ public class DiagramLayoutService {
level.putIfAbsent(n.getId(), 0);
}
// 初始网格:列 x 由层号决定,列内按定义顺序纵排、整列块以内容区中线居中
// 初始网格:行 y 由层号决定,层内按定义顺序横排、整层块以内容区竖直中线居中
Map<Integer, List<FlowModels.NodeVo>> layers = new LinkedHashMap<>();
int maxLevel = 0;
int maxRows = 1;
int maxCols = 1;
for (FlowModels.NodeVo n : nodes) {
int l = level.get(n.getId());
maxLevel = Math.max(maxLevel, l);
layers.computeIfAbsent(l, k -> new ArrayList<>()).add(n);
}
for (List<FlowModels.NodeVo> layer : layers.values()) {
maxRows = Math.max(maxRows, layer.size());
maxCols = Math.max(maxCols, layer.size());
}
int axisH = maxRows * ROW_H;
int axisW = (maxCols - 1) * (SLOT_W + GAP_X) + SLOT_W;
Map<String, int[]> box = new HashMap<>();
for (Map.Entry<Integer, List<FlowModels.NodeVo>> en : layers.entrySet()) {
List<FlowModels.NodeVo> layer = en.getValue();
int blockH = layer.size() * ROW_H;
int top0 = (axisH - blockH) / 2;
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[]{
en.getKey() * (SLOT_W + GAP_X) + (SLOT_W - wh[0]) / 2,
top0 + i * ROW_H + (ROW_H - wh[1]) / 2,
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)
// 并行分支组以 SPLIT 竖直中线为轴左右居中(内层组先处理,最后整体推回左边界 0)
centerParallelGroups(nodes, out, box);
// 归零(2026-10-03 三视图统一新增):全体平移使 minX=minY=0,positions 消费端无需再算包围盒
@@ -130,10 +130,10 @@ public class DiagramLayoutService {
}
/**
* 并行分支组垂直居中:分支不再从 SPLIT 行向下堆叠,而是以 SPLIT 中心线为轴上下对称展开。
* 并行分支组水平居中:分支不再从 SPLIT 行向右堆叠,而是以 SPLIT 竖直中线为轴左右对称展开。
* SPLIT↔JOIN 配对沿用编辑器口径(各出边可达集求交,取 BFS 序最早的 PARALLEL_JOIN);
* 组员 = SPLIT 到 JOIN 之间可达的全部节点;JOIN 中线吸附回 SPLIT 中线。
* 内层组先于外层组处理(嵌套并行整体平移不破坏内层居中);结束统一把最顶行抬回 0。
* 组员 = SPLIT 到 JOIN 之间可达的全部节点;JOIN 水平中点吸附回 SPLIT 中线。
* 内层组先于外层组处理(嵌套并行整体平移不破坏内层居中);结束统一把最左列推回 0。
*/
private void centerParallelGroups(List<FlowModels.NodeVo> nodes, Map<String, List<String>> out,
Map<String, int[]> box) {
@@ -141,12 +141,12 @@ public class DiagramLayoutService {
final String split;
final String join;
final List<String> members;
final int maxX;
Group(String split, String join, List<String> members, int maxX) {
final int maxY;
Group(String split, String join, List<String> members, int maxY) {
this.split = split;
this.join = join;
this.members = members;
this.maxX = maxX;
this.maxY = maxY;
}
}
List<Group> groups = new ArrayList<>();
@@ -224,37 +224,37 @@ public class DiagramLayoutService {
}
int maxLevel = 0;
for (String m : members) {
maxLevel = Math.max(maxLevel, box.get(m)[0]);
maxLevel = Math.max(maxLevel, box.get(m)[1]);
}
groups.add(new Group(sid, join, members, maxLevel));
}
// maxX 降序 = 内层组先处理
groups.sort((a, b) -> Integer.compare(b.maxX, a.maxX));
// maxY 降序 = 内层组先处理
groups.sort((a, b) -> Integer.compare(b.maxY, a.maxY));
for (Group gp : groups) {
int[] s = box.get(gp.split);
int axisY = s[1] + s[3] / 2;
int top = Integer.MAX_VALUE;
int bottom = Integer.MIN_VALUE;
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);
top = Math.min(top, b[1]);
bottom = Math.max(bottom, b[1] + b[3]);
left = Math.min(left, b[0]);
right = Math.max(right, b[0] + b[2]);
}
int delta = axisY - (top + bottom) / 2;
int delta = axisX - (left + right) / 2;
for (String m : gp.members) {
box.get(m)[1] += delta;
box.get(m)[0] += delta;
}
int[] j = box.get(gp.join);
j[1] += axisY - (j[1] + j[3] / 2);
j[0] += axisX - (j[0] + j[2] / 2);
}
// 上移出界(y<0)的组把整图平移回 0(同 delta 平移不破坏各组居中)
int minY = Integer.MAX_VALUE;
// 左移出界(x<0)的组把整图平移回 0(同 delta 平移不破坏各组居中)
int minX = Integer.MAX_VALUE;
for (int[] b : box.values()) {
minY = Math.min(minY, b[1]);
minX = Math.min(minX, b[0]);
}
if (minY < 0) {
if (minX < 0) {
for (int[] b : box.values()) {
b[1] -= minY;
b[0] -= minX;
}
}
}