Account for target joins when selecting route legs

This commit is contained in:
2026-09-24 17:53:47 -07:00
parent 427078461a
commit 497c1ad8b3
3 changed files with 338 additions and 10 deletions
+116 -1
View File
@@ -65,6 +65,69 @@ fn shared_length(a: P, b: P, c: P, d: P) -> f64 {
let s = dot(sub(d, a), u) / len;
(t.max(s).min(len) - t.min(s).max(0.)).max(0.)
}
// Closed-segment point intersections. A positive collinear interval is not a
// point: it remains represented by the existing shared-length metric.
fn point_contact(a: P, b: P, c: P, d: P) -> Option<P> {
let u = sub(b, a);
let v = sub(d, c);
let on_segment = |at: P, start: P, end: P| {
let delta = sub(end, start);
let len = length(delta);
if len < EPS {
return length(sub(at, start)) < EPS;
}
cross(delta, sub(at, start)).abs() / len <= EPS
&& dot(sub(at, start), delta) >= -EPS * len
&& dot(sub(at, end), delta) <= EPS * len
};
if length(u) < EPS {
return on_segment(a, c, d).then_some(a);
}
if length(v) < EPS {
return on_segment(c, a, b).then_some(c);
}
let denominator = cross(u, v);
if denominator.abs() < 1e-9 {
if shared_length(a, b, c, d) > EPS {
return None;
}
return [a, b, c, d]
.into_iter()
.find(|at| on_segment(*at, a, b) && on_segment(*at, c, d));
}
let t = cross(sub(c, a), v) / denominator;
let s = cross(sub(c, a), u) / denominator;
(t >= -1e-9 && t <= 1. + 1e-9 && s >= -1e-9 && s <= 1. + 1e-9)
.then_some((a.0 + t.clamp(0., 1.) * u.0, a.1 + t.clamp(0., 1.) * u.1))
}
fn point_contacts(a: &[P], b: &[P]) -> Vec<(P, bool)> {
let mut unique = BTreeMap::new();
for ab in a.windows(2) {
for cd in b.windows(2) {
if let Some(at) = point_contact(ab[0], ab[1], cd[0], cd[1]) {
let key = ((at.0 / EPS).round() as i64, (at.1 / EPS).round() as i64);
let proper = proper_cross(ab[0], ab[1], cd[0], cd[1]).is_some();
let entry = unique.entry(key).or_insert((at, false));
entry.1 |= proper;
}
}
}
unique.into_values().collect()
}
fn contact_role(points: &[P], at: P) -> &'static str {
if points
.first()
.into_iter()
.chain(points.last())
.any(|tip| length(sub(*tip, at)) < EPS)
{
"route_endpoint"
} else if points.iter().any(|vertex| length(sub(*vertex, at)) < EPS) {
"internal_vertex"
} else {
"segment_interior"
}
}
fn ancestor(view: &ProjectedView, owner: Id, mut node: Id) -> bool {
for _ in 0..view.nodes.len() {
let Some(parent) = view
@@ -142,8 +205,31 @@ fn metrics(project: &Project, diagram: Id) -> Value {
let mut shared = 0.;
let mut excluded_shared = 0.;
let mut excluded_stub_crossings = 0;
let mut contacts = Vec::new();
let mut common_terminal_contacts = 0;
for (i, a) in edges.iter().enumerate() {
for b in edges.iter().skip(i + 1) {
let ap: Vec<_> = a.points.iter().copied().map(p).collect();
let bp: Vec<_> = b.points.iter().copied().map(p).collect();
let ad = definitions[&a.id];
let bd = definitions[&b.id];
for (at, proper) in point_contacts(&ap, &bp) {
let common_terminal = [(ad.source, ap.first()), (ad.target, ap.last())]
.iter()
.any(|(owner, tip)| {
tip.is_some_and(|tip| length(sub(*tip, at)) < EPS)
&& [(bd.source, bp.first()), (bd.target, bp.last())]
.iter()
.any(|(other, tip)| {
owner == other
&& tip.is_some_and(|tip| length(sub(*tip, at)) < EPS)
})
});
common_terminal_contacts += usize::from(common_terminal);
contacts.push(json!({"edges":[a.id,b.id],"at":[at.0,at.1],
"proper_crossing":proper,"roles":[contact_role(&ap,at),contact_role(&bp,at)],
"common_physical_terminal":common_terminal}));
}
let mut unique = BTreeSet::new();
for (ai, ab) in a.points.windows(2).enumerate() {
for (bi, cd) in b.points.windows(2).enumerate() {
@@ -282,6 +368,8 @@ fn metrics(project: &Project, diagram: Id) -> Value {
json!({
"nodes":view.nodes.len(),"edges":edges.len(),"ports":view.ports.len(),
"proper_crossings":crossings.len(),"crossing_details":crossings,
"point_contacts":contacts.len(),"point_contact_details":contacts,
"common_physical_terminal_point_contacts":common_terminal_contacts,
"pairwise_collinear_shared_length":shared,
"excluded_common_terminal_shared_length":excluded_shared,"excluded_common_terminal_crossings":excluded_stub_crossings,
"nonterminal_proper_crossings":crossings.len()-excluded_stub_crossings,
@@ -297,6 +385,9 @@ fn metrics(project: &Project, diagram: Id) -> Value {
"drawing_area":routed.bounds.size.width as f64*routed.bounds.size.height as f64,
"route_bends":bends,"route_length":total_length,
"projected_nodes":view.nodes,"projected_ports":view.ports,
"route_endpoints":edges.iter().map(|e|json!({"id":e.id,
"source_owner":definitions[&e.id].source,"target_owner":definitions[&e.id].target,
"source":e.points.first(),"target":e.points.last()})).collect::<Vec<_>>(),
"routed_edges":edges.iter().map(|e|json!({"id":e.id,"points":e.points,"label":{
"text":e.label.text,"position":e.label.bounds.position,"size":e.label.bounds.size}})).collect::<Vec<_>>()
})
@@ -647,6 +738,29 @@ fn observe(name: &str, f: Builder) -> Value {
}
fn main() {
// Analytic checks validate the observer, not any layout's quality.
let horizontal = [(0., 0.), (10., 0.)];
assert_eq!(
point_contacts(&horizontal, &[(5., -5.), (5., 5.)]),
vec![((5., 0.), true)]
);
assert_eq!(
point_contacts(&horizontal, &[(5., 0.), (5., 5.)]),
vec![((5., 0.), false)]
);
let bend = [(5., -5.), (5., 0.), (8., 3.)];
assert_eq!(point_contacts(&horizontal, &bend), vec![((5., 0.), false)]);
assert_eq!(contact_role(&bend, (5., 0.)), "internal_vertex");
assert_eq!(contact_role(&horizontal, (5., 0.)), "segment_interior");
assert_eq!(
point_contacts(&horizontal, &[(10., 0.), (10., 5.)]),
vec![((10., 0.), false)]
);
assert_eq!(contact_role(&horizontal, (10., 0.)), "route_endpoint");
assert_eq!(
point_contacts(&horizontal, &[(10., 0.), (20., 0.)]),
vec![((10., 0.), false)]
);
assert!(point_contacts(&horizontal, &[(4., 0.), (12., 0.)]).is_empty());
assert_eq!(
proper_cross((0., 0.), (10., 0.), (5., -5.), (5., 5.)),
Some((5., 0.))
@@ -662,7 +776,8 @@ fn main() {
assert_eq!(inside_length((0., 2.), (10., 2.), (2., 2., 8., 8.)), 0.);
println!(
"{}",
json!({"probe":"auto_layout_probe_v1","reference_source":"715036e132677dd2bc2fb37ed316d93dc9987aec","metric_selfchecks":"passed", "quality_thresholds":null,
json!({"probe":"auto_layout_probe_v2","reference_source":"715036e132677dd2bc2fb37ed316d93dc9987aec","metric_selfchecks":"passed", "quality_thresholds":null,
"point_contact_definition":"Unexempted unique point per edge pair (EPS quantization), closed segments, proper-crossing flag ORed across segment witnesses; roles distinguish route endpoints/internal vertices/interiors. Positive collinear intervals are only in shared-length metric; a noncollinear witness at an overlap boundary is still a point contact. Common physical terminal requires equal owner and equal endpoint coordinate and is classified, never subtracted. Route endpoints emitted for exact comparison against the frozen prior run, not assumed invariant across node layout.",
"exclusions":"Raw strict-interior crossings and pairwise shared length are primary; separately bound common physical endpoint exclusions to PORT_STUB. Ancestor node containment excluded. Labels wholly inside endpoint-ancestor BODY below header exempt; headers retained. Penetration excludes endpoint nodes and endpoint-ancestor bodies. Label/stroke excludes own edge. Core approximate label bounds, no glyph/port-label metrics."})
);
for (name, f) in fixtures() {