Avoid automatic diagram label collisions with stable bounded placement

This commit is contained in:
2026-09-05 21:05:21 +03:00
parent 4b8545d57d
commit 65fa9ab276
2 changed files with 240 additions and 15 deletions
+138
View File
@@ -282,6 +282,144 @@ fn automatic_label_fraction_survives_its_first_authored_drag_without_a_jump() {
point(label.center.x + 15., label.center.y - 10.)
);
}
fn label_overlap(a: RoutingBounds, b: RoutingBounds) -> bool {
a.position.x < b.position.x + b.size.width
&& a.position.x + a.size.width > b.position.x
&& a.position.y < b.position.y + b.size.height
&& a.position.y + a.size.height > b.position.y
}
#[test]
fn workshop_automatic_labels_are_readable_without_mutating_authored_routes() {
let p = language::parse(include_str!("../../../examples/routing-workshop.arcadia")).unwrap();
let d = p
.diagrams
.values()
.find(|d| d.alias == "sa_system_boundary_and_functions")
.unwrap();
let view = project_diagram(&p, d);
let routed = route_diagram(&p, d, &view);
assert_eq!(routed.edges.len(), 12);
let labels: Vec<_> = routed
.edges
.values()
.filter(|e| !e.label.text.is_empty())
.collect();
let mut overlaps = Vec::new();
for (i, a) in labels.iter().enumerate() {
for b in labels.iter().skip(i + 1) {
if label_overlap(a.label.bounds, b.label.bounds) {
overlaps.push(format!(
"{} at {:?} / {} at {:?}",
a.label.text, a.label.center, b.label.text, b.label.center
));
}
}
}
assert!(
overlaps.is_empty(),
"Overlapping automatic workshop labels: {overlaps:#?}"
);
for edge in &view.edges {
let label = &routed.edges[&edge.id].label;
let mut ancestors = std::collections::BTreeSet::new();
for endpoint in [edge.source, edge.target] {
let mut parent = view
.nodes
.iter()
.find(|n| n.id == endpoint)
.unwrap()
.visual_parent;
while let Some(id) = parent {
ancestors.insert(id);
parent = view
.nodes
.iter()
.find(|n| n.id == id)
.and_then(|n| n.visual_parent);
}
}
for node in &view.nodes {
let size = if ancestors.contains(&node.id) {
Size {
height: node.size.height.min(64.),
..node.size
}
} else {
node.size
};
assert!(
!label_overlap(
label.bounds,
RoutingBounds {
position: node.position,
size
}
),
"{} overlaps {}",
label.text,
p.named(node.element)
);
}
}
let offset_count = labels
.iter()
.filter(|e| e.label.center != point(e.label.anchor.x, e.label.anchor.y - 18.))
.count();
eprintln!("Workshop labels: 12 clear labels; {offset_count} bounded offset placements");
// Explicit placement stays exact even when deliberately overlapping another
// label. Preview updates only the selected label; its path cannot jump.
let selected = *routed.edges.keys().last().unwrap();
let baseline = &routed.edges[&selected];
let destination = labels
.iter()
.find(|e| e.id != selected)
.unwrap()
.label
.center;
let mut edited = p.clone();
set_route(
&mut edited,
d.id,
selected,
vec![],
Some(EdgeLabel {
fraction: baseline.label.fraction,
offset: point(
destination.x - baseline.label.anchor.x,
destination.y - baseline.label.anchor.y,
),
}),
);
let preview = route_edge_preview(&edited, &edited.diagrams[&d.id], &view, &routed, selected);
assert_eq!(preview.edges[&selected].label.center, destination);
assert_eq!(preview.edges[&selected].points, baseline.points);
assert_eq!(
preview.edges[&selected],
route_diagram(&edited, &edited.diagrams[&d.id], &view).edges[&selected]
);
for (&id, edge) in &routed.edges {
if id != selected {
assert_eq!(preview.edges[&id], *edge);
}
}
// Resetting a label uses the same earlier-label occupancy as a settled solve.
edited.diagrams.get_mut(&d.id).unwrap().routes[0].label = None;
let reset = route_edge_preview(&edited, &edited.diagrams[&d.id], &view, &preview, selected);
assert_eq!(reset.edges[&selected], *baseline);
// An actual bend edit also places the automatic label against that prefix.
edited.diagrams.get_mut(&d.id).unwrap().routes[0]
.waypoints
.push(point(1100., 720.));
let pin_preview =
route_edge_preview(&edited, &edited.diagrams[&d.id], &view, &routed, selected);
assert_eq!(
pin_preview.edges[&selected],
route_diagram(&edited, &edited.diagrams[&d.id], &view).edges[&selected]
);
}
fn distance(a: Position, b: Position) -> f32 {
(a.x - b.x).hypot(a.y - b.y)
}