1use facett_core::render::{
24 Camera as CoreCamera, CircleInstance, CpuRenderer, Frame, LineInstance, MarkerInstance,
25 QuadInstance, Renderer, prim::shape,
26};
27
28use crate::backend::Backend;
29use crate::model::{BOX_H, BOX_W, Camera, Color, Decorations, GraphEdge, GraphModel};
30
31#[inline]
34fn col(c: Color) -> [f32; 4] {
35 [c.r as f32 / 255.0, c.g as f32 / 255.0, c.b as f32 / 255.0, c.a as f32 / 255.0]
36}
37
38pub fn lower(
47 model: &GraphModel,
48 decorations: &Decorations,
49 camera: &Camera,
50) -> (Vec<QuadInstance>, Vec<LineInstance>) {
51 let mut quads: Vec<QuadInstance> = Vec::with_capacity(model.nodes.len() * 2);
52 let mut lines: Vec<LineInstance> = Vec::with_capacity(model.edges.len() + decorations.edges.len());
53
54 let idx: std::collections::HashMap<&str, (f32, f32)> =
56 model.nodes.iter().map(|n| (n.id.as_str(), camera.project(n.pos))).collect();
57
58 let half = BOX_W * 0.5 * camera.zoom;
59
60 let mut push_edge = |e: &GraphEdge, w: f32| {
62 if let (Some(&(ax, ay)), Some(&(bx, by))) =
63 (idx.get(e.from.as_str()), idx.get(e.to.as_str()))
64 {
65 let a = [ax + half, ay];
66 let b = [bx - half, by];
67 lines.push(LineInstance::round(a, b, w * 0.5, 1.0, col(e.color)));
68 }
69 };
70 for e in &model.edges {
71 push_edge(e, (1.4 * camera.zoom).clamp(0.7, 3.0));
72 }
73 for e in &decorations.edges {
74 push_edge(e, (2.6 * camera.zoom).clamp(0.7, 4.0));
75 }
76
77 let base_r = (BOX_H * 0.5 * camera.zoom).max(2.0);
81 for n in &model.nodes {
82 let (cx, cy) = camera.project(n.pos);
83 let deco = decorations.nodes.get(&n.id);
84 let scale = deco.and_then(|d| d.scale).unwrap_or(1.0).max(0.05);
85 let r = (base_r * scale).max(2.0);
86 let corner = (5.0 * camera.zoom).clamp(0.0, r);
87 quads.push(
88 MarkerInstance {
89 center: [cx, cy],
90 radius: r,
91 corner,
92 color: col(n.fill),
93 aa: 1.0,
94 shape: shape::SQUARE,
95 }
96 .lower(),
97 );
98 if let Some(ring) = decorations.nodes.get(&n.id).and_then(|d| d.ring) {
99 quads.push(
101 CircleInstance { center: [cx, cy], radius: r + 2.0, color: col(ring), aa: 1.2 }
102 .lower(),
103 );
104 }
105 }
106
107 (quads, lines)
108}
109
110pub fn render_to_rgba_l0(
116 backend: Backend,
117 model: &GraphModel,
118 decorations: &Decorations,
119 camera: &Camera,
120 width: u32,
121 height: u32,
122 background: Color,
123) -> Frame {
124 let (quads, lines) = lower(model, decorations, camera);
125 let core_cam = CoreCamera {
127 pan_x: camera.pan_x,
128 pan_y: camera.pan_y,
129 zoom: camera.zoom,
130 ..CoreCamera::default()
131 };
132 #[cfg(feature = "testmatrix")]
137 facett_core::testmatrix::emit(
138 "facett-graphview::render_to_rgba_l0",
139 "l0_render",
140 !quads.is_empty() || !lines.is_empty() || (model.nodes.is_empty() && model.edges.is_empty()),
141 &format!("backend={backend:?} quads={} lines={} nodes={} edges={}", quads.len(), lines.len(), model.nodes.len(), model.edges.len()),
142 );
143 let _ = backend; let mut r = CpuRenderer::new([background.r, background.g, background.b, background.a]);
145 let canvas = r.begin(width, height, core_cam);
146 canvas.push_lines(&lines);
147 canvas.push_quads(&quads);
148 r.present()
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::backend::{GpuProbe, decide};
155 use crate::model::{GraphEdge, GraphNode, Pos};
156
157 fn sample() -> GraphModel {
158 GraphModel {
159 nodes: vec![
160 GraphNode { id: "a".into(), label: "a".into(), fill: Color::rgb(60, 90, 160), stroke: Color::WHITE, pos: Pos::new(0.0, 0.0) },
161 GraphNode { id: "b".into(), label: "b".into(), fill: Color::rgb(60, 140, 90), stroke: Color::WHITE, pos: Pos::new(300.0, -80.0) },
162 GraphNode { id: "c".into(), label: "c".into(), fill: Color::rgb(160, 90, 60), stroke: Color::WHITE, pos: Pos::new(300.0, 80.0) },
163 ],
164 edges: vec![
165 GraphEdge { from: "a".into(), to: "b".into(), color: Color::rgb(200, 200, 220), dashed: false, label: None },
166 GraphEdge { from: "a".into(), to: "c".into(), color: Color::rgb(200, 200, 220), dashed: true, label: None },
167 ],
168 }
169 }
170
171 #[test]
176 fn model_lowers_into_core_sdf_instances() {
177 let model = sample();
178 let cam = Camera::fit(&model, 640.0, 480.0, 40.0);
179 let (quads, lines) = lower(&model, &Decorations::default(), &cam);
180 assert_eq!(quads.len(), 3, "one rounded-square marker per node");
181 assert_eq!(lines.len(), 2, "one capsule per straight edge");
182 assert_eq!(quads[0].shape, shape::SQUARE);
184 let want = col(model.nodes[0].fill);
185 assert!((quads[0].color[2] - want[2]).abs() < 1e-6, "node fill colour carried into the L0 quad");
186 for q in &quads {
188 assert!(q.center[0] >= 0.0 && q.center[0] <= 640.0 && q.center[1] >= 0.0 && q.center[1] <= 480.0);
189 }
190 }
191
192 #[test]
196 fn l0_path_routes_through_core_canvas_and_renders_nonblank() {
197 let model = sample();
198 let cam = Camera::fit(&model, 640.0, 480.0, 40.0);
199 let backend = decide(GpuProbe::cpu_only());
200 assert_eq!(backend, Backend::CpuVello);
201 let frame: Frame = render_to_rgba_l0(
202 backend,
203 &model,
204 &Decorations::default(),
205 &cam,
206 640,
207 480,
208 Color::rgb(18, 20, 28),
209 );
210 assert_eq!(frame.rgba.len(), 640 * 480 * 4);
211 assert!(frame.lit_px() > 0, "L0 lane drew lit pixels");
212 let mut seen = std::collections::HashSet::new();
214 for px in frame.rgba.chunks_exact(4) {
215 seen.insert([px[0], px[1], px[2]]);
216 if seen.len() > 3 {
217 break;
218 }
219 }
220 assert!(seen.len() > 3, "L0 Canvas drew a real graph, not a flat pane");
221 }
222
223 #[test]
227 fn node_scale_decoration_enlarges_the_marker() {
228 use crate::model::NodeDecoration;
229 let model = sample();
230 let cam = Camera::fit(&model, 640.0, 480.0, 40.0);
231
232 let (base_quads, _) = lower(&model, &Decorations::default(), &cam);
234 let base_r = base_quads[0].radius;
235
236 let mut deco = Decorations::default();
238 deco.nodes.insert("a".into(), NodeDecoration { scale: Some(3.0), ..Default::default() });
239 let (quads, _) = lower(&model, &deco, &cam);
240 assert!((quads[0].radius - base_r * 3.0).abs() < 0.5, "node 'a' marker scaled 3× ({} vs {})", quads[0].radius, base_r * 3.0);
242 assert!((quads[1].radius - base_r).abs() < 0.5, "node 'b' marker unchanged at default size");
243 }
244}