Skip to content

Commit ae5181d

Browse files
committed
Improve auto link positioning to consider intermediate resources
- Add countResourcesInDirections() to count siblings in each direction (N/E/W/S) - Add adjustCountsForLCAChildren() to adjust counts based on intermediate LCA children - Enhance selectOptimalPosition() to choose based on resource counts and LCA direction - Improve perpendicular direction selection using resource position (dx/dy) - Add comprehensive test cases for the new logic This improvement helps links avoid intermediate resources more intelligently, resulting in better visual clarity in complex hierarchical diagrams.
1 parent e0f4325 commit ae5181d

File tree

2 files changed

+513
-19
lines changed

2 files changed

+513
-19
lines changed

internal/types/link.go

Lines changed: 306 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,36 +1004,267 @@ func AutoCalculatePositions(source, target *Resource) (sourcePos, targetPos Wind
10041004
direction := commonAncestor.direction
10051005
log.Infof("Auto-positioning: Found common ancestor with direction=%s", direction)
10061006

1007-
if direction == "vertical" {
1008-
// VerticalStack: use vertical connections
1007+
// Find LCA direct children that contain source and target
1008+
sourceChild := findChildAncestorInLCA(commonAncestor, source)
1009+
targetChild := findChildAncestorInLCA(commonAncestor, target)
1010+
1011+
if sourceChild == nil || targetChild == nil {
1012+
log.Warnf("Could not find LCA child ancestors, falling back to distance-based")
1013+
sourcePos, targetPos = calculateByDistance(dx, dy)
1014+
return sourcePos, targetPos
1015+
}
1016+
1017+
// Count resources in each direction (from resource to LCA child)
1018+
sourceCounts := countResourcesInDirections(source, sourceChild)
1019+
targetCounts := countResourcesInDirections(target, targetChild)
1020+
1021+
log.Infof("Auto-positioning: Source counts (before adjustment): N=%d, E=%d, W=%d, S=%d",
1022+
sourceCounts.North, sourceCounts.East, sourceCounts.West, sourceCounts.South)
1023+
log.Infof("Auto-positioning: Target counts (before adjustment): N=%d, E=%d, W=%d, S=%d",
1024+
targetCounts.North, targetCounts.East, targetCounts.West, targetCounts.South)
1025+
1026+
// Adjust counts based on LCA children relationship
1027+
adjustCountsForLCAChildren(commonAncestor, sourceChild, targetChild, &sourceCounts, &targetCounts)
1028+
1029+
log.Infof("Auto-positioning: Source counts (after adjustment): N=%d, E=%d, W=%d, S=%d",
1030+
sourceCounts.North, sourceCounts.East, sourceCounts.West, sourceCounts.South)
1031+
log.Infof("Auto-positioning: Target counts (after adjustment): N=%d, E=%d, W=%d, S=%d",
1032+
targetCounts.North, targetCounts.East, targetCounts.West, targetCounts.South)
1033+
1034+
// Select optimal positions based on counts and direction
1035+
sourcePos = selectOptimalPosition(sourceCounts, direction, dx, dy, true)
1036+
targetPos = selectOptimalPosition(targetCounts, direction, -dx, -dy, false)
1037+
} else {
1038+
// No common ancestor: use distance-based logic
1039+
sourcePos, targetPos = calculateByDistance(dx, dy)
1040+
}
1041+
1042+
log.Infof("Auto-positioning: Source=%v, Target=%v", sourcePos, targetPos)
1043+
1044+
return sourcePos, targetPos
1045+
}
1046+
1047+
// selectOptimalPosition selects the best position based on resource counts and LCA direction
1048+
func selectOptimalPosition(counts DirectionCounts, lcaDirection string, dx, dy int, isSource bool) Windrose {
1049+
// Find minimum count
1050+
minCount := counts.North
1051+
if counts.East < minCount {
1052+
minCount = counts.East
1053+
}
1054+
if counts.West < minCount {
1055+
minCount = counts.West
1056+
}
1057+
if counts.South < minCount {
1058+
minCount = counts.South
1059+
}
1060+
1061+
// Collect directions with minimum count
1062+
candidates := []Windrose{}
1063+
if counts.North == minCount {
1064+
candidates = append(candidates, WINDROSE_N)
1065+
}
1066+
if counts.East == minCount {
1067+
candidates = append(candidates, WINDROSE_E)
1068+
}
1069+
if counts.West == minCount {
1070+
candidates = append(candidates, WINDROSE_W)
1071+
}
1072+
if counts.South == minCount {
1073+
candidates = append(candidates, WINDROSE_S)
1074+
}
1075+
1076+
// If only one candidate, return it
1077+
if len(candidates) == 1 {
1078+
return candidates[0]
1079+
}
1080+
1081+
// Multiple candidates: prioritize based on LCA direction
1082+
// Priority: LCA direction (preferred) -> perpendicular (based on dx/dy) -> opposite (avoid)
1083+
1084+
if lcaDirection == "vertical" {
1085+
// Preferred: N or S (based on dy)
1086+
// Perpendicular: E or W (based on dx)
1087+
// Opposite: opposite of preferred
1088+
var preferred Windrose
1089+
var opposite Windrose
1090+
if dy > 0 {
1091+
preferred = WINDROSE_S
1092+
opposite = WINDROSE_N
1093+
} else {
1094+
preferred = WINDROSE_N
1095+
opposite = WINDROSE_S
1096+
}
1097+
1098+
// Check preferred direction
1099+
if containsWindrose(candidates, preferred) {
1100+
return preferred
1101+
}
1102+
1103+
// Check perpendicular (E/W) - choose based on dx
1104+
hasE := containsWindrose(candidates, WINDROSE_E)
1105+
hasW := containsWindrose(candidates, WINDROSE_W)
1106+
1107+
if hasE && hasW {
1108+
// Both perpendicular directions available, choose based on dx
1109+
if dx > 0 {
1110+
return WINDROSE_E
1111+
}
1112+
return WINDROSE_W
1113+
} else if hasE {
1114+
return WINDROSE_E
1115+
} else if hasW {
1116+
return WINDROSE_W
1117+
}
1118+
1119+
// Last resort: opposite
1120+
if containsWindrose(candidates, opposite) {
1121+
return opposite
1122+
}
1123+
1124+
} else if lcaDirection == "horizontal" {
1125+
// Preferred: E or W (based on dx)
1126+
// Perpendicular: N or S (based on dy)
1127+
// Opposite: opposite of preferred
1128+
var preferred Windrose
1129+
var opposite Windrose
1130+
if dx > 0 {
1131+
preferred = WINDROSE_E
1132+
opposite = WINDROSE_W
1133+
} else {
1134+
preferred = WINDROSE_W
1135+
opposite = WINDROSE_E
1136+
}
1137+
1138+
// Check preferred direction
1139+
if containsWindrose(candidates, preferred) {
1140+
return preferred
1141+
}
1142+
1143+
// Check perpendicular (N/S) - choose based on dy
1144+
hasN := containsWindrose(candidates, WINDROSE_N)
1145+
hasS := containsWindrose(candidates, WINDROSE_S)
1146+
1147+
if hasN && hasS {
1148+
// Both perpendicular directions available, choose based on dy
10091149
if dy > 0 {
1010-
sourcePos = WINDROSE_S
1011-
targetPos = WINDROSE_N
1012-
} else {
1013-
sourcePos = WINDROSE_N
1014-
targetPos = WINDROSE_S
1150+
return WINDROSE_S
10151151
}
1016-
} else if direction == "horizontal" {
1017-
// HorizontalStack: use horizontal connections
1152+
return WINDROSE_N
1153+
} else if hasN {
1154+
return WINDROSE_N
1155+
} else if hasS {
1156+
return WINDROSE_S
1157+
}
1158+
1159+
// Last resort: opposite
1160+
if containsWindrose(candidates, opposite) {
1161+
return opposite
1162+
}
1163+
1164+
} else {
1165+
// Unknown direction: fall back to distance-based
1166+
if abs(dx) > abs(dy) {
10181167
if dx > 0 {
1019-
sourcePos = WINDROSE_E
1020-
targetPos = WINDROSE_W
1168+
if containsWindrose(candidates, WINDROSE_E) {
1169+
return WINDROSE_E
1170+
}
10211171
} else {
1022-
sourcePos = WINDROSE_W
1023-
targetPos = WINDROSE_E
1172+
if containsWindrose(candidates, WINDROSE_W) {
1173+
return WINDROSE_W
1174+
}
10241175
}
10251176
} else {
1026-
// Unknown direction: fall back to distance-based logic
1027-
sourcePos, targetPos = calculateByDistance(dx, dy)
1177+
if dy > 0 {
1178+
if containsWindrose(candidates, WINDROSE_S) {
1179+
return WINDROSE_S
1180+
}
1181+
} else {
1182+
if containsWindrose(candidates, WINDROSE_N) {
1183+
return WINDROSE_N
1184+
}
1185+
}
1186+
}
1187+
}
1188+
1189+
// Fallback: return first candidate
1190+
return candidates[0]
1191+
}
1192+
1193+
// containsWindrose checks if a slice contains a Windrose value
1194+
func containsWindrose(slice []Windrose, value Windrose) bool {
1195+
for _, v := range slice {
1196+
if v == value {
1197+
return true
1198+
}
1199+
}
1200+
return false
1201+
}
1202+
1203+
// adjustCountsForLCAChildren adjusts counts based on LCA children relationship
1204+
// Example: LCA{V1{R1}, V2{R2}, V3{R3}}, R3->R1 link
1205+
// LCA children: V3->V1, with V2 in between
1206+
// If LCA.direction = "horizontal": V3.West += 1 (V2), V1.East += 1 (V2)
1207+
// If LCA.direction = "vertical": V3.North += 1 (V2), V1.South += 1 (V2)
1208+
func adjustCountsForLCAChildren(lca, sourceChild, targetChild *Resource, sourceCounts, targetCounts *DirectionCounts) {
1209+
if lca == nil || sourceChild == nil || targetChild == nil {
1210+
return
1211+
}
1212+
1213+
// Find indices of source and target children in LCA
1214+
sourceIndex := -1
1215+
targetIndex := -1
1216+
for i, child := range lca.children {
1217+
if child == sourceChild {
1218+
sourceIndex = i
10281219
}
1220+
if child == targetChild {
1221+
targetIndex = i
1222+
}
1223+
}
1224+
1225+
if sourceIndex == -1 || targetIndex == -1 || sourceIndex == targetIndex {
1226+
return
1227+
}
1228+
1229+
// Count resources between source and target children
1230+
var betweenCount int
1231+
if sourceIndex < targetIndex {
1232+
betweenCount = targetIndex - sourceIndex - 1
10291233
} else {
1030-
// No common ancestor: use distance-based logic
1031-
sourcePos, targetPos = calculateByDistance(dx, dy)
1234+
betweenCount = sourceIndex - targetIndex - 1
10321235
}
10331236

1034-
log.Infof("Auto-positioning: Source=%v, Target=%v", sourcePos, targetPos)
1237+
if betweenCount <= 0 {
1238+
return
1239+
}
10351240

1036-
return sourcePos, targetPos
1241+
log.Infof("Adjusting counts: %d resources between LCA children (sourceIndex=%d, targetIndex=%d)",
1242+
betweenCount, sourceIndex, targetIndex)
1243+
1244+
// Adjust counts based on LCA direction
1245+
if lca.direction == "horizontal" {
1246+
// Horizontal: West->East order
1247+
if sourceIndex < targetIndex {
1248+
// Source is West of Target
1249+
sourceCounts.East += betweenCount
1250+
targetCounts.West += betweenCount
1251+
} else {
1252+
// Source is East of Target
1253+
sourceCounts.West += betweenCount
1254+
targetCounts.East += betweenCount
1255+
}
1256+
} else if lca.direction == "vertical" {
1257+
// Vertical: North->South order
1258+
if sourceIndex < targetIndex {
1259+
// Source is North of Target
1260+
sourceCounts.South += betweenCount
1261+
targetCounts.North += betweenCount
1262+
} else {
1263+
// Source is South of Target
1264+
sourceCounts.North += betweenCount
1265+
targetCounts.South += betweenCount
1266+
}
1267+
}
10371268
}
10381269

