From 05559a6638aee7f31fba4dad44a38011150673c9 Mon Sep 17 00:00:00 2001 From: wznmickey Date: Wed, 18 Dec 2024 06:15:38 -0500 Subject: [PATCH 01/35] Bump `unicode-bidi` (#5598) Co-authored-by: Laurenz --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- tests/ref/issue-5490-bidi-invalid-range-2.png | Bin 0 -> 1496 bytes tests/ref/issue-5490-bidi-invalid-range.png | Bin 0 -> 2242 bytes tests/suite/layout/inline/bidi.typ | 16 ++++++++++++++++ 5 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 tests/ref/issue-5490-bidi-invalid-range-2.png create mode 100644 tests/ref/issue-5490-bidi-invalid-range.png diff --git a/Cargo.lock b/Cargo.lock index e6c1cf0f1..be5117da2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3135,9 +3135,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bidi-mirroring" diff --git a/Cargo.toml b/Cargo.toml index f4afefa43..b4f704f80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,7 +124,7 @@ toml = { version = "0.8", default-features = false, features = ["parse", "displa ttf-parser = "0.24.1" two-face = { version = "0.4.0", default-features = false, features = ["syntect-fancy"] } typed-arena = "2" -unicode-bidi = "0.3.13" +unicode-bidi = "0.3.18" unicode-ident = "1.0" unicode-math-class = "0.1" unicode-script = "0.5" diff --git a/tests/ref/issue-5490-bidi-invalid-range-2.png b/tests/ref/issue-5490-bidi-invalid-range-2.png new file mode 100644 index 0000000000000000000000000000000000000000..f93b6309b9357530a7d54a360dae4b2743bd9b1b GIT binary patch literal 1496 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 QIRF3v07*qoM6N<$f+UGPpa1{> literal 0 HcmV?d00001 diff --git a/tests/suite/layout/inline/bidi.typ b/tests/suite/layout/inline/bidi.typ index 5bdb470cb..5f8712d56 100644 --- a/tests/suite/layout/inline/bidi.typ +++ b/tests/suite/layout/inline/bidi.typ @@ -75,3 +75,19 @@ Lריווח #h(1cm) R #"\u{590}\u{591}\u{592}\u{593}" #"\u{30000}\u{30001}\u{30002}\u{30003}" + +--- issue-5490-bidi-invalid-range --- +#set text(lang: "he") +#set raw(lang: "python") +#set page(width: 240pt) +בדיקה האם מספר מתחלק במספר אחר. לדוגמה `if a % 2 == 0` + +--- issue-5490-bidi-invalid-range-2 --- +#table( + columns: (1fr, 1fr), + lines(6), + [ + #text(lang: "ar")[مجرد نص مؤقت لأغراض العرض التوضيحي. ] + #text(lang: "ar")[سلام] + ], +) From 24c08a7ec0aaf87d6dc2e1a2c47b7beb6d5ad2a4 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:37:32 +0100 Subject: [PATCH 02/35] Mention the `calc` module in the operator list (#5595) --- crates/typst-library/src/foundations/calc.rs | 2 +- docs/reference/scripting.md | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/foundations/calc.rs b/crates/typst-library/src/foundations/calc.rs index e7eb14055..fd4498e07 100644 --- a/crates/typst-library/src/foundations/calc.rs +++ b/crates/typst-library/src/foundations/calc.rs @@ -992,7 +992,7 @@ pub fn div_euclid( /// #calc.rem-euclid(1.75, 0.5) \ /// #calc.rem-euclid(decimal("1.75"), decimal("0.5")) /// ``` -#[func(title = "Euclidean Remainder")] +#[func(title = "Euclidean Remainder", keywords = ["modulo", "modulus"])] pub fn rem_euclid( /// The callsite span. span: Span, diff --git a/docs/reference/scripting.md b/docs/reference/scripting.md index 590bb6ec3..89508eee0 100644 --- a/docs/reference/scripting.md +++ b/docs/reference/scripting.md @@ -340,7 +340,10 @@ packages. For more details on this, see the ## Operators The following table lists all available unary and binary operators with effect, -arity (unary, binary) and precedence level (higher binds stronger). +arity (unary, binary) and precedence level (higher binds stronger). Some +operations, such as [modulus]($calc.rem-euclid), do not have a special syntax +and can be achieved using functions from the +[`calc`]($category/foundations/calc) module. | Operator | Effect | Arity | Precedence | |:----------:|---------------------------------|:------:|:----------:| From 257764181e52332a00079b9e3af03823fde1a15d Mon Sep 17 00:00:00 2001 From: Emmanuel Lesueur <48604057+Emm54321@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:58:57 +0100 Subject: [PATCH 03/35] New `curve` element that supersedes `path` (#5323) Co-authored-by: Laurenz --- crates/typst-layout/src/image.rs | 4 +- crates/typst-layout/src/lib.rs | 4 +- crates/typst-layout/src/shapes.rs | 405 ++++++++++--- crates/typst-library/src/layout/frame.rs | 22 +- crates/typst-library/src/routines.rs | 13 +- crates/typst-library/src/visualize/curve.rs | 532 ++++++++++++++++++ .../typst-library/src/visualize/image/mod.rs | 6 +- crates/typst-library/src/visualize/line.rs | 2 +- crates/typst-library/src/visualize/mod.rs | 3 + crates/typst-library/src/visualize/path.rs | 146 +---- crates/typst-library/src/visualize/polygon.rs | 2 +- crates/typst-library/src/visualize/shape.rs | 12 +- crates/typst-library/src/visualize/stroke.rs | 20 +- crates/typst-pdf/src/content.rs | 30 +- crates/typst-render/src/lib.rs | 4 +- crates/typst-render/src/shape.rs | 20 +- crates/typst-svg/src/lib.rs | 5 +- crates/typst-svg/src/shape.rs | 20 +- crates/typst/src/lib.rs | 1 + tests/ref/curve-close-intersection.png | Bin 0 -> 683 bytes tests/ref/curve-close-smooth.png | Bin 0 -> 1306 bytes tests/ref/curve-close-straight.png | Bin 0 -> 1121 bytes tests/ref/curve-cubic-inflection.png | Bin 0 -> 1552 bytes tests/ref/curve-cubic-mirror.png | Bin 0 -> 482 bytes tests/ref/curve-fill-rule.png | Bin 0 -> 570 bytes tests/ref/curve-line.png | Bin 0 -> 244 bytes tests/ref/curve-move-multiple-even-odd.png | Bin 0 -> 301 bytes tests/ref/curve-move-multiple-non-zero.png | Bin 0 -> 297 bytes tests/ref/curve-move-single.png | Bin 0 -> 262 bytes tests/ref/curve-quad-mirror.png | Bin 0 -> 490 bytes tests/ref/curve-stroke-gradient.png | Bin 0 -> 2136 bytes tests/ref/issue-curve-in-sized-container.png | Bin 0 -> 135 bytes tests/ref/stroke-zero-thickness.png | Bin 620 -> 631 bytes tests/suite/visualize/curve.typ | 163 ++++++ tests/suite/visualize/stroke.typ | 24 +- tests/suite/visualize/tiling.typ | 6 +- 36 files changed, 1132 insertions(+), 312 deletions(-) create mode 100644 crates/typst-library/src/visualize/curve.rs create mode 100644 tests/ref/curve-close-intersection.png create mode 100644 tests/ref/curve-close-smooth.png create mode 100644 tests/ref/curve-close-straight.png create mode 100644 tests/ref/curve-cubic-inflection.png create mode 100644 tests/ref/curve-cubic-mirror.png create mode 100644 tests/ref/curve-fill-rule.png create mode 100644 tests/ref/curve-line.png create mode 100644 tests/ref/curve-move-multiple-even-odd.png create mode 100644 tests/ref/curve-move-multiple-non-zero.png create mode 100644 tests/ref/curve-move-single.png create mode 100644 tests/ref/curve-quad-mirror.png create mode 100644 tests/ref/curve-stroke-gradient.png create mode 100644 tests/ref/issue-curve-in-sized-container.png create mode 100644 tests/suite/visualize/curve.typ diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index f44d68873..77e1d0838 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -10,7 +10,7 @@ use typst_library::layout::{ use typst_library::loading::Readable; use typst_library::text::families; use typst_library::visualize::{ - Image, ImageElem, ImageFit, ImageFormat, Path, RasterFormat, VectorFormat, + Curve, Image, ImageElem, ImageFit, ImageFormat, RasterFormat, VectorFormat, }; /// Layout the image. @@ -113,7 +113,7 @@ pub fn layout_image( // Create a clipping group if only part of the image should be visible. if fit == ImageFit::Cover && !target.fits(fitted) { - frame.clip(Path::rect(frame.size())); + frame.clip(Curve::rect(frame.size())); } Ok(frame) diff --git a/crates/typst-layout/src/lib.rs b/crates/typst-layout/src/lib.rs index 7069fc4dd..2e8c1129b 100644 --- a/crates/typst-layout/src/lib.rs +++ b/crates/typst-layout/src/lib.rs @@ -23,8 +23,8 @@ pub use self::pad::layout_pad; pub use self::pages::layout_document; pub use self::repeat::layout_repeat; pub use self::shapes::{ - layout_circle, layout_ellipse, layout_line, layout_path, layout_polygon, layout_rect, - layout_square, + layout_circle, layout_curve, layout_ellipse, layout_line, layout_path, + layout_polygon, layout_rect, layout_square, }; pub use self::stack::layout_stack; pub use self::transforms::{layout_move, layout_rotate, layout_scale, layout_skew}; diff --git a/crates/typst-layout/src/shapes.rs b/crates/typst-layout/src/shapes.rs index 2044c917e..7c56bf763 100644 --- a/crates/typst-layout/src/shapes.rs +++ b/crates/typst-layout/src/shapes.rs @@ -1,6 +1,6 @@ use std::f64::consts::SQRT_2; -use kurbo::ParamCurveExtrema; +use kurbo::{CubicBez, ParamCurveExtrema}; use typst_library::diag::{bail, SourceResult}; use typst_library::engine::Engine; use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain}; @@ -10,8 +10,9 @@ use typst_library::layout::{ Sides, Size, }; use typst_library::visualize::{ - CircleElem, EllipseElem, FillRule, FixedStroke, Geometry, LineElem, Paint, Path, - PathElem, PathVertex, PolygonElem, RectElem, Shape, SquareElem, Stroke, + CircleElem, CloseMode, Curve, CurveComponent, CurveElem, EllipseElem, FillRule, + FixedStroke, Geometry, LineElem, Paint, PathElem, PathVertex, PolygonElem, RectElem, + Shape, SquareElem, Stroke, }; use typst_syntax::Span; use typst_utils::{Get, Numeric}; @@ -71,8 +72,8 @@ pub fn layout_path( // Only create a path if there are more than zero points. // Construct a closed path given all points. - let mut path = Path::new(); - path.move_to(points[0]); + let mut curve = Curve::new(); + curve.move_(points[0]); let mut add_cubic = |from_point: Point, to_point: Point, @@ -80,7 +81,7 @@ pub fn layout_path( to: PathVertex| { let from_control_point = resolve(from.control_point_from()) + from_point; let to_control_point = resolve(to.control_point_to()) + to_point; - path.cubic_to(from_control_point, to_control_point, to_point); + curve.cubic(from_control_point, to_control_point, to_point); let p0 = kurbo::Point::new(from_point.x.to_raw(), from_point.y.to_raw()); let p1 = kurbo::Point::new( @@ -111,7 +112,7 @@ pub fn layout_path( let to_point = points[0]; add_cubic(from_point, to_point, from, to); - path.close_path(); + curve.close(); } if !size.is_finite() { @@ -129,7 +130,7 @@ pub fn layout_path( let mut frame = Frame::soft(size); let shape = Shape { - geometry: Geometry::Path(path), + geometry: Geometry::Curve(curve), stroke, fill, fill_rule, @@ -138,6 +139,256 @@ pub fn layout_path( Ok(frame) } +/// Layout the curve. +#[typst_macros::time(span = elem.span())] +pub fn layout_curve( + elem: &Packed, + _: &mut Engine, + _: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult { + let mut builder = CurveBuilder::new(region, styles); + + for item in elem.components() { + match item { + CurveComponent::Move(element) => { + let relative = element.relative(styles); + let point = builder.resolve_point(element.start, relative); + builder.move_(point); + } + + CurveComponent::Line(element) => { + let relative = element.relative(styles); + let point = builder.resolve_point(element.end, relative); + builder.line(point); + } + + CurveComponent::Quad(element) => { + let relative = element.relative(styles); + let end = builder.resolve_point(element.end, relative); + let control = match element.control { + Smart::Auto => { + control_c2q(builder.last_point, builder.last_control_from) + } + Smart::Custom(Some(p)) => builder.resolve_point(p, relative), + Smart::Custom(None) => end, + }; + builder.quad(control, end); + } + + CurveComponent::Cubic(element) => { + let relative = element.relative(styles); + let end = builder.resolve_point(element.end, relative); + let c1 = match element.control_start { + Some(Smart::Custom(p)) => builder.resolve_point(p, relative), + Some(Smart::Auto) => builder.last_control_from, + None => builder.last_point, + }; + let c2 = match element.control_end { + Some(p) => builder.resolve_point(p, relative), + None => end, + }; + builder.cubic(c1, c2, end); + } + + CurveComponent::Close(element) => { + builder.close(element.mode(styles)); + } + } + } + + let (curve, size) = builder.finish(); + if curve.is_empty() { + return Ok(Frame::soft(size)); + } + + if !size.is_finite() { + bail!(elem.span(), "cannot create curve with infinite size"); + } + + // 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, + Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default), + }; + + let mut frame = Frame::soft(size); + let shape = Shape { + geometry: Geometry::Curve(curve), + stroke, + fill, + fill_rule, + }; + frame.push(Point::zero(), FrameItem::Shape(shape, elem.span())); + Ok(frame) +} + +/// Builds a `Curve` from a [`CurveElem`]'s parts. +struct CurveBuilder<'a> { + /// The output curve. + curve: Curve, + /// The curve's bounds. + size: Size, + /// The region relative to which points are resolved. + region: Region, + /// The styles for the curve. + styles: StyleChain<'a>, + /// The next start point. + start_point: Point, + /// Mirror of the first cubic start control point (for closing). + start_control_into: Point, + /// The point we previously ended on. + last_point: Point, + /// Mirror of the last cubic control point (for auto control points). + last_control_from: Point, + /// Whether a component has been start. This does not mean that something + /// has been added to `self.curve` yet. + is_started: bool, + /// Whether anything was added to `self.curve` for the current component. + is_empty: bool, +} + +impl<'a> CurveBuilder<'a> { + /// Create a new curve builder. + fn new(region: Region, styles: StyleChain<'a>) -> Self { + Self { + curve: Curve::new(), + size: Size::zero(), + region, + styles, + start_point: Point::zero(), + start_control_into: Point::zero(), + last_point: Point::zero(), + last_control_from: Point::zero(), + is_started: false, + is_empty: true, + } + } + + /// Finish building, returning the curve and its bounding size. + fn finish(self) -> (Curve, Size) { + (self.curve, self.size) + } + + /// Move to a point, starting a new segment. + fn move_(&mut self, point: Point) { + // Delay calling `curve.move` in case there is another move element + // before any actual drawing. + self.expand_bounds(point); + self.start_point = point; + self.start_control_into = point; + self.last_point = point; + self.last_control_from = point; + self.is_started = true; + } + + /// Add a line segment. + fn line(&mut self, point: Point) { + if self.is_empty { + self.start_component(); + self.start_control_into = self.start_point; + } + self.curve.line(point); + self.expand_bounds(point); + self.last_point = point; + self.last_control_from = point; + } + + /// Add a quadratic curve segment. + fn quad(&mut self, control: Point, end: Point) { + let c1 = control_q2c(self.last_point, control); + let c2 = control_q2c(end, control); + self.cubic(c1, c2, end); + } + + /// Add a cubic curve segment. + fn cubic(&mut self, c1: Point, c2: Point, end: Point) { + if self.is_empty { + self.start_component(); + self.start_control_into = mirror_c(self.start_point, c1); + } + self.curve.cubic(c1, c2, end); + + let p0 = point_to_kurbo(self.last_point); + let p1 = point_to_kurbo(c1); + let p2 = point_to_kurbo(c2); + let p3 = point_to_kurbo(end); + let extrema = CubicBez::new(p0, p1, p2, p3).bounding_box(); + self.size.x.set_max(Abs::raw(extrema.x1)); + self.size.y.set_max(Abs::raw(extrema.y1)); + + self.last_point = end; + self.last_control_from = mirror_c(end, c2); + } + + /// Close the curve if it was opened. + fn close(&mut self, mode: CloseMode) { + if self.is_started && !self.is_empty { + if mode == CloseMode::Smooth { + self.cubic( + self.last_control_from, + self.start_control_into, + self.start_point, + ); + } + self.curve.close(); + self.last_point = self.start_point; + self.last_control_from = self.start_point; + } + self.is_started = false; + self.is_empty = true; + } + + /// Push the initial move component. + fn start_component(&mut self) { + self.curve.move_(self.start_point); + self.is_empty = false; + self.is_started = true; + } + + /// Expand the curve's bounding box. + fn expand_bounds(&mut self, point: Point) { + self.size.x.set_max(point.x); + self.size.y.set_max(point.y); + } + + /// Resolve the point relative to the region. + fn resolve_point(&self, point: Axes, relative: bool) -> Point { + let mut p = point + .resolve(self.styles) + .zip_map(self.region.size, Rel::relative_to) + .to_point(); + if relative { + p += self.last_point; + } + p + } +} + +/// Convert a cubic control point into a quadratic one. +fn control_c2q(p: Point, c: Point) -> Point { + 1.5 * c - 0.5 * p +} + +/// Convert a quadratic control point into a cubic one. +fn control_q2c(p: Point, c: Point) -> Point { + (p + 2.0 * c) / 3.0 +} + +/// Mirror a control point. +fn mirror_c(p: Point, c: Point) -> Point { + 2.0 * p - c +} + +/// Convert a point to a `kurbo::Point`. +fn point_to_kurbo(point: Point) -> kurbo::Point { + kurbo::Point::new(point.x.to_raw(), point.y.to_raw()) +} + /// Layout the polygon. #[typst_macros::time(span = elem.span())] pub fn layout_polygon( @@ -160,7 +411,7 @@ pub fn layout_polygon( let mut frame = Frame::hard(size); - // Only create a path if there are more than zero points. + // Only create a curve if there are more than zero points. if points.is_empty() { return Ok(frame); } @@ -174,16 +425,16 @@ pub fn layout_polygon( Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default), }; - // Construct a closed path given all points. - let mut path = Path::new(); - path.move_to(points[0]); + // Construct a closed curve given all points. + let mut curve = Curve::new(); + curve.move_(points[0]); for &point in &points[1..] { - path.line_to(point); + curve.line(point); } - path.close_path(); + curve.close(); let shape = Shape { - geometry: Geometry::Path(path), + geometry: Geometry::Curve(curve), stroke, fill, fill_rule, @@ -409,7 +660,7 @@ fn layout_shape( let size = frame.size() + outset.sum_by_axis(); let pos = Point::new(-outset.left, -outset.top); let shape = Shape { - geometry: Geometry::Path(Path::ellipse(size)), + geometry: Geometry::Curve(Curve::ellipse(size)), fill, stroke: stroke.left, fill_rule: FillRule::default(), @@ -448,13 +699,13 @@ fn quadratic_size(region: Region) -> Option { } } -/// Creates a new rectangle as a path. +/// Creates a new rectangle as a curve. pub fn clip_rect( size: Size, radius: &Corners>, stroke: &Sides>, outset: &Sides>, -) -> Path { +) -> Curve { let outset = outset.relative_to(size); let size = size + outset.sum_by_axis(); @@ -468,26 +719,30 @@ pub fn clip_rect( let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius)); let corners = corners_control_points(size, &radius, stroke, &stroke_widths); - let mut path = Path::new(); + let mut curve = Curve::new(); if corners.top_left.arc_inner() { - path.arc_move( + curve.arc_move( corners.top_left.start_inner(), corners.top_left.center_inner(), corners.top_left.end_inner(), ); } else { - path.move_to(corners.top_left.center_inner()); + curve.move_(corners.top_left.center_inner()); } for corner in [&corners.top_right, &corners.bottom_right, &corners.bottom_left] { if corner.arc_inner() { - path.arc_line(corner.start_inner(), corner.center_inner(), corner.end_inner()) + curve.arc_line( + corner.start_inner(), + corner.center_inner(), + corner.end_inner(), + ) } else { - path.line_to(corner.center_inner()); + curve.line(corner.center_inner()); } } - path.close_path(); - path.translate(Point::new(-outset.left, -outset.top)); - path + curve.close(); + curve.translate(Point::new(-outset.left, -outset.top)); + curve } /// Add a fill and stroke with optional radius and outset to the frame. @@ -592,25 +847,25 @@ fn segmented_rect( // fill shape with inner curve if let Some(fill) = fill { - let mut path = Path::new(); + let mut curve = Curve::new(); let c = corners.get_ref(Corner::TopLeft); if c.arc() { - path.arc_move(c.start(), c.center(), c.end()); + curve.arc_move(c.start(), c.center(), c.end()); } else { - path.move_to(c.center()); + curve.move_(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()); + curve.arc_line(c.start(), c.center(), c.end()); } else { - path.line_to(c.center()); + curve.line(c.center()); } } - path.close_path(); + curve.close(); res.push(Shape { - geometry: Geometry::Path(path), + geometry: Geometry::Curve(curve), fill: Some(fill), fill_rule: FillRule::default(), stroke: None, @@ -649,18 +904,18 @@ fn segmented_rect( res } -fn path_segment( +fn curve_segment( start: Corner, end: Corner, corners: &Corners, - path: &mut Path, + curve: &mut Curve, ) { // create start corner let c = corners.get_ref(start); if start == end || !c.arc() { - path.move_to(c.end()); + curve.move_(c.end()); } else { - path.arc_move(c.mid(), c.center(), c.end()); + curve.arc_move(c.mid(), c.center(), c.end()); } // create corners between start and end @@ -668,9 +923,9 @@ fn path_segment( while current != end { let c = corners.get_ref(current); if c.arc() { - path.arc_line(c.start(), c.center(), c.end()); + curve.arc_line(c.start(), c.center(), c.end()); } else { - path.line_to(c.end()); + curve.line(c.end()); } current = current.next_cw(); } @@ -678,11 +933,11 @@ fn path_segment( // create end corner let c = corners.get_ref(end); if !c.arc() { - path.line_to(c.start()); + curve.line(c.start()); } else if start == end { - path.arc_line(c.start(), c.center(), c.end()); + curve.arc_line(c.start(), c.center(), c.end()); } else { - path.arc_line(c.start(), c.center(), c.mid()); + curve.arc_line(c.start(), c.center(), c.mid()); } } @@ -739,11 +994,11 @@ fn stroke_segment( stroke: FixedStroke, ) -> Shape { // Create start corner. - let mut path = Path::new(); - path_segment(start, end, corners, &mut path); + let mut curve = Curve::new(); + curve_segment(start, end, corners, &mut curve); Shape { - geometry: Geometry::Path(path), + geometry: Geometry::Curve(curve), stroke: Some(stroke), fill: None, fill_rule: FillRule::default(), @@ -757,7 +1012,7 @@ fn fill_segment( corners: &Corners, stroke: &FixedStroke, ) -> Shape { - let mut path = Path::new(); + let mut curve = Curve::new(); // create the start corner // begin on the inside and finish on the outside @@ -765,33 +1020,33 @@ fn fill_segment( // 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()); + curve.move_(c.end_inner()); + curve.line(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()); + curve.arc_move(c.end_inner(), c.center_inner(), c.mid_inner()); } else { - path.move_to(c.end_inner()); + curve.move_(c.end_inner()); } if c.arc_outer() { - path.arc_line(c.mid_outer(), c.center_outer(), c.end_outer()); + curve.arc_line(c.mid_outer(), c.center_outer(), c.end_outer()); } else { - path.line_to(c.outer()); - path.line_to(c.end_outer()); + curve.line(c.outer()); + curve.line(c.end_outer()); } } - // create the clockwise outside path for the corners between start and end + // create the clockwise outside curve 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()); + curve.arc_line(c.start_outer(), c.center_outer(), c.end_outer()); } else { - path.line_to(c.outer()); + curve.line(c.outer()); } current = current.next_cw(); } @@ -803,46 +1058,46 @@ fn fill_segment( 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()); + curve.arc_line(c.start_outer(), c.center_outer(), c.end_outer()); } else { - path.line_to(c.outer()); - path.line_to(c.end_outer()); + curve.line(c.outer()); + curve.line(c.end_outer()); } if c.arc_inner() { - path.arc_line(c.end_inner(), c.center_inner(), c.start_inner()); + curve.arc_line(c.end_inner(), c.center_inner(), c.start_inner()); } else { - path.line_to(c.center_inner()); + curve.line(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()); + curve.arc_line(c.start_outer(), c.center_outer(), c.mid_outer()); } else { - path.line_to(c.outer()); + curve.line(c.outer()); } if c.arc_inner() { - path.arc_line(c.mid_inner(), c.center_inner(), c.start_inner()); + curve.arc_line(c.mid_inner(), c.center_inner(), c.start_inner()); } else { - path.line_to(c.center_inner()); + curve.line(c.center_inner()); } } - // create the counterclockwise inside path for the corners between start and end + // create the counterclockwise inside curve 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()); + curve.arc_line(c.end_inner(), c.center_inner(), c.start_inner()); } else { - path.line_to(c.center_inner()); + curve.line(c.center_inner()); } current = current.next_ccw(); } - path.close_path(); + curve.close(); Shape { - geometry: Geometry::Path(path), + geometry: Geometry::Curve(curve), stroke: None, fill: Some(stroke.paint.clone()), fill_rule: FillRule::default(), @@ -1027,25 +1282,25 @@ impl ControlPoints { } /// Helper to draw arcs with bezier curves. -trait PathExt { +trait CurveExt { 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); } -impl PathExt for Path { +impl CurveExt for Curve { 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); + self.cubic(arc[0], arc[1], end); } fn arc_move(&mut self, start: Point, center: Point, end: Point) { - self.move_to(start); + self.move_(start); self.arc(start, center, end); } fn arc_line(&mut self, start: Point, center: Point, end: Point) { - self.line_to(start); + self.line(start); self.arc(start, center, end); } } diff --git a/crates/typst-library/src/layout/frame.rs b/crates/typst-library/src/layout/frame.rs index fc8634e8f..e57eb27e8 100644 --- a/crates/typst-library/src/layout/frame.rs +++ b/crates/typst-library/src/layout/frame.rs @@ -15,7 +15,7 @@ use crate::layout::{ }; use crate::model::{Destination, LinkElem}; use crate::text::TextItem; -use crate::visualize::{Color, FixedStroke, Geometry, Image, Paint, Path, Shape}; +use crate::visualize::{Color, Curve, FixedStroke, Geometry, Image, Paint, Shape}; /// A finished layout with items at fixed positions. #[derive(Default, Clone, Hash)] @@ -374,14 +374,14 @@ impl Frame { } } - /// Clip the contents of a frame to a clip path. + /// Clip the contents of a frame to a clip curve. /// - /// The clip path can be the size of the frame in the case of a - /// rectangular frame. In the case of a frame with rounded corner, - /// this should be a path that matches the frame's outline. - pub fn clip(&mut self, clip_path: Path) { + /// The clip curve can be the size of the frame in the case of a rectangular + /// frame. In the case of a frame with rounded corner, this should be a + /// curve that matches the frame's outline. + pub fn clip(&mut self, clip_curve: Curve) { if !self.is_empty() { - self.group(|g| g.clip_path = Some(clip_path)); + self.group(|g| g.clip = Some(clip_curve)); } } @@ -447,7 +447,7 @@ impl Frame { self.push( pos - Point::splat(radius), FrameItem::Shape( - Geometry::Path(Path::ellipse(Size::splat(2.0 * radius))) + Geometry::Curve(Curve::ellipse(Size::splat(2.0 * radius))) .filled(Color::GREEN), Span::detached(), ), @@ -544,8 +544,8 @@ pub struct GroupItem { pub frame: Frame, /// A transformation to apply to the group. pub transform: Transform, - /// Whether the frame should be a clipping boundary. - pub clip_path: Option, + /// A curve which should be used to clip the group. + pub clip: Option, /// The group's label. pub label: Option