facett-core 0.1.7

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
//! **The CPU rect scissor** — geometry-level clipping of triangles to a widget
//! rect, the "ink never escapes the rect" guarantee.
//!
//! [`clip_poly_to_rect`] was **moved here verbatim** from `facett-map3d::lib`
//! (step 3 of the CONS-CORE migration); map3d re-exports it so its regression fence
//! (`clip_pipeline` / `frustum_scissor` / `no_spokes` / `near_plane_probe` /
//! `gpu_scissor`) stays green. The move is behavior-preserving — those tests are
//! the proof.
//!
//! [`ink_outside_rect`] is the reusable scissor **oracle** the spec calls for
//! (`scissor.ink_outside_rect == 0`): given triangles and a rect, it counts the
//! clipped-polygon vertices that still land outside the rect (must be 0 after a
//! correct scissor). The skins' tests keep their own local oracles unchanged; this
//! is the shared one for the L0 emit path.

use egui::Pos2;

/// **Sutherland–Hodgman** clip of a triangle (`tri`) to an axis-aligned `rect`,
/// returning the clipped convex polygon's vertices (3..=7 of them) or an empty Vec
/// when the triangle lies wholly outside the rect. This is the CPU painter's
/// **scissor**: it geometrically confines every face to the widget rect so nothing
/// a 3D/2D pass paints can land outside the component's own rectangle — independent
/// of whether the host renderer applies the egui clip_rect as a GPU scissor.
///
/// Moved verbatim from `facett-map3d` (CONS-CORE Phase A, step 3); re-exported back
/// there so behavior is identical.
pub fn clip_poly_to_rect(tri: &[Pos2; 3], rect: egui::Rect) -> Vec<Pos2> {
    // Clip against each of the four rect edges in turn. `inside` is the half-plane
    // test for that edge; `intersect` finds where segment a→b crosses it.
    let mut poly: Vec<Pos2> = tri.to_vec();
    let edges: [(f32, u8); 4] = [
        (rect.left(), 0),   // x >= left
        (rect.right(), 1),  // x <= right
        (rect.top(), 2),    // y >= top
        (rect.bottom(), 3), // y <= bottom
    ];
    for (bound, which) in edges {
        if poly.is_empty() {
            break;
        }
        let inside = |p: &Pos2| match which {
            0 => p.x >= bound,
            1 => p.x <= bound,
            2 => p.y >= bound,
            _ => p.y <= bound,
        };
        let intersect = |a: &Pos2, b: &Pos2| -> Pos2 {
            // Parametric crossing of segment a→b with the axis-aligned edge.
            let t = match which {
                0 | 1 => (bound - a.x) / (b.x - a.x),
                _ => (bound - a.y) / (b.y - a.y),
            };
            let t = t.clamp(0.0, 1.0);
            Pos2::new(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t)
        };
        let mut out: Vec<Pos2> = Vec::with_capacity(poly.len() + 1);
        for i in 0..poly.len() {
            let cur = poly[i];
            let prev = poly[(i + poly.len() - 1) % poly.len()];
            let (cin, pin) = (inside(&cur), inside(&prev));
            if cin {
                if !pin {
                    out.push(intersect(&prev, &cur));
                }
                out.push(cur);
            } else if pin {
                out.push(intersect(&prev, &cur));
            }
        }
        poly = out;
    }
    poly
}

/// The scissor **oracle** (`scissor.ink_outside_rect`): count the vertices of the
/// *scissored* polygons that still land outside `rect` (beyond `eps`). A correct
/// scissor leaves **0** — this is the bug oracle the spec's clip stage emits
/// (`ink_outside_rect == 0`). Pure, so both the emit path and tests call it.
pub fn ink_outside_rect(tris: &[[Pos2; 3]], rect: egui::Rect) -> usize {
    let eps = 1e-2;
    let mut escapes = 0usize;
    for tri in tris {
        for p in &clip_poly_to_rect(tri, rect) {
            let over = (rect.left() - p.x).max(p.x - rect.right()).max(rect.top() - p.y).max(p.y - rect.bottom());
            if over > eps {
                escapes += 1;
            }
        }
    }
    escapes
}

#[cfg(test)]
mod tests {
    use super::*;
    use egui::pos2;

    /// INJECT-ASSERT (the moved scissor, behavior-preserving): a triangle sprawling
    /// far past the rect is confined — every clipped vertex sits inside, a
    /// fully-outside face is dropped, an inside face is preserved unchanged. Mirrors
    /// map3d's `clip_poly_to_rect_confines_every_vertex_to_the_rect`.
    #[test]
    fn clip_confines_every_vertex_to_the_rect() {
        let rect = egui::Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0));
        let sprawl = [pos2(-500.0, 50.0), pos2(50.0, -500.0), pos2(600.0, 600.0)];
        let c = clip_poly_to_rect(&sprawl, rect);
        assert!(c.len() >= 3, "a crossing face still produces a polygon");
        for p in &c {
            assert!(p.x >= rect.left() - 1e-3 && p.x <= rect.right() + 1e-3, "x inside");
            assert!(p.y >= rect.top() - 1e-3 && p.y <= rect.bottom() + 1e-3, "y inside");
        }
        let outside = [pos2(200.0, 200.0), pos2(300.0, 200.0), pos2(250.0, 300.0)];
        assert!(clip_poly_to_rect(&outside, rect).len() < 3, "fully-outside face dropped");
        let within = [pos2(10.0, 10.0), pos2(90.0, 10.0), pos2(50.0, 90.0)];
        let cw = clip_poly_to_rect(&within, rect);
        assert_eq!(cw.len(), 3, "an inside face is preserved as-is");
    }

    /// INJECT-ASSERT (oracle): after scissoring, no ink escapes the rect — even for
    /// a wildly sprawling triangle. Raw (un-clipped) vertices DO escape; the oracle
    /// over the clipped polygons reads 0.
    #[test]
    fn ink_outside_rect_is_zero_after_scissor() {
        let rect = egui::Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0));
        let sprawl = [pos2(-9000.0, 40.0), pos2(40.0, -9000.0), pos2(9000.0, 9000.0)];
        assert_eq!(ink_outside_rect(&[sprawl], rect), 0, "scissor leaves no ink outside");
        let inside = [pos2(10.0, 10.0), pos2(90.0, 10.0), pos2(50.0, 90.0)];
        assert_eq!(ink_outside_rect(&[inside], rect), 0, "an inside face never escapes");
    }
}