mod rectangle;
mod ellipse;
mod line;
mod arrow;
mod freehand;
mod text;
mod group;
mod image;
pub use rectangle::Rectangle;
pub use ellipse::Ellipse;
pub use line::Line;
pub use arrow::Arrow;
pub use freehand::Freehand;
pub use text::{Text, FontFamily, FontWeight};
pub use group::Group;
pub use image::{Image, ImageFormat};
use kurbo::{Affine, BezPath, Point, Rect};
use peniko::Color;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct SerializableColor {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
impl SerializableColor {
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
pub fn black() -> Self {
Self::new(0, 0, 0, 255)
}
pub fn white() -> Self {
Self::new(255, 255, 255, 255)
}
pub fn transparent() -> Self {
Self::new(0, 0, 0, 0)
}
}
impl From<Color> for SerializableColor {
fn from(color: Color) -> Self {
let rgba = color.to_rgba8();
Self {
r: rgba.r,
g: rgba.g,
b: rgba.b,
a: rgba.a,
}
}
}
impl From<SerializableColor> for Color {
fn from(color: SerializableColor) -> Self {
Color::from_rgba8(color.r, color.g, color.b, color.a)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum Sloppiness {
Architect = 0,
#[default]
Artist = 1,
Cartoonist = 2,
}
impl Sloppiness {
pub fn roughness(&self) -> f64 {
match self {
Sloppiness::Architect => 0.0,
Sloppiness::Artist => 1.0,
Sloppiness::Cartoonist => 2.0,
}
}
pub fn next(self) -> Self {
match self {
Sloppiness::Architect => Sloppiness::Artist,
Sloppiness::Artist => Sloppiness::Cartoonist,
Sloppiness::Cartoonist => Sloppiness::Architect,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShapeStyle {
pub stroke_color: SerializableColor,
pub stroke_width: f64,
pub fill_color: Option<SerializableColor>,
pub sloppiness: Sloppiness,
#[serde(default = "generate_seed")]
pub seed: u32,
}
fn generate_seed() -> u32 {
use std::sync::atomic::{AtomicU32, Ordering};
static SEED_COUNTER: AtomicU32 = AtomicU32::new(1);
let counter = SEED_COUNTER.fetch_add(1, Ordering::Relaxed);
let mut x = counter.wrapping_mul(0x9E3779B9);
x ^= x >> 16;
x = x.wrapping_mul(0x85EBCA6B);
x ^= x >> 13;
x = x.wrapping_mul(0xC2B2AE35);
x ^= x >> 16;
x
}
impl ShapeStyle {
pub fn stroke(&self) -> Color {
self.stroke_color.into()
}
pub fn fill(&self) -> Option<Color> {
self.fill_color.map(|c| c.into())
}
pub fn set_stroke(&mut self, color: Color) {
self.stroke_color = color.into();
}
pub fn set_fill(&mut self, color: Option<Color>) {
self.fill_color = color.map(|c| c.into());
}
}
impl Default for ShapeStyle {
fn default() -> Self {
Self {
stroke_color: SerializableColor::black(),
stroke_width: 2.0,
fill_color: None,
sloppiness: Sloppiness::default(),
seed: generate_seed(),
}
}
}
pub type ShapeId = Uuid;
pub trait ShapeTrait {
fn id(&self) -> ShapeId;
fn bounds(&self) -> Rect;
fn hit_test(&self, point: Point, tolerance: f64) -> bool;
fn to_path(&self) -> BezPath;
fn style(&self) -> &ShapeStyle;
fn style_mut(&mut self) -> &mut ShapeStyle;
fn transform(&mut self, affine: Affine);
fn clone_box(&self) -> Box<dyn ShapeTrait + Send + Sync>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Shape {
Rectangle(Rectangle),
Ellipse(Ellipse),
Line(Line),
Arrow(Arrow),
Freehand(Freehand),
Text(Text),
Group(Group),
Image(Image),
}
impl Shape {
pub fn id(&self) -> ShapeId {
match self {
Shape::Rectangle(s) => s.id(),
Shape::Ellipse(s) => s.id(),
Shape::Line(s) => s.id(),
Shape::Arrow(s) => s.id(),
Shape::Freehand(s) => s.id(),
Shape::Text(s) => s.id(),
Shape::Group(s) => s.id(),
Shape::Image(s) => s.id(),
}
}
pub fn bounds(&self) -> Rect {
match self {
Shape::Rectangle(s) => s.bounds(),
Shape::Ellipse(s) => s.bounds(),
Shape::Line(s) => s.bounds(),
Shape::Arrow(s) => s.bounds(),
Shape::Freehand(s) => s.bounds(),
Shape::Text(s) => s.bounds(),
Shape::Group(s) => s.bounds(),
Shape::Image(s) => s.bounds(),
}
}
pub fn hit_test(&self, point: Point, tolerance: f64) -> bool {
match self {
Shape::Rectangle(s) => s.hit_test(point, tolerance),
Shape::Ellipse(s) => s.hit_test(point, tolerance),
Shape::Line(s) => s.hit_test(point, tolerance),
Shape::Arrow(s) => s.hit_test(point, tolerance),
Shape::Freehand(s) => s.hit_test(point, tolerance),
Shape::Text(s) => s.hit_test(point, tolerance),
Shape::Group(s) => s.hit_test(point, tolerance),
Shape::Image(s) => s.hit_test(point, tolerance),
}
}
pub fn to_path(&self) -> BezPath {
match self {
Shape::Rectangle(s) => s.to_path(),
Shape::Ellipse(s) => s.to_path(),
Shape::Line(s) => s.to_path(),
Shape::Arrow(s) => s.to_path(),
Shape::Freehand(s) => s.to_path(),
Shape::Text(s) => s.to_path(),
Shape::Group(s) => s.to_path(),
Shape::Image(s) => s.to_path(),
}
}
pub fn style(&self) -> &ShapeStyle {
match self {
Shape::Rectangle(s) => s.style(),
Shape::Ellipse(s) => s.style(),
Shape::Line(s) => s.style(),
Shape::Arrow(s) => s.style(),
Shape::Freehand(s) => s.style(),
Shape::Text(s) => s.style(),
Shape::Group(s) => s.style(),
Shape::Image(s) => s.style(),
}
}
pub fn style_mut(&mut self) -> &mut ShapeStyle {
match self {
Shape::Rectangle(s) => s.style_mut(),
Shape::Ellipse(s) => s.style_mut(),
Shape::Line(s) => s.style_mut(),
Shape::Arrow(s) => s.style_mut(),
Shape::Freehand(s) => s.style_mut(),
Shape::Text(s) => s.style_mut(),
Shape::Group(s) => s.style_mut(),
Shape::Image(s) => s.style_mut(),
}
}
pub fn transform(&mut self, affine: Affine) {
match self {
Shape::Rectangle(s) => s.transform(affine),
Shape::Ellipse(s) => s.transform(affine),
Shape::Line(s) => s.transform(affine),
Shape::Arrow(s) => s.transform(affine),
Shape::Freehand(s) => s.transform(affine),
Shape::Text(s) => s.transform(affine),
Shape::Group(s) => s.transform(affine),
Shape::Image(s) => s.transform(affine),
}
}
pub fn is_group(&self) -> bool {
matches!(self, Shape::Group(_))
}
pub fn as_group(&self) -> Option<&Group> {
match self {
Shape::Group(g) => Some(g),
_ => None,
}
}
pub fn as_group_mut(&mut self) -> Option<&mut Group> {
match self {
Shape::Group(g) => Some(g),
_ => None,
}
}
pub fn regenerate_id(&mut self) {
let new_id = Uuid::new_v4();
match self {
Shape::Rectangle(s) => s.id = new_id,
Shape::Ellipse(s) => s.id = new_id,
Shape::Line(s) => s.id = new_id,
Shape::Arrow(s) => s.id = new_id,
Shape::Freehand(s) => s.id = new_id,
Shape::Text(s) => s.id = new_id,
Shape::Group(s) => s.id = new_id,
Shape::Image(s) => s.id = new_id,
}
}
pub fn is_image(&self) -> bool {
matches!(self, Shape::Image(_))
}
pub fn as_image(&self) -> Option<&Image> {
match self {
Shape::Image(img) => Some(img),
_ => None,
}
}
}