fix(hatch): pick innermost boundary and detect nested holes

The HATCH/GRADIENT/BOUNDARY "pick inside" mode returned the first outline containing the click point, so the result depended on draw order and any nested boundary was ignored. For a small rectangle inside a big one, clicking inside the small shape would hatch the whole big rectangle (or the small one, depending on which was drawn first), and clicking in the gap between them always hatched the full big rectangle with no hole.

Resolve the click against every enclosing outline and choose the smallest-area one as the fill. Any other outline fully contained by that ring but not containing the click point becomes a hole. The boundary is emitted as NaN-separated rings, which the GPU and CPU rasteriser already fill via even-odd, so pattern, solid, and gradient hatches all render the correct region (the small shape, or the big-minus-small ring) regardless of draw order. DXF export now writes one boundary path per ring so nested hatches persist with real holes.
This commit is contained in:
Karim Jerbi committed 2026-07-13 01:32:05 +01:00
1 parent fb8e2aeec1
commit 934570862c
2 files changed
+211 -41

No files matched your search

+32 -13
View File
@@ -1651,25 +1651,44 @@ impl Scene {
// render-side `boundary`, the points arrive in local render space
// (world_offset already subtracted), so add `world_origin` back —
// otherwise the boundary wire lands `world_offset` away from the fill.
let verts: Vec<Vector2> = if let Some(wcs) = &model.boundary_wcs {
wcs.iter()
.filter(|v| v[0].is_finite() && v[1].is_finite())
.map(|&[x, y]| Vector2::new(x, y))
.collect()
} else {
// Build one DXF boundary path per ring. The command layer separates
// 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.
if let Some(wcs) = &model.boundary_wcs {
let mut ring: Vec<Vector2> = Vec::new();
let mut push_ring = |r: &mut Vec<Vector2>| {
if !r.is_empty() {
let edge = PolylineEdge::new(std::mem::take(r), true);
let mut path = BoundaryPath::external();
path.add_edge(BoundaryEdge::Polyline(edge));
dxf.paths.push(path);
}
};
for &[x, y] in wcs.iter() {
if x.is_finite() && y.is_finite() {
ring.push(Vector2::new(x, y));
} else {
push_ring(&mut ring);
}
}
push_ring(&mut ring);
}
if dxf.paths.is_empty() {
let wx = model.world_origin[0];
let wy = model.world_origin[1];
model
let verts: Vec<Vector2> = model
.boundary
.iter()
.filter(|v| v[0].is_finite() && v[1].is_finite())
.map(|&[x, y]| Vector2::new(x as f64 + wx, y as f64 + wy))
.collect()
};
let edge = PolylineEdge::new(verts, true);
let mut path = BoundaryPath::external();
path.add_edge(BoundaryEdge::Polyline(edge));
dxf.paths.push(path);
.collect();
let edge = PolylineEdge::new(verts, true);
let mut path = BoundaryPath::external();
path.add_edge(BoundaryEdge::Polyline(edge));
dxf.paths.push(path);
}
if let Some(entry) = crate::scene::model::hatch_patterns::find(&model.name) {
dxf.pattern = crate::scene::model::hatch_patterns::build_dxf_pattern(entry);
}