From 19f8cf08d730a73301dea209616a202c662f5c09 Mon Sep 17 00:00:00 2001 From: Chris Dail Date: Sun, 14 Jun 2026 10:46:53 -0700 Subject: [PATCH] Fix label overlap when districts glide to their latency distance The latency reposition moved hubs to their RTT radius without checking label collisions, so a gliding district could land on top of a neighbour. Make both placement paths overlap-aware via the existing LayoutSolver: keep distance = latency (the meaningful axis) but rotate the arbitrary angle to clear other labels. Track each hub's target position so same-tick moves avoid where others are heading, not just where they are. Co-Authored-By: Claude Opus 4.8 (1M context) --- Sources/NetworkCityApp/CityScene.swift | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Sources/NetworkCityApp/CityScene.swift b/Sources/NetworkCityApp/CityScene.swift index 299232e..2bece15 100644 --- a/Sources/NetworkCityApp/CityScene.swift +++ b/Sources/NetworkCityApp/CityScene.swift @@ -38,6 +38,7 @@ final class CityScene: SKScene { private var buildings: [String: BuildingNode] = [:] private var roads: [String: SKShapeNode] = [:] private var latencyRadius: [String: CGFloat] = [:] + private var targetPos: [String: CGPoint] = [:] // where each hub is heading private var ringKey: String? private let roadLayer = SKNode() private let carLayer = SKNode() @@ -234,6 +235,7 @@ final class CityScene: SKScene { let node = BuildingNode(title: title, subtitle: subtitle) node.position = placement(for: key, footprint: node.labelFootprint) + targetPos[key] = node.position node.alpha = 0 node.run(.fadeIn(withDuration: 0.6)) buildingLayer.addChild(node) @@ -266,8 +268,19 @@ final class CityScene: SKScene { if let current = latencyRadius[key], abs(current - target) < 18 { return } latencyRadius[key] = target + // Keep distance = latency, but rotate/nudge the (arbitrary) angle to + // clear other labels — otherwise gliding hubs land on top of each other. let angle = atan2(building.position.y, building.position.x) - let dest = CGPoint(x: cos(angle) * Double(target), y: sin(angle) * Double(target)) + let others = buildings.compactMap { (k, b) -> CGRect? in + guard k != key else { return nil } + let p = targetPos[k] ?? b.position + return b.labelFootprint.offsetBy(dx: p.x, dy: p.y) + } + let dest = LayoutSolver.placeNonOverlapping( + baseAngle: angle, baseRadius: Double(target), + localRect: building.labelFootprint, existing: others, pad: 18 + ) + targetPos[key] = dest building.run(.move(to: dest, duration: 0.8)) if let road = roads[key] { @@ -287,8 +300,9 @@ final class CityScene: SKScene { let ring = Double((h >> 16) % 1000) / 1000 let baseRadius = 165 + Double(ring) * Double(worldRadius - 165) - let existing = buildings.values.map { - $0.labelFootprint.offsetBy(dx: $0.position.x, dy: $0.position.y) + let existing = buildings.map { (k, b) in + let p = targetPos[k] ?? b.position + return b.labelFootprint.offsetBy(dx: p.x, dy: p.y) } return LayoutSolver.placeNonOverlapping( baseAngle: baseAngle, baseRadius: baseRadius,