From bb723b12ebe53fc31150ffd1f25e4a83071cdf90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcelo=20C=C3=A1rdenas?= Date: Thu, 8 Oct 2026 13:44:51 -0300 Subject: [PATCH] Free Transform: W/H fields reach 1000000% and X/Y the 300000 px document limit (#1267) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The options bar clamped W/H to ±10000% and X/Y to ±30000 px, while the engine takes any scale and documents go up to 300000 px a side. Overlays on large stitched mosaics need more. The limits are now named constants: ±1000000% (a 300 px layer can span the largest document) and ±300000 px. --- crates/ui-egui/src/transform_tool.rs | 53 +++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/transform_tool.rs b/crates/ui-egui/src/transform_tool.rs index 25c6f3a7fe..0fafb4a19b 100644 --- a/crates/ui-egui/src/transform_tool.rs +++ b/crates/ui-egui/src/transform_tool.rs @@ -1305,6 +1305,12 @@ fn readout(t: &TransformSession) -> (f64, f64, f64, f64) { /// Width of the mode, cancel and commit cluster kept on the right of the options bar. const ACTIONS_W: f32 = 140.0; +/// Reach of the reference point X/Y fields: the largest document side (`image.canvasSize`'s limit). +const POSITION_LIMIT_PX: f32 = 300_000.0; +/// Reach of the W/H fields. The engine takes any scale; this lets a 300 px layer span the largest +/// document (#1267). +const SCALE_LIMIT_PCT: f32 = 1_000_000.0; + /// Options bar while transforming: reference point X/Y, W/H %, angle, interpolation, and, pinned /// to the right, the warp switch, cancel and commit. pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) { @@ -1336,9 +1342,9 @@ fn transform_fields(app: &mut PhotocraftApp, ui: &mut egui::Ui, t: &TransformSes let mut px = t.pivot[0] as f32; let mut py = t.pivot[1] as f32; lbl(ui, "X:"); - let rx = crate::widgets::value_field(ui, &mut px, -30000.0..=30000.0, "px", 72.0); + let rx = crate::widgets::value_field(ui, &mut px, -POSITION_LIMIT_PX..=POSITION_LIMIT_PX, "px", 72.0); lbl(ui, "Y:"); - let ry = crate::widgets::value_field(ui, &mut py, -30000.0..=30000.0, "px", 72.0); + let ry = crate::widgets::value_field(ui, &mut py, -POSITION_LIMIT_PX..=POSITION_LIMIT_PX, "px", 72.0); if (rx.changed() || ry.changed()) && let Some(s) = app.ui.transform.as_mut() { @@ -1349,7 +1355,7 @@ fn transform_fields(app: &mut PhotocraftApp, ui: &mut egui::Ui, t: &TransformSes crate::widgets::vline(ui, 22.0); let (mut w, mut h) = (sx as f32, sy as f32); lbl(ui, "W:"); - let rw = crate::widgets::value_field(ui, &mut w, -10000.0..=10000.0, "%", 66.0); + let rw = crate::widgets::value_field(ui, &mut w, -SCALE_LIMIT_PCT..=SCALE_LIMIT_PCT, "%", 66.0); let link_id = egui::Id::new("transform-link"); let mut link: bool = ui.data(|d| d.get_temp(link_id)).unwrap_or(true); if crate::icons::button(ui, if link { "link" } else { "unlink" }, 22.0, link, tl!("Maintain aspect ratio")).clicked() { @@ -1357,7 +1363,7 @@ fn transform_fields(app: &mut PhotocraftApp, ui: &mut egui::Ui, t: &TransformSes ui.data_mut(|d| d.insert_temp(link_id, link)); } lbl(ui, "H:"); - let rh = crate::widgets::value_field(ui, &mut h, -10000.0..=10000.0, "%", 66.0); + let rh = crate::widgets::value_field(ui, &mut h, -SCALE_LIMIT_PCT..=SCALE_LIMIT_PCT, "%", 66.0); if rw.changed() || rh.changed() { let (kx, ky) = if link { let k = if rw.changed() { w as f64 / sx.max(1e-9) } else { h as f64 / sy.max(1e-9) }; @@ -1631,6 +1637,45 @@ mod tests { assert!(h.state().ui.transform.is_none()); } + /// Types `text` + Enter into the `n`th numeric field of the transform bar (X, Y, W, H, angle…). + fn type_into_bar_field(n: usize, text: &str) -> TransformSession { + use egui_kittest::Harness; + use egui_kittest::kittest::Queryable; + let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default()); + app.ui.transform = Some(session()); + let mut h = Harness::builder().with_size(vec2(900.0, 48.0)).build_ui_state( + |ui, app: &mut PhotocraftApp| { + if !ui.ctx().fonts(|f| f.families().contains(&egui::FontFamily::Name("medium".into()))) { + return; + } + ui.horizontal_centered(|ui| options_bar(app, ui)); + }, + app, + ); + PhotocraftApp::setup_context(&h.ctx, crate::theme::ThemeKind::Studio); + h.run_steps(6); + h.get_all_by_role(egui::accesskit::Role::SpinButton).nth(n).unwrap().click(); + h.run(); + for c in text.chars() { + h.event(egui::Event::Text(c.to_string())); + h.run(); + } + h.key_press(egui::Key::Enter); + h.run_steps(2); + h.state().ui.transform.clone().unwrap() + } + + /// Large documents need scales past 10000% and positions past 30000 px (#1267). + #[test] + fn transform_fields_reach_large_documents() { + let t = type_into_bar_field(2, "15000"); + let (sx, sy, _, _) = readout(&t); + assert!((sx - 15000.0).abs() < 0.5 && (sy - 15000.0).abs() < 0.5, "{sx} {sy}"); + assert!((t.quad[1][0] - t.quad[0][0] - 15_000.0).abs() < 1.0, "{:?}", t.quad); + let t = type_into_bar_field(0, "250000"); + assert!((t.pivot[0] - 250_000.0).abs() < 1.0, "{:?}", t.pivot); + } + fn drag(s: &mut TransformSession, from: [f64; 2], to: [f64; 2], mods: egui::Modifiers) { let g = Gesture { hit: hit(s, from, 8.0), start: from, quad0: s.quad, pivot0: s.pivot }; apply_drag(s, g, to, mods);