Add constraints calculation on WASM (#5894)

*  Add constraints calculation on WASM

*  Fix after review
This commit is contained in:
Alonso Torres 2025-02-19 10:40:04 +01:00 committed by GitHub
parent f5c913d26e
commit 6cb1aa24cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 746 additions and 76 deletions

View file

@ -106,8 +106,22 @@
[clip-content] [clip-content]
(h/call internal-module "_set_shape_clip_content" 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 (defn set-shape-type
[type {:keys [masked]}] [type {:keys [masked]}]
(h/call internal-module "_set_shape_type" (translate-shape-type type))
(cond (cond
(= type :circle) (= type :circle)
(h/call internal-module "_set_shape_kind_circle") (h/call internal-module "_set_shape_kind_circle")
@ -541,6 +555,8 @@
type (dm/get-prop shape :type) type (dm/get-prop shape :type)
masked (dm/get-prop shape :masked-group) masked (dm/get-prop shape :masked-group)
selrect (dm/get-prop shape :selrect) 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) clip-content (if (= type :frame)
(not (dm/get-prop shape :show-content)) (not (dm/get-prop shape :show-content))
false) false)
@ -569,6 +585,8 @@
(set-shape-type type {:masked masked}) (set-shape-type type {:masked masked})
(set-shape-clip-content clip-content) (set-shape-clip-content clip-content)
(set-shape-selrect selrect) (set-shape-selrect selrect)
(set-constraints-h constraint-h)
(set-constraints-v constraint-v)
(set-shape-rotation rotation) (set-shape-rotation rotation)
(set-shape-transform transform) (set-shape-transform transform)
(set-shape-blend-mode blend-mode) (set-shape-blend-mode blend-mode)

View file

@ -1,5 +1,51 @@
# Serialization # 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
Paths are made of segments of **28 bytes** each. The layout (assuming positions in a `Uint8Array`) is the following: Paths are made of segments of **28 bytes** each. The layout (assuming positions in a `Uint8Array`) is the following:

View file

@ -1,3 +1,4 @@
use skia::Rect;
use skia_safe as skia; use skia_safe as skia;
mod debug; mod debug;
@ -10,7 +11,7 @@ mod utils;
mod view; mod view;
use crate::mem::SerializableResult; 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::state::State;
use crate::utils::uuid_from_u32_quartet; 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"); let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer");
if let Some(shape) = state.current_shape() { 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() { if let Some(shape) = state.current_shape() {
match shape.kind() { match shape.kind() {
Kind::Rect(_, _) => {} 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] #[no_mangle]
pub extern "C" fn set_shape_selrect(left: f32, top: f32, right: f32, bottom: f32) { 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"); let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer");

View file

@ -1,4 +1,330 @@
use skia_safe as skia; use skia_safe::{Matrix, Point, Vector};
pub type Rect = skia::Rect; pub trait VectorExt {
pub type Point = (f32, f32); 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<f32>]) -> Option<(f32, f32)> {
let mut minv: Option<f32> = None;
let mut maxv: Option<f32> = 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<Self> {
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<f32> {
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<Point> {
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);
}
}

View file

@ -1,10 +1,8 @@
use skia_safe as skia; use skia_safe::{self as skia, Contains, Matrix, Rect};
use std::collections::HashMap; use std::collections::HashMap;
use uuid::Uuid; use uuid::Uuid;
use crate::math;
use crate::view::Viewbox; use crate::view::Viewbox;
use skia::{Contains, Matrix};
mod blend; mod blend;
mod cache; 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. // We use this bool to keep that we've traversed all the children inside this node.
pub visited_children: bool, pub visited_children: bool,
// This is used to clip the content of frames. // This is used to clip the content of frames.
pub clip_bounds: Option<math::Rect>, pub clip_bounds: Option<(Rect, Matrix)>,
// This is a flag to indicate that we've already drawn the mask of a masked group. // This is a flag to indicate that we've already drawn the mask of a masked group.
pub visited_mask: bool, pub visited_mask: bool,
// This bool indicates that we're drawing the mask shape. // This bool indicates that we're drawing the mask shape.
@ -273,27 +271,39 @@ impl RenderState {
&mut self, &mut self,
shape: &mut Shape, shape: &mut Shape,
modifiers: Option<&Matrix>, modifiers: Option<&Matrix>,
clip_bounds: Option<skia::Rect>, clip_bounds: Option<(Rect, Matrix)>,
) { ) {
if let Some(modifiers) = modifiers { if let Some((bounds, transform)) = clip_bounds {
self.drawing_surface.canvas().concat(&modifiers); 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 // Transform the shape in the center
let mut matrix = shape.transform.clone(); let mut matrix = shape.transform.clone();
matrix.post_translate(center); matrix.post_translate(center);
matrix.pre_translate(-center); matrix.pre_translate(-center);
self.drawing_surface.canvas().concat(&matrix); if let Some(modifiers) = modifiers {
matrix.post_concat(&modifiers);
if let Some(bounds) = clip_bounds {
self.drawing_surface
.canvas()
.clip_rect(bounds, skia::ClipOp::Intersect, true);
} }
self.drawing_surface.canvas().concat(&matrix);
match &shape.kind { match &shape.kind {
Kind::SVGRaw(sr) => { Kind::SVGRaw(sr) => {
if let Some(svg) = shape.svg.as_ref() { if let Some(svg) = shape.svg.as_ref() {
@ -503,7 +513,7 @@ impl RenderState {
.to_string(), .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_children {
if !visited_mask { if !visited_mask {
match element.kind { match element.kind {
@ -553,7 +563,7 @@ impl RenderState {
// If we didn't visited_children this shape, then we need to do // If we didn't visited_children this shape, then we need to do
if !node_render_state.id.is_nil() { 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); debug::render_debug_shape(self, element, false);
self.render_complete = render_complete; self.render_complete = render_complete;
continue; continue;
@ -622,7 +632,16 @@ impl RenderState {
if element.is_recursive() { if element.is_recursive() {
let children_clip_bounds = 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() { for child_id in element.children_ids().iter().rev() {
self.pending_nodes.push(NodeRenderState { self.pending_nodes.push(NodeRenderState {

View file

@ -49,7 +49,7 @@ pub fn render_debug_shape(render_state: &mut RenderState, element: &Shape, inter
}); });
paint.set_stroke_width(1.); 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 x = 100. + scaled_rect.x() * 0.2;
let y = 100. + scaled_rect.y() * 0.2; let y = 100. + scaled_rect.y() * 0.2;
let width = scaled_rect.width() * 0.2; let width = scaled_rect.width() * 0.2;

View file

@ -1,8 +1,5 @@
use crate::{ use crate::shapes::{Fill, ImageFill, Kind, Shape};
math, use skia_safe::{self as skia, RRect, Rect};
shapes::{Fill, ImageFill, Kind, Shape},
};
use skia_safe::{self as skia, RRect};
use super::RenderState; use super::RenderState;
@ -46,7 +43,7 @@ fn draw_image_fill_in_container(
let scaled_width = width * scale; let scaled_width = width * scale;
let scaled_height = height * 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.left - (scaled_width - container_width) / 2.0,
container.top - (scaled_height - container_height) / 2.0, container.top - (scaled_height - container_height) / 2.0,
scaled_width, scaled_width,

View file

@ -1,7 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::math::{self, Rect};
use crate::shapes::{Corners, Fill, ImageFill, Kind, Path, Shape, Stroke, StrokeCap, StrokeKind}; use crate::shapes::{Corners, Fill, ImageFill, Kind, Path, Shape, Stroke, StrokeCap, StrokeKind};
use skia::Rect;
use skia_safe::{self as skia, RRect}; use skia_safe::{self as skia, RRect};
use super::RenderState; use super::RenderState;
@ -297,7 +297,7 @@ fn draw_triangle_cap(
canvas.draw_path(&path, paint); 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 (width, height) = (size.0 as f32, size.1 as f32);
let image_aspect_ratio = width / height; 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_width = width * scale;
let scaled_height = height * scale; let scaled_height = height * scale;
math::Rect::from_xywh( Rect::from_xywh(
container.left - delta - (scaled_width - container_width) / 2.0, container.left - delta - (scaled_width - container_width) / 2.0,
container.top - delta - (scaled_height - container_height) / 2.0, container.top - delta - (scaled_height - container_height) / 2.0,
scaled_width + (2. * delta) + (scaled_width - container_width), scaled_width + (2. * delta) + (scaled_width - container_width),

View file

@ -1,10 +1,9 @@
use crate::math; use skia_safe::{self as skia, Matrix, Point, Rect};
use skia_safe as skia;
use std::collections::HashMap; use std::collections::HashMap;
use uuid::Uuid; use uuid::Uuid;
use crate::render::BlendMode; use crate::render::BlendMode;
use skia::Matrix;
mod blurs; mod blurs;
mod bools; mod bools;
@ -28,13 +27,45 @@ pub use strokes::*;
pub use svgraw::*; pub use svgraw::*;
pub use transform::*; pub use transform::*;
pub type CornerRadius = skia::Point; use crate::math::Bounds;
pub type CornerRadius = Point;
pub type Corners = [CornerRadius; 4]; 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)] #[derive(Debug, Clone, PartialEq)]
pub enum Kind { pub enum Kind {
Rect(math::Rect, Option<Corners>), Rect(Rect, Option<Corners>),
Circle(math::Rect), Circle(Rect),
Path(Path), Path(Path),
Bool(BoolType, Path), Bool(BoolType, Path),
SVGRaw(SVGRaw), SVGRaw(SVGRaw),
@ -91,9 +122,10 @@ pub type Color = skia::Color;
#[allow(dead_code)] #[allow(dead_code)]
pub struct Shape { pub struct Shape {
pub id: Uuid, pub id: Uuid,
pub shape_type: Type,
pub children: Vec<Uuid>, pub children: Vec<Uuid>,
pub kind: Kind, pub kind: Kind,
pub selrect: math::Rect, pub selrect: Rect,
pub transform: Matrix, pub transform: Matrix,
pub rotation: f32, pub rotation: f32,
pub constraint_h: Option<ConstraintH>, pub constraint_h: Option<ConstraintH>,
@ -114,9 +146,10 @@ impl Shape {
pub fn new(id: Uuid) -> Self { pub fn new(id: Uuid) -> Self {
Self { Self {
id, id,
shape_type: Type::Rect,
children: Vec::<Uuid>::new(), children: Vec::<Uuid>::new(),
kind: Kind::Rect(math::Rect::new_empty(), None), kind: Kind::Rect(Rect::new_empty(), None),
selrect: math::Rect::new_empty(), selrect: Rect::new_empty(),
transform: Matrix::default(), transform: Matrix::default(),
rotation: 0., rotation: 0.,
constraint_h: None, constraint_h: None,
@ -134,8 +167,12 @@ impl Shape {
} }
} }
pub fn kind(&self) -> Kind { pub fn set_shape_type(&mut self, shape_type: Type) {
self.kind.clone() 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) { pub fn set_selrect(&mut self, left: f32, top: f32, right: f32, bottom: f32) {
@ -155,6 +192,10 @@ impl Shape {
self.kind = kind; self.kind = kind;
} }
pub fn kind(&self) -> Kind {
self.kind.clone()
}
pub fn set_clip(&mut self, value: bool) { pub fn set_clip(&mut self, value: bool) {
self.clip_content = value; self.clip_content = value;
} }
@ -175,10 +216,18 @@ impl Shape {
self.constraint_h = constraint; 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<ConstraintV>) { pub fn set_constraint_v(&mut self, constraint: Option<ConstraintV>) {
self.constraint_v = constraint; 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) { pub fn set_hidden(&mut self, value: bool) {
self.hidden = value; self.hidden = value;
} }
@ -339,10 +388,36 @@ impl Shape {
self.hidden 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 self.selrect
} }
pub fn center(&self) -> Point {
self.selrect.center()
}
pub fn clip(&self) -> bool { pub fn clip(&self) -> bool {
self.clip_content self.clip_content
} }
@ -405,11 +480,11 @@ impl Shape {
.filter(|shadow| shadow.style() == ShadowStyle::Inner) .filter(|shadow| shadow.style() == ShadowStyle::Inner)
} }
pub fn to_path_transform(&self) -> Option<skia::Matrix> { pub fn to_path_transform(&self) -> Option<Matrix> {
match self.kind { match self.kind {
Kind::Path(_) | Kind::Bool(_, _) => { Kind::Path(_) | Kind::Bool(_, _) => {
let center = self.bounds().center(); let center = self.center();
let mut matrix = skia::Matrix::new_identity(); let mut matrix = Matrix::new_identity();
matrix.pre_translate(center); matrix.pre_translate(center);
matrix.pre_concat(&self.transform.invert()?); matrix.pre_concat(&self.transform.invert()?);
matrix.pre_translate(-center); matrix.pre_translate(-center);

View file

@ -1,7 +1,6 @@
use skia_safe as skia; use skia_safe::{self as skia, Rect};
use super::Color; use super::Color;
use crate::math;
use uuid::Uuid; use uuid::Uuid;
#[derive(Debug)] #[derive(Debug)]
@ -44,7 +43,7 @@ impl Gradient {
self.offsets.push(offset); self.offsets.push(offset);
} }
fn to_linear_shader(&self, rect: &math::Rect) -> Option<skia::Shader> { fn to_linear_shader(&self, rect: &Rect) -> Option<skia::Shader> {
let start = ( let start = (
rect.left + self.start.0 * rect.width(), rect.left + self.start.0 * rect.width(),
rect.top + self.start.1 * rect.height(), rect.top + self.start.1 * rect.height(),
@ -63,7 +62,7 @@ impl Gradient {
) )
} }
fn to_radial_shader(&self, rect: &math::Rect) -> Option<skia::Shader> { fn to_radial_shader(&self, rect: &Rect) -> Option<skia::Shader> {
let center = skia::Point::new( let center = skia::Point::new(
rect.left + self.start.0 * rect.width(), rect.left + self.start.0 * rect.width(),
rect.top + self.start.1 * rect.height(), 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 { match self {
Self::Solid(color) => { Self::Solid(color) => {
let mut p = skia::Paint::default(); let mut p = skia::Paint::default();

View file

@ -1,22 +1,162 @@
use std::collections::HashMap;
use skia::Matrix;
use skia_safe as skia; use skia_safe as skia;
use std::collections::HashSet; use std::collections::HashSet;
use uuid::Uuid; use uuid::Uuid;
use crate::shapes::{Shape, TransformEntry}; use crate::math::Bounds;
use crate::shapes::{ConstraintH, ConstraintV, Shape, TransformEntry};
use crate::state::State; use crate::state::State;
fn propagate_shape(_state: &State, shape: &Shape, transform: skia::Matrix) -> Vec<TransformEntry> { fn calculate_new_bounds(
let children: Vec<TransformEntry> = shape constraint_h: ConstraintH,
.children constraint_v: ConstraintV,
.iter() parent_before: &Bounds,
.map(|id| TransformEntry { parent_after: &Bounds,
id: id.clone(), child_bounds: &Bounds,
transform, ) -> (f32, f32, f32, f32) {
}) let (delta_left, scale_width) = match constraint_h {
.collect(); 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<Uuid, Shape>,
shape: &Shape,
transform: Matrix,
) -> Vec<TransformEntry> {
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<TransformEntry>) -> Vec<TransformEntry> { pub fn propagate_modifiers(state: &State, modifiers: Vec<TransformEntry>) -> Vec<TransformEntry> {
@ -28,7 +168,7 @@ pub fn propagate_modifiers(state: &State, modifiers: Vec<TransformEntry>) -> Vec
while let Some(entry) = entries.pop() { while let Some(entry) = entries.pop() {
if !processed.contains(&entry.id) { if !processed.contains(&entry.id) {
if let Some(shape) = state.shapes.get(&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); entries.append(&mut children);
processed.insert(entry.id); processed.insert(entry.id);
result.push(entry.clone()); result.push(entry.clone());
@ -38,3 +178,38 @@ pub fn propagate_modifiers(state: &State, modifiers: Vec<TransformEntry>) -> Vec
result result
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::shapes::Type;
use skia::Point;
#[test]
fn test_propagate_shape() {
let mut shapes = HashMap::<Uuid, Shape>::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);
}
}

View file

@ -1,7 +1,7 @@
use skia_safe as skia; use skia_safe as skia;
use std::array::TryFromSliceError; use std::array::TryFromSliceError;
use crate::math::Point; type Point = (f32, f32);
fn stringify_slice_err(_: TryFromSliceError) -> String { fn stringify_slice_err(_: TryFromSliceError) -> String {
format!("Error deserializing path") format!("Error deserializing path")

View file

@ -1,6 +1,5 @@
use crate::math;
use crate::shapes::fills::Fill; use crate::shapes::fills::Fill;
use skia_safe as skia; use skia_safe::{self as skia, Rect};
use std::collections::HashMap; use std::collections::HashMap;
use super::Corners; 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 { match self.kind {
StrokeKind::InnerStroke => math::Rect::from_xywh( StrokeKind::InnerStroke => Rect::from_xywh(
rect.left + (self.width / 2.), rect.left + (self.width / 2.),
rect.top + (self.width / 2.), rect.top + (self.width / 2.),
rect.width() - self.width, rect.width() - self.width,
rect.height() - self.width, rect.height() - self.width,
), ),
StrokeKind::CenterStroke => { 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.left - (self.width / 2.),
rect.top - (self.width / 2.), rect.top - (self.width / 2.),
rect.width() + self.width, rect.width() + self.width,
@ -158,7 +157,7 @@ impl Stroke {
pub fn to_paint( pub fn to_paint(
&self, &self,
rect: &math::Rect, rect: &Rect,
svg_attrs: &HashMap<String, String>, svg_attrs: &HashMap<String, String>,
scale: f32, scale: f32,
) -> skia::Paint { ) -> skia::Paint {
@ -223,7 +222,7 @@ impl Stroke {
pub fn to_stroked_paint( pub fn to_stroked_paint(
&self, &self,
is_open: bool, is_open: bool,
rect: &math::Rect, rect: &Rect,
svg_attrs: &HashMap<String, String>, svg_attrs: &HashMap<String, String>,
scale: f32, scale: f32,
) -> skia::Paint { ) -> skia::Paint {

View file

@ -12,6 +12,12 @@ pub struct TransformEntry {
pub transform: Matrix, pub transform: Matrix,
} }
impl TransformEntry {
pub fn new(id: Uuid, transform: Matrix) -> Self {
TransformEntry { id, transform }
}
}
impl SerializableResult for TransformEntry { impl SerializableResult for TransformEntry {
type BytesType = [u8; 40]; type BytesType = [u8; 40];

View file

@ -1,4 +1,4 @@
use crate::math::Rect; use skia_safe::Rect;
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub(crate) struct Viewbox { pub(crate) struct Viewbox {