diff --git a/src/app/properties.rs b/src/app/properties.rs index ce6168be3..c709723de 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -1573,7 +1573,12 @@ impl OpenCADStudio { entity, annotation_scale_handle, ); - for mut grip in dispatch::grips(contextual.as_ref()) { + let mut entity_grips = dispatch::grips(contextual.as_ref()); + entity_grips.extend(crate::scene::model::solid_history::primitive_grips( + &self.tabs[i].scene.document, + handle, + )); + for mut grip in entity_grips { // Subtract in f64: at UTM magnitudes an f32 cast before // the offset costs ~1 unit and draws the grip off the wire. grip.world.x -= wo[0]; diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index 2e7a1178d..831873bd6 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -8,6 +8,198 @@ use acadrust::types::{Vector2, Vector3}; use acadrust::EntityType; use cadkernel::brep::Body; +use crate::scene::model::object::{GripApply, GripDef, GripShape}; + +pub const GRIP_LENGTH: usize = 10_001; +pub const GRIP_WIDTH: usize = 10_002; +pub const GRIP_HEIGHT: usize = 10_003; +pub const GRIP_RADIUS: usize = 10_004; +pub const GRIP_MAJOR_RADIUS: usize = 10_005; +pub const GRIP_MINOR_RADIUS: usize = 10_006; +pub const GRIP_SIDES: usize = 10_007; + +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) +} + +fn world_point(transform: [f64; 16], point: [f64; 3]) -> Option { + Some(matrix(transform)?.transform_point3(glam::DVec3::from_array(point))) +} + +fn local_point(transform: [f64; 16], point: glam::DVec3) -> Option { + Some(matrix(transform)?.inverse().transform_point3(point)) +} + +fn grip(id: usize, world: glam::DVec3, shape: GripShape) -> GripDef { + GripDef { + id, + world, + is_midpoint: false, + shape, + dir: None, + } +} + +pub fn primitive_grips( + document: &acadrust::CadDocument, + handle: acadrust::Handle, +) -> Vec { + let Some(operation) = document.solid_history_operation(handle) else { + return Vec::new(); + }; + let mut grips = Vec::new(); + let mut add = |id, transform, point, shape| { + if let Some(world) = world_point(transform, point) { + grips.push(grip(id, world, shape)); + } + }; + match operation { + SolidHistoryOperation::Box(value) | SolidHistoryOperation::Wedge(value) => { + add( + GRIP_LENGTH, + value.base.transform, + [value.length, value.width * 0.5, 0.0], + GripShape::Square, + ); + add( + GRIP_WIDTH, + value.base.transform, + [value.length * 0.5, value.width, 0.0], + GripShape::Square, + ); + add( + GRIP_HEIGHT, + value.base.transform, + [value.length * 0.5, value.width * 0.5, value.height], + GripShape::Square, + ); + } + SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => { + add( + GRIP_RADIUS, + value.base.transform, + [value.major_radius, 0.0, value.height * 0.5], + GripShape::Square, + ); + add( + GRIP_HEIGHT, + value.base.transform, + [0.0, 0.0, value.height], + GripShape::Square, + ); + } + SolidHistoryOperation::Sphere(value) => add( + GRIP_RADIUS, + value.base.transform, + [value.radius, 0.0, 0.0], + GripShape::Square, + ), + SolidHistoryOperation::Torus(value) => { + add( + GRIP_MAJOR_RADIUS, + value.base.transform, + [value.major_radius, 0.0, 0.0], + GripShape::Square, + ); + add( + GRIP_MINOR_RADIUS, + value.base.transform, + [value.major_radius + value.minor_radius, 0.0, 0.0], + GripShape::Square, + ); + } + SolidHistoryOperation::Pyramid(value) => { + add( + GRIP_RADIUS, + value.base.transform, + [value.radius, 0.0, 0.0], + GripShape::Square, + ); + add( + GRIP_HEIGHT, + value.base.transform, + [0.0, 0.0, value.height], + GripShape::Square, + ); + let angle = (value.sides.clamp(3, 71) as f64 * 5.0).to_radians(); + add( + GRIP_SIDES, + value.base.transform, + [value.radius * angle.cos(), value.radius * angle.sin(), 0.0], + GripShape::Triangle, + ); + } + _ => {} + } + grips +} + +pub fn apply_primitive_grip( + operation: &mut SolidHistoryOperation, + grip_id: usize, + apply: GripApply, +) -> bool { + let GripApply::Absolute(world) = apply else { + return false; + }; + let Some(transform) = operation.base().map(|base| base.transform) else { + return false; + }; + let Some(local) = local_point(transform, world) else { + return false; + }; + let positive = |value: f64| value.abs().max(1e-6); + match operation { + SolidHistoryOperation::Box(value) | SolidHistoryOperation::Wedge(value) => { + match grip_id { + GRIP_LENGTH => value.length = positive(local.x), + GRIP_WIDTH => value.width = positive(local.y), + GRIP_HEIGHT => value.height = positive(local.z), + _ => return false, + } + } + SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => { + match grip_id { + GRIP_RADIUS => { + let radius = local.x.hypot(local.y).max(1e-6); + value.major_radius = radius; + value.minor_radius = radius; + value.x_radius = radius; + } + GRIP_HEIGHT => value.height = positive(local.z), + _ => return false, + } + } + SolidHistoryOperation::Sphere(value) if grip_id == GRIP_RADIUS => { + value.radius = local.length().max(1e-6); + } + SolidHistoryOperation::Torus(value) => match grip_id { + GRIP_MAJOR_RADIUS => { + value.major_radius = local.x.hypot(local.y).max(1e-6) + } + GRIP_MINOR_RADIUS => { + value.minor_radius = (local.x.hypot(local.y) - value.major_radius) + .abs() + .max(1e-6) + } + _ => return false, + }, + SolidHistoryOperation::Pyramid(value) => match grip_id { + GRIP_RADIUS => value.radius = local.x.hypot(local.y).max(1e-6), + GRIP_HEIGHT => value.height = positive(local.z), + GRIP_SIDES => { + let angle = local.y.atan2(local.x).rem_euclid(std::f64::consts::TAU); + value.sides = (angle.to_degrees() / 5.0).round() as i32; + value.sides = value.sides.clamp(3, 71); + } + _ => return false, + }, + _ => return false, + } + true +} + fn base(transform: [f64; 16]) -> SolidHistoryNodeBase { let mut base = SolidHistoryNodeBase::new(1); base.transform = transform; diff --git a/src/scene/modify.rs b/src/scene/modify.rs index ef72c8e4c..fb76d34f6 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -625,6 +625,54 @@ impl Scene { // ── Grip editing ────────────────────────────────────────────────────── + pub fn rebuild_solid_history( + &mut self, + handle: Handle, + operation: acadrust::objects::SolidHistoryOperation, + ) -> bool { + let Ok(body) = cadkernel::acis::rebuild_body(&operation) else { + return false; + }; + let Some(document) = crate::scene::convert::acis_export::planar_solid_to_sat(&body) else { + return false; + }; + let wires = crate::scene::model::solid_model::edge_wires(&body); + if self + .document + .update_solid_history(handle, operation) + .is_none() + { + return false; + } + let Some(EntityType::Solid3D(entity)) = self.document.get_entity_mut(handle) else { + return false; + }; + entity.set_sat_document(&document); + entity.wires = wires; + entity.silhouettes.clear(); + self.register_solid_model(handle, body); + true + } + + fn apply_solid_history_grip( + &mut self, + handle: Handle, + grip_id: usize, + apply: GripApply, + ) -> bool { + let Some(mut operation) = self.document.solid_history_operation(handle).cloned() else { + return false; + }; + if !crate::scene::model::solid_history::apply_primitive_grip( + &mut operation, + grip_id, + apply, + ) { + return false; + } + self.rebuild_solid_history(handle, operation) + } + pub(crate) fn translate_solid_geometry(&mut self, handle: Handle, delta: [f64; 3]) { if delta.iter().all(|value| value.abs() <= f64::EPSILON) { return; @@ -683,6 +731,9 @@ impl Scene { if self.is_layer_locked(handle) { return; } + if self.apply_solid_history_grip(handle, grip_id, apply.clone()) { + return; + } // For Solid3D / Region / Body, record the old point_of_reference so we // can translate the pre-tessellated MeshModel by the same delta after // the grip is applied (the ACIS data itself is not modified).