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