diff --git a/common/app/common/pages.cljc b/common/app/common/pages.cljc index 74f14b057..81791d652 100644 --- a/common/app/common/pages.cljc +++ b/common/app/common/pages.cljc @@ -46,6 +46,7 @@ (<= % max-safe-int))) (s/def ::component-id uuid?) (s/def ::component-file uuid?) +(s/def ::component-root? boolean?) (s/def ::shape-ref uuid?) (s/def ::safe-number @@ -122,7 +123,6 @@ (s/def :internal.shape/line-height ::safe-number) (s/def :internal.shape/locked boolean?) (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-lock boolean?) (s/def :internal.shape/rx ::safe-number) @@ -236,12 +236,8 @@ :width :size-group :height :size-group :proportion :size-group - :x :position-group - :y :position-group :rx :radius-group - :ry :radius-group - :points :points-group - :transform :transform-group}) + :ry :radius-group}) (def color-sync-attrs [:fill-color :stroke-color]) @@ -255,6 +251,7 @@ (s/keys :opt-un [::id ::component-id ::component-file + ::component-root? ::shape-ref]))) (s/def :internal.page/objects (s/map-of uuid? ::shape)) @@ -891,21 +888,21 @@ (defmethod process-operation :set [shape op] - (let [attr (:attr op) - val (:val op) - ignore (:ignore-touched op) + (let [attr (:attr op) + val (:val op) + ignore (:ignore-touched op) shape-ref (:shape-ref shape) - group (get component-sync-attrs attr)] + group (get component-sync-attrs attr)] (cond-> shape + (and shape-ref group (not ignore) (not= val (get shape attr))) + (update :touched #(conj (or % #{}) group)) + (nil? val) (dissoc attr) (some? val) - (assoc attr val) - - (and shape-ref group (not ignore)) - (update :touched #(conj (or % #{}) group))))) + (assoc attr val)))) (defmethod process-operation :set-touched [shape op] diff --git a/common/app/common/pages_helpers.cljc b/common/app/common/pages_helpers.cljc index c1e87dc26..1432923bd 100644 --- a/common/app/common/pages_helpers.cljc +++ b/common/app/common/pages_helpers.cljc @@ -31,15 +31,15 @@ (update page :objects #(into % (d/index-by :id objects-list)))) -(defn get-root-component - "Get the root shape linked to the component for this shape, if any" - [id objects] - (let [obj (get objects id)] - (if-let [component-id (:component-id obj)] - id - (if-let [parent-id (:parent-id obj)] - (get-root-component parent-id objects) - nil)))) +(defn get-root-shape + "Get the root shape linked to a component for this shape, if any" + [shape objects] + (if (:component-root? shape) + shape + (if-let [parent-id (:parent-id shape)] + (get-root-shape (get objects (:parent-id shape)) + objects) + nil))) (defn get-children "Retrieve all children ids recursively for a given object" @@ -58,7 +58,7 @@ (defn get-object-with-children "Retrieve a list with an object and all of its children" [id objects] - (map #(get objects %) (concat [id] (get-children id objects)))) + (map #(get objects %) (cons id (get-children id objects)))) (defn is-shape-grouped "Checks if a shape is inside a group" diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index e809a4c0b..5fcd337cd 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1148,12 +1148,9 @@ (update [_ state] (let [page-id (:current-page-id state) 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 :shape shape - :root-shape root-shape :selected (get-in state [:workspace-local :selected])}] (-> state (assoc-in [:workspace-local :context-menu] mdata)))) diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 21807df1f..00226a92e 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -155,9 +155,15 @@ {:type :set :attr :component-file :val nil} + {:type :set + :attr :component-root? + :val (:component-root? updated-shape)} {:type :set :attr :shape-ref - :val (:shape-ref updated-shape)}]}) + :val (:shape-ref updated-shape)} + {:type :set + :attr :touched + :val (:touched updated-shape)}]}) updated-shapes)) uchanges (conj uchanges @@ -176,11 +182,18 @@ {:type :set :attr :component-file :val (:component-file original-shape)} + {:type :set + :attr :component-root? + :val (:component-root? original-shape)} {:type :set :attr :shape-ref - :val (:shape-ref original-shape)}]})) + :val (:shape-ref original-shape)} + {:type :set + :attr :touched + :val (:touched original-shape)}]})) updated-shapes))] + (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) (dws/select-shapes (d/ordered-set (:id group)))))))))) @@ -239,10 +252,12 @@ (dwc/calculate-frame-overlap all-frames $)) (assoc $ :parent-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)) - (assoc :component-id (:id original-shape)) + (assoc :component-id (:id original-shape) + :component-root? true) (and (nil? (:parent-id original-shape)) (some? file-id)) (assoc :component-file file-id) @@ -251,7 +266,7 @@ (dissoc :component-file) (some? (:parent-id original-shape)) - (dissoc :component-id :component-file)))) + (dissoc :component-root?)))) [new-shape new-shapes _] (cph/clone-object component-shape @@ -285,9 +300,7 @@ (watch [_ state stream] (let [page-id (:current-page-id state) objects (dwc/lookup-page-objects state page-id) - root-id (cph/get-root-component id objects) - - shapes (cph/get-object-with-children root-id objects) + shapes (cph/get-object-with-children id objects) rchanges (map (fn [obj] {:type :mod-obj @@ -351,26 +364,38 @@ (ptk/reify ::reset-component ptk/WatchEvent (watch [_ state stream] - (let [page-id (:current-page-id state) - page (get-in state [:workspace-data :pages-index page-id]) - objects (dwc/lookup-page-objects state page-id) - root-id (cph/get-root-component id objects) - root-shape (get objects id) - file-id (get root-shape :component-file) + ;; ===== Uncomment this to debug ===== + ;; (js/console.info "##### RESET-COMPONENT of shape" (str id)) + (let [page-id (:current-page-id state) + page (get-in state [:workspace-data :pages-index page-id]) + objects (dwc/lookup-page-objects state page-id) + shape (get objects id) + file-id (get shape :component-file) - components - (if (nil? file-id) - (get-in state [:workspace-data :components]) - (get-in state [:workspace-libraries file-id :data :components])) + [all-shapes component root-component] + (dwlh/resolve-shapes-and-components shape + objects + 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] - (dwlh/generate-sync-shape-and-children-components root-shape - objects - components + (dwlh/generate-sync-shape-and-children-components shape + all-shapes + component + root-component (:id page) nil true)] + ;; ===== Uncomment this to debug ===== + ;; (js/console.debug "rchanges" (clj->js rchanges)) + (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) (defn update-component @@ -379,60 +404,34 @@ (ptk/reify ::update-component ptk/WatchEvent (watch [_ state stream] - (let [page-id (:current-page-id state) - objects (dwc/lookup-page-objects state page-id) - root-id (cph/get-root-component id objects) - root-shape (get objects id) + ;; ===== Uncomment this to debug ===== + ;; (js/console.info "##### UPDATE-COMPONENT of shape" (str id)) + (let [page-id (:current-page-id state) + 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) - component-objs (dwc/lookup-component-objects state component-id) - component-obj (get component-objs component-id) + [all-shapes component root-component] + (dwlh/resolve-shapes-and-components shape + objects + state + true) - ;; Clone again the original shape and its children, maintaing - ;; the ids of the cloned shapes. If the original shape has some - ;; new child shapes, the cloned ones will have new generated ids. - update-new-shape (fn [new-shape original-shape] - (cond-> new-shape - true - (assoc :frame-id nil) + ;; ===== 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)) - (= (:component-id original-shape) component-id) - (dissoc :component-id) + [rchanges uchanges] + (dwlh/generate-sync-shape-inverse shape + all-shapes + component + root-component + page-id)] - (some? (:shape-ref original-shape)) - (assoc :id (:shape-ref original-shape)))) - - 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))] + ;; ===== Uncomment this to debug ===== + ;; (js/console.debug "rchanges" (clj->js rchanges)) (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) @@ -450,6 +449,8 @@ ptk/WatchEvent (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) [rchanges2 uchanges2] (dwlh/generate-sync-library :components 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) rchanges (d/concat rchanges1 rchanges2 rchanges3 rchanges4 rchanges5 rchanges6) uchanges (d/concat uchanges1 uchanges2 uchanges3 uchanges4 uchanges5 uchanges6)] + ;; ===== Uncomment this to debug ===== + ;; (js/console.debug "rchanges" (clj->js rchanges)) (rx/concat (rx/of (dm/hide-tag :sync-dialog)) (when rchanges @@ -483,8 +486,15 @@ (ptk/reify ::sync-file-2nd-stage ptk/WatchEvent (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 + ;; ===== Uncomment this to debug ===== + ;; (js/console.debug "rchanges" (clj->js rchanges)) (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))) (def ignore-sync diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index 8225ea7b6..8585f0d10 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -21,15 +21,62 @@ (declare generate-sync-container) (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-components) +(declare generate-sync-shape-inverse) +(declare generate-sync-shape<-component) +(declare generate-sync-shape->component) (declare remove-component-and-ref) (declare remove-ref) +(declare reset-touched) (declare update-attrs) (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 ---- (defn generate-sync-file @@ -55,7 +102,7 @@ (let [[page-rchanges page-uchanges] (generate-sync-container asset-type library-id - library-items + state page (:id page) nil)] @@ -82,7 +129,7 @@ (let [[comp-rchanges comp-uchanges] (generate-sync-container asset-type library-id - library-items + state local-component nil (:id local-component))] @@ -91,13 +138,36 @@ (d/concat uchanges comp-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 in the given library." [asset-type library-id] (case asset-type :components - (fn [shape] (and (some? (:component-id shape)) + (fn [shape] (and (:component-root? shape) (= (:component-file shape) library-id))) :colors @@ -126,50 +196,28 @@ #(and (some? (:typography-ref-id %)) (= library-id (:typography-ref-file %))))))))) -(defn generate-sync-container - "Generate changes to synchronize all shapes in a particular container - (a page or a component)." - [asset-type library-id library-items 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 - 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)) +(defmulti generate-sync-shape + "Generate changes to synchronize one shape, that use the given type + of asset of the given library." + (fn [type _ _ _ _ _ _ _] type)) (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 - ;; children. All attributes of the component shape that have changed, and whose - ;; group have not been touched in the linked shape, will be copied to the shape. - ;; Any shape that is linked to a no-longer existent component shape will be - ;; detached. - (let [root-shape shape - components library-items - reset-touched? false] - (generate-sync-shape-and-children-components root-shape - objects - components + (generate-sync-shape-and-children-components shape + all-shapes + component + root-component page-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) new-content (ut/map-node update-node old-content) rchanges [(d/without-nils {:type :mod-obj @@ -191,128 +239,170 @@ [rchanges lchanges]))) (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 ;; color in the library is copied to the shape. - (if (= :text (:type shape)) - (let [update-node (fn [node] - (if-let [color (get library-items (:fill-color-ref-id node))] - (assoc node :fill-color (:value color)) - node))] - (generate-sync-text-shape shape page-id component-id update-node)) - (loop [attrs (seq cp/color-sync-attrs) - roperations [] - uoperations []] - (let [attr (first attrs)] - (if (nil? attr) - (if (empty? roperations) - empty-changes - (let [rchanges [(d/without-nils {:type :mod-obj - :page-id page-id - :component-id component-id - :id (:id shape) - :operations roperations})] - uchanges [(d/without-nils {:type :mod-obj - :page-id page-id - :component-id component-id - :id (:id shape) - :operations uoperations})]] - [rchanges uchanges])) - (let [attr-ref-id (keyword (str (name attr) "-ref-id"))] - (if-not (contains? shape attr-ref-id) - (recur (next attrs) - 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}] + (let [colors (get-assets library-id :colors state)] + (if (= :text (:type shape)) + (let [update-node (fn [node] + (if-let [color (get colors (:fill-color-ref-id node))] + (assoc node :fill-color (:value color)) + node))] + (generate-sync-text-shape shape page-id component-id update-node)) + (loop [attrs (seq cp/color-sync-attrs) + roperations [] + uoperations []] + (let [attr (first attrs)] + (if (nil? attr) + (if (empty? roperations) + empty-changes + (let [rchanges [(d/without-nils {:type :mod-obj + :page-id page-id + :component-id component-id + :id (:id shape) + :operations roperations})] + uchanges [(d/without-nils {:type :mod-obj + :page-id page-id + :component-id component-id + :id (:id shape) + :operations uoperations})]] + [rchanges uchanges])) + (let [attr-ref-id (keyword (str (name attr) "-ref-id"))] + (if-not (contains? shape attr-ref-id) (recur (next attrs) - (conj roperations roperation) - (conj uoperations uoperation)))))))))) + roperations + 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 - [_ 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 ;; of the typography are copied to the shape." - (let [update-node (fn [node] - (if-let [typography (get library-items (:typography-ref-id node))] + (let [typographies (get-assets library-id :typographies state) + update-node (fn [node] + (if-let [typography (get typographies (:typography-ref-id node))] (merge node (d/without-keys typography [:name :id])) 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 ---- -(defn generate-sync-shape-and-children-components - "Generate changes to synchronize one shape that is linked to a component, - and all its children. If reset-touched? is false, same considerations as - in generate-sync-shape :components. If it's true, all attributes of the - component that have changed will be copied, and the 'touched' flags in - 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- get-assets + [library-id asset-type state] + (if (nil? library-id) + (get-in state [:workspace-data asset-type]) + (get-in state [:workspace-libraries library-id :data asset-type]))) -(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 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) (remove-component-and-ref shape page-id component-id) (let [component-shape (get (:objects component) (:shape-ref shape))] @@ -324,15 +414,55 @@ root-component page-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] [[(d/without-nils {:type :mod-obj :id (:id shape) :page-id page-id :component-id component-id :operations [{:type :set + :attr :component-root? + :val nil} + {:type :set :attr :component-id :val nil} {:type :set @@ -348,6 +478,9 @@ :page-id page-id :component-id component-id :operations [{:type :set + :attr :component-root? + :val (:component-root? shape)} + {:type :set :attr :component-id :val (:component-id shape)} {:type :set @@ -359,7 +492,7 @@ {:type :set-touched :touched (:touched shape)}]})]]) -(defn remove-ref +(defn- -remove-ref [shape page-id component-id] [[(d/without-nils {:type :mod-obj :id (:id shape) @@ -380,32 +513,57 @@ {:type :set-touched :touched (:touched shape)}]})]]) -(defn update-attrs - "The main function that implements the sync algorithm." - [shape component-shape root-shape root-component page-id component-id reset-touched?] +(defn- reset-touched + [shape page-id component-id] + [[(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 === ;; (println "SYNC" - ;; "[C]" (:name component-shape) + ;; "[C]" (:name origin-shape) ;; "->" ;; (if page-id "[W]" ["C"]) - ;; (:name shape)) + ;; (:name dest-shape) + ;; (str options)) (let [; The position attributes need a special sync algorith, because we do ; not synchronize the absolute position, but the position relative of ; the container shape of the component. - new-pos (calc-new-pos shape component-shape root-shape root-component) - pos-group (get cp/component-sync-attrs :x) - touched (get shape :touched #{})] + new-pos (calc-new-pos dest-shape origin-shape dest-root origin-root) + touched (get dest-shape :touched #{})] (loop [attrs (seq (keys (dissoc cp/component-sync-attrs :x :y))) - roperations (if (or (not (touched pos-group)) reset-touched? true) - [{:type :set :attr :x :val (:x new-pos)} ; ^ TODO: the position-group is being set - {:type :set :attr :y :val (:y new-pos)}] ; | as touched somewhere. Investigate why. + roperations (if (or (not= (:x new-pos) (:x dest-shape)) + (not= (:y new-pos) (:y dest-shape))) + [{: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) - [{:type :set :attr :x :val (:x shape)} - {:type :set :attr :y :val (:y shape)}] + uoperations (if (or (not= (:x new-pos) (:x dest-shape)) + (not= (:y new-pos) (:y dest-shape))) + [{:type :set :attr :x :val (:x dest-shape)} + {:type :set :attr :y :val (:y dest-shape)}] [])] (let [attr (first attrs)] @@ -419,51 +577,50 @@ uoperations (if reset-touched? (conj uoperations {:type :set-touched - :touched (:touched shape)}) + :touched (:touched dest-shape)}) uoperations) rchanges [(d/without-nils {:type :mod-obj - :id (:id shape) + :id (:id dest-shape) :page-id page-id :component-id component-id :operations roperations})] uchanges [(d/without-nils {:type :mod-obj - :id (:id shape) + :id (:id dest-shape) :page-id page-id :component-id component-id :operations uoperations})]] [rchanges uchanges]) - (if-not (contains? shape attr) + (if-not (contains? dest-shape attr) (recur (next attrs) roperations uoperations) (let [roperation {:type :set :attr attr - :val (get component-shape attr) - :ignore-touched true} + :val (get origin-shape attr) + :ignore-touched (not set-touched?)} uoperation {:type :set :attr attr - :val (get shape attr) - :ignore-touched true} + :val (get dest-shape attr) + :ignore-touched (not set-touched?)} attr-group (get cp/component-sync-attrs attr)] - (if (or (not (touched attr-group)) reset-touched?) - (recur (next attrs) - (conj roperations roperation) - (conj uoperations uoperation)) + (if (and (touched attr-group) omit-touched?) (recur (next attrs) roperations - uoperations))))))))) + uoperations) + (recur (next attrs) + (conj roperations roperation) + (conj uoperations uoperation)))))))))) -(defn calc-new-pos - [shape component-shape root-shape root-component] - (let [root-pos (gpt/point (:x root-shape) (:y root-shape)) - root-component-pos (gpt/point (:x root-component) (:y root-component)) - component-pos (gpt/point (:x component-shape) (:y component-shape)) - delta (gpt/subtract component-pos root-component-pos) - shape-pos (gpt/point (:x shape) (:y shape)) - new-pos (gpt/add root-pos delta)] +(defn- calc-new-pos + [dest-shape origin-shape dest-root origin-root] + (let [root-pos (gpt/point (:x dest-root) (:y dest-root)) + origin-root-pos (gpt/point (:x origin-root) (:y origin-root)) + origin-pos (gpt/point (:x origin-shape) (:y origin-shape)) + delta (gpt/subtract origin-pos origin-root-pos) + shape-pos (gpt/point (:x dest-shape) (:y dest-shape)) + new-pos (gpt/add root-pos delta)] new-pos)) - diff --git a/frontend/src/app/main/store.cljs b/frontend/src/app/main/store.cljs index ac303d734..be3fc28d5 100644 --- a/frontend/src/app/main/store.cljs +++ b/frontend/src/app/main/store.cljs @@ -89,7 +89,8 @@ (letfn [(show-shape [shape-id level objects] (let [shape (get objects shape-id)] (println (str/pad (str (str/repeat " " level) - (:name shape)) + (:name shape) + (when (seq (:touched shape)) "*") {:length 20 :type :right}) (show-component shape objects)) @@ -102,24 +103,36 @@ (show-shape shape-id (inc level) objects)))))) (show-component [shape objects] - (let [root-id (cph/get-root-component (:id shape) objects) - root-shape (when root-id (get objects root-id)) - component-id (when root-shape (:component-id root-shape)) - component-file-id (when root-shape (:component-file root-shape)) - component-file (when component-file-id (get libraries component-file-id)) - shape-ref (:shape-ref shape) - component (when component-id - (if component-file - (get-in component-file [:data :components component-id]) - (get components component-id))) - component-shape (when (and component shape-ref) - (get-in component [:objects shape-ref]))] - (if component-shape - (str/format " %s--> %s%s" - (if (:component-id shape) "#" "-") + (if (nil? (:shape-ref shape)) + "" + (let [root-shape (cph/get-root-shape shape objects) + component-id (when root-shape (:component-id root-shape)) + component-file-id (when root-shape (:component-file root-shape)) + component-file (when component-file-id (get libraries component-file-id)) + component (when component-id + (if component-file + (get-in component-file [:data :components component-id]) + (get components component-id))) + component-shape (when (and component (:shape-ref shape)) + (get-in component [:objects (:shape-ref shape)]))] + (str/format " %s--> %s%s%s" + (cond (:component-root? shape) "#" + (:component-id shape) "@" + :else "-") (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]") (show-shape (:id root) 0 objects) diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index a7330c3cb..cc109cc50 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -20,6 +20,7 @@ [app.main.ui.icons :as i] [app.util.dom :as dom] [app.main.data.workspace :as dw] + [app.main.data.workspace.common :as dwc] [app.main.data.workspace.libraries :as dwl] [app.main.ui.hooks :refer [use-rxsub]] [app.main.ui.components.dropdown :refer [dropdown]])) @@ -46,7 +47,6 @@ [{:keys [mdata] :as props}] (let [{:keys [id] :as shape} (:shape mdata) selected (:selected mdata) - root-shape (:root-shape mdata) do-duplicate #(st/emit! dw/duplicate-selected) do-delete #(st/emit! dw/delete-selected) @@ -66,10 +66,12 @@ do-detach-component #(st/emit! (dwl/detach-component id)) do-reset-component #(st/emit! (dwl/reset-component id)) do-update-component #(do + (st/emit! dwc/start-undo-transaction) (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 - (:component-file root-shape)))] + (:component-file shape)))] [:* [:& menu-entry {:title "Copy" :shortcut "Ctrl + c" @@ -117,28 +119,30 @@ [:& menu-entry {:title "Lock" :on-click do-lock-shape}]) - [:& menu-separator] - - (if (nil? (:shape-ref shape)) - [:& menu-entry {:title "Create component" - :shortcut "Ctrl + K" - :on-click do-add-component}] + (when (nil? (:shape-ref shape)) [:* - [:& menu-entry {:title "Detach instance" - :on-click do-detach-component}] - [:& menu-entry {:title "Reset overrides" - :on-click do-reset-component}] - (if (nil? (:component-file root-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-entry {:title "Create component" + :shortcut "Ctrl + K" + :on-click do-add-component}]]) + + (when (:component-id shape) + [:* + [:& menu-separator] + [:& 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-entry {:title "Delete" :shortcut "Supr" - :on-click do-delete}] - ])) + :on-click do-delete}]])) (mf/defc viewport-context-menu [{:keys [mdata] :as props}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 42b89610c..889d04dcd 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -89,7 +89,8 @@ :default-value (:name shape "")}] [:span.element-name {:on-double-click on-click} - (:name shape "")]))) + (:name shape "") + (when (seq (:touched shape)) " *")]))) (defn- make-collapsed-iref [id] @@ -305,6 +306,7 @@ :component-id :component-file :shape-ref + :touched :metadata])] (persistent! (reduce-kv (fn [res id obj]