fix(diagram): CONDITION 多出口分支分列——不等长/空分支不再被拓扑分层压成单列直穿(用户实测 def-32 或签流程报错)。spreadConditionBranches 后处理:各出口分支按列对称展开(链形分支逐节点对列中心,内嵌并行回退整块平移),隐式汇合点(各分支可达集交集 BFS 最早节点)吸附回条件节点中线;等长同层分支为恒等变换不回归(golden 8/8)
build / build-push (push) Successful in 52s
build / build-push (push) Successful in 52s
This commit is contained in:
@@ -111,6 +111,10 @@ public class DiagramLayoutService {
|
||||
// 并行分支组以 SPLIT 竖直中线为轴左右居中(内层组先处理,最后整体推回左边界 0)
|
||||
centerParallelGroups(nodes, out, box);
|
||||
|
||||
// 条件分支分列(2026-10-12):CONDITION 多出口各分支水平分列、隐式汇合点吸附回条件节点
|
||||
// 中线——修复不等长/空分支被拓扑分层压成单列、跳层边直穿中间节点且出口文案错位的问题
|
||||
spreadConditionBranches(nodes, out, box);
|
||||
|
||||
// 归零(2026-10-03 三视图统一新增):全体平移使 minX=minY=0,positions 消费端无需再算包围盒
|
||||
if (!box.isEmpty()) {
|
||||
int minX = Integer.MAX_VALUE;
|
||||
@@ -129,6 +133,139 @@ public class DiagramLayoutService {
|
||||
return box;
|
||||
}
|
||||
|
||||
/**
|
||||
* 条件分支分列(2026-10-12):CONDITION 多出口按 SPLIT 同款"分支列"处理——第 i 个出口的
|
||||
* 分支成员整体平移到「条件节点竖直中线 + (i-(n-1)/2)·列距」的列中心,出口汇合点(各分支
|
||||
* 可达集交集的 BFS 最早节点,即隐式 JOIN)不移动、留在中线上。
|
||||
* <p>
|
||||
* 背景:等长分支天然被拓扑分层排进同一层横排(旧测试 conditionBranches 同层用例);
|
||||
* 不等长/空分支(如"校长室→抄送→审批 / 其他→直达结束")各层退化单列,跳层出口边变成
|
||||
* 穿过中间节点的直线、出口文案叠在链中间。本 pass 对已正常的等长场景是恒等变换
|
||||
* (列目标=当前居中位),对不等长场景把各分支拉回各自列。
|
||||
* 嵌套/顺序多个条件节点按定义序处理:外层先整体平移分支(内层条件节点随之移动),
|
||||
* 内层处理时以移动后的自身中线为轴再展开,位移可组合。
|
||||
*/
|
||||
private void spreadConditionBranches(List<FlowModels.NodeVo> nodes, Map<String, List<String>> out,
|
||||
Map<String, int[]> box) {
|
||||
int colSpan = SLOT_W + GAP_X;
|
||||
for (FlowModels.NodeVo cond : nodes) {
|
||||
if (!FlowDefModel.TYPE_CONDITION.equals(cond.getType()) || !box.containsKey(cond.getId())) {
|
||||
continue;
|
||||
}
|
||||
List<String> heads = out.getOrDefault(cond.getId(), new ArrayList<>());
|
||||
if (heads.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
// 各分支可达集(含汇合点),用于定位隐式 JOIN
|
||||
List<java.util.Set<String>> reachSets = new ArrayList<>();
|
||||
for (String head : heads) {
|
||||
java.util.Set<String> set = new java.util.HashSet<>();
|
||||
List<String> q = new ArrayList<>();
|
||||
q.add(head);
|
||||
while (!q.isEmpty()) {
|
||||
String cur = q.remove(0);
|
||||
if (!box.containsKey(cur) || set.contains(cur)) {
|
||||
continue;
|
||||
}
|
||||
set.add(cur);
|
||||
q.addAll(out.getOrDefault(cur, new ArrayList<>()));
|
||||
}
|
||||
reachSets.add(set);
|
||||
}
|
||||
java.util.Set<String> common = reachSets.isEmpty() ? new java.util.HashSet<>() : reachSets.get(0);
|
||||
for (java.util.Set<String> set : reachSets) {
|
||||
common = new java.util.HashSet<>(common);
|
||||
common.retainAll(set);
|
||||
}
|
||||
if (common.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
// 隐式 JOIN:BFS 序最早(近者优先,跳过条件节点自身)
|
||||
Map<String, Integer> order = new HashMap<>();
|
||||
order.put(cond.getId(), 0);
|
||||
List<String> bfs = new ArrayList<>();
|
||||
bfs.add(cond.getId());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
String join = null;
|
||||
int joinOrder = Integer.MAX_VALUE;
|
||||
for (String id : order.keySet()) {
|
||||
if (id.equals(cond.getId()) || !common.contains(id)) {
|
||||
continue;
|
||||
}
|
||||
int o = order.get(id);
|
||||
if (o < joinOrder) {
|
||||
joinOrder = o;
|
||||
join = id;
|
||||
}
|
||||
}
|
||||
if (join == null) {
|
||||
continue;
|
||||
}
|
||||
int[] cb = box.get(cond.getId());
|
||||
int axisX = cb[0] + cb[2] / 2;
|
||||
int n = heads.size();
|
||||
java.util.Set<String> forkClaimed = new java.util.HashSet<>();
|
||||
for (int i = 0; i < n; i++) {
|
||||
// 分支成员:从 head 出发、不穿隐式 JOIN、同一汇合点内不重复认领(菱形交叉兜底)
|
||||
List<String> members = new ArrayList<>();
|
||||
List<String> q = new ArrayList<>();
|
||||
q.add(heads.get(i));
|
||||
while (!q.isEmpty()) {
|
||||
String cur = q.remove(0);
|
||||
if (cur.equals(join) || !box.containsKey(cur) || forkClaimed.contains(cur)) {
|
||||
continue;
|
||||
}
|
||||
forkClaimed.add(cur);
|
||||
members.add(cur);
|
||||
q.addAll(out.getOrDefault(cur, new ArrayList<>()));
|
||||
}
|
||||
if (members.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
// 链形分支(每层至多一个成员)逐节点对齐到分支列中心——多层分支整块平移保不了同列;
|
||||
// 非链形(分支内嵌并行等)回退整块平移,保住内部结构不塌列
|
||||
boolean chain = true;
|
||||
java.util.Set<Integer> rows = new java.util.HashSet<>();
|
||||
for (String m : members) {
|
||||
if (!rows.add(box.get(m)[1])) {
|
||||
chain = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
int left = Integer.MAX_VALUE;
|
||||
int right = Integer.MIN_VALUE;
|
||||
for (String m : members) {
|
||||
int[] b = box.get(m);
|
||||
left = Math.min(left, b[0]);
|
||||
right = Math.max(right, b[0] + b[2]);
|
||||
}
|
||||
// (i·列距) − ((n−1)·列距)/2:偶数出口也有精确的 ±列距/2 偏移(colSpan 恒偶,整除无误差)
|
||||
int target = axisX + i * colSpan - (n - 1) * colSpan / 2;
|
||||
if (chain) {
|
||||
for (String m : members) {
|
||||
int[] b = box.get(m);
|
||||
b[0] += target - (b[0] + b[2] / 2);
|
||||
}
|
||||
} else {
|
||||
int delta = target - (left + right) / 2;
|
||||
if (delta != 0) {
|
||||
for (String m : members) {
|
||||
box.get(m)[0] += delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 并行分支组水平居中:分支不再从 SPLIT 行向右堆叠,而是以 SPLIT 竖直中线为轴左右对称展开。
|
||||
* SPLIT↔JOIN 配对沿用编辑器口径(各出边可达集求交,取 BFS 序最早的 PARALLEL_JOIN);
|
||||
|
||||
@@ -183,6 +183,60 @@ class DiagramLayoutServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void conditionUnequalBranches_forkIntoColumns_joinStaysOnAxis() {
|
||||
// def-32 同款(2026-10-12 用户报错场景):校长室线=抄送→科室审批(两层),其他线=直达结束;
|
||||
// 修复前各层退化单列、跳层出口边直穿中间节点。修复后:分支成员独占一列,隐式汇合点(end)
|
||||
// 留在条件节点中线上,cond→end 直落边在自己的车道里(不与分支列水平相交)。
|
||||
List<FlowModels.NodeVo> nodes = new ArrayList<>(Arrays.asList(
|
||||
node("start", FlowDefModel.TYPE_START),
|
||||
node("n1", FlowDefModel.TYPE_APPROVAL),
|
||||
node("cond", FlowDefModel.TYPE_CONDITION),
|
||||
node("cc1", FlowDefModel.TYPE_NOTIFY),
|
||||
node("n3", FlowDefModel.TYPE_APPROVAL),
|
||||
node("end", FlowDefModel.TYPE_END)));
|
||||
Map<String, int[]> box = layout(nodes,
|
||||
edge("start", "n1"), edge("n1", "cond"),
|
||||
edge("cond", "cc1"), edge("cond", "end"),
|
||||
edge("cc1", "n3"), edge("n3", "end"));
|
||||
assertEquals(cx(box.get("cc1")), cx(box.get("n3")), 1, "校长室线成员应同列");
|
||||
assertEquals(cx(box.get("cond")), cx(box.get("end")), 1, "隐式汇合点应留在条件节点中线");
|
||||
assertEquals((DiagramLayoutService.SLOT_W + DiagramLayoutService.GAP_X) / 2,
|
||||
Math.abs(cx(box.get("cond")) - cx(box.get("cc1"))), 1, "空分支时非空分支占 ±列距/2 位(与等长分支对称口径一致)");
|
||||
// 直落边车道无遮挡:cc1/n3 的盒体不得横跨 cond→end 垂直线(x=条件节点中线)
|
||||
int axis = cx(box.get("cond"));
|
||||
for (String m : new String[]{"cc1", "n3"}) {
|
||||
int[] b = box.get(m);
|
||||
assertTrue(b[0] + b[2] <= axis || b[0] >= axis, m + " 盒体跨中线会被直落边穿过");
|
||||
}
|
||||
for (int[] b : box.values()) {
|
||||
assertTrue(b[0] >= 0 && b[1] >= 0, "归零后不得有负坐标");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void conditionBothBranchesNonEmpty_multiLevelSideBySide() {
|
||||
// 两分支均多层且长度不等:A 线两层、B 线一层——各自成列、汇合点回中线
|
||||
List<FlowModels.NodeVo> nodes = new ArrayList<>(Arrays.asList(
|
||||
node("start", FlowDefModel.TYPE_START),
|
||||
node("cond", FlowDefModel.TYPE_CONDITION),
|
||||
node("a1", FlowDefModel.TYPE_APPROVAL),
|
||||
node("a2", FlowDefModel.TYPE_APPROVAL),
|
||||
node("b1", FlowDefModel.TYPE_APPROVAL),
|
||||
node("end", FlowDefModel.TYPE_END)));
|
||||
Map<String, int[]> box = layout(nodes,
|
||||
edge("start", "cond"),
|
||||
edge("cond", "a1"), edge("cond", "b1"),
|
||||
edge("a1", "a2"), edge("a2", "end"), edge("b1", "end"));
|
||||
assertEquals(cx(box.get("a1")), cx(box.get("a2")), 1, "A 线成员同列");
|
||||
assertEquals(cx(box.get("cond")), cx(box.get("end")), 1, "汇合点在中线");
|
||||
// A/B 两列分居中线两侧(±列距/2 对称)
|
||||
assertTrue((cx(box.get("a1")) < cx(box.get("cond"))) != (cx(box.get("b1")) < cx(box.get("cond"))),
|
||||
"两分支应分居中线两侧");
|
||||
assertEquals(DiagramLayoutService.SLOT_W + DiagramLayoutService.GAP_X,
|
||||
Math.abs(cx(box.get("b1")) - cx(box.get("a1"))), 1, "两分支列距");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cycle_defensive_fallbackLevelStillPositionsAllNodes() {
|
||||
List<FlowModels.NodeVo> nodes = new ArrayList<>(Arrays.asList(
|
||||
|
||||
Reference in New Issue
Block a user