1use std::hash::{Hash, Hasher};
8
9use kurbo::PathEl;
10use oxframe_core::BlendMode;
11
12use crate::draw::{DrawItem, Paint, Primitive, StrokeStyle};
13use crate::graph::NodeId;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum PassKind {
18 Vector,
20 Composite,
22}
23
24impl PassKind {
25 pub fn name(self) -> &'static str {
27 match self {
28 Self::Vector => "vector",
29 Self::Composite => "composite",
30 }
31 }
32}
33
34#[derive(Debug, Clone, PartialEq)]
36pub struct Pass {
37 pub kind: PassKind,
39 pub node: NodeId,
41 pub items: u32,
43 pub depth: u32,
45 pub opacity: f64,
47 pub blend: &'static str,
49 pub filtered: bool,
51}
52
53pub fn pass_graph(items: &[DrawItem]) -> Vec<Pass> {
57 let mut stack = vec![Scope::frame()];
58 for item in items {
59 match &item.prim {
60 Primitive::PushLayer(layer) => {
61 flush(&mut stack);
62 stack.push(Scope {
63 node: item.node,
64 items: 0,
65 run: 0,
66 depth: stack.len() as u32,
67 layer: Some(LayerInfo {
68 opacity: layer.opacity,
69 blend: blend_name(layer.blend),
70 filtered: layer.has_filter() || layer.clip.is_some(),
71 }),
72 passes: Vec::new(),
73 });
74 }
75 Primitive::PopLayer => {
76 flush(&mut stack);
77 let Some(mut scope) = stack.pop() else {
78 continue;
79 };
80 if stack.is_empty() {
81 stack.push(scope);
82 continue;
83 }
84 let info = scope.layer.unwrap_or(LayerInfo {
85 opacity: 1.0,
86 blend: "normal",
87 filtered: false,
88 });
89 let parent = stack.last_mut().expect("frame scope");
90 parent.items += scope.items;
91 parent.passes.append(&mut scope.passes);
92 parent.passes.push(Pass {
93 kind: PassKind::Composite,
94 node: scope.node,
95 items: scope.items,
96 depth: scope.depth - 1,
97 opacity: info.opacity,
98 blend: info.blend,
99 filtered: info.filtered,
100 });
101 }
102 _ => {
103 let scope = stack.last_mut().expect("frame scope");
104 scope.items += 1;
105 scope.run += 1;
106 }
107 }
108 }
109 flush(&mut stack);
110 let mut passes = stack.pop().map(|s| s.passes).unwrap_or_default();
111 if passes.is_empty() {
112 passes.push(Pass {
113 kind: PassKind::Vector,
114 node: NodeId(0),
115 items: 0,
116 depth: 0,
117 opacity: 1.0,
118 blend: "normal",
119 filtered: false,
120 });
121 }
122 passes
123}
124
125struct LayerInfo {
126 opacity: f64,
127 blend: &'static str,
128 filtered: bool,
129}
130
131struct Scope {
132 node: NodeId,
133 items: u32,
134 run: u32,
135 depth: u32,
136 layer: Option<LayerInfo>,
137 passes: Vec<Pass>,
138}
139
140impl Scope {
141 fn frame() -> Self {
142 Self {
143 node: NodeId(0),
144 items: 0,
145 run: 0,
146 depth: 0,
147 layer: None,
148 passes: Vec::new(),
149 }
150 }
151}
152
153fn flush(stack: &mut [Scope]) {
154 let Some(scope) = stack.last_mut() else {
155 return;
156 };
157 if scope.run == 0 {
158 return;
159 }
160 let run = scope.run;
161 scope.run = 0;
162 scope.passes.push(Pass {
163 kind: PassKind::Vector,
164 node: scope.node,
165 items: run,
166 depth: scope.depth,
167 opacity: 1.0,
168 blend: "normal",
169 filtered: false,
170 });
171}
172
173fn blend_name(blend: BlendMode) -> &'static str {
174 match blend {
175 BlendMode::Normal => "normal",
176 BlendMode::Multiply => "multiply",
177 BlendMode::Screen => "screen",
178 BlendMode::Overlay => "overlay",
179 BlendMode::Darken => "darken",
180 BlendMode::Lighten => "lighten",
181 BlendMode::ColorDodge => "color_dodge",
182 BlendMode::ColorBurn => "color_burn",
183 BlendMode::HardLight => "hard_light",
184 BlendMode::SoftLight => "soft_light",
185 BlendMode::Difference => "difference",
186 BlendMode::Exclusion => "exclusion",
187 BlendMode::Hue => "hue",
188 BlendMode::Saturation => "saturation",
189 BlendMode::Color => "color",
190 BlendMode::Luminosity => "luminosity",
191 }
192}
193
194pub fn layer_fingerprint(items: &[DrawItem], start: usize) -> u64 {
197 let mut hasher = std::collections::hash_map::DefaultHasher::new();
198 let mut depth = 0usize;
199 for item in &items[start..] {
200 item.node.0.hash(&mut hasher);
201 for c in item.transform.as_coeffs() {
202 c.to_bits().hash(&mut hasher);
203 }
204 match &item.prim {
205 Primitive::PushLayer(layer) => {
206 depth += 1;
207 hash_layer(&mut hasher, layer);
208 }
209 Primitive::PopLayer => {
210 2u8.hash(&mut hasher);
211 depth -= 1;
212 if depth == 0 {
213 break;
214 }
215 }
216 Primitive::Fill { path, paint } => {
217 3u8.hash(&mut hasher);
218 hash_path(&mut hasher, path);
219 hash_paint(&mut hasher, paint);
220 }
221 Primitive::Stroke { path, paint, style } => {
222 4u8.hash(&mut hasher);
223 hash_path(&mut hasher, path);
224 hash_paint(&mut hasher, paint);
225 hash_stroke(&mut hasher, style);
226 }
227 Primitive::Image {
228 image,
229 rect,
230 opacity,
231 } => {
232 5u8.hash(&mut hasher);
233 image.id.hash(&mut hasher);
234 rect.x0.to_bits().hash(&mut hasher);
235 rect.y0.to_bits().hash(&mut hasher);
236 rect.x1.to_bits().hash(&mut hasher);
237 rect.y1.to_bits().hash(&mut hasher);
238 opacity.to_bits().hash(&mut hasher);
239 }
240 }
241 }
242 hasher.finish()
243}
244
245fn hash_layer(hasher: &mut impl Hasher, layer: &crate::draw::Layer) {
246 1u8.hash(hasher);
247 layer.opacity.to_bits().hash(hasher);
248 blend_name(layer.blend).hash(hasher);
249 layer.blur.to_bits().hash(hasher);
250 if let Some(path) = &layer.clip {
251 hash_path(hasher, path);
252 }
253 if let Some(shadow) = &layer.shadow {
254 hash_color(hasher, shadow.color);
255 shadow.blur.to_bits().hash(hasher);
256 shadow.offset.x.to_bits().hash(hasher);
257 shadow.offset.y.to_bits().hash(hasher);
258 }
259}
260
261fn hash_path(hasher: &mut impl Hasher, path: &kurbo::BezPath) {
262 for el in path.elements() {
263 match el {
264 PathEl::MoveTo(p) => {
265 0u8.hash(hasher);
266 hash_point(hasher, *p);
267 }
268 PathEl::LineTo(p) => {
269 1u8.hash(hasher);
270 hash_point(hasher, *p);
271 }
272 PathEl::QuadTo(a, b) => {
273 2u8.hash(hasher);
274 hash_point(hasher, *a);
275 hash_point(hasher, *b);
276 }
277 PathEl::CurveTo(a, b, c) => {
278 3u8.hash(hasher);
279 hash_point(hasher, *a);
280 hash_point(hasher, *b);
281 hash_point(hasher, *c);
282 }
283 PathEl::ClosePath => 4u8.hash(hasher),
284 }
285 }
286}
287
288fn hash_point(hasher: &mut impl Hasher, p: kurbo::Point) {
289 p.x.to_bits().hash(hasher);
290 p.y.to_bits().hash(hasher);
291}
292
293fn hash_paint(hasher: &mut impl Hasher, paint: &Paint) {
294 match paint {
295 Paint::Solid(c) => {
296 0u8.hash(hasher);
297 hash_color(hasher, *c);
298 }
299 Paint::Gradient(g) => {
300 1u8.hash(hasher);
301 match g.kind {
302 oxframe_core::GradientKind::Linear { from, to } => {
303 0u8.hash(hasher);
304 hash_point(hasher, from);
305 hash_point(hasher, to);
306 }
307 oxframe_core::GradientKind::Radial { center, radius } => {
308 1u8.hash(hasher);
309 hash_point(hasher, center);
310 radius.to_bits().hash(hasher);
311 }
312 }
313 for (offset, color) in &g.stops {
314 offset.to_bits().hash(hasher);
315 hash_color(hasher, *color);
316 }
317 }
318 }
319}
320
321fn hash_color(hasher: &mut impl Hasher, c: oxframe_core::Color) {
322 c.r.to_bits().hash(hasher);
323 c.g.to_bits().hash(hasher);
324 c.b.to_bits().hash(hasher);
325 c.a.to_bits().hash(hasher);
326}
327
328fn hash_stroke(hasher: &mut impl Hasher, style: &StrokeStyle) {
329 style.width.to_bits().hash(hasher);
330 cap_tag(style.cap).hash(hasher);
331 join_tag(style.join).hash(hasher);
332 style.dash_offset.to_bits().hash(hasher);
333 for d in &style.dash {
334 d.to_bits().hash(hasher);
335 }
336}
337
338fn cap_tag(cap: crate::draw::LineCap) -> u8 {
339 use crate::draw::LineCap::{Butt, Round, Square};
340 match cap {
341 Butt => 0,
342 Round => 1,
343 Square => 2,
344 }
345}
346
347fn join_tag(join: crate::draw::LineJoin) -> u8 {
348 use crate::draw::LineJoin::{Bevel, Miter, Round};
349 match join {
350 Miter => 0,
351 Round => 1,
352 Bevel => 2,
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use oxframe_core::Color;
359
360 use super::*;
361 use crate::graph::SceneGraph;
362 use crate::{NodeBuilder, Rect, View};
363
364 #[test]
365 fn a_scene_without_effects_is_one_vector_pass() {
366 let graph = SceneGraph::new(80, 80);
367 graph.enter(|| {
368 View::current().add(Rect::new().size(20.0).fill(Color::WHITE));
369 View::current().add(Rect::new().size(10.0).x(15.0).fill(Color::BLACK));
370 });
371 let passes = pass_graph(&graph.draw_list());
372 assert_eq!(passes.len(), 1);
373 assert_eq!(passes[0].kind, PassKind::Vector);
374 assert!(passes[0].items >= 2);
375 }
376
377 #[test]
378 fn a_blur_is_a_composite_after_its_contents() {
379 let graph = SceneGraph::new(80, 80);
380 graph.enter(|| {
381 View::current().add(
382 Rect::new()
383 .size(20.0)
384 .fill(Color::WHITE)
385 .blur(2.0)
386 .name("soft"),
387 );
388 });
389 let passes = pass_graph(&graph.draw_list());
390 assert!(
391 passes.iter().any(|p| p.kind == PassKind::Vector),
392 "{passes:?}"
393 );
394 let composite = passes
395 .iter()
396 .find(|p| p.kind == PassKind::Composite)
397 .expect("blur opens a layer");
398 assert!(composite.filtered);
399 let vector_at = passes.iter().position(|p| p.kind == PassKind::Vector);
400 let composite_at = passes.iter().position(|p| p.kind == PassKind::Composite);
401 assert!(vector_at < composite_at);
402 }
403}