diff --git a/src/scene/hatch_model.rs b/src/scene/hatch_model.rs index 19e29b0c9..dfbbadb25 100644 --- a/src/scene/hatch_model.rs +++ b/src/scene/hatch_model.rs @@ -1,10 +1,12 @@ // HatchModel — CPU-side hatch fill data; rendered entirely on the GPU. use std::sync::Arc; // -// The boundary is a closed polygon in world XY coordinates (max 64 vertices). +// The boundary is a closed polygon in world XY coordinates. // The GPU fragment shader performs point-in-polygon and hatch-line tests so // no line geometry is ever tessellated on the CPU. +pub const MAX_HATCH_BOUNDARY_VERTS: usize = 1024; + /// One line family from a PAT-format hatch pattern. /// /// Format mirrors the standard PAT line format: @@ -41,7 +43,7 @@ pub enum HatchPattern { /// A hatched region defined by a closed polygon boundary. #[derive(Clone, Debug)] pub struct HatchModel { - /// World XY coordinates of the boundary polygon vertices (max 64). + /// World XY coordinates of the boundary polygon vertices. /// Arc so HatchModel::clone() is a pointer bump rather than a full Vec copy. pub boundary: Arc>, /// Fill pattern. diff --git a/src/scene/mod.rs b/src/scene/mod.rs index e98ffb7e4..724055e99 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1916,19 +1916,35 @@ impl Scene { fn hatch_model_from_dxf(dxf: &DxfHatch, color: [f32; 4], world_offset: [f64; 3]) -> Option { let [ox, oy, _oz] = world_offset; - let path = dxf - .paths - .iter() - .find(|p| p.flags.is_external()) - .or_else(|| dxf.paths.first())?; + let normal = (dxf.normal.x, dxf.normal.y, dxf.normal.z); + let to_xy = |x: f64, y: f64| -> [f32; 2] { + let (wx, wy, _) = crate::scene::transform::ocs_point_to_wcs( + (x, y, dxf.elevation), + normal, + ); + [(wx - ox) as f32, (wy - oy) as f32] + }; + if dxf.paths.is_empty() { + return None; + } let mut boundary: Vec<[f32; 2]> = Vec::new(); - for edge in &path.edges { - match edge { + for path in &dxf.paths { + let before_path = boundary.len(); + if !boundary.is_empty() { + boundary.push([f32::NAN, f32::NAN]); + } + let path_start = boundary.len(); + + for edge in &path.edges { + match edge { BoundaryEdge::Polyline(poly) => { let verts = &poly.vertices; let count = verts.len(); + if count == 0 { + continue; + } let seg_count = if poly.is_closed { count } else { @@ -1939,73 +1955,86 @@ impl Scene { let v1 = &verts[(i + 1) % count]; let bulge = v0.z; if bulge.abs() < 1e-9 { - boundary.push([(v0.x - ox) as f32, (v0.y - oy) as f32]); + boundary.push(to_xy(v0.x, v0.y)); } else { - let p0 = [(v0.x - ox) as f32, (v0.y - oy) as f32]; - let p1 = [(v1.x - ox) as f32, (v1.y - oy) as f32]; - let angle = 4.0 * (bulge as f32).atan(); + let p0 = [v0.x as f32, v0.y as f32]; + let p1 = [v1.x as f32, v1.y as f32]; + let theta = 4.0 * (bulge as f32).atan(); let dx = p1[0] - p0[0]; let dy = p1[1] - p0[1]; let d = (dx * dx + dy * dy).sqrt(); - let r = (d / 2.0) / (angle / 2.0).sin().abs(); + if d < 1e-9 { + boundary.push(to_xy(v0.x, v0.y)); + continue; + } + let r = (d * 0.5) / (theta * 0.5).sin().abs(); let mx = (p0[0] + p1[0]) * 0.5; let my = (p0[1] + p1[1]) * 0.5; - let len = d.max(1e-9); - let px = -dy / len; - let py = dx / len; + let px = -dy / d; + let py = dx / d; let sign = if bulge > 0.0 { 1.0_f32 } else { -1.0_f32 }; - let h = r - (r * r - d * d / 4.0).max(0.0).sqrt(); - let cx = mx - sign * px * (r - h); - let cy = my - sign * py * (r - h); + let center_offset = r * (theta * 0.5).cos(); + let cx = mx + sign * px * center_offset; + let cy = my + sign * py * center_offset; let a0 = (p0[1] - cy).atan2(p0[0] - cx); let a1 = (p1[1] - cy).atan2(p1[0] - cx); - let (sa, mut ea) = if bulge > 0.0 { (a0, a1) } else { (a1, a0) }; - if ea < sa { - ea += std::f32::consts::TAU; + let mut sweep = a1 - a0; + if bulge > 0.0 { + if sweep <= 0.0 { + sweep += std::f32::consts::TAU; + } + } else if sweep >= 0.0 { + sweep -= std::f32::consts::TAU; } - let span = ea - sa; - let segs = ((span.abs() / std::f32::consts::TAU) * 16.0) + if sweep.abs() < 1e-7 { + sweep = if bulge > 0.0 { + std::f32::consts::TAU + } else { + -std::f32::consts::TAU + }; + } + let segs = ((sweep.abs() / std::f32::consts::TAU) * 16.0) .ceil() .max(4.0) as u32; for j in 0..segs { - let t = sa + span * (j as f32 / segs as f32); - boundary.push([cx + r * t.cos(), cy + r * t.sin()]); + let a = a0 + sweep * (j as f32 / segs as f32); + boundary.push(to_xy( + (cx + r * a.cos()) as f64, + (cy + r * a.sin()) as f64, + )); } } } if poly.is_closed { - if let Some(&first) = boundary.first() { + if let Some(&first) = boundary.get(path_start) { boundary.push(first); } } } BoundaryEdge::Line(line) => { - boundary.push([(line.start.x - ox) as f32, (line.start.y - oy) as f32]); - boundary.push([(line.end.x - ox) as f32, (line.end.y - oy) as f32]); + boundary.push(to_xy(line.start.x, line.start.y)); + boundary.push(to_xy(line.end.x, line.end.y)); } BoundaryEdge::CircularArc(arc) => { - let cx = (arc.center.x - ox) as f32; - let cy = (arc.center.y - oy) as f32; - let r = arc.radius as f32; - let (sa, ea) = if arc.counter_clockwise { - (arc.start_angle as f32, arc.end_angle as f32) - } else { - (arc.end_angle as f32, arc.start_angle as f32) - }; - let mut end = ea; - if end < sa { - end += std::f32::consts::TAU; - } + let cx = arc.center.x; + let cy = arc.center.y; + let r = arc.radius; + let sa = arc.start_angle; + let ea = arc.end_angle; + let end = if ea >= sa { ea } else { ea + std::f64::consts::TAU }; let span = end - sa; - let segs = ((span / std::f32::consts::TAU) * 32.0).ceil().max(4.0) as u32; + let segs = ((span / std::f64::consts::TAU) * 32.0).ceil().max(4.0) as u32; for i in 0..=segs { - let t = sa + span * (i as f32 / segs as f32); - boundary.push([cx + r * t.cos(), cy + r * t.sin()]); + let t = sa + span * (i as f64 / segs as f64); + boundary.push(to_xy( + cx + r * t.cos(), + cy + r * t.sin(), + )); } } BoundaryEdge::EllipticArc(ell) => { - let cx = (ell.center.x - ox) as f32; - let cy = (ell.center.y - oy) as f32; + let cx = ell.center.x; + let cy = ell.center.y; let maj_x = ell.major_axis_endpoint.x as f32; let maj_y = ell.major_axis_endpoint.y as f32; let r_maj = (maj_x * maj_x + maj_y * maj_y).sqrt(); @@ -2026,10 +2055,10 @@ impl Scene { let t = sa + span * (i as f32 / segs as f32); let lx = r_maj * t.cos(); let ly = r_min * t.sin(); - boundary.push([ - cx + lx * rot.cos() - ly * rot.sin(), - cy + lx * rot.sin() + ly * rot.cos(), - ]); + boundary.push(to_xy( + cx + (lx * rot.cos() - ly * rot.sin()) as f64, + cy + (lx * rot.sin() + ly * rot.cos()) as f64, + )); } } BoundaryEdge::Spline(spline) => { @@ -2057,21 +2086,34 @@ impl Scene { for i in 0..=segs { let t = t0 + (t1 - t0) * (i as f64 / segs as f64); let p = bspl.subs(t); - boundary.push([(p.x - ox) as f32, (p.y - oy) as f32]); + boundary.push(to_xy(p.x, p.y)); } } else { for cp in &spline.control_points { - boundary.push([(cp.x - ox) as f32, (cp.y - oy) as f32]); + boundary.push(to_xy(cp.x, cp.y)); } } } } + } + + if boundary.len() == path_start { + boundary.truncate(before_path); + continue; + } + if boundary.len() >= path_start + 3 { + let first = boundary[path_start]; + let last = *boundary.last().unwrap(); + if (first[0] - last[0]).abs() > 1e-5 || (first[1] - last[1]).abs() > 1e-5 { + boundary.push(first); + } + } } if boundary.is_empty() { return None; } - boundary.truncate(64); + boundary.truncate(hatch_model::MAX_HATCH_BOUNDARY_VERTS); let pattern = if dxf.gradient_color.is_enabled() { let color2 = dxf @@ -2113,8 +2155,8 @@ impl Scene { pattern, name, color, - angle_offset: 0.0, - scale: 1.0, + angle_offset: dxf.pattern_angle as f32, + scale: dxf.pattern_scale as f32, }) } @@ -3683,4 +3725,3 @@ fn polyline_segment_fill( Some(boundary) } } - diff --git a/src/scene/pipeline/hatch_gpu.rs b/src/scene/pipeline/hatch_gpu.rs index a77dc55df..dc3c97617 100644 --- a/src/scene/pipeline/hatch_gpu.rs +++ b/src/scene/pipeline/hatch_gpu.rs @@ -2,10 +2,12 @@ // // Group 1 bindings per hatch: // binding 0 — HatchUniformData (64 bytes) mode, color, angle_offset, scale, gradient params -// binding 1 — BoundaryData (1024 bytes) up to 64 boundary polygon vertices +// binding 1 — BoundaryData (16384 bytes) boundary polygon vertices // binding 2 — FamilyBatchData (1296 bytes) up to 16 line families + 128 dash values -use crate::scene::hatch_model::{HatchModel, HatchPattern, PatFamily}; +use crate::scene::hatch_model::{ + HatchModel, HatchPattern, PatFamily, MAX_HATCH_BOUNDARY_VERTS, +}; use iced::wgpu; use iced::wgpu::util::DeviceExt; @@ -64,7 +66,7 @@ pub struct HatchUniformData { #[repr(C)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] pub struct BoundaryData { - pub verts: [[f32; 4]; 64], // world XY in .xy, .zw unused + pub verts: [[f32; 4]; MAX_HATCH_BOUNDARY_VERTS], // world XY in .xy, .zw unused } /// One line family packed for the shader (48 bytes). @@ -120,26 +122,21 @@ impl HatchGpu { }; // ── Bounding box ───────────────────────────────────────────────── - let min_x = model - .boundary - .iter() - .map(|v| v[0]) - .fold(f32::INFINITY, f32::min); - let max_x = model - .boundary - .iter() - .map(|v| v[0]) - .fold(f32::NEG_INFINITY, f32::max); - let min_y = model - .boundary - .iter() - .map(|v| v[1]) - .fold(f32::INFINITY, f32::min); - let max_y = model - .boundary - .iter() - .map(|v| v[1]) - .fold(f32::NEG_INFINITY, f32::max); + let (mut min_x, mut max_x, mut min_y, mut max_y) = ( + f32::INFINITY, + f32::NEG_INFINITY, + f32::INFINITY, + f32::NEG_INFINITY, + ); + for &[x, y] in model.boundary.iter() { + if !x.is_finite() || !y.is_finite() { + continue; + } + min_x = min_x.min(x); + max_x = max_x.max(x); + min_y = min_y.min(y); + max_y = max_y.max(y); + } let max_spacing = match &model.pattern { HatchPattern::Pattern(families) => families @@ -189,17 +186,22 @@ impl HatchGpu { let projs: Vec = model .boundary .iter() + .filter(|v| v[0].is_finite() && v[1].is_finite()) .map(|&[x, y]| x * grad_cos + y * grad_sin) .collect(); - let proj_min = projs.iter().cloned().fold(f32::INFINITY, f32::min); - let proj_max = projs.iter().cloned().fold(f32::NEG_INFINITY, f32::max); - (proj_min, (proj_max - proj_min).max(1e-4)) + if projs.is_empty() { + (0.0, 1.0) + } else { + let proj_min = projs.iter().cloned().fold(f32::INFINITY, f32::min); + let proj_max = projs.iter().cloned().fold(f32::NEG_INFINITY, f32::max); + (proj_min, (proj_max - proj_min).max(1e-4)) + } } else { (0.0, 1.0) }; // ── HatchUniformData ───────────────────────────────────────────── - let n = model.boundary.len().min(64); + let n = model.boundary.len().min(MAX_HATCH_BOUNDARY_VERTS); let uniform_data = HatchUniformData { color: model.color, color2, @@ -215,9 +217,14 @@ impl HatchGpu { // ── BoundaryData ───────────────────────────────────────────────── let mut boundary_data = BoundaryData { - verts: [[0.0; 4]; 64], + verts: [[0.0; 4]; MAX_HATCH_BOUNDARY_VERTS], }; - for (i, &[x, y]) in model.boundary.iter().take(64).enumerate() { + for (i, &[x, y]) in model + .boundary + .iter() + .take(MAX_HATCH_BOUNDARY_VERTS) + .enumerate() + { boundary_data.verts[i] = [x, y, 0.0, 0.0]; } diff --git a/src/shaders/hatch.wgsl b/src/shaders/hatch.wgsl index 4a809ac12..c6d21bf6e 100644 --- a/src/shaders/hatch.wgsl +++ b/src/shaders/hatch.wgsl @@ -42,7 +42,7 @@ struct HatchUniforms { @group(1) @binding(0) var h: HatchUniforms; struct Boundary { - verts: array, 64>, + verts: array, 1024>, } @group(1) @binding(1) var b: Boundary; @@ -87,18 +87,34 @@ struct VOut { // ── Point-in-polygon (ray casting) ──────────────────────────────────────── +fn valid_vertex(p: vec2) -> bool { + return p.x == p.x && p.y == p.y; +} + +fn edge_crosses(p: vec2, a: vec2, c: vec2) -> bool { + if (a.y > p.y) != (c.y > p.y) { + let x_int = (c.x - a.x) * (p.y - a.y) / (c.y - a.y) + a.x; + return p.x < x_int; + } + return false; +} + fn in_polygon(p: vec2) -> bool { var inside = false; let n = h.vcount; - var j = n - 1u; + var prev = vec2(0.0, 0.0); + var have_prev = false; for (var i = 0u; i < n; i++) { let vi = b.verts[i].xy; - let vj = b.verts[j].xy; - if (vi.y > p.y) != (vj.y > p.y) { - let x_int = (vj.x - vi.x) * (p.y - vi.y) / (vj.y - vi.y) + vi.x; - if p.x < x_int { inside = !inside; } + if !valid_vertex(vi) { + have_prev = false; + continue; } - j = i; + if have_prev && edge_crosses(p, prev, vi) { + inside = !inside; + } + prev = vi; + have_prev = true; } return inside; }