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 <noreply@anthropic.com>
This commit is contained in:
parent
98578cc5c0
commit
20629d587a
5 changed files with 270 additions and 24 deletions
|
|
@ -1839,16 +1839,25 @@ impl H7CAD {
|
|||
// ── UCS management ───────────────────────────────────────────
|
||||
cmd if cmd == "UCS" || cmd.starts_with("UCS ") => {
|
||||
use acadrust::tables::Ucs;
|
||||
let parts: Vec<&str> = cmd.splitn(3, ' ').collect();
|
||||
use acadrust::types::Vector3;
|
||||
use super::helpers::{ucs_to_wcs, ucs_z_axis, ucs_rotated_z};
|
||||
let parts: Vec<&str> = cmd.splitn(4, ' ').collect();
|
||||
let sub = parts.get(1).map(|s| s.to_uppercase()).unwrap_or_default();
|
||||
match sub.as_str() {
|
||||
"" | "LIST" | "?" => {
|
||||
let active_name = self.tabs[i].active_ucs.as_ref()
|
||||
.map(|u| u.name.clone())
|
||||
.unwrap_or_else(|| "WCS".into());
|
||||
let names: Vec<String> = self.tabs[i].scene.document
|
||||
.ucss.iter().map(|u| u.name.clone()).collect();
|
||||
if names.is_empty() {
|
||||
self.command_line.push_output("No named UCSs defined.");
|
||||
self.command_line.push_output(&format!(
|
||||
"Active UCS: {} | No named UCSs defined.", active_name
|
||||
));
|
||||
} else {
|
||||
self.command_line.push_output(&format!("UCSs: {}", names.join(", ")));
|
||||
self.command_line.push_output(&format!(
|
||||
"Active UCS: {} | Named: {}", active_name, names.join(", ")
|
||||
));
|
||||
}
|
||||
}
|
||||
"SAVE" | "S" => {
|
||||
|
|
@ -1856,11 +1865,18 @@ impl H7CAD {
|
|||
if name.is_empty() {
|
||||
self.command_line.push_error("Usage: UCS SAVE <name>");
|
||||
} else {
|
||||
// Save as WCS (identity) since we don't have active UCS state yet
|
||||
let ucs = Ucs::new(&name);
|
||||
// Save the current active UCS under this name.
|
||||
let ucs = match &self.tabs[i].active_ucs {
|
||||
Some(u) => {
|
||||
let mut saved = u.clone();
|
||||
saved.name = name.clone();
|
||||
saved
|
||||
}
|
||||
None => Ucs::new(&name), // save WCS (identity)
|
||||
};
|
||||
self.tabs[i].scene.document.ucss.add_or_replace(ucs);
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(&format!("UCS '{}' saved (WCS).", name));
|
||||
self.command_line.push_output(&format!("UCS '{}' saved.", name));
|
||||
}
|
||||
}
|
||||
"DELETE" | "DEL" | "D" => {
|
||||
|
|
@ -1875,19 +1891,125 @@ impl H7CAD {
|
|||
}
|
||||
}
|
||||
"W" | "WORLD" => {
|
||||
// Reset active UCS to WCS — currently just an informational message
|
||||
// as full UCS integration awaits WCS↔UCS transform pipeline
|
||||
self.tabs[i].active_ucs = None;
|
||||
self.command_line.push_output("UCS reset to World Coordinate System.");
|
||||
}
|
||||
_ => {
|
||||
// UCS <name> — try as a restore/apply shortcut
|
||||
let name = sub.clone();
|
||||
if self.tabs[i].scene.document.ucss.get(&name).is_some() {
|
||||
self.command_line.push_output(&format!("UCS '{}' is defined. (Full UCS activation pending transform pipeline.)", name));
|
||||
} else {
|
||||
self.command_line.push_error(
|
||||
"Usage: UCS LIST | UCS SAVE <name> | UCS DELETE <name> | UCS W"
|
||||
// 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
|
||||
let wcs_origin = if let Some(ref ucs) = self.tabs[i].active_ucs {
|
||||
ucs_to_wcs(pt, ucs)
|
||||
} else {
|
||||
pt
|
||||
};
|
||||
let ucs = self.tabs[i].active_ucs.get_or_insert_with(|| Ucs::new("*ACTIVE*"));
|
||||
ucs.origin = Vector3::new(
|
||||
wcs_origin.x as f64, wcs_origin.y as f64, wcs_origin.z as f64,
|
||||
);
|
||||
self.command_line.push_output(&format!(
|
||||
"UCS origin set to ({:.4}, {:.4}, {:.4}).",
|
||||
wcs_origin.x, wcs_origin.y, wcs_origin.z
|
||||
));
|
||||
} else {
|
||||
self.command_line.push_error("Usage: UCS ORIGIN x,y,z");
|
||||
}
|
||||
}
|
||||
// UCS Z angle — rotate active UCS around its Z axis by degrees
|
||||
"Z" => {
|
||||
let deg: Option<f32> = parts.get(2).and_then(|s| s.trim().parse().ok());
|
||||
if let Some(angle_deg) = deg {
|
||||
let rad = angle_deg.to_radians();
|
||||
let current = self.tabs[i].active_ucs.as_ref();
|
||||
let origin = current.map(|u| {
|
||||
glam::Vec3::new(
|
||||
u.origin.x as f32, u.origin.y as f32, u.origin.z as f32,
|
||||
)
|
||||
}).unwrap_or(glam::Vec3::ZERO);
|
||||
let mut new_ucs = ucs_rotated_z(origin, rad);
|
||||
// If already had axes, compose rotation on top
|
||||
if let Some(ref ucs) = self.tabs[i].active_ucs {
|
||||
let old_x = glam::Vec3::new(
|
||||
ucs.x_axis.x as f32, ucs.x_axis.y as f32, ucs.x_axis.z as f32,
|
||||
);
|
||||
let old_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);
|
||||
let rot = glam::Quat::from_axis_angle(z_ax, rad);
|
||||
let nx = rot * old_x;
|
||||
let ny = rot * old_y;
|
||||
new_ucs.x_axis = Vector3::new(
|
||||
nx.x as f64, nx.y as f64, nx.z as f64,
|
||||
);
|
||||
new_ucs.y_axis = Vector3::new(
|
||||
ny.x as f64, ny.y as f64, ny.z as f64,
|
||||
);
|
||||
}
|
||||
self.tabs[i].active_ucs = Some(new_ucs);
|
||||
self.command_line.push_output(&format!(
|
||||
"UCS rotated {:.2}° around Z.", angle_deg
|
||||
));
|
||||
} else {
|
||||
self.command_line.push_error("Usage: UCS Z <angle_degrees>");
|
||||
}
|
||||
}
|
||||
// UCS X angle — rotate around current UCS X axis
|
||||
"X" => {
|
||||
let deg: Option<f32> = parts.get(2).and_then(|s| s.trim().parse().ok());
|
||||
if let Some(angle_deg) = deg {
|
||||
let rad = angle_deg.to_radians();
|
||||
let ucs = self.tabs[i].active_ucs.get_or_insert_with(|| Ucs::new("*ACTIVE*"));
|
||||
let x_ax = glam::Vec3::new(
|
||||
ucs.x_axis.x as f32, ucs.x_axis.y as f32, ucs.x_axis.z as f32,
|
||||
);
|
||||
let old_y = glam::Vec3::new(
|
||||
ucs.y_axis.x as f32, ucs.y_axis.y as f32, ucs.y_axis.z as f32,
|
||||
);
|
||||
let rot = glam::Quat::from_axis_angle(x_ax, rad);
|
||||
let ny = rot * old_y;
|
||||
ucs.y_axis = Vector3::new(ny.x as f64, ny.y as f64, ny.z as f64);
|
||||
self.command_line.push_output(&format!(
|
||||
"UCS rotated {:.2}° around X.", angle_deg
|
||||
));
|
||||
} else {
|
||||
self.command_line.push_error("Usage: UCS X <angle_degrees>");
|
||||
}
|
||||
}
|
||||
// UCS Y angle — rotate around current UCS Y axis
|
||||
"Y" => {
|
||||
let deg: Option<f32> = parts.get(2).and_then(|s| s.trim().parse().ok());
|
||||
if let Some(angle_deg) = deg {
|
||||
let rad = angle_deg.to_radians();
|
||||
let ucs = self.tabs[i].active_ucs.get_or_insert_with(|| Ucs::new("*ACTIVE*"));
|
||||
let y_ax = glam::Vec3::new(
|
||||
ucs.y_axis.x as f32, ucs.y_axis.y as f32, ucs.y_axis.z as f32,
|
||||
);
|
||||
let old_x = glam::Vec3::new(
|
||||
ucs.x_axis.x as f32, ucs.x_axis.y as f32, ucs.x_axis.z as f32,
|
||||
);
|
||||
let rot = glam::Quat::from_axis_angle(y_ax, rad);
|
||||
let nx = rot * old_x;
|
||||
ucs.x_axis = Vector3::new(nx.x as f64, nx.y as f64, nx.z as f64);
|
||||
self.command_line.push_output(&format!(
|
||||
"UCS rotated {:.2}° around Y.", angle_deg
|
||||
));
|
||||
} else {
|
||||
self.command_line.push_error("Usage: UCS Y <angle_degrees>");
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// UCS <name> — activate a named UCS
|
||||
let name = sub.clone();
|
||||
if let Some(named) = self.tabs[i].scene.document.ucss.get(&name).cloned() {
|
||||
self.tabs[i].active_ucs = Some(named);
|
||||
self.command_line.push_output(&format!("UCS '{}' activated.", name));
|
||||
} else {
|
||||
self.command_line.push_error(&format!(
|
||||
"UCS '{}' not found. Usage: UCS LIST | SAVE <name> | DELETE <name> | W | ORIGIN x,y,z | X/Y/Z <angle>",
|
||||
name
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use crate::snap::SnapResult;
|
|||
use crate::scene::grip::GripEdit;
|
||||
use crate::scene::GripDef;
|
||||
use acadrust::{CadDocument, Handle};
|
||||
use acadrust::tables::Ucs;
|
||||
use crate::linetypes;
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
|
@ -28,6 +29,8 @@ pub(super) struct DocumentTab {
|
|||
pub(super) last_cursor_world: glam::Vec3,
|
||||
pub(super) history: HistoryState,
|
||||
pub(super) active_layer: String,
|
||||
/// Currently active UCS. `None` means WCS (identity transform).
|
||||
pub(super) active_ucs: Option<Ucs>,
|
||||
}
|
||||
|
||||
impl DocumentTab {
|
||||
|
|
@ -52,6 +55,7 @@ impl DocumentTab {
|
|||
last_cursor_world: glam::Vec3::ZERO,
|
||||
history: HistoryState::default(),
|
||||
active_layer: "0".to_string(),
|
||||
active_ucs: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::ui::overlay::GridPlane;
|
||||
use crate::scene::WireModel;
|
||||
use acadrust::tables::Ucs;
|
||||
|
||||
// ── Coordinate parsing ─────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -20,6 +21,50 @@ pub(super) fn parse_coord(text: &str) -> Option<glam::Vec3> {
|
|||
}
|
||||
}
|
||||
|
||||
// ── 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
|
||||
}
|
||||
|
||||
/// Convert a WCS point back to UCS local coordinates.
|
||||
#[allow(dead_code)]
|
||||
pub(super) fn wcs_to_ucs(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);
|
||||
let d = pt - o;
|
||||
glam::Vec3::new(d.dot(x), d.dot(y), d.dot(z_ax))
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// Build a UCS with `origin` and axes rotated by `angle_z_rad` around the Z axis.
|
||||
pub(super) fn ucs_rotated_z(origin: glam::Vec3, angle_z: f32) -> Ucs {
|
||||
let cos = angle_z.cos() as f64;
|
||||
let sin = angle_z.sin() as f64;
|
||||
let mut ucs = Ucs::new("*ACTIVE*");
|
||||
ucs.origin = acadrust::types::Vector3::new(
|
||||
origin.x as f64, origin.y as f64, origin.z as f64,
|
||||
);
|
||||
ucs.x_axis = acadrust::types::Vector3::new(cos, sin, 0.0);
|
||||
ucs.y_axis = acadrust::types::Vector3::new(-sin, cos, 0.0);
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use super::{H7CAD, Message, POLY_START_DELAY_MS};
|
||||
use super::helpers::{parse_coord, angle_close, ortho_constrain, polar_constrain};
|
||||
use super::helpers::{parse_coord, angle_close, ortho_constrain, polar_constrain, ucs_to_wcs, ucs_z_axis};
|
||||
use crate::scene::{self, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX};
|
||||
use crate::scene::grip::{find_hit_grip, GripEdit};
|
||||
use crate::scene::object::GripApply;
|
||||
|
|
@ -259,8 +259,14 @@ impl H7CAD {
|
|||
return Task::none();
|
||||
}
|
||||
|
||||
if let Some(pt) = parse_coord(&text) {
|
||||
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt));
|
||||
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
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
|
@ -741,10 +747,18 @@ impl H7CAD {
|
|||
if self.tabs[i].active_cmd.is_some() {
|
||||
let (vw, vh) = vp_size;
|
||||
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
||||
let cam = self.tabs[i].scene.camera.borrow();
|
||||
let cursor_paper = cam.pick_on_target_plane(p, bounds);
|
||||
let view_proj = cam.view_proj(bounds);
|
||||
drop(cam);
|
||||
let cursor_paper = if let Some(ref ucs) = self.tabs[i].active_ucs {
|
||||
let origin = glam::Vec3::new(
|
||||
ucs.origin.x as f32, ucs.origin.y as f32, ucs.origin.z as f32,
|
||||
);
|
||||
let normal = ucs_z_axis(ucs);
|
||||
self.tabs[i].scene.camera.borrow()
|
||||
.pick_on_plane(p, bounds, normal, origin)
|
||||
} else {
|
||||
self.tabs[i].scene.camera.borrow()
|
||||
.pick_on_target_plane(p, bounds)
|
||||
};
|
||||
let view_proj = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
// In MSPACE, map paper-space cursor to model space so that
|
||||
// command previews and snapping work in the correct coordinate space.
|
||||
let cursor_world = self.tabs[i].scene.paper_to_model(cursor_paper);
|
||||
|
|
@ -769,7 +783,11 @@ impl H7CAD {
|
|||
let mut pt = self.tabs[i].snap_result
|
||||
.map(|s| self.tabs[i].scene.paper_to_model(s.world))
|
||||
.unwrap_or(cursor_world);
|
||||
if self.tabs[i].active_cmd.is_some() { pt.z = 0.0; }
|
||||
// Clamp to world XY only when no UCS is active; with a UCS the
|
||||
// point already lies on the UCS XY plane.
|
||||
if self.tabs[i].active_cmd.is_some() && self.tabs[i].active_ucs.is_none() {
|
||||
pt.z = 0.0;
|
||||
}
|
||||
if let Some(base) = self.last_point {
|
||||
if self.ortho_mode {
|
||||
pt = ortho_constrain(pt, base);
|
||||
|
|
@ -899,7 +917,19 @@ impl H7CAD {
|
|||
let tangent_obj_at_click = snap_taken.and_then(|s| s.tangent_obj);
|
||||
|
||||
let world_pt = {
|
||||
let raw_paper = self.tabs[i].scene.camera.borrow().pick_on_target_plane(p, bounds);
|
||||
// Project screen point onto the active UCS XY plane (or world XY when
|
||||
// no UCS is active).
|
||||
let raw_paper = if let Some(ref ucs) = self.tabs[i].active_ucs {
|
||||
let origin = glam::Vec3::new(
|
||||
ucs.origin.x as f32, ucs.origin.y as f32, ucs.origin.z as f32,
|
||||
);
|
||||
let normal = ucs_z_axis(ucs);
|
||||
self.tabs[i].scene.camera.borrow()
|
||||
.pick_on_plane(p, bounds, normal, origin)
|
||||
} else {
|
||||
self.tabs[i].scene.camera.borrow()
|
||||
.pick_on_target_plane(p, bounds)
|
||||
};
|
||||
// Convert paper-space → model-space when inside a viewport.
|
||||
let raw = self.tabs[i].scene.paper_to_model(raw_paper);
|
||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
|
|
@ -920,7 +950,11 @@ impl H7CAD {
|
|||
let mut pt = snap_hit
|
||||
.map(|s| self.tabs[i].scene.paper_to_model(s.world))
|
||||
.unwrap_or(raw);
|
||||
pt.z = 0.0;
|
||||
// When no UCS is active clamp to world XY; with a UCS the point is
|
||||
// already constrained to that plane by the ray–plane intersection.
|
||||
if self.tabs[i].active_ucs.is_none() {
|
||||
pt.z = 0.0;
|
||||
}
|
||||
if let Some(base) = self.last_point {
|
||||
if self.ortho_mode {
|
||||
pt = ortho_constrain(pt, base);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue