diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 011839528..bf2cfd239 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1655,13 +1655,26 @@ impl Scene { // the outer boundary from its holes with NaN sentinels in // `boundary_wcs`; split on those so nested hatches (e.g. a small // rectangle inside a big one) persist with real holes instead of a - // single self-intersecting polyline. + // single self-intersecting polyline. Only the FIRST ring carries the + // external / outermost flags — every later ring is a hole and must not + // be flagged external, or DXF/DWG consumers treat the inner loop as + // another outer island rather than a hole. if let Some(wcs) = &model.boundary_wcs { let mut ring: Vec = Vec::new(); - let mut push_ring = |r: &mut Vec| { + let mut first = true; + let mut push_ring = |r: &mut Vec, is_outer: bool| { if !r.is_empty() { let edge = PolylineEdge::new(std::mem::take(r), true); - let mut path = BoundaryPath::external(); + let mut path = if is_outer { + let mut p = BoundaryPath::external(); + p.flags = acadrust::entities::hatch::BoundaryPathFlags::from_bits( + p.flags.bits() + | acadrust::entities::hatch::BoundaryPathFlags::OUTERMOST.bits(), + ); + p + } else { + BoundaryPath::new() + }; path.add_edge(BoundaryEdge::Polyline(edge)); dxf.paths.push(path); } @@ -1670,10 +1683,12 @@ impl Scene { if x.is_finite() && y.is_finite() { ring.push(Vector2::new(x, y)); } else { - push_ring(&mut ring); + let is_outer = first; + first = false; + push_ring(&mut ring, is_outer); } } - push_ring(&mut ring); + push_ring(&mut ring, first); } if dxf.paths.is_empty() { let wx = model.world_origin[0]; diff --git a/tests/block_hatch_export.rs b/tests/block_hatch_export.rs index 234b3d5e5..79d8e663e 100644 --- a/tests/block_hatch_export.rs +++ b/tests/block_hatch_export.rs @@ -266,3 +266,57 @@ fn app_created_hatch_roundtrips_catalog_spacing() { — build_dxf_pattern must store the world-frame offset, not the local step" ); } + +// Regression: a picked "big minus small" hatch must serialize the outer ring +// with the external / outermost flags and each hole ring WITHOUT them. If every +// NaN-separated ring were flagged external, DXF/DWG consumers would treat the +// inner loop as another outer island instead of a hole. +#[test] +fn nested_hatch_serializes_only_outer_as_external() { + use std::sync::Arc; + + use OpenCADStudio::scene::model::hatch_model::HatchModel; + + let outer: Vec<[f64; 2]> = vec![[-10.0, -10.0], [10.0, -10.0], [10.0, 10.0], [-10.0, 10.0]]; + let hole: Vec<[f64; 2]> = vec![[-5.0, -5.0], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; + + // Outer boundary + hole, NaN-separated, exactly as the HATCH command packs + // them in `boundary_wcs`. + let mut wcs: Vec<[f64; 2]> = outer.clone(); + wcs.push([f64::NAN, f64::NAN]); + wcs.extend(hole.iter().copied()); + + let boundary_f32: Vec<[f32; 2]> = wcs.iter().map(|&[x, y]| [x as f32, y as f32]).collect(); + + let model = HatchModel { + world_origin: [0.0, 0.0], + boundary: Arc::new(boundary_f32), + boundary_wcs: Some(Arc::new(wcs)), + pattern: HatchPattern::Solid, + name: "SOLID".into(), + color: [0.45, 0.45, 0.45, 0.60], + angle_offset: 0.0, + scale: 1.0, + vp_scissor: None, + draw_depth: 0.0, + }; + + let mut scene = Scene::new(); + scene.add_hatch(model); + + let dxf = scene + .document + .entities() + .find_map(|e| if let EntityType::Hatch(h) = e { Some(h) } else { None }) + .expect("nested hatch written to document"); + + assert_eq!(dxf.paths.len(), 2, "outer boundary + one hole path"); + + let ex = BoundaryPathFlags::EXTERNAL.bits(); + let out = BoundaryPathFlags::OUTERMOST.bits(); + + assert!(dxf.paths[0].flags.bits() & ex != 0, "outer path must be flagged external"); + assert!(dxf.paths[0].flags.bits() & out != 0, "outer path must be flagged outermost"); + assert!(dxf.paths[1].flags.bits() & ex == 0, "hole path must NOT be flagged external"); + assert!(dxf.paths[1].flags.bits() & out == 0, "hole path must NOT be flagged outermost"); +}