use crate::hbox::PureHorzBox;
use crate::length::Length;
pub type Point = (Length, Length);
pub type Dash = (Length, Length, Length);
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Color {
Gray(f64),
Rgb(f64, f64, f64),
Cmyk(f64, f64, f64, f64),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PathSeg {
Line(Point),
Bezier(Point, Point, Point),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Closing {
Open,
Line,
Bezier(Point, Point),
}
#[derive(Clone, Debug, PartialEq)]
pub struct Subpath {
pub start: Point,
pub segs: Vec<PathSeg>,
pub closing: Closing,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Path {
pub subpaths: Vec<Subpath>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PrePath {
pub start: Point,
pub segs: Vec<PathSeg>,
}
#[derive(Clone, Debug, PartialEq, syan::visit::Ast)]
#[subast(crate::graphics::GraphicsElem, crate::hbox::PureHorzBox)]
pub enum GraphicsElem {
Fill(Color, Path),
Stroke(Length, Color, Path),
DashedStroke(Length, Dash, Color, Path),
Text {
pt: Point,
contents: Vec<(Length, PureHorzBox)>,
width: Length,
height: Length,
depth: Length,
transform: Option<(f64, f64, f64, f64)>,
},
Group(Vec<GraphicsElem>),
Clip(Path, Vec<GraphicsElem>),
Destination { key: String, pt: Point },
}
fn shift_point(v: Point, pt: Point) -> Point {
(pt.0 + v.0, pt.1 + v.1)
}
fn linear_transform_point(mat: (f64, f64, f64, f64), pt: Point) -> Point {
let (a, b, c, d) = mat;
(pt.0 * a + pt.1 * b, pt.0 * c + pt.1 * d)
}
fn map_path(path: &Path, f: impl Fn(Point) -> Point) -> Path {
Path {
subpaths: path
.subpaths
.iter()
.map(|sub| Subpath {
start: f(sub.start),
segs: sub
.segs
.iter()
.map(|seg| match *seg {
PathSeg::Line(p) => PathSeg::Line(f(p)),
PathSeg::Bezier(c1, c2, p) => PathSeg::Bezier(f(c1), f(c2), f(p)),
})
.collect(),
closing: match sub.closing {
Closing::Open => Closing::Open,
Closing::Line => Closing::Line,
Closing::Bezier(c1, c2) => Closing::Bezier(f(c1), f(c2)),
},
})
.collect(),
}
}
pub fn shift_path(v: Point, path: &Path) -> Path {
map_path(path, |p| shift_point(v, p))
}
pub fn linear_transform_path(mat: (f64, f64, f64, f64), path: &Path) -> Path {
map_path(path, |p| linear_transform_point(mat, p))
}
#[derive(Clone, Debug, PartialEq)]
pub struct FrameDecoration {
pub width: crate::Length,
pub height: crate::Length,
pub pads: (crate::Length, crate::Length, crate::Length, crate::Length),
pub elems: Vec<GraphicsElem>,
}
pub fn shift_graphics(v: Point, elem: &GraphicsElem) -> GraphicsElem {
match elem {
GraphicsElem::Fill(c, p) => GraphicsElem::Fill(*c, shift_path(v, p)),
GraphicsElem::Stroke(w, c, p) => GraphicsElem::Stroke(*w, *c, shift_path(v, p)),
GraphicsElem::DashedStroke(w, d, c, p) => {
GraphicsElem::DashedStroke(*w, *d, *c, shift_path(v, p))
}
GraphicsElem::Text { pt, contents, width, height, depth, transform } => {
GraphicsElem::Text {
pt: shift_point(v, *pt),
contents: contents.clone(),
width: *width,
height: *height,
depth: *depth,
transform: *transform,
}
}
GraphicsElem::Group(gs) => {
GraphicsElem::Group(gs.iter().map(|g| shift_graphics(v, g)).collect())
}
GraphicsElem::Clip(path, gs) => GraphicsElem::Clip(
shift_path(v, path),
gs.iter().map(|g| shift_graphics(v, g)).collect(),
),
GraphicsElem::Destination { key, pt } => GraphicsElem::Destination {
key: key.clone(),
pt: shift_point(v, *pt),
},
}
}
pub fn linear_transform_graphics(mat: (f64, f64, f64, f64), elem: &GraphicsElem) -> GraphicsElem {
match elem {
GraphicsElem::Fill(c, p) => GraphicsElem::Fill(*c, linear_transform_path(mat, p)),
GraphicsElem::Stroke(w, c, p) => GraphicsElem::Stroke(*w, *c, linear_transform_path(mat, p)),
GraphicsElem::DashedStroke(w, d, c, p) => {
GraphicsElem::DashedStroke(*w, *d, *c, linear_transform_path(mat, p))
}
GraphicsElem::Text { pt, contents, width, height, depth, transform } => {
let (ma, mb, mc, md) = mat;
let (ta, tb, tc, td) = transform.unwrap_or((1.0, 0.0, 0.0, 1.0));
let composed = (
ma * ta + mb * tc,
ma * tb + mb * td,
mc * ta + md * tc,
mc * tb + md * td,
);
GraphicsElem::Text {
pt: linear_transform_point(mat, *pt),
contents: contents.clone(),
width: *width,
height: *height,
depth: *depth,
transform: Some(composed),
}
}
GraphicsElem::Group(gs) => GraphicsElem::Group(
gs.iter().map(|g| linear_transform_graphics(mat, g)).collect(),
),
GraphicsElem::Clip(path, gs) => GraphicsElem::Clip(
linear_transform_path(mat, path),
gs.iter().map(|g| linear_transform_graphics(mat, g)).collect(),
),
GraphicsElem::Destination { key, pt } => GraphicsElem::Destination {
key: key.clone(),
pt: linear_transform_point(mat, *pt),
},
}
}
fn bezier_axis_extent(r0: f64, r1: f64, r2: f64, r3: f64) -> (f64, f64) {
let a = -r0 + 3.0 * (r1 - r2) + r3;
let b = 2.0 * (r0 - 2.0 * r1 + r2);
let c = r1 - r0;
let bezier_point = |t: f64| -> f64 {
if t < 0.0 {
r0
} else if t > 1.0 {
r3
} else {
let u = 1.0 - t;
u * u * u * r0 + 3.0 * u * u * t * r1 + 3.0 * u * t * t * r2 + t * t * t * r3
}
};
let mut candidates = vec![r0, r3];
if a.abs() < 1e-12 {
if b.abs() > 1e-12 {
candidates.push(bezier_point(-c / b));
}
} else {
let disc = b * b - 4.0 * a * c;
if disc >= 0.0 {
let sq = disc.sqrt();
candidates.push(bezier_point((-b + sq) / (2.0 * a)));
candidates.push(bezier_point((-b - sq) / (2.0 * a)));
}
}
let min = candidates.iter().cloned().fold(f64::INFINITY, f64::min);
let max = candidates.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
(min, max)
}
pub fn path_bbox(path: &Path) -> (Point, Point) {
fn include(bounds: &mut (f64, f64, f64, f64), p: Point) {
bounds.0 = bounds.0.min(p.0 .0);
bounds.1 = bounds.1.max(p.0 .0);
bounds.2 = bounds.2.min(p.1 .0);
bounds.3 = bounds.3.max(p.1 .0);
}
fn include_axis_extents(bounds: &mut (f64, f64, f64, f64), ex: (f64, f64), ey: (f64, f64)) {
bounds.0 = bounds.0.min(ex.0);
bounds.1 = bounds.1.max(ex.1);
bounds.2 = bounds.2.min(ey.0);
bounds.3 = bounds.3.max(ey.1);
}
let mut bounds = (f64::INFINITY, f64::NEG_INFINITY, f64::INFINITY, f64::NEG_INFINITY);
for sub in &path.subpaths {
include(&mut bounds, sub.start);
let mut cur = sub.start;
for seg in &sub.segs {
match *seg {
PathSeg::Line(p) => {
include(&mut bounds, p);
cur = p;
}
PathSeg::Bezier(c1, c2, p) => {
let ex = bezier_axis_extent(cur.0 .0, c1.0 .0, c2.0 .0, p.0 .0);
let ey = bezier_axis_extent(cur.1 .0, c1.1 .0, c2.1 .0, p.1 .0);
include_axis_extents(&mut bounds, ex, ey);
cur = p;
}
}
}
if let Closing::Bezier(c1, c2) = sub.closing {
let ex = bezier_axis_extent(cur.0 .0, c1.0 .0, c2.0 .0, sub.start.0 .0);
let ey = bezier_axis_extent(cur.1 .0, c1.1 .0, c2.1 .0, sub.start.1 .0);
include_axis_extents(&mut bounds, ex, ey);
}
}
let (min_x, max_x, min_y, max_y) = bounds;
if min_x.is_infinite() {
return ((Length::ZERO, Length::ZERO), (Length::ZERO, Length::ZERO));
}
(
(Length(min_x), Length(min_y)),
(Length(max_x), Length(max_y)),
)
}
fn union_bbox((amin, amax): (Point, Point), (bmin, bmax): (Point, Point)) -> (Point, Point) {
(
(
Length(amin.0 .0.min(bmin.0 .0)),
Length(amin.1 .0.min(bmin.1 .0)),
),
(
Length(amax.0 .0.max(bmax.0 .0)),
Length(amax.1 .0.max(bmax.1 .0)),
),
)
}
pub fn graphics_bbox(elem: &GraphicsElem) -> Option<(Point, Point)> {
match elem {
GraphicsElem::Fill(_, p)
| GraphicsElem::Stroke(_, _, p)
| GraphicsElem::DashedStroke(_, _, _, p) => Some(path_bbox(p)),
GraphicsElem::Text { pt, width, height, depth, transform, .. } => {
match transform {
None => Some(((pt.0, pt.1 - *depth), (pt.0 + *width, pt.1 + *height))),
Some(mat) => {
let corners = [
(Length::ZERO, -*depth),
(*width, -*depth),
(*width, *height),
(Length::ZERO, *height),
];
let mut min = (f64::INFINITY, f64::INFINITY);
let mut max = (f64::NEG_INFINITY, f64::NEG_INFINITY);
for c in corners {
let t = linear_transform_point(*mat, c);
let (x, y) = (t.0 .0 + pt.0 .0, t.1 .0 + pt.1 .0);
min = (min.0.min(x), min.1.min(y));
max = (max.0.max(x), max.1.max(y));
}
Some((
(Length(min.0), Length(min.1)),
(Length(max.0), Length(max.1)),
))
}
}
}
GraphicsElem::Clip(path, _) => Some(path_bbox(path)),
GraphicsElem::Group(gs) => gs
.iter()
.filter_map(graphics_bbox)
.reduce(union_bbox),
GraphicsElem::Destination { .. } => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rect(x0: f64, y0: f64, x1: f64, y1: f64) -> Path {
Path {
subpaths: vec![Subpath {
start: (Length(x0), Length(y0)),
segs: vec![
PathSeg::Line((Length(x1), Length(y0))),
PathSeg::Line((Length(x1), Length(y1))),
PathSeg::Line((Length(x0), Length(y1))),
],
closing: Closing::Line,
}],
}
}
#[test]
fn shift_and_transform_recurse_into_clip_and_group() {
let fill = GraphicsElem::Fill(Color::Gray(0.0), rect(0.0, 0.0, 1.0, 1.0));
let group = GraphicsElem::Group(vec![fill.clone(), fill.clone()]);
let shifted_group = shift_graphics((Length(2.0), Length(3.0)), &group);
match &shifted_group {
GraphicsElem::Group(gs) => {
assert_eq!(gs.len(), 2);
for g in gs {
assert_eq!(
graphics_bbox(g),
Some(((Length(2.0), Length(3.0)), (Length(3.0), Length(4.0))))
);
}
}
other => panic!("expected Group, got {other:?}"),
}
let clip = GraphicsElem::Clip(rect(0.0, 0.0, 5.0, 5.0), vec![fill.clone()]);
let shifted_clip = shift_graphics((Length(1.0), Length(1.0)), &clip);
match &shifted_clip {
GraphicsElem::Clip(path, inner) => {
assert_eq!(
path_bbox(path),
((Length(1.0), Length(1.0)), (Length(6.0), Length(6.0)))
);
assert_eq!(
graphics_bbox(&inner[0]),
Some(((Length(1.0), Length(1.0)), (Length(2.0), Length(2.0))))
);
}
other => panic!("expected Clip, got {other:?}"),
}
let scaled_clip = linear_transform_graphics((2.0, 0.0, 0.0, 2.0), &clip);
match &scaled_clip {
GraphicsElem::Clip(path, inner) => {
assert_eq!(
path_bbox(path),
((Length(0.0), Length(0.0)), (Length(10.0), Length(10.0)))
);
assert_eq!(
graphics_bbox(&inner[0]),
Some(((Length(0.0), Length(0.0)), (Length(2.0), Length(2.0))))
);
}
other => panic!("expected Clip, got {other:?}"),
}
}
#[test]
fn bbox_option_semantics() {
assert_eq!(graphics_bbox(&GraphicsElem::Group(vec![])), None);
let a = GraphicsElem::Fill(Color::Gray(0.0), rect(0.0, 0.0, 1.0, 1.0));
let b = GraphicsElem::Fill(Color::Gray(0.0), rect(2.0, 2.0, 3.0, 3.0));
let group = GraphicsElem::Group(vec![a.clone(), b.clone()]);
assert_eq!(
graphics_bbox(&group),
Some(((Length(0.0), Length(0.0)), (Length(3.0), Length(3.0))))
);
let clip = GraphicsElem::Clip(rect(10.0, 10.0, 20.0, 20.0), vec![a]);
assert_eq!(
graphics_bbox(&clip),
Some(((Length(10.0), Length(10.0)), (Length(20.0), Length(20.0))))
);
}
}