feat(input): relative coordinate entry (@/#) and DYN default — #26
Koordinat girişi artık göreli/mutlak ayrımını destekliyor: - `@x,y[,z]` → son girilen noktaya göreli (UCS eksenlerinde döndürülür) - `#x,y[,z]` → her zaman mutlak (DYN durumundan bağımsız) - ön ek yok → DYN açıksa göreli, kapalıysa mutlak İlk noktada referans olmadığından göreli giriş mutlağa düşer. Çözülen WCS noktası last_point olarak saklanıyor, böylece ardışık göreli girişler zincirleniyor. parse_coord artık (Vec3, CoordKind) döndürüyor; relative offset için ucs_rotate_vec eklendi (origin translation olmadan UCS rotasyonu). UCS ORIGIN komutu prefix'i yok sayıp mutlak kalıyor. Ayrıca ViewCube snap refactor'undan kalan kullanılmayan kod temizlendi: angle_close, active_viewport_yaw_pitch, CubeRegion::snap_angles. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
451239c4f9
commit
874226bd43
5 changed files with 77 additions and 81 deletions
|
|
@ -3256,8 +3256,10 @@ impl OpenCADStudio {
|
||||||
// UCS ORIGIN x,y,z — shift the active UCS origin, keep axes
|
// UCS ORIGIN x,y,z — shift the active UCS origin, keep axes
|
||||||
"ORIGIN" | "O" => {
|
"ORIGIN" | "O" => {
|
||||||
let coord_str = parts.get(2).copied().unwrap_or("");
|
let coord_str = parts.get(2).copied().unwrap_or("");
|
||||||
if let Some(pt) = super::helpers::parse_coord(coord_str) {
|
if let Some((pt, _)) = super::helpers::parse_coord(coord_str) {
|
||||||
// `pt` is in current UCS space; convert to WCS
|
// `pt` is in current UCS space; convert to WCS.
|
||||||
|
// The @/# relative-coordinate prefix is ignored
|
||||||
|
// here — a UCS origin is always absolute.
|
||||||
let wcs_origin = if let Some(ref ucs) = self.tabs[i].active_ucs {
|
let wcs_origin = if let Some(ref ucs) = self.tabs[i].active_ucs {
|
||||||
ucs_to_wcs(pt, ucs)
|
ucs_to_wcs(pt, ucs)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,54 @@ use acadrust::tables::Ucs;
|
||||||
|
|
||||||
// ── Coordinate parsing ─────────────────────────────────────────────────────
|
// ── Coordinate parsing ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Parse a typed coordinate string into a world-space Vec3.
|
/// How a typed coordinate should be interpreted relative to the last
|
||||||
|
/// input point.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub(super) enum CoordKind {
|
||||||
|
/// `@x,y` prefix — offset from the last input point.
|
||||||
|
Relative,
|
||||||
|
/// `#x,y` prefix — world/UCS absolute, overriding DYN.
|
||||||
|
Absolute,
|
||||||
|
/// No prefix — the caller decides (DYN on → relative, off → absolute).
|
||||||
|
Default,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a typed coordinate string into a Vec3 plus its interpretation.
|
||||||
/// Accepts "x,y" → Vec3(x, y, 0)
|
/// Accepts "x,y" → Vec3(x, y, 0)
|
||||||
/// "x,y,z" → Vec3(x, y, z)
|
/// "x,y,z" → Vec3(x, y, z)
|
||||||
/// Separators: comma or semicolon; decimal point or decimal comma.
|
/// A leading `@` marks the value relative to the last point; a leading
|
||||||
pub(super) fn parse_coord(text: &str) -> Option<glam::Vec3> {
|
/// `#` forces absolute. Separators: comma or semicolon.
|
||||||
let parts: Vec<f32> = text
|
pub(super) fn parse_coord(text: &str) -> Option<(glam::Vec3, CoordKind)> {
|
||||||
|
let trimmed = text.trim();
|
||||||
|
let (kind, rest) = if let Some(r) = trimmed.strip_prefix('@') {
|
||||||
|
(CoordKind::Relative, r)
|
||||||
|
} else if let Some(r) = trimmed.strip_prefix('#') {
|
||||||
|
(CoordKind::Absolute, r)
|
||||||
|
} else {
|
||||||
|
(CoordKind::Default, trimmed)
|
||||||
|
};
|
||||||
|
let parts: Vec<f32> = rest
|
||||||
.split(|c| c == ',' || c == ';')
|
.split(|c| c == ',' || c == ';')
|
||||||
.map(|s| s.trim().replace(',', "."))
|
.map(|s| s.trim())
|
||||||
.filter_map(|s| s.parse().ok())
|
.filter_map(|s| s.parse().ok())
|
||||||
.collect();
|
.collect();
|
||||||
match parts.as_slice() {
|
match parts.as_slice() {
|
||||||
[x, y] => Some(glam::Vec3::new(*x, *y, 0.0)),
|
[x, y] => Some((glam::Vec3::new(*x, *y, 0.0), kind)),
|
||||||
[x, y, z] => Some(glam::Vec3::new(*x, *y, *z)),
|
[x, y, z] => Some((glam::Vec3::new(*x, *y, *z), kind)),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rotate a UCS-local offset into WCS without applying the origin
|
||||||
|
/// translation — used for relative coordinate entry, where only the
|
||||||
|
/// axis orientation matters, not the UCS origin.
|
||||||
|
pub(super) fn ucs_rotate_vec(offset: glam::Vec3, 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);
|
||||||
|
let z = ucs_z_axis(ucs);
|
||||||
|
x * offset.x + y * offset.y + z * offset.z
|
||||||
|
}
|
||||||
|
|
||||||
// ── UCS ↔ WCS transforms ───────────────────────────────────────────────────
|
// ── UCS ↔ WCS transforms ───────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Convert a point from UCS local coordinates to WCS.
|
/// Convert a point from UCS local coordinates to WCS.
|
||||||
|
|
@ -72,16 +103,6 @@ pub(super) fn ucs_rotated_z(origin: glam::Vec3, angle_z: f32) -> Ucs {
|
||||||
ucs
|
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 {
|
|
||||||
diff - std::f32::consts::TAU
|
|
||||||
} else {
|
|
||||||
diff
|
|
||||||
};
|
|
||||||
diff.abs() < tol
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Grid plane detection ───────────────────────────────────────────────────
|
// ── Grid plane detection ───────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Choose the grid plane whose normal is most aligned with the camera view direction.
|
/// Choose the grid plane whose normal is most aligned with the camera view direction.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use super::helpers::{
|
use super::helpers::{
|
||||||
angle_close, ortho_constrain, parse_coord, polar_constrain, ucs_to_wcs, ucs_z_axis,
|
ortho_constrain, parse_coord, polar_constrain, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis,
|
||||||
|
CoordKind,
|
||||||
};
|
};
|
||||||
use super::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
use super::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
||||||
use crate::modules::ModuleEvent;
|
use crate::modules::ModuleEvent;
|
||||||
|
|
@ -960,13 +961,40 @@ impl OpenCADStudio {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ucs_pt) = parse_coord(&text) {
|
if let Some((coord, kind)) = parse_coord(&text) {
|
||||||
// Typed coordinates are in active UCS space; convert to WCS.
|
// Resolve relative vs absolute. `@` forces relative,
|
||||||
let wcs_pt = if let Some(ref ucs) = self.tabs[i].active_ucs {
|
// `#` forces absolute, no prefix follows DYN: when
|
||||||
ucs_to_wcs(ucs_pt, ucs)
|
// dynamic input is on, bare coordinates are relative
|
||||||
} else {
|
// to the last point (issue #26).
|
||||||
ucs_pt
|
// `@` forces relative, `#` forces absolute, no
|
||||||
|
// prefix follows DYN (on → relative). The very
|
||||||
|
// first point has no reference, so relative falls
|
||||||
|
// back to absolute.
|
||||||
|
let want_relative = match kind {
|
||||||
|
CoordKind::Relative => true,
|
||||||
|
CoordKind::Absolute => false,
|
||||||
|
CoordKind::Default => self.dyn_input,
|
||||||
};
|
};
|
||||||
|
let ucs = self.tabs[i].active_ucs.clone();
|
||||||
|
let wcs_pt = match (want_relative, self.last_point) {
|
||||||
|
(true, Some(base)) => {
|
||||||
|
// Offset from the last point, rotated by the
|
||||||
|
// UCS axes (no origin translation).
|
||||||
|
let offset = match &ucs {
|
||||||
|
Some(u) => ucs_rotate_vec(coord, u),
|
||||||
|
None => coord,
|
||||||
|
};
|
||||||
|
base + offset
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Absolute: typed coordinates are in active UCS.
|
||||||
|
match &ucs {
|
||||||
|
Some(u) => ucs_to_wcs(coord, u),
|
||||||
|
None => coord,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.last_point = Some(wcs_pt);
|
||||||
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(wcs_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);
|
||||||
|
|
|
||||||
|
|
@ -2211,13 +2211,6 @@ impl Scene {
|
||||||
self.camera.borrow().view_rotation_mat()
|
self.camera.borrow().view_rotation_mat()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return (yaw, pitch) of the active viewport's camera, or None if PSPACE.
|
|
||||||
pub fn active_viewport_yaw_pitch(&self) -> Option<(f32, f32)> {
|
|
||||||
let h = self.active_viewport?;
|
|
||||||
let cam = self.camera_for_viewport(h)?;
|
|
||||||
Some((cam.yaw, cam.pitch))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the handle of the user viewport whose bounding rectangle contains
|
/// Return the handle of the user viewport whose bounding rectangle contains
|
||||||
/// the given paper-space point, or `None` if no viewport matches.
|
/// the given paper-space point, or `None` if no viewport matches.
|
||||||
pub fn viewport_at_paper_point(&self, px: f32, py: f32) -> Option<Handle> {
|
pub fn viewport_at_paper_point(&self, px: f32, py: f32) -> Option<Handle> {
|
||||||
|
|
|
||||||
|
|
@ -83,54 +83,6 @@ impl CubeRegion {
|
||||||
glam::Vec3::new(c[0], c[1], c[2]).normalize_or(glam::Vec3::Z)
|
glam::Vec3::new(c[0], c[1], c[2]).normalize_or(glam::Vec3::Z)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `(yaw, pitch, roll)` for the canonical camera orientation
|
|
||||||
/// pointed at this region.
|
|
||||||
///
|
|
||||||
/// `yaw` (rotate around world Z) and `pitch` (tilt toward world Z)
|
|
||||||
/// place the camera on the right side of the cube; `roll` rotates
|
|
||||||
/// around the camera's view axis so edge views land with the edge
|
|
||||||
/// vertical on screen instead of horizontal. Faces and corners
|
|
||||||
/// keep `roll = 0` (world up stays up).
|
|
||||||
pub fn snap_angles(self) -> (f32, f32, f32) {
|
|
||||||
use std::f32::consts::{FRAC_PI_2, FRAC_PI_4, PI};
|
|
||||||
let iso = (1.0_f32 / 2.0_f32.sqrt()).atan();
|
|
||||||
// For the 8 horizontal-cube edges (top + bottom rings), roll by
|
|
||||||
// ±π/2 around the camera's view axis so the edge sits vertical
|
|
||||||
// on screen — TOP face on one half, the side face on the other.
|
|
||||||
// The 4 vertical edges (FRONT-RIGHT etc.) already run along Z
|
|
||||||
// and read vertical with roll = 0.
|
|
||||||
let r = FRAC_PI_2;
|
|
||||||
match self.id() {
|
|
||||||
0 => (0.0, FRAC_PI_2, 0.0),
|
|
||||||
1 => (0.0, -FRAC_PI_2, 0.0),
|
|
||||||
2 => (0.0, 0.0, 0.0), // FRONT
|
|
||||||
3 => (PI, 0.0, 0.0), // BACK
|
|
||||||
4 => (FRAC_PI_2, 0.0, 0.0), // RIGHT
|
|
||||||
5 => (-FRAC_PI_2, 0.0, 0.0), // LEFT
|
|
||||||
6 => (0.0, FRAC_PI_4, r), // EDGE_TOP_FRONT
|
|
||||||
7 => (PI, FRAC_PI_4, r), // EDGE_TOP_BACK
|
|
||||||
8 => (FRAC_PI_2, FRAC_PI_4, r), // EDGE_TOP_RIGHT
|
|
||||||
9 => (-FRAC_PI_2, FRAC_PI_4, r), // EDGE_TOP_LEFT
|
|
||||||
10 => (0.0, -FRAC_PI_4, r), // EDGE_BOT_FRONT
|
|
||||||
11 => (PI, -FRAC_PI_4, r), // EDGE_BOT_BACK
|
|
||||||
12 => (FRAC_PI_2, -FRAC_PI_4, r), // EDGE_BOT_RIGHT
|
|
||||||
13 => (-FRAC_PI_2, -FRAC_PI_4, r), // EDGE_BOT_LEFT
|
|
||||||
14 => (FRAC_PI_4, 0.0, 0.0), // EDGE_FRONT_RIGHT
|
|
||||||
15 => (-FRAC_PI_4, 0.0, 0.0), // EDGE_FRONT_LEFT
|
|
||||||
16 => (PI * 0.75, 0.0, 0.0), // EDGE_BACK_RIGHT
|
|
||||||
17 => (-PI * 0.75, 0.0, 0.0), // EDGE_BACK_LEFT
|
|
||||||
18 => (FRAC_PI_4, iso, 0.0), // CORNER_TPF_R
|
|
||||||
19 => (-FRAC_PI_4, iso, 0.0), // CORNER_TPF_L
|
|
||||||
20 => (PI * 0.75, iso, 0.0), // CORNER_TBK_R
|
|
||||||
21 => (-PI * 0.75, iso, 0.0), // CORNER_TBK_L
|
|
||||||
22 => (FRAC_PI_4, -iso, 0.0), // CORNER_BTF_R
|
|
||||||
23 => (-FRAC_PI_4, -iso, 0.0), // CORNER_BTF_L
|
|
||||||
24 => (PI * 0.75, -iso, 0.0), // CORNER_BBK_R
|
|
||||||
25 => (-PI * 0.75, -iso, 0.0), // CORNER_BBK_L
|
|
||||||
_ => (0.0, 0.0, 0.0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn opposite(self) -> CubeRegion {
|
pub fn opposite(self) -> CubeRegion {
|
||||||
match self {
|
match self {
|
||||||
CubeRegion::Face(FACE_TOP) => CubeRegion::Face(FACE_BOTTOM),
|
CubeRegion::Face(FACE_TOP) => CubeRegion::Face(FACE_BOTTOM),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue