161 lines
5.3 KiB
Rust
161 lines
5.3 KiB
Rust
//! 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<GeometryIssue>),
|
|
}
|
|
|
|
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<Drag, Refusal> {
|
|
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<Option<Preview>, 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,
|
|
}))
|
|
}
|
|
}
|