mirror of
https://github.com/penpot/penpot.git
synced 2026-02-10 14:43:17 -05:00
Compare commits
3 Commits
develop
...
superalex-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79aff449ca | ||
|
|
1ee07225b5 | ||
|
|
de49a1e980 |
@@ -82,7 +82,7 @@ export default defineConfig({
|
||||
snapshotPathTemplate: "{testDir}/{testFilePath}-snapshots/{arg}.png",
|
||||
timeout: 2 * 60 * 1000,
|
||||
expect: {
|
||||
timeout: process.env.CI ? 20000 : 10000,
|
||||
timeout: process.env.CI ? 20000 : 100000,
|
||||
toHaveScreenshot: {
|
||||
maxDiffPixelRatio: 0.001,
|
||||
},
|
||||
|
||||
@@ -305,3 +305,19 @@ test("Renders a clipped frame with a large blur drop shadow", async ({
|
||||
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with solid, dotted, dashed and mixed stroke styles", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("render-wasm/get-file-stroke-styles.json");
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "b888b894-3697-80d3-8006-51cc8a55c200",
|
||||
pageId: "b888b894-3697-80d3-8006-51cc8a55c210",
|
||||
});
|
||||
await workspace.waitForFirstRenderWithoutUI();
|
||||
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 318 KiB After Width: | Height: | Size: 318 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
@@ -1,7 +1,8 @@
|
||||
use crate::math::{Matrix, Point, Rect};
|
||||
|
||||
use crate::shapes::{
|
||||
Corners, Fill, ImageFill, Path, Shape, Stroke, StrokeCap, StrokeKind, SvgAttrs, Type,
|
||||
Corners, Fill, ImageFill, Path, Shape, Stroke, StrokeCap, StrokeKind, StrokeStyle, SvgAttrs,
|
||||
Type,
|
||||
};
|
||||
use skia_safe::{self as skia, ImageFilter, RRect};
|
||||
|
||||
@@ -9,6 +10,41 @@ use super::{filters, RenderState, SurfaceId};
|
||||
use crate::render::filters::compose_filters;
|
||||
use crate::render::{get_dest_rect, get_source_rect};
|
||||
|
||||
fn point_on_rect_perimeter(rect: &Rect, mut distance: f32) -> Point {
|
||||
let width = rect.width();
|
||||
let height = rect.height();
|
||||
let perimeter = 2.0 * (width + height);
|
||||
|
||||
if perimeter <= 0.0 {
|
||||
return Point::new(rect.left, rect.top);
|
||||
}
|
||||
|
||||
// Envuelve la distancia dentro del perímetro
|
||||
distance = distance % perimeter;
|
||||
|
||||
let left = rect.left;
|
||||
let top = rect.top;
|
||||
let right = rect.right;
|
||||
let bottom = rect.bottom;
|
||||
|
||||
if distance <= width {
|
||||
return Point::new(left + distance, top);
|
||||
}
|
||||
|
||||
if distance <= width + height {
|
||||
let d = distance - width;
|
||||
return Point::new(right, top + d);
|
||||
}
|
||||
|
||||
if distance <= width + height + width {
|
||||
let d = distance - width - height;
|
||||
return Point::new(right - d, bottom);
|
||||
}
|
||||
|
||||
let d = distance - width - height - width;
|
||||
Point::new(left, bottom - d)
|
||||
}
|
||||
|
||||
// FIXME: See if we can simplify these arguments
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_stroke_on_rect(
|
||||
@@ -26,10 +62,151 @@ fn draw_stroke_on_rect(
|
||||
// Draw the different kind of strokes for a rect is straightforward, we just need apply a stroke to:
|
||||
// - The same rect if it's a center stroke
|
||||
// - A bigger rect if it's an outer stroke
|
||||
// - A smaller rect if it's an outer stroke
|
||||
// - A smaller rect if it's an inner stroke
|
||||
let stroke_rect = stroke.aligned_rect(rect, scale);
|
||||
let mut paint = stroke.to_paint(selrect, svg_attrs, antialias);
|
||||
|
||||
// Para estilos sólidos seguimos usando el path/stroke normal.
|
||||
// Para estilos no sólidos (dotted/dashed/mixed) empezamos a
|
||||
// personalizar la lógica por forma. Empezamos por los rectángulos
|
||||
// sin esquinas redondeadas con estilo dotted.
|
||||
if matches!(stroke.style, StrokeStyle::Dotted) && corners.is_none() {
|
||||
let mut paint = stroke.to_paint(selrect, svg_attrs, antialias);
|
||||
// Queremos dibujar círculos rellenos, no un stroke con path effect.
|
||||
paint.set_style(skia::PaintStyle::Fill);
|
||||
paint.set_path_effect(None);
|
||||
|
||||
// Aplicamos blur y shadow igual que antes.
|
||||
let filter = compose_filters(blur, shadow);
|
||||
paint.set_image_filter(filter);
|
||||
|
||||
// Rect base donde colocamos los puntos. Para dotted inner/outer,
|
||||
// outer_rect ya está ajustado para que el path esté en el borde
|
||||
// original de la forma.
|
||||
let border_rect = stroke.outer_rect(rect);
|
||||
let radius = stroke.width / 2.0;
|
||||
let advance = stroke.width + 5.0;
|
||||
|
||||
if border_rect.width() <= 0.0 || border_rect.height() <= 0.0 || radius <= 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let width = border_rect.width();
|
||||
let height = border_rect.height();
|
||||
let left = border_rect.left;
|
||||
let top = border_rect.top;
|
||||
let right = border_rect.right;
|
||||
let bottom = border_rect.bottom;
|
||||
|
||||
let mut draw_dots = |canvas: &skia::Canvas| {
|
||||
// 1) Círculos completos en cada vértice
|
||||
let corner_points = [
|
||||
Point::new(left, top),
|
||||
Point::new(right, top),
|
||||
Point::new(right, bottom),
|
||||
Point::new(left, bottom),
|
||||
];
|
||||
for p in &corner_points {
|
||||
canvas.draw_circle((p.x, p.y), radius, &paint);
|
||||
}
|
||||
|
||||
// Helper para dibujar media circunferencia orientada según lado/kind
|
||||
let mut draw_half_circle =
|
||||
|canvas: &skia::Canvas, center: Point, side: u8, kind: StrokeKind| {
|
||||
if matches!(kind, StrokeKind::Center) {
|
||||
canvas.draw_circle((center.x, center.y), radius, &paint);
|
||||
return;
|
||||
}
|
||||
|
||||
// Path base: media circunferencia con normal hacia +Y (abajo)
|
||||
let mut path = skia::Path::new();
|
||||
let rect = skia::Rect::from_xywh(
|
||||
-radius,
|
||||
-radius,
|
||||
radius * 2.0,
|
||||
radius * 2.0,
|
||||
);
|
||||
path.add_arc(rect, 0.0, 180.0);
|
||||
|
||||
// Ángulo de la normal "hacia dentro" según el lado
|
||||
let inward_normal_angle = match side {
|
||||
// top: interior hacia abajo
|
||||
0 => 90.0,
|
||||
// right: interior hacia la izquierda
|
||||
1 => 180.0,
|
||||
// bottom: interior hacia arriba
|
||||
2 => 270.0,
|
||||
// left: interior hacia la derecha
|
||||
_ => 0.0,
|
||||
};
|
||||
|
||||
// La normal del path base apunta a +Y
|
||||
let base_normal_angle = 90.0;
|
||||
|
||||
// Para inner usamos la normal hacia dentro;
|
||||
// para outer la invertimos (180 grados).
|
||||
let mut normal_angle = inward_normal_angle;
|
||||
if matches!(kind, StrokeKind::Outer) {
|
||||
normal_angle += 180.0;
|
||||
}
|
||||
|
||||
let angle = normal_angle - base_normal_angle;
|
||||
|
||||
let mut matrix = Matrix::new_identity();
|
||||
matrix.pre_rotate(angle, Point::new(0.0, 0.0));
|
||||
matrix.post_translate(center);
|
||||
path.transform(&matrix);
|
||||
|
||||
canvas.draw_path(&path, &paint);
|
||||
};
|
||||
|
||||
// 2) Entre vértices, puntos intermedios por cada lado
|
||||
// Lado 0: top (left -> right)
|
||||
if width > advance {
|
||||
let mut t = advance;
|
||||
while t < width {
|
||||
let p = Point::new(left + t, top);
|
||||
draw_half_circle(canvas, p, 0, stroke.kind);
|
||||
t += advance;
|
||||
}
|
||||
}
|
||||
|
||||
// Lado 1: right (top -> bottom)
|
||||
if height > advance {
|
||||
let mut t = advance;
|
||||
while t < height {
|
||||
let p = Point::new(right, top + t);
|
||||
draw_half_circle(canvas, p, 1, stroke.kind);
|
||||
t += advance;
|
||||
}
|
||||
}
|
||||
|
||||
// Lado 2: bottom (right -> left)
|
||||
if width > advance {
|
||||
let mut t = advance;
|
||||
while t < width {
|
||||
let p = Point::new(right - t, bottom);
|
||||
draw_half_circle(canvas, p, 2, stroke.kind);
|
||||
t += advance;
|
||||
}
|
||||
}
|
||||
|
||||
// Lado 3: left (bottom -> top)
|
||||
if height > advance {
|
||||
let mut t = advance;
|
||||
while t < height {
|
||||
let p = Point::new(left, bottom - t);
|
||||
draw_half_circle(canvas, p, 3, stroke.kind);
|
||||
t += advance;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
draw_dots(canvas);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let mut paint = stroke.to_paint(selrect, svg_attrs, antialias);
|
||||
// Apply both blur and shadow filters if present, composing them if necessary.
|
||||
let filter = compose_filters(blur, shadow);
|
||||
paint.set_image_filter(filter);
|
||||
@@ -62,15 +239,62 @@ fn draw_stroke_on_circle(
|
||||
// Draw the different kind of strokes for an oval is straightforward, we just need apply a stroke to:
|
||||
// - The same oval if it's a center stroke
|
||||
// - A bigger oval if it's an outer stroke
|
||||
// - A smaller oval if it's an outer stroke
|
||||
// - A smaller oval if it's an inner stroke
|
||||
let stroke_rect = stroke.aligned_rect(rect, scale);
|
||||
let mut paint = stroke.to_paint(selrect, svg_attrs, antialias);
|
||||
|
||||
// Apply both blur and shadow filters if present, composing them if necessary.
|
||||
let filter = compose_filters(blur, shadow);
|
||||
paint.set_image_filter(filter);
|
||||
|
||||
canvas.draw_oval(stroke_rect, &paint);
|
||||
// Helper to draw the stroke without any clipping logic.
|
||||
let draw_simple = |canvas: &skia::Canvas| {
|
||||
canvas.draw_oval(stroke_rect, &paint);
|
||||
};
|
||||
|
||||
// Igual que en el caso de los rectángulos: con blur activo, el clipping
|
||||
// introduce un corte visible en el blur. Para evitarlo, cuando hay blur
|
||||
// volvemos al comportamiento sin clipping.
|
||||
if blur.is_some() {
|
||||
draw_simple(canvas);
|
||||
return;
|
||||
}
|
||||
|
||||
// Para la mayoría de estilos sin blur, basta con dibujar sin clipping.
|
||||
// Solo mantenemos clipping para el caso problemático de dotted inner/outer
|
||||
// (evitar ver “medios puntos” cruzando el borde).
|
||||
if !matches!(stroke.style, StrokeStyle::Dotted)
|
||||
|| matches!(stroke.kind, StrokeKind::Center)
|
||||
{
|
||||
draw_simple(canvas);
|
||||
return;
|
||||
}
|
||||
|
||||
// For inner/outer dotted strokes, we need clipping to prevent stroke pattern
|
||||
// (like dotted circles) from appearing in wrong areas
|
||||
match stroke.kind {
|
||||
StrokeKind::Inner => {
|
||||
// Inner: clip to original rect to hide parts outside boundary
|
||||
canvas.save();
|
||||
let mut clip_path = skia::Path::new();
|
||||
clip_path.add_oval(rect, None);
|
||||
canvas.clip_path(&clip_path, skia::ClipOp::Intersect, antialias);
|
||||
canvas.draw_oval(stroke_rect, &paint);
|
||||
canvas.restore();
|
||||
}
|
||||
StrokeKind::Outer => {
|
||||
// Outer: clip to exclude original rect to hide parts inside boundary
|
||||
canvas.save();
|
||||
let mut clip_path = skia::Path::new();
|
||||
clip_path.add_oval(rect, None);
|
||||
canvas.clip_path(&clip_path, skia::ClipOp::Difference, antialias);
|
||||
canvas.draw_oval(stroke_rect, &paint);
|
||||
canvas.restore();
|
||||
}
|
||||
StrokeKind::Center => {
|
||||
// Center strokes don't need clipping
|
||||
canvas.draw_oval(stroke_rect, &paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_outer_stroke_path(
|
||||
|
||||
@@ -128,20 +128,28 @@ impl Stroke {
|
||||
}
|
||||
|
||||
pub fn outer_rect(&self, rect: &Rect) -> Rect {
|
||||
match self.kind {
|
||||
StrokeKind::Inner => Rect::from_xywh(
|
||||
rect.left + (self.width / 2.),
|
||||
rect.top + (self.width / 2.),
|
||||
rect.width() - self.width,
|
||||
rect.height() - self.width,
|
||||
),
|
||||
StrokeKind::Center => Rect::from_xywh(rect.left, rect.top, rect.width(), rect.height()),
|
||||
StrokeKind::Outer => Rect::from_xywh(
|
||||
rect.left - (self.width / 2.),
|
||||
rect.top - (self.width / 2.),
|
||||
rect.width() + self.width,
|
||||
rect.height() + self.width,
|
||||
),
|
||||
match (self.kind, self.style) {
|
||||
(StrokeKind::Inner, StrokeStyle::Dotted) | (StrokeKind::Outer, StrokeStyle::Dotted) => {
|
||||
// Boundary so circles center on it and semicircles match after clipping
|
||||
*rect
|
||||
}
|
||||
_ => match self.kind {
|
||||
StrokeKind::Inner => Rect::from_xywh(
|
||||
rect.left + (self.width / 2.),
|
||||
rect.top + (self.width / 2.),
|
||||
rect.width() - self.width,
|
||||
rect.height() - self.width,
|
||||
),
|
||||
StrokeKind::Center => {
|
||||
Rect::from_xywh(rect.left, rect.top, rect.width(), rect.height())
|
||||
}
|
||||
StrokeKind::Outer => Rect::from_xywh(
|
||||
rect.left - (self.width / 2.),
|
||||
rect.top - (self.width / 2.),
|
||||
rect.width() + self.width,
|
||||
rect.height() + self.width,
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +163,11 @@ impl Stroke {
|
||||
}
|
||||
|
||||
pub fn outer_corners(&self, corners: &Corners) -> Corners {
|
||||
if matches!(self.style, StrokeStyle::Dotted | StrokeStyle::Dashed) {
|
||||
// Path at boundary so no corner offset
|
||||
return *corners;
|
||||
}
|
||||
|
||||
let offset = match self.kind {
|
||||
StrokeKind::Center => 0.0,
|
||||
StrokeKind::Inner => -self.width / 2.0,
|
||||
@@ -200,18 +213,39 @@ impl Stroke {
|
||||
let path_effect = match self.style {
|
||||
StrokeStyle::Dotted => {
|
||||
let mut circle_path = skia::Path::new();
|
||||
let width = match self.kind {
|
||||
StrokeKind::Inner => self.width,
|
||||
StrokeKind::Center => self.width / 2.0,
|
||||
StrokeKind::Outer => self.width,
|
||||
};
|
||||
circle_path.add_circle((0.0, 0.0), width, None);
|
||||
// Radio base para el patrón de puntos
|
||||
let radius = self.width / 2.0;
|
||||
|
||||
// Usar media circunferencia que viva solo “después” del
|
||||
// origen del path (x >= 0) para que no haya nada del
|
||||
// patrón antes de que empiece el trazo.
|
||||
// - Inner: arco superior (y > 0)
|
||||
// - Outer: arco inferior (y < 0)
|
||||
// - Center: círculo completo como antes
|
||||
match self.kind {
|
||||
StrokeKind::Inner => {
|
||||
let rect =
|
||||
skia::Rect::from_xywh(0.0, -radius, radius * 2.0, radius * 2.0);
|
||||
// De (0, 0) a (2r, 0) pasando por y > 0
|
||||
circle_path.add_arc(rect, 180.0, 180.0);
|
||||
}
|
||||
StrokeKind::Outer => {
|
||||
let rect =
|
||||
skia::Rect::from_xywh(0.0, -radius, radius * 2.0, radius * 2.0);
|
||||
// De (0, 0) a (2r, 0) pasando por y < 0
|
||||
circle_path.add_arc(rect, 180.0, -180.0);
|
||||
}
|
||||
StrokeKind::Center => {
|
||||
circle_path.add_circle((0.0, 0.0), radius, None);
|
||||
}
|
||||
}
|
||||
|
||||
let advance = self.width + 5.0;
|
||||
skia::PathEffect::path_1d(
|
||||
&circle_path,
|
||||
advance,
|
||||
0.0,
|
||||
skia::path_1d_path_effect::Style::Translate,
|
||||
skia::path_1d_path_effect::Style::Rotate,
|
||||
)
|
||||
}
|
||||
StrokeStyle::Dashed => {
|
||||
|
||||
Reference in New Issue
Block a user