2026-03-22 22:44:32 +03:00
|
|
|
|
use crate::scene::WireModel;
|
2026-05-12 10:36:15 +03:00
|
|
|
|
use crate::ui::overlay::GridPlane;
|
2026-04-06 14:16:06 +03:00
|
|
|
|
use acadrust::tables::Ucs;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
|
|
|
|
|
// ── Coordinate parsing ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// Parse a typed coordinate string into a world-space Vec3.
|
|
|
|
|
|
/// 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
|
|
|
|
|
|
.split(|c| c == ',' || c == ';')
|
|
|
|
|
|
.map(|s| s.trim().replace(',', "."))
|
|
|
|
|
|
.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)),
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 14:16:06 +03:00
|
|
|
|
// ── 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 {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
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,
|
|
|
|
|
|
);
|
2026-04-06 14:16:06 +03:00
|
|
|
|
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 {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
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,
|
|
|
|
|
|
);
|
2026-04-06 14:16:06 +03:00
|
|
|
|
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*");
|
2026-05-12 10:36:15 +03:00
|
|
|
|
ucs.origin = acadrust::types::Vector3::new(origin.x as f64, origin.y as f64, origin.z as f64);
|
2026-04-06 14:16:06 +03:00
|
|
|
|
ucs.x_axis = acadrust::types::Vector3::new(cos, sin, 0.0);
|
|
|
|
|
|
ucs.y_axis = acadrust::types::Vector3::new(-sin, cos, 0.0);
|
|
|
|
|
|
ucs
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
|
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.
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-12 10:36:15 +03:00
|
|
|
|
if count > 0 {
|
|
|
|
|
|
sum / count as f32
|
|
|
|
|
|
} else {
|
|
|
|
|
|
glam::Vec3::ZERO
|
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 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 {
|
2026-04-24 01:44:33 +03:00
|
|
|
|
crate::scene::properties::entity_type_name(entity).to_string()
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn entity_type_key(entity: &acadrust::EntityType) -> String {
|
2026-04-24 01:43:13 +03:00
|
|
|
|
use acadrust::EntityType::*;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
match entity {
|
2026-04-24 01:43:13 +03:00
|
|
|
|
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",
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
.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 H7 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// H
|
|
|
|
|
|
stroke(&mut px, 4, 5, 4, 26, fg);
|
|
|
|
|
|
stroke(&mut px, 13, 5, 13, 26, fg);
|
|
|
|
|
|
stroke(&mut px, 4, 15, 13, 15, fg);
|
|
|
|
|
|
// 7
|
|
|
|
|
|
stroke(&mut px, 17, 5, 27, 5, fg);
|
|
|
|
|
|
stroke(&mut px, 27, 5, 20, 26, fg);
|
|
|
|
|
|
stroke(&mut px, 20, 16, 26, 16, fg);
|
|
|
|
|
|
|
|
|
|
|
|
px
|
|
|
|
|
|
}
|