Merge pull request #354 from uxbox/task/732/update-subcomponent

Task/732/update subcomponent
This commit is contained in:
Andrey Antukh 2020-10-15 11:20:40 +02:00 committed by GitHub
commit 3e14393c97
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 504 additions and 324 deletions

View file

@ -46,6 +46,7 @@
(<= % max-safe-int))) (<= % max-safe-int)))
(s/def ::component-id uuid?) (s/def ::component-id uuid?)
(s/def ::component-file uuid?) (s/def ::component-file uuid?)
(s/def ::component-root? boolean?)
(s/def ::shape-ref uuid?) (s/def ::shape-ref uuid?)
(s/def ::safe-number (s/def ::safe-number
@ -122,7 +123,6 @@
(s/def :internal.shape/line-height ::safe-number) (s/def :internal.shape/line-height ::safe-number)
(s/def :internal.shape/locked boolean?) (s/def :internal.shape/locked boolean?)
(s/def :internal.shape/page-id uuid?) (s/def :internal.shape/page-id uuid?)
(s/def :internal.shape/component-id uuid?)
(s/def :internal.shape/proportion ::safe-number) (s/def :internal.shape/proportion ::safe-number)
(s/def :internal.shape/proportion-lock boolean?) (s/def :internal.shape/proportion-lock boolean?)
(s/def :internal.shape/rx ::safe-number) (s/def :internal.shape/rx ::safe-number)
@ -236,12 +236,8 @@
:width :size-group :width :size-group
:height :size-group :height :size-group
:proportion :size-group :proportion :size-group
:x :position-group
:y :position-group
:rx :radius-group :rx :radius-group
:ry :radius-group :ry :radius-group})
:points :points-group
:transform :transform-group})
(def color-sync-attrs [:fill-color (def color-sync-attrs [:fill-color
:stroke-color]) :stroke-color])
@ -255,6 +251,7 @@
(s/keys :opt-un [::id (s/keys :opt-un [::id
::component-id ::component-id
::component-file ::component-file
::component-root?
::shape-ref]))) ::shape-ref])))
(s/def :internal.page/objects (s/map-of uuid? ::shape)) (s/def :internal.page/objects (s/map-of uuid? ::shape))
@ -891,21 +888,21 @@
(defmethod process-operation :set (defmethod process-operation :set
[shape op] [shape op]
(let [attr (:attr op) (let [attr (:attr op)
val (:val op) val (:val op)
ignore (:ignore-touched op) ignore (:ignore-touched op)
shape-ref (:shape-ref shape) shape-ref (:shape-ref shape)
group (get component-sync-attrs attr)] group (get component-sync-attrs attr)]
(cond-> shape (cond-> shape
(and shape-ref group (not ignore) (not= val (get shape attr)))
(update :touched #(conj (or % #{}) group))
(nil? val) (nil? val)
(dissoc attr) (dissoc attr)
(some? val) (some? val)
(assoc attr val) (assoc attr val))))
(and shape-ref group (not ignore))
(update :touched #(conj (or % #{}) group)))))
(defmethod process-operation :set-touched (defmethod process-operation :set-touched
[shape op] [shape op]

View file

@ -31,15 +31,15 @@
(update page :objects (update page :objects
#(into % (d/index-by :id objects-list)))) #(into % (d/index-by :id objects-list))))
(defn get-root-component (defn get-root-shape
"Get the root shape linked to the component for this shape, if any" "Get the root shape linked to a component for this shape, if any"
[id objects] [shape objects]
(let [obj (get objects id)] (if (:component-root? shape)
(if-let [component-id (:component-id obj)] shape
id (if-let [parent-id (:parent-id shape)]
(if-let [parent-id (:parent-id obj)] (get-root-shape (get objects (:parent-id shape))
(get-root-component parent-id objects) objects)
nil)))) nil)))
(defn get-children (defn get-children
"Retrieve all children ids recursively for a given object" "Retrieve all children ids recursively for a given object"
@ -58,7 +58,7 @@
(defn get-object-with-children (defn get-object-with-children
"Retrieve a list with an object and all of its children" "Retrieve a list with an object and all of its children"
[id objects] [id objects]
(map #(get objects %) (concat [id] (get-children id objects)))) (map #(get objects %) (cons id (get-children id objects))))
(defn is-shape-grouped (defn is-shape-grouped
"Checks if a shape is inside a group" "Checks if a shape is inside a group"

View file

@ -1148,12 +1148,9 @@
(update [_ state] (update [_ state]
(let [page-id (:current-page-id state) (let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id) objects (dwc/lookup-page-objects state page-id)
root-id (cph/get-root-component (:id shape) objects)
root-shape (get objects root-id)
mdata {:position position mdata {:position position
:shape shape :shape shape
:root-shape root-shape
:selected (get-in state [:workspace-local :selected])}] :selected (get-in state [:workspace-local :selected])}]
(-> state (-> state
(assoc-in [:workspace-local :context-menu] mdata)))) (assoc-in [:workspace-local :context-menu] mdata))))

View file

@ -155,9 +155,15 @@
{:type :set {:type :set
:attr :component-file :attr :component-file
:val nil} :val nil}
{:type :set
:attr :component-root?
:val (:component-root? updated-shape)}
{:type :set {:type :set
:attr :shape-ref :attr :shape-ref
:val (:shape-ref updated-shape)}]}) :val (:shape-ref updated-shape)}
{:type :set
:attr :touched
:val (:touched updated-shape)}]})
updated-shapes)) updated-shapes))
uchanges (conj uchanges uchanges (conj uchanges
@ -176,11 +182,18 @@
{:type :set {:type :set
:attr :component-file :attr :component-file
:val (:component-file original-shape)} :val (:component-file original-shape)}
{:type :set
:attr :component-root?
:val (:component-root? original-shape)}
{:type :set {:type :set
:attr :shape-ref :attr :shape-ref
:val (:shape-ref original-shape)}]})) :val (:shape-ref original-shape)}
{:type :set
:attr :touched
:val (:touched original-shape)}]}))
updated-shapes))] updated-shapes))]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dws/select-shapes (d/ordered-set (:id group)))))))))) (dws/select-shapes (d/ordered-set (:id group))))))))))
@ -239,10 +252,12 @@
(dwc/calculate-frame-overlap all-frames $)) (dwc/calculate-frame-overlap all-frames $))
(assoc $ :parent-id (assoc $ :parent-id
(or (:parent-id $) (:frame-id $))) (or (:parent-id $) (:frame-id $)))
(assoc $ :shape-ref (:id original-shape))) (assoc $ :shape-ref (:id original-shape))
(dissoc $ :touched))
(nil? (:parent-id original-shape)) (nil? (:parent-id original-shape))
(assoc :component-id (:id original-shape)) (assoc :component-id (:id original-shape)
:component-root? true)
(and (nil? (:parent-id original-shape)) (some? file-id)) (and (nil? (:parent-id original-shape)) (some? file-id))
(assoc :component-file file-id) (assoc :component-file file-id)
@ -251,7 +266,7 @@
(dissoc :component-file) (dissoc :component-file)
(some? (:parent-id original-shape)) (some? (:parent-id original-shape))
(dissoc :component-id :component-file)))) (dissoc :component-root?))))
[new-shape new-shapes _] [new-shape new-shapes _]
(cph/clone-object component-shape (cph/clone-object component-shape
@ -285,9 +300,7 @@
(watch [_ state stream] (watch [_ state stream]
(let [page-id (:current-page-id state) (let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id) objects (dwc/lookup-page-objects state page-id)
root-id (cph/get-root-component id objects) shapes (cph/get-object-with-children id objects)
shapes (cph/get-object-with-children root-id objects)
rchanges (map (fn [obj] rchanges (map (fn [obj]
{:type :mod-obj {:type :mod-obj
@ -351,26 +364,38 @@
(ptk/reify ::reset-component (ptk/reify ::reset-component
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [page-id (:current-page-id state) ;; ===== Uncomment this to debug =====
page (get-in state [:workspace-data :pages-index page-id]) ;; (js/console.info "##### RESET-COMPONENT of shape" (str id))
objects (dwc/lookup-page-objects state page-id) (let [page-id (:current-page-id state)
root-id (cph/get-root-component id objects) page (get-in state [:workspace-data :pages-index page-id])
root-shape (get objects id) objects (dwc/lookup-page-objects state page-id)
file-id (get root-shape :component-file) shape (get objects id)
file-id (get shape :component-file)
components [all-shapes component root-component]
(if (nil? file-id) (dwlh/resolve-shapes-and-components shape
(get-in state [:workspace-data :components]) objects
(get-in state [:workspace-libraries file-id :data :components])) state
true)
;; ===== Uncomment this to debug =====
;; _ (js/console.info "shape" (:name shape) "<- component" (:name component))
;; _ (js/console.debug "all-shapes" (clj->js all-shapes))
;; _ (js/console.debug "component" (clj->js component))
;; _ (js/console.debug "root-component" (clj->js root-component))
[rchanges uchanges] [rchanges uchanges]
(dwlh/generate-sync-shape-and-children-components root-shape (dwlh/generate-sync-shape-and-children-components shape
objects all-shapes
components component
root-component
(:id page) (:id page)
nil nil
true)] true)]
;; ===== Uncomment this to debug =====
;; (js/console.debug "rchanges" (clj->js rchanges))
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
(defn update-component (defn update-component
@ -379,60 +404,34 @@
(ptk/reify ::update-component (ptk/reify ::update-component
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [page-id (:current-page-id state) ;; ===== Uncomment this to debug =====
objects (dwc/lookup-page-objects state page-id) ;; (js/console.info "##### UPDATE-COMPONENT of shape" (str id))
root-id (cph/get-root-component id objects) (let [page-id (:current-page-id state)
root-shape (get objects id) objects (dwc/lookup-page-objects state page-id)
shape (get objects id)
file-id (get shape :component-file)
component-id (get root-shape :component-id) [all-shapes component root-component]
component-objs (dwc/lookup-component-objects state component-id) (dwlh/resolve-shapes-and-components shape
component-obj (get component-objs component-id) objects
state
true)
;; Clone again the original shape and its children, maintaing ;; ===== Uncomment this to debug =====
;; the ids of the cloned shapes. If the original shape has some ;; _ (js/console.info "shape" (:name shape) "-> component" (:name component))
;; new child shapes, the cloned ones will have new generated ids. ;; _ (js/console.debug "all-shapes" (clj->js all-shapes))
update-new-shape (fn [new-shape original-shape] ;; _ (js/console.debug "component" (clj->js component))
(cond-> new-shape ;; _ (js/console.debug "root-component" (clj->js root-component))
true
(assoc :frame-id nil)
(= (:component-id original-shape) component-id) [rchanges uchanges]
(dissoc :component-id) (dwlh/generate-sync-shape-inverse shape
all-shapes
component
root-component
page-id)]
(some? (:shape-ref original-shape)) ;; ===== Uncomment this to debug =====
(assoc :id (:shape-ref original-shape)))) ;; (js/console.debug "rchanges" (clj->js rchanges))
touch-shape (fn [original-shape _]
(into {} original-shape))
[new-shape new-shapes original-shapes]
(cph/clone-object root-shape nil objects update-new-shape touch-shape)
rchanges (d/concat
[{:type :mod-component
:id component-id
:name (:name new-shape)
:shapes new-shapes}]
(map (fn [shape]
{:type :mod-obj
:page-id page-id
:id (:id shape)
:operations [{:type :set-touched
:touched nil}]})
original-shapes))
uchanges (d/concat
[{:type :mod-component
:id component-id
:name (:name component-obj)
:shapes (vals component-objs)}]
(map (fn [shape]
{:type :mod-obj
:page-id page-id
:id (:id shape)
:operations [{:type :set-touched
:touched (:touched shape)}]})
original-shapes))]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
@ -450,6 +449,8 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
;; ===== Uncomment this to debug =====
;; (js/console.info "##### SYNC-FILE" (str (or file-id "local")))
(let [[rchanges1 uchanges1] (dwlh/generate-sync-file :components file-id state) (let [[rchanges1 uchanges1] (dwlh/generate-sync-file :components file-id state)
[rchanges2 uchanges2] (dwlh/generate-sync-library :components file-id state) [rchanges2 uchanges2] (dwlh/generate-sync-library :components file-id state)
[rchanges3 uchanges3] (dwlh/generate-sync-file :colors file-id state) [rchanges3 uchanges3] (dwlh/generate-sync-file :colors file-id state)
@ -458,6 +459,8 @@
[rchanges6 uchanges6] (dwlh/generate-sync-library :typographies file-id state) [rchanges6 uchanges6] (dwlh/generate-sync-library :typographies file-id state)
rchanges (d/concat rchanges1 rchanges2 rchanges3 rchanges4 rchanges5 rchanges6) rchanges (d/concat rchanges1 rchanges2 rchanges3 rchanges4 rchanges5 rchanges6)
uchanges (d/concat uchanges1 uchanges2 uchanges3 uchanges4 uchanges5 uchanges6)] uchanges (d/concat uchanges1 uchanges2 uchanges3 uchanges4 uchanges5 uchanges6)]
;; ===== Uncomment this to debug =====
;; (js/console.debug "rchanges" (clj->js rchanges))
(rx/concat (rx/concat
(rx/of (dm/hide-tag :sync-dialog)) (rx/of (dm/hide-tag :sync-dialog))
(when rchanges (when rchanges
@ -483,8 +486,15 @@
(ptk/reify ::sync-file-2nd-stage (ptk/reify ::sync-file-2nd-stage
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [[rchanges uchanges] (dwlh/generate-sync-file :components nil state)] ;; ===== Uncomment this to debug =====
;; (js/console.info "##### SYNC-FILE" (str (or file-id "local")) "(2nd stage)")
(let [[rchanges1 uchanges1] (dwlh/generate-sync-file :components nil state)
[rchanges2 uchanges2] (dwlh/generate-sync-library :components file-id state)
rchanges (d/concat rchanges1 rchanges2)
uchanges (d/concat uchanges1 uchanges2)]
(when rchanges (when rchanges
;; ===== Uncomment this to debug =====
;; (js/console.debug "rchanges" (clj->js rchanges))
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))) (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))))
(def ignore-sync (def ignore-sync

View file

@ -21,15 +21,62 @@
(declare generate-sync-container) (declare generate-sync-container)
(declare generate-sync-shape) (declare generate-sync-shape)
(declare has-asset-reference-fn)
(declare generate-sync-component-components) (declare get-assets)
(declare resolve-shapes-and-components)
(declare generate-sync-shape-and-children-components) (declare generate-sync-shape-and-children-components)
(declare generate-sync-shape-components) (declare generate-sync-shape-inverse)
(declare generate-sync-shape<-component)
(declare generate-sync-shape->component)
(declare remove-component-and-ref) (declare remove-component-and-ref)
(declare remove-ref) (declare remove-ref)
(declare reset-touched)
(declare update-attrs) (declare update-attrs)
(declare calc-new-pos) (declare calc-new-pos)
;; ---- Create a new component ----
(defn make-component-shape
"Clone the shape and all children. Generate new ids and detach
from parent and frame. Update the original shapes to have links
to the new ones."
[shape objects]
(assert (nil? (:component-id shape)))
(assert (nil? (:component-file shape)))
(assert (nil? (:shape-ref shape)))
(let [update-new-shape (fn [new-shape original-shape]
(cond-> new-shape
true
(assoc :frame-id nil)
(nil? (:parent-id new-shape))
(dissoc :component-id
:component-file
:component-root?
:shape-ref)))
;; Make the original shape an instance of the new component.
;; If one of the original shape children already was a component
;; instance, the 'instanceness' is copied into the new component.
update-original-shape (fn [original-shape new-shape]
(cond-> original-shape
true
(-> (assoc :shape-ref (:id new-shape))
(dissoc :touched))
(nil? (:parent-id new-shape))
(assoc :component-id (:id new-shape)
:component-file nil
:component-root? true)
(some? (:parent-id new-shape))
(dissoc :component-root?)))]
(cph/clone-object shape nil objects update-new-shape update-original-shape)))
;; ---- General library synchronization functions ---- ;; ---- General library synchronization functions ----
(defn generate-sync-file (defn generate-sync-file
@ -55,7 +102,7 @@
(let [[page-rchanges page-uchanges] (let [[page-rchanges page-uchanges]
(generate-sync-container asset-type (generate-sync-container asset-type
library-id library-id
library-items state
page page
(:id page) (:id page)
nil)] nil)]
@ -82,7 +129,7 @@
(let [[comp-rchanges comp-uchanges] (let [[comp-rchanges comp-uchanges]
(generate-sync-container asset-type (generate-sync-container asset-type
library-id library-id
library-items state
local-component local-component
nil nil
(:id local-component))] (:id local-component))]
@ -91,13 +138,36 @@
(d/concat uchanges comp-uchanges))) (d/concat uchanges comp-uchanges)))
[rchanges uchanges]))))) [rchanges uchanges])))))
(defn has-asset-reference-fn (defn- generate-sync-container
"Generate changes to synchronize all shapes in a particular container
(a page or a component) that are linked to the given library."
[asset-type library-id state container page-id component-id]
(let [has-asset-reference? (has-asset-reference-fn asset-type library-id)
linked-shapes (cph/select-objects has-asset-reference? container)]
(loop [shapes (seq linked-shapes)
rchanges []
uchanges []]
(if-let [shape (first shapes)]
(let [[shape-rchanges shape-uchanges]
(generate-sync-shape asset-type
library-id
state
(get container :objects)
page-id
component-id
shape)]
(recur (next shapes)
(d/concat rchanges shape-rchanges)
(d/concat uchanges shape-uchanges)))
[rchanges uchanges]))))
(defn- has-asset-reference-fn
"Gets a function that checks if a shape uses some asset of the given type "Gets a function that checks if a shape uses some asset of the given type
in the given library." in the given library."
[asset-type library-id] [asset-type library-id]
(case asset-type (case asset-type
:components :components
(fn [shape] (and (some? (:component-id shape)) (fn [shape] (and (:component-root? shape)
(= (:component-file shape) library-id))) (= (:component-file shape) library-id)))
:colors :colors
@ -126,50 +196,28 @@
#(and (some? (:typography-ref-id %)) #(and (some? (:typography-ref-id %))
(= library-id (:typography-ref-file %))))))))) (= library-id (:typography-ref-file %)))))))))
(defn generate-sync-container (defmulti generate-sync-shape
"Generate changes to synchronize all shapes in a particular container "Generate changes to synchronize one shape, that use the given type
(a page or a component)." of asset of the given library."
[asset-type library-id library-items container page-id component-id] (fn [type _ _ _ _ _ _ _] type))
(let [has-asset-reference? (has-asset-reference-fn asset-type library-id)
linked-shapes (cph/select-objects has-asset-reference? container)]
(loop [shapes (seq linked-shapes)
rchanges []
uchanges []]
(if-let [shape (first shapes)]
(let [[shape-rchanges shape-uchanges]
(generate-sync-shape asset-type
library-id
library-items
(get container :objects)
page-id
component-id
shape)]
(recur (next shapes)
(d/concat rchanges shape-rchanges)
(d/concat uchanges shape-uchanges)))
[rchanges uchanges]))))
(defmulti generate-sync-shape (fn [type _ _ _ _ _ _ _] type))
(defmethod generate-sync-shape :components (defmethod generate-sync-shape :components
[_ library-id library-items objects page-id component-id shape] [_ library-id state objects page-id component-id shape]
(let [[all-shapes component root-component]
(resolve-shapes-and-components shape
objects
state
false)]
;; Synchronize a shape that is the root instance of a component, and all of its (generate-sync-shape-and-children-components shape
;; children. All attributes of the component shape that have changed, and whose all-shapes
;; group have not been touched in the linked shape, will be copied to the shape. component
;; Any shape that is linked to a no-longer existent component shape will be root-component
;; detached.
(let [root-shape shape
components library-items
reset-touched? false]
(generate-sync-shape-and-children-components root-shape
objects
components
page-id page-id
component-id component-id
reset-touched?))) false)))
(defn generate-sync-text-shape [shape page-id component-id update-node] (defn- generate-sync-text-shape [shape page-id component-id update-node]
(let [old-content (:content shape) (let [old-content (:content shape)
new-content (ut/map-node update-node old-content) new-content (ut/map-node update-node old-content)
rchanges [(d/without-nils {:type :mod-obj rchanges [(d/without-nils {:type :mod-obj
@ -191,128 +239,170 @@
[rchanges lchanges]))) [rchanges lchanges])))
(defmethod generate-sync-shape :colors (defmethod generate-sync-shape :colors
[_ library-id library-items _ page-id component-id shape] [_ library-id state _ page-id component-id shape]
;; Synchronize a shape that uses some colors of the library. The value of the ;; Synchronize a shape that uses some colors of the library. The value of the
;; color in the library is copied to the shape. ;; color in the library is copied to the shape.
(if (= :text (:type shape)) (let [colors (get-assets library-id :colors state)]
(let [update-node (fn [node] (if (= :text (:type shape))
(if-let [color (get library-items (:fill-color-ref-id node))] (let [update-node (fn [node]
(assoc node :fill-color (:value color)) (if-let [color (get colors (:fill-color-ref-id node))]
node))] (assoc node :fill-color (:value color))
(generate-sync-text-shape shape page-id component-id update-node)) node))]
(loop [attrs (seq cp/color-sync-attrs) (generate-sync-text-shape shape page-id component-id update-node))
roperations [] (loop [attrs (seq cp/color-sync-attrs)
uoperations []] roperations []
(let [attr (first attrs)] uoperations []]
(if (nil? attr) (let [attr (first attrs)]
(if (empty? roperations) (if (nil? attr)
empty-changes (if (empty? roperations)
(let [rchanges [(d/without-nils {:type :mod-obj empty-changes
:page-id page-id (let [rchanges [(d/without-nils {:type :mod-obj
:component-id component-id :page-id page-id
:id (:id shape) :component-id component-id
:operations roperations})] :id (:id shape)
uchanges [(d/without-nils {:type :mod-obj :operations roperations})]
:page-id page-id uchanges [(d/without-nils {:type :mod-obj
:component-id component-id :page-id page-id
:id (:id shape) :component-id component-id
:operations uoperations})]] :id (:id shape)
[rchanges uchanges])) :operations uoperations})]]
(let [attr-ref-id (keyword (str (name attr) "-ref-id"))] [rchanges uchanges]))
(if-not (contains? shape attr-ref-id) (let [attr-ref-id (keyword (str (name attr) "-ref-id"))]
(recur (next attrs) (if-not (contains? shape attr-ref-id)
roperations
uoperations)
(let [color (get library-items (get shape attr-ref-id))
roperation {:type :set
:attr attr
:val (:value color)
:ignore-touched true}
uoperation {:type :set
:attr attr
:val (get shape attr)
:ignore-touched true}]
(recur (next attrs) (recur (next attrs)
(conj roperations roperation) roperations
(conj uoperations uoperation)))))))))) uoperations)
(let [color (get colors (get shape attr-ref-id))
roperation {:type :set
:attr attr
:val (:value color)
:ignore-touched true}
uoperation {:type :set
:attr attr
:val (get shape attr)
:ignore-touched true}]
(recur (next attrs)
(conj roperations roperation)
(conj uoperations uoperation)))))))))))
(defmethod generate-sync-shape :typographies (defmethod generate-sync-shape :typographies
[_ library-id library-items _ page-id component-id shape] [_ library-id state _ page-id component-id shape]
;; Synchronize a shape that uses some typographies of the library. The attributes ;; Synchronize a shape that uses some typographies of the library. The attributes
;; of the typography are copied to the shape." ;; of the typography are copied to the shape."
(let [update-node (fn [node] (let [typographies (get-assets library-id :typographies state)
(if-let [typography (get library-items (:typography-ref-id node))] update-node (fn [node]
(if-let [typography (get typographies (:typography-ref-id node))]
(merge node (d/without-keys typography [:name :id])) (merge node (d/without-keys typography [:name :id]))
node))] node))]
(generate-sync-text-shape shape page-id component-id update-node))) (generate-sync-text-shape shape page-id component-id update-node)))
;; ---- Create a new component ----
(defn make-component-shape
"Clone the shape and all children. Generate new ids and detach
from parent and frame. Update the original shapes to have links
to the new ones."
[shape objects]
(let [update-new-shape (fn [new-shape original-shape]
(assoc new-shape :frame-id nil))
;; If one of the original shape children already was a component
;; instance, the 'instanceness' is copied into the new component,
;; and the original shape now points to the new component.
update-original-shape (fn [original-shape new-shape]
(cond-> original-shape
true
(assoc :shape-ref (:id new-shape))
(nil? (:parent-id new-shape))
(assoc :component-id (:id new-shape)
:component-file nil)
(some? (:parent-id new-shape))
(assoc :component-id nil
:component-file nil)))]
(cph/clone-object shape nil objects update-new-shape update-original-shape)))
;; ---- Component synchronization helpers ---- ;; ---- Component synchronization helpers ----
(defn generate-sync-shape-and-children-components (defn- get-assets
"Generate changes to synchronize one shape that is linked to a component, [library-id asset-type state]
and all its children. If reset-touched? is false, same considerations as (if (nil? library-id)
in generate-sync-shape :components. If it's true, all attributes of the (get-in state [:workspace-data asset-type])
component that have changed will be copied, and the 'touched' flags in (get-in state [:workspace-libraries library-id :data asset-type])))
the shapes will be cleared."
[root-shape objects components page-id component-id reset-touched?]
(let [all-shapes (cph/get-object-with-children (:id root-shape) objects)
component (get components (:component-id root-shape))
root-component (get-in component [:objects (:shape-ref root-shape)])]
(loop [shapes (seq all-shapes)
rchanges []
uchanges []]
(let [shape (first shapes)]
(if (nil? shape)
[rchanges uchanges]
(let [[shape-rchanges shape-uchanges]
(generate-sync-shape-components
shape
root-shape
root-component
component
page-id
component-id
reset-touched?)]
(recur (next shapes)
(d/concat rchanges shape-rchanges)
(d/concat uchanges shape-uchanges))))))))
(defn generate-sync-shape-components (defn- get-component
[state file-id component-id]
(let [components (if (nil? file-id)
(get-in state [:workspace-data :components])
(get-in state [:workspace-libraries file-id :data :components]))]
(get components component-id)))
(defn resolve-shapes-and-components
"Get all shapes inside a component instance, and the component they are
linked with. If follow-indirection? is true, and the shape corresponding
to the root shape is also a component instance, follow the link and get
the final component."
[shape objects state follow-indirection?]
(loop [all-shapes (cph/get-object-with-children (:id shape) objects)
local-objects objects
local-shape shape]
(let [root-shape (cph/get-root-shape local-shape local-objects)
component (get-component state
(get root-shape :component-file)
(get root-shape :component-id))
component-shape (get-in component [:objects (:shape-ref local-shape)])]
(if (or (nil? (:component-id component-shape))
(not follow-indirection?))
[all-shapes component component-shape]
(let [resolve-indirection
(fn [shape]
(let [component-shape (get-in component [:objects (:shape-ref shape)])]
(-> shape
(assoc :shape-ref (:shape-ref component-shape))
(d/assoc-when :component-id (:component-id component-shape))
(d/assoc-when :component-file (:component-file component-shape)))))
new-shapes (map resolve-indirection all-shapes)]
(recur new-shapes
(:objects component)
component-shape))))))
(defn generate-sync-shape-and-children-components
"Generate changes to synchronize one shape that the root of a component
instance, and all its children, from the given component.
If reset? is false, all atributes of each component shape that have
changed, and whose group has not been touched in the instance shape will
be copied to this one.
If reset? is true, all changed attributes will be copied and the 'touched'
flags in the instance shape will be cleared."
[root-shape all-shapes component root-component page-id component-id reset?]
(loop [shapes (seq all-shapes)
rchanges []
uchanges []]
(let [shape (first shapes)]
(if (nil? shape)
[rchanges uchanges]
(let [[shape-rchanges shape-uchanges]
(generate-sync-shape<-component
shape
root-shape
root-component
component
page-id
component-id
reset?)]
(recur (next shapes)
(d/concat rchanges shape-rchanges)
(d/concat uchanges shape-uchanges)))))))
(defn- generate-sync-shape-inverse
"Generate changes to update the component a shape is linked to, from
the values in the shape and all its children.
All atributes of each instance shape that have changed, will be copied
to the component shape. Also clears the 'touched' flags in the source
shapes.
And if the component shapes are, in turn, instances of a second component,
their 'touched' flags will be set accordingly."
[root-shape all-shapes component root-component page-id]
(loop [shapes (seq all-shapes)
rchanges []
uchanges []]
(let [shape (first shapes)]
(if (nil? shape)
[rchanges uchanges]
(let [[shape-rchanges shape-uchanges]
(generate-sync-shape->component
shape
root-shape
root-component
component
page-id)]
(recur (next shapes)
(d/concat rchanges shape-rchanges)
(d/concat uchanges shape-uchanges)))))))
(defn- generate-sync-shape<-component
"Generate changes to synchronize one shape that is linked to other shape "Generate changes to synchronize one shape that is linked to other shape
inside a component. Same considerations as above about reset-touched?" inside a component. Same considerations as above about reset-touched?"
[shape root-shape root-component component page-id component-id reset-touched?] [shape root-shape root-component component page-id component-id reset?]
(if (nil? component) (if (nil? component)
(remove-component-and-ref shape page-id component-id) (remove-component-and-ref shape page-id component-id)
(let [component-shape (get (:objects component) (:shape-ref shape))] (let [component-shape (get (:objects component) (:shape-ref shape))]
@ -324,15 +414,55 @@
root-component root-component
page-id page-id
component-id component-id
reset-touched?))))) {:omit-touched? (not reset?)
:reset-touched? reset?
:set-touched? false})))))
(defn remove-component-and-ref (defn- generate-sync-shape->component
"Generate changes to synchronize one shape inside a component, with other
shape that is linked to it."
[shape root-shape root-component component page-id]
;; ===== Uncomment this to debug =====
;; (js/console.log "component" (clj->js component))
(if (nil? component)
empty-changes
(let [component-shape (get (:objects component) (:shape-ref shape))]
;; ===== Uncomment this to debug =====
;; (js/console.log "component-shape" (clj->js component-shape))
(if (nil? component-shape)
empty-changes
(let [;; ===== Uncomment this to debug =====
;; _(js/console.info "update" (:name shape) "->" (:name component-shape))
[rchanges1 uchanges1]
(update-attrs component-shape
shape
root-component
root-shape
nil
(:id root-component)
{:omit-touched? false
:reset-touched? false
:set-touched? true})
[rchanges2 uchanges2]
(reset-touched shape
page-id
nil)]
[(d/concat rchanges1 rchanges2)
(d/concat uchanges2 uchanges2)])))))
; ---- Operation generation helpers ----
(defn- remove-component-and-ref
[shape page-id component-id] [shape page-id component-id]
[[(d/without-nils {:type :mod-obj [[(d/without-nils {:type :mod-obj
:id (:id shape) :id (:id shape)
:page-id page-id :page-id page-id
:component-id component-id :component-id component-id
:operations [{:type :set :operations [{:type :set
:attr :component-root?
:val nil}
{:type :set
:attr :component-id :attr :component-id
:val nil} :val nil}
{:type :set {:type :set
@ -348,6 +478,9 @@
:page-id page-id :page-id page-id
:component-id component-id :component-id component-id
:operations [{:type :set :operations [{:type :set
:attr :component-root?
:val (:component-root? shape)}
{:type :set
:attr :component-id :attr :component-id
:val (:component-id shape)} :val (:component-id shape)}
{:type :set {:type :set
@ -359,7 +492,7 @@
{:type :set-touched {:type :set-touched
:touched (:touched shape)}]})]]) :touched (:touched shape)}]})]])
(defn remove-ref (defn- -remove-ref
[shape page-id component-id] [shape page-id component-id]
[[(d/without-nils {:type :mod-obj [[(d/without-nils {:type :mod-obj
:id (:id shape) :id (:id shape)
@ -380,32 +513,57 @@
{:type :set-touched {:type :set-touched
:touched (:touched shape)}]})]]) :touched (:touched shape)}]})]])
(defn update-attrs (defn- reset-touched
"The main function that implements the sync algorithm." [shape page-id component-id]
[shape component-shape root-shape root-component page-id component-id reset-touched?] [[(d/without-nils {:type :mod-obj
:id (:id shape)
:page-id page-id
:component-id component-id
:operations [{:type :set-touched
:touched nil}]})]
[(d/without-nils {:type :mod-obj
:id (:id shape)
:page-id page-id
:component-id component-id
:operations [{:type :set-touched
:touched (:touched shape)}]})]])
(defn- update-attrs
"The main function that implements the sync algorithm. Copy
attributes that have changed in the origin shape to the dest shape.
If omit-touched? is true, attributes whose group has been touched
in the destination shape will be ignored.
If reset-touched? is true, the 'touched' flags will be cleared in
the dest shape.
If set-touched? is true, the corresponding 'touched' flags will be
set in dest shape if they are different than their current values."
[dest-shape origin-shape dest-root origin-root page-id component-id
{:keys [omit-touched? reset-touched? set-touched?] :as options}]
;; === Uncomment this to debug synchronization === ;; === Uncomment this to debug synchronization ===
;; (println "SYNC" ;; (println "SYNC"
;; "[C]" (:name component-shape) ;; "[C]" (:name origin-shape)
;; "->" ;; "->"
;; (if page-id "[W]" ["C"]) ;; (if page-id "[W]" ["C"])
;; (:name shape)) ;; (:name dest-shape)
;; (str options))
(let [; The position attributes need a special sync algorith, because we do (let [; The position attributes need a special sync algorith, because we do
; not synchronize the absolute position, but the position relative of ; not synchronize the absolute position, but the position relative of
; the container shape of the component. ; the container shape of the component.
new-pos (calc-new-pos shape component-shape root-shape root-component) new-pos (calc-new-pos dest-shape origin-shape dest-root origin-root)
pos-group (get cp/component-sync-attrs :x) touched (get dest-shape :touched #{})]
touched (get shape :touched #{})]
(loop [attrs (seq (keys (dissoc cp/component-sync-attrs :x :y))) (loop [attrs (seq (keys (dissoc cp/component-sync-attrs :x :y)))
roperations (if (or (not (touched pos-group)) reset-touched? true) roperations (if (or (not= (:x new-pos) (:x dest-shape))
[{:type :set :attr :x :val (:x new-pos)} ; ^ TODO: the position-group is being set (not= (:y new-pos) (:y dest-shape)))
{:type :set :attr :y :val (:y new-pos)}] ; | as touched somewhere. Investigate why. [{:type :set :attr :x :val (:x new-pos)}
{:type :set :attr :y :val (:y new-pos)}]
[]) [])
uoperations (if (or (not (touched pos-group)) reset-touched? true) uoperations (if (or (not= (:x new-pos) (:x dest-shape))
[{:type :set :attr :x :val (:x shape)} (not= (:y new-pos) (:y dest-shape)))
{:type :set :attr :y :val (:y shape)}] [{:type :set :attr :x :val (:x dest-shape)}
{:type :set :attr :y :val (:y dest-shape)}]
[])] [])]
(let [attr (first attrs)] (let [attr (first attrs)]
@ -419,51 +577,50 @@
uoperations (if reset-touched? uoperations (if reset-touched?
(conj uoperations (conj uoperations
{:type :set-touched {:type :set-touched
:touched (:touched shape)}) :touched (:touched dest-shape)})
uoperations) uoperations)
rchanges [(d/without-nils {:type :mod-obj rchanges [(d/without-nils {:type :mod-obj
:id (:id shape) :id (:id dest-shape)
:page-id page-id :page-id page-id
:component-id component-id :component-id component-id
:operations roperations})] :operations roperations})]
uchanges [(d/without-nils {:type :mod-obj uchanges [(d/without-nils {:type :mod-obj
:id (:id shape) :id (:id dest-shape)
:page-id page-id :page-id page-id
:component-id component-id :component-id component-id
:operations uoperations})]] :operations uoperations})]]
[rchanges uchanges]) [rchanges uchanges])
(if-not (contains? shape attr) (if-not (contains? dest-shape attr)
(recur (next attrs) (recur (next attrs)
roperations roperations
uoperations) uoperations)
(let [roperation {:type :set (let [roperation {:type :set
:attr attr :attr attr
:val (get component-shape attr) :val (get origin-shape attr)
:ignore-touched true} :ignore-touched (not set-touched?)}
uoperation {:type :set uoperation {:type :set
:attr attr :attr attr
:val (get shape attr) :val (get dest-shape attr)
:ignore-touched true} :ignore-touched (not set-touched?)}
attr-group (get cp/component-sync-attrs attr)] attr-group (get cp/component-sync-attrs attr)]
(if (or (not (touched attr-group)) reset-touched?) (if (and (touched attr-group) omit-touched?)
(recur (next attrs)
(conj roperations roperation)
(conj uoperations uoperation))
(recur (next attrs) (recur (next attrs)
roperations roperations
uoperations))))))))) uoperations)
(recur (next attrs)
(conj roperations roperation)
(conj uoperations uoperation))))))))))
(defn calc-new-pos (defn- calc-new-pos
[shape component-shape root-shape root-component] [dest-shape origin-shape dest-root origin-root]
(let [root-pos (gpt/point (:x root-shape) (:y root-shape)) (let [root-pos (gpt/point (:x dest-root) (:y dest-root))
root-component-pos (gpt/point (:x root-component) (:y root-component)) origin-root-pos (gpt/point (:x origin-root) (:y origin-root))
component-pos (gpt/point (:x component-shape) (:y component-shape)) origin-pos (gpt/point (:x origin-shape) (:y origin-shape))
delta (gpt/subtract component-pos root-component-pos) delta (gpt/subtract origin-pos origin-root-pos)
shape-pos (gpt/point (:x shape) (:y shape)) shape-pos (gpt/point (:x dest-shape) (:y dest-shape))
new-pos (gpt/add root-pos delta)] new-pos (gpt/add root-pos delta)]
new-pos)) new-pos))

View file

@ -89,7 +89,8 @@
(letfn [(show-shape [shape-id level objects] (letfn [(show-shape [shape-id level objects]
(let [shape (get objects shape-id)] (let [shape (get objects shape-id)]
(println (str/pad (str (str/repeat " " level) (println (str/pad (str (str/repeat " " level)
(:name shape)) (:name shape)
(when (seq (:touched shape)) "*")
{:length 20 {:length 20
:type :right}) :type :right})
(show-component shape objects)) (show-component shape objects))
@ -102,24 +103,36 @@
(show-shape shape-id (inc level) objects)))))) (show-shape shape-id (inc level) objects))))))
(show-component [shape objects] (show-component [shape objects]
(let [root-id (cph/get-root-component (:id shape) objects) (if (nil? (:shape-ref shape))
root-shape (when root-id (get objects root-id)) ""
component-id (when root-shape (:component-id root-shape)) (let [root-shape (cph/get-root-shape shape objects)
component-file-id (when root-shape (:component-file root-shape)) component-id (when root-shape (:component-id root-shape))
component-file (when component-file-id (get libraries component-file-id)) component-file-id (when root-shape (:component-file root-shape))
shape-ref (:shape-ref shape) component-file (when component-file-id (get libraries component-file-id))
component (when component-id component (when component-id
(if component-file (if component-file
(get-in component-file [:data :components component-id]) (get-in component-file [:data :components component-id])
(get components component-id))) (get components component-id)))
component-shape (when (and component shape-ref) component-shape (when (and component (:shape-ref shape))
(get-in component [:objects shape-ref]))] (get-in component [:objects (:shape-ref shape)]))]
(if component-shape (str/format " %s--> %s%s%s"
(str/format " %s--> %s%s" (cond (:component-root? shape) "#"
(if (:component-id shape) "#" "-") (:component-id shape) "@"
:else "-")
(when component-file (str/format "<%s> " (:name component-file))) (when component-file (str/format "<%s> " (:name component-file)))
(:name component-shape)) (:name component-shape)
"")))] (if (or (:component-root? shape)
(nil? (:component-id shape)))
""
(let [component-id (:component-id shape)
component-file-id (:component-file shape)
component-file (when component-file-id (get libraries component-file-id))
component (if component-file
(get-in component-file [:data :components component-id])
(get components component-id))]
(str/format " (%s%s)"
(when component-file (str/format "<%s> " (:name component-file)))
(:name component))))))))]
(println "[Workspace]") (println "[Workspace]")
(show-shape (:id root) 0 objects) (show-shape (:id root) 0 objects)

View file

@ -20,6 +20,7 @@
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.libraries :as dwl]
[app.main.ui.hooks :refer [use-rxsub]] [app.main.ui.hooks :refer [use-rxsub]]
[app.main.ui.components.dropdown :refer [dropdown]])) [app.main.ui.components.dropdown :refer [dropdown]]))
@ -46,7 +47,6 @@
[{:keys [mdata] :as props}] [{:keys [mdata] :as props}]
(let [{:keys [id] :as shape} (:shape mdata) (let [{:keys [id] :as shape} (:shape mdata)
selected (:selected mdata) selected (:selected mdata)
root-shape (:root-shape mdata)
do-duplicate #(st/emit! dw/duplicate-selected) do-duplicate #(st/emit! dw/duplicate-selected)
do-delete #(st/emit! dw/delete-selected) do-delete #(st/emit! dw/delete-selected)
@ -66,10 +66,12 @@
do-detach-component #(st/emit! (dwl/detach-component id)) do-detach-component #(st/emit! (dwl/detach-component id))
do-reset-component #(st/emit! (dwl/reset-component id)) do-reset-component #(st/emit! (dwl/reset-component id))
do-update-component #(do do-update-component #(do
(st/emit! dwc/start-undo-transaction)
(st/emit! (dwl/update-component id)) (st/emit! (dwl/update-component id))
(st/emit! (dwl/sync-file nil))) (st/emit! (dwl/sync-file nil))
(st/emit! dwc/commit-undo-transaction))
do-navigate-component-file #(st/emit! (dwl/nav-to-component-file do-navigate-component-file #(st/emit! (dwl/nav-to-component-file
(:component-file root-shape)))] (:component-file shape)))]
[:* [:*
[:& menu-entry {:title "Copy" [:& menu-entry {:title "Copy"
:shortcut "Ctrl + c" :shortcut "Ctrl + c"
@ -117,28 +119,30 @@
[:& menu-entry {:title "Lock" [:& menu-entry {:title "Lock"
:on-click do-lock-shape}]) :on-click do-lock-shape}])
[:& menu-separator] (when (nil? (:shape-ref shape))
(if (nil? (:shape-ref shape))
[:& menu-entry {:title "Create component"
:shortcut "Ctrl + K"
:on-click do-add-component}]
[:* [:*
[:& menu-entry {:title "Detach instance" [:& menu-separator]
:on-click do-detach-component}] [:& menu-entry {:title "Create component"
[:& menu-entry {:title "Reset overrides" :shortcut "Ctrl + K"
:on-click do-reset-component}] :on-click do-add-component}]])
(if (nil? (:component-file root-shape))
[:& menu-entry {:title "Update master component" (when (:component-id shape)
:on-click do-update-component}] [:*
[:& menu-entry {:title "Go to master component file" [:& menu-separator]
:on-click do-navigate-component-file}])]) [:& menu-entry {:title "Detach instance"
:on-click do-detach-component}]
[:& menu-entry {:title "Reset overrides"
:on-click do-reset-component}]
(if (nil? (:component-file shape))
[:& menu-entry {:title "Update master component"
:on-click do-update-component}]
[:& menu-entry {:title "Go to master component file"
:on-click do-navigate-component-file}])])
[:& menu-separator] [:& menu-separator]
[:& menu-entry {:title "Delete" [:& menu-entry {:title "Delete"
:shortcut "Supr" :shortcut "Supr"
:on-click do-delete}] :on-click do-delete}]]))
]))
(mf/defc viewport-context-menu (mf/defc viewport-context-menu
[{:keys [mdata] :as props}] [{:keys [mdata] :as props}]

View file

@ -89,7 +89,8 @@
:default-value (:name shape "")}] :default-value (:name shape "")}]
[:span.element-name [:span.element-name
{:on-double-click on-click} {:on-double-click on-click}
(:name shape "")]))) (:name shape "")
(when (seq (:touched shape)) " *")])))
(defn- make-collapsed-iref (defn- make-collapsed-iref
[id] [id]
@ -305,6 +306,7 @@
:component-id :component-id
:component-file :component-file
:shape-ref :shape-ref
:touched
:metadata])] :metadata])]
(persistent! (persistent!
(reduce-kv (fn [res id obj] (reduce-kv (fn [res id obj]