From ca85856f52105dc7cece4512821a77ae1b8063fc Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 9 Aug 2026 09:59:19 +0300 Subject: [PATCH] refactor(hatch): clip the pattern against the boundary in the kernel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pattern's lines were clipped by sorting their crossings with the boundary and taking every other span. That is right where a line cuts a boundary cleanly and wrong wherever it grazes one: a line through a corner crosses twice in effectively one place, and from there every span is inverted — the fill lands in the holes and the solid comes out empty. A 1e-5 dedup on the crossings hid some of it and could not fix the rest, because which crossings to merge is not the question. The kernel divides the line by its crossings and then asks each span whether its middle is inside, which costs a containment test apiece and cannot get out of step. It also accepts a curved boundary rather than only a polygon, which this call site does not need yet and a section fill will. What stays is the PAT frame — the family angle, spacing, origin offset, the k range and the dash walk — which is a format convention rather than geometry. Co-Authored-By: Claude Opus 5 --- Cargo.lock | 2 +- src/scene/model/hatch_model.rs | 74 +++++++++++++++++++--------------- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bdc515374..5b4756a69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -935,7 +935,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git#5499242f1160018e066f78d4e462c9532e665a96" +source = "git+https://github.com/HakanSeven12/cadkernel.git#47655b21de5e2678fb12ec5f093d7e338d68409d" dependencies = [ "cavalier_contours", ] diff --git a/src/scene/model/hatch_model.rs b/src/scene/model/hatch_model.rs index f2d459605..cc0e4f296 100644 --- a/src/scene/model/hatch_model.rs +++ b/src/scene/model/hatch_model.rs @@ -1,12 +1,24 @@ // 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. // The GPU fragment shader performs point-in-polygon and hatch-line tests so // no line geometry is ever tessellated on the CPU. +use std::sync::Arc; + +use acadrust::kernel::geom2d::{ + inside_spans, Curve as KernelCurve, Line as KernelLine, Tolerance, XLine as KernelXLine, +}; + pub const MAX_HATCH_BOUNDARY_VERTS: usize = 1024; +/// How near two crossings along a pattern line must be to count as one. +/// +/// The boundary reaching here is already a chord approximation of the drawn +/// curves, so this only has to be finer than that sampling and coarser than +/// the rounding in it. +const CLIP_TOLERANCE_LINEAR: f64 = 1.0e-6; + /// One line family from a PAT-format hatch pattern. /// /// Format mirrors the standard PAT line format: @@ -269,6 +281,13 @@ impl HatchModel { if edges.is_empty() { return Vec::new(); } + // The boundary as kernel curves, built once for every pattern line to + // be clipped against. + let clip_tolerance = Tolerance::new(CLIP_TOLERANCE_LINEAR); + let boundary: Vec = edges + .iter() + .map(|(a, b)| KernelCurve::Line(KernelLine { start: *a, end: *b })) + .collect(); // ── AABB of the boundary in world coords. let mut min_x = f64::INFINITY; @@ -363,39 +382,28 @@ impl HatchModel { let lx = origin[0] + kf * step_x; let ly = origin[1] + kf * step_y; - // Intersect line P(t) = L + t·(cos_a, sin_a) against each - // boundary edge; collect t-values where the edge actually - // crosses (s ∈ [0,1]). - let mut ts: Vec = Vec::with_capacity(8); - for &(a, b) in &edges { - let ex = b[0] - a[0]; - let ey = b[1] - a[1]; - let det = ex * sin_a - ey * cos_a; // = sin_a·ex − cos_a·ey - if det.abs() < 1e-9 { - continue; - } - let rx = a[0] - lx; - let ry = a[1] - ly; - let t = (ex * ry - ey * rx) / det; - let s = (cos_a * ry - sin_a * rx) / det; - if s >= 0.0 && s <= 1.0 { - ts.push(t); - } - } - if ts.len() < 2 { - continue; - } - ts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); - // De-duplicate near-coincident hits (line clipping a vertex). - ts.dedup_by(|a, b| (*a - *b).abs() < 1e-5); - if ts.len() < 2 { - continue; - } + // The stretches of this pattern line that fall inside the + // boundary. The direction is a unit vector, so the kernel's + // parameter is distance along the line — the same `t` the + // dash walk below steps in. + // + // Each span is tested for containment rather than taken from + // alternating crossings. A line that grazes a corner crosses + // twice in one place, and an alternating walk inverts from + // there on: the fill lands in the holes and the solid comes + // out empty. + let spans = inside_spans( + &boundary, + &KernelCurve::XLine(KernelXLine { + base: [lx, ly], + direction: [cos_a, sin_a], + }), + clip_tolerance, + ); - // Even-odd: emit segments between consecutive pairs. - for pair in ts.chunks_exact(2) { - let t0 = pair[0]; - let t1 = pair[1]; + for span in spans { + let t0 = span[0]; + let t1 = span[1]; if t1 - t0 < 1e-6 { continue; }