@@ -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
10981385func (l * Link ) calculateLCABasedMidpoint (sourcePt , targetPt image.Point ) image.Point {
10991386 log .Infof ("=== LCA-based Midpoint Calculation ===" )
0 commit comments