New curve
element that supersedes path
(#5323)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
@ -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)
|
||||
|
@ -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};
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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>,
|
||||
|
532
crates/typst-library/src/visualize/curve.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
|
@ -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>>>,
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
@ -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})"));
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
BIN
tests/ref/curve-close-intersection.png
Normal file
After Width: | Height: | Size: 683 B |
BIN
tests/ref/curve-close-smooth.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
tests/ref/curve-close-straight.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
tests/ref/curve-cubic-inflection.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
tests/ref/curve-cubic-mirror.png
Normal file
After Width: | Height: | Size: 482 B |
BIN
tests/ref/curve-fill-rule.png
Normal file
After Width: | Height: | Size: 570 B |
BIN
tests/ref/curve-line.png
Normal file
After Width: | Height: | Size: 244 B |
BIN
tests/ref/curve-move-multiple-even-odd.png
Normal file
After Width: | Height: | Size: 301 B |
BIN
tests/ref/curve-move-multiple-non-zero.png
Normal file
After Width: | Height: | Size: 297 B |
BIN
tests/ref/curve-move-single.png
Normal file
After Width: | Height: | Size: 262 B |
BIN
tests/ref/curve-quad-mirror.png
Normal file
After Width: | Height: | Size: 490 B |
BIN
tests/ref/curve-stroke-gradient.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
tests/ref/issue-curve-in-sized-container.png
Normal file
After Width: | Height: | Size: 135 B |
Before Width: | Height: | Size: 620 B After Width: | Height: | Size: 631 B |
163
tests/suite/visualize/curve.typ
Normal 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)),
|
||||
),
|
||||
)
|
@ -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 ---
|
||||
|
@ -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)`
|
||||
|