fix(ucs): object snap tracking rays follow the active UCS

OTRACK alignment rays (0°/90° and polar steps, plus the polar pencil from
the last point) were built along world X/Y. They now rotate with the
active UCS via a Mat4 passed into otrack_snap (scene.viewcube_ucs_mat() —
the UCS in model space, identity in paper), so tracking lines and the
intersection lock align to the user's coordinate system like ortho,
polar, and the grid. sync_ucs_to_scene now reuses UcsXform::rotation_mat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-22 11:17:55 +03:00
commit 2509396ba0
4 changed files with 21 additions and 10 deletions

View file

@ -192,13 +192,7 @@ impl DocumentTab {
/// Push the active UCS rotation into the scene so the ViewCube composes with /// Push the active UCS rotation into the scene so the ViewCube composes with
/// it. Call after any change to `active_ucs`. /// it. Call after any change to `active_ucs`.
pub(super) fn sync_ucs_to_scene(&mut self) { pub(super) fn sync_ucs_to_scene(&mut self) {
let (_, x, y, z) = self.ucs_xform().axes(); self.scene.viewcube_ucs = self.ucs_xform().rotation_mat();
self.scene.viewcube_ucs = glam::Mat4::from_cols(
x.extend(0.0),
y.extend(0.0),
z.extend(0.0),
glam::Vec4::W,
);
} }
pub(super) fn new_drawing(n: usize) -> Self { pub(super) fn new_drawing(n: usize) -> Self {

View file

@ -118,6 +118,17 @@ impl UcsXform {
pub(super) fn axes(&self) -> (glam::Vec3, glam::Vec3, glam::Vec3, glam::Vec3) { pub(super) fn axes(&self) -> (glam::Vec3, glam::Vec3, glam::Vec3, glam::Vec3) {
(self.origin, self.x, self.y, self.z) (self.origin, self.x, self.y, self.z)
} }
/// UCS→world rotation matrix (columns = UCS axes). For consumers that take
/// a `Mat4` rotation directly (ViewCube, OTRACK ray directions).
pub(super) fn rotation_mat(&self) -> glam::Mat4 {
glam::Mat4::from_cols(
self.x.extend(0.0),
self.y.extend(0.0),
self.z.extend(0.0),
glam::Vec4::W,
)
}
} }
// ── UCS ↔ WCS transforms (thin wrappers over `UcsXform`) ──────────────────── // ── UCS ↔ WCS transforms (thin wrappers over `UcsXform`) ────────────────────

View file

@ -2839,12 +2839,14 @@ impl OpenCADStudio {
} else { } else {
None None
}; };
let ucs = self.tabs[i].scene.viewcube_ucs_mat();
self.snapper.otrack_snap( self.snapper.otrack_snap(
cursor_world, cursor_world,
view_proj, view_proj,
bounds, bounds,
step, step,
self.last_point, self.last_point,
ucs,
) )
} else { } else {
None None
@ -3464,8 +3466,9 @@ impl OpenCADStudio {
} else { } else {
None None
}; };
let ucs = self.tabs[i].scene.viewcube_ucs_mat();
self.snapper self.snapper
.otrack_snap(raw, vp_mat, bounds, step, self.last_point) .otrack_snap(raw, vp_mat, bounds, step, self.last_point, ucs)
} else { } else {
None None
}; };

View file

@ -263,6 +263,9 @@ impl Snapper {
bounds: iced::Rectangle, bounds: iced::Rectangle,
polar_step_deg: Option<f32>, polar_step_deg: Option<f32>,
last_point: Option<Vec3>, last_point: Option<Vec3>,
// UCS→world rotation: tracking rays run along the UCS axes, matching
// ortho/polar. Identity = world-aligned rays.
ucs: glam::Mat4,
) -> Option<OtrackHit> { ) -> Option<OtrackHit> {
if !self.otrack_enabled || self.tracking_points.is_empty() { if !self.otrack_enabled || self.tracking_points.is_empty() {
return None; return None;
@ -307,7 +310,7 @@ impl Snapper {
let ar = adeg.to_radians(); let ar = adeg.to_radians();
rays.push(Ray { rays.push(Ray {
origin: tp, origin: tp,
dir: Vec3::new(ar.cos(), ar.sin(), 0.0), dir: ucs.transform_vector3(Vec3::new(ar.cos(), ar.sin(), 0.0)),
group: gi, group: gi,
}); });
} }
@ -322,7 +325,7 @@ impl Snapper {
let ar = a.to_radians(); let ar = a.to_radians();
rays.push(Ray { rays.push(Ray {
origin: lp, origin: lp,
dir: Vec3::new(ar.cos(), ar.sin(), 0.0), dir: ucs.transform_vector3(Vec3::new(ar.cos(), ar.sin(), 0.0)),
group: POLAR_GROUP, group: POLAR_GROUP,
}); });
a += step; a += step;