//! Deterministic, bounded diagram geometry. Only authored pins belong in the model; //! callers share this derived geometry for drawing, picking, fitting and export. use crate::projection::{self, PortAnchorSide, ProjectedEdge, ProjectedNode, ProjectedView}; use crate::{Diagram, Id, Position, Project, RelationKind, Size}; use std::cmp::Ordering; use std::collections::{BTreeMap, BTreeSet, BinaryHeap}; use std::fmt; pub const PORT_STUB: f32 = 18.0; pub const CLEARANCE: f32 = 12.0; const HEADER: f32 = 64.0; const BEND_COST: f32 = 24.0; const MAX_GRID_CELLS: usize = 65_536; const MAX_SEARCH_STEPS: usize = 8_192; const VIEW_SEARCH_BUDGET: usize = 65_536; const EPSILON: f32 = 0.001; #[derive(Debug, Clone, Copy, PartialEq)] pub struct RoutingBounds { pub position: Position, pub size: Size, } #[derive(Debug, Clone, PartialEq)] pub struct RoutedView { pub edges: BTreeMap, pub bounds: RoutingBounds, } #[derive(Debug, Clone, PartialEq)] pub struct RoutedEdge { pub id: Id, pub points: Vec, pub label: EdgeLabelGeometry, /// Effective authored points, empty while a folded endpoint suspends routing. pub pins: Vec, /// Actual traversal fractions; these preserve order even on a reversing path. pub pin_fractions: Vec, pub warnings: Vec, pub custom_suspended: bool, } #[derive(Debug, Clone, PartialEq)] pub struct EdgeLabelGeometry { /// Empty for edge kinds without a permanently displayed label. pub text: String, /// Effective arc-length fraction, including an automatic collision-avoidance /// choice. Use this when the first label drag becomes authored metadata. pub fraction: f32, pub anchor: Position, pub center: Position, pub bounds: RoutingBounds, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum RouteWarning { PinInsideObstacle { pin: usize, obstacle: Id }, NoClearPath, SearchBudgetExhausted, PromotedEndpoints, } impl fmt::Display for RouteWarning { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::PinInsideObstacle { pin, .. } => { write!(f, "Bend {} is inside a card or its clearance", pin + 1) } Self::NoClearPath => { f.write_str("No clear route: move a bend or make more space between cards") } Self::SearchBudgetExhausted => { f.write_str("Routing search limit reached: add a bend or make more space") } Self::PromotedEndpoints => { f.write_str("Expand the endpoint to edit routing; stored bends resume on expansion") } } } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct PolylineProjection { pub point: Position, pub fraction: f32, pub distance: f32, } fn pos(x: f32, y: f32) -> Position { Position { x, y } } fn length(a: Position, b: Position) -> f32 { (b.x - a.x).hypot(b.y - a.y) } fn manhattan(a: Position, b: Position) -> f32 { (b.x - a.x).abs() + (b.y - a.y).abs() } fn total_length(points: &[Position]) -> f32 { points.windows(2).map(|p| length(p[0], p[1])).sum() } /// Clamped arc-length interpolation, also defined for zero-length polylines. pub fn point_along_polyline(points: &[Position], fraction: f32) -> Option { let first = *points.first()?; let mut remaining = total_length(points) * if fraction.is_finite() { fraction.clamp(0.0, 1.0) } else { 0.0 }; for pair in points.windows(2) { let len = length(pair[0], pair[1]); if len > 0.0 && remaining <= len { let t = remaining / len; return Some(pos( pair[0].x + (pair[1].x - pair[0].x) * t, pair[0].y + (pair[1].y - pair[0].y) * t, )); } remaining -= len; } Some(*points.last().unwrap_or(&first)) } fn on_segment(a: Position, b: Position, point: Position) -> Position { let dx = b.x - a.x; let dy = b.y - a.y; let squared = dx * dx + dy * dy; let t = if squared == 0.0 { 0.0 } else { ((point.x - a.x) * dx + (point.y - a.y) * dy) / squared } .clamp(0.0, 1.0); pos(a.x + dx * t, a.y + dy * t) } /// Nearest point, breaking equal-distance ties by earliest traversal position. pub fn project_onto_polyline(points: &[Position], point: Position) -> Option { let first = *points.first()?; let total = total_length(points); let mut best = PolylineProjection { point: first, fraction: 0.0, distance: length(first, point), }; let mut traversed = 0.0; for pair in points.windows(2) { let projected = on_segment(pair[0], pair[1], point); let distance = length(projected, point); if distance < best.distance { best = PolylineProjection { point: projected, fraction: if total > 0.0 { (traversed + length(pair[0], projected)) / total } else { 0.0 }, distance, }; } traversed += length(pair[0], pair[1]); } Some(best) } /// Insert after pins already traversed on the picked segment. Unlike nearest /// projection alone, the segment index disambiguates overlapping/reversed runs. pub fn pin_insertion_index(route: &RoutedEdge, segment_index: usize, point: Position) -> usize { let Some(pair) = route .points .get(segment_index..segment_index.saturating_add(2)) else { return route.pins.len(); }; let total = total_length(&route.points); if total == 0.0 { return route.pins.len(); } let before = total_length(&route.points[..=segment_index]); let fraction = (before + length(pair[0], on_segment(pair[0], pair[1], point))) / total; route .pin_fractions .iter() .take_while(|&&pin| pin <= fraction + f32::EPSILON) .count() } #[derive(Clone, Copy)] struct Rect { id: Id, left: f32, top: f32, right: f32, bottom: f32, } impl Rect { fn node(node: &ProjectedNode) -> Self { Self { id: node.id, left: node.position.x, top: node.position.y, right: node.position.x + node.size.width, bottom: node.position.y + node.size.height, } } fn inflate(self, amount: f32) -> Self { Self { left: self.left - amount, top: self.top - amount, right: self.right + amount, bottom: self.bottom + amount, ..self } } fn contains(self, p: Position) -> bool { p.x > self.left + EPSILON && p.x < self.right - EPSILON && p.y > self.top + EPSILON && p.y < self.bottom - EPSILON } fn crosses(self, a: Position, b: Position) -> bool { if a == b { return self.contains(a); } if (a.x - b.x).abs() < EPSILON { a.x > self.left + EPSILON && a.x < self.right - EPSILON && a.y.min(b.y) < self.bottom - EPSILON && a.y.max(b.y) > self.top + EPSILON } else if (a.y - b.y).abs() < EPSILON { a.y > self.top + EPSILON && a.y < self.bottom - EPSILON && a.x.min(b.x) < self.right - EPSILON && a.x.max(b.x) > self.left + EPSILON } else { true } } fn overlaps(self, bounds: RoutingBounds) -> bool { self.left < bounds.position.x + bounds.size.width && self.right > bounds.position.x && self.top < bounds.position.y + bounds.size.height && self.bottom > bounds.position.y } } fn clear(a: Position, b: Position, obstacles: &[Rect]) -> bool { !obstacles.iter().any(|r| r.crosses(a, b)) } fn clear_path(points: &[Position], obstacles: &[Rect]) -> bool { points.windows(2).all(|p| clear(p[0], p[1], obstacles)) } #[derive(Clone, Copy)] struct Endpoint { point: Position, stub: Position, } fn endpoints(node: &ProjectedNode, port: Option, view: &ProjectedView) -> Vec { if let Some(port) = port.and_then(|id| { view.ports .iter() .find(|p| p.element == id && p.owner == node.id) }) { let normal = match port.side { PortAnchorSide::Left => pos(-1.0, 0.0), PortAnchorSide::Right => pos(1.0, 0.0), PortAnchorSide::Bottom => pos(0.0, 1.0), }; return vec![Endpoint { point: port.position, stub: pos( port.position.x + normal.x * PORT_STUB, port.position.y + normal.y * PORT_STUB, ), }]; } let r = Rect::node(node); let mid = pos((r.left + r.right) / 2.0, (r.top + r.bottom) / 2.0); vec![ Endpoint { point: pos(r.left, mid.y), stub: pos(r.left - PORT_STUB, mid.y), }, Endpoint { point: pos(r.right, mid.y), stub: pos(r.right + PORT_STUB, mid.y), }, Endpoint { point: pos(mid.x, r.top), stub: pos(mid.x, r.top - PORT_STUB), }, Endpoint { point: pos(mid.x, r.bottom), stub: pos(mid.x, r.bottom + PORT_STUB), }, ] } fn ancestors(id: Id, nodes: &BTreeMap, out: &mut BTreeSet) { let mut current = nodes.get(&id).and_then(|n| n.visual_parent); let mut seen = BTreeSet::new(); while let Some(id) = current { if !seen.insert(id) { break; } out.insert(id); current = nodes.get(&id).and_then(|n| n.visual_parent); } } /// Route stable edge IDs in order so crossing costs never depend on input order. /// Search caps are presentation limits, reported on the affected edge, never /// semantic errors or a reason to discard an authored point. pub fn route_diagram(project: &Project, diagram: &Diagram, view: &ProjectedView) -> RoutedView { route_diagram_impl(project, diagram, view, None) } /// Transient route/label editing with unchanged projected nodes and ports. /// Retains other edges from a settled baseline and scores the selected edge /// against preceding stable IDs, exactly as a complete solve does. Recompute /// the complete view after commit, or whenever node/port geometry changes. pub fn route_edge_preview( project: &Project, diagram: &Diagram, view: &ProjectedView, baseline: &RoutedView, edge: Id, ) -> RoutedView { route_diagram_impl(project, diagram, view, Some((baseline, edge))) } fn route_diagram_impl( project: &Project, diagram: &Diagram, view: &ProjectedView, preview: Option<(&RoutedView, Id)>, ) -> RoutedView { let nodes: BTreeMap<_, _> = view.nodes.iter().map(|n| (n.id, n)).collect(); let rectangles: Vec<_> = nodes.values().map(|n| Rect::node(n)).collect(); let authored: BTreeMap<_, _> = diagram.routes.iter().map(|r| (r.edge, r)).collect(); let mut ordered: Vec<_> = view.edges.iter().collect(); ordered.sort_by_key(|e| e.id); let mut edges = BTreeMap::new(); let mut previous = Vec::new(); let mut budget = VIEW_SEARCH_BUDGET; let mut bounds = BoundsBuilder::default(); for node in &view.nodes { bounds.rect(node.position, node.size); } for port in &view.ports { bounds.rect( pos(port.position.x - 6.0, port.position.y - 6.0), Size { width: 12.0, height: 12.0, }, ); } for edge in ordered { if let Some(cached) = preview.and_then(|(baseline, changed)| { (edge.id != changed) .then(|| baseline.edges.get(&edge.id)) .flatten() }) { for &point in &cached.points { bounds.point(point); } if !cached.label.text.is_empty() { bounds.rect(cached.label.bounds.position, cached.label.bounds.size); } record_segments(&mut previous, &cached.points); edges.insert(edge.id, cached.clone()); continue; } let (Some(source), Some(target)) = (nodes.get(&edge.source), nodes.get(&edge.target)) else { continue; }; let stored = authored.get(&edge.id).copied(); let promoted = projection::edge_endpoint_placements(project, diagram, edge.id) .is_some_and(|(source, target)| source != edge.source || target != edge.target); let custom_suspended = promoted; let route = if promoted { None } else { stored }; let pins = route.map_or_else(Vec::new, |r| r.waypoints.clone()); let mut traversable = BTreeSet::new(); ancestors(edge.source, &nodes, &mut traversable); ancestors(edge.target, &nodes, &mut traversable); let obstacles: Vec<_> = rectangles .iter() .map(|r| { let mut r = *r; if traversable.contains(&r.id) { r.bottom = r.bottom.min(r.top + HEADER); } r.inflate(CLEARANCE) }) .collect(); let mut warnings = Vec::new(); if custom_suspended { warnings.push(RouteWarning::PromotedEndpoints); } for (pin, &point) in pins.iter().enumerate() { if let Some(obstacle) = obstacles.iter().find(|r| r.contains(point)) { warnings.push(RouteWarning::PinInsideObstacle { pin, obstacle: obstacle.id, }); } } let (points, pin_lengths, failure) = route_edge( edge, source, target, view, &pins, &obstacles, &previous, &mut budget, ); if let Some(warning) = failure { warnings.push(warning); } let total = total_length(&points); let pin_fractions = pin_lengths .into_iter() .map(|v| if total > 0.0 { v / total } else { 0.0 }) .collect(); let label = make_label( project, edge.id, &points, route.and_then(|r| r.label.as_ref()), &obstacles, ); for &point in &points { bounds.point(point); } if !label.text.is_empty() { bounds.rect(label.bounds.position, label.bounds.size); } record_segments(&mut previous, &points); edges.insert( edge.id, RoutedEdge { id: edge.id, points, label, pins, pin_fractions, warnings, custom_suspended, }, ); } RoutedView { edges, bounds: bounds.finish(), } } #[derive(Clone, Copy)] struct Segment { a: Position, b: Position, count: u32, } // Coincident authored routes can otherwise create thousands of identical // crossing tests. Keep their exact multiplicity without rescanning duplicates. fn record_segments(occupied: &mut Vec, points: &[Position]) { for pair in points.windows(2) { let (a, b) = (pair[0], pair[1]); if a == b { continue; } if let Some(existing) = occupied .iter_mut() .find(|s| (s.a == a && s.b == b) || (s.a == b && s.b == a)) { existing.count += 1; } else { occupied.push(Segment { a, b, count: 1 }); } } } type EdgePath = (Vec, Vec, Option); struct RoutingSpace<'a> { obstacles: &'a [Rect], xs: Vec, ys: Vec, } impl<'a> RoutingSpace<'a> { fn new(obstacles: &'a [Rect]) -> Self { Self { obstacles, xs: sorted_coordinates(obstacles.iter().flat_map(|r| [r.left, r.right]).collect()), ys: sorted_coordinates(obstacles.iter().flat_map(|r| [r.top, r.bottom]).collect()), } } } #[allow(clippy::too_many_arguments)] fn route_edge( edge: &ProjectedEdge, source: &ProjectedNode, target: &ProjectedNode, view: &ProjectedView, pins: &[Position], obstacles: &[Rect], previous: &[Segment], budget: &mut usize, ) -> EdgePath { let space = RoutingSpace::new(obstacles); let mut candidates = Vec::new(); for a in endpoints(source, edge.source_port, view) { for b in endpoints(target, edge.target_port, view) { let estimate = manhattan(a.stub, pins.first().copied().unwrap_or(b.stub)) + pins.windows(2).map(|p| manhattan(p[0], p[1])).sum::() + pins.last().map_or(0.0, |&p| manhattan(p, b.stub)); candidates.push((estimate, a, b)); } } candidates.sort_by(|a, b| { a.0.total_cmp(&b.0) .then_with(|| endpoint_order(a.1, b.1)) .then_with(|| endpoint_order(a.2, b.2)) }); let mut best: Option<(f32, Vec, Vec)> = None; let mut exhausted = false; // Try all side combinations cheaply before spending the bounded grid budget. for search in [false, true] { for &(estimate, a, b) in &candidates { if best .as_ref() .is_some_and(|(cost, _, _)| estimate + 2.0 * PORT_STUB > *cost) { continue; } if obstacles .iter() .any(|r| r.id != edge.source && r.crosses(a.point, a.stub)) || obstacles .iter() .any(|r| r.id != edge.target && r.crosses(b.point, b.stub)) { continue; } let mut points = vec![a.point, a.stub]; let mut occupied = previous.to_vec(); record_segments(&mut occupied, &points); let mut pin_lengths = Vec::new(); let mut distance = PORT_STUB; let mut start = a.stub; let mut failed = false; for (index, end) in pins .iter() .copied() .chain(std::iter::once(b.stub)) .enumerate() { let incoming = points.iter().rev().find(|&&p| p != start).copied(); let path = fast_path(start, end, &space, &occupied, incoming).or_else(|| { if !search { return None; } let (path, hit_limit) = grid_path(start, end, &space, &occupied, incoming, budget); exhausted |= hit_limit; path }); let Some(path) = path else { failed = true; break; }; distance += total_length(&path); record_segments(&mut occupied, &path); points.extend(path.into_iter().skip(1)); if index < pins.len() { pin_lengths.push(distance); } start = end; } if failed { continue; } points.push(b.point); let cost = path_cost(&points, previous); if best.as_ref().is_none_or(|(old, _, _)| cost < *old) { best = Some((cost, points, pin_lengths)); } } if best.is_some() { break; } } if let Some((_, points, pin_lengths)) = best { return (points, pin_lengths, None); } // Recoverable fallback retains every hard pin, in order, and the actual port // leads. Its warning is deliberately visible to consumers. let (_, a, b) = candidates[0]; let mut points = vec![a.point, a.stub]; let mut pin_lengths = Vec::new(); let mut distance = PORT_STUB; let mut start = a.stub; for (index, end) in pins .iter() .copied() .chain(std::iter::once(b.stub)) .enumerate() { let horizontal = compact(vec![start, pos(end.x, start.y), end]); let vertical = compact(vec![start, pos(start.x, end.y), end]); let count = |p: &[Position]| { p.windows(2) .map(|pair| { obstacles .iter() .filter(|r| r.crosses(pair[0], pair[1])) .count() }) .sum::() }; let path = if count(&horizontal) <= count(&vertical) { horizontal } else { vertical }; distance += total_length(&path); points.extend(path.into_iter().skip(1)); if index < pins.len() { pin_lengths.push(distance); } start = end; } points.push(b.point); ( points, pin_lengths, Some(if exhausted { RouteWarning::SearchBudgetExhausted } else { RouteWarning::NoClearPath }), ) } fn endpoint_order(a: Endpoint, b: Endpoint) -> Ordering { a.point .x .total_cmp(&b.point.x) .then_with(|| a.point.y.total_cmp(&b.point.y)) } /// Remove redundant elbows only within one pin-to-pin span. Reversals and hard /// pin/stub boundaries must survive; otherwise arc fractions cease to be true. fn compact(points: Vec) -> Vec { let mut result: Vec = Vec::new(); for point in points { if result.last() == Some(&point) { continue; } while result.len() >= 2 { let a = result[result.len() - 2]; let b = result[result.len() - 1]; if ((a.x == b.x && b.x == point.x) || (a.y == b.y && b.y == point.y)) && manhattan(a, point) >= manhattan(a, b) + manhattan(b, point) - EPSILON { result.pop(); } else { break; } } result.push(point); } result } fn base_cost(points: &[Position]) -> f32 { total_length(points) + points .windows(3) .map(|p| turn_cost(p[0], p[1], p[2])) .sum::() } fn turn_cost(before: Position, at: Position, after: Position) -> f32 { if before == at || at == after { return 0.0; } let dot = (at.x - before.x) * (after.x - at.x) + (at.y - before.y) * (after.y - at.y); if dot < 0.0 { 2.0 * BEND_COST } else if dot == 0.0 { BEND_COST } else { 0.0 } } fn crossing_cost(a: Position, b: Position, previous: &[Segment]) -> f32 { let horizontal = a.y == b.y; previous .iter() .map(|segment| { let (c, d) = (segment.a, segment.b); let cost = if horizontal == (c.y == d.y) { let (same, lo, hi) = if horizontal { ( a.y == c.y, a.x.min(b.x).max(c.x.min(d.x)), a.x.max(b.x).min(c.x.max(d.x)), ) } else { ( a.x == c.x, a.y.min(b.y).max(c.y.min(d.y)), a.y.max(b.y).min(c.y.max(d.y)), ) }; if same { ((hi - lo).max(0.0) * 0.12).min(80.0) } else { 0.0 } } else { let (h1, h2, v1, v2) = if horizontal { (a, b, c, d) } else { (c, d, a, b) }; if v1.x > h1.x.min(h2.x) && v1.x < h1.x.max(h2.x) && h1.y > v1.y.min(v2.y) && h1.y < v1.y.max(v2.y) { 24.0 } else { 0.0 } }; cost * segment.count as f32 }) .sum() } fn path_cost(points: &[Position], previous: &[Segment]) -> f32 { base_cost(points) + points .windows(2) .map(|p| crossing_cost(p[0], p[1], previous)) .sum::() } fn sorted_coordinates(mut values: Vec) -> Vec { values.sort_by(f32::total_cmp); values.dedup(); values } fn fast_path( start: Position, end: Position, space: &RoutingSpace<'_>, previous: &[Segment], incoming: Option, ) -> Option> { let obstacles = space.obstacles; if obstacles .iter() .any(|r| r.contains(start) || r.contains(end)) { return None; } if start == end { return Some(vec![start]); } let mut candidates = vec![ compact(vec![start, pos(end.x, start.y), end]), compact(vec![start, pos(start.x, end.y), end]), ]; let join = |path: &[Position]| { incoming .zip(path.get(1)) .map_or(0.0, |(before, &after)| turn_cost(before, start, after)) }; let estimate = |path: &[Position]| base_cost(path) + join(path); candidates.sort_by(|a, b| estimate(a).total_cmp(&estimate(b))); let mut best: Option<(f32, Vec)> = None; for path in &candidates { if !clear_path(path, obstacles) { continue; } let cost = path_cost(path, previous) + join(path); if best.as_ref().is_none_or(|(old, _)| cost < *old) { best = Some((cost, path.clone())); } } // No two-bend path can beat this admissible lower bound. Most clear spans // stop here, avoiding obstacle-coordinate candidates and their allocations. if best .as_ref() .is_some_and(|(cost, _)| *cost <= manhattan(start, end) + 2.0 * BEND_COST) { return best.map(|(_, path)| path); } let xs = std::iter::once((start.x + end.x) / 2.0).chain(space.xs.iter().copied()); let ys = std::iter::once((start.y + end.y) / 2.0).chain(space.ys.iter().copied()); candidates.extend(xs.map(|x| compact(vec![start, pos(x, start.y), pos(x, end.y), end]))); candidates.extend(ys.map(|y| compact(vec![start, pos(start.x, y), pos(end.x, y), end]))); candidates.sort_by(|a, b| estimate(a).total_cmp(&estimate(b))); for path in candidates { if best .as_ref() .is_some_and(|(cost, _)| estimate(&path) > *cost) { break; } if !clear_path(&path, obstacles) { continue; } let cost = path_cost(&path, previous) + join(&path); if best.as_ref().is_none_or(|(old, _)| cost < *old) { best = Some((cost, path)); } } best.map(|(_, path)| path) } #[derive(Clone, Copy)] struct QueueItem { estimate: f32, cost: f32, state: usize, } impl PartialEq for QueueItem { fn eq(&self, other: &Self) -> bool { self.estimate == other.estimate && self.cost == other.cost && self.state == other.state } } impl Eq for QueueItem {} impl PartialOrd for QueueItem { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for QueueItem { fn cmp(&self, other: &Self) -> Ordering { other .estimate .total_cmp(&self.estimate) .then_with(|| other.cost.total_cmp(&self.cost)) .then_with(|| other.state.cmp(&self.state)) } } fn grid_path( start: Position, end: Position, space: &RoutingSpace<'_>, previous: &[Segment], incoming: Option, budget: &mut usize, ) -> (Option>, bool) { let obstacles = space.obstacles; if obstacles .iter() .any(|r| r.contains(start) || r.contains(end)) { return (None, false); } let xs = sorted_coordinates( [start.x, end.x] .into_iter() .chain(space.xs.iter().copied()) .collect(), ); let ys = sorted_coordinates( [start.y, end.y] .into_iter() .chain(space.ys.iter().copied()) .collect(), ); let cells = xs.len() * ys.len(); if cells > MAX_GRID_CELLS || *budget == 0 { return (None, true); } let index = |p: Position| { ys.binary_search_by(|v| v.total_cmp(&p.y)).unwrap() * xs.len() + xs.binary_search_by(|v| v.total_cmp(&p.x)).unwrap() }; let position = |cell: usize| pos(xs[cell % xs.len()], ys[cell / xs.len()]); let first = index(start); let last = index(end); let mut costs = vec![f32::INFINITY; cells * 2]; let mut parents = vec![usize::MAX; cells * 2]; let mut queue = BinaryHeap::new(); for direction in 0..2 { costs[first * 2 + direction] = 0.0; queue.push(QueueItem { estimate: manhattan(start, end), cost: 0.0, state: first * 2 + direction, }); } let mut steps = 0; while let Some(current) = queue.pop() { if current.cost > costs[current.state] { continue; } if steps == MAX_SEARCH_STEPS || *budget == 0 { return (None, true); } steps += 1; *budget -= 1; let cell = current.state / 2; if cell == last { let mut path = Vec::new(); let mut state = current.state; loop { path.push(position(state / 2)); if parents[state] == usize::MAX { break; } state = parents[state]; } path.reverse(); return (Some(compact(path)), false); } let x = cell % xs.len(); let y = cell / xs.len(); let neighbours = [ x.checked_sub(1).map(|_| (cell - 1, 0)), (x + 1 < xs.len()).then_some((cell + 1, 0)), y.checked_sub(1).map(|_| (cell - xs.len(), 1)), (y + 1 < ys.len()).then_some((cell + xs.len(), 1)), ]; let a = position(cell); for (next, direction) in neighbours.into_iter().flatten() { let b = position(next); if !clear(a, b, obstacles) { continue; } let state = next * 2 + direction; let before = if parents[current.state] == usize::MAX { incoming } else { Some(position(parents[current.state] / 2)) }; let cost = current.cost + manhattan(a, b) + before.map_or(0.0, |before| turn_cost(before, a, b)) + crossing_cost(a, b, previous); if cost >= costs[state] { continue; } costs[state] = cost; parents[state] = current.state; queue.push(QueueItem { estimate: cost + manhattan(b, end), cost, state, }); } } (None, false) } fn label_text(project: &Project, edge: Id) -> String { let Some(relation) = project.relations.get(&edge) else { return String::new(); }; let text = match relation.kind() { RelationKind::Interaction | RelationKind::FunctionalExchange | RelationKind::BoundaryExchange => relation.name.as_str(), RelationKind::CapabilityInclude => "«include»", RelationKind::CapabilityExtend => "«extend»", RelationKind::CapabilityGeneralization => "specializes", RelationKind::MissionCapability => "exploits", RelationKind::MissionInvolvement | RelationKind::CapabilityInvolvement => "participates", _ => return String::new(), }; if text.chars().count() > 32 { text.chars().take(31).chain(std::iter::once('…')).collect() } else { text.to_owned() } } fn make_label( project: &Project, edge: Id, points: &[Position], authored: Option<&crate::EdgeLabel>, obstacles: &[Rect], ) -> EdgeLabelGeometry { let text = label_text(project, edge); let size = Size { width: if text.is_empty() { 0.0 } else { text.chars().count() as f32 * 7.0 + 16.0 }, height: if text.is_empty() { 0.0 } else { 24.0 }, }; let offset = authored.map_or(pos(0.0, -18.0), |l| l.offset); let geometry = |fraction| { let anchor = point_along_polyline(points, fraction).unwrap_or(pos(0.0, 0.0)); let center = pos(anchor.x + offset.x, anchor.y + offset.y); ( anchor, center, RoutingBounds { position: pos(center.x - size.width / 2.0, center.y - size.height / 2.0), size, }, ) }; let mut fraction = authored.map_or(0.5, |l| l.fraction); // Automatic labels prefer the middle, then clear segment centres. An // explicitly positioned label is exact, even if its author overlaps a card. if authored.is_none() && !text.is_empty() && obstacles.iter().any(|r| r.overlaps(geometry(fraction).2)) { let total = total_length(points); let mut traversed = 0.0; let mut alternatives = Vec::new(); if total > 0.0 { for pair in points.windows(2) { let len = length(pair[0], pair[1]); alternatives.push((traversed + len / 2.0) / total); traversed += len; } } alternatives.sort_by(|a, b| { (a - 0.5) .abs() .total_cmp(&(b - 0.5).abs()) .then_with(|| a.total_cmp(b)) }); if let Some(clear) = alternatives .into_iter() .find(|f| !obstacles.iter().any(|r| r.overlaps(geometry(*f).2))) { fraction = clear; } } let (anchor, center, bounds) = geometry(fraction); EdgeLabelGeometry { text, fraction, anchor, center, bounds, } } #[derive(Default)] struct BoundsBuilder { extents: Option<(f32, f32, f32, f32)>, } impl BoundsBuilder { fn point(&mut self, p: Position) { self.extents = Some(match self.extents { Some((left, top, right, bottom)) => { (left.min(p.x), top.min(p.y), right.max(p.x), bottom.max(p.y)) } None => (p.x, p.y, p.x, p.y), }); } fn rect(&mut self, position: Position, size: Size) { self.point(position); self.point(pos(position.x + size.width, position.y + size.height)); } fn finish(self) -> RoutingBounds { let (left, top, right, bottom) = self.extents.unwrap_or((0.0, 0.0, 0.0, 0.0)); RoutingBounds { position: pos(left, top), size: Size { width: right - left, height: bottom - top, }, } } }