Files
cadcraft/crates/engine/src/cmd/modify/transform.rs
T
NetterSilverandClaude Haiku 5.5 b9b7b3d439 Refuse invalid count in JSON copy instead of clamping it (#2294)
Found while testing crates/engine/src/cmd/modify/transform.rs:69 (run_copy)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-29 21:21:58 -04:00

110 lines
3.9 KiB
Rust

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<Vec<Handle>> {
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<usize> {
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<Value> {
let hs = targets(s, p)?;
let n = erase(s, &hs)?;
Ok(json!({ "erased": n }))
}
pub(super) fn delta_of(cmd: &str, p: &Value) -> Result<Vec2> {
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<Value> {
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<Value> {
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<Value> {
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::<Vec<_>>() }))
}
pub(super) fn run_scale(s: &mut Session, p: &Value) -> Result<Value> {
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::<Vec<_>>() }))
}
pub(super) fn run_mirror(s: &mut Session, p: &Value) -> Result<Value> {
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::<Vec<_>>() }))
}