diff --git a/src/app/automation.rs b/src/app/automation.rs index c2d8c59d3..15c9f48a8 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -224,6 +224,7 @@ impl OpenCADStudio { Ok(doc) => { let i = self.active_tab; self.tabs[i].scene.document = doc; + self.tabs[i].adopt_active_ucs_from_header(); self.tabs[i].current_path = Some(PathBuf::from(path)); self.tabs[i].is_start = false; self.tabs[i].scene.bump_geometry(); diff --git a/src/app/document.rs b/src/app/document.rs index 42da78413..c1672c135 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -165,6 +165,29 @@ pub(super) struct DocumentTab { } impl DocumentTab { + /// The active WCS↔UCS converter for this tab — identity when no UCS is set. + /// Every consumer that needs UCS-relative coordinates goes through this. + pub(super) fn ucs_xform(&self) -> super::helpers::UcsXform { + super::helpers::UcsXform::from_active(self.active_ucs.as_ref()) + } + + /// Adopt the document's saved current UCS (the header's model-space UCS) as + /// the active UCS, so the coordinate readout / icon / input follow the + /// file's coordinate system the moment it opens. An identity UCS clears it + /// back to plain WCS. Call wherever a document is loaded into the tab. + pub(super) fn adopt_active_ucs_from_header(&mut self) { + let h = &self.scene.document.header; + let mut u = Ucs::new(h.model_space_ucs_name.clone()); + u.origin = h.model_space_ucs_origin; + u.x_axis = h.model_space_ucs_x_axis; + u.y_axis = h.model_space_ucs_y_axis; + self.active_ucs = if super::helpers::UcsXform::from_ucs(&u).is_identity() { + None + } else { + Some(u) + }; + } + pub(super) fn new_drawing(n: usize) -> Self { let mut scene = Scene::new(); linetypes::populate_document(&mut scene.document); diff --git a/src/app/helpers.rs b/src/app/helpers.rs index 2cd10a909..c2d48098b 100644 --- a/src/app/helpers.rs +++ b/src/app/helpers.rs @@ -42,54 +42,102 @@ pub(super) fn parse_coord(text: &str) -> Option<(glam::Vec3, CoordKind)> { } } +// ── UCS ↔ WCS converter ───────────────────────────────────────────────────── + +/// The single bridge between WCS (how geometry and the file are stored) and the +/// active UCS (the coordinate system the user works in). Build one from the +/// tab's active UCS via [`DocumentTab::ucs_xform`](super::DocumentTab); the +/// `None` UCS yields the identity (plain WCS). +/// +/// Every system that has to speak UCS — the coordinate readout, typed input, +/// the UCS icon, snap/ortho, the ViewCube — goes through this one type instead +/// of re-deriving the axis math. Axes are orthonormal, so the inverse rotation +/// is just the transpose (the dot products in `to_ucs`); no matrix inversion. +#[derive(Clone, Copy)] +pub(super) struct UcsXform { + origin: glam::Vec3, + x: glam::Vec3, + y: glam::Vec3, + z: glam::Vec3, +} + +impl UcsXform { + /// Plain WCS — no active UCS. + pub(super) fn identity() -> Self { + Self { + origin: glam::Vec3::ZERO, + x: glam::Vec3::X, + y: glam::Vec3::Y, + z: glam::Vec3::Z, + } + } + + pub(super) fn from_ucs(ucs: &Ucs) -> Self { + let v = + |a: acadrust::types::Vector3| glam::Vec3::new(a.x as f32, a.y as f32, a.z as f32); + let x = v(ucs.x_axis).normalize_or(glam::Vec3::X); + let y = v(ucs.y_axis).normalize_or(glam::Vec3::Y); + let z = x.cross(y).normalize_or(glam::Vec3::Z); + Self { origin: v(ucs.origin), x, y, z } + } + + pub(super) fn from_active(ucs: Option<&Ucs>) -> Self { + ucs.map(Self::from_ucs).unwrap_or_else(Self::identity) + } + + /// True when this is plain WCS — lets callers skip the conversion. + pub(super) fn is_identity(&self) -> bool { + self.origin == glam::Vec3::ZERO + && self.x == glam::Vec3::X + && self.y == glam::Vec3::Y + && self.z == glam::Vec3::Z + } + + /// UCS point → WCS. + pub(super) fn to_wcs(&self, p: glam::Vec3) -> glam::Vec3 { + self.origin + self.x * p.x + self.y * p.y + self.z * p.z + } + + /// WCS point → UCS. + pub(super) fn to_ucs(&self, p: glam::Vec3) -> glam::Vec3 { + let d = p - self.origin; + glam::Vec3::new(d.dot(self.x), d.dot(self.y), d.dot(self.z)) + } + + /// UCS direction → WCS (rotation only, no origin shift). + pub(super) fn vec_to_wcs(&self, v: glam::Vec3) -> glam::Vec3 { + self.x * v.x + self.y * v.y + self.z * v.z + } + + /// WCS direction → UCS (rotation only, no origin shift). + #[allow(dead_code)] + pub(super) fn vec_to_ucs(&self, v: glam::Vec3) -> glam::Vec3 { + glam::Vec3::new(v.dot(self.x), v.dot(self.y), v.dot(self.z)) + } + + /// `(origin, x, y, z)` axes in WCS — for drawing the UCS icon. + pub(super) fn axes(&self) -> (glam::Vec3, glam::Vec3, glam::Vec3, glam::Vec3) { + (self.origin, self.x, self.y, self.z) + } +} + +// ── UCS ↔ WCS transforms (thin wrappers over `UcsXform`) ──────────────────── + /// 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 + UcsXform::from_ucs(ucs).vec_to_wcs(offset) } -// ── UCS ↔ WCS transforms ─────────────────────────────────────────────────── - /// Convert a point from UCS local coordinates to WCS. -/// -/// WCS = origin + x_axis*u + y_axis*v + z_axis*w pub(super) fn ucs_to_wcs(pt: glam::Vec3, ucs: &Ucs) -> glam::Vec3 { - let o = glam::Vec3::new( - ucs.origin.x as f32, - ucs.origin.y as f32, - ucs.origin.z as f32, - ); - 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_ax = ucs_z_axis(ucs); - o + x * pt.x + y * pt.y + z_ax * pt.z + UcsXform::from_ucs(ucs).to_wcs(pt) } /// Return the normalised Z axis of a UCS (cross product of X and Y axes). pub(super) fn ucs_z_axis(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, - ); - x.cross(y).normalize_or_zero() + UcsXform::from_ucs(ucs).axes().3 } /// Build a UCS with `origin` and axes rotated by `angle_z_rad` around the Z axis. diff --git a/src/app/update.rs b/src/app/update.rs index e0607017f..273f71ae3 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -560,6 +560,8 @@ impl OpenCADStudio { self.tabs[i].current_path = Some(path.clone()); self.tabs[i].scene.document = doc; + // Follow the file's saved current UCS from the moment it opens. + self.tabs[i].adopt_active_ucs_from_header(); // Route shared CJK ideographs to the language matching this // drawing's code page (web per-language font split). Drop the // glyph cache if it changed so Han re-resolves to the new diff --git a/src/app/view.rs b/src/app/view.rs index 4c39e1e34..137639afe 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -154,9 +154,11 @@ impl OpenCADStudio { let ucs_icon = if self.show_ucs_icon && !is_paper { let cam = tab.scene.camera.borrow(); + let (_, ux, uy, uz) = tab.ucs_xform().axes(); Some(overlay::UcsIconParams { view_proj: cam.view_proj(vp_bounds), bounds: vp_bounds, + axes: (ux, uy, uz), }) } else { None @@ -877,16 +879,19 @@ impl OpenCADStudio { || tab.scene.has_selected_viewport(); // The cursor is tracked in local render space; re-add the // model-space world offset so the readout shows true - // drawing coordinates (paper space carries no offset). + // drawing coordinates (paper space carries no offset), then + // report it in the active UCS — the readout follows the + // user's coordinate system, not raw WCS (no-op without UCS). let cursor_coord = { let lc = tab.last_cursor_world; if is_model { let wo = tab.scene.world_offset; - glam::Vec3::new( + let wcs = glam::Vec3::new( lc.x + wo[0] as f32, lc.y + wo[1] as f32, lc.z + wo[2] as f32, - ) + ); + tab.ucs_xform().to_ucs(wcs) } else { lc } diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 59ad5b61a..c6431907b 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -93,6 +93,9 @@ pub struct UcsIconParams { pub view_proj: Mat4, /// Viewport bounds (used for NDC → pixel conversion). pub bounds: iced::Rectangle, + /// The active UCS axis directions in world space (X, Y, Z). Plain WCS is + /// `(Vec3::X, Vec3::Y, Vec3::Z)`; a UCS rotates the tripod to match. + pub axes: (Vec3, Vec3, Vec3), } // ── Selection overlay ─────────────────────────────────────────────────── @@ -789,7 +792,7 @@ impl canvas::Program for SelectionCanvas { // ── UCS icon ────────────────────────────────────────────────────── if let Some(ref ucs) = self.ucs_icon { - draw_ucs_icon(&mut frame, ucs.view_proj, ucs.bounds); + draw_ucs_icon(&mut frame, ucs.view_proj, ucs.bounds, ucs.axes); } // ── Object Snap Tracking lines ──────────────────────────────────── @@ -1021,7 +1024,12 @@ const UCS_ICON_MARGIN: f32 = 50.0; const UCS_ICON_LEN: f32 = 38.0; // longest axis arm in screen pixels const UCS_ICON_TIP: f32 = 7.0; // arrowhead size in pixels -fn draw_ucs_icon(frame: &mut canvas::Frame, vp: Mat4, bounds: iced::Rectangle) { +fn draw_ucs_icon( + frame: &mut canvas::Frame, + vp: Mat4, + bounds: iced::Rectangle, + axes: (Vec3, Vec3, Vec3), +) { if bounds.width < 10.0 || bounds.height < 10.0 { return; } @@ -1041,10 +1049,14 @@ fn draw_ucs_icon(frame: &mut canvas::Frame, vp: Mat4, bounds: iced::Rectangle) { ) }; + // Project the UCS axis directions (not fixed world axes) so the tripod + // rotates to the active UCS. Directions are translation-invariant, so a + // common origin is fine. + let (ax, ay, az) = axes; let Some(org) = w2ndc(Vec3::ZERO) else { return }; - let Some(xn) = w2ndc(Vec3::X) else { return }; - let Some(yn) = w2ndc(Vec3::Y) else { return }; - let Some(zn) = w2ndc(Vec3::Z) else { return }; + let Some(xn) = w2ndc(ax) else { return }; + let Some(yn) = w2ndc(ay) else { return }; + let Some(zn) = w2ndc(az) else { return }; let org_s = ndc2s(org); let icon_origin = Point::new(