fix(properties): sync polyline segment widths with global width and display varies when differing

This commit is contained in:
Karim Jerbi committed 2026-09-10 12:30:33 +01:00
1 parent 4d3b8d8b79
commit 6ca16738f0
4 files changed
+349 -49

No files matched your search

+2
View File
@@ -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.
+158 -18
View File
@@ -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<f64> {
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<PropSection> {
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 = <LwPolyline as crate::entities::traits::MassPropsCalc>::mass_props(pline);
let cloud_arc_length = revision_cloud_arc_length(pline);
@@ -937,6 +957,14 @@ fn properties(pline: &LwPolyline) -> Vec<PropSection> {
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<PropSection> {
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);
}
}
+179 -11
View File
@@ -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<f64> {
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<PropSection> {
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);
}
}
+10 -20
View File
@@ -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()
}