Add non-zero and even-odd fill rules to path and polygon (#4580)

Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
HydroH 2024-07-22 22:24:29 +08:00 committed by GitHub
parent 684efa2e0e
commit 1d74c8e8bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 122 additions and 29 deletions

View File

@ -16,7 +16,8 @@ use typst::model::Destination;
use typst::text::{color::is_color_glyph, Font, TextItem, TextItemView}; use typst::text::{color::is_color_glyph, Font, TextItem, TextItemView};
use typst::utils::{Deferred, Numeric, SliceExt}; use typst::utils::{Deferred, Numeric, SliceExt};
use typst::visualize::{ 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; use crate::color_font::ColorFontMap;
@ -636,11 +637,13 @@ fn write_shape(ctx: &mut Builder, pos: Point, shape: &Shape) {
} }
} }
match (&shape.fill, stroke) { match (&shape.fill, &shape.fill_rule, stroke) {
(None, None) => unreachable!(), (None, _, None) => unreachable!(),
(Some(_), None) => ctx.content.fill_nonzero(), (Some(_), FillRule::NonZero, None) => ctx.content.fill_nonzero(),
(None, Some(_)) => ctx.content.stroke(), (Some(_), FillRule::EvenOdd, None) => ctx.content.fill_even_odd(),
(Some(_), Some(_)) => ctx.content.fill_nonzero_and_stroke(), (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(),
}; };
} }

View File

@ -1,7 +1,8 @@
use tiny_skia as sk; use tiny_skia as sk;
use typst::layout::{Abs, Axes, Point, Ratio, Size}; use typst::layout::{Abs, Axes, Point, Ratio, Size};
use typst::visualize::{ 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}; 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; 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); canvas.fill_path(&path, &paint, rule, ts, state.mask);
} }

View File

