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::BigDot(..)
100 | Object::Dot(..)
101 | Object::BigCircle(..)
102 | Object::SmallCircle(..) => {
103 self.render_circle(cell_size, object_sizes)
104 }
105 Object::Image(..) => self.render_image(cell_size),
106 Object::RawSVG(..) => self.render_raw_svg(),
107 };
108
109 Ok(match rendered {
110 svg::Node::Element(el) => el.dataset("object", id).into(),
111 svg::Node::SVG(svg) => {
112 if svg.trim().starts_with("<") {
113 let (before, after) =
114 svg.split_once(' ').unwrap_or(svg.split_once(">").expect("Malformed SVG tag, {svg} starts with < but doesn't contain a space or >"));
115 svg::Node::SVG(format!(
116 r#"{before} data-object="{id}" {after}"#
117 ))
118 } else {
119 eprintln!("Malformed raw SVG, {svg} doesn't start with <");
120 svg::Node::SVG(svg)
121 }
122 }
123 _ => {
124 panic!("Expected Element or SVG, got {:?}", rendered);
125 }
126 })
127 }
128}
129
130impl Object {
131 fn render_image(&self, cell_size: usize) -> svg::Node {
132 if let Object::Image(region, path) = self {
133 return svg::tag("image")
134 .coords(region.start.coords(cell_size))
135 .attr("width", region.width() * cell_size)
136 .attr("height", region.height() * cell_size)
137 .attr("href", path.clone())
138 .into();
139 }
140
141 panic!("Expected Image, got {:?}", self);
142 }
143
144 fn render_raw_svg(&self) -> svg::Node {
145 if let Object::RawSVG(svg) = self {
146 return svg::Node::SVG(svg.clone());
147 }
148
149 panic!("Expected RawSVG, got {:?}", self);
150 }
151
152 fn render_text(&self, cell_size: usize) -> svg::Node {
153 match self {
154 Object::Text(position, content, font_size)
155 | Object::CenteredText(position, content, font_size) => {
156 let centered = matches!(self, Object::CenteredText(..));
157
158 svg::tag("text")
159 .coords(
160 if centered {
161 position.as_centered()
162 } else {
163 position.as_corner()
164 }
165 .coords(cell_size),
166 )
167 .attr("font-size", format!("{}pt", font_size))
168 .attr("font-family", "Inconsolata")
169 .attr(
170 "dominant-baseline",
171 if centered { "middle" } else { "hanging" },
172 )
173 .attr(
174 "text-anchor",
175 if centered { "middle" } else { "start" },
176 )
177 .wrapping(vec![svg::Node::Text(content.to_string())])
178 .into()
179 }
180 _ => panic!("Expected Text, got {:?}", self),
181 }
182 }
183
184 // fn render_fitted_text(&self, cell_size: usize) -> svg::Node {
185 // if let Object::FittedText(region, content) = self {
186 // let (x, y) = region.start.coords(cell_size);
187 // let width = region.width() * cell_size as f32;
188 // let height = region.height() * cell_size as f32;
189
190 // return Box::new(
191 // svg::node::element::Text::new(content.clone())
192 // .set("x", x)
193 // .set("y", y)
194 // .set("")
195 // .set("font-size", format!("{}pt", 10.0))
196 // .set("font-family", "sans-serif"),
197 // );
198 // }
199
200 // panic!("Expected FittedText, got {:?}", self);
201 // }
202
203 fn render_rectangle(&self, cell_size: usize) -> svg::Node {
204 if let Object::Rectangle(start, end) = self {
205 return svg::tag("rect").region((start, end), cell_size).into();
206 }
207
208 panic!("Expected Rectangle, got {:?}", self);
209 }
210
211 fn render_polygon(&self, cell_size: usize) -> svg::Node {
212 if let Object::Polygon(start, lines) = self {
213 let mut path = svg::Path::new();
214 path.move_to(*start, cell_size);
215 for line in lines {
216 match line {
217 LineSegment::Straight(end)
218 | LineSegment::InwardCurve(end)
219 | LineSegment::OutwardCurve(end) => {
220 path.line_to(*end, cell_size);
221 }
222 };
223 }
224 path.close();
225 return path.node();
226 }
227
228 panic!("Expected Polygon, got {:?}", self);
229 }
230
231 fn render_line(&self, cell_size: usize) -> svg::Node {
232 if let Object::Line(start, end, width) = self {
233 return svg::tag("line")
234 .position_pair(*start, *end, cell_size)
235 .attr("stroke-width", *width)
236 .into();
237 }
238
239 panic!("Expected Line, got {:?}", self);
240 }
241
242 fn render_curve(&self, cell_size: usize) -> svg::Node {
243 if let Object::CurveOutward(start, end, stroke_width)
244 | Object::CurveInward(start, end, stroke_width) = self
245 {
246 let inward = matches!(self, Object::CurveInward(..));
247
248 let (start_x, start_y) = start.coords(cell_size);
249 let (end_x, end_y) = end.coords(cell_size);
250
251 let midpoint = ((start_x + end_x) / 2.0, (start_y + end_y) / 2.0);
252 let start_from_midpoint =
253 (start_x - midpoint.0, start_y - midpoint.1);
254 let end_from_midpoint = (end_x - midpoint.0, end_y - midpoint.1);
255
256 let control = {
257 let relative = (end_x - start_x, end_y - start_y);
258 if start_from_midpoint.0 * start_from_midpoint.1 > 0.0
259 && end_from_midpoint.0 * end_from_midpoint.1 > 0.0
260 {
261 if inward {
262 (
263 midpoint.0 + relative.0.abs() / 2.0,
264 midpoint.1 - relative.1.abs() / 2.0,
265 )
266 } else {
267 (
268 midpoint.0 - relative.0.abs() / 2.0,
269 midpoint.1 + relative.1.abs() / 2.0,
270 )
271 }
272 // diagonal line is going like this: /
273 } else if start_from_midpoint.0 * start_from_midpoint.1 < 0.0
274 && end_from_midpoint.0 * end_from_midpoint.1 < 0.0
275 {
276 if inward {
277 (
278 midpoint.0 - relative.0.abs() / 2.0,
279 midpoint.1 - relative.1.abs() / 2.0,
280 )
281 } else {
282 (
283 midpoint.0 + relative.0.abs() / 2.0,
284 midpoint.1 + relative.1.abs() / 2.0,
285 )
286 }
287 // line is horizontal
288 } else if start_y == end_y {
289 (
290 midpoint.0,
291 midpoint.1
292 + (if inward { -1.0 } else { 1.0 })
293 * relative.0.abs()
294 / 2.0,
295 )
296 // line is vertical
297 } else if start_x == end_x {
298 (
299 midpoint.0
300 + (if inward { -1.0 } else { 1.0 })
301 * relative.1.abs()
302 / 2.0,
303 midpoint.1,
304 )
305 } else {
306 unreachable!()
307 }
308 };
309
310 let mut path = svg::Path::new();
311 path.move_to(*start, cell_size);
312 path.quadratic_curve_to(control, *end, cell_size);
313 return path
314 .element()
315 .attr("stroke-width", format!("{stroke_width}"))
316 .into();
317 }
318
319 panic!("Expected Curve, got {:?}", self);
320 }
321
322 fn render_circle(
323 &self,
324 cell_size: usize,
325 object_sizes: ObjectSizes,
326 ) -> svg::Node {
327 let center = match self {
328 Object::BigDot(at) | Object::Dot(at) => at.coords(cell_size),
329 Object::BigCircle(at) | Object::SmallCircle(at) => {
330 at.as_centered().coords(cell_size)
331 }
332
333 _ => panic!(
334 "Expected BigDot, Dot, BigCircle or SmallCircle, got {:?}",
335 self
336 ),
337 };
338
339 let radius = match self {
340 Object::BigDot(_) => object_sizes.small_circle_radius,
341 Object::Dot(_) => object_sizes.dot_radius,
342 Object::BigCircle(_) => cell_size as f32 / 2.0,
343 Object::SmallCircle(_) => object_sizes.small_circle_radius,
344 _ => unreachable!(),
345 };
346
347 return svg::tag("circle")
348 .attr("cx", center.0)
349 .attr("cy", center.1)
350 .attr("r", radius)
351 .into();
352 }
353}