Skip to main content

ogeom_mesh/
hatch.rs

1//! Hatching in parametric space: parallel lines clipped to a face's trim.
2//!
3//! The rings are the same chart boundary every other consumer walks
4//! ([`face_boundary`]), so the hatch and
5//! the triangulation cannot disagree about where the face is. The lines run
6//! at an angle in the chart, spaced evenly, and each is cut to the inside
7//! intervals by even-odd crossing counting; holes split segments the same
8//! way they split everything else.
9//!
10//! Scanlines sit at half-spacing offsets, so a boundary lying exactly on a
11//! round coordinate (every axis-aligned face) is not grazed.
12
13use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
14use ogeom_math::Point2;
15use ogeom_topo::{Model, Shape};
16
17use crate::discretize::Deflection;
18use crate::triangulate::face_boundary;
19
20/// Hatch a face's chart with parallel lines.
21///
22/// `angle` is the line direction in the chart, radians from the `u` axis;
23/// `spacing` the perpendicular distance between lines. Returns the clipped
24/// segments in chart coordinates, each as its two endpoints; lifting them
25/// to space is the surface's own `point_at`, and which chart step is fine
26/// enough for that lift is the caller's deflection question, not this one.
27///
28/// # Errors
29///
30/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
31/// spacing is not finite and positive, plus whatever the boundary walk
32/// refuses.
33pub fn hatch_face(
34    model: &Model,
35    face: &Shape,
36    angle: f64,
37    spacing: f64,
38    deflection: Deflection,
39    tol: Tolerances,
40) -> OgeomResult<Vec<[Point2; 2]>> {
41    if !spacing.is_finite() || spacing <= tol.confusion() {
42        ogeom_bail!(Construction, "a hatch spacing of {spacing} draws nothing");
43    }
44    if !angle.is_finite() {
45        ogeom_bail!(Construction, "a hatch angle of {angle} is not a direction");
46    }
47    let rings = face_boundary(model, face, deflection, tol)?;
48
49    // Rotate the rings so the hatch runs horizontal, scan, rotate back.
50    let (sin, cos) = angle.sin_cos();
51    let turn = |p: Point2| Point2::new(p.x.mul_add(cos, p.y * sin), p.y.mul_add(cos, -(p.x * sin)));
52    let back = |p: Point2| Point2::new(p.x.mul_add(cos, -(p.y * sin)), p.y.mul_add(cos, p.x * sin));
53    let turned: Vec<Vec<Point2>> = rings
54        .iter()
55        .map(|ring| ring.iter().map(|p| turn(*p)).collect())
56        .collect();
57
58    let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
59    for ring in &turned {
60        for p in ring {
61            lo = lo.min(p.y);
62            hi = hi.max(p.y);
63        }
64    }
65    if !lo.is_finite() || hi <= lo {
66        return Ok(Vec::new());
67    }
68
69    let mut out = Vec::new();
70    let first = (lo / spacing).floor();
71    let mut k = first;
72    loop {
73        let level = (k + 0.5) * spacing;
74        k += 1.0;
75        if level >= hi {
76            break;
77        }
78        if level <= lo {
79            continue;
80        }
81        // Every crossing of every ring, in order: even-odd pairs are the
82        // inside intervals, holes included by the same counting.
83        let mut crossings: Vec<f64> = Vec::new();
84        for ring in &turned {
85            for i in 0..ring.len() {
86                let (a, b) = (ring[i], ring[(i + 1) % ring.len()]);
87                if (a.y > level) != (b.y > level) {
88                    crossings.push((b.x - a.x).mul_add((level - a.y) / (b.y - a.y), a.x));
89                }
90            }
91        }
92        crossings.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
93        for pair in crossings.as_chunks::<2>().0 {
94            if pair[1] - pair[0] > tol.parametric() {
95                out.push([
96                    back(Point2::new(pair[0], level)),
97                    back(Point2::new(pair[1], level)),
98                ]);
99            }
100        }
101    }
102    Ok(out)
103}