diff --git a/crates/conversion/typst2vec/src/pass/typst2vec.rs b/crates/conversion/typst2vec/src/pass/typst2vec.rs index 6802ab67..ac575940 100644 --- a/crates/conversion/typst2vec/src/pass/typst2vec.rs +++ b/crates/conversion/typst2vec/src/pass/typst2vec.rs @@ -774,6 +774,11 @@ impl Typst2VecPassImpl { self.stroke(stateful_stroke, stroke, &mut styles); } + match shape.fill_rule { + FillRule::NonZero => styles.push(PathStyle::FillRule("nonzero".into())), + FillRule::EvenOdd => styles.push(PathStyle::FillRule("evenodd".into())), + } + let mut shape_size = shape.geometry.bbox_size(); // Edge cases for strokes. if shape_size.x.to_pt() == 0.0 { diff --git a/crates/conversion/vec2canvas/Cargo.toml b/crates/conversion/vec2canvas/Cargo.toml index e40892c1..3ed29bb9 100644 --- a/crates/conversion/vec2canvas/Cargo.toml +++ b/crates/conversion/vec2canvas/Cargo.toml @@ -38,6 +38,7 @@ web-sys = { workspace = true, features = [ "Document", "TextMetrics", "DedicatedWorkerGlobalScope", + "CanvasWindingRule", ] } [features] diff --git a/crates/conversion/vec2canvas/src/device.rs b/crates/conversion/vec2canvas/src/device.rs index 4f919d25..9bce423a 100644 --- a/crates/conversion/vec2canvas/src/device.rs +++ b/crates/conversion/vec2canvas/src/device.rs @@ -1,4 +1,4 @@ -use web_sys::{ImageBitmap, ImageData, OffscreenCanvas, Path2d}; +use web_sys::{CanvasWindingRule, ImageBitmap, ImageData, OffscreenCanvas, Path2d}; pub trait CanvasDevice { #[doc = "The `restore()` method."] @@ -104,6 +104,10 @@ pub trait CanvasDevice { #[doc = ""] #[doc = "[MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fill)"] fn fill_with_path_2d(&self, path: &Path2d); + #[doc = "The `fill()` method."] + #[doc = ""] + #[doc = "[MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvasRenderingContext2D/fill)"] + fn fill_with_path_2d_and_winding(&self, path: &Path2d, winding: CanvasWindingRule); } impl CanvasDevice for web_sys::CanvasRenderingContext2d { @@ -208,6 +212,10 @@ impl CanvasDevice for web_sys::CanvasRenderingContext2d { fn fill_with_path_2d(&self, path: &Path2d) { self.fill_with_path_2d(path); } + + fn fill_with_path_2d_and_winding(&self, path: &Path2d, winding: CanvasWindingRule) { + self.fill_with_path_2d_and_winding(path, winding); + } } impl CanvasDevice for web_sys::OffscreenCanvasRenderingContext2d { @@ -311,4 +319,8 @@ impl CanvasDevice for web_sys::OffscreenCanvasRenderingContext2d { fn fill_with_path_2d(&self, path: &Path2d) { self.fill_with_path_2d(path); } + + fn fill_with_path_2d_and_winding(&self, path: &Path2d, winding: CanvasWindingRule) { + self.fill_with_path_2d_and_winding(path, winding); + } } diff --git a/crates/conversion/vec2canvas/src/ops.rs b/crates/conversion/vec2canvas/src/ops.rs index f3e4c2ad..451a40e7 100644 --- a/crates/conversion/vec2canvas/src/ops.rs +++ b/crates/conversion/vec2canvas/src/ops.rs @@ -19,7 +19,7 @@ use js_sys::Promise; use tiny_skia as sk; use wasm_bindgen::{prelude::Closure, JsCast, JsValue}; -use web_sys::{ImageBitmap, OffscreenCanvas, Path2d}; +use web_sys::{CanvasWindingRule, ImageBitmap, OffscreenCanvas, Path2d}; use reflexo::vector::ir::{ self, FlatGlyphItem, Image, ImageItem, ImmutStr, PathStyle, Rect, Scalar, @@ -249,6 +249,7 @@ impl CanvasOp for CanvasPathElem { let mut fill_color = "none".into(); let mut fill = false; + let mut fill_rule = None; let mut stroke_color = "none".into(); let mut stroke = false; let mut stroke_width = 0.; @@ -285,6 +286,13 @@ impl CanvasOp for CanvasPathElem { PathStyle::StrokeDashOffset(offset) => { canvas.set_line_dash_offset(offset.0 as f64); } + PathStyle::FillRule(rule) => { + fill_rule = match rule.as_ref() { + "nonzero" => Some(CanvasWindingRule::Nonzero), + "evenodd" => Some(CanvasWindingRule::Evenodd), + _ => None, + }; + } } } @@ -294,7 +302,14 @@ impl CanvasOp for CanvasPathElem { fill_color = "black".into() } canvas.set_fill_style(&fill_color.as_ref().into()); - canvas.fill_with_path_2d(&Path2d::new_with_path_string(&self.path_data.d).unwrap()); + if let Some(rule) = fill_rule { + canvas.fill_with_path_2d_and_winding( + &Path2d::new_with_path_string(&self.path_data.d).unwrap(), + rule, + ); + } else { + canvas.fill_with_path_2d(&Path2d::new_with_path_string(&self.path_data.d).unwrap()); + } } if stroke && stroke_width.abs() > 1e-5 { diff --git a/crates/conversion/vec2svg/src/backend/mod.rs b/crates/conversion/vec2svg/src/backend/mod.rs index a81b0e09..36429028 100644 --- a/crates/conversion/vec2svg/src/backend/mod.rs +++ b/crates/conversion/vec2svg/src/backend/mod.rs @@ -629,6 +629,9 @@ fn attach_path_styles<'a>( (offset.0 * scale.unwrap_or(1.)).to_string(), ); } + PathStyle::FillRule(rule) => { + p("fill-rule", rule.to_string()); + } } } diff --git a/crates/reflexo/src/vector/ir/visualize.rs b/crates/reflexo/src/vector/ir/visualize.rs index 23e37589..bfa07df8 100644 --- a/crates/reflexo/src/vector/ir/visualize.rs +++ b/crates/reflexo/src/vector/ir/visualize.rs @@ -115,6 +115,10 @@ pub enum PathStyle { /// `stroke-width` attribute. /// See StrokeWidth(Abs), + + /// `fill-rule` attribute. + /// See + FillRule(ImmutStr), } /// Item representing an `` element. diff --git a/fuzzers/corpora/visualize/path_04.typ b/fuzzers/corpora/visualize/path_04.typ new file mode 100644 index 00000000..c396a0f6 --- /dev/null +++ b/fuzzers/corpora/visualize/path_04.typ @@ -0,0 +1,59 @@ +#set page(height: 300pt, width: 200pt) +#table( + columns: (1fr, 1fr), + rows: (1fr, 1fr, 1fr), + align: center + horizon, + path( + fill: red, + closed: true, + ((0%, 0%), (4%, -4%)), + ((50%, 50%), (4%, -4%)), + ((0%, 50%), (4%, 4%)), + ((50%, 0%), (4%, 4%)), + ), + path( + fill: purple, + stroke: 1pt, + (0pt, 0pt), + (30pt, 30pt), + (0pt, 30pt), + (30pt, 0pt), + ), + + path( + fill: blue, + stroke: 1pt, + closed: true, + ((30%, 0%), (35%, 30%), (-20%, 0%)), + ((30%, 60%), (-20%, 0%), (0%, 0%)), + ((50%, 30%), (60%, -30%), (60%, 0%)), + ), + path( + stroke: 5pt, + closed: true, + (0pt, 30pt), + (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), + ), +) \ No newline at end of file diff --git a/fuzzers/corpora/visualize/polygon_02.typ b/fuzzers/corpora/visualize/polygon_02.typ new file mode 100644 index 00000000..39d71f5f --- /dev/null +++ b/fuzzers/corpora/visualize/polygon_02.typ @@ -0,0 +1,39 @@ + +#import "/contrib/templates/std-tests/preset.typ": * +#show: test-page + +#set page(width: 50pt) +#set polygon(stroke: 0.75pt, fill: blue) + +// These are not visible, but should also not give an error +#polygon() +#polygon((0em, 0pt)) +#polygon((0pt, 0pt), (10pt, 0pt)) +#polygon.regular(size: 0pt, vertices: 9) + +#polygon((5pt, 0pt), (0pt, 10pt), (10pt, 10pt)) +#polygon( + (0pt, 0pt), + (5pt, 5pt), + (10pt, 0pt), + (15pt, 5pt), + (5pt, 10pt), +) +#polygon(stroke: none, (5pt, 0pt), (0pt, 10pt), (10pt, 10pt)) +#polygon(stroke: 3pt, fill: none, (5pt, 0pt), (0pt, 10pt), (10pt, 10pt)) + +// Relative size +#polygon((0pt, 0pt), (100%, 5pt), (50%, 10pt)) + +// Antiparallelogram +#polygon((0pt, 5pt), (5pt, 0pt), (0pt, 10pt), (5pt, 15pt)) + +// 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) +} \ No newline at end of file