10391270
// findLowestCommonAncestor finds the lowest common ancestor using set method
@@ -1094,6 +1325,62 @@ func abs(x int) int {
10941325
return x
10951326
}
10961327

1328+
// DirectionCounts holds the count of resources in each direction
1329+
type DirectionCounts struct {
1330+
North int
1331+
East int
1332+
West int
1333+
South int
1334+
}
1335+
1336+
// countResourcesInDirections counts resources between target and LCA in each direction
1337+
// VerticalStack: North->South order, HorizontalStack: West->East order
1338+
func countResourcesInDirections(target, lca *Resource) DirectionCounts {
1339+
counts := DirectionCounts{}
1340+
1341+
if target == lca {
1342+
return counts
1343+
}
1344+
1345+
// Traverse from target to LCA
1346+
current := target
1347+
for current != nil && current != lca {
1348+
parent := current.GetParent()
1349+
if parent == nil {
1350+
break
1351+
}
1352+
1353+
// Find current's index in parent's children
1354+
currentIndex := -1
1355+
for i, child := range parent.children {
1356+
if child == current {
1357+
currentIndex = i
1358+
break
1359+
}
1360+
}
1361+
1362+
if currentIndex == -1 {
1363+
current = parent
1364+
continue
1365+
}
1366+
1367+
// Count siblings based on parent's direction
1368+
if parent.direction == "vertical" {
1369+
// VerticalStack: North->South order
1370+
counts.North += currentIndex
1371+
counts.South += len(parent.children) - currentIndex - 1
1372+
} else if parent.direction == "horizontal" {
1373+
// HorizontalStack: West->East order
1374+
counts.West += currentIndex
1375+
counts.East += len(parent.children) - currentIndex - 1
1376+
}
1377+
1378+
current = parent
1379+
}
1380+
1381+
return counts
1382+
}
1383+
10971384
// calculateLCABasedMidpoint calculates midpoint using LCA information
10981385
func (l *Link) calculateLCABasedMidpoint(sourcePt, targetPt image.Point) image.Point {
10991386
log.Infof("=== LCA-based Midpoint Calculation ===")

0 commit comments

Comments
 (0)