//! 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, anchor: Id, placements: BTreeMap, expanded_ids: BTreeSet, 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 { &self.placements } pub fn selected(&self) -> &BTreeMap { &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, anchor: Id, ) -> Result { 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 { 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, 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)) }