@ -5,7 +5,7 @@ use ttf_parser::OutlineBuilder;
use typst::foundations::Repr; use typst::foundations::Repr;
use typst::layout::{Angle, Axes, Frame, Quadrant, Ratio, Size, Transform}; use typst::layout::{Angle, Axes, Frame, Quadrant, Ratio, Size, Transform};
use typst::utils::hash128; 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 xmlwriter::XmlWriter;
use crate::{Id, SVGRenderer, State, SvgMatrix, SvgPathBuilder}; use crate::{Id, SVGRenderer, State, SvgMatrix, SvgPathBuilder};
@ -31,7 +31,13 @@ impl SVGRenderer {
} }
/// Write a fill attribute. /// 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 { match fill {
Paint::Solid(color) => self.xml.write_attribute("fill", &color.encode()), Paint::Solid(color) => self.xml.write_attribute("fill", &color.encode()),
Paint::Gradient(gradient) => { Paint::Gradient(gradient) => {
@ -43,6 +49,10 @@ impl SVGRenderer {
self.xml.write_attribute_fmt("fill", format_args!("url(#{id})")); 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. /// Pushes a gradient to the list of gradients to write SVG file.

View File

@ -17,6 +17,7 @@ impl SVGRenderer {
if let Some(paint) = &shape.fill { if let Some(paint) = &shape.fill {
self.write_fill( self.write_fill(
paint, paint,
shape.fill_rule,
self.shape_fill_size(state, paint, shape), self.shape_fill_size(state, paint, shape),
self.shape_paint_transform(state, paint, shape), self.shape_paint_transform(state, paint, shape),
); );

View File

@ -6,7 +6,7 @@ use ttf_parser::GlyphId;
use typst::layout::{Abs, Point, Ratio, Size, Transform}; use typst::layout::{Abs, Point, Ratio, Size, Transform};
use typst::text::{Font, TextItem}; use typst::text::{Font, TextItem};
use typst::utils::hash128; 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}; use crate::{SVGRenderer, State, SvgMatrix, SvgPathBuilder};
@ -138,6 +138,7 @@ impl SVGRenderer {
self.xml.write_attribute_fmt("x", format_args!("{x_offset}")); self.xml.write_attribute_fmt("x", format_args!("{x_offset}"));
self.write_fill( self.write_fill(
&text.fill, &text.fill,
FillRule::default(),
Size::new(Abs::pt(width), Abs::pt(height)), Size::new(Abs::pt(width), Abs::pt(height)),
self.text_paint_transform(state, &text.fill), self.text_paint_transform(state, &text.fill),
); );

View File

@ -18,7 +18,7 @@ use crate::symbols::Symbol;
use crate::syntax::{Span, Spanned}; use crate::syntax::{Span, Spanned};
use crate::text::TextElem; use crate::text::TextElem;
use crate::utils::Numeric; 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; use super::delimiter_alignment;
@ -597,6 +597,7 @@ fn line_item(length: Abs, vertical: bool, stroke: FixedStroke, span: Span) -> Fr
Shape { Shape {
geometry: line_geom, geometry: line_geom,
fill: None, fill: None,
fill_rule: FillRule::default(),
stroke: Some(stroke), stroke: Some(stroke),
}, },
span, span,

View File

@ -10,7 +10,7 @@ use crate::introspection::Locator;
use crate::layout::{ use crate::layout::{
Abs, Axes, BlockElem, Frame, FrameItem, Length, Point, Region, Rel, Size, 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}; use PathVertex::{AllControlPoints, MirroredControlPoint, Vertex};
@ -33,11 +33,12 @@ pub struct PathElem {
/// ///
/// When setting a fill, the default stroke disappears. To create a /// When setting a fill, the default stroke disappears. To create a
/// rectangle with both fill and stroke, you have to configure both. /// 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<Paint>, pub fill: Option<Paint>,
/// The rule used to fill the path.
#[default]
pub fill_rule: FillRule,
/// How to [stroke] the path. This can be: /// How to [stroke] the path. This can be:
/// ///
/// Can be set to `{none}` to disable the stroke or to `{auto}` for a /// 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. // Prepare fill and stroke.
let fill = elem.fill(styles); let fill = elem.fill(styles);
let fill_rule = elem.fill_rule(styles);
let stroke = match elem.stroke(styles) { let stroke = match elem.stroke(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()), Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None, Smart::Auto => None,
@ -154,7 +156,12 @@ fn layout_path(
}; };
let mut frame = Frame::soft(size); 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())); frame.push(Point::zero(), FrameItem::Shape(shape, elem.span()));
Ok(frame) Ok(frame)
} }

View File

@ -9,7 +9,7 @@ use crate::introspection::Locator;
use crate::layout::{Axes, BlockElem, Em, Frame, FrameItem, Length, Point, Region, Rel}; use crate::layout::{Axes, BlockElem, Em, Frame, FrameItem, Length, Point, Region, Rel};
use crate::syntax::Span; use crate::syntax::Span;
use crate::utils::Numeric; 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. /// A closed polygon.
/// ///
@ -32,11 +32,12 @@ pub struct PolygonElem {
/// ///
/// When setting a fill, the default stroke disappears. To create a /// When setting a fill, the default stroke disappears. To create a
/// rectangle with both fill and stroke, you have to configure both. /// 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<Paint>, pub fill: Option<Paint>,
/// The rule used to fill the polygon.
#[default]
pub fill_rule: FillRule,
/// How to [stroke] the polygon. This can be: /// How to [stroke] the polygon. This can be:
/// ///
/// Can be set to `{none}` to disable the stroke or to `{auto}` for a /// 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. // Prepare fill and stroke.
let fill = elem.fill(styles); let fill = elem.fill(styles);
let fill_rule = elem.fill_rule(styles);
let stroke = match elem.stroke(styles) { let stroke = match elem.stroke(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()), Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None, Smart::Auto => None,
@ -175,7 +177,12 @@ fn layout_polygon(
} }
path.close_path(); 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())); frame.push(Point::zero(), FrameItem::Shape(shape, elem.span()));
Ok(frame) Ok(frame)
} }

View File

@ -2,7 +2,9 @@ use std::f64::consts::SQRT_2;
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; 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::introspection::Locator;
use crate::layout::{ use crate::layout::{
Abs, Axes, BlockElem, Corner, Corners, Frame, FrameItem, Length, Point, Ratio, Abs, Axes, BlockElem, Corner, Corners, Frame, FrameItem, Length, Point, Ratio,
@ -583,10 +585,22 @@ pub struct Shape {
pub geometry: Geometry, pub geometry: Geometry,
/// The shape's background fill. /// The shape's background fill.
pub fill: Option<Paint>, pub fill: Option<Paint>,
/// The shape's fill rule.
pub fill_rule: FillRule,
/// The shape's border stroke. /// The shape's border stroke.
pub stroke: Option<FixedStroke>, pub stroke: Option<FixedStroke>,
} }
/// 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. /// A shape's geometry.
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Geometry { pub enum Geometry {
@ -601,12 +615,22 @@ pub enum Geometry {
impl Geometry { impl Geometry {
/// Fill the geometry without a stroke. /// Fill the geometry without a stroke.
pub fn filled(self, fill: Paint) -> Shape { 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. /// Stroke the geometry without a fill.
pub fn stroked(self, stroke: FixedStroke) -> Shape { 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. /// 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(rx, my), point(mx, ry), point(z, ry));
path.cubic_to(point(-mx, ry), point(-rx, my), point(-rx, z)); 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. /// Creates a new rectangle as a path.
@ -704,7 +733,12 @@ fn simple_rect(
fill: Option<Paint>, fill: Option<Paint>,
stroke: Option<FixedStroke>, stroke: Option<FixedStroke>,
) -> Vec<Shape> { ) -> Vec<Shape> {
vec![Shape { geometry: Geometry::Rect(size), fill, stroke }] vec![Shape {
geometry: Geometry::Rect(size),
fill,
stroke,
fill_rule: FillRule::default(),
}]
} }
fn corners_control_points( fn corners_control_points(
@ -779,6 +813,7 @@ fn segmented_rect(
res.push(Shape { res.push(Shape {
geometry: Geometry::Path(path), geometry: Geometry::Path(path),
fill: Some(fill), fill: Some(fill),
fill_rule: FillRule::default(),
stroke: None, stroke: None,
}); });
stroke_insert += 1; stroke_insert += 1;
@ -916,6 +951,7 @@ fn stroke_segment(
geometry: Geometry::Path(path), geometry: Geometry::Path(path),
stroke: Some(stroke), stroke: Some(stroke),
fill: None, fill: None,
fill_rule: FillRule::default(),
} }
} }
@ -1014,6 +1050,7 @@ fn fill_segment(
geometry: Geometry::Path(path), geometry: Geometry::Path(path),
stroke: None, stroke: None,
fill: Some(stroke.paint.clone()), fill: Some(stroke.paint.clone()),
fill_rule: FillRule::default(),
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -1,10 +1,10 @@
// Test paths. // Test paths.
--- path --- --- path ---
#set page(height: 200pt, width: 200pt) #set page(height: 300pt, width: 200pt)
#table( #table(
columns: (1fr, 1fr), columns: (1fr, 1fr),
rows: (1fr, 1fr), rows: (1fr, 1fr, 1fr),
align: center + horizon, align: center + horizon,
path( path(
fill: red, fill: red,
@ -37,6 +37,26 @@
(30pt, 30pt), (30pt, 30pt),
(15pt, 0pt), (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 --- --- path-bad-vertex ---

View File

@ -27,6 +27,8 @@
// Self-intersections // Self-intersections
#polygon((0pt, 10pt), (30pt, 20pt), (0pt, 30pt), (20pt, 0pt), (20pt, 35pt)) #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 // Regular polygon; should have equal side lengths
#for k in range(3, 9) {polygon.regular(size: 30pt, vertices: k,)} #for k in range(3, 9) {polygon.regular(size: 30pt, vertices: k,)}