forked from 1b-agent/opencadstudio-upstream
Block Definition command-line forms, block record data and dialog defaults
-BLOCK, -WBLOCK, -BEDIT, ATTSYNC, BASE and INSBASE follow the reference prompts and messages; empty blocks; WBLOCK keeps object coordinates and stores INSBASE; annotative and hyperlink data on the block record; block preview images; remembered dialog choices; block aliases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0a29be07f7
commit
d583d9d421
26 files changed
+1878
-435
No files matched your search
@@ -13,6 +13,7 @@ PL, *PLINE
|
||||
A, *ARC
|
||||
C, *CIRCLE
|
||||
B, *BLOCK
|
||||
-B, *-BLOCK
|
||||
I, *INSERT
|
||||
DO, *DONUT
|
||||
EL, *ELLIPSE
|
||||
@@ -58,6 +59,8 @@ AR, *ARRAY
|
||||
MA, *MATCHPROP
|
||||
VV, *QLAYER
|
||||
ATE, *ATTEDIT
|
||||
-ATE, *-ATTEDIT
|
||||
ATTE, *-ATTEDIT
|
||||
|
||||
; ── Clipboard ─────────────────────────────────────────────────────────────
|
||||
CC, *COPYCLIP
|
||||
@@ -68,6 +71,9 @@ PC, *PASTECLIP
|
||||
ATT, *ATTDEF
|
||||
-ATT, *-ATTDEF
|
||||
BE, *BEDIT
|
||||
BC, *BCLOSE
|
||||
BS, *BSAVE
|
||||
BVS, *BVSTATE
|
||||
IAT, *IMAGEATTACH
|
||||
XA, *XATTACH
|
||||
XR, *XREF
|
||||
@@ -77,6 +83,7 @@ ICL, *IMAGECLIP
|
||||
IAD, *IMAGEADJUST
|
||||
ADC, *ADCENTER
|
||||
W, *WBLOCK
|
||||
-W, *-WBLOCK
|
||||
|
||||
; ── Layers / groups ───────────────────────────────────────────────────────
|
||||
LA, *LAYERS
|
||||
|
||||
@@ -2397,7 +2397,7 @@ common =
|
||||
.hectometers = Hectometers
|
||||
.hyperlink-ellipsis = Hyperlink...
|
||||
.hyperlink-star = Hyperlink*
|
||||
.invalid-character-in-block-name = Invalid character '__ocs_fmt_0__' in block name (\ / : * ? " < > | = ` are not allowed)
|
||||
.invalid-character-in-block-name = Invalid character '__ocs_fmt_0__' in block name (\ < > / ? " : ; * | , = ` are not allowed)
|
||||
.light-years = Light Years
|
||||
.match-block-orientation = Match block orientation
|
||||
.microinches = Microinches
|
||||
@@ -6540,3 +6540,9 @@ keytips =
|
||||
.hint-tabs = Type a tab letter · Esc closes
|
||||
.hint-tab = { $tab } tab · Esc goes back one level
|
||||
.hint-items = Type an item letter · Esc goes back one level
|
||||
|
||||
block-def =
|
||||
.name-too-short = The name must be at least one character long.
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
@@ -2381,7 +2381,7 @@ common =
|
||||
.hectometers = Hektometre
|
||||
.hyperlink-ellipsis = Köprü...
|
||||
.hyperlink-star = Köprü*
|
||||
.invalid-character-in-block-name = Blok adında geçersiz karakter '__ocs_fmt_0__' (\ / : * ? " < > | = ` kullanılamaz)
|
||||
.invalid-character-in-block-name = Blok adında geçersiz karakter '__ocs_fmt_0__' (\ < > / ? " : ; * | , = ` kullanılamaz)
|
||||
.light-years = Işık Yılı
|
||||
.match-block-orientation = Blok yönünü eşle
|
||||
.microinches = Mikroinç
|
||||
@@ -6462,3 +6462,9 @@ keytips =
|
||||
.hint-tabs = Sekme harfini yazın · Esc kapatır
|
||||
.hint-tab = { $tab } sekmesi · Esc bir düzey geri
|
||||
.hint-items = Öğe harfini yazın · Esc bir düzey geri
|
||||
|
||||
block-def =
|
||||
.name-too-short = Ad en az bir karakter uzunluğunda olmalıdır.
|
||||
.name-unsupported-chars = Ad desteklenmeyen karakterler içeriyor. Şu karakterler desteklenmez: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = Blok tanımı değişti. Yeniden tanımlamak istiyor musunuz?
|
||||
.create-drawing-file = Çizim Dosyası Oluştur
|
||||
+12
-2
@@ -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"),
|
||||
],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,6 +491,9 @@ impl OpenCADStudio {
|
||||
self.tabs[i].history = history.main;
|
||||
self.tabs[i].scene.object_isolation = history.return_isolation;
|
||||
}
|
||||
if discard && session.created_for_edit {
|
||||
crate::scene::block_service::erase(&mut self.tabs[i].scene, &session.block_name);
|
||||
}
|
||||
self.tabs[i].active_block_edit = None;
|
||||
self.restore_after_block_edit_close(
|
||||
i,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
//! 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)
|
||||
}
|
||||
|
||||
/// Reconcile every editable reference of `block` with the block's
|
||||
/// attribute definitions: attributes whose tag is gone are dropped, new
|
||||
/// ones are added with their default value, and the values of the rest
|
||||
/// are kept.
|
||||
pub(in crate::app) fn attsync_block(&mut self, i: usize, block: &str) {
|
||||
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<_> = 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_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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
changes.push((ins.common.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,
|
||||
|
||||
@@ -512,6 +512,36 @@ impl OpenCADStudio {
|
||||
}
|
||||
};
|
||||
|
||||
// A name that is not a block yet opens an empty definition,
|
||||
// dropped again when the editor closes without saving.
|
||||
let mut created_for_edit = false;
|
||||
if self.tabs[i].scene.document.block_records.get(&block_name).is_none()
|
||||
&& cmd.starts_with("BEDIT_OPEN:")
|
||||
&& crate::ui::window::block_definition::invalid_block_name_char(&block_name)
|
||||
.is_none()
|
||||
&& !block_name.trim().is_empty()
|
||||
&& !block_name.starts_with('*')
|
||||
{
|
||||
let options = crate::scene::CreateBlockOptions {
|
||||
name: block_name.clone(),
|
||||
handles: Vec::new(),
|
||||
base_point: glam::DVec3::ZERO,
|
||||
world_to_block: codec::types::Transform::identity(),
|
||||
block_to_world: codec::types::Transform::identity(),
|
||||
mode: crate::ui::window::block_definition::BlockObjectMode::Retain,
|
||||
annotative: false,
|
||||
match_orientation: false,
|
||||
scale_uniformly: false,
|
||||
allow_exploding: true,
|
||||
unit: self.tabs[i].scene.document.header.insertion_units,
|
||||
description: String::new(),
|
||||
hyperlink_url: String::new(),
|
||||
hyperlink_desc: String::new(),
|
||||
redefine: false,
|
||||
};
|
||||
created_for_edit =
|
||||
self.tabs[i].scene.create_block_with_options(options).is_ok();
|
||||
}
|
||||
// Resolve the block record; reject external references (xrefs).
|
||||
let (br_handle, is_xref) =
|
||||
match self.tabs[i].scene.document.block_records.get(&block_name) {
|
||||
@@ -575,6 +605,7 @@ impl OpenCADStudio {
|
||||
return_camera,
|
||||
editor_camera: current_camera,
|
||||
editor_ucs: None,
|
||||
created_for_edit,
|
||||
});
|
||||
self.tabs[i].active_block_edit = Some(self.tabs[i].block_edits.len() - 1);
|
||||
// Every BEDIT tab owns an isolated local UCS. Never inherit or
|
||||
|
||||
@@ -5,6 +5,7 @@ use iced::Task;
|
||||
use std::path::PathBuf;
|
||||
|
||||
mod attdef;
|
||||
mod block_cmdline;
|
||||
mod blocks;
|
||||
mod blockvars;
|
||||
mod bvisibility;
|
||||
@@ -306,6 +307,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);
|
||||
}
|
||||
@@ -552,6 +556,8 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"INSNAME",
|
||||
"ATTMAN",
|
||||
"BATTMAN",
|
||||
"-BEDIT",
|
||||
"INSBASE",
|
||||
// Drawing-content overview.
|
||||
"ADCENTER",
|
||||
"CONTENTBROWSER",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
@@ -2327,6 +2347,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
@@ -1774,10 +1774,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()
|
||||
@@ -1789,6 +1794,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)
|
||||
}
|
||||
@@ -1796,14 +1805,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()
|
||||
}
|
||||
@@ -5745,13 +5752,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();
|
||||
@@ -5989,96 +5997,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() {
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
|
||||
pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static str)> {
|
||||
match source {
|
||||
"The name must be at least one character long." => Some(("block-def", "name-too-short")),
|
||||
"The name contains unsupported characters. The following characters are not supported: \\ < > / ? \" : ; * | , = `" => Some(("block-def", "name-unsupported-chars")),
|
||||
"The block definition has changed. Do you want to redefine it ?" => Some(("block-def", "definition-changed-redefine")),
|
||||
"Create Drawing File" => Some(("block-def", "create-drawing-file")),
|
||||
"Add to Working Set" => Some(("ref-edit", "add-to-working-set")),
|
||||
"Remove from Working Set" => Some(("ref-edit", "remove-from-working-set")),
|
||||
"Select reference:" => Some(("ref-edit", "select-reference")),
|
||||
@@ -5943,7 +5947,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"Hectometers" => Some(("common", "hectometers")),
|
||||
"Hyperlink*" => Some(("common", "hyperlink-star")),
|
||||
"Hyperlink..." => Some(("common", "hyperlink-ellipsis")),
|
||||
"Invalid character '{invalid_char}' in block name (\\ / : * ? \" < > | = ` are not allowed)" => Some(("common", "invalid-character-in-block-name")),
|
||||
"Invalid character '{invalid_char}' in block name (\\ < > / ? \" : ; * | , = ` are not allowed)" => Some(("common", "invalid-character-in-block-name")),
|
||||
"Light Years" => Some(("common", "light-years")),
|
||||
"Match block orientation" => Some(("common", "match-block-orientation")),
|
||||
"Microinches" => Some(("common", "microinches")),
|
||||
|
||||
@@ -70,6 +70,9 @@ pub struct BlockEditSession {
|
||||
/// Transient UCS being manipulated in this block tab. On commit its full
|
||||
/// frame is baked into the definition and this returns to `None`.
|
||||
pub editor_ucs: Option<codec::tables::Ucs>,
|
||||
/// The definition was created empty to be edited (a new name at BEDIT /
|
||||
/// -BEDIT); closing without saving removes it again.
|
||||
pub created_for_edit: bool,
|
||||
}
|
||||
|
||||
// ── BEDIT pick command ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,27 +1,56 @@
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use glam::DVec3;
|
||||
|
||||
// ── Command implementation ────────────────────────────────────────────────
|
||||
|
||||
/// Picks one point and delegates to the inline `BASE <x> <y> <z>` handler,
|
||||
/// which writes the active space's insertion base point onto the document.
|
||||
pub struct BaseCommand;
|
||||
/// BASE (`Enter base point <x,y,z>:`) and INSBASE
|
||||
/// (`Enter new value for INSBASE <x,y,z>:`): a point sets the active space's
|
||||
/// insertion base through the inline `BASE <x> <y> <z>` handler; Enter keeps
|
||||
/// the current one.
|
||||
pub struct BaseCommand {
|
||||
current: DVec3,
|
||||
variable: bool,
|
||||
}
|
||||
|
||||
impl BaseCommand {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
pub fn new(current: DVec3) -> Self {
|
||||
Self {
|
||||
current,
|
||||
variable: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The INSBASE system variable's prompt.
|
||||
pub fn insbase(current: DVec3) -> Self {
|
||||
Self {
|
||||
current,
|
||||
variable: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn point_text(p: DVec3) -> String {
|
||||
format!("{:.4},{:.4},{:.4}", p.x, p.y, p.z)
|
||||
}
|
||||
|
||||
impl CadCommand for BaseCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"BASE"
|
||||
if self.variable {
|
||||
"INSBASE"
|
||||
} else {
|
||||
"BASE"
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
t!("BASE Specify base point:").into_owned()
|
||||
if self.variable {
|
||||
format!(
|
||||
"Enter new value for INSBASE <{}>:",
|
||||
point_text(self.current)
|
||||
)
|
||||
} else {
|
||||
format!("Enter base point <{}>:", point_text(self.current))
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
@@ -29,7 +58,7 @@ impl CadCommand for BaseCommand {
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
CmdResult::Measurement(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,10 +1,8 @@
|
||||
use crate::t;
|
||||
use codec::Handle;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
@@ -15,84 +13,6 @@ pub fn tool() -> ToolDef {
|
||||
}
|
||||
}
|
||||
|
||||
enum Step {
|
||||
Name,
|
||||
Base { name: String },
|
||||
}
|
||||
|
||||
pub struct CreateBlockCommand {
|
||||
handles: Vec<Handle>,
|
||||
step: Step,
|
||||
}
|
||||
|
||||
impl CreateBlockCommand {
|
||||
pub fn new(handles: Vec<Handle>) -> Self {
|
||||
Self {
|
||||
handles,
|
||||
step: Step::Name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for CreateBlockCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"BLOCK"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::Name => t!(
|
||||
"BLOCK Enter block name [%{count} objects selected]:",
|
||||
count = self.handles.len()
|
||||
)
|
||||
.into_owned(),
|
||||
Step::Base { name } => t!(
|
||||
"BLOCK Specify base point for \"%{name}\" [%{count} objects]:",
|
||||
name = name,
|
||||
count = self.handles.len()
|
||||
)
|
||||
.into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
match &self.step {
|
||||
Step::Name => CmdResult::NeedPoint,
|
||||
Step::Base { name } => CmdResult::CreateBlock {
|
||||
handles: self.handles.clone(),
|
||||
name: name.clone(),
|
||||
base: pt,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
matches!(self.step, Step::Name)
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
if !matches!(self.step, Step::Name) {
|
||||
return None;
|
||||
}
|
||||
let name = text.trim();
|
||||
if name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
self.step = Step::Base {
|
||||
name: name.to_string(),
|
||||
};
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
|
||||
fn on_preview_wires(&mut self, _pt: DVec3) -> Vec<WireModel> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
/// One-shot point picker for the Block Definition dialog's "Pick point" button.
|
||||
pub struct BlockPickBasePointCommand;
|
||||
|
||||
@@ -102,7 +22,7 @@ impl CadCommand for BlockPickBasePointCommand {
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
crate::t!("BLOCK Specify insertion base point:").into_owned()
|
||||
"Specify insertion base point:".to_string()
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
@@ -150,9 +70,9 @@ impl CadCommand for BlockOnScreenCommand {
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.need_base {
|
||||
crate::t!("BLOCK Specify insertion base point:").into_owned()
|
||||
"Specify insertion base point:".to_string()
|
||||
} else {
|
||||
crate::t!("BLOCK Select objects:").into_owned()
|
||||
"Select objects:".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,7 +370,8 @@ mod tests {
|
||||
hyperlink_desc: "".to_string(),
|
||||
redefine: false,
|
||||
};
|
||||
assert!(scene.create_block_with_options(opt_no_handles).is_err());
|
||||
// No objects define an empty block.
|
||||
assert!(scene.create_block_with_options(opt_no_handles).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -4,6 +4,7 @@ mod attdef;
|
||||
pub(crate) mod attedit;
|
||||
mod attman;
|
||||
pub mod base_point;
|
||||
pub(crate) mod block_cmdline;
|
||||
mod content_browser;
|
||||
pub(crate) mod create_block;
|
||||
mod design_center;
|
||||
|
||||
@@ -186,8 +186,11 @@ pub fn extract_entities_to_doc_with_base(
|
||||
if handles.is_empty() {
|
||||
return Err(t!("No entities selected for WBLOCK.").into_owned());
|
||||
}
|
||||
let shift = crate::command::EntityTransform::Translate(-base_point);
|
||||
copy_with_dependencies(src, handles, &mut out, Some(&shift));
|
||||
// The objects keep their coordinates; the base point becomes the new
|
||||
// drawing's insertion base (INSBASE).
|
||||
copy_with_dependencies(src, handles, &mut out, None);
|
||||
out.header.model_space_insertion_base =
|
||||
codec::types::Vector3::new(base_point.x, base_point.y, base_point.z);
|
||||
if out.entities().count() == 0 {
|
||||
return Err(t!("None of the selected entities could be exported.").into_owned());
|
||||
}
|
||||
@@ -218,12 +221,16 @@ mod tests {
|
||||
|
||||
assert_eq!(out.header.insertion_units, 4);
|
||||
assert_eq!(out.entities().count(), 1);
|
||||
assert_eq!(
|
||||
out.header.model_space_insertion_base,
|
||||
Vector3::new(10.0, 20.0, 0.0)
|
||||
);
|
||||
let e = out.entities().next().unwrap();
|
||||
if let EntityType::Line(l) = e {
|
||||
assert!((l.start.x - 0.0).abs() < 1e-6);
|
||||
assert!((l.start.y - 0.0).abs() < 1e-6);
|
||||
assert!((l.end.x - 20.0).abs() < 1e-6);
|
||||
assert!((l.end.y - 20.0).abs() < 1e-6);
|
||||
assert!((l.start.x - 10.0).abs() < 1e-6);
|
||||
assert!((l.start.y - 20.0).abs() < 1e-6);
|
||||
assert!((l.end.x - 30.0).abs() < 1e-6);
|
||||
assert!((l.end.y - 40.0).abs() < 1e-6);
|
||||
} else {
|
||||
panic!("Expected Line entity");
|
||||
}
|
||||
|
||||
@@ -1975,6 +1975,39 @@ pub fn update_entity_from_annotation_style(
|
||||
true
|
||||
}
|
||||
|
||||
/// Whether the block definition `name` is annotative: its block record
|
||||
/// carries `AcadAnnotative` (`AnnotativeData { 1 1 }`), or, in drawings an
|
||||
/// earlier build wrote, its BLOCK entity does.
|
||||
pub fn block_is_annotative(doc: &CadDocument, name: &str) -> bool {
|
||||
let Some(br) = doc.block_records.get(name) else {
|
||||
return false;
|
||||
};
|
||||
let flag = |record: &codec::xdata::ExtendedDataRecord| {
|
||||
record
|
||||
.values
|
||||
.iter()
|
||||
.filter_map(|value| match value {
|
||||
codec::xdata::XDataValue::Integer16(value) => Some(*value),
|
||||
_ => None,
|
||||
})
|
||||
.last()
|
||||
.is_some_and(|value| value != 0)
|
||||
};
|
||||
if let Some(record) = doc
|
||||
.object_xdata(br.handle)
|
||||
.and_then(|xdata| xdata.get_record("AcadAnnotative"))
|
||||
{
|
||||
return flag(record);
|
||||
}
|
||||
doc.get_entity(br.block_entity_handle).is_some_and(|block| {
|
||||
block
|
||||
.common()
|
||||
.extended_data
|
||||
.get_record("AcadAnnotative")
|
||||
.is_some_and(flag)
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether an entity participates in annotation scaling.
|
||||
pub fn is_annotative(doc: &CadDocument, entity: &EntityType) -> bool {
|
||||
// Per-object annotation context (works regardless of style).
|
||||
|
||||
+161
-46
@@ -969,7 +969,7 @@ impl Scene {
|
||||
.get(name)
|
||||
.map(|br| (br.handle, br.block_entity_handle));
|
||||
|
||||
let (br_handle, _block_handle) = if let Some((existing_br_h, existing_blk_h)) = existing {
|
||||
let (br_handle, block_handle) = if let Some((existing_br_h, existing_blk_h)) = existing {
|
||||
if !options.redefine {
|
||||
return Err(format!("Block \"{name}\" already exists."));
|
||||
}
|
||||
@@ -991,7 +991,7 @@ impl Scene {
|
||||
br.units = options.unit;
|
||||
br.description = options.description.clone();
|
||||
br.explodable = options.allow_exploding;
|
||||
br.scale_uniformly = options.scale_uniformly;
|
||||
br.scale_uniformly = options.scale_uniformly || options.annotative;
|
||||
}
|
||||
// Update existing Block entity fields
|
||||
if let Some(EntityType::Block(b)) = self.document.get_entity_mut(existing_blk_h) {
|
||||
@@ -1002,26 +1002,6 @@ impl Scene {
|
||||
// base point here would subtract the picked base twice.
|
||||
b.base_point = codec::types::Vector3::ZERO;
|
||||
}
|
||||
if options.annotative {
|
||||
let annotative_vals = vec![codec::xdata::XDataValue::Integer16(1)];
|
||||
view::dispatch::set_entity_xdata(
|
||||
&mut self.document,
|
||||
existing_blk_h,
|
||||
"AcadAnnotative",
|
||||
Some(annotative_vals),
|
||||
);
|
||||
}
|
||||
if !options.hyperlink_url.is_empty() {
|
||||
let link_vals = vec![codec::xdata::XDataValue::String(
|
||||
options.hyperlink_url.clone(),
|
||||
)];
|
||||
view::dispatch::set_entity_xdata(
|
||||
&mut self.document,
|
||||
existing_blk_h,
|
||||
"PE_URL",
|
||||
Some(link_vals),
|
||||
);
|
||||
}
|
||||
(existing_br_h, existing_blk_h)
|
||||
} else {
|
||||
let next = self.document.next_handle();
|
||||
@@ -1036,7 +1016,7 @@ impl Scene {
|
||||
block_record.units = options.unit;
|
||||
block_record.description = options.description.clone();
|
||||
block_record.explodable = options.allow_exploding;
|
||||
block_record.scale_uniformly = options.scale_uniformly;
|
||||
block_record.scale_uniformly = options.scale_uniformly || options.annotative;
|
||||
self.document
|
||||
.block_records
|
||||
.add(block_record)
|
||||
@@ -1056,36 +1036,19 @@ impl Scene {
|
||||
self.document
|
||||
.add_entity(EntityType::BlockEnd(block_end))
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
if options.annotative {
|
||||
let annotative_vals = vec![codec::xdata::XDataValue::Integer16(1)];
|
||||
view::dispatch::set_entity_xdata(
|
||||
&mut self.document,
|
||||
block_handle,
|
||||
"AcadAnnotative",
|
||||
Some(annotative_vals),
|
||||
);
|
||||
}
|
||||
if !options.hyperlink_url.is_empty() {
|
||||
let link_vals = vec![codec::xdata::XDataValue::String(
|
||||
options.hyperlink_url.clone(),
|
||||
)];
|
||||
view::dispatch::set_entity_xdata(
|
||||
&mut self.document,
|
||||
block_handle,
|
||||
"PE_URL",
|
||||
Some(link_vals),
|
||||
);
|
||||
}
|
||||
(br_handle, block_handle)
|
||||
};
|
||||
self.write_block_definition_xdata(br_handle, block_handle, &options);
|
||||
|
||||
let source_entities: Vec<_> = options
|
||||
.handles
|
||||
.iter()
|
||||
.filter_map(|&h| self.document.get_entity(h).cloned().map(|e| (h, e)))
|
||||
.collect();
|
||||
if source_entities.is_empty() {
|
||||
// No objects at all defines an empty block (the reference's
|
||||
// "Continue" / an empty selection at -BLOCK); objects that all vanished
|
||||
// are an error.
|
||||
if source_entities.is_empty() && !options.handles.is_empty() {
|
||||
return Err("No valid entities selected for block creation.".into());
|
||||
}
|
||||
|
||||
@@ -1104,17 +1067,169 @@ impl Scene {
|
||||
}
|
||||
|
||||
let mut insert_handle = Handle::NULL;
|
||||
if options.mode == crate::ui::window::block_definition::BlockObjectMode::Convert {
|
||||
// An empty definition has nothing to convert into a reference.
|
||||
if options.mode == crate::ui::window::block_definition::BlockObjectMode::Convert
|
||||
&& !options.handles.is_empty()
|
||||
{
|
||||
let mut insert = DxfInsert::new(name, codec::types::Vector3::ZERO);
|
||||
codec::Entity::apply_transform(&mut insert, &options.block_to_world);
|
||||
insert_handle = self.add_entity(EntityType::Insert(insert));
|
||||
}
|
||||
|
||||
// The definition's thumbnail (BLOCK_RECORD 310), as the format keeps it.
|
||||
let thumbnail = self.block_thumbnail(name);
|
||||
if let Some(br) = self.document.block_records.get_mut(name) {
|
||||
br.preview_data = thumbnail;
|
||||
}
|
||||
|
||||
// Advance the block epoch so all consumers and viewport renderers re-tessellate
|
||||
self.bump_geometry();
|
||||
Ok(insert_handle)
|
||||
}
|
||||
|
||||
/// A 40 × 40, 32-bit bottom-up DIB of the block's geometry: white strokes
|
||||
/// on the dark preview background, fitted with a one-pixel margin. Empty
|
||||
/// for a block without drawable geometry.
|
||||
pub fn block_thumbnail(&self, name: &str) -> Vec<u8> {
|
||||
const SIZE: usize = 40;
|
||||
let wires = self.block_preview_wires(name);
|
||||
let mut lo = [f64::INFINITY; 2];
|
||||
let mut hi = [f64::NEG_INFINITY; 2];
|
||||
for wire in &wires {
|
||||
for p in &wire.points {
|
||||
if p[0].is_finite() && p[1].is_finite() {
|
||||
lo = [lo[0].min(p[0] as f64), lo[1].min(p[1] as f64)];
|
||||
hi = [hi[0].max(p[0] as f64), hi[1].max(p[1] as f64)];
|
||||
}
|
||||
}
|
||||
}
|
||||
if !lo[0].is_finite() {
|
||||
return Vec::new();
|
||||
}
|
||||
let span = (hi[0] - lo[0]).max(hi[1] - lo[1]).max(1e-9);
|
||||
let scale = (SIZE as f64 - 3.0) / span;
|
||||
let offset = [
|
||||
(SIZE as f64 - (hi[0] - lo[0]) * scale) / 2.0,
|
||||
(SIZE as f64 - (hi[1] - lo[1]) * scale) / 2.0,
|
||||
];
|
||||
let to_px = |p: &[f32; 3]| {
|
||||
(
|
||||
((p[0] as f64 - lo[0]) * scale + offset[0]).floor() as i64,
|
||||
((p[1] as f64 - lo[1]) * scale + offset[1]).floor() as i64,
|
||||
)
|
||||
};
|
||||
// Bottom-up rows of BGRA pixels.
|
||||
let mut pixels = vec![[0x30u8, 0x28, 0x21, 0xFF]; SIZE * SIZE];
|
||||
let mut plot = |x: i64, y: i64| {
|
||||
if (0..SIZE as i64).contains(&x) && (0..SIZE as i64).contains(&y) {
|
||||
pixels[y as usize * SIZE + x as usize] = [0xFF; 4];
|
||||
}
|
||||
};
|
||||
for wire in &wires {
|
||||
let mut prev: Option<(i64, i64)> = None;
|
||||
for p in &wire.points {
|
||||
if !(p[0].is_finite() && p[1].is_finite()) {
|
||||
prev = None;
|
||||
continue;
|
||||
}
|
||||
let (x1, y1) = to_px(p);
|
||||
let (mut x0, mut y0) = prev.unwrap_or((x1, y1));
|
||||
let (dx, dy) = ((x1 - x0).abs(), -(y1 - y0).abs());
|
||||
let (sx, sy) = (if x0 < x1 { 1 } else { -1 }, if y0 < y1 { 1 } else { -1 });
|
||||
let mut err = dx + dy;
|
||||
loop {
|
||||
plot(x0, y0);
|
||||
if x0 == x1 && y0 == y1 {
|
||||
break;
|
||||
}
|
||||
let e2 = 2 * err;
|
||||
if e2 >= dy {
|
||||
err += dy;
|
||||
x0 += sx;
|
||||
}
|
||||
if e2 <= dx {
|
||||
err += dx;
|
||||
y0 += sy;
|
||||
}
|
||||
}
|
||||
prev = Some((x1, y1));
|
||||
}
|
||||
}
|
||||
let mut dib = Vec::with_capacity(40 + SIZE * SIZE * 4);
|
||||
for value in [40u32, SIZE as u32, SIZE as u32] {
|
||||
dib.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
dib.extend_from_slice(&1u16.to_le_bytes());
|
||||
dib.extend_from_slice(&32u16.to_le_bytes());
|
||||
for value in [0u32, (SIZE * SIZE * 4) as u32, 1, 1, 0, 0] {
|
||||
dib.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
for pixel in pixels {
|
||||
dib.extend_from_slice(&pixel);
|
||||
}
|
||||
dib
|
||||
}
|
||||
|
||||
/// The definition-level xdata of a block, kept on its block record the
|
||||
/// way the format stores it: `AcadAnnotative` (`AnnotativeData { 1 1 }`)
|
||||
/// for an annotative block, `AcadAnnoPO` when its orientation matches the
|
||||
/// layout, and the `PE_URL` hyperlink. Markers an older definition kept
|
||||
/// on its BLOCK entity are dropped.
|
||||
pub fn write_block_definition_xdata(
|
||||
&mut self,
|
||||
br_handle: Handle,
|
||||
block_handle: Handle,
|
||||
options: &CreateBlockOptions,
|
||||
) {
|
||||
use codec::xdata::{ExtendedDataRecord, XDataValue as V};
|
||||
for app in ["AcadAnnotative", "AcadAnnoPO", "PE_URL"] {
|
||||
view::dispatch::set_entity_xdata(&mut self.document, block_handle, app, None);
|
||||
self.document.remove_object_xdata_record(br_handle, app);
|
||||
}
|
||||
let mut records = Vec::new();
|
||||
if options.annotative {
|
||||
let mut anno = ExtendedDataRecord::new("AcadAnnotative");
|
||||
for value in [
|
||||
V::String("AnnotativeData".into()),
|
||||
V::ControlString("{".into()),
|
||||
V::Integer16(1),
|
||||
V::Integer16(1),
|
||||
V::ControlString("}".into()),
|
||||
] {
|
||||
anno.add_value(value);
|
||||
}
|
||||
records.push(anno);
|
||||
if options.match_orientation {
|
||||
let mut po = ExtendedDataRecord::new("AcadAnnoPO");
|
||||
po.add_value(V::Integer16(1));
|
||||
records.push(po);
|
||||
}
|
||||
}
|
||||
let url = options.hyperlink_url.trim();
|
||||
if !url.is_empty() {
|
||||
let mut link = ExtendedDataRecord::new("PE_URL");
|
||||
link.add_value(V::String(url.to_string()));
|
||||
link.add_value(V::ControlString("{".into()));
|
||||
let description = options.hyperlink_desc.trim();
|
||||
if !description.is_empty() {
|
||||
link.add_value(V::String(description.to_string()));
|
||||
}
|
||||
link.add_value(V::ControlString("{".into()));
|
||||
link.add_value(V::Integer32(0));
|
||||
link.add_value(V::ControlString("}".into()));
|
||||
link.add_value(V::ControlString("}".into()));
|
||||
records.push(link);
|
||||
}
|
||||
for record in records {
|
||||
if !self.document.app_ids.contains(&record.application_name) {
|
||||
let mut app = codec::tables::AppId::new(&record.application_name);
|
||||
app.handle = self.document.allocate_handle();
|
||||
let _ = self.document.app_ids.add(app);
|
||||
}
|
||||
self.document.set_object_xdata_record(br_handle, record);
|
||||
}
|
||||
}
|
||||
|
||||
/// Define a new block named `name` from `entities` (owned, not yet in the
|
||||
/// document), with `base` as its insertion origin. Unlike
|
||||
/// [`create_block_from_entities`] this does NOT place an insert — the
|
||||
|
||||
+30
@@ -1370,3 +1370,33 @@ pub mod web_diag {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The user's Documents folder (the known folder, wherever it is
|
||||
/// redirected), else the home folder.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn documents_dir() -> Option<std::path::PathBuf> {
|
||||
use windows_sys::Win32::System::Com::CoTaskMemFree;
|
||||
use windows_sys::Win32::UI::Shell::{FOLDERID_Documents, SHGetKnownFolderPath};
|
||||
let mut raw: windows_sys::core::PWSTR = std::ptr::null_mut();
|
||||
// SAFETY: SHGetKnownFolderPath writes a CoTaskMemAlloc'd, NUL-terminated
|
||||
// wide string to `raw`, freed below.
|
||||
let hr = unsafe { SHGetKnownFolderPath(&FOLDERID_Documents, 0, std::ptr::null_mut(), &mut raw) };
|
||||
let path = if hr == 0 && !raw.is_null() {
|
||||
// SAFETY: `raw` is a valid NUL-terminated wide string from the call above.
|
||||
let len = unsafe { (0..).take_while(|&k| *raw.add(k) != 0).count() };
|
||||
let wide = unsafe { std::slice::from_raw_parts(raw, len) };
|
||||
Some(std::path::PathBuf::from(String::from_utf16_lossy(wide)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// SAFETY: freeing the buffer SHGetKnownFolderPath allocated (null is a no-op).
|
||||
unsafe { CoTaskMemFree(raw as *const core::ffi::c_void) };
|
||||
path.or_else(|| std::env::var_os("USERPROFILE").map(std::path::PathBuf::from))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn documents_dir() -> Option<std::path::PathBuf> {
|
||||
let home = std::env::var_os("HOME").map(std::path::PathBuf::from)?;
|
||||
let documents = home.join("Documents");
|
||||
Some(if documents.is_dir() { documents } else { home })
|
||||
}
|
||||
@@ -106,21 +106,24 @@ impl Clone for BlockDefinitionState {
|
||||
|
||||
impl BlockDefinitionState {
|
||||
pub fn new(existing_names: Vec<String>, pre_selected: Vec<Handle>, default_unit: i16) -> Self {
|
||||
// The on-screen switches, the conversion and Allow exploding start
|
||||
// where the dialog was last confirmed.
|
||||
let remembered = *crate::modules::insert::block_cmdline::defaults();
|
||||
Self {
|
||||
name: String::new(),
|
||||
name_combo: combo_box::State::new(existing_names.clone()),
|
||||
existing_names,
|
||||
base_point_specify_onscreen: false,
|
||||
base_point_x: "0.0000".to_string(),
|
||||
base_point_y: "0.0000".to_string(),
|
||||
base_point_z: "0.0000".to_string(),
|
||||
objects_specify_onscreen: false,
|
||||
base_point_specify_onscreen: remembered.base_on_screen,
|
||||
base_point_x: "0".to_string(),
|
||||
base_point_y: "0".to_string(),
|
||||
base_point_z: "0".to_string(),
|
||||
objects_specify_onscreen: remembered.objects_on_screen,
|
||||
selected_handles: pre_selected,
|
||||
object_mode: BlockObjectMode::Convert,
|
||||
object_mode: remembered.mode,
|
||||
annotative: false,
|
||||
match_orientation: false,
|
||||
scale_uniformly: false,
|
||||
allow_exploding: true,
|
||||
allow_exploding: remembered.allow_exploding,
|
||||
unit: default_unit,
|
||||
description: String::new(),
|
||||
description_content: text_editor::Content::new(),
|
||||
@@ -158,15 +161,20 @@ impl BlockDefinitionState {
|
||||
}
|
||||
|
||||
pub fn invalid_name_char(&self) -> Option<char> {
|
||||
self.name.chars().find(|&c| {
|
||||
matches!(
|
||||
c,
|
||||
'\\' | '/' | ':' | '*' | '?' | '"' | '<' | '>' | '|' | '=' | '`'
|
||||
)
|
||||
})
|
||||
invalid_block_name_char(&self.name)
|
||||
}
|
||||
}
|
||||
|
||||
/// The first character a block name may not hold: \ < > / ? " : ; * | , = `
|
||||
pub fn invalid_block_name_char(name: &str) -> Option<char> {
|
||||
name.chars().find(|&c| {
|
||||
matches!(
|
||||
c,
|
||||
'\\' | '<' | '>' | '/' | '?' | '"' | ':' | ';' | '*' | '|' | ',' | '=' | '`'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn group<'a>(
|
||||
title: impl Into<String>,
|
||||
body: impl Into<Element<'a, Message>>,
|
||||
@@ -235,7 +243,7 @@ pub fn view_window<'a>(
|
||||
state.invalid_name_char()
|
||||
{
|
||||
Some(
|
||||
text(crate::tf!("Invalid character '{invalid_char}' in block name (\\ / : * ? \" < > | = ` are not allowed)"))
|
||||
text(crate::tf!("Invalid character '{invalid_char}' in block name (\\ < > / ? \" : ; * | , = ` are not allowed)"))
|
||||
.size(10)
|
||||
.style(|theme: &Theme| text::Style {
|
||||
color: Some(theme.palette().danger.base.color),
|
||||
@@ -634,7 +642,8 @@ pub fn view_window<'a>(
|
||||
|
||||
// ── Confirm Redefinition Guard ────────────────────────────────────────────
|
||||
if let Some(name) = &state.confirm_redefine {
|
||||
let question = crate::tf!("Block \"{name}\" already exists.\nDo you want to redefine it?");
|
||||
let _ = name;
|
||||
let question = t!("The block definition has changed. Do you want to redefine it ?");
|
||||
let shield = mouse_area(
|
||||
container(Space::new())
|
||||
.width(Length::Fill)
|
||||
@@ -655,7 +664,7 @@ pub fn view_window<'a>(
|
||||
.align_x(iced::alignment::Horizontal::Center),
|
||||
Space::new().height(16),
|
||||
row![
|
||||
dialog_button(t!("Yes"), Message::BlockDefConfirmRedefine(true), false),
|
||||
dialog_button(t!("Redefine"), Message::BlockDefConfirmRedefine(true), false),
|
||||
// Default / safe focus on "No"
|
||||
dialog_button(t!("No"), Message::BlockDefConfirmRedefine(false), true),
|
||||
]
|
||||
|
||||
+15
-4
@@ -104,16 +104,27 @@ impl WblockState {
|
||||
default_path: String,
|
||||
) -> Self {
|
||||
let default_block = existing_blocks.first().cloned().unwrap_or_default();
|
||||
let remembered_mode = crate::modules::insert::block_cmdline::defaults().wblock_mode;
|
||||
Self {
|
||||
source_mode: WblockSourceMode::Objects,
|
||||
block_name: default_block,
|
||||
block_combo: combo_box::State::new(existing_blocks.clone()),
|
||||
existing_blocks,
|
||||
base_point_x: "0.00".to_string(),
|
||||
base_point_y: "0.00".to_string(),
|
||||
base_point_z: "0.00".to_string(),
|
||||
base_point_x: "0".to_string(),
|
||||
base_point_y: "0".to_string(),
|
||||
base_point_z: "0".to_string(),
|
||||
selected_handles: pre_selected,
|
||||
object_mode: WblockObjectMode::Retain,
|
||||
object_mode: match remembered_mode {
|
||||
crate::ui::window::block_definition::BlockObjectMode::Retain => {
|
||||
WblockObjectMode::Retain
|
||||
}
|
||||
crate::ui::window::block_definition::BlockObjectMode::Convert => {
|
||||
WblockObjectMode::Convert
|
||||
}
|
||||
crate::ui::window::block_definition::BlockObjectMode::Delete => {
|
||||
WblockObjectMode::Delete
|
||||
}
|
||||
},
|
||||
file_path: default_path,
|
||||
unit: default_unit,
|
||||
error_message: None,
|
||||
|
||||
Reference in new issue
Block a user