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
@@ -3654,6 +3654,36 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
}
|
||||
CmdResult::CreateBlockWithOptions { mut options } => {
|
||||
options.handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
||||
if options.handles.is_empty() {
|
||||
self.command_line
|
||||
.push_info(crate::t!("No editable objects selected.").as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
self.push_undo_snapshot(i, "BLOCK");
|
||||
let name = options.name.clone();
|
||||
match self.tabs[i].scene.create_block_with_options(*options) {
|
||||
Ok(insert_handle) => {
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.deselect_all();
|
||||
if !insert_handle.is_null() {
|
||||
self.tabs[i].scene.select_entity(insert_handle, false);
|
||||
}
|
||||
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());
|
||||
self.refresh_properties();
|
||||
self.refresh_block_palette();
|
||||
}
|
||||
Err(err) => {
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line.push_error(&err);
|
||||
}
|
||||
}
|
||||
}
|
||||
CmdResult::CommitHatch(hatch) => {
|
||||
let label = self.history_label_from_active_cmd(i, "HATCH");
|
||||
let pending = self.begin_undo(i, label, 1, true);
|
||||
|
||||
+472
-232
File diff suppressed because it is too large.
Load diff
@@ -495,8 +495,8 @@ impl OpenCADStudio {
|
||||
return Some(Task::none());
|
||||
}
|
||||
|
||||
let insert = match self.tabs[i].scene.document.get_entity(insert_handle) {
|
||||
Some(acadrust::EntityType::Insert(ins)) => ins.clone(),
|
||||
let block_name = match self.tabs[i].scene.document.get_entity(insert_handle) {
|
||||
Some(acadrust::EntityType::Insert(ins)) => ins.block_name.clone(),
|
||||
_ => {
|
||||
self.command_line
|
||||
.push_error(crate::t!("BEDIT: selected object is not a block reference.").as_ref());
|
||||
@@ -509,13 +509,13 @@ impl OpenCADStudio {
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get(&insert.block_name)
|
||||
.get(&block_name)
|
||||
{
|
||||
Some(br) => (br.handle, br.flags.is_xref),
|
||||
None => {
|
||||
self.command_line.push_error(crate::tf!(
|
||||
"BEDIT: block \"{}\" not found.",
|
||||
insert.block_name
|
||||
block_name
|
||||
).as_ref());
|
||||
return Some(Task::none());
|
||||
}
|
||||
@@ -532,7 +532,7 @@ impl OpenCADStudio {
|
||||
{
|
||||
self.tabs[i].active_cmd = None;
|
||||
return Some(Task::done(Message::BlockEditSwitch(
|
||||
insert.block_name.clone(),
|
||||
block_name.clone(),
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -543,7 +543,7 @@ impl OpenCADStudio {
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get(&insert.block_name)
|
||||
.get(&block_name)
|
||||
.unwrap();
|
||||
br.entity_handles
|
||||
.iter()
|
||||
@@ -564,7 +564,7 @@ impl OpenCADStudio {
|
||||
.entities()
|
||||
.filter_map(|entity| match entity {
|
||||
acadrust::EntityType::Insert(reference)
|
||||
if reference.block_name.eq_ignore_ascii_case(&insert.block_name)
|
||||
if reference.block_name.eq_ignore_ascii_case(&block_name)
|
||||
&& !reference.attributes.is_empty() =>
|
||||
{
|
||||
Some((
|
||||
@@ -607,7 +607,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
self.tabs[i].scene.block_edit_block = Some(br_handle);
|
||||
self.tabs[i].block_edits.push(BlockEditSession {
|
||||
block_name: insert.block_name.clone(),
|
||||
block_name: block_name.clone(),
|
||||
br_handle,
|
||||
return_layout,
|
||||
return_block,
|
||||
@@ -651,7 +651,7 @@ impl OpenCADStudio {
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_info(crate::tf!(
|
||||
"BEDIT: Editing block \"{}\". Use Save Block or Discard to finish.",
|
||||
insert.block_name
|
||||
block_name
|
||||
).as_ref());
|
||||
}
|
||||
|
||||
|
||||
+49
-29
@@ -81,11 +81,26 @@ mod coordinate_parsing_tests {
|
||||
#[test]
|
||||
fn parses_all_coordinate_forms() {
|
||||
let close = |a: glam::DVec3, b: glam::DVec3| (a - b).length() < 1e-9;
|
||||
assert!(close(parse_coord("1,2").unwrap().0, glam::dvec3(1.0, 2.0, 0.0)));
|
||||
assert!(close(parse_coord("1,2,3").unwrap().0, glam::dvec3(1.0, 2.0, 3.0)));
|
||||
assert!(close(parse_coord("10<90").unwrap().0, glam::dvec3(0.0, 10.0, 0.0)));
|
||||
assert!(close(parse_coord("10<90,4").unwrap().0, glam::dvec3(0.0, 10.0, 4.0)));
|
||||
assert!(close(parse_coord("10<0<30").unwrap().0, glam::dvec3(5.0 * 3.0_f64.sqrt(), 0.0, 5.0)));
|
||||
assert!(close(
|
||||
parse_coord("1,2").unwrap().0,
|
||||
glam::dvec3(1.0, 2.0, 0.0)
|
||||
));
|
||||
assert!(close(
|
||||
parse_coord("1,2,3").unwrap().0,
|
||||
glam::dvec3(1.0, 2.0, 3.0)
|
||||
));
|
||||
assert!(close(
|
||||
parse_coord("10<90").unwrap().0,
|
||||
glam::dvec3(0.0, 10.0, 0.0)
|
||||
));
|
||||
assert!(close(
|
||||
parse_coord("10<90,4").unwrap().0,
|
||||
glam::dvec3(0.0, 10.0, 4.0)
|
||||
));
|
||||
assert!(close(
|
||||
parse_coord("10<0<30").unwrap().0,
|
||||
glam::dvec3(5.0 * 3.0_f64.sqrt(), 0.0, 5.0)
|
||||
));
|
||||
assert_eq!(parse_coord("@10<0").unwrap().1, CoordKind::Relative);
|
||||
assert_eq!(parse_coord("#1,2").unwrap().1, CoordKind::Absolute);
|
||||
}
|
||||
@@ -102,8 +117,8 @@ mod coordinate_parsing_tests {
|
||||
/// the UCS icon, snap/ortho, the ViewCube — goes through this one type instead
|
||||
/// of re-deriving the axis math. Axes are orthonormal, so the inverse rotation
|
||||
/// is just the transpose (the dot products in `to_ucs`); no matrix inversion.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct UcsXform {
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct UcsXform {
|
||||
origin: glam::DVec3,
|
||||
x: glam::DVec3,
|
||||
y: glam::DVec3,
|
||||
@@ -112,7 +127,7 @@ pub(super) struct UcsXform {
|
||||
|
||||
impl UcsXform {
|
||||
/// Plain WCS — no active UCS.
|
||||
pub(super) fn identity() -> Self {
|
||||
pub(crate) fn identity() -> Self {
|
||||
Self {
|
||||
origin: glam::DVec3::ZERO,
|
||||
x: glam::DVec3::X,
|
||||
@@ -132,7 +147,12 @@ impl UcsXform {
|
||||
};
|
||||
let z = x.cross(raw_y).normalize_or(x.cross(fallback_z).normalize());
|
||||
let y = z.cross(x).normalize();
|
||||
Self { origin: v(ucs.origin), x, y, z }
|
||||
Self {
|
||||
origin: v(ucs.origin),
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn from_active(ucs: Option<&Ucs>) -> Self {
|
||||
@@ -191,10 +211,7 @@ impl UcsXform {
|
||||
|
||||
/// Full UCS-local → WCS transform, using `origin` as local zero while
|
||||
/// retaining this UCS's orthonormal axes.
|
||||
pub(super) fn to_wcs_transform_at(
|
||||
&self,
|
||||
origin: glam::DVec3,
|
||||
) -> acadrust::types::Transform {
|
||||
pub(crate) fn to_wcs_transform_at(&self, origin: glam::DVec3) -> acadrust::types::Transform {
|
||||
use acadrust::types::{Matrix4, Transform};
|
||||
Transform::from_matrix(Matrix4 {
|
||||
m: [
|
||||
@@ -207,10 +224,7 @@ impl UcsXform {
|
||||
}
|
||||
|
||||
/// Full WCS → UCS-local transform, using `origin` as the local zero.
|
||||
pub(super) fn to_ucs_transform_at(
|
||||
&self,
|
||||
origin: glam::DVec3,
|
||||
) -> acadrust::types::Transform {
|
||||
pub(crate) fn to_ucs_transform_at(&self, origin: glam::DVec3) -> acadrust::types::Transform {
|
||||
use acadrust::types::{Matrix4, Transform};
|
||||
Transform::from_matrix(Matrix4 {
|
||||
m: [
|
||||
@@ -326,7 +340,11 @@ pub(super) fn drafting_constrain(
|
||||
let radians = degrees.to_radians();
|
||||
glam::DVec2::new(radians.cos(), radians.sin())
|
||||
});
|
||||
let direction = if delta.dot(a).abs() >= delta.dot(c).abs() { a } else { c };
|
||||
let direction = if delta.dot(a).abs() >= delta.dot(c).abs() {
|
||||
a
|
||||
} else {
|
||||
c
|
||||
};
|
||||
let projected = direction * delta.dot(direction);
|
||||
let c = glam::DVec3::new(b.x + projected.x, b.y + projected.y, p.z);
|
||||
xf.to_wcs(c)
|
||||
@@ -393,8 +411,7 @@ pub(super) fn polar_constrain_near(
|
||||
tol_px: f32,
|
||||
xf: &UcsXform,
|
||||
) -> glam::DVec3 {
|
||||
polar_constrain_if_near(pt, base, step_deg, view_rot, eye, bounds, tol_px, xf)
|
||||
.unwrap_or(pt)
|
||||
polar_constrain_if_near(pt, base, step_deg, view_rot, eye, bounds, tol_px, xf).unwrap_or(pt)
|
||||
}
|
||||
|
||||
/// Hard axis lock (#312): the locked ray's direction — the nearest polar
|
||||
@@ -427,8 +444,16 @@ pub(super) fn axis_lock_capture(
|
||||
let radians = degrees.to_radians();
|
||||
glam::DVec2::new(radians.cos(), radians.sin())
|
||||
});
|
||||
let direction = if delta.dot(a).abs() >= delta.dot(b).abs() { a } else { b };
|
||||
let direction = if delta.dot(direction) < 0.0 { -direction } else { direction };
|
||||
let direction = if delta.dot(a).abs() >= delta.dot(b).abs() {
|
||||
a
|
||||
} else {
|
||||
b
|
||||
};
|
||||
let direction = if delta.dot(direction) < 0.0 {
|
||||
-direction
|
||||
} else {
|
||||
direction
|
||||
};
|
||||
direction.y.atan2(direction.x)
|
||||
};
|
||||
let dir_ucs = glam::DVec3::new(ang.cos(), ang.sin(), 0.0);
|
||||
@@ -439,11 +464,7 @@ pub(super) fn axis_lock_capture(
|
||||
/// Project `pt` onto the locked ray through `base` — the hard lock applies to
|
||||
/// EVERYTHING, including an osnap hit, so a snap far off-axis contributes only
|
||||
/// its along-axis component (#312).
|
||||
pub(super) fn axis_lock_apply(
|
||||
pt: glam::DVec3,
|
||||
base: glam::DVec3,
|
||||
dir: glam::DVec3,
|
||||
) -> glam::DVec3 {
|
||||
pub(super) fn axis_lock_apply(pt: glam::DVec3, base: glam::DVec3, dir: glam::DVec3) -> glam::DVec3 {
|
||||
base + dir * (pt - base).dot(dir)
|
||||
}
|
||||
|
||||
@@ -481,8 +502,7 @@ pub(super) fn entities_lower_left_by_bbox(
|
||||
|
||||
/// Generate the next available auto group name ("*A1", "*A2", …).
|
||||
pub(super) fn next_group_auto_name(scene: &crate::scene::Scene) -> String {
|
||||
let existing: rustc_hash::FxHashSet<String> =
|
||||
scene.groups().map(|g| g.name.clone()).collect();
|
||||
let existing: rustc_hash::FxHashSet<String> = scene.groups().map(|g| g.name.clone()).collect();
|
||||
for n in 1..=9999 {
|
||||
let name = format!("*A{n}");
|
||||
if !existing.contains(&name) {
|
||||
|
||||
@@ -487,6 +487,8 @@ pub(super) struct OpenCADStudio {
|
||||
layer_translator: Option<crate::ui::window::layer_translator::State>,
|
||||
/// Working copy of the Drawing Units dialog; `None` while it is closed.
|
||||
drawing_units: Option<crate::ui::window::drawing_units::State>,
|
||||
/// Working copy of the Block Definition dialog; `None` while it is closed.
|
||||
block_definition: Option<crate::ui::window::block_definition::BlockDefinitionState>,
|
||||
/// 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 =
|
||||
@@ -1857,6 +1859,7 @@ pub enum ModalKind {
|
||||
LayerStateManager,
|
||||
LayerTranslator,
|
||||
DrawingUnits,
|
||||
BlockDefinition,
|
||||
GeometricTolerance,
|
||||
DraftingSettings,
|
||||
AutoConstrainSettings,
|
||||
@@ -2830,6 +2833,30 @@ pub enum Message {
|
||||
DrawingUnitsField(crate::ui::window::drawing_units::Field),
|
||||
/// Drawing Units OK — write the working copy into the drawing.
|
||||
DrawingUnitsApply,
|
||||
/// Block Definition dialog field updates
|
||||
BlockDefName(String),
|
||||
BlockDefNameSelect(String),
|
||||
BlockDefBaseOnScreen(bool),
|
||||
BlockDefPickPoint,
|
||||
BlockDefBaseX(String),
|
||||
BlockDefBaseY(String),
|
||||
BlockDefBaseZ(String),
|
||||
BlockDefObjectsOnScreen(bool),
|
||||
BlockDefSelectObjects,
|
||||
BlockDefQuickSelect,
|
||||
BlockDefObjectMode(crate::ui::window::block_definition::BlockObjectMode),
|
||||
BlockDefAnnotative(bool),
|
||||
BlockDefMatchOrientation(bool),
|
||||
BlockDefScaleUniformly(bool),
|
||||
BlockDefAllowExploding(bool),
|
||||
BlockDefUnit(i16),
|
||||
BlockDefDescription(String),
|
||||
BlockDefDescriptionAction(iced::widget::text_editor::Action),
|
||||
BlockDefHyperlink,
|
||||
BlockDefApply,
|
||||
BlockDefConfirmRedefine(bool),
|
||||
BlockDefDismissError,
|
||||
BlockDefHelp,
|
||||
/// One structured feature-control-frame field changed.
|
||||
ToleranceDialogField(crate::ui::window::geometric_tolerance::Field),
|
||||
/// One structured feature-control-frame option changed.
|
||||
@@ -3902,6 +3929,7 @@ impl OpenCADStudio {
|
||||
last_layer_translation: None,
|
||||
layer_translator: None,
|
||||
drawing_units: None,
|
||||
block_definition: None,
|
||||
geometric_tolerance: None,
|
||||
pick_drag_rect: false,
|
||||
perf_hud: false,
|
||||
|
||||
@@ -1117,6 +1117,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
if let Some(state) = self.qselect.take() {
|
||||
self.qselect_settings = Some((&state).into());
|
||||
self.reset_modal_geometry();
|
||||
if self.block_definition.is_some() {
|
||||
self.active_modal = Some(crate::app::ModalKind::BlockDefinition);
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::{ArrowKey, Message, OpenCADStudio, TextEntryMode};
|
||||
use crate::command::CadCommand;
|
||||
use crate::scene::VIEWCUBE_DRAW_PX;
|
||||
use crate::ui::PropertiesPanel;
|
||||
use iced::time::Instant;
|
||||
@@ -206,6 +207,7 @@ impl OpenCADStudio {
|
||||
self.attr_editor_tab = crate::ui::window::attribute_editor::AttrTab::Attribute;
|
||||
}
|
||||
Some(GeometricTolerance) => self.geometric_tolerance = None,
|
||||
Some(BlockDefinition) => self.block_definition = None,
|
||||
Some(Hyperlink) => {
|
||||
self.hyperlink_editor_handles.clear();
|
||||
self.hyperlink_editor_url.clear();
|
||||
@@ -313,8 +315,34 @@ impl OpenCADStudio {
|
||||
{
|
||||
return self.update(Message::AliasEditorDraftCancel);
|
||||
}
|
||||
if self.active_modal == Some(super::ModalKind::BlockDefinition) {
|
||||
if let Some(ref mut state) = self.block_definition {
|
||||
if state.confirm_redefine.is_some() {
|
||||
state.confirm_redefine = None;
|
||||
return Task::none();
|
||||
}
|
||||
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) {
|
||||
if matches!(msg, Message::CommandFinalize)
|
||||
|| matches!(&msg, Message::ShortcutPressed(key) if key.rsplit('+').next() == Some("ENTER") || key.rsplit('+').next() == Some("RETURN"))
|
||||
{
|
||||
if let Some(ref state) = self.block_definition {
|
||||
if state.confirm_redefine.is_some() {
|
||||
// Default to "No" so an accidental Enter doesn't nuke a definition.
|
||||
return self.update(Message::BlockDefConfirmRedefine(false));
|
||||
} else {
|
||||
return self.update(Message::BlockDefApply);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if is_modal_blocked_key_msg(&msg) {
|
||||
return Task::none();
|
||||
}
|
||||
@@ -4213,6 +4241,12 @@ impl OpenCADStudio {
|
||||
return Task::none();
|
||||
}
|
||||
let description = self.hyperlink_editor_description.trim().to_owned();
|
||||
if let Some(block_def) = self.block_definition.as_mut() {
|
||||
block_def.hyperlink_url = url;
|
||||
block_def.hyperlink_desc = description;
|
||||
self.active_modal = Some(crate::app::ModalKind::BlockDefinition);
|
||||
return Task::none();
|
||||
}
|
||||
let values = if url.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -4236,6 +4270,12 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
Message::HyperlinkRemove => {
|
||||
if let Some(block_def) = self.block_definition.as_mut() {
|
||||
block_def.hyperlink_url.clear();
|
||||
block_def.hyperlink_desc.clear();
|
||||
self.active_modal = Some(crate::app::ModalKind::BlockDefinition);
|
||||
return Task::none();
|
||||
}
|
||||
let i = self.active_tab;
|
||||
let handles = self.hyperlink_editor_handles.clone();
|
||||
self.apply_property_op(i, "HYPERLINK", &handles, |app, handle| {
|
||||
@@ -4250,6 +4290,10 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
Message::HyperlinkCancel => {
|
||||
if self.block_definition.is_some() {
|
||||
self.active_modal = Some(crate::app::ModalKind::BlockDefinition);
|
||||
return Task::none();
|
||||
}
|
||||
self.close_active_modal();
|
||||
Task::none()
|
||||
}
|
||||
@@ -4606,6 +4650,189 @@ impl OpenCADStudio {
|
||||
);
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefName(name) => {
|
||||
if let Some(state) = self.block_definition.as_mut() {
|
||||
state.name = name;
|
||||
state.error_message = None;
|
||||
state.confirm_redefine = None;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefNameSelect(chosen) => {
|
||||
if let Some(state) = self.block_definition.as_mut() {
|
||||
state.name = chosen;
|
||||
state.error_message = None;
|
||||
state.confirm_redefine = None;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefBaseOnScreen(val) => {
|
||||
if let Some(state) = self.block_definition.as_mut() {
|
||||
state.base_point_specify_onscreen = val;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefPickPoint => {
|
||||
self.active_modal = None;
|
||||
let cmd = crate::modules::insert::create_block::BlockPickBasePointCommand;
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[self.active_tab].active_cmd = Some(Box::new(cmd));
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefBaseX(val) => {
|
||||
if let Some(state) = self.block_definition.as_mut() {
|
||||
state.base_point_x = val;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefBaseY(val) => {
|
||||
if let Some(state) = self.block_definition.as_mut() {
|
||||
state.base_point_y = val;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefBaseZ(val) => {
|
||||
if let Some(state) = self.block_definition.as_mut() {
|
||||
state.base_point_z = val;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefObjectsOnScreen(val) => {
|
||||
if let Some(state) = self.block_definition.as_mut() {
|
||||
state.objects_specify_onscreen = val;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefSelectObjects => {
|
||||
self.active_modal = None;
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
let cmd = SelectObjectsCommand::plain("BLOCK", "BLOCK_OBJECTS_GATHERED");
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[self.active_tab].active_cmd = Some(Box::new(cmd));
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefQuickSelect => {
|
||||
self.active_modal = None;
|
||||
self.on_qselect_open()
|
||||
}
|
||||
Message::BlockDefObjectMode(mode) => {
|
||||
if let Some(state) = self.block_definition.as_mut() {
|
||||
state.object_mode = mode;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefAnnotative(val) => {
|
||||
if let Some(state) = self.block_definition.as_mut() {
|
||||
state.annotative = val;
|
||||
if !val {
|
||||
state.match_orientation = false;
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefMatchOrientation(val) => {
|
||||
if let Some(state) = self.block_definition.as_mut() {
|
||||
state.match_orientation = val;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefScaleUniformly(val) => {
|
||||
if let Some(state) = self.block_definition.as_mut() {
|
||||
state.scale_uniformly = val;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefAllowExploding(val) => {
|
||||
if let Some(state) = self.block_definition.as_mut() {
|
||||
state.allow_exploding = val;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefUnit(unit) => {
|
||||
if let Some(state) = self.block_definition.as_mut() {
|
||||
state.unit = unit;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefDescription(desc) => {
|
||||
if let Some(state) = self.block_definition.as_mut() {
|
||||
state.description = desc;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefDescriptionAction(action) => {
|
||||
if let Some(state) = self.block_definition.as_mut() {
|
||||
state.description_content.perform(action);
|
||||
state.description = state.description_content.text();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefHyperlink => {
|
||||
if let Some(state) = self.block_definition.as_ref() {
|
||||
self.hyperlink_editor_url = state.hyperlink_url.clone();
|
||||
self.hyperlink_editor_description = state.hyperlink_desc.clone();
|
||||
self.hyperlink_editor_mixed = false;
|
||||
self.hyperlink_editor_dirty = false;
|
||||
self.hyperlink_editor_handles.clear();
|
||||
self.active_modal = Some(crate::app::ModalKind::Hyperlink);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefDismissError => {
|
||||
if let Some(state) = self.block_definition.as_mut() {
|
||||
state.error_message = None;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefHelp => {
|
||||
self.command_line.push_info(
|
||||
crate::t!("BLOCK creates a block definition from objects you select.").as_ref(),
|
||||
);
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockDefConfirmRedefine(confirmed) => {
|
||||
if !confirmed {
|
||||
if let Some(state) = self.block_definition.as_mut() {
|
||||
state.confirm_redefine = None;
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
self.commit_block_definition(true)
|
||||
}
|
||||
Message::BlockDefApply => {
|
||||
let Some(state) = self.block_definition.as_mut() else {
|
||||
return Task::none();
|
||||
};
|
||||
let name = state.name.trim().to_string();
|
||||
if name.is_empty() {
|
||||
state.error_message = Some(crate::t!("Block name cannot be empty.").into_owned());
|
||||
return Task::none();
|
||||
}
|
||||
if let Some(_ch) = state.invalid_name_char() {
|
||||
state.error_message = Some(
|
||||
crate::t!("Block name cannot contain: \\ / : * ? \" < > | = `").into_owned(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
if name.starts_with('*') {
|
||||
state.error_message =
|
||||
Some(crate::t!("Block name cannot start with '*'.").into_owned());
|
||||
return Task::none();
|
||||
}
|
||||
if !state.objects_specify_onscreen && 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();
|
||||
}
|
||||
let i = self.active_tab;
|
||||
if self.tabs[i].scene.document.block_records.get(&name).is_some() {
|
||||
state.confirm_redefine = Some(name);
|
||||
return Task::none();
|
||||
}
|
||||
self.commit_block_definition(false)
|
||||
}
|
||||
Message::ToleranceDialogField(field) => {
|
||||
if let Some(state) = self.geometric_tolerance.as_mut() {
|
||||
state.apply_field(field);
|
||||
@@ -5558,6 +5785,9 @@ impl OpenCADStudio {
|
||||
self.qselect_settings = Some((&state).into());
|
||||
}
|
||||
self.reset_modal_geometry();
|
||||
if self.block_definition.is_some() {
|
||||
self.active_modal = Some(super::ModalKind::BlockDefinition);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
@@ -5754,6 +5984,16 @@ impl OpenCADStudio {
|
||||
self.command_line
|
||||
.push_output(crate::tf!("QSELECT: {} object(s) selected.", matched).as_ref());
|
||||
self.refresh_properties();
|
||||
if let Some(ref mut block_def) = self.block_definition {
|
||||
block_def.selected_handles = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.into_iter()
|
||||
.map(|(h, _)| h)
|
||||
.collect();
|
||||
block_def.error_message = None;
|
||||
self.active_modal = Some(super::ModalKind::BlockDefinition);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ impl OpenCADStudio {
|
||||
Some(K::LayerStateManager) => crate::tr!("modal", "layer-state-manager"),
|
||||
Some(K::LayerTranslator) => crate::t!("Layer Translator").into_owned(),
|
||||
Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(),
|
||||
Some(K::BlockDefinition) => crate::t!("Block Definition").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(),
|
||||
@@ -499,6 +500,12 @@ impl OpenCADStudio {
|
||||
crate::ui::window::drawing_units::view_window(state, flow)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::BlockDefinition => {
|
||||
let state = self.block_definition.as_ref()?;
|
||||
sized_flow(ex, 580, 390, |flow| {
|
||||
crate::ui::window::block_definition::view_window(state, flow)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::GeometricTolerance => {
|
||||
let state = self.geometric_tolerance.as_ref()?;
|
||||
sized_flow(ex, 670, 530, |flow| {
|
||||
|
||||
Reference in new issue
Block a user