use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use crate::xml::{Element, Ns};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ViewBox {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl ViewBox {
fn parse(text: &str) -> Option<Self> {
let numbers: Vec<f32> = text
.split_whitespace()
.filter_map(|n| n.parse().ok())
.collect();
let [x, y, width, height] = numbers[..] else {
return None;
};
(width > 0.0 && height > 0.0).then_some(Self {
x,
y,
width,
height,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SubPath {
pub points: Vec<(f32, f32)>,
pub closed: bool,
pub fill: bool,
pub stroke: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Geometry {
pub view: ViewBox,
pub paths: Vec<SubPath>,
}
const CURVE_SEGMENTS: usize = 16;
const DEGREES_PER_SEGMENT: f32 = 6.0;
impl Geometry {
pub fn read_path(path: &Element) -> Option<Self> {
let view = ViewBox::parse(path.attr(&Ns::Svg, "viewBox")?)?;
let data = path.attr(&Ns::Svg, "d")?;
let mut pen = Pen::new();
pen.svg(data);
Some(Self {
view,
paths: pen.finish(),
})
}
pub fn refit(&mut self) {
let points = || self.paths.iter().flat_map(|path| path.points.iter());
let Some(&(x, y)) = points().next() else {
return;
};
let (mut left, mut right, mut top, mut bottom) = (x, x, y, y);
for &(x, y) in points() {
left = left.min(x);
right = right.max(x);
top = top.min(y);
bottom = bottom.max(y);
}
self.view = ViewBox {
x: left,
y: top,
width: (right - left).max(f32::EPSILON),
height: (bottom - top).max(f32::EPSILON),
};
}
pub fn read(geometry: &Element) -> Option<Self> {
let view = ViewBox::parse(geometry.attr(&Ns::Svg, "viewBox")?)?;
let path = geometry.attr(&Ns::Draw, "enhanced-path")?;
let formulas = Formulas::new(geometry, view);
let mut pen = Pen::new();
pen.run(path, &formulas);
let mut paths = pen.finish();
let flip_x = geometry.attr(&Ns::Draw, "mirror-horizontal") == Some("true");
let flip_y = geometry.attr(&Ns::Draw, "mirror-vertical") == Some("true");
if flip_x || flip_y {
for path in &mut paths {
for (x, y) in &mut path.points {
if flip_x {
*x = view.x + view.width - (*x - view.x);
}
if flip_y {
*y = view.y + view.height - (*y - view.y);
}
}
}
}
Some(Self { view, paths })
}
}
struct Formulas<'a> {
by_name: HashMap<&'a str, &'a str>,
modifiers: Vec<f32>,
view: ViewBox,
known: RefCell<HashMap<String, f32>>,
depth: Cell<u32>,
}
const MAX_DEPTH: u32 = 64;
impl<'a> Formulas<'a> {
fn new(geometry: &'a Element, view: ViewBox) -> Self {
let by_name = geometry
.elements()
.filter(|e| e.is(&Ns::Draw, "equation"))
.filter_map(|e| Some((e.attr(&Ns::Draw, "name")?, e.attr(&Ns::Draw, "formula")?)))
.collect();
let modifiers = geometry
.attr(&Ns::Draw, "modifiers")
.unwrap_or_default()
.split_whitespace()
.filter_map(|n| n.parse().ok())
.collect();
Self {
by_name,
modifiers,
view,
known: RefCell::new(HashMap::new()),
depth: Cell::new(0),
}
}
fn named(&self, name: &str) -> f32 {
if let Some(value) = self.known.borrow().get(name) {
return *value;
}
if self.depth.get() >= MAX_DEPTH {
return 0.0;
}
let Some(text) = self.by_name.get(name) else {
return 0.0;
};
self.depth.set(self.depth.get() + 1);
let value = self.eval(text);
self.depth.set(self.depth.get() - 1);
self.known.borrow_mut().insert(name.to_owned(), value);
value
}
fn modifier(&self, index: usize) -> f32 {
self.modifiers.get(index).copied().unwrap_or(0.0)
}
fn constant(&self, name: &str) -> Option<f32> {
Some(match name {
"left" => self.view.x,
"top" => self.view.y,
"right" => self.view.x + self.view.width,
"bottom" => self.view.y + self.view.height,
"width" | "logwidth" => self.view.width,
"height" | "logheight" => self.view.height,
"pi" => std::f32::consts::PI,
"hasstroke" | "hasfill" => 1.0,
"xstretch" | "ystretch" => 0.0,
_ => return None,
})
}
fn eval(&self, text: &str) -> f32 {
Expression {
text: text.as_bytes(),
at: 0,
formulas: self,
}
.expression()
}
}
struct Expression<'a, 'f> {
text: &'a [u8],
at: usize,
formulas: &'a Formulas<'f>,
}
impl Expression<'_, '_> {
fn skip(&mut self) {
while self.at < self.text.len() && self.text[self.at].is_ascii_whitespace() {
self.at += 1;
}
}
fn peek(&mut self) -> Option<u8> {
self.skip();
self.text.get(self.at).copied()
}
fn take(&mut self, byte: u8) -> bool {
if self.peek() == Some(byte) {
self.at += 1;
return true;
}
false
}
fn expression(&mut self) -> f32 {
let mut value = self.term();
loop {
if self.take(b'+') {
value += self.term();
} else if self.take(b'-') {
value -= self.term();
} else {
return value;
}
}
}
fn term(&mut self) -> f32 {
let mut value = self.factor();
loop {
if self.take(b'*') {
value *= self.factor();
} else if self.take(b'/') {
let divisor = self.factor();
value = if divisor == 0.0 { 0.0 } else { value / divisor };
} else {
return value;
}
}
}
fn factor(&mut self) -> f32 {
if self.take(b'-') {
return -self.factor();
}
if self.take(b'+') {
return self.factor();
}
if self.take(b'(') {
let value = self.expression();
self.take(b')');
return value;
}
if self.take(b'?') {
let name = self.word();
return self.formulas.named(&name);
}
if self.take(b'$') {
let index = self.word().parse().unwrap_or(0);
return self.formulas.modifier(index);
}
match self.peek() {
Some(byte) if byte.is_ascii_alphabetic() => {
let name = self.word();
if self.take(b'(') {
let arguments = self.arguments();
return call(&name, &arguments);
}
self.formulas.constant(&name).unwrap_or(0.0)
}
_ => self.number(),
}
}
fn arguments(&mut self) -> Vec<f32> {
let mut arguments = Vec::new();
if self.take(b')') {
return arguments;
}
loop {
arguments.push(self.expression());
if !self.take(b',') {
self.take(b')');
return arguments;
}
}
}
fn word(&mut self) -> String {
self.skip();
let start = self.at;
while self
.text
.get(self.at)
.is_some_and(|b| b.is_ascii_alphanumeric() || *b == b'_')
{
self.at += 1;
}
String::from_utf8_lossy(&self.text[start..self.at]).into_owned()
}
fn number(&mut self) -> f32 {
self.skip();
let start = self.at;
while self
.text
.get(self.at)
.is_some_and(|b| b.is_ascii_digit() || *b == b'.')
{
self.at += 1;
}
if start == self.at {
self.at += 1;
return 0.0;
}
String::from_utf8_lossy(&self.text[start..self.at])
.parse()
.unwrap_or(0.0)
}
}
fn call(name: &str, arguments: &[f32]) -> f32 {
let argument = |n: usize| arguments.get(n).copied().unwrap_or(0.0);
match name {
"abs" => argument(0).abs(),
"sqrt" => argument(0).max(0.0).sqrt(),
"sin" => argument(0).sin(),
"cos" => argument(0).cos(),
"tan" => argument(0).tan(),
"atan" => argument(0).atan(),
"atan2" => argument(0).atan2(argument(1)),
"min" => argument(0).min(argument(1)),
"max" => argument(0).max(argument(1)),
"if" => {
if argument(0) > 0.0 {
argument(1)
} else {
argument(2)
}
}
_ => 0.0,
}
}
struct Pen {
done: Vec<SubPath>,
points: Vec<(f32, f32)>,
closed: bool,
fill: bool,
stroke: bool,
}
impl Pen {
fn new() -> Self {
Self {
done: Vec::new(),
points: Vec::new(),
closed: false,
fill: true,
stroke: true,
}
}
fn at(&self) -> (f32, f32) {
self.points.last().copied().unwrap_or((0.0, 0.0))
}
fn brk(&mut self) {
if self.points.len() >= 2 {
self.done.push(SubPath {
points: std::mem::take(&mut self.points),
closed: self.closed,
fill: self.fill,
stroke: self.stroke,
});
} else {
self.points.clear();
}
self.closed = false;
}
fn finish(mut self) -> Vec<SubPath> {
self.brk();
self.done
}
fn run(&mut self, path: &str, formulas: &Formulas<'_>) {
let mut tokens = Tokens {
text: path.as_bytes(),
at: 0,
formulas,
pushed: None,
};
let mut command = None;
loop {
match tokens.next() {
Some(Token::Command(letter)) => {
command = Some(letter);
self.command(letter, &mut tokens);
}
Some(Token::Number(first)) => match command {
Some(letter) => {
tokens.pushed = Some(first);
self.command(letter, &mut tokens);
}
None => return,
},
None => return,
}
}
}
fn command(&mut self, letter: char, tokens: &mut Tokens<'_, '_>) {
match letter {
'M' => {
let point = tokens.point();
self.brk();
self.points.push(point);
}
'L' => {
let point = tokens.point();
self.points.push(point);
}
'C' => {
let (a, b, end) = (tokens.point(), tokens.point(), tokens.point());
self.cubic(a, b, end);
}
'Q' => {
let (control, end) = (tokens.point(), tokens.point());
let from = self.at();
let third = |a: f32, b: f32| a + 2.0 / 3.0 * (b - a);
self.cubic(
(third(from.0, control.0), third(from.1, control.1)),
(third(end.0, control.0), third(end.1, control.1)),
end,
);
}
'Z' => {
self.closed = true;
self.brk();
}
'N' => self.brk(),
'F' => self.fill = false,
'S' => self.stroke = false,
'T' | 'U' => {
let (centre, radii) = (tokens.point(), tokens.point());
let (from, to) = (tokens.number(), tokens.number());
if letter == 'U' {
self.brk();
}
self.arc(centre, radii, from, to);
}
'X' | 'Y' => {
let to = tokens.point();
self.quadrant(to, letter == 'X');
}
'A' | 'B' | 'W' | 'V' => {
let (corner, opposite) = (tokens.point(), tokens.point());
let (from, to) = (tokens.point(), tokens.point());
if letter == 'B' || letter == 'V' {
self.brk();
}
self.box_arc(corner, opposite, from, to, letter == 'W' || letter == 'V');
}
_ => {}
}
}
fn svg_curve(
&mut self,
lower: u8,
scan: &mut Numbers,
offset: impl Fn((f32, f32)) -> (f32, f32),
reflected: Option<(f32, f32)>,
) -> Option<(f32, f32)> {
let here = self.at();
let (first, second, end) = match lower {
b'c' => {
let (a, b, e) = (scan.point()?, scan.point()?, scan.point()?);
(offset(a), offset(b), offset(e))
}
b's' => {
let (b, e) = (scan.point()?, scan.point()?);
(reflected.unwrap_or(here), offset(b), offset(e))
}
b'q' => {
let (c, e) = (scan.point()?, scan.point()?);
let (c, e) = (offset(c), offset(e));
(quadratic(here, c), quadratic(e, c), e)
}
_ => {
let e = offset(scan.point()?);
let c = reflected.unwrap_or(here);
(quadratic(here, c), quadratic(e, c), e)
}
};
self.cubic(first, second, end);
Some((2.0 * end.0 - second.0, 2.0 * end.1 - second.1))
}
fn svg(&mut self, data: &str) {
let mut scan = Numbers {
text: data.as_bytes(),
at: 0,
};
let mut command = b' ';
let mut reflected: Option<(f32, f32)> = None;
let mut start = (0.0, 0.0);
loop {
if let Some(letter) = scan.command() {
command = letter;
} else if scan.peek_number().is_none() {
return;
}
let lower = command.to_ascii_lowercase();
let relative = command.is_ascii_lowercase();
let here = self.at();
let offset = |point: (f32, f32)| {
if relative {
(here.0 + point.0, here.1 + point.1)
} else {
point
}
};
match lower {
b'm' => {
let Some(to) = scan.point() else { return };
let to = offset(to);
self.brk();
self.points.push(to);
start = to;
reflected = None;
command = if relative { b'l' } else { b'L' };
}
b'l' => {
let Some(to) = scan.point() else { return };
self.points.push(offset(to));
reflected = None;
}
b'h' => {
let Some(x) = scan.number() else { return };
let x = if relative { here.0 + x } else { x };
self.points.push((x, here.1));
reflected = None;
}
b'v' => {
let Some(y) = scan.number() else { return };
let y = if relative { here.1 + y } else { y };
self.points.push((here.0, y));
reflected = None;
}
b'c' | b's' | b'q' | b't' => {
let Some(next) = self.svg_curve(lower, &mut scan, offset, reflected) else {
return;
};
reflected = Some(next);
}
b'a' => {
for _ in 0..3 {
if scan.number().is_none() {
return;
}
}
let (Some(_), Some(_)) = (scan.number(), scan.number()) else {
return;
};
let Some(to) = scan.point() else { return };
self.points.push(offset(to));
reflected = None;
}
b'z' => {
self.closed = true;
self.brk();
self.points.push(start);
reflected = None;
}
_ => return,
}
}
}
fn cubic(&mut self, a: (f32, f32), b: (f32, f32), end: (f32, f32)) {
let from = self.at();
for step in 1..=CURVE_SEGMENTS {
#[allow(clippy::cast_precision_loss)]
let t = step as f32 / CURVE_SEGMENTS as f32;
let u = 1.0 - t;
let blend = |p0: f32, p1: f32, p2: f32, p3: f32| {
u * u * u * p0 + 3.0 * u * u * t * p1 + 3.0 * u * t * t * p2 + t * t * t * p3
};
self.points.push((
blend(from.0, a.0, b.0, end.0),
blend(from.1, a.1, b.1, end.1),
));
}
}
fn arc(&mut self, centre: (f32, f32), radii: (f32, f32), from: f32, to: f32) {
let mut sweep = to - from;
if sweep <= 0.0 {
sweep += 360.0;
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let steps = ((sweep / DEGREES_PER_SEGMENT).ceil() as usize).max(2);
for step in 0..=steps {
#[allow(clippy::cast_precision_loss)]
let angle = (from + sweep * step as f32 / steps as f32).to_radians();
self.points.push((
centre.0 + radii.0 * angle.cos(),
centre.1 + radii.1 * angle.sin(),
));
}
}
fn quadrant(&mut self, to: (f32, f32), x_first: bool) {
let from = self.at();
let centre = if x_first {
(to.0, from.1)
} else {
(from.0, to.1)
};
let radii = ((to.0 - from.0).abs(), (to.1 - from.1).abs());
if radii.0 == 0.0 || radii.1 == 0.0 {
self.points.push(to);
return;
}
let angle_of = |p: (f32, f32)| (p.1 - centre.1).atan2(p.0 - centre.0).to_degrees();
let (start, end) = (angle_of(from), angle_of(to));
let mut sweep = end - start;
while sweep > 180.0 {
sweep -= 360.0;
}
while sweep < -180.0 {
sweep += 360.0;
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let steps = ((sweep.abs() / DEGREES_PER_SEGMENT).ceil() as usize).max(2);
for step in 1..=steps {
#[allow(clippy::cast_precision_loss)]
let angle = (start + sweep * step as f32 / steps as f32).to_radians();
self.points.push((
centre.0 + radii.0 * angle.cos(),
centre.1 + radii.1 * angle.sin(),
));
}
}
fn box_arc(
&mut self,
corner: (f32, f32),
opposite: (f32, f32),
from: (f32, f32),
to: (f32, f32),
clockwise: bool,
) {
let centre = (
f32::midpoint(corner.0, opposite.0),
f32::midpoint(corner.1, opposite.1),
);
let radii = (
(opposite.0 - corner.0).abs() / 2.0,
(opposite.1 - corner.1).abs() / 2.0,
);
if radii.0 == 0.0 || radii.1 == 0.0 {
self.points.push(to);
return;
}
let angle_of = |p: (f32, f32)| {
((p.1 - centre.1) / radii.1)
.atan2((p.0 - centre.0) / radii.0)
.to_degrees()
};
let (start, end) = (angle_of(from), angle_of(to));
let sweep = if clockwise { start - end } else { end - start };
let sweep = if sweep <= 0.0 { sweep + 360.0 } else { sweep };
let (a, b) = if clockwise {
(start, start - sweep)
} else {
(start, start + sweep)
};
self.arc_between(centre, radii, a, b);
}
fn arc_between(&mut self, centre: (f32, f32), radii: (f32, f32), from: f32, to: f32) {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let steps = (((to - from).abs() / DEGREES_PER_SEGMENT).ceil() as usize).max(2);
for step in 0..=steps {
#[allow(clippy::cast_precision_loss)]
let angle = (from + (to - from) * step as f32 / steps as f32).to_radians();
self.points.push((
centre.0 + radii.0 * angle.cos(),
centre.1 + radii.1 * angle.sin(),
));
}
}
}
fn quadratic(end: (f32, f32), control: (f32, f32)) -> (f32, f32) {
(
end.0 + 2.0 / 3.0 * (control.0 - end.0),
end.1 + 2.0 / 3.0 * (control.1 - end.1),
)
}
struct Numbers<'a> {
text: &'a [u8],
at: usize,
}
impl Numbers<'_> {
fn skip(&mut self) {
while self
.text
.get(self.at)
.is_some_and(|b| b.is_ascii_whitespace() || *b == b',')
{
self.at += 1;
}
}
fn command(&mut self) -> Option<u8> {
self.skip();
let byte = *self.text.get(self.at)?;
if byte.is_ascii_alphabetic() && !matches!(byte, b'e' | b'E') {
self.at += 1;
return Some(byte);
}
None
}
fn peek_number(&mut self) -> Option<u8> {
self.skip();
self.text
.get(self.at)
.copied()
.filter(|b| b.is_ascii_digit() || matches!(b, b'-' | b'+' | b'.'))
}
fn number(&mut self) -> Option<f32> {
self.peek_number()?;
let start = self.at;
if matches!(self.text.get(self.at), Some(b'-' | b'+')) {
self.at += 1;
}
let mut seen_point = false;
while let Some(byte) = self.text.get(self.at) {
match byte {
b'0'..=b'9' => self.at += 1,
b'.' if !seen_point => {
seen_point = true;
self.at += 1;
}
b'e' | b'E' => {
self.at += 1;
if matches!(self.text.get(self.at), Some(b'-' | b'+')) {
self.at += 1;
}
}
_ => break,
}
}
String::from_utf8_lossy(&self.text[start..self.at])
.parse()
.ok()
}
fn point(&mut self) -> Option<(f32, f32)> {
Some((self.number()?, self.number()?))
}
}
enum Token {
Command(char),
Number(f32),
}
struct Tokens<'a, 'f> {
text: &'a [u8],
at: usize,
formulas: &'a Formulas<'f>,
pushed: Option<f32>,
}
impl Tokens<'_, '_> {
fn next(&mut self) -> Option<Token> {
if let Some(number) = self.pushed.take() {
return Some(Token::Number(number));
}
while self
.text
.get(self.at)
.is_some_and(|b| b.is_ascii_whitespace() || *b == b',')
{
self.at += 1;
}
let byte = *self.text.get(self.at)?;
if byte.is_ascii_alphabetic() {
self.at += 1;
return Some(Token::Command(char::from(byte)));
}
Some(Token::Number(self.value()))
}
fn value(&mut self) -> f32 {
let mut expression = Expression {
text: self.text,
at: self.at,
formulas: self.formulas,
};
let value = expression.factor();
self.at = expression.at;
value
}
fn number(&mut self) -> f32 {
match self.next() {
Some(Token::Number(number)) => number,
_ => 0.0,
}
}
fn point(&mut self) -> (f32, f32) {
(self.number(), self.number())
}
}