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 ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-05-27 11:40:28 +03:00
|
|
|
|
/// 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.
|
2026-03-22 22:44:32 +03:00
|
|
|
|
/// Accepts "x,y" → Vec3(x, y, 0)
|
|
|
|
|
|
/// "x,y,z" → Vec3(x, y, z)
|
2026-05-27 11:40:28 +03:00
|
|
|
|
/// 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
|
2026-03-22 22:44:32 +03:00
|
|
|
|
.split(|c| c == ',' || c == ';')
|
2026-05-27 11:40:28 +03:00
|
|
|
|
.map(|s| s.trim())
|
2026-03-22 22:44:32 +03:00
|
|
|
|
.filter_map(|s| s.parse().ok())
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
match parts.as_slice() {
|
2026-05-27 11:40:28 +03:00
|
|
|
|
[x, y] => Some((glam::Vec3::new(*x, *y, 0.0), kind)),
|
|
|
|
|
|
[x, y, z] => Some((glam::Vec3::new(*x, *y, *z), kind)),
|
2026-03-22 22:44:32 +03:00
|
|
|
|
_ => None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
|
// ── UCS ↔ WCS converter ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// The single bridge between WCS (how geometry and the file are stored) and the
|
|
|
|
|
|
/// active UCS (the coordinate system the user works in). Build one from the
|
|
|
|
|
|
/// tab's active UCS via [`DocumentTab::ucs_xform`](super::DocumentTab); the
|
|
|
|
|
|
/// `None` UCS yields the identity (plain WCS).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Every system that has to speak UCS — the coordinate readout, typed input,
|
|
|
|
|
|
/// the UCS icon, snap/ortho, the ViewCube — goes through this one type instead
|
|
|
|
|
|
/// of re-deriving the axis math. Axes are orthonormal, so the inverse rotation
|
|
|
|
|
|
/// is just the transpose (the dot products in `to_ucs`); no matrix inversion.
|
|
|
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
|
|
pub(super) struct UcsXform {
|
|
|
|
|
|
origin: glam::Vec3,
|
|
|
|
|
|
x: glam::Vec3,
|
|
|
|
|
|
y: glam::Vec3,
|
|
|
|
|
|
z: glam::Vec3,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl UcsXform {
|
|
|
|
|
|
/// Plain WCS — no active UCS.
|
|
|
|
|
|
pub(super) fn identity() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
origin: glam::Vec3::ZERO,
|
|
|
|
|
|
x: glam::Vec3::X,
|
|
|
|
|
|
y: glam::Vec3::Y,
|
|
|
|
|
|
z: glam::Vec3::Z,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn from_ucs(ucs: &Ucs) -> Self {
|
|
|
|
|
|
let v =
|
|
|
|
|
|
|a: acadrust::types::Vector3| glam::Vec3::new(a.x as f32, a.y as f32, a.z as f32);
|
|
|
|
|
|
let x = v(ucs.x_axis).normalize_or(glam::Vec3::X);
|
|
|
|
|
|
let y = v(ucs.y_axis).normalize_or(glam::Vec3::Y);
|
|
|
|
|
|
let z = x.cross(y).normalize_or(glam::Vec3::Z);
|
|
|
|
|
|
Self { origin: v(ucs.origin), x, y, z }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn from_active(ucs: Option<&Ucs>) -> Self {
|
|
|
|
|
|
ucs.map(Self::from_ucs).unwrap_or_else(Self::identity)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// True when this is plain WCS — lets callers skip the conversion.
|
|
|
|
|
|
pub(super) fn is_identity(&self) -> bool {
|
|
|
|
|
|
self.origin == glam::Vec3::ZERO
|
|
|
|
|
|
&& self.x == glam::Vec3::X
|
|
|
|
|
|
&& self.y == glam::Vec3::Y
|
|
|
|
|
|
&& self.z == glam::Vec3::Z
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// UCS point → WCS.
|
|
|
|
|
|
pub(super) fn to_wcs(&self, p: glam::Vec3) -> glam::Vec3 {
|
|
|
|
|
|
self.origin + self.x * p.x + self.y * p.y + self.z * p.z
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// WCS point → UCS.
|
|
|
|
|
|
pub(super) fn to_ucs(&self, p: glam::Vec3) -> glam::Vec3 {
|
|
|
|
|
|
let d = p - self.origin;
|
|
|
|
|
|
glam::Vec3::new(d.dot(self.x), d.dot(self.y), d.dot(self.z))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// UCS direction → WCS (rotation only, no origin shift).
|
|
|
|
|
|
pub(super) fn vec_to_wcs(&self, v: glam::Vec3) -> glam::Vec3 {
|
|
|
|
|
|
self.x * v.x + self.y * v.y + self.z * v.z
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// WCS direction → UCS (rotation only, no origin shift).
|
|
|
|
|
|
pub(super) fn vec_to_ucs(&self, v: glam::Vec3) -> glam::Vec3 {
|
|
|
|
|
|
glam::Vec3::new(v.dot(self.x), v.dot(self.y), v.dot(self.z))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// `(origin, x, y, z)` axes in WCS — for drawing the UCS icon.
|
|
|
|
|
|
pub(super) fn axes(&self) -> (glam::Vec3, glam::Vec3, glam::Vec3, glam::Vec3) {
|
|
|
|
|
|
(self.origin, self.x, self.y, self.z)
|
|
|
|
|
|
}
|
2026-06-22 11:17:55 +03:00
|
|
|
|
|
|
|
|
|
|
/// UCS→world rotation matrix (columns = UCS axes). For consumers that take
|
|
|
|
|
|
/// a `Mat4` rotation directly (ViewCube, OTRACK ray directions).
|
|
|
|
|
|
pub(super) fn rotation_mat(&self) -> glam::Mat4 {
|
|
|
|
|
|
glam::Mat4::from_cols(
|
|
|
|
|
|
self.x.extend(0.0),
|
|
|
|
|
|
self.y.extend(0.0),
|
|
|
|
|
|
self.z.extend(0.0),
|
|
|
|
|
|
glam::Vec4::W,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── UCS ↔ WCS transforms (thin wrappers over `UcsXform`) ────────────────────
|
|
|
|
|
|
|
2026-05-27 11:40:28 +03:00
|
|
|
|
/// 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 {
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
|
UcsXform::from_ucs(ucs).vec_to_wcs(offset)
|
2026-05-27 11:40:28 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 14:16:06 +03:00
|
|
|
|
/// Convert a point from UCS local coordinates to WCS.
|
|
|
|
|
|
pub(super) fn ucs_to_wcs(pt: glam::Vec3, ucs: &Ucs) -> glam::Vec3 {
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
|
UcsXform::from_ucs(ucs).to_wcs(pt)
|
2026-04-06 14:16:06 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 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 {
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
|
UcsXform::from_ucs(ucs).axes().3
|
2026-04-06 14:16:06 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 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
|
|
|
|
// ── 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 ─────────────────────────────────────────────
|
|
|
|
|
|
|
2026-06-22 10:00:53 +03:00
|
|
|
|
/// Constrain `pt` to the nearest 90° direction from `base`, in the active UCS
|
|
|
|
|
|
/// plane — ortho follows the user's coordinate system, not world axes. `xf` is
|
|
|
|
|
|
/// identity for plain WCS, so the world-XY behaviour is unchanged there.
|
|
|
|
|
|
pub(super) fn ortho_constrain(pt: glam::Vec3, base: glam::Vec3, xf: &UcsXform) -> glam::Vec3 {
|
|
|
|
|
|
let p = xf.to_ucs(pt);
|
|
|
|
|
|
let b = xf.to_ucs(base);
|
|
|
|
|
|
let dx = (p.x - b.x).abs();
|
|
|
|
|
|
let dy = (p.y - b.y).abs();
|
|
|
|
|
|
let c = if dx >= dy {
|
|
|
|
|
|
glam::Vec3::new(p.x, b.y, p.z)
|
2026-03-22 22:44:32 +03:00
|
|
|
|
} else {
|
2026-06-22 10:00:53 +03:00
|
|
|
|
glam::Vec3::new(b.x, p.y, p.z)
|
|
|
|
|
|
};
|
|
|
|
|
|
xf.to_wcs(c)
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 10:00:53 +03:00
|
|
|
|
/// Constrain `pt` to the nearest polar angle multiple from `base`, measured in
|
|
|
|
|
|
/// the active UCS plane (identity `xf` = world XY, Z-up).
|
|
|
|
|
|
pub(super) fn polar_constrain(
|
|
|
|
|
|
pt: glam::Vec3,
|
|
|
|
|
|
base: glam::Vec3,
|
|
|
|
|
|
step_deg: f32,
|
|
|
|
|
|
xf: &UcsXform,
|
|
|
|
|
|
) -> glam::Vec3 {
|
|
|
|
|
|
let p = xf.to_ucs(pt);
|
|
|
|
|
|
let b = xf.to_ucs(base);
|
|
|
|
|
|
let dx = p.x - b.x;
|
|
|
|
|
|
let dy = p.y - b.y;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
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;
|
2026-06-22 10:00:53 +03:00
|
|
|
|
xf.to_wcs(glam::Vec3::new(
|
|
|
|
|
|
b.x + dist * snapped.cos(),
|
|
|
|
|
|
b.y + dist * snapped.sin(),
|
|
|
|
|
|
p.z,
|
|
|
|
|
|
))
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 23:44:31 +03:00
|
|
|
|
/// Polar constraint that only engages when the cursor is within `tol_px`
|
|
|
|
|
|
/// screen pixels of the nearest polar ray; otherwise the cursor is left free
|
|
|
|
|
|
/// so POLAR behaves as if off when pointing away from every angle (issue #70).
|
|
|
|
|
|
pub(super) fn polar_constrain_near(
|
|
|
|
|
|
pt: glam::Vec3,
|
|
|
|
|
|
base: glam::Vec3,
|
|
|
|
|
|
step_deg: f32,
|
2026-06-23 23:33:21 +03:00
|
|
|
|
view_rot: glam::Mat4,
|
|
|
|
|
|
eye: glam::DVec3,
|
2026-06-09 23:44:31 +03:00
|
|
|
|
bounds: iced::Rectangle,
|
|
|
|
|
|
tol_px: f32,
|
2026-06-22 10:00:53 +03:00
|
|
|
|
xf: &UcsXform,
|
2026-06-09 23:44:31 +03:00
|
|
|
|
) -> glam::Vec3 {
|
2026-06-22 10:00:53 +03:00
|
|
|
|
let snapped = polar_constrain(pt, base, step_deg, xf);
|
2026-06-09 23:44:31 +03:00
|
|
|
|
let to_screen = |w: glam::Vec3| {
|
2026-06-23 23:33:21 +03:00
|
|
|
|
let ndc = view_rot.project_point3((w.as_dvec3() - eye).as_vec3());
|
2026-06-09 23:44:31 +03:00
|
|
|
|
(
|
|
|
|
|
|
(ndc.x + 1.0) * 0.5 * bounds.width,
|
|
|
|
|
|
(1.0 - ndc.y) * 0.5 * bounds.height,
|
|
|
|
|
|
)
|
|
|
|
|
|
};
|
|
|
|
|
|
let (cx, cy) = to_screen(pt);
|
|
|
|
|
|
let (sx, sy) = to_screen(snapped);
|
|
|
|
|
|
if ((cx - sx).powi(2) + (cy - sy).powi(2)).sqrt() <= tol_px {
|
|
|
|
|
|
snapped
|
|
|
|
|
|
} else {
|
|
|
|
|
|
pt
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
|
// ── Clipboard / selection helpers ──────────────────────────────────────────
|
|
|
|
|
|
|
2026-07-08 18:57:11 +02:00
|
|
|
|
/// Cheap copy/paste anchor: the mean of each entity's bounding-box centre.
|
|
|
|
|
|
/// Replaces averaging every tessellated wire vertex, which cost O(total
|
|
|
|
|
|
/// geometry) and stalled a whole-drawing copy. The anchor only sets which point
|
|
|
|
|
|
/// of the selection sits under the cursor, so a per-entity bbox centre is an
|
|
|
|
|
|
/// equally valid, far cheaper origin.
|
|
|
|
|
|
pub(super) fn entities_centroid_by_bbox(
|
|
|
|
|
|
doc: &acadrust::CadDocument,
|
|
|
|
|
|
handles: &[acadrust::Handle],
|
|
|
|
|
|
) -> glam::DVec3 {
|
2026-06-24 10:42:58 +03:00
|
|
|
|
let mut sum = glam::DVec3::ZERO;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let mut count = 0usize;
|
2026-07-08 18:57:11 +02:00
|
|
|
|
for &h in handles {
|
|
|
|
|
|
let Some(e) = doc.get_entity(h) else { continue };
|
|
|
|
|
|
let bb = e.as_entity().bounding_box();
|
|
|
|
|
|
let c = glam::DVec3::new(
|
|
|
|
|
|
(bb.min.x + bb.max.x) * 0.5,
|
|
|
|
|
|
(bb.min.y + bb.max.y) * 0.5,
|
|
|
|
|
|
(bb.min.z + bb.max.z) * 0.5,
|
|
|
|
|
|
);
|
|
|
|
|
|
if c.x.is_finite() && c.y.is_finite() && c.z.is_finite() {
|
|
|
|
|
|
sum += c;
|
2026-06-24 10:42:58 +03:00
|
|
|
|
count += 1;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-12 10:36:15 +03:00
|
|
|
|
if count > 0 {
|
2026-06-24 10:42:58 +03:00
|
|
|
|
sum / count as f64
|
2026-05-12 10:36:15 +03:00
|
|
|
|
} else {
|
2026-06-24 10:42:58 +03:00
|
|
|
|
glam::DVec3::ZERO
|
2026-05-12 10:36:15 +03:00
|
|
|
|
}
|
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 {
|
2026-06-05 00:35:53 +03:00
|
|
|
|
let existing: rustc_hash::FxHashSet<String> =
|
2026-03-22 22:44:32 +03:00
|
|
|
|
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-05-26 17:18:47 +03:00
|
|
|
|
crate::entities::names::ui_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",
|
2026-07-02 23:28:37 +03:00
|
|
|
|
Helix(_) => "helix",
|
2026-04-24 01:43:13 +03:00
|
|
|
|
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",
|
2026-06-14 20:11:44 +03:00
|
|
|
|
Surface(_) => "surface",
|
2026-04-24 01:43:13 +03:00
|
|
|
|
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 ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-05-23 17:16:15 +03:00
|
|
|
|
/// Builds a 32×32 RGBA icon: red background with OCS drawn in white pixels.
|
2026-06-22 02:03:34 +03:00
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
2026-03-22 22:44:32 +03:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-23 17:16:15 +03:00
|
|
|
|
// 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);
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
|
|
|
|
|
px
|
|
|
|
|
|
}
|