387 lines
11 KiB
Rust
387 lines
11 KiB
Rust
//! Actual Scene primitives, with independently chosen points and semantic IDs.
|
|
//! These preserve the existing label hit band; they do not establish ink bounds
|
|
//! or authoring eligibility, and do not dispatch a spacing gesture.
|
|
use super::*;
|
|
use crate::document::Document;
|
|
|
|
struct Fixture {
|
|
project: Project,
|
|
owner: Id,
|
|
sender: Id,
|
|
receiver: Id,
|
|
call: Id,
|
|
reply: Id,
|
|
exchange: Id,
|
|
}
|
|
|
|
fn fixture() -> Fixture {
|
|
let project = language::parse(r#"arcadia 7;
|
|
project hits "Hit roles" {
|
|
oa_activity source "Source" {}
|
|
oa_activity target "Target" {}
|
|
interaction exchange "Contract" { source @source; target @target; }
|
|
oa_scenario conversation "Conversation" { viewpoint OperationalActivity; steps [@call, @reply]; }
|
|
oa_lifeline left "Left" { parent @conversation; represented @source; }
|
|
oa_lifeline right "Right" { parent @conversation; represented @target; }
|
|
oa_occurrence call "Call" { parent @conversation; body { Exchange { exchange @exchange; sender @left; receiver @right; message_kind Call; }; }; }
|
|
oa_occurrence reply "Reply" { parent @conversation; body { Exchange { exchange @exchange; sender @right; receiver @left; message_kind Reply; reply_to @call; }; }; }
|
|
}
|
|
"#).unwrap();
|
|
let id = |alias: &str| {
|
|
project
|
|
.identities()
|
|
.into_iter()
|
|
.find(|(_, name)| *name == alias)
|
|
.unwrap()
|
|
.0
|
|
};
|
|
Fixture {
|
|
owner: id("conversation"),
|
|
sender: id("left"),
|
|
receiver: id("right"),
|
|
call: id("call"),
|
|
reply: id("reply"),
|
|
exchange: id("exchange"),
|
|
project,
|
|
}
|
|
}
|
|
|
|
fn scene(f: &Fixture) -> Scene {
|
|
let document =
|
|
Document::from_project(f.project.clone(), source_project::ProjectLimits::default())
|
|
.unwrap();
|
|
let before = document.snapshot();
|
|
let result = Scene::sequence_geometry(
|
|
document.project(),
|
|
f.owner,
|
|
f.call,
|
|
Some(&BTreeMap::from([(f.sender, 160.), (f.receiver, 400.)])),
|
|
None,
|
|
);
|
|
assert_eq!(document.project(), &before.decoded.project);
|
|
assert_eq!(document.bundle(), &before.bundle);
|
|
assert_eq!(document.revision(), 0);
|
|
assert!(!document.can_undo() && !document.can_redo() && !document.dirty());
|
|
result
|
|
}
|
|
|
|
fn hit(scene: &Scene, point: Point, id: Id, role: SceneHitRole) {
|
|
assert_eq!(scene.hit_item(point), Some(SceneHit { id, role }));
|
|
assert_eq!(scene.hit(point), Some(id));
|
|
}
|
|
|
|
fn occurrence(p: &mut Project, parent: Id, body: OccurrenceBody) -> Id {
|
|
let mut element = p
|
|
.make_element(Kind::Occurrence, Layer::Oa, "Hit target", Some(parent))
|
|
.unwrap();
|
|
let id = element.id;
|
|
*element.shared_mut().unwrap() = SharedBody::Occurrence {
|
|
body,
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
};
|
|
p.elements.insert(id, element);
|
|
p.elements
|
|
.get_mut(&parent)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.push(id);
|
|
id
|
|
}
|
|
|
|
fn exchange_mut(p: &mut Project, id: Id) -> &mut OccurrenceBody {
|
|
let SharedBody::Occurrence { body, .. } =
|
|
p.elements.get_mut(&id).unwrap().shared_mut().unwrap()
|
|
else {
|
|
panic!("expected occurrence")
|
|
};
|
|
body
|
|
}
|
|
|
|
#[test]
|
|
fn exact_headers_include_unassigned_lifelines_but_guides_are_not_headers() {
|
|
let mut f = fixture();
|
|
*f.project
|
|
.elements
|
|
.get_mut(&f.receiver)
|
|
.unwrap()
|
|
.shared_mut()
|
|
.unwrap() = SharedBody::Lifeline { represented: None };
|
|
let s = scene(&f);
|
|
assert!(!s.lanes.contains_key(&f.receiver));
|
|
assert_eq!(s.render_lanes[&f.receiver], 400.);
|
|
for (id, x) in [(f.sender, 160.), (f.receiver, 400.)] {
|
|
// The actual header is x-102..x+102, y15..85, including its padding.
|
|
for point in [Point::new(x - 101., 16.), Point::new(x + 101., 84.)] {
|
|
hit(&s, point, id, SceneHitRole::LifelineHeader);
|
|
}
|
|
hit(&s, Point::new(x, 95.), id, SceneHitRole::LifelineGuide);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn repeated_exchange_occurrences_keep_distinct_message_body_and_label_hits() {
|
|
let f = fixture();
|
|
let s = scene(&f);
|
|
assert_eq!(s.rows[&f.call].1, 148.);
|
|
assert_eq!(s.rows[&f.reply].1, 240.);
|
|
for (id, y) in [(f.call, 148.), (f.reply, 240.)] {
|
|
hit(&s, Point::new(280., y), id, SceneHitRole::MessageBody);
|
|
hit(
|
|
&s,
|
|
Point::new(180., y - 16.),
|
|
id,
|
|
SceneHitRole::MessageLabel,
|
|
);
|
|
assert_ne!(id, f.exchange);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn self_call_elbow_and_receive_endpoint_are_messages_not_execution_hits() {
|
|
let mut f = fixture();
|
|
for id in [f.call, f.reply] {
|
|
let OccurrenceBody::Exchange {
|
|
sender,
|
|
receiver,
|
|
exchange,
|
|
..
|
|
} = exchange_mut(&mut f.project, id)
|
|
else {
|
|
unreachable!()
|
|
};
|
|
*sender = Some(f.sender);
|
|
*receiver = Some(f.sender);
|
|
*exchange = None;
|
|
}
|
|
let s = scene(&f);
|
|
let call = s.lines.iter().find(|l| l.id == f.call).unwrap();
|
|
assert_eq!(call.points.len(), 4);
|
|
assert_eq!(call.points[0].y, 148.);
|
|
assert_eq!(call.points[3].y, 172.);
|
|
hit(
|
|
&s,
|
|
Point::new(call.points[1].x, 160.),
|
|
f.call,
|
|
SceneHitRole::MessageBody,
|
|
);
|
|
hit(&s, call.points[3], f.call, SceneHitRole::MessageBody);
|
|
hit(
|
|
&s,
|
|
Point::new(180., 132.),
|
|
f.call,
|
|
SceneHitRole::MessageLabel,
|
|
);
|
|
let bar = s.activations.iter().find(|a| a.call == f.call).unwrap();
|
|
hit(
|
|
&s,
|
|
Point::new(bar.rect.x + 2., 190.),
|
|
f.call,
|
|
SceneHitRole::Activation,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn activation_shoulder_beats_another_calls_label_and_broad_message_tolerance() {
|
|
let mut f = fixture();
|
|
let mut inner = f.project.elements[&f.call].clone();
|
|
inner.id = Id::new_v4();
|
|
inner.alias = "inner_call_hit".into();
|
|
inner.name = "Pressure verification with a descriptive operation label".into();
|
|
let inner_id = inner.id;
|
|
f.project.elements.insert(inner_id, inner);
|
|
let mut inner_reply = f.project.elements[&f.reply].clone();
|
|
inner_reply.id = Id::new_v4();
|
|
inner_reply.alias = "inner_reply_hit".into();
|
|
let reply_id = inner_reply.id;
|
|
f.project.elements.insert(reply_id, inner_reply);
|
|
let OccurrenceBody::Exchange { reply_to, .. } = exchange_mut(&mut f.project, reply_id) else {
|
|
unreachable!()
|
|
};
|
|
*reply_to = Some(inner_id);
|
|
*f.project
|
|
.elements
|
|
.get_mut(&f.owner)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap() = vec![f.call, inner_id, reply_id, f.reply];
|
|
let mut s = scene(&f);
|
|
let inner = s.lines.iter().find(|l| l.id == inner_id).unwrap();
|
|
let outer = s.activations.iter().find(|a| a.call == f.call).unwrap();
|
|
let shoulder = Point::new(outer.rect.x + 1., 225.);
|
|
assert!(outer.rect.contains(shoulder));
|
|
assert!(inner.label_bounds().contains(shoulder));
|
|
let endpoint = *inner.points.last().unwrap();
|
|
for scale in [1., 0.05, 0.0003] {
|
|
s.scale = scale;
|
|
hit(&s, shoulder, f.call, SceneHitRole::Activation);
|
|
hit(&s, endpoint, inner_id, SceneHitRole::MessageBody);
|
|
hit(
|
|
&s,
|
|
Point::new(180., 225.),
|
|
inner_id,
|
|
SceneHitRole::MessageLabel,
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn create_delete_markers_are_distinct_from_their_own_message_body_and_label() {
|
|
let mut f = fixture();
|
|
for (id, kind) in [
|
|
(f.call, MessageKind::Create),
|
|
(f.reply, MessageKind::Delete),
|
|
] {
|
|
let OccurrenceBody::Exchange {
|
|
sender,
|
|
receiver,
|
|
message_kind,
|
|
reply_to,
|
|
..
|
|
} = exchange_mut(&mut f.project, id)
|
|
else {
|
|
unreachable!()
|
|
};
|
|
*sender = Some(f.sender);
|
|
*receiver = Some(f.receiver);
|
|
*message_kind = kind;
|
|
*reply_to = None;
|
|
}
|
|
let s = scene(&f);
|
|
for (id, y, label_y) in [(f.call, 148., 118.), (f.reply, 240., 224.)] {
|
|
hit(&s, Point::new(400., y), id, SceneHitRole::LifecycleMarker);
|
|
hit(&s, Point::new(280., y), id, SceneHitRole::MessageBody);
|
|
hit(
|
|
&s,
|
|
Point::new(180., label_y),
|
|
id,
|
|
SceneHitRole::MessageLabel,
|
|
);
|
|
}
|
|
// Capsule boundary owns its contact with the incoming Create arrow.
|
|
hit(
|
|
&s,
|
|
Point::new(330., 148.),
|
|
f.call,
|
|
SceneHitRole::LifecycleMarker,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn tiny_scale_nearest_hit_retains_the_winning_marker_or_message_role() {
|
|
let mut f = fixture();
|
|
let OccurrenceBody::Exchange {
|
|
sender,
|
|
receiver,
|
|
message_kind,
|
|
reply_to,
|
|
..
|
|
} = exchange_mut(&mut f.project, f.reply)
|
|
else {
|
|
unreachable!()
|
|
};
|
|
*sender = Some(f.sender);
|
|
*receiver = Some(f.receiver);
|
|
*message_kind = MessageKind::Delete;
|
|
*reply_to = None;
|
|
let mut s = scene(&f);
|
|
s.scale = 0.0003;
|
|
// Both are outside exact strokes/labels. Comparing all broad candidates
|
|
// chooses the actual nearer shape, even when their UUID is identical.
|
|
hit(
|
|
&s,
|
|
Point::new(280., 246.),
|
|
f.reply,
|
|
SceneHitRole::MessageBody,
|
|
);
|
|
hit(
|
|
&s,
|
|
Point::new(400., 265.),
|
|
f.reply,
|
|
SceneHitRole::LifecycleMarker,
|
|
);
|
|
hit(
|
|
&s,
|
|
Point::new(280., 154.),
|
|
f.call,
|
|
SceneHitRole::MessageBody,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn action_frame_and_use_boxes_do_not_advertise_message_or_header_roles() {
|
|
for body in [
|
|
OccurrenceBody::Action {
|
|
action: None,
|
|
lifeline: None,
|
|
},
|
|
OccurrenceBody::Fragment {
|
|
operator: FragmentOperator::Alternative,
|
|
branches: vec![],
|
|
},
|
|
OccurrenceBody::ScenarioUse {
|
|
scenario: None,
|
|
bindings: vec![],
|
|
},
|
|
OccurrenceBody::Exchange {
|
|
exchange: None,
|
|
sender: None,
|
|
receiver: None,
|
|
message_kind: MessageKind::Asynchronous,
|
|
operation: None,
|
|
reply_to: None,
|
|
values: vec![],
|
|
},
|
|
] {
|
|
let mut f = fixture();
|
|
let id = occurrence(&mut f.project, f.owner, body);
|
|
let s = scene(&f);
|
|
let item = s.boxes.iter().find(|b| b.id == id).unwrap();
|
|
hit(
|
|
&s,
|
|
item.rect.position() + iced::Vector::new(15., 15.),
|
|
id,
|
|
SceneHitRole::Box,
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn winning_primitive_role_does_not_come_from_its_shared_uuid() {
|
|
let f = fixture();
|
|
let mut s = scene(&f);
|
|
// A controlled overlapping primitive verifies reverse box order without
|
|
// inventing a core alias. Its ID intentionally equals a real header's ID.
|
|
s.boxes.push(BoxItem {
|
|
id: f.sender,
|
|
role: SceneHitRole::Box,
|
|
rect: Rectangle::new(Point::new(150., 20.), Size::new(20., 20.)),
|
|
title: "Overlay control".into(),
|
|
detail: String::new(),
|
|
marker: None,
|
|
});
|
|
hit(&s, Point::new(160., 30.), f.sender, SceneHitRole::Box);
|
|
hit(
|
|
&s,
|
|
Point::new(180., 30.),
|
|
f.sender,
|
|
SceneHitRole::LifelineHeader,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_points_never_produce_a_semantic_or_spacing_hit() {
|
|
let f = fixture();
|
|
let s = scene(&f);
|
|
for point in [
|
|
Point::new(f32::NAN, 148.),
|
|
Point::new(280., f32::NAN),
|
|
Point::new(f32::INFINITY, 148.),
|
|
Point::new(280., f32::NEG_INFINITY),
|
|
] {
|
|
assert_eq!(s.hit_item(point), None);
|
|
assert_eq!(s.hit(point), None);
|
|
}
|
|
}
|