Block Editor on the current drawing; a drawing with authoring elements offers it (stacked on #833) (#885)

* Block Editor: BEDIT, BSAVE, BSAVEAS and BCLOSE for static blocks

BEDIT opens a block definition in the drawing's own tab with its base point
at the origin, and the block's objects are edited with the ordinary commands.
BSAVE writes them back into the definition, so every reference in the drawing
updates (one undo step in the drawing); BSAVEAS saves under a new name;
BCLOSE goes back to the drawing, asking whether to save unsaved changes.

- The drawing waits untouched while the editor is open (blockedit.rs): its
  undo history, selection and views come back on BCLOSE. Objects left
  unchanged are saved back exactly, so no coordinate drifts.
- Files save the drawing outside the editor; autosave keeps the editor's
  unsaved changes in the block; the close/quit prompt's Save puts them into
  the block first.
- Right-click a selected block reference: Block Editor. In the editor the
  canvas is tinted, the base point is marked, and a bar offers Save Block,
  Save Block As and Close Block Editor (also in the right-click menu). The
  Block tool set's Block Editor button is now live. BLOCKEDITOR sysvar.
- Layout and paper-space commands are refused while the editor is open; a
  block can't be saved to contain a reference to itself.
- Dynamic blocks: their data is kept; the editor says it can't edit their
  parameters yet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Edit Block Definition dialog; BEDIT can start a new block

BEDIT typed or started from a menu or the Tool Sets with no block reference
selected opens the Edit Block Definition dialog: the drawing's blocks sorted
by name, a preview and the description. OK opens the Block Editor on the
block named; a name no block has yet starts a new block, which BSAVE creates
(`bedit {name, create: true}`). A selected block reference still opens at
once, and JSON calls never open the dialog.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Edit Block Definition: centred window, list box; gaps.md F3

The dialog opens centred on the window, the blocks are a full-width list
in a list box of fixed height, and gaps.md F3 mentions the dialog.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* BEDIT: find dynamic block data on the block's BLOCK_RECORD

A dynamic block's ACAD_ENHANCEDBLOCK dictionary hangs off its BLOCK_RECORD
table entry, not the BLOCK entity, so the "parameters are kept" note never
showed for AutoCAD files. Regression test with the data laid out as an
AutoCAD DXF has it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* BEDIT: a dynamic block reference opens its dynamic block

A dynamic block reference with values of its own shows an anonymous block
(`*U12`), so the right-click Block Editor (and BEDIT on a picked or selected
reference) refused it and asked for a name. The anonymous block's record
names its dynamic block in AcDbBlockRepBTag xdata; BEDIT now opens that, as
AutoCAD does. Regression test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Block Editor on the current drawing; opening a drawing with authoring elements offers it

A dynamic block's own drawing (model space with authoring elements: its
block record's ACAD_ENHANCEDBLOCK) opened from the UI asks, as AutoCAD does,
"<file> contains authoring elements. Open in Block Editor?" Yes / No /
Cancel. Yes opens the Block Editor on <Current Drawing>: the drawing's own
objects, edited where they are; BSAVE keeps them, BCLOSE without saving puts
the drawing back. <Current Drawing> heads the Edit Block Definition list.
BSAVEAS is not available there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Cody Vogt <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-13 06:02:48 -04:00
1 parent abe352564e
commit d826450e03
10 files changed
+243 -30

No files matched your search

+54 -2
View File
@@ -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
//! `<Current Drawing>`): 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 `<Current Drawing>`).
pub const CURRENT_DRAWING: &str = "<Current Drawing>";
/// 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<Drawing> {
// 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`
+19 -14
View File
@@ -16,14 +16,14 @@ pub(super) fn specs() -> Vec<CommandSpec> {
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? (\"<Current Drawing>\": 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<String> {
(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<Value> {
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<Value> {
} 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<Value> {
};
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)
+42
View File
@@ -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": "<current drawing>", "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);
}
+55 -10
View File
@@ -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<String> {
let mut names: Vec<String> =
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<Result<Value, String>> {
@@ -67,16 +117,14 @@ fn preview(app: &mut CadApp, name: &str) -> Option<Arc<DisplayList>> {
{
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() {
+3 -1
View File
@@ -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() {
+1
View File
@@ -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),
+3
View File
@@ -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 Новий блок
1 # CADCraft Ukrainian interface catalog — original translations by @dmatviichuk.
1295
1296
1297
1298
1299
1300
1301
1302
+4
View File
@@ -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());
}
}
}
}
+61 -3
View File
@@ -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()), ["<Current Drawing>", "door", "Window"]);
assert_eq!(app.bedit.name, "<Current Drawing>");
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<cadcraft_engine::doc::Drawing, String> {
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": "<Current Drawing>" })).unwrap();
assert_eq!(app.session.state().unwrap().block_editor(), Some("<Current Drawing>"));
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);
}
+1
View File
@@ -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 `<Current Drawing>`, 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 |