From 490c0f6cf901e35ced4e0a57322654a4b752eb81 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sat, 20 Jun 2026 21:14:13 +0300 Subject: [PATCH] feat(properties): show polyline length and area Line, Circle and Arc reported their lengths/quantities in the Geometry section, but polylines (and rectangles, which are closed polylines) only listed vertex count, closed flag and elevation. Add read-only Length and Area rows: length is the true path length with arc segments measured by their arc length, area treats the polyline as closed with a circular segment correction per bulge. Co-Authored-By: Claude Opus 4.8 --- src/entities/lwpolyline.rs | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index d1d89d367..910f8f329 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -323,6 +323,7 @@ fn grips(pline: &LwPolyline) -> Vec { } fn properties(pline: &LwPolyline) -> PropSection { + let (length, area) = length_and_area(pline); PropSection { title: "Geometry".into(), props: vec![ @@ -333,10 +334,50 @@ fn properties(pline: &LwPolyline) -> PropSection { if pline.is_closed { "Yes" } else { "No" }, ), edit("Elevation", "elevation", pline.elevation), + ro("Length", "length", format!("{length:.4}")), + ro("Area", "area", format!("{area:.4}")), ], } } +/// Path length and enclosed area of a polyline, accounting for arc +/// (bulge) segments. Length is the actual path — it excludes the implicit +/// closing segment when the polyline is open. Area always treats the +/// polyline as closed (last vertex joined back to the first), matching how +/// CAD property panels report a polyline's area. +fn length_and_area(pline: &LwPolyline) -> (f64, f64) { + let n = pline.vertices.len(); + if n < 2 { + return (0.0, 0.0); + } + let mut length = 0.0; + let mut chord_area = 0.0; // shoelace over chords, signed + let mut arc_area = 0.0; // bulge corrections, signed + for i in 0..n { + let v0 = &pline.vertices[i]; + let v1 = &pline.vertices[(i + 1) % n]; + let p0 = [v0.location.x, v0.location.y]; + let p1 = [v1.location.x, v1.location.y]; + // The wrap edge (last → first) is part of the closed-area shoelace + // regardless, but only contributes to length when the polyline is + // actually closed. + chord_area += p0[0] * p1[1] - p1[0] * p0[1]; + let is_wrap = i + 1 == n; + if is_wrap && !pline.is_closed { + continue; + } + let chord = ((p1[0] - p0[0]).powi(2) + (p1[1] - p0[1]).powi(2)).sqrt(); + match crate::entities::common::BulgeArc::from_bulge(p0, p1, v0.bulge) { + Some(arc) => { + length += arc.radius * arc.sweep.abs(); + arc_area += 0.5 * arc.radius * arc.radius * (arc.sweep - arc.sweep.sin()); + } + None => length += chord, + } + } + (length, (chord_area / 2.0 + arc_area).abs()) +} + fn apply_geom_prop(pline: &mut LwPolyline, field: &str, value: &str) { let Some(v) = parse_f64(value) else { return;