use cadcraft_geom::Vec3; use super::{Entity, Handle, Mat3, Result, Session, Value, Vec2, bad, bool_or, erase_kept, f64_req, json, point_param, point_req, targets}; // ---------------- core operations ---------------- pub(crate) fn transform_entities(s: &mut Session, hs: &[Handle], m: &Mat3, copy: bool) -> Result> { refuse_ole_frames(s, hs, m, copy)?; edit_entities(s, hs, copy, |e| e.kind.transform(m)) } /// Scale `hs` by `f` about `base` in 3D, as AutoCAD 2024's SCALE does: about the base point's XY, /// and every height of the objects about its Z, their thickness too. pub(crate) fn scale_entities(s: &mut Session, hs: &[Handle], base: Vec3, f: f64, copy: bool) -> Result> { let m = Mat3::scale_about(base.xy(), f); refuse_ole_frames(s, hs, &m, copy)?; edit_entities(s, hs, copy, |e| { e.kind.transform(&m); e.kind.scale_heights(base.z, f); e.common.thickness *= f; }) } /// An OLE2FRAME only translates or scales uniformly (its corners stay axis aligned): refuse any other `m` before anything /// changes, instead of reporting success and leaving the frame where it was. pub(crate) fn refuse_ole_frames(s: &Session, hs: &[Handle], m: &Mat3, copy: bool) -> Result<()> { if cadcraft_doc::ole_frame_accepts(m) || !holds_ole_frame(s, hs, copy)? { return Ok(()); } Err(ole_refusal()) } /// The refusal for a transform an OLE2FRAME cannot take. pub(crate) fn ole_refusal() -> crate::EngineError { crate::EngineError::Other( "An OLE object can only be moved, copied or scaled uniformly; other transforms (rotation, mirroring, non-uniform scale, alignment) are not supported.".into(), ) } /// Whether `hs` holds an OLE2FRAME a transform would touch. An in-place edit (`copy` false) skips /// an object on a locked layer (see edit_entities), so such a frame cannot be refused. pub(crate) fn holds_ole_frame(s: &Session, hs: &[Handle], copy: bool) -> Result { let d = s.doc()?; Ok(hs.iter().any(|h| { d.entity(*h).is_some_and(|e| { let locked = !copy && d.layer(&e.common.layer).is_some_and(|l| l.locked); !locked && matches!(&e.kind, cadcraft_doc::EntityKind::Unknown(u) if u.is_ole_frame()) }) })) } /// `p` at the current elevation (ELEVATION), the Z AutoCAD gives a point entered without one. pub(crate) fn at_elevation(s: &Session, p: Vec2) -> Vec3 { let z = s.doc().map(|d| d.header.f64("ELEVATION", 0.0)).ok().filter(|z| z.is_finite()); Vec3::new(p.x, p.y, z.unwrap_or(0.0)) } /// Apply `edit` to each of `hs` still in the drawing, or to a copy of each when `copy`; an object /// on a locked layer is left alone unless it is copied. The handles edited or made. fn edit_entities(s: &mut Session, hs: &[Handle], copy: bool, edit: impl Fn(&mut Entity)) -> Result> { let d = s.doc_mut()?; let mut out = Vec::new(); for h in hs { let Some(sp) = d.space_of(*h) else { continue }; if copy { let Some(e) = d.entity(*h).map(|e| (**e).clone()) else { continue }; let nh = d.new_handle(); let mut ne = Entity { handle: nh, ..e }; edit(&mut ne); if let Some(st) = d.space_mut(&sp) { st.push(ne); out.push(nh); } } else { let locked = d.entity(*h).and_then(|e| d.layer(&e.common.layer)).is_some_and(|l| l.locked); if locked { continue; } d.modify_entity(*h, &edit)?; out.push(*h); } } Ok(out) } pub(super) fn erase(s: &mut Session, hs: &[Handle]) -> Result { let d = s.doc()?; let free: Vec = hs.iter().copied().filter(|h| !d.entity(*h).and_then(|e| d.layer(&e.common.layer)).is_some_and(|l| l.locked)).collect(); let n = erase_kept(s, &free)?; s.set_selection(Vec::new()); Ok(n) } pub(super) fn run_erase(s: &mut Session, p: &Value) -> Result { let hs = targets(s, p)?; let n = erase(s, &hs)?; Ok(json!({ "erased": n })) } pub(super) fn delta_of(cmd: &str, p: &Value) -> Result { if let Some(d) = point_param(p, "delta") { return Ok(d); } let a = point_req(cmd, p, "from")?; let b = point_req(cmd, p, "to")?; Ok(b - a) } pub(super) fn run_move(s: &mut Session, p: &Value) -> Result { let hs = targets(s, p)?; let d = delta_of("move", p)?; let moved = transform_entities(s, &hs, &Mat3::translate(d), false)?; Ok(json!({ "moved": moved.len() })) } pub(super) fn run_copy(s: &mut Session, p: &Value) -> Result { let hs = targets(s, p)?; let d = delta_of("copy", p)?; let n = match p.get("count") { None => 1, Some(v) => v.as_u64().filter(|n| (1..=10_000).contains(n)).ok_or_else(|| bad("copy", "`count` must be an integer from 1 to 10000"))?, }; let mut out = Vec::new(); for k in 1..=n { out.extend(transform_entities(s, &hs, &Mat3::translate(d * k as f64), true)?.into_iter().map(|h| h.hex())); } Ok(json!({ "handles": out })) } pub(super) fn run_rotate(s: &mut Session, p: &Value) -> Result { let hs = targets(s, p)?; let base = point_req("rotate", p, "base")?; let a = f64_req("rotate", p, "angle")?.to_radians(); let r = transform_entities(s, &hs, &Mat3::rotate_about(base, a), bool_or(p, "copy", false))?; Ok(json!({ "handles": r.iter().map(|h| h.hex()).collect::>() })) } pub(super) fn run_scale(s: &mut Session, p: &Value) -> Result { let hs = targets(s, p)?; let base = point_req("scale", p, "base")?; let f = f64_req("scale", p, "factor")?; if f <= 0.0 { return Err(bad("scale", "factor must be positive")); } // A Z given with the base point, as AutoCAD takes one; without one, the current elevation. let base = match p.get("base").and_then(|b| b.get(2)) { Some(z) => z.as_f64().map(|z| Vec3::new(base.x, base.y, z)).ok_or_else(|| bad("scale", "`base` Z must be a number"))?, None => at_elevation(s, base), }; let r = scale_entities(s, &hs, base, f, bool_or(p, "copy", false))?; Ok(json!({ "handles": r.iter().map(|h| h.hex()).collect::>() })) } pub(super) fn run_mirror(s: &mut Session, p: &Value) -> Result { let hs = targets(s, p)?; let a = point_req("mirror", p, "p1")?; let b = point_req("mirror", p, "p2")?; if a.near(b, cadcraft_geom::EPS) { return Err(bad("mirror", "mirror line points coincide")); } let erase_src = bool_or(p, "erase", false); let r = transform_entities(s, &hs, &Mat3::mirror(a, b), !erase_src)?; Ok(json!({ "handles": r.iter().map(|h| h.hex()).collect::>() })) }