Preserve requested container minima after placement transfer
This commit is contained in:
@@ -967,7 +967,50 @@ fn import_placement(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
validate_candidate(&after)?;
|
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()
|
if final_view.nodes.len() != intended.len()
|
||||||
|| final_view
|
|| final_view
|
||||||
.nodes
|
.nodes
|
||||||
@@ -980,6 +1023,120 @@ fn import_placement(
|
|||||||
}
|
}
|
||||||
Ok(after)
|
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(
|
fn import_placement_cli(
|
||||||
project_file: &std::path::Path,
|
project_file: &std::path::Path,
|
||||||
diagram_text: &std::ffi::OsStr,
|
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.
|
failure, not from commitment to this prototype.
|
||||||
|
|
||||||
Windows only; Linux builds and the frozen published prerelease remain unchanged.
|
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