Merge pull request #963 from atirna/fix/polyline-thickness-editing

fix(modify): keep polyline thickness through join/convert/explode
This commit is contained in:
Hakan Seven authored and GitHub committed 2026-09-02 08:58:21 +03:00
commit bde9cc2715
3 files changed
+291 -13

No files matched your search

+97 -4
View File
@@ -153,6 +153,11 @@ fn explode_polyline2d(p: &Polyline2D) -> Vec<EntityType> {
let closed = p.is_closed();
let n_segs = if closed { n } else { n - 1 };
let elevation = p.elevation;
let normal = DVec3::new(p.normal.x, p.normal.y, p.normal.z)
.try_normalize()
.unwrap_or(DVec3::Z);
let normal = Vector3::new(normal.x, normal.y, normal.z);
let plane = crate::entities::curve::ocs_plane(normal.clone(), elevation);
let mut result = Vec::new();
for i in 0..n_segs {
@@ -164,13 +169,25 @@ fn explode_polyline2d(p: &Polyline2D) -> Vec<EntityType> {
if v0.bulge.abs() < 1e-10 {
let mut common = p.common.clone();
common.handle = Handle::NULL;
let start = plane.point_at(p0);
let end = plane.point_at(p1);
result.push(EntityType::Line(LineEnt {
common,
start: Vector3::new(p0[0], p0[1], elevation),
end: Vector3::new(p1[0], p1[1], elevation),
start: Vector3::new(start[0], start[1], start[2]),
end: Vector3::new(end[0], end[1], end[2]),
thickness: p.thickness,
normal: normal.clone(),
..LineEnt::new()
}));
} else if let Some(arc) = bulge_to_arc(p0, p1, v0.bulge, elevation, &p.common) {
} else if let Some(arc) = bulge_to_arc(
p0,
p1,
v0.bulge,
elevation,
&p.common,
p.thickness,
normal.clone(),
) {
result.push(arc);
}
}
@@ -237,12 +254,22 @@ fn explode_lwpolyline(p: &LwPolyline) -> Vec<EntityType> {
common,
start: Vector3::new(p0[0], p0[1], elevation),
end: Vector3::new(p1[0], p1[1], elevation),
thickness: p.thickness,
normal: p.normal.clone(),
..LineEnt::new()
};
result.push(EntityType::Line(line));
} else {
// Arc segment from bulge
if let Some(arc) = bulge_to_arc(p0, p1, v0.bulge, elevation, &p.common) {
if let Some(arc) = bulge_to_arc(
p0,
p1,
v0.bulge,
elevation,
&p.common,
p.thickness,
p.normal.clone(),
) {
result.push(arc);
}
}
@@ -258,6 +285,8 @@ fn bulge_to_arc(
bulge: f64,
elevation: f64,
common_src: &EntityCommon,
thickness: f64,
normal: Vector3,
) -> Option<EntityType> {
let ba = crate::entities::common::BulgeArc::from_bulge(p0, p1, bulge)?;
@@ -280,6 +309,8 @@ fn bulge_to_arc(
radius: ba.radius,
start_angle,
end_angle,
thickness,
normal,
..ArcEnt::new()
};
Some(EntityType::Arc(arc))
@@ -1720,4 +1751,66 @@ mod tests {
dim_line
);
}
// EXPLODE replaces the polyline with fresh Line/Arc entities; each piece
// must carry the source thickness instead of resetting to 0 (#916).
#[test]
fn explode_keeps_lwpolyline_thickness() {
use acadrust::entities::LwVertex;
use acadrust::types::Vector2;
let mut pl = LwPolyline::new();
// One straight span + one bulged span, so the result holds a Line and
// an Arc and both paths through the rebuild are covered.
let mut v1 = LwVertex::new(Vector2::new(0.0, 0.0));
v1.bulge = 1.0; // semicircle to the next vertex
pl.vertices = vec![
v1,
LwVertex::new(Vector2::new(10.0, 0.0)),
LwVertex::new(Vector2::new(20.0, 0.0)),
];
pl.thickness = 4.0;
let pieces = explode_polyline_segments(&EntityType::LwPolyline(pl));
assert!(pieces.len() >= 2, "expected a line and an arc piece");
for piece in &pieces {
match piece {
EntityType::Line(l) => assert!(
(l.thickness - 4.0).abs() < 1e-12,
"exploded line must keep thickness, got {}",
l.thickness
),
EntityType::Arc(a) => assert!(
(a.thickness - 4.0).abs() < 1e-12,
"exploded arc must keep thickness, got {}",
a.thickness
),
other => panic!("unexpected piece type: {other:?}"),
}
}
}
#[test]
fn explode_keeps_polyline2d_extrusion() {
use acadrust::entities::Vertex2D;
let mut pl = Polyline2D::new();
pl.vertices = vec![
Vertex2D::new(Vector3::new(0.0, 0.0, 0.0)),
Vertex2D::new(Vector3::new(10.0, 0.0, 0.0)),
];
pl.elevation = 2.0;
pl.thickness = -4.0;
pl.normal = Vector3::new(1.0, 0.0, 0.0);
let pieces = explode_polyline_segments(&EntityType::Polyline2D(pl));
let Some(EntityType::Line(line)) = pieces.first() else {
panic!("expected one line");
};
assert_eq!(pieces.len(), 1);
assert_eq!(line.normal, Vector3::new(1.0, 0.0, 0.0));
assert!((line.thickness + 4.0).abs() < 1e-12);
assert!((line.start.x - 2.0).abs() < 1e-12);
assert!((line.end.x - 2.0).abs() < 1e-12);
}
}
+123 -6
View File
@@ -94,6 +94,25 @@ fn v3(p: DVec3) -> Vector3 {
Vector3::new(p.x, p.y, p.z)
}
fn extrusion(entity: &EntityType) -> Option<(f64, DVec3)> {
let mut thickness = crate::scene::view::dispatch::entity_thickness(entity)?;
let normal = match entity {
EntityType::Arc(entity) => &entity.normal,
EntityType::Line(entity) => &entity.normal,
EntityType::LwPolyline(entity) => &entity.normal,
EntityType::Polyline2D(entity) => &entity.normal,
_ => return None,
};
let normal = DVec3::new(normal.x, normal.y, normal.z);
if thickness == 0.0 {
return Some((0.0, normal.try_normalize().unwrap_or(DVec3::Z)));
}
let length = normal.length();
let normal = normal.try_normalize()?;
thickness *= length;
(thickness.is_finite() && normal.is_finite()).then_some((thickness, normal))
}
/// Build the chain segments for one entity, or `None` for an entity type
/// JOIN can't carry (which aborts the whole join). Lines and arcs contribute
/// one segment; an OPEN polyline contributes one per span (bulges kept), so
@@ -143,12 +162,23 @@ fn segs_of(e: &EntityType) -> Option<Vec<Seg>> {
if p.is_closed() || p.vertices.len() < 2 {
return None;
}
let normal = DVec3::new(p.normal.x, p.normal.y, p.normal.z).try_normalize()?;
if normal.x.abs() > 1e-12 || normal.y.abs() > 1e-12 {
return None;
}
let cadkernel::geom2d::Curve::Polyline(curve) =
crate::entities::curve::entity_curve_xy(&EntityType::Polyline2D(p.clone()))?
else {
return None;
};
let z = crate::entities::curve::ocs_plane(p.normal.clone(), p.elevation).origin[2];
Some(
p.vertices
curve
.vertices
.windows(2)
.map(|w| Seg {
a: DVec3::new(w[0].location.x, w[0].location.y, w[0].location.z),
b: DVec3::new(w[1].location.x, w[1].location.y, w[1].location.z),
a: DVec3::new(w[0].position[0], w[0].position[1], z),
b: DVec3::new(w[1].position[0], w[1].position[1], z),
bulge: w[0].bulge,
})
.collect(),
@@ -176,6 +206,7 @@ pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec<Handle>,
}
let handles: Vec<Handle> = entities.iter().map(|(h, _)| *h).collect();
let common = entities[0].1.common().clone();
let (thickness, normal) = extrusion(entities[0].1)?;
let (chain, closed) = stitch(segs)?;
@@ -198,15 +229,24 @@ pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec<Handle>,
line.common.handle = Handle::NULL;
line.start = v3(verts.first().unwrap().0);
line.end = v3(verts.last().unwrap().0);
line.normal = v3(normal);
line.thickness = thickness;
return Some((handles, vec![EntityType::Line(line)]));
}
if planar {
if thickness != 0.0 && normal.x.hypot(normal.y) > 1e-9 {
return None;
}
let flipped = thickness != 0.0 && normal.z < 0.0;
let lw_verts: Vec<acadrust::entities::LwVertex> = verts
.iter()
.map(|(p, bulge)| {
let mut v = acadrust::entities::LwVertex::new(Vector2::new(p.x, p.y));
v.bulge = *bulge;
let mut v = acadrust::entities::LwVertex::new(Vector2::new(
if flipped { -p.x } else { p.x },
p.y,
));
v.bulge = if flipped { -*bulge } else { *bulge };
v
})
.collect();
@@ -215,7 +255,11 @@ pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec<Handle>,
pl.common.handle = Handle::NULL;
pl.vertices = lw_verts;
pl.is_closed = closed;
pl.elevation = z0;
pl.elevation = if flipped { -z0 } else { z0 };
pl.thickness = thickness;
if flipped {
pl.normal = Vector3::new(0.0, 0.0, -1.0);
}
return Some((handles, vec![EntityType::LwPolyline(pl)]));
}
@@ -224,6 +268,9 @@ pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec<Handle>,
if has_arc {
return None;
}
if thickness != 0.0 {
return None;
}
let mut pl = acadrust::entities::Polyline3D::new();
pl.common = common;
pl.common.handle = Handle::NULL;
@@ -284,6 +331,76 @@ fn stitch(mut segs: Vec<Seg>) -> Option<(Vec<Seg>, bool)> {
Some((chain, closed))
}
#[cfg(test)]
mod join_tests {
use super::*;
use acadrust::entities::{Line as LineEnt, LwVertex};
use acadrust::Handle;
fn line(x0: f64, y0: f64, x1: f64, y1: f64, thickness: f64) -> EntityType {
let mut l = LineEnt::new();
l.start = Vector3::new(x0, y0, 0.0);
l.end = Vector3::new(x1, y1, 0.0);
l.thickness = thickness;
EntityType::Line(l)
}
fn lw_2pts(p0: (f64, f64), p1: (f64, f64), thickness: f64) -> EntityType {
let mut pl = acadrust::entities::LwPolyline::new();
pl.vertices = vec![
LwVertex::new(Vector2::new(p0.0, p0.1)),
LwVertex::new(Vector2::new(p1.0, p1.1)),
];
pl.thickness = thickness;
EntityType::LwPolyline(pl)
}
// JOIN rebuilds the result entity; thickness must follow the chain's first
// entity (the same source `common` comes from), not reset to 0 (#916).
#[test]
fn join_keeps_source_thickness() {
let h1 = Handle::new(1);
let h2 = Handle::new(2);
let mut e1 = lw_2pts((0.0, 0.0), (10.0, 0.0), 2.5);
let EntityType::LwPolyline(source) = &mut e1 else {
unreachable!();
};
source.normal = Vector3::new(0.0, 0.0, -1.0);
// A corner keeps the chain from collapsing back to a single Line.
let e2 = line(10.0, 0.0, 10.0, 10.0, 0.0);
let (removed, out) = join_entities(&[(h1, &e1), (h2, &e2)]).expect("chain joins");
assert_eq!(removed.len(), 2);
let Some(EntityType::LwPolyline(pl)) = out.first() else {
panic!("expected joined lwpolyline");
};
assert!(
(pl.thickness - 2.5).abs() < 1e-12,
"joined polyline must keep source thickness, got {}",
pl.thickness
);
assert_eq!(pl.normal, Vector3::new(0.0, 0.0, -1.0));
}
// A collinear straight run collapses to a single Line; its thickness must
// come from the first source entity too, not default to 0.
#[test]
fn collinear_join_keeps_thickness() {
let h1 = Handle::new(1);
let h2 = Handle::new(2);
let e1 = line(0.0, 0.0, 5.0, 0.0, 1.25);
let e2 = line(5.0, 0.0, 10.0, 0.0, 0.0);
let (_, out) = join_entities(&[(h1, &e1), (h2, &e2)]).expect("chain joins");
let Some(EntityType::Line(l)) = out.first() else {
panic!("expected collapsed line");
};
assert!(
(l.thickness - 1.25).abs() < 1e-12,
"collapsed line must keep source thickness, got {}",
l.thickness
);
}
}
/// True when every vertex lies on one straight line (within tolerance).
fn is_collinear(verts: &[(DVec3, f64)]) -> bool {
if verts.len() < 3 {
+71 -3
View File
@@ -1,7 +1,7 @@
// PEDIT edits polylines and polygon meshes.
use acadrust::entities::LwVertex;
use acadrust::types::Vector2;
use acadrust::types::{Vector2, Vector3};
use acadrust::{EntityType, Handle};
use cadkernel::geom2d::nurbs::clamped_uniform_knots;
use cadkernel::geom2d::NurbsCurve;
@@ -685,14 +685,35 @@ pub fn convert_to_polyline(entity: &EntityType) -> Option<EntityType> {
let mut pl = acadrust::LwPolyline::new();
match entity {
EntityType::Line(l) => {
let normal = DVec3::new(l.normal.x, l.normal.y, l.normal.z).try_normalize()?;
let normal = Vector3::new(normal.x, normal.y, normal.z);
let start = [l.start.x, l.start.y, l.start.z];
let end = [l.end.x, l.end.y, l.end.z];
let elevation = DVec3::from_array(start).dot(DVec3::new(
normal.x, normal.y, normal.z,
));
let plane = crate::entities::curve::ocs_plane(normal.clone(), elevation);
let tolerance = cadkernel::space::coplanarity_tolerance(&[start, end]);
if !plane.contains(end, tolerance) {
return None;
}
let start = plane.project(start)?;
let end = plane.project(end)?;
pl.common = l.common.clone();
pl.thickness = l.thickness;
pl.elevation = elevation;
pl.normal = normal;
pl.vertices = vec![
LwVertex::new(Vector2::new(l.start.x, l.start.y)),
LwVertex::new(Vector2::new(l.end.x, l.end.y)),
LwVertex::new(Vector2::new(start[0], start[1])),
LwVertex::new(Vector2::new(end[0], end[1])),
];
}
EntityType::Arc(a) => {
let normal = DVec3::new(a.normal.x, a.normal.y, a.normal.z).try_normalize()?;
pl.common = a.common.clone();
pl.thickness = a.thickness;
pl.elevation = a.center.z;
pl.normal = Vector3::new(normal.x, normal.y, normal.z);
let (sa, ea) = (a.start_angle, a.end_angle);
let sweep = {
let s = (ea - sa).rem_euclid(TAU);
@@ -720,6 +741,53 @@ pub fn convert_to_polyline(entity: &EntityType) -> Option<EntityType> {
Some(EntityType::LwPolyline(pl))
}
#[cfg(test)]
mod convert_tests {
use super::*;
// PEDIT convert rebuilds the entity as a polyline; thickness must move
// onto the new entity instead of resetting to 0 (#916).
#[test]
fn convert_keeps_line_thickness() {
let mut l = acadrust::entities::Line::new();
l.start = acadrust::types::Vector3::new(2.0, 3.0, 4.0);
l.end = acadrust::types::Vector3::new(2.0, 5.0, 6.0);
l.normal = acadrust::types::Vector3::new(1.0, 0.0, 0.0);
l.thickness = 3.5;
let Some(EntityType::LwPolyline(pl)) = convert_to_polyline(&EntityType::Line(l)) else {
panic!("line must convert");
};
assert!(
(pl.thickness - 3.5).abs() < 1e-12,
"converted polyline must keep source thickness, got {}",
pl.thickness
);
assert_eq!(pl.normal, acadrust::types::Vector3::new(1.0, 0.0, 0.0));
assert!((pl.elevation - 2.0).abs() < 1e-12);
}
#[test]
fn convert_keeps_arc_thickness() {
let mut a = acadrust::entities::Arc::new();
a.center = acadrust::types::Vector3::new(0.0, 0.0, 4.0);
a.normal = acadrust::types::Vector3::new(0.0, 1.0, 0.0);
a.radius = 5.0;
a.start_angle = 0.0;
a.end_angle = std::f64::consts::FRAC_PI_2;
a.thickness = -2.0;
let Some(EntityType::LwPolyline(pl)) = convert_to_polyline(&EntityType::Arc(a)) else {
panic!("arc must convert");
};
assert!(
(pl.thickness - (-2.0)).abs() < 1e-12,
"converted polyline must keep negative thickness, got {}",
pl.thickness
);
assert_eq!(pl.normal, acadrust::types::Vector3::new(0.0, 1.0, 0.0));
assert!((pl.elevation - 4.0).abs() < 1e-12);
}
}
// ── Curve fitting ─────────────────────────────────────────────────────────
fn vert_xy(v: &LwVertex) -> DVec2 {