From 5f1499d380e223e7e1b2a8a96eb99e3ec95a56ac Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Sat, 30 Apr 2022 21:59:34 +0200 Subject: Add round corners and change arguments --- src/export/pdf.rs | 45 ++++++++--- src/export/render.rs | 38 +++++---- src/frame.rs | 177 ++++++++++++++++++++++++++++++++++++++++-- src/geom/angle.rs | 45 +++++++++++ src/geom/sides.rs | 46 +++++++++++ src/geom/transform.rs | 24 ++++++ src/library/graphics/shape.rs | 174 ++++++++++++++++++++++++++--------------- src/model/layout.rs | 6 +- src/model/styles.rs | 16 ++++ 9 files changed, 474 insertions(+), 97 deletions(-) (limited to 'src') diff --git a/src/export/pdf.rs b/src/export/pdf.rs index 067eb277..f5401dfb 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -16,9 +16,9 @@ use ttf_parser::{name_id, GlyphId, Tag}; use super::subset::subset; use crate::font::{find_name, FaceId, FontStore}; -use crate::frame::{Element, Frame, Geometry, Group, Shape, Text}; +use crate::frame::{rect_path, rect_paths, Element, Frame, Geometry, Group, Shape, Text}; use crate::geom::{ - self, Color, Em, Length, Numeric, Paint, Point, Size, Stroke, Transform, + self, Color, Em, Length, Numeric, Paint, Point, Sides, Size, Stroke, Transform, }; use crate::image::{Image, ImageId, ImageStore, RasterImage}; use crate::Context; @@ -499,16 +499,16 @@ impl<'a> PageExporter<'a> { } fn write_shape(&mut self, x: f32, y: f32, shape: &Shape) { - if shape.fill.is_none() && shape.stroke.is_none() { + if shape.fill.is_none() && shape.stroke.iter().all(Option::is_none) { return; } match shape.geometry { - Geometry::Rect(size) => { + Geometry::Rect(size, radius) => { let w = size.x.to_f32(); let h = size.y.to_f32(); if w > 0.0 && h > 0.0 { - self.content.rect(x, y, w, h); + self.write_path(x, y, &rect_path(size, radius)); } } Geometry::Ellipse(size) => { @@ -530,16 +530,37 @@ impl<'a> PageExporter<'a> { self.set_fill(fill); } - if let Some(stroke) = shape.stroke { - self.set_stroke(stroke); + // 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; + } } - 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(), + 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(), }; + + 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 c3b92d31..9c674acb 100644 --- a/src/export/render.rs +++ b/src/export/render.rs @@ -8,8 +8,9 @@ use ttf_parser::{GlyphId, OutlineBuilder}; use usvg::FitTo; use crate::frame::{Element, Frame, Geometry, Group, Shape, Text}; -use crate::geom::{self, Length, Paint, PathElement, Size, Stroke, Transform}; +use crate::geom::{self, Length, Paint, PathElement, Sides, 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. @@ -298,12 +299,7 @@ fn render_shape( shape: &Shape, ) -> Option<()> { let path = match shape.geometry { - 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::Rect(size, radius) => convert_path(&rect_path(size, radius))?, Geometry::Ellipse(size) => convert_path(&geom::Path::ellipse(size))?, Geometry::Line(target) => { let mut builder = sk::PathBuilder::new(); @@ -315,7 +311,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; } @@ -323,11 +319,27 @@ fn render_shape( canvas.fill_path(&path, &paint, rule, ts, mask); } - 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); + 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; + } + } + } } Some(()) diff --git a/src/frame.rs b/src/frame.rs index 5ee6e77e..f889601e 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -1,11 +1,13 @@ //! Finished layouts. use std::fmt::{self, Debug, Formatter, Write}; +use std::mem; use std::sync::Arc; use crate::font::FaceId; use crate::geom::{ - Align, Em, Length, Numeric, Paint, Path, Point, Size, Spec, Stroke, Transform, + Align, Angle, Em, Get, Length, Numeric, Paint, Path, Point, Side, Sides, Size, Spec, + Stroke, Transform, }; use crate::image::ImageId; use crate::util::{EcoString, MaybeShared}; @@ -306,7 +308,7 @@ pub struct Shape { /// The shape's background fill. pub fill: Option, /// The shape's border stroke. - pub stroke: Option, + pub stroke: Sides>, } /// A shape's geometry. @@ -314,8 +316,8 @@ pub struct Shape { 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 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. @@ -328,7 +330,7 @@ impl Geometry { Shape { geometry: self, fill: Some(fill), - stroke: None, + stroke: Sides::splat(None), } } @@ -337,7 +339,170 @@ impl Geometry { Shape { geometry: self, fill: None, - stroke: Some(stroke), + 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.clockwise()); + 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/angle.rs b/src/geom/angle.rs index 888442f7..65270ebd 100644 --- a/src/geom/angle.rs +++ b/src/geom/angle.rs @@ -64,6 +64,51 @@ impl Angle { pub fn cos(self) -> f64 { self.to_rad().cos() } + + /// Get the control points for a bezier curve that describes a circular arc + /// of this angle with the given radius. + pub fn bezier_arc( + self, + radius: Length, + rotate: bool, + mirror_x: bool, + mirror_y: bool, + ) -> [Point; 4] { + let end = Point::new(self.cos() * radius - radius, self.sin() * radius); + let center = Point::new(-radius, Length::zero()); + + let mut ts = if mirror_y { + Transform::mirror_y() + } else { + Transform::identity() + }; + + if mirror_x { + ts = ts.pre_concat(Transform::mirror_x()); + } + + if rotate { + ts = ts.pre_concat(Transform::rotate(Angle::deg(90.0))); + } + + let a = center * -1.0; + let b = end - center; + + let q1 = a.x.to_raw() * a.x.to_raw() + a.y.to_raw() * a.y.to_raw(); + let q2 = q1 + a.x.to_raw() * b.x.to_raw() + a.y.to_raw() * b.y.to_raw(); + let k2 = (4.0 / 3.0) * ((2.0 * q1 * q2).sqrt() - q2) + / (a.x.to_raw() * b.y.to_raw() - a.y.to_raw() * b.x.to_raw()); + + let control_1 = Point::new(center.x + a.x - k2 * a.y, center.y + a.y + k2 * a.x); + let control_2 = Point::new(center.x + b.x + k2 * b.y, center.y + b.y - k2 * b.x); + + [ + Point::zero(), + control_1.transform(ts), + control_2.transform(ts), + end.transform(ts), + ] + } } impl Numeric for Angle { diff --git a/src/geom/sides.rs b/src/geom/sides.rs index 3584a1ce..255c21ee 100644 --- a/src/geom/sides.rs +++ b/src/geom/sides.rs @@ -31,6 +31,32 @@ impl Sides { bottom: value, } } + + /// Maps the individual fields with `f`. + pub fn map(self, mut f: F) -> Sides + where + F: FnMut(T) -> U, + { + Sides { + left: f(self.left), + top: f(self.top), + right: f(self.right), + bottom: f(self.bottom), + } + } + + /// Returns an iterator over the sides. + pub fn iter(&self) -> impl Iterator { + [&self.left, &self.top, &self.right, &self.bottom].into_iter() + } + + /// Returns whether all sides are equal. + pub fn is_uniform(&self) -> bool + where + T: PartialEq, + { + self.left == self.top && self.top == self.right && self.right == self.bottom + } } impl Sides @@ -100,4 +126,24 @@ impl Side { Self::Bottom => Self::Top, } } + + /// The next side, clockwise. + pub fn clockwise(self) -> Self { + match self { + Self::Left => Self::Top, + Self::Top => Self::Right, + Self::Right => Self::Bottom, + Self::Bottom => Self::Left, + } + } + + /// The next side, counter-clockwise. + pub fn counter_clockwise(self) -> Self { + match self { + Self::Left => Self::Bottom, + Self::Top => Self::Left, + Self::Right => Self::Top, + Self::Bottom => Self::Right, + } + } } diff --git a/src/geom/transform.rs b/src/geom/transform.rs index 28a1af80..de2a9781 100644 --- a/src/geom/transform.rs +++ b/src/geom/transform.rs @@ -24,6 +24,30 @@ impl Transform { } } + /// Transform by mirroring along the x-axis. + pub fn mirror_x() -> Self { + Self { + sx: Ratio::one(), + ky: Ratio::zero(), + kx: Ratio::zero(), + sy: -Ratio::one(), + tx: Length::zero(), + ty: Length::zero(), + } + } + + /// Transform by mirroring along the y-axis. + pub fn mirror_y() -> Self { + Self { + sx: -Ratio::one(), + ky: Ratio::zero(), + kx: Ratio::zero(), + sy: Ratio::one(), + tx: Length::zero(), + ty: Length::zero(), + } + } + /// A translate transform. pub const fn translate(tx: Length, ty: Length) -> Self { Self { tx, ty, ..Self::identity() } diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs index 49c74c2f..7a1bfb1f 100644 --- a/src/library/graphics/shape.rs +++ b/src/library/graphics/shape.rs @@ -5,36 +5,46 @@ use crate::library::text::TextNode; /// Place a node into a sizable and fillable shape. #[derive(Debug, Hash)] -pub struct ShapeNode(pub Option); +pub struct AngularNode(pub Option); /// Place a node into a square. -pub type SquareNode = ShapeNode; +pub type SquareNode = AngularNode; /// Place a node into a rectangle. -pub type RectNode = ShapeNode; +pub type RectNode = AngularNode; -/// Place a node into a circle. -pub type CircleNode = ShapeNode; +// /// Place a node into a sizable and fillable shape. +// #[derive(Debug, Hash)] +// pub struct RoundNode(pub Option); -/// Place a node into an ellipse. -pub type EllipseNode = ShapeNode; +// /// Place a node into a circle. +// pub type CircleNode = RoundNode; + +// /// Place a node into an ellipse. +// pub type EllipseNode = RoundNode; #[node] -impl ShapeNode { +impl AngularNode { /// How to fill the shape. pub const FILL: Option = None; /// How to stroke the shape. #[property(resolve, fold)] - pub const STROKE: Smart> = Smart::Auto; + pub const STROKE: Smart>> = Smart::Auto; + /// How much to pad the shape's content. - pub const PADDING: Relative = Relative::zero(); + #[property(resolve, fold)] + pub const INSET: Sides>> = Sides::splat(Relative::zero()); + + /// How much to extend the shape's dimensions beyond the allocated space. + #[property(resolve, fold)] + pub const OUTSET: Sides>> = Sides::splat(Relative::zero()); + + /// How much to round the shape's corners. + #[property(resolve, fold)] + pub const RADIUS: Sides>> = Sides::splat(Relative::zero()); fn construct(_: &mut Context, args: &mut Args) -> TypResult { - let size = match S { - SQUARE => args.named::("size")?.map(Relative::from), - CIRCLE => args.named::("radius")?.map(|r| 2.0 * Relative::from(r)), - _ => None, - }; + let size = args.named::("size")?.map(Relative::from); let width = match size { None => args.named("width")?, @@ -52,7 +62,50 @@ impl ShapeNode { } } -impl Layout for ShapeNode { +castable! { + Sides>, + Expected: "stroke, dictionary with strokes for each side", + Value::None => { + Sides::splat(None) + }, + Value::Dict(values) => { + let get = |name: &str| values.get(name.into()).and_then(|v| v.clone().cast()).unwrap_or(None); + Sides { + top: get("top"), + right: get("right"), + bottom: get("bottom"), + left: get("left"), + } + }, + Value::Length(thickness) => Sides::splat(Some(RawStroke { + paint: Smart::Auto, + thickness: Smart::Custom(thickness), + })), + Value::Color(color) => Sides::splat(Some(RawStroke { + paint: Smart::Custom(color.into()), + thickness: Smart::Auto, + })), + @stroke: RawStroke => Sides::splat(Some(*stroke)), +} + +castable! { + Sides>>, + Expected: "length or dictionary of lengths for each side", + Value::None => Sides::splat(None), + Value::Dict(values) => { + let get = |name: &str| values.get(name.into()).and_then(|v| v.clone().cast()).unwrap_or(None); + Sides { + top: get("top"), + right: get("right"), + bottom: get("bottom"), + left: get("left"), + } + }, + Value::Length(l) => Sides::splat(Some(l.into())), + Value::Relative(r) => Sides::splat(Some(r)), +} + +impl Layout for AngularNode { fn layout( &self, ctx: &mut Context, @@ -61,50 +114,43 @@ impl Layout for ShapeNode { ) -> TypResult>> { let mut frames; if let Some(child) = &self.0 { - let mut padding = styles.get(Self::PADDING); - if is_round(S) { - padding.rel += Ratio::new(0.5 - SQRT_2 / 4.0); - } + let inset = styles.get(Self::INSET); // Pad the child. - let child = child.clone().padded(Sides::splat(padding)); + let child = child + .clone() + .padded(inset.map(|side| side.map(|abs| RawLength::from(abs)))); let mut pod = Regions::one(regions.first, regions.base, regions.expand); frames = child.layout(ctx, &pod, styles)?; // Relayout with full expansion into square region to make sure // the result is really a square or circle. - if is_quadratic(S) { - let length = if regions.expand.x || regions.expand.y { - let target = regions.expand.select(regions.first, Size::zero()); - target.x.max(target.y) - } else { - let size = frames[0].size; - let desired = size.x.max(size.y); - desired.min(regions.first.x).min(regions.first.y) - }; - - pod.first = Size::splat(length); - pod.expand = Spec::splat(true); - frames = child.layout(ctx, &pod, styles)?; - } + let length = if regions.expand.x || regions.expand.y { + let target = regions.expand.select(regions.first, Size::zero()); + target.x.max(target.y) + } else { + let size = frames[0].size; + let desired = size.x.max(size.y); + desired.min(regions.first.x).min(regions.first.y) + }; + + pod.first = Size::splat(length); + pod.expand = Spec::splat(true); + frames = child.layout(ctx, &pod, styles)?; } else { // The default size that a shape takes on if it has no child and // enough space. let mut size = Size::new(Length::pt(45.0), Length::pt(30.0)).min(regions.first); - if is_quadratic(S) { - let length = if regions.expand.x || regions.expand.y { - let target = regions.expand.select(regions.first, Size::zero()); - target.x.max(target.y) - } else { - size.x.min(size.y) - }; - size = Size::splat(length); + let length = if regions.expand.x || regions.expand.y { + let target = regions.expand.select(regions.first, Size::zero()); + target.x.max(target.y) } else { - size = regions.expand.select(regions.first, size); - } + size.x.min(size.y) + }; + size = Size::splat(length); frames = vec![Arc::new(Frame::new(size))]; } @@ -114,18 +160,28 @@ impl Layout for ShapeNode { // Add fill and/or stroke. let fill = styles.get(Self::FILL); let stroke = match styles.get(Self::STROKE) { - Smart::Auto => fill.is_none().then(Stroke::default), - Smart::Custom(stroke) => stroke.map(RawStroke::unwrap_or_default), + Smart::Auto if fill.is_none() => Sides::splat(Some(Stroke::default())), + Smart::Auto => Sides::splat(None), + Smart::Custom(strokes) => strokes.map(|s| Some(s.unwrap_or_default())), }; - if fill.is_some() || stroke.is_some() { - let geometry = if is_round(S) { - Geometry::Ellipse(frame.size) - } else { - Geometry::Rect(frame.size) - }; + let radius = { + let radius = styles.get(Self::RADIUS); - let shape = Shape { geometry, fill, stroke }; + Sides { + left: radius.left.relative_to(frame.size.x / 2.0), + top: radius.top.relative_to(frame.size.y / 2.0), + right: radius.right.relative_to(frame.size.x / 2.0), + bottom: radius.bottom.relative_to(frame.size.y / 2.0), + } + }; + + if fill.is_some() || stroke.iter().any(Option::is_some) { + let shape = Shape { + geometry: Geometry::Rect(frame.size, radius), + fill, + stroke, + }; frame.prepend(Point::zero(), Element::Shape(shape)); } @@ -152,13 +208,3 @@ const CIRCLE: ShapeKind = 2; /// A curve around two focal points. const ELLIPSE: ShapeKind = 3; - -/// Whether a shape kind is curvy. -fn is_round(kind: ShapeKind) -> bool { - matches!(kind, CIRCLE | ELLIPSE) -} - -/// Whether a shape kind has equal side length. -fn is_quadratic(kind: ShapeKind) -> bool { - matches!(kind, SQUARE | CIRCLE) -} diff --git a/src/model/layout.rs b/src/model/layout.rs index 51154286..63e8f088 100644 --- a/src/model/layout.rs +++ b/src/model/layout.rs @@ -353,7 +353,8 @@ 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).filled(self.fill); + let shape = Geometry::Rect(frame.size, Sides::splat(Length::zero())) + .filled(self.fill); Arc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape)); } Ok(frames) @@ -378,7 +379,8 @@ 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).stroked(self.stroke); + let shape = Geometry::Rect(frame.size, Sides::splat(Length::zero())) + .stroked(self.stroke); Arc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape)); } Ok(frames) diff --git a/src/model/styles.rs b/src/model/styles.rs index eb7a7053..2e752625 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -459,6 +459,22 @@ where } } +impl Fold for Sides> +where + T: Default, +{ + type Output = Sides; + + fn fold(self, outer: Self::Output) -> Self::Output { + Sides { + left: self.left.unwrap_or(outer.left), + right: self.right.unwrap_or(outer.right), + top: self.top.unwrap_or(outer.top), + bottom: self.bottom.unwrap_or(outer.bottom), + } + } +} + /// A scoped property barrier. /// /// Barriers interact with [scoped](StyleMap::scoped) styles: A scoped style -- cgit v1.2.3 From 84a4961a5dd03072b0e94c715957475d4ae21e4f Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Sun, 1 May 2022 13:21:07 +0200 Subject: Outset; fix folding --- src/frame.rs | 2 +- src/geom/sides.rs | 4 +- src/library/graphics/shape.rs | 90 ++++++++++++++++++++++++++++--------------- src/model/styles.rs | 23 ++++++++--- 4 files changed, 79 insertions(+), 40 deletions(-) (limited to 'src') diff --git a/src/frame.rs b/src/frame.rs index f889601e..2cf584d8 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -478,7 +478,7 @@ pub fn rect_paths( ]; for (side, radius) in sides.into_iter().zip(radius.windows(2)) { - let stroke_continuity = strokes.get(side) == strokes.get(side.clockwise()); + let stroke_continuity = strokes.get(side) == strokes.get(side.next_cw()); connection = connection.advance(stroke_continuity && side != Side::Left); always_continuous &= stroke_continuity; diff --git a/src/geom/sides.rs b/src/geom/sides.rs index 255c21ee..f214a1bf 100644 --- a/src/geom/sides.rs +++ b/src/geom/sides.rs @@ -128,7 +128,7 @@ impl Side { } /// The next side, clockwise. - pub fn clockwise(self) -> Self { + pub fn next_cw(self) -> Self { match self { Self::Left => Self::Top, Self::Top => Self::Right, @@ -138,7 +138,7 @@ impl Side { } /// The next side, counter-clockwise. - pub fn counter_clockwise(self) -> Self { + pub fn next_ccw(self) -> Self { match self { Self::Left => Self::Bottom, Self::Top => Self::Left, diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs index 7a1bfb1f..640c879b 100644 --- a/src/library/graphics/shape.rs +++ b/src/library/graphics/shape.rs @@ -102,6 +102,7 @@ castable! { } }, Value::Length(l) => Sides::splat(Some(l.into())), + Value::Ratio(r) => Sides::splat(Some(r.into())), Value::Relative(r) => Sides::splat(Some(r)), } @@ -117,40 +118,44 @@ impl Layout for AngularNode { let inset = styles.get(Self::INSET); // Pad the child. - let child = child - .clone() - .padded(inset.map(|side| side.map(|abs| RawLength::from(abs)))); + let child = child.clone().padded(inset.map(|side| side.map(RawLength::from))); let mut pod = Regions::one(regions.first, regions.base, regions.expand); frames = child.layout(ctx, &pod, styles)?; // Relayout with full expansion into square region to make sure // the result is really a square or circle. - let length = if regions.expand.x || regions.expand.y { - let target = regions.expand.select(regions.first, Size::zero()); - target.x.max(target.y) - } else { - let size = frames[0].size; - let desired = size.x.max(size.y); - desired.min(regions.first.x).min(regions.first.y) - }; - - pod.first = Size::splat(length); - pod.expand = Spec::splat(true); - frames = child.layout(ctx, &pod, styles)?; + if is_quadratic(S) { + let length = if regions.expand.x || regions.expand.y { + let target = regions.expand.select(regions.first, Size::zero()); + target.x.max(target.y) + } else { + let size = frames[0].size; + let desired = size.x.max(size.y); + desired.min(regions.first.x).min(regions.first.y) + }; + + pod.first = Size::splat(length); + pod.expand = Spec::splat(true); + frames = child.layout(ctx, &pod, styles)?; + } } else { // The default size that a shape takes on if it has no child and // enough space. let mut size = Size::new(Length::pt(45.0), Length::pt(30.0)).min(regions.first); - let length = if regions.expand.x || regions.expand.y { - let target = regions.expand.select(regions.first, Size::zero()); - target.x.max(target.y) + if is_quadratic(S) { + let length = if regions.expand.x || regions.expand.y { + let target = regions.expand.select(regions.first, Size::zero()); + target.x.max(target.y) + } else { + size.x.min(size.y) + }; + size = Size::splat(length); } else { - size.x.min(size.y) - }; - size = Size::splat(length); + size = regions.expand.select(regions.first, size); + } frames = vec![Arc::new(Frame::new(size))]; } @@ -162,27 +167,38 @@ impl Layout for AngularNode { 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) => strokes.map(|s| Some(s.unwrap_or_default())), + Smart::Custom(strokes) => strokes.map(|s| s.map(|s| s.unwrap_or_default())), }; - let radius = { - let radius = styles.get(Self::RADIUS); + let outset = styles.get(Self::OUTSET); + let outset = Sides { + left: outset.left.relative_to(frame.size.x), + top: outset.top.relative_to(frame.size.y), + right: outset.right.relative_to(frame.size.x), + bottom: outset.bottom.relative_to(frame.size.y), + }; - Sides { - left: radius.left.relative_to(frame.size.x / 2.0), - top: radius.top.relative_to(frame.size.y / 2.0), - right: radius.right.relative_to(frame.size.x / 2.0), - bottom: radius.bottom.relative_to(frame.size.y / 2.0), - } + let size = Spec::new( + frame.size.x + outset.left + outset.right, + frame.size.y + outset.top + outset.bottom, + ); + + let radius = styles.get(Self::RADIUS); + let radius = Sides { + left: radius.left.relative_to(size.x / 2.0), + top: radius.top.relative_to(size.y / 2.0), + right: radius.right.relative_to(size.x / 2.0), + bottom: radius.bottom.relative_to(size.y / 2.0), }; + if fill.is_some() || stroke.iter().any(Option::is_some) { let shape = Shape { - geometry: Geometry::Rect(frame.size, radius), + geometry: Geometry::Rect(size, radius), fill, stroke, }; - frame.prepend(Point::zero(), Element::Shape(shape)); + frame.prepend(Point::new(-outset.left, -outset.top), Element::Shape(shape)); } // Apply link if it exists. @@ -208,3 +224,13 @@ const CIRCLE: ShapeKind = 2; /// A curve around two focal points. const ELLIPSE: ShapeKind = 3; + +/// Whether a shape kind is curvy. +fn is_round(kind: ShapeKind) -> bool { + matches!(kind, CIRCLE | ELLIPSE) +} + +/// Whether a shape kind has equal side length. +fn is_quadratic(kind: ShapeKind) -> bool { + matches!(kind, SQUARE | CIRCLE) +} diff --git a/src/model/styles.rs b/src/model/styles.rs index 2e752625..00d1df0f 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use super::{Content, Show, ShowNode}; use crate::diag::{At, TypResult}; use crate::eval::{Args, Func, Node, Smart, Value}; -use crate::geom::{Numeric, Relative, Sides, Spec}; +use crate::geom::{Length, Numeric, Relative, Sides, Spec}; use crate::library::layout::PageNode; use crate::library::structure::{EnumNode, ListNode}; use crate::library::text::{FontFamily, ParNode, TextNode}; @@ -459,17 +459,30 @@ where } } -impl Fold for Sides> +impl Fold for Sides where - T: Default, + T: Fold, { - type Output = Sides; + type Output = Sides; + + fn fold(self, outer: Self::Output) -> Self::Output { + Sides { + left: self.left.fold(outer.left), + top: self.top.fold(outer.top), + right: self.right.fold(outer.right), + bottom: self.bottom.fold(outer.bottom), + } + } +} + +impl Fold for Sides>> { + type Output = Sides>; fn fold(self, outer: Self::Output) -> Self::Output { Sides { left: self.left.unwrap_or(outer.left), - right: self.right.unwrap_or(outer.right), top: self.top.unwrap_or(outer.top), + right: self.right.unwrap_or(outer.right), bottom: self.bottom.unwrap_or(outer.bottom), } } -- cgit v1.2.3 From 7b6f3a0ab9ae0dac19f62b62b9ecc96ea942a89e Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Mon, 2 May 2022 15:49:46 +0200 Subject: A new `Cast` implementation for `Sides` Reinstate circle --- src/eval/value.rs | 40 ++++++++++++++- src/library/graphics/shape.rs | 115 +++++++++++++++++++----------------------- 2 files changed, 91 insertions(+), 64 deletions(-) (limited to 'src') diff --git a/src/eval/value.rs b/src/eval/value.rs index 6ce815a4..c32614df 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use super::{ops, Args, Array, Dict, Func, RawLength}; use crate::diag::{with_alternative, StrResult}; use crate::geom::{ - Angle, Color, Dir, Em, Fraction, Length, Paint, Ratio, Relative, RgbaColor, + Angle, Color, Dir, Em, Fraction, Length, Paint, Ratio, Relative, RgbaColor, Sides, }; use crate::library::text::RawNode; use crate::model::{Content, Layout, LayoutNode}; @@ -596,6 +596,44 @@ impl Cast for Smart { } } +impl Cast for Sides { + fn is(value: &Value) -> bool { + matches!(value, Value::Dict(_)) || T::is(value) + } + + fn cast(value: Value) -> StrResult { + match value { + Value::Dict(dict) => { + for (key, _) in &dict { + if !matches!( + key.as_str(), + "left" | "top" | "right" | "bottom" | "x" | "y" | "rest" + ) { + return Err(format!("unexpected key {key:?}")); + } + } + + let sides = Sides { + left: dict.get("left".into()).or_else(|_| dict.get("x".into())), + top: dict.get("top".into()).or_else(|_| dict.get("y".into())), + right: dict.get("right".into()).or_else(|_| dict.get("x".into())), + bottom: dict.get("bottom".into()).or_else(|_| dict.get("y".into())), + } + .map(|side| { + side.or_else(|_| dict.get("rest".into())) + .and_then(|v| T::cast(v.clone())) + .unwrap_or_default() + }); + + Ok(sides) + } + v => T::cast(v) + .map(Sides::splat) + .map_err(|msg| with_alternative(msg, "dictionary")), + } + } +} + dynamic! { Dir: "direction", } diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs index 640c879b..f7cda9bf 100644 --- a/src/library/graphics/shape.rs +++ b/src/library/graphics/shape.rs @@ -5,26 +5,22 @@ use crate::library::text::TextNode; /// Place a node into a sizable and fillable shape. #[derive(Debug, Hash)] -pub struct AngularNode(pub Option); +pub struct ShapeNode(pub Option); /// Place a node into a square. -pub type SquareNode = AngularNode; +pub type SquareNode = ShapeNode; /// Place a node into a rectangle. -pub type RectNode = AngularNode; +pub type RectNode = ShapeNode; -// /// Place a node into a sizable and fillable shape. -// #[derive(Debug, Hash)] -// pub struct RoundNode(pub Option); +/// Place a node into a circle. +pub type CircleNode = ShapeNode; -// /// Place a node into a circle. -// pub type CircleNode = RoundNode; - -// /// Place a node into an ellipse. -// pub type EllipseNode = RoundNode; +/// Place a node into an ellipse. +pub type EllipseNode = ShapeNode; #[node] -impl AngularNode { +impl ShapeNode { /// How to fill the shape. pub const FILL: Option = None; /// How to stroke the shape. @@ -44,7 +40,11 @@ impl AngularNode { pub const RADIUS: Sides>> = Sides::splat(Relative::zero()); fn construct(_: &mut Context, args: &mut Args) -> TypResult { - let size = args.named::("size")?.map(Relative::from); + let size = match S { + SQUARE => args.named::("size")?.map(Relative::from), + CIRCLE => args.named::("radius")?.map(|r| 2.0 * Relative::from(r)), + _ => None, + }; let width = match size { None => args.named("width")?, @@ -60,53 +60,33 @@ impl AngularNode { Self(args.find()?).pack().sized(Spec::new(width, height)), )) } -} -castable! { - Sides>, - Expected: "stroke, dictionary with strokes for each side", - Value::None => { - Sides::splat(None) - }, - Value::Dict(values) => { - let get = |name: &str| values.get(name.into()).and_then(|v| v.clone().cast()).unwrap_or(None); - Sides { - top: get("top"), - right: get("right"), - bottom: get("bottom"), - left: get("left"), + fn set(args: &mut Args) -> TypResult { + let mut styles = StyleMap::new(); + styles.set_opt(Self::FILL, args.named("fill")?); + + if is_round(S) { + styles.set_opt( + Self::STROKE, + args.named::>>("stroke")? + .map(|some| some.map(Sides::splat)), + ); + } else { + styles.set_opt(Self::STROKE, args.named("stroke")?); } - }, - Value::Length(thickness) => Sides::splat(Some(RawStroke { - paint: Smart::Auto, - thickness: Smart::Custom(thickness), - })), - Value::Color(color) => Sides::splat(Some(RawStroke { - paint: Smart::Custom(color.into()), - thickness: Smart::Auto, - })), - @stroke: RawStroke => Sides::splat(Some(*stroke)), -} -castable! { - Sides>>, - Expected: "length or dictionary of lengths for each side", - Value::None => Sides::splat(None), - Value::Dict(values) => { - let get = |name: &str| values.get(name.into()).and_then(|v| v.clone().cast()).unwrap_or(None); - Sides { - top: get("top"), - right: get("right"), - bottom: get("bottom"), - left: get("left"), + styles.set_opt(Self::INSET, args.named("inset")?); + styles.set_opt(Self::OUTSET, args.named("outset")?); + + if S != CIRCLE { + styles.set_opt(Self::RADIUS, args.named("radius")?); } - }, - Value::Length(l) => Sides::splat(Some(l.into())), - Value::Ratio(r) => Sides::splat(Some(r.into())), - Value::Relative(r) => Sides::splat(Some(r)), + + Ok(styles) + } } -impl Layout for AngularNode { +impl Layout for ShapeNode { fn layout( &self, ctx: &mut Context, @@ -115,7 +95,13 @@ impl Layout for AngularNode { ) -> TypResult>> { let mut frames; if let Some(child) = &self.0 { - let inset = styles.get(Self::INSET); + let mut inset = styles.get(Self::INSET); + if is_round(S) { + inset = inset.map(|mut side| { + side.rel += Ratio::new(0.5 - SQRT_2 / 4.0); + side + }); + } // Pad the child. let child = child.clone().padded(inset.map(|side| side.map(RawLength::from))); @@ -164,10 +150,12 @@ impl Layout for AngularNode { // Add fill and/or stroke. let fill = styles.get(Self::FILL); - let stroke = match styles.get(Self::STROKE) { + let mut 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) => strokes.map(|s| s.map(|s| s.unwrap_or_default())), + Smart::Custom(strokes) => { + strokes.map(|s| s.map(RawStroke::unwrap_or_default)) + } }; let outset = styles.get(Self::OUTSET); @@ -191,13 +179,14 @@ impl Layout for AngularNode { bottom: radius.bottom.relative_to(size.y / 2.0), }; - - if fill.is_some() || stroke.iter().any(Option::is_some) { - let shape = Shape { - geometry: Geometry::Rect(size, radius), - fill, - stroke, + 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 shape = Shape { geometry, fill, stroke }; frame.prepend(Point::new(-outset.left, -outset.top), Element::Shape(shape)); } -- cgit v1.2.3 From f07395f9a47502c50f767f78a233d0e2a6e4445f Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Mon, 2 May 2022 16:51:14 +0200 Subject: Move rounding logic out of exporters --- src/export/pdf.rs | 46 +++------ src/export/render.rs | 42 ++++----- src/frame.rs | 211 +---------------------------------------- src/geom/mod.rs | 46 +++++++++ src/geom/rect.rs | 212 ++++++++++++++++++++++++++++++++++++++++++ src/library/graphics/shape.rs | 26 ++++-- src/model/layout.rs | 12 +-- 7 files changed, 311 insertions(+), 284 deletions(-) create mode 100644 src/geom/rect.rs (limited to 'src') diff --git a/src/export/pdf.rs b/src/export/pdf.rs index f5401dfb..7cd6fbfc 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 9c674acb..50257e1c 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 2cf584d8..dcaa7581 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 a6f53c87..bdd08fe5 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 00000000..839feda4 --- /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 f7cda9bf..e6fd2b7d 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 63e8f088..3b82ddc2 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) -- cgit v1.2.3 From 9b4397cdab25daff448cefb179a4699f64fa3d3f Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Mon, 2 May 2022 18:25:53 +0200 Subject: Tests for the new shape API --- src/eval/value.rs | 5 ++++- src/geom/rect.rs | 9 ++++++++- src/library/graphics/shape.rs | 10 +++------- 3 files changed, 15 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/eval/value.rs b/src/eval/value.rs index c32614df..352906aa 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -596,7 +596,10 @@ impl Cast for Smart { } } -impl Cast for Sides { +impl Cast for Sides +where + T: Cast + Default + Clone, +{ fn is(value: &Value) -> bool { matches!(value, Value::Dict(_)) || T::is(value) } diff --git a/src/geom/rect.rs b/src/geom/rect.rs index 839feda4..f0da2db6 100644 --- a/src/geom/rect.rs +++ b/src/geom/rect.rs @@ -28,7 +28,7 @@ impl Rect { res.push(Shape { geometry: self.fill_geometry(), fill, - stroke: stroke.left, + stroke: stroke.is_uniform().then(|| stroke.top).flatten(), }); } @@ -174,6 +174,8 @@ fn draw_side( } } +/// A state machine that indicates which sides of the border strokes in a 2D +/// polygon are connected to their neighboring sides. #[derive(Debug, Copy, Clone, PartialEq, Eq)] enum Connection { None, @@ -183,6 +185,9 @@ enum Connection { } impl Connection { + /// Advance to the next clockwise side of the polygon. The argument + /// indicates whether the border is connected on the right side of the next + /// edge. pub fn advance(self, right: bool) -> Self { match self { Self::Right | Self::Both => { @@ -202,10 +207,12 @@ impl Connection { } } + /// Whether there is a connection on the left. fn left(self) -> bool { matches!(self, Self::Left | Self::Both) } + /// Whether there is a connection on the right. 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 e6fd2b7d..a5523a2e 100644 --- a/src/library/graphics/shape.rs +++ b/src/library/graphics/shape.rs @@ -171,13 +171,9 @@ impl Layout for ShapeNode { frame.size.y + outset.top + outset.bottom, ); - let radius = styles.get(Self::RADIUS); - let radius = Sides { - left: radius.left.relative_to(size.x / 2.0), - top: radius.top.relative_to(size.y / 2.0), - right: radius.right.relative_to(size.x / 2.0), - bottom: radius.bottom.relative_to(size.y / 2.0), - }; + let radius = styles + .get(Self::RADIUS) + .map(|side| side.relative_to(size.x.min(size.y) / 2.0)); let pos = Point::new(-outset.left, -outset.top); -- cgit v1.2.3 From 33213abe7dfcb8d8065faadd2f5b72ec4b718af1 Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Mon, 2 May 2022 18:48:32 +0200 Subject: New page margins API --- src/library/layout/page.rs | 26 +++++--------------------- src/model/styles.rs | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/src/library/layout/page.rs b/src/library/layout/page.rs index 4307d2f9..801a137d 100644 --- a/src/library/layout/page.rs +++ b/src/library/layout/page.rs @@ -18,14 +18,9 @@ impl PageNode { /// Whether the page is flipped into landscape orientation. pub const FLIPPED: bool = false; - /// The left margin. - pub const LEFT: Smart> = Smart::Auto; - /// The right margin. - pub const RIGHT: Smart> = Smart::Auto; - /// The top margin. - pub const TOP: Smart> = Smart::Auto; - /// The bottom margin. - pub const BOTTOM: Smart> = Smart::Auto; + /// The page margin. + #[property(fold)] + pub const MARGINS: Sides>> = Sides::splat(Smart::Auto); /// How many columns the page has. pub const COLUMNS: NonZeroUsize = NonZeroUsize::new(1).unwrap(); @@ -54,13 +49,7 @@ impl PageNode { styles.set_opt(Self::WIDTH, args.named("width")?); styles.set_opt(Self::HEIGHT, args.named("height")?); - let all = args.named("margins")?; - let hor = args.named("horizontal")?; - let ver = args.named("vertical")?; - styles.set_opt(Self::LEFT, args.named("left")?.or(hor).or(all)); - styles.set_opt(Self::TOP, args.named("top")?.or(ver).or(all)); - styles.set_opt(Self::RIGHT, args.named("right")?.or(hor).or(all)); - styles.set_opt(Self::BOTTOM, args.named("bottom")?.or(ver).or(all)); + styles.set_opt(Self::MARGINS, args.named("margins")?); styles.set_opt(Self::FLIPPED, args.named("flipped")?); styles.set_opt(Self::FILL, args.named("fill")?); @@ -96,12 +85,7 @@ impl PageNode { // Determine the margins. let default = Relative::from(0.1190 * min); - let padding = Sides { - left: styles.get(Self::LEFT).unwrap_or(default), - right: styles.get(Self::RIGHT).unwrap_or(default), - top: styles.get(Self::TOP).unwrap_or(default), - bottom: styles.get(Self::BOTTOM).unwrap_or(default), - }; + let padding = styles.get(Self::MARGINS).map(|side| side.unwrap_or(default)); let mut child = self.0.clone(); diff --git a/src/model/styles.rs b/src/model/styles.rs index 00d1df0f..1fddfd0e 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use super::{Content, Show, ShowNode}; use crate::diag::{At, TypResult}; -use crate::eval::{Args, Func, Node, Smart, Value}; +use crate::eval::{Args, Func, Node, RawLength, Smart, Value}; use crate::geom::{Length, Numeric, Relative, Sides, Spec}; use crate::library::layout::PageNode; use crate::library::structure::{EnumNode, ListNode}; @@ -488,6 +488,19 @@ impl Fold for Sides>> { } } +impl Fold for Sides>> { + type Output = Sides>>; + + fn fold(self, outer: Self::Output) -> Self::Output { + Sides { + left: self.left.or(outer.left), + top: self.top.or(outer.top), + right: self.right.or(outer.right), + bottom: self.bottom.or(outer.bottom), + } + } +} + /// A scoped property barrier. /// /// Barriers interact with [scoped](StyleMap::scoped) styles: A scoped style -- cgit v1.2.3 From 6a8a0ec6ec8bb8cf346ee0dd2c45ddcfbee7fbe6 Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Tue, 3 May 2022 11:40:27 +0200 Subject: Code Review: Heap is Stack. Unsafe is Good. Spaghetti Code is Style. --- src/frame.rs | 8 +++ src/geom/angle.rs | 45 -------------- src/geom/path.rs | 45 ++++++++++++++ src/geom/rect.rs | 138 +++++++++++++++++------------------------- src/geom/sides.rs | 13 ++++ src/geom/transform.rs | 18 +----- src/library/graphics/shape.rs | 32 +++------- src/library/layout/page.rs | 5 +- src/model/styles.rs | 23 ++----- 9 files changed, 139 insertions(+), 188 deletions(-) (limited to 'src') diff --git a/src/frame.rs b/src/frame.rs index dcaa7581..80e25f3b 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -40,6 +40,14 @@ impl Frame { self.elements.insert(0, (pos, element)); } + /// Add multiple elements at a position in the background. + pub fn prepend_multiple(&mut self, insert: I) + where + I: IntoIterator, + { + self.elements.splice(0 .. 0, insert); + } + /// Add an element at a position in the foreground. pub fn push(&mut self, pos: Point, element: Element) { self.elements.push((pos, element)); diff --git a/src/geom/angle.rs b/src/geom/angle.rs index 65270ebd..888442f7 100644 --- a/src/geom/angle.rs +++ b/src/geom/angle.rs @@ -64,51 +64,6 @@ impl Angle { pub fn cos(self) -> f64 { self.to_rad().cos() } - - /// Get the control points for a bezier curve that describes a circular arc - /// of this angle with the given radius. - pub fn bezier_arc( - self, - radius: Length, - rotate: bool, - mirror_x: bool, - mirror_y: bool, - ) -> [Point; 4] { - let end = Point::new(self.cos() * radius - radius, self.sin() * radius); - let center = Point::new(-radius, Length::zero()); - - let mut ts = if mirror_y { - Transform::mirror_y() - } else { - Transform::identity() - }; - - if mirror_x { - ts = ts.pre_concat(Transform::mirror_x()); - } - - if rotate { - ts = ts.pre_concat(Transform::rotate(Angle::deg(90.0))); - } - - let a = center * -1.0; - let b = end - center; - - let q1 = a.x.to_raw() * a.x.to_raw() + a.y.to_raw() * a.y.to_raw(); - let q2 = q1 + a.x.to_raw() * b.x.to_raw() + a.y.to_raw() * b.y.to_raw(); - let k2 = (4.0 / 3.0) * ((2.0 * q1 * q2).sqrt() - q2) - / (a.x.to_raw() * b.y.to_raw() - a.y.to_raw() * b.x.to_raw()); - - let control_1 = Point::new(center.x + a.x - k2 * a.y, center.y + a.y + k2 * a.x); - let control_2 = Point::new(center.x + b.x + k2 * b.y, center.y + b.y - k2 * b.x); - - [ - Point::zero(), - control_1.transform(ts), - control_2.transform(ts), - end.transform(ts), - ] - } } impl Numeric for Angle { diff --git a/src/geom/path.rs b/src/geom/path.rs index 836be1b4..721cc20b 100644 --- a/src/geom/path.rs +++ b/src/geom/path.rs @@ -71,3 +71,48 @@ impl Path { self.0.push(PathElement::ClosePath); } } + +/// Get the control points for a bezier curve that describes a circular arc +/// of this angle with the given radius. +pub fn bezier_arc( + angle: Angle, + radius: Length, + rotate: bool, + mirror_x: bool, + mirror_y: bool, +) -> [Point; 4] { + let end = Point::new(angle.cos() * radius - radius, angle.sin() * radius); + let center = Point::new(-radius, Length::zero()); + + let mut ts = if mirror_y { + Transform::mirror_y() + } else { + Transform::identity() + }; + + if mirror_x { + ts = ts.pre_concat(Transform::mirror_x()); + } + + if rotate { + ts = ts.pre_concat(Transform::rotate(Angle::deg(90.0))); + } + + let a = center * -1.0; + let b = end - center; + + let q1 = a.x.to_raw() * a.x.to_raw() + a.y.to_raw() * a.y.to_raw(); + let q2 = q1 + a.x.to_raw() * b.x.to_raw() + a.y.to_raw() * b.y.to_raw(); + let k2 = (4.0 / 3.0) * ((2.0 * q1 * q2).sqrt() - q2) + / (a.x.to_raw() * b.y.to_raw() - a.y.to_raw() * b.x.to_raw()); + + let control_1 = Point::new(center.x + a.x - k2 * a.y, center.y + a.y + k2 * a.x); + let control_2 = Point::new(center.x + b.x + k2 * b.y, center.y + b.y - k2 * b.x); + + [ + Point::zero(), + control_1.transform(ts), + control_2.transform(ts), + end.transform(ts), + ] +} diff --git a/src/geom/rect.rs b/src/geom/rect.rs index f0da2db6..aa670f0a 100644 --- a/src/geom/rect.rs +++ b/src/geom/rect.rs @@ -3,7 +3,7 @@ use super::*; use std::mem; /// A rectangle with rounded corners. -#[derive(Debug, Clone)] +#[derive(Debug, Copy, Clone, PartialEq)] pub struct Rect { size: Size, radius: Sides, @@ -19,7 +19,7 @@ impl Rect { /// in the foreground. The function will output multiple items if the stroke /// properties differ by side. pub fn shapes( - &self, + self, fill: Option, stroke: Sides>, ) -> Vec { @@ -28,48 +28,64 @@ impl Rect { res.push(Shape { geometry: self.fill_geometry(), fill, - stroke: stroke.is_uniform().then(|| stroke.top).flatten(), + stroke: if stroke.is_uniform() { stroke.top } else { None }, }); } if !stroke.is_uniform() { - for (path, stroke) in self.stroke_segments(Some(stroke)) { - if !stroke.is_some() { - continue; + for (path, stroke) in self.stroke_segments(stroke) { + if stroke.is_some() { + res.push(Shape { + geometry: Geometry::Path(path), + fill: None, + stroke, + }); } - res.push(Shape { - geometry: Geometry::Path(path), - fill: None, - stroke, - }); } } 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(Sides::splat(None)); + assert_eq!(paths.len(), 1); + + Geometry::Path(paths.pop().unwrap().0) + } + } + /// Output the minimum number of paths along the rectangles border. fn stroke_segments( - &self, - strokes: Option>>, + self, + strokes: Sides>, ) -> Vec<(Path, Option)> { - let strokes = strokes.unwrap_or_else(|| Sides::splat(None)); let mut res = vec![]; - let mut connection = Connection::None; + let mut connection = Connection::default(); 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 { + let is_continuous = strokes.get(side) == strokes.get(side.next_cw()); + connection = connection.advance(is_continuous && side != Side::Left); + always_continuous &= is_continuous; + + draw_side( + &mut path, + side, + self.size, + self.radius.get(side.next_ccw()), + self.radius.get(side), + connection, + ); + + if !is_continuous { res.push((mem::take(&mut path), strokes.get(side))); } } @@ -84,19 +100,6 @@ impl Rect { 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 @@ -110,19 +113,18 @@ fn draw_side( 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); + let [a, b, c, d] = bezier_arc(angle, 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 angle_left = Angle::deg(if connection.prev { 90.0 } else { 45.0 }); + let angle_right = Angle::deg(if connection.next { 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) + let arc2 = bezier_arc(-angle_right, radius_right, true, true, false) .map(|x| x + Point::with_x(size.x - radius_right)); (arc1, arc2) @@ -131,8 +133,7 @@ fn draw_side( 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) + let arc2 = bezier_arc(angle_right, radius_right, false, false, false) .map(|x| x + Point::new(size.x, size.y - radius_right)); (arc1, arc2) @@ -141,8 +142,7 @@ fn draw_side( 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) + let arc2 = bezier_arc(angle_right, radius_right, true, false, false) .map(|x| x + Point::new(radius_right, size.y)); (arc1, arc2) @@ -151,15 +151,14 @@ fn draw_side( 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) + let arc2 = bezier_arc(-angle_right, radius_right, false, false, true) .map(|x| x + Point::with_y(radius_right)); (arc1, arc2) } }; - if !connection.left() { + if !connection.prev { path.move_to(if radius_left.is_zero() { arc1[3] } else { arc1[0] }); } @@ -169,51 +168,24 @@ fn draw_side( path.line_to(arc2[0]); - if !connection.right() && !radius_right.is_zero() { + if !connection.next && !radius_right.is_zero() { path.cubic_to(arc2[1], arc2[2], arc2[3]); } } /// A state machine that indicates which sides of the border strokes in a 2D /// polygon are connected to their neighboring sides. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum Connection { - None, - Left, - Right, - Both, +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] +struct Connection { + prev: bool, + next: bool, } impl Connection { /// Advance to the next clockwise side of the polygon. The argument /// indicates whether the border is connected on the right side of the next /// edge. - 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 - } - } - } - } - - /// Whether there is a connection on the left. - fn left(self) -> bool { - matches!(self, Self::Left | Self::Both) - } - - /// Whether there is a connection on the right. - fn right(self) -> bool { - matches!(self, Self::Right | Self::Both) + pub fn advance(self, next: bool) -> Self { + Self { prev: self.next, next } } } diff --git a/src/geom/sides.rs b/src/geom/sides.rs index f214a1bf..555bbd62 100644 --- a/src/geom/sides.rs +++ b/src/geom/sides.rs @@ -45,6 +45,19 @@ impl Sides { } } + /// Zip two instances into an instance. + pub fn zip(self, other: Sides, mut f: F) -> Sides + where + F: FnMut(T, V, Side) -> W, + { + Sides { + left: f(self.left, other.left, Side::Left), + top: f(self.top, other.top, Side::Top), + right: f(self.right, other.right, Side::Right), + bottom: f(self.bottom, other.bottom, Side::Bottom), + } + } + /// Returns an iterator over the sides. pub fn iter(&self) -> impl Iterator { [&self.left, &self.top, &self.right, &self.bottom].into_iter() diff --git a/src/geom/transform.rs b/src/geom/transform.rs index de2a9781..961ba487 100644 --- a/src/geom/transform.rs +++ b/src/geom/transform.rs @@ -26,26 +26,12 @@ impl Transform { /// Transform by mirroring along the x-axis. pub fn mirror_x() -> Self { - Self { - sx: Ratio::one(), - ky: Ratio::zero(), - kx: Ratio::zero(), - sy: -Ratio::one(), - tx: Length::zero(), - ty: Length::zero(), - } + Self::scale(Ratio::one(), -Ratio::one()) } /// Transform by mirroring along the y-axis. pub fn mirror_y() -> Self { - Self { - sx: -Ratio::one(), - ky: Ratio::zero(), - kx: Ratio::zero(), - sy: Ratio::one(), - tx: Length::zero(), - ty: Length::zero(), - } + Self::scale(-Ratio::one(), Ratio::one()) } /// A translate transform. diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs index a5523a2e..40b6e1e3 100644 --- a/src/library/graphics/shape.rs +++ b/src/library/graphics/shape.rs @@ -78,7 +78,7 @@ impl ShapeNode { styles.set_opt(Self::INSET, args.named("inset")?); styles.set_opt(Self::OUTSET, args.named("outset")?); - if S != CIRCLE { + if !is_round(S) { styles.set_opt(Self::RADIUS, args.named("radius")?); } @@ -97,10 +97,7 @@ impl Layout for ShapeNode { if let Some(child) = &self.0 { let mut inset = styles.get(Self::INSET); if is_round(S) { - inset = inset.map(|mut side| { - side.rel += Ratio::new(0.5 - SQRT_2 / 4.0); - side - }); + inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0)); } // Pad the child. @@ -158,18 +155,8 @@ impl Layout for ShapeNode { } }; - let outset = styles.get(Self::OUTSET); - let outset = Sides { - left: outset.left.relative_to(frame.size.x), - top: outset.top.relative_to(frame.size.y), - right: outset.right.relative_to(frame.size.x), - bottom: outset.bottom.relative_to(frame.size.y), - }; - - let size = Spec::new( - frame.size.x + outset.left + outset.right, - frame.size.y + outset.top + outset.bottom, - ); + let outset = styles.get(Self::OUTSET).relative_to(frame.size); + let size = frame.size + outset.sum_by_axis(); let radius = styles .get(Self::RADIUS) @@ -186,11 +173,12 @@ impl Layout for ShapeNode { }; 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)); - } + frame.prepend_multiple( + Rect::new(size, radius) + .shapes(fill, stroke) + .into_iter() + .map(|x| (pos, Element::Shape(x))), + ) } } diff --git a/src/library/layout/page.rs b/src/library/layout/page.rs index 801a137d..c8495e64 100644 --- a/src/library/layout/page.rs +++ b/src/library/layout/page.rs @@ -20,7 +20,8 @@ impl PageNode { /// The page margin. #[property(fold)] - pub const MARGINS: Sides>> = Sides::splat(Smart::Auto); + pub const MARGINS: Sides>>> = + Sides::splat(Smart::Auto); /// How many columns the page has. pub const COLUMNS: NonZeroUsize = NonZeroUsize::new(1).unwrap(); @@ -48,9 +49,7 @@ impl PageNode { styles.set_opt(Self::WIDTH, args.named("width")?); styles.set_opt(Self::HEIGHT, args.named("height")?); - styles.set_opt(Self::MARGINS, args.named("margins")?); - styles.set_opt(Self::FLIPPED, args.named("flipped")?); styles.set_opt(Self::FILL, args.named("fill")?); styles.set_opt(Self::COLUMNS, args.named("columns")?); diff --git a/src/model/styles.rs b/src/model/styles.rs index 1fddfd0e..ae4c1586 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -466,12 +466,7 @@ where type Output = Sides; fn fold(self, outer: Self::Output) -> Self::Output { - Sides { - left: self.left.fold(outer.left), - top: self.top.fold(outer.top), - right: self.right.fold(outer.right), - bottom: self.bottom.fold(outer.bottom), - } + self.zip(outer, |inner, outer, _| inner.fold(outer)) } } @@ -479,25 +474,15 @@ impl Fold for Sides>> { type Output = Sides>; fn fold(self, outer: Self::Output) -> Self::Output { - Sides { - left: self.left.unwrap_or(outer.left), - top: self.top.unwrap_or(outer.top), - right: self.right.unwrap_or(outer.right), - bottom: self.bottom.unwrap_or(outer.bottom), - } + self.zip(outer, |inner, outer, _| inner.unwrap_or(outer)) } } -impl Fold for Sides>> { +impl Fold for Sides>>> { type Output = Sides>>; fn fold(self, outer: Self::Output) -> Self::Output { - Sides { - left: self.left.or(outer.left), - top: self.top.or(outer.top), - right: self.right.or(outer.right), - bottom: self.bottom.or(outer.bottom), - } + self.zip(outer, |inner, outer, _| inner.unwrap_or(outer)) } } -- cgit v1.2.3 From 51d0de09c6f7e2af4db3b65c3fe9595c501b82c9 Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Tue, 3 May 2022 23:07:19 +0200 Subject: Code Review: Relax, it's much worse than you think --- src/geom/path.rs | 40 +++++---------------------- src/geom/rect.rs | 75 +++++++++++++++++++++++---------------------------- src/geom/sides.rs | 8 ++++++ src/geom/transform.rs | 17 +++++------- 4 files changed, 55 insertions(+), 85 deletions(-) (limited to 'src') diff --git a/src/geom/path.rs b/src/geom/path.rs index 721cc20b..d0c3c75d 100644 --- a/src/geom/path.rs +++ b/src/geom/path.rs @@ -72,33 +72,12 @@ impl Path { } } -/// Get the control points for a bezier curve that describes a circular arc -/// of this angle with the given radius. -pub fn bezier_arc( - angle: Angle, - radius: Length, - rotate: bool, - mirror_x: bool, - mirror_y: bool, -) -> [Point; 4] { - let end = Point::new(angle.cos() * radius - radius, angle.sin() * radius); - let center = Point::new(-radius, Length::zero()); - - let mut ts = if mirror_y { - Transform::mirror_y() - } else { - Transform::identity() - }; - - if mirror_x { - ts = ts.pre_concat(Transform::mirror_x()); - } - - if rotate { - ts = ts.pre_concat(Transform::rotate(Angle::deg(90.0))); - } - - let a = center * -1.0; +/// Get the control points for a bezier curve that describes a circular arc for +/// a start point, an end point and a center of the circle whose arc connects +/// the two. +pub fn bezier_arc(start: Point, center: Point, end: Point) -> [Point; 4] { + // https://stackoverflow.com/a/44829356/1567835 + let a = start - center; let b = end - center; let q1 = a.x.to_raw() * a.x.to_raw() + a.y.to_raw() * a.y.to_raw(); @@ -109,10 +88,5 @@ pub fn bezier_arc( let control_1 = Point::new(center.x + a.x - k2 * a.y, center.y + a.y + k2 * a.x); let control_2 = Point::new(center.x + b.x + k2 * b.y, center.y + b.y - k2 * b.x); - [ - Point::zero(), - control_1.transform(ts), - control_2.transform(ts), - end.transform(ts), - ] + [start, control_1, control_2, end] } diff --git a/src/geom/rect.rs b/src/geom/rect.rs index aa670f0a..34160b04 100644 --- a/src/geom/rect.rs +++ b/src/geom/rect.rs @@ -112,52 +112,45 @@ fn draw_side( radius_right: Length, connection: Connection, ) { - let reversed = |angle: Angle, radius, rotate, mirror_x, mirror_y| { - let [a, b, c, d] = bezier_arc(angle, radius, rotate, mirror_x, mirror_y); - [d, c, b, a] - }; - let angle_left = Angle::deg(if connection.prev { 90.0 } else { 45.0 }); let angle_right = Angle::deg(if connection.next { 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 = bezier_arc(-angle_right, 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 = bezier_arc(angle_right, 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 = bezier_arc(angle_right, 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 = bezier_arc(-angle_right, radius_right, false, false, true) - .map(|x| x + Point::with_y(radius_right)); - - (arc1, arc2) - } + let length = size.get(side.axis()); + + // The arcs for a border of the rectangle along the x-axis, starting at (0,0). + let p1 = Point::with_x(radius_left); + let mut arc1 = bezier_arc( + p1 + Point::new( + -angle_left.sin() * radius_left, + (1.0 - angle_left.cos()) * radius_left, + ), + Point::new(radius_left, radius_left), + p1, + ); + + let p2 = Point::with_x(length - radius_right); + let mut arc2 = bezier_arc( + p2, + Point::new(length - radius_right, radius_right), + p2 + Point::new( + angle_right.sin() * radius_right, + (1.0 - angle_right.cos()) * radius_right, + ), + ); + + let transform = match side { + Side::Left => Transform::rotate(Angle::deg(-90.0)) + .post_concat(Transform::translate(Length::zero(), size.y)), + Side::Bottom => Transform::rotate(Angle::deg(180.0)) + .post_concat(Transform::translate(size.x, size.y)), + Side::Right => Transform::rotate(Angle::deg(90.0)) + .post_concat(Transform::translate(size.x, Length::zero())), + _ => Transform::identity(), }; + arc1 = arc1.map(|x| x.transform(transform)); + arc2 = arc2.map(|x| x.transform(transform)); + if !connection.prev { path.move_to(if radius_left.is_zero() { arc1[3] } else { arc1[0] }); } diff --git a/src/geom/sides.rs b/src/geom/sides.rs index 555bbd62..43e470d2 100644 --- a/src/geom/sides.rs +++ b/src/geom/sides.rs @@ -159,4 +159,12 @@ impl Side { Self::Bottom => Self::Right, } } + + /// Return the corresponding axis. + pub fn axis(self) -> SpecAxis { + match self { + Self::Left | Self::Right => SpecAxis::Vertical, + Self::Top | Self::Bottom => SpecAxis::Horizontal, + } + } } diff --git a/src/geom/transform.rs b/src/geom/transform.rs index 961ba487..40c8e9e4 100644 --- a/src/geom/transform.rs +++ b/src/geom/transform.rs @@ -24,16 +24,6 @@ impl Transform { } } - /// Transform by mirroring along the x-axis. - pub fn mirror_x() -> Self { - Self::scale(Ratio::one(), -Ratio::one()) - } - - /// Transform by mirroring along the y-axis. - pub fn mirror_y() -> Self { - Self::scale(-Ratio::one(), Ratio::one()) - } - /// A translate transform. pub const fn translate(tx: Length, ty: Length) -> Self { Self { tx, ty, ..Self::identity() } @@ -63,7 +53,7 @@ impl Transform { } /// Pre-concatenate another transformation. - pub fn pre_concat(&self, prev: Self) -> Self { + pub fn pre_concat(self, prev: Self) -> Self { Transform { sx: self.sx * prev.sx + self.kx * prev.ky, ky: self.ky * prev.sx + self.sy * prev.ky, @@ -73,6 +63,11 @@ impl Transform { ty: self.ky.of(prev.tx) + self.sy.of(prev.ty) + self.ty, } } + + /// Post-concatenate another transformation. + pub fn post_concat(self, next: Self) -> Self { + next.pre_concat(self) + } } impl Default for Transform { -- cgit v1.2.3