0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-12 07:41:43 -05:00

🎉 Add :assing operation as altenative to :set

This commit is contained in:
Andrey Antukh 2024-08-19 12:21:26 +02:00
parent 9a3c953f0f
commit 2ec27de353
4 changed files with 150 additions and 122 deletions

View file

@ -12,6 +12,8 @@
[app.common.schema.desc-js-like :as smdj] [app.common.schema.desc-js-like :as smdj]
[app.common.schema.desc-native :as smdn] [app.common.schema.desc-native :as smdn]
[app.common.schema.generators :as sg] [app.common.schema.generators :as sg]
[malli.core :as m]
[malli.util :as mu]
[clojure.java.io :as io] [clojure.java.io :as io]
[clojure.pprint :refer [pprint print-table]] [clojure.pprint :refer [pprint print-table]]
[clojure.repl :refer :all] [clojure.repl :refer :all]

View file

@ -33,7 +33,18 @@
(def ^:private (def ^:private
schema:operation schema:operation
[:multi {:dispatch :type :title "Operation" ::smd/simplified true} [:multi {:dispatch :type
:title "Operation"
:decode/json #(update % :type keyword)
::smd/simplified true}
[:assign
[:map {:title "AssignOperation"}
[:type [:= :assign]]
;; NOTE: the full decoding is happening on the handler because it
;; needs a proper context of the current shape and its type
[:value [:map-of :keyword :any]]
[:ignore-touched {:optional true} :boolean]
[:ignore-geometry {:optional true} :boolean]]]
[:set [:set
[:map {:title "SetOperation"} [:map {:title "SetOperation"}
[:type [:= :set]] [:type [:= :set]]
@ -267,6 +278,16 @@
;; Page Transformation Changes ;; Page Transformation Changes
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:dynamic *touched-changes*
"A dynamic var that used for track changes that touch shapes on
first processing phase of changes. Should be set to a hash-set
instance and will contain changes that caused the touched
modification."
nil)
(defmulti process-change (fn [_ change] (:type change)))
(defmulti process-operation (fn [_ op] (:type op)))
;; Changes Processing Impl ;; Changes Processing Impl
(defn validate-shapes! (defn validate-shapes!
@ -295,8 +316,19 @@
nil)))) nil))))
(run! validate-shape!)))) (run! validate-shape!))))
(defmulti process-change (fn [_ change] (:type change))) (defn- process-touched-change
(defmulti process-operation (fn [_ _ op] (:type op))) [data {:keys [id page-id component-id]}]
(let [objects (if page-id
(-> data :pages-index (get page-id) :objects)
(-> data :components (get component-id) :objects))
shape (get objects id)
croot (ctn/get-component-shape objects shape {:allow-main? true})]
(if (and (some? croot) (ctk/main-instance? croot))
(ctkl/set-component-modified data (:component-id croot))
(if (some? component-id)
(ctkl/set-component-modified data component-id)
data))))
(defn process-changes (defn process-changes
([data items] ([data items]
@ -310,10 +342,15 @@
"expected valid changes" "expected valid changes"
(check-changes! items))) (check-changes! items)))
(let [result (reduce #(or (process-change %1 %2) %1) data items)] (binding [*touched-changes* (volatile! #{})]
(let [result (reduce #(or (process-change %1 %2) %1) data items)
result (reduce process-touched-change result @*touched-changes*)]
;; Validate result shapes (only on the backend) ;; Validate result shapes (only on the backend)
;;
;; TODO: (PERF) add changed shapes tracking and only validate
;; the tracked changes instead of iterate over all shapes
#?(:clj (validate-shapes! data result items)) #?(:clj (validate-shapes! data result items))
result))) result))))
(defmethod process-change :set-option (defmethod process-change :set-option
[data {:keys [page-id option value]}] [data {:keys [page-id option value]}]
@ -334,83 +371,51 @@
(d/update-in-when data [:pages-index page-id] update-container) (d/update-in-when data [:pages-index page-id] update-container)
(d/update-in-when data [:components component-id] update-container)))) (d/update-in-when data [:components component-id] update-container))))
(defn- process-operations
[objects {:keys [id operations] :as change}]
(if-let [shape (get objects id)]
(let [shape (reduce process-operation shape operations)
touched? (-> shape meta ::ctn/touched)]
;; NOTE: processing operation functions can assign
;; the ::ctn/touched metadata on shapes, in this case we
;; need to report them for to be used in the second
;; phase of changes procesing
(when touched? (some-> *touched-changes* (vswap! conj change)))
(assoc objects id shape))
objects))
(defmethod process-change :mod-obj (defmethod process-change :mod-obj
[data {:keys [id page-id component-id operations]}] [data {:keys [page-id component-id] :as change}]
(let [changed? (atom false)
process-and-register (partial process-operation
(fn [_shape] (reset! changed? true)))
update-fn (fn [objects]
(d/update-when objects id
#(reduce process-and-register % operations)))
check-modify-component (fn [data]
(if @changed?
;; When a shape is modified, if it belongs to a main component instance,
;; the component needs to be marked as modified.
(let [objects (if page-id
(-> data :pages-index (get page-id) :objects)
(-> data :components (get component-id) :objects))
shape (get objects id)
component-root (ctn/get-component-shape objects shape {:allow-main? true})]
(if (and (some? component-root) (ctk/main-instance? component-root))
(ctkl/set-component-modified data (:component-id component-root))
(if (some? component-id)
(ctkl/set-component-modified data component-id)
data)))
data))]
(as-> data $
(if page-id (if page-id
(d/update-in-when $ [:pages-index page-id :objects] update-fn) (d/update-in-when data [:pages-index page-id :objects] process-operations change)
(d/update-in-when $ [:components component-id :objects] update-fn)) (d/update-in-when data [:components component-id :objects] process-operations change)))
(check-modify-component $))))
(defmethod process-change :reorder-children (defn- process-children-reordering
[data {:keys [parent-id shapes page-id component-id]}] [objects {:keys [parent-id shapes] :as change}]
(let [changed? (atom false) (if-let [old-shapes (dm/get-in objects [parent-id :shapes])]
(let [id->idx
update-fn
(fn [objects]
(let [old-shapes (dm/get-in objects [parent-id :shapes])
id->idx
(update-vals (update-vals
(->> shapes (->> (d/enumerate shapes)
d/enumerate
(group-by second)) (group-by second))
(comp first first)) (comp first first))
new-shapes new-shapes
(into [] (sort-by #(d/nilv (id->idx %) -1) < old-shapes))] (vec (sort-by #(d/nilv (id->idx %) -1) < old-shapes))]
(reset! changed? (not= old-shapes new-shapes)) (if (not= old-shapes new-shapes)
(do
(some-> *touched-changes* (vswap! conj change))
(update objects parent-id assoc :shapes new-shapes))
objects))
(cond-> objects objects))
@changed?
(d/assoc-in-when [parent-id :shapes] new-shapes))))
check-modify-component (defmethod process-change :reorder-children
(fn [data] [data {:keys [page-id component-id] :as change}]
(if @changed?
;; When a shape is modified, if it belongs to a main component instance,
;; the component needs to be marked as modified.
(let [objects (if page-id
(-> data :pages-index (get page-id) :objects)
(-> data :components (get component-id) :objects))
shape (get objects parent-id)
component-root (ctn/get-component-shape objects shape {:allow-main? true})]
(if (and (some? component-root) (ctk/main-instance? component-root))
(ctkl/set-component-modified data (:component-id component-root))
data))
data))]
(as-> data $
(if page-id (if page-id
(d/update-in-when $ [:pages-index page-id :objects] update-fn) (d/update-in-when data [:pages-index page-id :objects] process-children-reordering change)
(d/update-in-when $ [:components component-id :objects] update-fn)) (d/update-in-when data [:components component-id :objects] process-children-reordering change)))
(check-modify-component $))))
(defmethod process-change :del-obj (defmethod process-change :del-obj
[data {:keys [page-id component-id id ignore-touched]}] [data {:keys [page-id component-id id ignore-touched]}]
@ -711,17 +716,33 @@
(ctyl/delete-typography data id)) (ctyl/delete-typography data id))
;; === Operations ;; === Operations
(def ^:private decode-shape
(sm/decoder cts/schema:shape sm/json-transformer))
(defmethod process-operation :assign
[{:keys [type] :as shape} {:keys [value] :as op}]
(let [modifications (assoc value :type type)
modifications (decode-shape modifications)]
(reduce-kv (fn [shape k v]
(process-operation shape {:type :set
:attr k
:val v
:ignore-touched (:ignore-touched op)
:ignore-geometry (:ignore-geometry op)}))
shape
modifications)))
(defmethod process-operation :set (defmethod process-operation :set
[on-changed shape op] [shape op]
(ctn/set-shape-attr shape (ctn/set-shape-attr shape
(:attr op) (:attr op)
(:val op) (:val op)
:on-changed on-changed
:ignore-touched (:ignore-touched op) :ignore-touched (:ignore-touched op)
:ignore-geometry (:ignore-geometry op))) :ignore-geometry (:ignore-geometry op)))
(defmethod process-operation :set-touched (defmethod process-operation :set-touched
[_ shape op] [shape op]
(let [touched (:touched op) (let [touched (:touched op)
in-copy? (ctk/in-component-copy? shape)] in-copy? (ctk/in-component-copy? shape)]
(if (or (not in-copy?) (nil? touched) (empty? touched)) (if (or (not in-copy?) (nil? touched) (empty? touched))
@ -729,7 +750,7 @@
(assoc shape :touched touched)))) (assoc shape :touched touched))))
(defmethod process-operation :set-remote-synced (defmethod process-operation :set-remote-synced
[_ shape op] [shape op]
(let [remote-synced (:remote-synced op) (let [remote-synced (:remote-synced op)
in-copy? (ctk/in-component-copy? shape)] in-copy? (ctk/in-component-copy? shape)]
(if (or (not in-copy?) (not remote-synced)) (if (or (not in-copy?) (not remote-synced))
@ -737,7 +758,7 @@
(assoc shape :remote-synced true)))) (assoc shape :remote-synced true))))
(defmethod process-operation :default (defmethod process-operation :default
[_ _ op] [_ op]
(ex/raise :type :not-implemented (ex/raise :type :not-implemented
:code :operation-not-implemented :code :operation-not-implemented
:context {:type (:type op)})) :context {:type (:type op)}))
@ -863,5 +884,3 @@
(defmethod frames-changed :default (defmethod frames-changed :default
[_ _] [_ _]
nil) nil)

View file

@ -540,38 +540,51 @@
;; --- SHAPE UPDATE ;; --- SHAPE UPDATE
(defn set-shape-attr (defn set-shape-attr
[shape attr val & {:keys [on-changed ignore-touched ignore-geometry]}] "Assign attribute to shape with touched logic.
The returned shape will contain a metadata associated with it
indicating if shape is touched or not."
[shape attr val & {:keys [ignore-touched ignore-geometry]}]
(let [group (get ctk/sync-attrs attr) (let [group (get ctk/sync-attrs attr)
shape-val (get shape attr) shape-val (get shape attr)
ignore (or ignore-touched (= attr :position-data)) ;; position-data is a derived attribute and
is-geometry? (and (or (= group :geometry-group) ;; never triggers touched by itself ignore?
(and (= group :content-group) (= (:type shape) :path))) (or ignore-touched
(not (#{:width :height} attr))) ;; :content in paths are also considered geometric ;; position-data is a derived attribute
;; TODO: the check of :width and :height probably may be removed (= attr :position-data))
;; after the check added in data/workspace/modifiers/check-delta
;; function. Better check it and test toroughly when activating is-geometry?
;; components-v2 mode. (and (or (= group :geometry-group) ;; never triggers touched by itself
in-copy? (ctk/in-component-copy? shape) (and (= group :content-group)
(= (:type shape) :path)))
;; :content in paths are also considered geometric
(not (#{:width :height} attr)))
;; TODO: the check of :width and :height probably may be
;; removed after the check added in
;; data/workspace/modifiers/check-delta function. Better check
;; it and test toroughly when activating components-v2 mode.
in-copy?
(ctk/in-component-copy? shape)
;; For geometric attributes, there are cases in that the value changes ;; For geometric attributes, there are cases in that the value changes
;; slightly (e.g. when rounding to pixel, or when recalculating text ;; slightly (e.g. when rounding to pixel, or when recalculating text
;; positions in different zoom levels). To take this into account, we ;; positions in different zoom levels). To take this into account, we
;; ignore geometric changes smaller than 1 pixel. ;; ignore geometric changes smaller than 1 pixel.
equal? (if is-geometry? equal?
(if is-geometry?
(gsh/close-attrs? attr val shape-val 1) (gsh/close-attrs? attr val shape-val 1)
(gsh/close-attrs? attr val shape-val))] (gsh/close-attrs? attr val shape-val))
;; Notify when value has changed, except when it has not moved relative to the touched?
;; component head. (and group (not equal?) (not (and ignore-geometry is-geometry?)))]
(when (and on-changed group (not equal?) (not (and ignore-geometry is-geometry?)))
(on-changed shape))
(cond-> shape (cond-> shape
;; Depending on the origin of the attribute change, we need or not to ;; Depending on the origin of the attribute change, we need or not to
;; set the "touched" flag for the group the attribute belongs to. ;; set the "touched" flag for the group the attribute belongs to.
;; In some cases we need to ignore touched only if the attribute is ;; In some cases we need to ignore touched only if the attribute is
;; geometric (position, width or transformation). ;; geometric (position, width or transformation).
(and in-copy? group (not ignore) (not equal?) (and in-copy? group (not ignore?) (not equal?)
(not (and ignore-geometry is-geometry?))) (not (and ignore-geometry is-geometry?)))
(-> (update :touched ctk/set-touched-group group) (-> (update :touched ctk/set-touched-group group)
(dissoc :remote-synced)) (dissoc :remote-synced))
@ -580,4 +593,7 @@
(dissoc attr) (dissoc attr)
(some? val) (some? val)
(assoc attr val)))) (assoc attr val)
:always
(vary-meta assoc ::touched touched?))))

View file

@ -125,7 +125,7 @@
(sm/register! ::stroke schema:stroke) (sm/register! ::stroke schema:stroke)
(def ^:private schema:shape-base-attrs (def schema:shape-base-attrs
[:map {:title "ShapeMinimalRecord"} [:map {:title "ShapeMinimalRecord"}
[:id ::sm/uuid] [:id ::sm/uuid]
[:name :string] [:name :string]
@ -137,13 +137,14 @@
[:parent-id ::sm/uuid] [:parent-id ::sm/uuid]
[:frame-id ::sm/uuid]]) [:frame-id ::sm/uuid]])
(def ^:private schema:shape-geom-attrs (def schema:shape-geom-attrs
[:map {:title "ShapeGeometryAttrs"} [:map {:title "ShapeGeometryAttrs"}
[:x ::sm/safe-number] [:x ::sm/safe-number]
[:y ::sm/safe-number] [:y ::sm/safe-number]
[:width ::sm/safe-number] [:width ::sm/safe-number]
[:height ::sm/safe-number]]) [:height ::sm/safe-number]])
;; FIXME: rename to shape-generic-attrs
(def schema:shape-attrs (def schema:shape-attrs
[:map {:title "ShapeAttrs"} [:map {:title "ShapeAttrs"}
[:component-id {:optional true} ::sm/uuid] [:component-id {:optional true} ::sm/uuid]
@ -159,7 +160,6 @@
[:masked-group {:optional true} :boolean] [:masked-group {:optional true} :boolean]
[:fills {:optional true} [:fills {:optional true}
[:vector {:gen/max 2} schema:fill]] [:vector {:gen/max 2} schema:fill]]
[:hide-fill-on-export {:optional true} :boolean]
[:proportion {:optional true} ::sm/safe-number] [:proportion {:optional true} ::sm/safe-number]
[:proportion-lock {:optional true} :boolean] [:proportion-lock {:optional true} :boolean]
[:constraints-h {:optional true} [:constraints-h {:optional true}
@ -193,12 +193,10 @@
(def schema:group-attrs (def schema:group-attrs
[:map {:title "GroupAttrs"} [:map {:title "GroupAttrs"}
[:type [:= :group]]
[:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]]) [:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]])
(def ^:private schema:frame-attrs (def ^:private schema:frame-attrs
[:map {:title "FrameAttrs"} [:map {:title "FrameAttrs"}
[:type [:= :frame]]
[:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]] [:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]
[:hide-fill-on-export {:optional true} :boolean] [:hide-fill-on-export {:optional true} :boolean]
[:show-content {:optional true} :boolean] [:show-content {:optional true} :boolean]
@ -206,26 +204,21 @@
(def ^:private schema:bool-attrs (def ^:private schema:bool-attrs
[:map {:title "BoolAttrs"} [:map {:title "BoolAttrs"}
[:type [:= :bool]]
[:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]] [:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]
[:bool-type [::sm/one-of bool-types]] [:bool-type [::sm/one-of bool-types]]
[:bool-content ::ctsp/content]]) [:bool-content ::ctsp/content]])
(def ^:private schema:rect-attrs (def ^:private schema:rect-attrs
[:map {:title "RectAttrs"} [:map {:title "RectAttrs"}])
[:type [:= :rect]]])
(def ^:private schema:circle-attrs (def ^:private schema:circle-attrs
[:map {:title "CircleAttrs"} [:map {:title "CircleAttrs"}])
[:type [:= :circle]]])
(def ^:private schema:svg-raw-attrs (def ^:private schema:svg-raw-attrs
[:map {:title "SvgRawAttrs"} [:map {:title "SvgRawAttrs"}])
[:type [:= :svg-raw]]])
(def schema:image-attrs (def schema:image-attrs
[:map {:title "ImageAttrs"} [:map {:title "ImageAttrs"}
[:type [:= :image]]
[:metadata [:metadata
[:map [:map
[:width {:gen/gen (sg/small-int :min 1)} :int] [:width {:gen/gen (sg/small-int :min 1)} :int]
@ -238,12 +231,10 @@
(def ^:private schema:path-attrs (def ^:private schema:path-attrs
[:map {:title "PathAttrs"} [:map {:title "PathAttrs"}
[:type [:= :path]]
[:content ::ctsp/content]]) [:content ::ctsp/content]])
(def ^:private schema:text-attrs (def ^:private schema:text-attrs
[:map {:title "TextAttrs"} [:map {:title "TextAttrs"}
[:type [:= :text]]
[:content {:optional true} [:maybe ::ctsx/content]]]) [:content {:optional true} [:maybe ::ctsx/content]]])
(defn- decode-shape (defn- decode-shape