Feat: DIST / ID / AREA inquiry commands
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 <noreply@anthropic.com>
This commit is contained in:
parent
db66982098
commit
a3bb2714c1
8 changed files with 225 additions and 0 deletions
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 | \
|
||||
|
|
|
|||
|
|
@ -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 ─────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
79
src/modules/home/inquiry/area.rs
Normal file
79
src/modules/home/inquiry/area.rs
Normal file
|
|
@ -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<Vec3>,
|
||||
}
|
||||
|
||||
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<WireModel> {
|
||||
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![],
|
||||
})
|
||||
}
|
||||
}
|
||||
74
src/modules/home/inquiry/dist.rs
Normal file
74
src/modules/home/inquiry/dist.rs
Normal file
|
|
@ -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<Vec3>,
|
||||
}
|
||||
|
||||
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<WireModel> {
|
||||
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![],
|
||||
})
|
||||
}
|
||||
}
|
||||
36
src/modules/home/inquiry/id.rs
Normal file
36
src/modules/home/inquiry/id.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
3
src/modules/home/inquiry/mod.rs
Normal file
3
src/modules/home/inquiry/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod area;
|
||||
pub mod dist;
|
||||
pub mod id;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue