diff --git a/crates/typst/src/doc.rs b/crates/typst/src/doc.rs index 16f8eb03c..e3913c1cd 100644 --- a/crates/typst/src/doc.rs +++ b/crates/typst/src/doc.rs @@ -12,7 +12,7 @@ use crate::eval::{cast, dict, ty, Dict, Value}; use crate::export::PdfPageLabel; use crate::font::Font; use crate::geom::{ - self, rounded_rect, Abs, Axes, Color, Corners, Dir, Em, FixedAlign, FixedStroke, + self, styled_rect, Abs, Axes, Color, Corners, Dir, Em, FixedAlign, FixedStroke, Geometry, Length, Numeric, Paint, Point, Rel, Shape, Sides, Size, Transform, }; use crate::image::Image; @@ -301,9 +301,8 @@ impl Frame { let outset = outset.relative_to(self.size()); let size = self.size() + outset.sum_by_axis(); let pos = Point::new(-outset.left, -outset.top); - let radius = radius.map(|side| side.relative_to(size.x.min(size.y) / 2.0)); self.prepend_multiple( - rounded_rect(size, radius, fill, stroke) + styled_rect(size, radius, fill, stroke) .into_iter() .map(|x| (pos, FrameItem::Shape(x, span))), ) diff --git a/crates/typst/src/geom/corners.rs b/crates/typst/src/geom/corners.rs index d21dc22e2..a7cd0eed8 100644 --- a/crates/typst/src/geom/corners.rs +++ b/crates/typst/src/geom/corners.rs @@ -110,6 +110,48 @@ pub enum Corner { BottomLeft, } +impl Corner { + /// The next corner, clockwise. + pub fn next_cw(self) -> Self { + match self { + Self::TopLeft => Self::TopRight, + Self::TopRight => Self::BottomRight, + Self::BottomRight => Self::BottomLeft, + Self::BottomLeft => Self::TopLeft, + } + } + + /// The next corner, counter-clockwise. + pub fn next_ccw(self) -> Self { + match self { + Self::TopLeft => Self::BottomLeft, + Self::TopRight => Self::TopLeft, + Self::BottomRight => Self::TopRight, + Self::BottomLeft => Self::BottomRight, + } + } + + /// The next side, clockwise. + pub fn side_cw(self) -> Side { + match self { + Self::TopLeft => Side::Top, + Self::TopRight => Side::Right, + Self::BottomRight => Side::Bottom, + Self::BottomLeft => Side::Left, + } + } + + /// The next side, counter-clockwise. + pub fn side_ccw(self) -> Side { + match self { + Self::TopLeft => Side::Left, + Self::TopRight => Side::Top, + Self::BottomRight => Side::Right, + Self::BottomLeft => Side::Bottom, + } + } +} + impl Reflect for Corners> { fn input() -> CastInfo { T::input() + Dict::input() diff --git a/crates/typst/src/geom/mod.rs b/crates/typst/src/geom/mod.rs index c5bcf84ea..0d685607d 100644 --- a/crates/typst/src/geom/mod.rs +++ b/crates/typst/src/geom/mod.rs @@ -17,8 +17,8 @@ mod paint; mod path; mod point; mod ratio; +mod rect; mod rel; -mod rounded; mod scalar; mod shape; mod sides; @@ -42,8 +42,8 @@ pub use self::paint::Paint; pub use self::path::{Path, PathItem}; pub use self::point::Point; pub use self::ratio::Ratio; +pub use self::rect::styled_rect; pub use self::rel::Rel; -pub use self::rounded::rounded_rect; pub use self::scalar::Scalar; pub use self::shape::{Geometry, Shape}; pub use self::sides::{Side, Sides}; diff --git a/crates/typst/src/geom/rect.rs b/crates/typst/src/geom/rect.rs new file mode 100644 index 000000000..108f5addb --- /dev/null +++ b/crates/typst/src/geom/rect.rs @@ -0,0 +1,541 @@ +use super::*; + +/// Helper to draw arcs with bezier curves. +trait PathExtension { + fn arc(&mut self, start: Point, center: Point, end: Point); + fn arc_move(&mut self, start: Point, center: Point, end: Point); + fn arc_line(&mut self, start: Point, center: Point, end: Point); +} + +/// Get the control points for a bezier curve that approximates a circular arc for +/// a start point, an end point and a center of the circle whose arc connects +/// the two. +fn bezier_arc_control(start: Point, center: Point, end: Point) -> [Point; 2] { + // 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(); + 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); + + [control_1, control_2] +} + +impl PathExtension for Path { + fn arc(&mut self, start: Point, center: Point, end: Point) { + let arc = bezier_arc_control(start, center, end); + self.cubic_to(arc[0], arc[1], end); + } + + fn arc_move(&mut self, start: Point, center: Point, end: Point) { + self.move_to(start); + self.arc(start, center, end); + } + fn arc_line(&mut self, start: Point, center: Point, end: Point) { + self.line_to(start); + self.arc(start, center, end); + } +} + +/// Create a styled rectangle with shapes. +/// - use rect primitive for simple rectangles +/// - stroke sides if possible +/// - use fill for sides for best looks +pub fn styled_rect( + size: Size, + radius: Corners>, + fill: Option, + stroke: Sides>, +) -> Vec { + if stroke.is_uniform() && radius.iter().cloned().all(Rel::is_zero) { + simple_rect(size, fill, stroke.top) + } else { + segmented_rect(size, radius, fill, stroke) + } +} + +/// Use rect primitive for the rectangle +fn simple_rect( + size: Size, + fill: Option, + stroke: Option, +) -> Vec { + vec![Shape { geometry: Geometry::Rect(size), fill, stroke }] +} + +/// Use stroke and fill for the rectangle +fn segmented_rect( + size: Size, + radius: Corners>, + fill: Option, + strokes: Sides>, +) -> Vec { + let mut res = vec![]; + let stroke_widths = strokes + .clone() + .map(|s| s.map(|s| s.thickness / 2.0).unwrap_or(Abs::zero())); + + let max_radius = (size.x.min(size.y)) / 2.0 + + stroke_widths.iter().cloned().min().unwrap_or(Abs::zero()); + + let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius)); + + let corners = Corners { + top_left: Corner::TopLeft, + top_right: Corner::TopRight, + bottom_right: Corner::BottomRight, + bottom_left: Corner::BottomLeft, + } + .map(|corner| ControlPoints { + radius: radius.get(corner), + stroke_before: stroke_widths.get(corner.side_ccw()), + stroke_after: stroke_widths.get(corner.side_cw()), + corner, + size, + same: match ( + strokes.get_ref(corner.side_ccw()), + strokes.get_ref(corner.side_cw()), + ) { + (Some(a), Some(b)) => a.paint == b.paint && a.dash_pattern == b.dash_pattern, + (None, None) => true, + _ => false, + }, + }); + + // insert stroked sides below filled sides + let mut stroke_insert = 0; + + // fill shape with inner curve + if let Some(fill) = fill { + let mut path = Path::new(); + let c = corners.get_ref(Corner::TopLeft); + if c.arc() { + path.arc_move(c.start(), c.center(), c.end()); + } else { + path.move_to(c.center()); + }; + + for corner in [Corner::TopRight, Corner::BottomRight, Corner::BottomLeft] { + let c = corners.get_ref(corner); + if c.arc() { + path.arc_line(c.start(), c.center(), c.end()); + } else { + path.line_to(c.center()); + } + } + path.close_path(); + res.push(Shape { + geometry: Geometry::Path(path), + fill: Some(fill), + stroke: None, + }); + stroke_insert += 1; + } + + let current = corners.iter().find(|c| !c.same).map(|c| c.corner); + if let Some(mut current) = current { + // multiple segments + // start at a corner with a change between sides and iterate clockwise all other corners + let mut last = current; + for _ in 0..4 { + current = current.next_cw(); + if corners.get_ref(current).same { + continue; + } + // create segment + let start = last; + let end = current; + last = current; + let stroke = match strokes.get_ref(start.side_cw()) { + None => continue, + Some(stroke) => stroke.clone(), + }; + let (shape, ontop) = segment(start, end, &corners, stroke); + if ontop { + res.push(shape); + } else { + res.insert(stroke_insert, shape); + stroke_insert += 1; + } + } + } else if let Some(stroke) = strokes.top { + // single segment + let (shape, _) = segment(Corner::TopLeft, Corner::TopLeft, &corners, stroke); + res.push(shape); + } + res +} + +/// Returns the shape for the segment and whether the shape should be drawn on top. +fn segment( + start: Corner, + end: Corner, + corners: &Corners, + stroke: FixedStroke, +) -> (Shape, bool) { + fn fill_corner(corner: &ControlPoints) -> bool { + corner.stroke_before != corner.stroke_after + || corner.radius() < corner.stroke_before + } + + fn fill_corners( + start: Corner, + end: Corner, + corners: &Corners, + ) -> bool { + if fill_corner(corners.get_ref(start)) { + return true; + } + if fill_corner(corners.get_ref(end)) { + return true; + } + let mut current = start.next_cw(); + while current != end { + if fill_corner(corners.get_ref(current)) { + return true; + } + current = current.next_cw(); + } + false + } + + let solid = stroke + .dash_pattern + .as_ref() + .map(|pattern| pattern.array.is_empty()) + .unwrap_or(true); + + let use_fill = solid && fill_corners(start, end, corners); + + let shape = if use_fill { + fill_segment(start, end, corners, stroke) + } else { + stroke_segment(start, end, corners, stroke) + }; + (shape, use_fill) +} + +/// Stroke the sides from `start` to `end` clockwise. +fn stroke_segment( + start: Corner, + end: Corner, + corners: &Corners, + stroke: FixedStroke, +) -> Shape { + // create start corner + let c = corners.get_ref(start); + let mut path = Path::new(); + if start == end || !c.arc() { + path.move_to(c.end()); + } else { + path.arc_move(c.mid(), c.center(), c.end()); + } + + // create corners between start and end + let mut current = start.next_cw(); + while current != end { + let c = corners.get_ref(current); + if c.arc() { + path.arc_line(c.start(), c.center(), c.end()); + } else { + path.line_to(c.end()); + } + current = current.next_cw(); + } + + // create end corner + let c = corners.get_ref(end); + if !c.arc() { + path.line_to(c.start()); + } else if start == end { + path.arc_line(c.start(), c.center(), c.end()); + } else { + path.arc_line(c.start(), c.center(), c.mid()); + } + + Shape { + geometry: Geometry::Path(path), + stroke: Some(stroke), + fill: None, + } +} + +/// Fill the sides from `start` to `end` clockwise. +fn fill_segment( + start: Corner, + end: Corner, + corners: &Corners, + stroke: FixedStroke, +) -> Shape { + let mut path = Path::new(); + + // create the start corner + // begin on the inside and finish on the outside + // no corner if start and end are equal + // half corner if different + if start == end { + let c = corners.get_ref(start); + path.move_to(c.end_inner()); + path.line_to(c.end_outer()); + } else { + let c = corners.get_ref(start); + + if c.arc_inner() { + path.arc_move(c.end_inner(), c.center_inner(), c.mid_inner()); + } else { + path.move_to(c.end_inner()); + } + + if c.arc_outer() { + path.arc_line(c.mid_outer(), c.center_outer(), c.end_outer()); + } else { + path.line_to(c.outer()); + path.line_to(c.end_outer()); + } + } + + // create the clockwise outside path for the corners between start and end + let mut current = start.next_cw(); + while current != end { + let c = corners.get_ref(current); + if c.arc_outer() { + path.arc_line(c.start_outer(), c.center_outer(), c.end_outer()); + } else { + path.line_to(c.outer()); + } + current = current.next_cw(); + } + + // create the end corner + // begin on the outside and finish on the inside + // full corner if start and end are equal + // half corner if different + if start == end { + let c = corners.get_ref(end); + if c.arc_outer() { + path.arc_line(c.start_outer(), c.center_outer(), c.end_outer()); + } else { + path.line_to(c.outer()); + path.line_to(c.end_outer()); + } + if c.arc_inner() { + path.arc_line(c.end_inner(), c.center_inner(), c.start_inner()); + } else { + path.line_to(c.center_inner()); + } + } else { + let c = corners.get_ref(end); + if c.arc_outer() { + path.arc_line(c.start_outer(), c.center_outer(), c.mid_outer()); + } else { + path.line_to(c.outer()); + } + if c.arc_inner() { + path.arc_line(c.mid_inner(), c.center_inner(), c.start_inner()); + } else { + path.line_to(c.center_inner()); + } + } + + // create the counterclockwise inside path for the corners between start and end + let mut current = end.next_ccw(); + while current != start { + let c = corners.get_ref(current); + if c.arc_inner() { + path.arc_line(c.end_inner(), c.center_inner(), c.start_inner()); + } else { + path.line_to(c.center_inner()); + } + current = current.next_ccw(); + } + + path.close_path(); + + Shape { + geometry: Geometry::Path(path), + stroke: None, + fill: Some(stroke.paint), + } +} + +/// Helper to calculate different control points for the corners. +/// Clockwise orientation from start to end. +/// ```text +/// O-------------------EO --- - Z: Zero/Origin ({x: 0, y: 0} for top left corner) +/// |\ ___----''' | | - O: Outer: intersection between the straight outer lines +/// | \ / | | - S_: start +/// | MO | | - M_: midpoint +/// | /Z\ __-----------E | - E_: end +/// |/ \M | ro - r_: radius +/// | /\ | | - middle of the stroke +/// | / \ | | - arc from S through M to E with center C and radius r +/// | | MI--EI------- | - outer curve +/// | | / \ | - arc from SO through MO to EO with center CO and radius ro +/// SO | | \ CO --- - inner curve +/// | | | \ - arc from SI through MI to EI with center CI and radius ri +/// |--S-SI-----CI C +/// |--ri--| +/// |-------r--------| +/// ``` +struct ControlPoints { + radius: Abs, + stroke_after: Abs, + stroke_before: Abs, + corner: Corner, + size: Size, + same: bool, +} + +impl ControlPoints { + /// Move and rotate the point from top-left to the required corner. + fn rotate(&self, point: Point) -> Point { + match self.corner { + Corner::TopLeft => point, + Corner::TopRight => Point { x: self.size.x - point.y, y: point.x }, + Corner::BottomRight => { + Point { x: self.size.x - point.x, y: self.size.y - point.y } + } + Corner::BottomLeft => Point { x: point.y, y: self.size.y - point.x }, + } + } + + /// Outside intersection of the sides. + pub fn outer(&self) -> Point { + self.rotate(Point { x: -self.stroke_before, y: -self.stroke_after }) + } + + /// Center for the outer arc. + pub fn center_outer(&self) -> Point { + let r = self.radius_outer(); + self.rotate(Point { + x: r - self.stroke_before, + y: r - self.stroke_after, + }) + } + + /// Center for the middle arc. + pub fn center(&self) -> Point { + let r = self.radius(); + self.rotate(Point { x: r, y: r }) + } + + /// Center for the inner arc. + pub fn center_inner(&self) -> Point { + let r = self.radius_inner(); + + self.rotate(Point { + x: self.stroke_before + r, + y: self.stroke_after + r, + }) + } + + /// Radius of the outer arc. + pub fn radius_outer(&self) -> Abs { + self.radius + } + + /// Radius of the middle arc. + pub fn radius(&self) -> Abs { + (self.radius - self.stroke_before.min(self.stroke_after)).max(Abs::zero()) + } + + /// Radius of the inner arc. + pub fn radius_inner(&self) -> Abs { + (self.radius - 2.0 * self.stroke_before.max(self.stroke_after)).max(Abs::zero()) + } + + /// Middle of the corner on the outside of the stroke. + pub fn mid_outer(&self) -> Point { + let c_i = self.center_inner(); + let c_o = self.center_outer(); + let o = self.outer(); + let r = self.radius_outer(); + + // https://math.stackexchange.com/a/311956 + // intersection between the line from inner center to outside and the outer arc + let a = (o.x - c_i.x).to_raw().powi(2) + (o.y - c_i.y).to_raw().powi(2); + let b = 2.0 * (o.x - c_i.x).to_raw() * (c_i.x - c_o.x).to_raw() + + 2.0 * (o.y - c_i.y).to_raw() * (c_i.y - c_o.y).to_raw(); + let c = (c_i.x - c_o.x).to_raw().powi(2) + (c_i.y - c_o.y).to_raw().powi(2) + - r.to_raw().powi(2); + let t = (-b + (b * b - 4.0 * a * c).sqrt()) / (2.0 * a); + c_i + t * (o - c_i) + } + + /// Middle of the corner in the middle of the stroke. + pub fn mid(&self) -> Point { + let center = self.center_outer(); + let outer = self.outer(); + let diff = outer - center; + center + diff / diff.hypot().to_raw() * self.radius().to_raw() + } + + /// Middle of the corner on the inside of the stroke. + pub fn mid_inner(&self) -> Point { + let center = self.center_inner(); + let outer = self.outer(); + let diff = outer - center; + center + diff / diff.hypot().to_raw() * self.radius_inner().to_raw() + } + + /// If an outer arc is required. + pub fn arc_outer(&self) -> bool { + self.radius_outer() > Abs::zero() + } + + pub fn arc(&self) -> bool { + self.radius() > Abs::zero() + } + + /// If an inner arc is required. + pub fn arc_inner(&self) -> bool { + self.radius_inner() > Abs::zero() + } + + /// Start of the corner on the outside of the stroke. + pub fn start_outer(&self) -> Point { + self.rotate(Point { + x: -self.stroke_before, + y: self.radius_outer() - self.stroke_after, + }) + } + + /// Start of the corner in the center of the stroke. + pub fn start(&self) -> Point { + self.rotate(Point::with_y(self.radius())) + } + + /// Start of the corner on the inside of the stroke. + pub fn start_inner(&self) -> Point { + self.rotate(Point { + x: self.stroke_before, + y: self.stroke_after + self.radius_inner(), + }) + } + + /// End of the corner on the outside of the stroke. + pub fn end_outer(&self) -> Point { + self.rotate(Point { + x: self.radius_outer() - self.stroke_before, + y: -self.stroke_after, + }) + } + + /// End of the corner in the center of the stroke. + pub fn end(&self) -> Point { + self.rotate(Point::with_x(self.radius())) + } + + /// End of the corner on the inside of the stroke. + pub fn end_inner(&self) -> Point { + self.rotate(Point { + x: self.stroke_before + self.radius_inner(), + y: self.stroke_after, + }) + } +} diff --git a/crates/typst/src/geom/rounded.rs b/crates/typst/src/geom/rounded.rs deleted file mode 100644 index abaf46de0..000000000 --- a/crates/typst/src/geom/rounded.rs +++ /dev/null @@ -1,182 +0,0 @@ -use super::*; - -/// Produce shapes that together make up a rounded rectangle. -pub fn rounded_rect( - size: Size, - radius: Corners, - 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: fill_geometry(size, radius), - fill, - stroke: if stroke.is_uniform() { stroke.top.clone() } else { None }, - }); - } - - if !stroke.is_uniform() { - for (path, stroke) in stroke_segments(size, radius, stroke) { - if stroke.is_some() { - 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(size: Size, radius: Corners) -> Geometry { - if radius.iter().copied().all(Abs::is_zero) { - Geometry::Rect(size) - } else { - let mut paths = stroke_segments(size, radius, 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( - size: Size, - radius: Corners, - stroke: Sides>, -) -> Vec<(Path, Option)> { - let mut res = vec![]; - - let mut connection = Connection::default(); - let mut path = Path::new(); - let mut always_continuous = true; - let max_radius = size.x.min(size.y).max(Abs::zero()) / 2.0; - - for side in [Side::Top, Side::Right, Side::Bottom, Side::Left] { - let continuous = stroke.get_ref(side) == stroke.get_ref(side.next_cw()); - connection = connection.advance(continuous && side != Side::Left); - always_continuous &= continuous; - - draw_side( - &mut path, - side, - size, - radius.get(side.start_corner()).clamp(Abs::zero(), max_radius), - radius.get(side.end_corner()).clamp(Abs::zero(), max_radius), - connection, - ); - - if !continuous { - res.push((std::mem::take(&mut path), stroke.get_ref(side).clone())); - } - } - - if always_continuous { - path.close_path(); - } - - if !path.0.is_empty() { - res.push((path, stroke.left)); - } - - res -} - -/// Draws one side of the rounded rectangle. Will always draw the left arc. The -/// right arc will be drawn halfway if and only if there is no connection. -fn draw_side( - path: &mut Path, - side: Side, - size: Size, - start_radius: Abs, - end_radius: Abs, - connection: Connection, -) { - 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 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(start_radius); - let mut arc1 = bezier_arc( - p1 + Point::new( - -angle_left.sin() * start_radius, - (1.0 - angle_left.cos()) * start_radius, - ), - Point::new(start_radius, start_radius), - p1, - ); - - let p2 = Point::with_x(length - end_radius); - let mut arc2 = bezier_arc( - p2, - Point::new(length - end_radius, end_radius), - p2 + Point::new( - angle_right.sin() * end_radius, - (1.0 - angle_right.cos()) * end_radius, - ), - ); - - let transform = match side { - Side::Left => Transform::rotate(Angle::deg(-90.0)) - .post_concat(Transform::translate(Abs::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, Abs::zero())), - _ => Transform::identity(), - }; - - arc1 = arc1.map(|x| x.transform(transform)); - arc2 = arc2.map(|x| x.transform(transform)); - - if !connection.prev { - path.move_to(if start_radius.is_zero() { arc1[3] } else { arc1[0] }); - } - - if !start_radius.is_zero() { - path.cubic_to(arc1[1], arc1[2], arc1[3]); - } - - path.line_to(arc2[0]); - - if !connection.next && !end_radius.is_zero() { - path.cubic_to(arc2[1], arc2[2], arc2[3]); - } -} - -/// 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. -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(); - 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); - - [start, control_1, control_2, end] -} - -/// Indicates which sides of the border strokes in a 2D polygon are connected to -/// their neighboring sides. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -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, next: bool) -> Self { - Self { prev: self.next, next } - } -} diff --git a/tests/ref/compiler/show-selector.png b/tests/ref/compiler/show-selector.png index be5ba4630..f82596980 100644 Binary files a/tests/ref/compiler/show-selector.png and b/tests/ref/compiler/show-selector.png differ diff --git a/tests/ref/meta/figure.png b/tests/ref/meta/figure.png index 373866988..83bd7b7ff 100644 Binary files a/tests/ref/meta/figure.png and b/tests/ref/meta/figure.png differ diff --git a/tests/ref/visualize/shape-fill-stroke.png b/tests/ref/visualize/shape-fill-stroke.png index cabbfb2e4..d4a4817af 100644 Binary files a/tests/ref/visualize/shape-fill-stroke.png and b/tests/ref/visualize/shape-fill-stroke.png differ diff --git a/tests/ref/visualize/shape-rect.png b/tests/ref/visualize/shape-rect.png index 48d40447b..3eda642f8 100644 Binary files a/tests/ref/visualize/shape-rect.png and b/tests/ref/visualize/shape-rect.png differ diff --git a/tests/ref/visualize/shape-rounded.png b/tests/ref/visualize/shape-rounded.png index 5e761a4d8..ec926d0a5 100644 Binary files a/tests/ref/visualize/shape-rounded.png and b/tests/ref/visualize/shape-rounded.png differ diff --git a/tests/ref/visualize/stroke.png b/tests/ref/visualize/stroke.png index 3aa87c4b4..bdfcae9f7 100644 Binary files a/tests/ref/visualize/stroke.png and b/tests/ref/visualize/stroke.png differ diff --git a/tests/typ/visualize/shape-fill-stroke.typ b/tests/typ/visualize/shape-fill-stroke.typ index 0d1331713..8d187400a 100644 --- a/tests/typ/visualize/shape-fill-stroke.typ +++ b/tests/typ/visualize/shape-fill-stroke.typ @@ -43,8 +43,51 @@ // Test stroke composition. #set square(stroke: 4pt) #set text(font: "Roboto") -#square( - stroke: (left: red, top: yellow, right: green, bottom: blue), - radius: 100%, align(center+horizon)[*G*], - inset: 8pt +#stack( + dir: ltr, + square( + stroke: (left: red, top: yellow, right: green, bottom: blue), + radius: 50%, align(center+horizon)[*G*], + inset: 8pt + ), + h(0.5cm), + square( + stroke: (left: red, top: yellow + 8pt, right: green, bottom: blue + 2pt), + radius: 50%, align(center+horizon)[*G*], + inset: 8pt + ), + h(0.5cm), + square( + stroke: (left: red, top: yellow, right: green, bottom: blue), + radius: 100%, align(center+horizon)[*G*], + inset: 8pt + ), +) + +// Join between different solid strokes +#set square(size: 20pt, stroke: 2pt) +#set square(stroke: (left: green + 4pt, top: black + 2pt, right: blue, bottom: black + 2pt)) +#stack( + dir: ltr, + square(), + h(0.2cm), + square(radius: (top-left: 0pt, rest: 1pt)), + h(0.2cm), + square(radius: (top-left: 0pt, rest: 8pt)), + h(0.2cm), + square(radius: (top-left: 0pt, rest: 100pt)), +) + + +// Join between solid and dotted strokes +#set square(stroke: (left: green + 4pt, top: black + 2pt, right: (paint: blue, dash: "dotted"), bottom: (paint: black, dash: "dotted"))) +#stack( + dir: ltr, + square(), + h(0.2cm), + square(radius: (top-left: 0pt, rest: 1pt)), + h(0.2cm), + square(radius: (top-left: 0pt, rest: 8pt)), + h(0.2cm), + square(radius: (top-left: 0pt, rest: 100pt)), ) diff --git a/tests/typ/visualize/shape-rect.typ b/tests/typ/visualize/shape-rect.typ index 17dd0f92a..98450c801 100644 --- a/tests/typ/visualize/shape-rect.typ +++ b/tests/typ/visualize/shape-rect.typ @@ -32,7 +32,7 @@ #stack( dir: ltr, spacing: 1fr, - rect(width: 2cm, radius: 60%), + rect(width: 2cm, radius: 30%), rect(width: 1cm, radius: (left: 10pt, right: 5pt)), rect(width: 1.25cm, radius: ( top-left: 2pt, diff --git a/tests/typ/visualize/shape-rounded.typ b/tests/typ/visualize/shape-rounded.typ index 862141ba6..42432dc9b 100644 --- a/tests/typ/visualize/shape-rounded.typ +++ b/tests/typ/visualize/shape-rounded.typ @@ -1,6 +1,53 @@ // Test rounded rectangles and squares. --- -// Ensure that radius is clamped. -#rect(radius: -20pt) -#square(radius: 30pt) +#set square(size: 20pt, stroke: 4pt) + +// no radius for non-rounded corners +#stack( + dir: ltr, + square(), + h(10pt), + square(radius: 0pt), + h(10pt), + square(radius: -10pt), +) + +#stack( + dir: ltr, + square(), + h(10pt), + square(radius: 0%), + h(10pt), + square(radius: -10%), +) + + +// small values for small radius +#stack( + dir: ltr, + square(radius: 1pt), + h(10pt), + square(radius: 5%), + h(10pt), + square(radius: 2pt), +) + +// large values for large radius or circle +#stack( + dir: ltr, + square(radius: 8pt), + h(10pt), + square(radius: 10pt), + h(10pt), + square(radius: 12pt), +) + +#stack( + dir: ltr, + square(radius: 45%), + h(10pt), + square(radius: 50%), + h(10pt), + square(radius: 55%), +)