use vello_cpu::color::{AlphaColor, Srgb};
use vello_cpu::kurbo::{BezPath, Cap, Join, PathEl, Point, RoundedRect, Shape, Stroke};
use vello_cpu::{Pixmap, RenderContext};
use crate::model::{BOX_H, BOX_W, Camera, Color, Decorations, GraphEdge, GraphModel};
pub struct Rendered {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
fn to_alpha(c: Color) -> AlphaColor<Srgb> {
AlphaColor::from_rgba8(c.r, c.g, c.b, c.a)
}
pub fn render(
model: &GraphModel,
decorations: &Decorations,
camera: &Camera,
width: u32,
height: u32,
background: Color,
) -> Rendered {
let mut ctx = RenderContext::new(width as u16, height as u16);
ctx.set_paint(to_alpha(background));
let full = vello_cpu::kurbo::Rect::new(0.0, 0.0, width as f64, height as f64);
ctx.fill_rect(&full);
let idx: std::collections::HashMap<&str, (f64, f64)> = model
.nodes
.iter()
.map(|n| {
let (x, y) = camera.project(n.pos);
(n.id.as_str(), (x as f64, y as f64))
})
.collect();
for e in &model.edges {
draw_edge(&mut ctx, &idx, e, camera.zoom);
}
for e in &decorations.edges {
draw_edge_styled(&mut ctx, &idx, e, camera.zoom, 2.6);
}
let hw = (BOX_W * 0.5 * camera.zoom) as f64;
let hh = (BOX_H * 0.5 * camera.zoom) as f64;
let radius = (5.0 * camera.zoom) as f64;
for n in &model.nodes {
let (cx, cy) = camera.project(n.pos);
let (cx, cy) = (cx as f64, cy as f64);
let rect = RoundedRect::new(cx - hw, cy - hh, cx + hw, cy + hh, radius);
ctx.set_paint(to_alpha(n.fill));
ctx.fill_path(&rect.to_path(0.1));
let deco = decorations.nodes.get(&n.id);
let (ring_col, ring_w) = match deco.and_then(|d| d.ring) {
Some(rc) => (rc, 2.4_f32 * camera.zoom),
None => (n.stroke, 1.4_f32 * camera.zoom),
};
ctx.set_stroke(Stroke::new(ring_w as f64).with_join(Join::Round));
ctx.set_paint(to_alpha(ring_col));
ctx.stroke_path(&rect.to_path(0.1));
}
let mut pixmap = Pixmap::new(width as u16, height as u16);
ctx.flush();
ctx.render_to_pixmap(&mut pixmap);
let rgba: Vec<u8> = pixmap
.take_unpremultiplied()
.into_iter()
.flat_map(|p| [p.r, p.g, p.b, p.a])
.collect();
#[cfg(feature = "testmatrix")]
{
let total = (width as usize) * (height as usize);
let lit = rgba.chunks_exact(4).filter(|p| p[3] != 0).count();
let geom = model.nodes.len() + model.edges.len();
facett_core::testmatrix::emit(
"facett-graphview::cpu::render",
"cpu_render",
(geom == 0 || lit > 0) && rgba.len() == total * 4,
&format!("nodes={} edges={} lit_px={lit} total_px={total}", model.nodes.len(), model.edges.len()),
);
}
Rendered { width, height, rgba }
}
fn draw_edge(
ctx: &mut RenderContext,
idx: &std::collections::HashMap<&str, (f64, f64)>,
e: &GraphEdge,
zoom: f32,
) {
draw_edge_styled(ctx, idx, e, zoom, 1.4);
}
fn draw_edge_styled(
ctx: &mut RenderContext,
idx: &std::collections::HashMap<&str, (f64, f64)>,
e: &GraphEdge,
zoom: f32,
base_w: f32,
) {
let (Some(&(ax0, ay)), Some(&(bx0, by))) = (idx.get(e.from.as_str()), idx.get(e.to.as_str()))
else {
return;
};
let half = (BOX_W * 0.5 * zoom) as f64;
let a = Point::new(ax0 + half, ay);
let b = Point::new(bx0 - half, by);
let midx = (a.x + b.x) * 0.5;
let mut path = BezPath::new();
path.push(PathEl::MoveTo(a));
path.push(PathEl::CurveTo(Point::new(midx, a.y), Point::new(midx, b.y), b));
let w = (base_w * zoom.clamp(0.5, 2.0)) as f64;
ctx.set_stroke(Stroke::new(w).with_caps(Cap::Round).with_join(Join::Round));
ctx.set_paint(to_alpha(e.color));
if e.dashed {
let mut dash = BezPath::new();
let seg = 0..=20;
let mut on = true;
let mut last = a;
for i in seg {
let t = i as f64 / 20.0;
let p = cubic_point(a, Point::new(midx, a.y), Point::new(midx, b.y), b, t);
if on {
dash.push(PathEl::MoveTo(last));
dash.push(PathEl::LineTo(p));
}
last = p;
on = !on;
}
ctx.stroke_path(&dash);
} else {
ctx.stroke_path(&path);
}
let dir = {
let (dx, dy) = (b.x - midx, 0.0_f64);
let len = (dx * dx + dy * dy).sqrt().max(1e-3);
(dx / len, dy / len)
};
let perp = (-dir.1, dir.0);
let head = (6.0 * zoom.clamp(0.6, 1.6)) as f64;
let mut ah = BezPath::new();
ah.push(PathEl::MoveTo(b));
ah.push(PathEl::LineTo(Point::new(
b.x - dir.0 * head + perp.0 * head * 0.5,
b.y - dir.1 * head + perp.1 * head * 0.5,
)));
ah.push(PathEl::MoveTo(b));
ah.push(PathEl::LineTo(Point::new(
b.x - dir.0 * head - perp.0 * head * 0.5,
b.y - dir.1 * head - perp.1 * head * 0.5,
)));
ctx.stroke_path(&ah);
}
fn cubic_point(p0: Point, p1: Point, p2: Point, p3: Point, t: f64) -> Point {
let mt = 1.0 - t;
let a = mt * mt * mt;
let b = 3.0 * mt * mt * t;
let c = 3.0 * mt * t * t;
let d = t * t * t;
Point::new(
a * p0.x + b * p1.x + c * p2.x + d * p3.x,
a * p0.y + b * p1.y + c * p2.y + d * p3.y,
)
}