From 1d74c8e8bfe47723f62eb0dbb3852c07a43be5fd Mon Sep 17 00:00:00 2001 From: HydroH Date: Mon, 22 Jul 2024 22:24:29 +0800 Subject: [PATCH] Add `non-zero` and `even-odd` fill rules to `path` and `polygon` (#4580) Co-authored-by: Laurenz --- crates/typst-pdf/src/content.rs | 15 ++++---- crates/typst-render/src/shape.rs | 8 +++-- crates/typst-svg/src/paint.rs | 14 ++++++-- crates/typst-svg/src/shape.rs | 1 + crates/typst-svg/src/text.rs | 3 +- crates/typst/src/math/matrix.rs | 3 +- crates/typst/src/visualize/path.rs | 17 +++++++--- crates/typst/src/visualize/polygon.rs | 17 +++++++--- crates/typst/src/visualize/shape.rs | 47 +++++++++++++++++++++++--- tests/ref/path.png | Bin 3150 -> 4364 bytes tests/ref/polygon.png | Bin 3375 -> 3642 bytes tests/suite/visualize/path.typ | 24 +++++++++++-- tests/suite/visualize/polygon.typ | 2 ++ 13 files changed, 122 insertions(+), 29 deletions(-) diff --git a/crates/typst-pdf/src/content.rs b/crates/typst-pdf/src/content.rs index d9830e43..e8876944 100644 --- a/crates/typst-pdf/src/content.rs +++ b/crates/typst-pdf/src/content.rs @@ -16,7 +16,8 @@ use typst::model::Destination; use typst::text::{color::is_color_glyph, Font, TextItem, TextItemView}; use typst::utils::{Deferred, Numeric, SliceExt}; use typst::visualize::{ - FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape, + FillRule, FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, + Shape, }; use crate::color_font::ColorFontMap; @@ -636,11 +637,13 @@ fn write_shape(ctx: &mut Builder, pos: Point, shape: &Shape) { } } - match (&shape.fill, stroke) { - (None, None) => unreachable!(), - (Some(_), None) => ctx.content.fill_nonzero(), - (None, Some(_)) => ctx.content.stroke(), - (Some(_), Some(_)) => ctx.content.fill_nonzero_and_stroke(), + match (&shape.fill, &shape.fill_rule, stroke) { + (None, _, None) => unreachable!(), + (Some(_), FillRule::NonZero, None) => ctx.content.fill_nonzero(), + (Some(_), FillRule::EvenOdd, None) => ctx.content.fill_even_odd(), + (None, _, Some(_)) => ctx.content.stroke(), + (Some(_), FillRule::NonZero, Some(_)) => ctx.content.fill_nonzero_and_stroke(), + (Some(_), FillRule::EvenOdd, Some(_)) => ctx.content.fill_even_odd_and_stroke(), }; } diff --git a/crates/typst-render/src/shape.rs b/crates/typst-render/src/shape.rs index 360c2a4f..f31262ef 100644 --- a/crates/typst-render/src/shape.rs +++ b/crates/typst-render/src/shape.rs @@ -1,7 +1,8 @@ use tiny_skia as sk; use typst::layout::{Abs, Axes, Point, Ratio, Size}; use typst::visualize::{ - DashPattern, FixedStroke, Geometry, LineCap, LineJoin, Path, PathItem, Shape, + DashPattern, FillRule, FixedStroke, Geometry, LineCap, LineJoin, Path, PathItem, + Shape, }; use crate::{paint, AbsExt, State}; @@ -51,7 +52,10 @@ pub fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Opt paint.anti_alias = false; } - let rule = sk::FillRule::default(); + let rule = match shape.fill_rule { + FillRule::NonZero => sk::FillRule::Winding, + FillRule::EvenOdd => sk::FillRule::EvenOdd, + }; canvas.fill_path(&path, &paint, rule, ts, state.mask); } diff --git a/crates/typst-svg/src/paint.rs b/crates/typst-svg/src/paint.rs index a382bd9d..364cdd23 100644 --- a/crates/typst-svg/src/paint.rs +++ b/crates/typst-svg/src/paint.rs @@ -5,7 +5,7 @@ use ttf_parser::OutlineBuilder; use typst::foundations::Repr; use typst::layout::{Angle, Axes, Frame, Quadrant, Ratio, Size, Transform}; use typst::utils::hash128; -use typst::visualize::{Color, Gradient, Paint, Pattern, RatioOrAngle}; +use typst::visualize::{Color, FillRule, Gradient, Paint, Pattern, RatioOrAngle}; use xmlwriter::XmlWriter; use crate::{Id, SVGRenderer, State, SvgMatrix, SvgPathBuilder}; @@ -31,7 +31,13 @@ impl SVGRenderer { } /// Write a fill attribute. - pub(super) fn write_fill(&mut self, fill: &Paint, size: Size, ts: Transform) { + pub(super) fn write_fill( + &mut self, + fill: &Paint, + fill_rule: FillRule, + size: Size, + ts: Transform, + ) { match fill { Paint::Solid(color) => self.xml.write_attribute("fill", &color.encode()), Paint::Gradient(gradient) => { @@ -43,6 +49,10 @@ impl SVGRenderer { self.xml.write_attribute_fmt("fill", format_args!("url(#{id})")); } } + match fill_rule { + FillRule::NonZero => self.xml.write_attribute("fill-rule", "nonzero"), + FillRule::EvenOdd => self.xml.write_attribute("fill-rule", "evenodd"), + } } /// Pushes a gradient to the list of gradients to write SVG file. diff --git a/crates/typst-svg/src/shape.rs b/crates/typst-svg/src/shape.rs index 4caae2fd..12be2e22 100644 --- a/crates/typst-svg/src/shape.rs +++ b/crates/typst-svg/src/shape.rs @@ -17,6 +17,7 @@ impl SVGRenderer { if let Some(paint) = &shape.fill { self.write_fill( paint, + shape.fill_rule, self.shape_fill_size(state, paint, shape), self.shape_paint_transform(state, paint, shape), ); diff --git a/crates/typst-svg/src/text.rs b/crates/typst-svg/src/text.rs index 04b75123..6af93398 100644 --- a/crates/typst-svg/src/text.rs +++ b/crates/typst-svg/src/text.rs @@ -6,7 +6,7 @@ use ttf_parser::GlyphId; use typst::layout::{Abs, Point, Ratio, Size, Transform}; use typst::text::{Font, TextItem}; use typst::utils::hash128; -use typst::visualize::{Image, Paint, RasterFormat, RelativeTo}; +use typst::visualize::{FillRule, Image, Paint, RasterFormat, RelativeTo}; use crate::{SVGRenderer, State, SvgMatrix, SvgPathBuilder}; @@ -138,6 +138,7 @@ impl SVGRenderer { self.xml.write_attribute_fmt("x", format_args!("{x_offset}")); self.write_fill( &text.fill, + FillRule::default(), Size::new(Abs::pt(width), Abs::pt(height)), self.text_paint_transform(state, &text.fill), ); diff --git a/crates/typst/src/math/matrix.rs b/crates/typst/src/math/matrix.rs index 95164e82..51449047 100644 --- a/crates/typst/src/math/matrix.rs +++ b/crates/typst/src/math/matrix.rs @@ -18,7 +18,7 @@ use crate::symbols::Symbol; use crate::syntax::{Span, Spanned}; use crate::text::TextElem; use crate::utils::Numeric; -use crate::visualize::{FixedStroke, Geometry, LineCap, Shape, Stroke}; +use crate::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape, Stroke}; use super::delimiter_alignment; @@ -597,6 +597,7 @@ fn line_item(length: Abs, vertical: bool, stroke: FixedStroke, span: Span) -> Fr Shape { geometry: line_geom, fill: None, + fill_rule: FillRule::default(), stroke: Some(stroke), }, span, diff --git a/crates/typst/src/visualize/path.rs b/crates/typst/src/visualize/path.rs index df911426..0ba412cd 100644 --- a/crates/typst/src/visualize/path.rs +++ b/crates/typst/src/visualize/path.rs @@ -10,7 +10,7 @@ use crate::introspection::Locator; use crate::layout::{ Abs, Axes, BlockElem, Frame, FrameItem, Length, Point, Region, Rel, Size, }; -use crate::visualize::{FixedStroke, Geometry, Paint, Shape, Stroke}; +use crate::visualize::{FillRule, FixedStroke, Geometry, Paint, Shape, Stroke}; use PathVertex::{AllControlPoints, MirroredControlPoint, Vertex}; @@ -33,11 +33,12 @@ pub struct PathElem { /// /// When setting a fill, the default stroke disappears. To create a /// rectangle with both fill and stroke, you have to configure both. - /// - /// Currently all paths are filled according to the [non-zero winding - /// rule](https://en.wikipedia.org/wiki/Nonzero-rule). pub fill: Option, + /// The rule used to fill the path. + #[default] + pub fill_rule: FillRule, + /// How to [stroke] the path. This can be: /// /// Can be set to `{none}` to disable the stroke or to `{auto}` for a @@ -147,6 +148,7 @@ fn layout_path( // Prepare fill and stroke. let fill = elem.fill(styles); + let fill_rule = elem.fill_rule(styles); let stroke = match elem.stroke(styles) { Smart::Auto if fill.is_none() => Some(FixedStroke::default()), Smart::Auto => None, @@ -154,7 +156,12 @@ fn layout_path( }; let mut frame = Frame::soft(size); - let shape = Shape { geometry: Geometry::Path(path), stroke, fill }; + let shape = Shape { + geometry: Geometry::Path(path), + stroke, + fill, + fill_rule, + }; frame.push(Point::zero(), FrameItem::Shape(shape, elem.span())); Ok(frame) } diff --git a/crates/typst/src/visualize/polygon.rs b/crates/typst/src/visualize/polygon.rs index 120f41fc..deb5e100 100644 --- a/crates/typst/src/visualize/polygon.rs +++ b/crates/typst/src/visualize/polygon.rs @@ -9,7 +9,7 @@ use crate::introspection::Locator; use crate::layout::{Axes, BlockElem, Em, Frame, FrameItem, Length, Point, Region, Rel}; use crate::syntax::Span; use crate::utils::Numeric; -use crate::visualize::{FixedStroke, Geometry, Paint, Path, Shape, Stroke}; +use crate::visualize::{FillRule, FixedStroke, Geometry, Paint, Path, Shape, Stroke}; /// A closed polygon. /// @@ -32,11 +32,12 @@ pub struct PolygonElem { /// /// When setting a fill, the default stroke disappears. To create a /// rectangle with both fill and stroke, you have to configure both. - /// - /// Currently all polygons are filled according to the - /// [non-zero winding rule](https://en.wikipedia.org/wiki/Nonzero-rule). pub fill: Option, + /// The rule used to fill the polygon. + #[default] + pub fill_rule: FillRule, + /// How to [stroke] the polygon. This can be: /// /// Can be set to `{none}` to disable the stroke or to `{auto}` for a @@ -161,6 +162,7 @@ fn layout_polygon( // Prepare fill and stroke. let fill = elem.fill(styles); + let fill_rule = elem.fill_rule(styles); let stroke = match elem.stroke(styles) { Smart::Auto if fill.is_none() => Some(FixedStroke::default()), Smart::Auto => None, @@ -175,7 +177,12 @@ fn layout_polygon( } path.close_path(); - let shape = Shape { geometry: Geometry::Path(path), stroke, fill }; + let shape = Shape { + geometry: Geometry::Path(path), + stroke, + fill, + fill_rule, + }; frame.push(Point::zero(), FrameItem::Shape(shape, elem.span())); Ok(frame) } diff --git a/crates/typst/src/visualize/shape.rs b/crates/typst/src/visualize/shape.rs index 8564e1dd..e8a68fe3 100644 --- a/crates/typst/src/visualize/shape.rs +++ b/crates/typst/src/visualize/shape.rs @@ -2,7 +2,9 @@ use std::f64::consts::SQRT_2; use crate::diag::SourceResult; use crate::engine::Engine; -use crate::foundations::{elem, Content, NativeElement, Packed, Show, Smart, StyleChain}; +use crate::foundations::{ + elem, Cast, Content, NativeElement, Packed, Show, Smart, StyleChain, +}; use crate::introspection::Locator; use crate::layout::{ Abs, Axes, BlockElem, Corner, Corners, Frame, FrameItem, Length, Point, Ratio, @@ -583,10 +585,22 @@ pub struct Shape { pub geometry: Geometry, /// The shape's background fill. pub fill: Option, + /// The shape's fill rule. + pub fill_rule: FillRule, /// The shape's border stroke. pub stroke: Option, } +/// A path filling rule. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum FillRule { + /// Specifies that "inside" is computed by a non-zero sum of signed edge crossings. + #[default] + NonZero, + /// Specifies that "inside" is computed by an odd number of edge crossings. + EvenOdd, +} + /// A shape's geometry. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum Geometry { @@ -601,12 +615,22 @@ pub enum Geometry { impl Geometry { /// Fill the geometry without a stroke. pub fn filled(self, fill: Paint) -> Shape { - Shape { geometry: self, fill: Some(fill), stroke: None } + Shape { + geometry: self, + fill: Some(fill), + fill_rule: FillRule::default(), + stroke: None, + } } /// Stroke the geometry without a fill. pub fn stroked(self, stroke: FixedStroke) -> Shape { - Shape { geometry: self, fill: None, stroke: Some(stroke) } + Shape { + geometry: self, + fill: None, + fill_rule: FillRule::default(), + stroke: Some(stroke), + } } /// The bounding box of the geometry. @@ -641,7 +665,12 @@ pub(crate) fn ellipse( path.cubic_to(point(rx, my), point(mx, ry), point(z, ry)); path.cubic_to(point(-mx, ry), point(-rx, my), point(-rx, z)); - Shape { geometry: Geometry::Path(path), stroke, fill } + Shape { + geometry: Geometry::Path(path), + stroke, + fill, + fill_rule: FillRule::default(), + } } /// Creates a new rectangle as a path. @@ -704,7 +733,12 @@ fn simple_rect( fill: Option, stroke: Option, ) -> Vec { - vec![Shape { geometry: Geometry::Rect(size), fill, stroke }] + vec![Shape { + geometry: Geometry::Rect(size), + fill, + stroke, + fill_rule: FillRule::default(), + }] } fn corners_control_points( @@ -779,6 +813,7 @@ fn segmented_rect( res.push(Shape { geometry: Geometry::Path(path), fill: Some(fill), + fill_rule: FillRule::default(), stroke: None, }); stroke_insert += 1; @@ -916,6 +951,7 @@ fn stroke_segment( geometry: Geometry::Path(path), stroke: Some(stroke), fill: None, + fill_rule: FillRule::default(), } } @@ -1014,6 +1050,7 @@ fn fill_segment( geometry: Geometry::Path(path), stroke: None, fill: Some(stroke.paint.clone()), + fill_rule: FillRule::default(), } } diff --git a/tests/ref/path.png b/tests/ref/path.png index 9643a476c07fb7271bc95b9ed6d02c6c6775aff1..5aa3b88217ad08e353b8b02483fb9e187e08cdf0 100644 GIT binary patch literal 4364 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 diff --git a/tests/suite/visualize/path.typ b/tests/suite/visualize/path.typ index bdd3dc72..95f7a803 100644 --- a/tests/suite/visualize/path.typ +++ b/tests/suite/visualize/path.typ @@ -1,10 +1,10 @@ // Test paths. --- path --- -#set page(height: 200pt, width: 200pt) +#set page(height: 300pt, width: 200pt) #table( columns: (1fr, 1fr), - rows: (1fr, 1fr), + rows: (1fr, 1fr, 1fr), align: center + horizon, path( fill: red, @@ -37,6 +37,26 @@ (30pt, 30pt), (15pt, 0pt), ), + path( + fill: red, + fill-rule: "non-zero", + closed: true, + (25pt, 0pt), + (10pt, 50pt), + (50pt, 20pt), + (0pt, 20pt), + (40pt, 50pt), + ), + path( + fill: red, + fill-rule: "even-odd", + closed: true, + (25pt, 0pt), + (10pt, 50pt), + (50pt, 20pt), + (0pt, 20pt), + (40pt, 50pt), + ), ) --- path-bad-vertex --- diff --git a/tests/suite/visualize/polygon.typ b/tests/suite/visualize/polygon.typ index a3f4c8ef..7d8342c8 100644 --- a/tests/suite/visualize/polygon.typ +++ b/tests/suite/visualize/polygon.typ @@ -27,6 +27,8 @@ // Self-intersections #polygon((0pt, 10pt), (30pt, 20pt), (0pt, 30pt), (20pt, 0pt), (20pt, 35pt)) +#polygon(fill-rule: "non-zero", (0pt, 10pt), (30pt, 20pt), (0pt, 30pt), (20pt, 0pt), (20pt, 35pt)) +#polygon(fill-rule: "even-odd", (0pt, 10pt), (30pt, 20pt), (0pt, 30pt), (20pt, 0pt), (20pt, 35pt)) // Regular polygon; should have equal side lengths #for k in range(3, 9) {polygon.regular(size: 30pt, vertices: k,)}