diff --git a/src/modules/draw/modify/explode.rs b/src/modules/draw/modify/explode.rs index b98d3b030..52606f06e 100644 --- a/src/modules/draw/modify/explode.rs +++ b/src/modules/draw/modify/explode.rs @@ -153,6 +153,11 @@ fn explode_polyline2d(p: &Polyline2D) -> Vec { 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,15 +169,25 @@ fn explode_polyline2d(p: &Polyline2D) -> Vec { 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, p.thickness) - { + } else if let Some(arc) = bulge_to_arc( + p0, + p1, + v0.bulge, + elevation, + &p.common, + p.thickness, + normal.clone(), + ) { result.push(arc); } } @@ -240,12 +255,21 @@ fn explode_lwpolyline(p: &LwPolyline) -> Vec { 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, p.thickness) { + if let Some(arc) = bulge_to_arc( + p0, + p1, + v0.bulge, + elevation, + &p.common, + p.thickness, + p.normal.clone(), + ) { result.push(arc); } } @@ -262,6 +286,7 @@ fn bulge_to_arc( elevation: f64, common_src: &EntityCommon, thickness: f64, + normal: Vector3, ) -> Option { let ba = crate::entities::common::BulgeArc::from_bulge(p0, p1, bulge)?; @@ -285,6 +310,7 @@ fn bulge_to_arc( start_angle, end_angle, thickness, + normal, ..ArcEnt::new() }; Some(EntityType::Arc(arc)) @@ -1763,4 +1789,28 @@ mod tests { } } } + + #[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); + } } diff --git a/src/modules/draw/modify/join.rs b/src/modules/draw/modify/join.rs index 32dc0411b..8d1eca9d7 100644 --- a/src/modules/draw/modify/join.rs +++ b/src/modules/draw/modify/join.rs @@ -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> { 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,9 +206,7 @@ pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec, } let handles: Vec = entities.iter().map(|(h, _)| *h).collect(); let common = entities[0].1.common().clone(); - // Thickness (DXF 39) lives on the entity, not in `common`, so the rebuilt - // chain must carry it too or a thick source joins into a flat result (#916). - let thickness = crate::scene::view::dispatch::entity_thickness(entities[0].1).unwrap_or(0.0); + let (thickness, normal) = extrusion(entities[0].1)?; let (chain, closed) = stitch(segs)?; @@ -201,16 +229,24 @@ pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec, 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 = 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(); @@ -219,8 +255,11 @@ pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec, 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)])); } @@ -229,6 +268,9 @@ pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec, 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; @@ -319,7 +361,11 @@ mod join_tests { fn join_keeps_source_thickness() { let h1 = Handle::new(1); let h2 = Handle::new(2); - let e1 = lw_2pts((0.0, 0.0), (10.0, 0.0), 2.5); + 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"); @@ -332,6 +378,7 @@ mod join_tests { "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 diff --git a/src/modules/draw/modify/pedit.rs b/src/modules/draw/modify/pedit.rs index d3d999776..54e53e178 100644 --- a/src/modules/draw/modify/pedit.rs +++ b/src/modules/draw/modify/pedit.rs @@ -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,16 +685,35 @@ pub fn convert_to_polyline(entity: &EntityType) -> Option { 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); @@ -731,8 +750,9 @@ mod convert_tests { #[test] fn convert_keeps_line_thickness() { let mut l = acadrust::entities::Line::new(); - l.start = acadrust::types::Vector3::new(0.0, 0.0, 0.0); - l.end = acadrust::types::Vector3::new(10.0, 0.0, 0.0); + 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"); @@ -742,12 +762,15 @@ mod convert_tests { "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, 0.0); + 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; @@ -760,6 +783,8 @@ mod convert_tests { "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); } }