diff --git a/src/app/commands.rs b/src/app/commands.rs index afcc2772b..435243134 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -3256,8 +3256,10 @@ impl OpenCADStudio { // UCS ORIGIN x,y,z — shift the active UCS origin, keep axes "ORIGIN" | "O" => { let coord_str = parts.get(2).copied().unwrap_or(""); - if let Some(pt) = super::helpers::parse_coord(coord_str) { - // `pt` is in current UCS space; convert to WCS + if let Some((pt, _)) = super::helpers::parse_coord(coord_str) { + // `pt` is in current UCS space; convert to WCS. + // The @/# relative-coordinate prefix is ignored + // here — a UCS origin is always absolute. let wcs_origin = if let Some(ref ucs) = self.tabs[i].active_ucs { ucs_to_wcs(pt, ucs) } else { diff --git a/src/app/helpers.rs b/src/app/helpers.rs index 791a7e0c1..0b7a2adac 100644 --- a/src/app/helpers.rs +++ b/src/app/helpers.rs @@ -4,23 +4,54 @@ use acadrust::tables::Ucs; // ── Coordinate parsing ───────────────────────────────────────────────────── -/// Parse a typed coordinate string into a world-space Vec3. +/// How a typed coordinate should be interpreted relative to the last +/// input point. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(super) enum CoordKind { + /// `@x,y` prefix — offset from the last input point. + Relative, + /// `#x,y` prefix — world/UCS absolute, overriding DYN. + Absolute, + /// No prefix — the caller decides (DYN on → relative, off → absolute). + Default, +} + +/// Parse a typed coordinate string into a Vec3 plus its interpretation. /// Accepts "x,y" → Vec3(x, y, 0) /// "x,y,z" → Vec3(x, y, z) -/// Separators: comma or semicolon; decimal point or decimal comma. -pub(super) fn parse_coord(text: &str) -> Option { - let parts: Vec = text +/// A leading `@` marks the value relative to the last point; a leading +/// `#` forces absolute. Separators: comma or semicolon. +pub(super) fn parse_coord(text: &str) -> Option<(glam::Vec3, CoordKind)> { + let trimmed = text.trim(); + let (kind, rest) = if let Some(r) = trimmed.strip_prefix('@') { + (CoordKind::Relative, r) + } else if let Some(r) = trimmed.strip_prefix('#') { + (CoordKind::Absolute, r) + } else { + (CoordKind::Default, trimmed) + }; + let parts: Vec = rest .split(|c| c == ',' || c == ';') - .map(|s| s.trim().replace(',', ".")) + .map(|s| s.trim()) .filter_map(|s| s.parse().ok()) .collect(); match parts.as_slice() { - [x, y] => Some(glam::Vec3::new(*x, *y, 0.0)), - [x, y, z] => Some(glam::Vec3::new(*x, *y, *z)), + [x, y] => Some((glam::Vec3::new(*x, *y, 0.0), kind)), + [x, y, z] => Some((glam::Vec3::new(*x, *y, *z), kind)), _ => None, } } +/// Rotate a UCS-local offset into WCS without applying the origin +/// translation — used for relative coordinate entry, where only the +/// axis orientation matters, not the UCS origin. +pub(super) fn ucs_rotate_vec(offset: glam::Vec3, ucs: &Ucs) -> glam::Vec3 { + let x = glam::Vec3::new(ucs.x_axis.x as f32, ucs.x_axis.y as f32, ucs.x_axis.z as f32); + let y = glam::Vec3::new(ucs.y_axis.x as f32, ucs.y_axis.y as f32, ucs.y_axis.z as f32); + let z = ucs_z_axis(ucs); + x * offset.x + y * offset.y + z * offset.z +} + // ── UCS ↔ WCS transforms ─────────────────────────────────────────────────── /// Convert a point from UCS local coordinates to WCS. @@ -72,16 +103,6 @@ pub(super) fn ucs_rotated_z(origin: glam::Vec3, angle_z: f32) -> Ucs { ucs } -pub(super) fn angle_close(a: f32, b: f32, tol: f32) -> bool { - let diff = (a - b).rem_euclid(std::f32::consts::TAU); - let diff = if diff > std::f32::consts::PI { - diff - std::f32::consts::TAU - } else { - diff - }; - diff.abs() < tol -} - // ── Grid plane detection ─────────────────────────────────────────────────── /// Choose the grid plane whose normal is most aligned with the camera view direction. diff --git a/src/app/update.rs b/src/app/update.rs index e68c98192..3fae01feb 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -1,5 +1,6 @@ use super::helpers::{ - angle_close, ortho_constrain, parse_coord, polar_constrain, ucs_to_wcs, ucs_z_axis, + ortho_constrain, parse_coord, polar_constrain, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis, + CoordKind, }; use super::{Message, OpenCADStudio, POLY_START_DELAY_MS}; use crate::modules::ModuleEvent; @@ -960,13 +961,40 @@ impl OpenCADStudio { return Task::none(); } - if let Some(ucs_pt) = parse_coord(&text) { - // Typed coordinates are in active UCS space; convert to WCS. - let wcs_pt = if let Some(ref ucs) = self.tabs[i].active_ucs { - ucs_to_wcs(ucs_pt, ucs) - } else { - ucs_pt + if let Some((coord, kind)) = parse_coord(&text) { + // Resolve relative vs absolute. `@` forces relative, + // `#` forces absolute, no prefix follows DYN: when + // dynamic input is on, bare coordinates are relative + // to the last point (issue #26). + // `@` forces relative, `#` forces absolute, no + // prefix follows DYN (on → relative). The very + // first point has no reference, so relative falls + // back to absolute. + let want_relative = match kind { + CoordKind::Relative => true, + CoordKind::Absolute => false, + CoordKind::Default => self.dyn_input, }; + let ucs = self.tabs[i].active_ucs.clone(); + let wcs_pt = match (want_relative, self.last_point) { + (true, Some(base)) => { + // Offset from the last point, rotated by the + // UCS axes (no origin translation). + let offset = match &ucs { + Some(u) => ucs_rotate_vec(coord, u), + None => coord, + }; + base + offset + } + _ => { + // Absolute: typed coordinates are in active UCS. + match &ucs { + Some(u) => ucs_to_wcs(coord, u), + None => coord, + } + } + }; + self.last_point = Some(wcs_pt); let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(wcs_pt)); if let Some(r) = result { return self.apply_cmd_result(r); diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 8d51b04e1..5f9ddd19f 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2211,13 +2211,6 @@ impl Scene { self.camera.borrow().view_rotation_mat() } - /// Return (yaw, pitch) of the active viewport's camera, or None if PSPACE. - pub fn active_viewport_yaw_pitch(&self) -> Option<(f32, f32)> { - let h = self.active_viewport?; - let cam = self.camera_for_viewport(h)?; - Some((cam.yaw, cam.pitch)) - } - /// Return the handle of the user viewport whose bounding rectangle contains /// the given paper-space point, or `None` if no viewport matches. pub fn viewport_at_paper_point(&self, px: f32, py: f32) -> Option { diff --git a/src/scene/pipeline/viewcube.rs b/src/scene/pipeline/viewcube.rs index c11c25096..be9eb2744 100644 --- a/src/scene/pipeline/viewcube.rs +++ b/src/scene/pipeline/viewcube.rs @@ -83,54 +83,6 @@ impl CubeRegion { glam::Vec3::new(c[0], c[1], c[2]).normalize_or(glam::Vec3::Z) } - /// Returns `(yaw, pitch, roll)` for the canonical camera orientation - /// pointed at this region. - /// - /// `yaw` (rotate around world Z) and `pitch` (tilt toward world Z) - /// place the camera on the right side of the cube; `roll` rotates - /// around the camera's view axis so edge views land with the edge - /// vertical on screen instead of horizontal. Faces and corners - /// keep `roll = 0` (world up stays up). - pub fn snap_angles(self) -> (f32, f32, f32) { - use std::f32::consts::{FRAC_PI_2, FRAC_PI_4, PI}; - let iso = (1.0_f32 / 2.0_f32.sqrt()).atan(); - // For the 8 horizontal-cube edges (top + bottom rings), roll by - // ±π/2 around the camera's view axis so the edge sits vertical - // on screen — TOP face on one half, the side face on the other. - // The 4 vertical edges (FRONT-RIGHT etc.) already run along Z - // and read vertical with roll = 0. - let r = FRAC_PI_2; - match self.id() { - 0 => (0.0, FRAC_PI_2, 0.0), - 1 => (0.0, -FRAC_PI_2, 0.0), - 2 => (0.0, 0.0, 0.0), // FRONT - 3 => (PI, 0.0, 0.0), // BACK - 4 => (FRAC_PI_2, 0.0, 0.0), // RIGHT - 5 => (-FRAC_PI_2, 0.0, 0.0), // LEFT - 6 => (0.0, FRAC_PI_4, r), // EDGE_TOP_FRONT - 7 => (PI, FRAC_PI_4, r), // EDGE_TOP_BACK - 8 => (FRAC_PI_2, FRAC_PI_4, r), // EDGE_TOP_RIGHT - 9 => (-FRAC_PI_2, FRAC_PI_4, r), // EDGE_TOP_LEFT - 10 => (0.0, -FRAC_PI_4, r), // EDGE_BOT_FRONT - 11 => (PI, -FRAC_PI_4, r), // EDGE_BOT_BACK - 12 => (FRAC_PI_2, -FRAC_PI_4, r), // EDGE_BOT_RIGHT - 13 => (-FRAC_PI_2, -FRAC_PI_4, r), // EDGE_BOT_LEFT - 14 => (FRAC_PI_4, 0.0, 0.0), // EDGE_FRONT_RIGHT - 15 => (-FRAC_PI_4, 0.0, 0.0), // EDGE_FRONT_LEFT - 16 => (PI * 0.75, 0.0, 0.0), // EDGE_BACK_RIGHT - 17 => (-PI * 0.75, 0.0, 0.0), // EDGE_BACK_LEFT - 18 => (FRAC_PI_4, iso, 0.0), // CORNER_TPF_R - 19 => (-FRAC_PI_4, iso, 0.0), // CORNER_TPF_L - 20 => (PI * 0.75, iso, 0.0), // CORNER_TBK_R - 21 => (-PI * 0.75, iso, 0.0), // CORNER_TBK_L - 22 => (FRAC_PI_4, -iso, 0.0), // CORNER_BTF_R - 23 => (-FRAC_PI_4, -iso, 0.0), // CORNER_BTF_L - 24 => (PI * 0.75, -iso, 0.0), // CORNER_BBK_R - 25 => (-PI * 0.75, -iso, 0.0), // CORNER_BBK_L - _ => (0.0, 0.0, 0.0), - } - } - pub fn opposite(self) -> CubeRegion { match self { CubeRegion::Face(FACE_TOP) => CubeRegion::Face(FACE_BOTTOM),