diff --git a/crates/typst-pdf/src/content.rs b/crates/typst-pdf/src/content.rs index d9830e439..e88769449 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 360c2a4f8..f31262eff 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 a382bd9d0..364cdd234 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 4caae2fdc..12be2e22d 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 04b75123b..6af933988 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 95164e822..514490470 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 df9114267..0ba412cde 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 120f41fcb..deb5e1009 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 8564e1ddf..e8a68fe38 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 9643a476c..5aa3b8821 100644 Binary files a/tests/ref/path.png and b/tests/ref/path.png differ diff --git a/tests/ref/polygon.png b/tests/ref/polygon.png index 1dc110831..05090e1ea 100644 Binary files a/tests/ref/polygon.png and b/tests/ref/polygon.png differ diff --git a/tests/suite/visualize/path.typ b/tests/suite/visualize/path.typ index bdd3dc726..95f7a803c 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 a3f4c8ef1..7d8342c8e 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,)}