use std::collections::HashMap;
use crate::geometry::Point;
use crate::path::Path;
use crate::scene::Font;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ShapeStyle {
Stroke,
Fill,
}
#[derive(Debug, Clone)]
pub struct Shape {
content: ShapeContent,
anchor: Point,
bbox: crate::geometry::Rect,
}
#[derive(Debug, Clone)]
enum ShapeContent {
Paths {
paths: Vec<Path>,
style: ShapeStyle,
},
Glyph {
font: Font,
glyph_id: u32,
em_bbox: crate::geometry::Rect,
em_origin: Point,
},
}
#[derive(Debug, Clone, Copy)]
pub enum ShapeKind<'a> {
Paths {
paths: &'a [Path],
style: ShapeStyle,
},
Glyph {
font: &'a Font,
glyph_id: u32,
em_bbox: crate::geometry::Rect,
em_origin: Point,
},
}
impl Shape {
pub fn new(paths: Vec<Path>, style: ShapeStyle, anchor: Point) -> Self {
let bbox = paths_bounding_box(&paths);
Self {
content: ShapeContent::Paths { paths, style },
anchor,
bbox,
}
}
pub fn glyph(
font: Font,
glyph_id: u32,
em_bbox: crate::geometry::Rect,
em_origin: Point,
anchor: Point,
) -> Self {
Self {
content: ShapeContent::Glyph {
font,
glyph_id,
em_bbox,
em_origin,
},
anchor,
bbox: em_bbox,
}
}
pub fn kind(&self) -> ShapeKind<'_> {
match &self.content {
ShapeContent::Paths { paths, style } => ShapeKind::Paths {
paths,
style: *style,
},
ShapeContent::Glyph {
font,
glyph_id,
em_bbox,
em_origin,
} => ShapeKind::Glyph {
font,
glyph_id: *glyph_id,
em_bbox: *em_bbox,
em_origin: *em_origin,
},
}
}
pub fn anchor(&self) -> Point {
self.anchor
}
pub fn bounding_box(&self) -> crate::geometry::Rect {
self.bbox
}
}
fn paths_bounding_box(paths: &[Path]) -> crate::geometry::Rect {
use crate::geometry::Shape as _;
let mut iter = paths.iter().map(|p| p.bounding_box());
match iter.next() {
None => crate::geometry::Rect::ZERO,
Some(first) => iter.fold(first, |acc, r| acc.union(r)),
}
}
#[derive(Debug, Default, Clone)]
pub struct ShapeRegistry {
shapes: HashMap<String, Shape>,
}
impl ShapeRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn with_builtins() -> Self {
let mut r = Self::new();
for &name in builtin::NAMES {
let s = builtin::lookup(name).expect("known built-in");
r.shapes.insert(name.to_string(), s);
}
r
}
pub fn shared_builtins() -> &'static ShapeRegistry {
static SHARED: std::sync::OnceLock<ShapeRegistry> = std::sync::OnceLock::new();
SHARED.get_or_init(ShapeRegistry::with_builtins)
}
pub fn insert(&mut self, name: impl Into<String>, shape: Shape) -> Option<Shape> {
self.shapes.insert(name.into(), shape)
}
pub fn get(&self, name: &str) -> Option<&Shape> {
self.shapes.get(name)
}
pub fn contains(&self, name: &str) -> bool {
self.shapes.contains_key(name)
}
pub fn remove(&mut self, name: &str) -> Option<Shape> {
self.shapes.remove(name)
}
pub fn names(&self) -> impl Iterator<Item = &str> + '_ {
self.shapes.keys().map(|s| s.as_str())
}
pub fn len(&self) -> usize {
self.shapes.len()
}
pub fn is_empty(&self) -> bool {
self.shapes.is_empty()
}
}
#[path = "shape_builtin.rs"]
pub mod builtin;
#[cfg(test)]
mod tests {
use super::*;
use crate::path::PathEl;
#[test]
fn with_builtins_has_all_names() {
let r = ShapeRegistry::with_builtins();
assert_eq!(r.len(), builtin::NAMES.len());
for name in builtin::NAMES {
assert!(r.get(name).is_some(), "missing {name}");
}
}
#[test]
fn fill_shapes_are_closed() {
let r = ShapeRegistry::with_builtins();
let fill_names = [
"circle",
"square",
"diamond",
"triangle-up",
"triangle-down",
"star",
"bowtie",
"square-cross",
"circle-plus",
"square-plus",
"arrow-closed",
"arrow-stealth",
"arrow-latex",
"arrow-thin",
"arrow-wedge",
"arrow-dot",
"arrow-square",
"arrow-diamond",
];
for name in fill_names {
let s = r.get(name).expect(name);
let ShapeKind::Paths { paths, style } = s.kind() else {
panic!("{name}: expected Paths variant");
};
assert_eq!(style, ShapeStyle::Fill, "{name}");
for sub in paths {
let last = sub.elements().last().expect("non-empty path");
assert!(
matches!(last, PathEl::ClosePath),
"{name} subpath not closed",
);
}
}
}
#[test]
fn stroke_shapes_are_open() {
let r = ShapeRegistry::with_builtins();
let stroke_names = [
"cross",
"plus",
"asterisk",
"hline",
"vline",
"arrow-open",
"arrow-fishtail",
"arrow-fork",
"arrow-feather",
"arrow-bar",
"arrow-bracket",
"arrow-cross",
];
for name in stroke_names {
let s = r.get(name).expect(name);
let ShapeKind::Paths { paths, style } = s.kind() else {
panic!("{name}: expected Paths variant");
};
assert_eq!(style, ShapeStyle::Stroke, "{name}");
for sub in paths {
let last = sub.elements().last().expect("non-empty path");
assert!(
!matches!(last, PathEl::ClosePath),
"{name} subpath unexpectedly closed",
);
}
}
}
#[test]
fn anchor_conventions() {
let r = ShapeRegistry::with_builtins();
let eps = 1e-9;
assert!((r.get("circle").unwrap().anchor().x - (-0.8)).abs() < eps);
assert!((r.get("square").unwrap().anchor().x - (-0.71)).abs() < eps);
assert!((r.get("diamond").unwrap().anchor().x - (-0.89)).abs() < eps);
assert_eq!(r.get("vline").unwrap().anchor(), Point::ORIGIN);
assert!((r.get("arrow-closed").unwrap().anchor().x - (-1.0)).abs() < eps);
assert!((r.get("arrow-stealth").unwrap().anchor().x - (-0.4)).abs() < eps);
let origin_names = [
"arrow-open",
"arrow-fishtail",
"arrow-fork",
"arrow-feather",
"arrow-bar",
"arrow-bracket",
"arrow-cross",
"arrow-dot",
"arrow-square",
"arrow-diamond",
];
for name in origin_names {
assert_eq!(r.get(name).unwrap().anchor(), Point::ORIGIN, "{name}");
}
}
#[test]
fn insert_remove_roundtrip() {
let mut r = ShapeRegistry::new();
assert!(r.is_empty());
assert!(r.insert("custom", builtin::circle()).is_none());
assert!(r.contains("custom"));
assert_eq!(r.len(), 1);
let prev = r.insert("custom", builtin::square()).expect("prev shape");
let ShapeKind::Paths { style, .. } = prev.kind() else {
panic!("expected Paths variant");
};
assert_eq!(style, ShapeStyle::Fill);
assert_eq!(r.len(), 1);
assert!(r.remove("custom").is_some());
assert!(r.is_empty());
assert!(!r.contains("custom"));
}
#[test]
fn paths_start_with_moveto() {
let r = ShapeRegistry::with_builtins();
for name in builtin::NAMES {
let s = r.get(name).expect(name);
let ShapeKind::Paths { paths, .. } = s.kind() else {
panic!("{name}: expected Paths variant");
};
for sub in paths {
let first = sub.elements().first().expect("non-empty path");
assert!(matches!(first, PathEl::MoveTo(_)), "{name} missing MoveTo",);
}
}
}
#[test]
fn glyph_shape_roundtrips_via_kind() {
let blob = crate::brush::Blob::new(std::sync::Arc::new(Vec::<u8>::new()));
let font = Font::new(blob, 0);
let em_bbox = crate::geometry::Rect::new(0.0, 0.0, 0.6, 1.0);
let em_origin = Point::new(0.05, 0.8);
let anchor = Point::new(-0.5, 0.0);
let s = Shape::glyph(font, 42, em_bbox, em_origin, anchor);
assert_eq!(s.anchor(), anchor);
assert_eq!(s.bounding_box(), em_bbox);
match s.kind() {
ShapeKind::Glyph {
glyph_id,
em_bbox: b,
em_origin: o,
..
} => {
assert_eq!(glyph_id, 42);
assert_eq!(b, em_bbox);
assert_eq!(o, em_origin);
}
_ => panic!("expected Glyph variant"),
}
}
#[test]
fn circle_bounding_box_has_expected_extent() {
let r = ShapeRegistry::with_builtins();
let circle = r.get("circle").expect("circle");
let bbox = circle.bounding_box();
assert!((bbox.width() - 1.6).abs() < 0.05);
assert!((bbox.height() - 1.6).abs() < 0.05);
}
#[test]
fn square_bounding_box_has_expected_extent() {
let r = ShapeRegistry::with_builtins();
let square = r.get("square").expect("square");
let bbox = square.bounding_box();
assert!((bbox.width() - 1.42).abs() < 1e-9);
assert!((bbox.height() - 1.42).abs() < 1e-9);
}
}