mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-09 12:00:50 -06:00
copy_all scanned the selection for an OLE frame once per matrix before copying. It now looks at the objects once and then only tests each matrix. Covered by the existing refusal tests (classic_polar_array_refuses_a_turned_frame_but_not_an_upright_one).
165 lines
6.6 KiB
Rust
165 lines
6.6 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>> {
|
|
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<Vec<Handle>> {
|
|
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<bool> {
|
|
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<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<_>>() }))
|
|
}
|