Plan Allocation work-group assignments as one exact transaction
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user