Files
cadcraft/crates/engine/src/cmd/modify/oops.rs
T
2ez4gcxandClaude Opus 5.5 b006110dc5 OOPS: bring back what -WBLOCK erased; keep the last erase through REFEDIT; the Block Editor (#2625)
* OOPS brings back what -WBLOCK erased

-WBLOCK erases the objects it writes, and OOPS brings back what the
last ERASE or BLOCK removed; the two landed separately, so OOPS never
saw what -WBLOCK erased. -WBLOCK now erases through erase_kept, as ERASE
and BLOCK do.

As AutoCAD 2024 does (accoreconsole, the drawing's entnext order read
back): OOPS after -WBLOCK brings its objects back, each where it was and
with its handle; only the last erase counts, -WBLOCK's or ERASE's; and
-WBLOCK * erases nothing, so OOPS still brings back the erase before it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* OOPS: keep the last erase through a refused REFEDIT command; Block Editor

During REFEDIT, a command that removes an object outside the working set
is undone (refedit::after_command), but erase_kept had already replaced
what OOPS kept: the objects came back, and the erase before them was
lost. erase_kept now leaves OOPS as it was when an object it removes is
outside the working set. That covers BLOCK, -WBLOCK, ERASE and the
Delete key.

In the Block Editor, its objects keep the handles they have in the block
definition, so in_blocks found them there and OOPS brought back nothing
erased in the editor. in_blocks now skips the block open there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 05:31:14 -05:00

182 lines
6.9 KiB
Rust

//! OOPS: bring back the objects the last ERASE (or the Delete key) removed, or BLOCK deleting the
//! objects it makes a block of, or -WBLOCK the objects it writes, with their handles and each where
//! it was in the draw order, as AutoCAD 2024 does (accoreconsole, the drawing's `entnext` order read
//! back afterwards).
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use cadcraft_doc::{Drawing, Entity, EntityKind, EntityStore, Handle, Space};
use serde_json::{Value, json};
use crate::{Result, Session};
/// One erased object, with the objects that were directly under and above it.
#[derive(Clone, Debug)]
struct Taken {
space: Space,
entity: Arc<Entity>,
below: Option<Handle>,
above: Option<Handle>,
}
/// What the drawing's last erase removed, for OOPS.
#[derive(Clone, Debug, Default)]
pub(crate) struct Erased(Vec<Taken>);
impl Erased {
/// Never bring back the objects `drop` picks.
pub(crate) fn forget(&mut self, drop: impl Fn(Handle) -> bool) {
self.0.retain(|t| !drop(t.entity.handle));
}
}
/// Remove the objects `hs` and keep them for OOPS in place of what an earlier erase kept.
/// Returns how many went; when none did, OOPS keeps what it had.
pub(crate) fn erase_kept(s: &mut Session, hs: &[Handle]) -> Result<usize> {
let d = s.doc_mut()?;
// Every neighbour is read before anything goes: objects erased together may be neighbours.
let at: Vec<(Handle, Space, Option<Handle>, Option<Handle>)> = hs
.iter()
.filter_map(|&h| {
let space = d.space_of(h)?;
let (below, above) = d.space(&space)?.neighbours(h)?;
Some((h, space, below, above))
})
.collect();
let mut taken = Vec::with_capacity(at.len());
for (h, space, below, above) in at {
if let Some(entity) = d.space_mut(&space).and_then(|st| st.remove(h)) {
taken.push(Taken { space, entity, below, above });
}
}
let n = taken.len();
// During REFEDIT, removing an object outside the working set gets the command undone
// (`refedit::after_command`), so what OOPS had stays.
let refused = taken.iter().any(|t| crate::cmd::blocks::refedit::locked(d, t.entity.handle));
if n > 0 && !refused {
s.state_mut()?.erased = Erased(taken);
}
Ok(n)
}
pub(super) fn run_oops(s: &mut Session, _p: &Value) -> Result<Value> {
let taken = s.state()?.erased.0.clone();
let editing = s.state()?.block_edit.as_ref().map(|b| b.name.clone());
let back: Vec<bool> = {
let d = s.doc()?;
let in_blocks = in_blocks(d, &taken, editing.as_deref());
taken.iter().map(|t| !in_blocks.contains(&t.entity.handle) && can_return(d, t)).collect()
};
// Nothing to bring back leaves the drawing as it is: no undo step.
if !back.contains(&true) {
return Ok(json!({ "restored": 0 }));
}
let n = restore(s.doc_mut()?, &taken, &back);
Ok(json!({ "restored": n }))
}
/// The handles of `taken` that a block's object has now (an undo can take the drawing back to
/// before an erased object was made, and a block's new object take its handle). The block open
/// in the Block Editor (`editing`) doesn't count: its objects there keep the handles they have in
/// it.
fn in_blocks(d: &Drawing, taken: &[Taken], editing: Option<&str>) -> HashSet<Handle> {
let hs: HashSet<Handle> = taken.iter().map(|t| t.entity.handle).collect();
let mut out = HashSet::new();
for b in d.blocks.values() {
if editing.is_some_and(|n| b.name.eq_ignore_ascii_case(n)) {
continue;
}
// Through whichever is smaller.
if b.entities.len() < hs.len() {
out.extend(b.entities.iter().map(|e| e.handle).filter(|h| hs.contains(h)));
} else {
out.extend(hs.iter().copied().filter(|h| b.entities.contains(*h)));
}
}
out
}
/// Whether `t` can come back: it is not in the drawing (OOPS again, or an undo brought it back),
/// its handle is one the drawing has given out, a reference being edited in place (REFEDIT)
/// doesn't keep it out, and its layout, layer and block are still there.
fn can_return(d: &Drawing, t: &Taken) -> bool {
let h = t.entity.handle;
h.0 < d.handseed
&& d.entity(h).is_none()
&& !crate::cmd::blocks::refedit::locked(d, h)
&& d.space(&t.space).is_some()
&& d.layer(&t.entity.common.layer).is_some()
&& match &t.entity.kind {
EntityKind::Insert(i) => d.block(&i.block).is_some(),
_ => true,
}
}
/// Put back the objects `back` marks. Each run of objects that were neighbours comes back from
/// its bottom one up, so each goes directly above the one that was under it.
fn restore(d: &mut Drawing, taken: &[Taken], back: &[bool]) -> usize {
let index: HashMap<Handle, usize> = taken.iter().enumerate().map(|(i, t)| (t.entity.handle, i)).collect();
let next: HashMap<Handle, usize> = taken.iter().enumerate().filter_map(|(i, t)| Some((t.below.filter(|b| index.contains_key(b))?, i))).collect();
let mut done = vec![false; taken.len()];
let mut n = 0;
for first in 0..taken.len() {
if taken[first].below.is_some_and(|b| index.contains_key(&b)) {
continue;
}
let mut at = Some(first);
while let Some(i) = at {
if std::mem::replace(&mut done[i], true) {
break;
}
if back[i] && put_back(d, taken, &index, i) {
n += 1;
}
at = next.get(&taken[i].entity.handle).copied();
}
}
n
}
/// Put `taken[i]` back directly above the nearest object that was under it and is here; when
/// those are gone, directly under the nearest one that was above it; else on top.
fn put_back(d: &mut Drawing, taken: &[Taken], index: &HashMap<Handle, usize>, i: usize) -> bool {
let t = &taken[i];
let Some(store) = d.space_mut(&t.space) else { return false };
let h = t.entity.handle;
store.push(Entity::clone(&t.entity));
match nearest(store, taken, index, t.below, |t| t.below) {
Some(Some(b)) => {
store.move_next_to(h, b, true);
}
Some(None) => {
store.send_to_back(h);
}
None => {
if let Some(Some(a)) = nearest(store, taken, index, t.above, |t| t.above) {
store.move_next_to(h, a, false);
}
}
}
true
}
/// From `h`, past objects erased with it that are not here, to the first object that is
/// (`Some(Some)`), the end of the draw order (`Some(None)`), or an object that is gone (`None`).
fn nearest(
store: &EntityStore,
taken: &[Taken],
index: &HashMap<Handle, usize>,
mut h: Option<Handle>,
step: fn(&Taken) -> Option<Handle>,
) -> Option<Option<Handle>> {
for _ in 0..=taken.len() {
let Some(x) = h else { return Some(None) };
if store.contains(x) {
return Some(Some(x));
}
h = step(taken.get(*index.get(&x)?)?);
}
None
}