forked from 1b-agent/opencadstudio-upstream
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 <[email protected]>
This commit is contained in:
commit
838e397a6c
23 files changed
+1860
-435
No files matched your search
@@ -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
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user