//! Pure Saved-only spacing preparation. Delta is measured from the original //! pointer in the captured camera's world coordinates, never from the last //! preview. The controller must own document/revision/visit, camera and hit-role //! admission; this module neither dispatches commands nor changes selection. use crate::behavior::scenario_layout::{ GeometryIssue, Representation, ResolvedScenarioPlan, presentation::checked_owner, resolve, }; use oa_core::{ Command, Id, Kind, OccurrenceBody, OccurrenceEndpoint, Project, ScenarioLanePosition, ScenarioMessagePosition, SharedBody, execute_batch, }; pub(crate) mod controller; #[cfg(test)] mod tests; pub(crate) use controller::{Event, Stamp}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Target { Lane(Id), Message(Id), } impl Target { pub(crate) fn id(self) -> Id { match self { Self::Lane(id) | Self::Message(id) => id, } } } #[derive(Debug, PartialEq, Eq)] pub(crate) enum Refusal { InvalidSavedView(Id), TargetNotRendered(Target), InvalidDelta, InvalidGesture, CameraChanged, TargetNotHit(Target), Command(String), Geometry(Vec), } pub(super) struct Drag { original: Project, diagram: Id, target: Target, origin: f32, original_geometry: ResolvedScenarioPlan, } pub(super) struct Preview { pub(super) command: Command, #[cfg(test)] pub(super) project: Project, pub(super) geometry: ResolvedScenarioPlan, } /// Capture an accepted, actually drawn target. A Saved view displaying automatic /// fallback is not an editable original, even if the target appears in fallback. pub(super) fn capture(project: &Project, diagram: Id, target: Target) -> Result { let representation = Representation::Saved { diagram }; let owner = checked_owner(project, representation).ok_or(Refusal::InvalidSavedView(diagram))?; let geometry = resolve(project, representation, target.id()).map_err(Refusal::Geometry)?; let element = project.elements.get(&target.id()); let origin = match target { Target::Lane(id) if element.is_some_and(|e| e.kind() == Kind::Lifeline && e.parent == Some(owner)) => { // Unassigned lifelines still have drawn headers. `scene.lanes` // excludes them only from semantic message-endpoint hit testing. geometry.scene.render_lanes.get(&id).copied() } Target::Message(id) if element.is_some_and(|e| { matches!( e.shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { .. }, .. }) ) }) => { // A lifecycle row can denote receive y; the public position means // send y. Read the actual outgoing endpoint, including self calls. geometry .scene .lines .iter() .find(|line| line.id == id) .and_then(|line| line.points.first()) .map(|point| point.y) } _ => None, } .ok_or(Refusal::TargetNotRendered(target))?; if !origin.is_finite() { return Err(Refusal::TargetNotRendered(target)); } Ok(Drag { original: project.clone(), diagram, target, origin, original_geometry: geometry, }) } impl Drag { /// None means there is no command to commit. In particular +0/-0, return to /// start, and sub-precision motion never materialize a missing override or /// normalize an existing explicit value. Document/redo are never touched. pub(super) fn prepare(&self, original_delta: f32) -> Result, Refusal> { if !original_delta.is_finite() { return Err(Refusal::InvalidDelta); } if original_delta == 0. { return Ok(None); } let position = (f64::from(self.origin) + f64::from(original_delta)) as f32; if position == self.origin { return Ok(None); } let command = match self.target { Target::Lane(lifeline) => Command::SetScenarioLanePosition { diagram: self.diagram, position: ScenarioLanePosition { lifeline, x: position, }, }, Target::Message(occurrence) => Command::SetScenarioMessagePosition { diagram: self.diagram, position: ScenarioMessagePosition { address: OccurrenceEndpoint { references: vec![], occurrence, }, send_y: position, }, }, }; let project = execute_batch(&self.original, std::slice::from_ref(&command)) .map_err(|error| Refusal::Command(error.to_string()))?; let geometry = resolve( &project, Representation::Saved { diagram: self.diagram, }, self.target.id(), ) .map_err(Refusal::Geometry)?; Ok(Some(Preview { command, #[cfg(test)] project, geometry, })) } }