Preserve requested container minima after placement transfer

This commit is contained in:
2026-09-24 21:24:57 -07:00
parent 4dd764fa6c
commit 2426d28648
2 changed files with 172 additions and 1 deletions
+158 -1
View File
@@ -967,7 +967,50 @@ fn import_placement(
}
}
validate_candidate(&after)?;
let final_view = projection::project_view(&after, diagram);
let mut final_view = projection::project_view(&after, diagram);
// Moving children inward can remove a formerly derived container minimum.
// Preserve authored size for true identity transfers, but materialize a
// requested unchanged effective size when reprojection would shrink it.
// This is one simultaneous restoration, never an iterative layout repair.
let mut restore_minima = Vec::new();
let original_sizes: BTreeMap<_, _> = view.nodes.iter().map(|n| (n.id, n.size)).collect();
let projected_ids: BTreeSet<_> = final_view.nodes.iter().map(|n| n.id).collect();
if projected_ids != visible || final_view.nodes.len() != visible.len() {
return Err(fail(
"Final Rust projection differs from intended node identities",
));
}
for node in &final_view.nodes {
let (position, size) = intended[&node.id];
if node.position == position && node.size == size {
continue;
}
if node.position != position
|| d.nodes[&node.id].collapsed
|| original_sizes[&node.id] != size
|| node.size.width > size.width
|| node.size.height > size.height
{
return Err(fail(
"Final Rust projection differs from intended node geometry",
));
}
restore_minima.push((node.id, size));
}
if !restore_minima.is_empty() {
for (id, size) in restore_minima {
after
.diagrams
.get_mut(&diagram)
.unwrap()
.nodes
.get_mut(&id)
.unwrap()
.size = size;
}
validate_candidate(&after)?;
final_view = projection::project_view(&after, diagram);
}
if final_view.nodes.len() != intended.len()
|| final_view
.nodes
@@ -980,6 +1023,120 @@ fn import_placement(
}
Ok(after)
}
#[cfg(test)]
mod placement_import_tests {
use super::*;
fn nested() -> Builder {
fixtures()
.into_iter()
.find(|(name, _)| *name == "two_containers_reversed_children")
.unwrap()
.1
}
fn identity(project: &Project, diagram: Id) -> PlacementPlan {
PlacementPlan {
schema: 1,
nodes: projection::project_view(project, diagram)
.nodes
.into_iter()
.map(|n| PlacementNode {
id: n.id,
position: n.position,
size: n.size,
})
.collect(),
}
}
#[test]
fn unchanged_effective_container_size_survives_inward_child_movement() {
let f = nested();
let before = projection::project_view(&f.project, f.diagram);
let mut plan = identity(&f.project, f.diagram);
let mut expected = f.project.clone();
for node in &mut plan.nodes {
let projected = before.nodes.iter().find(|n| n.id == node.id).unwrap();
if projected.visual_parent.is_some() && node.position.y == 330.0 {
node.position.y -= 20.0;
expected
.diagrams
.get_mut(&f.diagram)
.unwrap()
.nodes
.get_mut(&node.id)
.unwrap()
.position = node.position;
}
if projected.visual_parent.is_none() {
expected
.diagrams
.get_mut(&f.diagram)
.unwrap()
.nodes
.get_mut(&node.id)
.unwrap()
.size = node.size;
}
}
let after = import_placement(&f.project, f.diagram, plan).unwrap();
assert_eq!(after, expected);
let final_view = projection::project_view(&after, f.diagram);
for node in &final_view.nodes {
assert_eq!(
node.size,
before.nodes.iter().find(|n| n.id == node.id).unwrap().size
);
}
}
#[test]
fn identity_keeps_authored_expanded_and_folded_sizes() {
let mut f = nested();
assert_eq!(
import_placement(&f.project, f.diagram, identity(&f.project, f.diagram)).unwrap(),
f.project
);
let parent = projection::project_view(&f.project, f.diagram)
.nodes
.into_iter()
.find(|n| n.visual_parent.is_none())
.unwrap()
.id;
f.project
.diagrams
.get_mut(&f.diagram)
.unwrap()
.nodes
.get_mut(&parent)
.unwrap()
.collapsed = true;
assert_eq!(
import_placement(&f.project, f.diagram, identity(&f.project, f.diagram)).unwrap(),
f.project
);
}
#[test]
fn inconsistent_smaller_container_plan_still_rejects() {
let f = nested();
let child = projection::project_view(&f.project, f.diagram)
.nodes
.into_iter()
.find(|n| n.visual_parent.is_some())
.unwrap()
.id;
let mut plan = identity(&f.project, f.diagram);
plan.nodes
.iter_mut()
.find(|n| n.id == child)
.unwrap()
.position
.x += 1000.0;
assert!(import_placement(&f.project, f.diagram, plan).is_err());
}
}
fn import_placement_cli(
project_file: &std::path::Path,
diagram_text: &std::ffi::OsStr,
@@ -66,3 +66,17 @@ ordering remain distinct alternatives; choose the next action from the observed
failure, not from commitment to this prototype.
Windows only; Linux builds and the frozen published prerelease remain unchanged.
## Transfer correction after the first run
Run01 at 4dd764fa exposed a transfer bug in the unchanged-effective-size shortcut,
not an SCC placement choice: moving children inward removed part of a parent's
derived enclosure while the importer preserved its smaller authored size. The
parent was therefore projected smaller than the explicitly requested geometry.
Retain run01. Correct the importer with one simultaneous restoration of intended
unchanged effective minima for noncollapsed nodes whose first projection is
strictly smaller and at the exact intended position. Reject every other mismatch
before restoration; require an exact second projection. Add identity, inward-child
and inconsistent-plan regression checks. Then repeat the same corpus as run02.
The SCC ordering, gaps, packing, routing and quality criteria remain unchanged;
this is a correctness repair to faithfully execute the predeclared candidate.