Fix clipping when a box/block has a radius
(#2338)
This commit is contained in:
parent
a8af6b449a
commit
9bca0bce73
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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]
|
||||
|
@ -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};
|
||||
|
@ -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),
|
||||
|
@ -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 |
@ -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)
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user