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:
parent
9a3c953f0f
commit
2ec27de353
4 changed files with 150 additions and 122 deletions
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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?))))
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue