use super::{Entity, Handle, Mat3, Result, Session, Value, Vec2, bad, bool_or, 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> { 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 }; ne.kind.transform(m); 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, |e| e.kind.transform(m))?; out.push(*h); } } Ok(out) } pub(super) fn erase(s: &mut Session, hs: &[Handle]) -> Result { let d = s.doc_mut()?; let mut n = 0; for h in hs { let locked = d.entity(*h).and_then(|e| d.layer(&e.common.layer)).is_some_and(|l| l.locked); if !locked && d.remove_entity(*h).is_some() { n += 1; } } 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")); } let r = transform_entities(s, &hs, &Mat3::scale_about(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, 1e-12) { 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::>() })) }