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); frame.set_baseline(frame.baseline() - shift);
} }
// Clip the contents
if self.clip(styles) {
frame.clip();
}
// Prepare fill and stroke. // Prepare fill and stroke.
let fill = self.fill(styles); let fill = self.fill(styles);
let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default)); 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. // Add fill and/or stroke.
if fill.is_some() || stroke.iter().any(Option::is_some) { if fill.is_some() || stroke.iter().any(Option::is_some) {
let outset = self.outset(styles); let outset = self.outset(styles);
@ -408,17 +411,20 @@ impl Layout for BlockElem {
frames frames
}; };
// Clip the contents
if self.clip(styles) {
for frame in frames.iter_mut() {
frame.clip();
}
}
// Prepare fill and stroke. // Prepare fill and stroke.
let fill = self.fill(styles); let fill = self.fill(styles);
let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default)); 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. // Add fill and/or stroke.
if fill.is_some() || stroke.iter().any(Option::is_some) { if fill.is_some() || stroke.iter().any(Option::is_some) {
let mut skip = false; let mut skip = false;

View File

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

View File

@ -13,7 +13,7 @@ use crate::export::PdfPageLabel;
use crate::font::Font; use crate::font::Font;
use crate::geom::{ use crate::geom::{
self, styled_rect, Abs, Axes, Color, Corners, Dir, Em, FixedAlign, FixedStroke, 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::image::Image;
use crate::model::{Content, Location, MetaElem, StyleChain}; use crate::model::{Content, Location, MetaElem, StyleChain};
@ -351,10 +351,14 @@ impl Frame {
} }
} }
/// Clip the contents of a frame to its size. /// Clip the contents of a frame to a clip path.
pub fn clip(&mut self) { ///
/// 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() { 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. /// A transformation to apply to the group.
pub transform: Transform, pub transform: Transform,
/// Whether the frame should be a clipping boundary. /// Whether the frame should be a clipping boundary.
pub clips: bool, pub clip_path: Option<Path>,
} }
impl GroupItem { impl GroupItem {
@ -514,7 +518,7 @@ impl GroupItem {
Self { Self {
frame, frame,
transform: Transform::identity(), 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()); ctx.size(group.frame.size());
} }
if group.clips { if let Some(clip_path) = &group.clip_path {
let size = group.frame.size(); write_path(ctx, 0.0, 0.0, clip_path);
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);
ctx.content.clip_nonzero(); ctx.content.clip_nonzero();
ctx.content.end_path(); 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 mut mask = state.mask;
let storage; let storage;
if group.clips { if let Some(clip_path) = group.clip_path.as_ref() {
let size: geom::Axes<Abs> = group.frame.size(); if let Some(path) =
let w = size.x.to_f32(); convert_path(clip_path).and_then(|path| path.transform(state.transform))
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(mask) = mask { if let Some(mask) = mask {
let mut mask = mask.clone(); 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::eval::Repr;
use crate::font::Font; use crate::font::Font;
use crate::geom::{ use crate::geom::{
Abs, Angle, Axes, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin, Paint, self, Abs, Angle, Axes, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin,
PathItem, Point, Quadrant, Ratio, RatioOrAngle, Relative, Shape, Size, Transform, Paint, PathItem, Point, Quadrant, Ratio, RatioOrAngle, Relative, Shape, Size,
Transform,
}; };
use crate::image::{Image, ImageFormat, RasterFormat, VectorFormat}; use crate::image::{Image, ImageFormat, RasterFormat, VectorFormat};
use crate::util::hash128; use crate::util::hash128;
@ -269,16 +270,9 @@ impl SVGRenderer {
self.xml.start_element("g"); self.xml.start_element("g");
self.xml.write_attribute("class", "typst-group"); self.xml.write_attribute("class", "typst-group");
if group.clips { if let Some(clip_path) = &group.clip_path {
let hash = hash128(&group); let hash = hash128(&group);
let size = group.frame.size(); let id = self.clip_paths.insert_with(hash, || convert_path(clip_path));
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
});
self.xml.write_attribute_fmt("clip-path", format_args!("url(#{id})")); 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; let y = rect.y.to_pt() as f32;
builder.rect(x, y); builder.rect(x, y);
} }
Geometry::Path(p) => { Geometry::Path(p) => return convert_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(),
}
}
}
}; };
builder.0 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 /// Encode an image into a data URL. The format of the URL is
/// `data:image/{format};base64,`. /// `data:image/{format};base64,`.
#[comemo::memoize] #[comemo::memoize]

View File

@ -46,7 +46,7 @@ pub use self::paint::Paint;
pub use self::path::{Path, PathItem}; pub use self::path::{Path, PathItem};
pub use self::point::Point; pub use self::point::Point;
pub use self::ratio::Ratio; 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::rel::Rel;
pub use self::scalar::Scalar; pub use self::scalar::Scalar;
pub use self::shape::{Geometry, Shape}; 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. /// Create a styled rectangle with shapes.
/// - use rect primitive for simple rectangles /// - use rect primitive for simple rectangles
/// - stroke sides if possible /// - stroke sides if possible
@ -68,24 +81,13 @@ fn simple_rect(
vec![Shape { geometry: Geometry::Rect(size), fill, stroke }] vec![Shape { geometry: Geometry::Rect(size), fill, stroke }]
} }
/// Use stroke and fill for the rectangle fn corners_control_points(
fn segmented_rect(
size: Size, size: Size,
radius: Corners<Rel<Abs>>, radius: Corners<Abs>,
fill: Option<Paint>, strokes: &Sides<Option<FixedStroke>>,
strokes: Sides<Option<FixedStroke>>, stroke_widths: Sides<Abs>,
) -> Vec<Shape> { ) -> Corners<ControlPoints> {
let mut res = vec![]; Corners {
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 {
top_left: Corner::TopLeft, top_left: Corner::TopLeft,
top_right: Corner::TopRight, top_right: Corner::TopRight,
bottom_right: Corner::BottomRight, bottom_right: Corner::BottomRight,
@ -105,7 +107,67 @@ fn segmented_rect(
(None, None) => true, (None, None) => true,
_ => false, _ => 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 // insert stroked sides below filled sides
let mut stroke_insert = 0; let mut stroke_insert = 0;
@ -171,6 +233,43 @@ fn segmented_rect(
res 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. /// Returns the shape for the segment and whether the shape should be drawn on top.
fn segment( fn segment(
start: Corner, start: Corner,
@ -228,35 +327,8 @@ fn stroke_segment(
stroke: FixedStroke, stroke: FixedStroke,
) -> Shape { ) -> Shape {
// create start corner // create start corner
let c = corners.get_ref(start);
let mut path = Path::new(); let mut path = Path::new();
if start == end || !c.arc() { path_segment(start, end, corners, &mut path);
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());
}
Shape { Shape {
geometry: Geometry::Path(path), 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. /// Zip two instances into one.
pub fn zip<U>(self, other: Sides<U>) -> Sides<(T, U)> { pub fn zip<U>(self, other: Sides<U>) -> Sides<(T, U)> {
Sides { Sides {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -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, stroke: 1pt + black)[🐪, 🌋, 🏞]
Emoji: #box(height: 0.5em, clip: true, 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 But, soft! what light through yonder window breaks? It is the east, and Juliet
is the sun. 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)
)