experiment(layout): diagnose mandatory leads and test bounded flat separation

This commit is contained in:
2026-09-24 20:38:33 -07:00
parent 2850b915d6
commit 2be8c7f292
6 changed files with 898 additions and 41 deletions
+95 -1
View File
@@ -5,6 +5,8 @@ use oa_core::{
};
use serde_json::{Value, json};
use std::collections::{BTreeMap, BTreeSet};
#[path = "auto_layout_probe/clearance.rs"]
mod clearance;
#[path = "auto_layout_probe/layered.rs"]
mod layered;
@@ -1002,6 +1004,94 @@ fn import_placement_cli(
"final_projected_ports":projection::project_view(&after,diagram).ports})
);
}
fn lead_diagnostics(project: &Project, diagram: Id) -> Value {
let view = projection::project_view(project, diagram);
let endpoint = |e: &routing::LeadEndpoint| {
json!({"owner":e.owner,"tip":e.tip,"stub":e.stub,
"blockers":e.blockers.iter().map(|b|json!({"id":b.id,"bounds":{"position":b.bounds.position,"size":b.bounds.size}})).collect::<Vec<_>>(),
"stub_containers":e.stub_containers.iter().map(|b|json!({"id":b.id,"bounds":{"position":b.bounds.position,"size":b.bounds.size}})).collect::<Vec<_>>()})
};
json!(
routing::lead_incompatibilities(project, &project.diagrams[&diagram], &view)
.iter()
.map(
|r| json!({"edge":r.edge,"allowed_pair_count":r.allowed_pair_count,
"lead_clear_pair_count":r.lead_clear_pair_count,
"source":r.source.iter().map(&endpoint).collect::<Vec<_>>(),
"target":r.target.iter().map(&endpoint).collect::<Vec<_>>()})
)
.collect::<Vec<_>>()
)
}
fn repaired_candidate(before: &Project, diagram: Id) -> Result<Project, ModelError> {
let geometry = clearance::repair(before, diagram)?;
let view = projection::project_view(&geometry, diagram);
// Reconstruct metadata once from original to final geometry with the same
// audited reset/translation policy used by the placement reference.
import_placement(
before,
diagram,
PlacementPlan {
schema: 1,
nodes: view
.nodes
.iter()
.map(|n| PlacementNode {
id: n.id,
position: n.position,
size: n.size,
})
.collect(),
},
)
}
fn repair_placement_cli(project_file: &std::path::Path, diagram_text: &std::ffi::OsStr) {
let before: Project =
serde_json::from_slice(&std::fs::read(project_file).expect("Read Project"))
.expect("Decode Project");
let diagram: Id = diagram_text
.to_str()
.expect("UTF-8 UUID")
.parse()
.expect("Diagram UUID");
validate_candidate(&before).expect("Validate input");
assert!(before.diagrams.contains_key(&diagram), "Missing diagram");
let candidate = match repaired_candidate(&before, diagram) {
Err(error) => json!({"supported":false,"error":error.to_string()}),
Ok(after) => {
let duplicate = repaired_candidate(&before, diagram);
let mut previous = after.clone();
let mut stability = Vec::new();
for pass in 2..=10 {
match repaired_candidate(&previous, diagram) {
Ok(next) => {
stability.push(json!({"pass":pass,"equal_previous":next==previous,"equal_first":next==after}));
previous = next;
}
Err(error) => {
stability.push(json!({"pass":pass,"error":error.to_string()}));
break;
}
}
}
json!({"supported":true,"project":after,"metrics":metrics(&after,diagram),
"lead_incompatibilities":lead_diagnostics(&after,diagram),
"conservation":conservation(&before,&after,diagram),"roundtrip":roundtrip(&after),
"same_input_exact":duplicate.as_ref().is_ok_and(|p|*p==after),
"ten_repair_calls":stability})
}
};
println!(
"{}",
json!({"probe":"placement_clearance_v1","diagram":diagram,
"scope":"Experimental flat monotone separation only; lead clearance is necessary, not sufficient routing or readability acceptance. Unsupported topology and unresolved constraints remain explicit. Existing router and command behavior unchanged.",
"before_project":before,"before":metrics(&before,diagram),
"before_lead_incompatibilities":lead_diagnostics(&before,diagram),"candidate":candidate})
);
}
fn arrange_corpus(path: &std::path::Path) {
let manifest: Value = serde_json::from_slice(&std::fs::read(path).expect("Read manifest"))
.expect("Decode manifest");
@@ -1107,10 +1197,14 @@ fn main() {
);
return;
}
if args.len() == 4 && args[1] == "--repair-placement" {
repair_placement_cli(std::path::Path::new(&args[2]), &args[3]);
return;
}
assert_eq!(
args.len(),
1,
"Usage: auto_layout_probe [--route-corpus MANIFEST | --arrange-corpus MANIFEST | --import-placement PROJECT UUID PLAN]"
"Usage: auto_layout_probe [--route-corpus MANIFEST | --arrange-corpus MANIFEST | --import-placement PROJECT UUID PLAN | --repair-placement PROJECT UUID]"
);
println!(
"{}",
@@ -0,0 +1,278 @@
//! Bounded flat-placement experiment. Empty lead diagnostics are necessary only,
//! never a claim that routes, labels, or drawing quality are acceptable.
use oa_core::{Id, ModelError, Project, projection, routing};
use std::collections::BTreeMap;
const ROUNDS: usize = 4;
const WORK_LIMIT: usize = 10_000_000;
const MAX_SPAN: f32 = 100_000.0;
const MARGIN: f32 = 1.0;
fn fail(message: &str) -> ModelError {
ModelError(format!("Clearance probe: {message}"))
}
fn spend(work: &mut usize, amount: usize) -> Result<(), ModelError> {
*work = work.saturating_add(amount);
if *work > WORK_LIMIT {
return Err(fail("work limit exceeded"));
}
Ok(())
}
// A boundary constraint means start[to] >= start[from] + extent[from] + gap.
// Adjacent total-order constraints below separately preserve start ordering.
type Constraints = BTreeMap<(usize, usize), f32>;
fn solve(
baseline: &[f32],
extents: &[f32],
order: &[usize],
constraints: &Constraints,
work: &mut usize,
) -> Result<Vec<f32>, ModelError> {
let mut result = baseline.to_vec();
let mut previous = None;
for &to in order {
if let Some(from) = previous {
result[to] = result[to].max(result[from]);
}
for (&(from, target), &gap) in constraints {
spend(work, 1)?;
if target == to {
result[to] = result[to].max(result[from] + extents[from] + gap);
}
}
previous = Some(to);
}
let minimum = baseline.iter().copied().fold(f32::INFINITY, f32::min);
for (i, &value) in result.iter().enumerate() {
if !value.is_finite() || value + extents[i] - minimum > MAX_SPAN {
return Err(fail("coordinate/span bound exceeded"));
}
}
Ok(result)
}
/// Input is an already admitted placement candidate. Never modifies its routes,
/// authored anchors, sizes or semantics. Error drops the private clone.
pub fn repair(project: &Project, diagram: Id) -> Result<Project, ModelError> {
let original = project
.diagrams
.get(&diagram)
.ok_or_else(|| fail("missing diagram"))?;
if original.projection.scenario().is_some() {
return Err(fail("Scenario unsupported"));
}
let baseline = projection::project_diagram(project, original);
if baseline.nodes.len() > 256 || baseline.edges.len() > 1024 {
return Err(fail("graph bound exceeded"));
}
if baseline.nodes.len() != original.nodes.len()
|| baseline
.nodes
.iter()
.any(|n| n.collapsed || n.visual_parent.is_some())
{
return Err(fail("nested, folded or omitted placements unsupported"));
}
let mut nodes = baseline.nodes.clone();
nodes.sort_by_key(|n| n.id);
if nodes.iter().any(|n| {
!n.position.x.is_finite()
|| !n.position.y.is_finite()
|| !n.size.width.is_finite()
|| !n.size.height.is_finite()
|| n.size.width <= 0.0
|| n.size.height <= 0.0
}) {
return Err(fail("invalid initial geometry"));
}
let indices: BTreeMap<_, _> = nodes.iter().enumerate().map(|(i, n)| (n.id, i)).collect();
let coordinates = [
nodes.iter().map(|n| n.position.x).collect::<Vec<_>>(),
nodes.iter().map(|n| n.position.y).collect::<Vec<_>>(),
];
let extents = [
nodes.iter().map(|n| n.size.width).collect::<Vec<_>>(),
nodes.iter().map(|n| n.size.height).collect::<Vec<_>>(),
];
for axis in 0..2 {
let minimum = coordinates[axis]
.iter()
.copied()
.fold(f32::INFINITY, f32::min);
if coordinates[axis]
.iter()
.zip(&extents[axis])
.any(|(&start, &extent)| {
!(start + extent).is_finite() || start + extent - minimum > MAX_SPAN
})
{
return Err(fail("initial coordinate/span bound exceeded"));
}
}
let mut orders = [Vec::new(), Vec::new()];
let mut ranks = [vec![0; nodes.len()], vec![0; nodes.len()]];
for axis in 0..2 {
orders[axis] = (0..nodes.len()).collect();
orders[axis].sort_by(|&a, &b| {
coordinates[axis][a]
.total_cmp(&coordinates[axis][b])
.then(nodes[a].id.cmp(&nodes[b].id))
});
for (rank, &i) in orders[axis].iter().enumerate() {
ranks[axis][i] = rank;
}
}
let mut constraints = [Constraints::new(), Constraints::new()];
let mut work = 0;
// Preserve one existing disjoint axis per pair, X before Y. No initially
// overlapping pair is silently treated as successfully repaired.
for a in 0..nodes.len() {
for b in a + 1..nodes.len() {
spend(&mut work, 1)?;
let mut chosen = None;
for axis in 0..2 {
for (from, to) in [(a, b), (b, a)] {
let gap = coordinates[axis][to] - coordinates[axis][from] - extents[axis][from];
if gap >= 0.0 {
chosen = Some((axis, from, to, gap));
break;
}
}
if chosen.is_some() {
break;
}
}
let (axis, from, to, gap) =
chosen.ok_or_else(|| fail("initial node overlap unsupported"))?;
constraints[axis].insert((from, to), gap);
}
}
let mut result = project.clone();
for round in 0..=ROUNDS {
let current = &result.diagrams[&diagram];
let view = projection::project_diagram(&result, current);
if view.nodes.len() != nodes.len()
|| view.nodes.iter().any(|n| {
indices.get(&n.id).is_none_or(|&i| {
n.size != nodes[i].size || n.collapsed || n.visual_parent.is_some()
})
})
{
return Err(fail("reprojection changed node identity/size/topology"));
}
// Charge a conservative endpoint-pair/obstacle allowance before the
// diagnostic: at most four cardinal candidates at each endpoint.
spend(
&mut work,
view.edges
.len()
.saturating_mul(nodes.len().saturating_add(1))
.saturating_mul(16),
)?;
let failures = routing::lead_incompatibilities(&result, current, &view);
if failures.is_empty() {
return Ok(result);
}
if round == ROUNDS {
return Err(fail("unresolved mandatory leads after four adjustments"));
}
let mut strengthened = false;
for failure in failures {
if failure.allowed_pair_count != 1
|| failure.source.len() != 1
|| failure.target.len() != 1
{
return Err(fail("ambiguous endpoint choices unsupported"));
}
for endpoint in failure.source.iter().chain(&failure.target) {
let owner = *indices
.get(&endpoint.owner)
.ok_or_else(|| fail("unknown endpoint owner"))?;
let dx = endpoint.stub.x - endpoint.tip.x;
let dy = endpoint.stub.y - endpoint.tip.y;
if endpoint.blockers.is_empty() {
continue;
}
let (axis, forward) = match (dx, dy) {
(x, 0.0) if x != 0.0 => (0, x > 0.0),
(0.0, y) if y != 0.0 => (1, y > 0.0),
_ => return Err(fail("noncardinal/degenerate blocked lead")),
};
for blocker in &endpoint.blockers {
spend(&mut work, 1)?;
let other = *indices
.get(&blocker.id)
.ok_or_else(|| fail("unknown blocker"))?;
let (from, to) = if forward {
(owner, other)
} else {
(other, owner)
};
if ranks[axis][from] >= ranks[axis][to] {
return Err(fail("lead constraint opposes frozen spatial order"));
}
let required = routing::PORT_STUB + routing::CLEARANCE + MARGIN;
let old = constraints[axis].entry((from, to)).or_insert(0.0);
if *old < required {
*old = required;
strengthened = true;
}
}
}
}
if !strengthened {
return Err(fail(
"unresolved lead without a stronger monotone constraint",
));
}
let x = solve(
&coordinates[0],
&extents[0],
&orders[0],
&constraints[0],
&mut work,
)?;
let y = solve(
&coordinates[1],
&extents[1],
&orders[1],
&constraints[1],
&mut work,
)?;
let target = result.diagrams.get_mut(&diagram).unwrap();
for (i, node) in nodes.iter().enumerate() {
let placed = target.nodes.get_mut(&node.id).unwrap();
placed.position.x = x[i];
placed.position.y = y[i];
}
}
unreachable!("bounded loop returns on final round")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn separation_propagates_to_neighbor_without_moving_predecessor() {
let constraints = BTreeMap::from([((0, 1), 31.0), ((1, 2), 10.0)]);
let actual = solve(
&[-20.0, 100.0, 210.0],
&[100.0; 3],
&[0, 1, 2],
&constraints,
&mut 0,
)
.unwrap();
assert_eq!(actual, vec![-20.0, 111.0, 221.0]);
}
#[test]
fn extent_bound_rejects_constraint_expansion() {
let constraints = BTreeMap::from([((0, 1), MAX_SPAN)]);
assert!(solve(&[0.0, 20.0], &[10.0; 2], &[0, 1], &constraints, &mut 0).is_err());
}
}
+76 -40
View File
@@ -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];
+173
View File
@@ -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);
}
}
+233
View File
@@ -0,0 +1,233 @@
use oa_core::{projection, routing, *};
struct Fixture {
p: Project,
d: Id,
edge: Id,
source: Id,
target: Id,
blocker: Id,
}
fn fixture(ports: bool, gap: f32) -> Fixture {
let mut p = Project::empty();
let kind = if ports {
Kind::Function
} else {
Kind::DataType
};
let a = p.add_element(kind, Layer::Sa, "Source", None).unwrap();
let b = p.add_element(kind, Layer::Sa, "Target", None).unwrap();
let c = p.add_element(kind, Layer::Sa, "Blocker", None).unwrap();
let edge = p.relate(
if ports {
RelationKind::FunctionalExchange
} else {
RelationKind::DataAssociation
},
"Flow",
a,
b,
);
let d = p.ensure_diagram(Layer::Sa);
let source = p.place(d, a, Position { x: 0., y: 0. });
let target = p.place(
d,
b,
Position {
x: 500.,
y: 180. + gap,
},
);
let blocker = p.place(d, c, Position { x: 500., y: 20. });
for n in p.diagrams.get_mut(&d).unwrap().nodes.values_mut() {
n.size = Size {
width: 224.,
height: 160.,
};
}
if ports {
for (owner, end, side) in [
(source, EdgeEnd::Source, PortAnchorSide::Right),
(target, EdgeEnd::Target, PortAnchorSide::Top),
] {
let port = routing::semantic_port_endpoint(&p, edge, end).unwrap();
p.diagrams
.get_mut(&d)
.unwrap()
.nodes
.get_mut(&owner)
.unwrap()
.port_anchors
.push(PortAnchor {
port,
side,
offset: 0.5,
});
}
}
validate_candidate(&p).unwrap();
Fixture {
p,
d,
edge,
source,
target,
blocker,
}
}
fn report(f: &Fixture) -> Vec<routing::LeadIncompatibility> {
routing::lead_incompatibilities(
&f.p,
&f.p.diagrams[&f.d],
&projection::project_view(&f.p, f.d),
)
}
fn attachment_route(edge: Id) -> EdgeRoute {
EdgeRoute {
edge,
source_attachment: Some(EdgeAttachment {
side: PortAnchorSide::Top,
offset: 0.25,
}),
target_attachment: Some(EdgeAttachment {
side: PortAnchorSide::Bottom,
offset: 0.75,
}),
waypoints: vec![Position { x: 300., y: 400. }],
label: None,
}
}
#[test]
fn exact_twenty_gap_is_blocked_thirty_plus_clears_without_mutation() {
let f = fixture(true, 20.);
let original = f.p.clone();
let r = report(&f);
assert_eq!(r.len(), 1);
assert_eq!(
(
r[0].edge,
r[0].allowed_pair_count,
r[0].lead_clear_pair_count
),
(f.edge, 1, 0)
);
assert_eq!(r[0].source[0].owner, f.source);
let end = &r[0].target[0];
assert_eq!(end.owner, f.target);
assert_eq!(end.tip, Position { x: 612., y: 200. });
assert_eq!(end.stub, Position { x: 612., y: 182. });
assert_eq!(end.blockers.len(), 1);
assert_eq!(end.blockers[0].id, f.blocker);
assert_eq!(
end.blockers[0].bounds,
routing::RoutingBounds {
position: Position { x: 488., y: 8. },
size: Size {
width: 248.,
height: 184.
},
}
);
assert_eq!(end.stub_containers, end.blockers);
assert_eq!(f.p, original);
assert!(report(&fixture(true, 30.)).is_empty());
assert!(report(&fixture(true, 31.)).is_empty());
}
#[test]
fn semantic_manual_ports_override_dormant_route_attachments_and_pins() {
let mut f = fixture(true, 20.);
let expected = report(&f);
f.p.diagrams
.get_mut(&f.d)
.unwrap()
.routes
.push(attachment_route(f.edge));
assert_eq!(report(&f), expected);
let view = projection::project_view(&f.p, f.d);
let routed = routing::route_diagram(&f.p, &f.p.diagrams[&f.d], &view);
assert_eq!(
routed.edges[&f.edge].points.last(),
Some(&expected[0].target[0].tip)
);
assert!(
routed.edges[&f.edge]
.warnings
.contains(&routing::RouteWarning::NoClearPath)
);
}
#[test]
fn unconstrained_nonport_alternative_is_not_hard_blocked() {
let mut f = fixture(false, 20.);
assert!(report(&f).is_empty());
let mut route = attachment_route(f.edge);
route.source_attachment = Some(EdgeAttachment {
side: PortAnchorSide::Right,
offset: 0.5,
});
route.target_attachment = Some(EdgeAttachment {
side: PortAnchorSide::Top,
offset: 0.5,
});
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(route);
let r = report(&f);
assert_eq!(r.len(), 1);
assert_eq!(r[0].allowed_pair_count, 1);
assert_eq!(r[0].target[0].blockers[0].id, f.blocker);
}
#[test]
fn nested_endpoint_body_is_traversable_and_promoted_attachments_are_suspended() {
let mut f = fixture(false, 20.);
// DataType containment is not required for this routing-only view witness:
// construct the supplied projected forest explicitly, retaining real owners.
let mut view = projection::project_view(&f.p, f.d);
let parent = Id::new_v4();
let source = view.nodes.iter_mut().find(|n| n.id == f.source).unwrap();
source.visual_parent = Some(parent);
source.position = Position { x: 40., y: 100. };
let element = source.element;
view.nodes.push(projection::ProjectedNode {
id: parent,
element,
position: Position { x: 0., y: 0. },
size: Size {
width: 300.,
height: 300.,
},
visual_parent: None,
depth: 0,
collapsed: false,
allocation_summary: false,
});
let mut route = attachment_route(f.edge);
route.source_attachment = Some(EdgeAttachment {
side: PortAnchorSide::Right,
offset: 0.5,
});
route.target_attachment = Some(EdgeAttachment {
side: PortAnchorSide::Top,
offset: 0.5,
});
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(route);
let r = routing::lead_incompatibilities(&f.p, &f.p.diagrams[&f.d], &view);
assert_eq!(r.len(), 1);
assert!(
r[0].source[0].blockers.is_empty(),
"Ancestor body must not block source lead"
);
// Promotion changes actual projected owner: custom attachments no longer apply.
view.edges
.iter_mut()
.find(|e| e.id == f.edge)
.unwrap()
.source = parent;
let with_dormant = routing::lead_incompatibilities(&f.p, &f.p.diagrams[&f.d], &view);
f.p.diagrams.get_mut(&f.d).unwrap().routes.clear();
assert_eq!(
with_dormant,
routing::lead_incompatibilities(&f.p, &f.p.diagrams[&f.d], &view)
);
}
@@ -0,0 +1,43 @@
# Placement clearance experiment
Continuation of issue32 from connection-layout-reference results at2850b915.
The sparse external placement leaves20 units between rectangles where its
final Rust top port needs18 units of outward lead plus12 units of unrelated
obstacle clearance. This experiment addresses that necessary placement
condition. It does not adopt either previous placement candidate.
Use the router's actual endpoint selection, promotion/attachment policy and
lead collision predicate in a narrow read-only diagnostic. Preserve existing
router guard order, search, costs and fallback. Compare the five-case frozen
route output exactly with the released-router control, and exercise targeted
lead, alternative-endpoint and folded/ancestor tests.
First consumer is an isolated example-only flat repair: at most256 nodes,
1024 edges and four separation rounds. Freeze axis order and initial coordinate
floors; seed separation inequalities from every initially disjoint node pair.
Add a31-unit boundary gap (18+12+1 margin) for unambiguous cardinal singleton
lead blockers and recompute monotone coordinates. Reproject actual automatic
ports each round. Reject overlapping input, unsupported nested/folded/omitted
topology, conflicting order, projection size changes, excessive extent/work,
or unresolved constraints. No mutation escapes on failure. This narrower
first experiment deliberately leaves subtree/compound coordinate assignment
for a later planner increment.
Evaluate all five frozen original and Arrange-reset scenes, all four supported
old native candidates, and all three prior ELK geometry transfers, unchanged.
Keep unsupported cases and failures explicit. Reconstruct route reset/retained
translation once from input to final geometry using the audited import helper.
Check semantic/anchor/other-view conservation, round trips, repeat determinism
and ten repeated repair calls. Report all existing quality measures, actual
port changes, warnings and unrelated-node penetrations. Require the exact sparse
terminal conflict to disappear without introducing a new node overlap or routing
failure before calling this compatibility increment successful. Lead clearance
alone is not complete routability or readability acceptance.
Stop after one bounded candidate and independent review. If the sparse witness
cannot be repaired under frozen orders, retain the counterexample and move
constraints into initial coordinate assignment; do not increase iteration
counts or tune the corpus. Even success leaves prior shared-run, area,
compound/undirected and native usability limitations unresolved.
Windows only. No Linux build, release change or production Arrange adoption.