Merge pull request #924 from ramox81/codex/fix-box-command-properties

Complete BOX command, properties, and grips
This commit is contained in:
Hakan Seven authored and GitHub committed 2026-08-28 20:44:34 +03:00
commit d8f5753ffd
5 files changed
+996 -68

No files matched your search

+28 -7
View File
@@ -441,8 +441,21 @@ impl OpenCADStudio {
let display_entity = dispatch::entity_in_working_plane(contextual.as_ref(), plane);
let entity = &display_entity;
let group_names = self.tabs[i].scene.group_names_for_entity(handle);
let box_primitive =
crate::scene::model::solid_history::is_box_primitive(
&self.tabs[i].scene.document,
handle,
);
let mut sections =
dispatch::properties_sectioned(handle, entity, &text_style_names);
if box_primitive {
sections.retain(|section| {
!section.props.iter().any(|property| {
property.field.starts_with("acis_")
|| property.field.starts_with("s3d_")
})
});
}
sections.extend(
crate::scene::model::solid_history::primitive_properties(
&self.tabs[i].scene.document,
@@ -607,7 +620,7 @@ impl OpenCADStudio {
}
}
if matches!(
if !box_primitive && matches!(
entity,
acadrust::EntityType::Solid3D(_)
| acadrust::EntityType::Region(_)
@@ -681,12 +694,14 @@ impl OpenCADStudio {
}
}
sections.extend(crate::entities::object_data::sections(
&self.tabs[i].scene.document,
&self.tabs[i].scene.object_data_cache,
handle,
entity,
));
if !box_primitive {
sections.extend(crate::entities::object_data::sections(
&self.tabs[i].scene.document,
&self.tabs[i].scene.object_data_cache,
handle,
entity,
));
}
{
use crate::entities::common::ro_prop;
@@ -2371,6 +2386,12 @@ impl OpenCADStudio {
annotation_scale_handle,
);
let mut entity_grips = dispatch::grips(contextual.as_ref());
if crate::scene::model::solid_history::is_box_primitive(
&self.tabs[i].scene.document,
handle,
) {
entity_grips.clear();
}
// Dimension::grips() cannot see the document, so an automatic dimension
// text grip cannot resolve its real DIMSTYLE/annotation-scaled position
// there. Correct it here, where both the document and displayed annotation
+10 -1
View File
@@ -1900,7 +1900,16 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
}
self.push_undo_snapshot(i, "CHPROP");
if field == "tol_text_style" {
if crate::scene::model::solid_history::is_history_choice(field) {
for &handle in &handles {
if self.tabs[i].scene.is_layer_locked(handle) {
continue;
}
self.tabs[i]
.scene
.apply_solid_history_choice(handle, field, &value);
}
} else if field == "tol_text_style" {
use crate::entities::dim_override as dov;
use acadrust::xdata::XDataValue;
let style_handle = self.tabs[i]
+491 -39
View File
@@ -6,7 +6,7 @@ use acadrust::EntityType;
use cadkernel::brep::Body;
use glam::DVec3;
use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane};
use crate::scene::model::solid_model;
use crate::scene::model::wire_model::WireModel;
use crate::t;
@@ -23,6 +23,19 @@ pub enum Shape {
Torus,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum BoxStep {
FirstCorner,
CenterPoint,
OppositeCorner,
CubeSize,
Length,
Width,
Height,
HeightFirstPoint,
HeightSecondPoint,
}
impl Shape {
fn from_id(id: &str) -> Option<Shape> {
Some(match id {
@@ -64,6 +77,14 @@ pub struct PrimitiveCommand {
pts: Vec<DVec3>,
/// True once the footprint is set and we are collecting the height.
height_step: bool,
box_step: BoxStep,
box_centered: bool,
box_origin: Option<DVec3>,
box_length: Option<f64>,
box_width: Option<f64>,
box_angle: f64,
box_width_sign: f64,
box_height_anchor: Option<DVec3>,
plane: WorkingPlane,
}
@@ -73,6 +94,14 @@ impl PrimitiveCommand {
shape: Shape::from_id(id).unwrap_or(Shape::Box),
pts: Vec::new(),
height_step: false,
box_step: BoxStep::FirstCorner,
box_centered: false,
box_origin: None,
box_length: None,
box_width: None,
box_angle: 0.0,
box_width_sign: 1.0,
box_height_anchor: None,
plane: WorkingPlane::default(),
}
}
@@ -98,8 +127,12 @@ impl PrimitiveCommand {
}
fn cursor_height(&self, point: DVec3) -> f64 {
(self.plane.to_local(point).z - self.pts[0].z)
.max(1e-6)
let height = self.plane.to_local(point).z - self.pts[0].z;
if self.shape == Shape::Box {
height
} else {
height.max(1e-6)
}
}
fn place_preview(&self, mut preview: WireModel) -> WireModel {
@@ -125,46 +158,208 @@ impl PrimitiveCommand {
.to_cols_array()
}
fn history_transform_axes(
&self,
origin: DVec3,
x_axis: DVec3,
y_axis: DVec3,
) -> [f64; 16] {
let to_world_vector = |value: DVec3| {
self.plane.x * value.x + self.plane.y * value.y + self.plane.z * value.z
};
glam::DMat4::from_cols(
to_world_vector(x_axis).extend(0.0),
to_world_vector(y_axis).extend(0.0),
self.plane.z.extend(0.0),
self.plane.to_world(origin).extend(1.0),
)
.to_cols_array()
}
fn box_spec(&self, height: f64) -> Option<(DVec3, DVec3, DVec3, f64, f64, f64)> {
let (mut origin, x_axis, y_axis, length, width) =
if let (Some(origin), Some(length), Some(width)) =
(self.box_origin, self.box_length, self.box_width)
{
let (sin, cos) = self.box_angle.sin_cos();
(
origin,
DVec3::new(cos, sin, 0.0),
DVec3::new(-sin, cos, 0.0) * self.box_width_sign,
length,
width,
)
} else {
let (a, b) = (*self.pts.first()?, *self.pts.get(1)?);
(
DVec3::new(a.x.min(b.x), a.y.min(b.y), a.z),
DVec3::X,
DVec3::Y,
(b.x - a.x).abs(),
(b.y - a.y).abs(),
)
};
let height_abs = height.abs();
if !origin.is_finite()
|| !x_axis.is_finite()
|| !y_axis.is_finite()
|| !length.is_finite()
|| !width.is_finite()
|| !height_abs.is_finite()
|| length < 1e-6
|| width < 1e-6
|| height_abs < 1e-6
{
return None;
}
if height < 0.0 {
origin.z += height;
}
if !origin.is_finite() {
return None;
}
Some((origin, x_axis, y_axis, length, width, height_abs))
}
fn build_box(&self, height: f64) -> Option<(Body, SolidHistoryOperation)> {
use crate::scene::model::solid_history;
let (origin, x_axis, y_axis, length, width, height) = self.box_spec(height)?;
let base = solid_model::box_solid(
[length * 0.5, width * 0.5, height * 0.5],
length,
width,
height,
)?;
let placed = solid_model::placed(
&base,
x_axis.to_array(),
y_axis.to_array(),
DVec3::Z.to_array(),
origin.to_array(),
)?;
Some((
placed,
solid_history::box_op(
self.history_transform_axes(origin, x_axis, y_axis),
length,
width,
height,
),
))
}
fn set_box_corner_footprint(&mut self, point: DVec3) -> bool {
let first = self.pts[0];
let delta = point - first;
if delta.x.abs() < 1e-6 || delta.y.abs() < 1e-6 {
return false;
}
if self.box_centered {
self.pts = vec![
DVec3::new(first.x - delta.x.abs(), first.y - delta.y.abs(), first.z),
DVec3::new(first.x + delta.x.abs(), first.y + delta.y.abs(), first.z),
];
} else {
self.pts.push(point);
}
self.box_step = BoxStep::Height;
self.height_step = true;
true
}
fn set_box_cube(&mut self, size: f64) -> Option<CmdResult> {
let size = size.abs();
if !size.is_finite() || size < 1e-6 {
return None;
}
let origin = if self.box_centered {
self.pts[0] - DVec3::new(size * 0.5, size * 0.5, 0.0)
} else {
self.pts[0]
};
self.box_origin = Some(origin);
self.box_length = Some(size);
self.box_width = Some(size);
Some(self.commit(size))
}
fn set_box_length(&mut self, length: f64, angle: f64) -> bool {
let length = length.abs();
if !length.is_finite() || !angle.is_finite() || length < 1e-6 {
return false;
}
self.box_origin = self.pts.first().copied();
self.box_length = Some(length);
self.box_angle = angle;
self.box_step = BoxStep::Width;
true
}
fn set_box_width(&mut self, width: f64, sign: f64) -> bool {
let width = width.abs();
if !width.is_finite() || !sign.is_finite() || width < 1e-6 {
return false;
}
self.box_width = Some(width);
self.box_width_sign = if sign < 0.0 { -1.0 } else { 1.0 };
if self.box_centered {
let center = self.pts[0];
let (sin, cos) = self.box_angle.sin_cos();
let x_axis = DVec3::new(cos, sin, 0.0);
let y_axis = DVec3::new(-sin, cos, 0.0) * self.box_width_sign;
self.box_origin = Some(
center
- x_axis * self.box_length.unwrap_or_default() * 0.5
- y_axis * width * 0.5,
);
}
self.box_step = BoxStep::Height;
self.height_step = true;
true
}
fn box_preview(&self, height: f64) -> Option<WireModel> {
let (origin, x_axis, y_axis, length, width, height) = self.box_spec(height)?;
let base = [
origin,
origin + x_axis * length,
origin + x_axis * length + y_axis * width,
origin + y_axis * width,
];
let top = base.map(|point| point + DVec3::Z * height);
let mut points = Vec::new();
push_loop(&mut points, &base);
push_loop(&mut points, &top);
for index in 0..4 {
push_segment(&mut points, base[index], top[index]);
}
Some(self.place_preview(wire("primitive_height_preview", points)))
}
/// Build the persistent kernel body and its history node.
fn build(&self, height: f64) -> Option<(Body, SolidHistoryOperation)> {
use crate::scene::model::solid_history;
let (solid, history) = match self.shape {
Shape::Box | Shape::Wedge => {
Shape::Box => return self.build_box(height),
Shape::Wedge => {
let (a, b) = (self.pts[0], self.pts[1]);
let length = (b.x - a.x).abs();
let width = (b.y - a.y).abs();
if length < 1e-6 || width < 1e-6 || height < 1e-6 {
return None;
}
if self.shape == Shape::Box {
let origin = DVec3::new(a.x.min(b.x), a.y.min(b.y), a.z);
let center = [
(a.x + b.x) / 2.0,
(a.y + b.y) / 2.0,
a.z + height / 2.0,
];
(
solid_model::box_solid(center, length, width, height),
solid_history::box_op(
self.history_transform(origin),
length,
width,
height,
),
)
} else {
let origin = [a.x.min(b.x), a.y.min(b.y), a.z];
(
solid_model::wedge_solid(origin, length, width, height),
solid_history::wedge_op(
self.history_transform(DVec3::from_array(origin)),
length,
width,
height,
),
)
}
let origin = [a.x.min(b.x), a.y.min(b.y), a.z];
(
solid_model::wedge_solid(origin, length, width, height),
solid_history::wedge_op(
self.history_transform(DVec3::from_array(origin)),
length,
width,
height,
),
)
}
Shape::Cylinder | Shape::Cone => {
let c = self.pts[0];
@@ -245,9 +440,7 @@ impl PrimitiveCommand {
fn commit(&self, height: f64) -> CmdResult {
match self.build(height) {
Some((solid, history)) => {
// Built upright in its own frame, then put on the working
// plane — the same move `place_entity` makes for the ACIS
// copy, so the two stay on top of each other.
// Place the local body on the working plane.
let placed = solid_model::placed(
&solid,
[self.plane.x.x, self.plane.x.y, self.plane.x.z],
@@ -289,7 +482,12 @@ impl CadCommand for PrimitiveCommand {
}
fn cursor_axis(&self) -> Option<(DVec3, DVec3)> {
self.height_step.then(|| {
let constrained = if self.shape == Shape::Box {
self.box_step == BoxStep::Height
} else {
self.height_step
};
constrained.then(|| {
(
self.plane.to_world(self.pts[0]),
self.plane.z.normalize_or_zero(),
@@ -303,6 +501,26 @@ impl CadCommand for PrimitiveCommand {
fn prompt(&self) -> String {
let n = self.shape.name();
if self.shape == Shape::Box {
return match self.box_step {
BoxStep::FirstCorner => t!("%{n} Specify first corner or [Center]:", n = n),
BoxStep::CenterPoint => t!("%{n} Specify center point:", n = n),
BoxStep::OppositeCorner => {
t!("%{n} Specify other corner or [Cube/Length]:", n = n)
}
BoxStep::CubeSize => t!("%{n} Specify cube length:", n = n),
BoxStep::Length => t!("%{n} Specify length:", n = n),
BoxStep::Width => t!("%{n} Specify width:", n = n),
BoxStep::Height => t!("%{n} Specify height or [2Point]:", n = n),
BoxStep::HeightFirstPoint => {
t!("%{n} Specify first point for height:", n = n)
}
BoxStep::HeightSecondPoint => {
t!("%{n} Specify second point for height:", n = n)
}
}
.into_owned();
}
if self.height_step {
return t!("%{n} Specify height <Enter for default>:", n = n).into_owned();
}
@@ -321,7 +539,82 @@ impl CadCommand for PrimitiveCommand {
}
}
fn options(&self) -> Vec<CmdOption> {
if self.shape != Shape::Box {
return Vec::new();
}
match self.box_step {
BoxStep::FirstCorner => vec![CmdOption::new(t!("Center").as_ref(), "C")],
BoxStep::OppositeCorner => vec![
CmdOption::new(t!("Cube").as_ref(), "C"),
CmdOption::new(t!("Length").as_ref(), "L"),
],
BoxStep::Height => vec![CmdOption::new(t!("2Point").as_ref(), "2P")],
_ => Vec::new(),
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
if self.shape == Shape::Box {
let local = self.plane.to_local(pt);
if !local.is_finite() {
return CmdResult::NeedPoint;
}
return match self.box_step {
BoxStep::FirstCorner | BoxStep::CenterPoint => {
self.pts = vec![local];
self.box_step = BoxStep::OppositeCorner;
CmdResult::NeedPoint
}
BoxStep::OppositeCorner => {
self.set_box_corner_footprint(local);
CmdResult::NeedPoint
}
BoxStep::CubeSize => self
.set_box_cube((local - self.pts[0]).length())
.unwrap_or(CmdResult::NeedPoint),
BoxStep::Length => {
let delta = local - self.pts[0];
if !self.set_box_length(delta.x.hypot(delta.y), delta.y.atan2(delta.x)) {
return CmdResult::NeedPoint;
}
CmdResult::NeedPoint
}
BoxStep::Width => {
let delta = local - self.pts[0];
let normal = DVec3::new(-self.box_angle.sin(), self.box_angle.cos(), 0.0);
let signed = delta.dot(normal);
if !self.set_box_width(signed, signed) {
return CmdResult::NeedPoint;
}
CmdResult::NeedPoint
}
BoxStep::Height => {
let height = self.cursor_height(pt);
if height.abs() < 1e-6 {
CmdResult::NeedPoint
} else {
self.commit(height)
}
}
BoxStep::HeightFirstPoint => {
self.box_height_anchor = Some(local);
self.box_step = BoxStep::HeightSecondPoint;
CmdResult::NeedPoint
}
BoxStep::HeightSecondPoint => {
let Some(first) = self.box_height_anchor else {
return CmdResult::NeedPoint;
};
let height = (local - first).length();
if height < 1e-6 {
CmdResult::NeedPoint
} else {
self.commit(height)
}
}
};
}
if self.height_step {
return self.commit(self.cursor_height(pt));
}
@@ -339,6 +632,9 @@ impl CadCommand for PrimitiveCommand {
}
fn on_enter(&mut self) -> CmdResult {
if self.shape == Shape::Box {
return CmdResult::Cancel;
}
if self.height_step {
let h = self.default_height();
return self.commit(h);
@@ -351,10 +647,70 @@ impl CadCommand for PrimitiveCommand {
}
fn wants_text_input(&self) -> bool {
self.height_step
if self.shape == Shape::Box {
matches!(
self.box_step,
BoxStep::FirstCorner
| BoxStep::OppositeCorner
| BoxStep::CubeSize
| BoxStep::Length
| BoxStep::Width
| BoxStep::Height
)
} else {
self.height_step
}
}
fn point_step_accepts_keywords(&self) -> bool {
self.shape == Shape::Box
&& matches!(
self.box_step,
BoxStep::FirstCorner | BoxStep::OppositeCorner | BoxStep::Height
)
}
fn on_text_input(&mut self, raw: &str) -> Option<CmdResult> {
if self.shape == Shape::Box {
let token = raw.trim();
let upper = token.to_uppercase();
return match self.box_step {
BoxStep::FirstCorner if matches!(upper.as_str(), "C" | "CENTER") => {
self.box_centered = true;
self.box_step = BoxStep::CenterPoint;
Some(CmdResult::NeedPoint)
}
BoxStep::OppositeCorner if matches!(upper.as_str(), "C" | "CUBE") => {
self.box_step = BoxStep::CubeSize;
Some(CmdResult::NeedPoint)
}
BoxStep::OppositeCorner if matches!(upper.as_str(), "L" | "LENGTH") => {
self.box_step = BoxStep::Length;
Some(CmdResult::NeedPoint)
}
BoxStep::Height if matches!(upper.as_str(), "2P" | "2POINT") => {
self.box_step = BoxStep::HeightFirstPoint;
Some(CmdResult::NeedPoint)
}
BoxStep::CubeSize => crate::entities::common::parse_typed_length(token)
.and_then(|value| self.set_box_cube(value)),
BoxStep::Length => {
let value = crate::entities::common::parse_typed_length(token)?;
self.set_box_length(value, 0.0)
.then_some(CmdResult::NeedPoint)
}
BoxStep::Width => {
let value = crate::entities::common::parse_typed_length(token)?;
self.set_box_width(value, value)
.then_some(CmdResult::NeedPoint)
}
BoxStep::Height => {
let value = crate::entities::common::parse_typed_length(token)?;
(value.is_finite() && value.abs() >= 1e-6).then(|| self.commit(value))
}
_ => None,
};
}
if !self.height_step {
return None;
}
@@ -366,6 +722,75 @@ impl CadCommand for PrimitiveCommand {
if self.pts.is_empty() {
return None;
}
if self.shape == Shape::Box {
let local = self.plane.to_local(pt);
if !local.is_finite() {
return None;
}
return match self.box_step {
BoxStep::OppositeCorner => {
let first = self.pts[0];
let points = if self.box_centered {
let delta = local - first;
vec![
DVec3::new(
first.x - delta.x.abs(),
first.y - delta.y.abs(),
first.z,
),
DVec3::new(
first.x + delta.x.abs(),
first.y + delta.y.abs(),
first.z,
),
]
} else {
vec![first, local]
};
Some(self.place_preview(footprint_wire(Shape::Box, &points)))
}
BoxStep::CubeSize => {
let size = (local - self.pts[0]).length().max(1e-6);
let mut preview = PrimitiveCommand::new("BOX");
preview.plane = self.plane;
preview.pts = self.pts.clone();
preview.box_origin = Some(if self.box_centered {
self.pts[0] - DVec3::new(size * 0.5, size * 0.5, 0.0)
} else {
self.pts[0]
});
preview.box_length = Some(size);
preview.box_width = Some(size);
preview.box_preview(size)
}
BoxStep::Length => Some(self.place_preview(wire(
"primitive_preview",
vec![self.pts[0].as_vec3().to_array(), local.as_vec3().to_array()],
))),
BoxStep::Width => {
let delta = local - self.pts[0];
let normal = DVec3::new(-self.box_angle.sin(), self.box_angle.cos(), 0.0);
let signed = delta.dot(normal);
let mut preview = PrimitiveCommand::new("BOX");
preview.plane = self.plane;
preview.pts = self.pts.clone();
preview.box_origin = self.box_origin;
preview.box_length = self.box_length;
preview.box_width = Some(signed.abs().max(1e-6));
preview.box_angle = self.box_angle;
preview.box_width_sign = if signed < 0.0 { -1.0 } else { 1.0 };
preview.box_preview(1e-6)
}
BoxStep::Height => self.box_preview(self.cursor_height(pt)),
BoxStep::HeightSecondPoint => self.box_height_anchor.map(|first| {
self.place_preview(wire(
"primitive_preview",
vec![first.as_vec3().to_array(), local.as_vec3().to_array()],
))
}),
_ => None,
};
}
if self.height_step {
return Some(self.place_preview(height_wire(
self.shape,
@@ -381,15 +806,42 @@ impl CadCommand for PrimitiveCommand {
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec};
self.height_step.then(|| DynSpec {
let role = if self.shape == Shape::Box {
match self.box_step {
BoxStep::CubeSize | BoxStep::Length => DynRole::Distance,
BoxStep::Width => DynRole::Width,
BoxStep::Height => DynRole::Height,
_ => return None,
}
} else if self.height_step {
DynRole::Height
} else {
return None;
};
Some(DynSpec {
anchor: DynAnchor::Point(self.plane.to_world(self.pts[0])),
fields: vec![DynFieldSpec::new(DynRole::Height)],
fields: vec![DynFieldSpec::new(role)],
guide: DynGuide::None,
ref_point: None,
})
}
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
if self.shape == Shape::Box {
let local = self.plane.to_local(cursor);
if !local.is_finite() {
return None;
}
return match self.box_step {
BoxStep::CubeSize | BoxStep::Length => Some((local - self.pts[0]).length()),
BoxStep::Width => {
let normal = DVec3::new(-self.box_angle.sin(), self.box_angle.cos(), 0.0);
Some((local - self.pts[0]).dot(normal).abs())
}
BoxStep::Height => Some(self.cursor_height(cursor)),
_ => None,
};
}
self.height_step.then(|| self.cursor_height(cursor))
}
}
+419 -9
View File
@@ -1,8 +1,8 @@
use acadrust::entities::Solid3D;
use acadrust::objects::{
SolidHistoryBox, SolidHistoryBrep, SolidHistoryCylinder,
SolidHistoryNodeBase, SolidHistoryOperation, SolidHistoryPyramid,
SolidHistorySphere, SolidHistoryTorus,
DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep,
SolidHistoryCylinder, SolidHistoryNodeBase, SolidHistoryOperation,
SolidHistoryPyramid, SolidHistorySphere, SolidHistoryTorus,
};
use cadkernel::brep::Body;
@@ -19,6 +19,13 @@ pub const GRIP_RADIUS: usize = 10_004;
pub const GRIP_OUTER_RADIUS: usize = 10_005;
pub const GRIP_INNER_RADIUS: usize = 10_006;
pub const GRIP_SIDES: usize = 10_007;
pub const GRIP_BOX_CORNER_FIRST: usize = 10_100;
pub const GRIP_BOX_FACE_X_MIN: usize = 10_110;
pub const GRIP_BOX_FACE_X_MAX: usize = 10_111;
pub const GRIP_BOX_FACE_Y_MIN: usize = 10_112;
pub const GRIP_BOX_FACE_Y_MAX: usize = 10_113;
pub const GRIP_BOX_FACE_Z_MIN: usize = 10_114;
pub const GRIP_BOX_FACE_Z_MAX: usize = 10_115;
pub const PROP_LENGTH: &str = "solid_history_length";
pub const PROP_WIDTH: &str = "solid_history_width";
@@ -27,6 +34,12 @@ pub const PROP_RADIUS: &str = "solid_history_radius";
pub const PROP_OUTER_RADIUS: &str = "solid_history_outer_radius";
pub const PROP_INNER_RADIUS: &str = "solid_history_inner_radius";
pub const PROP_SIDES: &str = "solid_history_sides";
pub const PROP_POSITION_X: &str = "solid_history_position_x";
pub const PROP_POSITION_Y: &str = "solid_history_position_y";
pub const PROP_POSITION_Z: &str = "solid_history_position_z";
pub const PROP_ROTATION: &str = "solid_history_rotation";
pub const PROP_HISTORY: &str = "solid_history_record";
pub const PROP_SHOW_HISTORY: &str = "solid_history_show";
fn history_prop(label: &str, field: &'static str, value: impl ToString) -> Property {
Property {
@@ -36,6 +49,132 @@ fn history_prop(label: &str, field: &'static str, value: impl ToString) -> Prope
}
}
fn history_flags(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
) -> Option<(bool, bool)> {
let graph = document.solid_history_graph(handle)?;
let ObjectType::DynamicBlock(object) = document.objects.get(&graph.root)? else {
return None;
};
let DynamicBlockData::SolidHistory(history) = &object.data else {
return None;
};
Some((history.record_history, history.show_history))
}
pub fn is_box_primitive(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
) -> bool {
matches!(
document.solid_history_operation(handle),
Some(SolidHistoryOperation::Box(_))
)
}
pub fn reference_point(operation: &SolidHistoryOperation) -> Option<glam::DVec3> {
match operation {
SolidHistoryOperation::Box(value) => world_point(
value.base.transform,
[value.length * 0.5, value.width * 0.5, 0.0],
),
_ => None,
}
}
fn box_properties(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
value: &SolidHistoryBox,
) -> Vec<PropSection> {
let Some(position) = world_point(
value.base.transform,
[value.length * 0.5, value.width * 0.5, 0.0],
) else {
return Vec::new();
};
let rotation = matrix(value.base.transform)
.map(|matrix| matrix.x_axis.y.atan2(matrix.x_axis.x))
.unwrap_or(0.0);
let (record_history, show_history) = history_flags(document, handle).unwrap_or((false, false));
let show_value = if show_history { "Yes" } else { "No" };
vec![
PropSection {
title: t!("Geometry").into_owned(),
props: vec![
Property {
label: t!("Solid type").into_owned(),
field: "solid_history_type",
value: PropValue::ReadOnly(t!("Box").into_owned()),
},
crate::entities::common::edit_prop(
t!("Position X").as_ref(),
PROP_POSITION_X,
position.x,
),
crate::entities::common::edit_prop(
t!("Position Y").as_ref(),
PROP_POSITION_Y,
position.y,
),
crate::entities::common::edit_prop(
t!("Position Z").as_ref(),
PROP_POSITION_Z,
position.z,
),
crate::entities::common::edit_prop(
t!("Length").as_ref(),
PROP_LENGTH,
value.length,
),
crate::entities::common::edit_prop(
t!("Width").as_ref(),
PROP_WIDTH,
value.width,
),
crate::entities::common::edit_prop(
t!("Height").as_ref(),
PROP_HEIGHT,
value.height,
),
Property {
label: t!("Rotation").into_owned(),
field: PROP_ROTATION,
value: PropValue::EditText(
crate::entities::common::format_direction(rotation),
),
},
],
},
PropSection {
title: t!("Solid History").into_owned(),
props: vec![
Property {
label: t!("History").into_owned(),
field: PROP_HISTORY,
value: PropValue::Choice {
selected: if record_history { "Record" } else { "None" }.to_string(),
options: vec!["None".to_string(), "Record".to_string()],
},
},
Property {
label: t!("Show History").into_owned(),
field: PROP_SHOW_HISTORY,
value: if record_history {
PropValue::Choice {
selected: show_value.to_string(),
options: vec!["No".to_string(), "Yes".to_string()],
}
} else {
PropValue::ReadOnly(show_value.to_string())
},
},
],
},
]
}
pub fn primitive_properties(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
@@ -44,7 +183,8 @@ pub fn primitive_properties(
return Vec::new();
};
let props = match operation {
SolidHistoryOperation::Box(value) | SolidHistoryOperation::Wedge(value) => vec![
SolidHistoryOperation::Box(value) => return box_properties(document, handle, value),
SolidHistoryOperation::Wedge(value) => vec![
history_prop(t!("Length").as_ref(), PROP_LENGTH, value.length),
history_prop(t!("Width").as_ref(), PROP_WIDTH, value.width),
history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height),
@@ -93,23 +233,185 @@ pub fn is_primitive_property(field: &str) -> bool {
| PROP_OUTER_RADIUS
| PROP_INNER_RADIUS
| PROP_SIDES
| PROP_POSITION_X
| PROP_POSITION_Y
| PROP_POSITION_Z
| PROP_ROTATION
)
}
pub fn is_history_choice(field: &str) -> bool {
matches!(field, PROP_HISTORY | PROP_SHOW_HISTORY)
}
pub fn apply_history_choice(
document: &mut acadrust::CadDocument,
handle: acadrust::Handle,
field: &str,
value: &str,
) -> bool {
let Some(graph) = document.solid_history_graph(handle) else {
return false;
};
let Some(ObjectType::DynamicBlock(object)) = document.objects.get_mut(&graph.root) else {
return false;
};
let DynamicBlockData::SolidHistory(history) = &mut object.data else {
return false;
};
let before = (history.record_history, history.show_history);
match field {
PROP_HISTORY => {
history.record_history = if value.eq_ignore_ascii_case("Record") {
true
} else if value.eq_ignore_ascii_case("None") {
false
} else {
return false;
};
if !history.record_history {
history.show_history = false;
}
}
PROP_SHOW_HISTORY if history.record_history => {
history.show_history = if value.eq_ignore_ascii_case("Yes") {
true
} else if value.eq_ignore_ascii_case("No") {
false
} else {
return false;
};
}
_ => return false,
}
before != (history.record_history, history.show_history)
}
fn apply_box_geometry_property(
value: &mut SolidHistoryBox,
field: &str,
text: &str,
) -> Option<bool> {
if field == PROP_ROTATION {
let target = crate::entities::common::parse_direction(text)?;
if !target.is_finite() {
return Some(false);
}
let current = matrix(value.base.transform)?;
let center = current.transform_point3(glam::DVec3::new(
value.length * 0.5,
value.width * 0.5,
0.0,
));
if !center.is_finite() {
return Some(false);
}
let projected = current.x_axis.truncate();
if projected.length_squared() <= 1e-12 {
return Some(false);
}
let current_angle = projected.y.atan2(projected.x);
let delta = (target - current_angle + std::f64::consts::PI)
.rem_euclid(std::f64::consts::TAU)
- std::f64::consts::PI;
if delta.abs() <= 1e-12 {
return Some(false);
}
let updated = glam::DMat4::from_translation(center)
* glam::DMat4::from_rotation_z(delta)
* glam::DMat4::from_translation(-center)
* current;
if !updated.is_finite() || updated.determinant().abs() <= 1e-12 {
return Some(false);
}
value.base.transform = updated.to_cols_array();
return Some(true);
}
let axis = match field {
PROP_POSITION_X => 0,
PROP_POSITION_Y => 1,
PROP_POSITION_Z => 2,
_ => return None,
};
let target = crate::entities::common::parse_length(text)?;
if !target.is_finite() {
return Some(false);
}
let current = matrix(value.base.transform)?;
let center = current.transform_point3(glam::DVec3::new(
value.length * 0.5,
value.width * 0.5,
0.0,
));
if !center.is_finite() {
return Some(false);
}
if target == center[axis] {
return Some(false);
}
let mut delta = glam::DVec3::ZERO;
delta[axis] = target - center[axis];
value.base.transform = (glam::DMat4::from_translation(delta) * current).to_cols_array();
Some(true)
}
pub fn apply_primitive_property(
operation: &mut SolidHistoryOperation,
field: &str,
value: &str,
) -> bool {
let Ok(number) = value.trim().parse::<f64>() else {
if let SolidHistoryOperation::Box(box_value) = operation {
if let Some(applied) = apply_box_geometry_property(box_value, field, value) {
return applied;
}
}
let Some(number) = (if field == PROP_SIDES {
value.trim().parse::<f64>().ok()
} else {
crate::entities::common::parse_length(value)
}) else {
return false;
};
if !number.is_finite() {
return false;
}
let positive = || (number > 0.0).then_some(number);
if let SolidHistoryOperation::Box(value) = operation {
let Some(next) = positive() else {
return false;
};
let (current_size, local_shift) = match field {
PROP_LENGTH => (
value.length,
glam::DVec3::new((value.length - next) * 0.5, 0.0, 0.0),
),
PROP_WIDTH => (
value.width,
glam::DVec3::new(0.0, (value.width - next) * 0.5, 0.0),
),
PROP_HEIGHT => (value.height, glam::DVec3::ZERO),
_ => return false,
};
if !current_size.is_finite() || current_size < 1e-6 || next == current_size {
return false;
}
if local_shift != glam::DVec3::ZERO {
let Some(current) = matrix(value.base.transform) else {
return false;
};
value.base.transform =
(current * glam::DMat4::from_translation(local_shift)).to_cols_array();
}
match field {
PROP_LENGTH => value.length = next,
PROP_WIDTH => value.width = next,
PROP_HEIGHT => value.height = next,
_ => unreachable!(),
}
return true;
}
match operation {
SolidHistoryOperation::Box(value) | SolidHistoryOperation::Wedge(value) => match field {
SolidHistoryOperation::Wedge(value) => match field {
PROP_LENGTH => value.length = positive().unwrap_or(value.length),
PROP_WIDTH => value.width = positive().unwrap_or(value.width),
PROP_HEIGHT => value.height = positive().unwrap_or(value.height),
@@ -272,7 +574,7 @@ fn grip(
world,
is_midpoint: false,
shape,
dir: None,
dir: (shape == GripShape::Triangle).then_some(axis).flatten(),
axis,
}
}
@@ -296,7 +598,62 @@ pub fn primitive_grips(
}
};
match operation {
SolidHistoryOperation::Box(value) | SolidHistoryOperation::Wedge(value) => {
SolidHistoryOperation::Box(value) => {
for corner in 0..8 {
add(
GRIP_BOX_CORNER_FIRST + corner,
value.base.transform,
[
if corner & 1 == 0 { 0.0 } else { value.length },
if corner & 2 == 0 { 0.0 } else { value.width },
if corner & 4 == 0 { 0.0 } else { value.height },
],
GripShape::Square,
None,
);
}
for (id, point, axis) in [
(
GRIP_BOX_FACE_X_MIN,
[0.0, value.width * 0.5, value.height * 0.5],
[-1.0, 0.0, 0.0],
),
(
GRIP_BOX_FACE_X_MAX,
[value.length, value.width * 0.5, value.height * 0.5],
[1.0, 0.0, 0.0],
),
(
GRIP_BOX_FACE_Y_MIN,
[value.length * 0.5, 0.0, value.height * 0.5],
[0.0, -1.0, 0.0],
),
(
GRIP_BOX_FACE_Y_MAX,
[value.length * 0.5, value.width, value.height * 0.5],
[0.0, 1.0, 0.0],
),
(
GRIP_BOX_FACE_Z_MIN,
[value.length * 0.5, value.width * 0.5, 0.0],
[0.0, 0.0, -1.0],
),
(
GRIP_BOX_FACE_Z_MAX,
[value.length * 0.5, value.width * 0.5, value.height],
[0.0, 0.0, 1.0],
),
] {
add(
id,
value.base.transform,
point,
GripShape::Triangle,
Some(axis),
);
}
}
SolidHistoryOperation::Wedge(value) => {
add(
GRIP_LENGTH,
value.base.transform,
@@ -405,9 +762,62 @@ pub fn apply_primitive_grip(
let Some(local) = local_point(transform, world) else {
return false;
};
if !local.is_finite() {
return false;
}
let positive = |value: f64| value.abs().max(1e-6);
match operation {
SolidHistoryOperation::Box(value) | SolidHistoryOperation::Wedge(value) => {
SolidHistoryOperation::Box(value) => {
let old_size = glam::DVec3::new(value.length, value.width, value.height);
if !old_size.is_finite() || old_size.min_element() < 1e-6 {
return false;
}
let mut low = glam::DVec3::ZERO;
let mut high = old_size;
if (GRIP_BOX_CORNER_FIRST..GRIP_BOX_CORNER_FIRST + 8).contains(&grip_id) {
let corner = grip_id - GRIP_BOX_CORNER_FIRST;
for axis in 0..3 {
let opposite = if corner & (1 << axis) == 0 {
old_size[axis]
} else {
0.0
};
low[axis] = local[axis].min(opposite);
high[axis] = local[axis].max(opposite);
}
} else {
match grip_id {
GRIP_BOX_FACE_X_MIN => low.x = local.x.min(old_size.x - 1e-6),
GRIP_BOX_FACE_X_MAX => high.x = local.x.max(1e-6),
GRIP_BOX_FACE_Y_MIN => low.y = local.y.min(old_size.y - 1e-6),
GRIP_BOX_FACE_Y_MAX => high.y = local.y.max(1e-6),
GRIP_BOX_FACE_Z_MIN => low.z = local.z.min(old_size.z - 1e-6),
GRIP_BOX_FACE_Z_MAX => high.z = local.z.max(1e-6),
GRIP_LENGTH => high.x = positive(local.x),
GRIP_WIDTH => high.y = positive(local.y),
GRIP_HEIGHT => high.z = local.z.max(1e-6),
_ => return false,
}
}
let size = high - low;
if !size.is_finite() || size.min_element() < 1e-6 {
return false;
}
if low.abs().max_element() <= 1e-12
&& (size - old_size).abs().max_element() <= 1e-12
{
return false;
}
let Some(current) = matrix(value.base.transform) else {
return false;
};
value.base.transform =
(current * glam::DMat4::from_translation(low)).to_cols_array();
value.length = size.x;
value.width = size.y;
value.height = size.z;
}
SolidHistoryOperation::Wedge(value) => {
match grip_id {
GRIP_LENGTH => value.length = positive(local.x),
GRIP_WIDTH => value.width = positive(local.y),
+48 -12
View File
@@ -70,12 +70,7 @@ fn capture_text_orient(e: &EntityType) -> Option<TextOrient> {
}
}
/// MIRRTEXT off: after the mirror reflects a text's points and rotation, put
/// the glyphs back to right-reading. Restores the captured rotation / oblique /
/// x-scale, and for single-line TEXT also flips the horizontal justification
/// (left ↔ right) so the box lands mirror-symmetric to the source instead of
/// hugging the axis — AutoCAD's behaviour. Center/Middle/Aligned/Fit are already
/// symmetric about their (reflected) anchor, so they stay.
/// Restores readable text orientation after a mirror.
fn restore_text_orient(e: &mut EntityType, o: &TextOrient) {
match e {
EntityType::Text(t) => {
@@ -168,14 +163,10 @@ impl Scene {
.filter(|&h| !self.is_layer_locked(h))
.collect();
let handles = &handles[..];
// MIRRTEXT (header.mirror_text): when false AutoCAD positions text /
// mtext / shape by the mirror but keeps the original rotation +
// oblique so the text stays right-reading. Capture before the
// transform and re-apply afterwards.
// Preserve readable text orientation when MIRRTEXT is disabled.
let preserve_text_orientation =
matches!(t, EntityTransform::Mirror { .. }) && !self.document.header.mirror_text;
// MIRRTEXT on: toggle the group-71 flags after the reflect so text
// becomes a true glyph mirror (see `mirror_true_text_flags`).
// Toggle group-71 flags when glyph mirroring is enabled.
let mirror_true =
matches!(t, EntityTransform::Mirror { .. }) && self.document.header.mirror_text;
let mut text_orient_backup: Vec<(Handle, TextOrient)> = Vec::new();
@@ -773,6 +764,7 @@ impl Scene {
handle: Handle,
operation: acadrust::objects::SolidHistoryOperation,
) -> bool {
let box_primitive = matches!(operation, acadrust::objects::SolidHistoryOperation::Box(_));
let Some(graph) = self.document.create_solid_history(handle, operation) else {
return false;
};
@@ -780,9 +772,36 @@ impl Scene {
for node in graph.nodes {
self.record_undo_object_before(node, None);
}
if box_primitive {
let _ = crate::scene::model::solid_history::apply_history_choice(
&mut self.document,
handle,
crate::scene::model::solid_history::PROP_HISTORY,
"None",
);
}
self.sync_solid_reference_point(handle);
true
}
fn sync_solid_reference_point(&mut self, handle: Handle) {
let reference = self
.document
.solid_history_operation(handle)
.and_then(crate::scene::model::solid_history::reference_point);
let Some(reference) = reference else {
return;
};
let Some(EntityType::Solid3D(entity)) = self.document.get_entity_mut(handle) else {
return;
};
entity.point_of_reference = acadrust::types::Vector3::new(
reference.x,
reference.y,
reference.z,
);
}
fn copy_solid_history(&mut self, source: Handle, target: Handle) -> bool {
let Some(graph) = self.document.copy_solid_history(source, target) else {
return false;
@@ -822,6 +841,7 @@ impl Scene {
return false;
};
entity.set_sat_document(&document);
self.sync_solid_reference_point(handle);
self.register_solid_model(handle, body);
true
}
@@ -840,6 +860,7 @@ impl Scene {
return false;
};
entity.set_sat_document(&document);
self.sync_solid_reference_point(handle);
self.register_solid_model(handle, body);
true
}
@@ -894,6 +915,21 @@ impl Scene {
self.rebuild_solid_history(handle, operation)
}
pub fn apply_solid_history_choice(
&mut self,
handle: Handle,
field: &str,
value: &str,
) -> bool {
self.record_solid_history_before(handle);
crate::scene::model::solid_history::apply_history_choice(
&mut self.document,
handle,
field,
value,
)
}
fn apply_solid_history_grip(
&mut self,
handle: Handle,