From 70a8f5bf3f791584c2ff5092397a81aa9348f62b Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 2 Apr 2026 23:28:00 +0300 Subject: [PATCH] Feat: ZOOM IN/OUT/WINDOW commands; ZoomToWindow CmdResult Co-Authored-By: Claude Sonnet 4.6 --- src/app/cmd_result.rs | 8 ++++ src/app/commands.rs | 17 ++++++++ src/command/mod.rs | 2 + src/modules/view/mod.rs | 1 + src/modules/view/zoom_window.rs | 72 +++++++++++++++++++++++++++++++++ src/scene/mod.rs | 20 +++++++++ 6 files changed, 120 insertions(+) create mode 100644 src/modules/view/zoom_window.rs diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index f229d6f31..99cb617d8 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -356,6 +356,14 @@ impl H7CAD { self.command_line.push_info(&p); } } + + CmdResult::ZoomToWindow { p1, p2 } => { + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + self.tabs[i].scene.zoom_to_window(p1, p2); + self.command_line.push_output("Zoom Window"); + } } // Focus the command-line input while a command is active; blur it when the command ends. if self.tabs[i].active_cmd.is_some() { diff --git a/src/app/commands.rs b/src/app/commands.rs index 531ddb824..004ecbf59 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -710,6 +710,23 @@ impl H7CAD { self.command_line.push_output("Zoom Extents"); } + "ZOOM IN"|"ZI" => { + self.tabs[i].scene.zoom_camera(1.0 / 1.5); + self.command_line.push_output("Zoom In"); + } + + "ZOOM OUT"|"ZO" => { + self.tabs[i].scene.zoom_camera(1.5); + self.command_line.push_output("Zoom Out"); + } + + "ZOOM WINDOW"|"ZOOM W"|"ZW" => { + use crate::modules::view::zoom_window::ZoomWindowCommand; + let new_cmd = ZoomWindowCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "STRETCH"|"SS" => { let handles: Vec<_> = self.tabs[i].scene.selected_entities() .into_iter().map(|(h, _)| h).collect(); diff --git a/src/command/mod.rs b/src/command/mod.rs index fb5aeca24..b416c7cbf 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -91,6 +91,8 @@ pub enum CmdResult { }, /// Paste clipboard entities translated so their centroid lands at `base_pt`; end command. PasteClipboard { base_pt: Vec3 }, + /// Zoom the model-space camera to fit the given corner points; end command. + ZoomToWindow { p1: Vec3, p2: Vec3 }, } // ── Trait ───────────────────────────────────────────────────────────────── diff --git a/src/modules/view/mod.rs b/src/modules/view/mod.rs index 850ab29fd..b8c788dab 100644 --- a/src/modules/view/mod.rs +++ b/src/modules/view/mod.rs @@ -15,6 +15,7 @@ mod xray; mod zoom_ext; mod zoom_in; mod zoom_out; +pub mod zoom_window; use crate::modules::{CadModule, RibbonGroup}; diff --git a/src/modules/view/zoom_window.rs b/src/modules/view/zoom_window.rs new file mode 100644 index 000000000..052bd3195 --- /dev/null +++ b/src/modules/view/zoom_window.rs @@ -0,0 +1,72 @@ +// ZOOM WINDOW command — pick two corners to define the zoom area. + +use glam::Vec3; + +use crate::command::{CadCommand, CmdResult}; +use crate::scene::wire_model::WireModel; + +pub struct ZoomWindowCommand { + first: Option, +} + +impl ZoomWindowCommand { + pub fn new() -> Self { + Self { first: None } + } +} + +impl CadCommand for ZoomWindowCommand { + fn name(&self) -> &'static str { + "ZOOM WINDOW" + } + + fn prompt(&self) -> String { + if self.first.is_none() { + "ZOOM WINDOW Specify first corner:".into() + } else { + "ZOOM WINDOW Specify opposite corner:".into() + } + } + + fn on_point(&mut self, pt: Vec3) -> CmdResult { + if let Some(p1) = self.first { + CmdResult::ZoomToWindow { p1, p2: pt } + } else { + self.first = Some(pt); + CmdResult::NeedPoint + } + } + + fn on_enter(&mut self) -> CmdResult { + CmdResult::Cancel + } + + fn on_escape(&mut self) -> CmdResult { + CmdResult::Cancel + } + + fn on_mouse_move(&mut self, pt: Vec3) -> Option { + let p1 = self.first?; + let min = p1.min(pt); + let max = p1.max(pt); + // Draw a rectangle preview + Some(WireModel { + name: "zoom_window_preview".into(), + points: vec![ + [min.x, min.y, 0.0], + [max.x, min.y, 0.0], + [max.x, max.y, 0.0], + [min.x, max.y, 0.0], + [min.x, min.y, 0.0], + ], + color: WireModel::CYAN, + selected: false, + pattern_length: 0.0, + pattern: [0.0; 8], + line_weight_px: 1.0, + snap_pts: vec![], + tangent_geoms: vec![], + key_vertices: vec![], + }) + } +} diff --git a/src/scene/mod.rs b/src/scene/mod.rs index f9495cb0d..da1faaef4 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1687,6 +1687,26 @@ impl Scene { } } + /// Zoom the model-space camera in/out by a percentage. + /// factor > 1 = zoom out, factor < 1 = zoom in. + pub fn zoom_camera(&mut self, factor: f32) { + let mut cam = self.camera.borrow_mut(); + cam.distance = (cam.distance * factor).max(0.001); + drop(cam); + self.camera_generation += 1; + } + + /// Fit the camera to a world-space bounding box (corners p1, p2). + pub fn zoom_to_window(&mut self, p1: glam::Vec3, p2: glam::Vec3) { + let min = p1.min(p2); + let max = p1.max(p2); + if min == max { + return; + } + self.camera.borrow_mut().fit_to_bounds(min, max); + self.camera_generation += 1; + } + pub fn fit_all(&mut self) { let wires = self.entity_wires(); if wires.is_empty() {