REFEDIT, REFSET and REFCLOSE: edit block references in place (#818)
* REFEDIT, REFSET and REFCLOSE: edit block references in place (#813) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Move REFEDIT translation lines into the middle of the catalogs (avoids PR merge conflicts) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3dd678ff3d
commit
9464945f82
|
|
@ -10,7 +10,7 @@ scriptable CLI, a web build, Linux and FreeBSD builds, and a free licence.
|
|||
|
||||
**Why pre-alpha:** four of AutoCAD's five core workflows fail the [alpha gate](docs/roadmap.md#alpha-gate).
|
||||
Precise drafting lacks object snap tracking and has stubbed TRIM/EXTEND/OFFSET options; blocks
|
||||
can't be edited (no block editor, REFEDIT or xrefs); plots ignore plot styles; and a DWG saved by
|
||||
can't be edited in a block editor (REFEDIT edits them in place, xrefs not yet); plots ignore plot styles; and a DWG saved by
|
||||
CADCraft drops everything it doesn't model and has never been checked in
|
||||
AutoCAD. Dimensioning passes. Ready for real work (≈ 38%) is also under the ~40% alpha bar. Beta
|
||||
is ≈ 600–930 h away.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ mod header;
|
|||
pub mod library;
|
||||
mod plotstyle;
|
||||
mod plotter;
|
||||
mod refedit;
|
||||
mod store;
|
||||
mod tables;
|
||||
|
||||
|
|
@ -30,6 +31,7 @@ pub use gdt::*;
|
|||
pub use header::{HVal, Header};
|
||||
pub use plotstyle::*;
|
||||
pub use plotter::*;
|
||||
pub use refedit::{RefEdit, WorkItem};
|
||||
pub use store::EntityStore;
|
||||
pub use tables::*;
|
||||
|
||||
|
|
@ -95,6 +97,9 @@ pub struct Drawing {
|
|||
/// Objects hidden for this session by ISOLATEOBJECTS / HIDEOBJECTS. Never written to a file:
|
||||
/// a drawing opens with everything shown again.
|
||||
pub hidden_objects: BTreeSet<Handle>,
|
||||
/// The in-place block reference edit in progress (REFEDIT). Never written to a file; files are
|
||||
/// written from [`Drawing::committed`].
|
||||
pub refedit: Option<RefEdit>,
|
||||
}
|
||||
|
||||
impl Default for Drawing {
|
||||
|
|
@ -148,6 +153,7 @@ impl Drawing {
|
|||
scales: imperial_scales(),
|
||||
handseed: 0x100,
|
||||
hidden_objects: BTreeSet::new(),
|
||||
refedit: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
//! In-place editing of a block reference (REFEDIT / REFSET / REFCLOSE): the state kept with the
|
||||
//! drawing while it lasts.
|
||||
//!
|
||||
//! The reference being edited is hidden and the block's objects stand in the drawing as ordinary
|
||||
//! objects (the *working set*, transformed to where the reference shows them), the rest of the
|
||||
//! block as faded copies (*ghosts*) that cannot be edited. REFCLOSE writes the working set back
|
||||
//! into the block definition. The state lives in the [`Drawing`] itself, so undo and redo move
|
||||
//! the working copies and the state together and can never leave one without the other.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use cadcraft_geom::Mat3;
|
||||
|
||||
use crate::{Drawing, Entity, Handle, Space};
|
||||
|
||||
/// One object of the working set.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct WorkItem {
|
||||
/// The object in the drawing (a copy of the block object, or a drawing object added to the set).
|
||||
pub copy: Handle,
|
||||
/// The block object it was copied from; `None` for an object added from the drawing.
|
||||
pub source: Option<Handle>,
|
||||
}
|
||||
|
||||
/// An active in-place edit of a block reference. Never written to a file.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct RefEdit {
|
||||
/// The block being edited.
|
||||
pub block: String,
|
||||
/// The reference that was picked (hidden while editing).
|
||||
pub insert: Handle,
|
||||
/// Whether the reference was visible before it was hidden.
|
||||
pub insert_was_visible: bool,
|
||||
/// The space the reference is in.
|
||||
pub space: Space,
|
||||
/// Block coordinates to drawing coordinates, through the picked reference.
|
||||
pub to_world: Mat3,
|
||||
/// The working set.
|
||||
pub items: Vec<WorkItem>,
|
||||
/// The rest of the block as faded copies: (copy, block object).
|
||||
pub ghosts: Vec<(Handle, Handle)>,
|
||||
/// Block objects taken out of the working set with their edits (block coordinates, the block
|
||||
/// object's handle).
|
||||
pub released: Vec<Entity>,
|
||||
}
|
||||
|
||||
impl RefEdit {
|
||||
/// Whether `h` is a working set object.
|
||||
pub fn is_working(&self, h: Handle) -> bool {
|
||||
self.items.iter().any(|i| i.copy == h)
|
||||
}
|
||||
/// Whether `h` is something the edit manages (working set, ghost, the hidden reference).
|
||||
pub fn owns(&self, h: Handle) -> bool {
|
||||
h == self.insert || self.is_working(h) || self.ghosts.iter().any(|(g, _)| *g == h)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drawing {
|
||||
/// The drawing as REFCLOSE Discard would leave it: the working copies and ghosts gone, the
|
||||
/// reference shown, the block untouched. Borrowed when no edit is active. Files are written
|
||||
/// from this, so a saved or autosaved drawing never holds half an edit.
|
||||
pub fn committed(&self) -> Cow<'_, Drawing> {
|
||||
let Some(r) = &self.refedit else { return Cow::Borrowed(self) };
|
||||
let mut d = self.clone();
|
||||
d.end_refedit_discard(r);
|
||||
Cow::Owned(d)
|
||||
}
|
||||
|
||||
/// Remove the edit's working copies and ghosts, show the reference again and clear the state.
|
||||
pub fn end_refedit_discard(&mut self, r: &RefEdit) {
|
||||
for i in &r.items {
|
||||
if i.source.is_some() {
|
||||
self.remove_entity(i.copy);
|
||||
}
|
||||
}
|
||||
for (g, _) in &r.ghosts {
|
||||
self.remove_entity(*g);
|
||||
}
|
||||
let vis = r.insert_was_visible;
|
||||
let _ = self.modify_entity(r.insert, |e| e.common.visible = vis);
|
||||
self.refedit = None;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, St
|
|||
|
||||
mod attedit;
|
||||
mod extract;
|
||||
pub(crate) mod refedit;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
|
|
@ -55,6 +56,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
]
|
||||
.into_iter()
|
||||
.chain(xref::specs())
|
||||
.chain(refedit::specs())
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
|
@ -97,6 +99,9 @@ pub(crate) fn make_block(s: &mut Session, name: &str, base: Vec2, hs: &[Handle],
|
|||
if references_block(d, &ents, name) {
|
||||
return Err(bad("block", format!("Block \"{name}\" references itself.")));
|
||||
}
|
||||
if d.refedit.as_ref().is_some_and(|r| r.block.eq_ignore_ascii_case(name)) {
|
||||
return Err(bad("block", format!("Block \"{name}\" is being edited in place; finish with REFCLOSE first.")));
|
||||
}
|
||||
// Block names are case-insensitive: redefining keeps the existing definition's name.
|
||||
let name = d.blocks.keys().find(|k| k.eq_ignore_ascii_case(name)).cloned().unwrap_or_else(|| name.to_string());
|
||||
let name = name.as_str();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,469 @@
|
|||
//! REFEDIT, REFSET and REFCLOSE: edit a block reference in place.
|
||||
//!
|
||||
//! REFEDIT hides the picked reference and puts the block's objects (all, or the ones named) into
|
||||
//! the drawing where the reference shows them: the *working set*, ordinary objects that every
|
||||
//! command edits. The rest of the block stands there as faded copies, and everything else in the
|
||||
//! drawing is locked ([`crate::cmd::curves::is_locked`], enforced by [`after_command`]). Objects
|
||||
//! drawn while editing join the working set. REFSET adds objects to or removes them from it.
|
||||
//! REFCLOSE writes the working set back into the block definition, so every reference to the
|
||||
//! block shows the change, or discards the edit.
|
||||
//!
|
||||
//! The state is [`Drawing::refedit`]: it travels with undo, is never written to a file (files
|
||||
//! are written from [`Drawing::committed`]) and ends with the drawing it belongs to.
|
||||
//!
|
||||
//! Not yet: external references (their source drawing would have to be written back), nested
|
||||
//! references picked at a depth, annotative scale contexts and attribute synchronisation.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use cadcraft_doc::{Drawing, Entity, EntityStore, Handle, RefEdit, Transparency, WorkItem};
|
||||
use cadcraft_geom::Mat3;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::super::curves::handle_value;
|
||||
use super::super::machines::{SelOutcome, SelectPhase, SelectRun};
|
||||
use super::references_block;
|
||||
use crate::cmd::{CommandSpec, bad, bool_or, has_doc, str_param, targets};
|
||||
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
|
||||
|
||||
/// Transparency of the faded copies of the block's other objects.
|
||||
const GHOST_FADE: u8 = 70;
|
||||
|
||||
pub(super) fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
CommandSpec::new("refedit", "Edit Reference In-Place", run_refedit)
|
||||
.menu(&["Tools", "Xref and Block In-place Editing", "Edit Reference In-Place"])
|
||||
.params("{handle? | handles? | name? (a block reference, default the selection), objects?: [block object handles] (default all)} → {block, insert, working: [handles], faded}")
|
||||
.enabled(has_doc)
|
||||
.interactive(|_| Ok(Box::new(SelectRun::single("refedit", "REFEDIT", "Select reference")))),
|
||||
CommandSpec::new("refset", "Working Set", run_refset)
|
||||
.menu(&["Tools", "Xref and Block In-place Editing", "Add to Working Set"])
|
||||
.params("{add?: [handles], remove?: [handles]} (or {handles, mode?: \"add\"|\"remove\"}); with neither: lists the working set → {working: [handles], added, removed}")
|
||||
.enabled(editing)
|
||||
.interactive(|_| Ok(Box::new(RefsetM::default()))),
|
||||
CommandSpec::new("refclose", "Save Reference Edits", run_refclose)
|
||||
.menu(&["Tools", "Xref and Block In-place Editing", "Save Reference Edits"])
|
||||
.params("{option?: \"save\"|\"discard\" | save?: bool (default true)} → {block, saved, objects}")
|
||||
.enabled(editing)
|
||||
.interactive(|_| Ok(Box::new(RefcloseM))),
|
||||
]
|
||||
}
|
||||
|
||||
fn editing(s: &Session) -> std::result::Result<(), String> {
|
||||
has_doc(s)?;
|
||||
if s.doc().is_ok_and(|d| d.refedit.is_some()) { Ok(()) } else { Err("no block reference is being edited (REFEDIT)".into()) }
|
||||
}
|
||||
|
||||
fn active(s: &Session, cmd: &str) -> Result<RefEdit> {
|
||||
s.doc()?.refedit.clone().ok_or_else(|| bad(cmd, "no block reference is being edited (REFEDIT)"))
|
||||
}
|
||||
|
||||
/// A block object as the drawing shows it through the edited reference.
|
||||
fn to_host(to_world: &Mat3, e: &Entity, h: Handle, ghost: bool) -> Entity {
|
||||
let mut c = e.clone();
|
||||
c.handle = h;
|
||||
c.kind.transform(to_world);
|
||||
if ghost {
|
||||
c.common.transparency = Transparency::Percent(GHOST_FADE);
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
/// A drawing object as it belongs in the block, with the block object's handle `h`.
|
||||
fn to_block(inv: &Mat3, e: &Entity, h: Handle) -> Entity {
|
||||
let mut c = e.clone();
|
||||
c.handle = h;
|
||||
c.kind.transform(inv);
|
||||
c
|
||||
}
|
||||
|
||||
fn put(d: &mut Drawing, r: &RefEdit, e: Entity) -> Result<()> {
|
||||
d.ensure_layer(&e.common.layer);
|
||||
d.space_mut(&r.space).ok_or_else(|| bad("refedit", "the space of the reference is gone"))?.push(e);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hexes(hs: impl IntoIterator<Item = Handle>) -> Vec<String> {
|
||||
hs.into_iter().map(|h| h.hex()).collect()
|
||||
}
|
||||
|
||||
// ---------------- REFEDIT ----------------
|
||||
|
||||
fn run_refedit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if s.doc()?.refedit.is_some() {
|
||||
return Err(bad("refedit", "a reference is already being edited; finish with REFCLOSE"));
|
||||
}
|
||||
let d = s.doc()?;
|
||||
let picked = match str_param(p, "name") {
|
||||
Some(n) => d
|
||||
.model
|
||||
.iter()
|
||||
.chain(d.layouts.iter().flat_map(|l| l.entities.iter()))
|
||||
.find(|e| matches!(&e.kind, cadcraft_doc::EntityKind::Insert(i) if i.block.eq_ignore_ascii_case(n)))
|
||||
.map(|e| e.handle),
|
||||
None => targets(s, p)?.into_iter().find(|h| d.entity(*h).is_some_and(|e| matches!(e.kind, cadcraft_doc::EntityKind::Insert(_)))),
|
||||
};
|
||||
let ih = picked.ok_or_else(|| bad("refedit", "select a block reference"))?;
|
||||
let (ins, space) = match d.entity(ih).map(|e| &e.kind) {
|
||||
Some(cadcraft_doc::EntityKind::Insert(i)) => (i.clone(), d.space_of(ih).ok_or_else(|| bad("refedit", "the reference is in no space"))?),
|
||||
_ => return Err(bad("refedit", "select a block reference")),
|
||||
};
|
||||
let blk = d.block(&ins.block).cloned().ok_or_else(|| bad("refedit", format!("block \"{}\" not found", ins.block)))?;
|
||||
if blk.xref_path.is_some() {
|
||||
return Err(bad(
|
||||
"refedit",
|
||||
format!(
|
||||
"\"{}\" is an external reference: editing it in place would write its source drawing, which is not available yet. Open it with XOPEN, edit and save it, then reload.",
|
||||
blk.name
|
||||
),
|
||||
));
|
||||
}
|
||||
if blk.name.starts_with('*') || blk.array.is_some() {
|
||||
return Err(bad("refedit", format!("\"{}\" is generated geometry and can't be edited in place", blk.name)));
|
||||
}
|
||||
let to_world = ins.transform(blk.base.xy());
|
||||
let (sx, sy) = (ins.scale.x.abs(), ins.scale.y.abs());
|
||||
let finite = [to_world.a, to_world.b, to_world.c, to_world.d, to_world.e, to_world.f].iter().all(|v| v.is_finite());
|
||||
if !finite || to_world.det().abs() < 1e-12 || (sx - sy).abs() > 1e-9 * sx.max(sy) {
|
||||
return Err(bad("refedit", "the reference must be scaled equally in X and Y (and not flat) to be edited in place"));
|
||||
}
|
||||
let chosen: Option<HashSet<Handle>> = match p.get("objects") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(Value::String(a)) if a.eq_ignore_ascii_case("all") => None,
|
||||
Some(Value::Array(a)) => {
|
||||
let mut set = HashSet::new();
|
||||
for v in a {
|
||||
let h = handle_value(v).ok_or_else(|| bad("refedit", format!("`objects`: {v} is not an object handle")))?;
|
||||
if blk.entities.get(h).is_none() {
|
||||
return Err(bad("refedit", format!("object {} is not in block \"{}\"", h.hex(), blk.name)));
|
||||
}
|
||||
set.insert(h);
|
||||
}
|
||||
if set.is_empty() {
|
||||
return Err(bad("refedit", "`objects` is empty"));
|
||||
}
|
||||
Some(set)
|
||||
}
|
||||
Some(v) => return Err(bad("refedit", format!("`objects` must be \"all\" or an array of handles, got {v}"))),
|
||||
};
|
||||
let was_visible = d.entity(ih).is_some_and(|e| e.common.visible);
|
||||
let mut r = RefEdit {
|
||||
block: blk.name.clone(),
|
||||
insert: ih,
|
||||
insert_was_visible: was_visible,
|
||||
space,
|
||||
to_world,
|
||||
items: Vec::new(),
|
||||
ghosts: Vec::new(),
|
||||
released: Vec::new(),
|
||||
};
|
||||
let doc = s.doc_mut()?;
|
||||
for e in blk.entities.iter() {
|
||||
let working = chosen.as_ref().is_none_or(|c| c.contains(&e.handle));
|
||||
let h = doc.new_handle();
|
||||
put(doc, &r, to_host(&to_world, e, h, !working))?;
|
||||
if working {
|
||||
r.items.push(WorkItem { copy: h, source: Some(e.handle) });
|
||||
} else {
|
||||
r.ghosts.push((h, e.handle));
|
||||
}
|
||||
}
|
||||
doc.modify_entity(ih, |e| e.common.visible = false).map_err(|e| bad("refedit", e.to_string()))?;
|
||||
let out = json!({ "block": r.block, "insert": ih.hex(), "working": hexes(r.items.iter().map(|i| i.copy)), "faded": r.ghosts.len() });
|
||||
let sel: Vec<Handle> = r.items.iter().map(|i| i.copy).collect();
|
||||
doc.refedit = Some(r);
|
||||
s.set_selection(sel);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
// ---------------- REFSET ----------------
|
||||
|
||||
fn handle_list(p: &Value, key: &str) -> Result<Vec<Handle>> {
|
||||
match p.get(key) {
|
||||
None | Some(Value::Null) => Ok(Vec::new()),
|
||||
Some(Value::Array(a)) => {
|
||||
a.iter().map(|v| handle_value(v).ok_or_else(|| bad("refset", format!("`{key}`: {v} is not an object handle")))).collect()
|
||||
}
|
||||
Some(v) => Err(bad("refset", format!("`{key}` must be an array of object handles, got {v}"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_refset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (mut add, mut remove) = (handle_list(p, "add")?, handle_list(p, "remove")?);
|
||||
let hs = handle_list(p, "handles")?;
|
||||
if str_param(p, "mode").is_some_and(|m| m.to_ascii_lowercase().starts_with('r')) {
|
||||
remove.extend(hs)
|
||||
} else {
|
||||
add.extend(hs)
|
||||
}
|
||||
apply_refset(s, &add, &remove)
|
||||
}
|
||||
|
||||
fn apply_refset(s: &mut Session, add: &[Handle], remove: &[Handle]) -> Result<Value> {
|
||||
let mut r = active(s, "refset")?;
|
||||
let inv = r.to_world.inverse().ok_or_else(|| bad("refset", "the reference can't be inverted"))?;
|
||||
let d = s.doc()?;
|
||||
for h in add.iter().chain(remove) {
|
||||
if d.entity(*h).is_none() {
|
||||
return Err(bad("refset", format!("no object {}", h.hex())));
|
||||
}
|
||||
}
|
||||
if let Some(h) = remove.iter().find(|h| !r.is_working(**h)) {
|
||||
return Err(bad("refset", format!("object {} is not in the working set", h.hex())));
|
||||
}
|
||||
let blk = d.block(&r.block).cloned().ok_or_else(|| bad("refset", format!("block \"{}\" is gone; REFCLOSE Discard", r.block)))?;
|
||||
let (mut added, mut removed) = (0usize, 0usize);
|
||||
let doc = s.doc_mut()?;
|
||||
for h in add {
|
||||
if *h == r.insert || r.is_working(*h) {
|
||||
continue;
|
||||
}
|
||||
if let Some(pos) = r.ghosts.iter().position(|(g, _)| g == h) {
|
||||
// A faded block object joins the working set (with the edits it had before).
|
||||
let (g, src) = r.ghosts.remove(pos);
|
||||
doc.remove_entity(g);
|
||||
let cur = match r.released.iter().position(|e| e.handle == src) {
|
||||
Some(i) => Some(r.released.remove(i)),
|
||||
None => blk.entities.get(src).map(|e| (**e).clone()),
|
||||
};
|
||||
if let Some(cur) = cur {
|
||||
let c = doc.new_handle();
|
||||
put(doc, &r, to_host(&r.to_world, &cur, c, false))?;
|
||||
r.items.push(WorkItem { copy: c, source: Some(src) });
|
||||
added += 1;
|
||||
}
|
||||
} else {
|
||||
r.items.push(WorkItem { copy: *h, source: None });
|
||||
added += 1;
|
||||
}
|
||||
}
|
||||
for h in remove {
|
||||
let Some(pos) = r.items.iter().position(|i| i.copy == *h) else { continue };
|
||||
let item = r.items.remove(pos);
|
||||
if let Some(src) = item.source
|
||||
&& let Some(e) = doc.entity(*h).map(|e| (**e).clone())
|
||||
{
|
||||
// Back to the block, edits kept, shown faded.
|
||||
let kept = to_block(&inv, &e, src);
|
||||
doc.remove_entity(*h);
|
||||
let g = doc.new_handle();
|
||||
put(doc, &r, to_host(&r.to_world, &kept, g, true))?;
|
||||
r.ghosts.push((g, src));
|
||||
r.released.push(kept);
|
||||
}
|
||||
removed += 1;
|
||||
}
|
||||
let working = hexes(r.items.iter().map(|i| i.copy));
|
||||
doc.refedit = Some(r);
|
||||
Ok(json!({ "working": working, "added": added, "removed": removed }))
|
||||
}
|
||||
|
||||
// ---------------- REFCLOSE ----------------
|
||||
|
||||
fn run_refclose(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let save = match str_param(p, "option") {
|
||||
Some(o) if o.to_ascii_lowercase().starts_with('d') => false,
|
||||
Some(o) if o.to_ascii_lowercase().starts_with('s') => true,
|
||||
Some(o) => return Err(bad("refclose", format!("unknown option \"{o}\" (save or discard)"))),
|
||||
None => bool_or(p, "save", true),
|
||||
};
|
||||
close(s, save)
|
||||
}
|
||||
|
||||
fn close(s: &mut Session, save: bool) -> Result<Value> {
|
||||
let r = active(s, "refclose")?;
|
||||
// Work on a copy: the drawing changes only when everything went through.
|
||||
let mut nd: Drawing = s.doc()?.clone();
|
||||
let mut objects = 0;
|
||||
if save {
|
||||
objects = commit_block(&mut nd, &r)?;
|
||||
}
|
||||
nd.end_refedit_discard(&r);
|
||||
*s.doc_mut()? = nd;
|
||||
s.set_selection(Vec::new());
|
||||
Ok(json!({ "block": r.block, "saved": save, "objects": objects }))
|
||||
}
|
||||
|
||||
/// Replace the block's objects by the working set (and move the drawing objects added to it
|
||||
/// into the block). Returns the number of objects the block has now.
|
||||
fn commit_block(nd: &mut Drawing, r: &RefEdit) -> Result<usize> {
|
||||
let blk = nd.block(&r.block).cloned().ok_or_else(|| bad("refclose", format!("block \"{}\" is gone; use REFCLOSE Discard", r.block)))?;
|
||||
let inv = r.to_world.inverse().ok_or_else(|| bad("refclose", "the reference can't be inverted; use REFCLOSE Discard"))?;
|
||||
let mut ents: Vec<Entity> = Vec::new();
|
||||
for e in blk.entities.iter() {
|
||||
match r.items.iter().find(|i| i.source == Some(e.handle)) {
|
||||
// A working object the user erased leaves the block.
|
||||
Some(i) => {
|
||||
if let Some(c) = nd.entity(i.copy) {
|
||||
ents.push(to_block(&inv, c, e.handle));
|
||||
}
|
||||
}
|
||||
None => match r.released.iter().find(|x| x.handle == e.handle) {
|
||||
Some(x) => ents.push(x.clone()),
|
||||
None => ents.push((**e).clone()),
|
||||
},
|
||||
}
|
||||
}
|
||||
let mut moved = Vec::new();
|
||||
for i in r.items.iter().filter(|i| i.source.is_none()) {
|
||||
if let Some(c) = nd.entity(i.copy).map(|c| (**c).clone()) {
|
||||
let h = nd.new_handle();
|
||||
ents.push(to_block(&inv, &c, h));
|
||||
moved.push(i.copy);
|
||||
}
|
||||
}
|
||||
if references_block(nd, &ents, &r.block) {
|
||||
return Err(bad("refclose", format!("Block \"{}\" would reference itself; remove that reference or use REFCLOSE Discard.", r.block)));
|
||||
}
|
||||
let n = ents.len();
|
||||
let mut store = EntityStore::new();
|
||||
for e in ents {
|
||||
store.push(e);
|
||||
}
|
||||
let key = nd.blocks.keys().find(|k| k.eq_ignore_ascii_case(&r.block)).cloned().unwrap_or_else(|| r.block.clone());
|
||||
let mut nb = (*blk).clone();
|
||||
nb.entities = store;
|
||||
nd.blocks.insert(key, Arc::new(nb));
|
||||
for h in moved {
|
||||
nd.remove_entity(h);
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
// ---------------- while editing ----------------
|
||||
|
||||
/// After a command ran: objects it created join the working set, and a command that changed
|
||||
/// anything outside the working set is undone (returns the reason).
|
||||
pub(crate) fn after_command(s: &mut Session, before: &Arc<Drawing>, id: &str) -> Option<String> {
|
||||
if matches!(id, "refedit" | "refset" | "refclose") || !crate::cmd::find_command(id).is_some_and(|c| c.undoable) {
|
||||
return None;
|
||||
}
|
||||
let rb = before.refedit.as_ref()?;
|
||||
let st = s.state_mut().ok()?;
|
||||
if Arc::ptr_eq(before, &st.doc) {
|
||||
return None;
|
||||
}
|
||||
let ra = st.doc.refedit.as_ref().filter(|ra| ra.insert == rb.insert)?;
|
||||
let owned: HashSet<Handle> = rb.items.iter().map(|i| i.copy).chain(rb.ghosts.iter().map(|g| g.0)).chain([rb.insert]).collect();
|
||||
let stores = |d: &Drawing| std::iter::once(&d.model).chain(d.layouts.iter().map(|l| &l.entities)).cloned().collect::<Vec<_>>();
|
||||
let touched = stores(before)
|
||||
.iter()
|
||||
.any(|store| store.iter().any(|e| !owned.contains(&e.handle) && !st.doc.entity(e.handle).is_some_and(|a| Arc::ptr_eq(a, e))));
|
||||
if touched {
|
||||
st.doc = before.clone();
|
||||
return Some("Only the working set can be edited during REFEDIT (REFSET adds objects, REFCLOSE ends the edit).".into());
|
||||
}
|
||||
let seed = before.handseed;
|
||||
let mut new: Vec<Handle> = Vec::new();
|
||||
for store in stores(&st.doc) {
|
||||
new.extend(store.iter().map(|e| e.handle).filter(|h| h.0 >= seed && !ra.owns(*h)));
|
||||
}
|
||||
if !new.is_empty()
|
||||
&& let Some(r) = Arc::make_mut(&mut st.doc).refedit.as_mut()
|
||||
{
|
||||
r.items.extend(new.into_iter().map(|copy| WorkItem { copy, source: None }));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Whether `h` may not be edited because a reference is being edited and it is not in the working set.
|
||||
pub(crate) fn locked(d: &Drawing, h: Handle) -> bool {
|
||||
d.refedit.as_ref().is_some_and(|r| !r.is_working(h))
|
||||
}
|
||||
|
||||
// ---------------- interactive ----------------
|
||||
|
||||
#[derive(Default)]
|
||||
enum RStage {
|
||||
#[default]
|
||||
Mode,
|
||||
Pick(bool),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct RefsetM {
|
||||
stage: RStage,
|
||||
sel: SelectPhase,
|
||||
}
|
||||
|
||||
impl Interactive for RefsetM {
|
||||
fn name(&self) -> &'static str {
|
||||
"REFSET"
|
||||
}
|
||||
fn begin(&mut self, s: &mut Session) -> Result<Step> {
|
||||
active(s, "refset").map(|_| Step::Continue)
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
match self.stage {
|
||||
RStage::Mode => {
|
||||
let mut p = Prompt::new("Enter an option [Add/Remove] <Add>", Accept::TEXT);
|
||||
p.keywords = vec!["Add".into(), "Remove".into()];
|
||||
p.default = Some("Add".into());
|
||||
p
|
||||
}
|
||||
RStage::Pick(remove) => {
|
||||
let mut p = self.sel.prompt();
|
||||
p.message = format!("Select objects to {}", if remove { "remove" } else { "add" });
|
||||
p
|
||||
}
|
||||
}
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
match self.stage {
|
||||
RStage::Mode => {
|
||||
let remove = match i {
|
||||
Input::Keyword(k) | Input::Text(k) => k.to_ascii_lowercase().starts_with('r'),
|
||||
Input::Enter => false,
|
||||
_ => return Ok(Step::Cancel),
|
||||
};
|
||||
self.sel = SelectPhase::begin(s);
|
||||
self.stage = RStage::Pick(remove);
|
||||
if self.sel.done {
|
||||
let hs = self.sel.picked.clone();
|
||||
return self.finish(s, remove, &hs);
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
RStage::Pick(remove) => match self.sel.feed(s, &i)? {
|
||||
SelOutcome::More => Ok(Step::Continue),
|
||||
SelOutcome::Empty => Ok(Step::Done),
|
||||
SelOutcome::Done(hs) => self.finish(s, remove, &hs),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RefsetM {
|
||||
fn finish(&mut self, s: &mut Session, remove: bool, hs: &[Handle]) -> Result<Step> {
|
||||
let r = if remove { apply_refset(s, &[], hs) } else { apply_refset(s, hs, &[]) }?;
|
||||
s.echo(format!("{} object(s) in the working set.", r.get("working").and_then(Value::as_array).map_or(0, Vec::len)));
|
||||
s.set_selection(Vec::new());
|
||||
Ok(Step::Done)
|
||||
}
|
||||
}
|
||||
|
||||
struct RefcloseM;
|
||||
|
||||
impl Interactive for RefcloseM {
|
||||
fn name(&self) -> &'static str {
|
||||
"REFCLOSE"
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
let mut p = Prompt::new("Enter option [Save/Discard reference changes] <Save>", Accept::TEXT);
|
||||
p.keywords = vec!["Save".into(), "Discard".into()];
|
||||
p.default = Some("Save".into());
|
||||
p
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
let save = match i {
|
||||
Input::Keyword(k) | Input::Text(k) => !k.to_ascii_lowercase().starts_with('d'),
|
||||
Input::Enter => true,
|
||||
_ => return Ok(Step::Cancel),
|
||||
};
|
||||
let r = close(s, save)?;
|
||||
s.echo(if save { "Reference changes saved." } else { "Reference changes discarded." });
|
||||
let _ = r;
|
||||
Ok(Step::Done)
|
||||
}
|
||||
}
|
||||
|
|
@ -36,9 +36,9 @@ pub fn handle_param(p: &serde_json::Value, key: &str) -> Option<Handle> {
|
|||
p.get(key).and_then(handle_value)
|
||||
}
|
||||
|
||||
/// Is the entity on a locked layer?
|
||||
/// Is the entity on a locked layer, or outside the working set while a block reference is edited in place (REFEDIT)?
|
||||
pub fn is_locked(s: &Session, h: Handle) -> bool {
|
||||
s.doc().ok().is_some_and(|d| d.entity(h).and_then(|e| d.layer(&e.common.layer)).is_some_and(|l| l.locked))
|
||||
s.doc().ok().is_some_and(|d| d.entity(h).and_then(|e| d.layer(&e.common.layer)).is_some_and(|l| l.locked) || super::blocks::refedit::locked(d, h))
|
||||
}
|
||||
|
||||
// ---------------- arc constructions ----------------
|
||||
|
|
|
|||
|
|
@ -343,6 +343,9 @@ pub(crate) fn save_to(s: &mut Session, path: &str) -> Result<usize> {
|
|||
/// Save to `path` with `opts`. A drawing file then keeps the version and binary choice: the
|
||||
/// version goes into the drawing's `$ACADVER`, so later saves (QSAVE) write it again.
|
||||
fn save_as(s: &mut Session, cmd: &str, path: &str, opts: &SaveAs) -> Result<usize> {
|
||||
if s.doc()?.refedit.is_some() {
|
||||
return Err(bad(cmd, "A block reference is being edited in place: finish with REFCLOSE (Save or Discard) before saving."));
|
||||
}
|
||||
let bytes = write_bytes(s, cmd, path, opts)?;
|
||||
let n = commit_save(s, path, &bytes, store)?;
|
||||
if is_drawing_file(path) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ pub(crate) mod annoscale;
|
|||
mod annotate;
|
||||
mod array;
|
||||
mod audit;
|
||||
mod blocks;
|
||||
pub(crate) mod blocks;
|
||||
pub mod clipboard;
|
||||
pub mod constraints;
|
||||
mod draw;
|
||||
|
|
|
|||
|
|
@ -821,6 +821,11 @@ impl Session {
|
|||
return Err(EngineError::Internal(id.into(), msg));
|
||||
}
|
||||
};
|
||||
let violation = before.as_ref().and_then(|b| cmd::blocks::refedit::after_command(self, &b.0, spec.id));
|
||||
let result = match (violation, result) {
|
||||
(Some(m), Ok(_)) => Err(EngineError::Other(m)),
|
||||
(_, r) => r,
|
||||
};
|
||||
cmd::groups::after_command(self, before.as_ref().map(|b| &b.0));
|
||||
cmd::constraints::after_command(self, before.as_ref().map(|b| &b.0), spec.undoable && result.is_ok());
|
||||
assoc::after_command(self, before.as_ref().map(|b| &b.0), before.as_ref().map(|b| b.2));
|
||||
|
|
@ -1160,6 +1165,9 @@ impl Session {
|
|||
self.select_mode = None;
|
||||
let label = find_command(&run.id).map(|c| c.label).unwrap_or("Command");
|
||||
let _ = cancelled;
|
||||
if let Some(m) = cmd::blocks::refedit::after_command(self, &run.before, &run.id) {
|
||||
self.echo(m);
|
||||
}
|
||||
cmd::groups::after_command(self, Some(&run.before));
|
||||
cmd::constraints::after_command(self, Some(&run.before), true);
|
||||
assoc::after_command(self, Some(&run.before), None);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,293 @@
|
|||
//! REFEDIT, REFSET and REFCLOSE: editing a block reference in place (#813).
|
||||
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_engine::doc::{Common, Drawing, EntityKind, Handle, Insert, Space};
|
||||
use cadcraft_engine::geom::Vec3;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn h(v: &Value) -> String {
|
||||
v.get("handle").unwrap_or(&v["handles"][0]).as_str().unwrap().to_string()
|
||||
}
|
||||
|
||||
fn insert(s: &mut Session, block: &str, at: (f64, f64), scale: f64, rot_deg: f64) -> String {
|
||||
let kind = EntityKind::Insert(Insert {
|
||||
block: block.into(),
|
||||
insert: Vec3::new(at.0, at.1, 0.0),
|
||||
scale: Vec3::new(scale, scale, scale),
|
||||
rotation: rot_deg.to_radians(),
|
||||
attribs: Vec::new(),
|
||||
cols: 1,
|
||||
rows: 1,
|
||||
col_spacing: 0.0,
|
||||
row_spacing: 0.0,
|
||||
});
|
||||
s.doc_mut().unwrap().add(&Space::Model, Common::default(), kind).unwrap().hex()
|
||||
}
|
||||
|
||||
/// Block B = a line (0,0)-(10,0) and a circle at (20,0) r2; references A (at 100,0) and C
|
||||
/// (at 0,50, scaled 2, rotated 90 degrees).
|
||||
fn setup() -> (Session, String, String) {
|
||||
let mut s = Session::new();
|
||||
let l = h(&s.execute("line", &json!({ "points": [[0, 0], [10, 0]] })).unwrap());
|
||||
let c = h(&s.execute("circle", &json!({ "center": [20, 0], "radius": 2 })).unwrap());
|
||||
s.execute("block", &json!({ "name": "B", "base": [0, 0], "handles": [l, c], "keep": "delete" })).unwrap();
|
||||
let a = insert(&mut s, "B", (100.0, 0.0), 1.0, 0.0);
|
||||
let c = insert(&mut s, "B", (0.0, 50.0), 2.0, 90.0);
|
||||
(s, a, c)
|
||||
}
|
||||
|
||||
fn block_lines(d: &Drawing, name: &str) -> Vec<(Vec3, Vec3)> {
|
||||
d.block(name).unwrap().entities.iter().filter_map(|e| if let EntityKind::Line(l) = &e.kind { Some((l.a, l.b)) } else { None }).collect()
|
||||
}
|
||||
|
||||
fn handles(v: &Value) -> Vec<String> {
|
||||
v["working"].as_array().unwrap().iter().map(|x| x.as_str().unwrap().to_string()).collect()
|
||||
}
|
||||
|
||||
fn model_len(s: &Session) -> usize {
|
||||
s.doc().unwrap().model.len()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_and_save_updates_every_reference() {
|
||||
let (mut s, a, c) = setup();
|
||||
let before = model_len(&s);
|
||||
let r = s.execute("refedit", &json!({ "handle": a })).unwrap();
|
||||
let work = handles(&r);
|
||||
assert_eq!(work.len(), 2);
|
||||
// The reference is hidden, the block's objects stand where it showed them.
|
||||
let d = s.doc().unwrap();
|
||||
assert!(!d.entity(Handle::parse_hex(&a).unwrap()).unwrap().common.visible);
|
||||
assert_eq!(model_len(&s), before + 2);
|
||||
let line = work.iter().find_map(|w| match &d.entity(Handle::parse_hex(w).unwrap()).unwrap().kind {
|
||||
EntityKind::Line(l) => Some((w.clone(), l.clone())),
|
||||
_ => None,
|
||||
});
|
||||
let (lh, l) = line.unwrap();
|
||||
assert!((l.a.x - 100.0).abs() < 1e-9 && (l.b.x - 110.0).abs() < 1e-9);
|
||||
// Edit with normal commands: stretch it up by 5 and draw a new line.
|
||||
s.execute("move", &json!({ "handles": [lh], "delta": [0, 5] })).unwrap();
|
||||
s.execute("line", &json!({ "points": [[100, 20], [110, 20]] })).unwrap();
|
||||
// The new line joined the working set.
|
||||
let rs = s.execute("refset", &json!({})).unwrap();
|
||||
assert_eq!(rs["working"].as_array().unwrap().len(), 3);
|
||||
let out = s.execute("refclose", &json!({})).unwrap();
|
||||
assert_eq!(out["objects"], 3);
|
||||
let d = s.doc().unwrap();
|
||||
assert!(d.refedit.is_none());
|
||||
assert!(d.entity(Handle::parse_hex(&a).unwrap()).unwrap().common.visible);
|
||||
assert_eq!(model_len(&s), before, "working copies are gone, the block holds the objects");
|
||||
let lines = block_lines(d, "B");
|
||||
assert_eq!(lines.len(), 2);
|
||||
assert!(lines.iter().any(|(p, q)| (p.y - 5.0).abs() < 1e-9 && (q.x - 10.0).abs() < 1e-9));
|
||||
assert!(lines.iter().any(|(p, q)| (p.y - 20.0).abs() < 1e-9 && (p.x - 0.0).abs() < 1e-9 && (q.x - 10.0).abs() < 1e-9));
|
||||
// The second reference shows the change through its own transform.
|
||||
let _ = c;
|
||||
assert!(s.doc().unwrap().block("B").unwrap().entities.len() == 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discard_leaves_everything_as_it_was() {
|
||||
let (mut s, a, _) = setup();
|
||||
let before = s.doc().unwrap().clone();
|
||||
let r = s.execute("refedit", &json!({ "handle": a })).unwrap();
|
||||
s.execute("erase", &json!({ "handles": handles(&r) })).unwrap();
|
||||
s.execute("refclose", &json!({ "option": "discard" })).unwrap();
|
||||
let d = s.doc().unwrap();
|
||||
assert!(d.refedit.is_none());
|
||||
assert_eq!(d.block("B").unwrap().entities.len(), 2);
|
||||
assert_eq!(d.model.len(), before.model.len());
|
||||
assert!(d.entity(Handle::parse_hex(&a).unwrap()).unwrap().common.visible);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn erasing_a_working_object_removes_it_from_the_block() {
|
||||
let (mut s, a, _) = setup();
|
||||
let r = s.execute("refedit", &json!({ "handle": a })).unwrap();
|
||||
let w = handles(&r);
|
||||
s.execute("erase", &json!({ "handles": [w[0]] })).unwrap();
|
||||
s.execute("refclose", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().block("B").unwrap().entities.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn objects_outside_the_working_set_cannot_be_edited() {
|
||||
let (mut s, a, c) = setup();
|
||||
let other = h(&s.execute("line", &json!({ "points": [[300, 0], [310, 0]] })).unwrap());
|
||||
s.execute("refedit", &json!({ "handle": a })).unwrap();
|
||||
let before = s.doc().unwrap().clone();
|
||||
let _ = s.execute("erase", &json!({ "handles": [other.clone(), c.clone()] }));
|
||||
let _ = s.execute("move", &json!({ "handles": [other.clone()], "delta": [1, 1] }));
|
||||
let d = s.doc().unwrap();
|
||||
assert!(d.entity(Handle::parse_hex(&other).unwrap()).is_some());
|
||||
assert!(d.entity(Handle::parse_hex(&c).unwrap()).is_some());
|
||||
assert_eq!(d.model.len(), before.model.len());
|
||||
// Starting another edit, or saving, is refused.
|
||||
assert!(s.execute("refedit", &json!({ "handle": c })).is_err());
|
||||
assert!(s.execute("save", &json!({ "path": "/nonexistent-dir/x.dxf" })).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refset_adds_and_removes_objects() {
|
||||
let (mut s, a, _) = setup();
|
||||
let other = h(&s.execute("line", &json!({ "points": [[300, 0], [310, 0]] })).unwrap());
|
||||
// Edit only the circle: the line is a faded copy.
|
||||
let (circle_src, line_src) = {
|
||||
let b = s.doc().unwrap().block("B").unwrap();
|
||||
let c = b.entities.iter().find(|e| matches!(e.kind, EntityKind::Circle(_))).unwrap().handle;
|
||||
let l = b.entities.iter().find(|e| matches!(e.kind, EntityKind::Line(_))).unwrap().handle;
|
||||
(c.hex(), l.hex())
|
||||
};
|
||||
let r = s.execute("refedit", &json!({ "handle": a, "objects": [circle_src] })).unwrap();
|
||||
assert_eq!(r["faded"], 1);
|
||||
assert_eq!(handles(&r).len(), 1);
|
||||
// Add the host line: it goes into the block at close. Remove the circle: it stays in the block.
|
||||
let rs = s.execute("refset", &json!({ "add": [other], "remove": handles(&r) })).unwrap();
|
||||
assert_eq!(rs["added"], 1);
|
||||
assert_eq!(rs["removed"], 1);
|
||||
s.execute("refclose", &json!({})).unwrap();
|
||||
let d = s.doc().unwrap();
|
||||
let b = d.block("B").unwrap();
|
||||
assert_eq!(b.entities.len(), 3);
|
||||
assert!(b.entities.get(Handle::parse_hex(&line_src).unwrap()).is_some());
|
||||
assert!(d.entity(Handle::parse_hex(&other).unwrap()).is_none(), "moved into the block");
|
||||
// 300,0 relative to the reference at 100,0
|
||||
assert!(block_lines(d, "B").iter().any(|(p, q)| (p.x - 200.0).abs() < 1e-9 && (q.x - 210.0).abs() < 1e-9));
|
||||
assert!(s.execute("refset", &json!({ "add": ["FFFF"] })).is_err(), "no edit active");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_block_that_would_contain_itself_is_refused_and_the_edit_survives() {
|
||||
let (mut s, a, _) = setup();
|
||||
s.execute("refedit", &json!({ "handle": a })).unwrap();
|
||||
// A reference to B drawn while editing B.
|
||||
let nested = insert(&mut s, "B", (500.0, 0.0), 1.0, 0.0);
|
||||
let _ = nested;
|
||||
// inserted through the doc API: add it to the working set by hand
|
||||
s.execute("refset", &json!({ "add": [nested] })).unwrap();
|
||||
assert!(s.execute("refclose", &json!({})).is_err());
|
||||
assert!(s.doc().unwrap().refedit.is_some(), "the edit is still active");
|
||||
assert_eq!(s.doc().unwrap().block("B").unwrap().entities.len(), 2);
|
||||
s.execute("refclose", &json!({ "option": "discard" })).unwrap();
|
||||
assert!(s.doc().unwrap().refedit.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undo_takes_the_edit_back_with_its_state() {
|
||||
let (mut s, a, _) = setup();
|
||||
let n = model_len(&s);
|
||||
s.execute("refedit", &json!({ "handle": a })).unwrap();
|
||||
s.undo().unwrap();
|
||||
let d = s.doc().unwrap();
|
||||
assert!(d.refedit.is_none());
|
||||
assert_eq!(d.model.len(), n);
|
||||
assert!(d.entity(Handle::parse_hex(&a).unwrap()).unwrap().common.visible);
|
||||
s.redo().unwrap();
|
||||
assert!(s.doc().unwrap().refedit.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn files_never_hold_half_an_edit() {
|
||||
let (mut s, a, _) = setup();
|
||||
s.execute("refedit", &json!({ "handle": a })).unwrap();
|
||||
let d = s.doc().unwrap();
|
||||
let committed = d.committed();
|
||||
assert!(committed.refedit.is_none());
|
||||
assert_eq!(committed.model.len(), 2);
|
||||
assert!(committed.entity(Handle::parse_hex(&a).unwrap()).unwrap().common.visible);
|
||||
let dxf = cadcraft_io::write_dxf(d);
|
||||
let back = cadcraft_io::read_dxf(dxf.as_bytes()).unwrap();
|
||||
assert_eq!(back.model.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_requests_never_panic() {
|
||||
let (mut s, a, _) = setup();
|
||||
for p in [
|
||||
json!({}),
|
||||
json!({ "handle": "ZZZ" }),
|
||||
json!({ "handle": 1e300 }),
|
||||
json!({ "name": "nope" }),
|
||||
json!({ "handle": a, "objects": "x" }),
|
||||
json!({ "handle": a, "objects": [] }),
|
||||
json!({ "handle": a, "objects": ["FFFFFFFF"] }),
|
||||
json!({ "handle": a, "objects": [null, {}] }),
|
||||
] {
|
||||
assert!(s.execute("refedit", &p).is_err(), "{p}");
|
||||
}
|
||||
for cmd in ["refset", "refclose"] {
|
||||
assert!(s.execute(cmd, &json!({})).is_err());
|
||||
}
|
||||
// A reference with zero scale, a non-uniform one and a non-finite one can't be edited.
|
||||
let z = insert(&mut s, "B", (0.0, 0.0), 0.0, 0.0);
|
||||
assert!(s.execute("refedit", &json!({ "handle": z })).is_err());
|
||||
let nan = insert(&mut s, "B", (f64::NAN, 0.0), 1.0, 0.0);
|
||||
assert!(s.execute("refedit", &json!({ "handle": nan })).is_err());
|
||||
s.execute("refedit", &json!({ "handle": a })).unwrap();
|
||||
for p in [json!({ "add": "x" }), json!({ "remove": ["ZZ"] }), json!({ "add": [null] }), json!({ "handles": 3 }), json!({ "remove": ["FFFFFFF"] })]
|
||||
{
|
||||
assert!(s.execute("refset", &p).is_err(), "{p}");
|
||||
}
|
||||
assert!(s.execute("refclose", &json!({ "option": "maybe" })).is_err());
|
||||
assert!(s.doc().unwrap().refedit.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_block_that_references_itself_in_a_file_does_not_hang() {
|
||||
let (mut s, a, _) = setup();
|
||||
// Hostile drawing: B contains a reference to itself.
|
||||
let kind = EntityKind::Insert(Insert {
|
||||
block: "B".into(),
|
||||
insert: Vec3::ZERO,
|
||||
scale: Vec3::new(1.0, 1.0, 1.0),
|
||||
rotation: 0.0,
|
||||
attribs: vec![],
|
||||
cols: 1,
|
||||
rows: 1,
|
||||
col_spacing: 0.0,
|
||||
row_spacing: 0.0,
|
||||
});
|
||||
let d = s.doc_mut().unwrap();
|
||||
let hh = d.new_handle();
|
||||
let mut b = (**d.block("B").unwrap()).clone();
|
||||
b.entities.push(cadcraft_engine::doc::Entity::new(hh, kind));
|
||||
d.blocks.insert("B".into(), std::sync::Arc::new(b));
|
||||
s.execute("refedit", &json!({ "handle": a })).unwrap();
|
||||
assert!(s.execute("refclose", &json!({})).is_err());
|
||||
s.execute("refclose", &json!({ "option": "discard" })).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interactive_refedit_refset_and_refclose() {
|
||||
use cadcraft_engine::Input;
|
||||
let (mut s, a, _) = setup();
|
||||
let other = h(&s.execute("line", &json!({ "points": [[300, 0], [310, 0]] })).unwrap());
|
||||
s.start("refedit").unwrap();
|
||||
s.input(Input::Pick(vec![Handle::parse_hex(&a).unwrap()])).unwrap();
|
||||
assert!(s.running.is_none() && s.doc().unwrap().refedit.is_some());
|
||||
s.start("refset").unwrap();
|
||||
s.input(Input::Keyword("Add".into())).unwrap();
|
||||
s.input(Input::Pick(vec![Handle::parse_hex(&other).unwrap()])).unwrap();
|
||||
s.input(Input::Enter).unwrap();
|
||||
assert_eq!(s.doc().unwrap().refedit.as_ref().unwrap().items.len(), 3);
|
||||
s.start("refclose").unwrap();
|
||||
s.input(Input::Keyword("Save".into())).unwrap();
|
||||
assert!(s.doc().unwrap().refedit.is_none());
|
||||
assert_eq!(s.doc().unwrap().block("B").unwrap().entities.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xrefs_are_refused_with_a_message() {
|
||||
let mut s = Session::new();
|
||||
s.doc_mut().unwrap().blocks.insert(
|
||||
"X".into(),
|
||||
std::sync::Arc::new({
|
||||
let mut b = cadcraft_engine::doc::Block::new("X");
|
||||
b.xref_path = Some("x.dxf".into());
|
||||
b
|
||||
}),
|
||||
);
|
||||
let x = insert(&mut s, "X", (0.0, 0.0), 1.0, 0.0);
|
||||
let e = s.execute("refedit", &json!({ "handle": x })).unwrap_err();
|
||||
assert!(e.to_string().contains("external reference"), "{e}");
|
||||
}
|
||||
|
|
@ -1461,6 +1461,9 @@ pub(crate) fn dwg_bridge(d: &Drawing, dwg: Version) -> String {
|
|||
/// The drawing's DXF tags, declaring `version` in `$ACADVER`, with what `target` (the version the
|
||||
/// file ends up in: `version`, or the DWG version the bridge writes) has.
|
||||
fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> {
|
||||
// An in-place block edit in progress is not written: the drawing as before it.
|
||||
let committed = d.committed();
|
||||
let d: &Drawing = &committed;
|
||||
// Structural handles start above every entity handle.
|
||||
let mut w = W { t: Vec::new(), next: d.handseed.max(0x100).saturating_add(0x1000), pstyles: HashMap::new() };
|
||||
// Pre-allocate handles for block records.
|
||||
|
|
|
|||
|
|
@ -796,6 +796,11 @@
|
|||
Select first object Vyberte první objekt
|
||||
Select first object or Vyberte první objekt nebo
|
||||
Select first point Vyberte první bod
|
||||
Add to Working Set Přidat do pracovní sady
|
||||
Edit Reference In-Place Upravit referenci na místě
|
||||
Save Reference Edits Uložit úpravy reference
|
||||
Working Set Pracovní sada
|
||||
Xref and Block In-place Editing Úpravy xref a bloků na místě
|
||||
Select nested objects to copy or Vyberte vnořené objekty ke kopírování nebo
|
||||
Select object Vyberte objekt
|
||||
Select object to dimension Vyberte objekt ke kótování
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 6.
|
|
|
@ -673,6 +673,11 @@
|
|||
Select an object to change or Wählen Sie ein zu änderndes Objekt oder
|
||||
Select an object to measure or Wählen Sie ein zu messendes Objekt oder
|
||||
Select an object. Wählen Sie ein Objekt.
|
||||
Add to Working Set Zum Arbeitssatz hinzufügen
|
||||
Edit Reference In-Place Referenz an Ort und Stelle bearbeiten
|
||||
Save Reference Edits Referenzänderungen speichern
|
||||
Working Set Arbeitssatz
|
||||
Xref and Block In-place Editing Xref- und Block-Bearbeitung am Ort
|
||||
Select an open curve. Wählen Sie eine offene Kurve.
|
||||
Select an open line, arc, polyline, ellipse arc or spline. Wählen Sie eine offene Linie, einen Bogen, eine Polylinie, einen Ellipsenbogen oder einen Spline.
|
||||
Select arc or circle Wählen Sie einen Bogen oder Kreis
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 7.
|
|
|
@ -813,6 +813,11 @@
|
|||
Relative Relativas
|
||||
Remove Leader Eliminar directriz
|
||||
Remove the viewport override Eliminar modificación de la ventana gráfica
|
||||
Add to Working Set Añadir al conjunto de trabajo
|
||||
Edit Reference In-Place Editar referencia in situ
|
||||
Save Reference Edits Guardar cambios de la referencia
|
||||
Working Set Conjunto de trabajo
|
||||
Xref and Block In-place Editing Edición in situ de xrefs y bloques
|
||||
Rename Dimension Style Renombrar estilo de cota
|
||||
Rename Layer Renombrar capa
|
||||
Rename Layer State Renombrar estado de capas
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 7.
|
|
|
@ -658,6 +658,11 @@
|
|||
Select a line, arc, circle, polyline, ellipse or spline as the path. Sélectionner une ligne, un arc, un cercle, une polyligne, une ellipse ou une spline comme trajectoire.
|
||||
Select a line, circle, arc or polyline segment. Sélectionner une ligne, un cercle, un arc ou un segment de polyligne.
|
||||
Select a line. Sélectionner une ligne.
|
||||
Add to Working Set Ajouter au jeu de travail
|
||||
Edit Reference In-Place Modifier la référence sur place
|
||||
Save Reference Edits Enregistrer les modifications de la référence
|
||||
Working Set Jeu de travail
|
||||
Xref and Block In-place Editing Édition sur place des xrefs et des blocs
|
||||
Select a polyline. Sélectionner une polyligne.
|
||||
Select a text, mtext, attribute definition or dimension. Sélectionner un texte, un texte multiligne, une définition d'attribut ou une cote.
|
||||
Select an annotation object Sélectionner un objet d'annotation
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 7.
|
|
|
@ -657,6 +657,11 @@
|
|||
Select a line or an arc. Selecione uma linha ou um arco.
|
||||
Select a line or polyline segment Selecione uma linha ou segmento de polilinha
|
||||
Select a line, arc or open polyline. Selecione uma linha, arco ou polilinha aberta.
|
||||
Add to Working Set Adicionar ao conjunto de trabalho
|
||||
Edit Reference In-Place Editar referência no local
|
||||
Save Reference Edits Salvar alterações da referência
|
||||
Working Set Conjunto de trabalho
|
||||
Xref and Block In-place Editing Edição no local de xrefs e blocos
|
||||
Select a line, arc, circle, polyline, ellipse or spline as the path. Selecione uma linha, arco, círculo, polilinha, elipse ou spline como trajetória.
|
||||
Select a line, circle, arc or polyline segment. Selecione uma linha, círculo, arco ou segmento de polilinha.
|
||||
Select a line. Selecione uma linha.
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 7.
|
|
|
@ -658,6 +658,11 @@
|
|||
Select a line, arc, circle, polyline, ellipse or spline as the path. Выберите отрезок, дугу, круг, полилинию, эллипс или сплайн в качестве траектории.
|
||||
Select a line, circle, arc or polyline segment. Выберите отрезок, круг, дугу или сегмент полилинии.
|
||||
Select a line. Выберите отрезок.
|
||||
Add to Working Set Добавить в рабочий набор
|
||||
Edit Reference In-Place Редактировать ссылку на месте
|
||||
Save Reference Edits Сохранить изменения ссылки
|
||||
Working Set Рабочий набор
|
||||
Xref and Block In-place Editing Редактирование xref и блоков на месте
|
||||
Select a polyline. Выберите полилинию.
|
||||
Select a text, mtext, attribute definition or dimension. Выберите текст, многострочный текст, определение атрибута или размер.
|
||||
Select an annotation object Выберите аннотационный объект
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 7.
|
|
|
@ -817,6 +817,11 @@
|
|||
Select objects to array. Виберіть об'єкти для масиву.
|
||||
Select objects to stretch by crossing-window or crossing-polygon... Виберіть об'єкти для розтягування січною рамкою або січним багатокутником...
|
||||
Select objects: Виберіть об'єкти:
|
||||
Add to Working Set Додати до робочого набору
|
||||
Edit Reference In-Place Редагувати посилання на місці
|
||||
Save Reference Edits Зберегти зміни посилання
|
||||
Working Set Робочий набір
|
||||
Xref and Block In-place Editing Редагування xref і блоків на місці
|
||||
Select one polyline (Multiple is available through the JSON form). Виберіть одну полілінію (Multiple доступно через JSON).
|
||||
Select path curve Виберіть криву траєкторії
|
||||
Select point or object Виберіть точку або об'єкт
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 8.
|
|
|
@ -667,6 +667,11 @@
|
|||
Select an object 选择对象
|
||||
Select an object to change or Select an object to change or
|
||||
Select an object to measure or Select an object to measure or
|
||||
Add to Working Set 添加到工作集
|
||||
Edit Reference In-Place 就地编辑参照
|
||||
Save Reference Edits 保存参照编辑
|
||||
Working Set 工作集
|
||||
Xref and Block In-place Editing 外部参照和块的就地编辑
|
||||
Select an object. 选择一个对象。
|
||||
Select an open curve. 选择一条开曲线。
|
||||
Select an open line, arc, polyline, ellipse arc or spline. 选择开放的直线、圆弧、多段线、椭圆弧或样条曲线。
|
||||
|
|
@ -1011,4 +1016,4 @@
|
|||
Reverse zoom direction 反转缩放方向
|
||||
Calibrate 校准
|
||||
Move sideways to pan; push/pull or twist to zoom. 向侧面移动以平移;推拉或扭转以缩放。
|
||||
Left device button: zoom extents. Right device button: calibrate. 左侧设备按钮:范围缩放。右侧设备按钮:校准。
|
||||
Left device button: zoom extents. Right device button: calibrate. 左侧设备按钮:范围缩放。右侧设备按钮:校准。
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 7.
|
|
|
@ -25,7 +25,7 @@ do them first.
|
|||
| # | Gap | Evidence | User impact | Est. |
|
||||
|---|---|---|---|---|
|
||||
| F1 | **[alpha blocker]** Prompt options that still print "not available yet": STRETCH polygon and fence selection (crossing window only), -ATTEDIT one-at-a-time Position, Height, Angle and Style, colour books in -ATTEDIT Color, -RENAME of materials, plot styles and viewport configurations. SPLINEDIT Join, Edit vertex Add and Elevate order (#559), PEDIT Edit vertex and the other SPLINEDIT options (#558) done: Edit vertex Tangent directions last for the PEDIT session only (LWPOLYLINE stores none). MTEXT Dynamic/Static columns done (#547). SPLINE, MTEXT and MLEADER options done (#408): SPLINE Object fits the polyline's vertices (no spline frame is kept) (TRIM/EXTEND, PLINE arc mode and Length, XLINE done (#397); OFFSET, ROTATE and FILLET options done earlier) | `cmd/modify.rs`, `cmd/blocks/attedit.rs`, `cmd/props/rename.rs` | Low: the most-used commands are done | 2–4 |
|
||||
| F2 | XREF editing in place and clipping: REFEDIT and XCLIP are missing. XATTACH, the External References palette, -XREF (Bind/Detach/Path/Unload/Reload/Overlay), XBIND, XOPEN, overlays, relative paths and loading at open are done (#525). Unloaded state and xref-layer overrides last only for the session | `cmd/blocks/xref.rs` | Medium: xrefs can't be edited in place or clipped | 15–25 |
|
||||
| F2 | XREF editing in place and clipping: XCLIP is missing. REFEDIT, REFSET and REFCLOSE edit a block reference in place (working set, faded rest of the block, everything else locked, save or discard; #813); left: editing an xref in place (its source drawing is not written back; XOPEN opens it), picking nested references, a dimmed look for the locked drawing and a banner with Save and Discard, attribute sync of other references (ATTSYNC). XATTACH, the External References palette, -XREF (Bind/Detach/Path/Unload/Reload/Overlay), XBIND, XOPEN, overlays, relative paths and loading at open are done (#525). Unloaded state and xref-layer overrides last only for the session | `cmd/blocks/xref.rs` | Medium: xrefs can't be edited in place or clipped | 15–25 |
|
||||
| F3 | **[alpha blocker]** (block editor and REFEDIT; dynamic-block authoring is beta) No block editor (BEDIT) or dynamic blocks (parameters, actions, visibility, lookup); dynamic blocks from other files lose their behaviour | issue #185 offers help | High: standard libraries are dynamic | 50–75 |
|
||||
| F4 | Autosave leftovers: no autosave in the web build (browser storage); autosave files are DXF, not DWG; no AUDITCTL `.adt` log and no separate RECOVER mode in the file readers (SAVETIME, Drawing Recovery, AUDIT and RECOVER landed with #406) | `engine/src/autosave.rs`, `engine/src/audit.rs` | Low-medium (web users) | 3–5 |
|
||||
| F5 | Associative arrays (ASsociative, Levels, polar ROWs, ARRAYEDIT, ARRAYCLOSE) landed (#448); left: grip editing of arrays (count/spacing grips), ARRAYPATH is not associative, ARRAYEDIT has no item deletion (CTRL+click) and no Key point base points, levels only shift Z (no 3D view) | `cmd/array.rs`, `cmd/array/edit.rs` | Medium | 4–6 |
|
||||
|
|
|
|||
Loading…
Reference in New Issue