diff --git a/crates/engine/src/blockedit.rs b/crates/engine/src/blockedit.rs index a62b4620..776909a6 100644 --- a/crates/engine/src/blockedit.rs +++ b/crates/engine/src/blockedit.rs @@ -8,6 +8,11 @@ //! Layer, style and block table changes made in the editor belong to the drawing too, as in //! AutoCAD: saving keeps the editor's tables and only puts back what is the drawing's own (its //! model and paper space objects, groups, constraints, tiled viewports and hidden objects). +//! +//! The editor can also open on the drawing itself ([`CURRENT_DRAWING`], AutoCAD's +//! ``): a dynamic block's own drawing, whose model space holds the block's +//! objects and authoring elements. Its objects are edited where they are; BSAVE keeps the +//! changes as the drawing's, BCLOSE without saving puts the drawing back as it was. use std::sync::Arc; @@ -19,6 +24,25 @@ use crate::{DocState, EngineError, Result, Snapshot}; /// The undo step a save into the block records in the drawing. const UNDO_LABEL: &str = "Block Editor"; +/// The name the Block Editor gives the drawing itself (BEDIT ``). +pub const CURRENT_DRAWING: &str = ""; + +/// Whether block `name` (or `*Model_Space`, a drawing's own) carries dynamic block data from +/// another program: parameters and actions in the `ACAD_ENHANCEDBLOCK` dictionary of its +/// BLOCK_RECORD table entry. +pub fn has_dynamic_data(d: &Drawing, name: &str) -> bool { + let key = ("BLOCK_RECORD".to_string(), name.to_ascii_uppercase()); + let Some(xdict) = d.foreign.records.get(&key).and_then(|x| x.xdict) else { return false }; + d.foreign.objects.iter().any(|o| o.handle == xdict && o.tags.iter().any(|t| t.code == 3 && t.value.eq_ignore_ascii_case("ACAD_ENHANCEDBLOCK"))) +} + +/// Whether drawing `d` has authoring elements: it is a dynamic block's own drawing, its model +/// space carrying the block's parameters and actions (AutoCAD offers to open it in the Block +/// Editor). +pub fn has_authoring_elements(d: &Drawing) -> bool { + has_dynamic_data(d, "*Model_Space") +} + /// The Block Editor open in a drawing. #[derive(Clone, Debug)] pub struct BlockEdit { @@ -34,6 +58,8 @@ pub struct BlockEdit { original: EntityStore, /// The block's base point, at the origin in the editor. base: Vec2, + /// The editor is open on the drawing itself ([`CURRENT_DRAWING`]). + pub current: bool, } /// Why a block can't be opened in the Block Editor, if it can't. @@ -106,6 +132,13 @@ impl BlockEdit { /// The drawing with the editor's objects saved into block `name` (the edited block, or a new /// name for BSAVEAS), from `editor`, the editor's drawing. pub fn compose(&self, editor: &Drawing, name: &str) -> Result { + // The drawing itself: the editor's drawing is the drawing. + if self.current { + if !name.eq_ignore_ascii_case(CURRENT_DRAWING) { + return Err(EngineError::Other("The current drawing can't be saved as a block from the Block Editor; use WBLOCK or BLOCK.".into())); + } + return Ok(editor.clone()); + } if refers_to(editor, &editor.model, name) { return Err(EngineError::Other(format!("Block \"{name}\" can't contain a reference to itself."))); } @@ -177,6 +210,19 @@ impl DocState { if let Some(open) = self.block_editor() { return Err(EngineError::Other(format!("The Block Editor is already open on \"{open}\"; close it first (BCLOSE)."))); } + if name.trim().eq_ignore_ascii_case(CURRENT_DRAWING) { + let host = Box::new(self.clone()); + let editor = self.doc.clone(); + self.enter_block_editor(BlockEdit { + name: CURRENT_DRAWING.into(), + host, + saved: editor, + original: EntityStore::new(), + base: Vec2::ZERO, + current: true, + }); + return Ok(()); + } let block = match self.doc.block(name) { None if create => Arc::new(cadcraft_doc::Block::new(name)), _ => { @@ -196,6 +242,13 @@ impl DocState { let host = Box::new(self.clone()); let editor = Arc::new(d); self.doc = editor.clone(); + self.enter_block_editor(BlockEdit { name: block.name.clone(), host, saved: editor, original: block.entities.clone(), base, current: false }); + Ok(()) + } + + /// Start editing in the Block Editor `be` (the drawing is already the editor's): a fresh + /// undo history, nothing selected, model space. + fn enter_block_editor(&mut self, be: BlockEdit) { self.undo.clear(); self.redo.clear(); self.undo_ctl = Default::default(); @@ -206,9 +259,8 @@ impl DocState { self.tiles = Default::default(); self.views.clear(); self.view_history.clear(); - self.block_edit = Some(BlockEdit { name: block.name.clone(), host, saved: editor, original: block.entities.clone(), base }); + self.block_edit = Some(be); self.revision += 1; - Ok(()) } /// Save the Block Editor's objects into the block (BSAVE), or into a new block `as_name` diff --git a/crates/engine/src/cmd/blocks/bedit.rs b/crates/engine/src/cmd/blocks/bedit.rs index 695dbbdd..f11bb309 100644 --- a/crates/engine/src/cmd/blocks/bedit.rs +++ b/crates/engine/src/cmd/blocks/bedit.rs @@ -16,14 +16,14 @@ pub(super) fn specs() -> Vec { CommandSpec::new("bedit", "Block Editor", run_bedit) .menu(&["Tools", "Block Editor"]) .alias(&["be", "-bedit"]) - .params("{name? | handle? (a block reference: edits its block); default the one selected block reference, create?: bool (a name no block has yet starts a new block; BSAVE creates it)} → {block, new}") + .params("{name? (\"\": the drawing itself, a dynamic block's own drawing) | handle? (a block reference: edits its block); default the one selected block reference, create?: bool (a name no block has yet starts a new block; BSAVE creates it)} → {block, new}") .enabled(not_editing) .noundo() .interactive(|_| Ok(Box::new(BeditM))), CommandSpec::new("bsave", "Save Block", run_bsave).params("{} → {block}").enabled(editing).noundo(), CommandSpec::new("bsaveas", "Save Block As", run_bsaveas) .params("{name, replace?: bool (redefine an existing block of that name)} → {block}") - .enabled(editing) + .enabled(editing_a_block) .noundo() .interactive(|_| Ok(Box::new(BsaveasM::default()))), CommandSpec::new("bclose", "Close Block Editor", run_bclose) @@ -42,6 +42,17 @@ fn editing(s: &Session) -> std::result::Result<(), String> { } } +/// The Block Editor is open on a block (not on the drawing itself, which BSAVEAS can't save as one). +fn editing_a_block(s: &Session) -> std::result::Result<(), String> { + editing(s)?; + match s.state() { + Ok(st) if st.block_edit.as_ref().is_some_and(|b| b.current) => { + Err("The Block Editor is open on the current drawing; there is no block to save under another name.".into()) + } + _ => Ok(()), + } +} + fn not_editing(s: &Session) -> std::result::Result<(), String> { match s.state() { Ok(st) => match st.block_editor() { @@ -95,18 +106,9 @@ fn dynamic_definition(d: &Drawing, name: &str) -> Option { (kind == "BLOCK_RECORD").then(|| d.block(block).map(|b| b.name.clone())).flatten() } -/// Whether block `name` carries dynamic block data from another program (parameters and actions -/// in its `ACAD_ENHANCEDBLOCK` dictionary), which CADCraft keeps but can't edit yet. The dictionary -/// hangs off the block's BLOCK_RECORD table entry. -fn has_dynamic_data(d: &Drawing, name: &str) -> bool { - let key = ("BLOCK_RECORD".to_string(), name.to_ascii_uppercase()); - let Some(xdict) = d.foreign.records.get(&key).and_then(|x| x.xdict) else { return false }; - d.foreign.objects.iter().any(|o| o.handle == xdict && o.tags.iter().any(|t| t.code == 3 && t.value.eq_ignore_ascii_case("ACAD_ENHANCEDBLOCK"))) -} - /// Open the Block Editor on block `name` and frame its objects. fn open(s: &mut Session, name: &str, create: bool) -> Result { - let new = create && s.doc()?.block(name).is_none(); + let new = create && !name.eq_ignore_ascii_case(crate::blockedit::CURRENT_DRAWING) && s.doc()?.block(name).is_none(); s.state_mut()?.open_block_editor(name, create)?; let name = s.state()?.block_editor().unwrap_or(name).to_string(); // The block's objects with room around them (and the editor's bar above them). @@ -118,7 +120,9 @@ fn open(s: &mut Session, name: &str, create: bool) -> Result { } else { format!("Block Editor: editing \"{name}\". BSAVE saves it into the drawing; BCLOSE closes the editor.") }); - if s.state().is_ok_and(|st| st.block_edit.as_ref().is_some_and(|be| has_dynamic_data(&be.host.doc, &be.name))) { + let dynamic = + |be: &crate::blockedit::BlockEdit| crate::blockedit::has_dynamic_data(&be.host.doc, if be.current { "*Model_Space" } else { &be.name }); + if s.state().is_ok_and(|st| st.block_edit.as_ref().is_some_and(dynamic)) { s.echo("This is a dynamic block: its parameters and actions are kept but can't be edited in CADCraft yet."); } Ok(json!({ "block": name, "new": new })) @@ -145,7 +149,8 @@ fn run_bedit(s: &mut Session, p: &Value) -> Result { }; let create = bool_or(p, "create", false); let name = name.trim(); - if create && s.doc()?.block(name).is_none() && !super::valid_block_name(name) { + let current = name.eq_ignore_ascii_case(crate::blockedit::CURRENT_DRAWING); + if create && !current && s.doc()?.block(name).is_none() && !super::valid_block_name(name) { return Err(bad("bedit", format!("\"{name}\" is not a valid block name"))); } open(s, name, create) diff --git a/crates/engine/tests/block_editor.rs b/crates/engine/tests/block_editor.rs index 838a50cd..3b1404f1 100644 --- a/crates/engine/tests/block_editor.rs +++ b/crates/engine/tests/block_editor.rs @@ -328,3 +328,45 @@ fn bedit_can_start_a_new_block() { } assert!(s.state().unwrap().block_editor().is_none()); } + +#[test] +fn the_current_drawing_opens_in_the_block_editor() { + use cadcraft_engine::blockedit::{CURRENT_DRAWING, has_authoring_elements}; + use cadcraft_engine::doc::foreign::{Foreign, ForeignExtra, ForeignObject}; + use cadcraft_engine::doc::{Handle, RawTag}; + let mut s = Session::new(); + line(&mut s, [0.0, 0.0], [10.0, 0.0]); + // A dynamic block's own drawing: model space's record carries ACAD_ENHANCEDBLOCK. + let mut d = s.doc().unwrap().clone(); + assert!(!has_authoring_elements(&d)); + let mut f = Foreign::default(); + let tag = |code, value: &str| RawTag { code, value: value.into() }; + f.records.insert(("BLOCK_RECORD".into(), "*MODEL_SPACE".into()), ForeignExtra { xdict: Some(Handle(0xF00)), ..Default::default() }); + f.objects.push(ForeignObject { handle: Handle(0xF00), kind: "DICTIONARY".into(), tags: vec![tag(3, "ACAD_ENHANCEDBLOCK"), tag(360, "F01")] }); + d.foreign = std::sync::Arc::new(f); + assert!(has_authoring_elements(&d)); + s.open_drawing(d, "symbol", None); + + // Opened on the drawing itself: its objects are there to edit, and it says it is dynamic. + let r = s.execute("bedit", &json!({ "name": "", "create": true })).unwrap(); + assert_eq!(r, json!({ "block": CURRENT_DRAWING, "new": false })); + assert_eq!(editor(&s).as_deref(), Some(CURRENT_DRAWING)); + assert_eq!(s.doc().unwrap().model.len(), 1); + assert!(s.log.iter().any(|l| l.contains("dynamic block")), "{:?}", s.log); + // A block of it can't be saved under another name here. + assert!(s.execute("bsaveas", &json!({ "name": "Copy" })).is_err()); + // BCLOSE without saving puts the drawing back. + line(&mut s, [0.0, 5.0], [10.0, 5.0]); + s.execute("bclose", &json!({ "save": false })).unwrap(); + assert_eq!(editor(&s), None); + assert_eq!(s.doc().unwrap().model.len(), 1); + + // Saved, the change is the drawing's: one undo step takes it back. + s.execute("bedit", &json!({ "name": CURRENT_DRAWING })).unwrap(); + line(&mut s, [0.0, 5.0], [10.0, 5.0]); + s.execute("bclose", &json!({})).unwrap(); + assert_eq!(s.doc().unwrap().model.len(), 2); + assert!(s.doc().unwrap().block(CURRENT_DRAWING).is_none(), "no block is made"); + s.undo().unwrap(); + assert_eq!(s.doc().unwrap().model.len(), 1); +} diff --git a/crates/ui-egui/src/bedit.rs b/crates/ui-egui/src/bedit.rs index 1d0f9e78..0166e72a 100644 --- a/crates/ui-egui/src/bedit.rs +++ b/crates/ui-egui/src/bedit.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use cadcraft_color::Rgb; +use cadcraft_engine::blockedit::CURRENT_DRAWING; use cadcraft_engine::doc::{Drawing, EntityKind}; use cadcraft_geom::Vec2; use cadcraft_render::DisplayList; @@ -32,9 +33,58 @@ pub fn editable_blocks(d: &Drawing) -> Vec { let mut names: Vec = d.blocks.values().filter(|b| !b.anonymous && !b.name.starts_with('*') && b.xref_path.is_none()).map(|b| b.name.clone()).collect(); names.sort_by_key(|n| n.to_lowercase()); + // The drawing itself comes first, as in AutoCAD. + names.insert(0, CURRENT_DRAWING.to_string()); names } +/// Opening a drawing with authoring elements (a dynamic block's own drawing) asks whether to +/// open it in the Block Editor, as AutoCAD does: Yes edits the drawing itself in the Block +/// Editor, No opens it as usual, Cancel closes it again. +pub fn authoring_dialog(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) { + let name = app + .session + .state() + .ok() + .and_then(|st| st.path.clone()) + .and_then(|p| std::path::Path::new(&p).file_name().map(|f| f.to_string_lossy().into_owned())) + .unwrap_or_else(|| crate::i18n::t("This drawing").to_string()); + let mut choice: Option<&str> = None; + egui::Window::new(crate::tl!("Open in Block Editor")) + .id(egui::Id::new("Open in Block Editor")) + .open(open) + .collapsible(false) + .resizable(false) + .pivot(egui::Align2::CENTER_CENTER) + .default_pos(ctx.content_rect().center()) + .show(ctx, |ui| { + ui.set_max_width(420.0); + ui.label(format!("{name} {}", crate::i18n::t("contains authoring elements. Open in Block Editor?"))); + ui.add_space(8.0); + ui.horizontal(|ui| { + if ui.button(crate::tl!("Yes")).clicked() { + choice = Some("yes"); + } + if ui.button(crate::tl!("No")).clicked() { + choice = Some("no"); + } + if ui.button(crate::tl!("Cancel")).clicked() { + choice = Some("cancel"); + } + }); + }); + let Some(c) = choice else { return }; + *open = false; + let r = match c { + "yes" => app.run("bedit", json!({ "name": CURRENT_DRAWING })).map(|_| ()), + "cancel" => app.run("close", Value::Null).map(|_| ()), + _ => Ok(()), + }; + if let Err(e) = r { + app.session.echo(e); + } +} + /// BEDIT typed or from a menu without parameters opens the dialog, unless exactly one block /// reference is selected (it opens at once) or the Block Editor is open (BEDIT says why not). pub fn route(app: &mut CadApp, id: &str, params: &Value) -> Option> { @@ -67,16 +117,14 @@ fn preview(app: &mut CadApp, name: &str) -> Option> { { return Some(list.clone()); } - let block = st.doc.block(name)?; - let rough = cadcraft_render::build_entities(&st.doc, block.entities.iter().map(|e| e.as_ref()), &cadcraft_render::Options::default()); + // The drawing itself shows its model space. + let entities = if name.eq_ignore_ascii_case(CURRENT_DRAWING) { &st.doc.model } else { &st.doc.block(name)?.entities }; + let rough = cadcraft_render::build_entities(&st.doc, entities.iter().map(|e| e.as_ref()), &cadcraft_render::Options::default()); let size = rough.bounds.width().max(rough.bounds.height()); // About half a pixel of the preview's 200 points. let opts = cadcraft_render::Options { tolerance: (size / 400.0).max(1e-9), ..Default::default() }; - let list = Arc::new(if rough.bounds.is_empty() { - rough - } else { - cadcraft_render::build_entities(&st.doc, block.entities.iter().map(|e| e.as_ref()), &opts) - }); + let list = + Arc::new(if rough.bounds.is_empty() { rough } else { cadcraft_render::build_entities(&st.doc, entities.iter().map(|e| e.as_ref()), &opts) }); app.bedit.preview = Some((key, list.clone())); Some(list) } @@ -139,9 +187,6 @@ pub fn dialog(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) { egui::ScrollArea::vertical().id_salt("bedit-blocks").max_height(250.0).min_scrolled_height(250.0).show(ui, |ui| { ui.set_width(222.0); ui.with_layout(egui::Layout::top_down_justified(egui::Align::Min), |ui| { - if names.is_empty() { - ui.label(RichText::new(crate::tl!("No blocks defined in this drawing.")).color(t.text_dim)); - } for n in &names { let row = ui.selectable_label(existing.as_deref() == Some(n.as_str()), n); if row.clicked() { diff --git a/crates/ui-egui/src/canvas/blockedit.rs b/crates/ui-egui/src/canvas/blockedit.rs index e8405b2d..d30d2314 100644 --- a/crates/ui-egui/src/canvas/blockedit.rs +++ b/crates/ui-egui/src/canvas/blockedit.rs @@ -54,7 +54,9 @@ pub(super) fn paint(app: &mut CadApp, ctx: &egui::Context, p: &egui::Painter, xf if ui.add_enabled(unsaved, egui::Button::new(crate::tl!("Save Block"))).clicked() { action = Some(("bsave", false)); } - if ui.button(crate::tl!("Save Block As")).clicked() { + // The drawing itself has no block to save under another name. + let current = name == cadcraft_engine::blockedit::CURRENT_DRAWING; + if ui.add_enabled(!current, egui::Button::new(crate::tl!("Save Block As"))).clicked() { action = Some(("bsaveas", true)); } if ui.button(crate::tl!("Close Block Editor")).clicked() { diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 2436861e..d8ea880c 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -27,6 +27,7 @@ pub fn show(app: &mut CadApp, ctx: &egui::Context) { "commands" => commands(app, ctx, &mut open), "blocks" => blocks(app, ctx, &mut open), "bedit" => crate::bedit::dialog(app, ctx, &mut open), + "authoring" => crate::bedit::authoring_dialog(app, ctx, &mut open), "stylesmanager" => crate::plotstyles::dialog(app, ctx, &mut open), o if o == "options" || o.starts_with("options:") => crate::options::dialog(app, ctx, o, &mut open), other => crate::managers::dialog(app, ctx, other, &mut open), diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index a9fb2cc0..1452707d 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1295,5 +1295,8 @@ Unsaved changes Незбережені зміни Edit Block Definition Редагувати визначення блока Block to create or edit Блок для створення або редагування + Open in Block Editor Відкрити в редакторі блоків + This drawing Це креслення + contains authoring elements. Open in Block Editor? містить елементи розробки. Відкрити в редакторі блоків? Preview Попередній перегляд New block Новий блок diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 817a2400..c4817dbd 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -507,6 +507,10 @@ impl CadApp { if let Some(w) = v.get("warnings").and_then(Value::as_array).and_then(|w| w.first()).and_then(Value::as_str) { self.set_status(w.to_string()); } + // A dynamic block's own drawing: ask whether to open it in the Block Editor. + if self.session.state().is_ok_and(|st| cadcraft_engine::blockedit::has_authoring_elements(&st.doc)) { + self.ui.dialog = Some("authoring".into()); + } } } } diff --git a/crates/ui-egui/tests/bedit_dialog.rs b/crates/ui-egui/tests/bedit_dialog.rs index 3d7b6ebd..d12f1457 100644 --- a/crates/ui-egui/tests/bedit_dialog.rs +++ b/crates/ui-egui/tests/bedit_dialog.rs @@ -42,11 +42,12 @@ fn bedit_without_a_selected_reference_opens_the_dialog() { let mut app = app_with_blocks(); app.cmdline("BEDIT"); assert_eq!(app.ui.dialog.as_deref(), Some("bedit")); - // Blocks sorted by name, case-insensitively; the first is offered. - assert_eq!(bedit::editable_blocks(app.session.doc().unwrap()), ["door", "Window"]); - assert_eq!(app.bedit.name, "door"); + // The drawing itself, then the blocks sorted by name, case-insensitively; the first is offered. + assert_eq!(bedit::editable_blocks(app.session.doc().unwrap()), ["", "door", "Window"]); + assert_eq!(app.bedit.name, ""); assert!(app.session.state().unwrap().block_editor().is_none(), "the dialog changes nothing by itself"); + app.bedit.name = "door".into(); let ctx = egui::Context::default(); app.logic(&ctx); painted(&mut app, &ctx); @@ -103,3 +104,60 @@ fn a_selected_reference_and_json_calls_skip_the_dialog() { assert!(app.ui.dialog.is_none(), "JSON calls never open dialogs"); assert_eq!(app.session.state().unwrap().block_editor(), Some("door")); } + +/// What the stand-in reader opens: for a file saying "authoring", a dynamic block's own drawing +/// as AutoCAD saves one (model space's block record has an extension dictionary holding an +/// `ACAD_ENHANCEDBLOCK` evaluation graph); otherwise a plain drawing. Both have one line. +fn read(bytes: &[u8], _name: &str) -> Result { + use cadcraft_engine::doc::foreign::{Foreign, ForeignExtra, ForeignObject}; + use cadcraft_engine::doc::{Common, EntityKind, Handle, Line, RawTag, Space}; + use cadcraft_engine::geom::Vec3; + let mut d = cadcraft_engine::doc::Drawing::new_imperial(); + d.add(&Space::Model, Common::default(), EntityKind::Line(Line { a: Vec3::ZERO, b: Vec3::new(10.0, 0.0, 0.0) })).map_err(|e| e.to_string())?; + if bytes == b"authoring" { + let mut f = Foreign::default(); + let tag = |code, value: &str| RawTag { code, value: value.into() }; + f.records.insert(("BLOCK_RECORD".into(), "*MODEL_SPACE".into()), ForeignExtra { xdict: Some(Handle(0xF00)), ..Default::default() }); + f.objects.push(ForeignObject { handle: Handle(0xF00), kind: "DICTIONARY".into(), tags: vec![tag(3, "ACAD_ENHANCEDBLOCK"), tag(360, "F01")] }); + d.foreign = std::sync::Arc::new(f); + } + Ok(d) +} + +#[test] +fn opening_a_drawing_with_authoring_elements_offers_the_block_editor() { + use cadcraft_engine::cmd::file::{IoHooks, set_io}; + set_io(IoHooks { read, write: |_, _| Ok(Vec::new()), plot: None }); + let dir = std::env::temp_dir().join(format!("cadcraft-authoring-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let symbol = dir.join("Symbol.dxf"); + std::fs::write(&symbol, "authoring").unwrap(); + let mut app = CadApp::new(Session::new(), Services::default()); + app.open_path(&symbol.to_string_lossy()); + assert_eq!(app.ui.dialog.as_deref(), Some("authoring")); + let ctx = egui::Context::default(); + app.logic(&ctx); + painted(&mut app, &ctx); + let text = painted(&mut app, &ctx); + assert!(text.contains("Symbol.dxf contains authoring elements. Open in Block Editor?"), "{text}"); + for s in ["Yes", "No", "Cancel"] { + assert!( + text.contains(s), + "{s} missing from: +{text}" + ); + } + // Yes is BEDIT on the drawing itself. + app.run("bedit", json!({ "name": "" })).unwrap(); + assert_eq!(app.session.state().unwrap().block_editor(), Some("")); + assert_eq!(app.session.doc().unwrap().model.len(), 1); + + // A drawing without authoring elements opens without asking. + let plain = dir.join("Plain.dxf"); + std::fs::write(&plain, "plain").unwrap(); + let mut app = CadApp::new(Session::new(), Services::default()); + app.open_path(&plain.to_string_lossy()); + assert!(app.session.state().unwrap().path.is_some(), "opened"); + assert!(app.ui.dialog.is_none()); + let _ = std::fs::remove_dir_all(&dir); +} diff --git a/docs/gaps.md b/docs/gaps.md index a83028c5..30c9bbdf 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -152,6 +152,7 @@ Detail: [localization-parity.md](localization-parity.md). | 2026-10-12 | trivial | F13: HATCHEDIT refuses a bad scale or angle before editing (#724) | | 2026-10-12 | minor | F3: dynamic block references show their grips (distance arrows, position squares, visibility/lookup lists, per visibility state) and drag them (`blocks.reference.grips`, `blocks.reference.grip`) | | 2026-10-12 | minor | Multiline attributes read, drawn and saved (they lost their text before): F17 narrowed to creating them | +| 2026-10-13 | minor | F3: opening a dynamic block's own drawing (authoring elements in model space) offers the Block Editor on ``, as AutoCAD does | | 2026-10-12 | minor | Edit Block Definition dialog (block list, preview, description; a new name starts a new block): F3 text updated, hours unchanged | | 2026-10-12 | minor | Block Editor (BEDIT, BSAVE, BSAVEAS, BCLOSE) for static blocks landed: rows 4 (65–100 → 55–90 h) and F3 (50–75 → 40–65 h) narrowed to REFEDIT and dynamic blocks | | 2026-10-12 | minor | SCRIPT command reading `.scr` files landed (runs a file through the same script engine as CLI/MCP; nested scripts refused): E2 removed |