feat(blocks): implement WBLOCK dialog and unify dialog buttons and radio builders

This commit is contained in:
Karim Jerbi committed 2026-09-24 16:44:24 +01:00
1 parent dddf60d79e
commit 1a6279e841
33 files changed
+1308 -45

No files matched your search

+90 -4
View File
@@ -460,6 +460,41 @@ impl OpenCADStudio {
self.active_modal = Some(crate::app::ModalKind::BlockDefinition);
}
cmd if cmd.starts_with("WBLOCK_POINT_PICKED ") => {
let parts: Vec<f64> = cmd["WBLOCK_POINT_PICKED ".len()..]
.split_whitespace()
.filter_map(|s| s.parse::<f64>().ok())
.collect();
if parts.len() == 3 {
if let Some(state) = self.wblock.as_mut() {
state.base_point_x = format!("{:.4}", parts[0]);
state.base_point_y = format!("{:.4}", parts[1]);
state.base_point_z = format!("{:.4}", parts[2]);
}
}
self.tabs[i].active_cmd = None;
self.active_modal = Some(crate::app::ModalKind::WriteBlock);
}
"WBLOCK_POINT_CANCELLED" => {
self.tabs[i].active_cmd = None;
self.active_modal = Some(crate::app::ModalKind::WriteBlock);
}
"WBLOCK_OBJECTS_GATHERED" => {
let handles: Vec<_> = self.tabs[i]
.scene
.selected_entities()
.into_iter()
.map(|(h, _)| h)
.collect();
if let Some(state) = self.wblock.as_mut() {
state.selected_handles = handles;
}
self.tabs[i].active_cmd = None;
self.active_modal = Some(crate::app::ModalKind::WriteBlock);
}
"INSERT" => {
let blocks = self.tabs[i].scene.custom_block_names();
if blocks.is_empty() {
@@ -663,9 +698,60 @@ impl OpenCADStudio {
"PDFATTACH" => {
return Some(Task::done(Message::PdfAttachPick));
}
cmd if cmd == "WBLOCK" || cmd == "WB" || cmd.starts_with("WBLOCK ") => {
let arg = cmd.splitn(2, ' ').nth(1).unwrap_or("").trim();
if arg.is_empty() {
"XATTACH" => {
// Launch the file picker; XAttachPickResult will start the command.
return Some(Task::done(Message::XAttachPick));
}
"WBLOCK" | "WB" => {
let handles: Vec<_> = self.tabs[i]
.scene
.selected_entities()
.into_iter()
.map(|(h, _)| h)
.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]
.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('\\', "/"))
};
self.wblock = Some(
crate::ui::window::wblock::WblockState::new(
existing_blocks,
handles,
default_unit,
default_path,
),
);
self.active_modal = Some(crate::app::ModalKind::WriteBlock);
}
cmd if cmd == "-WBLOCK"
|| cmd == "-WB"
|| cmd.starts_with("-WBLOCK ")
|| cmd.starts_with("-WB ")
|| cmd.starts_with("WBLOCK ")
|| cmd.starts_with("WB ") =>
{
let rest = if let Some(r) = cmd.strip_prefix("-WBLOCK") {
r.trim()
} else if let Some(r) = cmd.strip_prefix("-WB") {
r.trim()
} else if let Some(r) = cmd.strip_prefix("WBLOCK") {
r.trim()
} else if let Some(r) = cmd.strip_prefix("WB") {
r.trim()
} else {
""
};
if rest.is_empty() {
// No argument: use selected entities (*) if any, else ask.
let sel: Vec<_> = self.tabs[i].scene.selected.iter().copied().collect();
if sel.is_empty() {
@@ -676,7 +762,7 @@ impl OpenCADStudio {
return Some(Task::done(Message::WblockSave("*".to_string())));
}
} else {
return Some(Task::done(Message::WblockSave(arg.to_string())));
return Some(Task::done(Message::WblockSave(rest.to_string())));
}
}
+22
View File
@@ -495,6 +495,8 @@ pub(super) struct OpenCADStudio {
block_definition: Option<crate::ui::window::block_definition::BlockDefinitionState>,
/// Working copy of the Attach External Reference dialog; None while closed.
xref_attach: Option<crate::ui::window::xref_attach::XrefAttachState>,
/// Working copy of the Write Block (WBLOCK) dialog; `None` while it is closed.
wblock: Option<crate::ui::window::wblock::WblockState>,
/// Working copy of the structured feature-control-frame editor.
geometric_tolerance: Option<crate::ui::window::geometric_tolerance::State>,
/// PICKDRAG (#226): false (default) = press-drag lassoes; true =
@@ -1883,6 +1885,7 @@ pub enum ModalKind {
DrawingUnits,
BlockDefinition,
XrefAttach,
WriteBlock,
GeometricTolerance,
DraftingSettings,
AutoConstrainSettings,
@@ -2880,6 +2883,24 @@ pub enum Message {
BlockDefConfirmRedefine(bool),
BlockDefDismissError,
BlockDefHelp,
/// Write Block (WBLOCK) dialog messages
WblockSourceMode(crate::ui::window::wblock::WblockSourceMode),
WblockBlockName(String),
WblockBlockSelect(String),
WblockPickPoint,
WblockBaseX(String),
WblockBaseY(String),
WblockBaseZ(String),
WblockSelectObjects,
WblockQuickSelect,
WblockObjectMode(crate::ui::window::wblock::WblockObjectMode),
WblockFilePath(String),
WblockBrowsePath,
WblockBrowsePathResult(Option<std::path::PathBuf>),
WblockUnit(i16),
WblockApply,
WblockDismissError,
WblockHelp,
/// One structured feature-control-frame field changed.
ToleranceDialogField(crate::ui::window::geometric_tolerance::Field),
/// One structured feature-control-frame option changed.
@@ -3962,6 +3983,7 @@ impl OpenCADStudio {
drawing_units: None,
block_definition: None,
xref_attach: None,
wblock: None,
geometric_tolerance: None,
pick_drag_rect: false,
perf_hud: false,
+2
View File
@@ -1163,6 +1163,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
self.reset_modal_geometry();
if self.block_definition.is_some() {
self.active_modal = Some(crate::app::ModalKind::BlockDefinition);
} else if self.wblock.is_some() {
self.active_modal = Some(crate::app::ModalKind::WriteBlock);
}
return Task::none();
}
+315
View File
@@ -209,6 +209,7 @@ impl OpenCADStudio {
Some(GeometricTolerance) => self.geometric_tolerance = None,
Some(BlockDefinition) => self.block_definition = None,
Some(XrefAttach) => self.xref_attach = None,
Some(WriteBlock) => self.wblock = None,
Some(Hyperlink) => {
self.hyperlink_editor_handles.clear();
self.hyperlink_editor_url.clear();
@@ -346,6 +347,14 @@ impl OpenCADStudio {
}
}
}
if self.active_modal == Some(super::ModalKind::WriteBlock) {
if let Some(ref mut state) = self.wblock {
if state.error_message.is_some() {
state.error_message = None;
return Task::none();
}
}
}
return self.update(Message::CloseModal);
}
if self.active_modal == Some(super::ModalKind::BlockDefinition) {
@@ -362,6 +371,13 @@ impl OpenCADStudio {
}
}
}
if self.active_modal == Some(super::ModalKind::WriteBlock) {
if matches!(msg, Message::CommandFinalize)
|| matches!(&msg, Message::ShortcutPressed(key) if key.rsplit('+').next() == Some("ENTER") || key.rsplit('+').next() == Some("RETURN"))
{
return self.update(Message::WblockApply);
}
}
if is_modal_blocked_key_msg(&msg) {
return Task::none();
}
@@ -4950,6 +4966,294 @@ impl OpenCADStudio {
}
self.commit_block_definition(false)
}
Message::WblockSourceMode(mode) => {
if let Some(state) = self.wblock.as_mut() {
state.source_mode = mode;
}
Task::none()
}
Message::WblockBlockName(name) => {
if let Some(state) = self.wblock.as_mut() {
state.block_name = name;
}
Task::none()
}
Message::WblockBlockSelect(name) => {
if let Some(state) = self.wblock.as_mut() {
state.block_name = name.clone();
let trimmed = name.trim();
if !trimmed.is_empty() {
let current_path = std::path::Path::new(&state.file_path);
let file_stem = current_path.file_stem().and_then(|s| s.to_str());
let is_default_or_block = file_stem == Some("new_block")
|| state.existing_blocks.iter().any(|b| Some(b.as_str()) == file_stem);
if is_default_or_block {
let new_file_name = format!("{}.dwg", trimmed);
if let Some(parent) = current_path.parent() {
if !parent.as_os_str().is_empty() {
state.file_path = parent.join(new_file_name).to_string_lossy().to_string();
} else {
state.file_path = new_file_name;
}
} else {
state.file_path = new_file_name;
}
}
}
}
Task::none()
}
Message::WblockPickPoint => {
self.active_modal = None;
let cmd = crate::modules::insert::wblock::WblockPickBasePointCommand;
self.command_line.push_info(&cmd.prompt());
self.tabs[self.active_tab].active_cmd = Some(Box::new(cmd));
Task::none()
}
Message::WblockBaseX(val) => {
if let Some(state) = self.wblock.as_mut() {
state.base_point_x = val;
}
Task::none()
}
Message::WblockBaseY(val) => {
if let Some(state) = self.wblock.as_mut() {
state.base_point_y = val;
}
Task::none()
}
Message::WblockBaseZ(val) => {
if let Some(state) = self.wblock.as_mut() {
state.base_point_z = val;
}
Task::none()
}
Message::WblockSelectObjects => {
self.active_modal = None;
use crate::modules::draw::select::SelectObjectsCommand;
let cmd = SelectObjectsCommand::plain("WBLOCK", "WBLOCK_OBJECTS_GATHERED");
self.command_line.push_info(&cmd.prompt());
self.tabs[self.active_tab].active_cmd = Some(Box::new(cmd));
Task::none()
}
Message::WblockQuickSelect => {
self.active_modal = None;
self.on_qselect_open()
}
Message::WblockObjectMode(mode) => {
if let Some(state) = self.wblock.as_mut() {
state.object_mode = mode;
}
Task::none()
}
Message::WblockFilePath(path) => {
if let Some(state) = self.wblock.as_mut() {
state.file_path = path;
}
Task::none()
}
Message::WblockBrowsePath => {
let default_name = if let Some(state) = self.wblock.as_ref() {
let p = std::path::Path::new(&state.file_path);
p.file_name()
.and_then(|f| f.to_str())
.unwrap_or("new_block.dwg")
.to_string()
} else {
"new_block.dwg".to_string()
};
Task::perform(
async move {
let path = crate::sys::file_dialog()
.set_title(crate::t!("Save Block As").as_ref())
.set_file_name(&default_name)
.add_filter(crate::t!("DWG Files").as_ref(), &["dwg"])
.add_filter(crate::t!("DXF Files").as_ref(), &["dxf"])
.save_file()
.await
.map(|h| crate::sys::handle_path(&h));
path
},
|path| Message::WblockBrowsePathResult(path),
)
}
Message::WblockBrowsePathResult(opt_path) => {
if let Some(path) = opt_path {
if let Some(state) = self.wblock.as_mut() {
state.file_path = path.to_string_lossy().to_string();
}
}
Task::none()
}
Message::WblockUnit(unit) => {
if let Some(state) = self.wblock.as_mut() {
state.unit = unit;
}
Task::none()
}
Message::WblockDismissError => {
if let Some(state) = self.wblock.as_mut() {
state.error_message = None;
}
Task::none()
}
Message::WblockHelp => {
self.command_line.push_info(
crate::t!("WBLOCK writes objects, a block, or the entire drawing to a new drawing file.").as_ref(),
);
Task::none()
}
Message::WblockApply => {
let Some(state) = self.wblock.as_mut() else {
return Task::none();
};
use crate::ui::window::wblock::{WblockObjectMode, WblockSourceMode};
match state.source_mode {
WblockSourceMode::Block => {
let name = state.block_name.trim();
if name.is_empty() {
state.error_message =
Some(crate::t!("Please select or enter a block name.").into_owned());
return Task::none();
}
let i = self.active_tab;
if self.tabs[i].scene.document.block_records.get(name).is_none() {
state.error_message = Some(
crate::tf!("Block \"{}\" does not exist in drawing.", name)
.into_owned(),
);
return Task::none();
}
}
WblockSourceMode::Objects => {
if state.selected_handles.is_empty() {
state.error_message = Some(
crate::t!("No objects selected. You must select objects to define a block.")
.into_owned(),
);
return Task::none();
}
}
WblockSourceMode::EntireDrawing => {}
}
let path_str = state.file_path.trim().to_string();
if path_str.is_empty() {
state.error_message =
Some(crate::t!("Please specify a file name and path.").into_owned());
return Task::none();
}
let mut path = std::path::PathBuf::from(path_str);
if path.extension().is_none() {
path.set_extension("dwg");
}
let i = self.active_tab;
if let Some(current_path) = self.tabs[i].current_path.as_ref() {
if current_path == &path {
state.error_message = Some(
crate::t!("Cannot write to the current drawing file.").into_owned(),
);
return Task::none();
}
}
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(),
};
let worker_display = display_name.clone();
file::background_task(
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())
},
move |result| Message::WblockWriteFinished(worker_display, path, result),
)
}
Message::ToleranceDialogField(field) => {
if let Some(state) = self.geometric_tolerance.as_mut() {
state.apply_field(field);
@@ -5904,6 +6208,8 @@ impl OpenCADStudio {
self.reset_modal_geometry();
if self.block_definition.is_some() {
self.active_modal = Some(super::ModalKind::BlockDefinition);
} else if self.wblock.is_some() {
self.active_modal = Some(super::ModalKind::WriteBlock);
}
Task::none()
}
@@ -6110,6 +6416,15 @@ impl OpenCADStudio {
.collect();
block_def.error_message = None;
self.active_modal = Some(super::ModalKind::BlockDefinition);
} else if let Some(ref mut wblock) = self.wblock {
wblock.selected_handles = self.tabs[i]
.scene
.selected_entities()
.into_iter()
.map(|(h, _)| h)
.collect();
wblock.error_message = None;
self.active_modal = Some(super::ModalKind::WriteBlock);
}
Task::none()
}
+7
View File
@@ -25,6 +25,7 @@ impl OpenCADStudio {
Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(),
Some(K::BlockDefinition) => crate::t!("Block Definition").into_owned(),
Some(K::XrefAttach) => crate::t!("Attach External Reference").into_owned(),
Some(K::WriteBlock) => crate::t!("Write Block").into_owned(),
Some(K::GeometricTolerance) => crate::t!("Geometric Tolerance").into_owned(),
Some(K::DraftingSettings) => crate::t!("Drafting Settings").into_owned(),
Some(K::AutoConstrainSettings) => crate::t!("Constraint Settings").into_owned(),
@@ -514,6 +515,12 @@ impl OpenCADStudio {
crate::ui::window::xref_attach::view_window(state, flow)
})
}
super::super::ModalKind::WriteBlock => {
let state = self.wblock.as_ref()?;
sized_flow(ex, 440, 395, |flow| {
crate::ui::window::wblock::view_window(state, flow)
})
}
super::super::ModalKind::GeometricTolerance => {
let state = self.geometric_tolerance.as_ref()?;
sized_flow(ex, 670, 530, |flow| {
+11
View File
@@ -3,6 +3,17 @@
pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static str)> {
match source {
"Entire drawing" => Some(("common", "entire-drawing")),
"Delete from drawing" => Some(("common", "delete-from-drawing")),
"Destination" => Some(("common", "destination")),
"File name and path:" => Some(("common", "file-name-and-path")),
"Insert units:" => Some(("common", "insert-units-label")),
"Please select or enter a block name." => Some(("common", "wblock-err-no-block-name")),
"Block \"{}\" does not exist in drawing." => Some(("common", "wblock-err-block-not-found")),
"Please specify a file name and path." => Some(("common", "wblock-err-no-path")),
"Cannot write to the current drawing file." => Some(("common", "wblock-err-current-drawing")),
"WBLOCK writes objects, a block, or the entire drawing to a new drawing file." => Some(("common", "wblock-help-info")),
"WBLOCK Specify insertion base point:" => Some(("insert", "wblock-specify-insertion-base-point")),
"Auto Constrain" => Some(("common", "auto-constrain")),
"Delete Constraints" => Some(("common", "delete-constraints")),
"Dimensional" => Some(("common", "dimensional")),
+106
View File
@@ -157,3 +157,109 @@ pub fn normalize_to_origin(out: &mut CadDocument) {
crate::scene::view::dispatch::apply_transform(entity, &shift);
}
}
/// One-shot point picker for the Write Block dialog's "Pick point" button.
pub struct WblockPickBasePointCommand;
impl crate::command::CadCommand for WblockPickBasePointCommand {
fn name(&self) -> &'static str {
"WBLOCK"
}
fn prompt(&self) -> String {
crate::t!("WBLOCK Specify insertion base point:").into_owned()
}
fn on_point(&mut self, pt: glam::DVec3) -> crate::command::CmdResult {
crate::command::CmdResult::Dispatch(format!("WBLOCK_POINT_PICKED {} {} {}", pt.x, pt.y, pt.z))
}
fn on_enter(&mut self) -> crate::command::CmdResult {
crate::command::CmdResult::Dispatch("WBLOCK_POINT_CANCELLED".to_string())
}
fn on_escape(&mut self) -> crate::command::CmdResult {
crate::command::CmdResult::Dispatch("WBLOCK_POINT_CANCELLED".to_string())
}
}
/// Build a standalone `CadDocument` from an explicit list of entity handles,
/// applying `base_point` translation and `unit` insertion units.
pub fn extract_entities_to_doc_with_base(
src: &CadDocument,
handles: &[acadrust::Handle],
base_point: glam::DVec3,
unit: i16,
) -> Result<CadDocument, String> {
let mut out = CadDocument::new();
out.header.insertion_units = unit;
extract_entities_into(src, handles, &mut out)?;
if base_point != glam::DVec3::ZERO {
let shift = crate::command::EntityTransform::Affine(
acadrust::types::Transform::from_translation(acadrust::types::Vector3::new(
-base_point.x,
-base_point.y,
-base_point.z,
)),
);
for entity in out.entities_mut() {
crate::scene::view::dispatch::apply_transform(entity, &shift);
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use acadrust::entities::Line;
use acadrust::types::Vector3;
#[test]
fn test_extract_entities_with_base_point_and_units() {
let mut doc = CadDocument::new();
let mut line = Line::new();
line.start = Vector3::new(10.0, 20.0, 0.0);
line.end = Vector3::new(30.0, 40.0, 0.0);
let h = doc.add_entity(EntityType::Line(line)).unwrap();
let out = extract_entities_to_doc_with_base(
&doc,
&[h],
glam::DVec3::new(10.0, 20.0, 0.0),
4, // Millimeters
)
.expect("extract should succeed");
assert_eq!(out.header.insertion_units, 4);
assert_eq!(out.entities().count(), 1);
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);
} else {
panic!("Expected Line entity");
}
}
#[test]
fn test_extract_block_to_doc() {
let mut doc = CadDocument::new();
let mut line = Line::new();
line.start = Vector3::new(1.0, 2.0, 0.0);
line.end = Vector3::new(3.0, 4.0, 0.0);
let h = doc.add_entity(EntityType::Line(line)).unwrap();
let mut block_record = acadrust::tables::BlockRecord::new("MY_BLOCK".to_string());
block_record.entity_handles.push(h);
let _ = doc.block_records.add(block_record);
let out = extract_block_to_doc(&doc, "MY_BLOCK").expect("block extract should succeed");
assert_eq!(out.entities().count(), 1);
assert!(extract_block_to_doc(&doc, "NON_EXISTENT").is_err());
}
}
+35 -1
View File
@@ -7,10 +7,44 @@
use crate::app::Message;
use crate::ui::style::common::muted_style;
use iced::widget::{button, container, pick_list, row, text, text_input, Space};
use iced::widget::{button, container, pick_list, radio, row, text, text_input, Space};
use iced::{Background, Border, Element, Length, Theme};
use std::borrow::Cow;
/// Standard dialog footer action button (OK, Cancel, Apply, Close, Help, etc.),
/// matching the Drafting Settings / Options appearance: 12px text, [6, 18] padding,
/// themed primary or secondary style.
pub fn dialog_button<'a>(
label: impl text::IntoFragment<'a>,
on_press: Message,
primary: bool,
) -> button::Button<'a, Message> {
button(text(label).size(12))
.on_press(on_press)
.padding([6, 18])
.style(if primary {
button::primary
} else {
button::secondary
})
}
/// Unified radio button helper ensuring consistent 16px size for exact pixel-grid concentric alignment.
pub fn form_radio<'a, T>(
label: impl Into<String>,
value: T,
selected: Option<T>,
on_click: impl Fn(T) -> Message + 'a,
) -> radio::Radio<'a, Message>
where
T: Copy + Eq + 'a,
{
radio(label, value, selected, on_click)
.size(16)
.spacing(6)
.text_size(11)
}
/// Width of the label column in a `label : control` row.
pub const LABEL_WIDTH: f32 = 92.0;
+2 -2
View File
@@ -168,7 +168,7 @@ pub fn view_window<'a>(
Some(relative),
Message::PointStyleSizeRelative,
)
.size(15)
.size(16)
.text_size(13),
radio(
t!("Set Size in Absolute Units"),
@@ -176,7 +176,7 @@ pub fn view_window<'a>(
Some(relative),
Message::PointStyleSizeRelative,
)
.size(15)
.size(16)
.text_size(13),
]
.spacing(6);
+14 -38
View File
@@ -5,7 +5,7 @@ use std::fmt;
use acadrust::Handle;
use iced::widget::{
button, checkbox, column, combo_box, container, mouse_area, pick_list, radio, row, stack, text,
button, checkbox, column, combo_box, container, mouse_area, pick_list, row, stack, text,
text_editor, text_input, Space,
};
use iced::{Background, Border, Element, Fill, Length, Theme};
@@ -14,7 +14,7 @@ use crate::app::Message;
use crate::modules::draw::units;
use crate::t;
use crate::ui::style::common::muted_style;
use crate::ui::style::form::{button_style, field_style};
use crate::ui::style::form::{button_style, dialog_button, field_style, form_radio};
static ICON_PICK_POINT: &[u8] = include_bytes!("../../../assets/icons/blocks/pick_point.svg");
static ICON_SELECT_OBJECTS: &[u8] =
@@ -353,33 +353,24 @@ pub fn view_window<'a>(
let btns_row = row![sel_obj_btn, qsel_btn].spacing(5).align_y(iced::Center);
let radios = column![
radio(
form_radio(
t!("Retain"),
BlockObjectMode::Retain,
Some(state.object_mode),
Message::BlockDefObjectMode,
)
.size(15)
.spacing(5)
.text_size(11),
radio(
),
form_radio(
t!("Convert to block"),
BlockObjectMode::Convert,
Some(state.object_mode),
Message::BlockDefObjectMode,
)
.size(15)
.spacing(5)
.text_size(11),
radio(
),
form_radio(
t!("Delete"),
BlockObjectMode::Delete,
Some(state.object_mode),
Message::BlockDefObjectMode,
)
.size(15)
.spacing(5)
.text_size(11),
),
]
.spacing(2);
@@ -580,20 +571,11 @@ pub fn view_window<'a>(
// ── Footer Bar ────────────────────────────────────────────────────────────
let actions = row![
Space::new().width(Fill),
button(text(t!("OK")).size(11))
.on_press(Message::BlockDefApply)
.style(button_style(true))
.padding([4, 16]),
button(text(t!("Cancel")).size(11))
.on_press(Message::CloseModal)
.style(button_style(false))
.padding([4, 12]),
button(text(t!("Help")).size(11))
.on_press(Message::BlockDefHelp)
.style(button_style(false))
.padding([4, 12]),
dialog_button(t!("OK"), Message::BlockDefApply, true),
dialog_button(t!("Cancel"), Message::CloseModal, false),
dialog_button(t!("Help"), Message::BlockDefHelp, false),
]
.spacing(6)
.spacing(8)
.align_y(iced::Center);
// ── Error Banner ──────────────────────────────────────────────────────────
@@ -664,15 +646,9 @@ pub fn view_window<'a>(
.align_x(iced::alignment::Horizontal::Center),
Space::new().height(16),
row![
button(text(t!("Yes")).size(12))
.on_press(Message::BlockDefConfirmRedefine(true))
.padding([6, 20])
.style(button_style(false)),
dialog_button(t!("Yes"), Message::BlockDefConfirmRedefine(true), false),
// Default / safe focus on "No"
button(text(t!("No")).size(12))
.on_press(Message::BlockDefConfirmRedefine(false))
.padding([6, 22])
.style(button_style(true)),
dialog_button(t!("No"), Message::BlockDefConfirmRedefine(false), true),
]
.spacing(12)
.align_y(iced::Center),
+1
View File
@@ -27,3 +27,4 @@ pub mod named_parameters;
pub mod xref_attach;
pub mod xref_help;
pub mod xref_manager;
pub mod wblock;
+492
View File
@@ -0,0 +1,492 @@
//! Write Block (WBLOCK) dialog window — write a block, selected entities, or entire drawing to an external DWG/DXF file.
use std::fmt;
use acadrust::Handle;
use iced::widget::{
button, column, combo_box, container, pick_list, row, text, text_input, Space,
};
use iced::{Background, Border, Element, Fill, Length, Theme};
use crate::app::Message;
use crate::modules::draw::units;
use crate::t;
use crate::ui::style::form::{button_style, dialog_button, field_style, form_radio};
static ICON_PICK_POINT: &[u8] = include_bytes!("../../../assets/icons/blocks/pick_point.svg");
static ICON_SELECT_OBJECTS: &[u8] =
include_bytes!("../../../assets/icons/blocks/select_objects.svg");
static ICON_QUICK_SELECT: &[u8] = include_bytes!("../../../assets/icons/blocks/quick_select.svg");
static ICON_WARN: &[u8] = include_bytes!("../../../assets/icons/ui/warning_triangle.svg");
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum WblockSourceMode {
Block,
EntireDrawing,
Objects,
}
impl Default for WblockSourceMode {
fn default() -> Self {
WblockSourceMode::Objects
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum WblockObjectMode {
Retain,
Convert,
Delete,
}
impl Default for WblockObjectMode {
fn default() -> Self {
WblockObjectMode::Retain
}
}
/// Dropdown entry for the Unit picker.
#[derive(Clone, PartialEq, Eq)]
pub struct UnitChoice {
pub code: i16,
pub label: &'static str,
}
impl fmt::Display for UnitChoice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = crate::i18n::translate(self.label);
f.write_str(name.as_ref())
}
}
/// The Write Block dialog's state.
pub struct WblockState {
pub source_mode: WblockSourceMode,
pub block_name: String,
pub existing_blocks: Vec<String>,
pub block_combo: combo_box::State<String>,
pub base_point_x: String,
pub base_point_y: String,
pub base_point_z: String,
pub selected_handles: Vec<Handle>,
pub object_mode: WblockObjectMode,
pub file_path: String,
pub unit: i16,
pub error_message: Option<String>,
}
impl Clone for WblockState {
fn clone(&self) -> Self {
Self {
source_mode: self.source_mode,
block_name: self.block_name.clone(),
existing_blocks: self.existing_blocks.clone(),
block_combo: self.block_combo.clone(),
base_point_x: self.base_point_x.clone(),
base_point_y: self.base_point_y.clone(),
base_point_z: self.base_point_z.clone(),
selected_handles: self.selected_handles.clone(),
object_mode: self.object_mode,
file_path: self.file_path.clone(),
unit: self.unit,
error_message: self.error_message.clone(),
}
}
}
impl WblockState {
pub fn new(
existing_blocks: Vec<String>,
pre_selected: Vec<Handle>,
default_unit: i16,
default_path: String,
) -> Self {
let default_block = existing_blocks.first().cloned().unwrap_or_default();
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(),
selected_handles: pre_selected,
object_mode: WblockObjectMode::Retain,
file_path: default_path,
unit: default_unit,
error_message: None,
}
}
pub fn parse_base_point(&self) -> glam::DVec3 {
let x = self.base_point_x.trim().parse::<f64>().unwrap_or(0.0);
let y = self.base_point_y.trim().parse::<f64>().unwrap_or(0.0);
let z = self.base_point_z.trim().parse::<f64>().unwrap_or(0.0);
glam::DVec3::new(x, y, z)
}
}
fn group<'a>(
title: impl Into<String>,
body: impl Into<Element<'a, Message>>,
height: impl Into<Length>,
) -> Element<'a, Message> {
container(
column![
text(title.into())
.size(11)
.style(|theme: &Theme| text::Style {
color: Some(theme.palette().background.strongest.text.scale_alpha(0.85)),
}),
body.into(),
]
.spacing(4),
)
.padding([5, 8])
.width(Fill)
.height(height)
.style(|theme: &Theme| container::Style {
border: Border {
width: 1.0,
radius: 4.0.into(),
color: theme.palette().background.strong.color,
},
..Default::default()
})
.into()
}
pub fn view_window<'a>(
state: &'a WblockState,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
let is_objects = state.source_mode == WblockSourceMode::Objects;
let is_block = state.source_mode == WblockSourceMode::Block;
// ── Source: Block Radio & Dropdown ─────────────────────────────────────────
let block_radio = form_radio(
t!("Block:"),
WblockSourceMode::Block,
Some(state.source_mode),
Message::WblockSourceMode,
);
let block_dropdown: Element<'a, Message> = if is_block {
combo_box(
&state.block_combo,
"",
if state.block_name.is_empty() {
None
} else {
Some(&state.block_name)
},
|chosen: String| Message::WblockBlockSelect(chosen),
)
.on_input(Message::WblockBlockName)
.size(11)
.padding([2, 5])
.width(Fill)
.into()
} else {
crate::ui::read_only::field(&state.block_name, 11.0, Fill)
};
let block_row = row![block_radio, block_dropdown]
.spacing(8)
.width(Fill)
.align_y(iced::Center);
let entire_drawing_radio = form_radio(
t!("Entire drawing"),
WblockSourceMode::EntireDrawing,
Some(state.source_mode),
Message::WblockSourceMode,
);
let objects_radio = form_radio(
t!("Objects"),
WblockSourceMode::Objects,
Some(state.source_mode),
Message::WblockSourceMode,
);
// ── Source: Base Point Sub-group ──────────────────────────────────────────
let pick_pt_icon = crate::ui::icons::semantic(ICON_PICK_POINT, 14.0);
let mut pick_pt_btn = button(
row![pick_pt_icon, text(t!("Pick point")).size(11)]
.spacing(6)
.align_y(iced::Center),
)
.padding([3, 10]);
if is_objects {
pick_pt_btn = pick_pt_btn
.on_press(Message::WblockPickPoint)
.style(button_style(false));
} else {
pick_pt_btn = pick_pt_btn.style(button_style(false));
}
let coord_row = |axis: &'static str, val: &'a str, on_input: fn(String) -> Message| {
let input: Element<'a, Message> = if is_objects {
text_input("", val)
.on_input(on_input)
.on_submit(Message::WblockApply)
.style(field_style)
.size(11)
.padding([2, 5])
.width(Fill)
.into()
} else {
crate::ui::read_only::field(val, 11.0, Fill)
};
row![
text(axis)
.size(11)
.style(|theme: &Theme| text::Style {
color: Some(theme.palette().background.strongest.text.scale_alpha(0.85)),
})
.width(Length::Fixed(16.0)),
input,
]
.spacing(4)
.align_y(iced::Center)
};
let base_point_group = group(
t!("Base point").into_owned(),
column![
pick_pt_btn,
Space::new().height(3),
coord_row("X:", &state.base_point_x, Message::WblockBaseX),
coord_row("Y:", &state.base_point_y, Message::WblockBaseY),
coord_row("Z:", &state.base_point_z, Message::WblockBaseZ),
]
.spacing(3),
Length::Fixed(142.0),
);
// ── Source: Objects Sub-group ─────────────────────────────────────────────
let sel_obj_icon = crate::ui::icons::semantic(ICON_SELECT_OBJECTS, 14.0);
let mut sel_obj_btn = button(
row![sel_obj_icon, text(t!("Select objects")).size(11)]
.spacing(6)
.align_y(iced::Center),
)
.padding([3, 8]);
let qsel_icon = crate::ui::icons::semantic(ICON_QUICK_SELECT, 14.0);
let mut qsel_btn = button(qsel_icon).padding([3, 6]);
if is_objects {
sel_obj_btn = sel_obj_btn
.on_press(Message::WblockSelectObjects)
.style(button_style(false));
qsel_btn = qsel_btn
.on_press(Message::WblockQuickSelect)
.style(button_style(false));
} else {
sel_obj_btn = sel_obj_btn.style(button_style(false));
qsel_btn = qsel_btn.style(button_style(false));
}
let obj_btns_row = row![sel_obj_btn, qsel_btn]
.spacing(5)
.align_y(iced::Center);
let object_radios = column![
form_radio(
t!("Retain"),
WblockObjectMode::Retain,
if is_objects { Some(state.object_mode) } else { None },
Message::WblockObjectMode,
),
form_radio(
t!("Convert to block"),
WblockObjectMode::Convert,
if is_objects { Some(state.object_mode) } else { None },
Message::WblockObjectMode,
),
form_radio(
t!("Delete from drawing"),
WblockObjectMode::Delete,
if is_objects { Some(state.object_mode) } else { None },
Message::WblockObjectMode,
),
]
.spacing(3);
let count = state.selected_handles.len();
let status_row: Element<'a, Message> = if !is_objects {
Space::new().height(14).into()
} else if count == 0 {
row![
crate::ui::icons::semantic(ICON_WARN, 13.0),
text(t!("No objects selected"))
.size(10)
.style(|theme: &Theme| text::Style {
color: Some(theme.palette().warning.base.color),
}),
]
.spacing(5)
.align_y(iced::Center)
.into()
} else {
row![text(crate::tf!("{count} objects selected"))
.size(10)
.style(|theme: &Theme| text::Style {
color: Some(theme.palette().primary.base.color),
}),]
.spacing(5)
.align_y(iced::Center)
.into()
};
let objects_subgroup = group(
t!("Objects").into_owned(),
column![obj_btns_row, Space::new().height(2), object_radios, Space::new().height(2), status_row,]
.spacing(2),
Length::Fixed(142.0),
);
let sub_panels = row![base_point_group, objects_subgroup]
.spacing(8)
.width(Fill);
let source_group = group(
t!("Source").into_owned(),
column![
block_row,
entire_drawing_radio,
objects_radio,
Space::new().height(4),
sub_panels,
]
.spacing(4),
Length::Shrink,
);
// ── Destination Group ─────────────────────────────────────────────────────
let file_input = text_input("", &state.file_path)
.on_input(Message::WblockFilePath)
.on_submit(Message::WblockApply)
.style(field_style)
.size(11)
.padding([3, 6])
.width(Fill);
let browse_btn = button(
container(text("...").size(11))
.width(Fill)
.align_x(iced::alignment::Horizontal::Center),
)
.width(Length::Fixed(28.0))
.on_press(Message::WblockBrowsePath)
.style(button::secondary)
.padding([3, 0]);
let path_row = row![file_input, browse_btn].spacing(5).align_y(iced::Center);
let unit_options: Vec<UnitChoice> = units::all()
.map(|(code, label)| UnitChoice { code, label })
.collect();
let selected_unit = unit_options
.iter()
.find(|choice| choice.code == state.unit)
.cloned();
let unit_picker = pick_list(selected_unit, unit_options, |choice| choice.to_string())
.on_select(|choice| Message::WblockUnit(choice.code))
.text_size(11)
.padding([2, 5])
.width(Fill);
let units_row = row![
text(t!("Insert units:"))
.size(11)
.style(|theme: &Theme| text::Style {
color: Some(theme.palette().background.strongest.text.scale_alpha(0.85)),
}),
Space::new().width(8),
unit_picker,
Space::new().width(Length::Fixed(33.0)),
]
.spacing(4)
.align_y(iced::Center);
let destination_group = group(
t!("Destination").into_owned(),
column![
text(t!("File name and path:"))
.size(11)
.style(|theme: &Theme| text::Style {
color: Some(theme.palette().background.strongest.text.scale_alpha(0.85)),
}),
path_row,
Space::new().height(4),
units_row,
]
.spacing(3),
Length::Shrink,
);
// ── Footer Bar ────────────────────────────────────────────────────────────
let actions = row![
Space::new().width(Fill),
dialog_button(t!("OK"), Message::WblockApply, true),
dialog_button(t!("Cancel"), Message::CloseModal, false),
dialog_button(t!("Help"), Message::WblockHelp, false),
]
.spacing(8)
.align_y(iced::Center);
// ── Error Banner ──────────────────────────────────────────────────────────
let mut main_column = column![source_group, destination_group, actions,]
.spacing(8)
.padding([8, 10])
.width(sizing.width);
if let Some(err) = &state.error_message {
let err_banner = container(
row![
crate::ui::icons::semantic(ICON_WARN, 16.0),
text(err.clone())
.size(11)
.style(|theme: &Theme| text::Style {
color: Some(theme.palette().background.strongest.text),
}),
Space::new().width(Fill),
button(text("×").size(12))
.on_press(Message::WblockDismissError)
.style(button_style(false))
.padding([1, 6]),
]
.spacing(8)
.align_y(iced::Center),
)
.padding([6, 10])
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().danger.base.color.scale_alpha(0.18),
)),
border: Border {
width: 1.0,
radius: 4.0.into(),
color: theme.palette().danger.base.color,
},
..Default::default()
});
main_column = column![err_banner, main_column]
.spacing(6)
.width(sizing.width)
.padding(10);
}
main_column.into()
}