mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-09 03:50:04 -06:00
There was no OOPS. It now brings back what the last ERASE removed (the Delete key too), or BLOCK deleting the objects it makes a block of, as AutoCAD 2024 does (accoreconsole, the drawing's entnext order read back): - Each object keeps its handle and goes back directly above the object that was under it, or under the one that was above it when that is gone. An erase records both neighbours (EntityStore::neighbours). - Only the last erase counts; entdel doesn't. OOPS says nothing and is one undo step. - An object stays out when it is already in the drawing, when an undo took the drawing back to before it was made, when its layer, block or layout is gone, and, during REFEDIT, when it isn't in the working set. REFCLOSE forgets the working copies; the Block Editor has an OOPS of its own. - The command's label is in the 17 interface catalogs. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
135 lines
5.2 KiB
Rust
135 lines
5.2 KiB
Rust
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<Vec<Handle>> {
|
|
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<Vec<Handle>> {
|
|
let m = Mat3::scale_about(base.xy(), f);
|
|
edit_entities(s, hs, copy, |e| {
|
|
e.kind.transform(&m);
|
|
e.kind.scale_heights(base.z, f);
|
|
e.common.thickness *= f;
|
|
})
|
|
}
|
|
|
|
/// `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<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 };
|
|
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<usize> {
|
|
let d = s.doc()?;
|
|
let free: Vec<Handle> = 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<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"));
|
|
}
|
|
// 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::<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, 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::<Vec<_>>() }))
|
|
}
|