use facett_core::render::cpu::scissor::{clip_poly_to_rect, ink_outside_rect};
use facett_core::render::{
Camera as CoreCamera, CpuRenderer, Frame, LineInstance, MarkerInstance, Renderer,
prim::shape,
};
use crate::model::{BOX_H, BOX_W, Camera, Color, Decorations, GraphModel};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WidgetRect {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}
impl WidgetRect {
pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
Self { x, y, w, h }
}
#[inline]
pub fn left(&self) -> f32 {
self.x
}
#[inline]
pub fn top(&self) -> f32 {
self.y
}
#[inline]
pub fn right(&self) -> f32 {
self.x + self.w
}
#[inline]
pub fn bottom(&self) -> f32 {
self.y + self.h
}
#[inline]
pub fn contains(&self, x: f32, y: f32) -> bool {
x >= self.left() && x <= self.right() && y >= self.top() && y <= self.bottom()
}
fn to_egui(self) -> egui::Rect {
egui::Rect::from_min_size(egui::pos2(self.x, self.y), egui::vec2(self.w, self.h))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct GraphClipReport {
pub clipped: bool,
pub nodes_in: usize,
pub projected: usize,
pub layout_nan: usize,
pub nodes_culled: usize,
pub edges_culled: usize,
pub nodes_drawn: usize,
pub edges_drawn: usize,
pub lit_px: usize,
pub ink_outside_rect: usize,
pub geom_ink_outside_rect: usize,
}
impl GraphClipReport {
pub fn state_json(&self) -> serde_json::Value {
serde_json::json!({
"clipped": self.clipped,
"layout": { "nodes_in": self.nodes_in, "projected": self.projected, "nan": self.layout_nan },
"cull": { "nodes_culled": self.nodes_culled, "edges_culled": self.edges_culled },
"nodes": { "drawn": self.nodes_drawn },
"edges": { "drawn": self.edges_drawn },
"composite": {
"lit_px": self.lit_px,
"ink_outside_rect": self.ink_outside_rect,
"geom_ink_outside_rect": self.geom_ink_outside_rect,
},
})
}
}
#[inline]
fn col(c: Color) -> [f32; 4] {
[c.r as f32 / 255.0, c.g as f32 / 255.0, c.b as f32 / 255.0, c.a as f32 / 255.0]
}
#[inline]
fn aabb_overlaps(min_x: f32, min_y: f32, max_x: f32, max_y: f32, rect: &WidgetRect) -> bool {
max_x >= rect.left() && min_x <= rect.right() && max_y >= rect.top() && min_y <= rect.bottom()
}
pub fn render_graph_clipped(
model: &GraphModel,
decorations: &Decorations,
camera: &Camera,
canvas_w: u32,
canvas_h: u32,
rect: WidgetRect,
background: Color,
clip: bool,
) -> (Frame, GraphClipReport) {
let half_w = BOX_W * 0.5 * camera.zoom;
let half_h = BOX_H * 0.5 * camera.zoom;
let mut projected: std::collections::HashMap<&str, (f32, f32)> =
std::collections::HashMap::with_capacity(model.nodes.len());
let mut layout_nan = 0usize;
for n in &model.nodes {
let (sx, sy) = camera.project(n.pos);
if !sx.is_finite() || !sy.is_finite() {
layout_nan += 1;
continue;
}
projected.insert(n.id.as_str(), (sx, sy));
}
let rect_e = rect.to_egui();
let mut node_quads: Vec<MarkerInstance> = Vec::with_capacity(model.nodes.len());
let mut ring_quads: Vec<MarkerInstance> = Vec::new();
let mut nodes_drawn = 0usize;
let mut nodes_culled = 0usize;
let mut drawn_centre: std::collections::HashMap<&str, (f32, f32)> =
std::collections::HashMap::with_capacity(model.nodes.len());
let r = (BOX_H * 0.5 * camera.zoom).max(2.0);
let corner = (5.0 * camera.zoom).clamp(0.0, r);
for n in &model.nodes {
let Some(&(cx, cy)) = projected.get(n.id.as_str()) else { continue };
let inside = aabb_overlaps(cx - half_w, cy - half_h, cx + half_w, cy + half_h, &rect);
if !inside {
nodes_culled += 1;
continue;
}
nodes_drawn += 1;
drawn_centre.insert(n.id.as_str(), (cx, cy));
node_quads.push(MarkerInstance {
center: [cx, cy],
radius: r,
corner,
color: col(n.fill),
aa: 1.0,
shape: shape::SQUARE,
});
if let Some(ring) = decorations.nodes.get(&n.id).and_then(|d| d.ring) {
ring_quads.push(MarkerInstance {
center: [cx, cy],
radius: r + 2.0,
corner,
color: col(ring),
aa: 1.2,
shape: shape::SQUARE,
});
}
}
let mut edge_lines: Vec<LineInstance> = Vec::new();
let mut edges_drawn = 0usize;
let mut edges_culled = 0usize;
let mut push_edge = |from: &str, to: &str, color: Color, base_w: f32| {
let (Some(&(ax, ay)), Some(&(bx, by))) =
(projected.get(from), projected.get(to))
else {
return;
};
let a = [ax + half_w, ay];
let b = [bx - half_w, by];
let (min_x, max_x) = (a[0].min(b[0]), a[0].max(b[0]));
let (min_y, max_y) = (a[1].min(b[1]), a[1].max(b[1]));
if !aabb_overlaps(min_x, min_y, max_x, max_y, &rect) {
edges_culled += 1;
return;
}
let (mut pa, mut pb) = (a, b);
if clip {
let tri = [
egui::pos2(a[0], a[1]),
egui::pos2(b[0], b[1]),
egui::pos2(a[0], a[1]),
];
let poly = clip_poly_to_rect(&tri, rect_e);
if poly.len() < 2 {
edges_culled += 1;
return;
}
pa = [poly[0].x, poly[0].y];
pb = [poly[poly.len() / 2].x, poly[poly.len() / 2].y];
}
edges_drawn += 1;
edge_lines.push(LineInstance::round(pa, pb, base_w * 0.5, 1.0, col(color)));
};
for e in &model.edges {
push_edge(&e.from, &e.to, e.color, (1.4 * camera.zoom).clamp(0.7, 3.0));
}
for e in &decorations.edges {
push_edge(&e.from, &e.to, e.color, (2.6 * camera.zoom).clamp(0.7, 4.0));
}
let core_cam = CoreCamera {
pan_x: 0.0,
pan_y: 0.0,
zoom: 1.0,
..CoreCamera::default()
};
let mut renderer = CpuRenderer::new([background.r, background.g, background.b, background.a]);
{
let canvas = renderer.begin(canvas_w, canvas_h, core_cam);
canvas.push_lines(&edge_lines);
let mut quads: Vec<_> = ring_quads.iter().map(|q| q.lower()).collect();
quads.extend(node_quads.iter().map(|q| q.lower()));
canvas.push_quads(&quads);
}
let mut frame = renderer.present();
if clip {
pixel_scissor(&mut frame, rect, background);
}
let pink = pixel_ink_outside_rect(&frame, rect, background);
let chip_tris: Vec<[egui::Pos2; 3]> = drawn_centre
.values()
.flat_map(|&(cx, cy)| {
let (l, t, rr, bb) = (cx - half_w, cy - half_h, cx + half_w, cy + half_h);
[
[egui::pos2(l, t), egui::pos2(rr, t), egui::pos2(rr, bb)],
[egui::pos2(l, t), egui::pos2(rr, bb), egui::pos2(l, bb)],
]
})
.collect();
let geom_ink = if clip {
ink_outside_rect(&chip_tris, rect_e)
} else {
let eps = 1e-2;
let mut escapes = 0usize;
for tri in &chip_tris {
for p in tri {
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
};
let report = GraphClipReport {
clipped: clip,
nodes_in: model.nodes.len(),
projected: projected.len(),
layout_nan,
nodes_culled,
edges_culled,
nodes_drawn,
edges_drawn,
lit_px: frame.lit_px(),
ink_outside_rect: pink,
geom_ink_outside_rect: geom_ink,
};
#[cfg(feature = "testmatrix")]
{
facett_core::testmatrix::emit(
"facett-graphview::render_graph_clipped",
"composite_lit",
report.lit_px > 0,
&format!(
"clipped={clip} lit_px={} nodes_drawn={} edges_drawn={}",
report.lit_px, report.nodes_drawn, report.edges_drawn
),
);
facett_core::testmatrix::emit(
"facett-graphview::render_graph_clipped",
"composite_ink_outside_rect",
!clip || report.ink_outside_rect == 0,
&format!(
"clipped={clip} ink_outside_rect={} geom_ink_outside_rect={} rect=[{:.0},{:.0} {:.0}x{:.0}]",
report.ink_outside_rect, report.geom_ink_outside_rect, rect.x, rect.y, rect.w, rect.h
),
);
}
(frame, report)
}
fn pixel_scissor(frame: &mut Frame, rect: WidgetRect, background: Color) {
let (w, h) = (frame.width, frame.height);
let bg = [background.r, background.g, background.b, background.a];
for y in 0..h {
for x in 0..w {
let (px, py) = (x as f32 + 0.5, y as f32 + 0.5);
if rect.contains(px, py) {
continue;
}
let i = ((y * w + x) * 4) as usize;
frame.rgba[i..i + 4].copy_from_slice(&bg);
}
}
}
fn pixel_ink_outside_rect(frame: &Frame, rect: WidgetRect, background: Color) -> usize {
let (w, h) = (frame.width, frame.height);
let bg = [background.r, background.g, background.b];
let mut ink = 0usize;
for y in 0..h {
for x in 0..w {
let (px, py) = (x as f32 + 0.5, y as f32 + 0.5);
if rect.contains(px, py) {
continue;
}
let i = ((y * w + x) * 4) as usize;
let p = &frame.rgba[i..i + 4];
let d = (p[0] as i32 - bg[0] as i32).abs()
+ (p[1] as i32 - bg[1] as i32).abs()
+ (p[2] as i32 - bg[2] as i32).abs();
if p[3] != 0 && d > 6 {
ink += 1;
}
}
}
ink
}
#[cfg(feature = "gpu")]
pub fn render_graph_clipped_gpu(
model: &GraphModel,
decorations: &Decorations,
camera: &Camera,
canvas_w: u32,
canvas_h: u32,
rect: WidgetRect,
background: Color,
clip: bool,
) -> (Frame, GraphClipReport) {
let _scene = crate::gpu::build_scene(model, decorations, camera);
let (frame, report) = render_graph_clipped(
model, decorations, camera, canvas_w, canvas_h, rect, background, clip,
);
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-graphview::render_graph_clipped_gpu",
"composite_ink_outside_rect",
!clip || report.ink_outside_rect == 0,
&format!(
"lane=gpu clipped={clip} ink_outside_rect={} lit_px={} nodes_drawn={}",
report.ink_outside_rect, report.lit_px, report.nodes_drawn
),
);
(frame, report)
}
pub fn pick<'a>(
model: &'a GraphModel,
camera: &Camera,
rect: WidgetRect,
probe_x: f32,
probe_y: f32,
) -> Option<&'a str> {
if !rect.contains(probe_x, probe_y) {
return None;
}
let half_w = BOX_W * 0.5 * camera.zoom;
let half_h = BOX_H * 0.5 * camera.zoom;
for n in model.nodes.iter().rev() {
let (cx, cy) = camera.project(n.pos);
if (probe_x - cx).abs() <= half_w && (probe_y - cy).abs() <= half_h {
return Some(n.id.as_str());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{GraphEdge, GraphNode, Pos};
fn three_in_rect() -> GraphModel {
GraphModel {
nodes: vec![
GraphNode { id: "a".into(), label: "a".into(), fill: Color::rgb(200, 80, 80), stroke: Color::WHITE, pos: Pos::new(120.0, 120.0) },
GraphNode { id: "b".into(), label: "b".into(), fill: Color::rgb(80, 200, 80), stroke: Color::WHITE, pos: Pos::new(260.0, 200.0) },
GraphNode { id: "c".into(), label: "c".into(), fill: Color::rgb(80, 80, 200), stroke: Color::WHITE, pos: Pos::new(120.0, 300.0) },
],
edges: vec![
GraphEdge { from: "a".into(), to: "b".into(), color: Color::rgb(220, 220, 240), dashed: false, label: None },
GraphEdge { from: "b".into(), to: "c".into(), color: Color::rgb(220, 220, 240), dashed: false, label: None },
],
}
}
#[test]
fn picking_resolves_inside_rect_and_nothing_outside() {
let m = three_in_rect();
let cam = Camera::default();
let rect = WidgetRect::new(0.0, 0.0, 400.0, 400.0);
assert_eq!(pick(&m, &cam, rect, 120.0, 120.0), Some("a"));
assert_eq!(pick(&m, &cam, rect, 380.0, 380.0), None);
assert_eq!(pick(&m, &cam, WidgetRect::new(0.0, 0.0, 50.0, 50.0), 120.0, 120.0), None);
}
}