diff --git a/crates/oa-core/src/routing.rs b/crates/oa-core/src/routing.rs index 0c079229..7ca0a4f8 100644 --- a/crates/oa-core/src/routing.rs +++ b/crates/oa-core/src/routing.rs @@ -38,6 +38,9 @@ pub struct RoutedEdge { pub pin_fractions: Vec, pub warnings: Vec, 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)] @@ -395,6 +398,36 @@ fn route_diagram_impl( r.inflate(CLEARANCE) }) .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(); if custom_suspended { warnings.push(RouteWarning::PromotedEndpoints); @@ -449,6 +482,7 @@ fn route_diagram_impl( pin_fractions, warnings, custom_suspended, + search_budget_before, }, ); } diff --git a/crates/oa-core/tests/routing_geometry.rs b/crates/oa-core/tests/routing_geometry.rs index bcfb443b..98cbe93b 100644 --- a/crates/oa-core/tests/routing_geometry.rs +++ b/crates/oa-core/tests/routing_geometry.rs @@ -614,6 +614,63 @@ fn single_edge_preview_matches_settled_geometry_and_preserves_other_routes() { 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] fn interpolation_projection_empty_and_repeated_points_are_well_defined() { assert_eq!(point_along_polyline(&[], 0.5), None);