From fdb6a6f33e7d01cf70d5363564d0a2842a4190b4 Mon Sep 17 00:00:00 2001 From: FredApps <153173854+FredApps@users.noreply.github.com> Date: Sun, 1 Nov 2026 17:33:45 +0100 Subject: [PATCH] fix(ui): rotate cropped image previews around the committed pivot Project the visible user-space bounds centre for the drag preview, matching the RotateBy engine operation. The full image centre differs for cropped artwork and caused the preview to jump on release. The regression compares actual painted preview corners with saved/reopened image corners and verifies a requested 90-degree turn on cropped and uncropped controls, including page rotation. Linux scoped clippy, 106 tests, wasm, formatting, layering and assets pass. Co-Authored-By: Codex GPT-6.1 --- .../ui-egui/src/tools/edit_text_ui/image.rs | 3 +- crates/ui-egui/tests/it/editing.rs | 66 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/tools/edit_text_ui/image.rs b/crates/ui-egui/src/tools/edit_text_ui/image.rs index 38c54c0b7c..281f7de80e 100644 --- a/crates/ui-egui/src/tools/edit_text_ui/image.rs +++ b/crates/ui-egui/src/tools/edit_text_ui/image.rs @@ -761,7 +761,8 @@ pub(crate) fn image_input(cx: &ImageCx<'_>, images: &[pdfcraft_engine::PageImage if view.image_selection.as_ref().is_some_and(|s| s.turning) { let shift = ui.input(|i| i.modifiers.shift); - let centre = obb.screen_center; + let visible = image.visible_rect.unwrap_or(image.rect); + let centre = user_to_screen(xf, info, page, visible[0] * 0.5 + visible[2] * 0.5, visible[1] * 0.5 + visible[3] * 0.5); let rot_knob = shown_knob; let drag_start = view.image_selection.as_ref().and_then(|s| s.drag).map_or(rot_knob, |d| d.start); let degrees = hover_pos.map_or(0.0, |p| turn_degrees_delta(centre, drag_start, p, shift)); diff --git a/crates/ui-egui/tests/it/editing.rs b/crates/ui-egui/tests/it/editing.rs index 173e373a3b..12935b6d0f 100644 --- a/crates/ui-egui/tests/it/editing.rs +++ b/crates/ui-egui/tests/it/editing.rs @@ -9549,3 +9549,69 @@ fn affine_review_resize_uses_press_delta_and_affine_frame() { assert!(placements[0].into_iter().zip(placements[1]).all(|(a, b)| (a - b).abs() < 1e-3), "press offset changed placement: {placements:?}"); } } + +#[test] +fn affine_review_cropped_rotation_preview_matches_saved_corners() { + let matrix = [70.710678, 70.710678, -70.710678, 70.710678, 170.710678, 100.0]; + for (clip, rotation) in [(None, 0), (Some([100.0, 170.0, 241.421356, 241.421356]), 0), (Some([100.0, 170.0, 241.421356, 241.421356]), 90)] { + let mut h = affine_review_harness(affine_review_image_fixture(matrix, clip, rotation)); + let view = &h.state().views[0]; + let id = view.id; + let doc = h.state().session.get(id).unwrap(); + let image = doc.page_images(0)[0].clone(); + let xf = view.page_xform(0).unwrap(); + let obb = pdfcraft_ui_egui::canvas::ImageObb::from_image(&image, &xf, &doc.info, 0); + let visible = image.visible_rect.unwrap_or(image.rect); + let p = doc.info.pages[0].user_to_view(((visible[0] + visible[2]) * 0.5) as f32, ((visible[1] + visible[3]) * 0.5) as f32); + let centre = xf.norm_to_screen(p[0] / xf.pw, p[1] / xf.ph); + let start = obb.s_rot; + let v = start - centre; + let finish = centre + egui::vec2(-v.y, v.x); + h.hover_at(start); + h.run_steps(1); + h.drag_at(start); + h.run_steps(1); + h.hover_at(finish); + h.run_steps(2); + assert!(h.state().views[0].image_selection.as_ref().unwrap().turning); + let preview = h + .output() + .shapes + .iter() + .find_map(|shape| match &shape.shape { + egui::Shape::Path(path) if path.closed && path.points.len() == 4 && path.stroke.width == 1.5 => Some(path.points.clone()), + _ => None, + }) + .expect("rotation preview outline"); + if clip.is_some() + && rotation == 0 + && let Ok(directory) = std::env::var("PDFCRAFT_AFFINE_SHOTS") + { + h.render().unwrap().save(std::path::Path::new(&directory).join("affine-rotation-preview.png")).unwrap(); + } + h.drop_at(finish); + h.run_steps(3); + if clip.is_some() + && rotation == 0 + && let Ok(directory) = std::env::var("PDFCRAFT_AFFINE_SHOTS") + { + h.render().unwrap().save(std::path::Path::new(&directory).join("affine-rotation-committed.png")).unwrap(); + } + let saved = h.state().session.save_bytes(id).unwrap(); + let mut reopened = pdfcraft_engine::Session::new(); + let reopened_id = reopened.open("saved.pdf", None, saved, None).unwrap(); + let doc = reopened.get(reopened_id).unwrap(); + let after = doc.page_images(0)[0].clone(); + let committed = pdfcraft_ui_egui::canvas::ImageObb::from_matrix(after.matrix, &xf, &doc.info, 0).corners(); + let (cx, cy) = ((visible[0] + visible[2]) * 0.5, (visible[1] + visible[3]) * 0.5); + let expected = pdfcraft_content::Matrix(image.matrix).then(&pdfcraft_content::Matrix([0.0, -1.0, 1.0, 0.0, cx - cy, cy + cx])).0; + assert!( + after.matrix.into_iter().zip(expected).all(|(a, b)| (a - b).abs() < 1e-3), + "knob must commit the requested 90-degree turn around visible bounds" + ); + assert!( + preview.iter().zip(committed).all(|(a, b)| a.distance(b) < 1.0), + "preview jumped after commit/save: clip={clip:?} rotation={rotation}: {preview:?} != {committed:?}" + ); + } +}