Fix clipping when a box/block has a radius (#2338)

This commit is contained in:
Sébastien d'Herbais de Thun 2023-10-10 11:51:22 +02:00 committed by GitHub
parent a8af6b449a
commit 9bca0bce73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 211 additions and 117 deletions

View File

@ -146,15 +146,18 @@ impl Layout for BoxElem {
frame.set_baseline(frame.baseline() - shift);
}
// Clip the contents
if self.clip(styles) {
frame.clip();
}
// Prepare fill and stroke.
let fill = self.fill(styles);
let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default));
// Clip the contents
if self.clip(styles) {
let outset = self.outset(styles).relative_to(frame.size());
let size = frame.size() + outset.sum_by_axis();
let radius = self.radius(styles);
frame.clip(path_rect(size, radius, &stroke));
}
// Add fill and/or stroke.
if fill.is_some() || stroke.iter().any(Option::is_some) {
let outset = self.outset(styles);
@ -408,17 +411,20 @@ impl Layout for BlockElem {
frames
};
// Clip the contents
if self.clip(styles) {
for frame in frames.iter_mut() {
frame.clip();
}
}
// Prepare fill and stroke.
let fill = self.fill(styles);
let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default));
// Clip the contents
if self.clip(styles) {
for frame in frames.iter_mut() {
let outset = self.outset(styles).relative_to(frame.size());
let size = frame.size() + outset.sum_by_axis();
let radius = self.radius(styles);
frame.clip(path_rect(size, radius, &stroke));
}
}
// Add fill and/or stroke.
if fill.is_some() || stroke.iter().any(Option::is_some) {
let mut skip = false;

View File

@ -1,7 +1,7 @@
use std::ffi::OsStr;
use std::path::Path;
use typst::geom::Smart;
use typst::geom::{self, Smart};
use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat};
use typst::util::option_eq;
@ -212,7 +212,7 @@ impl Layout for ImageElem {
// Create a clipping group if only part of the image should be visible.
if fit == ImageFit::Cover && !target.fits(fitted) {
frame.clip();
frame.clip(geom::Path::rect(frame.size()));
}
// Apply metadata.

View File

@ -13,7 +13,7 @@ use crate::export::PdfPageLabel;
use crate::font::Font;
use crate::geom::{
self, styled_rect, Abs, Axes, Color, Corners, Dir, Em, FixedAlign, FixedStroke,
Geometry, Length, Numeric, Paint, Point, Rel, Shape, Sides, Size, Transform,
Geometry, Length, Numeric, Paint, Path, Point, Rel, Shape, Sides, Size, Transform,
};
use crate::image::Image;
use crate::model::{Content, Location, MetaElem, StyleChain};
@ -351,10 +351,14 @@ impl Frame {
}
}
/// Clip the contents of a frame to its size.
pub fn clip(&mut self) {
/// Clip the contents of a frame to a clip path.
///
/// 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) {
if !self.is_empty() {
self.group(|g| g.clips = true);
self.group(|g| g.clip_path = Some(clip_path));
}
}
@ -505,7 +509,7 @@ pub struct GroupItem {
/// A transformation to apply to the group.
pub transform: Transform,
/// Whether the frame should be a clipping boundary.
pub clips: bool,
pub clip_path: Option<Path>,
}
impl GroupItem {
@ -514,7 +518,7 @@ impl GroupItem {
Self {
frame,
transform: Transform::identity(),
clips: false,
clip_path: None,
}
}
}

View File

