This repository has no description
1use itertools::Itertools;
2use measure_time::debug_time;
3
4use crate::{
5 ColoredObject, Object, Point, graphics::objects::{LineSegment, ObjectSizes}
6};
7
8use super::{
9 CSSRenderable, SVGAttributesRenderable, renderable::SVGRenderable, svg,
10};
11
12impl SVGRenderable for ColoredObject {
13 fn render_to_svg(
14 &self,
15 colormap: crate::ColorMapping,
16 cell_size: usize,
17 object_sizes: crate::graphics::objects::ObjectSizes,
18 id: &str,
19 ) -> anyhow::Result<svg::Node> {
20 debug_time!("render_to_svg/colored_object");
21 let plain_obj = self.object.render_to_svg(
22 colormap.clone(),
23 cell_size,
24 object_sizes,
25 id,
26 )?;
27
28 let mut css = self
29 .fill
30 .render_to_css(&colormap.clone(), !self.object.fillable());
31
32 let object_svg = if !self.transformations.is_empty()
33 || !self.filters.is_empty()
34 {
35 // transform-box is not supported by resvg yet
36 // css += "transform-box: fill-box; transform-origin: 50% 50%;";
37
38 let (center_x, center_y) =
39 self.object.region().center_coords(cell_size);
40
41 css += &format!("transform-origin: {center_x}px {center_y}px;");
42
43 css += self
44 .filters
45 .iter()
46 .map(|f| f.render_to_css_filled(&colormap))
47 .join(" ")
48 .as_ref();
49
50 svg::tag("g")
51 .dataset("object", id)
52 .with_attributes(self.transformations.render_to_svg_attributes(
53 colormap,
54 cell_size,
55 object_sizes,
56 id,
57 )?)
58 .wrapping(vec![plain_obj])
59 .attr("style", &css)
60 .into()
61 } else {
62 match plain_obj {
63 svg::Node::Element(el) => el.attr("style", &css).into(),
64 _ => plain_obj,
65 }
66 };
67
68 if let Some(region) = &self.clip_to {
69 Ok(svg::tag("g")
70 .attr("clip-path", region.clip_path_id())
71 .child(object_svg)
72 .into())
73 } else {
74 Ok(object_svg)
75 }
76 }
77}
78
79impl SVGRenderable for Object {
80 fn render_to_svg(
81 &self,
82 _colormap: crate::ColorMapping,
83 cell_size: usize,
84 object_sizes: crate::graphics::objects::ObjectSizes,
85 id: &str,
86 ) -> anyhow::Result<svg::Node> {
87 debug_time!("render_to_svg/object");
88 let rendered = match self {
89 Object::Text(..) | Object::CenteredText(..) => {
90 self.render_text(cell_size)
91 }
92 Object::Rectangle(..) => self.render_rectangle(cell_size),
93 Object::Polygon(..) => self.render_polygon(cell_size),
94 Object::Line(..) => self.render_line(cell_size),
95 Object::CurveInward(..) | Object::CurveOutward(..) => {
96 self.render_curve(cell_size)
97 }
98 Object::BigDot(..)
99 | Object::Dot(..)
100 | Object::BigCircle(..)
101 | Object::SmallCircle(..) => {
102 self.render_circle(cell_size, object_sizes)
103 }
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_circle(
319 &self,
320 cell_size: usize,
321 object_sizes: ObjectSizes,
322 ) -> svg::Node {
323 let center = match self {
324 Object::BigDot(at) | Object::Dot(at) => at.coords(cell_size),
325 Object::BigCircle(at) | Object::SmallCircle(at) => {
326 at.center_coords(cell_size)
327 }
328
329 _ => panic!(
330 "Expected BigDot, Dot, BigCircle or SmallCircle, got {:?}",
331 self
332 ),
333 };
334
335 let radius = match self {
336 Object::BigDot(_) => object_sizes.small_circle_radius,
337 Object::Dot(_) => object_sizes.dot_radius,
338 Object::BigCircle(_) => cell_size as f32 / 2.0,
339 Object::SmallCircle(_) => object_sizes.small_circle_radius,
340 _ => unreachable!(),
341 };
342
343 return svg::tag("circle")
344 .attr("cx", center.0)
345 .attr("cy", center.1)
346 .attr("r", radius)
347 .into();
348 }
349}