1use std::collections::HashMap;
2
3use tiny_skia as sk;
4
5use reflexo::{hash::Fingerprint, vector::ir::*};
6
7#[derive(Default)]
8pub struct Vec2BBoxPass {
9 bbox_caches: HashMap<(Fingerprint, Transform), Option<Rect>>,
10}
11
12impl Vec2BBoxPass {
13 pub fn bbox_of(&mut self, module: &Module, v: Fingerprint, ts: Transform) -> Option<Rect> {
17 if let Some(bbox) = self.bbox_caches.get(&(v, ts)) {
18 return *bbox;
19 }
20
21 let bbox = self.bbox_of_(module, v, ts);
22 eprintln!("bbox_of({v:?}, {ts:?}) = {bbox:?}");
23 self.bbox_caches.insert((v, ts), bbox);
24 bbox
25 }
26
27 fn bbox_of_(&mut self, module: &Module, v: Fingerprint, ts: Transform) -> Option<Rect> {
28 let item = module.get_item(&v).unwrap();
29 match item {
30 VecItem::Item(item) => {
31 let ts_group: Transform = item.0.clone().into();
32 let ts = ts.pre_concat(ts_group);
33 self.bbox_of(module, item.1, ts)
34 }
35 VecItem::Labelled(item) => self.bbox_of(module, item.1, ts),
36 VecItem::Group(g) => {
37 let mut r = Rect::default();
38 for (p, f) in g.0.iter() {
39 let sub_bbox = self.bbox_of(module, *f, ts);
40 if let Some(sub_bbox) = sub_bbox {
41 union(&mut r, *p, sub_bbox);
42 }
43 }
44 Some(r)
45 }
46 VecItem::Image(ImageItem { size, .. })
47 | VecItem::Link(LinkItem { size, .. })
48 | VecItem::SizedRawHtml(SizedRawHtmlItem { size, .. }) => self.rect(*size, ts),
49 VecItem::Text(t) => {
51 let width = t.width();
52 let height = t.shape.size.0;
53 tiny_skia_path::Rect::from_xywh(0.0, 0.0, width.0, height).map(|e| e.into())
54 }
55 VecItem::Path(p) => self.path(p, ts),
56 VecItem::ContentHint(..)
57 | VecItem::ColorTransform(..)
58 | VecItem::Pattern(..)
59 | VecItem::Gradient(..)
60 | VecItem::Color32(..)
61 | VecItem::Html(..)
62 | VecItem::None => None,
63 }
64 }
65
66 pub fn path(&mut self, p: &PathItem, ts: Transform) -> Option<Rect> {
67 Self::path_bbox(p, ts.into())
68 }
69
70 fn rect(&self, size: Axes<Scalar>, ts: Transform) -> Option<Rect> {
71 let r = tiny_skia_path::Rect::from_xywh(0.0, 0.0, size.x.0, size.y.0);
72 r.and_then(|e| e.transform(ts.into())).map(|e| e.into())
73 }
74
75 pub fn simple_path_bbox(p: &str, ts: sk::Transform) -> Option<Rect> {
76 let d = convert_path(p);
77 d.and_then(|e| e.transform(ts))
78 .and_then(|e| e.compute_tight_bounds())
79 .map(|e| e.into())
80 }
81
82 pub fn path_bbox(p: &PathItem, ts: sk::Transform) -> Option<Rect> {
83 let d = convert_path(&p.d);
84 d.and_then(|e| e.transform(ts))
85 .and_then(|e| e.compute_tight_bounds())
86 .and_then(|e| {
87 let Some(stroke) = p.styles.iter().find_map(|s| match s {
88 PathStyle::StrokeWidth(w) => Some(w.0),
89 _ => None,
90 }) else {
91 return Some(e);
92 };
93 let sk::Transform { sx, sy, kx, ky, .. } = ts;
94 let stroke_x = (stroke * (sx + ky)).abs();
95 let stroke_y = (stroke * (sy + kx)).abs();
96 let x = e.x() - stroke_x;
98 let y = e.y() - stroke_y;
99 let w = e.width() + stroke_x * 2.0;
100 let h = e.height() + stroke_y * 2.0;
101 tiny_skia_path::Rect::from_xywh(x, y, w, h)
102 })
103 .map(|e| e.into())
104 }
105}
106
107fn union(r: &mut Rect, p: Axes<Scalar>, sub_bbox: Rect) {
108 *r = r.union(&sub_bbox.translate(p))
109}
110
111fn convert_path(path_data: &str) -> Option<tiny_skia_path::Path> {
112 let mut builder = tiny_skia_path::PathBuilder::new();
113 for segment in svgtypes::SimplifyingPathParser::from(path_data) {
114 let segment = match segment {
115 Ok(v) => v,
116 Err(_) => break,
117 };
118
119 match segment {
120 svgtypes::SimplePathSegment::MoveTo { x, y } => {
121 builder.move_to(x as f32, y as f32);
122 }
123 svgtypes::SimplePathSegment::LineTo { x, y } => {
124 builder.line_to(x as f32, y as f32);
125 }
126 svgtypes::SimplePathSegment::Quadratic { x1, y1, x, y } => {
127 builder.quad_to(x1 as f32, y1 as f32, x as f32, y as f32);
128 }
129 svgtypes::SimplePathSegment::CurveTo {
130 x1,
131 y1,
132 x2,
133 y2,
134 x,
135 y,
136 } => {
137 builder.cubic_to(
138 x1 as f32, y1 as f32, x2 as f32, y2 as f32, x as f32, y as f32,
139 );
140 }
141 svgtypes::SimplePathSegment::ClosePath => {
142 builder.close();
143 }
144 }
145 }
146
147 builder.finish()
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_path_bbox() {
156 let data = "M 0 0 M 0 4.8 C 0 2.1490333 2.1490333 0 4.8 0 L 975.2 0 C 977.85095 0 980 2.1490333 980 4.8 L 980 122.256 C 980 124.90697 977.85095 127.056 975.2 127.056 L 4.8 127.056 C 2.1490333 127.056 0 124.90697 0 122.256 Z ";
157
158 let p = PathItem {
159 d: data.into(),
160 size: None,
161 styles: vec![],
162 };
163
164 let ts = sk::Transform::from_scale(4.5, 4.5);
165
166 assert!(Vec2BBoxPass::path_bbox(&p, ts).is_some());
167 }
168}