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,