diff --git a/src/app/commands.rs b/src/app/commands.rs index ac180707a..e59d484e2 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -2102,7 +2102,8 @@ impl OpenCADStudio { } else { for (handle, _) in &selected { if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) { - if let Some(props) = massprop_entity(entity) { + use crate::entities::traits::EntityTypeOps; + if let Some(props) = entity.mass_props() { self.command_line.push_output(&format!( "{} Area={:.4} Perimeter={:.4} Centroid=({:.4},{:.4})", entity_type_name(entity), @@ -2264,7 +2265,7 @@ impl OpenCADStudio { .document .entities() .filter_map(|e| { - let txt = entity_text_content(e)?; + use crate::entities::traits::EntityTypeOps; let txt = e.text_content()?; if txt.to_lowercase().contains(&search_lc) { Some(e.common().handle) } else { @@ -2287,7 +2288,7 @@ impl OpenCADStudio { self.push_undo_snapshot(i, "FIND/REPLACE"); for h in &targets { if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) { - replace_entity_text(e, search, rep); + crate::entities::traits::EntityTypeOps::replace_text(e, search, rep); count += 1; } } @@ -2306,7 +2307,7 @@ impl OpenCADStudio { } else { for h in &handles { if let Some(e) = self.tabs[i].scene.document.get_entity(*h) { - let txt = entity_text_content(e).unwrap_or_default(); + use crate::entities::traits::EntityTypeOps; let txt = e.text_content().unwrap_or_default(); self.command_line.push_output(&format!( " Handle {:X}: \"{}\"", h.value(), @@ -5378,196 +5379,7 @@ fn entity_type_name(entity: &acadrust::EntityType) -> &'static str { } } -fn entity_text_content(entity: &acadrust::EntityType) -> Option { - match entity { - acadrust::EntityType::Text(t) => Some(t.value.clone()), - acadrust::EntityType::MText(t) => Some(t.value.clone()), - acadrust::EntityType::AttributeDefinition(a) => Some(a.default_value.clone()), - acadrust::EntityType::AttributeEntity(a) => Some(a.get_value().to_string()), - _ => None, - } -} -// ── MASSPROP helpers ─────────────────────────────────────────────────────── - -struct MassProps { - area: f64, - perimeter: f64, - cx: f64, - cy: f64, -} - -fn massprop_entity(entity: &acadrust::EntityType) -> Option { - use std::f64::consts::{PI, TAU}; - - match entity { - acadrust::EntityType::Circle(c) => { - let r = c.radius; - Some(MassProps { - area: PI * r * r, - perimeter: TAU * r, - cx: c.center.x, - cy: c.center.y, - }) - } - acadrust::EntityType::Arc(a) => { - let r = a.radius; - let span = { - let s = (a.end_angle - a.start_angle).rem_euclid(TAU); - if s < 1e-6 { - TAU - } else { - s - } - }; - // Sector area (pie slice) - let area = 0.5 * r * r * span; - let arc_len = r * span; - // Centroid of arc (chord midpoint direction) - let mid_rad = a.start_angle + span / 2.0; - Some(MassProps { - area, - perimeter: arc_len, - cx: a.center.x + r * mid_rad.cos(), - cy: a.center.y + r * mid_rad.sin(), - }) - } - acadrust::EntityType::Line(l) => { - let dx = l.end.x - l.start.x; - let dy = l.end.y - l.start.y; - let len = (dx * dx + dy * dy).sqrt(); - Some(MassProps { - area: 0.0, - perimeter: len, - cx: (l.start.x + l.end.x) / 2.0, - cy: (l.start.y + l.end.y) / 2.0, - }) - } - acadrust::EntityType::LwPolyline(p) => { - let n = p.vertices.len(); - if n < 2 { - return None; - } - // Shoelace area + perimeter - let mut area_sum = 0.0f64; - let mut perimeter = 0.0f64; - let mut cx_sum = 0.0f64; - let mut cy_sum = 0.0f64; - let n_segs = if p.is_closed { n } else { n - 1 }; - for idx in 0..n_segs { - let v0 = &p.vertices[idx]; - let v1 = &p.vertices[(idx + 1) % n]; - let x0 = v0.location.x; - let y0 = v0.location.y; - let x1 = v1.location.x; - let y1 = v1.location.y; - area_sum += x0 * y1 - x1 * y0; - perimeter += ((x1 - x0).powi(2) + (y1 - y0).powi(2)).sqrt(); - cx_sum += (x0 + x1) * (x0 * y1 - x1 * y0); - cy_sum += (y0 + y1) * (x0 * y1 - x1 * y0); - } - let area = (area_sum / 2.0).abs(); - let (cx, cy) = if area > 1e-12 { - (cx_sum / (6.0 * area), cy_sum / (6.0 * area)) - } else { - let sx: f64 = p.vertices.iter().map(|v| v.location.x).sum::() / n as f64; - let sy: f64 = p.vertices.iter().map(|v| v.location.y).sum::() / n as f64; - (sx, sy) - }; - Some(MassProps { - area, - perimeter, - cx, - cy, - }) - } - acadrust::EntityType::Ellipse(e) => { - let a = (e.major_axis.x.powi(2) + e.major_axis.y.powi(2)).sqrt(); - let b = a * e.minor_axis_ratio; - let t0 = e.start_parameter; - let t1 = { - let mut t = e.end_parameter; - if t <= t0 { - t += TAU; - } - t - }; - let span = t1 - t0; - let is_full = (span - TAU).abs() < 1e-6; - let area = if is_full { - PI * a * b - } else { - // Sector area of ellipse approximated via 256-pt integration - let n = 256usize; - let mut s = 0.0f64; - for k in 0..n { - let t = t0 + span * (k as f64 / n as f64); - let tp = t0 + span * ((k + 1) as f64 / n as f64); - let nx = e.major_axis.x / a; - let ny = e.major_axis.y / a; - let x0 = a * t.cos() * nx - b * t.sin() * ny; - let y0 = a * t.cos() * ny + b * t.sin() * nx; - let x1 = a * tp.cos() * nx - b * tp.sin() * ny; - let y1 = a * tp.cos() * ny + b * tp.sin() * nx; - s += x0 * y1 - x1 * y0; - } - (s / 2.0).abs() - }; - // Arc length via 256-pt numerical integration - let nx = e.major_axis.x / a.max(1e-12); - let ny = e.major_axis.y / a.max(1e-12); - let perimeter = { - let n = 256usize; - let mut len = 0.0f64; - for k in 0..n { - let t = t0 + span * (k as f64 / n as f64); - let tp = t0 + span * ((k + 1) as f64 / n as f64); - let x0 = e.center.x + a * t.cos() * nx - b * t.sin() * ny; - let y0 = e.center.y + a * t.cos() * ny + b * t.sin() * nx; - let x1 = e.center.x + a * tp.cos() * nx - b * tp.sin() * ny; - let y1 = e.center.y + a * tp.cos() * ny + b * tp.sin() * nx; - len += (x1 - x0).hypot(y1 - y0); - } - len - }; - Some(MassProps { - area, - perimeter, - cx: e.center.x, - cy: e.center.y, - }) - } - _ => None, - } -} - -fn replace_entity_text(entity: &mut acadrust::EntityType, search: &str, rep: &str) { - let search_lc = search.to_lowercase(); - match entity { - acadrust::EntityType::Text(t) => { - if t.value.to_lowercase().contains(&search_lc) { - t.value = t.value.replace(search, rep); - } - } - acadrust::EntityType::MText(t) => { - if t.value.to_lowercase().contains(&search_lc) { - t.value = t.value.replace(search, rep); - } - } - acadrust::EntityType::AttributeDefinition(a) => { - if a.default_value.to_lowercase().contains(&search_lc) { - a.default_value = a.default_value.replace(search, rep); - } - } - acadrust::EntityType::AttributeEntity(a) => { - let cur = a.get_value().to_string(); - if cur.to_lowercase().contains(&search_lc) { - a.set_value(cur.replace(search, rep)); - } - } - _ => {} - } -} // ── DATAEXTRACTION ───────────────────────────────────────────────────────── diff --git a/src/entities/arc.rs b/src/entities/arc.rs index 2a745a23b..b5405b1f3 100644 --- a/src/entities/arc.rs +++ b/src/entities/arc.rs @@ -244,3 +244,29 @@ impl Transformable for Arc { apply_transform(self, t); } } + +impl crate::entities::traits::MassPropsCalc for acadrust::entities::Arc { + fn mass_props(&self) -> crate::entities::traits::MassProps { + use std::f64::consts::TAU; + let r = self.radius; + let span = { + let s = (self.end_angle - self.start_angle).rem_euclid(TAU); + if s < 1e-6 { + TAU + } else { + s + } + }; + // Sector area (pie slice) + let area = 0.5 * r * r * span; + let arc_len = r * span; + // Centroid of arc (chord midpoint direction) + let mid_rad = self.start_angle + span / 2.0; + crate::entities::traits::MassProps { + area, + perimeter: arc_len, + cx: self.center.x + r * mid_rad.cos(), + cy: self.center.y + r * mid_rad.sin(), + } + } +} diff --git a/src/entities/attribute.rs b/src/entities/attribute.rs index 0143c1637..2f5ea84b9 100644 --- a/src/entities/attribute.rs +++ b/src/entities/attribute.rs @@ -792,3 +792,28 @@ impl Transformable for AttributeEntity { }); } } + +impl crate::entities::traits::TextContent for acadrust::entities::AttributeDefinition { + fn text_content(&self) -> Option { + Some(self.default_value.clone()) + } + fn replace_text(&mut self, search: &str, rep: &str) { + let search_lc = search.to_lowercase(); + if self.default_value.to_lowercase().contains(&search_lc) { + self.default_value = self.default_value.replace(search, rep); + } + } +} + +impl crate::entities::traits::TextContent for acadrust::entities::AttributeEntity { + fn text_content(&self) -> Option { + Some(self.get_value().to_string()) + } + fn replace_text(&mut self, search: &str, rep: &str) { + let search_lc = search.to_lowercase(); + let cur = self.get_value().to_string(); + if cur.to_lowercase().contains(&search_lc) { + self.set_value(cur.replace(search, rep)); + } + } +} diff --git a/src/entities/circle.rs b/src/entities/circle.rs index b29ba42a7..ec2a21045 100644 --- a/src/entities/circle.rs +++ b/src/entities/circle.rs @@ -227,3 +227,16 @@ impl Transformable for Circle { apply_transform(self, t); } } + +impl crate::entities::traits::MassPropsCalc for Circle { + fn mass_props(&self) -> crate::entities::traits::MassProps { + use std::f64::consts::{PI, TAU}; + let r = self.radius; + crate::entities::traits::MassProps { + area: PI * r * r, + perimeter: TAU * r, + cx: self.center.x, + cy: self.center.y, + } + } +} diff --git a/src/entities/ellipse.rs b/src/entities/ellipse.rs index 04c2dad1b..d78b8c4df 100644 --- a/src/entities/ellipse.rs +++ b/src/entities/ellipse.rs @@ -276,3 +276,64 @@ impl Transformable for Ellipse { apply_transform(self, t); } } + +impl crate::entities::traits::MassPropsCalc for acadrust::entities::Ellipse { + fn mass_props(&self) -> crate::entities::traits::MassProps { + use std::f64::consts::{PI, TAU}; + let e = self; + let a = (e.major_axis.x.powi(2) + e.major_axis.y.powi(2)).sqrt(); + let b = a * e.minor_axis_ratio; + let t0 = e.start_parameter; + let t1 = { + let mut t = e.end_parameter; + if t <= t0 { + t += TAU; + } + t + }; + let span = t1 - t0; + let is_full = (span - TAU).abs() < 1e-6; + let area = if is_full { + PI * a * b + } else { + // Sector area of ellipse approximated via 256-pt integration + let n = 256usize; + let mut s = 0.0f64; + for k in 0..n { + let t = t0 + span * (k as f64 / n as f64); + let tp = t0 + span * ((k + 1) as f64 / n as f64); + let nx = e.major_axis.x / a; + let ny = e.major_axis.y / a; + let x0 = a * t.cos() * nx - b * t.sin() * ny; + let y0 = a * t.cos() * ny + b * t.sin() * nx; + let x1 = a * tp.cos() * nx - b * tp.sin() * ny; + let y1 = a * tp.cos() * ny + b * tp.sin() * nx; + s += x0 * y1 - x1 * y0; + } + (s / 2.0).abs() + }; + // Arc length via 256-pt numerical integration + let nx = e.major_axis.x / a.max(1e-12); + let ny = e.major_axis.y / a.max(1e-12); + let perimeter = { + let n = 256usize; + let mut len = 0.0f64; + for k in 0..n { + let t = t0 + span * (k as f64 / n as f64); + let tp = t0 + span * ((k + 1) as f64 / n as f64); + let x0 = e.center.x + a * t.cos() * nx - b * t.sin() * ny; + let y0 = e.center.y + a * t.cos() * ny + b * t.sin() * nx; + let x1 = e.center.x + a * tp.cos() * nx - b * tp.sin() * ny; + let y1 = e.center.y + a * tp.cos() * ny + b * tp.sin() * nx; + len += (x1 - x0).hypot(y1 - y0); + } + len + }; + crate::entities::traits::MassProps { + area, + perimeter, + cx: e.center.x, + cy: e.center.y, + } + } +} diff --git a/src/entities/line.rs b/src/entities/line.rs index 7147dfc00..5dfe7631f 100644 --- a/src/entities/line.rs +++ b/src/entities/line.rs @@ -181,3 +181,17 @@ impl Transformable for Line { apply_transform(self, t); } } + +impl crate::entities::traits::MassPropsCalc for acadrust::entities::Line { + fn mass_props(&self) -> crate::entities::traits::MassProps { + let dx = self.end.x - self.start.x; + let dy = self.end.y - self.start.y; + let len = (dx * dx + dy * dy).sqrt(); + crate::entities::traits::MassProps { + area: 0.0, + perimeter: len, + cx: (self.start.x + self.end.x) / 2.0, + cy: (self.start.y + self.end.y) / 2.0, + } + } +} diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index af1bbc5bf..a2a1fe571 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -549,3 +549,50 @@ pub(crate) fn wide_fills(pl: &acadrust::entities::LwPolyline) -> Vec crate::entities::traits::MassProps { + let p = self; + let n = p.vertices.len(); + if n < 2 { + return crate::entities::traits::MassProps { + area: 0.0, + perimeter: 0.0, + cx: 0.0, + cy: 0.0, + }; + } + // Shoelace area + perimeter + let mut area_sum = 0.0f64; + let mut perimeter = 0.0f64; + let mut cx_sum = 0.0f64; + let mut cy_sum = 0.0f64; + let n_segs = if p.is_closed { n } else { n - 1 }; + for idx in 0..n_segs { + let v0 = &p.vertices[idx]; + let v1 = &p.vertices[(idx + 1) % n]; + let x0 = v0.location.x; + let y0 = v0.location.y; + let x1 = v1.location.x; + let y1 = v1.location.y; + area_sum += x0 * y1 - x1 * y0; + perimeter += ((x1 - x0).powi(2) + (y1 - y0).powi(2)).sqrt(); + cx_sum += (x0 + x1) * (x0 * y1 - x1 * y0); + cy_sum += (y0 + y1) * (x0 * y1 - x1 * y0); + } + let area = (area_sum / 2.0).abs(); + let (cx, cy) = if area > 1e-12 { + (cx_sum / (6.0 * area), cy_sum / (6.0 * area)) + } else { + let sx: f64 = p.vertices.iter().map(|v| v.location.x).sum::() / n as f64; + let sy: f64 = p.vertices.iter().map(|v| v.location.y).sum::() / n as f64; + (sx, sy) + }; + crate::entities::traits::MassProps { + area, + perimeter, + cx, + cy, + } + } +} diff --git a/src/entities/mtext.rs b/src/entities/mtext.rs index a2e64a4cb..7ba44d4b6 100644 --- a/src/entities/mtext.rs +++ b/src/entities/mtext.rs @@ -317,3 +317,15 @@ impl Transformable for MText { apply_transform(self, t); } } + +impl crate::entities::traits::TextContent for acadrust::entities::MText { + fn text_content(&self) -> Option { + Some(self.value.clone()) + } + fn replace_text(&mut self, search: &str, rep: &str) { + let search_lc = search.to_lowercase(); + if self.value.to_lowercase().contains(&search_lc) { + self.value = self.value.replace(search, rep); + } + } +} diff --git a/src/entities/text.rs b/src/entities/text.rs index 13d0bad96..d6f2e10aa 100644 --- a/src/entities/text.rs +++ b/src/entities/text.rs @@ -332,3 +332,15 @@ impl Transformable for Text { apply_transform(self, t); } } + +impl crate::entities::traits::TextContent for acadrust::entities::Text { + fn text_content(&self) -> Option { + Some(self.value.clone()) + } + fn replace_text(&mut self, search: &str, rep: &str) { + let search_lc = search.to_lowercase(); + if self.value.to_lowercase().contains(&search_lc) { + self.value = self.value.replace(search, rep); + } + } +} diff --git a/src/entities/traits.rs b/src/entities/traits.rs index bcce509ac..c19179807 100644 --- a/src/entities/traits.rs +++ b/src/entities/traits.rs @@ -31,6 +31,30 @@ pub trait Transformable { fn apply_transform(&mut self, t: &EntityTransform); } +/// Inquiry-time mass / area / perimeter properties for entities whose +/// 2D footprint has a meaningful area or perimeter (Circle, Arc, Line, +/// LwPolyline, Ellipse). Entities outside this set get `None` via the +/// dispatcher. +#[derive(Clone, Copy, Debug)] +pub struct MassProps { + pub area: f64, + pub perimeter: f64, + pub cx: f64, + pub cy: f64, +} + +pub trait MassPropsCalc { + fn mass_props(&self) -> MassProps; +} + +/// Read / replace the visible string of text-like entities (Text, MText, +/// AttributeDefinition, AttributeEntity). `replace` rewrites every +/// occurrence of `search` with `rep`. +pub trait TextContent { + fn text_content(&self) -> Option; + fn replace_text(&mut self, search: &str, rep: &str); +} + pub trait EntityTypeOps { fn to_truck_entity(&self, document: &CadDocument) -> Option; fn grips(&self) -> Vec; @@ -38,6 +62,9 @@ pub trait EntityTypeOps { fn apply_geom_prop(&mut self, field: &str, value: &str); fn apply_grip(&mut self, grip_id: usize, apply: GripApply); fn apply_transform(&mut self, t: &EntityTransform); + fn mass_props(&self) -> Option; + fn text_content(&self) -> Option; + fn replace_text(&mut self, search: &str, rep: &str); } /// Per-dispatch-function entity-variant lists. Adding a new entity that @@ -148,4 +175,28 @@ impl EntityTypeOps for EntityType { _ => {}, ) } + + fn mass_props(&self) -> Option { + dispatch!(self, + |e| Some(MassPropsCalc::mass_props(e)), + [Circle, Arc, Line, LwPolyline, Ellipse], + _ => None, + ) + } + + fn text_content(&self) -> Option { + dispatch!(self, + |e| TextContent::text_content(e), + [Text, MText, AttributeDefinition, AttributeEntity], + _ => None, + ) + } + + fn replace_text(&mut self, search: &str, rep: &str) { + dispatch!(self, + |e| TextContent::replace_text(e, search, rep), + [Text, MText, AttributeDefinition, AttributeEntity], + _ => {}, + ) + } }