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