From a3bb2714c18b6b20bc0644b451339872e945413a Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 2 Apr 2026 23:36:53 +0300 Subject: [PATCH] Feat: DIST / ID / AREA inquiry commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add three classic CAD measurement commands: - DIST (DI): two-point pick → distance, angle, delta XYZ - ID: one-point pick → coordinates - AREA: multi-point polygon pick (Enter to close) → area + perimeter New CmdResult::Measurement(String) variant ends active command and prints result to command line without modifying the document. Co-Authored-By: Claude Sonnet 4.6 --- src/app/cmd_result.rs | 7 +++ src/app/commands.rs | 23 ++++++++++ src/command/mod.rs | 2 + src/modules/home/inquiry/area.rs | 79 ++++++++++++++++++++++++++++++++ src/modules/home/inquiry/dist.rs | 74 ++++++++++++++++++++++++++++++ src/modules/home/inquiry/id.rs | 36 +++++++++++++++ src/modules/home/inquiry/mod.rs | 3 ++ src/modules/home/mod.rs | 1 + 8 files changed, 225 insertions(+) create mode 100644 src/modules/home/inquiry/area.rs create mode 100644 src/modules/home/inquiry/dist.rs create mode 100644 src/modules/home/inquiry/id.rs create mode 100644 src/modules/home/inquiry/mod.rs diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index 99cb617d8..62e097507 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -364,6 +364,13 @@ impl H7CAD { self.tabs[i].scene.zoom_to_window(p1, p2); self.command_line.push_output("Zoom Window"); } + CmdResult::Measurement(msg) => { + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + self.restore_pre_cmd_tangent(); + self.command_line.push_output(&msg); + } } // 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 676bbaf54..96e164169 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -970,6 +970,28 @@ impl H7CAD { } } + // ── Inquiry ────────────────────────────────────────────────────── + "DIST"|"DI" => { + use crate::modules::home::inquiry::dist::DistCommand; + let cmd = DistCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + + "ID" => { + use crate::modules::home::inquiry::id::IdCommand; + let cmd = IdCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + + "AREA" => { + use crate::modules::home::inquiry::area::AreaCommand; + let cmd = AreaCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + "HELP"|"?" => { self.command_line.push_output( "Draw: LINE CIRCLE ARC PLINE RECT POLY POINT ELLIPSE SPLINE RAY XLINE HATCH | \ @@ -977,6 +999,7 @@ impl H7CAD { Array: ARRAY ARRAYRECT ARRAYPOLAR ARRAYPATH | \ Text: TEXT MTEXT LEADER MLEADER | \ Dimension: DIMLINEAR DIMANGULAR DIMRADIUS | \ + Inquiry: DIST ID AREA LIST | \ View: ZOOM EXTENTS VIEW LIST/SAVE/RESTORE/DELETE | \ Layer: LAYER LIST/NEW/ON/OFF/FREEZE/THAW/LOCK/UNLOCK/COLOR/SET | \ Viewport: MVIEW VPLAYER VPORTS MS PS DRAWORDER | \ diff --git a/src/command/mod.rs b/src/command/mod.rs index b416c7cbf..5a93c254c 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -93,6 +93,8 @@ pub enum CmdResult { PasteClipboard { base_pt: Vec3 }, /// Zoom the model-space camera to fit the given corner points; end command. ZoomToWindow { p1: Vec3, p2: Vec3 }, + /// Print a measurement result to the command line and end the command. + Measurement(String), } // ── Trait ───────────────────────────────────────────────────────────────── diff --git a/src/modules/home/inquiry/area.rs b/src/modules/home/inquiry/area.rs new file mode 100644 index 000000000..4a2f2a7d0 --- /dev/null +++ b/src/modules/home/inquiry/area.rs @@ -0,0 +1,79 @@ +// AREA command — compute area and perimeter of a polygon picked point by point. +// Press Enter to close and calculate. + +use glam::Vec3; + +use crate::command::{CadCommand, CmdResult}; +use crate::scene::wire_model::WireModel; + +pub struct AreaCommand { + points: Vec, +} + +impl AreaCommand { + pub fn new() -> Self { + Self { points: vec![] } + } +} + +impl CadCommand for AreaCommand { + fn name(&self) -> &'static str { + "AREA" + } + + fn prompt(&self) -> String { + if self.points.is_empty() { + "AREA Specify first corner point (Enter to cancel):".into() + } else { + format!( + "AREA Specify next point ({} picked, Enter to calculate):", + self.points.len() + ) + } + } + + fn on_point(&mut self, pt: Vec3) -> CmdResult { + self.points.push(pt); + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + if self.points.len() < 3 { + return CmdResult::Cancel; + } + // Shoelace formula in the XZ plane (Y-up world) + let n = self.points.len(); + let mut area_sum = 0.0f32; + let mut perimeter = 0.0f32; + for idx in 0..n { + let a = self.points[idx]; + let b = self.points[(idx + 1) % n]; + area_sum += a.x * b.z - b.x * a.z; + perimeter += (b - a).length(); + } + let area = (area_sum * 0.5).abs(); + let msg = format!("Area = {area:.4}, Perimeter = {perimeter:.4}"); + CmdResult::Measurement(msg) + } + + fn on_mouse_move(&mut self, pt: Vec3) -> Option { + if self.points.is_empty() { + return None; + } + let mut pts: Vec<[f32; 3]> = self.points.iter().map(|p| [p.x, p.y, p.z]).collect(); + pts.push([pt.x, pt.y, pt.z]); + pts.push([self.points[0].x, self.points[0].y, self.points[0].z]); + Some(WireModel { + name: "area_preview".into(), + points: pts, + 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/modules/home/inquiry/dist.rs b/src/modules/home/inquiry/dist.rs new file mode 100644 index 000000000..07bdd6dc9 --- /dev/null +++ b/src/modules/home/inquiry/dist.rs @@ -0,0 +1,74 @@ +// DIST command — measure distance and angle between two picked points. + +use glam::Vec3; + +use crate::command::{CadCommand, CmdResult}; +use crate::scene::wire_model::WireModel; + +pub struct DistCommand { + first: Option, +} + +impl DistCommand { + pub fn new() -> Self { + Self { first: None } + } +} + +impl CadCommand for DistCommand { + fn name(&self) -> &'static str { + "DIST" + } + + fn prompt(&self) -> String { + if self.first.is_none() { + "DIST Specify first point:".into() + } else { + "DIST Specify second point:".into() + } + } + + fn on_point(&mut self, pt: Vec3) -> CmdResult { + if let Some(p1) = self.first { + let delta = pt - p1; + let dist = delta.length(); + let dx = delta.x; + let dy = delta.z; // Y-up world: Z is the "drawing Y" + let dz = delta.y; // Y-up world: Y is elevation + + // Angle in XY plane (XZ in world coords) — degrees from +X + let angle_xy = dy.atan2(dx).to_degrees(); + // Angle from XY plane toward Z (elevation angle) + let dist_xy = dx.hypot(dy); + let angle_z = dz.atan2(dist_xy).to_degrees(); + + let msg = format!( + "Distance = {dist:.4}, Angle in XY Plane = {angle_xy:.4}°, Angle from XY Plane = {angle_z:.4}°\n Delta X = {dx:.4}, Delta Y = {dy:.4}, Delta Z = {dz:.4}", + ); + CmdResult::Measurement(msg) + } else { + self.first = Some(pt); + CmdResult::NeedPoint + } + } + + fn on_enter(&mut self) -> CmdResult { + CmdResult::Cancel + } + + fn on_mouse_move(&mut self, pt: Vec3) -> Option { + let p1 = self.first?; + Some(WireModel { + name: "dist_preview".into(), + points: vec![[p1.x, p1.y, p1.z], [pt.x, pt.y, pt.z]], + 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/modules/home/inquiry/id.rs b/src/modules/home/inquiry/id.rs new file mode 100644 index 000000000..6a5478558 --- /dev/null +++ b/src/modules/home/inquiry/id.rs @@ -0,0 +1,36 @@ +// ID command — report coordinates of a picked point. + +use glam::Vec3; + +use crate::command::{CadCommand, CmdResult}; + +pub struct IdCommand; + +impl IdCommand { + pub fn new() -> Self { + Self + } +} + +impl CadCommand for IdCommand { + fn name(&self) -> &'static str { + "ID" + } + + fn prompt(&self) -> String { + "ID Specify point:".into() + } + + fn on_point(&mut self, pt: Vec3) -> CmdResult { + // Y-up world: X stays X, Z→drawing Y, Y→elevation Z + let x = pt.x; + let y = pt.z; + let z = pt.y; + let msg = format!("X = {x:.4}, Y = {y:.4}, Z = {z:.4}"); + CmdResult::Measurement(msg) + } + + fn on_enter(&mut self) -> CmdResult { + CmdResult::Cancel + } +} diff --git a/src/modules/home/inquiry/mod.rs b/src/modules/home/inquiry/mod.rs new file mode 100644 index 000000000..c46240252 --- /dev/null +++ b/src/modules/home/inquiry/mod.rs @@ -0,0 +1,3 @@ +pub mod area; +pub mod dist; +pub mod id; diff --git a/src/modules/home/mod.rs b/src/modules/home/mod.rs index 2f2d2bc2c..57d236315 100644 --- a/src/modules/home/mod.rs +++ b/src/modules/home/mod.rs @@ -5,6 +5,7 @@ pub mod defaults; mod donate; pub mod draw; pub mod groups; +pub mod inquiry; pub mod layers; pub mod modify; pub mod properties;