forked from 1b-agent/opencadstudio-upstream
fix(hatch): flag only the outer ring as external when serializing holes
When writing a hatched region with holes to DXF/DWG, every NaN-separated ring was created with BoundaryPath::external(), so consumers treated inner loops as outer islands instead of holes. Only the first (outer) ring should carry the external / outermost flags; hole rings are left unflagged. A picked big-minus-small hatch now persists with the small rectangle as a real hole. Adds a regression test asserting only the first boundary path is flagged external.
This commit is contained in:
1 parent
934570862c
commit
5299a483da
2 files changed
+74
-5
No files matched your search
+20
-5
@@ -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<Vector2> = Vec::new();
|
||||
let mut push_ring = |r: &mut Vec<Vector2>| {
|
||||
let mut first = true;
|
||||
let mut push_ring = |r: &mut Vec<Vector2>, 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];
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
Reference in new issue
Block a user