cad-editor/src/app/helpers.rs
Hakan Seven 47f74a9b98 feat(clipboard): paste menu with paste-to-original and paste-as-block
Turn the ribbon Clipboard "Paste" button into a dropdown:
- Paste — existing pick-a-point paste.
- Paste to Original Coordinates — drops the clipboard at its source
  coordinates with no prompt.
- Paste as Block — wraps the clipboard contents in a new block definition
  and starts an interactive insert (prompts for the drop point, with the
  geometry rubber-banding under the cursor).

Also fixes two bugs the new commands exposed:
- entities_centroid summed the NaN separator points wire models carry,
  so the paste base point came out NaN; non-finite points are now
  skipped. A NaN base previously froze block tessellation outright.
- PASTEORIG added clipboard entities with their original handles
  (duplicates on same-document paste); it now goes through
  add_entity_clone and merges referenced records like the other pastes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 16:09:47 +03:00

337 lines
12 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,
)
}
/// 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_proj: glam::Mat4,
bounds: iced::Rectangle,
tol_px: f32,
) -> glam::Vec3 {
let snapped = polar_constrain(pt, base, step_deg);
let to_screen = |w: glam::Vec3| {
let ndc = view_proj.project_point3(w);
(
(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::Vec3 {
let mut sum = glam::Vec3::ZERO;
let mut count = 0usize;
for w in wires {
for p in &w.points {
let v = glam::Vec3::from(*p);
// 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 v.is_finite() {
sum += v;
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: 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.
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
}