Avoid automatic diagram label collisions with stable bounded placement
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user