use super::palette::Color;
use super::typography::TextStyle;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Point {
pub x: f32,
pub y: f32,
}
impl Point {
pub const fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
pub fn distance(&self, other: &Point) -> f32 {
((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
}
pub fn midpoint(&self, other: &Point) -> Point {
Point::new(f32::midpoint(self.x, other.x), f32::midpoint(self.y, other.y))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Size {
pub width: f32,
pub height: f32,
}
impl Size {
pub const fn new(width: f32, height: f32) -> Self {
Self { width, height }
}
pub fn area(&self) -> f32 {
self.width * self.height
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Rect {
pub position: Point,
pub size: Size,
pub corner_radius: f32,
pub fill: Option<Color>,
pub stroke: Option<Color>,
pub stroke_width: f32,
}
impl Rect {
pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
Self {
position: Point::new(x, y),
size: Size::new(width, height),
corner_radius: 0.0,
fill: None,
stroke: None,
stroke_width: 1.0,
}
}
pub fn with_radius(mut self, radius: f32) -> Self {
self.corner_radius = radius;
self
}
pub fn with_fill(mut self, color: Color) -> Self {
self.fill = Some(color);
self
}
pub fn with_stroke(mut self, color: Color, width: f32) -> Self {
self.stroke = Some(color);
self.stroke_width = width;
self
}
pub fn center(&self) -> Point {
Point::new(
self.position.x + self.size.width / 2.0,
self.position.y + self.size.height / 2.0,
)
}
pub fn right(&self) -> f32 {
self.position.x + self.size.width
}
pub fn bottom(&self) -> f32 {
self.position.y + self.size.height
}
pub fn contains(&self, point: &Point) -> bool {
point.x >= self.position.x
&& point.x <= self.right()
&& point.y >= self.position.y
&& point.y <= self.bottom()
}
pub fn intersects(&self, other: &Rect) -> bool {
self.position.x < other.right()
&& self.right() > other.position.x
&& self.position.y < other.bottom()
&& self.bottom() > other.position.y
}
pub fn to_svg(&self) -> String {
let mut attrs = format!(
"x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\"",
self.position.x, self.position.y, self.size.width, self.size.height
);
if self.corner_radius > 0.0 {
attrs.push_str(&format!(" rx=\"{}\"", self.corner_radius));
}
if let Some(fill) = &self.fill {
attrs.push_str(&format!(" fill=\"{}\"", fill.to_css_hex()));
} else {
attrs.push_str(" fill=\"none\"");
}
if let Some(stroke) = &self.stroke {
attrs.push_str(&format!(
" stroke=\"{}\" stroke-width=\"{}\"",
stroke.to_css_hex(),
self.stroke_width
));
}
format!("<rect {}/>", attrs)
}
}
impl Default for Rect {
fn default() -> Self {
Self::new(0.0, 0.0, 100.0, 100.0)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Circle {
pub center: Point,
pub radius: f32,
pub fill: Option<Color>,
pub stroke: Option<Color>,
pub stroke_width: f32,
}
impl Circle {
pub fn new(cx: f32, cy: f32, r: f32) -> Self {
Self { center: Point::new(cx, cy), radius: r, fill: None, stroke: None, stroke_width: 1.0 }
}
pub fn with_fill(mut self, color: Color) -> Self {
self.fill = Some(color);
self
}
pub fn with_stroke(mut self, color: Color, width: f32) -> Self {
self.stroke = Some(color);
self.stroke_width = width;
self
}
pub fn bounds(&self) -> Rect {
Rect::new(
self.center.x - self.radius,
self.center.y - self.radius,
self.radius * 2.0,
self.radius * 2.0,
)
}
pub fn contains(&self, point: &Point) -> bool {
self.center.distance(point) <= self.radius
}
pub fn intersects(&self, other: &Circle) -> bool {
self.center.distance(&other.center) < self.radius + other.radius
}
pub fn to_svg(&self) -> String {
let mut attrs =
format!("cx=\"{}\" cy=\"{}\" r=\"{}\"", self.center.x, self.center.y, self.radius);
if let Some(fill) = &self.fill {
attrs.push_str(&format!(" fill=\"{}\"", fill.to_css_hex()));
} else {
attrs.push_str(" fill=\"none\"");
}
if let Some(stroke) = &self.stroke {
attrs.push_str(&format!(
" stroke=\"{}\" stroke-width=\"{}\"",
stroke.to_css_hex(),
self.stroke_width
));
}
format!("<circle {}/>", attrs)
}
}
impl Default for Circle {
fn default() -> Self {
Self::new(50.0, 50.0, 25.0)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Line {
pub start: Point,
pub end: Point,
pub stroke: Color,
pub stroke_width: f32,
pub dash_array: Option<String>,
}
impl Line {
pub fn new(x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
Self {
start: Point::new(x1, y1),
end: Point::new(x2, y2),
stroke: Color::rgb(0, 0, 0),
stroke_width: 1.0,
dash_array: None,
}
}
pub fn with_stroke(mut self, color: Color) -> Self {
self.stroke = color;
self
}
pub fn with_stroke_width(mut self, width: f32) -> Self {
self.stroke_width = width;
self
}
pub fn with_dash(mut self, pattern: &str) -> Self {
self.dash_array = Some(pattern.to_string());
self
}
pub fn length(&self) -> f32 {
self.start.distance(&self.end)
}
pub fn midpoint(&self) -> Point {
self.start.midpoint(&self.end)
}
pub fn to_svg(&self) -> String {
let mut attrs = format!(
"x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" stroke=\"{}\" stroke-width=\"{}\"",
self.start.x,
self.start.y,
self.end.x,
self.end.y,
self.stroke.to_css_hex(),
self.stroke_width
);
if let Some(dash) = &self.dash_array {
attrs.push_str(&format!(" stroke-dasharray=\"{}\"", dash));
}
format!("<line {}/>", attrs)
}
}
#[derive(Debug, Clone)]
pub enum PathCommand {
MoveTo(f32, f32),
LineTo(f32, f32),
HorizontalTo(f32),
VerticalTo(f32),
QuadraticTo { cx: f32, cy: f32, x: f32, y: f32 },
CubicTo { cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32 },
ArcTo { rx: f32, ry: f32, rotation: f32, large_arc: bool, sweep: bool, x: f32, y: f32 },
Close,
}
impl PathCommand {
pub fn to_svg(&self) -> String {
match self {
Self::MoveTo(x, y) => format!("M {} {}", x, y),
Self::LineTo(x, y) => format!("L {} {}", x, y),
Self::HorizontalTo(x) => format!("H {}", x),
Self::VerticalTo(y) => format!("V {}", y),
Self::QuadraticTo { cx, cy, x, y } => format!("Q {} {} {} {}", cx, cy, x, y),
Self::CubicTo { cx1, cy1, cx2, cy2, x, y } => {
format!("C {} {} {} {} {} {}", cx1, cy1, cx2, cy2, x, y)
}
Self::ArcTo { rx, ry, rotation, large_arc, sweep, x, y } => format!(
"A {} {} {} {} {} {} {}",
rx,
ry,
rotation,
i32::from(*large_arc),
i32::from(*sweep),
x,
y
),
Self::Close => "Z".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct Path {
pub commands: Vec<PathCommand>,
pub fill: Option<Color>,
pub stroke: Option<Color>,
pub stroke_width: f32,
}
impl Path {
pub fn new() -> Self {
Self { commands: Vec::new(), fill: None, stroke: None, stroke_width: 1.0 }
}
pub fn move_to(mut self, x: f32, y: f32) -> Self {
self.commands.push(PathCommand::MoveTo(x, y));
self
}
pub fn line_to(mut self, x: f32, y: f32) -> Self {
self.commands.push(PathCommand::LineTo(x, y));
self
}
pub fn quad_to(mut self, cx: f32, cy: f32, x: f32, y: f32) -> Self {
self.commands.push(PathCommand::QuadraticTo { cx, cy, x, y });
self
}
pub fn cubic_to(mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32) -> Self {
self.commands.push(PathCommand::CubicTo { cx1, cy1, cx2, cy2, x, y });
self
}
pub fn close(mut self) -> Self {
self.commands.push(PathCommand::Close);
self
}
pub fn with_fill(mut self, color: Color) -> Self {
self.fill = Some(color);
self
}
pub fn with_stroke(mut self, color: Color, width: f32) -> Self {
self.stroke = Some(color);
self.stroke_width = width;
self
}
pub fn to_path_data(&self) -> String {
self.commands.iter().map(|c| c.to_svg()).collect::<Vec<_>>().join(" ")
}
pub fn to_svg(&self) -> String {
let mut attrs = format!("d=\"{}\"", self.to_path_data());
if let Some(fill) = &self.fill {
attrs.push_str(&format!(" fill=\"{}\"", fill.to_css_hex()));
} else {
attrs.push_str(" fill=\"none\"");
}
if let Some(stroke) = &self.stroke {
attrs.push_str(&format!(
" stroke=\"{}\" stroke-width=\"{}\"",
stroke.to_css_hex(),
self.stroke_width
));
}
format!("<path {}/>", attrs)
}
}
impl Default for Path {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct Text {
pub position: Point,
pub content: String,
pub style: TextStyle,
}
impl Text {
pub fn new(x: f32, y: f32, content: &str) -> Self {
Self {
position: Point::new(x, y),
content: content.to_string(),
style: TextStyle::default(),
}
}
pub fn with_style(mut self, style: TextStyle) -> Self {
self.style = style;
self
}
pub fn to_svg(&self) -> String {
let style_attrs = self.style.to_svg_attrs();
format!(
"<text x=\"{}\" y=\"{}\" {}>{}</text>",
self.position.x,
self.position.y,
style_attrs,
html_escape(&self.content)
)
}
}
fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
#[derive(Debug, Clone)]
pub struct ArrowMarker {
pub id: String,
pub color: Color,
pub size: f32,
}
impl ArrowMarker {
pub fn new(id: &str, color: Color) -> Self {
Self { id: id.to_string(), color, size: 10.0 }
}
pub fn with_size(mut self, size: f32) -> Self {
self.size = size;
self
}
pub fn to_svg_def(&self) -> String {
format!(
r#"<marker id="{}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="{}" markerHeight="{}" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="{}"/>
</marker>"#,
self.id,
self.size,
self.size,
self.color.to_css_hex()
)
}
}
#[cfg(test)]
#[path = "shapes_tests.rs"]
mod tests;