1use crate::Facet;
8
9#[derive(Debug, Clone)]
11pub struct RenderReport {
12 pub title: String,
13 pub state: serde_json::Value,
15 pub vertices: usize,
17}
18
19impl RenderReport {
20 pub fn drew(&self) -> bool {
21 self.vertices > 0
22 }
23}
24
25#[allow(deprecated)] fn capture(ctx: &egui::Context, facet: &mut dyn Facet, size: (f32, f32)) -> RenderReport {
29 let title = facet.title().to_string();
30 crate::trace::emit_in(
33 "facet.render",
34 &serde_json::json!({ "title": title, "size": [size.0, size.1] }),
35 );
36 let input = egui::RawInput {
37 screen_rect: Some(egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(size.0, size.1))),
38 ..Default::default()
39 };
40 let output = ctx.run(input, |ctx| {
41 egui::CentralPanel::default().show(ctx, |ui| facet.ui(ui));
42 });
43 let prims = ctx.tessellate(output.shapes, output.pixels_per_point);
44 let vertices = prims
45 .iter()
46 .map(|p| match &p.primitive {
47 egui::epaint::Primitive::Mesh(m) => m.vertices.len(),
48 _ => 0,
49 })
50 .sum();
51 let report = RenderReport { title, state: facet.state_json(), vertices };
52 log(&report);
53 trail(Kind::Render, format!("{} size={}x{} → {} verts", report.title, size.0 as i32, size.1 as i32, vertices));
54 dump_state(&report);
55 crate::trace::emit_out(
59 "facet.render",
60 &serde_json::json!({
61 "title": report.title,
62 "vertices": report.vertices,
63 "drew": report.drew(),
64 "state": report.state,
65 }),
66 );
67 report
68}
69
70pub fn render_sized(facet: &mut dyn Facet, size: (f32, f32)) -> RenderReport {
72 capture(&egui::Context::default(), facet, size)
73}
74
75pub fn headless_render(facet: &mut dyn Facet) -> RenderReport {
77 render_sized(facet, (800.0, 600.0))
78}
79
80pub fn render_themed(facet: &mut dyn Facet, theme: crate::Theme) -> RenderReport {
82 let ctx = egui::Context::default();
83 crate::set_theme(&ctx, theme);
84 capture(&ctx, facet, (800.0, 600.0))
85}
86
87pub fn render_themed_sized(facet: &mut dyn Facet, theme: crate::Theme, size: (f32, f32)) -> RenderReport {
93 let ctx = egui::Context::default();
94 crate::set_theme(&ctx, theme);
95 capture(&ctx, facet, size)
96}
97
98pub fn log(r: &RenderReport) {
101 let full = r.state.to_string();
102 let shown: String = if full.chars().count() > 160 {
103 full.chars().take(159).chain(std::iter::once('…')).collect()
104 } else {
105 full
106 };
107 eprintln!("facett: {:<14} {:>7} verts · {}", r.title, r.vertices, shown);
108}
109
110#[derive(Clone, Copy, Debug, PartialEq, Eq)]
121pub enum Kind {
122 Render,
124 State,
126 Case,
128}
129
130impl Kind {
131 pub fn tag(self) -> &'static str {
132 match self {
133 Kind::Render => "RENDER",
134 Kind::State => "STATE",
135 Kind::Case => "CASE",
136 }
137 }
138}
139
140fn next_seq() -> u64 {
142 use std::sync::atomic::{AtomicU64, Ordering};
143 static SEQ: AtomicU64 = AtomicU64::new(0);
144 SEQ.fetch_add(1, Ordering::Relaxed) + 1
145}
146
147fn now_stamp() -> String {
151 let now = std::time::SystemTime::now()
152 .duration_since(std::time::UNIX_EPOCH)
153 .unwrap_or_default();
154 let total_ms = now.as_millis();
155 let ms = (total_ms % 1000) as u64;
156 let secs = (total_ms / 1000) as u64;
157 let h = (secs / 3600) % 24;
158 let m = (secs / 60) % 60;
159 let s = secs % 60;
160 format!("{h:02}:{m:02}:{s:02}.{ms:03}")
161}
162
163pub fn trail(kind: Kind, detail: impl AsRef<str>) {
169 let stamp = now_stamp();
170 let seq = next_seq();
171 let detail = detail.as_ref();
172 let line = format!("facett ACTION {stamp} {seq:>5} [{}] {detail}", kind.tag());
173 eprintln!("{line}");
174 if let Ok(path) = std::env::var("FACETT_TRAIL") {
175 use std::io::Write;
176 if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
177 let _ = writeln!(f, "{line}");
178 }
179 }
180}
181
182pub fn dump_state(r: &RenderReport) {
187 eprintln!("facett STATE {} = {}", r.title, r.state);
188 trail(Kind::State, format!("{} state={}", r.title, r.state));
189}
190
191pub fn case_summary(component: &str, axis: &str, r: &RenderReport) {
196 eprintln!(
197 "facett CASE {:<14} {:<16} → {:>8} verts drew={} state={}",
198 component,
199 axis,
200 r.vertices,
201 r.drew(),
202 r.state,
203 );
204 trail(Kind::Case, format!("{component} {axis} verts={} drew={}", r.vertices, r.drew()));
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::{Scene, hash_color};
211
212 struct Tiny(Scene);
213 impl Facet for Tiny {
214 fn title(&self) -> &str {
215 "tiny"
216 }
217 fn ui(&mut self, ui: &mut egui::Ui) {
218 crate::draw(ui, &self.0, crate::Layout::Circular, "empty");
219 }
220 fn state_json(&self) -> serde_json::Value {
221 serde_json::json!({ "nodes": self.0.nodes.len() })
222 }
223 }
224
225 #[test]
226 fn now_stamp_is_hms_millis_shaped() {
227 let s = now_stamp();
228 assert_eq!(s.len(), 12, "stamp `{s}` should be HH:MM:SS.mmm");
230 assert_eq!(s.matches(':').count(), 2, "stamp `{s}` needs two colons");
231 assert_eq!(s.matches('.').count(), 1, "stamp `{s}` needs one dot");
232 }
233
234 #[test]
235 fn seq_is_monotonic() {
236 let a = next_seq();
237 let b = next_seq();
238 assert!(b > a, "seq must strictly increase: {a} then {b}");
239 }
240
241 #[test]
242 fn kind_tags_are_distinct() {
243 let tags = [Kind::Render.tag(), Kind::State.tag(), Kind::Case.tag()];
244 for (i, t) in tags.iter().enumerate() {
245 assert!(!t.is_empty());
246 assert!(!tags[..i].contains(t), "duplicate tag {t}");
247 }
248 }
249
250 #[test]
251 fn headless_render_captures_state_and_draws() {
252 let mut scene = Scene::new();
253 let a = scene.node("a", hash_color("a"));
254 let b = scene.node("b", hash_color("b"));
255 scene.edge(a, b);
256 let mut t = Tiny(scene);
257 let r = headless_render(&mut t);
258 assert_eq!(r.title, "tiny");
259 assert_eq!(r.state["nodes"], 2);
260 assert!(r.drew(), "a 2-node graph should tessellate to vertices");
261 }
262}