experiment(layout): diagnose mandatory leads and test bounded flat separation
This commit is contained in:
@@ -6,7 +6,9 @@ use crate::{
|
||||
RelationBody, RelationKind, Size,
|
||||
};
|
||||
mod attachments;
|
||||
mod leads;
|
||||
pub use attachments::{attachment_from_point, attachment_point, semantic_port_endpoint};
|
||||
pub use leads::{LeadBlocker, LeadEndpoint, LeadIncompatibility, lead_incompatibilities};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::{BTreeMap, BTreeSet, BinaryHeap};
|
||||
use std::fmt;
|
||||
@@ -420,6 +422,73 @@ fn endpoints(
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn effective_route<'a>(
|
||||
project: &Project,
|
||||
diagram: &Diagram,
|
||||
edge: &ProjectedEdge,
|
||||
stored: Option<&'a crate::EdgeRoute>,
|
||||
) -> (
|
||||
bool,
|
||||
Option<&'a crate::EdgeRoute>,
|
||||
[Option<EdgeAttachment>; 2],
|
||||
) {
|
||||
let promoted = projection::edge_endpoint_placements(project, diagram, edge.id)
|
||||
.is_some_and(|(source, target)| source != edge.source || target != edge.target);
|
||||
let route = if promoted { None } else { stored };
|
||||
let attachments = [EdgeEnd::Source, EdgeEnd::Target].map(|end| {
|
||||
semantic_port_endpoint(project, edge.id, end)
|
||||
.is_none()
|
||||
.then(|| route.and_then(|r| r.attachment(end)))
|
||||
.flatten()
|
||||
});
|
||||
(promoted, route, attachments)
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn edge_endpoints(
|
||||
project: &Project,
|
||||
diagram: &Diagram,
|
||||
edge: &ProjectedEdge,
|
||||
source: &ProjectedNode,
|
||||
target: &ProjectedNode,
|
||||
view: &ProjectedView,
|
||||
attachments: [Option<EdgeAttachment>; 2],
|
||||
) -> [Vec<Endpoint>; 2] {
|
||||
[
|
||||
endpoints(
|
||||
project,
|
||||
diagram,
|
||||
source,
|
||||
semantic_port_endpoint(project, edge.id, EdgeEnd::Source).or(edge.source_port),
|
||||
view,
|
||||
attachments[0],
|
||||
),
|
||||
endpoints(
|
||||
project,
|
||||
diagram,
|
||||
target,
|
||||
semantic_port_endpoint(project, edge.id, EdgeEnd::Target).or(edge.target_port),
|
||||
view,
|
||||
attachments[1],
|
||||
),
|
||||
]
|
||||
}
|
||||
fn allowed_endpoint_pair(edge: &ProjectedEdge, a: Endpoint, b: Endpoint, coincident: bool) -> bool {
|
||||
!(edge.source == edge.target
|
||||
&& edge.source_port.is_none()
|
||||
&& edge.target_port.is_none()
|
||||
&& a.point == b.point
|
||||
&& a.stub == b.stub
|
||||
&& !coincident)
|
||||
}
|
||||
fn lead_pair_clear(edge: &ProjectedEdge, a: Endpoint, b: Endpoint, obstacles: &[Rect]) -> bool {
|
||||
!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))
|
||||
}
|
||||
|
||||
fn ancestors(id: Id, nodes: &BTreeMap<Id, &ProjectedNode>, out: &mut BTreeSet<Id>) {
|
||||
let mut current = nodes.get(&id).and_then(|n| n.visual_parent);
|
||||
let mut seen = BTreeSet::new();
|
||||
@@ -512,16 +581,8 @@ fn route_diagram_impl(
|
||||
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 attachments = [EdgeEnd::Source, EdgeEnd::Target].map(|end| {
|
||||
semantic_port_endpoint(project, edge.id, end)
|
||||
.is_none()
|
||||
.then(|| route.and_then(|r| r.attachment(end)))
|
||||
.flatten()
|
||||
});
|
||||
let (custom_suspended, route, attachments) =
|
||||
effective_route(project, diagram, edge, stored);
|
||||
let pins = route.map_or_else(Vec::new, |r| r.waypoints.clone());
|
||||
let obstacles = edge_obstacles(edge, &nodes, &rectangles, &headers);
|
||||
|
||||
@@ -559,24 +620,11 @@ fn route_diagram_impl(
|
||||
});
|
||||
}
|
||||
}
|
||||
let ends = edge_endpoints(project, diagram, edge, source, target, view, attachments);
|
||||
let (points, pin_lengths, failure) = route_edge(
|
||||
edge,
|
||||
&endpoints(
|
||||
project,
|
||||
diagram,
|
||||
source,
|
||||
semantic_port_endpoint(project, edge.id, EdgeEnd::Source).or(edge.source_port),
|
||||
view,
|
||||
attachments[0],
|
||||
),
|
||||
&endpoints(
|
||||
project,
|
||||
diagram,
|
||||
target,
|
||||
semantic_port_endpoint(project, edge.id, EdgeEnd::Target).or(edge.target_port),
|
||||
view,
|
||||
attachments[1],
|
||||
),
|
||||
&ends[0],
|
||||
&ends[1],
|
||||
&pins,
|
||||
&obstacles,
|
||||
&previous,
|
||||
@@ -793,13 +841,7 @@ fn route_edge(
|
||||
for &b in target_ends {
|
||||
// A real self-relation must leave and return at distinct anchors.
|
||||
// Reusing one anchor makes the cheapest path a degenerate stub.
|
||||
if edge.source == edge.target
|
||||
&& edge.source_port.is_none()
|
||||
&& edge.target_port.is_none()
|
||||
&& a.point == b.point
|
||||
&& a.stub == b.stub
|
||||
&& !coincident
|
||||
{
|
||||
if !allowed_endpoint_pair(edge, a, b, coincident) {
|
||||
continue;
|
||||
}
|
||||
let estimate = manhattan(a.stub, pins.first().copied().unwrap_or(b.stub))
|
||||
@@ -823,13 +865,7 @@ fn route_edge(
|
||||
}) {
|
||||
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))
|
||||
{
|
||||
if !lead_pair_clear(edge, a, b, obstacles) {
|
||||
continue;
|
||||
}
|
||||
let mut points = vec![a.point, a.stub];
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
//! Necessary terminal-lead checks shared with experimental placement consumers.
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct LeadBlocker {
|
||||
pub id: Id,
|
||||
/// The actual routing obstacle: clearance inflated, ancestor body excluded.
|
||||
pub bounds: RoutingBounds,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct LeadEndpoint {
|
||||
pub owner: Id,
|
||||
pub tip: Position,
|
||||
pub stub: Position,
|
||||
pub blockers: Vec<LeadBlocker>,
|
||||
/// Separate information only; not part of the router's early lead guard.
|
||||
pub stub_containers: Vec<LeadBlocker>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct LeadIncompatibility {
|
||||
pub edge: Id,
|
||||
pub source: Vec<LeadEndpoint>,
|
||||
pub target: Vec<LeadEndpoint>,
|
||||
pub allowed_pair_count: usize,
|
||||
pub lead_clear_pair_count: usize,
|
||||
}
|
||||
|
||||
fn blocker(r: &Rect) -> LeadBlocker {
|
||||
LeadBlocker {
|
||||
id: r.id,
|
||||
bounds: RoutingBounds {
|
||||
position: pos(r.left, r.top),
|
||||
size: Size {
|
||||
width: r.right - r.left,
|
||||
height: r.bottom - r.top,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn describe(owner: Id, endpoint: Endpoint, obstacles: &[Rect]) -> LeadEndpoint {
|
||||
LeadEndpoint {
|
||||
owner,
|
||||
tip: endpoint.point,
|
||||
stub: endpoint.stub,
|
||||
blockers: obstacles
|
||||
.iter()
|
||||
.filter(|r| r.id != owner && r.crosses(endpoint.point, endpoint.stub))
|
||||
.map(blocker)
|
||||
.collect(),
|
||||
stub_containers: obstacles
|
||||
.iter()
|
||||
.filter(|r| r.contains(endpoint.stub))
|
||||
.map(blocker)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return only edges for which every allowed endpoint pair fails the router's
|
||||
/// existing mandatory-lead guard. Empty output is NECESSARY ONLY: it does not
|
||||
/// establish a clear route, clear pins, successful search, or label clearance.
|
||||
/// Uses the supplied final projection; does not modify the Project or choose
|
||||
/// different ports/attachments. Stub containment is reported separately and
|
||||
/// does not change the lead guard or its historical pinned/degenerate behavior.
|
||||
pub fn lead_incompatibilities(
|
||||
project: &Project,
|
||||
diagram: &Diagram,
|
||||
view: &ProjectedView,
|
||||
) -> Vec<LeadIncompatibility> {
|
||||
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 headers: BTreeMap<_, _> = nodes
|
||||
.values()
|
||||
.map(|node| {
|
||||
(
|
||||
node.id,
|
||||
if diagram.projection.state_machine().is_some() && !node.collapsed {
|
||||
projection::state_header_height(project, node.element)
|
||||
} else {
|
||||
HEADER
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut ordered: Vec<_> = view.edges.iter().collect();
|
||||
ordered.sort_by_key(|e| e.id);
|
||||
let mut result = Vec::new();
|
||||
for edge in ordered {
|
||||
let (Some(source), Some(target)) = (nodes.get(&edge.source), nodes.get(&edge.target))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let (_, _, attachments) =
|
||||
effective_route(project, diagram, edge, authored.get(&edge.id).copied());
|
||||
let ends = edge_endpoints(project, diagram, edge, source, target, view, attachments);
|
||||
let obstacles = edge_obstacles(edge, &nodes, &rectangles, &headers);
|
||||
let coincident = edge.source == edge.target
|
||||
&& ends[0].len() == 1
|
||||
&& ends[1].len() == 1
|
||||
&& ends[0][0].point == ends[1][0].point
|
||||
&& ends[0][0].stub == ends[1][0].stub;
|
||||
let mut allowed_pair_count = 0;
|
||||
let mut lead_clear_pair_count = 0;
|
||||
for &a in &ends[0] {
|
||||
for &b in &ends[1] {
|
||||
if !allowed_endpoint_pair(edge, a, b, coincident) {
|
||||
continue;
|
||||
}
|
||||
allowed_pair_count += 1;
|
||||
if lead_pair_clear(edge, a, b, &obstacles) {
|
||||
lead_clear_pair_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if lead_clear_pair_count == 0 {
|
||||
result.push(LeadIncompatibility {
|
||||
edge: edge.id,
|
||||
source: ends[0]
|
||||
.iter()
|
||||
.map(|&e| describe(edge.source, e, &obstacles))
|
||||
.collect(),
|
||||
target: ends[1]
|
||||
.iter()
|
||||
.map(|&e| describe(edge.target, e, &obstacles))
|
||||
.collect(),
|
||||
allowed_pair_count,
|
||||
lead_clear_pair_count,
|
||||
});
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn coincident_selfloop_exception_and_stub_containment_remain_separate() {
|
||||
let owner = Id::from_u128(1);
|
||||
let edge = ProjectedEdge {
|
||||
id: Id::from_u128(2),
|
||||
source: owner,
|
||||
target: owner,
|
||||
source_port: None,
|
||||
target_port: None,
|
||||
trace: false,
|
||||
oriented: true,
|
||||
};
|
||||
let endpoint = Endpoint {
|
||||
point: pos(0., 0.),
|
||||
stub: pos(18., 0.),
|
||||
};
|
||||
assert!(!allowed_endpoint_pair(&edge, endpoint, endpoint, false));
|
||||
assert!(allowed_endpoint_pair(&edge, endpoint, endpoint, true));
|
||||
let obstacles = [Rect {
|
||||
id: owner,
|
||||
left: -1.,
|
||||
right: 20.,
|
||||
top: -1.,
|
||||
bottom: 1.,
|
||||
}];
|
||||
assert!(lead_pair_clear(&edge, endpoint, endpoint, &obstacles));
|
||||
let diagnostic = describe(owner, endpoint, &obstacles);
|
||||
assert!(diagnostic.blockers.is_empty());
|
||||
assert_eq!(diagnostic.stub_containers.len(), 1);
|
||||
// This diagnostic must not be folded into the historical early guard.
|
||||
assert_eq!(diagnostic.stub_containers[0].id, owner);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user