Feat: implement WCS↔UCS coordinate transform pipeline

- Add `active_ucs: Option<Ucs>` per-tab state to DocumentTab
- Add `ucs_to_wcs`, `wcs_to_ucs`, `ucs_z_axis`, `ucs_rotated_z` helpers
- Add `Camera::pick_on_plane` for ray–plane intersection against any plane
- Mouse picks project onto the active UCS XY plane instead of world XY
- Typed coordinates are converted from UCS space to WCS before dispatch
- UCS command now really activates/deactivates a UCS:
  - `UCS W` resets to WCS (clears active_ucs)
  - `UCS <name>` activates a named UCS
  - `UCS SAVE <name>` saves the current active UCS
  - `UCS ORIGIN x,y,z` shifts origin (in current UCS space)
  - `UCS X/Y/Z <angle>` rotates the UCS around its own axes

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Sonnet 4.6 committed 2026-04-06 14:16:06 +03:00
1 parent 98578cc5c0
commit 20629d587a
5 files changed
+272 -26

No files matched your search

+41
View File
@@ -101,6 +101,47 @@ impl Camera {
OPENGL_TO_WGPU * proj * view
}
/// Project a screen point onto an arbitrary world-space plane.
///
/// The plane is defined by `plane_normal` (unit vector) and a `plane_point`
/// that lies on it. Returns the intersection of the view ray with the plane;
/// falls back to `plane_point` when the ray is nearly parallel to the plane.
pub fn pick_on_plane(
&self,
screen: Point,
bounds: Rectangle,
plane_normal: Vec3,
plane_point: Vec3,
) -> Vec3 {
let ndc_x = (screen.x / bounds.width) * 2.0 - 1.0;
let ndc_y = 1.0 - (screen.y / bounds.height) * 2.0;
let inv = self.view_proj(bounds).inverse();
let (ray_origin, ray_dir) = match self.projection {
Projection::Perspective => {
let near_pt = inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.0));
let far_pt = inv.project_point3(Vec3::new(ndc_x, ndc_y, 1.0));
let dir = (far_pt - near_pt).normalize();
(near_pt, dir)
}
Projection::Orthographic => {
let origin = inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.0));
let forward = (self.target - self.eye()).normalize();
(origin, forward)
}
};
let denom = ray_dir.dot(plane_normal);
if denom.abs() < 1e-6 {
return plane_point;
}
let t = (plane_point - ray_origin).dot(plane_normal) / denom;
if t < 0.0 {
return plane_point;
}
ray_origin + ray_dir * t
}
pub fn pick_on_target_plane(&self, screen: Point, bounds: Rectangle) -> Vec3 {
let ndc_x = (screen.x / bounds.width) * 2.0 - 1.0;
let ndc_y = 1.0 - (screen.y / bounds.height) * 2.0;