use oa_core::{projection::*, routing::*, *}; use std::time::Instant; fn point(x: f32, y: f32) -> Position { Position { x, y } } fn fixture() -> (Project, Id, Id, Id, Id) { let mut p = Project::empty(); let a = p .add_element(Kind::Function, Layer::Sa, "Send", None) .unwrap(); let b = p .add_element(Kind::Function, Layer::Sa, "Receive", None) .unwrap(); let edge = p.relate(RelationKind::FunctionalExchange, "Status report", a, b); let d = p.ensure_diagram(Layer::Sa); p.place(d, a, point(0., 100.)); p.place(d, b, point(800., 100.)); (p, d, a, b, edge) } fn projected(p: &Project, d: Id) -> ProjectedView { project_diagram(p, &p.diagrams[&d]) } fn route(p: &Project, d: Id) -> RoutedView { route_diagram(p, &p.diagrams[&d], &projected(p, d)) } fn set_route(p: &mut Project, d: Id, edge: Id, pins: Vec, label: Option) { p.diagrams.get_mut(&d).unwrap().routes.push(EdgeRoute { edge, source_attachment: None, target_attachment: None, waypoints: pins, label, }); } fn orthogonal(points: &[Position]) { assert!(points.iter().all(|p| p.x.is_finite() && p.y.is_finite())); assert!( points .windows(2) .all(|p| p[0].x == p[1].x || p[0].y == p[1].y), "{points:?}" ); } fn avoids(points: &[Position], position: Position, size: Size, clearance: f32) { let (left, right) = (position.x - clearance, position.x + size.width + clearance); let (top, bottom) = (position.y - clearance, position.y + size.height + clearance); for segment in points.windows(2) { let (a, b) = (segment[0], segment[1]); let crosses = if a.x == b.x { a.x > left + 0.001 && a.x < right - 0.001 && a.y.min(b.y) < bottom - 0.001 && a.y.max(b.y) > top + 0.001 } else { a.y > top + 0.001 && a.y < bottom - 0.001 && a.x.min(b.x) < right - 0.001 && a.x.max(b.x) > left + 0.001 }; assert!( !crosses, "Segment {a:?} -> {b:?} intersects rectangle {position:?} {size:?}" ); } } #[test] fn functional_endpoints_preserve_real_ports_and_normals_while_detouring() { let (mut p, d, _, _, edge) = fixture(); let obstacle = p .add_element(Kind::Function, Layer::Sa, "Unrelated computation", None) .unwrap(); p.place(d, obstacle, point(380., 110.)); let view = projected(&p, d); let result = route_diagram(&p, &p.diagrams[&d], &view); let path = &result.edges[&edge]; assert!(path.warnings.is_empty(), "{:?}", path.warnings); orthogonal(&path.points); let projected_edge = view.edges.iter().find(|e| e.id == edge).unwrap(); let source = view .ports .iter() .find(|p| Some(p.element) == projected_edge.source_port) .unwrap(); let target = view .ports .iter() .find(|p| Some(p.element) == projected_edge.target_port) .unwrap(); assert_eq!(path.points[0], source.position); assert_eq!( path.points[1], point(source.position.x + PORT_STUB, source.position.y) ); assert_eq!(*path.points.last().unwrap(), target.position); assert_eq!( path.points[path.points.len() - 2], point(target.position.x - PORT_STUB, target.position.y) ); let obstacle = view.nodes.iter().find(|n| n.element == obstacle).unwrap(); avoids(&path.points, obstacle.position, obstacle.size, CLEARANCE); } #[test] fn bottom_standard_ports_are_approached_from_below() { let (p, d, _, _, edge) = fixture(); let mut view = projected(&p, d); for port in &mut view.ports { let owner = view.nodes.iter().find(|n| n.id == port.owner).unwrap(); port.side = PortAnchorSide::Bottom; port.position = point( owner.position.x + owner.size.width / 2., owner.position.y + owner.size.height, ); } let result = route_diagram(&p, &p.diagrams[&d], &view); let points = &result.edges[&edge].points; assert_eq!(points[1], point(points[0].x, points[0].y + PORT_STUB)); let n = points.len(); assert_eq!( points[n - 2], point(points[n - 1].x, points[n - 1].y + PORT_STUB) ); assert!(result.edges[&edge].warnings.is_empty()); } #[test] fn nested_endpoint_enclosure_is_traversable_but_its_header_and_siblings_are_not() { let (mut p, d, a, _, edge) = fixture(); let container = p .add_element(Kind::Function, Layer::Sa, "Supervise", None) .unwrap(); p.elements.get_mut(&a).unwrap().parent = Some(container); let parent = p.place(d, container, point(-60., 0.)); p.diagrams .get_mut(&d) .unwrap() .nodes .get_mut(&parent) .unwrap() .size = Size { width: 680., height: 430., }; let sibling = p .add_element(Kind::Function, Layer::Sa, "Other work", Some(container)) .unwrap(); p.place(d, sibling, point(350., 110.)); let view = projected(&p, d); let result = route_diagram(&p, &p.diagrams[&d], &view); let routed = &result.edges[&edge]; assert!(routed.warnings.is_empty(), "{:?}", routed.warnings); let parent = view.nodes.iter().find(|n| n.element == container).unwrap(); avoids( &routed.points, parent.position, Size { width: parent.size.width, height: 64., }, CLEARANCE, ); let sibling = view.nodes.iter().find(|n| n.element == sibling).unwrap(); avoids(&routed.points, sibling.position, sibling.size, CLEARANCE); assert!(routed.points.iter().any(|p| p.x > parent.position.x && p.x < parent.position.x + parent.size.width && p.y > parent.position.y + 64.)); } #[test] fn authored_pins_keep_traversal_order_even_when_reversing_and_repeated() { let (mut p, d, _, _, edge) = fixture(); let pins = vec![point(650., 400.), point(300., 400.), point(650., 400.)]; set_route(&mut p, d, edge, pins.clone(), None); let result = route(&p, d); let r = &result.edges[&edge]; assert_eq!(r.pins, pins); assert!(r.warnings.is_empty(), "{:?}", r.warnings); assert!(r.pin_fractions.windows(2).all(|p| p[0] < p[1])); for (pin, fraction) in r.pins.iter().zip(&r.pin_fractions) { assert!(distance(*pin, point_along_polyline(&r.points, *fraction).unwrap()) < 0.01); } let first_reverse = r .points .windows(2) .position(|p| p[0] == pins[0] && p[1] == pins[1]) .unwrap(); assert_eq!(pin_insertion_index(r, first_reverse, point(500., 400.)), 1); assert_eq!( pin_insertion_index(r, first_reverse + 1, point(500., 400.)), 2 ); orthogonal(&r.points); } #[test] fn single_pin_above_an_oa_interaction_makes_a_detour_without_a_retraced_spur() { let mut p = Project::empty(); let a = p .add_element(Kind::Activity, Layer::Oa, "Observe", None) .unwrap(); let b = p .add_element(Kind::Activity, Layer::Oa, "Decide", None) .unwrap(); let d = p.ensure_diagram(Layer::Oa); p.place(d, a, point(0., 300.)); p.place(d, b, point(800., 300.)); let edge = p.relate(RelationKind::Interaction, "Observation", a, b); let pin = point(500., 100.); set_route(&mut p, d, edge, vec![pin], None); let result = route(&p, d); let r = &result.edges[&edge]; assert!(r.warnings.is_empty(), "{:?}", r.warnings); assert!(r.points.contains(&pin)); for turn in r.points.windows(3) { let dot = (turn[1].x - turn[0].x) * (turn[2].x - turn[1].x) + (turn[1].y - turn[0].y) * (turn[2].y - turn[1].y); assert!( dot >= 0., "Adjacent reversal creates an unhelpful pin spur: {:?}", r.points ); } for (i, a) in r.points.windows(2).enumerate() { for b in r.points.windows(2).skip(i + 1) { let overlap = if a[0].y == a[1].y && b[0].y == b[1].y && a[0].y == b[0].y { a[0].x.max(a[1].x).min(b[0].x.max(b[1].x)) - a[0].x.min(a[1].x).max(b[0].x.min(b[1].x)) } else if a[0].x == a[1].x && b[0].x == b[1].x && a[0].x == b[0].x { a[0].y.max(a[1].y).min(b[0].y.max(b[1].y)) - a[0].y.min(a[1].y).max(b[0].y.min(b[1].y)) } else { 0.0 }; assert!(overlap <= 0., "Retraced span: {:?}", r.points); } } } #[test] fn automatic_label_fraction_survives_its_first_authored_drag_without_a_jump() { let (mut p, d, _, _, edge) = fixture(); let view = projected(&p, d); let source_port = view .edges .iter() .find(|e| e.id == edge) .unwrap() .source_port; let y = view .ports .iter() .find(|p| Some(p.element) == source_port) .unwrap() .position .y; let obstacle = p .add_element(Kind::Function, Layer::Sa, "Above the line", None) .unwrap(); p.place(d, obstacle, point(450., y - 117.)); set_route(&mut p, d, edge, vec![point(350., y)], None); let result = route(&p, d); let label = result.edges[&edge].label.clone(); assert_ne!( label.fraction, 0.5, "Fixture must exercise automatic collision-avoidance relocation" ); assert_eq!( label.anchor, point_along_polyline(&result.edges[&edge].points, label.fraction).unwrap() ); let offset = point( label.center.x - label.anchor.x + 15., label.center.y - label.anchor.y - 10., ); p.diagrams.get_mut(&d).unwrap().routes[0].label = Some(EdgeLabel { fraction: label.fraction, offset, }); let moved = route(&p, d).edges[&edge].label.clone(); assert_eq!( moved.center, 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 {:?}, node {:?}, path {:?}", label.text, p.named(node.element), label.bounds, node, routed.edges[&edge.id].points ); } } 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) } #[test] fn blocked_pin_is_retained_and_warned_without_nan_or_diagonal_fallback() { let (mut p, d, _, _, edge) = fixture(); let pin = point(50., 140.); set_route(&mut p, d, edge, vec![pin], None); let result = route(&p, d); let r = &result.edges[&edge]; assert_eq!(r.pins, vec![pin]); assert!(r.points.contains(&pin)); assert!( r.warnings .iter() .any(|w| matches!(w, RouteWarning::PinInsideObstacle { pin: 0, .. })) ); assert!(r.warnings.contains(&RouteWarning::NoClearPath)); orthogonal(&r.points); } #[test] fn collapse_suspends_only_promoted_endpoints_and_never_changes_stored_route() { let (mut p, d, a, _, edge) = fixture(); let group = p .add_element(Kind::Function, Layer::Sa, "Group", None) .unwrap(); p.elements.get_mut(&a).unwrap().parent = Some(group); let parent = p.place(d, group, point(-60., 0.)); set_route( &mut p, d, edge, vec![point(500., 400.)], Some(EdgeLabel { fraction: 0.4, offset: point(20., 40.), }), ); let stored = p.diagrams[&d].routes.clone(); assert!(!route(&p, d).edges[&edge].custom_suspended); p.diagrams .get_mut(&d) .unwrap() .nodes .get_mut(&parent) .unwrap() .collapsed = true; let folded = route(&p, d); assert!(folded.edges[&edge].custom_suspended); assert!(folded.edges[&edge].pins.is_empty()); assert!( folded.edges[&edge] .warnings .contains(&RouteWarning::PromotedEndpoints) ); assert_eq!(p.diagrams[&d].routes, stored); p.diagrams.get_mut(&d).unwrap().routes.clear(); assert!( route(&p, d).edges[&edge].custom_suspended, "A promoted endpoint cannot accept its first bend either" ); p.diagrams.get_mut(&d).unwrap().routes = stored.clone(); p.diagrams .get_mut(&d) .unwrap() .nodes .get_mut(&parent) .unwrap() .collapsed = false; assert_eq!(route(&p, d).edges[&edge].pins, stored[0].waypoints); let unrelated = p .add_element(Kind::Function, Layer::Sa, "Unrelated group", None) .unwrap(); let node = p.place(d, unrelated, point(1100., 100.)); p.diagrams .get_mut(&d) .unwrap() .nodes .get_mut(&node) .unwrap() .collapsed = true; assert!(!route(&p, d).edges[&edge].custom_suspended); } fn maze() -> (Project, Id, Id) { let (mut p, d, _, _, edge) = fixture(); let initial = projected(&p, d); let source_port = initial .edges .iter() .find(|e| e.id == edge) .unwrap() .source_port; let y = initial .ports .iter() .find(|p| Some(p.element) == source_port) .unwrap() .position .y; for (index, (position, size)) in [ ( point(230., y - 143.), Size { width: 100., height: 120., }, ), ( point(230., y + 27.), Size { width: 100., height: 160., }, ), ( point(400., y - 73.), Size { width: 100., height: 130., }, ), ] .into_iter() .enumerate() { let id = p .add_element(Kind::Function, Layer::Sa, &format!("Wall {index}"), None) .unwrap(); let node = p.place(d, id, position); p.diagrams .get_mut(&d) .unwrap() .nodes .get_mut(&node) .unwrap() .size = size; } (p, d, edge) } #[test] fn compressed_grid_search_finds_multi_turn_exit_from_a_narrow_corridor() { let (p, d, edge) = maze(); let view = projected(&p, d); let result = route_diagram(&p, &p.diagrams[&d], &view); let r = &result.edges[&edge]; assert!(r.warnings.is_empty(), "{:?}: {:?}", r.warnings, r.points); orthogonal(&r.points); assert!( r.points.len() >= 7, "The blocked up/down and forward exits require several turns: {:?}", r.points ); for wall in view .nodes .iter() .filter(|n| p.named(n.element).starts_with("Wall")) { avoids(&r.points, wall.position, wall.size, CLEARANCE); } } #[test] fn excessive_search_grid_falls_back_with_an_explicit_warning_and_preserved_endpoints() { let (p, d, edge) = maze(); let mut view = projected(&p, d); // Distant cards do not alter the local maze, but their distinct coordinates // deliberately exceed the compressed-grid admission limit. for i in 0..150 { view.nodes.push(ProjectedNode { id: Id::from_u128(i + 1), element: Id::from_u128(i + 1000), position: point(10_000. + i as f32 * 333., 10_000. + i as f32 * 377.), size: Size { width: 100., height: 94., }, visual_parent: None, depth: 0, collapsed: false, allocation_summary: false, }); } let result = route_diagram(&p, &p.diagrams[&d], &view); let r = &result.edges[&edge]; assert!( r.warnings.contains(&RouteWarning::SearchBudgetExhausted), "{:?}", r.warnings ); orthogonal(&r.points); let endpoint = view.edges.iter().find(|e| e.id == edge).unwrap(); assert_eq!( r.points[0], view.ports .iter() .find(|p| Some(p.element) == endpoint.source_port) .unwrap() .position ); } #[test] fn nonport_relations_try_clear_alternate_sides_and_label_only_supported_roles() { let (mut p, d, a, b, functional) = fixture(); let include = p.relate(RelationKind::CapabilityInclude, "Hidden custom name", a, b); let mut view = projected(&p, d); // The projection is the geometry seam; model validation separately restricts // the semantic types permitted for each relationship role. view.edges.retain(|e| e.id == include); let source = view.nodes.iter().find(|n| n.element == a).unwrap(); let owner = source.id; let right_midpoint = point( source.position.x + source.size.width, source.position.y + source.size.height / 2., ); view.nodes.push(ProjectedNode { id: Id::from_u128(1), element: Id::from_u128(2), position: point(right_midpoint.x + 2., right_midpoint.y - 40.), size: Size { width: 90., height: 94., }, visual_parent: None, depth: 0, collapsed: false, allocation_summary: false, }); let result = route_diagram(&p, &p.diagrams[&d], &view); let r = &result.edges[&include]; assert!(r.warnings.is_empty(), "{:?}", r.warnings); assert_ne!(r.points[0], right_midpoint); assert_eq!(r.label.text, "«include»"); let mut ordinary = view; ordinary.edges[0].id = Id::from_u128(3); ordinary.edges[0].source = owner; let result = route_diagram(&p, &p.diagrams[&d], &ordinary); assert!(result.edges[&Id::from_u128(3)].label.text.is_empty()); assert!(!result.edges.contains_key(&functional)); } #[test] fn label_geometry_and_bounds_are_shared_exact_and_unicode_safe() { let (mut p, d, _, _, edge) = fixture(); p.relations.get_mut(&edge).unwrap().name = "温度と圧力の状態を定期的に送信する長い機能交換名のテストデータ表示です".into(); let authored = EdgeLabel { fraction: 0.2, offset: point(-700., -500.), }; set_route( &mut p, d, edge, vec![point(500., 400.)], Some(authored.clone()), ); let result = route(&p, d); let r = &result.edges[&edge]; assert_eq!(r.label.text.chars().count(), 32); assert!(r.label.text.ends_with('…')); assert_eq!( r.label.bounds.size, Size { width: 240., height: 24. } ); assert_eq!( r.label.anchor, point_along_polyline(&r.points, authored.fraction).unwrap() ); assert_eq!( r.label.center, point( r.label.anchor.x + authored.offset.x, r.label.anchor.y + authored.offset.y ) ); assert!(result.bounds.position.x <= r.label.bounds.position.x); assert!(result.bounds.position.y <= r.label.bounds.position.y); assert!(result.bounds.position.x + result.bounds.size.width >= 1024.); assert!(result.bounds.position.y + result.bounds.size.height >= 400.); } #[test] fn deterministic_under_input_and_route_list_permutations() { let (mut p, d, a, b, edge) = fixture(); let second = p.relate(RelationKind::FunctionalExchange, "Acknowledgement", b, a); set_route(&mut p, d, edge, vec![point(450., 300.)], None); set_route(&mut p, d, second, vec![point(550., 400.)], None); let view = projected(&p, d); let original = route_diagram(&p, &p.diagrams[&d], &view); let mut shuffled = view; shuffled.edges.reverse(); shuffled.nodes.reverse(); shuffled.ports.reverse(); p.diagrams.get_mut(&d).unwrap().routes.reverse(); assert_eq!(route_diagram(&p, &p.diagrams[&d], &shuffled), original); } #[test] fn single_edge_preview_matches_settled_geometry_and_preserves_other_routes() { let (mut p, d, a, b, first) = fixture(); let second = p.relate(RelationKind::FunctionalExchange, "Return", b, a); let view = projected(&p, d); let baseline = route_diagram(&p, &p.diagrams[&d], &view); // Choose the later ID to exercise baseline crossing costs; earlier paths // are unchanged by this edit, exactly as in the complete stable-ID solve. let changed = first.max(second); let unchanged = first.min(second); set_route( &mut p, d, changed, vec![point(450., 400.)], Some(EdgeLabel { fraction: 0.2, offset: point(-800., -500.), }), ); let preview = route_edge_preview(&p, &p.diagrams[&d], &view, &baseline, changed); let complete = route_diagram(&p, &p.diagrams[&d], &view); assert_eq!(preview.edges[&changed], complete.edges[&changed]); assert_eq!(preview.edges[&unchanged], baseline.edges[&unchanged]); 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); assert_eq!(project_onto_polyline(&[], point(1., 2.)), None); let p = [ point(0., 0.), point(0., 0.), point(100., 0.), point(100., 100.), ]; assert_eq!(point_along_polyline(&p, 0.75), Some(point(100., 50.))); assert_eq!(point_along_polyline(&p, -1.), Some(point(0., 0.))); assert_eq!(point_along_polyline(&p, 2.), Some(point(100., 100.))); let projection = project_onto_polyline(&p, point(110., 50.)).unwrap(); assert_eq!( projection, PolylineProjection { point: point(100., 50.), fraction: 0.75, distance: 10. } ); assert_eq!( point_along_polyline(&[point(7., 8.); 3], 0.5), Some(point(7., 8.)) ); } #[test] fn routing_100_nodes_150_edges_has_bounded_work_and_reports_actual_time() { let mut p = Project::empty(); let d = p.ensure_diagram(Layer::Sa); let mut elements = Vec::new(); for i in 0..100 { let id = p .add_element(Kind::Function, Layer::Sa, &format!("Function {i}"), None) .unwrap(); p.place(d, id, point((i % 10) as f32 * 400., (i / 10) as f32 * 280.)); elements.push(id); } for i in 0..150 { let source = i % 100; let target = if i < 90 { source + 10 } else { (source / 10) * 10 + (source + 1) % 10 }; p.relate( RelationKind::FunctionalExchange, &format!("Flow {i}"), elements[source], elements[target], ); } let view = projected(&p, d); assert_eq!(view.nodes.len(), 100); assert_eq!(view.edges.len(), 150); let start = Instant::now(); let routed = route_diagram(&p, &p.diagrams[&d], &view); let elapsed = start.elapsed(); eprintln!( "100-node/150-edge settled routing: {elapsed:?}; {} warnings", routed .edges .values() .map(|r| r.warnings.len()) .sum::() ); assert_eq!(routed.edges.len(), 150); for edge in routed.edges.values() { orthogonal(&edge.points); } // A deliberately loose debug regression guard, not the interactive target. assert!( elapsed.as_secs() < 10, "Unexpected multi-second routing regression: {elapsed:?}" ); // A harder admitted model: every edge has eight coincident detour pins. // Report settled work separately from the interactive single-edge preview. let edge_ids: Vec<_> = p.relations.keys().copied().collect(); for &edge in &edge_ids { set_route( &mut p, d, edge, (0..8) .map(|i| point(-100. - (i % 2) as f32 * 100., -100. - i as f32 * 80.)) .collect(), None, ); } let started = Instant::now(); let baseline = route_diagram(&p, &p.diagrams[&d], &view); let settled = started.elapsed(); let selected = *edge_ids.last().unwrap(); let changed = p .diagrams .get_mut(&d) .unwrap() .routes .iter_mut() .find(|r| r.edge == selected) .unwrap(); changed.waypoints[7].x -= 35.; let moved = changed.waypoints[7]; let started = Instant::now(); let preview = route_edge_preview(&p, &p.diagrams[&d], &view, &baseline, selected); let preview_time = started.elapsed(); eprintln!( "100-node/150-edge/eight-pins settled: {settled:?}; single-edge preview: {preview_time:?}; {} warnings", baseline .edges .values() .map(|r| r.warnings.len()) .sum::() ); assert!(preview.edges[&selected].points.contains(&moved)); for &edge in &edge_ids[..edge_ids.len() - 1] { assert_eq!(preview.edges[&edge], baseline.edges[&edge]); } assert!( preview_time.as_secs() < 1, "Single route preview must not stall for seconds: {preview_time:?}" ); }