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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.7 committed 2026-05-27 11:40:28 +03:00
1 parent 451239c4f9
commit 874226bd43
5 files changed
+77 -81

No files matched your search

+4 -2
View File
@@ -3256,8 +3256,10 @@ impl OpenCADStudio {
// 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
if let Some((pt, _)) = super::helpers::parse_coord(coord_str) {
// `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 {
ucs_to_wcs(pt, ucs)
} else {
+38 -17
View File
@@ -4,23 +4,54 @@ use acadrust::tables::Ucs;
// ── 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)
/// "x,y,z" → Vec3(x, y, z)
/// Separators: comma or semicolon; decimal point or decimal comma.
pub(super) fn parse_coord(text: &str) -> Option<glam::Vec3> {
let parts: Vec<f32> = text
/// A leading `@` marks the value relative to the last point; a leading
/// `#` forces absolute. Separators: comma or semicolon.
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 == ';')
.map(|s| s.trim().replace(',', "."))
.map(|s| s.trim())
.filter_map(|s| s.parse().ok())
.collect();
match parts.as_slice() {
[x, y] => Some(glam::Vec3::new(*x, *y, 0.0)),
[x, y, z] => Some(glam::Vec3::new(*x, *y, *z)),
[x, y] => Some((glam::Vec3::new(*x, *y, 0.0), kind)),
[x, y, z] => Some((glam::Vec3::new(*x, *y, *z), kind)),
_ => 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 ───────────────────────────────────────────────────
/// 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
}
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 ───────────────────────────────────────────────────
/// Choose the grid plane whose normal is most aligned with the camera view direction.
+35 -7
View File
@@ -1,5 +1,6 @@
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 crate::modules::ModuleEvent;
@@ -960,13 +961,40 @@ impl OpenCADStudio {
return Task::none();
}
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
if let Some((coord, kind)) = parse_coord(&text) {
// Resolve relative vs absolute. `@` forces relative,
// `#` forces absolute, no prefix follows DYN: when
// dynamic input is on, bare coordinates are relative
// to the last point (issue #26).
// `@` 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));
if let Some(r) = result {
return self.apply_cmd_result(r);
-7
View File
@@ -2211,13 +2211,6 @@ impl Scene {
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
/// the given paper-space point, or `None` if no viewport matches.
pub fn viewport_at_paper_point(&self, px: f32, py: f32) -> Option<Handle> {
-48
View File
@@ -83,54 +83,6 @@ impl CubeRegion {
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 {
match self {
CubeRegion::Face(FACE_TOP) => CubeRegion::Face(FACE_BOTTOM),