diff --git a/src/app/document.rs b/src/app/document.rs index b2584528c..28c21d892 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -195,6 +195,28 @@ impl DocumentTab { self.scene.viewcube_ucs = self.ucs_xform().rotation_mat(); } + /// UCS→render(wire)-space affine for commands that build axis-aligned + /// geometry. Columns are the UCS axes; translation is the UCS origin in wire + /// space. Identity outside model space (no UCS there). + pub(super) fn ucs_wire_affine(&self) -> glam::Mat4 { + if self.scene.current_layout != "Model" { + return glam::Mat4::IDENTITY; + } + let (o, x, y, z) = self.ucs_xform().axes(); + let wo = self.scene.world_offset; + let origin = glam::Vec3::new( + o.x - wo[0] as f32, + o.y - wo[1] as f32, + o.z - wo[2] as f32, + ); + glam::Mat4::from_cols( + x.extend(0.0), + y.extend(0.0), + z.extend(0.0), + origin.extend(1.0), + ) + } + /// Grid origin (render/wire space) and UCS→world rotation for grid snap and /// the grid overlay. Identity / origin-at-zero outside model space. pub(super) fn ucs_grid_basis(&self) -> (glam::Vec3, glam::Mat4) { diff --git a/src/app/update.rs b/src/app/update.rs index ec07898d2..8b62ff302 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -1660,6 +1660,7 @@ impl OpenCADStudio { .map(|c| c.wants_text_input()) .unwrap_or(false) { + self.push_ucs_to_cmd(i); if let Some(result) = self.tabs[i] .active_cmd .as_mut() @@ -1687,6 +1688,7 @@ impl OpenCADStudio { self.dyn_user_reshaped = false; self.sync_dyn_fields(); self.reset_tracking_after_point(); + self.push_ucs_to_cmd(i); let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt)); if let Some(r) = result { let task = self.apply_cmd_result(r); @@ -1728,6 +1730,7 @@ impl OpenCADStudio { self.dyn_user_reshaped = false; self.sync_dyn_fields(); self.reset_tracking_after_point(); + self.push_ucs_to_cmd(i); let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(wcs_pt)); if let Some(r) = result { let task = self.apply_cmd_result(r); @@ -1744,6 +1747,7 @@ impl OpenCADStudio { return Task::none(); } + self.push_ucs_to_cmd(i); if let Some(result) = self.tabs[i] .active_cmd .as_mut() @@ -2971,6 +2975,9 @@ impl OpenCADStudio { s.screen.y += tile_b.y; } + // Give the command the current UCS before it builds its + // rubber-band preview (and, by persistence, its commit). + self.push_ucs_to_cmd(i); let mut previews = if needs_structure { let mut p = self.tabs[i] .active_cmd @@ -8347,6 +8354,16 @@ impl OpenCADStudio { /// values. Locked fields use their typed buffer; the rest fall back to /// the live cursor-derived value. Returns `None` when the field set /// isn't one we know how to turn into a point. + /// Hand the active command the current UCS (as a UCS→wire affine) so + /// axis-aligned constructions build square to the user's coordinate system. + /// No-op for commands that don't override `set_ucs`. + fn push_ucs_to_cmd(&mut self, i: usize) { + let ucs = self.tabs[i].ucs_wire_affine(); + if let Some(c) = self.tabs[i].active_cmd.as_mut() { + c.set_ucs(ucs); + } + } + fn dyn_resolve_point(&self) -> Option { use super::document::DynComponent; let i = self.active_tab; @@ -8606,6 +8623,7 @@ impl OpenCADStudio { self.dyn_user_reshaped = false; self.sync_dyn_fields(); self.reset_tracking_after_point(); + self.push_ucs_to_cmd(i); let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt)); let task = result.map(|r| self.apply_cmd_result(r))?; self.refresh_active_cmd_preview(i); @@ -8664,6 +8682,7 @@ impl OpenCADStudio { self.dyn_user_reshaped = false; self.sync_dyn_fields(); self.reset_tracking_after_point(); + self.push_ucs_to_cmd(i); let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt)); for f in self.tabs[i].dyn_fields.iter_mut() { f.buffer = None; diff --git a/src/command/mod.rs b/src/command/mod.rs index ee4215a4f..46f74264c 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -416,6 +416,13 @@ pub trait CadCommand: Send { /// Current prompt string to display in the command line. fn prompt(&self) -> String; + /// Push the active UCS into the command as a UCS→render(wire)-space affine + /// (identity = plain WCS). Commands that build axis-aligned geometry (RECT, + /// rectangular ARRAY, …) override this to store it and rotate their implicit + /// axes into the UCS; most commands work purely from picked points and + /// ignore it. Called before each point / preview dispatch. + fn set_ucs(&mut self, _ucs: glam::Mat4) {} + /// Called when the user left-clicks in the viewport (point pick). fn on_point(&mut self, pt: Vec3) -> CmdResult; diff --git a/src/modules/draw/draw/shapes.rs b/src/modules/draw/draw/shapes.rs index c20eed469..281a678da 100644 --- a/src/modules/draw/draw/shapes.rs +++ b/src/modules/draw/draw/shapes.rs @@ -17,7 +17,32 @@ use acadrust::{EntityType, LwPolyline}; use crate::command::{CadCommand, CmdResult}; use crate::modules::IconKind; use crate::scene::model::wire_model::WireModel; -use glam::Vec3; +use glam::{Mat4, Vec3}; + +/// Build the four corners of an axis-aligned box between opposite corners `a` +/// and `b`, axis-aligned in the active UCS (`ucs` = UCS→wire affine, identity = +/// world). The two given corners stay put; the other two are placed square to +/// the UCS axes instead of the world axes. +fn ucs_box_corners(a: Vec3, b: Vec3, ucs: Mat4) -> [Vec3; 4] { + let inv = ucs.inverse(); + let au = inv.transform_point3(a); + let bu = inv.transform_point3(b); + [ + ucs.transform_point3(Vec3::new(au.x, au.y, au.z)), + ucs.transform_point3(Vec3::new(bu.x, au.y, au.z)), + ucs.transform_point3(Vec3::new(bu.x, bu.y, au.z)), + ucs.transform_point3(Vec3::new(au.x, bu.y, au.z)), + ] +} + +/// Four corners of a box centred at `c` with half-extents taken from `corner`, +/// axis-aligned in the active UCS (`ucs` = UCS→wire affine, identity = world). +fn ucs_box_around_center(c: Vec3, corner: Vec3, ucs: Mat4) -> [Vec3; 4] { + let d = ucs.inverse().transform_vector3(corner - c); + let rx = ucs.transform_vector3(Vec3::new(d.x.abs(), 0.0, 0.0)); + let ry = ucs.transform_vector3(Vec3::new(0.0, d.y.abs(), 0.0)); + [c - rx - ry, c + rx - ry, c + rx + ry, c - rx + ry] +} const TAU: f32 = std::f32::consts::TAU; const PI: f32 = std::f32::consts::PI; @@ -120,11 +145,12 @@ fn angle_xy(from: Vec3, to: Vec3) -> f32 { pub struct RectCommand { a: Option, + ucs: Mat4, } impl RectCommand { pub fn new() -> Self { - Self { a: None } + Self { a: None, ucs: Mat4::IDENTITY } } } @@ -132,6 +158,9 @@ impl CadCommand for RectCommand { fn name(&self) -> &'static str { "RECT" } + fn set_ucs(&mut self, ucs: Mat4) { + self.ucs = ucs; + } fn prompt(&self) -> String { if self.a.is_none() { "RECT Specify first corner:".into() @@ -145,12 +174,15 @@ impl CadCommand for RectCommand { self.a = Some(pt); CmdResult::NeedPoint } - Some(a) => CmdResult::CommitAndExit(make_pline(&[ - [a.x as f64, a.y as f64], - [pt.x as f64, a.y as f64], - [pt.x as f64, pt.y as f64], - [a.x as f64, pt.y as f64], - ])), + Some(a) => { + let c = ucs_box_corners(a, pt, self.ucs); + CmdResult::CommitAndExit(make_pline(&[ + [c[0].x as f64, c[0].y as f64], + [c[1].x as f64, c[1].y as f64], + [c[2].x as f64, c[2].y as f64], + [c[3].x as f64, c[3].y as f64], + ])) + } } } fn on_enter(&mut self) -> CmdResult { @@ -161,11 +193,12 @@ impl CadCommand for RectCommand { } fn on_mouse_move(&mut self, pt: Vec3) -> Option { let a = self.a?; + let c = ucs_box_corners(a, pt, self.ucs); Some(wire_loop(vec![ - [a.x, a.y, a.z], - [pt.x, a.y, a.z], - [pt.x, pt.y, a.z], - [a.x, pt.y, a.z], + [c[0].x, c[0].y, c[0].z], + [c[1].x, c[1].y, c[1].z], + [c[2].x, c[2].y, c[2].z], + [c[3].x, c[3].y, c[3].z], ])) } fn dyn_spec(&self) -> Option { @@ -302,11 +335,12 @@ impl CadCommand for RectRotCommand { pub struct RectCenCommand { center: Option, + ucs: Mat4, } impl RectCenCommand { pub fn new() -> Self { - Self { center: None } + Self { center: None, ucs: Mat4::IDENTITY } } } @@ -314,6 +348,9 @@ impl CadCommand for RectCenCommand { fn name(&self) -> &'static str { "RECT_CEN" } + fn set_ucs(&mut self, ucs: Mat4) { + self.ucs = ucs; + } fn prompt(&self) -> String { if self.center.is_none() { "RECT CEN Specify center point:".into() @@ -328,15 +365,12 @@ impl CadCommand for RectCenCommand { CmdResult::NeedPoint } Some(c) => { - let hw = (pt.x - c.x).abs() as f64; - let hh = (pt.y - c.y).abs() as f64; - let cx = c.x as f64; - let cy = c.y as f64; + let q = ucs_box_around_center(c, pt, self.ucs); CmdResult::CommitAndExit(make_pline(&[ - [cx - hw, cy - hh], - [cx + hw, cy - hh], - [cx + hw, cy + hh], - [cx - hw, cy + hh], + [q[0].x as f64, q[0].y as f64], + [q[1].x as f64, q[1].y as f64], + [q[2].x as f64, q[2].y as f64], + [q[3].x as f64, q[3].y as f64], ])) } } @@ -349,13 +383,12 @@ impl CadCommand for RectCenCommand { } fn on_mouse_move(&mut self, pt: Vec3) -> Option { let c = self.center?; - let hw = (pt.x - c.x).abs(); - let hh = (pt.y - c.y).abs(); + let q = ucs_box_around_center(c, pt, self.ucs); Some(wire_loop(vec![ - [c.x - hw, c.y - hh, c.z], - [c.x + hw, c.y - hh, c.z], - [c.x + hw, c.y + hh, c.z], - [c.x - hw, c.y + hh, c.z], + [q[0].x, q[0].y, q[0].z], + [q[1].x, q[1].y, q[1].z], + [q[2].x, q[2].y, q[2].z], + [q[3].x, q[3].y, q[3].z], ])) } fn dyn_spec(&self) -> Option { diff --git a/src/modules/draw/modify/array.rs b/src/modules/draw/modify/array.rs index 8621d33c3..8461fe525 100644 --- a/src/modules/draw/modify/array.rs +++ b/src/modules/draw/modify/array.rs @@ -14,7 +14,7 @@ // 1. Center point → 2. Item count (text) → 3. Total angle in degrees (text) use acadrust::Handle; -use glam::Vec3; +use glam::{Mat4, Vec3}; use crate::command::{CadCommand, CmdResult, EntityTransform}; use crate::modules::draw::defaults; @@ -62,6 +62,7 @@ pub struct ArrayRectCommand { default_cols: u32, default_row_sp: f32, default_col_sp: f32, + ucs: Mat4, } impl ArrayRectCommand { @@ -74,20 +75,27 @@ impl ArrayRectCommand { default_cols: defaults::get_array_cols() as u32, default_row_sp: defaults::get_array_row_sp(), default_col_sp: defaults::get_array_col_sp(), + ucs: Mat4::IDENTITY, } } - fn build_transforms(rows: u32, cols: u32, row_sp: f32, col_sp: f32) -> Vec { + /// Row/column offsets run along the active UCS axes (`ucs` rotates the + /// world-frame grid step), identity = world. + fn build_transforms( + rows: u32, + cols: u32, + row_sp: f32, + col_sp: f32, + ucs: Mat4, + ) -> Vec { let mut t = Vec::new(); for r in 0..rows { for c in 0..cols { if r == 0 && c == 0 { continue; } - t.push(EntityTransform::Translate(Vec3::new( - col_sp * c as f32, - row_sp * r as f32, - 0.0, + t.push(EntityTransform::Translate(ucs.transform_vector3( + Vec3::new(col_sp * c as f32, row_sp * r as f32, 0.0), ))); } } @@ -100,6 +108,10 @@ impl CadCommand for ArrayRectCommand { "ARRAYRECT" } + fn set_ucs(&mut self, ucs: Mat4) { + self.ucs = ucs; + } + fn prompt(&self) -> String { match self.step { RectStep::Rows => format!("ARRAYRECT Enter row count <{}>:", self.default_rows), @@ -176,7 +188,7 @@ impl CadCommand for ArrayRectCommand { }; Some(CmdResult::BatchCopy( self.handles.clone(), - Self::build_transforms(rows, cols, row_sp, col_sp), + Self::build_transforms(rows, cols, row_sp, col_sp, self.ucs), )) } } @@ -201,7 +213,7 @@ impl CadCommand for ArrayRectCommand { } RectStep::ColSp { rows, cols, row_sp } => (rows, cols, row_sp, self.default_col_sp), }; - Self::build_transforms(rows, cols, row_sp, col_sp) + Self::build_transforms(rows, cols, row_sp, col_sp, self.ucs) .iter() .flat_map(|t| { if let EntityTransform::Translate(delta) = t {