Prototype pure saved Scenario spacing command previews
This commit is contained in:
@@ -5,7 +5,10 @@ use super::*;
|
||||
use iced::widget::{Column, column, text_input};
|
||||
use std::{cell::RefCell, collections::BTreeSet};
|
||||
mod camera;
|
||||
// Pure spacing experiment; no production gesture or command dispatch is wired.
|
||||
pub(crate) mod focus;
|
||||
#[cfg(test)]
|
||||
mod spacing;
|
||||
use super::scenario_layout::{
|
||||
Representation,
|
||||
presentation::{self, ScenarioPresentation},
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
//! 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,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(super) enum Target {
|
||||
Lane(Id),
|
||||
Message(Id),
|
||||
}
|
||||
impl Target {
|
||||
fn id(self) -> Id {
|
||||
match self {
|
||||
Self::Lane(id) | Self::Message(id) => id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(super) enum Refusal {
|
||||
InvalidSavedView(Id),
|
||||
TargetNotRendered(Target),
|
||||
InvalidDelta,
|
||||
Command(String),
|
||||
Geometry(Vec<GeometryIssue>),
|
||||
}
|
||||
|
||||
pub(super) struct Drag {
|
||||
original: Project,
|
||||
diagram: Id,
|
||||
target: Target,
|
||||
origin: f32,
|
||||
}
|
||||
|
||||
pub(super) struct Preview {
|
||||
pub(super) command: Command,
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
project,
|
||||
geometry,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
use super::*;
|
||||
use crate::behavior::scenario_layout::Diagnostic;
|
||||
use crate::document::Document;
|
||||
use oa_core::{
|
||||
Diagram, DiagramLayout, ProjectionKind, ScenarioLayout, language, source_project::ProjectLimits,
|
||||
};
|
||||
|
||||
fn id(project: &Project, alias: &str) -> Id {
|
||||
project
|
||||
.identities()
|
||||
.into_iter()
|
||||
.find(|(_, name)| *name == alias)
|
||||
.unwrap()
|
||||
.0
|
||||
}
|
||||
|
||||
fn fixture(layout: Option<ScenarioLayout>) -> (Document, Id, Id) {
|
||||
let mut project = language::parse(FIXTURE).unwrap();
|
||||
let owner = id(&project, "conversation");
|
||||
let mut add = |alias: &str, layout: Option<ScenarioLayout>| {
|
||||
let diagram = Id::new_v4();
|
||||
project.diagrams.insert(
|
||||
diagram,
|
||||
Diagram {
|
||||
id: diagram,
|
||||
alias: alias.into(),
|
||||
name: alias.into(),
|
||||
description: String::new(),
|
||||
layer: oa_core::Layer::Oa,
|
||||
projection: ProjectionKind::Scenario { scenario: owner },
|
||||
nodes: Default::default(),
|
||||
routes: vec![],
|
||||
view_options: Default::default(),
|
||||
layout: layout.map(DiagramLayout::Scenario),
|
||||
},
|
||||
);
|
||||
diagram
|
||||
};
|
||||
let a = add("saved_a", layout);
|
||||
let b = add("saved_b", Some(ScenarioLayout::default()));
|
||||
let document = Document::from_project(project, ProjectLimits::default()).unwrap();
|
||||
(document, a, b)
|
||||
}
|
||||
|
||||
fn resolved(project: &Project, diagram: Id, selected: Id) -> ResolvedScenarioPlan {
|
||||
resolve(project, Representation::Saved { diagram }, selected).unwrap()
|
||||
}
|
||||
|
||||
fn same_geometry(a: &ResolvedScenarioPlan, b: &ResolvedScenarioPlan) {
|
||||
assert_eq!(a.bounds, b.bounds);
|
||||
assert_eq!(a.lanes, b.lanes);
|
||||
assert_eq!(a.plan.frames, b.plan.frames);
|
||||
assert_eq!(a.plan.scopes, b.plan.scopes);
|
||||
let (a, b) = (&a.scene, &b.scene);
|
||||
assert_eq!(a.rows, b.rows);
|
||||
assert_eq!(a.sequence_bounds, b.sequence_bounds);
|
||||
assert_eq!(a.size, b.size);
|
||||
assert_eq!(a.layout_origin, b.layout_origin);
|
||||
assert_eq!(a.lanes, b.lanes);
|
||||
assert_eq!(a.render_lanes, b.render_lanes);
|
||||
assert_eq!(a.activations, b.activations);
|
||||
assert_eq!(a.lifecycle, b.lifecycle);
|
||||
assert_eq!(a.open_calls, b.open_calls);
|
||||
let slots = |scene: &crate::behavior::Scene| {
|
||||
scene
|
||||
.slots
|
||||
.iter()
|
||||
.map(|item| {
|
||||
(
|
||||
item.slot.parent,
|
||||
item.slot.before,
|
||||
item.y,
|
||||
item.label.clone(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(slots(a), slots(b));
|
||||
let lines = |scene: &crate::behavior::Scene| {
|
||||
scene
|
||||
.lines
|
||||
.iter()
|
||||
.map(|line| {
|
||||
(
|
||||
line.id,
|
||||
line.points.clone(),
|
||||
line.label_at,
|
||||
line.label.clone(),
|
||||
line.dashed,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let boxes = |scene: &crate::behavior::Scene| {
|
||||
scene
|
||||
.boxes
|
||||
.iter()
|
||||
.map(|item| {
|
||||
(
|
||||
item.id,
|
||||
item.rect,
|
||||
item.title.clone(),
|
||||
item.detail.clone(),
|
||||
item.marker,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(lines(a), lines(b));
|
||||
assert_eq!(boxes(a), boxes(b));
|
||||
}
|
||||
|
||||
fn expected_message(project: &Project, diagram: Id, occurrence: Id, y: f32) -> Project {
|
||||
// Independent expected mutation: tests begin with no message overrides.
|
||||
let mut expected = project.clone();
|
||||
expected.diagrams.get_mut(&diagram).unwrap().layout =
|
||||
Some(DiagramLayout::Scenario(ScenarioLayout {
|
||||
lanes: vec![],
|
||||
messages: vec![ScenarioMessagePosition {
|
||||
address: OccurrenceEndpoint {
|
||||
references: vec![],
|
||||
occurrence,
|
||||
},
|
||||
send_y: y,
|
||||
}],
|
||||
}));
|
||||
expected
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_repeated_message_moves_in_one_view_with_exact_document_undo_redo_and_reopen() {
|
||||
let (mut document, a, b) = fixture(None);
|
||||
let original = document.snapshot();
|
||||
let before = document.project().clone();
|
||||
let first = id(&before, "first");
|
||||
let second = id(&before, "second");
|
||||
let baseline = resolved(&before, a, second);
|
||||
assert_eq!(baseline.scene.rows[&first].1, 148.);
|
||||
assert_eq!(baseline.scene.rows[&second].1, 240.);
|
||||
let untouched = resolved(&before, b, second);
|
||||
let drag = capture(&before, a, Target::Message(second)).unwrap();
|
||||
let preview = drag.prepare(-40.).unwrap().unwrap();
|
||||
let expected = expected_message(&before, a, second, 200.);
|
||||
assert_eq!(preview.project, expected);
|
||||
assert_eq!(preview.geometry.scene.rows[&first].1, 148.);
|
||||
assert_eq!(preview.geometry.scene.rows[&second].1, 200.);
|
||||
let line = preview
|
||||
.geometry
|
||||
.scene
|
||||
.lines
|
||||
.iter()
|
||||
.find(|line| line.id == second)
|
||||
.unwrap();
|
||||
assert_eq!(line.points.first().unwrap().y, 200.);
|
||||
assert_eq!(line.points.last().unwrap().y, 200.);
|
||||
assert_eq!(
|
||||
preview.geometry.scene.activations,
|
||||
baseline.scene.activations
|
||||
);
|
||||
assert_eq!(preview.geometry.scene.lifecycle, baseline.scene.lifecycle);
|
||||
same_geometry(&resolved(&preview.project, b, second), &untouched);
|
||||
assert_eq!(drag.original, before);
|
||||
assert!(std::sync::Arc::ptr_eq(&original, &document.snapshot()));
|
||||
assert!(!document.can_undo());
|
||||
|
||||
let returned_selection = document.execute(preview.command).unwrap();
|
||||
assert_eq!(
|
||||
returned_selection,
|
||||
Some(a),
|
||||
"controller must preserve the dragged semantic selection separately"
|
||||
);
|
||||
assert_eq!(document.project(), &expected);
|
||||
assert_eq!(document.revision(), 1);
|
||||
let accepted = document.bundle().clone();
|
||||
same_geometry(&resolved(document.project(), a, second), &preview.geometry);
|
||||
assert!(document.undo());
|
||||
assert_eq!(document.project(), &before);
|
||||
assert_eq!(document.bundle(), &original.bundle);
|
||||
assert!(!document.can_undo());
|
||||
assert!(document.redo());
|
||||
assert_eq!(document.project(), &expected);
|
||||
assert_eq!(document.bundle(), &accepted);
|
||||
let reopened = Document::from_bundle(accepted, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(reopened.project(), &expected);
|
||||
same_geometry(&resolved(reopened.project(), a, second), &preview.geometry);
|
||||
assert!(!reopened.dirty() && !reopened.can_undo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_lane_move_changes_actual_header_and_attached_endpoints_only_in_selected_view() {
|
||||
let (document, a, b) = fixture(None);
|
||||
let before = document.project();
|
||||
let right = id(before, "right");
|
||||
let first = id(before, "first");
|
||||
let other = resolved(before, b, right);
|
||||
let preview = capture(before, a, Target::Lane(right))
|
||||
.unwrap()
|
||||
.prepare(100.)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let mut expected = before.clone();
|
||||
expected.diagrams.get_mut(&a).unwrap().layout = Some(DiagramLayout::Scenario(ScenarioLayout {
|
||||
lanes: vec![ScenarioLanePosition {
|
||||
lifeline: right,
|
||||
x: 500.,
|
||||
}],
|
||||
messages: vec![],
|
||||
}));
|
||||
assert_eq!(preview.project, expected);
|
||||
assert_eq!(preview.geometry.scene.lanes[&right], 500.);
|
||||
let header = preview
|
||||
.geometry
|
||||
.scene
|
||||
.boxes
|
||||
.iter()
|
||||
.find(|item| item.id == right)
|
||||
.unwrap();
|
||||
assert_eq!(header.rect.center_x(), 500.);
|
||||
let line = preview
|
||||
.geometry
|
||||
.scene
|
||||
.lines
|
||||
.iter()
|
||||
.find(|line| line.id == first)
|
||||
.unwrap();
|
||||
assert_eq!(line.points.first().unwrap().x, 160.);
|
||||
assert_eq!(line.points.last().unwrap().x, 500.);
|
||||
assert!(line.points.iter().all(|point| point.y == 148.));
|
||||
same_geometry(&resolved(&preview.project, b, right), &other);
|
||||
assert_eq!(document.project(), before);
|
||||
assert!(!document.dirty() && !document.can_undo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drawn_unassigned_lifeline_can_move_without_assigning_a_semantic_participant() {
|
||||
let (document, a, _) = fixture(None);
|
||||
let mut project = document.project().clone();
|
||||
let owner = id(&project, "conversation");
|
||||
let lane = project
|
||||
.add_element(
|
||||
Kind::Lifeline,
|
||||
oa_core::Layer::Oa,
|
||||
"Unassigned header",
|
||||
Some(owner),
|
||||
)
|
||||
.unwrap();
|
||||
let document = Document::from_project(project, ProjectLimits::default()).unwrap();
|
||||
let baseline = resolved(document.project(), a, lane);
|
||||
assert!(!baseline.scene.lanes.contains_key(&lane));
|
||||
let before_header = baseline
|
||||
.scene
|
||||
.boxes
|
||||
.iter()
|
||||
.find(|item| item.id == lane)
|
||||
.unwrap()
|
||||
.rect;
|
||||
let preview = capture(document.project(), a, Target::Lane(lane))
|
||||
.unwrap()
|
||||
.prepare(70.)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let after_header = preview
|
||||
.geometry
|
||||
.scene
|
||||
.boxes
|
||||
.iter()
|
||||
.find(|item| item.id == lane)
|
||||
.unwrap()
|
||||
.rect;
|
||||
assert_eq!(after_header.x, before_header.x + 70.);
|
||||
assert_eq!(after_header.y, before_header.y);
|
||||
assert_eq!(after_header.size(), before_header.size());
|
||||
assert_eq!(preview.project.elements, document.project().elements);
|
||||
assert_eq!(preview.project.relations, document.project().relations);
|
||||
assert!(!document.can_undo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_call_spacing_uses_send_y_and_preserves_reply_and_actual_activation_finish() {
|
||||
let (document, a, _) = fixture(None);
|
||||
let before = document.project();
|
||||
let call = id(before, "call");
|
||||
let reply = id(before, "reply");
|
||||
let baseline = resolved(before, a, call);
|
||||
let preview = capture(before, a, Target::Message(call))
|
||||
.unwrap()
|
||||
.prepare(-22.)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(preview.project, expected_message(before, a, call, 310.));
|
||||
let line = preview
|
||||
.geometry
|
||||
.scene
|
||||
.lines
|
||||
.iter()
|
||||
.find(|line| line.id == call)
|
||||
.unwrap();
|
||||
assert_eq!(line.points.first().unwrap().y, 310.);
|
||||
assert_eq!(line.points.last().unwrap().y, 334.);
|
||||
assert_eq!(preview.geometry.scene.rows[&reply].1, 424.);
|
||||
assert_eq!(preview.geometry.scene.activations.len(), 1);
|
||||
let mut expected_activation = baseline.scene.activations.clone();
|
||||
expected_activation[0].rect.y = 334.;
|
||||
expected_activation[0].rect.height = 90.;
|
||||
assert_eq!(preview.geometry.scene.activations, expected_activation);
|
||||
assert_eq!(preview.geometry.scene.lifecycle, baseline.scene.lifecycle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feasible_spacing_crossing_remains_authored_with_diagnostic_and_unchanged_steps() {
|
||||
let (document, a, _) = fixture(None);
|
||||
let before = document.project();
|
||||
let first = id(before, "first");
|
||||
let second = id(before, "second");
|
||||
let owner = id(before, "conversation");
|
||||
let preview = capture(before, a, Target::Message(second))
|
||||
.unwrap()
|
||||
.prepare(-120.)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(preview.project, expected_message(before, a, second, 120.));
|
||||
assert_eq!(preview.geometry.scene.rows[&second].1, 120.);
|
||||
assert_eq!(preview.geometry.scene.rows[&first].1, 148.);
|
||||
assert_eq!(
|
||||
preview.project.elements[&owner].steps(),
|
||||
before.elements[&owner].steps()
|
||||
);
|
||||
let shown = crate::behavior::scenario_layout::presentation::present(
|
||||
&preview.project,
|
||||
Representation::Saved { diagram: a },
|
||||
second,
|
||||
);
|
||||
assert!(shown.capabilities().coordinate_input);
|
||||
assert!(
|
||||
shown
|
||||
.diagnostics()
|
||||
.contains(&Diagnostic::SourceOrderCrossing {
|
||||
scope: owner,
|
||||
before: first,
|
||||
after: second
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_self_call_move_is_refused_and_automatic_fallback_is_not_editable_capture() {
|
||||
let (document, a, _) = fixture(None);
|
||||
let before = document.project();
|
||||
let call = id(before, "call");
|
||||
let drag = capture(before, a, Target::Message(call)).unwrap();
|
||||
let Err(Refusal::Geometry(issues)) = drag.prepare(118.) else {
|
||||
panic!("send450 / receive474 after reply424 must be refused")
|
||||
};
|
||||
assert!(issues.iter().any(|issue| matches!(issue, GeometryIssue::TypedActivationCrossingNotSupported { call: id, .. } if *id == call)));
|
||||
assert_eq!(&drag.original, before);
|
||||
let invalid_geometry = expected_message(before, a, call, 450.);
|
||||
let shown = crate::behavior::scenario_layout::presentation::present(
|
||||
&invalid_geometry,
|
||||
Representation::Saved { diagram: a },
|
||||
call,
|
||||
);
|
||||
assert!(shown.capabilities().geometry_navigation && !shown.capabilities().coordinate_input);
|
||||
assert!(matches!(
|
||||
capture(&invalid_geometry, a, Target::Message(call)),
|
||||
Err(Refusal::Geometry(_))
|
||||
));
|
||||
assert!(!document.dirty() && !document.can_undo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_view_foreign_identity_and_latent_message_are_not_spacing_targets() {
|
||||
let (document, a, b) = fixture(None);
|
||||
let p = document.project();
|
||||
let unfinished = id(p, "unfinished");
|
||||
let source = id(p, "source");
|
||||
assert!(matches!(
|
||||
capture(p, Id::nil(), Target::Lane(source)),
|
||||
Err(Refusal::InvalidSavedView(_))
|
||||
));
|
||||
for target in [
|
||||
Target::Lane(source),
|
||||
Target::Lane(b),
|
||||
Target::Message(source),
|
||||
Target::Message(unfinished),
|
||||
] {
|
||||
assert!(
|
||||
matches!(capture(p, a, target), Err(Refusal::TargetNotRendered(found)) if found == target)
|
||||
);
|
||||
}
|
||||
// A stored latent coordinate is not permission to fabricate a painted arrow.
|
||||
let mut latent = expected_message(p, a, unfinished, 2000.);
|
||||
let foreign_owner = latent
|
||||
.add_element(Kind::Scenario, oa_core::Layer::Oa, "Foreign", None)
|
||||
.unwrap();
|
||||
let foreign_lane = latent
|
||||
.add_element(
|
||||
Kind::Lifeline,
|
||||
oa_core::Layer::Oa,
|
||||
"Foreign lane",
|
||||
Some(foreign_owner),
|
||||
)
|
||||
.unwrap();
|
||||
*latent
|
||||
.elements
|
||||
.get_mut(&foreign_lane)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = SharedBody::Lifeline {
|
||||
represented: Some(source),
|
||||
};
|
||||
let latent_document = Document::from_project(latent, ProjectLimits::default()).unwrap();
|
||||
assert!(matches!(
|
||||
capture(latent_document.project(), a, Target::Message(unfinished)),
|
||||
Err(Refusal::TargetNotRendered(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
capture(latent_document.project(), a, Target::Lane(foreign_lane)),
|
||||
Err(Refusal::TargetNotRendered(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_motion_return_to_start_and_subprecision_preserve_sparse_intent_and_real_redo() {
|
||||
let parsed = language::parse(FIXTURE).unwrap();
|
||||
let left = id(&parsed, "left");
|
||||
let second = id(&parsed, "second");
|
||||
let layouts = [
|
||||
None,
|
||||
Some(ScenarioLayout::default()),
|
||||
Some(ScenarioLayout {
|
||||
lanes: vec![],
|
||||
messages: vec![ScenarioMessagePosition {
|
||||
address: OccurrenceEndpoint {
|
||||
references: vec![],
|
||||
occurrence: second,
|
||||
},
|
||||
send_y: 240.,
|
||||
}],
|
||||
}),
|
||||
Some(ScenarioLayout {
|
||||
lanes: vec![ScenarioLanePosition {
|
||||
lifeline: left,
|
||||
x: -0.,
|
||||
}],
|
||||
messages: vec![],
|
||||
}),
|
||||
];
|
||||
for (index, layout) in layouts.into_iter().enumerate() {
|
||||
let (mut document, a, _) = fixture(layout);
|
||||
let owner = id(document.project(), "conversation");
|
||||
document
|
||||
.execute(Command::Rename(owner, "A real redo entry".into()))
|
||||
.unwrap();
|
||||
let redo = document.snapshot();
|
||||
assert!(document.undo());
|
||||
let before = document.snapshot();
|
||||
let revision = document.revision();
|
||||
let target = if index == 3 {
|
||||
Target::Lane(left)
|
||||
} else {
|
||||
Target::Message(second)
|
||||
};
|
||||
let drag = capture(document.project(), a, target).unwrap();
|
||||
if index == 3 {
|
||||
let Some(DiagramLayout::Scenario(layout)) = &drag.original.diagrams[&a].layout else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(layout.lanes[0].x.to_bits(), (-0_f32).to_bits());
|
||||
}
|
||||
assert!(drag.prepare(10.).unwrap().is_some());
|
||||
for delta in [0., -0.] {
|
||||
assert!(drag.prepare(delta).unwrap().is_none());
|
||||
}
|
||||
if index != 3 {
|
||||
assert!(drag.prepare(f32::from_bits(1)).unwrap().is_none());
|
||||
}
|
||||
assert_eq!(
|
||||
serde_json::to_vec(&drag.original).unwrap(),
|
||||
serde_json::to_vec(document.project()).unwrap()
|
||||
);
|
||||
assert!(std::sync::Arc::ptr_eq(&before, &document.snapshot()));
|
||||
assert_eq!(document.bundle(), &before.bundle);
|
||||
assert_eq!(document.revision(), revision);
|
||||
assert!(document.can_redo());
|
||||
assert!(document.redo());
|
||||
assert_eq!(document.project(), &redo.decoded.project);
|
||||
assert_eq!(document.bundle(), &redo.bundle);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_or_out_of_range_motion_cannot_mutate_original() {
|
||||
let (document, a, _) = fixture(None);
|
||||
let drag = capture(
|
||||
document.project(),
|
||||
a,
|
||||
Target::Message(id(document.project(), "second")),
|
||||
)
|
||||
.unwrap();
|
||||
for delta in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
|
||||
assert!(matches!(drag.prepare(delta), Err(Refusal::InvalidDelta)));
|
||||
}
|
||||
assert!(matches!(drag.prepare(f32::MAX), Err(Refusal::Command(_))));
|
||||
assert_eq!(&drag.original, document.project());
|
||||
assert!(!document.dirty() && !document.can_undo());
|
||||
}
|
||||
|
||||
const FIXTURE: &str = r#"arcadia 7;
|
||||
project spacing_probe "Saved spacing probe" id "71000000-0000-4000-8000-000000000001" {
|
||||
oa_activity source "Source" id "71000000-0000-4000-8000-000000000002" {}
|
||||
oa_activity target "Target" id "71000000-0000-4000-8000-000000000003" {}
|
||||
interaction exchange "Repeated exchange" id "71000000-0000-4000-8000-000000000004" { source @source; target @target; }
|
||||
oa_scenario conversation "Conversation" id "71000000-0000-4000-8000-000000000005" { viewpoint OperationalActivity; steps [@first, @second, @call, @reply, @unfinished]; }
|
||||
oa_lifeline left "Left" id "71000000-0000-4000-8000-000000000006" { parent @conversation; represented @source; }
|
||||
oa_lifeline right "Right" id "71000000-0000-4000-8000-000000000007" { parent @conversation; represented @target; }
|
||||
oa_occurrence first "First" id "71000000-0000-4000-8000-000000000008" { parent @conversation; body { Exchange { exchange @exchange; sender @left; receiver @right; message_kind Asynchronous; }; }; }
|
||||
oa_occurrence second "Second" id "71000000-0000-4000-8000-000000000009" { parent @conversation; body { Exchange { exchange @exchange; sender @left; receiver @right; message_kind Asynchronous; }; }; }
|
||||
oa_occurrence call "Call" id "71000000-0000-4000-8000-00000000000a" { parent @conversation; body { Exchange { exchange none; sender @right; receiver @right; message_kind Call; }; }; }
|
||||
oa_occurrence reply "Reply" id "71000000-0000-4000-8000-00000000000b" { parent @conversation; body { Exchange { exchange none; sender @right; receiver @right; message_kind Reply; reply_to @call; }; }; }
|
||||
oa_occurrence unfinished "Unfinished" id "71000000-0000-4000-8000-00000000000c" { parent @conversation; body { Exchange { exchange @exchange; sender @left; receiver none; message_kind Asynchronous; }; }; }
|
||||
}
|
||||
"#;
|
||||
Reference in New Issue
Block a user