0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-11 07:11:32 -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-native :as smdn]
[app.common.schema.generators :as sg]
[malli.core :as m]
[malli.util :as mu]
[clojure.java.io :as io]
[clojure.pprint :refer [pprint print-table]]
[clojure.repl :refer :all]

View file

@ -33,7 +33,18 @@
(def ^:private
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
[:map {:title "SetOperation"}
[:type [:= :set]]
@ -267,6 +278,16 @@
;; 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
(defn validate-shapes!
@ -295,8 +316,19 @@
nil))))
(run! validate-shape!))))
(defmulti process-change (fn [_ change] (:type change)))
(defmulti process-operation (fn [_ _ op] (:type op)))
(defn- process-touched-change
[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
([data items]
@ -310,10 +342,15 @@
"expected valid changes"
(check-changes! items)))
(let [result (reduce #(or (process-change %1 %2) %1) data items)]
;; Validate result shapes (only on the backend)
#?(:clj (validate-shapes! data result items))
result)))
(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)
;;
;; TODO: (PERF) add changed shapes tracking and only validate
;; the tracked changes instead of iterate over all shapes
#?(:clj (validate-shapes! data result items))
result))))
(defmethod process-change :set-option
[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 [: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
[data {:keys [id page-id component-id operations]}]
(let [changed? (atom false)
[data {:keys [page-id component-id] :as change}]
(if page-id
(d/update-in-when data [:pages-index page-id :objects] process-operations change)
(d/update-in-when data [:components component-id :objects] process-operations change)))
process-and-register (partial process-operation
(fn [_shape] (reset! changed? true)))
(defn- process-children-reordering
[objects {:keys [parent-id shapes] :as change}]
(if-let [old-shapes (dm/get-in objects [parent-id :shapes])]
(let [id->idx
(update-vals
(->> (d/enumerate shapes)
(group-by second))
(comp first first))
update-fn (fn [objects]
(d/update-when objects id
#(reduce process-and-register % operations)))
new-shapes
(vec (sort-by #(d/nilv (id->idx %) -1) < old-shapes))]
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))]
(if (not= old-shapes new-shapes)
(do
(some-> *touched-changes* (vswap! conj change))
(update objects parent-id assoc :shapes new-shapes))
objects))
(as-> data $
(if page-id
(d/update-in-when $ [:pages-index page-id :objects] update-fn)
(d/update-in-when $ [:components component-id :objects] update-fn))
(check-modify-component $))))
objects))
(defmethod process-change :reorder-children
[data {:keys [parent-id shapes page-id component-id]}]
(let [changed? (atom false)
update-fn
(fn [objects]
(let [old-shapes (dm/get-in objects [parent-id :shapes])
id->idx
(update-vals
(->> shapes
d/enumerate
(group-by second))
(comp first first))
new-shapes
(into [] (sort-by #(d/nilv (id->idx %) -1) < old-shapes))]
(reset! changed? (not= old-shapes new-shapes))
(cond-> objects
@changed?
(d/assoc-in-when [parent-id :shapes] new-shapes))))
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 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
(d/update-in-when $ [:pages-index page-id :objects] update-fn)
(d/update-in-when $ [:components component-id :objects] update-fn))
(check-modify-component $))))
[data {:keys [page-id component-id] :as change}]
(if page-id
(d/update-in-when data [:pages-index page-id :objects] process-children-reordering change)
(d/update-in-when data [:components component-id :objects] process-children-reordering change)))
(defmethod process-change :del-obj
[data {:keys [page-id component-id id ignore-touched]}]
@ -711,33 +716,49 @@
(ctyl/delete-typography data id))
;; === 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
[on-changed shape op]
[shape op]
(ctn/set-shape-attr shape
(:attr op)
(:val op)
:on-changed on-changed
:ignore-touched (:ignore-touched op)
:ignore-geometry (:ignore-geometry op)))
(defmethod process-operation :set-touched
[_ shape op]
(let [touched (:touched op)
[shape op]
(let [touched (:touched op)
in-copy? (ctk/in-component-copy? shape)]
(if (or (not in-copy?) (nil? touched) (empty? touched))
(dissoc shape :touched)
(assoc shape :touched touched))))
(defmethod process-operation :set-remote-synced
[_ shape op]
[shape 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))
(dissoc shape :remote-synced)
(assoc shape :remote-synced true))))
(defmethod process-operation :default
[_ _ op]
[_ op]
(ex/raise :type :not-implemented
:code :operation-not-implemented
:context {:type (:type op)}))
@ -863,5 +884,3 @@
(defmethod frames-changed :default
[_ _]
nil)

View file

@ -540,38 +540,51 @@
;; --- SHAPE UPDATE
(defn set-shape-attr
[shape attr val & {:keys [on-changed ignore-touched ignore-geometry]}]
(let [group (get ctk/sync-attrs 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
(and (= group :content-group) (= (:type shape) :path)))
(not (#{:width :height} attr))) ;; :content in paths are also considered geometric
;; 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)
"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)
shape-val (get shape attr)
ignore?
(or ignore-touched
;; position-data is a derived attribute
(= attr :position-data))
is-geometry?
(and (or (= group :geometry-group) ;; never triggers touched by itself
(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
;; slightly (e.g. when rounding to pixel, or when recalculating text
;; positions in different zoom levels). To take this into account, we
;; ignore geometric changes smaller than 1 pixel.
equal? (if is-geometry?
(gsh/close-attrs? attr val shape-val 1)
(gsh/close-attrs? attr val shape-val))]
equal?
(if is-geometry?
(gsh/close-attrs? attr val shape-val 1)
(gsh/close-attrs? attr val shape-val))
;; Notify when value has changed, except when it has not moved relative to the
;; component head.
(when (and on-changed group (not equal?) (not (and ignore-geometry is-geometry?)))
(on-changed shape))
touched?
(and group (not equal?) (not (and ignore-geometry is-geometry?)))]
(cond-> shape
;; Depending on the origin of the attribute change, we need or not 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
;; 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?)))
(-> (update :touched ctk/set-touched-group group)
(dissoc :remote-synced))
@ -580,4 +593,7 @@
(dissoc attr)
(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)
(def ^:private schema:shape-base-attrs
(def schema:shape-base-attrs
[:map {:title "ShapeMinimalRecord"}
[:id ::sm/uuid]
[:name :string]
@ -137,13 +137,14 @@
[:parent-id ::sm/uuid]
[:frame-id ::sm/uuid]])
(def ^:private schema:shape-geom-attrs
(def schema:shape-geom-attrs
[:map {:title "ShapeGeometryAttrs"}
[:x ::sm/safe-number]
[:y ::sm/safe-number]
[:width ::sm/safe-number]
[:height ::sm/safe-number]])
;; FIXME: rename to shape-generic-attrs
(def schema:shape-attrs
[:map {:title "ShapeAttrs"}
[:component-id {:optional true} ::sm/uuid]
@ -159,7 +160,6 @@
[:masked-group {:optional true} :boolean]
[:fills {:optional true}
[:vector {:gen/max 2} schema:fill]]
[:hide-fill-on-export {:optional true} :boolean]
[:proportion {:optional true} ::sm/safe-number]
[:proportion-lock {:optional true} :boolean]
[:constraints-h {:optional true}
@ -193,12 +193,10 @@
(def schema:group-attrs
[:map {:title "GroupAttrs"}
[:type [:= :group]]
[:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]])
(def ^:private schema:frame-attrs
[:map {:title "FrameAttrs"}
[:type [:= :frame]]
[:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]
[:hide-fill-on-export {:optional true} :boolean]
[:show-content {:optional true} :boolean]
@ -206,26 +204,21 @@
(def ^:private schema:bool-attrs
[:map {:title "BoolAttrs"}
[:type [:= :bool]]
[:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]
[:bool-type [::sm/one-of bool-types]]
[:bool-content ::ctsp/content]])
(def ^:private schema:rect-attrs
[:map {:title "RectAttrs"}
[:type [:= :rect]]])
[:map {:title "RectAttrs"}])
(def ^:private schema:circle-attrs
[:map {:title "CircleAttrs"}
[:type [:= :circle]]])
[:map {:title "CircleAttrs"}])
(def ^:private schema:svg-raw-attrs
[:map {:title "SvgRawAttrs"}
[:type [:= :svg-raw]]])
[:map {:title "SvgRawAttrs"}])
(def schema:image-attrs
[:map {:title "ImageAttrs"}
[:type [:= :image]]
[:metadata
[:map
[:width {:gen/gen (sg/small-int :min 1)} :int]
@ -238,12 +231,10 @@
(def ^:private schema:path-attrs
[:map {:title "PathAttrs"}
[:type [:= :path]]
[:content ::ctsp/content]])
(def ^:private schema:text-attrs
[:map {:title "TextAttrs"}
[:type [:= :text]]
[:content {:optional true} [:maybe ::ctsx/content]]])
(defn- decode-shape