mirror of
https://github.com/penpot/penpot.git
synced 2026-10-28 21:40:34 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
472ba8dde5
@ -24,6 +24,7 @@ use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use options::RenderOptions;
|
||||
pub(crate) use strokes::per_side_stroke_outline;
|
||||
pub use surfaces::{SurfaceId, Surfaces};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
@ -128,22 +128,22 @@ fn inner_corners_collapse(stroke: &Stroke, rect: &Rect, radii: &Option<Corners>)
|
||||
.any(|(shape, (want, got))| shape.x > 0.0 && (want.x <= 0.0 || want != got))
|
||||
}
|
||||
|
||||
/// Draws a rect/frame stroke whose sides have different widths as the area
|
||||
/// between an outer and an inner (rounded) rect, mitered like CSS borders.
|
||||
/// Dashed/dotted styles cut that band per side, each side fitting a whole
|
||||
/// number of dashes the way a browser fits a dashed border.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_per_side_stroke_on_rect(
|
||||
canvas: &skia::Canvas,
|
||||
struct PerSideGeometry {
|
||||
band: skia::Path,
|
||||
pattern: Option<skia::Path>,
|
||||
wedges: Option<skia::Path>,
|
||||
trim_to_band: bool,
|
||||
}
|
||||
|
||||
fn per_side_geometry(
|
||||
stroke: &Stroke,
|
||||
style: StrokeStyle,
|
||||
rect: &Rect,
|
||||
corners: &Option<Corners>,
|
||||
paint: &skia::Paint,
|
||||
widths: [f32; 4],
|
||||
scale: f32,
|
||||
miter: Option<[f32; 4]>,
|
||||
antialias: bool,
|
||||
) {
|
||||
) -> PerSideGeometry {
|
||||
// Strokes sharing a border box are shaped by the whole box: a side this one
|
||||
// does not paint still sets the radii its corners curve to, as in CSS.
|
||||
let band_widths: [f32; 4] = match miter {
|
||||
@ -182,11 +182,6 @@ fn draw_per_side_stroke_on_rect(
|
||||
// and the whole outer rect is filled, like CSS collapsing borders.
|
||||
let has_hole = inner.width() > 0.0 && inner.height() > 0.0;
|
||||
|
||||
let mut fill_paint = paint.clone();
|
||||
fill_paint.set_style(skia::PaintStyle::Fill);
|
||||
fill_paint.set_path_effect(None);
|
||||
fill_paint.set_anti_alias(antialias);
|
||||
|
||||
let mut pb = skia::PathBuilder::new();
|
||||
let (outer_radii, inner_radii) = match corners {
|
||||
Some(radii) => {
|
||||
@ -231,7 +226,6 @@ fn draw_per_side_stroke_on_rect(
|
||||
|
||||
// Solid is the common case and the whole band is one fill. A collapsed
|
||||
// band has no sides left to walk, so it fills solid too.
|
||||
let style = stroke.with_width(stroke.max_width()).style_at_scale(scale);
|
||||
let pattern = if style == StrokeStyle::Solid || !has_hole {
|
||||
None
|
||||
} else {
|
||||
@ -251,8 +245,68 @@ fn draw_per_side_stroke_on_rect(
|
||||
// so each one keeps only its own wedge of the corner.
|
||||
let wedges = miter.and_then(|profile| side_wedges(&outer, outer_radii, profile, widths));
|
||||
|
||||
if pattern.is_none() && wedges.is_none() {
|
||||
canvas.draw_path(&band, &fill_paint);
|
||||
// Square corners need no clip: the side strips tile the band exactly.
|
||||
// Rounded corners curve past them, and dots straddle the boundary.
|
||||
let rounded = corners.is_some_and(|radii| radii.iter().any(|r| r.x > 0.0 || r.y > 0.0));
|
||||
let trim_to_band = rounded || style == StrokeStyle::Dotted;
|
||||
|
||||
PerSideGeometry {
|
||||
band,
|
||||
pattern,
|
||||
wedges,
|
||||
trim_to_band,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn per_side_stroke_outline(
|
||||
stroke: &Stroke,
|
||||
rect: &Rect,
|
||||
corners: &Option<Corners>,
|
||||
widths: [f32; 4],
|
||||
miter: Option<[f32; 4]>,
|
||||
) -> skia::Path {
|
||||
let geometry = per_side_geometry(stroke, stroke.style, rect, corners, widths, miter, true);
|
||||
let outline = match geometry.pattern {
|
||||
Some(pattern) if geometry.trim_to_band => pattern
|
||||
.op(&geometry.band, skia::PathOp::Intersect)
|
||||
.unwrap_or(pattern),
|
||||
Some(pattern) => pattern,
|
||||
None => geometry.band,
|
||||
};
|
||||
match &geometry.wedges {
|
||||
Some(wedges) => outline
|
||||
.op(wedges, skia::PathOp::Intersect)
|
||||
.unwrap_or(outline),
|
||||
None => outline,
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws a rect/frame stroke whose sides have different widths as the area
|
||||
/// between an outer and an inner (rounded) rect, mitered like CSS borders.
|
||||
/// Dashed/dotted styles cut that band per side, each side fitting a whole
|
||||
/// number of dashes the way a browser fits a dashed border.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_per_side_stroke_on_rect(
|
||||
canvas: &skia::Canvas,
|
||||
stroke: &Stroke,
|
||||
rect: &Rect,
|
||||
corners: &Option<Corners>,
|
||||
paint: &skia::Paint,
|
||||
widths: [f32; 4],
|
||||
scale: f32,
|
||||
miter: Option<[f32; 4]>,
|
||||
antialias: bool,
|
||||
) {
|
||||
let mut fill_paint = paint.clone();
|
||||
fill_paint.set_style(skia::PaintStyle::Fill);
|
||||
fill_paint.set_path_effect(None);
|
||||
fill_paint.set_anti_alias(antialias);
|
||||
|
||||
let style = stroke.with_width(stroke.max_width()).style_at_scale(scale);
|
||||
let geometry = per_side_geometry(stroke, style, rect, corners, widths, miter, antialias);
|
||||
|
||||
if geometry.pattern.is_none() && geometry.wedges.is_none() {
|
||||
canvas.draw_path(&geometry.band, &fill_paint);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -271,22 +325,19 @@ fn draw_per_side_stroke_on_rect(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(wedges) = &wedges {
|
||||
if let Some(wedges) = &geometry.wedges {
|
||||
canvas.clip_path(wedges, skia::ClipOp::Intersect, antialias);
|
||||
}
|
||||
|
||||
match &pattern {
|
||||
match &geometry.pattern {
|
||||
Some(pattern) => {
|
||||
// Square corners need no clip: the side strips tile the band exactly.
|
||||
// Rounded corners curve past them, and dots straddle the boundary.
|
||||
let rounded = corners.is_some_and(|radii| radii.iter().any(|r| r.x > 0.0 || r.y > 0.0));
|
||||
if rounded || style == StrokeStyle::Dotted {
|
||||
canvas.clip_path(&band, skia::ClipOp::Intersect, antialias);
|
||||
if geometry.trim_to_band {
|
||||
canvas.clip_path(&geometry.band, skia::ClipOp::Intersect, antialias);
|
||||
}
|
||||
canvas.draw_path(pattern, &fill_paint);
|
||||
}
|
||||
None => {
|
||||
canvas.draw_path(&band, &fill_paint);
|
||||
canvas.draw_path(&geometry.band, &fill_paint);
|
||||
}
|
||||
}
|
||||
canvas.restore();
|
||||
@ -1538,3 +1589,51 @@ mod per_side_tests {
|
||||
assert!(!pattern.contains((227.0, 50.0)), "right gap was filled");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn per_side_stroke_is_an_evenodd_band() {
|
||||
let stroke = Stroke::new_center_stroke(2., StrokeStyle::Solid, None, None, None, None);
|
||||
let rect = Rect::from_xywh(0., 0., 100., 50.);
|
||||
let path = per_side_stroke_outline(&stroke, &rect, &None, [2., 4., 6., 8.], None);
|
||||
|
||||
assert_eq!(path.fill_type(), skia::PathFillType::EvenOdd);
|
||||
assert_eq!(
|
||||
path.compute_tight_bounds(),
|
||||
Rect::from_ltrb(-4., -1., 102., 53.)
|
||||
);
|
||||
assert!(!path.contains(Point::new(50., 25.)));
|
||||
assert!(path.contains(Point::new(-2., 25.)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn per_side_dashed_stroke_keeps_its_gaps() {
|
||||
let stroke = Stroke::new_center_stroke(4., StrokeStyle::Dashed, None, None, None, None);
|
||||
let rect = Rect::from_xywh(0., 0., 200., 100.);
|
||||
let path = per_side_stroke_outline(&stroke, &rect, &None, [4., 4., 4., 8.], None);
|
||||
|
||||
let hits = (10..190)
|
||||
.filter(|x| path.contains(Point::new(*x as f32, 0.)))
|
||||
.count();
|
||||
assert!(hits > 0 && hits < 180, "hits: {hits}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn per_side_stroke_miters_against_its_siblings() {
|
||||
let stroke = Stroke::new_center_stroke(4., StrokeStyle::Solid, None, None, None, None);
|
||||
let rect = Rect::from_xywh(0., 0., 100., 50.);
|
||||
let path = per_side_stroke_outline(
|
||||
&stroke,
|
||||
&rect,
|
||||
&None,
|
||||
[4., 0., 0., 0.],
|
||||
Some([4., 4., 0., 0.]),
|
||||
);
|
||||
|
||||
assert!(path.contains(Point::new(101., -1.5)));
|
||||
assert!(!path.contains(Point::new(99., 1.5)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,8 @@ use std::mem::size_of;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::shapes::{stroke_to_path, Path, Segment, ToPath};
|
||||
use crate::render::per_side_stroke_outline;
|
||||
use crate::shapes::{stroke_to_path, Path, Segment, Stroke, ToPath, Type};
|
||||
use crate::{mem, with_current_shape, with_current_shape_mut};
|
||||
|
||||
const RAW_SEGMENT_DATA_SIZE: usize = size_of::<RawSegmentData>();
|
||||
@ -247,17 +248,34 @@ pub extern "C" fn convert_stroke_to_path(stroke_index: i32) -> *mut u8 {
|
||||
with_current_shape!(state, |shape: &Shape| {
|
||||
let idx = stroke_index as usize;
|
||||
if let Some(stroke) = shape.strokes.get(idx) {
|
||||
let shape_path = shape.to_path(&state.shapes);
|
||||
let path_transform = shape.to_path_transform();
|
||||
let per_side = stroke
|
||||
.per_side_widths()
|
||||
.filter(|_| matches!(shape.shape_type, Type::Rect(_) | Type::Frame(_)));
|
||||
|
||||
if let Some(path) = stroke_to_path(
|
||||
stroke,
|
||||
&shape_path,
|
||||
path_transform.as_ref(),
|
||||
&shape.selrect,
|
||||
shape.svg_attrs.as_ref(),
|
||||
false,
|
||||
) {
|
||||
let path = match per_side {
|
||||
Some(widths) => {
|
||||
let outline = per_side_stroke_outline(
|
||||
stroke,
|
||||
&shape.selrect,
|
||||
&shape.shape_type.corners(),
|
||||
widths,
|
||||
Stroke::per_side_profile(shape.visible_strokes()),
|
||||
);
|
||||
Some(Path::from_skia_path_accurate(
|
||||
outline.make_transform(&shape.centered_transform()),
|
||||
))
|
||||
}
|
||||
None => stroke_to_path(
|
||||
stroke,
|
||||
&shape.to_path(&state.shapes),
|
||||
shape.to_path_transform().as_ref(),
|
||||
&shape.selrect,
|
||||
shape.svg_attrs.as_ref(),
|
||||
false,
|
||||
),
|
||||
};
|
||||
|
||||
if let Some(path) = path {
|
||||
even_odd = path.is_even_odd();
|
||||
result = path
|
||||
.segments()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user