Files
OneCAD-Draft/src/app/helpers.rs
T
Hakan SevenandClaude Opus 4.7 874226bd43 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]>
2026-05-27 11:40:28 +03:00

303 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::scene::WireModel;
use crate::ui::overlay::GridPlane;
use acadrust::tables::Ucs;
// ── Coordinate parsing ─────────────────────────────────────────────────────
/// 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)
/// 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())
.filter_map(|s| s.parse().ok())
.collect();
match parts.as_slice() {
[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.
///
/// 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
}
/// 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
}
// ── Grid plane detection ───────────────────────────────────────────────────
/// Choose the grid plane whose normal is most aligned with the camera view direction.
pub(super) fn grid_plane_from_camera(pitch: f32, yaw: f32) -> GridPlane {
let fz = pitch.sin().abs();
let fy = (pitch.cos() * yaw.cos()).abs();
let fx = (pitch.cos() * yaw.sin()).abs();
if fz >= fy && fz >= fx {
GridPlane::Xy
} else if fy >= fx {
GridPlane::Xz
} else {
GridPlane::Yz
}
}
// ── Drawing constraint helpers ─────────────────────────────────────────────
/// Constrain `pt` to the nearest 90° direction from `base` (XY plane, Z-up).
pub(super) fn ortho_constrain(pt: glam::Vec3, base: glam::Vec3) -> glam::Vec3 {
let dx = (pt.x - base.x).abs();
let dy = (pt.y - base.y).abs();
if dx >= dy {
glam::Vec3::new(pt.x, base.y, pt.z)
} else {
glam::Vec3::new(base.x, pt.y, pt.z)
}
}
/// Constrain `pt` to the nearest polar angle multiple from `base` (XY plane, Z-up).
pub(super) fn polar_constrain(pt: glam::Vec3, base: glam::Vec3, step_deg: f32) -> glam::Vec3 {
let dx = pt.x - base.x;
let dy = pt.y - base.y;
let dist = (dx * dx + dy * dy).sqrt();
if dist < 1e-6 {
return pt;
}
let step = step_deg.to_radians();
let angle = dy.atan2(dx);
let snapped = (angle / step).round() * step;
glam::Vec3::new(
base.x + dist * snapped.cos(),
base.y + dist * snapped.sin(),
pt.z,
)
}
// ── Clipboard / selection helpers ──────────────────────────────────────────
/// Compute the centroid of a set of wire models (average of all points).
pub(super) fn entities_centroid(wires: &[WireModel]) -> glam::Vec3 {
let mut sum = glam::Vec3::ZERO;
let mut count = 0usize;
for w in wires {
for p in &w.points {
sum += glam::Vec3::from(*p);
count += 1;
}
}
if count > 0 {
sum / count as f32
} else {
glam::Vec3::ZERO
}
}
/// Generate the next available auto group name ("*A1", "*A2", …).
pub(super) fn next_group_auto_name(scene: &crate::scene::Scene) -> String {
let existing: std::collections::HashSet<String> =
scene.groups().map(|g| g.name.clone()).collect();
for n in 1..=9999 {
let name = format!("*A{n}");
if !existing.contains(&name) {
return name;
}
}
"*A".to_string()
}
// ── Entity type labels ─────────────────────────────────────────────────────
pub(super) fn entity_type_label(entity: &acadrust::EntityType) -> String {
crate::entities::names::ui_name(entity).to_string()
}
pub(super) fn entity_type_key(entity: &acadrust::EntityType) -> String {
use acadrust::EntityType::*;
match entity {
Point(_) => "point",
Line(_) => "line",
Circle(_) => "circle",
Arc(_) => "arc",
Ellipse(_) => "ellipse",
Spline(_) => "spline",
LwPolyline(_) | Polyline(_) => "pline",
Polyline2D(_) => "pline2d",
Polyline3D(_) => "pline3d",
PolyfaceMesh(_) => "polyface",
PolygonMesh(_) => "polymesh",
Text(_) => "text",
MText(_) => "mtext",
Dimension(_) => "dimension",
Leader(_) => "leader",
MultiLeader(_) => "multileader",
Tolerance(_) => "tolerance",
Insert(_) => "insert",
Block(_) => "block",
BlockEnd(_) => "blockend",
Hatch(_) => "hatch",
Solid(_) => "solid",
Face3D(_) => "face3d",
Solid3D(_) => "solid3d",
Region(_) => "region",
Body(_) => "body",
Mesh(_) => "mesh",
Ray(_) => "ray",
XLine(_) => "xline",
MLine(_) => "mline",
Viewport(_) => "viewport",
RasterImage(_) => "rasterimage",
Wipeout(_) => "wipeout",
Underlay(_) => "underlay",
Shape(_) => "shape",
Table(_) => "table",
AttributeDefinition(_) => "attdef",
AttributeEntity(_) => "attrib",
Ole2Frame(_) => "ole2frame",
Seqend(_) => "seqend",
Unknown(_) => "unknown",
}
.to_string()
}
pub(super) fn title_case_word(value: &str) -> String {
let mut chars = value.chars();
match chars.next() {
Some(first) => {
let mut out = first.to_uppercase().collect::<String>();
out.push_str(chars.as_str());
out
}
None => String::new(),
}
}
// ── Window icon ────────────────────────────────────────────────────────────
/// Builds a 32×32 RGBA icon: red background with OCS drawn in white pixels.
pub(super) fn build_window_icon() -> Vec<u8> {
const W: usize = 32;
const SZ: usize = W * W * 4;
let bg = [176u8, 48, 32, 255];
let fg = [255u8, 255, 255, 255];
let mut px = vec![0u8; SZ];
for i in 0..W * W {
px[i * 4..i * 4 + 4].copy_from_slice(&bg);
}
fn stroke(px: &mut Vec<u8>, ax: i32, ay: i32, bx: i32, by: i32, fg: [u8; 4]) {
let steps = ((bx - ax).abs().max((by - ay).abs()) * 3).max(1);
for s in 0..=steps {
let t = s as f32 / steps as f32;
let cx = ax as f32 + (bx - ax) as f32 * t;
let cy = ay as f32 + (by - ay) as f32 * t;
for dy in -1i32..=1 {
for dx in -1i32..=1 {
let ix = cx.round() as i32 + dx;
let iy = cy.round() as i32 + dy;
if ix >= 0 && ix < W as i32 && iy >= 0 && iy < W as i32 {
let idx = (iy as usize * W + ix as usize) * 4;
px[idx..idx + 4].copy_from_slice(&fg);
}
}
}
}
}
// O
stroke(&mut px, 3, 6, 9, 6, fg);
stroke(&mut px, 3, 25, 9, 25, fg);
stroke(&mut px, 3, 6, 3, 25, fg);
stroke(&mut px, 9, 6, 9, 25, fg);
// C
stroke(&mut px, 12, 6, 18, 6, fg);
stroke(&mut px, 12, 25, 18, 25, fg);
stroke(&mut px, 12, 6, 12, 25, fg);
// S
stroke(&mut px, 21, 6, 27, 6, fg);
stroke(&mut px, 21, 6, 21, 15, fg);
stroke(&mut px, 21, 15, 27, 15, fg);
stroke(&mut px, 27, 15, 27, 25, fg);
stroke(&mut px, 21, 25, 27, 25, fg);
px
}