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::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)

View File

@ -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};

View File

@ -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<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.
#[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<Abs> {
}
}
/// Creates a new rectangle as a path.
/// Creates a new rectangle as a curve.
pub fn clip_rect(
size: Size,
radius: &Corners<Rel<Abs>>,
stroke: &Sides<Option<FixedStroke>>,
outset: &Sides<Rel<Abs>>,
) -> 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<ControlPoints>,
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<ControlPoints>,
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);
}
}

View File

@ -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<Path>,
/// A curve which should be used to clip the group.
pub clip: Option<Curve>,
/// The group's label.
pub label: Option<Label>,
/// The group's logical parent. All elements in this group are logically
@ -559,7 +559,7 @@ impl GroupItem {
Self {
frame,
transform: Transform::identity(),
clip_path: None,
clip: None,
label: None,
parent: None,
}

View File

@ -22,8 +22,8 @@ use crate::layout::{
use crate::math::EquationElem;
use crate::model::{DocumentInfo, EnumElem, ListElem, TableElem};
use crate::visualize::{
CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem,
SquareElem,
CircleElem, CurveElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem,
RectElem, SquareElem,
};
use crate::World;
@ -241,6 +241,15 @@ routines! {
region: Region,
) -> 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`].
fn layout_path(
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)]
pub fit: ImageFit,
/// Whether text in SVG images should be converted into paths before
/// embedding. This will result in the text becoming unselectable in
/// the output.
/// Whether text in SVG images should be converted into curves before
/// embedding. This will result in the text becoming unselectable in the
/// output.
#[default(false)]
pub flatten_text: bool,
}

View File

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

View File

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

View File

@ -1,6 +1,3 @@
use kurbo::ParamCurveExtrema;
use typst_utils::Numeric;
use self::PathVertex::{AllControlPoints, MirroredControlPoint, Vertex};
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
@ -8,7 +5,7 @@ use crate::foundations::{
array, cast, elem, Array, Content, NativeElement, Packed, Reflect, Show, Smart,
StyleChain,
};
use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size};
use crate::layout::{Axes, BlockElem, Length, Rel};
use crate::visualize::{FillRule, Paint, Stroke};
/// 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)),
/// )
/// ```
///
/// # Deprecation
/// This element is deprecated. The [`curve`] element should be used instead.
#[elem(Show)]
pub struct PathElem {
/// 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.
///
/// See the [path documentation]($path.fill-rule) for an example.
/// See the [curve documentation]($curve.fill-rule) for an example.
#[default]
pub fill_rule: FillRule,

View File

@ -4,7 +4,7 @@ use crate::foundations::{
elem, Cast, Content, NativeElement, Packed, Show, Smart, StyleChain,
};
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.
///
@ -395,7 +395,7 @@ pub struct Shape {
pub stroke: Option<FixedStroke>,
}
/// A path filling rule.
/// A fill rule for curve drawing.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum FillRule {
/// Specifies that "inside" is computed by a non-zero sum of signed edge crossings.
@ -412,8 +412,8 @@ pub enum Geometry {
Line(Point),
/// A rectangle with its origin in the topleft corner.
Rect(Size),
/// A bezier path.
Path(Path),
/// A curve consisting of movements, lines, and Bezier segments.
Curve(Curve),
}
impl Geometry {
@ -441,8 +441,8 @@ impl Geometry {
pub fn bbox_size(&self) -> Size {
match self {
Self::Line(line) => Size::new(line.x, line.y),
Self::Rect(s) => *s,
Self::Path(p) => p.bbox_size(),
Self::Rect(rect) => *rect,
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}`.
///
/// ```example
/// #let points = ((15pt, 0pt), (0pt, 30pt), (30pt, 30pt), (10pt, 20pt))
/// #set path(stroke: 6pt + blue)
/// #let items = (
/// curve.move((15pt, 0pt)),
/// curve.line((0pt, 30pt)),
/// curve.line((30pt, 30pt)),
/// curve.line((10pt, 20pt)),
/// )
///
/// #set curve(stroke: 6pt + blue)
/// #stack(
/// dir: ltr,
/// spacing: 1cm,
/// path(stroke: (miter-limit: 1), ..points),
/// path(stroke: (miter-limit: 4), ..points),
/// path(stroke: (miter-limit: 5), ..points),
/// dir: ltr,
/// spacing: 1cm,
/// curve(stroke: (miter-limit: 1), ..items),
/// curve(stroke: (miter-limit: 4), ..items),
/// curve(stroke: (miter-limit: 5), ..items),
/// )
/// ```
#[external]

View File

@ -19,7 +19,7 @@ use typst_library::model::Destination;
use typst_library::text::color::should_outline;
use typst_library::text::{Font, Glyph, TextItem, TextItemView};
use typst_library::visualize::{
FillRule, FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem,
Curve, CurveItem, FillRule, FixedStroke, Geometry, Image, LineCap, LineJoin, Paint,
Shape,
};
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));
if let Some(clip_path) = &group.clip_path {
write_path(ctx, 0.0, 0.0, clip_path);
if let Some(clip_curve) = &group.clip {
write_curve(ctx, 0.0, 0.0, clip_curve);
ctx.content.clip_nonzero();
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());
match shape.geometry {
match &shape.geometry {
Geometry::Line(target) => {
let dx = target.x.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);
}
}
Geometry::Path(ref path) => {
write_path(ctx, x, y, path);
Geometry::Curve(curve) => {
write_curve(ctx, x, y, curve);
}
}
@ -698,17 +698,13 @@ fn write_shape(ctx: &mut Builder, pos: Point, shape: &Shape) -> SourceResult<()>
Ok(())
}
/// Encode a bezier path into the content stream.
fn write_path(ctx: &mut Builder, x: f32, y: f32, path: &Path) {
for elem in &path.0 {
/// Encode a curve into the content stream.
fn write_curve(ctx: &mut Builder, x: f32, y: f32, curve: &Curve) {
for elem in &curve.0 {
match elem {
PathItem::MoveTo(p) => {
ctx.content.move_to(x + p.x.to_f32(), y + p.y.to_f32())
}
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(
CurveItem::Move(p) => 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(
x + p1.x.to_f32(),
y + p1.y.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(),
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 storage;
if let Some(clip_path) = group.clip_path.as_ref() {
if let Some(path) = shape::convert_path(clip_path)
if let Some(clip_curve) = group.clip.as_ref() {
if let Some(path) = shape::convert_curve(clip_curve)
.and_then(|path| path.transform(state.transform))
{
if let Some(mask) = mask {

View File

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

View File

@ -255,9 +255,10 @@ impl SVGRenderer {
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 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})"));
}

View File

@ -2,7 +2,7 @@ use ecow::EcoString;
use ttf_parser::OutlineBuilder;
use typst_library::layout::{Abs, Ratio, Size, Transform};
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;
@ -166,22 +166,18 @@ fn convert_geometry_to_path(geometry: &Geometry) -> EcoString {
let y = rect.y.to_pt() as f32;
builder.rect(x, y);
}
Geometry::Path(p) => return convert_path(p),
Geometry::Curve(p) => return convert_curve(p),
};
builder.0
}
pub fn convert_path(path: &Path) -> EcoString {
pub fn convert_curve(curve: &Curve) -> EcoString {
let mut builder = SvgPathBuilder::default();
for item in &path.0 {
for item in &curve.0 {
match item {
PathItem::MoveTo(m) => {
builder.move_to(m.x.to_pt() as f32, m.y.to_pt() as f32)
}
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(
CurveItem::Move(m) => 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(
c1.x.to_pt() as f32,
c1.y.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.y.to_pt() as f32,
),
PathItem::ClosePath => builder.close(),
CurveItem::Close => builder.close(),
}
}
builder.0

View File

@ -348,6 +348,7 @@ pub static ROUTINES: Routines = Routines {
layout_repeat: typst_layout::layout_repeat,
layout_pad: typst_layout::layout_pad,
layout_line: typst_layout::layout_line,
layout_curve: typst_layout::layout_curve,
layout_path: typst_layout::layout_path,
layout_polygon: typst_layout::layout_polygon,
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: 0pt)[A][B]
#path(
fill: red,
#curve(
stroke: none,
closed: true,
((0%, 0%), (4%, -4%)),
((50%, 50%), (4%, -4%)),
((0%, 50%), (4%, 4%)),
((50%, 0%), (4%, 4%)),
curve.move((0pt, 30pt)),
curve.line((30pt, 30pt)),
curve.line((15pt, 0pt)),
curve.close()
)
#path(
fill: red,
#curve(
stroke: 0pt,
closed: true,
((0%, 0%), (4%, -4%)),
((50%, 50%), (4%, -4%)),
((0%, 50%), (4%, 4%)),
((50%, 0%), (4%, 4%)),
curve.move((0pt, 30pt)),
curve.line((30pt, 30pt)),
curve.line((15pt, 0pt)),
curve.close()
)
--- stroke-text ---

View File

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