diff --git a/common/src/app/common/files/migrations.cljc b/common/src/app/common/files/migrations.cljc index d495c7c06e..038e289157 100644 --- a/common/src/app/common/files/migrations.cljc +++ b/common/src/app/common/files/migrations.cljc @@ -26,6 +26,7 @@ [app.common.types.component :as ctk] [app.common.types.container :as ctn] [app.common.types.file :as ctf] + [app.common.types.fill :as types.fill] [app.common.types.path :as path] [app.common.types.path.segment :as path.segment] [app.common.types.shape :as cts] @@ -826,7 +827,7 @@ (d/update-when :components d/update-vals update-container)))) (def ^:private valid-fill? - (sm/lazy-validator ::cts/fill)) + (sm/lazy-validator types.fill/schema:fill)) (defmethod migrate-data "legacy-43" [data _] diff --git a/common/src/app/common/types/fill.cljc b/common/src/app/common/types/fill.cljc new file mode 100644 index 0000000000..2cfcda37c0 --- /dev/null +++ b/common/src/app/common/types/fill.cljc @@ -0,0 +1,68 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.types.fill + (:require + [app.common.schema :as sm] + [app.common.types.color :as types.color] + [app.common.types.fill.impl :as impl] + [clojure.set :as set])) + +(def ^:const MAX-GRADIENT-STOPS impl/MAX-GRADIENT-STOPS) +(def ^:const MAX-FILLS impl/MAX-FILLS) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; SCHEMAS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def schema:fill-attrs + [:map {:title "FillAttrs"} + [:fill-color-ref-file {:optional true} ::sm/uuid] + [:fill-color-ref-id {:optional true} ::sm/uuid] + [:fill-opacity {:optional true} [::sm/number {:min 0 :max 1}]] + [:fill-color {:optional true} types.color/schema:hex-color] + [:fill-color-gradient {:optional true} types.color/schema:gradient] + [:fill-image {:optional true} types.color/schema:image]]) + +(def fill-attrs + "A set of attrs that corresponds to fill data type" + (sm/keys schema:fill-attrs)) + +(def valid-fill-attrs + "A set used for proper check if color should contain only one of the + attrs listed in this set." + #{:fill-image :fill-color :fill-color-gradient}) + +(defn has-valid-fill-attrs? + "Check if color has correct color attrs" + [color] + (let [attrs (set (keys color)) + result (set/intersection attrs valid-fill-attrs)] + (= 1 (count result)))) + +(def schema:fill + [:and schema:fill-attrs + [:fn has-valid-fill-attrs?]]) + +(def check-fill + (sm/check-fn schema:fill)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; HELPERS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; CONSTRUCTORS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn from-plain + [o] + (assert (every? check-fill o) "expected valid fills vector") + (impl/from-plain o)) + +(defn fills? + [o] + (impl/fills? o)) diff --git a/common/src/app/common/types/fill/impl.cljc b/common/src/app/common/types/fill/impl.cljc new file mode 100644 index 0000000000..5094e4b296 --- /dev/null +++ b/common/src/app/common/types/fill/impl.cljc @@ -0,0 +1,404 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.types.fill.impl + (:require + #?(:clj [clojure.data.json :as json]) + #?(:cljs [app.common.weak-map :as weak-map]) + [app.common.buffer :as buf] + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.math :as mth] + [app.common.transit :as t])) + +;; FIXME: Get these from the wasm module, and tweak the values +;; (we'd probably want 12 stops at most) +(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 28) +(def ^:const METADATA-BYTE-SIZE 36) +(def ^:const FILL-BYTE-SIZE + (+ 4 (mth/max GRADIENT-BYTE-SIZE + IMAGE-BYTE-SIZE + SOLID-BYTE-SIZE))) + +(def ^:private xf:take-stops + (take MAX-GRADIENT-STOPS)) + +(def ^:private xf:take-fills + (take MAX-FILLS)) + +(defn- hex->rgb + "Encode an hex string as rgb (int32)" + [hex] + (let [hex (subs hex 1)] + #?(:clj (Integer/parseInt hex 16) + :cljs (js/parseInt hex 16)))) + +(defn- rgb->rgba + "Use the first 2 bytes of in32 for encode the alpha channel" + [n alpha] + (let [result (mth/floor (* alpha 0xff)) + result (unchecked-int result) + result (bit-shift-left result 24) + result (bit-or result n)] + result)) + +(defn- get-color-hex + [n] + (let [n (bit-and n 0x00ffffff) + n #?(:clj n :cljs (.toString n 16))] + (dm/str "#" #?(:clj (String/format "%06x" (into-array Object [n])) + :cljs (.padStart n 6 "0"))))) + +(defn- get-color-alpha + [rgb] + (let [n (bit-and rgb 0xff000000) + n (unsigned-bit-shift-right n 24)] + (mth/precision (/ (float n) 0xff) 2))) + +(defn- write-solid-fill + [offset buffer color alpha] + (buf/write-byte buffer (+ offset 0) 0x00) + (buf/write-int buffer (+ offset 4) + (-> (hex->rgb color) + (rgb->rgba alpha))) + (+ offset FILL-BYTE-SIZE)) + +(defn- write-gradient-fill + [offset buffer gradient opacity] + (let [start-x (:start-x gradient) + start-y (:start-y gradient) + end-x (:end-x gradient) + end-y (:end-y gradient) + width (:width gradient 0) + stops (into [] xf:take-stops (:stops gradient)) + type (if (= (:type gradient) :linear) + 0x01 + 0x02)] + + (buf/write-byte buffer (+ offset 0) type) + (buf/write-float buffer (+ offset 4) start-x) + (buf/write-float buffer (+ offset 8) start-y) + (buf/write-float buffer (+ offset 12) end-x) + (buf/write-float buffer (+ offset 16) end-y) + (buf/write-float buffer (+ offset 20) opacity) + (buf/write-float buffer (+ offset 24) width) + (buf/write-byte buffer (+ offset 28) (count stops)) + + (loop [stops (seq stops) + offset' (+ offset 32)] + (if-let [stop (first stops)] + (let [color (-> (hex->rgb (:color stop)) + (rgb->rgba (:opacity stop 1)))] + ;; NOTE: we write the color as signed integer but on rust + ;; side it will be read as unsigned, on the end the binary + ;; repr of the data is the same independently on how it is + ;; interpreted + (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))))) + +(defn- write-image-fill + [offset buffer opacity image] + (let [image-id (get image :id) + image-width (get image :width) + image-height (get image :height)] + (buf/write-byte buffer (+ offset 0) 0x03) + (buf/write-uuid buffer (+ offset 4) image-id) + (buf/write-float buffer (+ offset 20) opacity) + (buf/write-int buffer (+ offset 24) image-width) + (buf/write-int buffer (+ offset 28) image-height) + (+ offset FILL-BYTE-SIZE))) + +(defn- write-metadata + [offset buffer fill] + (let [ref-id (:fill-color-ref-id fill) + ref-file (:fill-color-ref-file fill) + mtype (dm/get-in fill [:fill-image :mtype])] + + (when mtype + (let [val (case mtype + "image/jpeg" 0x01 + "image/png" 0x02 + "image/gif" 0x03 + "image/webp" 0x04 + "image/svg+xml" 0x05)] + (buf/write-short buffer (+ offset 2) val))) + + (if (and (some? ref-file) + (some? ref-id)) + (do + (buf/write-byte buffer (+ offset 0) 0x01) + (buf/write-uuid buffer (+ offset 4) ref-file) + (buf/write-uuid buffer (+ offset 20) ref-id)) + (do + (buf/write-byte buffer (+ offset 0) 0x00))))) + +(defn- read-stop + [buffer offset] + (let [rgba (buf/read-int buffer (+ offset 0)) + soff (buf/read-float buffer (+ offset 4))] + {:color (get-color-hex rgba) + :opacity (get-color-alpha rgba) + :offset (mth/precision soff 2)})) + +(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) + type (buf/read-byte dbuffer doffset) + refs? (buf/read-bool mbuffer (+ moffset 0)) + fill (case type + 0 + (let [rgba (buf/read-int dbuffer (+ doffset 4))] + {:fill-color (get-color-hex rgba) + :fill-opacity (get-color-alpha rgba)}) + + (1 2) + (let [start-x (buf/read-float dbuffer (+ doffset 4)) + start-y (buf/read-float dbuffer (+ doffset 8)) + end-x (buf/read-float dbuffer (+ doffset 12)) + end-y (buf/read-float dbuffer (+ doffset 16)) + alpha (buf/read-float dbuffer (+ doffset 20)) + width (buf/read-float dbuffer (+ doffset 24)) + stops (buf/read-byte dbuffer (+ doffset 28)) + type (if (= type 1) + :linear + :radial) + stops (loop [index 0 + result []] + (if (< index stops) + (recur (inc index) + (conj result (read-stop dbuffer (+ doffset 32 (* GRADIENT-STOP-SIZE index))))) + result))] + + {:fill-opacity alpha + :fill-color-gradient {:start-x start-x + :start-y start-y + :end-x end-x + :end-y end-y + :width width + :stops stops + :type type}}) + + 3 + (let [id (buf/read-uuid dbuffer (+ doffset 4)) + alpha (buf/read-float dbuffer (+ doffset 20)) + width (buf/read-int dbuffer (+ doffset 24)) + height (buf/read-int dbuffer (+ doffset 28)) + mtype (buf/read-short mbuffer (+ moffset 2)) + mtype (case mtype + 0x01 "image/jpeg" + 0x02 "image/png" + 0x03 "image/gif" + 0x04 "image/webp" + 0x05 "image/svg+xml")] + {:fill-opacity alpha + :fill-image {:id id + :width width + :height height + :mtype mtype + ;; FIXME: we are not encodign the name, looks useless + :name "sample"}}))] + + (if refs? + (let [ref-file (buf/read-uuid mbuffer (+ moffset 4)) + ref-id (buf/read-uuid mbuffer (+ moffset 20))] + (-> fill + (assoc :fill-color-ref-id ref-id) + (assoc :fill-color-ref-file ref-file))) + fill))) + +(declare from-plain) + +#?(:clj + (deftype Fills [size dbuffer mbuffer ^:unsynchronized-mutable hash] + Object + (equals [_ other] + (if (instance? Fills other) + (and (buf/equals? dbuffer (.-dbuffer ^Fills other)) + (buf/equals? mbuffer (.-mbuffer ^Fills other))) + false)) + + json/JSONWriter + (-write [this writter options] + (json/-write (vec this) writter options)) + + clojure.lang.IHashEq + (hasheq [this] + (when-not hash + (set! hash (clojure.lang.Murmur3/hashOrdered (seq this)))) + hash) + + clojure.lang.Sequential + clojure.lang.Seqable + (seq [_] + (when (pos? size) + ((fn next-seq [i] + (when (< i size) + (cons (read-fill dbuffer mbuffer i) + (lazy-seq (next-seq (inc i)))))) + 0))) + + clojure.lang.IReduceInit + (reduce [_ f start] + (loop [index 0 + result start] + (if (< index size) + (let [result (f result (read-fill dbuffer mbuffer index))] + (if (reduced? result) + @result + (recur (inc index) result))) + result))) + + clojure.lang.Indexed + (nth [_ i] + (if (d/in-range? size i) + (read-fill dbuffer mbuffer i) + nil)) + + (nth [_ i default] + (if (d/in-range? size i) + (read-fill dbuffer mbuffer i) + default)) + + clojure.lang.Counted + (count [_] size)) + + :cljs + #_:clj-kondo/ignore + (deftype Fills [size dbuffer mbuffer cache ^:mutable __hash] + cljs.core/ISequential + cljs.core/IEquiv + (-equiv [this other] + (if (instance? Fills other) + (and ^boolean (buf/equals? (.-dbuffer ^Fills other) dbuffer) + ^boolean (buf/equals? (.-mbuffer ^Fills other) mbuffer)) + false)) + + cljs.core/IEncodeJS + (-clj->js [this] + (clj->js (vec this))) + + ;; cljs.core/APersistentVector + cljs.core/IAssociative + (-assoc [coll k v] + (if (number? k) + (-> (vec coll) + (assoc k v) + (from-plain)) + (throw (js/Error. "Vector's key for assoc must be a number.")))) + + (-contains-key? [coll k] + (if (integer? k) + (and (<= 0 k) (< k size)) + false)) + + cljs.core/IReduce + (-reduce [_ f] + (loop [index 1 + result (if (pos? size) + (read-fill dbuffer mbuffer 0) + nil)] + (if (< index size) + (let [result (f result (read-fill dbuffer mbuffer index))] + (if (reduced? result) + @result + (recur (inc index) result))) + result))) + + (-reduce [_ f start] + (loop [index 0 + result start] + (if (< index size) + (let [result (f result (read-fill dbuffer mbuffer index))] + (if (reduced? result) + @result + (recur (inc index) result))) + result))) + + cljs.core/IHash + (-hash [coll] + (caching-hash coll hash-ordered-coll __hash)) + + cljs.core/ICounted + (-count [_] size) + + cljs.core/IIndexed + (-nth [_ i] + (if (d/in-range? size i) + (read-fill dbuffer mbuffer i) + nil)) + + (-nth [_ i default] + (if (d/in-range? i size) + (read-fill dbuffer mbuffer i) + default)) + + cljs.core/ISeqable + (-seq [this] + (when (pos? size) + ((fn next-seq [i] + (when (< i size) + (cons (read-fill dbuffer mbuffer i) + (lazy-seq (next-seq (inc i)))))) + 0))))) + +(defn from-plain + [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))] + + (buf/write-byte dbuffer 0 total) + + (loop [index 0] + (when (< index total) + (let [fill (nth fills index) + doffset (+ 4 (* index FILL-BYTE-SIZE)) + moffset (* index METADATA-BYTE-SIZE) + opacity (get fill :fill-opacity 1)] + + (if-let [color (get fill :fill-color)] + (do + (write-solid-fill doffset dbuffer color opacity) + (write-metadata moffset mbuffer fill) + (recur (inc index))) + (if-let [gradient (get fill :fill-color-gradient)] + (do + (write-gradient-fill doffset dbuffer gradient opacity) + (write-metadata moffset mbuffer fill) + (recur (inc index))) + (if-let [image (get fill :fill-image)] + (do + (write-image-fill doffset dbuffer opacity image) + (write-metadata moffset mbuffer fill) + (recur (inc index))) + (recur (inc index)))))))) + + #?(:cljs (Fills. total dbuffer mbuffer (weak-map/create) nil) + :clj (Fills. total dbuffer mbuffer nil)))) + +(defn fills? + [o] + (instance? Fills o)) + +(t/add-handlers! + {:id "penpot/fills" + :class Fills + :wfn (fn [^Fills fills] + (vec fills)) + :rfn #?(:cljs from-plain + :clj identity)}) diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index 3f4e1efe97..f015751c02 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -21,6 +21,7 @@ [app.common.text :as txt] [app.common.transit :as t] [app.common.types.color :as types.color] + [app.common.types.fill :refer [schema:fill]] [app.common.types.grid :as ctg] [app.common.types.path :as path] [app.common.types.path.segment :as path.segment] @@ -119,43 +120,6 @@ (def schema:points [:vector {:gen/max 4 :gen/min 4} ::gpt/point]) -;; FIXME: generate from schema for make schema unique source of truth -(def fill-attrs - "A set of attrs that corresponds to fill data type" - #{:fill-color :fill-opacity :fill-color-gradient :fill-color-ref-id :fill-color-ref-file}) - -(def ^:private schema:fill-attrs - [:map {:title "FillAttrs"} - [:fill-color-ref-file {:optional true} ::sm/uuid] - [:fill-color-ref-id {:optional true} ::sm/uuid] - [:fill-opacity {:optional true} [::sm/number {:min 0 :max 1}]] - [:fill-color {:optional true} types.color/schema:hex-color] - [:fill-color-gradient {:optional true} types.color/schema:gradient] - [:fill-image {:optional true} types.color/schema:image]]) - -;; FIXME: the register is necessary until this is moved to a separated -;; ns because it is used on shapes.text -(def valid-fill-attrs - "A set used for proper check if color should contain only one of the - attrs listed in this set." - #{:fill-image :fill-color :fill-color-gradient}) - -(defn has-valid-fill-attrs? - "Check if color has correct color attrs" - [color] - (let [attrs (set (keys color)) - result (set/intersection attrs valid-fill-attrs)] - (= 1 (count result)))) - -(def schema:fill - (sm/register! - ^{::sm/type ::fill} - [:and schema:fill-attrs - [:fn has-valid-fill-attrs?]])) - -(def check-fill - (sm/check-fn schema:fill)) - ;; FIXME: the register is necessary until this is moved to a separated ;; ns because it is used on shapes.text (def valid-stroke-attrs @@ -805,8 +769,3 @@ (d/patch-object (select-keys props basic-extract-props)) (cond-> (cfh/text-shape? shape) (patch-text-props props)) (cond-> (cfh/frame-shape? shape) (patch-layout-props props))))) - -;; FIXME: Get these from the wasm module, and tweak the values -;; (we'd probably want 12 stops at most) -(def MAX-GRADIENT-STOPS 16) -(def MAX-FILLS 8) diff --git a/common/src/app/common/types/shape/text.cljc b/common/src/app/common/types/shape/text.cljc index 9b0e6908c9..03dca9fb74 100644 --- a/common/src/app/common/types/shape/text.cljc +++ b/common/src/app/common/types/shape/text.cljc @@ -7,6 +7,7 @@ (ns app.common.types.shape.text (:require [app.common.schema :as sm] + [app.common.types.fill :refer [schema:fill]] [app.common.types.shape :as-alias shape] [app.common.types.shape.text.position-data :as-alias position-data])) @@ -34,7 +35,7 @@ [:key {:optional true} :string] [:fills {:optional true} [:maybe - [:vector {:gen/max 2} ::shape/fill]]] + [:vector {:gen/max 2} schema:fill]]] [:font-family {:optional true} :string] [:font-size {:optional true} :string] [:font-style {:optional true} :string] @@ -51,7 +52,7 @@ [:key {:optional true} :string] [:fills {:optional true} [:maybe - [:vector {:gen/max 2} ::shape/fill]]] + [:vector {:gen/max 2} schema:fill]]] [:font-family {:optional true} :string] [:font-size {:optional true} :string] [:font-style {:optional true} :string] @@ -75,7 +76,7 @@ [:y ::sm/safe-number] [:width ::sm/safe-number] [:height ::sm/safe-number] - [:fills [:vector {:gen/max 2} ::shape/fill]] + [:fills [:vector {:gen/max 2} schema:fill]] [:font-family {:optional true} :string] [:font-size {:optional true} :string] [:font-style {:optional true} :string] diff --git a/common/test/common_tests/runner.cljc b/common/test/common_tests/runner.cljc index 52078b36a3..c09ae65416 100644 --- a/common/test/common_tests/runner.cljc +++ b/common/test/common_tests/runner.cljc @@ -39,6 +39,7 @@ [common-tests.time-test] [common-tests.types.absorb-assets-test] [common-tests.types.components-test] + [common-tests.types.fill-test] [common-tests.types.modifiers-test] [common-tests.types.path-data-test] [common-tests.types.shape-decode-encode-test] @@ -91,6 +92,7 @@ 'common-tests.types.components-test 'common-tests.types.modifiers-test 'common-tests.types.path-data-test + 'common-tests.types.fill-test 'common-tests.types.shape-decode-encode-test 'common-tests.types.shape-interactions-test 'common-tests.types.tokens-lib-test diff --git a/common/test/common_tests/types/fill_test.cljc b/common/test/common_tests/types/fill_test.cljc new file mode 100644 index 0000000000..de1514cccb --- /dev/null +++ b/common/test/common_tests/types/fill_test.cljc @@ -0,0 +1,214 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns common-tests.types.fill-test + (:require + #?(:clj [app.common.fressian :as fres]) + [app.common.data :as d] + [app.common.exceptions :as ex] + [app.common.math :as mth] + [app.common.pprint :as pp] + [app.common.pprint :as pp] + [app.common.schema.generators :as sg] + [app.common.schema.test :as smt] + [app.common.transit :as trans] + [app.common.types.fill :as types.fill] + [app.common.uuid :as uuid] + [clojure.test :as t])) + +(defn equivalent-fill? + [fill-a fill-b] + ;; (prn "-------------------") + ;; (app.common.pprint/pprint fill-a) + ;; (app.common.pprint/pprint fill-b) + + (and (= (get fill-a :fill-color-ref-file) + (get fill-b :fill-color-ref-file)) + + (= (get fill-a :fill-color-ref-id) + (get fill-b :fill-color-ref-id)) + + (or (and (contains? fill-a :fill-color) + (= (:fill-color fill-a) + (:fill-color fill-b)) + (mth/close? (:fill-opacity fill-a 1.0) + (:fill-opacity fill-b 1.0))) + (and (contains? fill-a :fill-image) + (mth/close? (:fill-opacity fill-a 1.0) + (:fill-opacity fill-b 1.0)) + (let [image-a (:fill-image fill-a) + image-b (:fill-image fill-b)] + (and (= (:id image-a) + (:id image-b)) + (= (:mtype image-a) + (:mtype image-b)) + (mth/close? (:width image-a) + (:width image-b)) + (mth/close? (:height image-a) + (:height image-b))))) + (and (contains? fill-a :fill-color-gradient) + (mth/close? (:fill-opacity fill-a 1) + (:fill-opacity fill-b 1)) + (let [gradient-a (:fill-color-gradient fill-a) + gradient-b (:fill-color-gradient fill-b)] + (and (= (count (:stops gradient-a)) + (count (:stops gradient-b))) + (= (get gradient-a :type) + (get gradient-b :type)) + (mth/close? (get gradient-a :start-x) + (get gradient-b :start-x)) + (mth/close? (get gradient-a :start-y) + (get gradient-b :start-y)) + (mth/close? (get gradient-a :end-x) + (get gradient-b :end-x)) + (mth/close? (get gradient-a :end-y) + (get gradient-b :end-y)) + (mth/close? (get gradient-a :width) + (get gradient-b :width)) + (every? true? + (map (fn [stop-a stop-b] + (and (= (get stop-a :color) + (get stop-b :color)) + (mth/close? (get stop-a :opacity 1) + (get stop-b :opacity 1)) + (mth/close? (get stop-a :offset) + (get stop-b :offset)))) + (get gradient-a :stops) + (get gradient-b :stops))))))))) + + +(def sample-fill-1 + {:fill-color "#fabada" + :fill-opacity 0.7}) + +(t/deftest build-from-plain-1 + (let [fills (types.fill/from-plain [sample-fill-1])] + (t/is (types.fill/fills? fills)) + (t/is (= 1 (count fills))) + (t/is (equivalent-fill? (first fills) sample-fill-1)))) + +(def sample-fill-2 + {:fill-color-ref-file #uuid "4fcb3db7-d281-8004-8006-3a97e2e142ad" + :fill-color-ref-id #uuid "fb19956a-c9e0-8056-8006-3a9c78f531c6" + :fill-image {:width 200, :height 100, :mtype "image/gif", + :id #uuid "b30f028d-cc2f-8035-8006-3a93bd0e137b", + :name "ovba", + :keep-aspect-ratio false}}) + +(t/deftest build-from-plain-2 + (let [fills (types.fill/from-plain [sample-fill-2])] + (t/is (types.fill/fills? fills)) + (t/is (= 1 (count fills))) + (t/is (equivalent-fill? (first fills) sample-fill-2)))) + +(def sample-fill-3 + {:fill-color-ref-id #uuid "fb19956a-c9e0-8056-8006-3a9c78f531c6" + :fill-color-ref-file #uuid "fb19956a-c9e0-8056-8006-3a9c78f531c5" + :fill-color-gradient + {:type :linear, + :start-x 0.75, + :start-y 3.0, + :end-x 1.0, + :end-y 1.5, + :width 200, + :stops [{:color "#631aa8", :offset 0.5}]}}) + +(t/deftest build-from-plain-3 + (let [fills (types.fill/from-plain [sample-fill-3])] + (t/is (types.fill/fills? fills)) + (t/is (= 1 (count fills))) + (t/is (equivalent-fill? (first fills) sample-fill-3)))) + +(def sample-fill-4 + {:fill-color-ref-file #uuid "2eef07f1-e38a-8062-8006-3aa264d5b784", + :fill-color-gradient + {:type :radial, + :start-x 0.5, + :start-y -1.0, + :end-x -0.5, + :end-y 2, + :width 0.5, + :stops [{:color "#781025", :offset 0.0} {:color "#035c3f", :offset 0.2}]}, + :fill-opacity 1.0, + :fill-color-ref-id #uuid "2eef07f1-e38a-8062-8006-3aa264d5b785"}) + +(t/deftest build-from-plain-4 + (let [fills (types.fill/from-plain [sample-fill-4])] + (t/is (types.fill/fills? fills)) + (t/is (= 1 (count fills))) + (t/is (equivalent-fill? (first fills) sample-fill-4)))) + +(def sample-fill-5 + {:fill-color-ref-file #uuid "b0f76f9a-f548-806e-8006-3aa4456131d1", + :fill-color-ref-id #uuid "b0f76f9a-f548-806e-8006-3aa445618851", + :fill-color-gradient + {:type :radial, + :start-x -0.86, + :start-y 6.0, + :end-x 0.25, + :end-y -0.5, + :width 3.8, + :stops [{:color "#bba1aa", :opacity 0.37, :offset 0.84}]}}) + +(t/deftest build-from-plain-5 + (let [fills (types.fill/from-plain [sample-fill-5])] + (t/is (types.fill/fills? fills)) + (t/is (= 1 (count fills))) + (t/is (equivalent-fill? (first fills) sample-fill-5)))) + +(def sample-fill-6 + {:fill-color-gradient + {:type :linear, + :start-x 3.5, + :start-y 0.39, + :end-x -1.87, + :end-y 1.95, + :width 2.62, + :stops [{:color "#e15610", :offset 0.4} {:color "#005a9e", :opacity 0.62, :offset 0.81}]}}) + +(t/deftest build-from-plain-6 + (let [fills (types.fill/from-plain [sample-fill-6])] + (t/is (types.fill/fills? fills)) + (t/is (= 1 (count fills))) + (t/is (equivalent-fill? (first fills) sample-fill-6)))) + +(t/deftest fills-datatype-roundtrip + (smt/check! + (smt/for [fill (->> (sg/generator types.fill/schema:fill) + (sg/fmap d/without-nils) + (sg/fmap (fn [fill] + (cond-> fill + (and (not (and (contains? fill :fill-color-ref-id) + (contains? fill :fill-color-ref-file))) + (or (contains? fill :fill-color-ref-id) + (contains? fill :fill-color-ref-file))) + (-> (assoc :fill-color-ref-file (uuid/next)) + (assoc :fill-color-ref-id (uuid/next)))))))] + (let [bfills (types.fill/from-plain [fill])] + (and (= (count bfills) 1) + (equivalent-fill? (first bfills) fill)))) + {:num 2000})) + +(t/deftest equality-operation + (let [fills1 (types.fill/from-plain [sample-fill-6]) + fills2 (types.fill/from-plain [sample-fill-6])] + (t/is (= fills1 fills2)))) + +(t/deftest reduce-impl + (let [fills1 (types.fill/from-plain [sample-fill-6]) + fills2 (reduce (fn [result fill] + (conj result fill)) + [] + fills1) + fills3 (types.fill/from-plain fills2)] + (t/is (= fills1 fills3)))) + +(t/deftest indexed-access + (let [fills1 (types.fill/from-plain [sample-fill-6]) + fill0 (nth fills1 0) + fill1 (nth fills1 1)] + (t/is (nil? fill1)) + (t/is (equivalent-fill? fill0 sample-fill-6)))) diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 13770e3b82..b78f2c4072 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -13,6 +13,7 @@ [app.common.schema :as sm] [app.common.text :as txt] [app.common.types.color :as ctc] + [app.common.types.fill :as types.fill] [app.common.types.shape :as shp] [app.common.types.shape.shadow :refer [check-shadow]] [app.config :as cfg] @@ -144,7 +145,7 @@ (d/without-nils) :always - (shp/check-fill)) + (types.fill/check-fill)) transform-attrs #(transform % fill)] @@ -856,7 +857,7 @@ (update state :colorpicker (fn [{:keys [stops editing-stop] :as state}] (let [cap-stops? (or (features/active-feature? state "render-wasm/v1") (contains? cfg/flags :frontend-binary-fills)) - can-add-stop? (or (not cap-stops?) (< (count stops) shp/MAX-GRADIENT-STOPS))] + can-add-stop? (or (not cap-stops?) (< (count stops) types.fill/MAX-GRADIENT-STOPS))] (if can-add-stop? (if (cc/uniform-spread? stops) ;; Add to uniform @@ -902,7 +903,7 @@ (fn [state] (let [stops (:stops state) cap-stops? (or (features/active-feature? state "render-wasm/v1") (contains? cfg/flags :frontend-binary-fills)) - can-add-stop? (or (not cap-stops?) (< (count stops) shp/MAX-GRADIENT-STOPS))] + can-add-stop? (or (not cap-stops?) (< (count stops) types.fill/MAX-GRADIENT-STOPS))] (if can-add-stop? (let [new-stop (-> (cc/interpolate-gradient stops offset) (split-color-components)) stops (conj stops new-stop) @@ -922,8 +923,12 @@ (update state :colorpicker (fn [state] (let [stop (or (:editing-stop state) 0) - cap-stops? (or (features/active-feature? state "render-wasm/v1") (contains? cfg/flags :frontend-binary-fills)) - stops (mapv split-color-components (if cap-stops? (take shp/MAX-GRADIENT-STOPS stops) stops))] + cap-stops? (or (features/active-feature? state "render-wasm/v1") + (contains? cfg/flags :frontend-binary-fills)) + stops (mapv split-color-components + (if cap-stops? + (take types.fill/MAX-GRADIENT-STOPS stops) + stops))] (-> state (assoc :current-color (get stops stop)) (assoc :stops stops)))))))) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index e8d063634f..53106f25eb 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -16,8 +16,8 @@ [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.common.text :as txt] + [app.common.types.fill :as types.fill] [app.common.types.modifiers :as ctm] - [app.common.types.shape :as types.shape] [app.common.uuid :as uuid] [app.main.data.event :as ev] [app.main.data.helpers :as dsh] @@ -237,7 +237,7 @@ (defn- to-new-fills [data] - [(d/without-nils (select-keys data types.shape/fill-attrs))]) + [(d/without-nils (select-keys data types.fill/fill-attrs))]) (defn- shape-current-values [shape pred attrs] @@ -247,7 +247,7 @@ (if (txt/is-text-node? node) (let [fills (cond - (types.shape/has-valid-fill-attrs? node) + (types.fill/has-valid-fill-attrs? node) (to-new-fills node) (some? (:fills node)) @@ -473,7 +473,7 @@ (defn migrate-node [node] - (let [color-attrs (not-empty (select-keys node types.shape/fill-attrs))] + (let [color-attrs (not-empty (select-keys node types.fill/fill-attrs))] (cond-> node (nil? (:fills node)) (assoc :fills []) diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index bfd54a182c..97abf54432 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -12,7 +12,7 @@ [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] - [app.common.types.shape :as shp] + [app.common.types.fill :as types.fill] [app.config :as cfg] [app.main.data.event :as-alias ev] [app.main.data.modal :as modal] @@ -439,7 +439,7 @@ (when (= selected-mode :gradient) [:> gradients* {:type (:type state) - :stops (if cap-stops? (vec (take shp/MAX-GRADIENT-STOPS (:stops state))) (:stops state)) + :stops (if cap-stops? (vec (take types.fill/MAX-GRADIENT-STOPS (:stops state))) (:stops state)) :editing-stop (:editing-stop state) :on-stop-edit-start handle-stop-edit-start :on-stop-edit-finish handle-stop-edit-finish diff --git a/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs b/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs index d7bd7823ee..961affdc17 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs @@ -11,7 +11,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.math :as mth] - [app.common.types.shape :as shp] + [app.common.types.fill :as types.fill] [app.config :as cfg] [app.main.features :as features] [app.main.ui.components.numeric-input :refer [numeric-input*]] @@ -288,7 +288,7 @@ (when on-reverse-stops (on-reverse-stops)))) cap-stops? (or (features/use-feature "render-wasm/v1") (contains? cfg/flags :frontend-binary-fills)) - add-stop-disabled? (when cap-stops? (>= (count stops) shp/MAX-GRADIENT-STOPS))] + add-stop-disabled? (when cap-stops? (>= (count stops) types.fill/MAX-GRADIENT-STOPS))] [:div {:class (stl/css :gradient-panel)} [:div {:class (stl/css :gradient-preview)} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index 283208a55b..9090730cbe 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -10,7 +10,7 @@ [app.common.colors :as clr] [app.common.data :as d] [app.common.types.color :as ctc] - [app.common.types.shape :as shp] + [app.common.types.fill :as types.fill] [app.common.types.shape.attrs :refer [default-color]] [app.config :as cfg] [app.main.data.workspace.colors :as dc] @@ -24,6 +24,7 @@ [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) +;; FIXME:revisit this (def fill-attrs [:fills :fill-color @@ -55,12 +56,12 @@ ;; Excluding nil values values (d/without-nils values) fills (if (contains? cfg/flags :frontend-binary-fills) - (take shp/MAX-FILLS (d/nilv (:fills values) [])) + (take types.fill/MAX-FILLS (d/nilv (:fills values) [])) (:fills values)) has-fills? (or (= :multiple fills) (some? (seq fills))) can-add-fills? (if (contains? cfg/flags :frontend-binary-fills) (and (not (= :multiple fills)) - (< (count fills) shp/MAX-FILLS)) + (< (count fills) types.fill/MAX-FILLS)) (not (= :multiple fills))) state* (mf/use-state has-fills?) diff --git a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs index 3893216da1..2d75db9457 100644 --- a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs @@ -15,7 +15,7 @@ [app.common.geom.shapes :as gsh] [app.common.geom.shapes.points :as gsp] [app.common.math :as mth] - [app.common.types.shape :as shp] + [app.common.types.fill :as types.fill] [app.config :as cfg] [app.main.data.workspace.colors :as dc] [app.main.features :as features] @@ -135,7 +135,7 @@ handler-state (mf/use-state {:display? false :offset 0 :hover nil}) cap-stops? (or (features/use-feature "render-wasm/v1") (contains? cfg/flags :frontend-binary-fills)) - can-add-stop? (if cap-stops? (< (count stops) shp/MAX-GRADIENT-STOPS) true) + can-add-stop? (if cap-stops? (< (count stops) types.fill/MAX-GRADIENT-STOPS) true) endpoint-on-pointer-down (fn [position event] @@ -527,7 +527,7 @@ gradient (:gradient state) cap-stops? (or (features/use-feature "render-wasm/v1") (contains? cfg/flags :frontend-binary-fills)) stops (if cap-stops? - (vec (take shp/MAX-GRADIENT-STOPS (:stops state))) + (vec (take types.fill/MAX-GRADIENT-STOPS (:stops state))) (:stops state)) editing-stop (:editing-stop state)] diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs index 72485a73b6..c1dc10c8f7 100644 --- a/frontend/src/app/plugins/shape.cljs +++ b/frontend/src/app/plugins/shape.cljs @@ -20,6 +20,7 @@ [app.common.types.component :as ctk] [app.common.types.container :as ctn] [app.common.types.file :as ctf] + [app.common.types.fill :as types.fill] [app.common.types.grid :as ctg] [app.common.types.path :as path] [app.common.types.path.segment :as path.segm] @@ -708,7 +709,7 @@ id (:id shape) value (parser/parse-fills value)] (cond - (not (sm/validate [:vector ::cts/fill] value)) + (not (sm/validate [:vector types.fill/schema:fill] value)) (u/display-not-valid :fills value) (cfh/text-shape? shape) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index a4e02bee9f..8be51712a2 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -12,8 +12,8 @@ [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.types.fill :as types.fill] [app.common.types.path :as path] - [app.common.types.shape :as shp] [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] [app.config :as cf] @@ -240,7 +240,7 @@ (defn set-shape-fills [fills] - (let [fills (take shp/MAX-FILLS fills) + (let [fills (take types.fill/MAX-FILLS fills) image-fills (filter :fill-image fills) offset (mem/alloc-bytes (* (count fills) sr-fills/FILL-BYTE-SIZE)) heap (mem/get-heap-u8) @@ -302,7 +302,9 @@ (let [id (dm/get-prop image :id) buffer (uuid/get-u32 id) cached-image? (h/call wasm/internal-module "_is_image_cached" (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3))] - (sr-fills/write-image-fill! offset dview id opacity (dm/get-prop image :width) (dm/get-prop image :height)) + (sr-fills/write-image-fill! offset dview id opacity + (dm/get-prop image :width) + (dm/get-prop image :height)) (h/call wasm/internal-module "_add_shape_stroke_fill") (when (== cached-image? 0) (store-image id))) diff --git a/frontend/src/app/render_wasm/serializers/fills.cljs b/frontend/src/app/render_wasm/serializers/fills.cljs index 404e29e22f..b39e4a0dbd 100644 --- a/frontend/src/app/render_wasm/serializers/fills.cljs +++ b/frontend/src/app/render_wasm/serializers/fills.cljs @@ -1,7 +1,7 @@ (ns app.render-wasm.serializers.fills (:require [app.common.data.macros :as dm] - [app.common.types.shape :as shp] + [app.common.types.fill :as types.fill] [app.common.uuid :as uuid] [app.render-wasm.serializers.color :as clr])) @@ -41,7 +41,7 @@ end-x (:end-x gradient) end-y (:end-y gradient) width (or (:width gradient) 0) - stops (take shp/MAX-GRADIENT-STOPS (:stops gradient)) + stops (take types.fill/MAX-GRADIENT-STOPS (:stops gradient)) type (if (= (:type gradient) :linear) 0x01 0x02)] (.setUint8 dview offset type true) (.setFloat32 dview (+ offset 4) start-x true) @@ -78,4 +78,4 @@ (some? image) (let [id (dm/get-prop image :id)] - (write-image-fill! offset dview id opacity (dm/get-prop image :width) (dm/get-prop image :height)))))) \ No newline at end of file + (write-image-fill! offset dview id opacity (dm/get-prop image :width) (dm/get-prop image :height))))))