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
+49
-29
@@ -81,11 +81,26 @@ mod coordinate_parsing_tests {
|
||||
#[test]
|
||||
fn parses_all_coordinate_forms() {
|
||||
let close = |a: glam::DVec3, b: glam::DVec3| (a - b).length() < 1e-9;
|
||||
assert!(close(parse_coord("1,2").unwrap().0, glam::dvec3(1.0, 2.0, 0.0)));
|
||||
assert!(close(parse_coord("1,2,3").unwrap().0, glam::dvec3(1.0, 2.0, 3.0)));
|
||||
assert!(close(parse_coord("10<90").unwrap().0, glam::dvec3(0.0, 10.0, 0.0)));
|
||||
assert!(close(parse_coord("10<90,4").unwrap().0, glam::dvec3(0.0, 10.0, 4.0)));
|
||||
assert!(close(parse_coord("10<0<30").unwrap().0, glam::dvec3(5.0 * 3.0_f64.sqrt(), 0.0, 5.0)));
|
||||
assert!(close(
|
||||
parse_coord("1,2").unwrap().0,
|
||||
glam::dvec3(1.0, 2.0, 0.0)
|
||||
));
|
||||
assert!(close(
|
||||
parse_coord("1,2,3").unwrap().0,
|
||||
glam::dvec3(1.0, 2.0, 3.0)
|
||||
));
|
||||
assert!(close(
|
||||
parse_coord("10<90").unwrap().0,
|
||||
glam::dvec3(0.0, 10.0, 0.0)
|
||||
));
|
||||
assert!(close(
|
||||
parse_coord("10<90,4").unwrap().0,
|
||||
glam::dvec3(0.0, 10.0, 4.0)
|
||||
));
|
||||
assert!(close(
|
||||
parse_coord("10<0<30").unwrap().0,
|
||||
glam::dvec3(5.0 * 3.0_f64.sqrt(), 0.0, 5.0)
|
||||
));
|
||||
assert_eq!(parse_coord("@10<0").unwrap().1, CoordKind::Relative);
|
||||
assert_eq!(parse_coord("#1,2").unwrap().1, CoordKind::Absolute);
|
||||
}
|
||||
@@ -102,8 +117,8 @@ mod coordinate_parsing_tests {
|
||||
/// the UCS icon, snap/ortho, the ViewCube — goes through this one type instead
|
||||
/// of re-deriving the axis math. Axes are orthonormal, so the inverse rotation
|
||||
/// is just the transpose (the dot products in `to_ucs`); no matrix inversion.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct UcsXform {
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct UcsXform {
|
||||
origin: glam::DVec3,
|
||||
x: glam::DVec3,
|
||||
y: glam::DVec3,
|
||||
@@ -112,7 +127,7 @@ pub(super) struct UcsXform {
|
||||
|
||||
impl UcsXform {
|
||||
/// Plain WCS — no active UCS.
|
||||
pub(super) fn identity() -> Self {
|
||||
pub(crate) fn identity() -> Self {
|
||||
Self {
|
||||
origin: glam::DVec3::ZERO,
|
||||
x: glam::DVec3::X,
|
||||
@@ -132,7 +147,12 @@ impl UcsXform {
|
||||
};
|
||||
let z = x.cross(raw_y).normalize_or(x.cross(fallback_z).normalize());
|
||||
let y = z.cross(x).normalize();
|
||||
Self { origin: v(ucs.origin), x, y, z }
|
||||
Self {
|
||||
origin: v(ucs.origin),
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn from_active(ucs: Option<&Ucs>) -> Self {
|
||||
@@ -191,10 +211,7 @@ impl UcsXform {
|
||||
|
||||
/// Full UCS-local → WCS transform, using `origin` as local zero while
|
||||
/// retaining this UCS's orthonormal axes.
|
||||
pub(super) fn to_wcs_transform_at(
|
||||
&self,
|
||||
origin: glam::DVec3,
|
||||
) -> acadrust::types::Transform {
|
||||
pub(crate) fn to_wcs_transform_at(&self, origin: glam::DVec3) -> acadrust::types::Transform {
|
||||
use acadrust::types::{Matrix4, Transform};
|
||||
Transform::from_matrix(Matrix4 {
|
||||
m: [
|
||||
@@ -207,10 +224,7 @@ impl UcsXform {
|
||||
}
|
||||
|
||||
/// Full WCS → UCS-local transform, using `origin` as the local zero.
|
||||
pub(super) fn to_ucs_transform_at(
|
||||
&self,
|
||||
origin: glam::DVec3,
|
||||
) -> acadrust::types::Transform {
|
||||
pub(crate) fn to_ucs_transform_at(&self, origin: glam::DVec3) -> acadrust::types::Transform {
|
||||
use acadrust::types::{Matrix4, Transform};
|
||||
Transform::from_matrix(Matrix4 {
|
||||
m: [
|
||||
@@ -326,7 +340,11 @@ pub(super) fn drafting_constrain(
|
||||
let radians = degrees.to_radians();
|
||||
glam::DVec2::new(radians.cos(), radians.sin())
|
||||
});
|
||||
let direction = if delta.dot(a).abs() >= delta.dot(c).abs() { a } else { c };
|
||||
let direction = if delta.dot(a).abs() >= delta.dot(c).abs() {
|
||||
a
|
||||
} else {
|
||||
c
|
||||
};
|
||||
let projected = direction * delta.dot(direction);
|
||||
let c = glam::DVec3::new(b.x + projected.x, b.y + projected.y, p.z);
|
||||
xf.to_wcs(c)
|
||||
@@ -393,8 +411,7 @@ pub(super) fn polar_constrain_near(
|
||||
tol_px: f32,
|
||||
xf: &UcsXform,
|
||||
) -> glam::DVec3 {
|
||||
polar_constrain_if_near(pt, base, step_deg, view_rot, eye, bounds, tol_px, xf)
|
||||
.unwrap_or(pt)
|
||||
polar_constrain_if_near(pt, base, step_deg, view_rot, eye, bounds, tol_px, xf).unwrap_or(pt)
|
||||
}
|
||||
|
||||
/// Hard axis lock (#312): the locked ray's direction — the nearest polar
|
||||
@@ -427,8 +444,16 @@ pub(super) fn axis_lock_capture(
|
||||
let radians = degrees.to_radians();
|
||||
glam::DVec2::new(radians.cos(), radians.sin())
|
||||
});
|
||||
let direction = if delta.dot(a).abs() >= delta.dot(b).abs() { a } else { b };
|
||||
let direction = if delta.dot(direction) < 0.0 { -direction } else { direction };
|
||||
let direction = if delta.dot(a).abs() >= delta.dot(b).abs() {
|
||||
a
|
||||
} else {
|
||||
b
|
||||
};
|
||||
let direction = if delta.dot(direction) < 0.0 {
|
||||
-direction
|
||||
} else {
|
||||
direction
|
||||
};
|
||||
direction.y.atan2(direction.x)
|
||||
};
|
||||
let dir_ucs = glam::DVec3::new(ang.cos(), ang.sin(), 0.0);
|
||||
@@ -439,11 +464,7 @@ pub(super) fn axis_lock_capture(
|
||||
/// Project `pt` onto the locked ray through `base` — the hard lock applies to
|
||||
/// EVERYTHING, including an osnap hit, so a snap far off-axis contributes only
|
||||
/// its along-axis component (#312).
|
||||
pub(super) fn axis_lock_apply(
|
||||
pt: glam::DVec3,
|
||||
base: glam::DVec3,
|
||||
dir: glam::DVec3,
|
||||
) -> glam::DVec3 {
|
||||
pub(super) fn axis_lock_apply(pt: glam::DVec3, base: glam::DVec3, dir: glam::DVec3) -> glam::DVec3 {
|
||||
base + dir * (pt - base).dot(dir)
|
||||
}
|
||||
|
||||
@@ -481,8 +502,7 @@ pub(super) fn entities_lower_left_by_bbox(
|
||||
|
||||
/// Generate the next available auto group name ("*A1", "*A2", …).
|
||||
pub(super) fn next_group_auto_name(scene: &crate::scene::Scene) -> String {
|
||||
let existing: rustc_hash::FxHashSet<String> =
|
||||
scene.groups().map(|g| g.name.clone()).collect();
|
||||
let existing: rustc_hash::FxHashSet<String> = scene.groups().map(|g| g.name.clone()).collect();
|
||||
for n in 1..=9999 {
|
||||
let name = format!("*A{n}");
|
||||
if !existing.contains(&name) {
|
||||
|
||||
Reference in new issue
Block a user