233 lines
7.4 KiB
Rust
233 lines
7.4 KiB
Rust
//! Explicit, semantics-preserving arrangement of one or more Allocation cards.
|
|
//!
|
|
//! The App must admit the entire original typed selection before converting it
|
|
//! to placement/element identities. These values do not grant input authority.
|
|
use super::*;
|
|
|
|
#[derive(Debug)]
|
|
pub struct Plan {
|
|
accepted: Project,
|
|
diagram: Id,
|
|
selected: BTreeMap<Id, Id>,
|
|
anchor: Id,
|
|
placements: BTreeMap<Id, NodePlacement>,
|
|
expanded_ids: BTreeSet<Id>,
|
|
index: ViewIndex,
|
|
before_visible: ProjectedView,
|
|
before_expanded: ProjectedView,
|
|
}
|
|
|
|
impl Plan {
|
|
pub fn project(&self) -> &Project {
|
|
&self.accepted
|
|
}
|
|
|
|
pub fn index(&self) -> &ViewIndex {
|
|
&self.index
|
|
}
|
|
|
|
pub fn affected(&self) -> &BTreeMap<Id, NodePlacement> {
|
|
&self.placements
|
|
}
|
|
|
|
pub fn selected(&self) -> &BTreeMap<Id, Id> {
|
|
&self.selected
|
|
}
|
|
|
|
pub fn anchor(&self) -> Id {
|
|
self.anchor
|
|
}
|
|
|
|
pub fn diagram(&self) -> &Diagram {
|
|
&self.accepted.diagrams[&self.diagram]
|
|
}
|
|
}
|
|
|
|
/// Capture every original placement identity before normalizing semantic roots.
|
|
/// Document/revision/selection-generation and clean-owner guards belong to App.
|
|
pub fn capture(
|
|
p: &Project,
|
|
diagram: Id,
|
|
selected: &BTreeMap<Id, Id>,
|
|
anchor: Id,
|
|
) -> Result<Plan, Refusal> {
|
|
validate_candidate(p).map_err(|e| Refusal::InvalidCandidate(e.to_string()))?;
|
|
let d = p.diagrams.get(&diagram).ok_or(Refusal::StaleScope)?;
|
|
if d.projection != ProjectionKind::Allocation
|
|
|| selected.is_empty()
|
|
|| !selected.contains_key(&anchor)
|
|
{
|
|
return Err(Refusal::Ineligible(
|
|
"Arrange requires selected placements and a selected anchor in an Allocation view."
|
|
.into(),
|
|
));
|
|
}
|
|
let index = ViewIndex::new(p);
|
|
let before_visible = projection::project_diagram_with_index(p, d, &index);
|
|
let visible: BTreeMap<_, _> = before_visible
|
|
.nodes
|
|
.iter()
|
|
.map(|n| (n.id, n.element))
|
|
.collect();
|
|
let mut work = BTreeMap::new();
|
|
let mut performers = Vec::new();
|
|
for (placement, element) in selected {
|
|
let node = d.nodes.get(placement).ok_or(Refusal::StaleScope)?;
|
|
let record = p.elements.get(element).ok_or(Refusal::StaleScope)?;
|
|
if node.element != *element || visible.get(placement) != Some(element) {
|
|
return Err(Refusal::StaleScope);
|
|
}
|
|
if record.layer() != d.layer || !(record.kind().is_work() || record.kind().is_performer()) {
|
|
return Err(Refusal::Ineligible(
|
|
"Arrange requires only activities, functions or performers in this layer.".into(),
|
|
));
|
|
}
|
|
if record.kind().is_work() {
|
|
work.insert(*element, *placement);
|
|
} else {
|
|
performers.push(*placement);
|
|
}
|
|
}
|
|
|
|
let mut placements = BTreeMap::new();
|
|
for (element, placement) in &work {
|
|
let root = &p.elements[element];
|
|
let mut ancestor = root.parent;
|
|
let mut covered = false;
|
|
let mut seen = BTreeSet::new();
|
|
while let Some(id) = ancestor {
|
|
if !seen.insert(id) {
|
|
return Err(Refusal::StaleScope);
|
|
}
|
|
let parent = p.elements.get(&id).ok_or(Refusal::StaleScope)?;
|
|
if parent.kind() != root.kind() {
|
|
break;
|
|
}
|
|
if work.contains_key(&id) {
|
|
covered = true;
|
|
break;
|
|
}
|
|
ancestor = parent.parent;
|
|
}
|
|
if !covered {
|
|
placements.extend(capture_validated(p, diagram, *element, *placement)?.placements);
|
|
}
|
|
}
|
|
for performer in performers {
|
|
for id in
|
|
std::iter::once(performer).chain(projection::descendant_nodes(p, diagram, performer))
|
|
{
|
|
placements.insert(id, d.nodes.get(&id).ok_or(Refusal::StaleScope)?.clone());
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
selected: selected.clone(),
|
|
anchor,
|
|
placements,
|
|
expanded_ids,
|
|
index,
|
|
before_visible,
|
|
before_expanded,
|
|
})
|
|
}
|
|
|
|
/// 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(
|
|
plan: &Plan,
|
|
requested: Position,
|
|
presentation: PresentationContext,
|
|
) -> Result<PlacementPlan, Refusal> {
|
|
if presentation.target.is_some() {
|
|
return Err(Refusal::Ineligible(
|
|
"Arrange only has no performer destination.".into(),
|
|
));
|
|
}
|
|
if !requested.x.is_finite()
|
|
|| !requested.y.is_finite()
|
|
|| !presentation.viewport.valid()
|
|
|| !presentation.zoom.is_finite()
|
|
|| presentation.zoom <= 0.
|
|
{
|
|
return Err(Refusal::LayoutRequired(
|
|
"Make room or adjust the view, then drag again.".into(),
|
|
));
|
|
}
|
|
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 =
|
|
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,
|
|
delta,
|
|
unfolded: None,
|
|
requested,
|
|
presentation,
|
|
commands,
|
|
})
|
|
}
|
|
|
|
/// Recompute the candidate and compare the complete expected Project before the
|
|
/// caller performs one source-admitted transaction. None means no history entry.
|
|
pub fn finish(
|
|
plan: &Plan,
|
|
live: &Project,
|
|
placement: &PlacementPlan,
|
|
) -> Result<Option<Command>, Refusal> {
|
|
if live != &plan.accepted {
|
|
return Err(Refusal::StaleScope);
|
|
}
|
|
let checked = place(plan, placement.requested, placement.presentation)?;
|
|
if checked.diagram != placement.diagram
|
|
|| checked.view != placement.view
|
|
|| checked.delta != placement.delta
|
|
|| checked.unfolded != placement.unfolded
|
|
|| checked.commands != placement.commands
|
|
{
|
|
return Err(Refusal::StaleScope);
|
|
}
|
|
let mut expected = plan.accepted.clone();
|
|
expected
|
|
.diagrams
|
|
.insert(checked.diagram.id, checked.diagram);
|
|
let command = Command::Batch(checked.commands);
|
|
let actual = prepare_command(live, &command)
|
|
.map_err(|e| Refusal::InvalidCandidate(e.to_string()))?
|
|
.project;
|
|
exact_project(&expected, &actual)?;
|
|
Ok((actual != *live).then_some(command))
|
|
}
|