feat(ucs): ortho/polar constrain in the active UCS plane

Ortho and polar drawing constraints snapped to world X/Y; under an active
UCS they now snap to the UCS axes, routed through the shared UcsXform
(identity = unchanged world-XY behaviour). Applied at all three input
sites (grip drag, command point, command preview). Typed coordinates
already flow through the converter via ucs_to_wcs / ucs_rotate_vec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-22 10:00:53 +03:00
1 parent 4aa1e90dcf
commit 746a2c97be
2 files changed
+41 -21

No files matched your search

+32 -18
View File
@@ -169,21 +169,34 @@ pub(super) fn grid_plane_from_camera(pitch: f32, yaw: f32) -> GridPlane {
// ── Drawing constraint helpers ─────────────────────────────────────────────
/// Constrain `pt` to the nearest 90° direction from `base` (XY plane, Z-up).
pub(super) fn ortho_constrain(pt: glam::Vec3, base: glam::Vec3) -> glam::Vec3 {
let dx = (pt.x - base.x).abs();
let dy = (pt.y - base.y).abs();
if dx >= dy {
glam::Vec3::new(pt.x, base.y, pt.z)
/// Constrain `pt` to the nearest 90° direction from `base`, in the active UCS
/// plane — ortho follows the user's coordinate system, not world axes. `xf` is
/// identity for plain WCS, so the world-XY behaviour is unchanged there.
pub(super) fn ortho_constrain(pt: glam::Vec3, base: glam::Vec3, xf: &UcsXform) -> glam::Vec3 {
let p = xf.to_ucs(pt);
let b = xf.to_ucs(base);
let dx = (p.x - b.x).abs();
let dy = (p.y - b.y).abs();
let c = if dx >= dy {
glam::Vec3::new(p.x, b.y, p.z)
} else {
glam::Vec3::new(base.x, pt.y, pt.z)
}
glam::Vec3::new(b.x, p.y, p.z)
};
xf.to_wcs(c)
}
/// Constrain `pt` to the nearest polar angle multiple from `base` (XY plane, Z-up).
pub(super) fn polar_constrain(pt: glam::Vec3, base: glam::Vec3, step_deg: f32) -> glam::Vec3 {
let dx = pt.x - base.x;
let dy = pt.y - base.y;
/// Constrain `pt` to the nearest polar angle multiple from `base`, measured in
/// the active UCS plane (identity `xf` = world XY, Z-up).
pub(super) fn polar_constrain(
pt: glam::Vec3,
base: glam::Vec3,
step_deg: f32,
xf: &UcsXform,
) -> glam::Vec3 {
let p = xf.to_ucs(pt);
let b = xf.to_ucs(base);
let dx = p.x - b.x;
let dy = p.y - b.y;
let dist = (dx * dx + dy * dy).sqrt();
if dist < 1e-6 {
return pt;
@@ -191,11 +204,11 @@ pub(super) fn polar_constrain(pt: glam::Vec3, base: glam::Vec3, step_deg: f32) -
let step = step_deg.to_radians();
let angle = dy.atan2(dx);
let snapped = (angle / step).round() * step;
glam::Vec3::new(
base.x + dist * snapped.cos(),
base.y + dist * snapped.sin(),
pt.z,
)
xf.to_wcs(glam::Vec3::new(
b.x + dist * snapped.cos(),
b.y + dist * snapped.sin(),
p.z,
))
}
/// Polar constraint that only engages when the cursor is within `tol_px`
@@ -208,8 +221,9 @@ pub(super) fn polar_constrain_near(
view_proj: glam::Mat4,
bounds: iced::Rectangle,
tol_px: f32,
xf: &UcsXform,
) -> glam::Vec3 {
let snapped = polar_constrain(pt, base, step_deg);
let snapped = polar_constrain(pt, base, step_deg, xf);
let to_screen = |w: glam::Vec3| {
let ndc = view_proj.project_point3(w);
(
+9 -3
View File
@@ -2653,8 +2653,9 @@ impl OpenCADStudio {
if snap_hit.is_none() {
let base = grip.origin_world;
let ucs_xf = self.tabs[i].ucs_xform();
if self.ortho_mode {
snapped = ortho_constrain(snapped, base);
snapped = ortho_constrain(snapped, base, &ucs_xf);
} else if self.polar_mode {
snapped = polar_constrain_near(
snapped,
@@ -2663,6 +2664,7 @@ impl OpenCADStudio {
vp_mat,
bounds,
self.snapper.osnap_radius_px,
&ucs_xf,
);
}
}
@@ -2871,8 +2873,9 @@ impl OpenCADStudio {
.is_some_and(|s| s.snap_type != crate::snap::SnapType::Grid);
if !osnap_locked {
if let Some(base) = self.last_point {
let ucs_xf = self.tabs[i].ucs_xform();
if self.ortho_mode {
pt = ortho_constrain(pt, base);
pt = ortho_constrain(pt, base, &ucs_xf);
} else if self.polar_mode {
pt = polar_constrain_near(
pt,
@@ -2881,6 +2884,7 @@ impl OpenCADStudio {
view_proj,
bounds,
self.snapper.osnap_radius_px,
&ucs_xf,
);
}
}
@@ -3477,8 +3481,9 @@ impl OpenCADStudio {
// Object snap wins over ortho/polar — a snapped point
// commits as-is. Grid snap still combines. (#132)
if let Some(base) = self.last_point {
let ucs_xf = self.tabs[i].ucs_xform();
if self.ortho_mode {
pt = ortho_constrain(pt, base);
pt = ortho_constrain(pt, base, &ucs_xf);
} else if self.polar_mode {
pt = polar_constrain_near(
pt,
@@ -3487,6 +3492,7 @@ impl OpenCADStudio {
vp_mat,
bounds,
self.snapper.osnap_radius_px,
&ucs_xf,
);
}
}