diff --git a/src/entities/common.rs b/src/entities/common.rs index 1d1fbee41..57af9ab2f 100644 --- a/src/entities/common.rs +++ b/src/entities/common.rs @@ -4,6 +4,8 @@ use std::collections::HashSet; use crate::scene::model::object::{GripDef, GripShape, PropValue, Property}; use crate::scene::model::wire_model::PatternStationPiece; +pub const VARIES_LABEL: &str = "*VARIES*"; + /// Linear / angular unit format pulled from the document header so the /// per-thread properties pipeline can format values consistently without /// passing the document through every callsite. diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index 8b125fe39..67fa6e418 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -6,7 +6,7 @@ use crate::t; use crate::command::EntityTransform; use crate::entities::common::{ 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, + rectangle_grip, ro_prop as ro, square_grip, stepper_prop as stepper, VARIES_LABEL, }; use crate::entities::traits::RenderConvertible; use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject}; @@ -18,14 +18,38 @@ const REVCLOUD_BULGE: f64 = 0.5; const MAX_REVCLOUD_VERTICES: usize = 100_000; const WIDTH_EPSILON: f64 = 1.0e-9; -fn effective_width(width: f64, constant_width: f64) -> f64 { - if width > WIDTH_EPSILON { - width +pub(crate) fn vertex_segment_widths(vertex: &LwVertex, constant_width: f64) -> (f64, f64) { + if vertex.start_width > WIDTH_EPSILON || vertex.end_width > WIDTH_EPSILON { + (vertex.start_width, vertex.end_width) } else { - constant_width + (constant_width, constant_width) } } +pub(crate) fn lwpolyline_global_width(pline: &LwPolyline) -> Option { + let count = pline.vertices.len(); + let seg_count = if pline.is_closed { + count + } else { + count.saturating_sub(1) + }; + if seg_count == 0 { + return Some(pline.constant_width); + } + let (w0_start, w0_end) = vertex_segment_widths(&pline.vertices[0], pline.constant_width); + if (w0_start - w0_end).abs() > 1e-6 { + return None; + } + let w0 = w0_start; + for i in 1..seg_count { + let (sw, ew) = vertex_segment_widths(&pline.vertices[i], pline.constant_width); + if (sw - ew).abs() > 1e-6 || (sw - w0).abs() > 1e-6 { + return None; + } + } + Some(w0) +} + /// Midpoint position on an arc segment defined by its bulge. fn arc_midpoint(p0: [f64; 2], p1: [f64; 2], bulge: f64) -> [f64; 2] { match crate::entities::common::BulgeArc::from_bulge(p0, p1, bulge) { @@ -504,8 +528,7 @@ fn band_verts(pline: &LwPolyline) -> Vec<([f64; 2], f64, f64, f64)> { .vertices .iter() .map(|v| { - let sw = effective_width(v.start_width, c); - let ew = effective_width(v.end_width, c); + let (sw, ew) = vertex_segment_widths(v, c); ([v.location.x, v.location.y], v.bulge, sw, ew) }) .collect() @@ -899,11 +922,8 @@ fn properties(pline: &LwPolyline) -> Vec { let v = pline.vertices.get(vi); let vx = v.map_or(0.0, |v| v.location.x); let vy = v.map_or(0.0, |v| v.location.y); - let start_w = v.map_or(0.0, |v| { - effective_width(v.start_width, pline.constant_width) - }); - let end_w = v.map_or(0.0, |v| { - effective_width(v.end_width, pline.constant_width) + let (start_w, end_w) = v.map_or((0.0, 0.0), |v| { + vertex_segment_widths(v, pline.constant_width) }); let mp = ::mass_props(pline); let cloud_arc_length = revision_cloud_arc_length(pline); @@ -937,6 +957,14 @@ fn properties(pline: &LwPolyline) -> Vec { arc_length, )); } + let global_width_prop = match lwpolyline_global_width(pline) { + Some(gw) => edit(t!("Global width").as_ref(), "global_width", gw), + None => Property { + label: t!("Global width").into_owned(), + field: "global_width", + value: PropValue::EditText(VARIES_LABEL.to_string()), + }, + }; let mut geometry_props = vec![ stepper(t!("Current Vertex").as_ref(), "current_vertex", vertex_label), edit(t!("Vertex X").as_ref(), "vertex_x", vx), @@ -948,7 +976,7 @@ fn properties(pline: &LwPolyline) -> Vec { 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), + global_width_prop, 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)), @@ -1015,7 +1043,13 @@ fn apply_geom_prop(pline: &mut LwPolyline, field: &str, value: &str) { }; match field { "elevation" => pline.elevation = v, - "global_width" if v.is_finite() && v >= 0.0 => pline.constant_width = v, + "global_width" if v.is_finite() && v >= 0.0 => { + pline.constant_width = v; + for vtx in &mut pline.vertices { + vtx.start_width = v; + vtx.end_width = v; + } + } "vertex_x" => { if let Some(vtx) = pline.vertices.get_mut(vi) { vtx.location.x = v; @@ -1027,11 +1061,31 @@ fn apply_geom_prop(pline: &mut LwPolyline, field: &str, value: &str) { } } "start_width" if v.is_finite() && v >= 0.0 => { + if pline.constant_width > WIDTH_EPSILON { + let c = pline.constant_width; + for vtx in &mut pline.vertices { + if vtx.start_width <= WIDTH_EPSILON && vtx.end_width <= WIDTH_EPSILON { + vtx.start_width = c; + vtx.end_width = c; + } + } + pline.constant_width = 0.0; + } if let Some(vtx) = pline.vertices.get_mut(vi) { vtx.start_width = v; } } "end_width" if v.is_finite() && v >= 0.0 => { + if pline.constant_width > WIDTH_EPSILON { + let c = pline.constant_width; + for vtx in &mut pline.vertices { + if vtx.start_width <= WIDTH_EPSILON && vtx.end_width <= WIDTH_EPSILON { + vtx.start_width = c; + vtx.end_width = c; + } + } + pline.constant_width = 0.0; + } if let Some(vtx) = pline.vertices.get_mut(vi) { vtx.end_width = v; } @@ -1252,8 +1306,8 @@ impl crate::entities::traits::Grippable for LwPolyline { new_v.location.x = midpoint[0]; new_v.location.y = midpoint[1]; new_v.vertex_id = 0; - let effective_start = effective_width(v0.start_width, self.constant_width); - let effective_end = effective_width(v0.end_width, self.constant_width); + let (effective_start, effective_end) = + vertex_segment_widths(&v0, self.constant_width); let middle_width = (effective_start + effective_end) * 0.5; self.vertices[i0].end_width = middle_width; new_v.start_width = middle_width; @@ -1325,8 +1379,9 @@ pub(crate) fn wide_fills(pl: &acadrust::entities::LwPolyline) -> ([f64; 2], Vec< for i in 0..seg_count { let v0 = &verts[i]; let v1 = &verts[(i + 1) % n]; - let hw0 = effective_width(v0.start_width, pl.constant_width) as f32 * 0.5; - let hw1 = effective_width(v0.end_width, pl.constant_width) as f32 * 0.5; + let (sw, ew) = vertex_segment_widths(v0, pl.constant_width); + let hw0 = sw as f32 * 0.5; + let hw1 = ew as f32 * 0.5; if hw0 < 1e-6 && hw1 < 1e-6 { continue; } @@ -1381,3 +1436,88 @@ impl crate::entities::traits::MassPropsCalc for acadrust::entities::LwPolyline { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::entities::traits::PropertyEditable; + use acadrust::entities::{LwPolyline, LwVertex}; + use acadrust::Vector2; + + fn make_test_lwpolyline(count: usize, constant_width: f64) -> LwPolyline { + let mut pl = LwPolyline::default(); + pl.constant_width = constant_width; + for i in 0..count { + pl.vertices.push(LwVertex::new(Vector2::new(i as f64 * 10.0, 0.0))); + } + pl + } + + #[test] + fn test_lwpolyline_uniform_width_shows_global_width() { + let pl = make_test_lwpolyline(3, 5.0); + assert_eq!(lwpolyline_global_width(&pl), Some(5.0)); + + let props = pl.geometry_properties(&[]); + let geom_props = &props[0].props; + let gw = geom_props.iter().find(|p| p.field == "global_width").unwrap(); + match &gw.value { + PropValue::EditText(val) => assert_eq!(parse_f64(val), Some(5.0)), + _ => panic!("expected EditText"), + } + } + + #[test] + fn test_lwpolyline_varying_vertex_width_shows_varies() { + let mut pl = make_test_lwpolyline(3, 5.0); + pl.vertices[0].start_width = 2.0; + pl.vertices[0].end_width = 3.0; + + assert_eq!(lwpolyline_global_width(&pl), None); + + let props = pl.geometry_properties(&[]); + let geom_props = &props[0].props; + let gw = geom_props.iter().find(|p| p.field == "global_width").unwrap(); + match &gw.value { + PropValue::EditText(val) => assert_eq!(val, VARIES_LABEL), + _ => panic!("expected EditText with VARIES_LABEL"), + } + } + + #[test] + fn test_lwpolyline_modifying_global_width_sets_all_vertex_widths() { + let mut pl = make_test_lwpolyline(3, 5.0); + pl.vertices[0].start_width = 2.0; + pl.vertices[0].end_width = 3.0; + + pl.apply_geom_prop("global_width", "10.0"); + + assert_eq!(pl.constant_width, 10.0); + for v in &pl.vertices { + assert_eq!(v.start_width, 10.0); + assert_eq!(v.end_width, 10.0); + } + assert_eq!(lwpolyline_global_width(&pl), Some(10.0)); + } + + #[test] + fn test_lwpolyline_modifying_segment_width_materializes_constant_width() { + let mut pl = make_test_lwpolyline(3, 5.0); + // Initially constant_width is 5.0, vertices start/end are 0.0 + assert_eq!(pl.vertices[0].start_width, 0.0); + + // Edit start width of current vertex (vertex 0) to 2.0 + pl.apply_geom_prop("start_width", "2.0"); + + // Vertex 0 start width should be 2.0, end width materialized to 5.0 + assert_eq!(pl.vertices[0].start_width, 2.0); + assert_eq!(pl.vertices[0].end_width, 5.0); + + // Vertex 1 and 2 should have materialized 5.0 instead of collapsing to 0.0 + assert_eq!(pl.vertices[1].start_width, 5.0); + assert_eq!(pl.vertices[1].end_width, 5.0); + + // Global width must now be VARIES + assert_eq!(lwpolyline_global_width(&pl), None); + } +} diff --git a/src/entities/polyline.rs b/src/entities/polyline.rs index de67a35dd..5b23a3ead 100644 --- a/src/entities/polyline.rs +++ b/src/entities/polyline.rs @@ -4,7 +4,7 @@ use crate::t; use crate::command::EntityTransform; use crate::entities::common::{ edit_prop as edit, format_length, parse_f64, ro_prop as ro, square_grip, - stepper_prop as stepper, + stepper_prop as stepper, VARIES_LABEL, }; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject}; @@ -609,6 +609,51 @@ impl Grippable for Polyline2D { } } +pub(crate) fn polyline2d_vertex_segment_widths( + vertex: &acadrust::entities::Vertex2D, + default_start: f64, + default_end: f64, +) -> (f64, f64) { + if vertex.start_width > 1e-9 || vertex.end_width > 1e-9 { + (vertex.start_width, vertex.end_width) + } else { + (default_start, default_end) + } +} + +pub(crate) fn polyline2d_global_width(pline: &Polyline2D) -> Option { + let filtered = drawn_vertices2d(pline); + let verts: &[acadrust::entities::Vertex2D] = + filtered.as_deref().unwrap_or(&pline.vertices); + let count = verts.len(); + let seg_count = if pline.is_closed() { + count + } else { + count.saturating_sub(1) + }; + if seg_count == 0 { + if (pline.start_width - pline.end_width).abs() < 1e-6 { + return Some(pline.start_width); + } else { + return None; + } + } + let (w0_start, w0_end) = + polyline2d_vertex_segment_widths(&verts[0], pline.start_width, pline.end_width); + if (w0_start - w0_end).abs() > 1e-6 { + return None; + } + let w0 = w0_start; + for i in 1..seg_count { + let (sw, ew) = + polyline2d_vertex_segment_widths(&verts[i], pline.start_width, pline.end_width); + if (sw - ew).abs() > 1e-6 || (sw - w0).abs() > 1e-6 { + return None; + } + } + Some(w0) +} + impl PropertyEditable for Polyline2D { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { let n = self.vertices.len(); @@ -631,14 +676,24 @@ impl PropertyEditable for Polyline2D { let v = self.vertices.get(vi); let vertex_x = v.map(|v| v.location.x).unwrap_or_default(); let vertex_y = v.map(|v| v.location.y).unwrap_or_default(); - let seg_start_w = v.map(|v| v.start_width).unwrap_or_default(); - let seg_end_w = v.map(|v| v.end_width).unwrap_or_default(); + let (seg_start_w, seg_end_w) = v.map_or((0.0, 0.0), |v| { + polyline2d_vertex_segment_widths(v, self.start_width, self.end_width) + }); let vertex_label = if n == 0 { "—".to_string() } else { format!("{} / {}", vi + 1, n) }; + let global_width_prop = match polyline2d_global_width(self) { + Some(gw) => edit(t!("Global width").as_ref(), "pl2_start_w", gw), + None => Property { + label: t!("Global width").into_owned(), + field: "pl2_start_w", + value: PropValue::EditText(VARIES_LABEL.to_string()), + }, + }; + vec![ PropSection { title: t!("Geometry").into_owned(), @@ -648,7 +703,7 @@ impl PropertyEditable for Polyline2D { edit(t!("Vertex Y").as_ref(), "pl2_vertex_y", vertex_y), edit(t!("Start segment width").as_ref(), "pl2_seg_start_w", seg_start_w), edit(t!("End segment width").as_ref(), "pl2_seg_end_w", seg_end_w), - edit(t!("Global width").as_ref(), "pl2_start_w", self.start_width), + global_width_prop, edit(t!("Elevation").as_ref(), "pl2_elevation", self.elevation), ro(t!("Area").as_ref(), "pl2_area", format!("{area:.4}")), ro(t!("Length").as_ref(), "pl2_length", format!("{length:.4}")), @@ -706,9 +761,13 @@ impl PropertyEditable for Polyline2D { } "pl2_start_w" => { if let Some(v) = parse_f64(value) { - if v >= 0.0 { + if v.is_finite() && v >= 0.0 { self.start_width = v; self.end_width = v; + for vtx in &mut self.vertices { + vtx.start_width = v; + vtx.end_width = v; + } } } } @@ -723,16 +782,44 @@ impl PropertyEditable for Polyline2D { } } "pl2_seg_start_w" => { - if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.get_mut(vi)) { - if v >= 0.0 { - vert.start_width = v; + if let Some(v) = parse_f64(value) { + if v.is_finite() && v >= 0.0 { + if self.start_width > 1e-9 || self.end_width > 1e-9 { + let def_s = self.start_width; + let def_e = self.end_width; + for vtx in &mut self.vertices { + if vtx.start_width <= 1e-9 && vtx.end_width <= 1e-9 { + vtx.start_width = def_s; + vtx.end_width = def_e; + } + } + self.start_width = 0.0; + self.end_width = 0.0; + } + if let Some(vert) = self.vertices.get_mut(vi) { + vert.start_width = v; + } } } } "pl2_seg_end_w" => { - if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.get_mut(vi)) { - if v >= 0.0 { - vert.end_width = v; + if let Some(v) = parse_f64(value) { + if v.is_finite() && v >= 0.0 { + if self.start_width > 1e-9 || self.end_width > 1e-9 { + let def_s = self.start_width; + let def_e = self.end_width; + for vtx in &mut self.vertices { + if vtx.start_width <= 1e-9 && vtx.end_width <= 1e-9 { + vtx.start_width = def_s; + vtx.end_width = def_e; + } + } + self.start_width = 0.0; + self.end_width = 0.0; + } + if let Some(vert) = self.vertices.get_mut(vi) { + vert.end_width = v; + } } } } @@ -1331,3 +1418,84 @@ pub(crate) fn wide_fills(pl: &acadrust::entities::Polyline2D) -> ([f64; 2], Vec< } (origin, out) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::entities::traits::PropertyEditable; + use acadrust::entities::{Polyline2D, Vertex2D}; + use acadrust::Vector3; + + fn make_test_polyline2d(count: usize, start_w: f64, end_w: f64) -> Polyline2D { + let mut pl = Polyline2D::default(); + pl.start_width = start_w; + pl.end_width = end_w; + for i in 0..count { + pl.vertices.push(Vertex2D::new(Vector3::new(i as f64 * 10.0, 0.0, 0.0))); + } + pl + } + + #[test] + fn test_polyline2d_uniform_width_shows_global_width() { + let pl = make_test_polyline2d(3, 4.0, 4.0); + assert_eq!(polyline2d_global_width(&pl), Some(4.0)); + + let props = pl.geometry_properties(&[]); + let geom_props = &props[0].props; + let gw = geom_props.iter().find(|p| p.field == "pl2_start_w").unwrap(); + match &gw.value { + PropValue::EditText(val) => assert_eq!(parse_f64(val), Some(4.0)), + _ => panic!("expected EditText"), + } + } + + #[test] + fn test_polyline2d_varying_vertex_width_shows_varies() { + let mut pl = make_test_polyline2d(3, 4.0, 4.0); + pl.vertices[0].start_width = 1.0; + pl.vertices[0].end_width = 2.0; + + assert_eq!(polyline2d_global_width(&pl), None); + + let props = pl.geometry_properties(&[]); + let geom_props = &props[0].props; + let gw = geom_props.iter().find(|p| p.field == "pl2_start_w").unwrap(); + match &gw.value { + PropValue::EditText(val) => assert_eq!(val, VARIES_LABEL), + _ => panic!("expected EditText with VARIES_LABEL"), + } + } + + #[test] + fn test_polyline2d_modifying_global_width_sets_all_vertex_widths() { + let mut pl = make_test_polyline2d(3, 4.0, 4.0); + pl.vertices[0].start_width = 1.0; + pl.vertices[0].end_width = 2.0; + + pl.apply_geom_prop("pl2_start_w", "8.0"); + + assert_eq!(pl.start_width, 8.0); + assert_eq!(pl.end_width, 8.0); + for v in &pl.vertices { + assert_eq!(v.start_width, 8.0); + assert_eq!(v.end_width, 8.0); + } + assert_eq!(polyline2d_global_width(&pl), Some(8.0)); + } + + #[test] + fn test_polyline2d_modifying_segment_width_materializes_default_width() { + let mut pl = make_test_polyline2d(3, 4.0, 4.0); + assert_eq!(pl.vertices[0].start_width, 0.0); + + pl.apply_geom_prop("pl2_seg_start_w", "1.5"); + + assert_eq!(pl.vertices[0].start_width, 1.5); + assert_eq!(pl.vertices[0].end_width, 4.0); + assert_eq!(pl.vertices[1].start_width, 4.0); + assert_eq!(pl.vertices[1].end_width, 4.0); + + assert_eq!(polyline2d_global_width(&pl), None); + } +} diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 8c9a22af5..143dde897 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -215,17 +215,12 @@ fn polyline_segment_widths(entity: &EntityType) -> Vec<(f32, f32)> { (0..seg_count) .map(|i| { let v = &p.vertices[i]; - let sw = if v.start_width > 1e-9 { - v.start_width + let (sw, ew) = if v.start_width > 1e-9 || v.end_width > 1e-9 { + (v.start_width, v.end_width) } else { - c - } as f32; - let ew = if v.end_width > 1e-9 { - v.end_width - } else { - c - } as f32; - (sw, ew) + (c, c) + }; + (sw as f32, ew as f32) }) .collect() } @@ -244,17 +239,12 @@ fn polyline_segment_widths(entity: &EntityType) -> Vec<(f32, f32)> { (0..seg_count) .map(|i| { let v = &verts[i]; - let sw = if v.start_width > 1e-9 { - v.start_width + let (sw, ew) = if v.start_width > 1e-9 || v.end_width > 1e-9 { + (v.start_width, v.end_width) } else { - def_start - } as f32; - let ew = if v.end_width > 1e-9 { - v.end_width - } else { - def_end - } as f32; - (sw, ew) + (def_start, def_end) + }; + (sw as f32, ew as f32) }) .collect() }