forked from 1b-agent/opencadstudio-upstream
feat(blocks): implement Block Definition dialog (BLOCK/BMAKE) with on-screen sequencing
Implement the complete Block Definition dialog window launched via BLOCK
and BMAKE commands, featuring standard CAD layout, workflows, and entity
creation pipelines.
- Dialog UI & Layout:
- 2-column compact CAD grid with synchronized vertical heights (580 × 390px).
- Editable combobox with existing block names, invalid character validation,
and inline redefinition badge.
- Multi-line text editor widget for block descriptions (replacing single-line input).
- Quick Select (QSELECT) button integration restoring dialog state upon completion.
- Confirmation guard when attempting to redefine an existing block definition.
- Centered radio buttons, clean coordinate inputs, and accessible keyboard shortcuts.
- Base Point & On-Screen Workflow:
- BlockOnScreenCommand strictly enforces prompt sequencing: Base point is
prompted before object selection.
- Dynamically recalculates world_to_block and block_to_world affine transforms
when base point is specified on-screen, preventing stale (0,0,0) normalization.
- Supports Retain, Convert to block (INSERT placement), and Delete modes.
- Explode Enforcement:
- Guarded EXPLODE command to prevent decomposing blocks flagged as non-explodable.
- Dynamic Themed Icons:
- Added pick_point.svg, select_objects.svg, quick_select.svg, and warning_triangle.svg.
- Routed all icons through the semantic theming engine so line work (#B4B6B9)
and accents (#6DB7ED) adapt automatically to the active theme.
- Tests:
- Added unit tests for retain/convert/delete modes, redefinition, explodable flag,
on-screen base point transform recalculation, and command sequencing.
This commit is contained in:
1 parent
889a19c362
commit
652f4d6bbc
19 files changed
+2316
-318
No files matched your search
+169
-42
@@ -1,6 +1,24 @@
|
||||
// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged.
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateBlockOptions {
|
||||
pub name: String,
|
||||
pub handles: Vec<Handle>,
|
||||
pub base_point: glam::DVec3,
|
||||
pub world_to_block: acadrust::types::Transform,
|
||||
pub block_to_world: acadrust::types::Transform,
|
||||
pub mode: crate::ui::window::block_definition::BlockObjectMode,
|
||||
pub annotative: bool,
|
||||
pub match_orientation: bool,
|
||||
pub scale_uniformly: bool,
|
||||
pub allow_exploding: bool,
|
||||
pub unit: i16,
|
||||
pub description: String,
|
||||
pub hyperlink_url: String,
|
||||
pub hyperlink_desc: String,
|
||||
pub redefine: bool,
|
||||
}
|
||||
|
||||
/// Order sampled boundary edges into one tip-to-tail loop.
|
||||
pub(super) fn chain_path_edges(polys: Vec<Vec<[f64; 2]>>) -> Vec<[f64; 2]> {
|
||||
chain_path_edges_with_directions(polys).0
|
||||
@@ -845,18 +863,151 @@ impl Scene {
|
||||
world_to_block: &acadrust::types::Transform,
|
||||
block_to_world: &acadrust::types::Transform,
|
||||
) -> Result<Handle, String> {
|
||||
let name = name.trim();
|
||||
self.create_block_with_options(CreateBlockOptions {
|
||||
name: name.to_string(),
|
||||
handles: handles.to_vec(),
|
||||
base_point: glam::DVec3::ZERO,
|
||||
world_to_block: *world_to_block,
|
||||
block_to_world: *block_to_world,
|
||||
mode: crate::ui::window::block_definition::BlockObjectMode::Convert,
|
||||
annotative: false,
|
||||
match_orientation: false,
|
||||
scale_uniformly: false,
|
||||
allow_exploding: true,
|
||||
unit: 0,
|
||||
description: String::new(),
|
||||
hyperlink_url: String::new(),
|
||||
hyperlink_desc: String::new(),
|
||||
redefine: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_block_with_options(
|
||||
&mut self,
|
||||
options: CreateBlockOptions,
|
||||
) -> Result<Handle, String> {
|
||||
let name = options.name.trim();
|
||||
if name.is_empty() {
|
||||
return Err("Block name cannot be empty.".into());
|
||||
}
|
||||
if name.starts_with('*') {
|
||||
return Err("Block name cannot start with '*'.".into());
|
||||
}
|
||||
if self.document.block_records.get(name).is_some() {
|
||||
return Err(format!("Block \"{name}\" already exists."));
|
||||
}
|
||||
|
||||
let source_entities: Vec<_> = handles
|
||||
let existing = self
|
||||
.document
|
||||
.block_records
|
||||
.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 {
|
||||
if !options.redefine {
|
||||
return Err(format!("Block \"{name}\" already exists."));
|
||||
}
|
||||
// Redefining: remove all existing entities owned by this block record (except Block/BlockEnd)
|
||||
let owned: Vec<Handle> = self
|
||||
.document
|
||||
.entities()
|
||||
.filter(|e| {
|
||||
e.common().owner_handle == existing_br_h
|
||||
&& !matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_))
|
||||
})
|
||||
.map(|e| e.common().handle)
|
||||
.collect();
|
||||
for h in owned {
|
||||
self.document.remove_entity(h);
|
||||
}
|
||||
// Update existing BlockRecord fields
|
||||
if let Some(br) = self.document.block_records.get_mut(name) {
|
||||
br.units = options.unit;
|
||||
br.description = options.description.clone();
|
||||
br.explodable = options.allow_exploding;
|
||||
br.scale_uniformly = options.scale_uniformly;
|
||||
}
|
||||
// Update existing Block entity fields
|
||||
if let Some(EntityType::Block(b)) = self.document.get_entity_mut(existing_blk_h) {
|
||||
b.description = options.description.clone();
|
||||
}
|
||||
if options.annotative {
|
||||
let annotative_vals = vec![acadrust::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![acadrust::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();
|
||||
let br_handle = Handle::new(next);
|
||||
let block_handle = Handle::new(next + 1);
|
||||
let end_handle = Handle::new(next + 2);
|
||||
|
||||
let mut block_record = acadrust::tables::BlockRecord::new(name);
|
||||
block_record.handle = br_handle;
|
||||
block_record.block_entity_handle = block_handle;
|
||||
block_record.block_end_handle = end_handle;
|
||||
block_record.units = options.unit;
|
||||
block_record.description = options.description.clone();
|
||||
block_record.explodable = options.allow_exploding;
|
||||
block_record.scale_uniformly = options.scale_uniformly;
|
||||
self.document
|
||||
.block_records
|
||||
.add(block_record)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut block = Block::new(name, acadrust::types::Vector3::ZERO);
|
||||
block.common.handle = block_handle;
|
||||
block.common.owner_handle = br_handle;
|
||||
block.description = options.description.clone();
|
||||
self.document
|
||||
.add_entity(EntityType::Block(block))
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut block_end = BlockEnd::new();
|
||||
block_end.common.handle = end_handle;
|
||||
block_end.common.owner_handle = br_handle;
|
||||
self.document
|
||||
.add_entity(EntityType::BlockEnd(block_end))
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
if options.annotative {
|
||||
let annotative_vals = vec![acadrust::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![acadrust::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)
|
||||
};
|
||||
|
||||
let source_entities: Vec<_> = options
|
||||
.handles
|
||||
.iter()
|
||||
.filter_map(|&h| self.document.get_entity(h).cloned().map(|e| (h, e)))
|
||||
.collect();
|
||||
@@ -864,35 +1015,7 @@ impl Scene {
|
||||
return Err("No valid entities selected for block creation.".into());
|
||||
}
|
||||
|
||||
let next = self.document.next_handle();
|
||||
let br_handle = Handle::new(next);
|
||||
let block_handle = Handle::new(next + 1);
|
||||
let end_handle = Handle::new(next + 2);
|
||||
|
||||
let mut block_record = acadrust::tables::BlockRecord::new(name);
|
||||
block_record.handle = br_handle;
|
||||
block_record.block_entity_handle = block_handle;
|
||||
block_record.block_end_handle = end_handle;
|
||||
self.document
|
||||
.block_records
|
||||
.add(block_record)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut block = Block::new(name, acadrust::types::Vector3::ZERO);
|
||||
block.common.handle = block_handle;
|
||||
block.common.owner_handle = br_handle;
|
||||
self.document
|
||||
.add_entity(EntityType::Block(block))
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut block_end = BlockEnd::new();
|
||||
block_end.common.handle = end_handle;
|
||||
block_end.common.owner_handle = br_handle;
|
||||
self.document
|
||||
.add_entity(EntityType::BlockEnd(block_end))
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let local = EntityTransform::Affine(*world_to_block);
|
||||
let local = EntityTransform::Affine(options.world_to_block);
|
||||
for (old_handle, mut entity) in source_entities {
|
||||
view::dispatch::apply_transform(&mut entity, &local);
|
||||
entity = crate::modules::draw::modify::explode::normalize_entity_for_block(entity);
|
||||
@@ -901,15 +1024,19 @@ impl Scene {
|
||||
self.document
|
||||
.add_entity(entity)
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.erase_entities(&[old_handle]);
|
||||
if options.mode != crate::ui::window::block_definition::BlockObjectMode::Retain {
|
||||
self.erase_entities(&[old_handle]);
|
||||
}
|
||||
}
|
||||
|
||||
let mut insert = DxfInsert::new(name, acadrust::types::Vector3::ZERO);
|
||||
acadrust::Entity::apply_transform(&mut insert, block_to_world);
|
||||
let insert_handle = self.add_entity(EntityType::Insert(insert));
|
||||
// A new block definition landed in the document; advance the block
|
||||
// epoch so consumers (the block palette stale check) notice it even
|
||||
// when the panel stays open. Mirrors `define_block_from_owned_entities`.
|
||||
let mut insert_handle = Handle::NULL;
|
||||
if options.mode == crate::ui::window::block_definition::BlockObjectMode::Convert {
|
||||
let mut insert = DxfInsert::new(name, acadrust::types::Vector3::ZERO);
|
||||
acadrust::Entity::apply_transform(&mut insert, &options.block_to_world);
|
||||
insert_handle = self.add_entity(EntityType::Insert(insert));
|
||||
}
|
||||
|
||||
// Advance the block epoch so all consumers and viewport renderers re-tessellate
|
||||
self.bump_geometry();
|
||||
Ok(insert_handle)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ pub use dimension_assoc::{ReferenceStatus, ResolvedReference};
|
||||
pub use page_setup::{apply_default_page_setup, document_page_setups, rotated_margins};
|
||||
mod dwg_native_constraints;
|
||||
mod entity;
|
||||
pub use entity::CreateBlockOptions;
|
||||
#[cfg(test)]
|
||||
mod hatch_boundary;
|
||||
mod group_layer;
|
||||
|
||||
Reference in new issue
Block a user