refactor(commands): per-entity MassProps + TextContent traits

Two free helpers in app/commands.rs lived as big match-on-EntityType
blocks: massprop_entity (Circle/Arc/Line/LwPolyline/Ellipse area +
perimeter + centroid, ~140 lines) and entity_text_content /
replace_entity_text (Text/MText/AttributeDefinition/AttributeEntity).

Move each to its own trait in entities/traits.rs and implement
per-entity:

  MassPropsCalc  — Circle, Arc, Line, LwPolyline, Ellipse
  TextContent    — Text, MText, AttributeDefinition, AttributeEntity

EntityTypeOps gets dispatch shims that route to the trait via the
existing macro, so callsites just call entity.mass_props() /
entity.text_content() / entity.replace_text(search, rep).

The remaining free helpers in commands.rs (entity_list_details,
entity_extra_info, entity_type_name, build_data_extraction_csv) are
single-command formatting helpers tightly coupled to LIST / EXTRACT /
text-format output. Distributing them as per-entity methods would
inflate every entity file with command-side formatting concerns
without making the command-side any cleaner — left in place.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-05-23 23:30:34 +03:00
commit e78321190a
10 changed files with 266 additions and 193 deletions

View file

@ -549,3 +549,50 @@ pub(crate) fn wide_fills(pl: &acadrust::entities::LwPolyline) -> Vec<Vec<[f32; 2
}
out
}
impl crate::entities::traits::MassPropsCalc for acadrust::entities::LwPolyline {
fn mass_props(&self) -> 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::<f64>() / n as f64;
let sy: f64 = p.vertices.iter().map(|v| v.location.y).sum::<f64>() / n as f64;
(sx, sy)
};
crate::entities::traits::MassProps {
area,
perimeter,
cx,
cy,
}
}
}