forked from 1b-agent/opencadstudio-upstream
Merge pull request #963 from atirna/fix/polyline-thickness-editing
fix(modify): keep polyline thickness through join/convert/explode
This commit is contained in:
3 files changed
+291
-13
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user