1use super::palette::Color;
6use super::typography::TextStyle;
7
8#[derive(Debug, Clone, Copy, PartialEq, Default)]
10pub struct Point {
11 pub x: f32,
12 pub y: f32,
13}
14
15impl Point {
16 pub const fn new(x: f32, y: f32) -> Self {
17 Self { x, y }
18 }
19
20 pub fn distance(&self, other: &Point) -> f32 {
22 ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
23 }
24
25 pub fn midpoint(&self, other: &Point) -> Point {
27 Point::new(f32::midpoint(self.x, other.x), f32::midpoint(self.y, other.y))
28 }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Default)]
33pub struct Size {
34 pub width: f32,
35 pub height: f32,
36}
37
38impl Size {
39 pub const fn new(width: f32, height: f32) -> Self {
40 Self { width, height }
41 }
42
43 pub fn area(&self) -> f32 {
45 self.width * self.height
46 }
47}
48
49#[derive(Debug, Clone, PartialEq)]
51pub struct Rect {
52 pub position: Point,
54 pub size: Size,
56 pub corner_radius: f32,
58 pub fill: Option<Color>,
60 pub stroke: Option<Color>,
62 pub stroke_width: f32,
64}
65
66impl Rect {
67 pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
69 Self {
70 position: Point::new(x, y),
71 size: Size::new(width, height),
72 corner_radius: 0.0,
73 fill: None,
74 stroke: None,
75 stroke_width: 1.0,
76 }
77 }
78
79 pub fn with_radius(mut self, radius: f32) -> Self {
81 self.corner_radius = radius;
82 self
83 }
84
85 pub fn with_fill(mut self, color: Color) -> Self {
87 self.fill = Some(color);
88 self
89 }
90
91 pub fn with_stroke(mut self, color: Color, width: f32) -> Self {
93 self.stroke = Some(color);
94 self.stroke_width = width;
95 self
96 }
97
98 pub fn center(&self) -> Point {
100 Point::new(
101 self.position.x + self.size.width / 2.0,
102 self.position.y + self.size.height / 2.0,
103 )
104 }
105
106 pub fn right(&self) -> f32 {
108 self.position.x + self.size.width
109 }
110
111 pub fn bottom(&self) -> f32 {
113 self.position.y + self.size.height
114 }
115
116 pub fn contains(&self, point: &Point) -> bool {
118 point.x >= self.position.x
119 && point.x <= self.right()
120 && point.y >= self.position.y
121 && point.y <= self.bottom()
122 }
123
124 pub fn intersects(&self, other: &Rect) -> bool {
126 self.position.x < other.right()
127 && self.right() > other.position.x
128 && self.position.y < other.bottom()
129 && self.bottom() > other.position.y
130 }
131
132 pub fn to_svg(&self) -> String {
134 let mut attrs = format!(
135 "x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\"",
136 self.position.x, self.position.y, self.size.width, self.size.height
137 );
138
139 if self.corner_radius > 0.0 {
140 attrs.push_str(&format!(" rx=\"{}\"", self.corner_radius));
141 }
142
143 if let Some(fill) = &self.fill {
144 attrs.push_str(&format!(" fill=\"{}\"", fill.to_css_hex()));
145 } else {
146 attrs.push_str(" fill=\"none\"");
147 }
148
149 if let Some(stroke) = &self.stroke {
150 attrs.push_str(&format!(
151 " stroke=\"{}\" stroke-width=\"{}\"",
152 stroke.to_css_hex(),
153 self.stroke_width
154 ));
155 }
156
157 format!("<rect {}/>", attrs)
158 }
159}
160
161impl Default for Rect {
162 fn default() -> Self {
163 Self::new(0.0, 0.0, 100.0, 100.0)
164 }
165}
166
167#[derive(Debug, Clone, PartialEq)]
169pub struct Circle {
170 pub center: Point,
172 pub radius: f32,
174 pub fill: Option<Color>,
176 pub stroke: Option<Color>,
178 pub stroke_width: f32,
180}
181
182impl Circle {
183 pub fn new(cx: f32, cy: f32, r: f32) -> Self {
185 Self { center: Point::new(cx, cy), radius: r, fill: None, stroke: None, stroke_width: 1.0 }
186 }
187
188 pub fn with_fill(mut self, color: Color) -> Self {
190 self.fill = Some(color);
191 self
192 }
193
194 pub fn with_stroke(mut self, color: Color, width: f32) -> Self {
196 self.stroke = Some(color);
197 self.stroke_width = width;
198 self
199 }
200
201 pub fn bounds(&self) -> Rect {
203 Rect::new(
204 self.center.x - self.radius,
205 self.center.y - self.radius,
206 self.radius * 2.0,
207 self.radius * 2.0,
208 )
209 }
210
211 pub fn contains(&self, point: &Point) -> bool {
213 self.center.distance(point) <= self.radius
214 }
215
216 pub fn intersects(&self, other: &Circle) -> bool {
218 self.center.distance(&other.center) < self.radius + other.radius
219 }
220
221 pub fn to_svg(&self) -> String {
223 let mut attrs =
224 format!("cx=\"{}\" cy=\"{}\" r=\"{}\"", self.center.x, self.center.y, self.radius);
225
226 if let Some(fill) = &self.fill {
227 attrs.push_str(&format!(" fill=\"{}\"", fill.to_css_hex()));
228 } else {
229 attrs.push_str(" fill=\"none\"");
230 }
231
232 if let Some(stroke) = &self.stroke {
233 attrs.push_str(&format!(
234 " stroke=\"{}\" stroke-width=\"{}\"",
235 stroke.to_css_hex(),
236 self.stroke_width
237 ));
238 }
239
240 format!("<circle {}/>", attrs)
241 }
242}
243
244impl Default for Circle {
245 fn default() -> Self {
246 Self::new(50.0, 50.0, 25.0)
247 }
248}
249
250#[derive(Debug, Clone, PartialEq)]
252pub struct Line {
253 pub start: Point,
255 pub end: Point,
257 pub stroke: Color,
259 pub stroke_width: f32,
261 pub dash_array: Option<String>,
263}
264
265impl Line {
266 pub fn new(x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
268 Self {
269 start: Point::new(x1, y1),
270 end: Point::new(x2, y2),
271 stroke: Color::rgb(0, 0, 0),
272 stroke_width: 1.0,
273 dash_array: None,
274 }
275 }
276
277 pub fn with_stroke(mut self, color: Color) -> Self {
279 self.stroke = color;
280 self
281 }
282
283 pub fn with_stroke_width(mut self, width: f32) -> Self {
285 self.stroke_width = width;
286 self
287 }
288
289 pub fn with_dash(mut self, pattern: &str) -> Self {
291 self.dash_array = Some(pattern.to_string());
292 self
293 }
294
295 pub fn length(&self) -> f32 {
297 self.start.distance(&self.end)
298 }
299
300 pub fn midpoint(&self) -> Point {
302 self.start.midpoint(&self.end)
303 }
304
305 pub fn to_svg(&self) -> String {
307 let mut attrs = format!(
308 "x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" stroke=\"{}\" stroke-width=\"{}\"",
309 self.start.x,
310 self.start.y,
311 self.end.x,
312 self.end.y,
313 self.stroke.to_css_hex(),
314 self.stroke_width
315 );
316
317 if let Some(dash) = &self.dash_array {
318 attrs.push_str(&format!(" stroke-dasharray=\"{}\"", dash));
319 }
320
321 format!("<line {}/>", attrs)
322 }
323}
324
325#[derive(Debug, Clone)]
327pub enum PathCommand {
328 MoveTo(f32, f32),
330 LineTo(f32, f32),
332 HorizontalTo(f32),
334 VerticalTo(f32),
336 QuadraticTo { cx: f32, cy: f32, x: f32, y: f32 },
338 CubicTo { cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32 },
340 ArcTo { rx: f32, ry: f32, rotation: f32, large_arc: bool, sweep: bool, x: f32, y: f32 },
342 Close,
344}
345
346impl PathCommand {
347 pub fn to_svg(&self) -> String {
349 match self {
350 Self::MoveTo(x, y) => format!("M {} {}", x, y),
351 Self::LineTo(x, y) => format!("L {} {}", x, y),
352 Self::HorizontalTo(x) => format!("H {}", x),
353 Self::VerticalTo(y) => format!("V {}", y),
354 Self::QuadraticTo { cx, cy, x, y } => format!("Q {} {} {} {}", cx, cy, x, y),
355 Self::CubicTo { cx1, cy1, cx2, cy2, x, y } => {
356 format!("C {} {} {} {} {} {}", cx1, cy1, cx2, cy2, x, y)
357 }
358 Self::ArcTo { rx, ry, rotation, large_arc, sweep, x, y } => format!(
359 "A {} {} {} {} {} {} {}",
360 rx,
361 ry,
362 rotation,
363 i32::from(*large_arc),
364 i32::from(*sweep),
365 x,
366 y
367 ),
368 Self::Close => "Z".to_string(),
369 }
370 }
371}
372
373#[derive(Debug, Clone)]
375pub struct Path {
376 pub commands: Vec<PathCommand>,
378 pub fill: Option<Color>,
380 pub stroke: Option<Color>,
382 pub stroke_width: f32,
384}
385
386impl Path {
387 pub fn new() -> Self {
389 Self { commands: Vec::new(), fill: None, stroke: None, stroke_width: 1.0 }
390 }
391
392 pub fn move_to(mut self, x: f32, y: f32) -> Self {
394 self.commands.push(PathCommand::MoveTo(x, y));
395 self
396 }
397
398 pub fn line_to(mut self, x: f32, y: f32) -> Self {
400 self.commands.push(PathCommand::LineTo(x, y));
401 self
402 }
403
404 pub fn quad_to(mut self, cx: f32, cy: f32, x: f32, y: f32) -> Self {
406 self.commands.push(PathCommand::QuadraticTo { cx, cy, x, y });
407 self
408 }
409
410 pub fn cubic_to(mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32) -> Self {
412 self.commands.push(PathCommand::CubicTo { cx1, cy1, cx2, cy2, x, y });
413 self
414 }
415
416 pub fn close(mut self) -> Self {
418 self.commands.push(PathCommand::Close);
419 self
420 }
421
422 pub fn with_fill(mut self, color: Color) -> Self {
424 self.fill = Some(color);
425 self
426 }
427
428 pub fn with_stroke(mut self, color: Color, width: f32) -> Self {
430 self.stroke = Some(color);
431 self.stroke_width = width;
432 self
433 }
434
435 pub fn to_path_data(&self) -> String {
437 self.commands.iter().map(|c| c.to_svg()).collect::<Vec<_>>().join(" ")
438 }
439
440 pub fn to_svg(&self) -> String {
442 let mut attrs = format!("d=\"{}\"", self.to_path_data());
443
444 if let Some(fill) = &self.fill {
445 attrs.push_str(&format!(" fill=\"{}\"", fill.to_css_hex()));
446 } else {
447 attrs.push_str(" fill=\"none\"");
448 }
449
450 if let Some(stroke) = &self.stroke {
451 attrs.push_str(&format!(
452 " stroke=\"{}\" stroke-width=\"{}\"",
453 stroke.to_css_hex(),
454 self.stroke_width
455 ));
456 }
457
458 format!("<path {}/>", attrs)
459 }
460}
461
462impl Default for Path {
463 fn default() -> Self {
464 Self::new()
465 }
466}
467
468#[derive(Debug, Clone)]
470pub struct Text {
471 pub position: Point,
473 pub content: String,
475 pub style: TextStyle,
477}
478
479impl Text {
480 pub fn new(x: f32, y: f32, content: &str) -> Self {
482 Self {
483 position: Point::new(x, y),
484 content: content.to_string(),
485 style: TextStyle::default(),
486 }
487 }
488
489 pub fn with_style(mut self, style: TextStyle) -> Self {
491 self.style = style;
492 self
493 }
494
495 pub fn to_svg(&self) -> String {
497 let style_attrs = self.style.to_svg_attrs();
498 format!(
499 "<text x=\"{}\" y=\"{}\" {}>{}</text>",
500 self.position.x,
501 self.position.y,
502 style_attrs,
503 html_escape(&self.content)
504 )
505 }
506}
507
508fn html_escape(s: &str) -> String {
510 s.replace('&', "&")
511 .replace('<', "<")
512 .replace('>', ">")
513 .replace('"', """)
514 .replace('\'', "'")
515}
516
517#[derive(Debug, Clone)]
519pub struct ArrowMarker {
520 pub id: String,
522 pub color: Color,
524 pub size: f32,
526}
527
528impl ArrowMarker {
529 pub fn new(id: &str, color: Color) -> Self {
531 Self { id: id.to_string(), color, size: 10.0 }
532 }
533
534 pub fn with_size(mut self, size: f32) -> Self {
536 self.size = size;
537 self
538 }
539
540 pub fn to_svg_def(&self) -> String {
542 format!(
543 r#"<marker id="{}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="{}" markerHeight="{}" orient="auto-start-reverse">
544 <path d="M 0 0 L 10 5 L 0 10 z" fill="{}"/>
545</marker>"#,
546 self.id,
547 self.size,
548 self.size,
549 self.color.to_css_hex()
550 )
551 }
552}
553
554#[cfg(test)]
555#[path = "shapes_tests.rs"]
556mod tests;