forked from 1b-agent/opencadstudio-upstream
entities_centroid summed the f32 points (double-single HIGH half) alone, so at UTM scale (~5.7e6) it both quantized each term ~0.5 m and lost low bits across the running total — drifting the paste base and PASTEBLOCK's block base metres off, which placed the wrapped block at the wrong spot/scale. It now reconstructs each vertex's absolute f64 from points + points_low and accumulates in f64. clipboard_centroid is DVec3; the PASTECLIP commit delta is fully f64; define_block_from_owned_entities takes an f64 base so the block-local fold (Translate(-base)) stays precise. The interactive ghost anchors (PasteCommand, InsertBlockCommand) still take the f32 cast — display only; the committed geometry is f64. Co-Authored-By: Claude Opus 4.8 <[email protected]>
421 lines
15 KiB
Rust
421 lines
15 KiB
Rust
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,
|
||
}
|
||
}
|
||
|
||
// ── 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)
|
||
}
|
||
|
||
/// 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,
|
||
)
|
||
}
|
||
}
|
||
|
||
// ── UCS ↔ WCS transforms (thin wrappers over `UcsXform`) ────────────────────
|
||
|
||
/// 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 {
|
||
UcsXform::from_ucs(ucs).vec_to_wcs(offset)
|
||
}
|
||
|
||
/// Convert a point from UCS local coordinates to WCS.
|
||
pub(super) fn ucs_to_wcs(pt: glam::Vec3, ucs: &Ucs) -> glam::Vec3 {
|
||
UcsXform::from_ucs(ucs).to_wcs(pt)
|
||
}
|
||
|
||
/// 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 {
|
||
UcsXform::from_ucs(ucs).axes().3
|
||
}
|
||
|
||
/// 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`, 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)
|
||
} else {
|
||
glam::Vec3::new(b.x, p.y, p.z)
|
||
};
|
||
xf.to_wcs(c)
|
||
}
|
||
|
||
/// 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;
|
||
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;
|
||
xf.to_wcs(glam::Vec3::new(
|
||
b.x + dist * snapped.cos(),
|
||
b.y + dist * snapped.sin(),
|
||
p.z,
|
||
))
|
||
}
|
||
|
||
/// 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,
|
||
view_rot: glam::Mat4,
|
||
eye: glam::DVec3,
|
||
bounds: iced::Rectangle,
|
||
tol_px: f32,
|
||
xf: &UcsXform,
|
||
) -> glam::Vec3 {
|
||
let snapped = polar_constrain(pt, base, step_deg, xf);
|
||
let to_screen = |w: glam::Vec3| {
|
||
let ndc = view_rot.project_point3((w.as_dvec3() - eye).as_vec3());
|
||
(
|
||
(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
|
||
}
|
||
}
|
||
|
||
// ── Clipboard / selection helpers ──────────────────────────────────────────
|
||
|
||
/// Compute the centroid of a set of wire models (average of all points).
|
||
pub(super) fn entities_centroid(wires: &[WireModel]) -> glam::DVec3 {
|
||
// Reconstruct each vertex's absolute f64 from the double-single high/low
|
||
// pair and accumulate in f64: summing the f32 `points` alone at UTM scale
|
||
// (~5.7e6) both quantizes each term ~0.5 m and loses low bits across the
|
||
// running total, drifting the paste base / block base metres off.
|
||
let mut sum = glam::DVec3::ZERO;
|
||
let mut count = 0usize;
|
||
for w in wires {
|
||
for (i, p) in w.points.iter().enumerate() {
|
||
// Wire models carry NaN points as separators between disjoint
|
||
// segments; summing them poisons the whole centroid into NaN,
|
||
// which then makes every paste base point NaN. (#129)
|
||
if !p[0].is_finite() || !p[1].is_finite() || !p[2].is_finite() {
|
||
continue;
|
||
}
|
||
let l = w.points_low.get(i).copied().unwrap_or([0.0; 3]);
|
||
sum += glam::DVec3::new(
|
||
p[0] as f64 + l[0] as f64,
|
||
p[1] as f64 + l[1] as f64,
|
||
p[2] as f64 + l[2] as f64,
|
||
);
|
||
count += 1;
|
||
}
|
||
}
|
||
if count > 0 {
|
||
sum / count as f64
|
||
} else {
|
||
glam::DVec3::ZERO
|
||
}
|
||
}
|
||
|
||
/// Generate the next available auto group name ("*A1", "*A2", …).
|
||
pub(super) fn next_group_auto_name(scene: &crate::scene::Scene) -> String {
|
||
let existing: rustc_hash::FxHashSet<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",
|
||
Surface(_) => "surface",
|
||
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.
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
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
|
||
}
|