diff --git a/crates/io/src/dxf_read/entities.rs b/crates/io/src/dxf_read/entities.rs index fc55fe335..12abae7da 100644 --- a/crates/io/src/dxf_read/entities.rs +++ b/crates/io/src/dxf_read/entities.rs @@ -201,7 +201,17 @@ pub(super) fn entity_with_table_budget(kind: &str, tags: &[Tag], table_cells_lef }) } "LEADER" => EntityKind::Leader(Leader { - vertices: t.pts(10).into_iter().map(|p| p.to3(0.0)).collect(), + // Each group 10 starts a vertex. Scope Y/Z to it so an omitted Z cannot + // shift a later vertex's elevation onto this one. + vertices: t + .all_f(10) + .into_iter() + .zip(tags.split(|tag| tag.code == 10).skip(1)) + .map(|(x, vertex)| { + let v = T(vertex); + Vec3::new(x, v.fd(20, 0.0), v.fd(30, 0.0)) + }) + .collect(), arrow: t.i(71).unwrap_or(1) != 0, spline: t.i(72).unwrap_or(0) != 0, style: t.s(3).unwrap_or_else(|| "Standard".into()), diff --git a/crates/io/tests/it/leader_vertex_elevation.rs b/crates/io/tests/it/leader_vertex_elevation.rs new file mode 100644 index 000000000..2566a861b --- /dev/null +++ b/crates/io/tests/it/leader_vertex_elevation.rs @@ -0,0 +1,106 @@ +//! LEADER vertex elevations survive the shared DXF read path and DWG bridge (#2245). + +use cadcraft_color::Color; +use cadcraft_doc::{Common, DimStyle, Drawing, EntityKind, Handle, Leader, Lineweight, Space}; +use cadcraft_dxf::Tag; +use cadcraft_geom::Vec3; +use cadcraft_io::{SaveOptions, Version, read, write_with}; + +type TestResult = Result>; + +fn drawing() -> TestResult { + let mut d = Drawing::new_metric(); + d.dim_styles.push(DimStyle { name: "Elevation".into(), ..DimStyle::default() }); + let common = Common { color: Color::Index(3), lineweight: Lineweight::Mm100(25), ltscale: 2.5, visible: false, ..Common::default() }; + for vertices in [ + vec![Vec3::new(1.0, 2.0, 4.0), Vec3::new(5.0, 6.0, 7.0), Vec3::new(9.0, 10.0, -3.0)], + vec![Vec3::new(1.0, 2.0, 0.0), Vec3::new(5.0, 6.0, 0.0), Vec3::new(9.0, 10.0, 0.0)], + ] { + for arrow in [false, true] { + for spline in [false, true] { + let leader = Leader { vertices: vertices.clone(), arrow, spline, style: "Elevation".into() }; + d.add(&Space::Model, common.clone(), EntityKind::Leader(leader))?; + } + } + } + Ok(d) +} + +fn check_round_trips(name: &str, binary: bool) -> TestResult { + let original = drawing()?; + for version in Version::ALL { + let mut d = original.clone(); + for pass in 1..=2 { + let bytes = write_with(&d, name, &SaveOptions { version: Some(version), binary })?; + d = read(&bytes, name)?; + assert_eq!(d.model.len(), original.model.len(), "{name} {version:?}, pass={pass}"); + for expected in original.model.iter() { + let actual = d.entity(expected.handle).ok_or("LEADER handle lost")?; + let (EntityKind::Leader(want), EntityKind::Leader(got)) = (&expected.kind, &actual.kind) else { + return Err("LEADER entity type changed".into()); + }; + assert_eq!(got.vertices, want.vertices, "XYZ/order: {name} {version:?}, pass={pass}, {:?}", expected.handle); + assert_eq!((got.arrow, got.spline, &got.style), (want.arrow, want.spline, &want.style), "LEADER properties"); + assert_eq!(actual.common, expected.common, "Common properties: {name} {version:?}, pass={pass}"); + } + } + } + Ok(()) +} + +#[test] +fn dxf_preserves_leader_vertex_elevations() -> TestResult { + for binary in [false, true] { + check_round_trips("leader.dxf", binary)?; + } + Ok(()) +} + +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn dwg_preserves_leader_vertex_elevations() -> TestResult { + check_round_trips("leader.dwg", false) +} + +#[test] +fn missing_z_defaults_only_the_corresponding_vertex() -> TestResult { + let vertices = [Vec3::new(1.0, 2.0, 4.0), Vec3::new(5.0, 6.0, 7.0), Vec3::new(9.0, 10.0, -3.0)]; + // All eight presence patterns include wholly 2D input and omitted first/middle/last Zs. + // Author the groups directly, independently of CADCraft's entity writer. + for z_mask in 0..8 { + let mut tags = vec![ + Tag::s(0, "SECTION"), + Tag::s(2, "ENTITIES"), + Tag::s(0, "LEADER"), + Tag::s(5, "10"), + Tag::s(100, "AcDbEntity"), + Tag::s(8, "0"), + Tag::s(100, "AcDbLeader"), + Tag::s(3, "Standard"), + Tag::i(71, 0), + Tag::i(72, 1), + Tag::i(76, 3), + ]; + let mut expected = Vec::new(); + for (i, p) in vertices.iter().enumerate() { + tags.push(Tag::f(10, p.x)); + let has_z = z_mask & (1 << i) != 0; + if has_z { + // Y and Z can appear in either order within this vertex. + tags.push(Tag::f(30, p.z)); + } + tags.push(Tag::f(20, p.y)); + expected.push(Vec3::new(p.x, p.y, if has_z { p.z } else { 0.0 })); + } + tags.extend([Tag::s(0, "ENDSEC"), Tag::s(0, "EOF")]); + let dxf = cadcraft_dxf::write_ascii(&tags); + let d = read(dxf.as_bytes(), "missing-z.dxf")?; + let e = d.entity(Handle(0x10)).ok_or("LEADER missing")?; + let EntityKind::Leader(l) = &e.kind else { return Err("expected LEADER".into()) }; + assert_eq!(l.vertices, expected, "Z presence mask {z_mask:03b}"); + assert!(!l.arrow && l.spline); + assert_eq!(l.style, "Standard"); + assert_eq!(e.common, Common::default()); + } + Ok(()) +} diff --git a/crates/io/tests/it/main.rs b/crates/io/tests/it/main.rs index 7f6ad8f20..52f37068a 100644 --- a/crates/io/tests/it/main.rs +++ b/crates/io/tests/it/main.rs @@ -33,6 +33,7 @@ mod hatch_pattern_def; mod helix_roundtrip; mod layout_paper_units; mod layout_plot_window; +mod leader_vertex_elevation; #[cfg(not(target_arch = "wasm32"))] mod low_handseed; mod mleader_content;