diff --git a/src/app/properties.rs b/src/app/properties.rs index 57efdd611..cf4a5d093 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -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 diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 49f50fd08..efd4d5b8d 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1900,7 +1900,16 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } 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] diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs index b7dc9d25e..cfaa65ca2 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -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 { Some(match id { @@ -64,6 +77,14 @@ pub struct PrimitiveCommand { pts: Vec, /// True once the footprint is set and we are collecting the height. height_step: bool, + box_step: BoxStep, + box_centered: bool, + box_origin: Option, + box_length: Option, + box_width: Option, + box_angle: f64, + box_width_sign: f64, + box_height_anchor: Option, 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 { + 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 { + 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 :", n = n).into_owned(); } @@ -321,7 +539,82 @@ impl CadCommand for PrimitiveCommand { } } + fn options(&self) -> Vec { + 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 { + 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 { 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 { + 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)) } } diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index 9879bd2d6..a7c05f7be 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -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 { + 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 { + 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 { + 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::() 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::().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), diff --git a/src/scene/modify.rs b/src/scene/modify.rs index 958929005..b75e0eb9d 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -70,12 +70,7 @@ fn capture_text_orient(e: &EntityType) -> Option { } } -/// 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,