diff --git a/crates/oa-desktop/src/behavior/scenario/spacing/controller/tests.rs b/crates/oa-desktop/src/behavior/scenario/spacing/controller/tests.rs index 87d5f13e..1344207d 100644 --- a/crates/oa-desktop/src/behavior/scenario/spacing/controller/tests.rs +++ b/crates/oa-desktop/src/behavior/scenario/spacing/controller/tests.rs @@ -328,3 +328,71 @@ fn zoomed_out_message_tolerance_matches_actual_canvas_hit() { state.begin_spacing(doc.project(), event).unwrap(); assert_eq!(state.spacing_token(), Some(token)); } + +#[test] +fn fractional_screen_packet_preserves_subpixel_motion_in_preview_and_finish() { + let (doc, owner, diagram, _, message) = fixture(); + let original = doc.snapshot(); + assert!(doc.project().diagrams[&diagram].layout.is_none()); + let mut state = State::default(); + let document = Id::new_v4(); + state.enter_representation(document, owner, Representation::Saved { diagram }); + state.presentation.viewport = Some(Size::new(900., 600.)); + state.presentation.fit = false; + state.presentation.zoom = Some(0.5); + // 406 + 2^-15 puts the press just below the 512 screen-coordinate boundary. + state.presentation.pan = Vector::new(0., f32::from_bits(0x43cb_0001)); + let camera = state.camera_boundary(); + let context = state.context(document, owner, doc.revision(), 4).unwrap(); + let baseline = resolve(doc.project(), context.representation, message).unwrap(); + let line = baseline.scene.lines.iter().find(|line| line.id == message).unwrap(); + assert_eq!(line.points[0].y, 148.); + let begin = packet(doc.project(), &state, context, 9, Target::Message(message)); + let Event::Begin { press_screen, canvas_bounds, scale, pan, .. } = begin else { + unreachable!() + }; + assert_eq!(canvas_bounds.y, 23.); + assert_eq!(scale, 0.5); + assert_eq!(pan.y.to_bits(), 0x43cb_0001); + assert_eq!(press_screen.y.to_bits(), 0x43fb_8001); // 503 + 2^-15. + let end = press_screen + Vector::new(0., 10.); + assert_eq!(end.y, 513.); + assert!(canvas_bounds.contains(press_screen) && canvas_bounds.contains(end)); + // The submitted f32 displacement is 10 - 2^-15, not exactly 10. + assert_eq!((end.y - press_screen.y).to_bits(), 0x411f_ffe0); + assert_eq!(((end.y - press_screen.y) / scale).to_bits(), 0x419f_ffe0); + // Independent oracle: 148 + (10 - 2^-15) / .5 = 168 - 2^-14. + let expected_y = f32::from_bits(0x4327_fffc); + let mut expected = original.decoded.project.clone(); + expected.diagrams.get_mut(&diagram).unwrap().layout = Some( + oa_core::DiagramLayout::Scenario(oa_core::ScenarioLayout { + lanes: vec![], + messages: vec![oa_core::ScenarioMessagePosition { + address: oa_core::OccurrenceEndpoint { + references: vec![], + occurrence: message, + }, + send_y: expected_y, + }], + }), + ); + state.begin_spacing(doc.project(), begin).unwrap(); + state.move_spacing(end); + let preview = state.spacing.as_ref().unwrap().preview.as_ref().unwrap(); + assert_eq!(preview.project, expected); + let line = preview.geometry.scene.lines.iter().find(|line| line.id == message).unwrap(); + assert_eq!(line.points[0].y.to_bits(), expected_y.to_bits()); + let Finish::Commit { command, selected, scale, pan } = state.finish_spacing(end).unwrap() else { + panic!("fractional motion must remain a commit") + }; + assert_eq!(selected, message); + assert_eq!(scale, 0.5); + assert_eq!(pan.y.to_bits(), 0x43cb_0001); + let committed = oa_core::execute_batch(doc.project(), &[command]).unwrap(); + assert_eq!(committed, expected); + assert!(state.spacing_token().is_none()); + assert_eq!(doc.project(), &original.decoded.project); + assert_eq!(doc.compiled().bundle, original.bundle); + assert!(!doc.dirty() && !doc.can_undo() && !doc.can_redo()); + assert!(camera == state.camera_boundary()); +} diff --git a/crates/oa-desktop/src/behavior/scenario/workspace/spacing_tests.rs b/crates/oa-desktop/src/behavior/scenario/workspace/spacing_tests.rs index 3f50d198..5f3bd640 100644 --- a/crates/oa-desktop/src/behavior/scenario/workspace/spacing_tests.rs +++ b/crates/oa-desktop/src/behavior/scenario/workspace/spacing_tests.rs @@ -156,8 +156,26 @@ async fn saved_message_same_batch_pointer_events_commit_once_and_one_undo_redo_r let target = id(&app, "updated_report"); let before = app.document.snapshot(); let revision = app.document.revision(); - let start = screen(&mut mounted, &app, Point::new(400., 252.)); + let projected = screen(&mut mounted, &app, Point::new(400., 252.)); + // This fixture asks for exactly +20 world units. A fractional start near a + // screen-coordinate exponent boundary need not retain an exact +10 delta. + let start = Point::new(projected.x.round(), projected.y.round()); + let mut geometry = resolved(&app); + let camera = app.behavior.scenario.presentation.camera(geometry.bounds); + assert_eq!(camera.scale, 0.5); + let bounds = mounted.geometry(&app).bounds("scenario-viewport"); + let world = Point::new( + (start.x - bounds.x - camera.pan.x) / camera.scale, + (start.y - bounds.y - camera.pan.y) / camera.scale, + ); + geometry.scene.scale = camera.scale; + geometry.scene.camera_pan = camera.pan; + let hit = geometry.scene.hit_item(world).unwrap(); + assert_eq!(hit.id, target); + assert_eq!(hit.role, crate::behavior::SceneHitRole::MessageBody); let end = start + Vector::new(0., 10.); // .5 display scale: +20 authored y. + assert_eq!(end.y - start.y, 10.); + assert!(bounds.contains(start) && bounds.contains(end)); let messages = mounted .batch( &mut app,