Add support for boolean shapes

This commit is contained in:
alonso.torres
2025-07-22 11:13:57 +02:00
committed by Andrey Antukh
parent 6176027263
commit cd1be43384
21 changed files with 1386 additions and 119 deletions

View File

@@ -223,12 +223,16 @@
(-> (calc-bool-content* shape objects)
(impl/path-data)))
(def update-bool-shape* nil)
(defn update-bool-shape
"Calculates the selrect+points for the boolean shape"
[shape objects]
(let [content (calc-bool-content shape objects)
shape (assoc shape :content content)]
(update-geometry shape)))
(if update-bool-shape*
(update-bool-shape* shape objects)
(let [content (calc-bool-content shape objects)
shape (assoc shape :content content)]
(update-geometry shape))))
(defn shape-with-open-path?
[shape]

View File

@@ -21,10 +21,24 @@
[app.main.data.helpers :as dsh]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.shapes :as dwsh]
[app.main.features :as features]
[app.main.store :as st]
[app.render-wasm.api :as wasm.api]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
(defn update-bool-shape
[shape objects]
(let [content
(if (features/active-feature? @st/state "render-wasm/v1")
(wasm.api/calculate-bool (:bool-type shape) (reverse (:shapes shape)))
(path/calc-bool-content shape objects))]
(-> shape
(path/update-geometry content))))
(set! path/update-bool-shape* update-bool-shape)
(defn- create-bool-shape
[id type name shapes objects]
(let [shape-id
@@ -51,7 +65,7 @@
(-> shape
(merge (select-keys head path/bool-style-properties))
(cts/setup-shape)
(path/update-bool-shape objects))]
(update-bool-shape objects))]
[shape (cph/get-position-on-parent objects (:id head))]))
@@ -67,7 +81,7 @@
(assoc :type :bool)
(assoc :bool-type type)
(merge (select-keys head bool/style-properties))
(path/update-bool-shape objects))))
(update-bool-shape objects))))
(defn create-bool
[type & {:keys [ids force-shape-id]}]

View File

@@ -6,15 +6,24 @@
(ns app.main.data.workspace.path.shapes-to-path
(:require
[app.common.data :as d]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cph]
[app.common.types.container :as ctn]
[app.common.types.path :as path]
[app.common.types.text :as txt]
[app.main.data.changes :as dch]
[app.main.data.helpers :as dsh]
[app.main.features :as features]
[app.render-wasm.api :as wasm.api]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(def ^:private dissoc-attrs
[:x :y :width :height
:rx :ry :r1 :r2 :r3 :r4
:metadata])
(defn convert-selected-to-path
([]
(convert-selected-to-path nil))
@@ -22,21 +31,53 @@
(ptk/reify ::convert-selected-to-path
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
objects (dsh/lookup-page-objects state)
selected (->> (or ids (dsh/lookup-selected state))
(remove #(ctn/has-any-copy-parent? objects (get objects %))))
(if (features/active-feature? state "render-wasm/v1")
(let [page-id (:current-page-id state)
objects (dsh/lookup-page-objects state)
selected
(->> (or ids (dsh/lookup-selected state))
(remove #(ctn/has-any-copy-parent? objects (get objects %))))
children-ids
(into #{}
(mapcat #(cph/get-children-ids objects %))
selected)
children-ids
(into #{}
(mapcat #(cph/get-children-ids objects %))
selected)
changes
(-> (pcb/empty-changes it page-id)
(pcb/with-objects objects)
;; FIXME: use with-objects? true
(pcb/update-shapes selected #(path/convert-to-path % objects))
(pcb/remove-objects children-ids))]
changes
(-> (pcb/empty-changes it page-id)
(pcb/with-objects objects)
(pcb/update-shapes
selected
(fn [shape]
(let [content (wasm.api/shape-to-path (:id shape))]
(-> shape
(assoc :type :path)
(cond-> (cph/text-shape? shape)
(assoc :fills
(->> (txt/node-seq txt/is-text-node? (:content shape))
(map :fills)
(first))))
(cond-> (cph/image-shape? shape)
(assoc :fill-image (get shape :metadata)))
(d/without-keys dissoc-attrs)
(path/update-geometry content)))))
(pcb/remove-objects children-ids))]
(rx/of (dch/commit-changes changes)))
(rx/of (dch/commit-changes changes)))))))
(let [page-id (:current-page-id state)
objects (dsh/lookup-page-objects state)
selected (->> (or ids (dsh/lookup-selected state))
(remove #(ctn/has-any-copy-parent? objects (get objects %))))
children-ids
(into #{}
(mapcat #(cph/get-children-ids objects %))
selected)
changes
(-> (pcb/empty-changes it page-id)
(pcb/with-objects objects)
(pcb/update-shapes selected path/convert-to-path {:with-objects? true})
(pcb/remove-objects children-ids))]
(rx/of (dch/commit-changes changes))))))))

View File

@@ -11,6 +11,7 @@
[app.main.data.workspace.bool :as dwb]
[app.main.data.workspace.path.shapes-to-path :as dwps]
[app.main.data.workspace.shortcuts :as sc]
[app.main.features :as features]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
@@ -36,8 +37,15 @@
(and (= (count selected) 1)
(not (contains? #{:group :bool} (:type (first selected)))))
disabled-bool-btns (or (empty? selected) has-invalid-shapes? first-not-group-like?)
disabled-flatten (or (empty? selected) has-invalid-shapes?)
disabled-bool-btns
(if (features/active-feature? @st/state "render-wasm/v1")
false
(or (empty? selected) has-invalid-shapes? first-not-group-like?))
disabled-flatten
(if (features/active-feature? @st/state "render-wasm/v1")
false
(or (empty? selected) has-invalid-shapes?))
set-bool
(mf/use-fn

View File

@@ -71,8 +71,9 @@
:code code
:icon icon
:permissions (into #{} (map str) permissions)})]
(when (sm/validate ::ctp/registry-entry manifest)
manifest)))
(if (sm/validate ::ctp/registry-entry manifest)
manifest
(.error js/console (clj->js (sm/explain ::ctp/registry-entry manifest))))))
(defn save-to-store
[]

View File

@@ -54,6 +54,7 @@
(def GRID-LAYOUT-ROW-ENTRY-SIZE 5)
(def GRID-LAYOUT-COLUMN-ENTRY-SIZE 5)
(def GRID-LAYOUT-CELL-ENTRY-SIZE 37)
(def RAW-SEGMENT-SIZE 28)
(defn modifier-get-entries-size
"Returns the list of a modifier list in bytes"
@@ -714,6 +715,7 @@
opacity (dm/get-prop shape :opacity)
hidden (dm/get-prop shape :hidden)
content (dm/get-prop shape :content)
bool-type (dm/get-prop shape :bool-type)
grow-type (dm/get-prop shape :grow-type)
blur (dm/get-prop shape :blur)
corners (when (some? (dm/get-prop shape :r1))
@@ -740,6 +742,8 @@
(set-masked masked))
(when (some? blur)
(set-shape-blur blur))
(when (= type :bool)
(set-shape-bool-type bool-type))
(when (and (some? content)
(or (= type :path)
(= type :bool)))
@@ -1053,6 +1057,51 @@
(h/call wasm/internal-module "_free_bytes")
[row column]))
(defn shape-to-path
[id]
(use-shape id)
(let [offset (h/call wasm/internal-module "_current_to_path")
heapu32 (mem/get-heap-u32)
heapu8 (mem/get-heap-u8)
len (aget heapu32 (mem/ptr8->ptr32 offset))
from-offset (+ offset 4)
to-offset (+ offset 4 (* len RAW-SEGMENT-SIZE))
data (js/Uint8Array. (.slice heapu8 from-offset to-offset))
content (path/from-bytes data)]
(h/call wasm/internal-module "_free_bytes")
content))
(defn calculate-bool
[bool-type ids]
(let [num-ids (count ids)
offset (mem/alloc-bytes (* CHILD-ENTRY-SIZE num-ids))
heap (mem/get-heap-u32)]
(loop [entries (seq ids)
current-offset offset]
(when-not (empty? entries)
(let [id (first entries)]
(sr/heapu32-set-uuid id heap (mem/ptr8->ptr32 current-offset))
(recur (rest entries) (+ current-offset CHILD-ENTRY-SIZE))))))
(let [offset (h/call wasm/internal-module "_calculate_bool" (sr/translate-bool-type bool-type))
heapu32 (mem/get-heap-u32)
heapu8 (mem/get-heap-u8)
len (aget heapu32 (mem/ptr8->ptr32 offset))
from-offset (+ offset 4)
to-offset (+ offset 4 (* len RAW-SEGMENT-SIZE))
data (js/Uint8Array. (.slice heapu8 from-offset to-offset))
content (path/from-bytes data)]
(h/call wasm/internal-module "_free_bytes")
content))
(defonce module
(delay
(if (exists? js/dynamicImport)

View File

@@ -168,7 +168,7 @@
:union 0
:difference 1
:intersection 2
:exclusion 3
:exclude 3
0))
(defn translate-blur-type

20
render-wasm/Cargo.lock generated
View File

@@ -23,6 +23,15 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bezier-rs"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bde3aa314326e2f984f81adcb399c64b93eed3c0f2cd4258b711bf494c5741de"
dependencies = [
"glam",
]
[[package]]
name = "bindgen"
version = "0.71.1"
@@ -176,6 +185,15 @@ dependencies = [
"xml-rs",
]
[[package]]
name = "glam"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945"
dependencies = [
"serde",
]
[[package]]
name = "glob"
version = "0.3.1"
@@ -394,7 +412,9 @@ name = "render"
version = "0.1.0"
dependencies = [
"base64",
"bezier-rs",
"gl",
"glam",
"indexmap",
"skia-safe",
"uuid",

View File

@@ -18,7 +18,9 @@ path = "src/main.rs"
[dependencies]
base64 = "0.22.1"
bezier-rs = "0.4.0"
gl = "0.14.0"
glam = "0.24.2"
indexmap = "2.7.1"
skia-safe = { version = "0.86.0", default-features = false, features = [
"gl",

View File

@@ -40,6 +40,7 @@ macro_rules! with_state_mut {
}};
}
#[macro_export]
macro_rules! with_state {
($state:ident, $block:block) => {{
let $state = unsafe {
@@ -505,7 +506,7 @@ pub extern "C" fn set_structure_modifiers() {
let Some(shape) = state.shapes.get(&entry.id) else {
continue;
};
for id in shape.all_children_with_self(&state.shapes, true) {
for id in shape.all_children(&state.shapes, true, true) {
state.scale_content.insert(id, entry.value);
}
}

View File

@@ -1,5 +1,7 @@
use skia_safe as skia;
pub mod bools;
pub type Rect = skia::Rect;
pub type Matrix = skia::Matrix;
pub type Vector = skia::Vector;
@@ -22,7 +24,16 @@ pub fn is_close_to(current: f32, value: f32) -> bool {
(current - value).abs() <= THRESHOLD
}
pub fn identitish(m: Matrix) -> bool {
pub fn is_close_matrix(m: &Matrix, other: &Matrix) -> bool {
is_close_to(m.scale_x(), other.scale_x())
&& is_close_to(m.scale_y(), other.scale_y())
&& is_close_to(m.translate_x(), other.translate_x())
&& is_close_to(m.translate_y(), other.translate_y())
&& is_close_to(m.skew_x(), other.skew_x())
&& is_close_to(m.skew_y(), other.skew_y())
}
pub fn identitish(m: &Matrix) -> bool {
is_close_to(m.scale_x(), 1.0)
&& is_close_to(m.scale_y(), 1.0)
&& is_close_to(m.translate_x(), 0.0)
@@ -328,6 +339,11 @@ impl Bounds {
Rect::from_ltrb(self.min_x(), self.min_y(), self.max_x(), self.max_y())
}
pub fn from_rect(r: &Rect) -> Self {
let [nw, ne, se, sw] = r.to_quad();
Self::new(nw, ne, se, sw)
}
pub fn min_x(&self) -> f32 {
self.nw.x.min(self.ne.x).min(self.sw.x).min(self.se.x)
}

View File

@@ -0,0 +1,562 @@
use super::Matrix;
use crate::render::{RenderState, SurfaceId};
use crate::shapes::{BoolType, Path, Segment, Shape, StructureEntry, ToPath, Type};
use crate::state::ShapesPool;
use crate::uuid::Uuid;
use bezier_rs::{Bezier, BezierHandles, ProjectionOptions, TValue};
use glam::DVec2;
use indexmap::IndexSet;
use skia_safe as skia;
use std::cmp::Ordering;
use std::collections::{BTreeMap, HashMap};
const INTERSECT_THRESHOLD_SAME: f32 = 0.1;
const INTERSECT_THRESHOLD_DIFFERENT: f32 = 0.5;
const INTERSECT_ERROR: f64 = 0.1;
const INTERSECT_MIN_SEPARATION: f64 = 0.05;
const PROJECT_OPTS: ProjectionOptions = ProjectionOptions {
lut_size: 20,
convergence_epsilon: 0.01,
convergence_limit: 10,
iteration_limit: 20,
};
fn to_point(v: DVec2) -> skia::Point {
skia::Point::new(v.x as f32, v.y as f32)
}
pub fn path_to_beziers(path: &Path) -> Vec<Bezier> {
let mut start: Option<(f64, f64)> = None;
let mut prev: Option<(f64, f64)> = None;
path.segments()
.iter()
.filter_map(|s| match s {
Segment::MoveTo((x, y)) => {
let x = f64::from(*x);
let y = f64::from(*y);
prev = Some((x, y));
start = Some((x, y));
None
}
Segment::LineTo((x2, y2)) => {
let (x1, y1) = prev?;
let x2 = f64::from(*x2);
let y2 = f64::from(*y2);
let s = Bezier::from_linear_coordinates(x1, y1, x2, y2);
prev = Some((x2, y2));
Some(s)
}
Segment::CurveTo(((c1x, c1y), (c2x, c2y), (x2, y2))) => {
let (x1, y1) = prev?;
let x2 = f64::from(*x2);
let y2 = f64::from(*y2);
let c1x = f64::from(*c1x);
let c1y = f64::from(*c1y);
let c2x = f64::from(*c2x);
let c2y = f64::from(*c2y);
let s = Bezier::from_cubic_coordinates(x1, y1, c1x, c1y, c2x, c2y, x2, y2);
prev = Some((x2, y2));
Some(s)
}
Segment::Close => {
let (x1, y1) = prev?;
let (x2, y2) = start?;
let s = Bezier::from_linear_coordinates(x1, y1, x2, y2);
prev = Some((x2, y2));
Some(s)
}
})
.collect()
}
pub fn split_intersections(segment: Bezier, intersections: &[f64]) -> Vec<Bezier> {
if intersections.is_empty() {
return vec![segment];
}
let mut result = Vec::new();
let mut intersections = intersections.to_owned();
intersections.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
let mut prev = 0.0;
let mut cur_segment = segment;
for t_i in &intersections {
let rti = (t_i - prev) / (1.0 - prev);
let [s, rest] = cur_segment.split(TValue::Parametric(rti));
prev = *t_i;
cur_segment = rest;
result.push(s);
}
result.push(cur_segment);
result
}
pub fn split_segments(path_a: &Path, path_b: &Path) -> (Vec<Bezier>, Vec<Bezier>) {
let path_a = path_to_beziers(path_a);
let path_b = path_to_beziers(path_b);
let mut intersects_a = Vec::<Vec<f64>>::with_capacity(path_a.len());
intersects_a.resize_with(path_a.len(), Default::default);
let mut intersects_b = Vec::<Vec<f64>>::with_capacity(path_b.len());
intersects_b.resize_with(path_b.len(), Default::default);
for i in 0..path_a.len() {
for j in 0..path_b.len() {
let segment_a = path_a[i];
let segment_b = path_b[j];
let intersections_a = segment_a.intersections(
&segment_b,
Some(INTERSECT_ERROR),
Some(INTERSECT_MIN_SEPARATION),
);
intersects_b[j].extend(intersections_a.iter().map(|t_a| {
segment_b.project(
segment_a.evaluate(TValue::Parametric(*t_a)),
Some(PROJECT_OPTS),
)
}));
intersects_a[i].extend(intersections_a);
}
}
let mut result_a = Vec::new();
for i in 0..path_a.len() {
let cur_segment = path_a[i];
result_a.extend(split_intersections(cur_segment, &intersects_a[i]));
}
let mut result_b = Vec::new();
for i in 0..path_b.len() {
let cur_segment = path_b[i];
result_b.extend(split_intersections(cur_segment, &intersects_b[i]));
}
(result_a, result_b)
}
fn union(
path_a: &Path,
segments_a: Vec<Bezier>,
path_b: &Path,
segments_b: Vec<Bezier>,
) -> Vec<(BezierSource, Bezier)> {
let mut result = Vec::new();
result.extend(
segments_a
.iter()
.filter(|s| !path_b.contains(to_point(s.evaluate(TValue::Parametric(0.5)))))
.copied()
.map(|b| (BezierSource::A, b)),
);
result.extend(
segments_b
.iter()
.filter(|s| !path_a.contains(to_point(s.evaluate(TValue::Parametric(0.5)))))
.copied()
.map(|b| (BezierSource::B, b)),
);
result
}
fn intersection(
path_a: &Path,
segments_a: Vec<Bezier>,
path_b: &Path,
segments_b: Vec<Bezier>,
) -> Vec<(BezierSource, Bezier)> {
let mut result = Vec::new();
result.extend(
segments_a
.iter()
.filter(|s| path_b.contains(to_point(s.evaluate(TValue::Parametric(0.5)))))
.copied()
.map(|b| (BezierSource::A, b)),
);
result.extend(
segments_b
.iter()
.filter(|s| path_a.contains(to_point(s.evaluate(TValue::Parametric(0.5)))))
.copied()
.map(|b| (BezierSource::B, b)),
);
result
}
fn difference(
path_a: &Path,
segments_a: Vec<Bezier>,
path_b: &Path,
segments_b: Vec<Bezier>,
) -> Vec<(BezierSource, Bezier)> {
let mut result = Vec::new();
result.extend(
segments_a
.iter()
.filter(|s| !path_b.contains(to_point(s.evaluate(TValue::Parametric(0.5)))))
.copied()
.map(|b| (BezierSource::A, b)),
);
result.extend(
segments_b
.iter()
.filter(|s| path_a.contains(to_point(s.evaluate(TValue::Parametric(0.5)))))
.copied()
.map(|s| s.reverse())
.map(|b| (BezierSource::B, b)),
);
result
}
fn exclusion(segments_a: Vec<Bezier>, segments_b: Vec<Bezier>) -> Vec<(BezierSource, Bezier)> {
let mut result = Vec::new();
result.extend(segments_a.iter().copied().map(|b| (BezierSource::A, b)));
result.extend(
segments_b
.iter()
.copied()
.map(|s| s.reverse())
.map(|b| (BezierSource::B, b)),
);
result
}
#[derive(Debug, Clone, PartialEq, Copy)]
enum BezierSource {
A,
B,
}
#[derive(Debug, Clone)]
struct BezierStart(BezierSource, DVec2);
impl PartialEq for BezierStart {
fn eq(&self, other: &Self) -> bool {
let x1 = self.1.x as f32;
let y1 = self.1.y as f32;
let x2 = other.1.x as f32;
let y2 = other.1.y as f32;
if self.0 == other.0 {
(x1 - x2).abs() <= INTERSECT_THRESHOLD_SAME
&& (y1 - y2).abs() <= INTERSECT_THRESHOLD_SAME
} else {
(x1 - x2).abs() <= INTERSECT_THRESHOLD_DIFFERENT
&& (y1 - y2).abs() <= INTERSECT_THRESHOLD_DIFFERENT
}
}
}
impl Eq for BezierStart {}
impl PartialOrd for BezierStart {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for BezierStart {
fn cmp(&self, other: &Self) -> Ordering {
let x1 = self.1.x as f32;
let y1 = self.1.y as f32;
let x2 = other.1.x as f32;
let y2 = other.1.y as f32;
let (equal_x, equal_y) = if self.0 == other.0 {
(
(x1 - x2).abs() <= INTERSECT_THRESHOLD_SAME,
(y1 - y2).abs() <= INTERSECT_THRESHOLD_SAME,
)
} else {
(
(x1 - x2).abs() <= INTERSECT_THRESHOLD_DIFFERENT,
(y1 - y2).abs() <= INTERSECT_THRESHOLD_DIFFERENT,
)
};
if equal_x && equal_y {
Ordering::Equal
} else if equal_x && y1 > y2 || !equal_x && x1 > x2 {
Ordering::Greater
} else {
Ordering::Less
}
}
}
type BM<'a> = BTreeMap<BezierStart, Vec<(BezierSource, Bezier)>>;
fn init_bm(beziers: &[(BezierSource, Bezier)]) -> BM {
let mut bm = BM::default();
for entry @ (source, bezier) in beziers.iter() {
let value = *entry;
let key = BezierStart(*source, bezier.start);
if let Some(v) = bm.get_mut(&key) {
v.push(value);
} else {
bm.insert(key, vec![value]);
}
}
bm
}
fn find_next(tree: &mut BM, key: BezierStart) -> Option<(BezierSource, Bezier)> {
let val = tree.get_mut(&key)?;
let first = val.pop()?;
if val.is_empty() {
tree.remove(&key);
}
Some(first)
}
fn pop_first(tree: &mut BM) -> Option<(BezierSource, Bezier)> {
let key = tree.keys().take(1).next()?.clone();
let val = tree.get_mut(&key)?;
let first = val.pop()?;
if val.is_empty() {
tree.remove(&key);
}
Some(first)
}
fn push_bezier(result: &mut Vec<Segment>, bezier: &Bezier) {
match bezier.handles {
BezierHandles::Linear => {
result.push(Segment::LineTo((bezier.end.x as f32, bezier.end.y as f32)));
}
BezierHandles::Quadratic { handle } => {
result.push(Segment::CurveTo((
(handle.x as f32, handle.y as f32),
(handle.x as f32, handle.y as f32),
(bezier.end.x as f32, bezier.end.y as f32),
)));
}
BezierHandles::Cubic {
handle_start,
handle_end,
} => {
result.push(Segment::CurveTo((
(handle_start.x as f32, handle_start.y as f32),
(handle_end.x as f32, handle_end.y as f32),
(bezier.end.x as f32, bezier.end.y as f32),
)));
}
}
}
fn beziers_to_segments(beziers: &[(BezierSource, Bezier)]) -> Vec<Segment> {
let mut result = Vec::new();
let mut bm = init_bm(beziers);
while let Some(bezier) = pop_first(&mut bm) {
result.push(Segment::MoveTo((
bezier.1.start.x as f32,
bezier.1.start.y as f32,
)));
push_bezier(&mut result, &bezier.1);
let mut next_p = BezierStart(bezier.0, bezier.1.end);
loop {
let Some(next) = find_next(&mut bm, next_p) else {
break;
};
push_bezier(&mut result, &next.1);
next_p = BezierStart(next.0, next.1.end);
}
}
result
}
pub fn bool_from_shapes(
bool_type: BoolType,
children_ids: &IndexSet<Uuid>,
shapes: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> Path {
if children_ids.is_empty() {
return Path::default();
}
let Some(child) = shapes.get(&children_ids[children_ids.len() - 1]) else {
return Path::default();
};
let mut current_path = child.to_path(shapes, modifiers, structure);
for idx in (0..children_ids.len() - 1).rev() {
let Some(other) = shapes.get(&children_ids[idx]) else {
continue;
};
let other_path = other.to_path(shapes, modifiers, structure);
let (segs_a, segs_b) = split_segments(&current_path, &other_path);
let beziers = match bool_type {
BoolType::Union => union(&current_path, segs_a, &other_path, segs_b),
BoolType::Difference => difference(&current_path, segs_a, &other_path, segs_b),
BoolType::Intersection => intersection(&current_path, segs_a, &other_path, segs_b),
BoolType::Exclusion => exclusion(segs_a, segs_b),
};
current_path = Path::new(beziers_to_segments(&beziers));
}
current_path
}
pub fn update_bool_to_path(
shape: &Shape,
shapes: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> Shape {
let mut shape = shape.clone();
let children_ids = shape.modified_children_ids(structure.get(&shape.id), true);
let Type::Bool(bool_data) = &mut shape.shape_type else {
return shape;
};
bool_data.path = bool_from_shapes(
bool_data.bool_type,
&children_ids,
shapes,
modifiers,
structure,
);
shape
}
#[allow(dead_code)]
// Debug utility for boolean shapes
pub fn debug_render_bool_paths(
render_state: &mut RenderState,
shape: &Shape,
shapes: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) {
let canvas = render_state.surfaces.canvas(SurfaceId::Strokes);
let mut shape = shape.clone();
let children_ids = shape.modified_children_ids(structure.get(&shape.id), true);
let Type::Bool(bool_data) = &mut shape.shape_type else {
return;
};
if children_ids.is_empty() {
return;
}
let Some(child) = shapes.get(&children_ids[children_ids.len() - 1]) else {
return;
};
let mut current_path = child.to_path(shapes, modifiers, structure);
for idx in (0..children_ids.len() - 1).rev() {
let Some(other) = shapes.get(&children_ids[idx]) else {
continue;
};
let other_path = other.to_path(shapes, modifiers, structure);
let (segs_a, segs_b) = split_segments(&current_path, &other_path);
let beziers = match bool_data.bool_type {
BoolType::Union => union(&current_path, segs_a, &other_path, segs_b),
BoolType::Difference => difference(&current_path, segs_a, &other_path, segs_b),
BoolType::Intersection => intersection(&current_path, segs_a, &other_path, segs_b),
BoolType::Exclusion => exclusion(segs_a, segs_b),
};
current_path = Path::new(beziers_to_segments(&beziers));
if idx == 0 {
for b in &beziers {
let mut paint = skia::Paint::default();
paint.set_color(skia::Color::RED);
paint.set_alpha_f(1.0);
paint.set_style(skia::PaintStyle::Stroke);
let mut path = skia::Path::default();
path.move_to((b.1.start.x as f32, b.1.start.y as f32));
match b.1.handles {
BezierHandles::Linear => {
path.line_to((b.1.end.x as f32, b.1.end.y as f32));
}
BezierHandles::Quadratic { handle } => {
path.quad_to(
(handle.x as f32, handle.y as f32),
(b.1.end.x as f32, b.1.end.y as f32),
);
}
BezierHandles::Cubic {
handle_start,
handle_end,
} => {
path.cubic_to(
(handle_start.x as f32, handle_start.y as f32),
(handle_end.x as f32, handle_end.y as f32),
(b.1.end.x as f32, b.1.end.y as f32),
);
}
}
canvas.draw_path(&path, &paint);
let mut v1 = b.1.normal(TValue::Parametric(1.0));
v1 *= 0.5;
let v2 = v1.perp();
let p1 = b.1.end + v1 + v2;
let p2 = b.1.end - v1 + v2;
canvas.draw_line(
(b.1.end.x as f32, b.1.end.y as f32),
(p1.x as f32, p1.y as f32),
&paint,
);
canvas.draw_line(
(b.1.end.x as f32, b.1.end.y as f32),
(p2.x as f32, p2.y as f32),
&paint,
);
let v3 = b.1.normal(TValue::Parametric(0.0));
let p3 = b.1.start + v3;
let p4 = b.1.start - v3;
canvas.draw_line(
(b.1.start.x as f32, b.1.start.y as f32),
(p3.x as f32, p3.y as f32),
&paint,
);
canvas.draw_line(
(b.1.start.x as f32, b.1.start.y as f32),
(p4.x as f32, p4.y as f32),
&paint,
);
}
}
}
}

View File

@@ -18,7 +18,7 @@ use std::collections::{HashMap, HashSet};
use gpu_state::GpuState;
use options::RenderOptions;
use surfaces::{SurfaceId, Surfaces};
pub use surfaces::{SurfaceId, Surfaces};
use crate::performance;
use crate::shapes::{Corners, Fill, Shape, SolidColor, StructureEntry, Type};
@@ -28,6 +28,9 @@ use crate::uuid::Uuid;
use crate::view::Viewbox;
use crate::wapi;
use crate::math;
use crate::math::bools;
pub use blend::BlendMode;
pub use fonts::*;
pub use images::*;
@@ -199,6 +202,28 @@ pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
.into()
}
fn is_modified_child(
shape: &Shape,
shapes: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
) -> bool {
if modifiers.is_empty() {
return false;
}
let ids = shape.all_children(shapes, true, false);
let default = &Matrix::default();
let parent_modifier = modifiers.get(&shape.id).unwrap_or(default);
// Returns true if the transform of any child is different to the parent's
ids.iter().any(|id| {
!math::is_close_matrix(
parent_modifier,
modifiers.get(id).unwrap_or(&Matrix::default()),
)
})
}
impl RenderState {
pub fn new(width: i32, height: i32) -> RenderState {
// This needs to be done once per WebGL context.
@@ -397,8 +422,10 @@ impl RenderState {
pub fn render_shape(
&mut self,
shapes: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
shape: &Shape,
modifiers: Option<&Matrix>,
scale_content: Option<&f32>,
) {
let shape = if let Some(scale_content) = scale_content {
@@ -420,8 +447,8 @@ impl RenderState {
// We don't want to change the value in the global state
let mut shape: Cow<Shape> = Cow::Borrowed(shape);
if let Some(modifiers) = modifiers {
shape.to_mut().apply_transform(modifiers);
if let Some(shape_modifiers) = modifiers.get(&shape.id) {
shape.to_mut().apply_transform(shape_modifiers);
}
let center = shape.center();
@@ -431,8 +458,10 @@ impl RenderState {
match &shape.shape_type {
Type::SVGRaw(sr) => {
if let Some(modifiers) = modifiers {
self.surfaces.canvas(SurfaceId::Fills).concat(modifiers);
if let Some(shape_modifiers) = modifiers.get(&shape.id) {
self.surfaces
.canvas(SurfaceId::Fills)
.concat(shape_modifiers);
}
self.surfaces.canvas(SurfaceId::Fills).concat(&matrix);
if let Some(svg) = shape.svg.as_ref() {
@@ -520,6 +549,19 @@ impl RenderState {
s.canvas().concat(&matrix);
});
let shape = if let Type::Bool(_) = &shape.shape_type {
// If any child transform doesn't match the parent transform means
// that the children is transformed and we need to recalculate the
// boolean
if is_modified_child(&shape, shapes, modifiers) {
&bools::update_bool_to_path(&shape, shapes, modifiers, structure)
} else {
&shape
}
} else {
&shape
};
let has_fill_none = matches!(
shape.svg_attrs.get("fill").map(String::as_str),
Some("none")
@@ -532,23 +574,24 @@ impl RenderState {
if let Some(fills_to_render) = self.nested_fills.last() {
let fills_to_render = fills_to_render.clone();
for fill in fills_to_render.iter() {
fills::render(self, &shape, fill, antialias);
fills::render(self, shape, fill, antialias);
}
}
} else {
for fill in shape.fills().rev() {
fills::render(self, &shape, fill, antialias);
fills::render(self, shape, fill, antialias);
}
}
for stroke in shape.visible_strokes().rev() {
shadows::render_stroke_drop_shadows(self, &shape, stroke, antialias);
strokes::render(self, &shape, stroke, None, None, None, antialias, None);
shadows::render_stroke_inner_shadows(self, &shape, stroke, antialias);
shadows::render_stroke_drop_shadows(self, shape, stroke, antialias);
strokes::render(self, shape, stroke, None, None, None, antialias, None);
shadows::render_stroke_inner_shadows(self, shape, stroke, antialias);
}
shadows::render_fill_inner_shadows(self, &shape, antialias);
shadows::render_fill_drop_shadows(self, &shape, antialias);
shadows::render_fill_inner_shadows(self, shape, antialias);
shadows::render_fill_drop_shadows(self, shape, antialias);
// bools::debug_render_bool_paths(self, shape, shapes, modifiers, structure);
}
};
self.apply_drawing_to_render_canvas(Some(&shape));
@@ -751,9 +794,11 @@ impl RenderState {
#[inline]
pub fn render_shape_exit(
&mut self,
tree: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
element: &Shape,
visited_mask: bool,
modifiers: Option<&Matrix>,
scale_content: Option<&f32>,
) {
if visited_mask {
@@ -815,7 +860,7 @@ impl RenderState {
element_fills
.to_mut()
.set_fills([Fill::Solid(SolidColor(skia::Color::WHITE))].to_vec());
self.render_shape(&element_fills, modifiers, scale_content);
self.render_shape(tree, modifiers, structure, &element_fills, scale_content);
self.surfaces.canvas(SurfaceId::Current).restore();
@@ -823,7 +868,7 @@ impl RenderState {
let mut element_strokes: Cow<Shape> = Cow::Borrowed(element);
element_strokes.to_mut().clear_fills();
element_strokes.to_mut().clear_shadows();
self.render_shape(&element_strokes, modifiers, scale_content);
self.render_shape(tree, modifiers, structure, &element_strokes, scale_content);
// TODO: drop shadows. With thos approach actually drop shadows for frames with clipped content are lost.
}
@@ -901,9 +946,11 @@ impl RenderState {
if visited_children {
self.render_shape_exit(
tree,
modifiers,
structure,
element,
visited_mask,
modifiers.get(&node_id),
scale_content.get(&element.id),
);
continue;
@@ -932,8 +979,10 @@ impl RenderState {
self.render_shape_enter(element, mask);
if !node_render_state.is_root() && self.focus_mode.is_active() {
self.render_shape(
tree,
modifiers,
structure,
element,
modifiers.get(&element.id),
scale_content.get(&element.id),
);
} else if visited_children {

View File

@@ -110,6 +110,10 @@ impl FontStore {
pub fn get_fallback(&self) -> &HashSet<String> {
&self.fallback_fonts
}
pub fn get_emoji_font(&self, _size: f32) -> Option<Font> {
None
}
}
fn load_default_provider(font_mgr: &FontMgr) -> skia::textlayout::TypefaceFontProvider {

View File

@@ -18,9 +18,11 @@ pub mod modifiers;
mod paths;
mod rects;
mod shadows;
mod shape_to_path;
mod strokes;
mod svgraw;
mod text;
pub mod text_paths;
mod transform;
pub use blurs::*;
@@ -35,6 +37,7 @@ pub use modifiers::*;
pub use paths::*;
pub use rects::*;
pub use shadows::*;
pub use shape_to_path::*;
pub use strokes::*;
pub use svgraw::*;
pub use text::*;
@@ -791,23 +794,27 @@ impl Shape {
}
}
pub fn all_children_with_self(
pub fn all_children(
&self,
shapes: &ShapesPool,
include_hidden: bool,
include_self: bool,
) -> IndexSet<Uuid> {
once(self.id)
.chain(
self.children_ids(include_hidden)
.into_iter()
.flat_map(|id| {
shapes
.get(&id)
.map(|s| s.all_children_with_self(shapes, include_hidden))
.unwrap_or_default()
}),
)
.collect()
let all_children = self
.children_ids(include_hidden)
.into_iter()
.flat_map(|id| {
shapes
.get(&id)
.map(|s| s.all_children(shapes, include_hidden, true))
.unwrap_or_default()
});
if include_self {
once(self.id).chain(all_children).collect()
} else {
all_children.collect()
}
}
pub fn image_filter(&self, scale: f32) -> Option<skia::ImageFilter> {
@@ -928,6 +935,17 @@ impl Shape {
path.transform(transform);
}
}
if let Type::Text(text) = &mut self.shape_type {
text.transform(transform);
}
}
pub fn transformed(&self, transform: Option<&Matrix>) -> Self {
let mut shape = self.clone();
if let Some(transform) = transform {
shape.apply_transform(transform);
}
shape
}
pub fn is_absolute(&self) -> bool {

View File

@@ -6,7 +6,9 @@ pub mod grid_layout;
use common::GetBounds;
use crate::math::bools;
use crate::math::{self as math, identitish, Bounds, Matrix, Point};
use crate::shapes::{
auto_height, set_paragraphs_width, ConstraintH, ConstraintV, Frame, Group, GrowType, Layout,
Modifier, Shape, StructureEntry, TransformEntry, Type,
@@ -28,7 +30,7 @@ fn propagate_children(
) -> VecDeque<Modifier> {
let children_ids = shape.modified_children_ids(structure.get(&shape.id), true);
if children_ids.is_empty() || identitish(transform) {
if children_ids.is_empty() || identitish(&transform) {
return VecDeque::new();
}
@@ -109,6 +111,31 @@ fn calculate_group_bounds(
shape_bounds.with_points(result)
}
fn calculate_bool_bounds(
shape: &Shape,
shapes: &ShapesPool,
bounds: &HashMap<Uuid, Bounds>,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> Option<Bounds> {
let shape_bounds = bounds.find(shape);
let children_ids = shape.modified_children_ids(structure.get(&shape.id), true);
let Type::Bool(bool_data) = &shape.shape_type else {
return Some(shape_bounds);
};
let path = bools::bool_from_shapes(
bool_data.bool_type,
&children_ids,
shapes,
modifiers,
structure,
);
Some(path.bounds())
}
fn set_pixel_precision(transform: &mut Matrix, bounds: &mut Bounds) {
let tr = bounds.transform_matrix().unwrap_or_default();
let tr_inv = tr.invert().unwrap_or_default();
@@ -227,6 +254,7 @@ fn propagate_reflow(
bounds: &mut HashMap<Uuid, Bounds>,
layout_reflows: &mut Vec<Uuid>,
reflown: &mut HashSet<Uuid>,
modifiers: &HashMap<Uuid, Matrix>,
) {
let Some(shape) = state.shapes.get(id) else {
return;
@@ -278,11 +306,8 @@ fn propagate_reflow(
}
}
Type::Bool(_) => {
// TODO: How to calculate from rust the new box? we need to calculate the
// new path... impossible right now. I'm going to use for the moment the group
// calculation
if let Some(shape_bounds) =
calculate_group_bounds(shape, shapes, bounds, &state.structure)
calculate_bool_bounds(shape, shapes, bounds, modifiers, &state.structure)
{
bounds.insert(shape.id, shape_bounds);
reflow_parent = true;
@@ -391,6 +416,7 @@ pub fn propagate_modifiers(
&mut bounds,
&mut layout_reflows,
&mut reflown,
&modifiers,
),
}
}

View File

@@ -1,5 +1,7 @@
use skia_safe::{self as skia, Matrix};
use crate::math;
type Point = (f32, f32);
#[derive(Debug, PartialEq, Copy, Clone)]
@@ -23,6 +25,18 @@ impl Default for Path {
}
}
fn to_verb(v: u8) -> skia::path::Verb {
match v {
0 => skia::path::Verb::Move,
1 => skia::path::Verb::Line,
2 => skia::path::Verb::Quad,
3 => skia::path::Verb::Conic,
4 => skia::path::Verb::Cubic,
5 => skia::path::Verb::Close,
_ => skia::path::Verb::Done,
}
}
impl Path {
pub fn new(segments: Vec<Segment>) -> Self {
let mut open = true;
@@ -50,8 +64,11 @@ impl Path {
None
}
};
if let (Some(start), Some(destination)) = (start, destination) {
if destination == start {
if math::is_close_to(destination.0, start.0)
&& math::is_close_to(destination.1, start.1)
{
skia_path.close();
open = false;
}
@@ -65,15 +82,113 @@ impl Path {
}
}
pub fn from_skia_path(path: skia::Path) -> Self {
let nv = path.count_verbs();
let mut verbs = vec![0; nv];
path.get_verbs(&mut verbs);
let np = path.count_points();
let mut points = Vec::with_capacity(np);
points.resize(np, skia::Point::default());
path.get_points(&mut points);
let mut segments = Vec::new();
let mut current_point = 0;
for verb in verbs {
let verb = to_verb(verb);
match verb {
skia::path::Verb::Move => {
let p = points[current_point];
segments.push(Segment::MoveTo((p.x, p.y)));
current_point += 1;
}
skia::path::Verb::Line => {
let p = points[current_point];
segments.push(Segment::LineTo((p.x, p.y)));
current_point += 1;
}
skia::path::Verb::Quad => {
let p1 = points[current_point];
let p2 = points[current_point + 1];
segments.push(Segment::CurveTo(((p1.x, p1.y), (p1.x, p1.y), (p2.x, p2.y))));
current_point += 2;
}
skia::path::Verb::Conic => {
// TODO: There is no way currently to access the conic weight
// to transform this correctly
let p1 = points[current_point];
let p2 = points[current_point + 1];
segments.push(Segment::CurveTo(((p1.x, p1.y), (p1.x, p1.y), (p2.x, p2.y))));
current_point += 2;
}
skia::path::Verb::Cubic => {
let p1 = points[current_point];
let p2 = points[current_point + 1];
let p3 = points[current_point + 2];
segments.push(Segment::CurveTo(((p1.x, p1.y), (p2.x, p2.y), (p3.x, p3.y))));
current_point += 3;
}
skia::path::Verb::Close => {
segments.push(Segment::Close);
}
skia::path::Verb::Done => {
segments.push(Segment::Close);
}
}
}
Path::new(segments)
}
pub fn to_skia_path(&self) -> skia::Path {
self.skia_path.snapshot()
}
pub fn contains(&self, p: skia::Point) -> bool {
self.skia_path.contains(p)
}
pub fn is_open(&self) -> bool {
self.open
}
pub fn transform(&mut self, mtx: &Matrix) {
self.segments.iter_mut().for_each(|s| match s {
Segment::MoveTo(p) => {
let np = mtx.map_point(skia::Point::new(p.0, p.1));
p.0 = np.x;
p.1 = np.y;
}
Segment::LineTo(p) => {
let np = mtx.map_point(skia::Point::new(p.0, p.1));
p.0 = np.x;
p.1 = np.y;
}
Segment::CurveTo((c1, c2, p)) => {
let nc1 = mtx.map_point(skia::Point::new(c1.0, c1.1));
c1.0 = nc1.x;
c1.1 = nc1.y;
let nc2 = mtx.map_point(skia::Point::new(c2.0, c2.1));
c2.0 = nc2.x;
c2.1 = nc2.y;
let np = mtx.map_point(skia::Point::new(p.0, p.1));
p.0 = np.x;
p.1 = np.y;
}
_ => {}
});
self.skia_path.transform(mtx);
}
pub fn segments(&self) -> &Vec<Segment> {
&self.segments
}
pub fn bounds(&self) -> math::Bounds {
math::Bounds::from_rect(self.skia_path.bounds())
}
}

View File

@@ -0,0 +1,200 @@
use skia_safe::Matrix;
use super::{Corners, Path, Segment, Shape, StructureEntry, Type};
use crate::math;
use crate::shapes::text_paths::TextPaths;
use crate::state::ShapesPool;
use crate::uuid::Uuid;
use std::collections::HashMap;
const BEZIER_CIRCLE_C: f32 = 0.551_915_05;
pub trait ToPath {
fn to_path(
&self,
shapes: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> Path;
}
enum CornerType {
TopLeft,
TopRight,
BottomRight,
BottomLeft,
}
fn make_corner(
corner_type: CornerType,
from: (f32, f32),
to: (f32, f32),
r: math::Point,
) -> Segment {
let x = match &corner_type {
CornerType::TopLeft => from.0,
CornerType::TopRight => from.0 - r.x,
CornerType::BottomRight => to.0 - r.x,
CornerType::BottomLeft => to.0,
};
let y = match &corner_type {
CornerType::TopLeft => from.1 - r.y,
CornerType::TopRight => from.1,
CornerType::BottomRight => to.1 - (r.y * 2.0),
CornerType::BottomLeft => to.1 - r.y,
};
let width = r.x * 2.0;
let height = r.y * 2.0;
let c = BEZIER_CIRCLE_C;
let c1x = x + (width / 2.0) * (1.0 - c);
let c2x = x + (width / 2.0) * (1.0 + c);
let c1y = y + (height / 2.0) * (1.0 - c);
let c2y = y + (height / 2.0) * (1.0 + c);
let h1 = match &corner_type {
CornerType::TopLeft => (from.0, c1y),
CornerType::TopRight => (c2x, from.1),
CornerType::BottomRight => (from.0, c2y),
CornerType::BottomLeft => (c1x, from.1),
};
let h2 = match &corner_type {
CornerType::TopLeft => (c1x, to.1),
CornerType::TopRight => (to.0, c1y),
CornerType::BottomRight => (c2x, to.1),
CornerType::BottomLeft => (to.0, c2y),
};
Segment::CurveTo((h1, h2, to))
}
pub fn rect_segments(shape: &Shape, corners: Option<Corners>) -> Vec<Segment> {
let sr = shape.selrect;
if let Some([r1, r2, r3, r4]) = corners {
let p1 = (sr.x(), sr.y() + r1.y);
let p2 = (sr.x() + r1.x, sr.y());
let p3 = (sr.x() + sr.width() - r2.x, sr.y());
let p4 = (sr.x() + sr.width(), sr.y() + r2.y);
let p5 = (sr.x() + sr.width(), sr.y() + sr.height() - r3.y);
let p6 = (sr.x() + sr.width() - r3.x, sr.y() + sr.height());
let p7 = (sr.x() + r4.x, sr.y() + sr.height());
let p8 = (sr.x(), sr.y() + sr.height() - r4.y);
vec![
Segment::MoveTo(p1),
make_corner(CornerType::TopLeft, p1, p2, r1),
Segment::LineTo(p3),
make_corner(CornerType::TopRight, p3, p4, r2),
Segment::LineTo(p5),
make_corner(CornerType::BottomRight, p5, p6, r3),
Segment::LineTo(p7),
make_corner(CornerType::BottomLeft, p7, p8, r4),
Segment::LineTo(p1),
]
} else {
vec![
Segment::MoveTo((sr.x(), sr.y())),
Segment::LineTo((sr.x() + sr.width(), sr.y())),
Segment::LineTo((sr.x() + sr.width(), sr.y() + sr.height())),
Segment::LineTo((sr.x(), sr.y() + sr.height())),
Segment::Close,
]
}
}
pub fn circle_segments(shape: &Shape) -> Vec<Segment> {
let sr = shape.selrect;
let mx = sr.x() + sr.width() / 2.0;
let my = sr.y() + sr.height() / 2.0;
let ex = sr.x() + sr.width();
let ey = sr.y() + sr.height();
let c = BEZIER_CIRCLE_C;
let c1x = sr.x() + (sr.width() / 2.0 * (1.0 - c));
let c2x = sr.x() + (sr.width() / 2.0 * (1.0 + c));
let c1y = sr.y() + (sr.height() / 2.0 * (1.0 - c));
let c2y = sr.y() + (sr.height() / 2.0 * (1.0 + c));
let p1x = mx;
let p1y = sr.y();
let p2x = ex;
let p2y = my;
let p3x = mx;
let p3y = ey;
let p4x = sr.x();
let p4y = my;
vec![
Segment::MoveTo((p1x, p1y)),
Segment::CurveTo(((c2x, p1y), (p2x, c1y), (p2x, p2y))),
Segment::CurveTo(((p2x, c2y), (c2x, p3y), (p3x, p3y))),
Segment::CurveTo(((c1x, p3y), (p4x, c2y), (p4x, p4y))),
Segment::CurveTo(((p4x, c1y), (c1x, p1y), (p1x, p1y))),
]
}
fn join_paths(path: Path, other: Path) -> Path {
let mut segments = path.segments().clone();
segments.extend(other.segments().iter());
Path::new(segments)
}
impl ToPath for Shape {
fn to_path(
&self,
shapes: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> Path {
let shape = self.transformed(modifiers.get(&self.id));
match shape.shape_type {
Type::Frame(ref frame) => {
let children = shape.modified_children_ids(structure.get(&shape.id), true);
let mut result = Path::new(rect_segments(&shape, frame.corners));
for id in children {
let Some(shape) = shapes.get(&id) else {
continue;
};
result = join_paths(result, shape.to_path(shapes, modifiers, structure));
}
result
}
Type::Group(_) => {
let children = shape.modified_children_ids(structure.get(&shape.id), true);
let mut result = Path::default();
for id in children {
let Some(shape) = shapes.get(&id) else {
continue;
};
result = join_paths(result, shape.to_path(shapes, modifiers, structure));
}
result
}
Type::Bool(bool_data) => bool_data.path,
Type::Rect(ref rect) => Path::new(rect_segments(&shape, rect.corners)),
Type::Path(path_data) => path_data,
Type::Circle => Path::new(circle_segments(&shape)),
Type::SVGRaw(_) => Path::default(),
Type::Text(text) => {
let text_paths = TextPaths::new(text);
let mut result = Path::default();
for (path, _) in text_paths.get_paths(true) {
result = join_paths(result, Path::from_skia_path(path));
}
result
}
}
}
}

View File

@@ -1,5 +1,5 @@
use crate::{
math::Rect,
math::{Matrix, Rect},
render::{default_font, DEFAULT_EMOJI_FONT},
};
use skia_safe::{
@@ -181,6 +181,16 @@ impl TextContent {
let height = auto_height(&mut paragraphs, self.width());
(self.width(), height)
}
pub fn transform(&mut self, transform: &Matrix) {
let left = self.bounds.left();
let right = self.bounds.right();
let top = self.bounds.top();
let bottom = self.bounds.bottom();
let p1 = transform.map_point(skia::Point::new(left, top));
let p2 = transform.map_point(skia::Point::new(right, bottom));
self.bounds = Rect::from_ltrb(p1.x, p1.y, p2.x, p2.y);
}
}
impl Default for TextContent {

View File

@@ -1,7 +1,12 @@
use crate::shapes::text::TextContent;
use skia_safe::{self as skia, textlayout::ParagraphBuilder, Path, Paint};
use skia_safe::{
self as skia, textlayout::Paragraph as SkiaParagraph, textlayout::ParagraphBuilder,
FontMetrics, Point, Rect, TextBlob,
};
use std::ops::Deref;
use crate::{with_state_mut, STATE};
pub struct TextPaths(TextContent);
// Note: This class is not being currently used.
@@ -11,11 +16,10 @@ impl TextPaths {
pub fn new(content: TextContent) -> Self {
Self(content)
}
pub fn get_skia_paragraphs(&self) -> Vec<ParagraphBuilder> {
let mut paragraphs = self.to_paragraphs();
self.collect_paragraphs(&mut paragraphs);
paragraphs
pub fn get_skia_paragraphs(&self) -> Vec<Vec<ParagraphBuilder>> {
let paragraphs = self.to_paragraphs();
self.collect_paragraphs(paragraphs)
}
pub fn get_paths(&self, antialias: bool) -> Vec<(skia::Path, skia::Paint)> {
@@ -23,64 +27,67 @@ impl TextPaths {
let mut offset_y = self.bounds.y();
let mut paragraphs = self.get_skia_paragraphs();
for paragraph_builder in paragraphs.iter_mut() {
// 1. Get paragraph and set the width layout
let mut skia_paragraph = paragraph_builder.build();
let text = paragraph_builder.get_text();
let paragraph_width = self.bounds.width();
skia_paragraph.layout(paragraph_width);
let mut line_offset_y = offset_y;
for paragraphs in paragraphs.iter_mut() {
for paragraph_builder in paragraphs.iter_mut() {
// 1. Get paragraph and set the width layout
let mut skia_paragraph = paragraph_builder.build();
let text = paragraph_builder.get_text();
let paragraph_width = self.bounds.width();
skia_paragraph.layout(paragraph_width);
// 2. Iterate through each line in the paragraph
for line_metrics in skia_paragraph.get_line_metrics() {
let line_baseline = line_metrics.baseline as f32;
let start = line_metrics.start_index;
let end = line_metrics.end_index;
let mut line_offset_y = offset_y;
// 3. Get styles present in line for each text leaf
let style_metrics = line_metrics.get_style_metrics(start..end);
// 2. Iterate through each line in the paragraph
for line_metrics in skia_paragraph.get_line_metrics() {
let line_baseline = line_metrics.baseline as f32;
let start = line_metrics.start_index;
let end = line_metrics.end_index;
let mut offset_x = 0.0;
// 3. Get styles present in line for each text leaf
let style_metrics = line_metrics.get_style_metrics(start..end);
for (i, (start_index, style_metric)) in style_metrics.iter().enumerate() {
let end_index = style_metrics.get(i + 1).map_or(end, |next| next.0);
let mut offset_x = 0.0;
let start_byte = text
.char_indices()
.nth(*start_index)
.map(|(i, _)| i)
.unwrap_or(0);
let end_byte = text
.char_indices()
.nth(end_index)
.map(|(i, _)| i)
.unwrap_or(text.len());
for (i, (start_index, style_metric)) in style_metrics.iter().enumerate() {
let end_index = style_metrics.get(i + 1).map_or(end, |next| next.0);
let leaf_text = &text[start_byte..end_byte];
let start_byte = text
.char_indices()
.nth(*start_index)
.map(|(i, _)| i)
.unwrap_or(0);
let end_byte = text
.char_indices()
.nth(end_index)
.map(|(i, _)| i)
.unwrap_or(text.len());
let font = skia_paragraph.get_font_at(*start_index);
let leaf_text = &text[start_byte..end_byte];
let blob_offset_x = self.bounds.x() + line_metrics.left as f32 + offset_x;
let blob_offset_y = line_offset_y;
let font = skia_paragraph.get_font_at(*start_index);
// 4. Get the path for each text leaf
if let Some((text_path, paint)) = self.generate_text_path(
leaf_text,
&font,
blob_offset_x,
blob_offset_y,
style_metric,
antialias,
) {
let text_width = font.measure_text(leaf_text, None).0;
offset_x += text_width;
paths.push((text_path, paint));
let blob_offset_x = self.bounds.x() + line_metrics.left as f32 + offset_x;
let blob_offset_y = line_offset_y;
// 4. Get the path for each text leaf
if let Some((text_path, paint)) = self.generate_text_path(
leaf_text,
&font,
blob_offset_x,
blob_offset_y,
style_metric,
antialias,
) {
let text_width = font.measure_text(leaf_text, None).0;
offset_x += text_width;
paths.push((text_path, paint));
}
}
line_offset_y = offset_y + line_baseline;
}
line_offset_y = offset_y + line_baseline;
offset_y += skia_paragraph.height();
}
offset_y += skia_paragraph.height();
}
paths
}
@@ -164,7 +171,6 @@ impl TextPaths {
}
}
fn get_text_blob_path(
leaf_text: &str,
font: &skia::Font,

View File

@@ -1,5 +1,12 @@
use crate::shapes::{Path, Segment};
use crate::{mem, with_current_shape_mut, STATE};
#![allow(unused_mut, unused_variables)]
use indexmap::IndexSet;
use mem::SerializableResult;
use uuid::Uuid;
use crate::math::bools;
use crate::shapes::{BoolType, Path, Segment, ToPath};
use crate::uuid;
use crate::{mem, with_current_shape, with_current_shape_mut, with_state, STATE};
const RAW_SEGMENT_DATA_SIZE: usize = size_of::<RawSegmentData>();
@@ -13,6 +20,19 @@ enum RawSegmentData {
Close = 0x04,
}
impl RawSegmentData {
pub fn from_segment(segment: Segment) -> Self {
match segment {
Segment::MoveTo(to) => RawSegmentData::MoveTo(RawMoveCommand::new(to)),
Segment::LineTo(to) => RawSegmentData::LineTo(RawLineCommand::new(to)),
Segment::CurveTo((c1, c2, to)) => {
RawSegmentData::CurveTo(RawCurveCommand::new(c1, c2, to))
}
Segment::Close => RawSegmentData::Close,
}
}
}
impl From<[u8; size_of::<RawSegmentData>()]> for RawSegmentData {
fn from(bytes: [u8; size_of::<RawSegmentData>()]) -> Self {
unsafe { std::mem::transmute(bytes) }
@@ -30,6 +50,28 @@ impl TryFrom<&[u8]> for RawSegmentData {
}
}
impl SerializableResult for RawSegmentData {
type BytesType = [u8; RAW_SEGMENT_DATA_SIZE];
fn from_bytes(bytes: Self::BytesType) -> Self {
unsafe { std::mem::transmute(bytes) }
}
fn as_bytes(&self) -> Self::BytesType {
let ptr = self as *const RawSegmentData as *const u8;
let bytes: &[u8] = unsafe { std::slice::from_raw_parts(ptr, RAW_SEGMENT_DATA_SIZE) };
let mut result = [0; RAW_SEGMENT_DATA_SIZE];
result.copy_from_slice(bytes);
result
}
// The generic trait doesn't know the size of the array. This is why the
// clone needs to be here even if it could be generic.
fn clone_to_slice(&self, slice: &mut [u8]) {
slice.clone_from_slice(&self.as_bytes());
}
}
#[repr(C, align(4))]
#[derive(Debug, PartialEq, Clone, Copy)]
struct RawMoveCommand {
@@ -37,6 +79,15 @@ struct RawMoveCommand {
x: f32,
y: f32,
}
impl RawMoveCommand {
pub fn new((x, y): (f32, f32)) -> Self {
Self {
_padding: [0u32; 4],
x,
y,
}
}
}
#[repr(C, align(4))]
#[derive(Debug, PartialEq, Clone, Copy)]
@@ -46,6 +97,16 @@ struct RawLineCommand {
y: f32,
}
impl RawLineCommand {
pub fn new((x, y): (f32, f32)) -> Self {
Self {
_padding: [0u32; 4],
x,
y,
}
}
}
#[repr(C, align(4))]
#[derive(Debug, PartialEq, Clone, Copy)]
struct RawCurveCommand {
@@ -57,6 +118,19 @@ struct RawCurveCommand {
y: f32,
}
impl RawCurveCommand {
pub fn new((c1_x, c1_y): (f32, f32), (c2_x, c2_y): (f32, f32), (x, y): (f32, f32)) -> Self {
Self {
c1_x,
c1_y,
c2_x,
c2_y,
x,
y,
}
}
}
impl From<RawSegmentData> for Segment {
fn from(value: RawSegmentData) -> Self {
match value {
@@ -92,6 +166,53 @@ pub extern "C" fn set_shape_path_content() {
});
}
#[no_mangle]
pub extern "C" fn current_to_path() -> *mut u8 {
let mut result = Vec::<RawSegmentData>::default();
with_current_shape!(state, |shape: &Shape| {
let path = shape.to_path(&state.shapes, &state.modifiers, &state.structure);
result = path
.segments()
.iter()
.copied()
.map(RawSegmentData::from_segment)
.collect();
});
mem::write_vec(result)
}
#[no_mangle]
pub extern "C" fn calculate_bool(raw_bool_type: u8) -> *mut u8 {
let bytes = mem::bytes_or_empty();
let entries: IndexSet<Uuid> = bytes
.chunks(size_of::<<Uuid as SerializableResult>::BytesType>())
.map(|data| Uuid::from_bytes(data.try_into().unwrap()))
.collect();
mem::free_bytes();
let bool_type = BoolType::from(raw_bool_type);
let result;
with_state!(state, {
let path = bools::bool_from_shapes(
bool_type,
&entries,
&state.shapes,
&state.modifiers,
&state.structure,
);
result = path
.segments()
.iter()
.copied()
.map(RawSegmentData::from_segment)
.collect();
});
mem::write_vec(result)
}
// Extracts a string from the bytes slice until the next null byte (0) and returns the result as a `String`.
// Updates the `start` index to the end of the extracted string.
fn extract_string(start: &mut usize, bytes: &[u8]) -> String {