diff --git a/common/src/app/common/pages/changes_builder.cljc b/common/src/app/common/pages/changes_builder.cljc index 8ffc38e13..ea2e70cc5 100644 --- a/common/src/app/common/pages/changes_builder.cljc +++ b/common/src/app/common/pages/changes_builder.cljc @@ -286,37 +286,38 @@ update-shape (fn [changes id] (let [old-obj (get objects id) - new-obj (update-fn old-obj) + new-obj (update-fn old-obj)] + (if (= old-obj new-obj) + changes + (let [attrs (or attrs (d/concat-set (keys old-obj) (keys new-obj))) - attrs (or attrs (d/concat-set (keys old-obj) (keys new-obj))) + {rops :rops uops :uops} + (reduce #(generate-operation %1 %2 old-obj new-obj ignore-geometry?) + {:rops [] :uops []} + attrs) - {rops :rops uops :uops} - (reduce #(generate-operation %1 %2 old-obj new-obj ignore-geometry?) - {:rops [] :uops []} - attrs) + uops (cond-> uops + (seq uops) + (d/preconj {:type :set-touched :touched (:touched old-obj)})) - uops (cond-> uops - (seq uops) - (d/preconj {:type :set-touched :touched (:touched old-obj)})) + change (cond-> {:type :mod-obj + :id id} - change (cond-> {:type :mod-obj - :id id} + (some? page-id) + (assoc :page-id page-id) - (some? page-id) - (assoc :page-id page-id) + (some? component-id) + (assoc :component-id component-id))] - (some? component-id) - (assoc :component-id component-id))] + (cond-> changes + (seq rops) + (update :redo-changes conj (assoc change :operations rops)) - (cond-> changes - (seq rops) - (update :redo-changes conj (assoc change :operations rops)) + (seq uops) + (update :undo-changes d/preconj (assoc change :operations uops)))))))] - (seq uops) - (update :undo-changes d/preconj (assoc change :operations uops)))))] - - (-> (reduce update-shape changes ids) - (apply-changes-local))))) + (-> (reduce update-shape changes ids) + (apply-changes-local))))) (defn remove-objects [changes ids] diff --git a/common/src/app/common/spec/color.cljc b/common/src/app/common/spec/color.cljc index 627094a99..8fbedb11b 100644 --- a/common/src/app/common/spec/color.cljc +++ b/common/src/app/common/spec/color.cljc @@ -6,8 +6,10 @@ (ns app.common.spec.color (:require - [app.common.spec :as us] - [clojure.spec.alpha :as s])) + [app.common.data :as d] + [app.common.spec :as us] + [app.common.text :as txt] + [clojure.spec.alpha :as s])) ;; TODO: waiting clojure 1.11 to rename this all :internal.stuff to a ;; more consistent name. @@ -46,7 +48,7 @@ :internal.gradient/width :internal.gradient/stops])) -;;; --- COLORS +;; --- COLORS (s/def :internal.color/name string?) (s/def :internal.color/path (s/nilable string?)) @@ -54,6 +56,15 @@ (s/def :internal.color/color (s/nilable string?)) (s/def :internal.color/opacity (s/nilable ::us/safe-number)) (s/def :internal.color/gradient (s/nilable ::gradient)) +(s/def :internal.color/ref-id uuid?) +(s/def :internal.color/ref-file uuid?) + +(s/def ::shape-color + (s/keys :req-un [:us/color + :internal.color/opacity] + :opt-un [:internal.color/gradient + :internal.color/ref-id + :internal.color/ref-file])) (s/def ::color (s/keys :opt-un [::id @@ -70,6 +81,197 @@ :internal.color/opacity :internal.color/gradient])) +;; --- Helpers for color in different parts of a shape +;; fill +(defn fill->shape-color + [fill] + (d/without-nils {:color (:fill-color fill) + :opacity (:fill-opacity fill) + :gradient (:fill-color-gradient fill) + :ref-id (:fill-color-ref-id fill) + :ref-file (:fill-color-ref-file fill)})) +(defn set-fill-color + [shape position color opacity gradient] + (update-in shape [:fills position] + (fn [fill] + (d/without-nils (assoc fill + :fill-color color + :fill-opacity opacity + :fill-color-gradient gradient))))) + +(defn detach-fill-color + [shape position] + (-> shape + (d/dissoc-in [:fills position :fill-color-ref-id]) + (d/dissoc-in [:fills position :fill-color-ref-file]))) + +;; stroke + +(defn stroke->shape-color + [stroke] + (d/without-nils {:color (:stroke-color stroke) + :opacity (:stroke-opacity stroke) + :gradient (:stroke-color-gradient stroke) + :ref-id (:stroke-color-ref-id stroke) + :ref-file (:stroke-color-ref-file stroke)})) + +(defn set-stroke-color + [shape position color opacity gradient] + (update-in shape [:strokes position] + (fn [stroke] + (d/without-nils (assoc stroke + :stroke-color color + :stroke-opacity opacity + :stroke-color-gradient gradient))))) + +(defn detach-stroke-color + [shape position] + (-> shape + (d/dissoc-in [:strokes position :stroke-color-ref-id]) + (d/dissoc-in [:strokes position :stroke-color-ref-file]))) + +;; shadow + +(defn shadow->shape-color + [shadow] + (d/without-nils {:color (-> shadow :color :color) + :opacity (-> shadow :color :opacity) + :gradient (-> shadow :color :gradient) + :ref-id (-> shadow :color :id) + :ref-file (-> shadow :color :file-id)})) + +(defn set-shadow-color + [shape position color opacity gradient] + (update-in shape [:shadow position :color] + (fn [shadow-color] + (d/without-nils (assoc shadow-color + :color color + :opacity opacity + :gradient gradient))))) + +(defn detach-shadow-color + [shape position] + (-> shape + (d/dissoc-in [:shadow position :color :id]) + (d/dissoc-in [:shadow position :color :file-id]))) + +;; grid + +(defn grid->shape-color + [grid] + (d/without-nils {:color (-> grid :params :color :color) + :opacity (-> grid :params :color :opacity) + :gradient (-> grid :params :color :gradient) + :ref-id (-> grid :params :color :id) + :ref-file (-> grid :params :color :file-id)})) + +(defn set-grid-color + [shape position color opacity gradient] + (update-in shape [:grids position :params :color] + (fn [grid-color] + (d/without-nils (assoc grid-color + :color color + :opacity opacity + :gradient gradient))))) + +(defn detach-grid-color + [shape position] + (-> shape + (d/dissoc-in [:grids position :params :color :id]) + (d/dissoc-in [:grids position :params :color :file-id]))) + +;; --- Helpers for all colors in a shape + +(defn get-text-node-colors + "Get all colors used by a node of a text shape" + [node] + (concat (map fill->shape-color (:fills node)) + (map stroke->shape-color (:strokes node)))) + +(defn get-all-colors + "Get all colors used by a shape, in any section." + [shape] + (concat (map fill->shape-color (:fills shape)) + (map stroke->shape-color (:strokes shape)) + (map shadow->shape-color (:shadow shape)) + (when (= (:type shape) :frame) + (map grid->shape-color (:grids shape))) + (when (= (:type shape) :text) + (reduce (fn [colors node] + (concat colors (get-text-node-colors node))) + () + (txt/node-seq (:content shape)))))) + +(defn uses-library-colors? + "Check if the shape uses any color in the given library." + [shape library-id] + (let [all-colors (get-all-colors shape)] + (some #(and (some? (:ref-id %)) + (= (:ref-file %) library-id)) + all-colors))) + +(defn sync-shape-colors + "Look for usage of any color of the given library inside the shape, + and, in this case, copy the library color into the shape." + [shape library-id library-colors] + (let [sync-color (fn [shape position shape-color set-fn detach-fn] + (if (= (:ref-file shape-color) library-id) + (let [library-color (get library-colors (:ref-id shape-color))] + (if (some? library-color) + (set-fn shape + position + (:color library-color) + (:opacity library-color) + (:gradient library-color)) + (detach-fn shape position))) + shape)) + + sync-fill (fn [shape [position fill]] + (sync-color shape + position + (fill->shape-color fill) + set-fill-color + detach-fill-color)) + + sync-stroke (fn [shape [position stroke]] + (sync-color shape + position + (stroke->shape-color stroke) + set-stroke-color + detach-stroke-color)) + + sync-shadow (fn [shape [position shadow]] + (sync-color shape + position + (shadow->shape-color shadow) + set-shadow-color + detach-shadow-color)) + + sync-grid (fn [shape [position grid]] + (sync-color shape + position + (grid->shape-color grid) + set-grid-color + detach-grid-color)) + + sync-text-node (fn [node] + (as-> node $ + (reduce sync-fill $ (d/enumerate (:fills $))) + (reduce sync-stroke $ (d/enumerate (:strokes $))))) + + sync-text (fn [shape] + (let [content (:content shape) + new-content (txt/transform-nodes sync-text-node content)] + (if (not= content new-content) + (assoc shape :content new-content) + shape)))] + + (as-> shape $ + (reduce sync-fill $ (d/enumerate (:fills $))) + (reduce sync-stroke $ (d/enumerate (:strokes $))) + (reduce sync-shadow $ (d/enumerate (:shadow $))) + (reduce sync-grid $ (d/enumerate (:grids $))) + (sync-text $)))) diff --git a/common/src/app/common/spec/shape.cljc b/common/src/app/common/spec/shape.cljc index e2732a0f4..42ae67f60 100644 --- a/common/src/app/common/spec/shape.cljc +++ b/common/src/app/common/spec/shape.cljc @@ -166,11 +166,11 @@ ::blocked ::collapsed ::fills - ::fill-color - ::fill-opacity - ::fill-color-gradient - ::fill-color-ref-file - ::fill-color-ref-id + ::fill-color ;; TODO: remove these attributes + ::fill-opacity ;; when backward compatibility + ::fill-color-gradient ;; is no longer needed + ::fill-color-ref-file ;; + ::fill-color-ref-id ;; ::hide-fill-on-export ::font-family ::font-size @@ -196,10 +196,10 @@ ::exports ::shapes ::strokes - ::stroke-color - ::stroke-color-ref-file - ::stroke-color-ref-id - ::stroke-opacity + ::stroke-color ;; TODO: same thing + ::stroke-color-ref-file ;; + ::stroke-color-ref-i ;; + ::stroke-opacity ;; ::stroke-style ::stroke-width ::stroke-alignment diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index a018111e9..700e87c0e 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -14,6 +14,7 @@ [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] [app.common.spec :as us] + [app.common.spec.color :as color] [app.common.text :as txt] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.groups :as dwg] @@ -24,18 +25,10 @@ ;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default (log/set-level! :warn) -(defonce color-sync-attrs - [[:fill-color-ref-id :fill-color-ref-file :color :fill-color] - [:fill-color-ref-id :fill-color-ref-file :gradient :fill-color-gradient] - [:fill-color-ref-id :fill-color-ref-file :opacity :fill-opacity] - - [:stroke-color-ref-id :stroke-color-ref-file :color :stroke-color] - [:stroke-color-ref-id :stroke-color-ref-file :gradient :stroke-color-gradient] - [:stroke-color-ref-id :stroke-color-ref-file :opacity :stroke-opacity]]) - (declare generate-sync-container) (declare generate-sync-shape) -(declare has-asset-reference-fn) +(declare generate-sync-text-shape) +(declare uses-assets?) (declare get-assets) (declare generate-sync-shape-direct) @@ -60,7 +53,7 @@ "" (str "<" (get-in state [:workspace-libraries file-id :name]) ">"))) -;; ---- Create a new component ---- +;; ---- Components and instances creation ---- (defn make-component-shape "Clone the shape and all children. Generate new ids and detach @@ -278,9 +271,8 @@ (log/debug :msg "Sync page in local file" :page-id (:id container)) (log/debug :msg "Sync component in local library" :component-id (:id container))) - (let [has-asset-reference? (has-asset-reference-fn asset-type library-id (cph/page? container)) - linked-shapes (->> (vals (:objects container)) - (filter has-asset-reference?))] + (let [linked-shapes (->> (vals (:objects container)) + (filter #(uses-assets? asset-type % library-id (cph/page? container))))] (loop [shapes (seq linked-shapes) changes (-> (pcb/empty-changes it) (pcb/with-container container) @@ -295,49 +287,34 @@ shape)) changes)))) -(defn- has-asset-reference-fn - "Gets a function that checks if a shape uses some asset of the given type - in the given library." - [asset-type library-id page?] - (case asset-type - :components - (fn [shape] (and (:component-id shape) - (or (:component-root? shape) (not page?)) - (= (:component-file shape) library-id))) +(defmulti uses-assets? + "Checks if a shape uses some asset of the given type in the given library." + (fn [asset-type _ _ _] asset-type)) - :colors - (fn [shape] - (if (= (:type shape) :text) - (->> shape - :content - ;; Check if any node in the content has a reference for the library - (txt/node-seq - #(or (and (some? (:stroke-color-ref-id %)) - (= library-id (:stroke-color-ref-file %))) - (and (some? (:fill-color-ref-id %)) - (= library-id (:fill-color-ref-file %)))))) - (some - #(let [attr (name %) - attr-ref-id (keyword (str attr "-ref-id")) - attr-ref-file (keyword (str attr "-ref-file"))] - (and (get shape attr-ref-id) - (= library-id (get shape attr-ref-file)))) - (map #(nth % 3) color-sync-attrs)))) +(defmethod uses-assets? :components + [_ shape library-id page?] + (and (some? (:component-id shape)) + (= (:component-file shape) library-id) + (or (:component-root? shape) (not page?)))) ; avoid nested components inside pages - :typographies - (fn [shape] - (and (= (:type shape) :text) - (->> shape - :content - ;; Check if any node in the content has a reference for the library - (txt/node-seq - #(and (some? (:typography-ref-id %)) - (= library-id (:typography-ref-file %))))))))) +(defmethod uses-assets? :colors + [_ shape library-id _] + (color/uses-library-colors? shape library-id)) + +(defmethod uses-assets? :typographies + [_ shape library-id _] + (and (= (:type shape) :text) + (->> shape + :content + ;; Check if any node in the content has a reference for the library + (txt/node-seq + #(and (some? (:typography-ref-id %)) + (= (:typography-ref-file %) library-id)))))) (defmulti generate-sync-shape - "Generate changes to synchronize one shape with all assets of the given type + "Generate changes to synchronize one shape from all assets of the given type that is using, in the given library." - (fn [type _changes _library-id _state _container _shape] type)) + (fn [asset-type _changes _library-id _state _container _shape] asset-type)) (defmethod generate-sync-shape :components [_ changes _library-id state container shape] @@ -345,6 +322,37 @@ libraries (wsh/get-libraries state)] (generate-sync-shape-direct changes libraries container shape-id false))) +(defmethod generate-sync-shape :colors + [_ changes library-id state _ shape] + (log/debug :msg "Sync colors of shape" :shape (:name shape)) + + ;; Synchronize a shape that uses some colors of the library. The value of the + ;; color in the library is copied to the shape. + (let [library-colors (get-assets library-id :colors state)] + (pcb/update-shapes changes + [(:id shape)] + #(color/sync-shape-colors % library-id library-colors)))) + +(defmethod generate-sync-shape :typographies + [_ changes library-id state container shape] + (log/debug :msg "Sync typographies of shape" :shape (:name shape)) + + ;; Synchronize a shape that uses some typographies of the library. The attributes + ;; of the typography are copied to the shape." + (let [typographies (get-assets library-id :typographies state) + update-node (fn [node] + (if-let [typography (get typographies (:typography-ref-id node))] + (merge node (dissoc typography :name :id)) + (dissoc node :typography-ref-id + :typography-ref-file)))] + (generate-sync-text-shape changes shape container update-node))) + +(defn- get-assets + [library-id asset-type state] + (if (= library-id (:current-file-id state)) + (get-in state [:workspace-data asset-type]) + (get-in state [:workspace-libraries library-id :data asset-type]))) + (defn- generate-sync-text-shape [changes shape container update-node] (let [old-content (:content shape) @@ -368,99 +376,6 @@ changes changes'))) -(defmethod generate-sync-shape :colors - [_ changes library-id state container shape] - (log/debug :msg "Sync colors of shape" :shape (:name shape)) - - ;; Synchronize a shape that uses some colors of the library. The value of the - ;; color in the library is copied to the shape. - (let [colors (get-assets library-id :colors state)] - (if (= :text (:type shape)) - (let [update-node (fn [node] - (if-let [color (get colors (:fill-color-ref-id node))] - (assoc node - :fill-color (:color color) - :fill-opacity (:opacity color) - :fill-color-gradient (:gradient color)) - (assoc node - :fill-color-ref-id nil - :fill-color-ref-file nil)))] - (generate-sync-text-shape changes shape container update-node)) - (loop [attrs (seq color-sync-attrs) - roperations [] - uoperations []] - (let [[attr-ref-id attr-ref-file color-attr attr] (first attrs)] - (if (nil? attr) - (if (empty? roperations) - changes - (-> changes - (update :redo-changes (make-change - container - {:type :mod-obj - :id (:id shape) - :operations roperations})) - (update :undo-changes (make-change - container - {:type :mod-obj - :id (:id shape) - :operations uoperations})))) - (if-not (contains? shape attr-ref-id) - (recur (next attrs) - roperations - uoperations) - (let [color (get colors (get shape attr-ref-id)) - roperations' (if color - [{:type :set - :attr attr - :val (color-attr color) - :ignore-touched true}] - ;; If the referenced color does no longer exist in the library, - ;; we must unlink the color in the shape - [{:type :set - :attr attr-ref-id - :val nil - :ignore-touched true} - {:type :set - :attr attr-ref-file - :val nil - :ignore-touched true}]) - uoperations' (if color - [{:type :set - :attr attr - :val (get shape attr) - :ignore-touched true}] - [{:type :set - :attr attr-ref-id - :val (get shape attr-ref-id) - :ignore-touched true} - {:type :set - :attr attr-ref-file - :val (get shape attr-ref-file) - :ignore-touched true}])] - (recur (next attrs) - (into roperations roperations') - (into uoperations uoperations')))))))))) - -(defmethod generate-sync-shape :typographies - [_ changes library-id state container shape] - (log/debug :msg "Sync typographies of shape" :shape (:name shape)) - - ;; Synchronize a shape that uses some typographies of the library. The attributes - ;; of the typography are copied to the shape." - (let [typographies (get-assets library-id :typographies state) - update-node (fn [node] - (if-let [typography (get typographies (:typography-ref-id node))] - (merge node (dissoc typography :name :id)) - (dissoc node :typography-ref-id - :typography-ref-file)))] - (generate-sync-text-shape changes shape container update-node))) - -(defn- get-assets - [library-id asset-type state] - (if (= library-id (:current-file-id state)) - (get-in state [:workspace-data asset-type]) - (get-in state [:workspace-libraries library-id :data asset-type]))) - ;; ---- Component synchronization helpers ----