1use vello_cpu::color::{AlphaColor, Srgb};
12use vello_cpu::kurbo::{BezPath, Cap, Join, PathEl, Point, RoundedRect, Shape, Stroke};
13use vello_cpu::{Pixmap, RenderContext};
14
15use crate::model::{BOX_H, BOX_W, Camera, Color, Decorations, GraphEdge, GraphModel};
16
17pub struct Rendered {
19 pub width: u32,
20 pub height: u32,
21 pub rgba: Vec<u8>,
23}
24
25fn to_alpha(c: Color) -> AlphaColor<Srgb> {
26 AlphaColor::from_rgba8(c.r, c.g, c.b, c.a)
27}
28
29pub fn render(
32 model: &GraphModel,
33 decorations: &Decorations,
34 camera: &Camera,
35 width: u32,
36 height: u32,
37 background: Color,
38) -> Rendered {
39 let mut ctx = RenderContext::new(width as u16, height as u16);
40
41 ctx.set_paint(to_alpha(background));
43 let full = vello_cpu::kurbo::Rect::new(0.0, 0.0, width as f64, height as f64);
44 ctx.fill_rect(&full);
45
46 let idx: std::collections::HashMap<&str, (f64, f64)> = model
48 .nodes
49 .iter()
50 .map(|n| {
51 let (x, y) = camera.project(n.pos);
52 (n.id.as_str(), (x as f64, y as f64))
53 })
54 .collect();
55
56 for e in &model.edges {
58 draw_edge(&mut ctx, &idx, e, camera.zoom);
59 }
60 for e in &decorations.edges {
61 draw_edge_styled(&mut ctx, &idx, e, camera.zoom, 2.6);
63 }
64
65 let hw = (BOX_W * 0.5 * camera.zoom) as f64;
67 let hh = (BOX_H * 0.5 * camera.zoom) as f64;
68 let radius = (5.0 * camera.zoom) as f64;
69 for n in &model.nodes {
70 let (cx, cy) = camera.project(n.pos);
71 let (cx, cy) = (cx as f64, cy as f64);
72 let rect = RoundedRect::new(cx - hw, cy - hh, cx + hw, cy + hh, radius);
73
74 ctx.set_paint(to_alpha(n.fill));
76 ctx.fill_path(&rect.to_path(0.1));
77
78 let deco = decorations.nodes.get(&n.id);
80 let (ring_col, ring_w) = match deco.and_then(|d| d.ring) {
81 Some(rc) => (rc, 2.4_f32 * camera.zoom),
82 None => (n.stroke, 1.4_f32 * camera.zoom),
83 };
84 ctx.set_stroke(Stroke::new(ring_w as f64).with_join(Join::Round));
85 ctx.set_paint(to_alpha(ring_col));
86 ctx.stroke_path(&rect.to_path(0.1));
87 }
88
89 let mut pixmap = Pixmap::new(width as u16, height as u16);
90 ctx.flush();
91 ctx.render_to_pixmap(&mut pixmap);
92
93 let rgba: Vec<u8> = pixmap
94 .take_unpremultiplied()
95 .into_iter()
96 .flat_map(|p| [p.r, p.g, p.b, p.a])
97 .collect();
98
99 #[cfg(feature = "testmatrix")]
104 {
105 let total = (width as usize) * (height as usize);
106 let lit = rgba.chunks_exact(4).filter(|p| p[3] != 0).count();
107 let geom = model.nodes.len() + model.edges.len();
108 facett_core::testmatrix::emit(
109 "facett-graphview::cpu::render",
110 "cpu_render",
111 (geom == 0 || lit > 0) && rgba.len() == total * 4,
112 &format!("nodes={} edges={} lit_px={lit} total_px={total}", model.nodes.len(), model.edges.len()),
113 );
114 }
115
116 Rendered { width, height, rgba }
117}
118
119fn draw_edge(
120 ctx: &mut RenderContext,
121 idx: &std::collections::HashMap<&str, (f64, f64)>,
122 e: &GraphEdge,
123 zoom: f32,
124) {
125 draw_edge_styled(ctx, idx, e, zoom, 1.4);
126}
127
128fn draw_edge_styled(
133 ctx: &mut RenderContext,
134 idx: &std::collections::HashMap<&str, (f64, f64)>,
135 e: &GraphEdge,
136 zoom: f32,
137 base_w: f32,
138) {
139 let (Some(&(ax0, ay)), Some(&(bx0, by))) = (idx.get(e.from.as_str()), idx.get(e.to.as_str()))
140 else {
141 return;
142 };
143 let half = (BOX_W * 0.5 * zoom) as f64;
144 let a = Point::new(ax0 + half, ay);
145 let b = Point::new(bx0 - half, by);
146 let midx = (a.x + b.x) * 0.5;
147
148 let mut path = BezPath::new();
149 path.push(PathEl::MoveTo(a));
150 path.push(PathEl::CurveTo(Point::new(midx, a.y), Point::new(midx, b.y), b));
151
152 let w = (base_w * zoom.clamp(0.5, 2.0)) as f64;
153 ctx.set_stroke(Stroke::new(w).with_caps(Cap::Round).with_join(Join::Round));
154 ctx.set_paint(to_alpha(e.color));
155 if e.dashed {
156 let mut dash = BezPath::new();
159 let seg = 0..=20;
160 let mut on = true;
161 let mut last = a;
162 for i in seg {
163 let t = i as f64 / 20.0;
164 let p = cubic_point(a, Point::new(midx, a.y), Point::new(midx, b.y), b, t);
165 if on {
166 dash.push(PathEl::MoveTo(last));
167 dash.push(PathEl::LineTo(p));
168 }
169 last = p;
170 on = !on;
171 }
172 ctx.stroke_path(&dash);
173 } else {
174 ctx.stroke_path(&path);
175 }
176
177 let dir = {
181 let (dx, dy) = (b.x - midx, 0.0_f64);
182 let len = (dx * dx + dy * dy).sqrt().max(1e-3);
183 (dx / len, dy / len)
184 };
185 let perp = (-dir.1, dir.0);
186 let head = (6.0 * zoom.clamp(0.6, 1.6)) as f64;
187 let mut ah = BezPath::new();
188 ah.push(PathEl::MoveTo(b));
189 ah.push(PathEl::LineTo(Point::new(
190 b.x - dir.0 * head + perp.0 * head * 0.5,
191 b.y - dir.1 * head + perp.1 * head * 0.5,
192 )));
193 ah.push(PathEl::MoveTo(b));
194 ah.push(PathEl::LineTo(Point::new(
195 b.x - dir.0 * head - perp.0 * head * 0.5,
196 b.y - dir.1 * head - perp.1 * head * 0.5,
197 )));
198 ctx.stroke_path(&ah);
199}
200
201fn cubic_point(p0: Point, p1: Point, p2: Point, p3: Point, t: f64) -> Point {
203 let mt = 1.0 - t;
204 let a = mt * mt * mt;
205 let b = 3.0 * mt * mt * t;
206 let c = 3.0 * mt * t * t;
207 let d = t * t * t;
208 Point::new(
209 a * p0.x + b * p1.x + c * p2.x + d * p3.x,
210 a * p0.y + b * p1.y + c * p2.y + d * p3.y,
211 )
212}