@ -462,14 +462,8 @@ fn write_group(ctx: &mut PageContext, pos: Point, group: &GroupItem) {
ctx.size(group.frame.size());
}
if group.clips {
let size = group.frame.size();
let w = size.x.to_f32();
let h = size.y.to_f32();
ctx.content.move_to(0.0, 0.0);
ctx.content.line_to(w, 0.0);
ctx.content.line_to(w, h);
ctx.content.line_to(0.0, h);
if let Some(clip_path) = &group.clip_path {
write_path(ctx, 0.0, 0.0, clip_path);
ctx.content.clip_nonzero();
ctx.content.end_path();
}

View File

@ -183,13 +183,9 @@ fn render_group(canvas: &mut sk::Pixmap, state: State, group: &GroupItem) {
let mut mask = state.mask;
let storage;
if group.clips {
let size: geom::Axes<Abs> = group.frame.size();
let w = size.x.to_f32();
let h = size.y.to_f32();
if let Some(path) = sk::Rect::from_xywh(0.0, 0.0, w, h)
.map(sk::PathBuilder::from_rect)
.and_then(|path| path.transform(state.transform))
if let Some(clip_path) = group.clip_path.as_ref() {
if let Some(path) =
convert_path(clip_path).and_then(|path| path.transform(state.transform))
{
if let Some(mask) = mask {
let mut mask = mask.clone();

View File

@ -12,8 +12,9 @@ use crate::doc::{Frame, FrameItem, FrameKind, GroupItem, TextItem};
use crate::eval::Repr;
use crate::font::Font;
use crate::geom::{
Abs, Angle, Axes, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin, Paint,
PathItem, Point, Quadrant, Ratio, RatioOrAngle, Relative, Shape, Size, Transform,
self, Abs, Angle, Axes, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin,
Paint, PathItem, Point, Quadrant, Ratio, RatioOrAngle, Relative, Shape, Size,
Transform,
};
use crate::image::{Image, ImageFormat, RasterFormat, VectorFormat};
use crate::util::hash128;
@ -269,16 +270,9 @@ impl SVGRenderer {
self.xml.start_element("g");
self.xml.write_attribute("class", "typst-group");
if group.clips {
if let Some(clip_path) = &group.clip_path {
let hash = hash128(&group);
let size = group.frame.size();
let x = size.x.to_pt();
let y = size.y.to_pt();
let id = self.clip_paths.insert_with(hash, || {
let mut builder = SvgPathBuilder(EcoString::new());
builder.rect(x as f32, y as f32);
builder.0
});
let id = self.clip_paths.insert_with(hash, || convert_path(clip_path));
self.xml.write_attribute_fmt("clip-path", format_args!("url(#{id})"));
}
@ -1014,31 +1008,35 @@ fn convert_geometry_to_path(geometry: &Geometry) -> EcoString {
let y = rect.y.to_pt() as f32;
builder.rect(x, y);
}
Geometry::Path(p) => {
for item in &p.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(
c1.x.to_pt() as f32,
c1.y.to_pt() as f32,
c2.x.to_pt() as f32,
c2.y.to_pt() as f32,
t.x.to_pt() as f32,
t.y.to_pt() as f32,
),
PathItem::ClosePath => builder.close(),
}
}
}
Geometry::Path(p) => return convert_path(p),
};
builder.0
}
fn convert_path(path: &geom::Path) -> EcoString {
let mut builder = SvgPathBuilder::default();
for item in &path.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(
c1.x.to_pt() as f32,
c1.y.to_pt() as f32,
c2.x.to_pt() as f32,
c2.y.to_pt() as f32,
t.x.to_pt() as f32,
t.y.to_pt() as f32,
),
PathItem::ClosePath => builder.close(),
}
}
builder.0
}
/// Encode an image into a data URL. The format of the URL is
/// `data:image/{format};base64,`.
#[comemo::memoize]

View File

@ -46,7 +46,7 @@ pub use self::paint::Paint;
pub use self::path::{Path, PathItem};
pub use self::point::Point;
pub use self::ratio::Ratio;
pub use self::rect::styled_rect;
pub use self::rect::{path_rect, styled_rect};
pub use self::rel::Rel;
pub use self::scalar::Scalar;
pub use self::shape::{Geometry, Shape};

View File

@ -42,6 +42,19 @@ impl PathExtension for Path {
}
}
/// Creates a new rectangle as a path.
pub fn path_rect(
size: Size,
radius: Corners<Rel<Abs>>,
stroke: &Sides<Option<FixedStroke>>,
) -> Path {
if stroke.is_uniform() && radius.iter().cloned().all(Rel::is_zero) {
Path::rect(size)
} else {
segmented_path_rect(size, radius, stroke)
}
}
/// Create a styled rectangle with shapes.
/// - use rect primitive for simple rectangles
/// - stroke sides if possible
@ -68,24 +81,13 @@ fn simple_rect(
vec![Shape { geometry: Geometry::Rect(size), fill, stroke }]
}
/// Use stroke and fill for the rectangle
fn segmented_rect(
fn corners_control_points(
size: Size,
radius: Corners<Rel<Abs>>,
fill: Option<Paint>,
strokes: Sides<Option<FixedStroke>>,
) -> Vec<Shape> {
let mut res = vec![];
let stroke_widths = strokes
.clone()
.map(|s| s.map(|s| s.thickness / 2.0).unwrap_or(Abs::zero()));
let max_radius = (size.x.min(size.y)) / 2.0
+ stroke_widths.iter().cloned().min().unwrap_or(Abs::zero());
let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius));
let corners = Corners {
radius: Corners<Abs>,
strokes: &Sides<Option<FixedStroke>>,
stroke_widths: Sides<Abs>,
) -> Corners<ControlPoints> {
Corners {
top_left: Corner::TopLeft,
top_right: Corner::TopRight,
bottom_right: Corner::BottomRight,
@ -105,7 +107,67 @@ fn segmented_rect(
(None, None) => true,
_ => false,
},
});
})
}
fn segmented_path_rect(
size: Size,
radius: Corners<Rel<Abs>>,
strokes: &Sides<Option<FixedStroke>>,
) -> Path {
let stroke_widths = strokes
.as_ref()
.map(|s| s.as_ref().map_or(Abs::zero(), |s| s.thickness / 2.0));
let max_radius = (size.x.min(size.y)) / 2.0
+ stroke_widths.iter().cloned().min().unwrap_or(Abs::zero());
let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius));
// insert stroked sides below filled sides
let mut path = Path::new();
let corners = corners_control_points(size, radius, strokes, stroke_widths);
let current = corners.iter().find(|c| !c.same).map(|c| c.corner);
if let Some(mut current) = current {
// multiple segments
// start at a corner with a change between sides and iterate clockwise all other corners
let mut last = current;
for _ in 0..4 {
current = current.next_cw();
if corners.get_ref(current).same {
continue;
}
// create segment
let start = last;
let end = current;
last = current;
path_segment(start, end, &corners, &mut path);
}
} else if strokes.top.is_some() {
// single segment
path_segment(Corner::TopLeft, Corner::TopLeft, &corners, &mut path);
}
path
}
/// Use stroke and fill for the rectangle
fn segmented_rect(
size: Size,
radius: Corners<Rel<Abs>>,
fill: Option<Paint>,
strokes: Sides<Option<FixedStroke>>,
) -> Vec<Shape> {
let mut res = vec![];
let stroke_widths = strokes
.as_ref()
.map(|s| s.as_ref().map_or(Abs::zero(), |s| s.thickness / 2.0));
let max_radius = (size.x.min(size.y)) / 2.0
+ stroke_widths.iter().cloned().min().unwrap_or(Abs::zero());
let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius));
let corners = corners_control_points(size, radius, &strokes, stroke_widths);
// insert stroked sides below filled sides
let mut stroke_insert = 0;
@ -171,6 +233,43 @@ fn segmented_rect(
res
}
fn path_segment(
start: Corner,
end: Corner,
corners: &Corners<ControlPoints>,
path: &mut Path,
) {
// create start corner
let c = corners.get_ref(start);
if start == end || !c.arc() {
path.move_to(c.end());
} else {
path.arc_move(c.mid(), c.center(), c.end());
}
// create corners between start and end
let mut current = start.next_cw();
while current != end {
let c = corners.get_ref(current);
if c.arc() {
path.arc_line(c.start(), c.center(), c.end());
} else {
path.line_to(c.end());
}
current = current.next_cw();
}
// create end corner
let c = corners.get_ref(end);
if !c.arc() {
path.line_to(c.start());
} else if start == end {
path.arc_line(c.start(), c.center(), c.end());
} else {
path.arc_line(c.start(), c.center(), c.mid());
}
}
/// Returns the shape for the segment and whether the shape should be drawn on top.
fn segment(
start: Corner,
@ -228,35 +327,8 @@ fn stroke_segment(
stroke: FixedStroke,
) -> Shape {
// create start corner
let c = corners.get_ref(start);
let mut path = Path::new();
if start == end || !c.arc() {
path.move_to(c.end());
} else {
path.arc_move(c.mid(), c.center(), c.end());
}
// create corners between start and end
let mut current = start.next_cw();
while current != end {
let c = corners.get_ref(current);
if c.arc() {
path.arc_line(c.start(), c.center(), c.end());
} else {
path.line_to(c.end());
}
current = current.next_cw();
}
// create end corner
let c = corners.get_ref(end);
if !c.arc() {
path.line_to(c.start());
} else if start == end {
path.arc_line(c.start(), c.center(), c.end());
} else {
path.arc_line(c.start(), c.center(), c.mid());
}
path_segment(start, end, corners, &mut path);
Shape {
geometry: Geometry::Path(path),

View File

@ -46,6 +46,16 @@ impl<T> Sides<T> {
}
}
/// Convert from `&Sides<T>` to `Sides<&T>`.
pub fn as_ref(&self) -> Sides<&T> {
Sides {
left: &self.left,
top: &self.top,
right: &self.right,
bottom: &self.bottom,
}
}
/// Zip two instances into one.
pub fn zip<U>(self, other: Sides<U>) -> Sides<(T, U)> {
Sides {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -7,13 +7,13 @@ world 1
Space
Hello #box(width: 1em, height: 1em, clip: true)[#rect(width: 3em, height: 3em, fill: red)]
Hello #box(width: 1em, height: 1em, clip: true)[#rect(width: 3em, height: 3em, fill: red)]
world 2
---
// Test cliping text
#block(width: 5em, height: 2em, clip: false, stroke: 1pt + black)[
But, soft! what light through
But, soft! what light through
]
#v(2em)
@ -24,7 +24,7 @@ world 2
]
---
// Test cliping svg glyphs
// Test clipping svg glyphs
Emoji: #box(height: 0.5em, stroke: 1pt + black)[🐪, 🌋, 🏞]
Emoji: #box(height: 0.5em, clip: true, stroke: 1pt + black)[🐪, 🌋, 🏞]
@ -40,3 +40,17 @@ First!
But, soft! what light through yonder window breaks? It is the east, and Juliet
is the sun.
]
---
// Test clipping with `radius`.
#set page(height: 60pt)
#box(
radius: 5pt,
stroke: 2pt + black,
width: 20pt,
height: 20pt,
clip: true,
image("/files/rhino.png", width: 30pt)
)