This repository has no description
1use itertools::Itertools;
2use measure_time::debug_time;
3
4use crate::{
5 graphics::objects::{LineSegment, ObjectSizes},
6 ColoredObject, Object,
7};
8
9use super::{renderable::SVGRenderable, CSSRenderable, SVGAttributesRenderable};
10
11impl SVGRenderable for ColoredObject {
12 fn render_to_svg(
13 &self,
14 colormap: crate::ColorMapping,
15 cell_size: usize,
16 object_sizes: crate::graphics::objects::ObjectSizes,
17 id: &str,
18 ) -> anyhow::Result<svg::node::element::Element> {
19 debug_time!("render_to_svg/colored_object");
20 let mut obj = self.object.render_to_svg(
21 colormap.clone(),
22 cell_size,
23 object_sizes,
24 id,
25 )?;
26
27 let mut css = String::new();
28 if !matches!(self.object, Object::RawSVG(..)) {
29 css = self
30 .fill
31 .render_to_css(&colormap.clone(), !self.object.fillable());
32 }
33
34 if !self.transformations.is_empty() || !self.filters.is_empty() {
35 obj = svg::node::element::Group::new()
36 .set("data-object", id)
37 .add(obj)
38 .into();
39
40 let attributes = obj.get_attributes_mut();
41
42 for (key, value) in self.transformations.render_to_svg_attributes(
43 colormap.clone(),
44 cell_size,
45 object_sizes,
46 id,
47 )? {
48 attributes.insert(key, value.into());
49 }
50
51 let start = self.object.region().start.coords(cell_size);
52 let (w, h) = (
53 self.object.region().width() * cell_size,
54 self.object.region().height() * cell_size,
55 );
56
57 attributes.insert(
58 "transform-origin".to_string(),
59 format!(
60 "{} {}",
61 start.0 + (w as f32 / 2.0),
62 start.1 + (h as f32 / 2.0)
63 )
64 .into(),
65 );
66
67 css += "transform-box: fill-box;";
68
69 css += self
70 .filters
71 .iter()
72 .map(|f| f.render_to_css_filled(&colormap))
73 .join(" ")
74 .as_ref();
75 }
76
77 obj.get_attributes_mut().insert("style".into(), css.into());
78
79 Ok(obj)
80 }
81}
82
83impl SVGRenderable for Object {
84 fn render_to_svg(
85 &self,
86 _colormap: crate::ColorMapping,
87 cell_size: usize,
88 object_sizes: crate::graphics::objects::ObjectSizes,
89 id: &str,
90 ) -> anyhow::Result<svg::node::element::Element> {
91 debug_time!("render_to_svg/object");
92 let mut rendered = match self {
93 Object::Text(..) | Object::CenteredText(..) => {
94 self.render_text(cell_size)
95 }
96 Object::Rectangle(..) => self.render_rectangle(cell_size),
97 Object::Polygon(..) => self.render_polygon(cell_size),
98 Object::Line(..) => self.render_line(cell_size),
99 Object::CurveInward(..) | Object::CurveOutward(..) => {
100 self.render_curve(cell_size)
101 }
102 Object::SmallCircle(..) => {
103 self.render_small_circle(cell_size, object_sizes)
104 }
105 Object::Dot(..) => self.render_dot(cell_size, object_sizes),
106 Object::BigCircle(..) => self.render_big_circle(cell_size),
107 Object::Image(..) => self.render_image(cell_size),
108 Object::RawSVG(..) => self.render_raw_svg(),
109 };
110
111 // Ok(group.set("data-object", id).add(rendered).into())
112 rendered
113 .get_attributes_mut()
114 .insert("data-object".into(), id.into());
115 Ok(rendered)
116 }
117}
118
119impl Object {
120 fn render_image(&self, cell_size: usize) -> svg::node::element::Element {
121 if let Object::Image(region, path) = self {
122 let (x, y) = region.start.coords(cell_size);
123 return svg::node::element::Image::new()
124 .set("x", x)
125 .set("y", y)
126 .set("width", region.width() * cell_size)
127 .set("height", region.height() * cell_size)
128 .set("href", path.clone())
129 .into();
130 }
131
132 panic!("Expected Image, got {:?}", self);
133 }
134
135 fn render_raw_svg(&self) -> svg::node::element::Element {
136 if let Object::RawSVG(svg) = self {
137 return svg.clone();
138 }
139
140 panic!("Expected RawSVG, got {:?}", self);
141 }
142
143 fn render_text(&self, cell_size: usize) -> svg::node::element::Element {
144 if let Object::Text(position, content, font_size)
145 | Object::CenteredText(position, content, font_size) = self
146 {
147 let centered = matches!(self, Object::CenteredText(..));
148
149 let coords = if centered {
150 position.center_coords(cell_size)
151 } else {
152 position.coords(cell_size)
153 };
154
155 let mut node = svg::node::element::Text::new(content.clone())
156 .set("x", coords.0)
157 .set("y", coords.1)
158 .set("font-size", format!("{}pt", font_size))
159 .set("font-family", "Inconsolata");
160
161 if centered {
162 node = node
163 .set("text-anchor", "middle")
164 // FIXME does not work with imagemagick
165 .set("dominant-baseline", "middle");
166 } else {
167 // FIXME does not work with imagemagick
168 // see https://legacy.imagemagick.org/discourse-server/viewtopic.php?t=31540
169 node = node.set("dominant-baseline", "hanging")
170 }
171
172 return node.into();
173 }
174
175 panic!("Expected Text, got {:?}", self);
176 }
177
178 // fn render_fitted_text(&self, cell_size: usize) -> svg::node::element::Element {
179 // if let Object::FittedText(region, content) = self {
180 // let (x, y) = region.start.coords(cell_size);
181 // let width = region.width() * cell_size as f32;
182 // let height = region.height() * cell_size as f32;
183
184 // return Box::new(
185 // svg::node::element::Text::new(content.clone())
186 // .set("x", x)
187 // .set("y", y)
188 // .set("")
189 // .set("font-size", format!("{}pt", 10.0))
190 // .set("font-family", "sans-serif"),
191 // );
192 // }
193
194 // panic!("Expected FittedText, got {:?}", self);
195 // }
196
197 fn render_rectangle(&self, cell_size: usize) -> svg::node::element::Element {
198 if let Object::Rectangle(start, end) = self {
199 return svg::node::element::Rectangle::new()
200 .set("x", start.coords(cell_size).0)
201 .set("y", start.coords(cell_size).1)
202 .set("width", start.distances(end).0 * cell_size)
203 .set("height", start.distances(end).1 * cell_size)
204 .into();
205 }
206
207 panic!("Expected Rectangle, got {:?}", self);
208 }
209
210 fn render_polygon(&self, cell_size: usize) -> svg::node::element::Element {
211 if let Object::Polygon(start, lines) = self {
212 let mut path = svg::node::element::path::Data::new();
213 path = path.move_to(start.coords(cell_size));
214 for line in lines {
215 path = match line {
216 LineSegment::Straight(end)
217 | LineSegment::InwardCurve(end)
218 | LineSegment::OutwardCurve(end) => {
219 path.line_to(end.coords(cell_size))
220 }
221 };
222 }
223 path = path.close();
224 return svg::node::element::Path::new().set("d", path).into();
225 }
226
227 panic!("Expected Polygon, got {:?}", self);
228 }
229
230 fn render_line(&self, cell_size: usize) -> svg::node::element::Element {
231 if let Object::Line(start, end, width) = self {
232 return svg::node::element::Line::new()
233 .set("x1", start.coords(cell_size).0)
234 .set("y1", start.coords(cell_size).1)
235 .set("x2", end.coords(cell_size).0)
236 .set("y2", end.coords(cell_size).1)
237 .set("stroke-width", *width)
238 .into();
239 }
240
241 panic!("Expected Line, got {:?}", self);
242 }
243
244 fn render_curve(&self, cell_size: usize) -> svg::node::element::Element {
245 if let Object::CurveOutward(start, end, stroke_width)
246 | Object::CurveInward(start, end, stroke_width) = self
247 {
248 let inward = matches!(self, Object::CurveInward(..));
249
250 let (start_x, start_y) = start.coords(cell_size);
251 let (end_x, end_y) = end.coords(cell_size);
252
253 let midpoint = ((start_x + end_x) / 2.0, (start_y + end_y) / 2.0);
254 let start_from_midpoint =
255 (start_x - midpoint.0, start_y - midpoint.1);
256 let end_from_midpoint = (end_x - midpoint.0, end_y - midpoint.1);
257
258 let control = {
259 let relative = (end_x - start_x, end_y - start_y);
260 if start_from_midpoint.0 * start_from_midpoint.1 > 0.0
261 && end_from_midpoint.0 * end_from_midpoint.1 > 0.0
262 {
263 if inward {
264 (
265 midpoint.0 + relative.0.abs() / 2.0,
266 midpoint.1 - relative.1.abs() / 2.0,
267 )
268 } else {
269 (
270 midpoint.0 - relative.0.abs() / 2.0,
271 midpoint.1 + relative.1.abs() / 2.0,
272 )
273 }
274 // diagonal line is going like this: /
275 } else if start_from_midpoint.0 * start_from_midpoint.1 < 0.0
276 && end_from_midpoint.0 * end_from_midpoint.1 < 0.0
277 {
278 if inward {
279 (
280 midpoint.0 - relative.0.abs() / 2.0,
281 midpoint.1 - relative.1.abs() / 2.0,
282 )
283 } else {
284 (
285 midpoint.0 + relative.0.abs() / 2.0,
286 midpoint.1 + relative.1.abs() / 2.0,
287 )
288 }
289 // line is horizontal
290 } else if start_y == end_y {
291 (
292 midpoint.0,
293 midpoint.1
294 + (if inward { -1.0 } else { 1.0 })
295 * relative.0.abs()
296 / 2.0,
297 )
298 // line is vertical
299 } else if start_x == end_x {
300 (
301 midpoint.0
302 + (if inward { -1.0 } else { 1.0 })
303 * relative.1.abs()
304 / 2.0,
305 midpoint.1,
306 )
307 } else {
308 unreachable!()
309 }
310 };
311
312 return svg::node::element::Path::new()
313 .set(
314 "d",
315 svg::node::element::path::Data::new()
316 .move_to(start.coords(cell_size))
317 .quadratic_curve_to((control, end.coords(cell_size))),
318 )
319 .set("stroke-width", format!("{stroke_width}"))
320 .into();
321 }
322
323 panic!("Expected Curve, got {:?}", self);
324 }
325
326 fn render_small_circle(
327 &self,
328 cell_size: usize,
329 object_sizes: ObjectSizes,
330 ) -> svg::node::element::Element {
331 if let Object::SmallCircle(center) = self {
332 return svg::node::element::Circle::new()
333 .set("cx", center.coords(cell_size).0)
334 .set("cy", center.coords(cell_size).1)
335 .set("r", object_sizes.small_circle_radius)
336 .into();
337 }
338
339 panic!("Expected SmallCircle, got {:?}", self);
340 }
341
342 fn render_dot(
343 &self,
344 cell_size: usize,
345 object_sizes: ObjectSizes,
346 ) -> svg::node::element::Element {
347 if let Object::Dot(center) = self {
348 return svg::node::element::Circle::new()
349 .set("cx", center.coords(cell_size).0)
350 .set("cy", center.coords(cell_size).1)
351 .set("r", object_sizes.dot_radius)
352 .into();
353 }
354
355 panic!("Expected Dot, got {:?}", self);
356 }
357
358 fn render_big_circle(&self, cell_size: usize) -> svg::node::element::Element {
359 if let Object::BigCircle(topleft) = self {
360 let (cx, cy) = {
361 let (x, y) = topleft.coords(cell_size);
362 (x + cell_size as f32 / 2.0, y + cell_size as f32 / 2.0)
363 };
364
365 return svg::node::element::Circle::new()
366 .set("cx", cx)
367 .set("cy", cy)
368 .set("r", cell_size / 2)
369 .into();
370 }
371
372 panic!("Expected BigCircle, got {:?}", self);
373 }
374}