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
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Pick point">
|
||||||
|
<line x1="7" y1="2" x2="7" y2="12" stroke="#6DB7ED" stroke-width="1.75" stroke-linecap="round"/>
|
||||||
|
<line x1="2" y1="7" x2="12" y2="7" stroke="#6DB7ED" stroke-width="1.75" stroke-linecap="round"/>
|
||||||
|
<circle cx="7" cy="7" r="2" fill="#6DB7ED" stroke="none"/>
|
||||||
|
<path d="M 8.5 8.5 L 8.5 20.5 L 12.2 16.8 L 15.5 21.5 L 18 20 L 14.8 15.2 L 20.5 15.2 Z" fill="#1A1A1A" stroke="#B4B6B9" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 553 B |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Quick Select">
|
||||||
|
<rect x="12" y="7" width="9.5" height="13.5" rx="1.5" stroke="#B4B6B9" stroke-width="1.5" fill="none"/>
|
||||||
|
<line x1="14.5" y1="10.5" x2="19" y2="10.5" stroke="#B4B6B9" stroke-width="1.2" stroke-linecap="round"/>
|
||||||
|
<line x1="14.5" y1="13.5" x2="19" y2="13.5" stroke="#B4B6B9" stroke-width="1.2" stroke-linecap="round"/>
|
||||||
|
<line x1="14.5" y1="16.5" x2="17.5" y2="16.5" stroke="#B4B6B9" stroke-width="1.2" stroke-linecap="round"/>
|
||||||
|
<polygon points="10.5,2 5.5,9.5 9.5,9.5 8.5,15.5 14.5,8 10.5,8" fill="#6DB7ED" stroke="#1A1A1A" stroke-width="0.8" stroke-linejoin="round"/>
|
||||||
|
<path d="M 2.5 7 L 2.5 17.5 L 5.5 14.5 L 8 18.5 L 10 17.5 L 7.5 13.5 L 11 13.5 Z" fill="#1A1A1A" stroke="#B4B6B9" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 861 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Select objects">
|
||||||
|
<rect x="3" y="3" width="12" height="12" rx="1.5" stroke="#6DB7ED" stroke-width="1.6" stroke-dasharray="2.5 2" fill="#6DB7ED" fill-opacity="0.15"/>
|
||||||
|
<path d="M 9.5 9.5 L 9.5 21 L 13 17.5 L 16 22 L 18.5 20.5 L 15.5 16 L 21 16 Z" fill="#1A1A1A" stroke="#B4B6B9" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 438 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M12 2.5 L22.5 20.5 H1.5 Z" fill="#F0C040" stroke="#F9A825" stroke-width="1.5" stroke-linejoin="round"/>
|
||||||
|
<line x1="12" y1="8.5" x2="12" y2="14" stroke="#1A1A1A" stroke-width="2.2" stroke-linecap="round"/>
|
||||||
|
<circle cx="12" cy="17.5" r="1.2" fill="#1A1A1A"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 350 B |
@@ -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) => {
|
CmdResult::CommitHatch(hatch) => {
|
||||||
let label = self.history_label_from_active_cmd(i, "HATCH");
|
let label = self.history_label_from_active_cmd(i, "HATCH");
|
||||||
let pending = self.begin_undo(i, label, 1, true);
|
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());
|
return Some(Task::none());
|
||||||
}
|
}
|
||||||
|
|
||||||
let insert = match self.tabs[i].scene.document.get_entity(insert_handle) {
|
let block_name = match self.tabs[i].scene.document.get_entity(insert_handle) {
|
||||||
Some(acadrust::EntityType::Insert(ins)) => ins.clone(),
|
Some(acadrust::EntityType::Insert(ins)) => ins.block_name.clone(),
|
||||||
_ => {
|
_ => {
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_error(crate::t!("BEDIT: selected object is not a block reference.").as_ref());
|
.push_error(crate::t!("BEDIT: selected object is not a block reference.").as_ref());
|
||||||
@@ -509,13 +509,13 @@ impl OpenCADStudio {
|
|||||||
.scene
|
.scene
|
||||||
.document
|
.document
|
||||||
.block_records
|
.block_records
|
||||||
.get(&insert.block_name)
|
.get(&block_name)
|
||||||
{
|
{
|
||||||
Some(br) => (br.handle, br.flags.is_xref),
|
Some(br) => (br.handle, br.flags.is_xref),
|
||||||
None => {
|
None => {
|
||||||
self.command_line.push_error(crate::tf!(
|
self.command_line.push_error(crate::tf!(
|
||||||
"BEDIT: block \"{}\" not found.",
|
"BEDIT: block \"{}\" not found.",
|
||||||
insert.block_name
|
block_name
|
||||||
).as_ref());
|
).as_ref());
|
||||||
return Some(Task::none());
|
return Some(Task::none());
|
||||||
}
|
}
|
||||||
@@ -532,7 +532,7 @@ impl OpenCADStudio {
|
|||||||
{
|
{
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
return Some(Task::done(Message::BlockEditSwitch(
|
return Some(Task::done(Message::BlockEditSwitch(
|
||||||
insert.block_name.clone(),
|
block_name.clone(),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,7 +543,7 @@ impl OpenCADStudio {
|
|||||||
.scene
|
.scene
|
||||||
.document
|
.document
|
||||||
.block_records
|
.block_records
|
||||||
.get(&insert.block_name)
|
.get(&block_name)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
br.entity_handles
|
br.entity_handles
|
||||||
.iter()
|
.iter()
|
||||||
@@ -564,7 +564,7 @@ impl OpenCADStudio {
|
|||||||
.entities()
|
.entities()
|
||||||
.filter_map(|entity| match entity {
|
.filter_map(|entity| match entity {
|
||||||
acadrust::EntityType::Insert(reference)
|
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() =>
|
&& !reference.attributes.is_empty() =>
|
||||||
{
|
{
|
||||||
Some((
|
Some((
|
||||||
@@ -607,7 +607,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
self.tabs[i].scene.block_edit_block = Some(br_handle);
|
self.tabs[i].scene.block_edit_block = Some(br_handle);
|
||||||
self.tabs[i].block_edits.push(BlockEditSession {
|
self.tabs[i].block_edits.push(BlockEditSession {
|
||||||
block_name: insert.block_name.clone(),
|
block_name: block_name.clone(),
|
||||||
br_handle,
|
br_handle,
|
||||||
return_layout,
|
return_layout,
|
||||||
return_block,
|
return_block,
|
||||||
@@ -651,7 +651,7 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.command_line.push_info(crate::tf!(
|
self.command_line.push_info(crate::tf!(
|
||||||
"BEDIT: Editing block \"{}\". Use Save Block or Discard to finish.",
|
"BEDIT: Editing block \"{}\". Use Save Block or Discard to finish.",
|
||||||
insert.block_name
|
block_name
|
||||||
).as_ref());
|
).as_ref());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+49
-29
@@ -81,11 +81,26 @@ mod coordinate_parsing_tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn parses_all_coordinate_forms() {
|
fn parses_all_coordinate_forms() {
|
||||||
let close = |a: glam::DVec3, b: glam::DVec3| (a - b).length() < 1e-9;
|
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(
|
||||||
assert!(close(parse_coord("1,2,3").unwrap().0, glam::dvec3(1.0, 2.0, 3.0)));
|
parse_coord("1,2").unwrap().0,
|
||||||
assert!(close(parse_coord("10<90").unwrap().0, glam::dvec3(0.0, 10.0, 0.0)));
|
glam::dvec3(1.0, 2.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,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("@10<0").unwrap().1, CoordKind::Relative);
|
||||||
assert_eq!(parse_coord("#1,2").unwrap().1, CoordKind::Absolute);
|
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
|
/// 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
|
/// 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.
|
/// is just the transpose (the dot products in `to_ucs`); no matrix inversion.
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub(super) struct UcsXform {
|
pub(crate) struct UcsXform {
|
||||||
origin: glam::DVec3,
|
origin: glam::DVec3,
|
||||||
x: glam::DVec3,
|
x: glam::DVec3,
|
||||||
y: glam::DVec3,
|
y: glam::DVec3,
|
||||||
@@ -112,7 +127,7 @@ pub(super) struct UcsXform {
|
|||||||
|
|
||||||
impl UcsXform {
|
impl UcsXform {
|
||||||
/// Plain WCS — no active UCS.
|
/// Plain WCS — no active UCS.
|
||||||
pub(super) fn identity() -> Self {
|
pub(crate) fn identity() -> Self {
|
||||||
Self {
|
Self {
|
||||||
origin: glam::DVec3::ZERO,
|
origin: glam::DVec3::ZERO,
|
||||||
x: glam::DVec3::X,
|
x: glam::DVec3::X,
|
||||||
@@ -132,7 +147,12 @@ impl UcsXform {
|
|||||||
};
|
};
|
||||||
let z = x.cross(raw_y).normalize_or(x.cross(fallback_z).normalize());
|
let z = x.cross(raw_y).normalize_or(x.cross(fallback_z).normalize());
|
||||||
let y = z.cross(x).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 {
|
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
|
/// Full UCS-local → WCS transform, using `origin` as local zero while
|
||||||
/// retaining this UCS's orthonormal axes.
|
/// retaining this UCS's orthonormal axes.
|
||||||
pub(super) fn to_wcs_transform_at(
|
pub(crate) fn to_wcs_transform_at(&self, origin: glam::DVec3) -> acadrust::types::Transform {
|
||||||
&self,
|
|
||||||
origin: glam::DVec3,
|
|
||||||
) -> acadrust::types::Transform {
|
|
||||||
use acadrust::types::{Matrix4, Transform};
|
use acadrust::types::{Matrix4, Transform};
|
||||||
Transform::from_matrix(Matrix4 {
|
Transform::from_matrix(Matrix4 {
|
||||||
m: [
|
m: [
|
||||||
@@ -207,10 +224,7 @@ impl UcsXform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Full WCS → UCS-local transform, using `origin` as the local zero.
|
/// Full WCS → UCS-local transform, using `origin` as the local zero.
|
||||||
pub(super) fn to_ucs_transform_at(
|
pub(crate) fn to_ucs_transform_at(&self, origin: glam::DVec3) -> acadrust::types::Transform {
|
||||||
&self,
|
|
||||||
origin: glam::DVec3,
|
|
||||||
) -> acadrust::types::Transform {
|
|
||||||
use acadrust::types::{Matrix4, Transform};
|
use acadrust::types::{Matrix4, Transform};
|
||||||
Transform::from_matrix(Matrix4 {
|
Transform::from_matrix(Matrix4 {
|
||||||
m: [
|
m: [
|
||||||
@@ -326,7 +340,11 @@ pub(super) fn drafting_constrain(
|
|||||||
let radians = degrees.to_radians();
|
let radians = degrees.to_radians();
|
||||||
glam::DVec2::new(radians.cos(), radians.sin())
|
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 projected = direction * delta.dot(direction);
|
||||||
let c = glam::DVec3::new(b.x + projected.x, b.y + projected.y, p.z);
|
let c = glam::DVec3::new(b.x + projected.x, b.y + projected.y, p.z);
|
||||||
xf.to_wcs(c)
|
xf.to_wcs(c)
|
||||||
@@ -393,8 +411,7 @@ pub(super) fn polar_constrain_near(
|
|||||||
tol_px: f32,
|
tol_px: f32,
|
||||||
xf: &UcsXform,
|
xf: &UcsXform,
|
||||||
) -> glam::DVec3 {
|
) -> glam::DVec3 {
|
||||||
polar_constrain_if_near(pt, base, step_deg, view_rot, eye, bounds, tol_px, xf)
|
polar_constrain_if_near(pt, base, step_deg, view_rot, eye, bounds, tol_px, xf).unwrap_or(pt)
|
||||||
.unwrap_or(pt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hard axis lock (#312): the locked ray's direction — the nearest polar
|
/// 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();
|
let radians = degrees.to_radians();
|
||||||
glam::DVec2::new(radians.cos(), radians.sin())
|
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(a).abs() >= delta.dot(b).abs() {
|
||||||
let direction = if delta.dot(direction) < 0.0 { -direction } else { direction };
|
a
|
||||||
|
} else {
|
||||||
|
b
|
||||||
|
};
|
||||||
|
let direction = if delta.dot(direction) < 0.0 {
|
||||||
|
-direction
|
||||||
|
} else {
|
||||||
|
direction
|
||||||
|
};
|
||||||
direction.y.atan2(direction.x)
|
direction.y.atan2(direction.x)
|
||||||
};
|
};
|
||||||
let dir_ucs = glam::DVec3::new(ang.cos(), ang.sin(), 0.0);
|
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
|
/// 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
|
/// EVERYTHING, including an osnap hit, so a snap far off-axis contributes only
|
||||||
/// its along-axis component (#312).
|
/// its along-axis component (#312).
|
||||||
pub(super) fn axis_lock_apply(
|
pub(super) fn axis_lock_apply(pt: glam::DVec3, base: glam::DVec3, dir: glam::DVec3) -> glam::DVec3 {
|
||||||
pt: glam::DVec3,
|
|
||||||
base: glam::DVec3,
|
|
||||||
dir: glam::DVec3,
|
|
||||||
) -> glam::DVec3 {
|
|
||||||
base + dir * (pt - base).dot(dir)
|
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", …).
|
/// Generate the next available auto group name ("*A1", "*A2", …).
|
||||||
pub(super) fn next_group_auto_name(scene: &crate::scene::Scene) -> String {
|
pub(super) fn next_group_auto_name(scene: &crate::scene::Scene) -> String {
|
||||||
let existing: rustc_hash::FxHashSet<String> =
|
let existing: rustc_hash::FxHashSet<String> = scene.groups().map(|g| g.name.clone()).collect();
|
||||||
scene.groups().map(|g| g.name.clone()).collect();
|
|
||||||
for n in 1..=9999 {
|
for n in 1..=9999 {
|
||||||
let name = format!("*A{n}");
|
let name = format!("*A{n}");
|
||||||
if !existing.contains(&name) {
|
if !existing.contains(&name) {
|
||||||
|
|||||||
@@ -487,6 +487,8 @@ pub(super) struct OpenCADStudio {
|
|||||||
layer_translator: Option<crate::ui::window::layer_translator::State>,
|
layer_translator: Option<crate::ui::window::layer_translator::State>,
|
||||||
/// Working copy of the Drawing Units dialog; `None` while it is closed.
|
/// Working copy of the Drawing Units dialog; `None` while it is closed.
|
||||||
drawing_units: Option<crate::ui::window::drawing_units::State>,
|
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.
|
/// Working copy of the structured feature-control-frame editor.
|
||||||
geometric_tolerance: Option<crate::ui::window::geometric_tolerance::State>,
|
geometric_tolerance: Option<crate::ui::window::geometric_tolerance::State>,
|
||||||
/// PICKDRAG (#226): false (default) = press-drag lassoes; true =
|
/// PICKDRAG (#226): false (default) = press-drag lassoes; true =
|
||||||
@@ -1857,6 +1859,7 @@ pub enum ModalKind {
|
|||||||
LayerStateManager,
|
LayerStateManager,
|
||||||
LayerTranslator,
|
LayerTranslator,
|
||||||
DrawingUnits,
|
DrawingUnits,
|
||||||
|
BlockDefinition,
|
||||||
GeometricTolerance,
|
GeometricTolerance,
|
||||||
DraftingSettings,
|
DraftingSettings,
|
||||||
AutoConstrainSettings,
|
AutoConstrainSettings,
|
||||||
@@ -2830,6 +2833,30 @@ pub enum Message {
|
|||||||
DrawingUnitsField(crate::ui::window::drawing_units::Field),
|
DrawingUnitsField(crate::ui::window::drawing_units::Field),
|
||||||
/// Drawing Units OK — write the working copy into the drawing.
|
/// Drawing Units OK — write the working copy into the drawing.
|
||||||
DrawingUnitsApply,
|
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.
|
/// One structured feature-control-frame field changed.
|
||||||
ToleranceDialogField(crate::ui::window::geometric_tolerance::Field),
|
ToleranceDialogField(crate::ui::window::geometric_tolerance::Field),
|
||||||
/// One structured feature-control-frame option changed.
|
/// One structured feature-control-frame option changed.
|
||||||
@@ -3902,6 +3929,7 @@ impl OpenCADStudio {
|
|||||||
last_layer_translation: None,
|
last_layer_translation: None,
|
||||||
layer_translator: None,
|
layer_translator: None,
|
||||||
drawing_units: None,
|
drawing_units: None,
|
||||||
|
block_definition: None,
|
||||||
geometric_tolerance: None,
|
geometric_tolerance: None,
|
||||||
pick_drag_rect: false,
|
pick_drag_rect: false,
|
||||||
perf_hud: 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() {
|
if let Some(state) = self.qselect.take() {
|
||||||
self.qselect_settings = Some((&state).into());
|
self.qselect_settings = Some((&state).into());
|
||||||
self.reset_modal_geometry();
|
self.reset_modal_geometry();
|
||||||
|
if self.block_definition.is_some() {
|
||||||
|
self.active_modal = Some(crate::app::ModalKind::BlockDefinition);
|
||||||
|
}
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::{ArrowKey, Message, OpenCADStudio, TextEntryMode};
|
use super::{ArrowKey, Message, OpenCADStudio, TextEntryMode};
|
||||||
|
use crate::command::CadCommand;
|
||||||
use crate::scene::VIEWCUBE_DRAW_PX;
|
use crate::scene::VIEWCUBE_DRAW_PX;
|
||||||
use crate::ui::PropertiesPanel;
|
use crate::ui::PropertiesPanel;
|
||||||
use iced::time::Instant;
|
use iced::time::Instant;
|
||||||
@@ -206,6 +207,7 @@ impl OpenCADStudio {
|
|||||||
self.attr_editor_tab = crate::ui::window::attribute_editor::AttrTab::Attribute;
|
self.attr_editor_tab = crate::ui::window::attribute_editor::AttrTab::Attribute;
|
||||||
}
|
}
|
||||||
Some(GeometricTolerance) => self.geometric_tolerance = None,
|
Some(GeometricTolerance) => self.geometric_tolerance = None,
|
||||||
|
Some(BlockDefinition) => self.block_definition = None,
|
||||||
Some(Hyperlink) => {
|
Some(Hyperlink) => {
|
||||||
self.hyperlink_editor_handles.clear();
|
self.hyperlink_editor_handles.clear();
|
||||||
self.hyperlink_editor_url.clear();
|
self.hyperlink_editor_url.clear();
|
||||||
@@ -313,8 +315,34 @@ impl OpenCADStudio {
|
|||||||
{
|
{
|
||||||
return self.update(Message::AliasEditorDraftCancel);
|
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);
|
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) {
|
if is_modal_blocked_key_msg(&msg) {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
@@ -4213,6 +4241,12 @@ impl OpenCADStudio {
|
|||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
let description = self.hyperlink_editor_description.trim().to_owned();
|
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() {
|
let values = if url.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@@ -4236,6 +4270,12 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::HyperlinkRemove => {
|
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 i = self.active_tab;
|
||||||
let handles = self.hyperlink_editor_handles.clone();
|
let handles = self.hyperlink_editor_handles.clone();
|
||||||
self.apply_property_op(i, "HYPERLINK", &handles, |app, handle| {
|
self.apply_property_op(i, "HYPERLINK", &handles, |app, handle| {
|
||||||
@@ -4250,6 +4290,10 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::HyperlinkCancel => {
|
Message::HyperlinkCancel => {
|
||||||
|
if self.block_definition.is_some() {
|
||||||
|
self.active_modal = Some(crate::app::ModalKind::BlockDefinition);
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
self.close_active_modal();
|
self.close_active_modal();
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
@@ -4606,6 +4650,189 @@ impl OpenCADStudio {
|
|||||||
);
|
);
|
||||||
Task::none()
|
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) => {
|
Message::ToleranceDialogField(field) => {
|
||||||
if let Some(state) = self.geometric_tolerance.as_mut() {
|
if let Some(state) = self.geometric_tolerance.as_mut() {
|
||||||
state.apply_field(field);
|
state.apply_field(field);
|
||||||
@@ -5558,6 +5785,9 @@ impl OpenCADStudio {
|
|||||||
self.qselect_settings = Some((&state).into());
|
self.qselect_settings = Some((&state).into());
|
||||||
}
|
}
|
||||||
self.reset_modal_geometry();
|
self.reset_modal_geometry();
|
||||||
|
if self.block_definition.is_some() {
|
||||||
|
self.active_modal = Some(super::ModalKind::BlockDefinition);
|
||||||
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5754,6 +5984,16 @@ impl OpenCADStudio {
|
|||||||
self.command_line
|
self.command_line
|
||||||
.push_output(crate::tf!("QSELECT: {} object(s) selected.", matched).as_ref());
|
.push_output(crate::tf!("QSELECT: {} object(s) selected.", matched).as_ref());
|
||||||
self.refresh_properties();
|
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()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ impl OpenCADStudio {
|
|||||||
Some(K::LayerStateManager) => crate::tr!("modal", "layer-state-manager"),
|
Some(K::LayerStateManager) => crate::tr!("modal", "layer-state-manager"),
|
||||||
Some(K::LayerTranslator) => crate::t!("Layer Translator").into_owned(),
|
Some(K::LayerTranslator) => crate::t!("Layer Translator").into_owned(),
|
||||||
Some(K::DrawingUnits) => crate::t!("Drawing Units").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::GeometricTolerance) => crate::t!("Geometric Tolerance").into_owned(),
|
||||||
Some(K::DraftingSettings) => crate::t!("Drafting Settings").into_owned(),
|
Some(K::DraftingSettings) => crate::t!("Drafting Settings").into_owned(),
|
||||||
Some(K::AutoConstrainSettings) => crate::t!("Constraint 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)
|
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 => {
|
super::super::ModalKind::GeometricTolerance => {
|
||||||
let state = self.geometric_tolerance.as_ref()?;
|
let state = self.geometric_tolerance.as_ref()?;
|
||||||
sized_flow(ex, 670, 530, |flow| {
|
sized_flow(ex, 670, 530, |flow| {
|
||||||
|
|||||||
@@ -1520,6 +1520,10 @@ pub enum CmdResult {
|
|||||||
name: String,
|
name: String,
|
||||||
base: DVec3,
|
base: DVec3,
|
||||||
},
|
},
|
||||||
|
/// Create a block definition with full dialog options (Retain/Convert/Delete, units, annotative, etc.).
|
||||||
|
CreateBlockWithOptions {
|
||||||
|
options: Box<crate::scene::CreateBlockOptions>,
|
||||||
|
},
|
||||||
/// Apply a transform to selected entities and end the command.
|
/// Apply a transform to selected entities and end the command.
|
||||||
TransformSelected(Vec<Handle>, EntityTransform),
|
TransformSelected(Vec<Handle>, EntityTransform),
|
||||||
/// Copy selected entities with a transform; command stays active for more copies.
|
/// Copy selected entities with a transform; command stays active for more copies.
|
||||||
|
|||||||
@@ -98,11 +98,20 @@ pub fn explode_entity(entity: &EntityType, document: &CadDocument) -> Vec<Entity
|
|||||||
{
|
{
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
EntityType::Insert(ins) => ins
|
EntityType::Insert(ins) => {
|
||||||
.explode_from_document(document)
|
let explodable = document
|
||||||
.into_iter()
|
.block_records
|
||||||
.map(normalize_insert_entity)
|
.get(&ins.block_name)
|
||||||
.collect(),
|
.map_or(true, |br| br.explodable);
|
||||||
|
if !explodable {
|
||||||
|
vec![]
|
||||||
|
} else {
|
||||||
|
ins.explode_from_document(document)
|
||||||
|
.into_iter()
|
||||||
|
.map(normalize_insert_entity)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
EntityType::MLine(ml) => explode_mline(ml),
|
EntityType::MLine(ml) => explode_mline(ml),
|
||||||
EntityType::Dimension(dim) => explode_dimension(dim, document),
|
EntityType::Dimension(dim) => explode_dimension(dim, document),
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
@@ -1500,6 +1509,18 @@ mod tests {
|
|||||||
assert!(explode_entity(&EntityType::Insert(insert), &doc).is_empty());
|
assert!(explode_entity(&EntityType::Insert(insert), &doc).is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unexplodable_block_is_not_exploded() {
|
||||||
|
let mut doc = CadDocument::new();
|
||||||
|
let mut block = acadrust::tables::BlockRecord::new("NO_EXPLODE");
|
||||||
|
block.handle = doc.allocate_handle();
|
||||||
|
block.explodable = false;
|
||||||
|
doc.block_records.add(block).unwrap();
|
||||||
|
|
||||||
|
let insert = acadrust::entities::Insert::new("NO_EXPLODE", Vector3::new(0.0, 0.0, 0.0));
|
||||||
|
assert!(explode_entity(&EntityType::Insert(insert), &doc).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
/// A dimension created without a geometry block gets a real `*D` block on
|
/// A dimension created without a geometry block gets a real `*D` block on
|
||||||
/// bake, its `block_name` resolves to that block, and a second bake is a
|
/// bake, its `block_name` resolves to that block, and a second bake is a
|
||||||
/// no-op (already has a valid block).
|
/// no-op (already has a valid block).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
use crate::t;
|
||||||
use acadrust::Handle;
|
use acadrust::Handle;
|
||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
use crate::t;
|
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult};
|
use crate::command::{CadCommand, CmdResult};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
@@ -92,3 +92,560 @@ impl CadCommand for CreateBlockCommand {
|
|||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One-shot point picker for the Block Definition dialog's "Pick point" button.
|
||||||
|
pub struct BlockPickBasePointCommand;
|
||||||
|
|
||||||
|
impl CadCommand for BlockPickBasePointCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"BLOCK"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
crate::t!("BLOCK Specify insertion base point:").into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||||
|
CmdResult::Dispatch(format!("BLOCK_POINT_PICKED {} {} {}", pt.x, pt.y, pt.z))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
CmdResult::Dispatch("BLOCK_POINT_CANCELLED".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_escape(&mut self) -> CmdResult {
|
||||||
|
CmdResult::Dispatch("BLOCK_POINT_CANCELLED".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Command handling the on-screen prompting sequence when "Specify On-screen" is checked in the dialog.
|
||||||
|
/// Enforces standard command ordering: Base point is ALWAYS prompted before object selection.
|
||||||
|
pub struct BlockOnScreenCommand {
|
||||||
|
options: crate::scene::CreateBlockOptions,
|
||||||
|
ucs: crate::app::helpers::UcsXform,
|
||||||
|
need_base: bool,
|
||||||
|
need_objects: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlockOnScreenCommand {
|
||||||
|
pub fn new(
|
||||||
|
options: crate::scene::CreateBlockOptions,
|
||||||
|
ucs: crate::app::helpers::UcsXform,
|
||||||
|
need_base: bool,
|
||||||
|
need_objects: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
options,
|
||||||
|
ucs,
|
||||||
|
need_base,
|
||||||
|
need_objects,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for BlockOnScreenCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"BLOCK"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
if self.need_base {
|
||||||
|
crate::t!("BLOCK Specify insertion base point:").into_owned()
|
||||||
|
} else {
|
||||||
|
crate::t!("BLOCK Select objects:").into_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||||
|
if self.need_base {
|
||||||
|
self.options.base_point = pt;
|
||||||
|
self.options.world_to_block = self.ucs.to_ucs_transform_at(pt);
|
||||||
|
self.options.block_to_world = self.ucs.to_wcs_transform_at(pt);
|
||||||
|
self.need_base = false;
|
||||||
|
if self.need_objects {
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
} else {
|
||||||
|
CmdResult::CreateBlockWithOptions {
|
||||||
|
options: Box::new(self.options.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_selection_gathering(&self) -> bool {
|
||||||
|
!self.need_base && self.need_objects
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
|
||||||
|
self.options.handles = handles;
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
if self.need_base {
|
||||||
|
return CmdResult::Cancel;
|
||||||
|
}
|
||||||
|
if self.options.handles.is_empty() {
|
||||||
|
return CmdResult::Cancel;
|
||||||
|
}
|
||||||
|
CmdResult::CreateBlockWithOptions {
|
||||||
|
options: Box::new(self.options.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_escape(&mut self) -> CmdResult {
|
||||||
|
CmdResult::Cancel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::scene::{CreateBlockOptions, Scene};
|
||||||
|
use crate::ui::window::block_definition::BlockObjectMode;
|
||||||
|
use acadrust::entities::Line;
|
||||||
|
use acadrust::types::{Transform, Vector3};
|
||||||
|
|
||||||
|
fn make_test_scene() -> (Scene, Handle) {
|
||||||
|
let mut scene = Scene::new();
|
||||||
|
let mut line = Line::new();
|
||||||
|
line.start = Vector3::new(0.0, 0.0, 0.0);
|
||||||
|
line.end = Vector3::new(10.0, 10.0, 0.0);
|
||||||
|
let h = scene.add_entity(acadrust::EntityType::Line(line));
|
||||||
|
(scene, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_block_retain_mode() {
|
||||||
|
let (mut scene, line_h) = make_test_scene();
|
||||||
|
let options = CreateBlockOptions {
|
||||||
|
name: "BLK_RETAIN".to_string(),
|
||||||
|
handles: vec![line_h],
|
||||||
|
base_point: DVec3::ZERO,
|
||||||
|
world_to_block: Transform::identity(),
|
||||||
|
block_to_world: Transform::identity(),
|
||||||
|
mode: BlockObjectMode::Retain,
|
||||||
|
annotative: false,
|
||||||
|
match_orientation: false,
|
||||||
|
scale_uniformly: false,
|
||||||
|
allow_exploding: true,
|
||||||
|
unit: 0,
|
||||||
|
description: "Retain test".to_string(),
|
||||||
|
hyperlink_url: "".to_string(),
|
||||||
|
hyperlink_desc: "".to_string(),
|
||||||
|
redefine: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = scene.create_block_with_options(options);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(result.unwrap(), Handle::NULL); // No insert created in Retain mode
|
||||||
|
|
||||||
|
// Original line must still exist in document
|
||||||
|
assert!(scene.document.get_entity(line_h).is_some());
|
||||||
|
// Block definition must exist
|
||||||
|
assert!(scene.document.block_records.get("BLK_RETAIN").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_block_convert_mode() {
|
||||||
|
let (mut scene, line_h) = make_test_scene();
|
||||||
|
let options = CreateBlockOptions {
|
||||||
|
name: "BLK_CONVERT".to_string(),
|
||||||
|
handles: vec![line_h],
|
||||||
|
base_point: DVec3::ZERO,
|
||||||
|
world_to_block: Transform::identity(),
|
||||||
|
block_to_world: Transform::identity(),
|
||||||
|
mode: BlockObjectMode::Convert,
|
||||||
|
annotative: false,
|
||||||
|
match_orientation: false,
|
||||||
|
scale_uniformly: true,
|
||||||
|
allow_exploding: true,
|
||||||
|
unit: 4, // mm
|
||||||
|
description: "Convert test".to_string(),
|
||||||
|
hyperlink_url: "".to_string(),
|
||||||
|
hyperlink_desc: "".to_string(),
|
||||||
|
redefine: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = scene.create_block_with_options(options);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let insert_h = result.unwrap();
|
||||||
|
assert_ne!(insert_h, Handle::NULL);
|
||||||
|
|
||||||
|
// Original line was erased from model space
|
||||||
|
assert!(scene.document.get_entity(line_h).is_none());
|
||||||
|
// Insert entity exists in drawing
|
||||||
|
let insert_entity = scene.document.get_entity(insert_h);
|
||||||
|
assert!(insert_entity.is_some());
|
||||||
|
if let Some(acadrust::EntityType::Insert(ins)) = insert_entity {
|
||||||
|
assert_eq!(ins.block_name, "BLK_CONVERT");
|
||||||
|
} else {
|
||||||
|
panic!("Expected Insert entity");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check BlockRecord properties
|
||||||
|
let br = scene.document.block_records.get("BLK_CONVERT").unwrap();
|
||||||
|
assert_eq!(br.units, 4);
|
||||||
|
assert!(br.scale_uniformly);
|
||||||
|
assert!(br.explodable);
|
||||||
|
assert_eq!(br.description, "Convert test");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_block_delete_mode() {
|
||||||
|
let (mut scene, line_h) = make_test_scene();
|
||||||
|
let options = CreateBlockOptions {
|
||||||
|
name: "BLK_DELETE".to_string(),
|
||||||
|
handles: vec![line_h],
|
||||||
|
base_point: DVec3::ZERO,
|
||||||
|
world_to_block: Transform::identity(),
|
||||||
|
block_to_world: Transform::identity(),
|
||||||
|
mode: BlockObjectMode::Delete,
|
||||||
|
annotative: false,
|
||||||
|
match_orientation: false,
|
||||||
|
scale_uniformly: false,
|
||||||
|
allow_exploding: true,
|
||||||
|
unit: 0,
|
||||||
|
description: "".to_string(),
|
||||||
|
hyperlink_url: "".to_string(),
|
||||||
|
hyperlink_desc: "".to_string(),
|
||||||
|
redefine: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = scene.create_block_with_options(options);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(result.unwrap(), Handle::NULL);
|
||||||
|
|
||||||
|
// Original line erased
|
||||||
|
assert!(scene.document.get_entity(line_h).is_none());
|
||||||
|
// Block definition exists
|
||||||
|
assert!(scene.document.block_records.get("BLK_DELETE").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_block_redefinition() {
|
||||||
|
let (mut scene, line1_h) = make_test_scene();
|
||||||
|
let options1 = CreateBlockOptions {
|
||||||
|
name: "REDEF_BLOCK".to_string(),
|
||||||
|
handles: vec![line1_h],
|
||||||
|
base_point: DVec3::ZERO,
|
||||||
|
world_to_block: Transform::identity(),
|
||||||
|
block_to_world: Transform::identity(),
|
||||||
|
mode: BlockObjectMode::Retain,
|
||||||
|
annotative: false,
|
||||||
|
match_orientation: false,
|
||||||
|
scale_uniformly: false,
|
||||||
|
allow_exploding: true,
|
||||||
|
unit: 0,
|
||||||
|
description: "Initial".to_string(),
|
||||||
|
hyperlink_url: "".to_string(),
|
||||||
|
hyperlink_desc: "".to_string(),
|
||||||
|
redefine: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(scene.create_block_with_options(options1).is_ok());
|
||||||
|
|
||||||
|
// Attempting to create duplicate without redefine flag fails
|
||||||
|
let line2_h = {
|
||||||
|
let mut line2 = Line::new();
|
||||||
|
line2.start = Vector3::new(5.0, 5.0, 0.0);
|
||||||
|
line2.end = Vector3::new(15.0, 15.0, 0.0);
|
||||||
|
scene.add_entity(acadrust::EntityType::Line(line2))
|
||||||
|
};
|
||||||
|
|
||||||
|
let options2 = CreateBlockOptions {
|
||||||
|
name: "REDEF_BLOCK".to_string(),
|
||||||
|
handles: vec![line2_h],
|
||||||
|
base_point: DVec3::ZERO,
|
||||||
|
world_to_block: Transform::identity(),
|
||||||
|
block_to_world: Transform::identity(),
|
||||||
|
mode: BlockObjectMode::Retain,
|
||||||
|
annotative: false,
|
||||||
|
match_orientation: false,
|
||||||
|
scale_uniformly: false,
|
||||||
|
allow_exploding: true,
|
||||||
|
unit: 6, // meters
|
||||||
|
description: "Redefined".to_string(),
|
||||||
|
hyperlink_url: "".to_string(),
|
||||||
|
hyperlink_desc: "".to_string(),
|
||||||
|
redefine: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let err_res = scene.create_block_with_options(options2.clone());
|
||||||
|
assert!(err_res.is_err());
|
||||||
|
assert!(err_res.unwrap_err().contains("already exists"));
|
||||||
|
|
||||||
|
// Redefine with redefine: true succeeds and updates fields
|
||||||
|
let mut options3 = options2;
|
||||||
|
options3.redefine = true;
|
||||||
|
let redef_res = scene.create_block_with_options(options3);
|
||||||
|
assert!(redef_res.is_ok());
|
||||||
|
|
||||||
|
let br = scene.document.block_records.get("REDEF_BLOCK").unwrap();
|
||||||
|
assert_eq!(br.description, "Redefined");
|
||||||
|
assert_eq!(br.units, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_block_validation() {
|
||||||
|
let (mut scene, line_h) = make_test_scene();
|
||||||
|
|
||||||
|
// Empty name
|
||||||
|
let opt_empty_name = CreateBlockOptions {
|
||||||
|
name: " ".to_string(),
|
||||||
|
handles: vec![line_h],
|
||||||
|
base_point: DVec3::ZERO,
|
||||||
|
world_to_block: Transform::identity(),
|
||||||
|
block_to_world: Transform::identity(),
|
||||||
|
mode: BlockObjectMode::Retain,
|
||||||
|
annotative: false,
|
||||||
|
match_orientation: false,
|
||||||
|
scale_uniformly: false,
|
||||||
|
allow_exploding: true,
|
||||||
|
unit: 0,
|
||||||
|
description: "".to_string(),
|
||||||
|
hyperlink_url: "".to_string(),
|
||||||
|
hyperlink_desc: "".to_string(),
|
||||||
|
redefine: false,
|
||||||
|
};
|
||||||
|
assert!(scene.create_block_with_options(opt_empty_name).is_err());
|
||||||
|
|
||||||
|
// Anonymous '*' prefix blocked
|
||||||
|
let opt_star = CreateBlockOptions {
|
||||||
|
name: "*ANON".to_string(),
|
||||||
|
handles: vec![line_h],
|
||||||
|
base_point: DVec3::ZERO,
|
||||||
|
world_to_block: Transform::identity(),
|
||||||
|
block_to_world: Transform::identity(),
|
||||||
|
mode: BlockObjectMode::Retain,
|
||||||
|
annotative: false,
|
||||||
|
match_orientation: false,
|
||||||
|
scale_uniformly: false,
|
||||||
|
allow_exploding: true,
|
||||||
|
unit: 0,
|
||||||
|
description: "".to_string(),
|
||||||
|
hyperlink_url: "".to_string(),
|
||||||
|
hyperlink_desc: "".to_string(),
|
||||||
|
redefine: false,
|
||||||
|
};
|
||||||
|
assert!(scene.create_block_with_options(opt_star).is_err());
|
||||||
|
|
||||||
|
// Empty handles
|
||||||
|
let opt_no_handles = CreateBlockOptions {
|
||||||
|
name: "EMPTY_HANDLES".to_string(),
|
||||||
|
handles: vec![],
|
||||||
|
base_point: DVec3::ZERO,
|
||||||
|
world_to_block: Transform::identity(),
|
||||||
|
block_to_world: Transform::identity(),
|
||||||
|
mode: BlockObjectMode::Retain,
|
||||||
|
annotative: false,
|
||||||
|
match_orientation: false,
|
||||||
|
scale_uniformly: false,
|
||||||
|
allow_exploding: true,
|
||||||
|
unit: 0,
|
||||||
|
description: "".to_string(),
|
||||||
|
hyperlink_url: "".to_string(),
|
||||||
|
hyperlink_desc: "".to_string(),
|
||||||
|
redefine: false,
|
||||||
|
};
|
||||||
|
assert!(scene.create_block_with_options(opt_no_handles).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_block_on_screen_command_ordering() {
|
||||||
|
let dummy_options = CreateBlockOptions {
|
||||||
|
name: "ON_SCREEN_TEST".to_string(),
|
||||||
|
handles: vec![],
|
||||||
|
base_point: DVec3::ZERO,
|
||||||
|
world_to_block: Transform::identity(),
|
||||||
|
block_to_world: Transform::identity(),
|
||||||
|
mode: BlockObjectMode::Convert,
|
||||||
|
annotative: false,
|
||||||
|
match_orientation: false,
|
||||||
|
scale_uniformly: false,
|
||||||
|
allow_exploding: true,
|
||||||
|
unit: 0,
|
||||||
|
description: "".to_string(),
|
||||||
|
hyperlink_url: "".to_string(),
|
||||||
|
hyperlink_desc: "".to_string(),
|
||||||
|
redefine: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// When both base point and objects are to be specified on-screen:
|
||||||
|
let ucs = crate::app::helpers::UcsXform::identity();
|
||||||
|
let mut cmd = BlockOnScreenCommand::new(dummy_options, ucs, true, true);
|
||||||
|
|
||||||
|
// 1. Initial step must be insertion base point
|
||||||
|
assert!(cmd.prompt().contains("Specify insertion base point"));
|
||||||
|
|
||||||
|
// 2. Pick base point -> prompt switches to object selection
|
||||||
|
let picked_pt = DVec3::new(10.0, 20.0, 0.0);
|
||||||
|
let res1 = cmd.on_point(picked_pt);
|
||||||
|
assert!(matches!(res1, CmdResult::NeedPoint));
|
||||||
|
assert!(cmd.prompt().contains("Select objects"));
|
||||||
|
assert!(cmd.is_selection_gathering());
|
||||||
|
|
||||||
|
// 3. User attempts enter before selecting objects -> cancelled
|
||||||
|
assert!(matches!(cmd.on_enter(), CmdResult::Cancel));
|
||||||
|
|
||||||
|
// 4. Complete selection with handles
|
||||||
|
let fake_handle = Handle::new(42);
|
||||||
|
cmd.on_selection_complete(vec![fake_handle]);
|
||||||
|
|
||||||
|
// 5. User presses enter to finish selection
|
||||||
|
let finish = cmd.on_enter();
|
||||||
|
match finish {
|
||||||
|
CmdResult::CreateBlockWithOptions { options } => {
|
||||||
|
assert_eq!(options.name, "ON_SCREEN_TEST");
|
||||||
|
assert_eq!(options.base_point, picked_pt);
|
||||||
|
assert_eq!(options.handles, vec![fake_handle]);
|
||||||
|
assert_eq!(options.world_to_block, ucs.to_ucs_transform_at(picked_pt));
|
||||||
|
assert_eq!(options.block_to_world, ucs.to_wcs_transform_at(picked_pt));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected CreateBlockWithOptions"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_block_on_screen_base_point_sets_correct_transforms() {
|
||||||
|
let (mut scene, line_h) = make_test_scene();
|
||||||
|
let initial_options = CreateBlockOptions {
|
||||||
|
name: "ON_SCREEN_BASE_TEST".to_string(),
|
||||||
|
handles: vec![line_h],
|
||||||
|
base_point: DVec3::ZERO,
|
||||||
|
world_to_block: Transform::identity(),
|
||||||
|
block_to_world: Transform::identity(),
|
||||||
|
mode: BlockObjectMode::Convert,
|
||||||
|
annotative: false,
|
||||||
|
match_orientation: false,
|
||||||
|
scale_uniformly: false,
|
||||||
|
allow_exploding: true,
|
||||||
|
unit: 0,
|
||||||
|
description: "".to_string(),
|
||||||
|
hyperlink_url: "".to_string(),
|
||||||
|
hyperlink_desc: "".to_string(),
|
||||||
|
redefine: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// User picked objects upfront in dialog, but checked "Specify base point on screen"
|
||||||
|
let ucs = crate::app::helpers::UcsXform::identity();
|
||||||
|
let mut cmd = BlockOnScreenCommand::new(initial_options, ucs, true, false);
|
||||||
|
|
||||||
|
// Pick base point at (10, 10, 0) (end of the line in make_test_scene)
|
||||||
|
let pick_pt = DVec3::new(10.0, 10.0, 0.0);
|
||||||
|
let res = cmd.on_point(pick_pt);
|
||||||
|
|
||||||
|
let created_options = match res {
|
||||||
|
CmdResult::CreateBlockWithOptions { options } => *options,
|
||||||
|
_ => {
|
||||||
|
panic!("Expected CreateBlockWithOptions immediately when objects already selected")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(created_options.base_point, pick_pt);
|
||||||
|
assert_eq!(
|
||||||
|
created_options.world_to_block,
|
||||||
|
ucs.to_ucs_transform_at(pick_pt)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
created_options.block_to_world,
|
||||||
|
ucs.to_wcs_transform_at(pick_pt)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create the block with these options and verify insert position and definition entities
|
||||||
|
let insert_h = scene.create_block_with_options(created_options).unwrap();
|
||||||
|
assert_ne!(insert_h, Handle::NULL);
|
||||||
|
|
||||||
|
// In Convert mode, the insert entity must be placed at the picked base point
|
||||||
|
let insert_entity = scene.document.get_entity(insert_h).unwrap();
|
||||||
|
if let acadrust::EntityType::Insert(ins) = insert_entity {
|
||||||
|
assert_eq!(ins.block_name, "ON_SCREEN_BASE_TEST");
|
||||||
|
assert!((ins.insert_point.x - 10.0).abs() < 1e-6);
|
||||||
|
assert!((ins.insert_point.y - 10.0).abs() < 1e-6);
|
||||||
|
assert!((ins.insert_point.z - 0.0).abs() < 1e-6);
|
||||||
|
} else {
|
||||||
|
panic!("Expected Insert entity");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inside the block table record, the line entity must have been transformed relative to (10, 10, 0)
|
||||||
|
// Original line was (0,0,0) to (10,10,0), so inside the block it must be (-10,-10,0) to (0,0,0)
|
||||||
|
let br = scene
|
||||||
|
.document
|
||||||
|
.block_records
|
||||||
|
.get("ON_SCREEN_BASE_TEST")
|
||||||
|
.unwrap();
|
||||||
|
let block_line = scene
|
||||||
|
.document
|
||||||
|
.entities()
|
||||||
|
.find(|e| {
|
||||||
|
e.common().owner_handle == br.handle && matches!(e, acadrust::EntityType::Line(_))
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
if let acadrust::EntityType::Line(l) = block_line {
|
||||||
|
assert!((l.start.x - (-10.0)).abs() < 1e-6);
|
||||||
|
assert!((l.start.y - (-10.0)).abs() < 1e-6);
|
||||||
|
assert!((l.end.x - 0.0).abs() < 1e-6);
|
||||||
|
assert!((l.end.y - 0.0).abs() < 1e-6);
|
||||||
|
} else {
|
||||||
|
panic!("Expected Line entity inside block");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_explodable_flag_prevents_explode() {
|
||||||
|
let (mut scene, line_h) = make_test_scene();
|
||||||
|
let options = CreateBlockOptions {
|
||||||
|
name: "BLK_NO_EXPLODE".to_string(),
|
||||||
|
handles: vec![line_h],
|
||||||
|
base_point: DVec3::ZERO,
|
||||||
|
world_to_block: Transform::identity(),
|
||||||
|
block_to_world: Transform::identity(),
|
||||||
|
mode: BlockObjectMode::Convert,
|
||||||
|
annotative: false,
|
||||||
|
match_orientation: false,
|
||||||
|
scale_uniformly: false,
|
||||||
|
allow_exploding: false,
|
||||||
|
unit: 0,
|
||||||
|
description: "".to_string(),
|
||||||
|
hyperlink_url: "".to_string(),
|
||||||
|
hyperlink_desc: "".to_string(),
|
||||||
|
redefine: false,
|
||||||
|
};
|
||||||
|
let insert_h = scene.create_block_with_options(options).unwrap();
|
||||||
|
let insert = scene.document.get_entity(insert_h).unwrap();
|
||||||
|
let pieces = crate::modules::draw::modify::explode::explode_entity(insert, &scene.document);
|
||||||
|
assert!(pieces.is_empty(), "Unexplodable block must not explode");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_explodable_flag_allows_explode() {
|
||||||
|
let (mut scene, line_h) = make_test_scene();
|
||||||
|
let options = CreateBlockOptions {
|
||||||
|
name: "BLK_CAN_EXPLODE".to_string(),
|
||||||
|
handles: vec![line_h],
|
||||||
|
base_point: DVec3::ZERO,
|
||||||
|
world_to_block: Transform::identity(),
|
||||||
|
block_to_world: Transform::identity(),
|
||||||
|
mode: BlockObjectMode::Convert,
|
||||||
|
annotative: false,
|
||||||
|
match_orientation: false,
|
||||||
|
scale_uniformly: false,
|
||||||
|
allow_exploding: true,
|
||||||
|
unit: 0,
|
||||||
|
description: "".to_string(),
|
||||||
|
hyperlink_url: "".to_string(),
|
||||||
|
hyperlink_desc: "".to_string(),
|
||||||
|
redefine: false,
|
||||||
|
};
|
||||||
|
let insert_h = scene.create_block_with_options(options).unwrap();
|
||||||
|
let insert = scene.document.get_entity(insert_h).unwrap();
|
||||||
|
let pieces = crate::modules::draw::modify::explode::explode_entity(insert, &scene.document);
|
||||||
|
assert_eq!(
|
||||||
|
pieces.len(),
|
||||||
|
1,
|
||||||
|
"Explodable block should explode into 1 line"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+169
-42
@@ -1,6 +1,24 @@
|
|||||||
// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged.
|
|
||||||
use super::*;
|
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.
|
/// Order sampled boundary edges into one tip-to-tail loop.
|
||||||
pub(super) fn chain_path_edges(polys: Vec<Vec<[f64; 2]>>) -> Vec<[f64; 2]> {
|
pub(super) fn chain_path_edges(polys: Vec<Vec<[f64; 2]>>) -> Vec<[f64; 2]> {
|
||||||
chain_path_edges_with_directions(polys).0
|
chain_path_edges_with_directions(polys).0
|
||||||
@@ -845,18 +863,151 @@ impl Scene {
|
|||||||
world_to_block: &acadrust::types::Transform,
|
world_to_block: &acadrust::types::Transform,
|
||||||
block_to_world: &acadrust::types::Transform,
|
block_to_world: &acadrust::types::Transform,
|
||||||
) -> Result<Handle, String> {
|
) -> 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() {
|
if name.is_empty() {
|
||||||
return Err("Block name cannot be empty.".into());
|
return Err("Block name cannot be empty.".into());
|
||||||
}
|
}
|
||||||
if name.starts_with('*') {
|
if name.starts_with('*') {
|
||||||
return Err("Block name cannot start with '*'.".into());
|
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()
|
.iter()
|
||||||
.filter_map(|&h| self.document.get_entity(h).cloned().map(|e| (h, e)))
|
.filter_map(|&h| self.document.get_entity(h).cloned().map(|e| (h, e)))
|
||||||
.collect();
|
.collect();
|
||||||
@@ -864,35 +1015,7 @@ impl Scene {
|
|||||||
return Err("No valid entities selected for block creation.".into());
|
return Err("No valid entities selected for block creation.".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let next = self.document.next_handle();
|
let local = EntityTransform::Affine(options.world_to_block);
|
||||||
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);
|
|
||||||
for (old_handle, mut entity) in source_entities {
|
for (old_handle, mut entity) in source_entities {
|
||||||
view::dispatch::apply_transform(&mut entity, &local);
|
view::dispatch::apply_transform(&mut entity, &local);
|
||||||
entity = crate::modules::draw::modify::explode::normalize_entity_for_block(entity);
|
entity = crate::modules::draw::modify::explode::normalize_entity_for_block(entity);
|
||||||
@@ -901,15 +1024,19 @@ impl Scene {
|
|||||||
self.document
|
self.document
|
||||||
.add_entity(entity)
|
.add_entity(entity)
|
||||||
.map_err(|e| e.to_string())?;
|
.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);
|
let mut insert_handle = Handle::NULL;
|
||||||
acadrust::Entity::apply_transform(&mut insert, block_to_world);
|
if options.mode == crate::ui::window::block_definition::BlockObjectMode::Convert {
|
||||||
let insert_handle = self.add_entity(EntityType::Insert(insert));
|
let mut insert = DxfInsert::new(name, acadrust::types::Vector3::ZERO);
|
||||||
// A new block definition landed in the document; advance the block
|
acadrust::Entity::apply_transform(&mut insert, &options.block_to_world);
|
||||||
// epoch so consumers (the block palette stale check) notice it even
|
insert_handle = self.add_entity(EntityType::Insert(insert));
|
||||||
// when the panel stays open. Mirrors `define_block_from_owned_entities`.
|
}
|
||||||
|
|
||||||
|
// Advance the block epoch so all consumers and viewport renderers re-tessellate
|
||||||
self.bump_geometry();
|
self.bump_geometry();
|
||||||
Ok(insert_handle)
|
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};
|
pub use page_setup::{apply_default_page_setup, document_page_setups, rotated_margins};
|
||||||
mod dwg_native_constraints;
|
mod dwg_native_constraints;
|
||||||
mod entity;
|
mod entity;
|
||||||
|
pub use entity::CreateBlockOptions;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod hatch_boundary;
|
mod hatch_boundary;
|
||||||
mod group_layer;
|
mod group_layer;
|
||||||
|
|||||||
@@ -0,0 +1,696 @@
|
|||||||
|
//! Block Definition dialog window — create or redefine a block definition.
|
||||||
|
//! Prompted when the user runs the `BLOCK` or `BMAKE` command.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use acadrust::Handle;
|
||||||
|
use iced::widget::{
|
||||||
|
button, checkbox, column, combo_box, container, mouse_area, pick_list, radio, row, stack, text,
|
||||||
|
text_editor, 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::common::muted_style;
|
||||||
|
use crate::ui::style::form::{button_style, field_style};
|
||||||
|
|
||||||
|
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 BlockObjectMode {
|
||||||
|
Retain,
|
||||||
|
Convert,
|
||||||
|
Delete,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BlockObjectMode {
|
||||||
|
fn default() -> Self {
|
||||||
|
BlockObjectMode::Convert
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dropdown entry for the Block 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 Block Definition dialog's working copy.
|
||||||
|
pub struct BlockDefinitionState {
|
||||||
|
pub name: String,
|
||||||
|
pub existing_names: Vec<String>,
|
||||||
|
pub name_combo: combo_box::State<String>,
|
||||||
|
pub base_point_specify_onscreen: bool,
|
||||||
|
pub base_point_x: String,
|
||||||
|
pub base_point_y: String,
|
||||||
|
pub base_point_z: String,
|
||||||
|
pub objects_specify_onscreen: bool,
|
||||||
|
pub selected_handles: Vec<Handle>,
|
||||||
|
pub object_mode: BlockObjectMode,
|
||||||
|
pub annotative: bool,
|
||||||
|
pub match_orientation: bool,
|
||||||
|
pub scale_uniformly: bool,
|
||||||
|
pub allow_exploding: bool,
|
||||||
|
pub unit: i16,
|
||||||
|
pub description: String,
|
||||||
|
pub description_content: text_editor::Content,
|
||||||
|
pub hyperlink_url: String,
|
||||||
|
pub hyperlink_desc: String,
|
||||||
|
pub error_message: Option<String>,
|
||||||
|
pub confirm_redefine: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for BlockDefinitionState {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
name: self.name.clone(),
|
||||||
|
existing_names: self.existing_names.clone(),
|
||||||
|
name_combo: self.name_combo.clone(),
|
||||||
|
base_point_specify_onscreen: self.base_point_specify_onscreen,
|
||||||
|
base_point_x: self.base_point_x.clone(),
|
||||||
|
base_point_y: self.base_point_y.clone(),
|
||||||
|
base_point_z: self.base_point_z.clone(),
|
||||||
|
objects_specify_onscreen: self.objects_specify_onscreen,
|
||||||
|
selected_handles: self.selected_handles.clone(),
|
||||||
|
object_mode: self.object_mode,
|
||||||
|
annotative: self.annotative,
|
||||||
|
match_orientation: self.match_orientation,
|
||||||
|
scale_uniformly: self.scale_uniformly,
|
||||||
|
allow_exploding: self.allow_exploding,
|
||||||
|
unit: self.unit,
|
||||||
|
description: self.description.clone(),
|
||||||
|
description_content: text_editor::Content::with_text(&self.description),
|
||||||
|
hyperlink_url: self.hyperlink_url.clone(),
|
||||||
|
hyperlink_desc: self.hyperlink_desc.clone(),
|
||||||
|
error_message: self.error_message.clone(),
|
||||||
|
confirm_redefine: self.confirm_redefine.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlockDefinitionState {
|
||||||
|
pub fn new(existing_names: Vec<String>, pre_selected: Vec<Handle>, default_unit: i16) -> Self {
|
||||||
|
Self {
|
||||||
|
name: String::new(),
|
||||||
|
name_combo: combo_box::State::new(existing_names.clone()),
|
||||||
|
existing_names,
|
||||||
|
base_point_specify_onscreen: false,
|
||||||
|
base_point_x: "0.0000".to_string(),
|
||||||
|
base_point_y: "0.0000".to_string(),
|
||||||
|
base_point_z: "0.0000".to_string(),
|
||||||
|
objects_specify_onscreen: false,
|
||||||
|
selected_handles: pre_selected,
|
||||||
|
object_mode: BlockObjectMode::Convert,
|
||||||
|
annotative: false,
|
||||||
|
match_orientation: false,
|
||||||
|
scale_uniformly: false,
|
||||||
|
allow_exploding: true,
|
||||||
|
unit: default_unit,
|
||||||
|
description: String::new(),
|
||||||
|
description_content: text_editor::Content::new(),
|
||||||
|
hyperlink_url: String::new(),
|
||||||
|
hyperlink_desc: String::new(),
|
||||||
|
error_message: None,
|
||||||
|
confirm_redefine: 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_existing_name(&self) -> bool {
|
||||||
|
let trimmed = self.name.trim();
|
||||||
|
!trimmed.is_empty()
|
||||||
|
&& self
|
||||||
|
.existing_names
|
||||||
|
.iter()
|
||||||
|
.any(|existing| existing.eq_ignore_ascii_case(trimmed))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invalid_name_char(&self) -> Option<char> {
|
||||||
|
self.name.chars().find(|&c| {
|
||||||
|
matches!(
|
||||||
|
c,
|
||||||
|
'\\' | '/' | ':' | '*' | '?' | '"' | '<' | '>' | '|' | '=' | '`'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn labeled_checkbox<'a>(
|
||||||
|
label: impl Into<String>,
|
||||||
|
is_checked: bool,
|
||||||
|
on_toggle: impl Fn(bool) -> Message + 'a,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
row![
|
||||||
|
checkbox(is_checked).on_toggle(on_toggle).size(14),
|
||||||
|
text(label.into()).size(11),
|
||||||
|
]
|
||||||
|
.spacing(5)
|
||||||
|
.align_y(iced::Center)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_window<'a>(
|
||||||
|
state: &'a BlockDefinitionState,
|
||||||
|
sizing: crate::ui::modal::ModalSizing,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
// ── Name Row ─────────────────────────────────────────────────────────────
|
||||||
|
let name_combo = combo_box(
|
||||||
|
&state.name_combo,
|
||||||
|
t!("Block name").as_ref(),
|
||||||
|
if state.name.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(&state.name)
|
||||||
|
},
|
||||||
|
|chosen: String| Message::BlockDefNameSelect(chosen),
|
||||||
|
)
|
||||||
|
.on_input(Message::BlockDefName)
|
||||||
|
.size(11)
|
||||||
|
.padding([3, 6])
|
||||||
|
.width(Fill);
|
||||||
|
|
||||||
|
let name_hint: Option<Element<'a, Message>> = if let Some(invalid_char) =
|
||||||
|
state.invalid_name_char()
|
||||||
|
{
|
||||||
|
Some(
|
||||||
|
text(crate::tf!("Invalid character '{invalid_char}' in block name (\\ / : * ? \" < > | = ` are not allowed)"))
|
||||||
|
.size(10)
|
||||||
|
.style(|theme: &Theme| text::Style {
|
||||||
|
color: Some(theme.palette().danger.base.color),
|
||||||
|
})
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
} else if state.is_existing_name() {
|
||||||
|
Some(
|
||||||
|
text(crate::tf!(
|
||||||
|
"Block \"{}\" already exists — will redefine upon OK",
|
||||||
|
state.name.trim()
|
||||||
|
))
|
||||||
|
.size(10)
|
||||||
|
.style(|theme: &Theme| text::Style {
|
||||||
|
color: Some(theme.palette().primary.base.color),
|
||||||
|
})
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut name_column = column![text(t!("Name:")).size(11).style(muted_style), name_combo,]
|
||||||
|
.spacing(3)
|
||||||
|
.width(Fill);
|
||||||
|
|
||||||
|
if let Some(hint) = name_hint {
|
||||||
|
name_column = name_column.push(hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
let name_section = name_column;
|
||||||
|
|
||||||
|
// ── Group: Base Point ─────────────────────────────────────────────────────
|
||||||
|
let base_onscreen = labeled_checkbox(
|
||||||
|
t!("Specify On-screen"),
|
||||||
|
state.base_point_specify_onscreen,
|
||||||
|
Message::BlockDefBaseOnScreen,
|
||||||
|
);
|
||||||
|
|
||||||
|
let pick_pt_icon = crate::ui::icons::semantic(ICON_PICK_POINT, 15.0);
|
||||||
|
|
||||||
|
let mut pick_pt_btn = button(
|
||||||
|
row![pick_pt_icon, text(t!("Pick point")).size(11)]
|
||||||
|
.spacing(5)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
)
|
||||||
|
.padding([3, 8]);
|
||||||
|
|
||||||
|
if !state.base_point_specify_onscreen {
|
||||||
|
pick_pt_btn = pick_pt_btn
|
||||||
|
.on_press(Message::BlockDefPickPoint)
|
||||||
|
.style(button_style(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
let coord_row = |axis: &'static str, val: &'a str, on_input: fn(String) -> Message| {
|
||||||
|
let is_enabled = !state.base_point_specify_onscreen;
|
||||||
|
let input: Element<'a, Message> = if is_enabled {
|
||||||
|
text_input("", val)
|
||||||
|
.on_input(on_input)
|
||||||
|
.on_submit(Message::BlockDefApply)
|
||||||
|
.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(muted_style)
|
||||||
|
.width(Length::Fixed(16.0)),
|
||||||
|
input,
|
||||||
|
]
|
||||||
|
.spacing(4)
|
||||||
|
.align_y(iced::Center)
|
||||||
|
};
|
||||||
|
|
||||||
|
let base_point_group = group(
|
||||||
|
t!("Base point").into_owned(),
|
||||||
|
column![
|
||||||
|
base_onscreen,
|
||||||
|
Space::new().height(1),
|
||||||
|
pick_pt_btn,
|
||||||
|
Space::new().height(2),
|
||||||
|
coord_row("X:", &state.base_point_x, Message::BlockDefBaseX),
|
||||||
|
coord_row("Y:", &state.base_point_y, Message::BlockDefBaseY),
|
||||||
|
coord_row("Z:", &state.base_point_z, Message::BlockDefBaseZ),
|
||||||
|
]
|
||||||
|
.spacing(3),
|
||||||
|
Length::Fixed(152.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Group: Objects ────────────────────────────────────────────────────────
|
||||||
|
let objects_onscreen = labeled_checkbox(
|
||||||
|
t!("Specify On-screen"),
|
||||||
|
state.objects_specify_onscreen,
|
||||||
|
Message::BlockDefObjectsOnScreen,
|
||||||
|
);
|
||||||
|
|
||||||
|
let sel_obj_icon = crate::ui::icons::semantic(ICON_SELECT_OBJECTS, 15.0);
|
||||||
|
|
||||||
|
let mut sel_obj_btn = button(
|
||||||
|
row![sel_obj_icon, text(t!("Select objects")).size(11)]
|
||||||
|
.spacing(5)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
)
|
||||||
|
.padding([3, 8]);
|
||||||
|
|
||||||
|
if !state.objects_specify_onscreen {
|
||||||
|
sel_obj_btn = sel_obj_btn
|
||||||
|
.on_press(Message::BlockDefSelectObjects)
|
||||||
|
.style(button_style(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
let qsel_icon = crate::ui::icons::semantic(ICON_QUICK_SELECT, 15.0);
|
||||||
|
let mut qsel_btn = button(qsel_icon).padding([3, 5]).style(button_style(false));
|
||||||
|
|
||||||
|
if !state.objects_specify_onscreen {
|
||||||
|
qsel_btn = qsel_btn.on_press(Message::BlockDefQuickSelect);
|
||||||
|
}
|
||||||
|
|
||||||
|
let btns_row = row![sel_obj_btn, qsel_btn].spacing(5).align_y(iced::Center);
|
||||||
|
|
||||||
|
let radios = column![
|
||||||
|
radio(
|
||||||
|
t!("Retain"),
|
||||||
|
BlockObjectMode::Retain,
|
||||||
|
Some(state.object_mode),
|
||||||
|
Message::BlockDefObjectMode,
|
||||||
|
)
|
||||||
|
.size(15)
|
||||||
|
.spacing(5)
|
||||||
|
.text_size(11),
|
||||||
|
radio(
|
||||||
|
t!("Convert to block"),
|
||||||
|
BlockObjectMode::Convert,
|
||||||
|
Some(state.object_mode),
|
||||||
|
Message::BlockDefObjectMode,
|
||||||
|
)
|
||||||
|
.size(15)
|
||||||
|
.spacing(5)
|
||||||
|
.text_size(11),
|
||||||
|
radio(
|
||||||
|
t!("Delete"),
|
||||||
|
BlockObjectMode::Delete,
|
||||||
|
Some(state.object_mode),
|
||||||
|
Message::BlockDefObjectMode,
|
||||||
|
)
|
||||||
|
.size(15)
|
||||||
|
.spacing(5)
|
||||||
|
.text_size(11),
|
||||||
|
]
|
||||||
|
.spacing(2);
|
||||||
|
|
||||||
|
let count = state.selected_handles.len();
|
||||||
|
let status_row: Element<'a, Message> = 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().background.strongest.text.scale_alpha(0.75)),
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
.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_group = group(
|
||||||
|
t!("Objects").into_owned(),
|
||||||
|
column![
|
||||||
|
objects_onscreen,
|
||||||
|
Space::new().height(1),
|
||||||
|
btns_row,
|
||||||
|
Space::new().height(2),
|
||||||
|
radios,
|
||||||
|
Space::new().height(2),
|
||||||
|
status_row,
|
||||||
|
]
|
||||||
|
.spacing(2),
|
||||||
|
Length::Fixed(152.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Group: Behavior ───────────────────────────────────────────────────────
|
||||||
|
let annotative_cb = labeled_checkbox(
|
||||||
|
t!("Annotative"),
|
||||||
|
state.annotative,
|
||||||
|
Message::BlockDefAnnotative,
|
||||||
|
);
|
||||||
|
|
||||||
|
let match_orient_cb: Element<'a, Message> = if state.annotative {
|
||||||
|
row![
|
||||||
|
Space::new().width(16),
|
||||||
|
checkbox(state.match_orientation)
|
||||||
|
.on_toggle(Message::BlockDefMatchOrientation)
|
||||||
|
.size(13),
|
||||||
|
column![
|
||||||
|
text(t!("Match block orientation")).size(10),
|
||||||
|
text(t!("to layout")).size(10),
|
||||||
|
]
|
||||||
|
.spacing(1),
|
||||||
|
]
|
||||||
|
.spacing(5)
|
||||||
|
.align_y(iced::Center)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
row![
|
||||||
|
Space::new().width(16),
|
||||||
|
checkbox(false).size(13),
|
||||||
|
column![
|
||||||
|
text(t!("Match block orientation"))
|
||||||
|
.size(10)
|
||||||
|
.style(muted_style),
|
||||||
|
text(t!("to layout")).size(10).style(muted_style),
|
||||||
|
]
|
||||||
|
.spacing(1),
|
||||||
|
]
|
||||||
|
.spacing(5)
|
||||||
|
.align_y(iced::Center)
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let scale_uniform_cb = labeled_checkbox(
|
||||||
|
t!("Scale uniformly"),
|
||||||
|
state.scale_uniformly,
|
||||||
|
Message::BlockDefScaleUniformly,
|
||||||
|
);
|
||||||
|
|
||||||
|
let allow_explode_cb = labeled_checkbox(
|
||||||
|
t!("Allow exploding"),
|
||||||
|
state.allow_exploding,
|
||||||
|
Message::BlockDefAllowExploding,
|
||||||
|
);
|
||||||
|
|
||||||
|
let behavior_group = group(
|
||||||
|
t!("Behavior").into_owned(),
|
||||||
|
column![
|
||||||
|
annotative_cb,
|
||||||
|
Space::new().height(2),
|
||||||
|
match_orient_cb,
|
||||||
|
Space::new().height(4),
|
||||||
|
scale_uniform_cb,
|
||||||
|
Space::new().height(4),
|
||||||
|
allow_explode_cb,
|
||||||
|
]
|
||||||
|
.spacing(3),
|
||||||
|
Length::Fixed(152.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Group: Settings (Column 1 Bottom) ─────────────────────────────────────
|
||||||
|
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::BlockDefUnit(choice.code))
|
||||||
|
.text_size(11)
|
||||||
|
.padding([2, 5])
|
||||||
|
.width(Fill);
|
||||||
|
|
||||||
|
let hyperlink_btn = button(
|
||||||
|
text(if state.hyperlink_url.is_empty() {
|
||||||
|
t!("Hyperlink...")
|
||||||
|
} else {
|
||||||
|
t!("Hyperlink*")
|
||||||
|
})
|
||||||
|
.size(11),
|
||||||
|
)
|
||||||
|
.on_press(Message::BlockDefHyperlink)
|
||||||
|
.style(button_style(false))
|
||||||
|
.padding([3, 14]);
|
||||||
|
|
||||||
|
let settings_group = group(
|
||||||
|
t!("Settings").into_owned(),
|
||||||
|
column![
|
||||||
|
text(t!("Block unit:")).size(10).style(muted_style),
|
||||||
|
unit_picker,
|
||||||
|
Space::new().height(6),
|
||||||
|
row![
|
||||||
|
Space::new().width(Fill),
|
||||||
|
hyperlink_btn,
|
||||||
|
Space::new().width(Fill)
|
||||||
|
],
|
||||||
|
]
|
||||||
|
.spacing(3),
|
||||||
|
Length::Fixed(102.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Group: Description (Column 2 Bottom) ──────────────────────────────────
|
||||||
|
let desc_editor = text_editor(&state.description_content)
|
||||||
|
.on_action(Message::BlockDefDescriptionAction)
|
||||||
|
.placeholder("Enter block description...")
|
||||||
|
.size(11)
|
||||||
|
.padding([4, 6])
|
||||||
|
.height(Fill)
|
||||||
|
.style(|theme: &Theme, status| {
|
||||||
|
let palette = theme.palette();
|
||||||
|
let border_color = match status {
|
||||||
|
text_editor::Status::Focused { .. } => palette.primary.base.color,
|
||||||
|
_ => palette.background.neutral.color,
|
||||||
|
};
|
||||||
|
text_editor::Style {
|
||||||
|
background: Background::Color(palette.background.base.color),
|
||||||
|
border: Border {
|
||||||
|
color: border_color,
|
||||||
|
width: 1.0,
|
||||||
|
radius: 3.0.into(),
|
||||||
|
},
|
||||||
|
placeholder: palette.background.base.text.scale_alpha(0.48),
|
||||||
|
value: palette.background.base.text,
|
||||||
|
selection: palette.primary.base.color.scale_alpha(0.5),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let description_group = group(
|
||||||
|
t!("Description").into_owned(),
|
||||||
|
desc_editor,
|
||||||
|
Length::Fixed(102.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Layout Assembly: Left Column & Right Column ───────────────────────────
|
||||||
|
let left_column = column![base_point_group, settings_group,]
|
||||||
|
.spacing(6)
|
||||||
|
.width(Length::FillPortion(1));
|
||||||
|
|
||||||
|
let top_right_row = row![objects_group, behavior_group,].spacing(6).width(Fill);
|
||||||
|
|
||||||
|
let right_column = column![top_right_row, description_group,]
|
||||||
|
.spacing(6)
|
||||||
|
.width(Length::FillPortion(2));
|
||||||
|
|
||||||
|
let main_body = row![left_column, right_column].spacing(6).width(Fill);
|
||||||
|
|
||||||
|
// ── 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]),
|
||||||
|
]
|
||||||
|
.spacing(6)
|
||||||
|
.align_y(iced::Center);
|
||||||
|
|
||||||
|
// ── Error Banner ──────────────────────────────────────────────────────────
|
||||||
|
let mut main_column = column![name_section, main_body, actions,]
|
||||||
|
.spacing(6)
|
||||||
|
.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::BlockDefDismissError)
|
||||||
|
.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(8)
|
||||||
|
.width(sizing.width)
|
||||||
|
.padding(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
let main_content = main_column.into();
|
||||||
|
|
||||||
|
// ── Confirm Redefinition Guard ────────────────────────────────────────────
|
||||||
|
if let Some(name) = &state.confirm_redefine {
|
||||||
|
let question = crate::tf!("Block \"{name}\" already exists.\nDo you want to redefine it?");
|
||||||
|
let shield = mouse_area(
|
||||||
|
container(Space::new())
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.style(|theme: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(
|
||||||
|
theme.palette().background.strongest.color.scale_alpha(0.55),
|
||||||
|
)),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.on_press(Message::BlockDefConfirmRedefine(false));
|
||||||
|
|
||||||
|
let panel = container(
|
||||||
|
column![
|
||||||
|
text(question)
|
||||||
|
.size(13)
|
||||||
|
.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)),
|
||||||
|
// Default / safe focus on "No"
|
||||||
|
button(text(t!("No")).size(12))
|
||||||
|
.on_press(Message::BlockDefConfirmRedefine(false))
|
||||||
|
.padding([6, 22])
|
||||||
|
.style(button_style(true)),
|
||||||
|
]
|
||||||
|
.spacing(12)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
]
|
||||||
|
.spacing(0)
|
||||||
|
.align_x(iced::alignment::Horizontal::Center),
|
||||||
|
)
|
||||||
|
.padding([20, 24])
|
||||||
|
.style(container::rounded_box);
|
||||||
|
|
||||||
|
return stack![
|
||||||
|
main_content,
|
||||||
|
shield,
|
||||||
|
container(panel)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.center_x(Fill)
|
||||||
|
.center_y(Fill),
|
||||||
|
]
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
main_content
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod about;
|
pub mod about;
|
||||||
|
pub mod block_definition;
|
||||||
pub mod block_palette;
|
pub mod block_palette;
|
||||||
pub mod browser;
|
pub mod browser;
|
||||||
pub mod layout_manager;
|
pub mod layout_manager;
|
||||||
|
|||||||
Reference in new issue
Block a user