diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 3ca24e625..099a954fd 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -106,8 +106,22 @@ [clip-content] (h/call internal-module "_set_shape_clip_content" clip-content)) +(defn- translate-shape-type + [type] + (case type + :frame 0 + :group 1 + :bool 2 + :rect 3 + :path 4 + :text 5 + :circle 6 + :svg-raw 7 + :image 8)) + (defn set-shape-type [type {:keys [masked]}] + (h/call internal-module "_set_shape_type" (translate-shape-type type)) (cond (= type :circle) (h/call internal-module "_set_shape_kind_circle") @@ -541,6 +555,8 @@ type (dm/get-prop shape :type) masked (dm/get-prop shape :masked-group) selrect (dm/get-prop shape :selrect) + constraint-h (dm/get-prop shape :constraints-h) + constraint-v (dm/get-prop shape :constraints-v) clip-content (if (= type :frame) (not (dm/get-prop shape :show-content)) false) @@ -569,6 +585,8 @@ (set-shape-type type {:masked masked}) (set-shape-clip-content clip-content) (set-shape-selrect selrect) + (set-constraints-h constraint-h) + (set-constraints-v constraint-v) (set-shape-rotation rotation) (set-shape-transform transform) (set-shape-blend-mode blend-mode) diff --git a/render-wasm/docs/serialization.md b/render-wasm/docs/serialization.md index 08ecb179e..7d3e463ba 100644 --- a/render-wasm/docs/serialization.md +++ b/render-wasm/docs/serialization.md @@ -1,5 +1,51 @@ # Serialization +## Shape Type + +Shape types are serialized as `u8`: + +| Value | Field | +| ----- | ---------- | +| 0 | Frame | +| 1 | Group | +| 2 | Bool | +| 3 | Rect | +| 4 | Path | +| 5 | Text | +| 6 | Circle | +| 7 | SvgRaw | +| 8 | Image | +| \_ | Rect | + + +## Horizontal Constraint + +Horizontal constraints are serialized as `u8`: + +| Value | Field | +| ----- | --------- | +| 0 | Left | +| 1 | Right | +| 2 | LeftRight | +| 3 | Center | +| 4 | Scale | +| \_ | None | + + +## Vertical Constraint + +Vertical constraints are serialized as `u8`: + +| Value | Field | +| ----- | --------- | +| 0 | Top | +| 1 | Bottom | +| 2 | TopBottom | +| 3 | Center | +| 4 | Scale | +| \_ | None | + + ## Paths Paths are made of segments of **28 bytes** each. The layout (assuming positions in a `Uint8Array`) is the following: diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 464ebb1b2..8016bcf94 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -1,3 +1,4 @@ +use skia::Rect; use skia_safe as skia; mod debug; @@ -10,7 +11,7 @@ mod utils; mod view; use crate::mem::SerializableResult; -use crate::shapes::{BoolType, ConstraintH, ConstraintV, Group, Kind, Path, TransformEntry}; +use crate::shapes::{BoolType, ConstraintH, ConstraintV, Group, Kind, Path, TransformEntry, Type}; use crate::state::State; use crate::utils::uuid_from_u32_quartet; @@ -142,7 +143,7 @@ pub extern "C" fn set_shape_kind_circle() { let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { - shape.set_kind(Kind::Circle(math::Rect::new_empty())); + shape.set_kind(Kind::Circle(Rect::new_empty())); } } @@ -153,7 +154,7 @@ pub extern "C" fn set_shape_kind_rect() { if let Some(shape) = state.current_shape() { match shape.kind() { Kind::Rect(_, _) => {} - _ => shape.set_kind(Kind::Rect(math::Rect::new_empty(), None)), + _ => shape.set_kind(Kind::Rect(Rect::new_empty(), None)), } } } @@ -185,6 +186,15 @@ pub extern "C" fn set_shape_bool_type(raw_bool_type: u8) { } } +#[no_mangle] +pub unsafe extern "C" fn set_shape_type(shape_type: u8) { + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); + + if let Some(shape) = state.current_shape() { + shape.set_shape_type(Type::from(shape_type)); + } +} + #[no_mangle] pub extern "C" fn set_shape_selrect(left: f32, top: f32, right: f32, bottom: f32) { let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); diff --git a/render-wasm/src/math.rs b/render-wasm/src/math.rs index d402b23dc..67c1025db 100644 --- a/render-wasm/src/math.rs +++ b/render-wasm/src/math.rs @@ -1,4 +1,330 @@ -use skia_safe as skia; +use skia_safe::{Matrix, Point, Vector}; -pub type Rect = skia::Rect; -pub type Point = (f32, f32); +pub trait VectorExt { + fn new_points(a: &Point, b: &Point) -> Vector; +} + +impl VectorExt for Vector { + // Creates a vector from two points + fn new_points(from: &Point, to: &Point) -> Vector { + Vector::new(to.x - from.x, to.y - from.y) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Bounds { + pub nw: Point, + pub ne: Point, + pub se: Point, + pub sw: Point, +} + +fn vec_min_max(arr: &[Option]) -> Option<(f32, f32)> { + let mut minv: Option = None; + let mut maxv: Option = None; + + for it in arr { + if let Some(it) = *it { + match minv { + None => minv = Some(it), + Some(n) => minv = Some(f32::min(it, n)), + } + match maxv { + None => maxv = Some(it), + Some(n) => maxv = Some(f32::max(it, n)), + } + } + } + + Some((minv?, maxv?)) +} + +impl Bounds { + pub fn new(nw: Point, ne: Point, se: Point, sw: Point) -> Self { + Self { nw, ne, se, sw } + } + + pub fn horizontal_vec(&self) -> Vector { + Vector::new_points(&self.nw, &self.ne) + } + + pub fn vertical_vec(&self) -> Vector { + Vector::new_points(&self.nw, &self.sw) + } + + pub fn hv(&self, scalar: f32) -> Vector { + let mut hv = self.horizontal_vec(); + hv.normalize(); + hv.scale(scalar); + hv + } + + pub fn vv(&self, scalar: f32) -> Vector { + let mut vv = self.vertical_vec(); + vv.normalize(); + vv.scale(scalar); + vv + } + + pub fn width(&self) -> f32 { + Point::distance(self.nw, self.ne) + } + + pub fn height(&self) -> f32 { + Point::distance(self.nw, self.sw) + } + + pub fn transform(&self, mtx: &Matrix) -> Self { + Self { + nw: mtx.map_point(self.nw), + ne: mtx.map_point(self.ne), + se: mtx.map_point(self.se), + sw: mtx.map_point(self.sw), + } + } + + pub fn transform_mut(&mut self, mtx: &Matrix) { + self.nw = mtx.map_point(self.nw); + self.ne = mtx.map_point(self.ne); + self.se = mtx.map_point(self.se); + self.sw = mtx.map_point(self.sw); + } + + pub fn box_bounds(&self, other: &Self) -> Option { + let hv = self.horizontal_vec(); + let vv = self.vertical_vec(); + + let hr = Ray::new(self.nw, hv); + let vr = Ray::new(self.nw, vv); + + let (min_ht, max_ht) = vec_min_max(&[ + intersect_rays_t(&hr, &Ray::new(other.nw, vv)), + intersect_rays_t(&hr, &Ray::new(other.ne, vv)), + intersect_rays_t(&hr, &Ray::new(other.sw, vv)), + intersect_rays_t(&hr, &Ray::new(other.se, vv)), + ])?; + + let (min_vt, max_vt) = vec_min_max(&[ + intersect_rays_t(&vr, &Ray::new(other.nw, hv)), + intersect_rays_t(&vr, &Ray::new(other.ne, hv)), + intersect_rays_t(&vr, &Ray::new(other.sw, hv)), + intersect_rays_t(&vr, &Ray::new(other.se, hv)), + ])?; + + let nw = intersect_rays(&Ray::new(hr.t(min_ht), vv), &Ray::new(vr.t(min_vt), hv))?; + let ne = intersect_rays(&Ray::new(hr.t(max_ht), vv), &Ray::new(vr.t(min_vt), hv))?; + let sw = intersect_rays(&Ray::new(hr.t(min_ht), vv), &Ray::new(vr.t(max_vt), hv))?; + let se = intersect_rays(&Ray::new(hr.t(max_ht), vv), &Ray::new(vr.t(max_vt), hv))?; + + Some(Self { nw, ne, se, sw }) + } + + pub fn left(&self, p: Point) -> f32 { + let hr = Ray::new(p, self.horizontal_vec()); + let vr = Ray::new(self.nw, self.vertical_vec()); + if let Some(project_point) = intersect_rays(&hr, &vr) { + if vr.is_positive_side(&p) { + -Point::distance(project_point, p) + } else { + Point::distance(project_point, p) + } + } else { + // This should not happen. All points should have a proyection so the + // intersection ray should always exist + 0.0 + } + } + + pub fn right(&self, p: Point) -> f32 { + let hr = Ray::new(p, self.horizontal_vec()); + let vr = Ray::new(self.ne, self.vertical_vec()); + if let Some(project_point) = intersect_rays(&hr, &vr) { + if vr.is_positive_side(&p) { + Point::distance(project_point, p) + } else { + -Point::distance(project_point, p) + } + } else { + // This should not happen. All points should have a proyection so the + // intersection ray should always exist + 0.0 + } + } + + pub fn top(&self, p: Point) -> f32 { + let vr = Ray::new(p, self.vertical_vec()); + let hr = Ray::new(self.nw, self.horizontal_vec()); + if let Some(project_point) = intersect_rays(&vr, &hr) { + if hr.is_positive_side(&p) { + Point::distance(project_point, p) + } else { + -Point::distance(project_point, p) + } + } else { + // This should not happen. All points should have a proyection so the + // intersection ray should always exist + 0.0 + } + } + + pub fn bottom(&self, p: Point) -> f32 { + let vr = Ray::new(p, self.vertical_vec()); + let hr = Ray::new(self.sw, self.horizontal_vec()); + if let Some(project_point) = intersect_rays(&vr, &hr) { + if hr.is_positive_side(&p) { + -Point::distance(project_point, p) + } else { + Point::distance(project_point, p) + } + } else { + // This should not happen. All points should have a proyection so the + // intersection ray should always exist + 0.0 + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Ray { + origin: Point, + direction: Vector, +} + +impl Ray { + pub fn new(origin: Point, direction: Vector) -> Self { + Self { origin, direction } + } + + pub fn t(&self, t: f32) -> Point { + self.origin + self.direction * t + } + + pub fn is_positive_side(&self, p: &Point) -> bool { + let a = self.direction.y; + let b = -self.direction.x; + let c = self.direction.x * self.origin.y - self.direction.y * self.origin.x; + let v = p.x * a + p.y * b + c; + v < 0.0 + } +} + +pub fn intersect_rays_t(ray1: &Ray, ray2: &Ray) -> Option { + let p1 = ray1.origin; + let d1 = ray1.direction; + let p2 = ray2.origin; + let d2 = ray2.direction; + + // Calculate the determinant to check if the rays are parallel + let determinant = d1.cross(d2); + if determinant.abs() < f32::EPSILON { + // Parallel rays, no intersection + return None; + } + + // Solve for t1 and t2 parameters + let diff = p2 - p1; + + Some(diff.cross(d2) / determinant) +} + +pub fn intersect_rays(ray1: &Ray, ray2: &Ray) -> Option { + if let Some(t) = intersect_rays_t(ray1, ray2) { + Some(ray1.t(t)) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ray_parameter() { + let r = Ray::new(Point::new(0.0, 0.0), Vector::new(0.5, 0.5)); + assert_eq!(r.t(1.0), Point::new(0.5, 0.5)); + assert_eq!(r.t(2.0), Point::new(1.0, 1.0)); + assert_eq!(r.t(-2.0), Point::new(-1.0, -1.0)); + } + + #[test] + fn test_intersect() { + // Test Cases for Ray-Ray Intersections + // Simple Intersection at (2, 2) + let r1 = Ray::new(Point::new(0.0, 0.0), Vector::new(1.0, 1.0)); + let r2 = Ray::new(Point::new(0.0, 4.0), Vector::new(1.0, -1.0)); + assert_eq!(intersect_rays(&r1, &r2), Some(Point::new(2.0, 2.0))); + + // Parallel Rays (No Intersection) + let r1 = Ray::new(Point::new(0.0, 0.0), Vector::new(1.0, 1.0)); + let r2 = Ray::new(Point::new(0.0, 2.0), Vector::new(1.0, 1.0)); + assert_eq!(intersect_rays(&r1, &r2), None); + + // Coincident Rays (Infinite Intersections) + let r1 = Ray::new(Point::new(0.0, 0.0), Vector::new(1.0, 1.0)); + let r2 = Ray::new(Point::new(1.0, 1.0), Vector::new(1.0, 1.0)); + assert_eq!(intersect_rays(&r1, &r2), None); + + let r1 = Ray::new(Point::new(1.0, 0.0), Vector::new(2.0, 1.0)); + let r2 = Ray::new(Point::new(4.0, 4.0), Vector::new(-1.0, -1.0)); + assert_eq!(intersect_rays(&r1, &r2), Some(Point::new(-1.0, -1.0))); + + let r1 = Ray::new(Point::new(1.0, 1.0), Vector::new(3.0, 2.0)); + let r2 = Ray::new(Point::new(4.0, 0.0), Vector::new(-2.0, 3.0)); + assert_eq!( + intersect_rays(&r1, &r2), + Some(Point::new(2.6153846, 2.0769231)) + ); + } + + #[test] + fn test_vec_min_max() { + assert_eq!(None, vec_min_max(&[])); + assert_eq!(None, vec_min_max(&[None, None])); + assert_eq!(Some((1.0, 1.0)), vec_min_max(&[None, Some(1.0)])); + assert_eq!( + Some((0.0, 1.0)), + vec_min_max(&[Some(0.3), None, Some(0.0), Some(0.7), Some(1.0), Some(0.1)]) + ); + } + + #[test] + fn test_box_bounds() { + let b1 = Bounds::new( + Point::new(1.0, 5.0), + Point::new(5.0, 5.0), + Point::new(5.0, 1.0), + Point::new(1.0, 1.0), + ); + let b2 = Bounds::new( + Point::new(3.0, 4.0), + Point::new(4.0, 3.0), + Point::new(3.0, 2.0), + Point::new(2.0, 3.0), + ); + let result = b1.box_bounds(&b2); + assert_eq!( + Some(Bounds::new( + Point::new(2.0, 4.0), + Point::new(4.0, 4.0), + Point::new(4.0, 2.0), + Point::new(2.0, 2.0), + )), + result + ) + } + + #[test] + fn test_bounds_distances() { + let b1 = Bounds::new( + Point::new(1.0, 10.0), + Point::new(8.0, 10.0), + Point::new(8.0, 1.0), + Point::new(1.0, 1.0), + ); + assert_eq!(b1.left(Point::new(4.0, 8.0)), -3.0); + assert_eq!(b1.top(Point::new(4.0, 8.0)), -2.0); + assert_eq!(b1.right(Point::new(7.0, 6.0),), -1.0); + assert_eq!(b1.bottom(Point::new(7.0, 6.0),), -5.0); + } +} diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 55964ea9d..17c0564e3 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -1,10 +1,8 @@ -use skia_safe as skia; +use skia_safe::{self as skia, Contains, Matrix, Rect}; use std::collections::HashMap; use uuid::Uuid; -use crate::math; use crate::view::Viewbox; -use skia::{Contains, Matrix}; mod blend; mod cache; @@ -45,7 +43,7 @@ pub struct NodeRenderState { // We use this bool to keep that we've traversed all the children inside this node. pub visited_children: bool, // This is used to clip the content of frames. - pub clip_bounds: Option, + pub clip_bounds: Option<(Rect, Matrix)>, // This is a flag to indicate that we've already drawn the mask of a masked group. pub visited_mask: bool, // This bool indicates that we're drawing the mask shape. @@ -273,27 +271,39 @@ impl RenderState { &mut self, shape: &mut Shape, modifiers: Option<&Matrix>, - clip_bounds: Option, + clip_bounds: Option<(Rect, Matrix)>, ) { - if let Some(modifiers) = modifiers { - self.drawing_surface.canvas().concat(&modifiers); - } + if let Some((bounds, transform)) = clip_bounds { + self.drawing_surface.canvas().concat(&transform); + self.drawing_surface + .canvas() + .clip_rect(bounds, skia::ClipOp::Intersect, true); - let center = shape.bounds().center(); + if self.options.is_debug_visible() { + let mut paint = skia::Paint::default(); + paint.set_style(skia::PaintStyle::Stroke); + paint.set_color(skia::Color::from_argb(255, 255, 0, 0)); + paint.set_stroke_width(4.); + self.drawing_surface.canvas().draw_rect(bounds, &paint); + } + + self.drawing_surface + .canvas() + .concat(&transform.invert().unwrap()); + } + let center = shape.center(); // Transform the shape in the center let mut matrix = shape.transform.clone(); matrix.post_translate(center); matrix.pre_translate(-center); - self.drawing_surface.canvas().concat(&matrix); - - if let Some(bounds) = clip_bounds { - self.drawing_surface - .canvas() - .clip_rect(bounds, skia::ClipOp::Intersect, true); + if let Some(modifiers) = modifiers { + matrix.post_concat(&modifiers); } + self.drawing_surface.canvas().concat(&matrix); + match &shape.kind { Kind::SVGRaw(sr) => { if let Some(svg) = shape.svg.as_ref() { @@ -503,7 +513,7 @@ impl RenderState { .to_string(), )?; - let render_complete = self.viewbox.area.contains(element.bounds()); + let render_complete = self.viewbox.area.contains(element.selrect()); if visited_children { if !visited_mask { match element.kind { @@ -553,7 +563,7 @@ impl RenderState { // If we didn't visited_children this shape, then we need to do if !node_render_state.id.is_nil() { - if !element.bounds().intersects(self.viewbox.area) || element.hidden() { + if !element.selrect().intersects(self.viewbox.area) || element.hidden() { debug::render_debug_shape(self, element, false); self.render_complete = render_complete; continue; @@ -622,7 +632,16 @@ impl RenderState { if element.is_recursive() { let children_clip_bounds = - (!node_render_state.id.is_nil() & element.clip()).then(|| element.bounds()); + (!node_render_state.id.is_nil() & element.clip()).then(|| { + let bounds = element.selrect(); + let mut transform = element.transform; + transform.post_translate(bounds.center()); + transform.pre_translate(-bounds.center()); + if let Some(modifiers) = modifiers.get(&element.id) { + transform.post_concat(&modifiers); + } + (bounds, transform) + }); for child_id in element.children_ids().iter().rev() { self.pending_nodes.push(NodeRenderState { diff --git a/render-wasm/src/render/debug.rs b/render-wasm/src/render/debug.rs index ada9d8b66..2a38d2803 100644 --- a/render-wasm/src/render/debug.rs +++ b/render-wasm/src/render/debug.rs @@ -49,7 +49,7 @@ pub fn render_debug_shape(render_state: &mut RenderState, element: &Shape, inter }); paint.set_stroke_width(1.); - let mut scaled_rect = element.bounds(); + let mut scaled_rect = element.selrect(); let x = 100. + scaled_rect.x() * 0.2; let y = 100. + scaled_rect.y() * 0.2; let width = scaled_rect.width() * 0.2; diff --git a/render-wasm/src/render/fills.rs b/render-wasm/src/render/fills.rs index 152f93bbc..b9e594c7c 100644 --- a/render-wasm/src/render/fills.rs +++ b/render-wasm/src/render/fills.rs @@ -1,8 +1,5 @@ -use crate::{ - math, - shapes::{Fill, ImageFill, Kind, Shape}, -}; -use skia_safe::{self as skia, RRect}; +use crate::shapes::{Fill, ImageFill, Kind, Shape}; +use skia_safe::{self as skia, RRect, Rect}; use super::RenderState; @@ -46,7 +43,7 @@ fn draw_image_fill_in_container( let scaled_width = width * scale; let scaled_height = height * scale; - let dest_rect = math::Rect::from_xywh( + let dest_rect = Rect::from_xywh( container.left - (scaled_width - container_width) / 2.0, container.top - (scaled_height - container_height) / 2.0, scaled_width, diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index 2a1996878..32960e196 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; -use crate::math::{self, Rect}; use crate::shapes::{Corners, Fill, ImageFill, Kind, Path, Shape, Stroke, StrokeCap, StrokeKind}; +use skia::Rect; use skia_safe::{self as skia, RRect}; use super::RenderState; @@ -297,7 +297,7 @@ fn draw_triangle_cap( canvas.draw_path(&path, paint); } -fn calculate_scaled_rect(size: (i32, i32), container: &math::Rect, delta: f32) -> math::Rect { +fn calculate_scaled_rect(size: (i32, i32), container: &Rect, delta: f32) -> Rect { let (width, height) = (size.0 as f32, size.1 as f32); let image_aspect_ratio = width / height; @@ -315,7 +315,7 @@ fn calculate_scaled_rect(size: (i32, i32), container: &math::Rect, delta: f32) - let scaled_width = width * scale; let scaled_height = height * scale; - math::Rect::from_xywh( + Rect::from_xywh( container.left - delta - (scaled_width - container_width) / 2.0, container.top - delta - (scaled_height - container_height) / 2.0, scaled_width + (2. * delta) + (scaled_width - container_width), diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index e776f8b5b..1e12bdaba 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -1,10 +1,9 @@ -use crate::math; -use skia_safe as skia; +use skia_safe::{self as skia, Matrix, Point, Rect}; + use std::collections::HashMap; use uuid::Uuid; use crate::render::BlendMode; -use skia::Matrix; mod blurs; mod bools; @@ -28,13 +27,45 @@ pub use strokes::*; pub use svgraw::*; pub use transform::*; -pub type CornerRadius = skia::Point; +use crate::math::Bounds; + +pub type CornerRadius = Point; pub type Corners = [CornerRadius; 4]; +#[derive(Debug, Clone, PartialEq)] +pub enum Type { + Frame, + Group, + Bool, + Rect, + Path, + Text, + Circle, + SvgRaw, + Image, +} + +impl Type { + pub fn from(value: u8) -> Self { + match value { + 0 => Type::Frame, + 1 => Type::Group, + 2 => Type::Bool, + 3 => Type::Rect, + 4 => Type::Path, + 5 => Type::Text, + 6 => Type::Circle, + 7 => Type::SvgRaw, + 8 => Type::Image, + _ => Type::Rect, + } + } +} + #[derive(Debug, Clone, PartialEq)] pub enum Kind { - Rect(math::Rect, Option), - Circle(math::Rect), + Rect(Rect, Option), + Circle(Rect), Path(Path), Bool(BoolType, Path), SVGRaw(SVGRaw), @@ -91,9 +122,10 @@ pub type Color = skia::Color; #[allow(dead_code)] pub struct Shape { pub id: Uuid, + pub shape_type: Type, pub children: Vec, pub kind: Kind, - pub selrect: math::Rect, + pub selrect: Rect, pub transform: Matrix, pub rotation: f32, pub constraint_h: Option, @@ -114,9 +146,10 @@ impl Shape { pub fn new(id: Uuid) -> Self { Self { id, + shape_type: Type::Rect, children: Vec::::new(), - kind: Kind::Rect(math::Rect::new_empty(), None), - selrect: math::Rect::new_empty(), + kind: Kind::Rect(Rect::new_empty(), None), + selrect: Rect::new_empty(), transform: Matrix::default(), rotation: 0., constraint_h: None, @@ -134,8 +167,12 @@ impl Shape { } } - pub fn kind(&self) -> Kind { - self.kind.clone() + pub fn set_shape_type(&mut self, shape_type: Type) { + self.shape_type = shape_type; + } + + pub fn is_frame(&self) -> bool { + self.shape_type == Type::Frame } pub fn set_selrect(&mut self, left: f32, top: f32, right: f32, bottom: f32) { @@ -155,6 +192,10 @@ impl Shape { self.kind = kind; } + pub fn kind(&self) -> Kind { + self.kind.clone() + } + pub fn set_clip(&mut self, value: bool) { self.clip_content = value; } @@ -175,10 +216,18 @@ impl Shape { self.constraint_h = constraint; } + pub fn constraint_h(&self, default: ConstraintH) -> ConstraintH { + self.constraint_h.clone().unwrap_or(default) + } + pub fn set_constraint_v(&mut self, constraint: Option) { self.constraint_v = constraint; } + pub fn constraint_v(&self, default: ConstraintV) -> ConstraintV { + self.constraint_v.clone().unwrap_or(default) + } + pub fn set_hidden(&mut self, value: bool) { self.hidden = value; } @@ -339,10 +388,36 @@ impl Shape { self.hidden } - pub fn bounds(&self) -> math::Rect { + // TODO: Maybe store this inside the shape + pub fn bounds(&self) -> Bounds { + let mut bounds = Bounds::new( + Point::new(self.selrect.x(), self.selrect.y()), + Point::new(self.selrect.x() + self.selrect.width(), self.selrect.y()), + Point::new( + self.selrect.x() + self.selrect.width(), + self.selrect.y() + self.selrect.height(), + ), + Point::new(self.selrect.x(), self.selrect.y() + self.selrect.height()), + ); + + let center = self.center(); + let mut matrix = self.transform.clone(); + matrix.post_translate(center); + matrix.pre_translate(-center); + + bounds.transform_mut(&matrix); + + bounds + } + + pub fn selrect(&self) -> Rect { self.selrect } + pub fn center(&self) -> Point { + self.selrect.center() + } + pub fn clip(&self) -> bool { self.clip_content } @@ -405,11 +480,11 @@ impl Shape { .filter(|shadow| shadow.style() == ShadowStyle::Inner) } - pub fn to_path_transform(&self) -> Option { + pub fn to_path_transform(&self) -> Option { match self.kind { Kind::Path(_) | Kind::Bool(_, _) => { - let center = self.bounds().center(); - let mut matrix = skia::Matrix::new_identity(); + let center = self.center(); + let mut matrix = Matrix::new_identity(); matrix.pre_translate(center); matrix.pre_concat(&self.transform.invert()?); matrix.pre_translate(-center); diff --git a/render-wasm/src/shapes/fills.rs b/render-wasm/src/shapes/fills.rs index cad8d4c96..d94c21fc0 100644 --- a/render-wasm/src/shapes/fills.rs +++ b/render-wasm/src/shapes/fills.rs @@ -1,7 +1,6 @@ -use skia_safe as skia; +use skia_safe::{self as skia, Rect}; use super::Color; -use crate::math; use uuid::Uuid; #[derive(Debug)] @@ -44,7 +43,7 @@ impl Gradient { self.offsets.push(offset); } - fn to_linear_shader(&self, rect: &math::Rect) -> Option { + fn to_linear_shader(&self, rect: &Rect) -> Option { let start = ( rect.left + self.start.0 * rect.width(), rect.top + self.start.1 * rect.height(), @@ -63,7 +62,7 @@ impl Gradient { ) } - fn to_radial_shader(&self, rect: &math::Rect) -> Option { + fn to_radial_shader(&self, rect: &Rect) -> Option { let center = skia::Point::new( rect.left + self.start.0 * rect.width(), rect.top + self.start.1 * rect.height(), @@ -159,7 +158,7 @@ impl Fill { }) } - pub fn to_paint(&self, rect: &math::Rect) -> skia::Paint { + pub fn to_paint(&self, rect: &Rect) -> skia::Paint { match self { Self::Solid(color) => { let mut p = skia::Paint::default(); diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index 2bfa7d76b..c4d28215b 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -1,22 +1,162 @@ +use std::collections::HashMap; + +use skia::Matrix; use skia_safe as skia; use std::collections::HashSet; use uuid::Uuid; -use crate::shapes::{Shape, TransformEntry}; +use crate::math::Bounds; +use crate::shapes::{ConstraintH, ConstraintV, Shape, TransformEntry}; use crate::state::State; -fn propagate_shape(_state: &State, shape: &Shape, transform: skia::Matrix) -> Vec { - let children: Vec = shape - .children - .iter() - .map(|id| TransformEntry { - id: id.clone(), - transform, - }) - .collect(); +fn calculate_new_bounds( + constraint_h: ConstraintH, + constraint_v: ConstraintV, + parent_before: &Bounds, + parent_after: &Bounds, + child_bounds: &Bounds, +) -> (f32, f32, f32, f32) { + let (delta_left, scale_width) = match constraint_h { + ConstraintH::Scale => { + let width_scale = parent_after.width() / parent_before.width(); + let target_left = parent_before.left(child_bounds.nw) * width_scale; + let current_left = parent_after.left(child_bounds.nw); + (target_left - current_left, width_scale) + } + ConstraintH::Left => { + let target_left = parent_before.left(child_bounds.nw); + let current_left = parent_after.left(child_bounds.nw); + (target_left - current_left, 1.0) + } + ConstraintH::Right => { + let target_right = parent_before.right(child_bounds.ne); + let current_right = parent_after.right(child_bounds.ne); + (current_right - target_right, 1.0) + } + ConstraintH::LeftRight => { + let target_left = parent_before.left(child_bounds.nw); + let target_right = parent_before.right(child_bounds.ne); + let current_left = parent_after.left(child_bounds.nw); + let new_width = parent_after.width() - target_left - target_right; + let width_scale = new_width / child_bounds.width(); + (target_left - current_left, width_scale) + } + ConstraintH::Center => { + let delta_width = parent_after.width() - parent_before.width(); + let delta_left = delta_width / 2.0; + (delta_left, 1.0) + } + }; - children + let (delta_top, scale_height) = match constraint_v { + ConstraintV::Scale => { + let height_scale = parent_after.height() / parent_before.height(); + let target_top = parent_before.top(child_bounds.nw) * height_scale; + let current_top = parent_after.top(child_bounds.nw); + (target_top - current_top, height_scale) + } + ConstraintV::Top => { + let height_scale = 1.0; + let target_top = parent_before.top(child_bounds.nw); + let current_top = parent_after.top(child_bounds.nw); + (target_top - current_top, height_scale) + } + ConstraintV::Bottom => { + let target_bottom = parent_before.bottom(child_bounds.sw); + let current_bottom = parent_after.bottom(child_bounds.sw); + (current_bottom - target_bottom, 1.0) + } + ConstraintV::TopBottom => { + let target_top = parent_before.top(child_bounds.nw); + let target_bottom = parent_before.bottom(child_bounds.sw); + let current_top = parent_after.top(child_bounds.nw); + let new_height = parent_after.height() - target_top - target_bottom; + let height_scale = new_height / child_bounds.height(); + (target_top - current_top, height_scale) + } + ConstraintV::Center => { + let delta_height = parent_after.height() - parent_before.height(); + let delta_top = delta_height / 2.0; + (delta_top, 1.0) + } + }; + + (delta_left, delta_top, scale_width, scale_height) +} + +fn propagate_shape( + shapes: &HashMap, + shape: &Shape, + transform: Matrix, +) -> Vec { + if shape.children.len() == 0 { + return vec![]; + } + + let parent_bounds_before = shape.bounds(); + let parent_bounds_after = parent_bounds_before.transform(&transform); + let mut result = Vec::new(); + + for child_id in shape.children.iter() { + if let Some(child) = shapes.get(child_id) { + let constraint_h = child.constraint_h(if shape.is_frame() { + ConstraintH::Left + } else { + ConstraintH::Scale + }); + + let constraint_v = child.constraint_v(if shape.is_frame() { + ConstraintV::Top + } else { + ConstraintV::Scale + }); + // if the constrains are scale & scale or the transform has only moves we + // can propagate as is + if (constraint_h == ConstraintH::Scale && constraint_v == ConstraintV::Scale) + || transform.is_translate() + { + result.push(TransformEntry::new(child_id.clone(), transform)); + continue; + } + + if let Some(child_bounds_before) = parent_bounds_before.box_bounds(&child.bounds()) { + let (delta_left, delta_top, scale_width, scale_height) = calculate_new_bounds( + constraint_h, + constraint_v, + &parent_bounds_before, + &parent_bounds_after, + &child_bounds_before, + ); + + // Translate position + let th = parent_bounds_after.hv(delta_left); + let tv = parent_bounds_after.vv(delta_top); + let mut transform = Matrix::translate(th + tv); + let child_bounds = child_bounds_before.transform(&transform); + + // Scale shape + let center = child.center(); + let mut parent_transform = shape.transform; + parent_transform.post_translate(center); + parent_transform.pre_translate(-center); + + let parent_transform_inv = &parent_transform.invert().unwrap(); + let origin = parent_transform_inv.map_point(child_bounds.nw); + + let mut scale = Matrix::scale((scale_width, scale_height)); + scale.post_translate(origin); + scale.post_concat(&parent_transform); + scale.pre_translate(-origin); + scale.pre_concat(&parent_transform_inv); + + transform.post_concat(&scale); + result.push(TransformEntry::new(child_id.clone(), transform)); + } + } + } + + result } pub fn propagate_modifiers(state: &State, modifiers: Vec) -> Vec { @@ -28,7 +168,7 @@ pub fn propagate_modifiers(state: &State, modifiers: Vec) -> Vec while let Some(entry) = entries.pop() { if !processed.contains(&entry.id) { if let Some(shape) = state.shapes.get(&entry.id) { - let mut children = propagate_shape(state, shape, entry.transform); + let mut children = propagate_shape(&state.shapes, shape, entry.transform); entries.append(&mut children); processed.insert(entry.id); result.push(entry.clone()); @@ -38,3 +178,38 @@ pub fn propagate_modifiers(state: &State, modifiers: Vec) -> Vec result } + +#[cfg(test)] +mod tests { + use super::*; + + use crate::shapes::Type; + use skia::Point; + + #[test] + fn test_propagate_shape() { + let mut shapes = HashMap::::new(); + + let child_id = Uuid::new_v4(); + let mut child = Shape::new(child_id); + child.set_selrect(3.0, 3.0, 2.0, 2.0); + shapes.insert(child_id, child); + + let parent_id = Uuid::new_v4(); + let mut parent = Shape::new(parent_id); + parent.set_shape_type(Type::Group); + parent.add_child(child_id); + parent.set_selrect(1.0, 1.0, 5.0, 5.0); + shapes.insert(parent_id, parent.clone()); + + let mut transform = Matrix::scale((2.0, 1.5)); + let x = parent.selrect.x(); + let y = parent.selrect.y(); + transform.post_translate(Point::new(x, y)); + transform.pre_translate(Point::new(-x, -y)); + + let result = propagate_shape(&shapes, &parent, transform); + + assert_eq!(result.len(), 1); + } +} diff --git a/render-wasm/src/shapes/paths.rs b/render-wasm/src/shapes/paths.rs index 628b8ddd5..d3b4de1c8 100644 --- a/render-wasm/src/shapes/paths.rs +++ b/render-wasm/src/shapes/paths.rs @@ -1,7 +1,7 @@ use skia_safe as skia; use std::array::TryFromSliceError; -use crate::math::Point; +type Point = (f32, f32); fn stringify_slice_err(_: TryFromSliceError) -> String { format!("Error deserializing path") diff --git a/render-wasm/src/shapes/strokes.rs b/render-wasm/src/shapes/strokes.rs index 52d992096..d68b95bbd 100644 --- a/render-wasm/src/shapes/strokes.rs +++ b/render-wasm/src/shapes/strokes.rs @@ -1,6 +1,5 @@ -use crate::math; use crate::shapes::fills::Fill; -use skia_safe as skia; +use skia_safe::{self as skia, Rect}; use std::collections::HashMap; use super::Corners; @@ -122,18 +121,18 @@ impl Stroke { } } - pub fn outer_rect(&self, rect: &math::Rect) -> math::Rect { + pub fn outer_rect(&self, rect: &Rect) -> Rect { match self.kind { - StrokeKind::InnerStroke => math::Rect::from_xywh( + StrokeKind::InnerStroke => Rect::from_xywh( rect.left + (self.width / 2.), rect.top + (self.width / 2.), rect.width() - self.width, rect.height() - self.width, ), StrokeKind::CenterStroke => { - math::Rect::from_xywh(rect.left, rect.top, rect.width(), rect.height()) + Rect::from_xywh(rect.left, rect.top, rect.width(), rect.height()) } - StrokeKind::OuterStroke => math::Rect::from_xywh( + StrokeKind::OuterStroke => Rect::from_xywh( rect.left - (self.width / 2.), rect.top - (self.width / 2.), rect.width() + self.width, @@ -158,7 +157,7 @@ impl Stroke { pub fn to_paint( &self, - rect: &math::Rect, + rect: &Rect, svg_attrs: &HashMap, scale: f32, ) -> skia::Paint { @@ -223,7 +222,7 @@ impl Stroke { pub fn to_stroked_paint( &self, is_open: bool, - rect: &math::Rect, + rect: &Rect, svg_attrs: &HashMap, scale: f32, ) -> skia::Paint { diff --git a/render-wasm/src/shapes/transform.rs b/render-wasm/src/shapes/transform.rs index b80b08e26..9808fdf96 100644 --- a/render-wasm/src/shapes/transform.rs +++ b/render-wasm/src/shapes/transform.rs @@ -12,6 +12,12 @@ pub struct TransformEntry { pub transform: Matrix, } +impl TransformEntry { + pub fn new(id: Uuid, transform: Matrix) -> Self { + TransformEntry { id, transform } + } +} + impl SerializableResult for TransformEntry { type BytesType = [u8; 40]; diff --git a/render-wasm/src/view.rs b/render-wasm/src/view.rs index 399d51138..1c2351152 100644 --- a/render-wasm/src/view.rs +++ b/render-wasm/src/view.rs @@ -1,4 +1,4 @@ -use crate::math::Rect; +use skia_safe::Rect; #[derive(Debug, Copy, Clone)] pub(crate) struct Viewbox {