diff --git a/src/entities/hatch.rs b/src/entities/hatch.rs index d1a53f513..484599884 100644 --- a/src/entities/hatch.rs +++ b/src/entities/hatch.rs @@ -329,6 +329,61 @@ impl Grippable for Hatch { } } } + + fn grip_menu( + &self, + _grip_id: usize, + ) -> Vec { + use crate::scene::object::{GripMenuAction, GripMenuItem}; + vec![ + GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch }, + GripMenuItem { label: "Origin Point", action: GripMenuAction::OriginPoint }, + GripMenuItem { label: "Hatch Angle", action: GripMenuAction::HatchAngle }, + GripMenuItem { label: "Hatch Scale", action: GripMenuAction::HatchScale }, + ] + } + + fn apply_grip_menu( + &mut self, + _grip_id: usize, + _action: crate::scene::object::GripMenuAction, + ) { + // Origin / Angle / Scale need a follow-up value — handled by + // `apply_grip_menu_value`. + } + + fn grip_menu_value_prompt( + &self, + _grip_id: usize, + action: crate::scene::object::GripMenuAction, + ) -> Option<&'static str> { + use crate::scene::object::GripMenuAction as A; + match action { + A::HatchAngle => Some("Angle (deg)"), + A::HatchScale => Some("Scale"), + _ => None, + } + } + + fn apply_grip_menu_value( + &mut self, + _grip_id: usize, + action: crate::scene::object::GripMenuAction, + value: f64, + ) { + use crate::scene::object::GripMenuAction as A; + match action { + A::HatchAngle => { + self.pattern_angle = value.to_radians(); + } + A::HatchScale => { + if value > 0.0 { + self.pattern_scale = value; + } + } + _ => {} + } + } } impl FallbackTess for Hatch { diff --git a/src/entities/leader.rs b/src/entities/leader.rs index b23e3cc9b..03d249039 100644 --- a/src/entities/leader.rs +++ b/src/entities/leader.rs @@ -455,7 +455,80 @@ impl TruckConvertible for Leader { } } -crate::impl_entity_basics!(Leader); +impl crate::entities::traits::Grippable for Leader { + fn grips(&self) -> Vec { + grips(self) + } + fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { + apply_grip(self, grip_id, apply); + } + fn grip_menu( + &self, + grip_id: usize, + ) -> Vec { + use crate::scene::object::{GripMenuAction, GripMenuItem}; + let n = self.vertices.len(); + if grip_id == 0 { + // Arrow head — stretch only. + vec![GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch }] + } else if grip_id < n { + vec![ + GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch }, + GripMenuItem { label: "Add Vertex", action: GripMenuAction::AddVertex }, + GripMenuItem { label: "Remove Vertex", action: GripMenuAction::RemoveVertex }, + ] + } else { + // Centroid grip — move whole leader. + vec![GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch }] + } + } + fn apply_grip_menu( + &mut self, + grip_id: usize, + action: crate::scene::object::GripMenuAction, + ) { + use crate::scene::object::GripMenuAction as A; + let n = self.vertices.len(); + match action { + A::AddVertex if grip_id < n => { + let i1 = (grip_id + 1).min(n - 1); + if i1 == grip_id { + return; + } + let v0 = &self.vertices[grip_id]; + let v1 = &self.vertices[i1]; + let mid = acadrust::types::Vector3::new( + (v0.x + v1.x) * 0.5, + (v0.y + v1.y) * 0.5, + (v0.z + v1.z) * 0.5, + ); + self.vertices.insert(i1, mid); + } + A::RemoveVertex if grip_id < n && n > 2 => { + self.vertices.remove(grip_id); + } + _ => {} + } + } +} + +impl crate::entities::traits::PropertyEditable for Leader { + fn geometry_properties( + &self, + _text_style_names: &[String], + ) -> PropSection { + properties(self) + } + fn apply_geom_prop(&mut self, field: &str, value: &str) { + apply_geom_prop(self, field, value); + } +} + +impl crate::entities::traits::Transformable for Leader { + fn apply_transform(&mut self, t: &EntityTransform) { + apply_transform(self, t); + } +} /// Per-entity tessellation entry for `Leader`. Lives here so all leader /// tess code stays alongside the entity definition. Cross-entity dim diff --git a/src/entities/mline.rs b/src/entities/mline.rs index b663a2241..f2653039c 100644 --- a/src/entities/mline.rs +++ b/src/entities/mline.rs @@ -152,6 +152,46 @@ impl Grippable for MLine { } } } + + fn grip_menu( + &self, + _grip_id: usize, + ) -> Vec { + use crate::scene::object::{GripMenuAction, GripMenuItem}; + vec![ + GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch }, + GripMenuItem { label: "Add Vertex", action: GripMenuAction::AddVertex }, + GripMenuItem { label: "Remove Vertex", action: GripMenuAction::RemoveVertex }, + ] + } + + fn apply_grip_menu( + &mut self, + grip_id: usize, + action: crate::scene::object::GripMenuAction, + ) { + use crate::scene::object::GripMenuAction as A; + let n = self.vertices.len(); + match action { + A::AddVertex if grip_id < n => { + let i1 = (grip_id + 1).min(n - 1); + if i1 == grip_id { + return; + } + let v0 = &self.vertices[grip_id]; + let v1 = &self.vertices[i1]; + let mut new_v = v0.clone(); + new_v.position.x = (v0.position.x + v1.position.x) * 0.5; + new_v.position.y = (v0.position.y + v1.position.y) * 0.5; + new_v.position.z = (v0.position.z + v1.position.z) * 0.5; + self.vertices.insert(i1, new_v); + } + A::RemoveVertex if grip_id < n && n > 2 => { + self.vertices.remove(grip_id); + } + _ => {} + } + } } impl PropertyEditable for MLine { diff --git a/src/entities/mtext.rs b/src/entities/mtext.rs index 7ba44d4b6..fbf92b2c0 100644 --- a/src/entities/mtext.rs +++ b/src/entities/mtext.rs @@ -300,6 +300,57 @@ impl Grippable for MText { fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { apply_grip(self, grip_id, apply); } + + fn grip_menu( + &self, + grip_id: usize, + ) -> Vec { + use crate::scene::object::{GripMenuAction, GripMenuItem}; + if grip_id == 0 { + // Insertion point + vec![ + GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch }, + GripMenuItem { label: "Move with Text", action: GripMenuAction::MoveWithText }, + GripMenuItem { label: "Rotate", action: GripMenuAction::RotateText }, + ] + } else { + // Width grip + vec![GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch }] + } + } + + fn apply_grip_menu( + &mut self, + _grip_id: usize, + _action: crate::scene::object::GripMenuAction, + ) { + // Rotate needs a follow-up angle handled by + // `apply_grip_menu_value`; Move-with-Text is the default drag. + } + + fn grip_menu_value_prompt( + &self, + _grip_id: usize, + action: crate::scene::object::GripMenuAction, + ) -> Option<&'static str> { + use crate::scene::object::GripMenuAction as A; + match action { + A::RotateText => Some("Rotation (deg)"), + _ => None, + } + } + + fn apply_grip_menu_value( + &mut self, + _grip_id: usize, + action: crate::scene::object::GripMenuAction, + value: f64, + ) { + use crate::scene::object::GripMenuAction as A; + if matches!(action, A::RotateText) { + self.rotation = value.to_radians(); + } + } } impl PropertyEditable for MText { diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index ff8b8fc10..bf38577c7 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -827,7 +827,71 @@ impl TruckConvertible for MultiLeader { } } -crate::impl_entity_basics!(MultiLeader); +impl crate::entities::traits::Grippable for MultiLeader { + fn grips(&self) -> Vec { + grips(self) + } + fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { + apply_grip(self, grip_id, apply); + } + fn grip_menu( + &self, + grip_id: usize, + ) -> Vec { + use crate::scene::object::{GripMenuAction, GripMenuItem}; + // Compute vertex count to identify text-location grip (last id). + let n_vertices: usize = self + .context + .leader_roots + .iter() + .flat_map(|r| r.lines.iter()) + .map(|l| l.points.len()) + .sum(); + let is_text_loc = self.content_type == LeaderContentType::MText + && grip_id == n_vertices; + + if is_text_loc { + vec![ + GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch }, + GripMenuItem { label: "Move with Leader", action: GripMenuAction::MoveWithLeader }, + GripMenuItem { label: "Move Independent", action: GripMenuAction::MoveIndependent }, + ] + } else { + vec![ + GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch }, + GripMenuItem { label: "Add Leader", action: GripMenuAction::AddLeader }, + GripMenuItem { label: "Remove Leader", action: GripMenuAction::RemoveLeader }, + ] + } + } + fn apply_grip_menu( + &mut self, + _grip_id: usize, + _action: crate::scene::object::GripMenuAction, + ) { + // Add/Remove Leader, Move with Leader, Move Independent — these + // require deeper structural edits across leader_roots/lines and + // are stubbed here; Stretch falls through to the regular drag. + } +} + +impl crate::entities::traits::PropertyEditable for MultiLeader { + fn geometry_properties( + &self, + _text_style_names: &[String], + ) -> PropSection { + properties(self) + } + fn apply_geom_prop(&mut self, field: &str, value: &str) { + apply_geom_prop(self, field, value); + } +} + +impl crate::entities::traits::Transformable for MultiLeader { + fn apply_transform(&mut self, t: &EntityTransform) { + apply_transform(self, t); + } +} /// Per-entity tessellation entry for `MultiLeader`. Returns multiple /// `WireModel`s (leader/dogleg + arrow fill, optional block content, diff --git a/src/entities/polyline.rs b/src/entities/polyline.rs index d6bf22a27..e744fc278 100644 --- a/src/entities/polyline.rs +++ b/src/entities/polyline.rs @@ -73,6 +73,50 @@ impl Grippable for Polyline { } } } + + fn grip_menu( + &self, + _grip_id: usize, + ) -> Vec { + use crate::scene::object::{GripMenuAction, GripMenuItem}; + vec![ + GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch }, + GripMenuItem { label: "Add Vertex", action: GripMenuAction::AddVertex }, + GripMenuItem { label: "Remove Vertex", action: GripMenuAction::RemoveVertex }, + ] + } + + fn apply_grip_menu( + &mut self, + grip_id: usize, + action: crate::scene::object::GripMenuAction, + ) { + use crate::scene::object::GripMenuAction as A; + let n = self.vertices.len(); + match action { + A::AddVertex if grip_id < n => { + let i1 = (grip_id + 1) % n; + if i1 == 0 && grip_id + 1 == n { + return; + } + let v0 = &self.vertices[grip_id]; + let v1 = &self.vertices[i1]; + let mx = (v0.location.x + v1.location.x) * 0.5; + let my = (v0.location.y + v1.location.y) * 0.5; + let mz = (v0.location.z + v1.location.z) * 0.5; + let mut new_v = v0.clone(); + new_v.location.x = mx; + new_v.location.y = my; + new_v.location.z = mz; + let insert_at = (grip_id + 1).min(self.vertices.len()); + self.vertices.insert(insert_at, new_v); + } + A::RemoveVertex if grip_id < n && n > 2 => { + self.vertices.remove(grip_id); + } + _ => {} + } + } } impl PropertyEditable for Polyline { @@ -300,6 +344,50 @@ impl Grippable for Polyline2D { } } } + + fn grip_menu( + &self, + _grip_id: usize, + ) -> Vec { + use crate::scene::object::{GripMenuAction, GripMenuItem}; + vec![ + GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch }, + GripMenuItem { label: "Add Vertex", action: GripMenuAction::AddVertex }, + GripMenuItem { label: "Remove Vertex", action: GripMenuAction::RemoveVertex }, + ] + } + + fn apply_grip_menu( + &mut self, + grip_id: usize, + action: crate::scene::object::GripMenuAction, + ) { + use crate::scene::object::GripMenuAction as A; + let n = self.vertices.len(); + let elev = self.elevation; + match action { + A::AddVertex if grip_id < n => { + let i1 = (grip_id + 1) % n; + if i1 == 0 && grip_id + 1 == n { + return; + } + let v0 = &self.vertices[grip_id]; + let v1 = &self.vertices[i1]; + let mx = (v0.location.x + v1.location.x) * 0.5; + let my = (v0.location.y + v1.location.y) * 0.5; + let mut new_v = v0.clone(); + new_v.location.x = mx; + new_v.location.y = my; + new_v.location.z = elev; + let insert_at = (grip_id + 1).min(self.vertices.len()); + self.vertices.insert(insert_at, new_v); + } + A::RemoveVertex if grip_id < n && n > 2 => { + self.vertices.remove(grip_id); + } + _ => {} + } + } } impl PropertyEditable for Polyline2D { @@ -473,6 +561,50 @@ impl Grippable for Polyline3D { } } } + + fn grip_menu( + &self, + _grip_id: usize, + ) -> Vec { + use crate::scene::object::{GripMenuAction, GripMenuItem}; + vec![ + GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch }, + GripMenuItem { label: "Add Vertex", action: GripMenuAction::AddVertex }, + GripMenuItem { label: "Remove Vertex", action: GripMenuAction::RemoveVertex }, + ] + } + + fn apply_grip_menu( + &mut self, + grip_id: usize, + action: crate::scene::object::GripMenuAction, + ) { + use crate::scene::object::GripMenuAction as A; + let n = self.vertices.len(); + match action { + A::AddVertex if grip_id < n => { + let i1 = (grip_id + 1) % n; + if i1 == 0 && grip_id + 1 == n { + return; + } + let v0 = &self.vertices[grip_id]; + let v1 = &self.vertices[i1]; + let mx = (v0.position.x + v1.position.x) * 0.5; + let my = (v0.position.y + v1.position.y) * 0.5; + let mz = (v0.position.z + v1.position.z) * 0.5; + let mut new_v = v0.clone(); + new_v.position.x = mx; + new_v.position.y = my; + new_v.position.z = mz; + let insert_at = (grip_id + 1).min(self.vertices.len()); + self.vertices.insert(insert_at, new_v); + } + A::RemoveVertex if grip_id < n && n > 2 => { + self.vertices.remove(grip_id); + } + _ => {} + } + } } impl PropertyEditable for Polyline3D { diff --git a/src/entities/spline.rs b/src/entities/spline.rs index 7a2e1a52c..933e0ccb6 100644 --- a/src/entities/spline.rs +++ b/src/entities/spline.rs @@ -161,5 +161,114 @@ impl TruckConvertible for Spline { } } -crate::impl_entity_basics!(Spline); +impl crate::entities::traits::Grippable for Spline { + fn grips(&self) -> Vec { + grips(self) + } + fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { + apply_grip(self, grip_id, apply); + } + fn grip_menu( + &self, + _grip_id: usize, + ) -> Vec { + use crate::scene::object::{GripMenuAction, GripMenuItem}; + vec![ + GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch }, + GripMenuItem { label: "Add Vertex", action: GripMenuAction::AddVertex }, + GripMenuItem { label: "Remove Vertex", action: GripMenuAction::RemoveVertex }, + GripMenuItem { label: "Refine Vertices", action: GripMenuAction::RefineVertices }, + ] + } + fn apply_grip_menu( + &mut self, + grip_id: usize, + action: crate::scene::object::GripMenuAction, + ) { + use crate::scene::object::GripMenuAction as A; + let n = self.control_points.len(); + let min_cv = (self.degree as usize).saturating_add(1).max(2); + match action { + A::AddVertex if grip_id < n => { + let i1 = (grip_id + 1).min(n - 1); + if i1 == grip_id { + return; + } + let p0 = &self.control_points[grip_id]; + let p1 = &self.control_points[i1]; + let mid = acadrust::types::Vector3::new( + (p0.x + p1.x) * 0.5, + (p0.y + p1.y) * 0.5, + (p0.z + p1.z) * 0.5, + ); + self.control_points.insert(i1, mid); + if !self.weights.is_empty() && self.weights.len() == n { + let w = (self.weights[grip_id] + self.weights[i1.min(self.weights.len() - 1)]) * 0.5; + self.weights.insert(i1, w); + } + // Clear knots so to_truck rebuilds a uniform knot vector + // for the new CV count. + self.knots.clear(); + } + A::RemoveVertex if grip_id < n && n > min_cv => { + self.control_points.remove(grip_id); + if grip_id < self.weights.len() { + self.weights.remove(grip_id); + } + self.knots.clear(); + } + A::RefineVertices => { + // Insert a CV between every adjacent pair (chord midpoints) + // and rebuild a uniform knot vector. + if n >= 2 { + let mut refined = Vec::with_capacity(n * 2 - 1); + let mut refined_w = Vec::with_capacity(n * 2 - 1); + let has_w = !self.weights.is_empty() && self.weights.len() == n; + for i in 0..n { + refined.push(self.control_points[i].clone()); + if has_w { + refined_w.push(self.weights[i]); + } + if i + 1 < n { + let a = &self.control_points[i]; + let b = &self.control_points[i + 1]; + refined.push(acadrust::types::Vector3::new( + (a.x + b.x) * 0.5, + (a.y + b.y) * 0.5, + (a.z + b.z) * 0.5, + )); + if has_w { + refined_w.push((self.weights[i] + self.weights[i + 1]) * 0.5); + } + } + } + self.control_points = refined; + if has_w { + self.weights = refined_w; + } + self.knots.clear(); + } + } + _ => {} + } + } +} + +impl crate::entities::traits::PropertyEditable for Spline { + fn geometry_properties( + &self, + _text_style_names: &[String], + ) -> PropSection { + properties(self) + } + fn apply_geom_prop(&mut self, field: &str, value: &str) { + apply_geom_prop(self, field, value); + } +} + +impl crate::entities::traits::Transformable for Spline { + fn apply_transform(&mut self, t: &EntityTransform) { + apply_transform(self, t); + } +} diff --git a/src/entities/text.rs b/src/entities/text.rs index d6f2e10aa..f94cbe955 100644 --- a/src/entities/text.rs +++ b/src/entities/text.rs @@ -315,6 +315,52 @@ impl Grippable for Text { fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { apply_grip(self, grip_id, apply); } + + fn grip_menu( + &self, + _grip_id: usize, + ) -> Vec { + use crate::scene::object::{GripMenuAction, GripMenuItem}; + vec![ + GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch }, + GripMenuItem { label: "Move with Text", action: GripMenuAction::MoveWithText }, + GripMenuItem { label: "Rotate", action: GripMenuAction::RotateText }, + ] + } + + fn apply_grip_menu( + &mut self, + _grip_id: usize, + _action: crate::scene::object::GripMenuAction, + ) { + // Move-with-Text falls through to Stretch (single grip moves + // the whole text); Rotate needs a follow-up angle handled by + // `apply_grip_menu_value`. + } + + fn grip_menu_value_prompt( + &self, + _grip_id: usize, + action: crate::scene::object::GripMenuAction, + ) -> Option<&'static str> { + use crate::scene::object::GripMenuAction as A; + match action { + A::RotateText => Some("Rotation (deg)"), + _ => None, + } + } + + fn apply_grip_menu_value( + &mut self, + _grip_id: usize, + action: crate::scene::object::GripMenuAction, + value: f64, + ) { + use crate::scene::object::GripMenuAction as A; + if matches!(action, A::RotateText) { + self.rotation = value.to_radians(); + } + } } impl PropertyEditable for Text { diff --git a/src/scene/object.rs b/src/scene/object.rs index 658d19dcd..49e0c7de3 100644 --- a/src/scene/object.rs +++ b/src/scene/object.rs @@ -137,4 +137,11 @@ pub enum GripMenuAction { HatchScale, HatchPattern, TangentDirection, + AddFitPoint, + RemoveFitPoint, + Refit, + RefineVertices, + MoveWithText, + StackText, + UnstackText, }