Preserve requested container minima after placement transfer
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user