Preserve settled route budgets and paths during edge previews

This commit is contained in:
2026-09-05 20:41:00 +03:00
parent 68b1efff70
commit 30c7db105a
2 changed files with 91 additions and 0 deletions
+34
View File
@@ -38,6 +38,9 @@ pub struct RoutedEdge {
pub pin_fractions: Vec<f32>, pub pin_fractions: Vec<f32>,
pub warnings: Vec<RouteWarning>, pub warnings: Vec<RouteWarning>,
pub custom_suspended: bool, pub custom_suspended: bool,
// Derived solver state, never persisted. A preview must inherit the same
// remaining work budget as this edge had in stable-ID settled traversal.
search_budget_before: usize,
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@@ -395,6 +398,36 @@ fn route_diagram_impl(
r.inflate(CLEARANCE) r.inflate(CLEARANCE)
}) })
.collect(); .collect();
let cached = preview.and_then(|(baseline, changed)| {
(edge.id == changed)
.then(|| baseline.edges.get(&edge.id))
.flatten()
});
if let Some(cached) = cached {
budget = cached.search_budget_before;
if cached.pins == pins && cached.custom_suspended == custom_suspended {
// Label-only editing cannot change the connection beneath the
// pointer, including a settled fallback with routing warnings.
let mut updated = cached.clone();
updated.label = make_label(
project,
edge.id,
&updated.points,
route.and_then(|r| r.label.as_ref()),
&obstacles,
);
for &point in &updated.points {
bounds.point(point);
}
if !updated.label.text.is_empty() {
bounds.rect(updated.label.bounds.position, updated.label.bounds.size);
}
record_segments(&mut previous, &updated.points);
edges.insert(edge.id, updated);
continue;
}
}
let search_budget_before = budget;
let mut warnings = Vec::new(); let mut warnings = Vec::new();
if custom_suspended { if custom_suspended {
warnings.push(RouteWarning::PromotedEndpoints); warnings.push(RouteWarning::PromotedEndpoints);
@@ -449,6 +482,7 @@ fn route_diagram_impl(
pin_fractions, pin_fractions,
warnings, warnings,
custom_suspended, custom_suspended,
search_budget_before,
}, },
); );
} }
+57
View File
@@ -614,6 +614,63 @@ fn single_edge_preview_matches_settled_geometry_and_preserves_other_routes() {
assert_eq!(preview.bounds, complete.bounds); assert_eq!(preview.bounds, complete.bounds);
} }
#[test]
fn exhausted_search_budget_survives_label_and_pin_previews() {
let (mut p, d, original) = maze();
// Valid reuse of one typed port pair: many distinct exchanges through the
// same multi-turn corridor exhaust the shared per-view search budget.
let template = p.relations.remove(&original).unwrap();
for i in 0..1500 {
let mut edge = template.clone();
edge.id = Id::from_u128(100 + i);
edge.alias = format!("budget_flow_{i}");
p.relations.insert(edge.id, edge);
}
validate_candidate(&p).unwrap();
let view = projected(&p, d);
let baseline = route_diagram(&p, &p.diagrams[&d], &view);
let exhausted = baseline
.edges
.values()
.find(|edge| edge.warnings.contains(&RouteWarning::SearchBudgetExhausted))
.expect("Fixture must exhaust the cumulative budget");
let id = exhausted.id;
set_route(
&mut p,
d,
id,
vec![],
Some(EdgeLabel {
fraction: exhausted.label.fraction,
offset: point(0., -40.),
}),
);
let label_preview = route_edge_preview(&p, &p.diagrams[&d], &view, &baseline, id);
assert_eq!(label_preview.edges[&id].points, exhausted.points);
assert_eq!(
label_preview.edges[&id].pin_fractions,
exhausted.pin_fractions
);
assert_eq!(label_preview.edges[&id].warnings, exhausted.warnings);
assert_ne!(
label_preview.edges[&id].label.center,
exhausted.label.center
);
assert_eq!(
label_preview.edges[&id],
route_diagram(&p, &p.diagrams[&d], &view).edges[&id]
);
// Adding a pin requires actual rerouting. Its inherited pre-edge budget
// must agree with the full solve, rather than obtaining a fresh view budget.
p.diagrams.get_mut(&d).unwrap().routes[0]
.waypoints
.push(point(650., 400.));
let pin_preview = route_edge_preview(&p, &p.diagrams[&d], &view, &baseline, id);
let full = route_diagram(&p, &p.diagrams[&d], &view);
assert_eq!(pin_preview.edges[&id], full.edges[&id]);
assert!(pin_preview.edges[&id].points.contains(&point(650., 400.)));
}
#[test] #[test]
fn interpolation_projection_empty_and_repeated_points_are_well_defined() { fn interpolation_projection_empty_and_repeated_points_are_well_defined() {
assert_eq!(point_along_polyline(&[], 0.5), None); assert_eq!(point_along_polyline(&[], 0.5), None);