Add bounded external placement comparison and preservation adapter

This commit is contained in:
2026-09-24 20:12:01 -07:00
parent ac5d7ef34a
commit 8a2010d8a6
10 changed files with 851 additions and 1 deletions
+253 -1
View File
@@ -794,6 +794,246 @@ fn route_corpus(path: &std::path::Path) {
);
}
}
// External placement is an experimental geometry transfer, not a production command.
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct PlacementPlan {
schema: u32,
nodes: Vec<PlacementNode>,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct PlacementNode {
id: Id,
position: Position,
size: Size,
}
fn import_placement(
original: &Project,
diagram: Id,
plan: PlacementPlan,
) -> Result<Project, ModelError> {
let fail = |message: &str| ModelError(message.to_owned());
validate_candidate(original)?;
let d = original
.diagrams
.get(&diagram)
.ok_or_else(|| fail("Missing diagram"))?;
if d.projection.scenario().is_some() {
return Err(fail("Scenario placement import is unsupported"));
}
if plan.schema != 1 || d.nodes.len() > 256 || plan.nodes.len() > 256 {
return Err(fail("Placement schema or 256-node bound exceeded"));
}
let view = projection::project_view(original, diagram);
if view.edges.len() > 1024 {
return Err(fail("Placement 1024-visible-edge bound exceeded"));
}
let visible: BTreeSet<_> = view.nodes.iter().map(|node| node.id).collect();
let mut supplied = BTreeSet::new();
for node in &plan.nodes {
if !supplied.insert(node.id)
|| !visible.contains(&node.id)
|| !node.position.x.is_finite()
|| !node.position.y.is_finite()
|| !node.size.width.is_finite()
|| !node.size.height.is_finite()
|| node.size.width <= 0.0
|| node.size.height <= 0.0
{
return Err(fail("Invalid, duplicate or out-of-scope placement"));
}
}
if supplied != visible {
return Err(fail(
"Plan must contain every projected visible node exactly once",
));
}
let eligible: BTreeSet<_> = view
.edges
.iter()
.filter_map(|edge| {
let owners = projection::edge_endpoint_placements(original, d, edge.id)?;
(owners == (edge.source, edge.target)
&& visible.contains(&edge.source)
&& visible.contains(&edge.target))
.then_some(edge.id)
})
.collect();
// Recover core visual ancestry, including Allocation grouping, without
// reimplementing private parents(). This temporary clone is never returned.
let mut unfolded = original.clone();
for node in unfolded
.diagrams
.get_mut(&diagram)
.unwrap()
.nodes
.values_mut()
{
node.collapsed = false;
}
let full = projection::project_view(&unfolded, diagram);
let parents: BTreeMap<_, _> = full.nodes.iter().map(|n| (n.id, n.visual_parent)).collect();
let mut hidden_owner = BTreeMap::new();
for id in parents.keys() {
let mut seen = BTreeSet::from([*id]);
let mut parent = parents[id];
let mut nearest = None;
while let Some(ancestor) = parent {
if seen.len() > 256 || !seen.insert(ancestor) {
return Err(fail("Invalid cyclic or excessive visual ancestry"));
}
if nearest.is_none() && visible.contains(&ancestor) && d.nodes[&ancestor].collapsed {
nearest = Some(ancestor);
}
parent = *parents
.get(&ancestor)
.ok_or_else(|| fail("Missing visual ancestor"))?;
}
if !visible.contains(id)
&& let Some(owner) = nearest
{
hidden_owner.insert(*id, owner);
}
}
let mut after = original.clone();
let ad = after.diagrams.get_mut(&diagram).unwrap();
for node in plan.nodes {
let placed = ad
.nodes
.get_mut(&node.id)
.ok_or_else(|| fail("Projected node is not stored"))?;
if placed.collapsed {
let compact = view.nodes.iter().find(|n| n.id == node.id).unwrap().size;
if node.size != compact {
return Err(fail(
"Collapsed placement plan must retain exact projected compact size; stored expanded size is preserved",
));
}
} else {
placed.size = node.size;
}
placed.position = node.position;
}
for (id, owner) in hidden_owner {
let delta = Position {
x: ad.nodes[&owner].position.x - d.nodes[&owner].position.x,
y: ad.nodes[&owner].position.y - d.nodes[&owner].position.y,
};
let hidden = ad.nodes.get_mut(&id).unwrap();
hidden.position.x = d.nodes[&id].position.x + delta.x;
hidden.position.y = d.nodes[&id].position.y + delta.y;
}
let deltas: BTreeMap<_, _> = ad
.nodes
.iter()
.map(|(id, n)| {
(
*id,
Position {
x: n.position.x - d.nodes[id].position.x,
y: n.position.y - d.nodes[id].position.y,
},
)
})
.collect();
let translated: BTreeMap<_, _> = projection::translated_routes(original, d, &deltas)
.into_iter()
.map(|route| (route.edge, route))
.collect();
for route in &mut ad.routes {
if eligible.contains(&route.edge) {
route.waypoints.clear();
route.label = None;
} else if let Some(moved) = translated.get(&route.edge) {
*route = moved.clone();
}
}
for node in ad.nodes.values() {
if !node.position.x.is_finite() || !node.position.y.is_finite() {
return Err(fail("Placement translation overflow"));
}
}
validate_candidate(&after)?;
Ok(after)
}
fn import_placement_cli(
project_file: &std::path::Path,
diagram_text: &std::ffi::OsStr,
plan_file: &std::path::Path,
) {
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");
let plan: PlacementPlan =
serde_json::from_slice(&std::fs::read(plan_file).expect("Read placement plan"))
.expect("Decode placement plan");
let after = import_placement(&before, diagram, plan).expect("Validate placement transfer");
println!(
"{}",
json!({"probe":"external_placement_v1", "diagram":diagram,
"scope":"Visible positions and expanded-node sizes imported; collapsed planned size must equal original projected compact size, stored expanded size preserved. Hidden descendants translated with nearest visible collapsed ancestor; unrelated omitted placements exact. Existing eligible routes reset, retained routes use original-to-final translation. No ELK paths, labels or ports imported; no Layout executed.",
"before_project":before,"after_project":after,"before":metrics(&before,diagram),"after":metrics(&after,diagram),
"conservation":conservation(&before,&after,diagram),"roundtrip":roundtrip(&after),
"final_projected_ports":projection::project_view(&after,diagram).ports})
);
}
fn arrange_corpus(path: &std::path::Path) {
let manifest: Value = serde_json::from_slice(&std::fs::read(path).expect("Read manifest"))
.expect("Decode manifest");
assert_eq!(manifest["schema"], 1);
let mut names = BTreeSet::new();
let cases = manifest["cases"].as_array().expect("Cases");
assert!(!cases.is_empty(), "Empty corpus");
println!(
"{}",
json!({"probe":"arrange_corpus_v1","manifest":manifest,"quality_thresholds":null})
);
for case in cases {
let name = case["name"].as_str().expect("Case name");
assert!(names.insert(name), "Duplicate case name");
let file = std::path::Path::new(case["project_file"].as_str().expect("Project file"));
assert!(
file.components()
.all(|c| matches!(c, std::path::Component::Normal(_))),
"Relative normal project path required"
);
let project: Project = serde_json::from_slice(
&std::fs::read(path.parent().unwrap().join(file)).expect("Read Project"),
)
.expect("Decode Project");
let diagram: Id = serde_json::from_value(case["diagram"].clone()).expect("Diagram UUID");
validate_candidate(&project).expect("Validate corpus Project");
assert!(project.diagrams.contains_key(&diagram), "Missing diagram");
assert!(
project.diagrams[&diagram].nodes.len() <= 256,
"256-node bound"
);
assert!(
projection::project_view(&project, diagram).edges.len() <= 1024,
"1024-edge bound"
);
println!(
"{}",
observe(
name,
Builder {
project,
diagram,
next: 0,
witness: None
}
)
);
}
}
fn main() {
// Analytic checks validate the observer, not any layout's quality.
let horizontal = [(0., 0.), (10., 0.)];
@@ -837,10 +1077,22 @@ fn main() {
route_corpus(std::path::Path::new(&args[2]));
return;
}
if args.len() == 3 && args[1] == "--arrange-corpus" {
arrange_corpus(std::path::Path::new(&args[2]));
return;
}
if args.len() == 5 && args[1] == "--import-placement" {
import_placement_cli(
std::path::Path::new(&args[2]),
&args[3],
std::path::Path::new(&args[4]),
);
return;
}
assert_eq!(
args.len(),
1,
"Usage: auto_layout_probe [--route-corpus MANIFEST]"
"Usage: auto_layout_probe [--route-corpus MANIFEST | --arrange-corpus MANIFEST | --import-placement PROJECT UUID PLAN]"
);
println!(
"{}",