Merge pull request #1793 from ramox81/codex/block-definition-command-line

Block Definition: command-line forms, block record data and dialog defaults

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-15 00:14:21 +03:00
commit 838e397a6c
23 files changed
+1860 -435

No files matched your search

+12 -2
View File
@@ -79,8 +79,9 @@ const WEB_ALIAS_KEY: &str = "opencadstudio.aliases";
/// defaults add aliases or change a default target, so existing profiles can be
/// migrated forward (see `introduced_at` / `migrate_aliases`). Version 1 shipped
/// the pre-REDRAW table; version 2 introduced the REDRAW-family aliases; version 3
/// introduced HB (HATCHTOBACK); version 4 introduced ER (EXTERNALREFERENCES).
const DEFAULT_ALIASES_VERSION: u32 = 9;
/// introduced HB (HATCHTOBACK); version 4 introduced ER (EXTERNALREFERENCES);
/// version 10 the block command-line forms and block editor shortcuts.
const DEFAULT_ALIASES_VERSION: u32 = 10;
#[cfg(target_arch = "wasm32")]
const WEB_ALIAS_VERSION_KEY: &str = "opencadstudio.aliases.version";
@@ -104,6 +105,15 @@ fn introduced_at(version: u32) -> &'static [(&'static str, &'static str)] {
7 => &[("DV", "DVIEW")],
8 => &[("-XR", "-XREF"), ("XC", "XCLIP"), ("ICL", "IMAGECLIP")],
9 => &[("IAD", "IMAGEADJUST")],
10 => &[
("-B", "-BLOCK"),
("-W", "-WBLOCK"),
("BC", "BCLOSE"),
("BS", "BSAVE"),
("BVS", "BVSTATE"),
("-ATE", "-ATTEDIT"),
("ATTE", "-ATTEDIT"),
],
_ => &[],
}
}
+11 -3
View File
@@ -764,16 +764,21 @@ impl OpenCADStudio {
mut options: Box<crate::scene::CreateBlockOptions>,
) -> Option<Task<Message>> {
let i = self.active_tab;
// No objects at all defines an empty block; objects that were all on
// locked layers define nothing.
let picked = options.handles.len();
options
.handles
.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
if options.handles.is_empty() {
if picked > 0 && options.handles.is_empty() {
self.command_line
.push_info(crate::t!("No editable objects selected.").as_ref());
return Some(Task::none());
}
self.push_undo_snapshot(i, "BLOCK");
let name = options.name.clone();
let redefined = options.redefine
&& self.tabs[i].scene.document.block_records.get(&name).is_some();
match self.tabs[i].scene.create_block_with_options(*options) {
Ok(insert_handle) => {
self.tabs[i].dirty = true;
@@ -784,8 +789,11 @@ impl OpenCADStudio {
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.command_line
.push_output(crate::tf!("Block \"{name}\" created.").as_ref());
// A new definition is not reported; a redefinition is.
if redefined {
self.command_line
.push_output(&format!("Block \"{name}\" redefined"));
}
self.refresh_properties();
self.note_recent_block(&name);
self.refresh_block_palette();
+242
View File
@@ -0,0 +1,242 @@
//! Command-line Block Definition commands: -BLOCK, -WBLOCK, -BEDIT,
//! ATTSYNC, INSBASE, and the BATTMAN check for attributed blocks. The
//! interactive steps live in `modules::insert::block_cmdline`.
use crate::app::{Message, OpenCADStudio};
use crate::modules::insert::block_cmdline::{
AttsyncCommand, BlockTable, DashBeditCommand, DashBlockCommand, DashWblockCommand,
WblockSource,
};
use iced::Task;
impl OpenCADStudio {
pub(super) fn dispatch_block_cmdline(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
use crate::command::CadCommand;
match cmd {
"-BLOCK" | "-BMAKE" => {
let preselected: Vec<_> = self.tabs[i]
.scene
.selected_entities()
.into_iter()
.map(|(h, _)| h)
.collect();
let table = BlockTable::of(&self.tabs[i].scene.document);
let units = self.tabs[i].scene.document.header.insertion_units;
let command =
DashBlockCommand::new(table, self.tabs[i].ucs_xform(), preselected, units);
self.command_line.push_output(&command.settings_line());
self.start_block_cmdline(i, Box::new(command));
}
"-WBLOCK" | "-WB" => {
let preselected: Vec<_> = self.tabs[i].scene.selected.iter().copied().collect();
let table = BlockTable::of(&self.tabs[i].scene.document);
let command = DashWblockCommand::new(table, preselected);
self.command_line.push_output(&command.settings_line());
if crate::sys::file_dia() {
// FILEDIA 1: the output file comes from the file dialog,
// the rest from the command line.
return Some(Task::done(Message::WblockSave("-".to_string())));
}
self.start_block_cmdline(i, Box::new(command));
}
cmd if cmd.starts_with("_-WBLOCKFILE ") => {
let path = cmd["_-WBLOCKFILE ".len()..].trim().to_string();
let preselected: Vec<_> = self.tabs[i].scene.selected.iter().copied().collect();
let table = BlockTable::of(&self.tabs[i].scene.document);
let mut command = DashWblockCommand::new(table, preselected);
let _ = command.on_text_input(&path);
self.start_block_cmdline(i, Box::new(command));
}
cmd if cmd.starts_with("_WBLOCKWRITE ") => {
self.tabs[i].active_cmd = None;
let Ok(request) =
serde_json::from_str::<serde_json::Value>(&cmd["_WBLOCKWRITE ".len()..])
else {
return Some(Task::none());
};
let path = std::path::PathBuf::from(request["path"].as_str().unwrap_or(""));
let source = &request["source"];
let source = if let Some(name) = source["block"].as_str() {
WblockSource::Block(name.to_string())
} else if source["drawing"].as_bool() == Some(true) {
WblockSource::Drawing
} else {
let handles = source["handles"]
.as_array()
.map(|a| {
a.iter()
.filter_map(|v| v.as_u64())
.map(codec::Handle::new)
.collect()
})
.unwrap_or_default();
let b = &source["base"];
let base = glam::DVec3::new(
b[0].as_f64().unwrap_or(0.0),
b[1].as_f64().unwrap_or(0.0),
b[2].as_f64().unwrap_or(0.0),
);
let mode = crate::modules::insert::block_cmdline::mode_from_code(
source["mode"].as_u64().unwrap_or(0) as u8,
);
WblockSource::Objects {
handles,
base,
mode,
}
};
let unit = self.tabs[i].scene.document.header.insertion_units;
return Some(self.wblock_write(i, source, path, unit));
}
"-BEDIT" => {
if self.tabs[i].refedit_session.is_some() {
self.command_line.push_error(
crate::t!("BEDIT: finish the active REFEDIT (REFCLOSE) first.").as_ref(),
);
return Some(Task::none());
}
let table = BlockTable::of(&self.tabs[i].scene.document);
self.start_block_cmdline(i, Box::new(DashBeditCommand::new(table)));
}
"ATTSYNC" => {
let doc = &self.tabs[i].scene.document;
let table = BlockTable::of(doc);
let references = doc
.entities()
.filter_map(|entity| match entity {
codec::EntityType::Insert(insert) => {
Some((insert.common.handle, insert.block_name.clone()))
}
_ => None,
})
.collect();
self.start_block_cmdline(i, Box::new(AttsyncCommand::new(table, references)));
}
cmd if cmd.starts_with("_ATTSYNCBLOCK ") => {
self.tabs[i].active_cmd = None;
let block = cmd["_ATTSYNCBLOCK ".len()..].trim().to_string();
self.attsync_block(i, &block);
self.command_line.push_output("ATTSYNC complete.");
}
"INSBASE" => {
let base = self.current_insertion_base(i);
let command = crate::modules::insert::base_point::BaseCommand::insbase(base);
self.start_block_cmdline(i, Box::new(command));
}
cmd if cmd == "ATTMAN"
|| cmd == "BATTMAN"
|| cmd.starts_with("ATTMAN ")
|| cmd.starts_with("BATTMAN ") =>
{
if BlockTable::of(&self.tabs[i].scene.document)
.attributed
.is_empty()
{
self.command_line
.push_output("This drawing contains no attributed blocks.");
return Some(Task::none());
}
self.open_attedit_dialog();
}
_ => return None,
}
Some(self.finish_dispatch(cmd))
}
fn start_block_cmdline(&mut self, i: usize, command: Box<dyn crate::command::CadCommand>) {
self.command_line.push_info(&command.prompt());
self.command_line.set_step_options(command.options());
self.tabs[i].active_cmd = Some(command);
}
/// The insertion base of the active space (INSBASE / BASE).
pub(in crate::app) fn current_insertion_base(&self, i: usize) -> glam::DVec3 {
let header = &self.tabs[i].scene.document.header;
let base = if self.tabs[i].scene.current_layout != "Model" {
header.paper_space_insertion_base
} else {
header.model_space_insertion_base
};
glam::DVec3::new(base.x, base.y, base.z)
}
/// Rebuild the attributes of every editable reference of `block` from the
/// block's attribute definitions, in their order: each attribute takes
/// its definition's placement and text settings through the reference's
/// transform, keeps the value it had (new ones take the default), and
/// attributes whose definition is gone are dropped. Constant definitions
/// carry no attribute.
pub(in crate::app) fn attsync_block(&mut self, i: usize, block: &str) {
use codec::Entity as _;
let doc = &self.tabs[i].scene.document;
let attdefs: Vec<codec::entities::AttributeDefinition> = doc
.block_records
.get(block)
.map(|br| {
br.entity_handles
.iter()
.filter_map(|h| doc.get_entity(*h))
.filter_map(|e| match e {
codec::EntityType::AttributeDefinition(a) if !a.flags.constant => {
Some(a.clone())
}
_ => None,
})
.collect()
})
.unwrap_or_default();
let base = crate::scene::render_graph::block_base_point(doc, block);
let inserts: Vec<_> = doc
.entities()
.filter_map(|entity| match entity {
codec::EntityType::Insert(insert)
if insert.block_name.eq_ignore_ascii_case(block)
&& !self.tabs[i].scene.is_layer_locked(insert.common.handle) =>
{
Some(insert.common.handle)
}
_ => None,
})
.collect();
if inserts.is_empty() {
return;
}
self.push_undo_snapshot(i, "ATTSYNC");
let mut changes = Vec::new();
for handle in inserts {
let Some(codec::EntityType::Insert(ins)) = self.tabs[i].scene.document.get_entity(handle)
else {
continue;
};
let transform = crate::scene::render_graph::insert_transform_with_base(ins, base);
let mut rebuilt = Vec::with_capacity(attdefs.len());
for def in &attdefs {
let previous = ins
.attributes
.iter()
.find(|a| a.tag.eq_ignore_ascii_case(&def.tag));
let mut attribute = codec::entities::AttributeEntity::from_definition(
def,
previous.map(|a| a.value.clone()),
);
attribute.apply_transform(&transform);
attribute.common.handle = previous.map_or(codec::Handle::NULL, |a| a.common.handle);
attribute.common.owner_handle = handle;
rebuilt.push(attribute);
}
for attribute in &mut rebuilt {
if attribute.common.handle.is_null() {
attribute.common.handle = self.tabs[i].scene.document.allocate_handle();
}
}
if let Some(codec::EntityType::Insert(ins)) =
self.tabs[i].scene.document.get_entity_mut(handle)
{
ins.attributes = rebuilt;
changes.push((handle, crate::scene::ChangeKind::Modified));
}
}
self.tabs[i].scene.bump_entities(&changes);
self.tabs[i].dirty = true;
}
}
+33 -153
View File
@@ -55,6 +55,14 @@ impl OpenCADStudio {
return Task::none();
};
self.active_modal = None;
{
let mut remembered = crate::modules::insert::block_cmdline::defaults();
remembered.base_on_screen = state.base_point_specify_onscreen;
remembered.objects_on_screen = state.objects_specify_onscreen;
remembered.mode = state.object_mode;
remembered.allow_exploding = state.allow_exploding;
}
self.persist_settings_if_changed();
let i = self.active_tab;
let base_point = state.parse_base_point();
let ucs = self.tabs[i].ucs_xform();
@@ -121,7 +129,7 @@ impl OpenCADStudio {
let rest = cmd.strip_prefix("BASE").unwrap_or("").trim();
if rest.is_empty() {
use crate::modules::insert::base_point::BaseCommand;
let c = BaseCommand::new();
let c = BaseCommand::new(self.current_insertion_base(i));
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
} else {
@@ -150,20 +158,10 @@ impl OpenCADStudio {
.model_space_insertion_base = pt;
}
self.tabs[i].dirty = true;
let space = if is_paper {
"paper space"
} else {
"model space"
};
self.command_line.push_output(
crate::tf!(
"Base point ({}, {}, {}) set for {space}.",
nums[0],
nums[1],
z
)
.as_ref(),
);
if z != 0.0 {
self.command_line
.push_output("Beware, Z insertion base is not zero.");
}
} else {
self.command_line
.push_error(crate::t!("Usage: BASE <x> <y> [z]").as_ref());
@@ -470,26 +468,6 @@ impl OpenCADStudio {
self.active_modal = Some(crate::app::ModalKind::BlockDefinition);
}
"-BLOCK" | "-BMAKE" => {
let handles: Vec<_> = self.tabs[i]
.scene
.selected_entities()
.into_iter()
.map(|(h, _)| h)
.collect();
if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand;
let cmd = SelectObjectsCommand::new("-BLOCK");
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
} else {
use crate::modules::insert::create_block::CreateBlockCommand;
let cmd = CreateBlockCommand::new(handles);
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
}
cmd if cmd.starts_with("BLOCK_POINT_PICKED ") => {
let parts: Vec<f64> = cmd["BLOCK_POINT_PICKED ".len()..]
.split_whitespace()
@@ -631,108 +609,21 @@ impl OpenCADStudio {
}
}
// ATTSYNC <block> — reconcile every insert of <block> against the
// block's current attribute definitions: drop attributes whose tag
// no longer exists and add any newly-defined ones (keeping the values
// of attributes that remain).
"ATTSYNC" => {
use crate::command::ValuePromptCommand;
let c = ValuePromptCommand::new("ATTSYNC", "ATTSYNC block name to sync:");
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
// ATTSYNC <block>: the named block, without prompts.
cmd if cmd.starts_with("ATTSYNC ") => {
let arg = cmd.trim_start_matches("ATTSYNC").trim();
let blocks = self.tabs[i].scene.custom_block_names();
if arg.is_empty() {
self.command_line.push_info(
crate::tf!(
"Usage: ATTSYNC <block name>. Blocks: {}",
if blocks.is_empty() {
"(none)".to_string()
} else {
blocks.join(", ")
}
)
.as_ref(),
);
return Some(Task::none());
}
let Some(block) = blocks.iter().find(|b| b.eq_ignore_ascii_case(arg)).cloned()
else {
self.command_line
.push_error(crate::tf!("ATTSYNC: no block named \"{arg}\".").as_ref());
return Some(Task::none());
};
// Gather the block's attribute definitions (tag, default value).
let doc = &self.tabs[i].scene.document;
let attdefs: Vec<(String, String)> = doc
.block_records
.get(&block)
.map(|br| {
br.entity_handles
.iter()
.filter_map(|h| doc.get_entity(*h))
.filter_map(|e| match e {
codec::EntityType::AttributeDefinition(a) => {
Some((a.tag.clone(), a.default_value.clone()))
}
_ => None,
})
.collect()
})
.unwrap_or_default();
let inserts: Vec<_> = self.tabs[i]
.scene
.document
.entities()
.filter_map(|entity| match entity {
codec::EntityType::Insert(insert)
if insert.block_name.eq_ignore_ascii_case(&block)
&& !self.tabs[i].scene.is_layer_locked(insert.common.handle) =>
{
Some(insert.common.handle)
}
_ => None,
})
.collect();
if inserts.is_empty() {
self.command_line
.push_error(crate::t!("ATTSYNC: no editable block references.").as_ref());
return Some(Task::none());
}
self.push_undo_snapshot(i, "ATTSYNC");
let mut synced = 0usize;
let mut changes = Vec::new();
for handle in inserts {
let Some(codec::EntityType::Insert(ins)) =
self.tabs[i].scene.document.get_entity_mut(handle)
else {
continue;
};
ins.attributes
.retain(|a| attdefs.iter().any(|(t, _)| t.eq_ignore_ascii_case(&a.tag)));
for (tag, default) in &attdefs {
if !ins
.attributes
.iter()
.any(|a| a.tag.eq_ignore_ascii_case(tag))
{
ins.attributes.push(codec::entities::AttributeEntity::new(
tag.clone(),
default.clone(),
));
}
let arg = cmd.trim_start_matches("ATTSYNC").trim().to_string();
let table =
crate::modules::insert::block_cmdline::BlockTable::of(&self.tabs[i].scene.document);
match table.attributed.iter().find(|b| b.eq_ignore_ascii_case(&arg)) {
Some(block) => {
let block = block.clone();
self.attsync_block(i, &block);
self.command_line.push_output("ATTSYNC complete.");
}
synced += 1;
changes.push((ins.common.handle, crate::scene::ChangeKind::Modified));
None => self.command_line.push_output(&format!(
"Block table contains no attributed block named {arg}."
)),
}
self.tabs[i].scene.bump_entities(&changes);
self.tabs[i].dirty = true;
self.command_line.push_output(crate::tf!(
"ATTSYNC: synchronised {synced} insert(s) of \"{block}\" against {} attribute definition(s).",
attdefs.len()
).as_ref());
}
// ADCENTER / CONTENTBROWSER — report the drawing's named content
@@ -792,17 +683,6 @@ impl OpenCADStudio {
self.open_blocks_palette(Some(tab));
}
// ATTMAN / BATTMAN — the Block Attribute Manager. Rather than a
// command-line listing, both route to the attribute editor
// (ATTEDIT): edit the selected block's attributes, or pick a block.
cmd if cmd == "ATTMAN"
|| cmd == "BATTMAN"
|| cmd.starts_with("ATTMAN ")
|| cmd.starts_with("BATTMAN ") =>
{
self.open_attedit_dialog();
}
"PDFIMPORT" => {
use crate::command::CadCommand;
let command = crate::modules::insert::pdf_import::PdfImportCommand::new();
@@ -937,16 +817,16 @@ impl OpenCADStudio {
.collect();
let existing_blocks = self.tabs[i].scene.custom_block_names();
let default_unit = self.tabs[i].scene.document.header.insertion_units;
let default_folder = self.tabs[i]
// "new block" in the drawing's folder, or in Documents for an
// unsaved drawing; the file type is added when it is written.
let folder = self.tabs[i]
.current_path
.as_ref()
.and_then(|p| p.parent())
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
let default_path = if default_folder.is_empty() {
"new_block.dwg".to_string()
} else {
format!("{}/new_block.dwg", default_folder.replace('\\', "/"))
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
.or_else(crate::sys::documents_dir);
let default_path = match folder {
Some(folder) => folder.join("new block").to_string_lossy().to_string(),
None => "new block".to_string(),
};
self.wblock = Some(crate::ui::window::wblock::WblockState::new(
existing_blocks,
+6
View File
@@ -5,6 +5,7 @@ use iced::Task;
use std::path::PathBuf;
mod attdef;
mod block_cmdline;
mod blocks;
mod blockvars;
mod bvisibility;
@@ -333,6 +334,9 @@ impl OpenCADStudio {
if let Some(t) = self.dispatch_bvisibility(cmd, i) {
return Some(t);
}
if let Some(t) = self.dispatch_block_cmdline(cmd, i) {
return Some(t);
}
if let Some(t) = self.dispatch_blocks(cmd, i) {
return Some(t);
}
@@ -579,6 +583,8 @@ inventory::submit!(crate::command::CommandRegistration {
"INSNAME",
"ATTMAN",
"BATTMAN",
"-BEDIT",
"INSBASE",
// Drawing-content overview.
"ADCENTER",
"CONTENTBROWSER",
+20
View File
@@ -421,6 +421,20 @@ pub struct UserSettings {
pub image_frame: i16,
/// The Attribute Definition dialog's "Specify on-screen" choice.
pub attdef_on_screen: bool,
/// Block Definition dialog: base point "Specify On-screen".
pub block_base_on_screen: bool,
/// Block Definition dialog: objects "Specify On-screen".
pub block_objects_on_screen: bool,
/// Block Definition dialog / -BLOCK object conversion (0 Retain,
/// 1 Convert, 2 Delete).
pub block_object_mode: u8,
/// Block Definition dialog: "Allow exploding".
pub block_allow_exploding: bool,
/// Block Definition dialog: open the new block in the block editor.
pub block_open_in_editor: bool,
/// Write Block dialog / -WBLOCK object conversion (0 Retain, 1 Convert,
/// 2 Delete).
pub wblock_object_mode: u8,
/// FIELDDISPLAY: fields show on a gray background.
pub field_display: bool,
/// XDWGFADECTL: how much referenced drawings fade (-90..=90; negative is off).
@@ -892,6 +906,12 @@ impl Default for UserSettings {
grip_object_limit: DEFAULT_GRIP_OBJECT_LIMIT,
image_frame: 1,
attdef_on_screen: true,
block_base_on_screen: false,
block_objects_on_screen: false,
block_object_mode: 1,
block_allow_exploding: true,
block_open_in_editor: false,
wblock_object_mode: 0,
field_display: true,
xref_fade: 50,
file_dia: true,
+1 -8
View File
@@ -629,14 +629,7 @@ impl OpenCADStudio {
self.block_palette.source_tab_id = Some(self.tabs[i].id);
self.block_palette.source_block_epoch = self.tabs[i].scene.block_epoch;
let doc = &self.tabs[i].scene.document;
let annotative = |name: &str| {
doc.block_records.get(name).is_some_and(|br| {
matches!(
doc.get_entity(br.block_entity_handle),
Some(codec::EntityType::Block(b)) if b.common.extended_data.records().iter().any(|r| r.application_name == "AcadAnnotative")
)
})
};
let annotative = |name: &str| crate::scene::annotative::block_is_annotative(doc, name);
let entries: Vec<BlockEntry> = names
.into_iter()
.map(|name| BlockEntry {
+116
View File
@@ -768,6 +768,8 @@ impl OpenCADStudio {
/// Snapshot the persisted UI preferences from live state.
pub(in crate::app) fn current_settings(&self) -> crate::app::settings::UserSettings {
// One copy: the guard must not be held across the literal below.
let block_defaults = *crate::modules::insert::block_cmdline::defaults();
crate::app::settings::UserSettings {
spacemouse: self.spacemouse_preferences,
dyn_input: self.dyn_input,
@@ -795,6 +797,14 @@ impl OpenCADStudio {
grip_object_limit: self.grip_object_limit,
image_frame: crate::scene::frame::profile_image_mode(),
attdef_on_screen: crate::modules::draw::draw::attdef::session().on_screen,
block_base_on_screen: block_defaults.base_on_screen,
block_objects_on_screen: block_defaults.objects_on_screen,
block_object_mode: crate::modules::insert::block_cmdline::mode_code(block_defaults.mode),
block_allow_exploding: block_defaults.allow_exploding,
block_open_in_editor: block_defaults.open_in_editor,
wblock_object_mode: crate::modules::insert::block_cmdline::mode_code(
block_defaults.wblock_mode,
),
field_display: crate::entities::field::display(),
xref_fade: crate::scene::cache::block_cache::xref_fade_ctl(),
file_dia: crate::sys::file_dia(),
@@ -920,6 +930,16 @@ impl OpenCADStudio {
self.grip_object_limit = s.grip_object_limit.clamp(0, 32767);
crate::scene::frame::set_profile_image_mode(s.image_frame);
crate::modules::draw::draw::attdef::session().on_screen = s.attdef_on_screen;
{
use crate::modules::insert::block_cmdline as bc;
let mut remembered = bc::defaults();
remembered.base_on_screen = s.block_base_on_screen;
remembered.objects_on_screen = s.block_objects_on_screen;
remembered.mode = bc::mode_from_code(s.block_object_mode);
remembered.allow_exploding = s.block_allow_exploding;
remembered.open_in_editor = s.block_open_in_editor;
remembered.wblock_mode = bc::mode_from_code(s.wblock_object_mode);
}
crate::entities::field::set_display(s.field_display);
crate::scene::cache::block_cache::set_xref_fade_ctl(s.xref_fade);
self.ribbon.xref_fade = crate::scene::cache::block_cache::xref_fade_ctl();
@@ -2336,6 +2356,102 @@ impl OpenCADStudio {
])
}
/// Write `source` to `path` (WBLOCK and -WBLOCK): an existing block, the
/// whole drawing, or objects, which then are kept, converted to a block
/// named after the file, or deleted.
pub(in crate::app) fn wblock_write(
&mut self,
i: usize,
source: crate::modules::insert::block_cmdline::WblockSource,
path: std::path::PathBuf,
unit: i16,
) -> Task<Message> {
let mut path = path;
if path.extension().is_none() {
path.set_extension("dwg");
}
let document = self.tabs[i].scene.document_for_save();
if let crate::modules::insert::block_cmdline::WblockSource::Objects {
handles,
base,
mode,
} = &source
{
match mode {
crate::ui::window::block_definition::BlockObjectMode::Retain => {}
crate::ui::window::block_definition::BlockObjectMode::Delete => {
self.push_undo_snapshot(i, "WBLOCK");
self.tabs[i].scene.erase_entities(handles);
self.tabs[i].dirty = true;
self.tabs[i].scene.bump_geometry();
self.refresh_properties();
}
crate::ui::window::block_definition::BlockObjectMode::Convert => {
let name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("WBLOCK")
.to_string();
self.push_undo_snapshot(i, "WBLOCK");
let ucs = self.tabs[i].ucs_xform();
let options = crate::scene::CreateBlockOptions {
name,
handles: handles.clone(),
base_point: *base,
world_to_block: ucs.to_ucs_transform_at(*base),
block_to_world: ucs.to_wcs_transform_at(*base),
mode: crate::ui::window::block_definition::BlockObjectMode::Convert,
annotative: false,
match_orientation: false,
scale_uniformly: false,
allow_exploding: true,
unit,
description: String::new(),
hyperlink_url: String::new(),
hyperlink_desc: String::new(),
redefine: true,
};
let _ = self.tabs[i].scene.create_block_with_options(options);
self.tabs[i].dirty = true;
self.tabs[i].scene.bump_geometry();
self.refresh_properties();
}
}
}
let display = match &source {
crate::modules::insert::block_cmdline::WblockSource::Block(name) => name.clone(),
_ => "*".to_string(),
};
let worker_path = path.clone();
background_task(
crate::t!("WBLOCK save"),
move || {
let out_doc = match source {
crate::modules::insert::block_cmdline::WblockSource::Block(name) => {
let mut d =
crate::modules::insert::wblock::extract_block_to_doc(&document, &name)
.map_err(|e| e.to_string())?;
d.header.insertion_units = unit;
d
}
crate::modules::insert::block_cmdline::WblockSource::Drawing => {
let mut d = document.clone();
d.header.insertion_units = unit;
d
}
crate::modules::insert::block_cmdline::WblockSource::Objects { handles, base, .. } => {
crate::modules::insert::wblock::extract_entities_to_doc_with_base(
&document, &handles, base, unit,
)
.map_err(|e| e.to_string())?
}
};
crate::io::save(&out_doc, &worker_path).map_err(|e| e.to_string())
},
move |result| Message::WblockWriteFinished(display, path, result),
)
}
pub(super) fn on_wblock_save_result_some(
&mut self,
block_name: String,
+37 -100
View File
@@ -1837,10 +1837,15 @@ impl OpenCADStudio {
Message::WblockSave(block_name) => {
let name = block_name.clone();
let title = if block_name == "-" {
crate::t!("Create Drawing File").into_owned()
} else {
crate::t!("Save Block As").into_owned()
};
Task::perform(
async move {
let path = crate::sys::file_dialog()
.set_title(crate::t!("Save Block As").as_ref())
.set_title(&title)
.set_file_name("block.dwg")
.add_filter(crate::t!("DWG Files").as_ref(), &["dwg"])
.save_file()
@@ -1852,6 +1857,10 @@ impl OpenCADStudio {
)
}
// -WBLOCK with FILEDIA 1: the file came from the dialog, the
// command line asks for the rest.
Message::WblockSaveResult(block_name, Some(path)) if block_name == "-" => self
.dispatch_command(&format!("_-WBLOCKFILE {}", path.display())),
Message::WblockSaveResult(block_name, Some(path)) => {
self.on_wblock_save_result_some(block_name, path)
}
@@ -1859,14 +1868,12 @@ impl OpenCADStudio {
Message::WblockSaveResult(_, None) => Task::none(),
Message::WblockWriteFinished(block_name, path, result) => {
match result {
Ok(()) => self.command_line.push_output(
crate::tf!("WBLOCK Saved \"{block_name}\" → \"{}\"", path.display())
.as_ref(),
),
Err(error) => self
.command_line
.push_error(crate::tf!("WBLOCK save failed: {error}").as_ref()),
// A written file is not reported (as in the reference); a
// failed write is.
let _ = (&block_name, &path);
if let Err(error) = result {
self.command_line
.push_error(crate::tf!("WBLOCK save failed: {error}").as_ref());
}
Task::none()
}
@@ -5820,13 +5827,14 @@ impl OpenCADStudio {
};
let name = state.name.trim().to_string();
if name.is_empty() {
state.error_message =
Some(crate::t!("Block name cannot be empty.").into_owned());
state.error_message = Some(
crate::t!("The name must be at least one character long.").into_owned(),
);
return Task::none();
}
if let Some(_ch) = state.invalid_name_char() {
state.error_message = Some(
crate::t!("Block name cannot contain: \\ / : * ? \" < > | = `")
crate::t!("The name contains unsupported characters. The following characters are not supported: \\ < > / ? \" : ; * | , = `")
.into_owned(),
);
return Task::none();
@@ -6064,96 +6072,25 @@ impl OpenCADStudio {
let state = self.wblock.take().unwrap();
self.active_modal = None;
let document = self.tabs[i].scene.document_for_save();
let source_mode = state.source_mode;
let block_name = state.block_name.trim().to_string();
let handles = state.selected_handles.clone();
let base_point = state.parse_base_point();
let unit = state.unit;
let object_mode = state.object_mode;
if source_mode == WblockSourceMode::Objects {
match object_mode {
WblockObjectMode::Retain => {}
WblockObjectMode::Delete => {
self.push_undo_snapshot(i, "WBLOCK");
self.tabs[i].scene.erase_entities(&handles);
self.tabs[i].dirty = true;
self.tabs[i].scene.bump_geometry();
self.refresh_properties();
}
WblockObjectMode::Convert => {
let block_name_for_conv = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("WBLOCK")
.to_string();
self.push_undo_snapshot(i, "WBLOCK");
let ucs = self.tabs[i].ucs_xform();
let world_to_block = ucs.to_ucs_transform_at(base_point);
let block_to_world = ucs.to_wcs_transform_at(base_point);
let options = crate::scene::CreateBlockOptions {
name: block_name_for_conv,
handles: handles.clone(),
base_point,
world_to_block,
block_to_world,
mode: crate::ui::window::block_definition::BlockObjectMode::Convert,
annotative: false,
match_orientation: false,
scale_uniformly: false,
allow_exploding: true,
unit,
description: String::new(),
hyperlink_url: String::new(),
hyperlink_desc: String::new(),
redefine: true,
};
let _ = self.tabs[i].scene.create_block_with_options(options);
self.tabs[i].dirty = true;
self.tabs[i].scene.bump_geometry();
self.refresh_properties();
}
}
}
let worker_path = path.clone();
let display_name = match source_mode {
WblockSourceMode::Block => block_name.clone(),
WblockSourceMode::EntireDrawing => "*".to_string(),
WblockSourceMode::Objects => "*".to_string(),
use crate::modules::insert::block_cmdline::WblockSource;
use crate::ui::window::block_definition::BlockObjectMode;
let mode = match state.object_mode {
WblockObjectMode::Retain => BlockObjectMode::Retain,
WblockObjectMode::Convert => BlockObjectMode::Convert,
WblockObjectMode::Delete => BlockObjectMode::Delete,
};
let worker_display = display_name.clone();
file::background_task(
crate::t!("WBLOCK save"),
move || {
let out_doc = match source_mode {
WblockSourceMode::Block => {
let mut d = crate::modules::insert::wblock::extract_block_to_doc(
&document,
&block_name,
)
.map_err(|e| e.to_string())?;
d.header.insertion_units = unit;
d
}
WblockSourceMode::EntireDrawing => {
let mut d = document.clone();
d.header.insertion_units = unit;
d
}
WblockSourceMode::Objects => {
crate::modules::insert::wblock::extract_entities_to_doc_with_base(
&document, &handles, base_point, unit,
)
.map_err(|e| e.to_string())?
}
};
crate::io::save(&out_doc, &worker_path).map_err(|e| e.to_string())
crate::modules::insert::block_cmdline::defaults().wblock_mode = mode;
self.persist_settings_if_changed();
let source = match state.source_mode {
WblockSourceMode::Block => WblockSource::Block(state.block_name.trim().to_string()),
WblockSourceMode::EntireDrawing => WblockSource::Drawing,
WblockSourceMode::Objects => WblockSource::Objects {
handles: state.selected_handles.clone(),
base: state.parse_base_point(),
mode,
},
move |result| Message::WblockWriteFinished(worker_display, path, result),
)
};
self.wblock_write(i, source, path, state.unit)
}
Message::ToleranceDialogField(field) => {
if let Some(state) = self.geometric_tolerance.as_mut() {