diff --git a/common/src/app/common/types/fills/impl.cljc b/common/src/app/common/types/fills/impl.cljc index eec8271c52..eca9d65156 100644 --- a/common/src/app/common/types/fills/impl.cljc +++ b/common/src/app/common/types/fills/impl.cljc @@ -20,15 +20,15 @@ (def ^:const MAX-GRADIENT-STOPS 16) (def ^:const MAX-FILLS 8) -(def ^:const GRADIENT-STOP-SIZE 8) -(def ^:const GRADIENT-BYTE-SIZE 156) -(def ^:const SOLID-BYTE-SIZE 4) -(def ^:const IMAGE-BYTE-SIZE 36) -(def ^:const METADATA-BYTE-SIZE 36) -(def ^:const FILL-BYTE-SIZE - (+ 4 (mth/max GRADIENT-BYTE-SIZE - IMAGE-BYTE-SIZE - SOLID-BYTE-SIZE))) +(def ^:const GRADIENT-STOP-U8-SIZE 8) +(def ^:const GRADIENT-U8-SIZE 156) +(def ^:const SOLID-U8-SIZE 4) +(def ^:const IMAGE-U8-SIZE 36) +(def ^:const METADATA-U8-SIZE 36) +(def ^:const FILL-U8-SIZE + (+ 4 (mth/max GRADIENT-U8-SIZE + IMAGE-U8-SIZE + SOLID-U8-SIZE))) (def ^:private xf:take-stops (take MAX-GRADIENT-STOPS)) @@ -78,7 +78,7 @@ (buf/write-int buffer (+ offset 4) (-> (hex->rgb color) (rgb->rgba opacity))) - (+ offset FILL-BYTE-SIZE)) + (+ offset FILL-U8-SIZE)) (defn write-gradient-fill [offset buffer opacity gradient] @@ -114,8 +114,8 @@ (buf/write-int buffer (+ offset' 0) color) (buf/write-float buffer (+ offset' 4) (:offset stop)) (recur (rest stops) - (+ offset' GRADIENT-STOP-SIZE))) - (+ offset FILL-BYTE-SIZE))))) + (+ offset' GRADIENT-STOP-U8-SIZE))) + (+ offset FILL-U8-SIZE))))) (defn write-image-fill [offset buffer opacity image] @@ -132,7 +132,7 @@ (buf/write-short buffer (+ offset 22) 0) ;; 2-byte padding (reserved for future use) (buf/write-int buffer (+ offset 24) image-width) (buf/write-int buffer (+ offset 28) image-height) - (+ offset FILL-BYTE-SIZE))) + (+ offset FILL-U8-SIZE))) (defn- write-metadata [offset buffer fill] @@ -169,8 +169,8 @@ (defn- read-fill "Read segment from binary buffer at specified index" [dbuffer mbuffer index] - (let [doffset (+ 4 (* index FILL-BYTE-SIZE)) - moffset (* index METADATA-BYTE-SIZE) + (let [doffset (+ 4 (* index FILL-U8-SIZE)) + moffset (* index METADATA-U8-SIZE) type (buf/read-byte dbuffer doffset) refs? (buf/read-bool mbuffer (+ moffset 0)) fill (case type @@ -195,7 +195,7 @@ result []] (if (< index stops) (recur (inc index) - (conj result (read-stop dbuffer (+ doffset 32 (* GRADIENT-STOP-SIZE index))))) + (conj result (read-stop dbuffer (+ doffset 32 (* GRADIENT-STOP-U8-SIZE index))))) result))] {:fill-opacity opacity @@ -410,8 +410,8 @@ [fills] (let [fills (into [] xf:take-fills fills) total (count fills) - dbuffer (buf/allocate (+ 4 (* MAX-FILLS FILL-BYTE-SIZE))) - mbuffer (buf/allocate (* total METADATA-BYTE-SIZE))] + dbuffer (buf/allocate (+ 4 (* MAX-FILLS FILL-U8-SIZE))) + mbuffer (buf/allocate (* total METADATA-U8-SIZE))] (buf/write-byte dbuffer 0 total) @@ -419,8 +419,8 @@ image-ids #{}] (if (< index total) (let [fill (nth fills index) - doffset (+ 4 (* index FILL-BYTE-SIZE)) - moffset (* index METADATA-BYTE-SIZE) + doffset (+ 4 (* index FILL-U8-SIZE)) + moffset (* index METADATA-U8-SIZE) opacity (get fill :fill-opacity 1)] (if-let [color (get fill :fill-color)] diff --git a/common/src/app/common/types/path.cljc b/common/src/app/common/types/path.cljc index 34aba833b8..8f6d2c7b5a 100644 --- a/common/src/app/common/types/path.cljc +++ b/common/src/app/common/types/path.cljc @@ -216,12 +216,19 @@ :content (vec contents) :cause cause))))) +(def wasm:calc-bool-content + "A overwrite point for setup a WASM version of the `calc-bool-content*` function" + nil) + (defn calc-bool-content "Calculate the boolean content from shape and objects. Returns a packed PathData instance" [shape objects] - (-> (calc-bool-content* shape objects) - (impl/path-data))) + (let [content (if (fn? wasm:calc-bool-content) + (wasm:calc-bool-content (get shape :bool-type) + (get shape :shapes)) + (calc-bool-content* shape objects))] + (impl/path-data content))) (defn update-bool-shape "Calculates the selrect+points for the boolean shape" diff --git a/common/src/app/common/types/path/impl.cljc b/common/src/app/common/types/path/impl.cljc index 1efb35c152..6e513f0057 100644 --- a/common/src/app/common/types/path/impl.cljc +++ b/common/src/app/common/types/path/impl.cljc @@ -28,7 +28,7 @@ #?(:clj (set! *warn-on-reflection* true)) -(def ^:const SEGMENT-BYTE-SIZE 28) +(def ^:const SEGMENT-U8-SIZE 28) (defprotocol IPathData (-write-to [_ buffer offset] "write the content to the specified buffer") @@ -107,7 +107,7 @@ f (dm/get-prop m :f)] (loop [index 0] (when (< index size) - (let [offset (* index SEGMENT-BYTE-SIZE)] + (let [offset (* index SEGMENT-U8-SIZE)] (impl-transform-segment buffer offset a b c d e f) (recur (inc index))))))) @@ -116,7 +116,7 @@ (loop [index 0 result (transient initial)] (if (< index size) - (let [offset (* index SEGMENT-BYTE-SIZE) + (let [offset (* index SEGMENT-U8-SIZE) type (buf/read-short buffer offset) c1x (buf/read-float buffer (+ offset 4)) c1y (buf/read-float buffer (+ offset 8)) @@ -141,7 +141,7 @@ (loop [index 0 result initial] (if (< index size) - (let [offset (* index SEGMENT-BYTE-SIZE) + (let [offset (* index SEGMENT-U8-SIZE) type (buf/read-short buffer offset) c1x (buf/read-float buffer (+ offset 4)) c1y (buf/read-float buffer (+ offset 8)) @@ -162,7 +162,7 @@ (defn impl-lookup [buffer index f] - (let [offset (* index SEGMENT-BYTE-SIZE) + (let [offset (* index SEGMENT-U8-SIZE) type (buf/read-short buffer offset) c1x (buf/read-float buffer (+ offset 4)) c1y (buf/read-float buffer (+ offset 8)) @@ -225,7 +225,7 @@ :cljs (StringBuffer.))] (loop [index 0] (when (< index size) - (let [offset (* index SEGMENT-BYTE-SIZE) + (let [offset (* index SEGMENT-U8-SIZE) type (buf/read-short buffer offset)] (to-string-segment* buffer offset type builder) (recur (inc index))))) @@ -235,7 +235,7 @@ (defn- read-segment "Read segment from binary buffer at specified index" [buffer index] - (let [offset (* index SEGMENT-BYTE-SIZE) + (let [offset (* index SEGMENT-U8-SIZE) type (buf/read-short buffer offset)] (case (long type) 1 (let [x (buf/read-float buffer (+ offset 20)) @@ -348,7 +348,7 @@ IPathData (-get-byte-size [_] - (* size SEGMENT-BYTE-SIZE)) + (* size SEGMENT-U8-SIZE)) (-write-to [_ _ _] (throw (RuntimeException. "not implemented")))) @@ -576,13 +576,13 @@ (cond (instance? ByteBuffer buffer) (let [size (.capacity ^ByteBuffer buffer) - count (long (/ size SEGMENT-BYTE-SIZE)) + count (long (/ size SEGMENT-U8-SIZE)) buffer (.order ^ByteBuffer buffer ByteOrder/LITTLE_ENDIAN)] (PathData. count buffer nil)) (bytes? buffer) (let [size (alength ^bytes buffer) - count (long (/ size SEGMENT-BYTE-SIZE)) + count (long (/ size SEGMENT-U8-SIZE)) buffer (ByteBuffer/wrap buffer)] (PathData. count (.order buffer ByteOrder/LITTLE_ENDIAN) @@ -594,7 +594,7 @@ (cond (instance? js/ArrayBuffer buffer) (let [size (.-byteLength buffer) - count (long (/ size SEGMENT-BYTE-SIZE))] + count (long (/ size SEGMENT-U8-SIZE))] (PathData. count (js/DataView. buffer) (weak-map/create) @@ -603,12 +603,15 @@ (instance? js/DataView buffer) (let [buffer' (.-buffer ^js/DataView buffer) size (.-byteLength ^js/ArrayBuffer buffer') - count (long (/ size SEGMENT-BYTE-SIZE))] + count (long (/ size SEGMENT-U8-SIZE))] (PathData. count buffer (weak-map/create) nil)) (instance? js/Uint8Array buffer) (from-bytes (.-buffer buffer)) + (instance? js/Uint32Array buffer) + (from-bytes (.-buffer buffer)) + (instance? js/Int8Array buffer) (from-bytes (.-buffer buffer)) @@ -624,11 +627,11 @@ (assert (check-plain-content segments)) (let [total (count segments) - buffer (buf/allocate (* total SEGMENT-BYTE-SIZE))] + buffer (buf/allocate (* total SEGMENT-U8-SIZE))] (loop [index 0] (when (< index total) (let [segment (nth segments index) - offset (* index SEGMENT-BYTE-SIZE)] + offset (* index SEGMENT-U8-SIZE)] (case (get segment :command) :move-to (let [params (get segment :params) diff --git a/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs b/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs index 9068ad43d5..db61b5b026 100644 --- a/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs +++ b/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs @@ -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)))))))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.cljs index f644156d93..7c6c620dc2 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.cljs @@ -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 diff --git a/frontend/src/app/plugins/register.cljs b/frontend/src/app/plugins/register.cljs index 12be74150a..c7865eb6cc 100644 --- a/frontend/src/app/plugins/register.cljs +++ b/frontend/src/app/plugins/register.cljs @@ -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 [] diff --git a/frontend/src/app/render_wasm.cljs b/frontend/src/app/render_wasm.cljs index c0eb40231c..89642811bf 100644 --- a/frontend/src/app/render_wasm.cljs +++ b/frontend/src/app/render_wasm.cljs @@ -7,6 +7,7 @@ (ns app.render-wasm "A WASM based render API" (:require + [app.common.types.path] [app.common.types.shape :as shape] [app.render-wasm.api :as api] [app.render-wasm.shape :as wasm.shape])) @@ -15,5 +16,8 @@ (defn initialize [enabled?] + (if enabled? + (set! app.common.types.path/wasm:calc-bool-content api/calculate-bool) + (set! app.common.types.path/wasm:calc-bool-content nil)) (set! app.common.types.shape/wasm-enabled? enabled?) (set! app.common.types.shape/wasm-create-shape wasm.shape/create-shape)) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 2f9fc38f0b..1faa24b187 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -15,6 +15,7 @@ [app.common.types.fills :as types.fills] [app.common.types.fills.impl :as types.fills.impl] [app.common.types.path :as path] + [app.common.types.path.impl :as path.impl] [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] [app.config :as cf] @@ -38,9 +39,7 @@ [promesa.core :as p] [rumext.v2 :as mf])) -;; (defonce internal-frame-id nil) -;; (defonce wasm/internal-module #js {}) -(defonce use-dpr? (contains? cf/flags :render-wasm-dpr)) +(def use-dpr? (contains? cf/flags :render-wasm-dpr)) ;; ;; List of common entry sizes. @@ -48,29 +47,32 @@ ;; All of these entries are in bytes so we need to adjust ;; these values to work with TypedArrays of 32 bits. ;; -(def CHILD-ENTRY-SIZE 16) -(def MODIFIER-ENTRY-SIZE 40) -(def MODIFIER-ENTRY-TRANSFORM-OFFSET 16) -(def GRID-LAYOUT-ROW-ENTRY-SIZE 5) -(def GRID-LAYOUT-COLUMN-ENTRY-SIZE 5) -(def GRID-LAYOUT-CELL-ENTRY-SIZE 37) +(def ^:const UUID-U8-SIZE 16) +(def ^:const UUID-U32-SIZE (/ UUID-U8-SIZE 4)) + +(def ^:const MODIFIER-U8-SIZE 40) +(def ^:const MODIFIER-TRANSFORM-U8-OFFSET-SIZE 16) + +(def ^:const GRID-LAYOUT-ROW-U8-SIZE 5) +(def ^:const GRID-LAYOUT-COLUMN-U8-SIZE 5) +(def ^:const GRID-LAYOUT-CELL-U8-SIZE 37) (defn modifier-get-entries-size "Returns the list of a modifier list in bytes" [modifiers] - (mem/get-list-size modifiers MODIFIER-ENTRY-SIZE)) + (mem/get-list-size modifiers MODIFIER-U8-SIZE)) (defn grid-layout-get-row-entries-size [rows] - (mem/get-list-size rows GRID-LAYOUT-ROW-ENTRY-SIZE)) + (mem/get-list-size rows GRID-LAYOUT-ROW-U8-SIZE)) (defn grid-layout-get-column-entries-size [columns] - (mem/get-list-size columns GRID-LAYOUT-COLUMN-ENTRY-SIZE)) + (mem/get-list-size columns GRID-LAYOUT-COLUMN-U8-SIZE)) (defn grid-layout-get-cell-entries-size [cells] - (mem/get-list-size cells GRID-LAYOUT-CELL-ENTRY-SIZE)) + (mem/get-list-size cells GRID-LAYOUT-CELL-U8-SIZE)) (def dpr (if use-dpr? (if (exists? js/window) js/window.devicePixelRatio 1.0) 1.0)) @@ -169,25 +171,25 @@ (h/call wasm/internal-module "_set_shape_rotation" rotation)) (defn set-shape-children - [shape-ids] - (let [num-shapes (count shape-ids)] + [children] + (let [heap (mem/get-heap-u32) + length (count children)] (perf/begin-measure "set-shape-children") - (when (> num-shapes 0) - (let [offset (mem/alloc-bytes (* CHILD-ENTRY-SIZE num-shapes)) - heap (mem/get-heap-u32)] - - (loop [entries (seq shape-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))))))) + (when (pos? length) + (let [offset (mem/alloc->offset-32 (* UUID-U8-SIZE length))] + (reduce (fn [offset id] + (sr/heapu32-set-uuid id heap offset) + (+ offset UUID-U32-SIZE)) + offset + children))) (let [result (h/call wasm/internal-module "_set_children")] (perf/end-measure "set-shape-children") result))) -(defn- get-string-length [string] (+ (count string) 1)) +(defn- get-string-length + [string] + (+ (count string) 1)) (defn- fetch-image [shape-id image-id] @@ -205,7 +207,7 @@ ;; is possible (if image size modulo ;; permits it) (let [size (.-byteLength image) - offset (mem/alloc-bytes size) + offset (mem/alloc size) heap (mem/get-heap-u8) data (js/Uint8Array. image)] (.set heap data offset) @@ -252,7 +254,7 @@ (if (empty? fills) (h/call wasm/internal-module "_clear_shape_fills") (let [fills (types.fills/coerce fills) - offset (mem/alloc-bytes-32 (types.fills/get-byte-size fills)) + offset (mem/alloc->offset-32 (types.fills/get-byte-size fills)) heap (mem/get-heap-u32)] ;; write fills to the heap @@ -287,7 +289,7 @@ style (-> stroke :stroke-style sr/translate-stroke-style) cap-start (-> stroke :stroke-cap-start sr/translate-stroke-cap) cap-end (-> stroke :stroke-cap-end sr/translate-stroke-cap) - offset (mem/alloc-bytes types.fills.impl/FILL-BYTE-SIZE) + offset (mem/alloc types.fills.impl/FILL-U8-SIZE) heap (mem/get-heap-u8) dview (js/DataView. (.-buffer heap))] (case align @@ -324,7 +326,7 @@ (merge style)) str (sr/serialize-path-attrs attrs) size (count str) - offset (mem/alloc-bytes size)] + offset (mem/alloc size)] (h/call wasm/internal-module "stringToUTF8" str offset size) (h/call wasm/internal-module "_set_shape_path_attrs" (count attrs)))) @@ -333,7 +335,7 @@ [content] (let [pdata (path/content content) size (path/get-byte-size content) - offset (mem/alloc-bytes size) + offset (mem/alloc size) heap (mem/get-heap-u8)] (path/write-to pdata (.-buffer heap) offset) (h/call wasm/internal-module "_set_shape_path_content"))) @@ -341,7 +343,7 @@ (defn set-shape-svg-raw-content [content] (let [size (get-string-length content) - offset (mem/alloc-bytes size)] + offset (mem/alloc size)] (h/call wasm/internal-module "stringToUTF8" content offset size) (h/call wasm/internal-module "_set_shape_svg_raw_content"))) @@ -466,7 +468,7 @@ (defn set-grid-layout-rows [entries] (let [size (grid-layout-get-row-entries-size entries) - offset (mem/alloc-bytes size) + offset (mem/alloc size) heap (js/Uint8Array. @@ -479,13 +481,13 @@ (let [{:keys [type value]} (first entries)] (.set heap (sr/u8 (sr/translate-grid-track-type type)) (+ current-offset 0)) (.set heap (sr/f32->u8 value) (+ current-offset 1)) - (recur (rest entries) (+ current-offset GRID-LAYOUT-ROW-ENTRY-SIZE))))) + (recur (rest entries) (+ current-offset GRID-LAYOUT-ROW-U8-SIZE))))) (h/call wasm/internal-module "_set_grid_rows"))) (defn set-grid-layout-columns [entries] (let [size (grid-layout-get-column-entries-size entries) - offset (mem/alloc-bytes size) + offset (mem/alloc size) heap (js/Uint8Array. @@ -498,14 +500,14 @@ (let [{:keys [type value]} (first entries)] (.set heap (sr/u8 (sr/translate-grid-track-type type)) (+ current-offset 0)) (.set heap (sr/f32->u8 value) (+ current-offset 1)) - (recur (rest entries) (+ current-offset GRID-LAYOUT-COLUMN-ENTRY-SIZE))))) + (recur (rest entries) (+ current-offset GRID-LAYOUT-COLUMN-U8-SIZE))))) (h/call wasm/internal-module "_set_grid_columns"))) (defn set-grid-layout-cells [cells] (let [entries (vals cells) size (grid-layout-get-cell-entries-size entries) - offset (mem/alloc-bytes size) + offset (mem/alloc size) heap (js/Uint8Array. @@ -551,7 +553,7 @@ ;; shape_id_d: [u8; 4], (.set heap (sr/uuid->u8 (or (-> cell :shapes first) uuid/zero)) (+ current-offset 21)) - (recur (rest entries) (+ current-offset GRID-LAYOUT-CELL-ENTRY-SIZE))))) + (recur (rest entries) (+ current-offset GRID-LAYOUT-CELL-U8-SIZE))))) (h/call wasm/internal-module "_set_grid_cells"))) @@ -670,10 +672,10 @@ ([] (let [offset (h/call wasm/internal-module "_get_text_dimensions") heapf32 (mem/get-heap-f32) - width (aget heapf32 (mem/ptr8->ptr32 offset)) - height (aget heapf32 (mem/ptr8->ptr32 (+ offset 4))) - max-width (aget heapf32 (mem/ptr8->ptr32 (+ offset 8)))] - (h/call wasm/internal-module "_free_bytes") + width (aget heapf32 (mem/->offset-32 offset)) + height (aget heapf32 (mem/->offset-32 (+ offset 4))) + max-width (aget heapf32 (mem/->offset-32 (+ offset 8)))] + (mem/free) {:width width :height height :max-width max-width}))) (defn set-view-box @@ -714,6 +716,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 +743,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))) @@ -819,7 +824,7 @@ (defn set-focus-mode [entries] - (let [offset (mem/alloc-bytes-32 (* (count entries) 16)) + (let [offset (mem/alloc->offset-32 (* (count entries) 16)) heapu32 (mem/get-heap-u32)] (loop [entries (seq entries) @@ -827,7 +832,7 @@ (when-not (empty? entries) (let [id (first entries)] (sr/heapu32-set-uuid id heapu32 current-offset) - (recur (rest entries) (+ current-offset (mem/ptr8->ptr32 16)))))) + (recur (rest entries) (+ current-offset (mem/->offset-32 16)))))) (h/call wasm/internal-module "_set_focus_mode") (clear-drawing-cache) @@ -836,7 +841,7 @@ (defn set-structure-modifiers [entries] (when-not (empty? entries) - (let [offset (mem/alloc-bytes-32 (mem/get-list-size entries 44)) + (let [offset (mem/alloc->offset-32 (mem/get-list-size entries 44)) heapu32 (mem/get-heap-u32) heapf32 (mem/get-heap-f32)] (loop [entries (seq entries) @@ -854,7 +859,7 @@ (defn propagate-modifiers [entries pixel-precision] (when (d/not-empty? entries) - (let [offset (mem/alloc-bytes-32 (modifier-get-entries-size entries)) + (let [offset (mem/alloc->offset-32 (modifier-get-entries-size entries)) heapf32 (mem/get-heap-f32) heapu32 (mem/get-heap-u32)] @@ -863,24 +868,24 @@ (when-not (empty? entries) (let [{:keys [id transform]} (first entries)] (sr/heapu32-set-uuid id heapu32 current-offset) - (sr/heapf32-set-matrix transform heapf32 (+ current-offset (mem/ptr8->ptr32 MODIFIER-ENTRY-TRANSFORM-OFFSET))) - (recur (rest entries) (+ current-offset (mem/ptr8->ptr32 MODIFIER-ENTRY-SIZE)))))) + (sr/heapf32-set-matrix transform heapf32 (+ current-offset (mem/->offset-32 MODIFIER-TRANSFORM-U8-OFFSET-SIZE))) + (recur (rest entries) (+ current-offset (mem/->offset-32 MODIFIER-U8-SIZE)))))) (let [result-offset (h/call wasm/internal-module "_propagate_modifiers" pixel-precision) heapf32 (mem/get-heap-f32) heapu32 (mem/get-heap-u32) - len (aget heapu32 (mem/ptr8->ptr32 result-offset)) + len (aget heapu32 (mem/->offset-32 result-offset)) result (->> (range 0 len) - (mapv #(dr/heap32->entry heapu32 heapf32 (mem/ptr8->ptr32 (+ result-offset 4 (* % MODIFIER-ENTRY-SIZE))))))] - (h/call wasm/internal-module "_free_bytes") + (mapv #(dr/heap32->entry heapu32 heapf32 (mem/->offset-32 (+ result-offset 4 (* % MODIFIER-U8-SIZE))))))] + (mem/free) result)))) (defn propagate-apply [entries pixel-precision] (when (d/not-empty? entries) - (let [offset (mem/alloc-bytes-32 (modifier-get-entries-size entries)) + (let [offset (mem/alloc->offset-32 (modifier-get-entries-size entries)) heapf32 (mem/get-heap-f32) heapu32 (mem/get-heap-u32)] @@ -889,25 +894,25 @@ (when-not (empty? entries) (let [{:keys [id transform]} (first entries)] (sr/heapu32-set-uuid id heapu32 current-offset) - (sr/heapf32-set-matrix transform heapf32 (+ current-offset (mem/ptr8->ptr32 MODIFIER-ENTRY-TRANSFORM-OFFSET))) - (recur (rest entries) (+ current-offset (mem/ptr8->ptr32 MODIFIER-ENTRY-SIZE)))))) + (sr/heapf32-set-matrix transform heapf32 (+ current-offset (mem/->offset-32 MODIFIER-TRANSFORM-U8-OFFSET-SIZE))) + (recur (rest entries) (+ current-offset (mem/->offset-32 MODIFIER-U8-SIZE)))))) (let [offset (h/call wasm/internal-module "_propagate_apply" pixel-precision) heapf32 (mem/get-heap-f32) - width (aget heapf32 (mem/ptr8->ptr32 (+ offset 0))) - height (aget heapf32 (mem/ptr8->ptr32 (+ offset 4))) - cx (aget heapf32 (mem/ptr8->ptr32 (+ offset 8))) - cy (aget heapf32 (mem/ptr8->ptr32 (+ offset 12))) + width (aget heapf32 (mem/->offset-32 (+ offset 0))) + height (aget heapf32 (mem/->offset-32 (+ offset 4))) + cx (aget heapf32 (mem/->offset-32 (+ offset 8))) + cy (aget heapf32 (mem/->offset-32 (+ offset 12))) - a (aget heapf32 (mem/ptr8->ptr32 (+ offset 16))) - b (aget heapf32 (mem/ptr8->ptr32 (+ offset 20))) - c (aget heapf32 (mem/ptr8->ptr32 (+ offset 24))) - d (aget heapf32 (mem/ptr8->ptr32 (+ offset 28))) - e (aget heapf32 (mem/ptr8->ptr32 (+ offset 32))) - f (aget heapf32 (mem/ptr8->ptr32 (+ offset 36))) + a (aget heapf32 (mem/->offset-32 (+ offset 16))) + b (aget heapf32 (mem/->offset-32 (+ offset 20))) + c (aget heapf32 (mem/->offset-32 (+ offset 24))) + d (aget heapf32 (mem/->offset-32 (+ offset 28))) + e (aget heapf32 (mem/->offset-32 (+ offset 32))) + f (aget heapf32 (mem/->offset-32 (+ offset 36))) transform (gmt/matrix a b c d e f)] - (h/call wasm/internal-module "_free_bytes") + (mem/free) (request-render "set-modifiers") {:width width @@ -918,7 +923,7 @@ (defn get-selection-rect [entries] (when (d/not-empty? entries) - (let [offset (mem/alloc-bytes-32 (* (count entries) 16)) + (let [offset (mem/alloc->offset-32 (* (count entries) 16)) heapu32 (mem/get-heap-u32)] (loop [entries (seq entries) @@ -926,26 +931,27 @@ (when-not (empty? entries) (let [id (first entries)] (sr/heapu32-set-uuid id heapu32 current-offset) - (recur (rest entries) (+ current-offset (mem/ptr8->ptr32 16)))))) + (recur (rest entries) (+ current-offset (mem/->offset-32 16)))))) (let [offset (h/call wasm/internal-module "_get_selection_rect") - heapf32 (mem/get-heap-f32) - width (aget heapf32 (mem/ptr8->ptr32 (+ offset 0))) - height (aget heapf32 (mem/ptr8->ptr32 (+ offset 4))) - cx (aget heapf32 (mem/ptr8->ptr32 (+ offset 8))) - cy (aget heapf32 (mem/ptr8->ptr32 (+ offset 12))) - a (aget heapf32 (mem/ptr8->ptr32 (+ offset 16))) - b (aget heapf32 (mem/ptr8->ptr32 (+ offset 20))) - c (aget heapf32 (mem/ptr8->ptr32 (+ offset 24))) - d (aget heapf32 (mem/ptr8->ptr32 (+ offset 28))) - e (aget heapf32 (mem/ptr8->ptr32 (+ offset 32))) - f (aget heapf32 (mem/ptr8->ptr32 (+ offset 36))) - transform (gmt/matrix a b c d e f)] - (h/call wasm/internal-module "_free_bytes") + heap (mem/get-heap-f32) + width (aget heap (mem/->offset-32 (+ offset 0))) + height (aget heap (mem/->offset-32 (+ offset 4))) + cx (aget heap (mem/->offset-32 (+ offset 8))) + cy (aget heap (mem/->offset-32 (+ offset 12))) + a (aget heap (mem/->offset-32 (+ offset 16))) + b (aget heap (mem/->offset-32 (+ offset 20))) + c (aget heap (mem/->offset-32 (+ offset 24))) + d (aget heap (mem/->offset-32 (+ offset 28))) + e (aget heap (mem/->offset-32 (+ offset 32))) + f (aget heap (mem/->offset-32 (+ offset 36)))] + + (mem/free) {:width width :height height :center (gpt/point cx cy) - :transform transform})))) + :transform (gmt/matrix a b c d e f)})))) + (defn set-canvas-background [background] @@ -960,7 +966,7 @@ (defn set-modifiers [modifiers] (when-not (empty? modifiers) - (let [offset (mem/alloc-bytes-32 (* MODIFIER-ENTRY-SIZE (count modifiers))) + (let [offset (mem/alloc->offset-32 (* MODIFIER-U8-SIZE (count modifiers))) heapu32 (mem/get-heap-u32) heapf32 (mem/get-heap-f32)] @@ -969,8 +975,8 @@ (when-not (empty? entries) (let [{:keys [id transform]} (first entries)] (sr/heapu32-set-uuid id heapu32 current-offset) - (sr/heapf32-set-matrix transform heapf32 (+ current-offset (mem/ptr8->ptr32 MODIFIER-ENTRY-TRANSFORM-OFFSET))) - (recur (rest entries) (+ current-offset (mem/ptr8->ptr32 MODIFIER-ENTRY-SIZE)))))) + (sr/heapf32-set-matrix transform heapf32 (+ current-offset (mem/->offset-32 MODIFIER-TRANSFORM-U8-OFFSET-SIZE))) + (recur (rest entries) (+ current-offset (mem/->offset-32 MODIFIER-U8-SIZE)))))) (h/call wasm/internal-module "_set_modifiers") @@ -1048,11 +1054,50 @@ (get position :x) (get position :y)) heapi32 (mem/get-heap-i32) - row (aget heapi32 (mem/ptr8->ptr32 (+ offset 0))) - column (aget heapi32 (mem/ptr8->ptr32 (+ offset 4)))] - (h/call wasm/internal-module "_free_bytes") + row (aget heapi32 (mem/->offset-32 (+ offset 0))) + column (aget heapi32 (mem/->offset-32 (+ offset 4)))] + (mem/free) [row column])) +(defn shape-to-path + [id] + (use-shape id) + (let [offset (h/call wasm/internal-module "_current_to_path") + offset (mem/->offset-32 offset) + heapu32 (mem/get-heap-u32) + + length (aget heapu32 offset) + data (mem/slice heapu32 + (+ offset 1) + (+ offset 1 (* length (/ path.impl/SEGMENT-U8-SIZE 4)))) + content (path/from-bytes data)] + (mem/free) + content)) + +(defn calculate-bool + [bool-type ids] + (let [num-ids (count ids) + offset (mem/alloc->offset-32 (* UUID-U8-SIZE num-ids)) + heap (mem/get-heap-u32)] + + (reduce (fn [offset id] + (sr/heapu32-set-uuid id heap offset) + (+ offset UUID-U32-SIZE)) + offset + (rseq ids))) + + (let [offset (h/call wasm/internal-module "_calculate_bool" (sr/translate-bool-type bool-type)) + offset (mem/->offset-32 offset) + heapu32 (mem/get-heap-u32) + + length (aget heapu32 offset) + data (mem/slice heapu32 + (+ offset 1) + (+ offset 1 (* length (/ path.impl/SEGMENT-U8-SIZE 4)))) + content (path/from-bytes data)] + (mem/free) + content)) + (defonce module (delay (if (exists? js/dynamicImport) diff --git a/frontend/src/app/render_wasm/api/texts.cljs b/frontend/src/app/render_wasm/api/texts.cljs index 6da007450f..5aff401ef5 100644 --- a/frontend/src/app/render_wasm/api/texts.cljs +++ b/frontend/src/app/render_wasm/api/texts.cljs @@ -35,7 +35,7 @@ (some? image) (types.fills.impl/write-image-fill offset dview opacity image)) - (+ offset types.fills.impl/FILL-BYTE-SIZE))) + (+ offset types.fills.impl/FILL-U8-SIZE))) current-offset fills)) @@ -51,7 +51,7 @@ num-leaves (count leaves) paragraph-attr-size 48 total-fills (total-fills-count leaves) - total-fills-size (* types.fills.impl/FILL-BYTE-SIZE total-fills) + total-fills-size (* types.fills.impl/FILL-U8-SIZE total-fills) leaf-attr-size 56 metadata-size (+ paragraph-attr-size (* num-leaves leaf-attr-size) total-fills-size) text-buffer (utf8->buffer text) @@ -137,7 +137,7 @@ ;; Allocate memory and set buffer (let [total-size (.-byteLength buffer) - metadata-offset (mem/alloc-bytes total-size) + metadata-offset (mem/alloc total-size) heap (mem/get-heap-u8)] (.set heap (js/Uint8Array. buffer) metadata-offset))) diff --git a/frontend/src/app/render_wasm/mem.cljs b/frontend/src/app/render_wasm/mem.cljs index ddfaf77c23..fec386b5a5 100644 --- a/frontend/src/app/render_wasm/mem.cljs +++ b/frontend/src/app/render_wasm/mem.cljs @@ -9,36 +9,30 @@ [app.render-wasm.helpers :as h] [app.render-wasm.wasm :as wasm])) -(defn ptr8->ptr32 - "Returns a 32-bit (4-byte aligned) pointer of an 8-bit pointer" +(defn ->offset-32 + "Convert a 8-bit (1 byte) offset to a 32-bit (4 bytes) offset" [value] ;; Divides the value by 4 (bit-shift-right value 2)) -(defn ptr32->ptr8 - "Returns a 8-bit pointer of a 32-bit (4-byte aligned) pointer" - [value] - ;; Multiplies by 4 - (bit-shift-left value 2)) - (defn get-list-size "Returns the size of a list in bytes" [list list-item-size] (* list-item-size (count list))) -(defn alloc-bytes - "Allocates an arbitrary amount of bytes" +(defn alloc + "Allocates an arbitrary amount of bytes (aligned to 4 bytes). + Returns an offset of 8 bits (1 byte) size." [size] (when (= size 0) (js/console.trace "Tried to allocate 0 bytes")) (h/call wasm/internal-module "_alloc_bytes" size)) -(defn alloc-bytes-32 - "Allocates a 4-byte aligned amount of bytes" +(defn alloc->offset-32 + "Allocates an arbitrary amount of bytes (aligned to 4 bytes). + Returns an offset of 32 bits (4 bytes) size." [size] - (when (= size 0) - (js/console.trace "Tried to allocate 0 bytes")) - (ptr8->ptr32 (h/call wasm/internal-module "_alloc_bytes" size))) + (-> (alloc size) (->offset-32))) (defn get-heap-u8 "Returns a Uint8Array view of the heap" @@ -59,3 +53,13 @@ "Returns a Float32Array view of the heap" [] (unchecked-get ^js wasm/internal-module "HEAPF32")) + +(defn free + [] + (h/call wasm/internal-module "_free_bytes")) + +(defn slice + "Returns a copy of a portion of a typed array into a new typed array + object selected from start to end." + [heap start end] + (.slice ^js heap start end)) diff --git a/frontend/src/app/render_wasm/serializers.cljs b/frontend/src/app/render_wasm/serializers.cljs index 779e35b01c..1d6754a939 100644 --- a/frontend/src/app/render_wasm/serializers.cljs +++ b/frontend/src/app/render_wasm/serializers.cljs @@ -168,7 +168,7 @@ :union 0 :difference 1 :intersection 2 - :exclusion 3 + :exclude 3 0)) (defn translate-blur-type diff --git a/render-wasm/Cargo.lock b/render-wasm/Cargo.lock index 3efed8c0ed..c37c1b8d14 100644 --- a/render-wasm/Cargo.lock +++ b/render-wasm/Cargo.lock @@ -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", diff --git a/render-wasm/Cargo.toml b/render-wasm/Cargo.toml index ba3a38c792..2623c3d093 100644 --- a/render-wasm/Cargo.toml +++ b/render-wasm/Cargo.toml @@ -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", diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 280adbd5fc..d7036eb5c5 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -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); } } diff --git a/render-wasm/src/math.rs b/render-wasm/src/math.rs index 2d6d06062a..cf80b4221a 100644 --- a/render-wasm/src/math.rs +++ b/render-wasm/src/math.rs @@ -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) } diff --git a/render-wasm/src/math/bools.rs b/render-wasm/src/math/bools.rs new file mode 100644 index 0000000000..5e43764826 --- /dev/null +++ b/render-wasm/src/math/bools.rs @@ -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 { + 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 { + 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, Vec) { + let path_a = path_to_beziers(path_a); + let path_b = path_to_beziers(path_b); + + let mut intersects_a = Vec::>::with_capacity(path_a.len()); + intersects_a.resize_with(path_a.len(), Default::default); + + let mut intersects_b = Vec::>::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, + path_b: &Path, + segments_b: Vec, +) -> 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, + path_b: &Path, + segments_b: Vec, +) -> 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, + path_b: &Path, + segments_b: Vec, +) -> 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, segments_b: Vec) -> 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 { + 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>; + +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, 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 { + 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, + shapes: &ShapesPool, + modifiers: &HashMap, + structure: &HashMap>, +) -> 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(¤t_path, &other_path); + + let beziers = match bool_type { + BoolType::Union => union(¤t_path, segs_a, &other_path, segs_b), + BoolType::Difference => difference(¤t_path, segs_a, &other_path, segs_b), + BoolType::Intersection => intersection(¤t_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, + structure: &HashMap>, +) -> 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, + structure: &HashMap>, +) { + 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(¤t_path, &other_path); + + let beziers = match bool_data.bool_type { + BoolType::Union => union(¤t_path, segs_a, &other_path, segs_b), + BoolType::Difference => difference(¤t_path, segs_a, &other_path, segs_b), + BoolType::Intersection => intersection(¤t_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, + ); + } + } + } +} diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 49098d9991..371aa2d72f 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -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, +) -> 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, + structure: &HashMap>, 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 = 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, + structure: &HashMap>, 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 = 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; @@ -944,8 +991,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 { diff --git a/render-wasm/src/render/fonts.rs b/render-wasm/src/render/fonts.rs index c78bc929e9..917fe134a8 100644 --- a/render-wasm/src/render/fonts.rs +++ b/render-wasm/src/render/fonts.rs @@ -110,6 +110,10 @@ impl FontStore { pub fn get_fallback(&self) -> &HashSet { &self.fallback_fonts } + + pub fn get_emoji_font(&self, _size: f32) -> Option { + None + } } fn load_default_provider(font_mgr: &FontMgr) -> skia::textlayout::TypefaceFontProvider { diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index f17f0cef6a..3e0a58233b 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -19,9 +19,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::*; @@ -36,6 +38,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::*; @@ -827,23 +830,27 @@ impl Shape { } } - pub fn all_children_with_self( + pub fn all_children( &self, shapes: &ShapesPool, include_hidden: bool, + include_self: bool, ) -> IndexSet { - 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() + } } /// Returns all ancestor shapes of this shape, traversing up the parent hierarchy @@ -1002,6 +1009,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 { diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index 0e0c59bd7a..2f6ddb8920 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -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 { 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, + modifiers: &HashMap, + structure: &HashMap>, +) -> Option { + 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, layout_reflows: &mut Vec, reflown: &mut HashSet, + modifiers: &HashMap, ) { 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, ), } } diff --git a/render-wasm/src/shapes/paths.rs b/render-wasm/src/shapes/paths.rs index 29a91bff82..e5489a16e8 100644 --- a/render-wasm/src/shapes/paths.rs +++ b/render-wasm/src/shapes/paths.rs @@ -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) -> 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 { + &self.segments + } + + pub fn bounds(&self) -> math::Bounds { + math::Bounds::from_rect(self.skia_path.bounds()) + } } diff --git a/render-wasm/src/shapes/shape_to_path.rs b/render-wasm/src/shapes/shape_to_path.rs new file mode 100644 index 0000000000..ef63eec9dc --- /dev/null +++ b/render-wasm/src/shapes/shape_to_path.rs @@ -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, + structure: &HashMap>, + ) -> 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) -> Vec { + 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 { + 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, + structure: &HashMap>, + ) -> 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 + } + } + } +} diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index c1ddc8dd19..c4a0feeb8d 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -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 { diff --git a/render-wasm/src/shapes/text_paths.rs b/render-wasm/src/shapes/text_paths.rs index 0f08921070..45f13a0c31 100644 --- a/render-wasm/src/shapes/text_paths.rs +++ b/render-wasm/src/shapes/text_paths.rs @@ -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 { - let mut paragraphs = self.to_paragraphs(); - self.collect_paragraphs(&mut paragraphs); - paragraphs + + pub fn get_skia_paragraphs(&self) -> Vec> { + 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, diff --git a/render-wasm/src/wasm/paths.rs b/render-wasm/src/wasm/paths.rs index 1aaedcf149..a211eaaff5 100644 --- a/render-wasm/src/wasm/paths.rs +++ b/render-wasm/src/wasm/paths.rs @@ -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::(); @@ -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::()]> for RawSegmentData { fn from(bytes: [u8; size_of::()]) -> 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 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::::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 = bytes + .chunks(size_of::<::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 {