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