diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index c6273d1c33..ac4629f0b8 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -10,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( @@ -29,6 +64,148 @@ fn draw_stroke_on_rect( // - A bigger rect if it's an outer stroke // - A smaller rect if it's an inner stroke let stroke_rect = stroke.aligned_rect(rect, scale); + + // 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); diff --git a/render-wasm/src/shapes/strokes.rs b/render-wasm/src/shapes/strokes.rs index baa40c88d2..0f0521fe29 100644 --- a/render-wasm/src/shapes/strokes.rs +++ b/render-wasm/src/shapes/strokes.rs @@ -213,39 +213,30 @@ 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, - }; + // Radio base para el patrón de puntos + let radius = self.width / 2.0; - // Experimental: usar exactamente media circunferencia: - // - Inner: arco superior (mirando “hacia dentro”) - // - Outer: arco inferior (mirando “hacia fuera”) + // 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( - -width, - -width, - width * 2.0, - width * 2.0, - ); - // De (width, 0) a (-width, 0) pasando por y > 0 - circle_path.add_arc(rect, 0.0, 180.0); + 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( - -width, - -width, - width * 2.0, - width * 2.0, - ); - // De (width, 0) a (-width, 0) pasando por y < 0 - circle_path.add_arc(rect, 0.0, -180.0); + 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), width, None); + circle_path.add_circle((0.0, 0.0), radius, None); } }