diff --git a/src/export/pdf.rs b/src/export/pdf.rs index f5401dfb8..7cd6fbfc7 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -16,9 +16,10 @@ use ttf_parser::{name_id, GlyphId, Tag}; use super::subset::subset; use crate::font::{find_name, FaceId, FontStore}; -use crate::frame::{rect_path, rect_paths, Element, Frame, Geometry, Group, Shape, Text}; +use crate::frame::{Element, Frame, Group, Text}; use crate::geom::{ - self, Color, Em, Length, Numeric, Paint, Point, Sides, Size, Stroke, Transform, + self, Color, Em, Geometry, Length, Numeric, Paint, Point, Shape, Size, Stroke, + Transform, }; use crate::image::{Image, ImageId, ImageStore, RasterImage}; use crate::Context; @@ -499,16 +500,16 @@ impl<'a> PageExporter<'a> { } fn write_shape(&mut self, x: f32, y: f32, shape: &Shape) { - if shape.fill.is_none() && shape.stroke.iter().all(Option::is_none) { + if shape.fill.is_none() && shape.stroke.is_none() { return; } match shape.geometry { - Geometry::Rect(size, radius) => { + Geometry::Rect(size) => { let w = size.x.to_f32(); let h = size.y.to_f32(); if w > 0.0 && h > 0.0 { - self.write_path(x, y, &rect_path(size, radius)); + self.content.rect(x, y, w, h); } } Geometry::Ellipse(size) => { @@ -530,37 +531,16 @@ impl<'a> PageExporter<'a> { self.set_fill(fill); } - // The stroke does not exist or is non-uniform. - let mut use_stroke = false; - if shape.stroke.is_uniform() || !matches!(shape.geometry, Geometry::Rect(_, _)) { - if let Some(stroke) = shape.stroke.top { - self.set_stroke(stroke); - use_stroke = true; - } + if let Some(stroke) = shape.stroke { + self.set_stroke(stroke); } - match (shape.fill, use_stroke) { - (None, false) => self.content.end_path(), - (Some(_), false) => self.content.fill_nonzero(), - (None, true) => self.content.stroke(), - (Some(_), true) => self.content.fill_nonzero_and_stroke(), + match (shape.fill, shape.stroke) { + (None, None) => unreachable!(), + (Some(_), None) => self.content.fill_nonzero(), + (None, Some(_)) => self.content.stroke(), + (Some(_), Some(_)) => self.content.fill_nonzero_and_stroke(), }; - - if let Geometry::Rect(size, radius) = shape.geometry { - if !use_stroke { - for (path, stroke) in rect_paths(size, radius, Some(shape.stroke)) { - if let Some(stroke) = stroke { - self.write_shape(x, y, &Shape { - geometry: Geometry::Path(path), - fill: None, - stroke: Sides::splat(Some(stroke)), - }); - } else { - continue; - } - } - } - } } fn write_path(&mut self, x: f32, y: f32, path: &geom::Path) { diff --git a/src/export/render.rs b/src/export/render.rs index 9c674acb9..50257e1c2 100644 --- a/src/export/render.rs +++ b/src/export/render.rs @@ -7,10 +7,11 @@ use tiny_skia as sk; use ttf_parser::{GlyphId, OutlineBuilder}; use usvg::FitTo; -use crate::frame::{Element, Frame, Geometry, Group, Shape, Text}; -use crate::geom::{self, Length, Paint, PathElement, Sides, Size, Stroke, Transform}; +use crate::frame::{Element, Frame, Group, Text}; +use crate::geom::{ + self, Geometry, Length, Paint, PathElement, Shape, Size, Stroke, Transform, +}; use crate::image::{Image, RasterImage, Svg}; -use crate::library::prelude::{rect_path, rect_paths}; use crate::Context; /// Export a frame into a rendered image. @@ -299,7 +300,12 @@ fn render_shape( shape: &Shape, ) -> Option<()> { let path = match shape.geometry { - Geometry::Rect(size, radius) => convert_path(&rect_path(size, radius))?, + Geometry::Rect(size) => { + let w = size.x.to_f32(); + let h = size.y.to_f32(); + let rect = sk::Rect::from_xywh(0.0, 0.0, w, h)?; + sk::PathBuilder::from_rect(rect) + } Geometry::Ellipse(size) => convert_path(&geom::Path::ellipse(size))?, Geometry::Line(target) => { let mut builder = sk::PathBuilder::new(); @@ -311,7 +317,7 @@ fn render_shape( if let Some(fill) = shape.fill { let mut paint: sk::Paint = fill.into(); - if matches!(shape.geometry, Geometry::Rect(_, _)) { + if matches!(shape.geometry, Geometry::Rect(_)) { paint.anti_alias = false; } @@ -319,27 +325,11 @@ fn render_shape( canvas.fill_path(&path, &paint, rule, ts, mask); } - if shape.stroke.is_uniform() || !matches!(shape.geometry, Geometry::Rect(_, _)) { - if let Some(Stroke { paint, thickness }) = shape.stroke.top { - let paint = paint.into(); - let mut stroke = sk::Stroke::default(); - stroke.width = thickness.to_f32(); - canvas.stroke_path(&path, &paint, &stroke, ts, mask); - } - } else { - if let Geometry::Rect(size, radius) = shape.geometry { - for (path, stroke) in rect_paths(size, radius, Some(shape.stroke)) { - if let Some(stroke) = stroke { - render_shape(canvas, ts, mask, &Shape { - geometry: Geometry::Path(path), - fill: None, - stroke: Sides::splat(Some(stroke)), - })?; - } else { - continue; - } - } - } + if let Some(Stroke { paint, thickness }) = shape.stroke { + let paint = paint.into(); + let mut stroke = sk::Stroke::default(); + stroke.width = thickness.to_f32(); + canvas.stroke_path(&path, &paint, &stroke, ts, mask); } Some(()) diff --git a/src/frame.rs b/src/frame.rs index 2cf584d84..dcaa7581e 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -1,13 +1,11 @@ //! Finished layouts. use std::fmt::{self, Debug, Formatter, Write}; -use std::mem; use std::sync::Arc; use crate::font::FaceId; use crate::geom::{ - Align, Angle, Em, Get, Length, Numeric, Paint, Path, Point, Side, Sides, Size, Spec, - Stroke, Transform, + Align, Em, Length, Numeric, Paint, Point, Shape, Size, Spec, Transform, }; use crate::image::ImageId; use crate::util::{EcoString, MaybeShared}; @@ -299,210 +297,3 @@ pub struct Glyph { /// The first character of the glyph's cluster. pub c: char, } - -/// A geometric shape with optional fill and stroke. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct Shape { - /// The shape's geometry. - pub geometry: Geometry, - /// The shape's background fill. - pub fill: Option, - /// The shape's border stroke. - pub stroke: Sides>, -} - -/// A shape's geometry. -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum Geometry { - /// A line to a point (relative to its position). - Line(Point), - /// A rectangle with its origin in the topleft corner and a border radius. - Rect(Size, Sides), - /// A ellipse with its origin in the topleft corner. - Ellipse(Size), - /// A bezier path. - Path(Path), -} - -impl Geometry { - /// Fill the geometry without a stroke. - pub fn filled(self, fill: Paint) -> Shape { - Shape { - geometry: self, - fill: Some(fill), - stroke: Sides::splat(None), - } - } - - /// Stroke the geometry without a fill. - pub fn stroked(self, stroke: Stroke) -> Shape { - Shape { - geometry: self, - fill: None, - stroke: Sides::splat(Some(stroke)), - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum Connection { - None, - Left, - Right, - Both, -} - -impl Connection { - pub fn advance(self, right: bool) -> Self { - match self { - Self::Right | Self::Both => { - if right { - Self::Both - } else { - Self::Left - } - } - Self::Left | Self::None => { - if right { - Self::Right - } else { - Self::None - } - } - } - } - - fn left(self) -> bool { - matches!(self, Self::Left | Self::Both) - } - - fn right(self) -> bool { - matches!(self, Self::Right | Self::Both) - } -} - -/// Draws one side of the rounded rectangle. Will always draw the left arc. The -/// right arc will be drawn halfway iff there is no connection. -fn draw_side( - path: &mut Path, - side: Side, - size: Size, - radius_left: Length, - radius_right: Length, - connection: Connection, -) { - let reversed = |angle: Angle, radius, rotate, mirror_x, mirror_y| { - let [a, b, c, d] = angle.bezier_arc(radius, rotate, mirror_x, mirror_y); - [d, c, b, a] - }; - - let angle_left = Angle::deg(if connection.left() { 90.0 } else { 45.0 }); - let angle_right = Angle::deg(if connection.right() { 90.0 } else { 45.0 }); - - let (arc1, arc2) = match side { - Side::Top => { - let arc1 = reversed(angle_left, radius_left, true, true, false) - .map(|x| x + Point::with_x(radius_left)); - let arc2 = (-angle_right) - .bezier_arc(radius_right, true, true, false) - .map(|x| x + Point::with_x(size.x - radius_right)); - - (arc1, arc2) - } - Side::Right => { - let arc1 = reversed(-angle_left, radius_left, false, false, false) - .map(|x| x + Point::new(size.x, radius_left)); - - let arc2 = angle_right - .bezier_arc(radius_right, false, false, false) - .map(|x| x + Point::new(size.x, size.y - radius_right)); - - (arc1, arc2) - } - Side::Bottom => { - let arc1 = reversed(-angle_left, radius_left, true, false, false) - .map(|x| x + Point::new(size.x - radius_left, size.y)); - - let arc2 = angle_right - .bezier_arc(radius_right, true, false, false) - .map(|x| x + Point::new(radius_right, size.y)); - - (arc1, arc2) - } - Side::Left => { - let arc1 = reversed(angle_left, radius_left, false, false, true) - .map(|x| x + Point::with_y(size.y - radius_left)); - - let arc2 = (-angle_right) - .bezier_arc(radius_right, false, false, true) - .map(|x| x + Point::with_y(radius_right)); - - (arc1, arc2) - } - }; - - if !connection.left() { - path.move_to(if radius_left.is_zero() { arc1[3] } else { arc1[0] }); - } - - if !radius_left.is_zero() { - path.cubic_to(arc1[1], arc1[2], arc1[3]); - } - - path.line_to(arc2[0]); - - if !connection.right() && !radius_right.is_zero() { - path.cubic_to(arc2[1], arc2[2], arc2[3]); - } -} - -pub fn rect_paths( - size: Size, - radius: Sides, - strokes: Option>>, -) -> Vec<(Path, Option)> { - let strokes = strokes.unwrap_or_else(|| Sides::splat(None)); - let mut res = vec![]; - - let mut connection = Connection::None; - let mut path = Path::new(); - let sides = [Side::Top, Side::Right, Side::Bottom, Side::Left]; - let mut always_continuous = true; - - let radius = [ - radius.left, - radius.top, - radius.right, - radius.bottom, - radius.left, - ]; - - for (side, radius) in sides.into_iter().zip(radius.windows(2)) { - let stroke_continuity = strokes.get(side) == strokes.get(side.next_cw()); - connection = connection.advance(stroke_continuity && side != Side::Left); - always_continuous &= stroke_continuity; - - draw_side(&mut path, side, size, radius[0], radius[1], connection); - - if !stroke_continuity { - res.push((mem::take(&mut path), strokes.get(side))); - } - } - - if always_continuous { - path.close_path(); - } - - if !path.0.is_empty() { - res.push((path, strokes.left)); - } - - res -} - -pub fn rect_path(size: Size, radius: Sides) -> Path { - let mut paths = rect_paths(size, radius, None); - assert_eq!(paths.len(), 1); - - paths.pop().unwrap().0 -} diff --git a/src/geom/mod.rs b/src/geom/mod.rs index a6f53c87e..bdd08fe5a 100644 --- a/src/geom/mod.rs +++ b/src/geom/mod.rs @@ -13,6 +13,7 @@ mod paint; mod path; mod point; mod ratio; +mod rect; mod relative; mod scalar; mod sides; @@ -30,6 +31,7 @@ pub use paint::*; pub use path::*; pub use point::*; pub use ratio::*; +pub use rect::*; pub use relative::*; pub use scalar::*; pub use sides::*; @@ -60,6 +62,50 @@ pub trait Get { } } +/// A geometric shape with optional fill and stroke. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Shape { + /// The shape's geometry. + pub geometry: Geometry, + /// The shape's background fill. + pub fill: Option, + /// The shape's border stroke. + pub stroke: Option, +} + +/// A shape's geometry. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Geometry { + /// A line to a point (relative to its position). + Line(Point), + /// A rectangle with its origin in the topleft corner. + Rect(Size), + /// A ellipse with its origin in the topleft corner. + Ellipse(Size), + /// A bezier path. + Path(Path), +} + +impl Geometry { + /// Fill the geometry without a stroke. + pub fn filled(self, fill: Paint) -> Shape { + Shape { + geometry: self, + fill: Some(fill), + stroke: None, + } + } + + /// Stroke the geometry without a fill. + pub fn stroked(self, stroke: Stroke) -> Shape { + Shape { + geometry: self, + fill: None, + stroke: Some(stroke), + } + } +} + /// A numeric type. pub trait Numeric: Sized diff --git a/src/geom/rect.rs b/src/geom/rect.rs new file mode 100644 index 000000000..839feda46 --- /dev/null +++ b/src/geom/rect.rs @@ -0,0 +1,212 @@ +use super::*; + +use std::mem; + +/// A rectangle with rounded corners. +#[derive(Debug, Clone)] +pub struct Rect { + size: Size, + radius: Sides, +} + +impl Rect { + /// Create a new rectangle. + pub fn new(size: Size, radius: Sides) -> Self { + Self { size, radius } + } + + /// Output all constituent shapes of the rectangle in order. The last one is + /// in the foreground. The function will output multiple items if the stroke + /// properties differ by side. + pub fn shapes( + &self, + fill: Option, + stroke: Sides>, + ) -> Vec { + let mut res = vec![]; + if fill.is_some() || (stroke.iter().any(Option::is_some) && stroke.is_uniform()) { + res.push(Shape { + geometry: self.fill_geometry(), + fill, + stroke: stroke.left, + }); + } + + if !stroke.is_uniform() { + for (path, stroke) in self.stroke_segments(Some(stroke)) { + if !stroke.is_some() { + continue; + } + res.push(Shape { + geometry: Geometry::Path(path), + fill: None, + stroke, + }); + } + } + + res + } + + /// Output the minimum number of paths along the rectangles border. + fn stroke_segments( + &self, + strokes: Option>>, + ) -> Vec<(Path, Option)> { + let strokes = strokes.unwrap_or_else(|| Sides::splat(None)); + let mut res = vec![]; + + let mut connection = Connection::None; + let mut path = Path::new(); + let mut always_continuous = true; + + for side in [Side::Top, Side::Right, Side::Bottom, Side::Left] { + let radius = [self.radius.get(side.next_ccw()), self.radius.get(side)]; + + let stroke_continuity = strokes.get(side) == strokes.get(side.next_cw()); + connection = connection.advance(stroke_continuity && side != Side::Left); + always_continuous &= stroke_continuity; + + draw_side(&mut path, side, self.size, radius[0], radius[1], connection); + + if !stroke_continuity { + res.push((mem::take(&mut path), strokes.get(side))); + } + } + + if always_continuous { + path.close_path(); + } + + if !path.0.is_empty() { + res.push((path, strokes.left)); + } + + res + } + + /// Output the shape of the rectangle as a path or primitive rectangle, + /// depending on whether it is rounded. + fn fill_geometry(&self) -> Geometry { + if self.radius.iter().copied().all(Length::is_zero) { + Geometry::Rect(self.size) + } else { + let mut paths = self.stroke_segments(None); + assert_eq!(paths.len(), 1); + + Geometry::Path(paths.pop().unwrap().0) + } + } +} + +/// Draws one side of the rounded rectangle. Will always draw the left arc. The +/// right arc will be drawn halfway iff there is no connection. +fn draw_side( + path: &mut Path, + side: Side, + size: Size, + radius_left: Length, + radius_right: Length, + connection: Connection, +) { + let reversed = |angle: Angle, radius, rotate, mirror_x, mirror_y| { + let [a, b, c, d] = angle.bezier_arc(radius, rotate, mirror_x, mirror_y); + [d, c, b, a] + }; + + let angle_left = Angle::deg(if connection.left() { 90.0 } else { 45.0 }); + let angle_right = Angle::deg(if connection.right() { 90.0 } else { 45.0 }); + + let (arc1, arc2) = match side { + Side::Top => { + let arc1 = reversed(angle_left, radius_left, true, true, false) + .map(|x| x + Point::with_x(radius_left)); + let arc2 = (-angle_right) + .bezier_arc(radius_right, true, true, false) + .map(|x| x + Point::with_x(size.x - radius_right)); + + (arc1, arc2) + } + Side::Right => { + let arc1 = reversed(-angle_left, radius_left, false, false, false) + .map(|x| x + Point::new(size.x, radius_left)); + + let arc2 = angle_right + .bezier_arc(radius_right, false, false, false) + .map(|x| x + Point::new(size.x, size.y - radius_right)); + + (arc1, arc2) + } + Side::Bottom => { + let arc1 = reversed(-angle_left, radius_left, true, false, false) + .map(|x| x + Point::new(size.x - radius_left, size.y)); + + let arc2 = angle_right + .bezier_arc(radius_right, true, false, false) + .map(|x| x + Point::new(radius_right, size.y)); + + (arc1, arc2) + } + Side::Left => { + let arc1 = reversed(angle_left, radius_left, false, false, true) + .map(|x| x + Point::with_y(size.y - radius_left)); + + let arc2 = (-angle_right) + .bezier_arc(radius_right, false, false, true) + .map(|x| x + Point::with_y(radius_right)); + + (arc1, arc2) + } + }; + + if !connection.left() { + path.move_to(if radius_left.is_zero() { arc1[3] } else { arc1[0] }); + } + + if !radius_left.is_zero() { + path.cubic_to(arc1[1], arc1[2], arc1[3]); + } + + path.line_to(arc2[0]); + + if !connection.right() && !radius_right.is_zero() { + path.cubic_to(arc2[1], arc2[2], arc2[3]); + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum Connection { + None, + Left, + Right, + Both, +} + +impl Connection { + pub fn advance(self, right: bool) -> Self { + match self { + Self::Right | Self::Both => { + if right { + Self::Both + } else { + Self::Left + } + } + Self::Left | Self::None => { + if right { + Self::Right + } else { + Self::None + } + } + } + } + + fn left(self) -> bool { + matches!(self, Self::Left | Self::Both) + } + + fn right(self) -> bool { + matches!(self, Self::Right | Self::Both) + } +} diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs index f7cda9bf9..e6fd2b7da 100644 --- a/src/library/graphics/shape.rs +++ b/src/library/graphics/shape.rs @@ -150,7 +150,7 @@ impl Layout for ShapeNode { // Add fill and/or stroke. let fill = styles.get(Self::FILL); - let mut stroke = match styles.get(Self::STROKE) { + let stroke = match styles.get(Self::STROKE) { Smart::Auto if fill.is_none() => Sides::splat(Some(Stroke::default())), Smart::Auto => Sides::splat(None), Smart::Custom(strokes) => { @@ -179,15 +179,23 @@ impl Layout for ShapeNode { bottom: radius.bottom.relative_to(size.y / 2.0), }; - if fill.is_some() || (stroke.iter().any(Option::is_some) && stroke.is_uniform()) { - let geometry = if is_round(S) { - Geometry::Ellipse(size) - } else { - Geometry::Rect(size, radius) - }; + let pos = Point::new(-outset.left, -outset.top); - let shape = Shape { geometry, fill, stroke }; - frame.prepend(Point::new(-outset.left, -outset.top), Element::Shape(shape)); + if fill.is_some() || stroke.iter().any(Option::is_some) { + if is_round(S) { + let shape = Shape { + geometry: Geometry::Ellipse(size), + fill, + stroke: stroke.left, + }; + frame.prepend(pos, Element::Shape(shape)); + } else { + for shape in + Rect::new(size, radius).shapes(fill, stroke).into_iter().rev() + { + frame.prepend(pos, Element::Shape(shape)); + } + } } // Apply link if it exists. diff --git a/src/model/layout.rs b/src/model/layout.rs index 63e8f0882..3b82ddc28 100644 --- a/src/model/layout.rs +++ b/src/model/layout.rs @@ -8,8 +8,10 @@ use std::sync::Arc; use super::{Barrier, NodeId, Resolve, StyleChain, StyleEntry}; use crate::diag::TypResult; use crate::eval::{RawAlign, RawLength}; -use crate::frame::{Element, Frame, Geometry}; -use crate::geom::{Align, Length, Paint, Point, Relative, Sides, Size, Spec, Stroke}; +use crate::frame::{Element, Frame}; +use crate::geom::{ + Align, Geometry, Length, Paint, Point, Relative, Sides, Size, Spec, Stroke, +}; use crate::library::graphics::MoveNode; use crate::library::layout::{AlignNode, PadNode}; use crate::util::Prehashed; @@ -353,8 +355,7 @@ impl Layout for FillNode { ) -> TypResult>> { let mut frames = self.child.layout(ctx, regions, styles)?; for frame in &mut frames { - let shape = Geometry::Rect(frame.size, Sides::splat(Length::zero())) - .filled(self.fill); + let shape = Geometry::Rect(frame.size).filled(self.fill); Arc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape)); } Ok(frames) @@ -379,8 +380,7 @@ impl Layout for StrokeNode { ) -> TypResult>> { let mut frames = self.child.layout(ctx, regions, styles)?; for frame in &mut frames { - let shape = Geometry::Rect(frame.size, Sides::splat(Length::zero())) - .stroked(self.stroke); + let shape = Geometry::Rect(frame.size).stroked(self.stroke); Arc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape)); } Ok(frames)