diff --git a/src/entities/common.rs b/src/entities/common.rs index 617bf97fe..68fe7e3ce 100644 --- a/src/entities/common.rs +++ b/src/entities/common.rs @@ -148,6 +148,18 @@ pub fn format_length(value: f64) -> String { } } +/// Format an area with the drawing's linear precision. Area stays a decimal +/// scalar even when linear distances use architectural or fractional notation. +pub fn format_area(value: f64) -> String { + let ctx = unit_context(); + let precision = ctx.luprec.max(0).min(15) as usize; + if ctx.lunits == 1 { + format!("{:.*e}", precision, value) + } else { + format!("{:.*}", precision, value) + } +} + /// Format an angle (input in radians) using AUNITS / AUPREC. pub fn format_angle(value_rad: f64) -> String { let ctx = unit_context(); diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index bcea38533..6fcf09fa6 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -5,8 +5,8 @@ use crate::t; use crate::command::EntityTransform; use crate::entities::common::{ - edit_prop as edit, parse_f64, rectangle_grip, ro_prop as ro, square_grip, - stepper_prop as stepper, + edit_prop as edit, edit_scalar_prop as edit_scalar, format_area, format_length, parse_f64, + rectangle_grip, ro_prop as ro, square_grip, stepper_prop as stepper, }; use crate::entities::traits::RenderConvertible; use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject}; @@ -814,6 +814,33 @@ fn set_revision_cloud_arc_length(pline: &mut LwPolyline, requested: f64) { pline.vertices = cloud_vertices(&points, magnitude * sign, width_ratios); } +pub(crate) fn is_rectangle(pline: &LwPolyline) -> bool { + if pline.common.extended_data.get_record("OCS_RECTANGLE").is_some() { + return true; + } + if !pline.is_closed + || pline.vertices.len() != 4 + || pline.vertices.iter().any(|vertex| vertex.bulge.abs() > 1.0e-9) + { + return false; + } + let edges: Vec = (0..4) + .map(|index| { + let from = pline.vertices[index].location; + let to = pline.vertices[(index + 1) % 4].location; + glam::DVec2::new(to.x - from.x, to.y - from.y) + }) + .collect(); + let lengths: Vec = edges.iter().map(|edge| edge.length()).collect(); + if lengths.iter().any(|length| *length <= 1.0e-12) { + return false; + } + let tolerance = 1.0e-9; + edges[0].dot(edges[1]).abs() <= tolerance * lengths[0] * lengths[1] + && edges[0].perp_dot(edges[2]).abs() <= tolerance * lengths[0] * lengths[2] + && edges[1].perp_dot(edges[3]).abs() <= tolerance * lengths[1] * lengths[3] +} + fn properties(pline: &LwPolyline) -> Vec { let n = pline.vertices.len(); // The panel's Current Vertex focus, clamped to this polyline's range. @@ -831,10 +858,8 @@ fn properties(pline: &LwPolyline) -> Vec { let cloud_arc_length = revision_cloud_arc_length(pline); let vertex_label = if n == 0 { "—".to_string() - } else if cloud_arc_length.is_some() { - format!("{}", vi + 1) } else { - format!("{} / {}", vi + 1, n) + format!("{}", vi + 1) }; let mut misc_props = vec![ Property { @@ -861,20 +886,26 @@ fn properties(pline: &LwPolyline) -> Vec { arc_length, )); } + let mut geometry_props = vec![ + stepper(t!("Current Vertex").as_ref(), "current_vertex", vertex_label), + edit(t!("Vertex X").as_ref(), "vertex_x", vx), + edit(t!("Vertex Y").as_ref(), "vertex_y", vy), + edit_scalar(t!("Bulge").as_ref(), "bulge", v.map_or(0.0, |vertex| vertex.bulge)), + ]; + if !is_rectangle(pline) { + geometry_props.push(edit(t!("Start segment width").as_ref(), "start_width", start_w)); + geometry_props.push(edit(t!("End segment width").as_ref(), "end_width", end_w)); + } + geometry_props.extend([ + edit(t!("Global width").as_ref(), "global_width", pline.constant_width), + edit(t!("Elevation").as_ref(), "elevation", pline.elevation), + ro(t!("Area").as_ref(), "area", format_area(mp.area)), + ro(t!("Length").as_ref(), "length", format_length(mp.perimeter)), + ]); vec![ PropSection { title: t!("Geometry").into_owned(), - props: vec![ - stepper(t!("Current Vertex").as_ref(), "current_vertex", vertex_label), - edit(t!("Vertex X").as_ref(), "vertex_x", vx), - edit(t!("Vertex Y").as_ref(), "vertex_y", vy), - edit(t!("Start segment width").as_ref(), "start_width", start_w), - edit(t!("End segment width").as_ref(), "end_width", end_w), - edit(t!("Global width").as_ref(), "global_width", pline.constant_width), - edit(t!("Elevation").as_ref(), "elevation", pline.elevation), - ro(t!("Area").as_ref(), "area", format!("{:.4}", mp.area)), - ro(t!("Length").as_ref(), "length", format!("{:.4}", mp.perimeter)), - ], + props: geometry_props, }, PropSection { title: t!("Misc").into_owned(), @@ -954,6 +985,11 @@ fn apply_geom_prop(pline: &mut LwPolyline, field: &str, value: &str) { vtx.end_width = v; } } + "bulge" => { + if let Some(vtx) = pline.vertices.get_mut(vi) { + vtx.bulge = v.clamp(-1.0e6, 1.0e6); + } + } _ => {} } } diff --git a/src/scene/cache/properties.rs b/src/scene/cache/properties.rs index 66de0882f..19f79e26a 100644 --- a/src/scene/cache/properties.rs +++ b/src/scene/cache/properties.rs @@ -91,6 +91,10 @@ pub fn general_section(entity: &EntityType) -> PropSection { ], }; + if matches!(entity, EntityType::LwPolyline(polyline) if crate::entities::lwpolyline::is_rectangle(polyline)) { + section.props.retain(|prop| prop.field != "handle"); + } + // Thickness (DXF 39) is a General-group property, but only the entity // types that carry an extrusion thickness expose it (line, circle, arc, // polyline, text, 2D solid, …). Show it right after Hyperlink for those.