//! 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); } }