diff --git a/src/app/properties.rs b/src/app/properties.rs index cf4a5d093..62bb3f19c 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -441,14 +441,14 @@ 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( + let specialized_primitive = + crate::scene::model::solid_history::has_specialized_primitive_properties( &self.tabs[i].scene.document, handle, ); let mut sections = dispatch::properties_sectioned(handle, entity, &text_style_names); - if box_primitive { + if specialized_primitive { sections.retain(|section| { !section.props.iter().any(|property| { property.field.starts_with("acis_") @@ -620,7 +620,7 @@ impl OpenCADStudio { } } - if !box_primitive && matches!( + if !specialized_primitive && matches!( entity, acadrust::EntityType::Solid3D(_) | acadrust::EntityType::Region(_) @@ -694,7 +694,7 @@ impl OpenCADStudio { } } - if !box_primitive { + if !specialized_primitive { sections.extend(crate::entities::object_data::sections( &self.tabs[i].scene.document, &self.tabs[i].scene.object_data_cache, diff --git a/src/app/update/command.rs b/src/app/update/command.rs index ce1b6256d..a73fac21f 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1940,7 +1940,16 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } self.push_undo_snapshot(i, "CHPROP"); - if crate::scene::model::solid_history::is_history_choice(field) { + if crate::scene::model::solid_history::is_primitive_choice(field) { + for &handle in &handles { + if self.tabs[i].scene.is_layer_locked(handle) { + continue; + } + self.tabs[i] + .scene + .apply_solid_history_property(handle, field, &value); + } + } else if crate::scene::model::solid_history::is_history_choice(field) { for &handle in &handles { if self.tabs[i].scene.is_layer_locked(handle) { continue; diff --git a/src/entities/object_data.rs b/src/entities/object_data.rs index 1807ee628..bed43563d 100644 --- a/src/entities/object_data.rs +++ b/src/entities/object_data.rs @@ -151,7 +151,8 @@ fn history_step_id(operation: &SolidHistoryOperation) -> Option { SolidHistoryOperation::Unknown => return None, SolidHistoryOperation::Box(value) | SolidHistoryOperation::Wedge(value) => &value.base, SolidHistoryOperation::Sphere(value) => &value.base, - SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => &value.base, + SolidHistoryOperation::Cylinder(value) => &value.base, + SolidHistoryOperation::Cone(value) => &value.base, SolidHistoryOperation::Pyramid(value) => &value.base, SolidHistoryOperation::Torus(value) => &value.base, SolidHistoryOperation::Boolean(value) => &value.base, @@ -225,7 +226,7 @@ fn history_operation_text(operation: &SolidHistoryOperation) -> String { history_base_text(&value.base), value.radius ), - SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => format!( + SolidHistoryOperation::Cylinder(value) => format!( "{}; height {:.6}; radii {:.6}/{:.6}; x-radius {:.6}", history_base_text(&value.base), value.height, @@ -233,6 +234,14 @@ fn history_operation_text(operation: &SolidHistoryOperation) -> String { value.minor_radius, value.x_radius ), + SolidHistoryOperation::Cone(value) => format!( + "{}; height {:.6}; base radii {:.6}/{:.6}; top radius {:.6}", + history_base_text(&value.base), + value.height, + value.base_x_radius, + value.base_y_radius, + value.top_radius + ), SolidHistoryOperation::Pyramid(value) => format!( "{}; height {:.6}; sides {}; radii {:.6}/{:.6}", history_base_text(&value.base), diff --git a/src/scene/mod.rs b/src/scene/mod.rs index afa0e3be8..e54209b74 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2746,6 +2746,7 @@ impl Scene { self.meshes.remove(&handle); } self.solid_models.insert(handle, solid); + self.sync_solid_reference_point(handle); // Only this solid's mesh changed — report just its handle so the mesh / // wire caches patch it in rather than rebuilding the whole drawing (and // so a bulk register loop stays O(n), not O(n²)). diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index a7c05f7be..dc00c16ea 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -1,6 +1,6 @@ use acadrust::entities::Solid3D; use acadrust::objects::{ - DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, + DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryCone, SolidHistoryCylinder, SolidHistoryNodeBase, SolidHistoryOperation, SolidHistoryPyramid, SolidHistorySphere, SolidHistoryTorus, }; @@ -31,6 +31,9 @@ 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_BASE_RADIUS: &str = "solid_history_base_radius"; +pub const PROP_TOP_RADIUS: &str = "solid_history_top_radius"; +pub const PROP_ELLIPTICAL: &str = "solid_history_elliptical"; 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"; @@ -73,16 +76,108 @@ pub fn is_box_primitive( ) } +pub fn has_specialized_primitive_properties( + document: &acadrust::CadDocument, + handle: acadrust::Handle, +) -> bool { + matches!( + document.solid_history_operation(handle), + Some(SolidHistoryOperation::Box(_) | SolidHistoryOperation::Cone(_)) + ) +} + 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], ), + SolidHistoryOperation::Cone(value) => world_point(value.base.transform, [0.0; 3]), _ => None, } } +fn cone_properties( + document: &acadrust::CadDocument, + handle: acadrust::Handle, + value: &SolidHistoryCone, +) -> Vec { + let Some(position) = world_point(value.base.transform, [0.0; 3]) else { + return Vec::new(); + }; + let scale = value + .base_x_radius + .abs() + .max(value.base_y_radius.abs()) + .max(1.0); + let elliptical = (value.base_x_radius - value.base_y_radius).abs() > 1e-9 * scale; + 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!("Cone").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, + ), + Property { + label: t!("Elliptical").into_owned(), + field: PROP_ELLIPTICAL, + value: PropValue::Choice { + selected: if elliptical { "Yes" } else { "No" }.to_string(), + options: vec!["No".to_string(), "Yes".to_string()], + }, + }, + history_prop(t!("Base radius").as_ref(), PROP_BASE_RADIUS, value.base_x_radius), + history_prop(t!("Top radius").as_ref(), PROP_TOP_RADIUS, value.top_radius), + history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height), + ], + }, + 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()) + }, + }, + ], + }, + ] +} + fn box_properties( document: &acadrust::CadDocument, handle: acadrust::Handle, @@ -184,12 +279,13 @@ pub fn primitive_properties( }; let props = match operation { SolidHistoryOperation::Box(value) => return box_properties(document, handle, value), + SolidHistoryOperation::Cone(value) => return cone_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), ], - SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => vec![ + SolidHistoryOperation::Cylinder(value) => vec![ history_prop(t!("Radius").as_ref(), PROP_RADIUS, value.major_radius), history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height), ], @@ -230,6 +326,8 @@ pub fn is_primitive_property(field: &str) -> bool { | PROP_WIDTH | PROP_HEIGHT | PROP_RADIUS + | PROP_BASE_RADIUS + | PROP_TOP_RADIUS | PROP_OUTER_RADIUS | PROP_INNER_RADIUS | PROP_SIDES @@ -240,6 +338,10 @@ pub fn is_primitive_property(field: &str) -> bool { ) } +pub fn is_primitive_choice(field: &str) -> bool { + field == PROP_ELLIPTICAL +} + pub fn is_history_choice(field: &str) -> bool { matches!(field, PROP_HISTORY | PROP_SHOW_HISTORY) } @@ -355,6 +457,32 @@ fn apply_box_geometry_property( Some(true) } +fn apply_cone_position_property( + value: &mut SolidHistoryCone, + field: &str, + text: &str, +) -> Option { + 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 origin = current.transform_point3(glam::DVec3::ZERO); + if !origin.is_finite() || target == origin[axis] { + return Some(false); + } + let mut delta = glam::DVec3::ZERO; + delta[axis] = target - origin[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, @@ -365,6 +493,36 @@ pub fn apply_primitive_property( return applied; } } + if let SolidHistoryOperation::Cone(cone_value) = operation { + if let Some(applied) = apply_cone_position_property(cone_value, field, value) { + return applied; + } + if field == PROP_ELLIPTICAL { + let elliptical = if value.eq_ignore_ascii_case("Yes") { + true + } else if value.eq_ignore_ascii_case("No") { + false + } else { + return false; + }; + let scale = cone_value + .base_x_radius + .abs() + .max(cone_value.base_y_radius.abs()) + .max(1.0); + let currently_elliptical = + (cone_value.base_x_radius - cone_value.base_y_radius).abs() > 1e-9 * scale; + if elliptical == currently_elliptical { + return false; + } + cone_value.base_y_radius = if elliptical { + cone_value.base_x_radius * 0.5 + } else { + cone_value.base_x_radius + }; + return true; + } + } let Some(number) = (if field == PROP_SIDES { value.trim().parse::().ok() } else { @@ -417,7 +575,7 @@ pub fn apply_primitive_property( PROP_HEIGHT => value.height = positive().unwrap_or(value.height), _ => return false, }, - SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => match field { + SolidHistoryOperation::Cylinder(value) => match field { PROP_RADIUS => { let Some(radius) = positive() else { return false; @@ -429,6 +587,28 @@ pub fn apply_primitive_property( PROP_HEIGHT => value.height = positive().unwrap_or(value.height), _ => return false, }, + SolidHistoryOperation::Cone(value) => match field { + PROP_BASE_RADIUS => { + let Some(radius) = positive() else { + return false; + }; + let ratio = if value.base_x_radius > 1e-9 { + value.base_y_radius / value.base_x_radius + } else { + 1.0 + }; + value.base_x_radius = radius; + value.base_y_radius = radius * ratio; + } + PROP_TOP_RADIUS => { + if number < 0.0 { + return false; + } + value.top_radius = number; + } + 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); } @@ -477,7 +657,7 @@ pub fn apply_primitive_property( }, _ => return false, } - positive().is_some() || field == PROP_SIDES + positive().is_some() || field == PROP_SIDES || field == PROP_TOP_RADIUS } fn matrix(transform: [f64; 16]) -> Option { @@ -676,7 +856,7 @@ pub fn primitive_grips( Some([0.0, 0.0, 1.0]), ); } - SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => { + SolidHistoryOperation::Cylinder(value) => { add( GRIP_RADIUS, value.base.transform, @@ -692,6 +872,22 @@ pub fn primitive_grips( Some([0.0, 0.0, 1.0]), ); } + SolidHistoryOperation::Cone(value) => { + add( + GRIP_RADIUS, + value.base.transform, + [value.base_x_radius, 0.0, 0.0], + GripShape::Square, + None, + ); + add( + GRIP_HEIGHT, + value.base.transform, + [0.0, 0.0, value.height], + GripShape::Square, + Some([0.0, 0.0, 1.0]), + ); + } SolidHistoryOperation::Sphere(value) => add( GRIP_RADIUS, value.base.transform, @@ -825,7 +1021,7 @@ pub fn apply_primitive_grip( _ => return false, } } - SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => { + SolidHistoryOperation::Cylinder(value) => { match grip_id { GRIP_RADIUS => { let radius = local.x.hypot(local.y).max(1e-6); @@ -837,6 +1033,20 @@ pub fn apply_primitive_grip( _ => return false, } } + SolidHistoryOperation::Cone(value) => match grip_id { + GRIP_RADIUS => { + let radius = local.x.hypot(local.y).max(1e-6); + let ratio = if value.base_x_radius > 1e-9 { + value.base_y_radius / value.base_x_radius + } else { + 1.0 + }; + value.base_x_radius = radius; + value.base_y_radius = radius * ratio; + } + GRIP_HEIGHT => value.height = local.z.max(1e-6), + _ => return false, + }, SolidHistoryOperation::Sphere(value) if grip_id == GRIP_RADIUS => { value.radius = local.length().max(1e-6); } @@ -932,17 +1142,19 @@ pub fn cylinder_op( pub fn cone_op( transform: [f64; 16], - radius: f64, + base_x_radius: f64, + base_y_radius: f64, + top_radius: f64, height: f64, ) -> SolidHistoryOperation { - SolidHistoryOperation::Cone(SolidHistoryCylinder { + SolidHistoryOperation::Cone(SolidHistoryCone { base: base(transform), operation_major: 1, height, - major_radius: radius, - minor_radius: radius, - x_radius: radius, - ..SolidHistoryCylinder::default() + base_x_radius, + base_y_radius, + top_radius, + ..SolidHistoryCone::default() }) } diff --git a/src/scene/modify.rs b/src/scene/modify.rs index f376f9307..917d695b2 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -791,7 +791,11 @@ impl Scene { handle: Handle, operation: acadrust::objects::SolidHistoryOperation, ) -> bool { - let box_primitive = matches!(operation, acadrust::objects::SolidHistoryOperation::Box(_)); + let user_facing_primitive = matches!( + operation, + acadrust::objects::SolidHistoryOperation::Box(_) + | acadrust::objects::SolidHistoryOperation::Cone(_) + ); let Some(graph) = self.document.create_solid_history(handle, operation) else { return false; }; @@ -799,7 +803,7 @@ impl Scene { for node in graph.nodes { self.record_undo_object_before(node, None); } - if box_primitive { + if user_facing_primitive { let _ = crate::scene::model::solid_history::apply_history_choice( &mut self.document, handle, @@ -811,7 +815,7 @@ impl Scene { true } - fn sync_solid_reference_point(&mut self, handle: Handle) { + pub(crate) fn sync_solid_reference_point(&mut self, handle: Handle) { let reference = self .document .solid_history_operation(handle)