New curve element that supersedes path (#5323)

Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
Emmanuel Lesueur 2024-12-18 16:58:57 +01:00 committed by GitHub
parent 24c08a7ec0
commit 257764181e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 1132 additions and 312 deletions

View File

@ -10,7 +10,7 @@ use typst_library::layout::{
use typst_library::loading::Readable; use typst_library::loading::Readable;
use typst_library::text::families; use typst_library::text::families;
use typst_library::visualize::{ use typst_library::visualize::{
Image, ImageElem, ImageFit, ImageFormat, Path, RasterFormat, VectorFormat, Curve, Image, ImageElem, ImageFit, ImageFormat, RasterFormat, VectorFormat,
}; };
/// Layout the image. /// Layout the image.
@ -113,7 +113,7 @@ pub fn layout_image(
// Create a clipping group if only part of the image should be visible. // Create a clipping group if only part of the image should be visible.
if fit == ImageFit::Cover && !target.fits(fitted) { if fit == ImageFit::Cover && !target.fits(fitted) {
frame.clip(Path::rect(frame.size())); frame.clip(Curve::rect(frame.size()));
} }
Ok(frame) Ok(frame)

View File

@ -23,8 +23,8 @@ pub use self::pad::layout_pad;
pub use self::pages::layout_document; pub use self::pages::layout_document;
pub use self::repeat::layout_repeat; pub use self::repeat::layout_repeat;
pub use self::shapes::{ pub use self::shapes::{
layout_circle, layout_ellipse, layout_line, layout_path, layout_polygon, layout_rect, layout_circle, layout_curve, layout_ellipse, layout_line, layout_path,
layout_square, layout_polygon, layout_rect, layout_square,
}; };
pub use self::stack::layout_stack; pub use self::stack::layout_stack;
pub use self::transforms::{layout_move, layout_rotate, layout_scale, layout_skew}; pub use self::transforms::{layout_move, layout_rotate, layout_scale, layout_skew};

View File

@ -1,6 +1,6 @@
use std::f64::consts::SQRT_2; use std::f64::consts::SQRT_2;
use kurbo::ParamCurveExtrema; use kurbo::{CubicBez, ParamCurveExtrema};
use typst_library::diag::{bail, SourceResult}; use typst_library::diag::{bail, SourceResult};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain}; use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain};
@ -10,8 +10,9 @@ use typst_library::layout::{
Sides, Size, Sides, Size,
}; };
use typst_library::visualize::{ use typst_library::visualize::{
CircleElem, EllipseElem, FillRule, FixedStroke, Geometry, LineElem, Paint, Path, CircleElem, CloseMode, Curve, CurveComponent, CurveElem, EllipseElem, FillRule,
PathElem, PathVertex, PolygonElem, RectElem, Shape, SquareElem, Stroke, FixedStroke, Geometry, LineElem, Paint, PathElem, PathVertex, PolygonElem, RectElem,
Shape, SquareElem, Stroke,
}; };
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::{Get, Numeric}; use typst_utils::{Get, Numeric};
@ -71,8 +72,8 @@ pub fn layout_path(
// Only create a path if there are more than zero points. // Only create a path if there are more than zero points.
// Construct a closed path given all points. // Construct a closed path given all points.
let mut path = Path::new(); let mut curve = Curve::new();
path.move_to(points[0]); curve.move_(points[0]);
let mut add_cubic = |from_point: Point, let mut add_cubic = |from_point: Point,
to_point: Point, to_point: Point,
@ -80,7 +81,7 @@ pub fn layout_path(
to: PathVertex| { to: PathVertex| {
let from_control_point = resolve(from.control_point_from()) + from_point; let from_control_point = resolve(from.control_point_from()) + from_point;
let to_control_point = resolve(to.control_point_to()) + to_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 p0 = kurbo::Point::new(from_point.x.to_raw(), from_point.y.to_raw());
let p1 = kurbo::Point::new( let p1 = kurbo::Point::new(
@ -111,7 +112,7 @@ pub fn layout_path(
let to_point = points[0]; let to_point = points[0];
add_cubic(from_point, to_point, from, to); add_cubic(from_point, to_point, from, to);
path.close_path(); curve.close();
} }
if !size.is_finite() { if !size.is_finite() {
@ -129,7 +130,7 @@ pub fn layout_path(
let mut frame = Frame::soft(size); let mut frame = Frame::soft(size);
let shape = Shape { let shape = Shape {
geometry: Geometry::Path(path), geometry: Geometry::Curve(curve),
stroke, stroke,
fill, fill,
fill_rule, fill_rule,
@ -138,6 +139,256 @@ pub fn layout_path(
Ok(frame) Ok(frame)
} }
/// Layout the curve.
#[typst_macros::time(span = elem.span())]
pub fn layout_curve(
elem: &Packed<CurveElem>,
_: &mut Engine,
_: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
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<Rel>, 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. /// Layout the polygon.
#[typst_macros::time(span = elem.span())] #[typst_macros::time(span = elem.span())]
pub fn layout_polygon( pub fn layout_polygon(
@ -160,7 +411,7 @@ pub fn layout_polygon(
let mut frame = Frame::hard(size); 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() { if points.is_empty() {
return Ok(frame); return Ok(frame);
} }
@ -174,16 +425,16 @@ pub fn layout_polygon(
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default), Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
}; };
// Construct a closed path given all points. // Construct a closed curve given all points.
let mut path = Path::new(); let mut curve = Curve::new();
path.move_to(points[0]); curve.move_(points[0]);
for &point in &points[1..] { for &point in &points[1..] {
path.line_to(point); curve.line(point);
} }
path.close_path(); curve.close();
let shape = Shape { let shape = Shape {
geometry: Geometry::Path(path), geometry: Geometry::Curve(curve),
stroke, stroke,
fill, fill,
fill_rule, fill_rule,
@ -409,7 +660,7 @@ fn layout_shape(
let size = frame.size() + outset.sum_by_axis(); let size = frame.size() + outset.sum_by_axis();
let pos = Point::new(-outset.left, -outset.top); let pos = Point::new(-outset.left, -outset.top);
let shape = Shape { let shape = Shape {
geometry: Geometry::Path(Path::ellipse(size)), geometry: Geometry::Curve(Curve::ellipse(size)),
fill, fill,
stroke: stroke.left, stroke: stroke.left,
fill_rule: FillRule::default(), fill_rule: FillRule::default(),
@ -448,13 +699,13 @@ fn quadratic_size(region: Region) -> Option<Abs> {
} }
} }
/// Creates a new rectangle as a path. /// Creates a new rectangle as a curve.
pub fn clip_rect( pub fn clip_rect(
size: Size, size: Size,
radius: &Corners<Rel<Abs>>, radius: &Corners<Rel<Abs>>,
stroke: &Sides<Option<FixedStroke>>, stroke: &Sides<Option<FixedStroke>>,
outset: &Sides<Rel<Abs>>, outset: &Sides<Rel<Abs>>,
) -> Path { ) -> Curve {
let outset = outset.relative_to(size); let outset = outset.relative_to(size);
let size = size + outset.sum_by_axis(); 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 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 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() { if corners.top_left.arc_inner() {
path.arc_move( curve.arc_move(
corners.top_left.start_inner(), corners.top_left.start_inner(),
corners.top_left.center_inner(), corners.top_left.center_inner(),
corners.top_left.end_inner(), corners.top_left.end_inner(),
); );
} else { } 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] { for corner in [&corners.top_right, &corners.bottom_right, &corners.bottom_left] {
if corner.arc_inner() { 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 { } else {
path.line_to(corner.center_inner()); curve.line(corner.center_inner());
} }
} }
path.close_path(); curve.close();
path.translate(Point::new(-outset.left, -outset.top)); curve.translate(Point::new(-outset.left, -outset.top));
path curve
} }
/// Add a fill and stroke with optional radius and outset to the frame. /// 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 // fill shape with inner curve
if let Some(fill) = fill { if let Some(fill) = fill {
let mut path = Path::new(); let mut curve = Curve::new();
let c = corners.get_ref(Corner::TopLeft); let c = corners.get_ref(Corner::TopLeft);
if c.arc() { if c.arc() {
path.arc_move(c.start(), c.center(), c.end()); curve.arc_move(c.start(), c.center(), c.end());
} else { } else {
path.move_to(c.center()); curve.move_(c.center());
}; };
for corner in [Corner::TopRight, Corner::BottomRight, Corner::BottomLeft] { for corner in [Corner::TopRight, Corner::BottomRight, Corner::BottomLeft] {
let c = corners.get_ref(corner); let c = corners.get_ref(corner);
if c.arc() { if c.arc() {
path.arc_line(c.start(), c.center(), c.end()); curve.arc_line(c.start(), c.center(), c.end());
} else { } else {
path.line_to(c.center()); curve.line(c.center());
} }
} }
path.close_path(); curve.close();
res.push(Shape { res.push(Shape {
geometry: Geometry::Path(path), geometry: Geometry::Curve(curve),
fill: Some(fill), fill: Some(fill),
fill_rule: FillRule::default(), fill_rule: FillRule::default(),
stroke: None, stroke: None,
@ -649,18 +904,18 @@ fn segmented_rect(
res res
} }
fn path_segment( fn curve_segment(
start: Corner, start: Corner,
end: Corner, end: Corner,
corners: &Corners<ControlPoints>, corners: &Corners<ControlPoints>,
path: &mut Path, curve: &mut Curve,
) { ) {
// create start corner // create start corner
let c = corners.get_ref(start); let c = corners.get_ref(start);
if start == end || !c.arc() { if start == end || !c.arc() {
path.move_to(c.end()); curve.move_(c.end());
} else { } 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 // create corners between start and end
@ -668,9 +923,9 @@ fn path_segment(
while current != end { while current != end {
let c = corners.get_ref(current); let c = corners.get_ref(current);
if c.arc() { if c.arc() {
path.arc_line(c.start(), c.center(), c.end()); curve.arc_line(c.start(), c.center(), c.end());
} else { } else {
path.line_to(c.end()); curve.line(c.end());
} }
current = current.next_cw(); current = current.next_cw();
} }
@ -678,11 +933,11 @@ fn path_segment(
// create end corner // create end corner
let c = corners.get_ref(end); let c = corners.get_ref(end);
if !c.arc() { if !c.arc() {
path.line_to(c.start()); curve.line(c.start());
} else if start == end { } else if start == end {
path.arc_line(c.start(), c.center(), c.end()); curve.arc_line(c.start(), c.center(), c.end());
} else { } 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, stroke: FixedStroke,
) -> Shape { ) -> Shape {
// Create start corner. // Create start corner.
let mut path = Path::new(); let mut curve = Curve::new();
path_segment(start, end, corners, &mut path); curve_segment(start, end, corners, &mut curve);
Shape { Shape {
geometry: Geometry::Path(path), geometry: Geometry::Curve(curve),
stroke: Some(stroke), stroke: Some(stroke),
fill: None, fill: None,
fill_rule: FillRule::default(), fill_rule: FillRule::default(),
@ -757,7 +1012,7 @@ fn fill_segment(
corners: &Corners<ControlPoints>, corners: &Corners<ControlPoints>,
stroke: &FixedStroke, stroke: &FixedStroke,
) -> Shape { ) -> Shape {
let mut path = Path::new(); let mut curve = Curve::new();
// create the start corner // create the start corner
// begin on the inside and finish on the outside // begin on the inside and finish on the outside
@ -765,33 +1020,33 @@ fn fill_segment(
// half corner if different // half corner if different
if start == end { if start == end {
let c = corners.get_ref(start); let c = corners.get_ref(start);
path.move_to(c.end_inner()); curve.move_(c.end_inner());
path.line_to(c.end_outer()); curve.line(c.end_outer());
} else { } else {
let c = corners.get_ref(start); let c = corners.get_ref(start);
if c.arc_inner() { 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 { } else {
path.move_to(c.end_inner()); curve.move_(c.end_inner());
} }
if c.arc_outer() { 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 { } else {
path.line_to(c.outer()); curve.line(c.outer());
path.line_to(c.end_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(); let mut current = start.next_cw();
while current != end { while current != end {
let c = corners.get_ref(current); let c = corners.get_ref(current);
if c.arc_outer() { 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 { } else {
path.line_to(c.outer()); curve.line(c.outer());
} }
current = current.next_cw(); current = current.next_cw();
} }
@ -803,46 +1058,46 @@ fn fill_segment(
if start == end { if start == end {
let c = corners.get_ref(end); let c = corners.get_ref(end);
if c.arc_outer() { 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 { } else {
path.line_to(c.outer()); curve.line(c.outer());
path.line_to(c.end_outer()); curve.line(c.end_outer());
} }
if c.arc_inner() { 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 { } else {
path.line_to(c.center_inner()); curve.line(c.center_inner());
} }
} else { } else {
let c = corners.get_ref(end); let c = corners.get_ref(end);
if c.arc_outer() { 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 { } else {
path.line_to(c.outer()); curve.line(c.outer());
} }
if c.arc_inner() { 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 { } 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(); let mut current = end.next_ccw();
while current != start { while current != start {
let c = corners.get_ref(current); let c = corners.get_ref(current);
if c.arc_inner() { 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 { } else {
path.line_to(c.center_inner()); curve.line(c.center_inner());
} }
current = current.next_ccw(); current = current.next_ccw();
} }
path.close_path(); curve.close();
Shape { Shape {
geometry: Geometry::Path(path), geometry: Geometry::Curve(curve),
stroke: None, stroke: None,
fill: Some(stroke.paint.clone()), fill: Some(stroke.paint.clone()),
fill_rule: FillRule::default(), fill_rule: FillRule::default(),
@ -1027,25 +1282,25 @@ impl ControlPoints {
} }
/// Helper to draw arcs with bezier curves. /// Helper to draw arcs with bezier curves.
trait PathExt { trait CurveExt {
fn arc(&mut self, start: Point, center: Point, end: Point); fn arc(&mut self, start: Point, center: Point, end: Point);
fn arc_move(&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); 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) { fn arc(&mut self, start: Point, center: Point, end: Point) {
let arc = bezier_arc_control(start, center, end); 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) { fn arc_move(&mut self, start: Point, center: Point, end: Point) {
self.move_to(start); self.move_(start);
self.arc(start, center, end); self.arc(start, center, end);
} }
fn arc_line(&mut self, start: Point, center: Point, end: Point) { fn arc_line(&mut self, start: Point, center: Point, end: Point) {
self.line_to(start); self.line(start);
self.arc(start, center, end); self.arc(start, center, end);
} }
} }

View File

@ -15,7 +15,7 @@ use crate::layout::{
}; };
use crate::model::{Destination, LinkElem}; use crate::model::{Destination, LinkElem};
use crate::text::TextItem; 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. /// A finished layout with items at fixed positions.
#[derive(Default, Clone, Hash)] #[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 /// The clip curve can be the size of the frame in the case of a rectangular
/// rectangular frame. In the case of a frame with rounded corner, /// frame. In the case of a frame with rounded corner, this should be a
/// this should be a path that matches the frame's outline. /// curve that matches the frame's outline.
pub fn clip(&mut self, clip_path: Path) { pub fn clip(&mut self, clip_curve: Curve) {
if !self.is_empty() { 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( self.push(
pos - Point::splat(radius), pos - Point::splat(radius),
FrameItem::Shape( FrameItem::Shape(
Geometry::Path(Path::ellipse(Size::splat(2.0 * radius))) Geometry::Curve(Curve::ellipse(Size::splat(2.0 * radius)))
.filled(Color::GREEN), .filled(Color::GREEN),
Span::detached(), Span::detached(),
), ),
@ -544,8 +544,8 @@ pub struct GroupItem {
pub frame: Frame, pub frame: Frame,
/// A transformation to apply to the group. /// A transformation to apply to the group.
pub transform: Transform, pub transform: Transform,
/// Whether the frame should be a clipping boundary. /// A curve which should be used to clip the group.
pub clip_path: Option<Path>, pub clip: Option<Curve>,
/// The group's label. /// The group's label.
pub label: Option<Label>, pub label: Option<Label>,
/// The group's logical parent. All elements in this group are logically /// The group's logical parent. All elements in this group are logically
@ -559,7 +559,7 @@ impl GroupItem {
Self { Self {
frame, frame,
transform: Transform::identity(), transform: Transform::identity(),
clip_path: None, clip: None,
label: None, label: None,
parent: None, parent: None,
} }

View File

@ -22,8 +22,8 @@ use crate::layout::{
use crate::math::EquationElem; use crate::math::EquationElem;
use crate::model::{DocumentInfo, EnumElem, ListElem, TableElem}; use crate::model::{DocumentInfo, EnumElem, ListElem, TableElem};
use crate::visualize::{ use crate::visualize::{
CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem, CircleElem, CurveElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem,
SquareElem, RectElem, SquareElem,
}; };
use crate::World; use crate::World;
@ -241,6 +241,15 @@ routines! {
region: Region, region: Region,
) -> SourceResult<Frame> ) -> SourceResult<Frame>
/// Lays out a [`CurveElem`].
fn layout_curve(
elem: &Packed<CurveElem>,
_: &mut Engine,
_: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame>
/// Lays out a [`PathElem`]. /// Lays out a [`PathElem`].
fn layout_path( fn layout_path(
elem: &Packed<PathElem>, elem: &Packed<PathElem>,

View File

@ -0,0 +1,532 @@
use kurbo::ParamCurveExtrema;
use typst_macros::{scope, Cast};
use typst_utils::Numeric;
use crate::diag::{bail, HintedStrResult, HintedString, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, Content, NativeElement, Packed, Show, Smart, StyleChain,
};
use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size};
use crate::visualize::{FillRule, Paint, Stroke};
/// A curve consisting of movements, lines, and Beziér segments.
///
/// At any point in time, there is a conceptual pen or cursor.
/// - Move elements move the cursor without drawing.
/// - Line/Quadratic/Cubic elements draw a segment from the cursor to a new
/// position, potentially with control point for a Beziér curve.
/// - Close elements draw a straight or smooth line back to the start of the
/// curve or the latest preceding move segment.
///
/// For layout purposes, the bounding box of the curve is a tight rectangle
/// containing all segments as well as the point `{(0pt, 0pt)}`.
///
/// Positions may be specified absolutely (i.e. relatively to `{(0pt, 0pt)}`),
/// or relative to the current pen/cursor position, that is, the position where
/// the previous segment ended.
///
/// Beziér curve control points can be skipped by passing `{none}` or
/// automatically mirrored from the preceding segment by passing `{auto}`.
///
/// # Example
/// ```example
/// #curve(
/// fill: blue.lighten(80%),
/// stroke: blue,
/// curve.move((0pt, 50pt)),
/// curve.line((100pt, 50pt)),
/// curve.cubic(none, (90pt, 0pt), (50pt, 0pt)),
/// curve.close(),
/// )
/// ```
#[elem(scope, Show)]
pub struct CurveElem {
/// How to fill the curve.
///
/// When setting a fill, the default stroke disappears. To create a
/// rectangle with both fill and stroke, you have to configure both.
pub fill: Option<Paint>,
/// The drawing rule used to fill the curve.
///
/// ```example
/// // We use `.with` to get a new
/// // function that has the common
/// // arguments pre-applied.
/// #let star = curve.with(
/// fill: red,
/// curve.move((25pt, 0pt)),
/// curve.line((10pt, 50pt)),
/// curve.line((50pt, 20pt)),
/// curve.line((0pt, 20pt)),
/// curve.line((40pt, 50pt)),
/// curve.close(),
/// )
///
/// #star(fill-rule: "non-zero")
/// #star(fill-rule: "even-odd")
/// ```
#[default]
pub fill_rule: FillRule,
/// How to [stroke] the curve. This can be:
///
/// Can be set to `{none}` to disable the stroke or to `{auto}` for a
/// stroke of `{1pt}` black if and if only if no fill is given.
///
/// ```example
/// #let down = curve.line((40pt, 40pt), relative: true)
/// #let up = curve.line((40pt, -40pt), relative: true)
///
/// #curve(
/// stroke: 4pt + gradient.linear(red, blue),
/// down, up, down, up, down,
/// )
/// ```
#[resolve]
#[fold]
pub stroke: Smart<Option<Stroke>>,
/// The components of the curve, in the form of moves, line and Beziér
/// segment, and closes.
#[variadic]
pub components: Vec<CurveComponent>,
}
impl Show for Packed<CurveElem> {
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_curve)
.pack()
.spanned(self.span()))
}
}
#[scope]
impl CurveElem {
#[elem]
type CurveMove;
#[elem]
type CurveLine;
#[elem]
type CurveQuad;
#[elem]
type CurveCubic;
#[elem]
type CurveClose;
}
/// A component used for curve creation.
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum CurveComponent {
Move(Packed<CurveMove>),
Line(Packed<CurveLine>),
Quad(Packed<CurveQuad>),
Cubic(Packed<CurveCubic>),
Close(Packed<CurveClose>),
}
cast! {
CurveComponent,
self => match self {
Self::Move(element) => element.into_value(),
Self::Line(element) => element.into_value(),
Self::Quad(element) => element.into_value(),
Self::Cubic(element) => element.into_value(),
Self::Close(element) => element.into_value(),
},
v: Content => {
v.try_into()?
}
}
impl TryFrom<Content> for CurveComponent {
type Error = HintedString;
fn try_from(value: Content) -> HintedStrResult<Self> {
value
.into_packed::<CurveMove>()
.map(Self::Move)
.or_else(|value| value.into_packed::<CurveLine>().map(Self::Line))
.or_else(|value| value.into_packed::<CurveQuad>().map(Self::Quad))
.or_else(|value| value.into_packed::<CurveCubic>().map(Self::Cubic))
.or_else(|value| value.into_packed::<CurveClose>().map(Self::Close))
.or_else(|_| bail!("expecting a curve element"))
}
}
/// Starts a new curve component.
///
/// If no `curve.move` element is passed, the curve will start at
/// `{(0pt, 0pt)}`.
///
/// ```example
/// #curve(
/// fill: blue.lighten(80%),
/// fill-rule: "even-odd",
/// stroke: blue,
/// curve.line((50pt, 0pt)),
/// curve.line((50pt, 50pt)),
/// curve.line((0pt, 50pt)),
/// curve.close(),
/// curve.move((10pt, 10pt)),
/// curve.line((40pt, 10pt)),
/// curve.line((40pt, 40pt)),
/// curve.line((10pt, 40pt)),
/// curve.close(),
/// )
/// ```
#[elem(name = "move", title = "Curve Move")]
pub struct CurveMove {
/// The starting point for the new component.
#[required]
pub start: Axes<Rel<Length>>,
/// Whether the coordinates are relative to the previous point.
#[default(false)]
pub relative: bool,
}
/// Adds a straight line from the current point to a following one.
///
/// ```example
/// #curve(
/// stroke: blue,
/// curve.line((50pt, 0pt)),
/// curve.line((50pt, 50pt)),
/// curve.line((100pt, 50pt)),
/// curve.line((100pt, 0pt)),
/// curve.line((150pt, 0pt)),
/// )
/// ```
#[elem(name = "line", title = "Curve Line")]
pub struct CurveLine {
/// The point at which the line shall end.
#[required]
pub end: Axes<Rel<Length>>,
/// Whether the coordinates are relative to the previous point.
///
/// ```example
/// #curve(
/// stroke: blue,
/// curve.line((50pt, 0pt), relative: true),
/// curve.line((0pt, 50pt), relative: true),
/// curve.line((50pt, 0pt), relative: true),
/// curve.line((0pt, -50pt), relative: true),
/// curve.line((50pt, 0pt), relative: true),
/// )
/// ```
#[default(false)]
pub relative: bool,
}
/// Adds a quadratic Beziér curve segment from the last point to `end`, using
/// `control` as the control point.
///
/// ```example
/// // Function to illustrate where the control point is.
/// #let mark((x, y)) = place(
/// dx: x - 1pt, dy: y - 1pt,
/// circle(fill: aqua, radius: 2pt),
/// )
///
/// #mark((20pt, 20pt))
///
/// #curve(
/// stroke: blue,
/// curve.move((0pt, 100pt)),
/// curve.quad((20pt, 20pt), (100pt, 0pt)),
/// )
/// ```
#[elem(name = "quad", title = "Curve Quadratic Segment")]
pub struct CurveQuad {
/// The control point of the quadratic Beziér curve.
///
/// - If `{auto}` and this segment follows another quadratic Beziér curve,
/// the previous control point will be mirrored.
/// - If `{none}`, the control point defaults to `end`, and the curve will
/// be a straight line.
///
/// ```example
/// #curve(
/// stroke: 2pt,
/// curve.quad((20pt, 40pt), (40pt, 40pt), relative: true),
/// curve.quad(auto, (40pt, -40pt), relative: true),
/// )
/// ```
#[required]
pub control: Smart<Option<Axes<Rel<Length>>>>,
/// The point at which the segment shall end.
#[required]
pub end: Axes<Rel<Length>>,
/// Whether the `control` and `end` coordinates are relative to the previous
/// point.
#[default(false)]
pub relative: bool,
}
/// Adds a cubic Beziér curve segment from the last point to `end`, using
/// `control-start` and `control-end` as the control points.
///
/// ```example
/// // Function to illustrate where the control points are.
/// #let handle(start, end) = place(
/// line(stroke: red, start: start, end: end)
/// )
///
/// #handle((0pt, 80pt), (10pt, 20pt))
/// #handle((90pt, 60pt), (100pt, 0pt))
///
/// #curve(
/// stroke: blue,
/// curve.move((0pt, 80pt)),
/// curve.cubic((10pt, 20pt), (90pt, 60pt), (100pt, 0pt)),
/// )
/// ```
#[elem(name = "cubic", title = "Curve Cubic Segment")]
pub struct CurveCubic {
/// The control point going out from the start of the curve segment.
///
/// - If `{auto}` and this element follows another `curve.cubic` element,
/// the last control point will be mirrored. In SVG terms, this makes
/// `curve.cubic` behave like the `S` operator instead of the `C` operator.
///
/// - If `{none}`, the curve has no first control point, or equivalently,
/// the control point defaults to the curve's starting point.
///
/// ```example
/// #curve(
/// stroke: blue,
/// curve.move((0pt, 50pt)),
/// // - No start control point
/// // - End control point at `(20pt, 0pt)`
/// // - End point at `(50pt, 0pt)`
/// curve.cubic(none, (20pt, 0pt), (50pt, 0pt)),
/// // - No start control point
/// // - No end control point
/// // - End point at `(50pt, 0pt)`
/// curve.cubic(none, none, (100pt, 50pt)),
/// )
///
/// #curve(
/// stroke: blue,
/// curve.move((0pt, 50pt)),
/// curve.cubic(none, (20pt, 0pt), (50pt, 0pt)),
/// // Passing `auto` instead of `none` means the start control point
/// // mirrors the end control point of the previous curve. Mirror of
/// // `(20pt, 0pt)` w.r.t `(50pt, 0pt)` is `(80pt, 0pt)`.
/// curve.cubic(auto, none, (100pt, 50pt)),
/// )
///
/// #curve(
/// stroke: blue,
/// curve.move((0pt, 50pt)),
/// curve.cubic(none, (20pt, 0pt), (50pt, 0pt)),
/// // `(80pt, 0pt)` is the same as `auto` in this case.
/// curve.cubic((80pt, 0pt), none, (100pt, 50pt)),
/// )
/// ```
#[required]
pub control_start: Option<Smart<Axes<Rel<Length>>>>,
/// The control point going into the end point of the curve segment.
///
/// If set to `{none}`, the curve has no end control point, or equivalently,
/// the control point defaults to the curve's end point.
#[required]
pub control_end: Option<Axes<Rel<Length>>>,
/// The point at which the curve segment shall end.
#[required]
pub end: Axes<Rel<Length>>,
/// Whether the `control-start`, `control-end`, and `end` coordinates are
/// relative to the previous point.
#[default(false)]
pub relative: bool,
}
/// Closes the curve by adding a segment from the last point to the start of the
/// curve (or the last preceding `curve.move` point).
///
/// ```example
/// // We define a function to show the same shape with
/// // both closing modes.
/// #let shape(mode: "smooth") = curve(
/// fill: blue.lighten(80%),
/// stroke: blue,
/// curve.move((0pt, 50pt)),
/// curve.line((100pt, 50pt)),
/// curve.cubic(auto, (90pt, 0pt), (50pt, 0pt)),
/// curve.close(mode: mode),
/// )
///
/// #shape(mode: "smooth")
/// #shape(mode: "straight")
/// ```
#[elem(name = "close", title = "Curve Close")]
pub struct CurveClose {
/// How to close the curve.
pub mode: CloseMode,
}
/// How to close a curve.
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Cast)]
pub enum CloseMode {
/// Closes the curve with a smooth segment that takes into account the
/// control point opposite the start point.
#[default]
Smooth,
/// Closes the curve with a straight line.
Straight,
}
/// A curve consisting of movements, lines, and Beziér segments.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Curve(pub Vec<CurveItem>);
/// An item in a curve.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum CurveItem {
Move(Point),
Line(Point),
Cubic(Point, Point, Point),
Close,
}
impl Curve {
/// Creates an empty curve.
pub const fn new() -> Self {
Self(vec![])
}
/// Creates a curve that describes a rectangle.
pub fn rect(size: Size) -> Self {
let z = Abs::zero();
let point = Point::new;
let mut curve = Self::new();
curve.move_(point(z, z));
curve.line(point(size.x, z));
curve.line(point(size.x, size.y));
curve.line(point(z, size.y));
curve.close();
curve
}
/// Creates a curve that describes an axis-aligned ellipse.
pub fn ellipse(size: Size) -> Self {
// https://stackoverflow.com/a/2007782
let z = Abs::zero();
let rx = size.x / 2.0;
let ry = size.y / 2.0;
let m = 0.551784;
let mx = m * rx;
let my = m * ry;
let point = |x, y| Point::new(x + rx, y + ry);
let mut curve = Curve::new();
curve.move_(point(-rx, z));
curve.cubic(point(-rx, -my), point(-mx, -ry), point(z, -ry));
curve.cubic(point(mx, -ry), point(rx, -my), point(rx, z));
curve.cubic(point(rx, my), point(mx, ry), point(z, ry));
curve.cubic(point(-mx, ry), point(-rx, my), point(-rx, z));
curve
}
/// Push a [`Move`](CurveItem::Move) item.
pub fn move_(&mut self, p: Point) {
self.0.push(CurveItem::Move(p));
}
/// Push a [`Line`](CurveItem::Line) item.
pub fn line(&mut self, p: Point) {
self.0.push(CurveItem::Line(p));
}
/// Push a [`Cubic`](CurveItem::Cubic) item.
pub fn cubic(&mut self, p1: Point, p2: Point, p3: Point) {
self.0.push(CurveItem::Cubic(p1, p2, p3));
}
/// Push a [`Close`](CurveItem::Close) item.
pub fn close(&mut self) {
self.0.push(CurveItem::Close);
}
/// Check if the curve is empty.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// Translate all points in this curve by the given offset.
pub fn translate(&mut self, offset: Point) {
if offset.is_zero() {
return;
}
for item in self.0.iter_mut() {
match item {
CurveItem::Move(p) => *p += offset,
CurveItem::Line(p) => *p += offset,
CurveItem::Cubic(p1, p2, p3) => {
*p1 += offset;
*p2 += offset;
*p3 += offset;
}
CurveItem::Close => (),
}
}
}
/// Computes the size of the bounding box of this curve.
pub fn bbox_size(&self) -> Size {
let mut min_x = Abs::inf();
let mut min_y = Abs::inf();
let mut max_x = -Abs::inf();
let mut max_y = -Abs::inf();
let mut cursor = Point::zero();
for item in self.0.iter() {
match item {
CurveItem::Move(to) => {
min_x = min_x.min(cursor.x);
min_y = min_y.min(cursor.y);
max_x = max_x.max(cursor.x);
max_y = max_y.max(cursor.y);
cursor = *to;
}
CurveItem::Line(to) => {
min_x = min_x.min(cursor.x);
min_y = min_y.min(cursor.y);
max_x = max_x.max(cursor.x);
max_y = max_y.max(cursor.y);
cursor = *to;
}
CurveItem::Cubic(c0, c1, end) => {
let cubic = kurbo::CubicBez::new(
kurbo::Point::new(cursor.x.to_pt(), cursor.y.to_pt()),
kurbo::Point::new(c0.x.to_pt(), c0.y.to_pt()),
kurbo::Point::new(c1.x.to_pt(), c1.y.to_pt()),
kurbo::Point::new(end.x.to_pt(), end.y.to_pt()),
);
let bbox = cubic.bounding_box();
min_x = min_x.min(Abs::pt(bbox.x0)).min(Abs::pt(bbox.x1));
min_y = min_y.min(Abs::pt(bbox.y0)).min(Abs::pt(bbox.y1));
max_x = max_x.max(Abs::pt(bbox.x0)).max(Abs::pt(bbox.x1));
max_y = max_y.max(Abs::pt(bbox.y0)).max(Abs::pt(bbox.y1));
cursor = *end;
}
CurveItem::Close => (),
}
}
Size::new(max_x - min_x, max_y - min_y)
}
}

View File

@ -95,9 +95,9 @@ pub struct ImageElem {
#[default(ImageFit::Cover)] #[default(ImageFit::Cover)]
pub fit: ImageFit, pub fit: ImageFit,
/// Whether text in SVG images should be converted into paths before /// Whether text in SVG images should be converted into curves before
/// embedding. This will result in the text becoming unselectable in /// embedding. This will result in the text becoming unselectable in the
/// the output. /// output.
#[default(false)] #[default(false)]
pub flatten_text: bool, pub flatten_text: bool,
} }

View File

@ -25,7 +25,7 @@ pub struct LineElem {
#[resolve] #[resolve]
pub start: Axes<Rel<Length>>, pub start: Axes<Rel<Length>>,
/// The offset from `start` where the line ends. /// The point where the line ends.
#[resolve] #[resolve]
pub end: Option<Axes<Rel<Length>>>, pub end: Option<Axes<Rel<Length>>>,

View File

@ -1,6 +1,7 @@
//! Drawing and visualization. //! Drawing and visualization.
mod color; mod color;
mod curve;
mod gradient; mod gradient;
mod image; mod image;
mod line; mod line;
@ -12,6 +13,7 @@ mod stroke;
mod tiling; mod tiling;
pub use self::color::*; pub use self::color::*;
pub use self::curve::*;
pub use self::gradient::*; pub use self::gradient::*;
pub use self::image::*; pub use self::image::*;
pub use self::line::*; pub use self::line::*;
@ -46,6 +48,7 @@ pub(super) fn define(global: &mut Scope) {
global.define_elem::<EllipseElem>(); global.define_elem::<EllipseElem>();
global.define_elem::<CircleElem>(); global.define_elem::<CircleElem>();
global.define_elem::<PolygonElem>(); global.define_elem::<PolygonElem>();
global.define_elem::<CurveElem>();
global.define_elem::<PathElem>(); global.define_elem::<PathElem>();
// Compatibility. // Compatibility.

View File

@ -1,6 +1,3 @@
use kurbo::ParamCurveExtrema;
use typst_utils::Numeric;
use self::PathVertex::{AllControlPoints, MirroredControlPoint, Vertex}; use self::PathVertex::{AllControlPoints, MirroredControlPoint, Vertex};
use crate::diag::{bail, SourceResult}; use crate::diag::{bail, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
@ -8,7 +5,7 @@ use crate::foundations::{
array, cast, elem, Array, Content, NativeElement, Packed, Reflect, Show, Smart, array, cast, elem, Array, Content, NativeElement, Packed, Reflect, Show, Smart,
StyleChain, StyleChain,
}; };
use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size}; use crate::layout::{Axes, BlockElem, Length, Rel};
use crate::visualize::{FillRule, Paint, Stroke}; use crate::visualize::{FillRule, Paint, Stroke};
/// A path through a list of points, connected by Bezier curves. /// A path through a list of points, connected by Bezier curves.
@ -24,6 +21,9 @@ use crate::visualize::{FillRule, Paint, Stroke};
/// ((50%, 0pt), (40pt, 0pt)), /// ((50%, 0pt), (40pt, 0pt)),
/// ) /// )
/// ``` /// ```
///
/// # Deprecation
/// This element is deprecated. The [`curve`] element should be used instead.
#[elem(Show)] #[elem(Show)]
pub struct PathElem { pub struct PathElem {
/// How to fill the path. /// How to fill the path.
@ -156,141 +156,3 @@ cast! {
} }
}, },
} }
/// A bezier path.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Path(pub Vec<PathItem>);
/// An item in a bezier path.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum PathItem {
MoveTo(Point),
LineTo(Point),
CubicTo(Point, Point, Point),
ClosePath,
}
impl Path {
/// Create an empty path.
pub const fn new() -> Self {
Self(vec![])
}
/// Create a path that describes a rectangle.
pub fn rect(size: Size) -> Self {
let z = Abs::zero();
let point = Point::new;
let mut path = Self::new();
path.move_to(point(z, z));
path.line_to(point(size.x, z));
path.line_to(point(size.x, size.y));
path.line_to(point(z, size.y));
path.close_path();
path
}
/// Create a path that describes an axis-aligned ellipse.
pub fn ellipse(size: Size) -> Self {
// https://stackoverflow.com/a/2007782
let z = Abs::zero();
let rx = size.x / 2.0;
let ry = size.y / 2.0;
let m = 0.551784;
let mx = m * rx;
let my = m * ry;
let point = |x, y| Point::new(x + rx, y + ry);
let mut path = Path::new();
path.move_to(point(-rx, z));
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(rx, my), point(mx, ry), point(z, ry));
path.cubic_to(point(-mx, ry), point(-rx, my), point(-rx, z));
path
}
/// Push a [`MoveTo`](PathItem::MoveTo) item.
pub fn move_to(&mut self, p: Point) {
self.0.push(PathItem::MoveTo(p));
}
/// Push a [`LineTo`](PathItem::LineTo) item.
pub fn line_to(&mut self, p: Point) {
self.0.push(PathItem::LineTo(p));
}
/// Push a [`CubicTo`](PathItem::CubicTo) item.
pub fn cubic_to(&mut self, p1: Point, p2: Point, p3: Point) {
self.0.push(PathItem::CubicTo(p1, p2, p3));
}
/// Push a [`ClosePath`](PathItem::ClosePath) item.
pub fn close_path(&mut self) {
self.0.push(PathItem::ClosePath);
}
/// Translate all points in this path by the given offset.
pub fn translate(&mut self, offset: Point) {
if offset.is_zero() {
return;
}
for item in self.0.iter_mut() {
match item {
PathItem::MoveTo(p) => *p += offset,
PathItem::LineTo(p) => *p += offset,
PathItem::CubicTo(p1, p2, p3) => {
*p1 += offset;
*p2 += offset;
*p3 += offset;
}
PathItem::ClosePath => (),
}
}
}
/// Computes the size of bounding box of this path.
pub fn bbox_size(&self) -> Size {
let mut min_x = Abs::inf();
let mut min_y = Abs::inf();
let mut max_x = -Abs::inf();
let mut max_y = -Abs::inf();
let mut cursor = Point::zero();
for item in self.0.iter() {
match item {
PathItem::MoveTo(to) => {
min_x = min_x.min(cursor.x);
min_y = min_y.min(cursor.y);
max_x = max_x.max(cursor.x);
max_y = max_y.max(cursor.y);
cursor = *to;
}
PathItem::LineTo(to) => {
min_x = min_x.min(cursor.x);
min_y = min_y.min(cursor.y);
max_x = max_x.max(cursor.x);
max_y = max_y.max(cursor.y);
cursor = *to;
}
PathItem::CubicTo(c0, c1, end) => {
let cubic = kurbo::CubicBez::new(
kurbo::Point::new(cursor.x.to_pt(), cursor.y.to_pt()),
kurbo::Point::new(c0.x.to_pt(), c0.y.to_pt()),
kurbo::Point::new(c1.x.to_pt(), c1.y.to_pt()),
kurbo::Point::new(end.x.to_pt(), end.y.to_pt()),
);
let bbox = cubic.bounding_box();
min_x = min_x.min(Abs::pt(bbox.x0)).min(Abs::pt(bbox.x1));
min_y = min_y.min(Abs::pt(bbox.y0)).min(Abs::pt(bbox.y1));
max_x = max_x.max(Abs::pt(bbox.x0)).max(Abs::pt(bbox.x1));
max_y = max_y.max(Abs::pt(bbox.y0)).max(Abs::pt(bbox.y1));
cursor = *end;
}
PathItem::ClosePath => (),
}
}
Size::new(max_x - min_x, max_y - min_y)
}
}

View File

@ -35,7 +35,7 @@ pub struct PolygonElem {
/// The drawing rule used to fill the polygon. /// The drawing rule used to fill the polygon.
/// ///
/// See the [path documentation]($path.fill-rule) for an example. /// See the [curve documentation]($curve.fill-rule) for an example.
#[default] #[default]
pub fill_rule: FillRule, pub fill_rule: FillRule,

View File

@ -4,7 +4,7 @@ use crate::foundations::{
elem, Cast, Content, NativeElement, Packed, Show, Smart, StyleChain, elem, Cast, Content, NativeElement, Packed, Show, Smart, StyleChain,
}; };
use crate::layout::{Abs, BlockElem, Corners, Length, Point, Rel, Sides, Size, Sizing}; use crate::layout::{Abs, BlockElem, Corners, Length, Point, Rel, Sides, Size, Sizing};
use crate::visualize::{FixedStroke, Paint, Path, Stroke}; use crate::visualize::{Curve, FixedStroke, Paint, Stroke};
/// A rectangle with optional content. /// A rectangle with optional content.
/// ///
@ -395,7 +395,7 @@ pub struct Shape {
pub stroke: Option<FixedStroke>, pub stroke: Option<FixedStroke>,
} }
/// A path filling rule. /// A fill rule for curve drawing.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)] #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum FillRule { pub enum FillRule {
/// Specifies that "inside" is computed by a non-zero sum of signed edge crossings. /// Specifies that "inside" is computed by a non-zero sum of signed edge crossings.
@ -412,8 +412,8 @@ pub enum Geometry {
Line(Point), Line(Point),
/// A rectangle with its origin in the topleft corner. /// A rectangle with its origin in the topleft corner.
Rect(Size), Rect(Size),
/// A bezier path. /// A curve consisting of movements, lines, and Bezier segments.
Path(Path), Curve(Curve),
} }
impl Geometry { impl Geometry {
@ -441,8 +441,8 @@ impl Geometry {
pub fn bbox_size(&self) -> Size { pub fn bbox_size(&self) -> Size {
match self { match self {
Self::Line(line) => Size::new(line.x, line.y), Self::Line(line) => Size::new(line.x, line.y),
Self::Rect(s) => *s, Self::Rect(rect) => *rect,
Self::Path(p) => p.bbox_size(), Self::Curve(curve) => curve.bbox_size(),
} }
} }
} }

View File

@ -170,14 +170,20 @@ impl Stroke {
/// If set to `{auto}`, the value is inherited, defaulting to `{4.0}`. /// If set to `{auto}`, the value is inherited, defaulting to `{4.0}`.
/// ///
/// ```example /// ```example
/// #let points = ((15pt, 0pt), (0pt, 30pt), (30pt, 30pt), (10pt, 20pt)) /// #let items = (
/// #set path(stroke: 6pt + blue) /// curve.move((15pt, 0pt)),
/// curve.line((0pt, 30pt)),
/// curve.line((30pt, 30pt)),
/// curve.line((10pt, 20pt)),
/// )
///
/// #set curve(stroke: 6pt + blue)
/// #stack( /// #stack(
/// dir: ltr, /// dir: ltr,
/// spacing: 1cm, /// spacing: 1cm,
/// path(stroke: (miter-limit: 1), ..points), /// curve(stroke: (miter-limit: 1), ..items),
/// path(stroke: (miter-limit: 4), ..points), /// curve(stroke: (miter-limit: 4), ..items),
/// path(stroke: (miter-limit: 5), ..points), /// curve(stroke: (miter-limit: 5), ..items),
/// ) /// )
/// ``` /// ```
#[external] #[external]

View File

@ -19,7 +19,7 @@ use typst_library::model::Destination;
use typst_library::text::color::should_outline; use typst_library::text::color::should_outline;
use typst_library::text::{Font, Glyph, TextItem, TextItemView}; use typst_library::text::{Font, Glyph, TextItem, TextItemView};
use typst_library::visualize::{ use typst_library::visualize::{
FillRule, FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Curve, CurveItem, FillRule, FixedStroke, Geometry, Image, LineCap, LineJoin, Paint,
Shape, Shape,
}; };
use typst_syntax::Span; use typst_syntax::Span;
@ -404,8 +404,8 @@ fn write_group(ctx: &mut Builder, pos: Point, group: &GroupItem) -> SourceResult
} }
ctx.transform(translation.pre_concat(group.transform)); ctx.transform(translation.pre_concat(group.transform));
if let Some(clip_path) = &group.clip_path { if let Some(clip_curve) = &group.clip {
write_path(ctx, 0.0, 0.0, clip_path); write_curve(ctx, 0.0, 0.0, clip_curve);
ctx.content.clip_nonzero(); ctx.content.clip_nonzero();
ctx.content.end_path(); ctx.content.end_path();
} }
@ -667,7 +667,7 @@ fn write_shape(ctx: &mut Builder, pos: Point, shape: &Shape) -> SourceResult<()>
ctx.set_opacities(stroke, shape.fill.as_ref()); ctx.set_opacities(stroke, shape.fill.as_ref());
match shape.geometry { match &shape.geometry {
Geometry::Line(target) => { Geometry::Line(target) => {
let dx = target.x.to_f32(); let dx = target.x.to_f32();
let dy = target.y.to_f32(); let dy = target.y.to_f32();
@ -681,8 +681,8 @@ fn write_shape(ctx: &mut Builder, pos: Point, shape: &Shape) -> SourceResult<()>
ctx.content.rect(x, y, w, h); ctx.content.rect(x, y, w, h);
} }
} }
Geometry::Path(ref path) => { Geometry::Curve(curve) => {
write_path(ctx, x, y, path); write_curve(ctx, x, y, curve);
} }
} }
@ -698,17 +698,13 @@ fn write_shape(ctx: &mut Builder, pos: Point, shape: &Shape) -> SourceResult<()>
Ok(()) Ok(())
} }
/// Encode a bezier path into the content stream. /// Encode a curve into the content stream.
fn write_path(ctx: &mut Builder, x: f32, y: f32, path: &Path) { fn write_curve(ctx: &mut Builder, x: f32, y: f32, curve: &Curve) {
for elem in &path.0 { for elem in &curve.0 {
match elem { match elem {
PathItem::MoveTo(p) => { CurveItem::Move(p) => ctx.content.move_to(x + p.x.to_f32(), y + p.y.to_f32()),
ctx.content.move_to(x + p.x.to_f32(), y + p.y.to_f32()) CurveItem::Line(p) => ctx.content.line_to(x + p.x.to_f32(), y + p.y.to_f32()),
} CurveItem::Cubic(p1, p2, p3) => ctx.content.cubic_to(
PathItem::LineTo(p) => {
ctx.content.line_to(x + p.x.to_f32(), y + p.y.to_f32())
}
PathItem::CubicTo(p1, p2, p3) => ctx.content.cubic_to(
x + p1.x.to_f32(), x + p1.x.to_f32(),
y + p1.y.to_f32(), y + p1.y.to_f32(),
x + p2.x.to_f32(), x + p2.x.to_f32(),
@ -716,7 +712,7 @@ fn write_path(ctx: &mut Builder, x: f32, y: f32, path: &Path) {
x + p3.x.to_f32(), x + p3.x.to_f32(),
y + p3.y.to_f32(), y + p3.y.to_f32(),
), ),
PathItem::ClosePath => ctx.content.close_path(), CurveItem::Close => ctx.content.close_path(),
}; };
} }
} }

View File

@ -193,8 +193,8 @@ fn render_group(canvas: &mut sk::Pixmap, state: State, pos: Point, group: &Group
let mut mask = state.mask; let mut mask = state.mask;
let storage; let storage;
if let Some(clip_path) = group.clip_path.as_ref() { if let Some(clip_curve) = group.clip.as_ref() {
if let Some(path) = shape::convert_path(clip_path) if let Some(path) = shape::convert_curve(clip_curve)
.and_then(|path| path.transform(state.transform)) .and_then(|path| path.transform(state.transform))
{ {
if let Some(mask) = mask { if let Some(mask) = mask {

View File

@ -1,7 +1,7 @@
use tiny_skia as sk; use tiny_skia as sk;
use typst_library::layout::{Abs, Axes, Point, Ratio, Size}; use typst_library::layout::{Abs, Axes, Point, Ratio, Size};
use typst_library::visualize::{ use typst_library::visualize::{
DashPattern, FillRule, FixedStroke, Geometry, LineCap, LineJoin, Path, PathItem, Curve, CurveItem, DashPattern, FillRule, FixedStroke, Geometry, LineCap, LineJoin,
Shape, Shape,
}; };
@ -10,7 +10,7 @@ use crate::{paint, AbsExt, State};
/// Render a geometrical shape into the canvas. /// Render a geometrical shape into the canvas.
pub fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Option<()> { pub fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Option<()> {
let ts = state.transform; let ts = state.transform;
let path = match shape.geometry { let path = match &shape.geometry {
Geometry::Line(target) => { Geometry::Line(target) => {
let mut builder = sk::PathBuilder::new(); let mut builder = sk::PathBuilder::new();
builder.line_to(target.x.to_f32(), target.y.to_f32()); builder.line_to(target.x.to_f32(), target.y.to_f32());
@ -33,7 +33,7 @@ pub fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Opt
sk::PathBuilder::from_rect(rect) sk::PathBuilder::from_rect(rect)
} }
Geometry::Path(ref path) => convert_path(path)?, Geometry::Curve(curve) => convert_curve(curve)?,
}; };
if let Some(fill) = &shape.fill { if let Some(fill) = &shape.fill {
@ -119,18 +119,18 @@ pub fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Opt
Some(()) Some(())
} }
/// Convert a Typst path into a tiny-skia path. /// Convert a Typst curve into a tiny-skia path.
pub fn convert_path(path: &Path) -> Option<sk::Path> { pub fn convert_curve(curve: &Curve) -> Option<sk::Path> {
let mut builder = sk::PathBuilder::new(); let mut builder = sk::PathBuilder::new();
for elem in &path.0 { for elem in &curve.0 {
match elem { match elem {
PathItem::MoveTo(p) => { CurveItem::Move(p) => {
builder.move_to(p.x.to_f32(), p.y.to_f32()); builder.move_to(p.x.to_f32(), p.y.to_f32());
} }
PathItem::LineTo(p) => { CurveItem::Line(p) => {
builder.line_to(p.x.to_f32(), p.y.to_f32()); builder.line_to(p.x.to_f32(), p.y.to_f32());
} }
PathItem::CubicTo(p1, p2, p3) => { CurveItem::Cubic(p1, p2, p3) => {
builder.cubic_to( builder.cubic_to(
p1.x.to_f32(), p1.x.to_f32(),
p1.y.to_f32(), p1.y.to_f32(),
@ -140,7 +140,7 @@ pub fn convert_path(path: &Path) -> Option<sk::Path> {
p3.y.to_f32(), p3.y.to_f32(),
); );
} }
PathItem::ClosePath => { CurveItem::Close => {
builder.close(); builder.close();
} }
}; };

View File

@ -255,9 +255,10 @@ impl SVGRenderer {
self.xml.write_attribute("data-typst-label", &label.resolve()); self.xml.write_attribute("data-typst-label", &label.resolve());
} }
if let Some(clip_path) = &group.clip_path { if let Some(clip_curve) = &group.clip {
let hash = hash128(&group); let hash = hash128(&group);
let id = self.clip_paths.insert_with(hash, || shape::convert_path(clip_path)); let id =
self.clip_paths.insert_with(hash, || shape::convert_curve(clip_curve));
self.xml.write_attribute_fmt("clip-path", format_args!("url(#{id})")); self.xml.write_attribute_fmt("clip-path", format_args!("url(#{id})"));
} }

View File

@ -2,7 +2,7 @@ use ecow::EcoString;
use ttf_parser::OutlineBuilder; use ttf_parser::OutlineBuilder;
use typst_library::layout::{Abs, Ratio, Size, Transform}; use typst_library::layout::{Abs, Ratio, Size, Transform};
use typst_library::visualize::{ use typst_library::visualize::{
FixedStroke, Geometry, LineCap, LineJoin, Paint, Path, PathItem, RelativeTo, Shape, Curve, CurveItem, FixedStroke, Geometry, LineCap, LineJoin, Paint, RelativeTo, Shape,
}; };
use crate::paint::ColorEncode; use crate::paint::ColorEncode;
@ -166,22 +166,18 @@ fn convert_geometry_to_path(geometry: &Geometry) -> EcoString {
let y = rect.y.to_pt() as f32; let y = rect.y.to_pt() as f32;
builder.rect(x, y); builder.rect(x, y);
} }
Geometry::Path(p) => return convert_path(p), Geometry::Curve(p) => return convert_curve(p),
}; };
builder.0 builder.0
} }
pub fn convert_path(path: &Path) -> EcoString { pub fn convert_curve(curve: &Curve) -> EcoString {
let mut builder = SvgPathBuilder::default(); let mut builder = SvgPathBuilder::default();
for item in &path.0 { for item in &curve.0 {
match item { match item {
PathItem::MoveTo(m) => { CurveItem::Move(m) => builder.move_to(m.x.to_pt() as f32, m.y.to_pt() as f32),
builder.move_to(m.x.to_pt() as f32, m.y.to_pt() as f32) CurveItem::Line(l) => builder.line_to(l.x.to_pt() as f32, l.y.to_pt() as f32),
} CurveItem::Cubic(c1, c2, t) => builder.curve_to(
PathItem::LineTo(l) => {
builder.line_to(l.x.to_pt() as f32, l.y.to_pt() as f32)
}
PathItem::CubicTo(c1, c2, t) => builder.curve_to(
c1.x.to_pt() as f32, c1.x.to_pt() as f32,
c1.y.to_pt() as f32, c1.y.to_pt() as f32,
c2.x.to_pt() as f32, c2.x.to_pt() as f32,
@ -189,7 +185,7 @@ pub fn convert_path(path: &Path) -> EcoString {
t.x.to_pt() as f32, t.x.to_pt() as f32,
t.y.to_pt() as f32, t.y.to_pt() as f32,
), ),
PathItem::ClosePath => builder.close(), CurveItem::Close => builder.close(),
} }
} }
builder.0 builder.0

View File

@ -348,6 +348,7 @@ pub static ROUTINES: Routines = Routines {
layout_repeat: typst_layout::layout_repeat, layout_repeat: typst_layout::layout_repeat,
layout_pad: typst_layout::layout_pad, layout_pad: typst_layout::layout_pad,
layout_line: typst_layout::layout_line, layout_line: typst_layout::layout_line,
layout_curve: typst_layout::layout_curve,
layout_path: typst_layout::layout_path, layout_path: typst_layout::layout_path,
layout_polygon: typst_layout::layout_polygon, layout_polygon: typst_layout::layout_polygon,
layout_rect: typst_layout::layout_rect, layout_rect: typst_layout::layout_rect,

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

BIN
tests/ref/curve-line.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 620 B

After

Width:  |  Height:  |  Size: 631 B

View File

@ -0,0 +1,163 @@
// Test curves.
--- curve-move-single ---
#curve(
stroke: 5pt,
curve.move((0pt, 30pt)),
curve.line((30pt, 30pt)),
curve.line((15pt, 0pt)),
curve.close()
)
--- curve-move-multiple-even-odd ---
#curve(
fill: yellow,
stroke: yellow.darken(20%),
fill-rule: "even-odd",
curve.move((10pt, 10pt)),
curve.line((20pt, 10pt)),
curve.line((20pt, 20pt)),
curve.close(),
curve.move((0pt, 5pt)),
curve.line((25pt, 5pt)),
curve.line((25pt, 30pt)),
curve.close(mode: "smooth"),
)
--- curve-move-multiple-non-zero ---
#curve(
fill: yellow,
stroke: yellow.darken(20%),
curve.move((10pt, 10pt)),
curve.line((20pt, 10pt)),
curve.line((20pt, 20pt)),
curve.close(),
curve.move((0pt, 5pt)),
curve.line((25pt, 5pt)),
curve.line((25pt, 30pt)),
curve.close(mode: "smooth"),
)
--- curve-line ---
#curve(
fill: purple,
stroke: 3pt + purple.lighten(50%),
curve.move((0pt, 0pt)),
curve.line((30pt, 30pt)),
curve.line((0pt, 30pt)),
curve.line((30pt, 0pt)),
)
--- curve-quad-mirror ---
#curve(
stroke: 2pt,
curve.quad((20pt, 40pt), (40pt, 40pt), relative: true),
curve.quad(auto, (40pt, -40pt), relative: true),
)
--- curve-cubic-mirror ---
#set page(height: 100pt)
#curve(
fill: red,
curve.move((0%, 0%)),
curve.cubic((-4%, 4%), (54%, 46%), (50%, 50%)),
curve.cubic(auto, (4%, 54%), (0%, 50%)),
curve.cubic(auto, (54%, 4%), (50%, 0%)),
curve.close(),
)
--- curve-cubic-inflection ---
#set page(height: 120pt)
#curve(
fill: blue.lighten(80%),
stroke: blue,
curve.move((30%, 0%)),
curve.cubic((10%, 0%), (10%, 60%), (30%, 60%)),
curve.cubic(none, (110%, 0%), (50%, 30%)),
curve.cubic((110%, 30%), (65%, 30%), (30%, 0%)),
curve.close(mode: "straight")
)
--- curve-close-smooth ---
#curve(
fill: blue.lighten(80%),
stroke: blue,
curve.move((0pt, 40pt)),
curve.cubic((0pt, 70pt), (10pt, 80pt), (40pt, 80pt)),
curve.cubic(auto, (80pt, 70pt), (80pt, 40pt)),
curve.cubic(auto, (70pt, 0pt), (40pt, 0pt)),
curve.close(mode: "smooth")
)
--- curve-close-straight ---
#curve(
fill: blue.lighten(80%),
stroke: blue,
curve.move((0pt, 40pt)),
curve.cubic((0pt, 70pt), (10pt, 80pt), (40pt, 80pt)),
curve.cubic(auto, (80pt, 70pt), (80pt, 40pt)),
curve.cubic(auto, (70pt, 0pt), (40pt, 0pt)),
curve.close(mode: "straight")
)
--- curve-close-intersection ---
#curve(
fill: yellow,
stroke: black,
curve.move((10pt, 10pt)),
curve.cubic((5pt, 20pt), (15pt, 20pt), (20pt, 0pt), relative: true),
curve.cubic(auto, (15pt, -10pt), (20pt, 0pt), relative: true),
curve.close(mode: "straight")
)
--- curve-stroke-gradient ---
#set page(width: auto)
#let down = curve.line((40pt, 40pt), relative: true)
#let up = curve.line((40pt, -40pt), relative: true)
#curve(
stroke: 4pt + gradient.linear(red, blue),
down, up, down, up, down,
)
--- curve-fill-rule ---
#stack(
dir: ltr,
curve(
fill: red,
fill-rule: "non-zero",
curve.move((25pt, 0pt)),
curve.line((10pt, 50pt)),
curve.line((50pt, 20pt)),
curve.line((0pt, 20pt)),
curve.line((40pt, 50pt)),
curve.close()
),
curve(
fill: red,
fill-rule: "even-odd",
curve.move((25pt, 0pt)),
curve.line((10pt, 50pt)),
curve.line((50pt, 20pt)),
curve.line((0pt, 20pt)),
curve.line((40pt, 50pt)),
curve.close()
)
)
--- curve-infinite-length ---
// Error: 2-67 cannot create curve with infinite size
#curve(curve.move((0pt, 0pt)), curve.line((float.inf * 1pt, 0pt)))
--- issue-curve-in-sized-container ---
// Curves/Paths used to implement `LayoutMultiple` rather than `LayoutSingle`
// without fulfilling the necessary contract of respecting region expansion.
#block(
fill: aqua,
width: 20pt,
height: 15pt,
curve(
curve.move((0pt, 0pt)),
curve.line((10pt, 10pt)),
),
)

View File

@ -59,24 +59,20 @@
#table(columns: 2, stroke: none)[A][B] #table(columns: 2, stroke: none)[A][B]
#table(columns: 2, stroke: 0pt)[A][B] #table(columns: 2, stroke: 0pt)[A][B]
#path( #curve(
fill: red,
stroke: none, stroke: none,
closed: true, curve.move((0pt, 30pt)),
((0%, 0%), (4%, -4%)), curve.line((30pt, 30pt)),
((50%, 50%), (4%, -4%)), curve.line((15pt, 0pt)),
((0%, 50%), (4%, 4%)), curve.close()
((50%, 0%), (4%, 4%)),
) )
#path( #curve(
fill: red,
stroke: 0pt, stroke: 0pt,
closed: true, curve.move((0pt, 30pt)),
((0%, 0%), (4%, -4%)), curve.line((30pt, 30pt)),
((50%, 50%), (4%, -4%)), curve.line((15pt, 0pt)),
((0%, 50%), (4%, 4%)), curve.close()
((50%, 0%), (4%, 4%)),
) )
--- stroke-text --- --- stroke-text ---

View File

@ -61,9 +61,9 @@
) )
--- tiling-zero-sized --- --- tiling-zero-sized ---
// Error: 15-51 tile size must be non-zero // Error: 15-52 tile size must be non-zero
// Hint: 15-51 try setting the size manually // Hint: 15-52 try setting the size manually
#line(stroke: tiling(path((0pt, 0pt), (1em, 0pt)))) #line(stroke: tiling(curve(curve.move((1em, 0pt)))))
--- tiling-spacing-negative --- --- tiling-spacing-negative ---
// Test with spacing set to `(-10pt, -10pt)` // Test with spacing set to `(-10pt, -10pt)`