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:
Karim Jerbi committed 2026-09-22 17:05:15 +01:00
1 parent 889a19c362
commit 652f4d6bbc
19 files changed
+2316 -318

No files matched your search

+30
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+9 -9
View File
@@ -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
View File
@@ -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) {
+28
View File
@@ -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,
+3
View File
@@ -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();
}
{
+240
View File
@@ -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()
}
+7
View File
@@ -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| {