Plan Allocation work-group assignments as one exact transaction

This commit is contained in:
2026-09-23 11:20:58 -07:00
parent eed80ffd6e
commit 33e880103c
7 changed files with 1891 additions and 252 deletions
+32 -142
View File
@@ -121,20 +121,10 @@ pub fn capture(
}
}
// Geometry never changes visual ancestry in Arrange. Recover the complete
// ancestry once, including hidden nodes. This all-unfolded projection is
// used only for identity lookup, never as a collision-admission scene.
let mut all_unfolded = d.clone();
for node in all_unfolded.nodes.values_mut() {
node.collapsed = false;
}
let all_view = projection::project_diagram_with_index(p, &all_unfolded, &index);
let ancestry = ancestors(&all_view);
let mut expanded_ids: BTreeSet<_> = placements.keys().copied().collect();
for id in placements.keys() {
expanded_ids.extend(ancestry.get(id).ok_or(Refusal::StaleScope)?.iter().copied());
}
let before_expanded = diagnostic(p, d, &expanded_ids, &index);
// Arrange preserves ancestry, so accepted diagnostic identities also apply
// to every translated candidate.
let expanded_ids = geometry::expansion_ids(p, d, &index, &placements)?;
let before_expanded = geometry::expanded_view(p, d, &index, &expanded_ids);
Ok(Plan {
accepted: p.clone(),
diagram,
@@ -148,21 +138,6 @@ pub fn capture(
})
}
fn diagnostic(
project: &Project,
diagram: &Diagram,
expanded_ids: &BTreeSet<Id>,
index: &ViewIndex,
) -> ProjectedView {
let mut expanded = diagram.clone();
for id in expanded_ids {
if let Some(node) = expanded.nodes.get_mut(id) {
node.collapsed = false;
}
}
projection::project_diagram_with_index(project, &expanded, index)
}
/// Snap the captured anchor once; preview the complete union without cloning or
/// validating a Project, rebuilding its semantic index, or expanding scope.
pub fn place(
@@ -185,120 +160,35 @@ pub fn place(
"Make room or adjust the view, then drag again.".into(),
));
}
let anchor = &plan.placements[&plan.anchor];
let delta = Position {
x: (requested.x / 10.).round() * 10. - anchor.position.x,
y: (requested.y / 10.).round() * 10. - anchor.position.y,
};
let mut diagram = plan.diagram().clone();
let mut commands = Vec::new();
let mut deltas = BTreeMap::new();
for (id, original) in &plan.placements {
let node = diagram.nodes.get_mut(id).ok_or(Refusal::StaleScope)?;
node.position = Position {
x: original.position.x + delta.x,
y: original.position.y + delta.y,
};
if !(Rect {
position: node.position,
size: node.size,
})
.valid()
{
return Err(Refusal::InvalidCandidate(
"Moved coordinates exceed the model bounds.".into(),
));
}
deltas.insert(*id, delta);
if node != original {
commands.push(Command::EditNode {
diagram: diagram.id,
original: *id,
node: node.clone(),
});
}
}
for route in projection::translated_routes(&plan.accepted, plan.diagram(), &deltas) {
if route.waypoints.iter().any(|point| {
!point.x.is_finite()
|| !point.y.is_finite()
|| point.x.abs() > MAX_COORDINATE
|| point.y.abs() > MAX_COORDINATE
}) {
return Err(Refusal::InvalidCandidate(
"Moved route exceeds the model bounds.".into(),
));
}
let stored = diagram
.routes
.iter_mut()
.find(|stored| stored.edge == route.edge)
.ok_or(Refusal::StaleScope)?;
*stored = route.clone();
commands.push(Command::SetEdgeRoute {
diagram: diagram.id,
route,
});
}
let geometry::Translation {
diagram,
commands,
delta,
} = geometry::translate(
&plan.accepted,
plan.diagram(),
plan.diagram(),
&plan.placements,
plan.anchor,
requested,
)?;
let view = projection::project_diagram_with_index(&plan.accepted, &diagram, &plan.index);
let expanded = diagnostic(&plan.accepted, &diagram, &plan.expanded_ids, &plan.index);
if view.nodes.iter().chain(&expanded.nodes).any(|node| {
!(Rect {
position: node.position,
size: node.size,
})
.valid()
}) {
return Err(Refusal::InvalidCandidate(
"The projected enclosure exceeds the model bounds.".into(),
));
}
let before: BTreeMap<_, _> = plan
.before_expanded
.nodes
.iter()
.map(|n| (n.id, n))
.collect();
let after: BTreeMap<_, _> = expanded.nodes.iter().map(|n| (n.id, n)).collect();
for id in plan.placements.keys() {
let old = before.get(id).ok_or(Refusal::StaleScope)?;
let new = after.get(id).ok_or(Refusal::StaleScope)?;
if (new.position.x - old.position.x - delta.x).abs() > 0.01
|| (new.position.y - old.position.y - delta.y).abs() > 0.01
|| (new.size.width - old.size.width).abs() > 0.01
|| (new.size.height - old.size.height).abs() > 0.01
{
return Err(Refusal::LayoutRequired(
"This selection requires an explicit layout; its footprint cannot be translated unchanged."
.into(),
));
}
}
if overlaps(&plan.before_visible, &view) || overlaps(&plan.before_expanded, &expanded) {
return Err(Refusal::LayoutRequired(
"Make room before arranging: the candidate would introduce or enlarge overlapping cards."
.into(),
));
}
for (id, element) in &plan.selected {
let selected = view
.nodes
.iter()
.find(|node| node.id == *id && node.element == *element)
.ok_or_else(|| Refusal::LayoutRequired("A selected header would be hidden.".into()))?;
let header = Rect {
position: selected.position,
size: Size {
width: selected.size.width,
height: selected.size.height.min(62.),
},
};
if !presentation.viewport.contains(header) {
return Err(Refusal::LayoutRequired(
"Make room or adjust the view, then drag again.".into(),
));
}
}
let expanded =
geometry::expanded_view(&plan.accepted, &diagram, &plan.index, &plan.expanded_ids);
// Preserve Arrange's refusal precedence: projected bounds, then rigidity,
// then overlap growth. The shared scene check combines bounds and overlaps.
let scenes = match geometry::check_scenes(
&plan.before_visible,
&plan.before_expanded,
&view,
&expanded,
) {
Err(error @ Refusal::InvalidCandidate(_)) => return Err(error),
result => result,
};
geometry::check_rigid(&plan.before_expanded, &expanded, &plan.placements, delta)?;
scenes?;
geometry::check_headers(&view, &plan.selected, presentation.viewport)?;
Ok(PlacementPlan {
diagram,
view,