diff --git a/src/app/properties.rs b/src/app/properties.rs index c709723de..7d6ddd3cf 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -358,6 +358,12 @@ impl OpenCADStudio { let group_names = self.tabs[i].scene.group_names_for_entity(handle); let mut sections = dispatch::properties_sectioned(handle, entity, &text_style_names); + sections.extend( + crate::scene::model::solid_history::primitive_properties( + &self.tabs[i].scene.document, + handle, + ), + ); // Turn the Material row into an editable picker: the source // options (ByLayer / ByBlock) plus every named material the @@ -1387,7 +1393,11 @@ impl OpenCADStudio { .iter() .map(|(handle, entity)| (*handle, entity)) .collect(); - let sections = aggregate_sections(&local_refs, &text_style_names); + let mut sections = aggregate_sections(&local_refs, &text_style_names); + sections.extend(aggregate_solid_history_sections( + &self.tabs[i].scene.document, + &local_refs.iter().map(|(handle, _)| *handle).collect::>(), + )); ui::PropertiesPanel { choice_combos: sections .iter() @@ -1903,6 +1913,28 @@ pub(super) fn aggregate_sections( result } +fn aggregate_solid_history_sections( + document: &acadrust::CadDocument, + handles: &[Handle], +) -> Vec { + let mut sections = handles.iter().map(|handle| { + crate::scene::model::solid_history::primitive_properties(document, *handle) + }); + let Some(mut merged) = sections.next() else { + return Vec::new(); + }; + if merged.is_empty() { + return Vec::new(); + } + for next in sections { + if next.is_empty() { + return Vec::new(); + } + merged = merge_sections(&merged, &next); + } + merged +} + fn merge_sections( left: &[crate::scene::model::object::PropSection], right: &[crate::scene::model::object::PropSection], diff --git a/src/app/update/command.rs b/src/app/update/command.rs index acf512bec..0035ec898 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -2206,15 +2206,36 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } } _ => { - if let Some(entity) = - self.tabs[i].scene.document.get_entity_mut(handle) - { - crate::scene::view::dispatch::apply_geom_prop_in_working_plane( - entity, + if crate::scene::model::solid_history::is_primitive_property( + field, + ) { + self.tabs[i].scene.apply_solid_history_property( + handle, + field, + &val, + ); + } else if self.tabs[i] + .scene + .apply_solid_position_property( + handle, field, &val, plane, - ); + ) + .is_none() + { + if let Some(entity) = self.tabs[i] + .scene + .document + .get_entity_mut(handle) + { + crate::scene::view::dispatch::apply_geom_prop_in_working_plane( + entity, + field, + &val, + plane, + ); + } } } } diff --git a/src/entities/solid3d.rs b/src/entities/solid3d.rs index 022b62070..92fa3b4d3 100644 --- a/src/entities/solid3d.rs +++ b/src/entities/solid3d.rs @@ -1,9 +1,6 @@ // Grippable + PropertyEditable for Solid3D, Region, Body. // -// Geometry lives in ACIS data — we cannot edit it via the properties panel. -// We expose the point_of_reference as a translate grip and show ACIS size -// as read-only info. Grip translate also updates stored wire points; the -// caller translates the mesh vertices to match. +// Shared grips and properties for modeler entities. use acadrust::entities::{Body, Region, Solid3D, Surface}; use cadkernel::space::polygon; diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index 6b72397d5..cdf789548 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -8,8 +8,11 @@ use acadrust::types::{Vector2, Vector3}; use acadrust::EntityType; use cadkernel::brep::Body; -use crate::scene::model::object::{GripApply, GripDef, GripShape}; use crate::command::EntityTransform; +use crate::scene::model::object::{ + GripApply, GripDef, GripShape, PropSection, PropValue, Property, +}; +use crate::t; pub const GRIP_LENGTH: usize = 10_001; pub const GRIP_WIDTH: usize = 10_002; @@ -19,6 +22,142 @@ pub const GRIP_MAJOR_RADIUS: usize = 10_005; pub const GRIP_MINOR_RADIUS: usize = 10_006; pub const GRIP_SIDES: usize = 10_007; +pub const PROP_LENGTH: &str = "solid_history_length"; +pub const PROP_WIDTH: &str = "solid_history_width"; +pub const PROP_HEIGHT: &str = "solid_history_height"; +pub const PROP_RADIUS: &str = "solid_history_radius"; +pub const PROP_MAJOR_RADIUS: &str = "solid_history_major_radius"; +pub const PROP_MINOR_RADIUS: &str = "solid_history_minor_radius"; +pub const PROP_SIDES: &str = "solid_history_sides"; + +fn history_prop(label: &str, field: &'static str, value: impl ToString) -> Property { + Property { + label: label.to_string(), + field, + value: PropValue::EditText(value.to_string()), + } +} + +pub fn primitive_properties( + document: &acadrust::CadDocument, + handle: acadrust::Handle, +) -> Vec { + let Some(operation) = document.solid_history_operation(handle) else { + return Vec::new(); + }; + let props = match operation { + SolidHistoryOperation::Box(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), + ], + SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => vec![ + history_prop(t!("Radius").as_ref(), PROP_RADIUS, value.major_radius), + history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height), + ], + SolidHistoryOperation::Sphere(value) => vec![history_prop( + t!("Radius").as_ref(), + PROP_RADIUS, + value.radius, + )], + SolidHistoryOperation::Torus(value) => vec![ + history_prop( + t!("Major Radius").as_ref(), + PROP_MAJOR_RADIUS, + value.major_radius, + ), + history_prop( + t!("Minor Radius").as_ref(), + PROP_MINOR_RADIUS, + value.minor_radius, + ), + ], + SolidHistoryOperation::Pyramid(value) => vec![ + history_prop(t!("Radius").as_ref(), PROP_RADIUS, value.radius), + history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height), + history_prop(t!("Sides").as_ref(), PROP_SIDES, value.sides), + ], + _ => return Vec::new(), + }; + vec![PropSection { + title: t!("Primitive").into_owned(), + props, + }] +} + +pub fn is_primitive_property(field: &str) -> bool { + matches!( + field, + PROP_LENGTH + | PROP_WIDTH + | PROP_HEIGHT + | PROP_RADIUS + | PROP_MAJOR_RADIUS + | PROP_MINOR_RADIUS + | PROP_SIDES + ) +} + +pub fn apply_primitive_property( + operation: &mut SolidHistoryOperation, + field: &str, + value: &str, +) -> bool { + let Ok(number) = value.trim().parse::() else { + return false; + }; + if !number.is_finite() { + return false; + } + let positive = || (number > 0.0).then_some(number); + match operation { + SolidHistoryOperation::Box(value) | 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), + _ => return false, + }, + SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => match field { + PROP_RADIUS => { + let Some(radius) = positive() else { + return false; + }; + value.major_radius = radius; + value.minor_radius = radius; + value.x_radius = radius; + } + PROP_HEIGHT => value.height = positive().unwrap_or(value.height), + _ => return false, + }, + SolidHistoryOperation::Sphere(value) if field == PROP_RADIUS => { + value.radius = positive().unwrap_or(value.radius); + } + SolidHistoryOperation::Torus(value) => match field { + PROP_MAJOR_RADIUS => value.major_radius = positive().unwrap_or(value.major_radius), + PROP_MINOR_RADIUS => value.minor_radius = positive().unwrap_or(value.minor_radius), + _ => return false, + }, + SolidHistoryOperation::Pyramid(value) => match field { + PROP_RADIUS => value.radius = positive().unwrap_or(value.radius), + PROP_HEIGHT => value.height = positive().unwrap_or(value.height), + PROP_SIDES => { + let rounded = number.round(); + if (number - rounded).abs() > 1e-9 { + return false; + } + let sides = rounded as i32; + if !(3..=71).contains(&sides) { + return false; + } + value.sides = sides; + } + _ => return false, + }, + _ => return false, + } + positive().is_some() || field == PROP_SIDES +} + fn matrix(transform: [f64; 16]) -> Option { let matrix = glam::DMat4::from_cols_array(&transform); (matrix.is_finite() && matrix.determinant().abs() > 1e-12).then_some(matrix) diff --git a/src/scene/modify.rs b/src/scene/modify.rs index 158f2b648..4001d93ec 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -788,6 +788,62 @@ impl Scene { self.rebuild_solid_history(handle, operation) } + pub fn apply_solid_history_property( + &mut self, + handle: Handle, + field: &str, + value: &str, + ) -> bool { + let Some(mut operation) = self.document.solid_history_operation(handle).cloned() else { + return false; + }; + if !crate::scene::model::solid_history::apply_primitive_property( + &mut operation, + field, + value, + ) { + return false; + } + self.rebuild_solid_history(handle, operation) + } + + pub fn apply_solid_position_property( + &mut self, + handle: Handle, + field: &str, + value: &str, + plane: crate::command::WorkingPlane, + ) -> Option { + let axis = match field { + "s3d_px" | "rgn_px" | "bdy_px" | "srf_px" => 0, + "s3d_py" | "rgn_py" | "bdy_py" | "srf_py" => 1, + "s3d_pz" | "rgn_pz" | "bdy_pz" | "srf_pz" => 2, + _ => return None, + }; + let Ok(target) = value.trim().parse::() else { + return Some(false); + }; + if !target.is_finite() { + return Some(false); + } + let Some(point) = self + .document + .get_entity(handle) + .and_then(crate::entities::solid3d::point_of_reference) + else { + return Some(false); + }; + let world = glam::DVec3::new(point.x, point.y, point.z); + let mut local = plane.to_local(world); + local[axis] = target; + let delta = plane.to_world(local) - world; + if delta.length_squared() <= f64::EPSILON { + return Some(true); + } + self.transform_entities(&[handle], &EntityTransform::Translate(delta)); + Some(true) + } + pub(crate) fn translate_solid_geometry(&mut self, handle: Handle, delta: [f64; 3]) { if delta.iter().all(|value| value.abs() <= f64::EPSILON) { return;