From 57b2141166dc8e3b992125a46d8157103312eff5 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 6 Apr 2020 13:29:58 +0200 Subject: [PATCH 1/6] :sparkles: Fixes problem with frame movement --- frontend/src/uxbox/main/data/workspace.cljs | 12 +++++++----- frontend/src/uxbox/main/geom.cljs | 19 ++++++++++--------- frontend/src/uxbox/main/ui/shapes/frame.cljs | 14 +++++--------- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 26e584e89..843d2191d 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -1505,7 +1505,7 @@ (let [page-id (::page-id state) objects (get-in state [:workspace-data page-id :objects]) - ;; Updates the displacement data for a single shape + ;; Updates the resize data for a single shape materialize-shape (fn [state id mtx] (update-in @@ -1525,9 +1525,11 @@ (fn [state id] (let [shape (get objects id) mtx (:resize-modifier shape (gmt/matrix))] - (-> state - (materialize-shape id mtx) - (materialize-children id mtx))))] + (if (= (:type shape) :frame) + (materialize-shape state id mtx) + (-> state + (materialize-shape id mtx) + (materialize-children id mtx)))))] (reduce update-shapes state ids))) ptk/WatchEvent @@ -1628,7 +1630,7 @@ (dissoc :displacement-modifier) (geom/transform xfmt)) - shapes (->> (:shapes frame) + shapes (->> (helpers/get-children id objects) (map #(get objects %)) (map #(geom/transform % xfmt)) (d/index-by :id)) diff --git a/frontend/src/uxbox/main/geom.cljs b/frontend/src/uxbox/main/geom.cljs index 43d33730b..b8023a064 100644 --- a/frontend/src/uxbox/main/geom.cljs +++ b/frontend/src/uxbox/main/geom.cljs @@ -588,12 +588,13 @@ (> ry2 sy1)))) (defn transform-shape - [frame shape] - (let [ds-modifier (:displacement-modifier shape) - rz-modifier (:resize-modifier shape) - ds-modifier' (:displacement-modifier frame)] - (cond-> shape - (gmt/matrix? ds-modifier') (transform ds-modifier') - (gmt/matrix? rz-modifier) (transform rz-modifier) - frame (move (gpt/point (- (:x frame)) (- (:y frame)))) - (gmt/matrix? ds-modifier) (transform ds-modifier)))) + ([shape] (transform-shape nil shape)) + ([frame shape] + (let [ds-modifier (:displacement-modifier shape) + rz-modifier (:resize-modifier shape) + frame-ds-modifier (:displacement-modifier frame)] + (cond-> shape + (gmt/matrix? rz-modifier) (transform rz-modifier) + frame (move (gpt/point (- (:x frame)) (- (:y frame)))) + (gmt/matrix? frame-ds-modifier) (transform frame-ds-modifier) + (gmt/matrix? ds-modifier) (transform ds-modifier))))) diff --git a/frontend/src/uxbox/main/ui/shapes/frame.cljs b/frontend/src/uxbox/main/ui/shapes/frame.cljs index 4e2e46088..87a4f4336 100644 --- a/frontend/src/uxbox/main/ui/shapes/frame.cljs +++ b/frontend/src/uxbox/main/ui/shapes/frame.cljs @@ -102,17 +102,11 @@ [:& frame-shape {:shape shape :childs childs}]]))))) -(defn frame-shape +(defn frame-shape [shape-wrapper] (mf/fnc frame-shape [{:keys [shape childs] :as props}] - (let [rotation (:rotation shape) - ds-modifier (:displacement-modifier shape) - rz-modifier (:resize-modifier shape) - shape (cond-> shape - (gmt/matrix? rz-modifier) (geom/transform rz-modifier) - (gmt/matrix? ds-modifier) (geom/transform ds-modifier)) - + (let [shape (geom/transform-shape shape) {:keys [id x y width height]} shape props (-> (attrs/extract-style-attrs shape) @@ -126,5 +120,7 @@ [:svg {:x x :y y :width width :height height} [:> "rect" props] (for [item childs] - [:& shape-wrapper {:frame shape :shape item :key (:id item)}])]))) + [:& shape-wrapper {:frame shape + :shape item + :key (:id item)}])]))) From 9d0b71a36c50057470698ff3488e2a9ba91ef028 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 6 Apr 2020 15:19:14 +0200 Subject: [PATCH 2/6] :sparkles: Allow selection of elements inside group --- frontend/src/uxbox/main/data/workspace.cljs | 31 ++++++++++++++++++-- frontend/src/uxbox/main/refs.cljs | 25 ++++++++++++---- frontend/src/uxbox/main/ui/shapes/group.cljs | 28 ++++++++++-------- 3 files changed, 65 insertions(+), 19 deletions(-) diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 843d2191d..afef7e2c1 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -1485,6 +1485,27 @@ (when-not (empty? rch) (rx/of (commit-changes rch uch {:commit-local? true})))))))) +(defn- adjust-group-shapes [state ids] + (let [page-id (::page-id state) + objects (get-in state [:workspace-data page-id :objects]) + groups-to-adjust (->> ids + (map #(helpers/get-parent % objects)) + (map #(get objects %)) + (filter #(= (:type %) :group)) + (map #(:id %)) + distinct) + + update-group + (fn [group] + (let [group-objects (map #(get objects %) (:shapes group)) + selrect (geom/selection-rect group-objects)] + (merge group (select-keys selrect [:x :y :width :height])))) + + reduce-fn + #(update-in %1 [:workspace-data page-id :objects %2] update-group)] + + (reduce reduce-fn state groups-to-adjust))) + (defn assoc-resize-modifier-in-bulk [ids xfmt] (us/verify ::set-of-uuid ids) @@ -1530,7 +1551,10 @@ (-> state (materialize-shape id mtx) (materialize-children id mtx)))))] - (reduce update-shapes state ids))) + + (as-> state $ + (reduce update-shapes $ ids) + (adjust-group-shapes $ ids)))) ptk/WatchEvent (watch [_ state stream] @@ -1558,6 +1582,7 @@ (assoc-in state [:workspace-data page-id :objects id]))))] (reduce rfn state ids))))) + (defn materialize-displacement-in-bulk [ids] (ptk/reify ::materialize-displacement-in-bulk @@ -1590,7 +1615,9 @@ (materialize-shape id mtx) (materialize-children id mtx))))] - (reduce update-shapes state ids))) + (as-> state $ + (reduce update-shapes $ ids) + (adjust-group-shapes $ ids)))) ptk/WatchEvent (watch [_ state stream] diff --git a/frontend/src/uxbox/main/refs.cljs b/frontend/src/uxbox/main/refs.cljs index 2a8fab50c..981cb315c 100644 --- a/frontend/src/uxbox/main/refs.cljs +++ b/frontend/src/uxbox/main/refs.cljs @@ -12,7 +12,8 @@ (:require [lentes.core :as l] [beicon.core :as rx] [uxbox.main.constants :as c] - [uxbox.main.store :as st])) + [uxbox.main.store :as st] + [uxbox.main.data.helpers :as helpers])) (def profile (-> (l/key :profile) @@ -62,10 +63,24 @@ (defn objects-by-id [ids] (let [set-ids (set ids)] - (-> (l/lens #(let [page-id (get-in % [:workspace-page :id]) - objects (get-in % [:workspace-data page-id :objects])] - (filter (fn [it] (set-ids (:id it))) (vals objects)))) - (l/derive st/state)))) + (-> (l/lens (fn [state] + (let [page-id (get-in state [:workspace-page :id]) + objects (get-in state [:workspace-data page-id :objects])] + (mapv #(get objects %) set-ids)))) + (l/derive st/state)))) + +(defn is-child-selected? [id] + (let [is-child-selector + (fn [state] + (let [page-id (get-in state [:workspace-page :id]) + objects (get-in state [:workspace-data page-id :objects]) + selected (get-in state [:workspace-local :selected]) + shape (get objects id) + children (helpers/get-children id objects)] + (some selected children)))] + + (-> (l/lens is-child-selector) + (l/derive st/state)))) (def selected-shapes (-> (l/key :selected) diff --git a/frontend/src/uxbox/main/ui/shapes/group.cljs b/frontend/src/uxbox/main/ui/shapes/group.cljs index d5bb51cab..eaea47d1a 100644 --- a/frontend/src/uxbox/main/ui/shapes/group.cljs +++ b/frontend/src/uxbox/main/ui/shapes/group.cljs @@ -15,7 +15,7 @@ [uxbox.main.ui.shapes.common :as common] [uxbox.main.ui.shapes.attrs :as attrs])) -(defonce ^:dynamic *debug* (atom false)) +(defonce ^:dynamic *debug* (atom true)) (declare translate-to-frame) (declare group-shape) @@ -29,6 +29,7 @@ on-mouse-down #(common/on-mouse-down % shape) on-context-menu #(common/on-context-menu % shape) children (-> (refs/objects-by-id (:shapes shape)) mf/deref) + is-child-selected? (-> (refs/is-child-selected? (:id shape)) mf/deref) on-double-click (fn [event] (dom/stop-propagation event) @@ -40,7 +41,8 @@ :on-double-click on-double-click} [:& (group-shape shape-wrapper) {:frame frame :shape (geom/transform-shape frame shape) - :children children}]]))) + :children children + :is-child-selected? is-child-selected?}]]))) (defn group-shape [shape-wrapper] (mf/fnc group-shape @@ -49,6 +51,7 @@ (let [frame (unchecked-get props "frame") shape (unchecked-get props "shape") children (unchecked-get props "children") + is-child-selected? (unchecked-get props "is-child-selected?") {:keys [id x y width height rotation displacement-modifier resize-modifier]} shape @@ -61,17 +64,18 @@ [:g {:transform transform} (for [item children] [:& shape-wrapper {:frame frame - :shape (-> item - (assoc :displacement-modifier displacement-modifier) - (assoc :resize-modifier resize-modifier)) + :shape (cond-> item + displacement-modifier (assoc :displacement-modifier displacement-modifier) + resize-modifier (assoc :resize-modifier resize-modifier)) :key (:id item)}]) - [:rect {:x x - :y y - :fill (if (deref *debug*) "red" "transparent") - :opacity 0.8 - :id (str "group-" id) - :width width - :height height}]]))) + (when (not is-child-selected?) + [:rect {:x x + :y y + :fill (if (deref *debug*) "red" "transparent") + :opacity 0.8 + :id (str "group-" id) + :width width + :height height}])]))) From f99b3bfdb8473eeac002c2554424a7594ffadbf5 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 6 Apr 2020 15:39:28 +0200 Subject: [PATCH 3/6] :sparkles: Adds options menu for groups --- .../main/ui/workspace/sidebar/options.cljs | 2 + .../ui/workspace/sidebar/options/group.cljs | 20 +++ .../workspace/sidebar/options/measures.cljs | 145 ++++++++++++++++++ .../ui/workspace/sidebar/options/rect.cljs | 137 +---------------- 4 files changed, 168 insertions(+), 136 deletions(-) create mode 100644 frontend/src/uxbox/main/ui/workspace/sidebar/options/group.cljs create mode 100644 frontend/src/uxbox/main/ui/workspace/sidebar/options/measures.cljs diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options.cljs index 17701a409..09f08a852 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options.cljs @@ -14,6 +14,7 @@ [uxbox.main.store :as st] [uxbox.main.refs :as refs] [uxbox.main.ui.workspace.sidebar.options.frame :as frame] + [uxbox.main.ui.workspace.sidebar.options.group :as group] [uxbox.main.ui.workspace.sidebar.options.rect :as rect] [uxbox.main.ui.workspace.sidebar.options.icon :as icon] [uxbox.main.ui.workspace.sidebar.options.circle :as circle] @@ -30,6 +31,7 @@ [:div (case (:type shape) :frame [:& frame/options {:shape shape}] + :group [:& group/options {:shape shape}] :text [:& text/options {:shape shape}] :rect [:& rect/options {:shape shape}] :icon [:& icon/options {:shape shape}] diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options/group.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options/group.cljs new file mode 100644 index 000000000..df0c4f0a4 --- /dev/null +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options/group.cljs @@ -0,0 +1,20 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2015-2020 Andrey Antukh +;; Copyright (c) 2015-2020 Juan de la Cruz + +(ns uxbox.main.ui.workspace.sidebar.options.group + (:require + [rumext.alpha :as mf] + [uxbox.main.ui.workspace.sidebar.options.measures :refer [measures-menu]])) + +(mf/defc options + [{:keys [shape] :as props}] + [:div + [:& measures-menu {:shape shape}]]) + diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options/measures.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options/measures.cljs new file mode 100644 index 000000000..1df1d64d7 --- /dev/null +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options/measures.cljs @@ -0,0 +1,145 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns uxbox.main.ui.workspace.sidebar.options.measures + (:require + [rumext.alpha :as mf] + [uxbox.builtins.icons :as i] + [uxbox.main.store :as st] + [uxbox.common.data :as d] + [uxbox.util.dom :as dom] + [uxbox.main.data.workspace :as udw] + [uxbox.util.math :as math] + [uxbox.util.i18n :refer [t] :as i18n])) + +(mf/defc measures-menu + [{:keys [shape] :as props}] + (let [locale (i18n/use-locale) + + on-size-change + (fn [event attr] + (let [value (-> (dom/get-target event) + (dom/get-value) + (d/parse-integer 0))] + (st/emit! (udw/update-rect-dimensions (:id shape) attr value)))) + + on-proportion-lock-change + (fn [event] + (st/emit! (udw/toggle-shape-proportion-lock (:id shape)))) + + on-position-change + (fn [event attr] + (let [value (-> (dom/get-target event) + (dom/get-value) + (d/parse-integer 0))] + (st/emit! (udw/update-position (:id shape) {attr value})))) + + on-rotation-change + (fn [event] + (let [value (-> (dom/get-target event) + (dom/get-value) + (d/parse-integer 0))] + (st/emit! (udw/update-shape (:id shape) {:rotation value})))) + + on-radius-change + (fn [event] + (let [value (-> (dom/get-target event) + (dom/get-value) + (d/parse-integer 0))] + (st/emit! (udw/update-shape (:id shape) {:rx value :ry value})))) + + on-width-change #(on-size-change % :width) + on-height-change #(on-size-change % :height) + on-pos-x-change #(on-position-change % :x) + on-pos-y-change #(on-position-change % :y)] + + [:div.element-set + [:div.element-set-content + + ;; WIDTH & HEIGHT + [:div.row-flex + [:span.element-set-subtitle (t locale "workspace.options.size")] + [:div.lock-size {:class (when (:proportion-lock shape) "selected") + :on-click on-proportion-lock-change} + (if (:proportion-lock shape) + i/lock + i/unlock)] + [:div.input-element.pixels + [:input.input-text {:type "number" + :min "0" + :no-validate true + :on-change on-width-change + :value (str (-> (:width shape) + (d/coalesce 0) + (math/round)))}]] + + + [:div.input-element.pixels + [:input.input-text {:type "number" + :min "0" + :no-validate true + :on-change on-height-change + :value (str (-> (:height shape) + (d/coalesce 0) + (math/round)))}]]] + + ;; POSITION + [:div.row-flex + [:span.element-set-subtitle (t locale "workspace.options.position")] + [:div.input-element.pixels + [:input.input-text {:placeholder "x" + :type "number" + :no-validate true + :on-change on-pos-x-change + :value (str (-> (:x shape) + (d/coalesce 0) + (math/round)))}]] + [:div.input-element.pixels + [:input.input-text {:placeholder "y" + :type "number" + :no-validate true + :on-change on-pos-y-change + :value (str (-> (:y shape) + (d/coalesce 0) + (math/round)))}]]] + + [:div.row-flex + [:span.element-set-subtitle (t locale "workspace.options.rotation")] + [:div.input-element.degrees + [:input.input-text + {:placeholder "" + :type "number" + :no-validate true + :min "0" + :max "360" + :on-change on-rotation-change + :value (str (-> (:rotation shape) + (d/coalesce 0) + (math/round)))}]] + [:input.slidebar + {:type "range" + :min "0" + :max "360" + :step "1" + :no-validate true + :on-change on-rotation-change + :value (str (-> (:rotation shape) + (d/coalesce 0)))}]] + + [:div.row-flex + [:span.element-set-subtitle (t locale "workspace.options.radius")] + [:div.input-element.pixels + [:input.input-text + {:placeholder "rx" + :type "number" + :on-change on-radius-change + :value (str (-> (:rx shape) + (d/coalesce 0) + (math/round)))}]] + [:div.input-element]]]])) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options/rect.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options/rect.cljs index 1e694142b..f75b68014 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/options/rect.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options/rect.cljs @@ -10,144 +10,9 @@ (ns uxbox.main.ui.workspace.sidebar.options.rect (:require [rumext.alpha :as mf] - [uxbox.common.data :as d] - [uxbox.builtins.icons :as i] - [uxbox.main.data.workspace :as udw] - [uxbox.main.geom :as geom] - [uxbox.main.store :as st] [uxbox.main.ui.workspace.sidebar.options.fill :refer [fill-menu]] [uxbox.main.ui.workspace.sidebar.options.stroke :refer [stroke-menu]] - [uxbox.util.dom :as dom] - [uxbox.util.geom.point :as gpt] - [uxbox.util.i18n :as i18n :refer [tr t]] - [uxbox.util.math :as math])) - -(mf/defc measures-menu - [{:keys [shape] :as props}] - (let [locale (i18n/use-locale) - - on-size-change - (fn [event attr] - (let [value (-> (dom/get-target event) - (dom/get-value) - (d/parse-integer 0))] - (st/emit! (udw/update-rect-dimensions (:id shape) attr value)))) - - on-proportion-lock-change - (fn [event] - (st/emit! (udw/toggle-shape-proportion-lock (:id shape)))) - - on-position-change - (fn [event attr] - (let [value (-> (dom/get-target event) - (dom/get-value) - (d/parse-integer 0))] - (st/emit! (udw/update-position (:id shape) {attr value})))) - - on-rotation-change - (fn [event] - (let [value (-> (dom/get-target event) - (dom/get-value) - (d/parse-integer 0))] - (st/emit! (udw/update-shape (:id shape) {:rotation value})))) - - on-radius-change - (fn [event] - (let [value (-> (dom/get-target event) - (dom/get-value) - (d/parse-integer 0))] - (st/emit! (udw/update-shape (:id shape) {:rx value :ry value})))) - - on-width-change #(on-size-change % :width) - on-height-change #(on-size-change % :height) - on-pos-x-change #(on-position-change % :x) - on-pos-y-change #(on-position-change % :y)] - - [:div.element-set - [:div.element-set-content - - ;; WIDTH & HEIGHT - [:div.row-flex - [:span.element-set-subtitle (t locale "workspace.options.size")] - [:div.lock-size {:class (when (:proportion-lock shape) "selected") - :on-click on-proportion-lock-change} - (if (:proportion-lock shape) - i/lock - i/unlock)] - [:div.input-element.pixels - [:input.input-text {:type "number" - :min "0" - :no-validate true - :on-change on-width-change - :value (str (-> (:width shape) - (d/coalesce 0) - (math/round)))}]] - - - [:div.input-element.pixels - [:input.input-text {:type "number" - :min "0" - :no-validate true - :on-change on-height-change - :value (str (-> (:height shape) - (d/coalesce 0) - (math/round)))}]]] - - ;; POSITION - [:div.row-flex - [:span.element-set-subtitle (t locale "workspace.options.position")] - [:div.input-element.pixels - [:input.input-text {:placeholder "x" - :type "number" - :no-validate true - :on-change on-pos-x-change - :value (str (-> (:x shape) - (d/coalesce 0) - (math/round)))}]] - [:div.input-element.pixels - [:input.input-text {:placeholder "y" - :type "number" - :no-validate true - :on-change on-pos-y-change - :value (str (-> (:y shape) - (d/coalesce 0) - (math/round)))}]]] - - [:div.row-flex - [:span.element-set-subtitle (t locale "workspace.options.rotation")] - [:div.input-element.degrees - [:input.input-text - {:placeholder "" - :type "number" - :no-validate true - :min "0" - :max "360" - :on-change on-rotation-change - :value (str (-> (:rotation shape) - (d/coalesce 0) - (math/round)))}]] - [:input.slidebar - {:type "range" - :min "0" - :max "360" - :step "1" - :no-validate true - :on-change on-rotation-change - :value (str (-> (:rotation shape) - (d/coalesce 0)))}]] - - [:div.row-flex - [:span.element-set-subtitle (t locale "workspace.options.radius")] - [:div.input-element.pixels - [:input.input-text - {:placeholder "rx" - :type "number" - :on-change on-radius-change - :value (str (-> (:rx shape) - (d/coalesce 0) - (math/round)))}]] - [:div.input-element]]]])) - + [uxbox.main.ui.workspace.sidebar.options.measures :refer [measures-menu]])) (mf/defc options [{:keys [shape] :as props}] From 185b1f9ee101f798b4b3af4b87fb413c4772e8a2 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 7 Apr 2020 09:15:35 +0200 Subject: [PATCH 4/6] :sparkles: Allows selection of inside items with double click --- frontend/src/uxbox/main/data/workspace.cljs | 13 +++++++++++++ frontend/src/uxbox/main/geom.cljs | 14 ++++++++++++++ frontend/src/uxbox/main/ui/shapes/group.cljs | 13 +++++++++---- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index afef7e2c1..fcc4253ae 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -1203,6 +1203,19 @@ (->> (impl-match-by-selrect state selrect) (assoc-in state [:workspace-local :selected])))))) +(defn select-inside-group + [group-id position] + (ptk/reify ::select-inside-group + ptk/UpdateEvent + (update [_ state] + (let [page-id (::page-id state) + objects (get-in state [:workspace-data page-id :objects]) + group (get objects group-id) + children (map #(get objects %) (:shapes group)) + selected (->> children (filter #(geom/has-point? % position)) first)] + (cond-> state + selected (assoc-in [:workspace-local :selected] #{(:id selected)})))))) + ;; --- Update Shape Attrs (defn update-shape diff --git a/frontend/src/uxbox/main/geom.cljs b/frontend/src/uxbox/main/geom.cljs index b8023a064..0558e826b 100644 --- a/frontend/src/uxbox/main/geom.cljs +++ b/frontend/src/uxbox/main/geom.cljs @@ -587,6 +587,20 @@ (< ry1 sy2) (> ry2 sy1)))) +(defn has-point? + [shape position] + (let [{:keys [x y]} position + selrect {:x1 (- x 5) + :y1 (- y 5) + :x2 (+ x 5) + :y2 (+ y 5) + :x (- x 5) + :y (- y 5) + :width 10 + :height 10 + :type :rect}] + (overlaps? shape selrect))) + (defn transform-shape ([shape] (transform-shape nil shape)) ([frame shape] diff --git a/frontend/src/uxbox/main/ui/shapes/group.cljs b/frontend/src/uxbox/main/ui/shapes/group.cljs index eaea47d1a..cdf73601e 100644 --- a/frontend/src/uxbox/main/ui/shapes/group.cljs +++ b/frontend/src/uxbox/main/ui/shapes/group.cljs @@ -13,9 +13,12 @@ [uxbox.util.dom :as dom] [uxbox.util.interop :as itr] [uxbox.main.ui.shapes.common :as common] - [uxbox.main.ui.shapes.attrs :as attrs])) + [uxbox.main.ui.shapes.attrs :as attrs] + [uxbox.main.data.workspace :as dw] + [uxbox.main.store :as st] + [uxbox.main.streams :as ms])) -(defonce ^:dynamic *debug* (atom true)) +(defonce ^:dynamic *debug* (atom false)) (declare translate-to-frame) (declare group-shape) @@ -34,7 +37,9 @@ (fn [event] (dom/stop-propagation event) (dom/prevent-default event) - #_(st/emit! (dw/select-inside-group)))] + (st/emit! (dw/select-inside-group + (:id shape) + @ms/mouse-position)))] [:g.shape {:on-mouse-down on-mouse-down :on-context-menu on-context-menu @@ -73,7 +78,7 @@ [:rect {:x x :y y :fill (if (deref *debug*) "red" "transparent") - :opacity 0.8 + :opacity 0.5 :id (str "group-" id) :width width :height height}])]))) From 20c6ae867b5b6bdffa82a1e47a1b5963a5003d02 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 7 Apr 2020 14:33:05 +0200 Subject: [PATCH 5/6] :bug: Fixes problem when moving child --- frontend/src/uxbox/main/data/helpers.cljs | 17 +++++++++++++++++ frontend/src/uxbox/main/data/workspace.cljs | 9 +++++---- .../src/uxbox/main/ui/workspace/selection.cljs | 9 +++++++-- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/frontend/src/uxbox/main/data/helpers.cljs b/frontend/src/uxbox/main/data/helpers.cljs index 9a3009aaa..b0ab21386 100644 --- a/frontend/src/uxbox/main/data/helpers.cljs +++ b/frontend/src/uxbox/main/data/helpers.cljs @@ -37,6 +37,23 @@ (:id shape)))] (some check-parenthood (vals objects)))) +(defn calculate-child-parent-map + [objects] + (let [red-fn + (fn [acc {:keys [id shapes]}] + ;; Insert every pair shape -> parent into accumulated value + (into acc (map #(vector % id) (or shapes []))))] + (reduce red-fn {} (vals objects)))) + +(defn get-all-parents + [shape-id objects] + (let [child->parent (calculate-child-parent-map objects) + rec-fn (fn [cur result] + (if-let [parent (child->parent cur)] + (recur parent (conj result parent)) + (vec (reverse result))))] + (rec-fn shape-id []))) + (defn replace-shapes "Replace inside shapes the value `to-replace-id` for the value in items keeping the same order. `to-replace-id` can be a set, a sequable or a single value. Any of these will be changed into a diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index fcc4253ae..00f2b822b 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -1502,20 +1502,21 @@ (let [page-id (::page-id state) objects (get-in state [:workspace-data page-id :objects]) groups-to-adjust (->> ids - (map #(helpers/get-parent % objects)) + (mapcat #(reverse (helpers/get-all-parents % objects))) (map #(get objects %)) (filter #(= (:type %) :group)) (map #(:id %)) distinct) update-group - (fn [group] - (let [group-objects (map #(get objects %) (:shapes group)) + (fn [state group] + (let [objects (get-in state [:workspace-data page-id :objects]) + group-objects (map #(get objects %) (:shapes group)) selrect (geom/selection-rect group-objects)] (merge group (select-keys selrect [:x :y :width :height])))) reduce-fn - #(update-in %1 [:workspace-data page-id :objects %2] update-group)] + #(update-in %1 [:workspace-data page-id :objects %2] (partial update-group %1))] (reduce reduce-fn state groups-to-adjust))) diff --git a/frontend/src/uxbox/main/ui/workspace/selection.cljs b/frontend/src/uxbox/main/ui/workspace/selection.cljs index 68b97766f..eb093f0d2 100644 --- a/frontend/src/uxbox/main/ui/workspace/selection.cljs +++ b/frontend/src/uxbox/main/ui/workspace/selection.cljs @@ -250,10 +250,15 @@ [{:keys [shapes selected zoom] :as props}] (let [shape (geom/selection-rect shapes) on-resize #(do (dom/stop-propagation %2) - (st/emit! (start-resize %1 selected shape)))] + (st/emit! (start-resize %1 selected shape))) + + on-rotate #(do (dom/stop-propagation %) + (println "ROTATE!"))] + [:& controls {:shape shape :zoom zoom - :on-resize on-resize}])) + :on-resize on-resize + :on-rotate on-rotate}])) (mf/defc single-selection-handlers [{:keys [shape zoom objects] :as props}] From e6200aae4cfd3512141e1926a56b9cc6c4103b2d Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 8 Apr 2020 10:43:45 +0200 Subject: [PATCH 6/6] :sparkles: Adds new method to move objects --- .../tests/uxbox/tests/test_common_pages.clj | 159 ++++++++++++++++-- common/uxbox/common/data.cljc | 5 + common/uxbox/common/pages.cljc | 84 ++++++++- frontend/src/uxbox/main/data/workspace.cljs | 68 ++++---- 4 files changed, 260 insertions(+), 56 deletions(-) diff --git a/backend/tests/uxbox/tests/test_common_pages.clj b/backend/tests/uxbox/tests/test_common_pages.clj index 7d3347d49..bfd40df0e 100644 --- a/backend/tests/uxbox/tests/test_common_pages.clj +++ b/backend/tests/uxbox/tests/test_common_pages.clj @@ -15,19 +15,42 @@ (t/deftest process-change-add-obj-1 (let [data cp/default-page-data - id (uuid/next) - chg {:type :add-obj - :id id - :frame-id uuid/zero - :obj {:id id - :frame-id uuid/zero - :type :rect - :name "rect"}} - res (cp/process-changes data [chg])] + id-a (uuid/next) + id-b (uuid/next) + id-c (uuid/next)] + (t/testing "Adds single object" + (let [chg {:type :add-obj + :id id-a + :frame-id uuid/zero + :obj {:id id-a + :frame-id uuid/zero + :type :rect + :name "rect"}} + res (cp/process-changes data [chg])] - (t/is (= 2 (count (:objects res)))) - (t/is (= (:obj chg) (get-in res [:objects id]))) - (t/is (= [id] (get-in res [:objects uuid/zero :shapes]))))) + (t/is (= 2 (count (:objects res)))) + (t/is (= (:obj chg) (get-in res [:objects id-a]))) + (t/is (= [id-a] (get-in res [:objects uuid/zero :shapes]))))) + (t/testing "Adds several objects with different indexes" + (let [data cp/default-page-data + + chg (fn [id index] {:type :add-obj + :id id + :frame-id uuid/zero + :index index + :obj {:id id + :frame-id uuid/zero + :type :rect + :name (str id)}}) + res (cp/process-changes data [(chg id-a 0) + (chg id-b 0) + (chg id-c 1)])] + + (t/is (= 4 (count (:objects res)))) + (t/is (not (nil? (get-in res [:objects id-a])))) + (t/is (not (nil? (get-in res [:objects id-b])))) + (t/is (not (nil? (get-in res [:objects id-c])))) + (t/is (= [id-b id-c id-a] (get-in res [:objects uuid/zero :shapes]))))))) (t/deftest process-change-mod-obj (let [data cp/default-page-data @@ -177,3 +200,115 @@ (t/is (= [id3 id1 id2] (get-in res [:objects uuid/zero :shapes]))))) )) +(t/deftest process-changes-move-objects + (let [frame-a-id (uuid/next) + frame-b-id (uuid/next) + group-a-id (uuid/next) + group-b-id (uuid/next) + rect-a-id (uuid/next) + rect-b-id (uuid/next) + rect-c-id (uuid/next) + rect-d-id (uuid/next) + rect-e-id (uuid/next) + data (-> cp/default-page-data + (assoc-in [cp/root :shapes] [frame-a-id]) + (assoc-in [:objects frame-a-id] {:id frame-a-id :name "Frame a" :type :frame}) + (assoc-in [:objects frame-b-id] {:id frame-b-id :name "Frame b" :type :frame}) + + ;; Groups + (assoc-in [:objects group-a-id] {:id group-a-id :name "Group A" :type :group :frame-id frame-a-id}) + (assoc-in [:objects group-b-id] {:id group-b-id :name "Group B" :type :group :frame-id frame-a-id}) + + ;; Shapes + (assoc-in [:objects rect-a-id] {:id rect-a-id :name "Rect A" :type :rect :frame-id frame-a-id}) + (assoc-in [:objects rect-b-id] {:id rect-b-id :name "Rect B" :type :rect :frame-id frame-a-id}) + (assoc-in [:objects rect-c-id] {:id rect-c-id :name "Rect C" :type :rect :frame-id frame-a-id}) + (assoc-in [:objects rect-d-id] {:id rect-d-id :name "Rect D" :type :rect :frame-id frame-a-id}) + (assoc-in [:objects rect-e-id] {:id rect-e-id :name "Rect E" :type :rect :frame-id frame-a-id}) + + ;; Relationships + (assoc-in [:objects cp/root :shapes] [frame-a-id frame-b-id]) + (assoc-in [:objects frame-a-id :shapes] [group-a-id group-b-id rect-e-id]) + (assoc-in [:objects group-a-id :shapes] [rect-a-id rect-b-id rect-c-id]) + (assoc-in [:objects group-b-id :shapes] [rect-d-id]))] + (t/testing "Create new group an add objects from the same group" + (let [new-group-id (uuid/next) + changes [{:type :add-obj + :id new-group-id + :frame-id frame-a-id + :obj {:id new-group-id + :type :group + :frame-id frame-a-id + :name "Group C"}} + {:type :mov-objects + :parent-id new-group-id + :shapes [rect-b-id rect-c-id]}] + res (cp/process-changes data changes)] + (t/is (= [group-a-id group-b-id rect-e-id new-group-id] (get-in res [:objects frame-a-id :shapes]))) + (t/is (= [rect-b-id rect-c-id] (get-in res [:objects new-group-id :shapes]))) + (t/is (= [rect-a-id] (get-in res [:objects group-a-id :shapes]))))) + + (t/testing "Move elements to an existing group at index" + (let [changes [{:type :mov-objects + :parent-id group-b-id + :index 0 + :shapes [rect-a-id rect-c-id]}] + res (cp/process-changes data changes)] + (t/is (= [group-a-id group-b-id rect-e-id] (get-in res [:objects frame-a-id :shapes]))) + (t/is (= [rect-b-id] (get-in res [:objects group-a-id :shapes]))) + (t/is (= [rect-a-id rect-c-id rect-d-id] (get-in res [:objects group-b-id :shapes]))))) + + (t/testing "Move elements from group and frame to an existing group at index" + (let [changes [{:type :mov-objects + :parent-id group-b-id + :index 0 + :shapes [rect-a-id rect-e-id]}] + res (cp/process-changes data changes)] + (t/is (= [group-a-id group-b-id] (get-in res [:objects frame-a-id :shapes]))) + (t/is (= [rect-b-id rect-c-id] (get-in res [:objects group-a-id :shapes]))) + (t/is (= [rect-a-id rect-e-id rect-d-id] (get-in res [:objects group-b-id :shapes]))))) + + (t/testing "Move elements from several groups" + (let [changes [{:type :mov-objects + :parent-id group-b-id + :index 0 + :shapes [rect-a-id rect-e-id]}] + res (cp/process-changes data changes)] + (t/is (= [group-a-id group-b-id] (get-in res [:objects frame-a-id :shapes]))) + (t/is (= [rect-b-id rect-c-id] (get-in res [:objects group-a-id :shapes]))) + (t/is (= [rect-a-id rect-e-id rect-d-id] (get-in res [:objects group-b-id :shapes]))))) + + (t/testing "Move elements and delete the empty group" + (let [changes [{:type :mov-objects + :parent-id group-a-id + :shapes [rect-d-id]}] + res (cp/process-changes data changes)] + (t/is (= [group-a-id rect-e-id] (get-in res [:objects frame-a-id :shapes]))) + (t/is (nil? (get-in res [:objects group-b-id]))))) + + (t/testing "Move elements to a group with different frame" + (let [changes [{:type :mov-objects + :parent-id frame-b-id + :shapes [group-a-id]}] + res (cp/process-changes data changes)] + (t/is (= [group-b-id rect-e-id] (get-in res [:objects frame-a-id :shapes]))) + (t/is (= [group-a-id] (get-in res [:objects frame-b-id :shapes]))) + (t/is (= frame-b-id (get-in res [:objects group-a-id :frame-id]))) + (t/is (= frame-b-id (get-in res [:objects rect-a-id :frame-id]))) + (t/is (= frame-b-id (get-in res [:objects rect-b-id :frame-id]))) + (t/is (= frame-b-id (get-in res [:objects rect-c-id :frame-id]))))) + + (t/testing "Move elements to frame zero" + (let [changes [{:type :mov-objects + :parent-id cp/root + :shapes [group-a-id] + :index 0}] + res (cp/process-changes data changes)] + (t/is (= [group-a-id frame-a-id frame-b-id] (get-in res [:objects cp/root :shapes]))))) + + (t/testing "Don't allow to move inside self" + (let [changes [{:type :mov-objects + :parent-id group-a-id + :shapes [group-a-id]}] + res (cp/process-changes data changes)] + (t/is (= data res)))))) diff --git a/common/uxbox/common/data.cljc b/common/uxbox/common/data.cljc index d1c4bb607..ade40bfcc 100644 --- a/common/uxbox/common/data.cljc +++ b/common/uxbox/common/data.cljc @@ -90,6 +90,11 @@ (persistent! (reduce #(dissoc! %1 %2) (transient data) keys))) +(defn insert-at-index [vector index elements] + (let [[before after] (split-at index vector)] + (concat [] before elements after))) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Parsing / Conversion ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/common/uxbox/common/pages.cljc b/common/uxbox/common/pages.cljc index a04a09ba9..49ffe4d0d 100644 --- a/common/uxbox/common/pages.cljc +++ b/common/uxbox/common/pages.cljc @@ -25,6 +25,7 @@ (s/def ::shape-id uuid?) (s/def ::session-id uuid?) (s/def ::name string?) +(s/def ::parent-id uuid?) ;; Page Options (s/def ::grid-x number?) @@ -151,6 +152,10 @@ (s/keys :req-un [::id ::frame-id] :opt-un [::session-id])) +(defmethod change-spec-impl :mov-objects [_] + (s/keys :req-un [::parent-id ::shapes] + :opt-un [::index])) + (s/def ::change (s/multi-spec change-spec-impl :type)) (s/def ::changes (s/coll-of ::change)) @@ -201,15 +206,9 @@ (update-in [:objects frame-id :shapes] (fn [shapes] (cond - (some #{id} shapes) - shapes - - (nil? index) - (conj shapes id) - - :else - (let [[before after] (split-at index shapes)] - (d/concat [] before [id] after)))))))) + (some #{id} shapes) shapes + (nil? index) (conj shapes id) + :else (d/insert-at-index shapes index [id]))))))) (defmethod process-change :mod-obj [data {:keys [id operations] :as change}] @@ -238,6 +237,73 @@ (seq shapes) ; Recursive delete all dependend objects (as-> $ (reduce #(or (process-change %1 {:type :del-obj :id %2}) %1) $ shapes)))))) +(defn- calculate-child-parent-map + [objects] + (let [red-fn + (fn [acc {:keys [id shapes]}] + ;; Insert every pair shape -> parent into accumulated value + (into acc (map #(vector % id) (or shapes []))))] + (reduce red-fn {} (vals objects)))) + +(defn- calculate-invalid-targets [shape-id objects] + (let [result #{shape-id} + children (get-in objects [shape-id :shape]) + reduce-fn (fn [result child-id] + (into result (calculate-invalid-targets child-id objects)))] + (reduce reduce-fn result children))) + +(defmethod process-change :mov-objects + [data {:keys [parent-id shapes index] :as change}] + (let [child->parent (calculate-child-parent-map (:objects data)) + ;; Check if the move from shape-id -> parent-id is valid + is-valid-move + (fn [shape-id] + (let [invalid (calculate-invalid-targets shape-id (:objects data))] + (not (invalid parent-id)))) + + valid? (every? is-valid-move shapes) + + ;; Add items into the :shapes property of the target parent-id + add-items + (fn [old-shapes] + (let [old-shapes (or old-shapes [])] + (if index + (d/insert-at-index old-shapes index shapes) + (into old-shapes shapes)))) + + ;; Remove from the old :shapes the references that have been moved + remove-in-parent + (fn [data shape-id] + (let [parent-id (child->parent shape-id) + filter-shape (partial filterv #(not (= % shape-id)) ) + data-removed (update-in data [:objects parent-id :shapes] filter-shape) + parent (get-in data-removed [:objects parent-id])] + ;; When the group is empty we should remove it + (if (and (= :group (:type parent)) + (empty? (:shapes parent))) + (-> data-removed + (update :objects dissoc parent-id ) + (recur parent-id)) + data-removed))) + + ;; Frame-id of the target element + frame-id (if (= :frame (get-in data [:objects parent-id :type])) + parent-id + (get-in data [:objects parent-id :frame-id])) + + ;; Updates the frame-id references that might be outdated + update-frame-ids + (fn update-frame-ids [data shape-id] + (as-> data $ + (assoc-in $ [:objects shape-id :frame-id] frame-id) + (reduce update-frame-ids $ (get-in $ [:objects shape-id :shapes]))))] + (if valid? + (as-> data $ + (update-in $ [:objects parent-id :shapes] add-items) + (reduce remove-in-parent $ shapes) + (reduce update-frame-ids $ (get-in $ [:objects parent-id :shapes]))) + data))) + (defmethod process-operation :set [shape op] (let [attr (:attr op) diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 00f2b822b..e66585658 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -2211,7 +2211,7 @@ {:id id :type :group :name (name (gensym "Group-")) - :shapes (vec selected) + :shapes [] :frame-id frame-id :x (:x selection-rect) :y (:y selection-rect) @@ -2227,30 +2227,26 @@ (if (not-empty selected) (let [page-id (get-in state [:workspace-page :id]) objects (get-in state [:workspace-data page-id :objects]) - parent (get-parent (first selected) (vals objects)) - parent-id (:id parent) selected-objects (map (partial get objects) selected) selection-rect (geom/selection-rect selected-objects) frame-id (-> selected-objects first :frame-id) - group-shape (group-shape id frame-id selected selection-rect)] + group-shape (group-shape id frame-id selected selection-rect) + frame-children (get-in objects [frame-id :shapes]) + index-frame (->> frame-children (map-indexed vector) (filter #(selected (second %))) first first)] - (let [updated-parent (helpers/replace-shapes parent selected id) - rchanges [{:type :add-obj + (let [rchanges [{:type :add-obj :id id :frame-id frame-id - :obj group-shape} - {:type :mod-obj - :id parent-id - :operations [{:type :set - :attr :shapes - :val (:shapes updated-parent)}]}] - uchanges [{:type :del-obj - :id id} - {:type :mod-obj - :id parent-id - :operations [{:type :set - :attr :shapes - :val (:shapes parent)}]}]] + :obj group-shape + :index index-frame} + {:type :mov-objects + :parent-id id + :shapes (into [] selected)}] + uchanges [{:type :mov-objects + :parent-id frame-id + :shapes (into [] selected)} + {:type :del-obj + :id id}]] (rx/of (commit-changes rchanges uchanges {:commit-local? true}) (fn [state] (assoc-in state [:workspace-local :selected] #{id}))))) rx/empty)))))) @@ -2264,26 +2260,28 @@ group (get-in state [:workspace-data (::page-id state) :objects group-id])] (if (and (= (count selected) 1) (= (:type group) :group)) (let [objects (get-in state [:workspace-data (::page-id state) :objects]) + shapes (get-in objects [group-id :shapes]) parent-id (helpers/get-parent group-id objects) - parent (get objects parent-id)] - (let [changed-parent (helpers/replace-shapes parent group-id (:shapes group)) - rchanges [{:type :mod-obj - :id parent-id - :operations [{:type :set :attr :shapes :val (:shapes changed-parent)}]} - - ;; Need to modify the object otherwise the children will be deleted - {:type :mod-obj - :id group-id - :operations [{:type :set :attr :shapes :val []}]} - {:type :del-obj - :id group-id}] + parent (get objects parent-id) + index-in-parent (->> (:shapes parent) + (map-indexed vector) + (filter #(#{group-id} (second %))) + first first)] + (let [rchanges [{:type :mov-objects + :parent-id parent-id + :shapes shapes + :index index-in-parent}] uchanges [{:type :add-obj :id group-id :frame-id (:frame-id group) - :obj group} - {:type :mod-obj - :id parent-id - :operations [{:type :set :attr :shapes :val (:shapes parent)}]}]] + :obj (assoc group :shapes [])} + {:type :mov-objects + :parent-id group-id + :shapes shapes} + {:type :mov-objects + :parent-id parent-id + :shapes [group-id] + :index index-in-parent}]] (rx/of (commit-changes rchanges uchanges {:commit-local? true})))) rx/empty)))))