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 ───────────────────────────────────────────
|
// ── UCS management ───────────────────────────────────────────
|
||||||
cmd if cmd == "UCS" || cmd.starts_with("UCS ") => {
|
cmd if cmd == "UCS" || cmd.starts_with("UCS ") => {
|
||||||
use acadrust::tables::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();
|
let sub = parts.get(1).map(|s| s.to_uppercase()).unwrap_or_default();
|
||||||
match sub.as_str() {
|
match sub.as_str() {
|
||||||
"" | "LIST" | "?" => {
|
"" | "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
|
let names: Vec<String> = self.tabs[i].scene.document
|
||||||
.ucss.iter().map(|u| u.name.clone()).collect();
|
.ucss.iter().map(|u| u.name.clone()).collect();
|
||||||
if names.is_empty() {
|
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 {
|
} 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" => {
|
"SAVE" | "S" => {
|
||||||
|
|
@ -1856,11 +1865,18 @@ impl H7CAD {
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
self.command_line.push_error("Usage: UCS SAVE <name>");
|
self.command_line.push_error("Usage: UCS SAVE <name>");
|
||||||
} else {
|
} else {
|
||||||
// Save as WCS (identity) since we don't have active UCS state yet
|
// Save the current active UCS under this name.
|
||||||
let ucs = Ucs::new(&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].scene.document.ucss.add_or_replace(ucs);
|
||||||
self.tabs[i].dirty = true;
|
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" => {
|
"DELETE" | "DEL" | "D" => {
|
||||||
|
|
@ -1875,19 +1891,125 @@ impl H7CAD {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"W" | "WORLD" => {
|
"W" | "WORLD" => {
|
||||||
// Reset active UCS to WCS — currently just an informational message
|
self.tabs[i].active_ucs = None;
|
||||||
// as full UCS integration awaits WCS↔UCS transform pipeline
|
|
||||||
self.command_line.push_output("UCS reset to World Coordinate System.");
|
self.command_line.push_output("UCS reset to World Coordinate System.");
|
||||||
}
|
}
|
||||||
_ => {
|
// UCS ORIGIN x,y,z — shift the active UCS origin, keep axes
|
||||||
// UCS <name> — try as a restore/apply shortcut
|
"ORIGIN" | "O" => {
|
||||||
let name = sub.clone();
|
let coord_str = parts.get(2).copied().unwrap_or("");
|
||||||
if self.tabs[i].scene.document.ucss.get(&name).is_some() {
|
if let Some(pt) = super::helpers::parse_coord(coord_str) {
|
||||||
self.command_line.push_output(&format!("UCS '{}' is defined. (Full UCS activation pending transform pipeline.)", name));
|
// `pt` is in current UCS space; convert to WCS
|
||||||
} else {
|
let wcs_origin = if let Some(ref ucs) = self.tabs[i].active_ucs {
|
||||||
self.command_line.push_error(
|
ucs_to_wcs(pt, ucs)
|
||||||
"Usage: UCS LIST | UCS SAVE <name> | UCS DELETE <name> | UCS W"
|
} 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::grip::GripEdit;
|
||||||
use crate::scene::GripDef;
|
use crate::scene::GripDef;
|
||||||
use acadrust::{CadDocument, Handle};
|
use acadrust::{CadDocument, Handle};
|
||||||
|
use acadrust::tables::Ucs;
|
||||||
use crate::linetypes;
|
use crate::linetypes;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
|
@ -28,6 +29,8 @@ pub(super) struct DocumentTab {
|
||||||
pub(super) last_cursor_world: glam::Vec3,
|
pub(super) last_cursor_world: glam::Vec3,
|
||||||
pub(super) history: HistoryState,
|
pub(super) history: HistoryState,
|
||||||
pub(super) active_layer: String,
|
pub(super) active_layer: String,
|
||||||
|
/// Currently active UCS. `None` means WCS (identity transform).
|
||||||
|
pub(super) active_ucs: Option<Ucs>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DocumentTab {
|
impl DocumentTab {
|
||||||
|
|
@ -52,6 +55,7 @@ impl DocumentTab {
|
||||||
last_cursor_world: glam::Vec3::ZERO,
|
last_cursor_world: glam::Vec3::ZERO,
|
||||||
history: HistoryState::default(),
|
history: HistoryState::default(),
|
||||||
active_layer: "0".to_string(),
|
active_layer: "0".to_string(),
|
||||||
|
active_ucs: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::ui::overlay::GridPlane;
|
use crate::ui::overlay::GridPlane;
|
||||||
use crate::scene::WireModel;
|
use crate::scene::WireModel;
|
||||||
|
use acadrust::tables::Ucs;
|
||||||
|
|
||||||
// ── Coordinate parsing ─────────────────────────────────────────────────────
|
// ── 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 {
|
pub(super) fn angle_close(a: f32, b: f32, tol: f32) -> bool {
|
||||||
let diff = (a - b).rem_euclid(std::f32::consts::TAU);
|
let diff = (a - b).rem_euclid(std::f32::consts::TAU);
|
||||||
let diff = if diff > std::f32::consts::PI {
|
let diff = if diff > std::f32::consts::PI {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use super::{H7CAD, Message, POLY_START_DELAY_MS};
|
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::{self, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX};
|
||||||
use crate::scene::grip::{find_hit_grip, GripEdit};
|
use crate::scene::grip::{find_hit_grip, GripEdit};
|
||||||
use crate::scene::object::GripApply;
|
use crate::scene::object::GripApply;
|
||||||
|
|
@ -259,8 +259,14 @@ impl H7CAD {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(pt) = parse_coord(&text) {
|
if let Some(ucs_pt) = parse_coord(&text) {
|
||||||
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt));
|
// 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 {
|
if let Some(r) = result {
|
||||||
return self.apply_cmd_result(r);
|
return self.apply_cmd_result(r);
|
||||||
}
|
}
|
||||||
|
|
@ -741,10 +747,18 @@ impl H7CAD {
|
||||||
if self.tabs[i].active_cmd.is_some() {
|
if self.tabs[i].active_cmd.is_some() {
|
||||||
let (vw, vh) = vp_size;
|
let (vw, vh) = vp_size;
|
||||||
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
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 = if let Some(ref ucs) = self.tabs[i].active_ucs {
|
||||||
let cursor_paper = cam.pick_on_target_plane(p, bounds);
|
let origin = glam::Vec3::new(
|
||||||
let view_proj = cam.view_proj(bounds);
|
ucs.origin.x as f32, ucs.origin.y as f32, ucs.origin.z as f32,
|
||||||
drop(cam);
|
);
|
||||||
|
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
|
// In MSPACE, map paper-space cursor to model space so that
|
||||||
// command previews and snapping work in the correct coordinate space.
|
// command previews and snapping work in the correct coordinate space.
|
||||||
let cursor_world = self.tabs[i].scene.paper_to_model(cursor_paper);
|
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
|
let mut pt = self.tabs[i].snap_result
|
||||||
.map(|s| self.tabs[i].scene.paper_to_model(s.world))
|
.map(|s| self.tabs[i].scene.paper_to_model(s.world))
|
||||||
.unwrap_or(cursor_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 let Some(base) = self.last_point {
|
||||||
if self.ortho_mode {
|
if self.ortho_mode {
|
||||||
pt = ortho_constrain(pt, base);
|
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 tangent_obj_at_click = snap_taken.and_then(|s| s.tangent_obj);
|
||||||
|
|
||||||
let world_pt = {
|
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.
|
// Convert paper-space → model-space when inside a viewport.
|
||||||
let raw = self.tabs[i].scene.paper_to_model(raw_paper);
|
let raw = self.tabs[i].scene.paper_to_model(raw_paper);
|
||||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||||
|
|
@ -920,7 +950,11 @@ impl H7CAD {
|
||||||
let mut pt = snap_hit
|
let mut pt = snap_hit
|
||||||
.map(|s| self.tabs[i].scene.paper_to_model(s.world))
|
.map(|s| self.tabs[i].scene.paper_to_model(s.world))
|
||||||
.unwrap_or(raw);
|
.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 let Some(base) = self.last_point {
|
||||||
if self.ortho_mode {
|
if self.ortho_mode {
|
||||||
pt = ortho_constrain(pt, base);
|
pt = ortho_constrain(pt, base);
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,47 @@ impl Camera {
|
||||||
OPENGL_TO_WGPU * proj * view
|
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 {
|
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_x = (screen.x / bounds.width) * 2.0 - 1.0;
|
||||||
let ndc_y = 1.0 - (screen.y / bounds.height) * 2.0;
|
let ndc_y = 1.0 - (screen.y / bounds.height) * 2.0;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue