graphic design tool
git clone https://git.lucas.co/cce-designer.git
feat: pinch-to-zoom in the 3D viewport
Trackpad pinches now zoom the viewport camera 1:1 via the engine's new
Application::handle_pinch hook (spreading fingers 2x halves the camera
distance), feeding the existing zoom-inertia accumulator so the release
glide matches ctrl-wheel zooms. Pinches outside the viewport fall back
to the engine's ctrl+wheel synthesis, so the graph zooms as before.
Also routes ctrl+wheel over the viewport to the widget's zoom branch —
the routed wheel pass skips ctrl'd events, so that branch was
unreachable (ctrl+scroll zoom in the viewport did nothing).
Co-Authored-By: Claude Fable 5 <[email protected]>
src/app.rs | 35 +++++++++++++++++++++++++++++++----
src/application.rs | 13 +++++++++++++
src/viewport_3d.rs | 16 ++++++++++++++++
3 files changed, 60 insertions(+), 4 deletions(-)
diff --git a/src/app.rs b/src/app.rs
index 4c9e385..9d38793 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1282,6 +1282,16 @@ impl State {
}
}
+ /// Whether the cursor sits over the 3D viewport pane — the wheel arm's
+ /// routing test, shared with `handle_pinch`.
+ pub fn cursor_in_viewport(&self) -> bool {
+ let node_area_y = self.positions[CONTENT_IDX].1;
+ self.cursor_x >= self.content_right_x()
+ && self.cursor_x < self.splitter_layout.splitter2_x
+ && self.cursor_y >= node_area_y
+ && self.cursor_y < self.height - STATUS_H
+ }
+
// --- Pane edge-resize hotspots. Each is the single source of truth for its zone:
// the press handlers arm the matching `AppDrag` off it, and `pane_resize_cursor`
// shows the resize cursor over it, so the two can't drift apart.
@@ -3765,10 +3775,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
// eprintln!("DEBUG MOUSEWHEEL: delta={:?}, phase={:?}, cursor=({}, {}), in_network_pane={}", delta, phase, self.cursor_x, self.cursor_y, in_network_pane);
let node_area_y = self.positions[CONTENT_IDX].1;
- let in_viewport = self.cursor_x >= self.content_right_x()
- && self.cursor_x < self.splitter_layout.splitter2_x
- && self.cursor_y >= node_area_y
- && self.cursor_y < self.height - STATUS_H;
+ let in_viewport = self.cursor_in_viewport();
let mut focus_changed = false;
let mut new_pane = None;
@@ -3923,6 +3930,26 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
}
}
}
+ } else if in_viewport && self.modifiers.control_key() {
+ // Ctrl+wheel zoom over the 3D viewport. The routed pass
+ // above deliberately skips wheels while ctrl is held, so
+ // hand the event to the viewport widget here — its ctrl
+ // branch is the camera zoom. (Trackpad pinches take the
+ // direct `handle_pinch` path and never reach this.)
+ self.slots.get_dyn_mut(VIEWPORT_IDX).set_modifiers(
+ self.modifiers.control_key(),
+ self.modifiers.shift_key(),
+ self.modifiers.alt_key(),
+ );
+ let wheel_ev = cce_ui::widget::Event::MouseWheel {
+ delta: *delta,
+ x: self.cursor_x,
+ y: self.cursor_y,
+ local_x: self.cursor_x,
+ local_y: self.cursor_y,
+ };
+ let vp = self.slots.viewport.id();
+ self.ui_context.propagate_event(&wheel_ev, vp)
} else {
false
};
diff --git a/src/application.rs b/src/application.rs
index f67e350..d0a873c 100644
--- a/src/application.rs
+++ b/src/application.rs
@@ -276,6 +276,19 @@ impl Application for State {
}
}
+ fn handle_pinch(&mut self, factor: f32, pos: LogicalPosition, needs_rebuild: &mut bool) -> bool {
+ self.cursor_x = pos.x;
+ self.cursor_y = pos.y;
+ // 1:1 camera zoom over the 3D viewport; anywhere else falls back to
+ // the engine's ctrl+wheel synthesis (which is what zooms the graph).
+ if self.is_detached_network || !self.cursor_in_viewport() {
+ return false;
+ }
+ self.viewport_mut().pinch_zoom(factor);
+ *needs_rebuild = true;
+ true
+ }
+
fn handle_key_input(&mut self, event: &KeyEvent, needs_rebuild: &mut bool) -> Option<CustomEvent> {
self.sync_modifiers_from_ctx();
let ev = WindowEvent::KeyboardInput { event: event.clone() };
diff --git a/src/viewport_3d.rs b/src/viewport_3d.rs
index b3399ff..7ab6538 100644
--- a/src/viewport_3d.rs
+++ b/src/viewport_3d.rs
@@ -127,6 +127,22 @@ impl Viewport3D {
self.scroll_lock = 0;
}
+ /// Trackpad pinch: direct-manipulation camera zoom. `factor` is the
+ /// scale change since the last gesture update (engine `handle_pinch`
+ /// semantics), applied 1:1 — spreading fingers 2x halves the camera
+ /// distance. Feeds the same accumulator as the ctrl-wheel zoom so the
+ /// release inertia matches.
+ pub fn pinch_zoom(&mut self, factor: f32) {
+ if factor <= 0.0 {
+ return;
+ }
+ let dy = factor.ln();
+ self.zoom = (self.zoom * (-dy).exp()).clamp(0.05, 20.0);
+ self.is_zooming = true;
+ self.last_zoom_time = std::time::Instant::now();
+ self.zoom_accum += dy;
+ }
+
pub fn get_matrices(&self, aspect: f32, custom_camera_pos: Option<Vec3>, custom_camera_rot: Option<Vec3>, custom_pivot: Option<Vec3>) -> (Mat4, Mat4, Mat4) {
let camera_pos = custom_camera_pos.unwrap_or(Vec3::new(2.5, 1.8, 2.5));
let pivot = custom_pivot.unwrap_or(Vec3::ZERO);