fix(xclip): clip hatch fills and render nested-block hatches
Hatch fills render through the separate HatchModel pipeline, so the block wire clip never touched them: hatches outside an XCLIP boundary were still drawn. Clip each hatch boundary loop to the clip polygon (Sutherland-Hodgman, even-odd islands preserved) when the insert carries a spatial filter. Also fix hatches inside the boundary that were never drawn: the hatch collector exploded each insert only one level, so hatches nested in sub-blocks were dropped entirely. Walk the full block tree (depth guarded) so nested hatches render and clip like the rest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
dd1ec3ad40
commit
540d5f7b78
2 changed files with 191 additions and 14 deletions
|
|
@ -3703,23 +3703,57 @@ impl Scene {
|
|||
continue;
|
||||
}
|
||||
let selected = self.selected.contains(&ins.common.handle);
|
||||
for sub in ins
|
||||
// XCLIP: clip this insert's exploded hatch fills to the boundary,
|
||||
// matching how the line geometry is clipped in expand_insert.
|
||||
let clip_poly = xclip::insert_spatial_filter(&self.document, ins)
|
||||
.map(|sf| xclip::world_clip_polygon(sf, ins, hatch_offset));
|
||||
// Walk the full block tree: `explode_from_document` only descends
|
||||
// one level, so nested INSERTs are re-exploded here. Each level
|
||||
// bakes its transform into the children it returns, so nested
|
||||
// hatches land in the correct world position. A depth guard keeps
|
||||
// a malformed cyclic block reference from looping forever.
|
||||
let normalize = crate::modules::home::modify::explode::normalize_insert_entity;
|
||||
let mut stack: Vec<(EntityType, usize)> = ins
|
||||
.explode_from_document(&self.document)
|
||||
.into_iter()
|
||||
.map(crate::modules::home::modify::explode::normalize_insert_entity)
|
||||
{
|
||||
let EntityType::Hatch(dxf) = sub else {
|
||||
continue;
|
||||
};
|
||||
if dxf.common.invisible || layer_hidden(&dxf.common.layer) {
|
||||
continue;
|
||||
}
|
||||
let color = self.render_style(&EntityType::Hatch(dxf.clone())).0;
|
||||
if let Some(mut model) = Self::hatch_model_from_dxf(&dxf, color, hatch_offset) {
|
||||
if selected {
|
||||
model.color = [0.15, 0.55, 1.00, model.color[3]];
|
||||
.map(|e| (normalize(e), 0usize))
|
||||
.collect();
|
||||
while let Some((sub, depth)) = stack.pop() {
|
||||
match sub {
|
||||
EntityType::Insert(nins) => {
|
||||
if depth >= 32 {
|
||||
continue;
|
||||
}
|
||||
for e in nins.explode_from_document(&self.document) {
|
||||
stack.push((normalize(e), depth + 1));
|
||||
}
|
||||
}
|
||||
models.push(model);
|
||||
EntityType::Hatch(dxf) => {
|
||||
if dxf.common.invisible || layer_hidden(&dxf.common.layer) {
|
||||
continue;
|
||||
}
|
||||
let color = self.render_style(&EntityType::Hatch(dxf.clone())).0;
|
||||
if let Some(mut model) =
|
||||
Self::hatch_model_from_dxf(&dxf, color, hatch_offset)
|
||||
{
|
||||
if let Some(poly) = &clip_poly {
|
||||
let clipped = xclip::clip_hatch_boundary(
|
||||
&model.boundary,
|
||||
model.world_origin,
|
||||
poly,
|
||||
);
|
||||
if clipped.is_empty() {
|
||||
continue;
|
||||
}
|
||||
model.boundary = std::sync::Arc::new(clipped);
|
||||
}
|
||||
if selected {
|
||||
model.color = [0.15, 0.55, 1.00, model.color[3]];
|
||||
}
|
||||
models.push(model);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,6 +110,116 @@ pub fn clip_wires(wires: &mut Vec<WireModel>, poly: &[[f32; 2]]) {
|
|||
wires.retain(|w| !w.points.is_empty() || !w.fill_tris.is_empty());
|
||||
}
|
||||
|
||||
/// Clip a hatch fill boundary to `poly`.
|
||||
///
|
||||
/// `boundary` is a hatch's NaN-separated loops in f32 offsets from
|
||||
/// `world_origin` (the [`HatchModel`](crate::scene::hatch_model::HatchModel)
|
||||
/// representation); `poly` is the clip ring in the same world space the hatch
|
||||
/// occupies. Each loop is intersected with the clip polygon independently —
|
||||
/// the even-odd island structure is preserved because intersecting every loop
|
||||
/// with the same region distributes over the even-odd fill. Returns the
|
||||
/// clipped loops in offsets from `world_origin`; empty if nothing survives.
|
||||
pub fn clip_hatch_boundary(
|
||||
boundary: &[[f32; 2]],
|
||||
world_origin: [f64; 2],
|
||||
poly: &[[f32; 2]],
|
||||
) -> Vec<[f32; 2]> {
|
||||
if poly.len() < 3 {
|
||||
return boundary.to_vec();
|
||||
}
|
||||
let (ox, oy) = (world_origin[0] as f32, world_origin[1] as f32);
|
||||
let mut out: Vec<[f32; 2]> = Vec::new();
|
||||
let mut i = 0;
|
||||
while i < boundary.len() {
|
||||
if !boundary[i][0].is_finite() || !boundary[i][1].is_finite() {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let start = i;
|
||||
while i < boundary.len()
|
||||
&& boundary[i][0].is_finite()
|
||||
&& boundary[i][1].is_finite()
|
||||
{
|
||||
i += 1;
|
||||
}
|
||||
// Lift the loop into the clip polygon's world space.
|
||||
let loop_abs: Vec<[f32; 2]> = boundary[start..i]
|
||||
.iter()
|
||||
.map(|p| [p[0] + ox, p[1] + oy])
|
||||
.collect();
|
||||
let clipped = clip_polygon_2d(&loop_abs, poly);
|
||||
if clipped.len() >= 3 {
|
||||
if !out.is_empty() {
|
||||
out.push([f32::NAN, f32::NAN]);
|
||||
}
|
||||
for p in clipped {
|
||||
out.push([p[0] - ox, p[1] - oy]);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Sutherland–Hodgman clip of a closed 2D polygon `subject` against the convex
|
||||
/// polygon `clip`.
|
||||
fn clip_polygon_2d(subject: &[[f32; 2]], clip: &[[f32; 2]]) -> Vec<[f32; 2]> {
|
||||
let n = clip.len();
|
||||
let mut area2 = 0.0f32;
|
||||
let mut j = n - 1;
|
||||
for i in 0..n {
|
||||
area2 += clip[j][0] * clip[i][1] - clip[i][0] * clip[j][1];
|
||||
j = i;
|
||||
}
|
||||
let ccw = area2 > 0.0;
|
||||
|
||||
let mut output: Vec<[f32; 2]> = subject.to_vec();
|
||||
let mut j = n - 1;
|
||||
for i in 0..n {
|
||||
if output.is_empty() {
|
||||
break;
|
||||
}
|
||||
let (a, b) = (clip[j], clip[i]);
|
||||
j = i;
|
||||
let inside = |p: &[f32; 2]| {
|
||||
let cr = (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
|
||||
if ccw {
|
||||
cr >= 0.0
|
||||
} else {
|
||||
cr <= 0.0
|
||||
}
|
||||
};
|
||||
let input = std::mem::take(&mut output);
|
||||
let len = input.len();
|
||||
for k in 0..len {
|
||||
let cur = input[k];
|
||||
let prev = input[(k + len - 1) % len];
|
||||
let cur_in = inside(&cur);
|
||||
let prev_in = inside(&prev);
|
||||
if cur_in {
|
||||
if !prev_in {
|
||||
output.push(line_cross_2d(prev, cur, a, b));
|
||||
}
|
||||
output.push(cur);
|
||||
} else if prev_in {
|
||||
output.push(line_cross_2d(prev, cur, a, b));
|
||||
}
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn line_cross_2d(p0: [f32; 2], p1: [f32; 2], a: [f32; 2], b: [f32; 2]) -> [f32; 2] {
|
||||
let r = (p1[0] - p0[0], p1[1] - p0[1]);
|
||||
let s = (b[0] - a[0], b[1] - a[1]);
|
||||
let denom = r.0 * s.1 - r.1 * s.0;
|
||||
let t = if denom.abs() < 1e-12 {
|
||||
0.0
|
||||
} else {
|
||||
((a[0] - p0[0]) * s.1 - (a[1] - p0[1]) * s.0) / denom
|
||||
};
|
||||
[p0[0] + r.0 * t, p0[1] + r.1 * t]
|
||||
}
|
||||
|
||||
/// Ray-cast point-in-polygon test for a closed ring.
|
||||
fn point_in_poly(x: f32, y: f32, poly: &[[f32; 2]]) -> bool {
|
||||
let mut inside = false;
|
||||
|
|
@ -420,6 +530,39 @@ mod tests {
|
|||
assert!(pts.iter().any(|p| (p[0] - 10.0).abs() < 1e-3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hatch_boundary_clipped_kept_and_dropped() {
|
||||
let clip = square(); // 0..10
|
||||
// Hatch loop straddling the right edge → clipped to x<=10.
|
||||
let straddle = [[5.0, 5.0], [15.0, 5.0], [15.0, 8.0], [5.0, 8.0]];
|
||||
let out = clip_hatch_boundary(&straddle, [0.0, 0.0], &clip);
|
||||
assert!(!out.is_empty());
|
||||
assert!(out.iter().all(|p| p[0].is_nan() || p[0] <= 10.0 + 1e-3));
|
||||
|
||||
// Hatch fully inside → unchanged vertex count.
|
||||
let inside = [[1.0, 1.0], [4.0, 1.0], [4.0, 4.0], [1.0, 4.0]];
|
||||
let out_in = clip_hatch_boundary(&inside, [0.0, 0.0], &clip);
|
||||
assert_eq!(out_in.len(), 4);
|
||||
|
||||
// Hatch fully outside → dropped.
|
||||
let outside = [[20.0, 20.0], [25.0, 20.0], [25.0, 25.0]];
|
||||
let out_out = clip_hatch_boundary(&outside, [0.0, 0.0], &clip);
|
||||
assert!(out_out.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hatch_boundary_respects_world_origin() {
|
||||
// Same geometry as the straddle case but expressed as offsets from a
|
||||
// large world_origin — clipping must account for the origin shift.
|
||||
let clip = vec![[1000.0, 1000.0], [1010.0, 1000.0], [1010.0, 1010.0], [1000.0, 1010.0]];
|
||||
let origin = [1000.0, 1000.0];
|
||||
let loop_off = [[5.0, 5.0], [15.0, 5.0], [15.0, 8.0], [5.0, 8.0]];
|
||||
let out = clip_hatch_boundary(&loop_off, origin, &clip);
|
||||
assert!(!out.is_empty());
|
||||
// Offsets must stay <= 10 (i.e. world x <= 1010).
|
||||
assert!(out.iter().all(|p| p[0].is_nan() || p[0] <= 10.0 + 1e-3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn world_polygon_applies_inverse_block_then_insert() {
|
||||
use acadrust::types::{Matrix4, Vector2};
|
||||
|
|
|
|||
Loading…
Reference in a new issue