diff --git a/common/uxbox/common/pages.cljc b/common/uxbox/common/pages.cljc index 69875531d..413f98373 100644 --- a/common/uxbox/common/pages.cljc +++ b/common/uxbox/common/pages.cljc @@ -222,7 +222,7 @@ (update-in [:objects frame-id :shapes] (fn [s] (filterv #(not= % id) s))) (seq shapes) ; Recursive delete all dependend objects - (as-> $ (reduce #(process-change %1 {:type :del-obj :id %2}) $ shapes)))))) + (as-> $ (reduce #(or (process-change %1 {:type :del-obj :id %2}) %1) $ shapes)))))) (defmethod process-operation :set [shape op] diff --git a/frontend/src/uxbox/main/data/helpers.cljs b/frontend/src/uxbox/main/data/helpers.cljs index d51a19a2a..9a3009aaa 100644 --- a/frontend/src/uxbox/main/data/helpers.cljs +++ b/frontend/src/uxbox/main/data/helpers.cljs @@ -16,5 +16,54 @@ (if shapes (concat shapes - (mapcat get-children shapes)) + (mapcat #(get-children % objects) shapes)) []))) + +(defn is-shape-grouped + "Checks if a shape is inside a group" + [shape-id objects] + (let [contains-shape-fn + (fn [{:keys [shapes]}] ((set shapes) shape-id)) + + shapes (remove #(= (:type %) :frame) (vals objects))] + (some contains-shape-fn shapes))) + +(defn get-parent + "Retrieve the id of the parent for the shape-id (if exists" + [shape-id objects] + (let [check-parenthood + (fn [shape] (when (and (:shapes shape) + ((set (:shapes shape)) shape-id)) + (:id shape)))] + (some check-parenthood (vals objects)))) + +(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 + set to make the replacement" + [shape to-replace-id items] + (let [should-replace + (cond + (set? to-replace-id) to-replace-id + (seqable? to-replace-id) (set to-replace-id) + :else #{to-replace-id}) + + ;; This function replaces the first ocurrence of the set `should-replace` for the + ;; value in `items`. Next elements that match are removed but not replaced again + ;; so for example: + ;; should-replace = #{2 3 5} + ;; (replace-fn [ 1 2 3 4 5] ["a" "b"] []) + ;; => [ 1 "a" "b" 4 ] + replace-fn + (fn [to-replace acc shapes] + (if (empty? shapes) + acc + (let [cur (first shapes) + rest (subvec shapes 1)] + (if (should-replace cur) + (recur [] (into acc to-replace) rest) + (recur to-replace (conj acc cur) rest))))) + + replace-shapes (partial replace-fn (if (seqable? items) items [items]) [])] + + (update shape :shapes replace-shapes))) diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 486a7ae73..7a6b28137 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -1001,7 +1001,9 @@ (assoc :id id)) frame-id (calculate-frame-overlap objects shape) shape (merge shape-default-attrs shape {:frame-id frame-id})] - (impl-assoc-shape state shape))) + (-> state + (impl-assoc-shape shape) + (assoc-in [:workspace-local :selected] #{id})))) ptk/WatchEvent (watch [_ state stream] @@ -1185,6 +1187,9 @@ data (get-in state [:workspace-data page-id]) match (fn [acc {:keys [type id] :as shape}] (cond + (helpers/is-shape-grouped (:id shape) (:objects data)) + acc + (geom/contained-in? shape selrect) (conj acc id) @@ -1347,7 +1352,6 @@ shapes (map lookup selected) shape? #(not= (:type %) :frame)] - (cond (and (= (count shapes) 1) (= (:type (first shapes)) :frame)) @@ -1951,7 +1955,7 @@ (watch [_ state stream] (let [project-id (get-in state [:workspace-project :id]) file-id (get-in state [:workspace-page :file-id]) - path-params {:file-id file-id} + path-params {:project-id project-id :file-id file-id} query-params {:page-id page-id}] (rx/of (rt/nav :workspace path-params query-params)))))) @@ -2188,47 +2192,71 @@ (defn create-group [] (let [id (uuid/next)] (ptk/reify ::create-group - ptk/UpdateEvent - (update [_ state] + ptk/WatchEvent + (watch [_ state stream] (let [selected (get-in state [:workspace-local :selected])] - (if (and selected (-> selected count (> 1))) + (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) - new-shape (group-shape id (-> selected-objects first :frame-id) selected selection-rect) - objects-removed (-> objects - #_(apply dissoc $ selected) - (assoc (:id new-shape) new-shape) - (update-in [(:id parent) :shapes] - (fn [shapes] (filter #(not (selected %)) shapes))) - (update-in [(:id parent) :shapes] conj (:id new-shape)))] - (-> state - (assoc-in [:workspace-data page-id :objects] objects-removed ) - (assoc-in [:workspace-local :selected] #{(:id new-shape)}))) - state))) + frame-id (-> selected-objects first :frame-id) + group-shape (group-shape id frame-id selected selection-rect)] - ptk/WatchEvent - (watch [_ state stream] - (let [obj (get-in state [:workspace-data (::page-id state) :objects id]) - frame-id (:frame-id obj) - frame (get-in state [:workspace-data (::page-id state) :objects frame-id])] - (rx/of (commit-changes [{:type :add-obj - :id id - :frame-id (:frame-id obj) - :obj obj} - {:type :mod-obj - :id frame-id - :operations [{:type :set - :attr :shapes - :val (:shapes frame)}]}] - [{:type :del-obj :id id} - {:type :mod-obj - :id frame-id - :operations [{:type :set - :attr :shapes - :val (into (:shapes frame) (:shapes obj))}]}]))))))) + (let [updated-parent (helpers/replace-shapes parent selected id) + 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)}]}]] + (rx/of (commit-changes rchanges uchanges {:commit-local? true}) + (fn [state] (assoc-in state [:workspace-local :selected] #{id}))))) + rx/empty)))))) + +(defn remove-group [] + (ptk/reify ::remove-group + ptk/WatchEvent + (watch [_ state stream] + (let [selected (get-in state [:workspace-local :selected]) + group-id (first selected) + 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]) + 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}] + 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)}]}]] + (rx/of (commit-changes rchanges uchanges {:commit-local? true})))) + rx/empty))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Shortcuts @@ -2255,7 +2283,7 @@ "ctrl+c" #(rx/of copy-selected) "ctrl+v" #(rx/of paste) "ctrl+g" #(rx/of (create-group)) - ;; "ctrl+shift+g" #(rx/of remove-group) + "ctrl+shift+g" #(rx/of (remove-group)) "esc" #(rx/of :interrupt deselect-all) "delete" #(rx/of delete-selected) "ctrl+up" #(rx/of (vertical-order-selected :up)) diff --git a/frontend/src/uxbox/main/geom.cljs b/frontend/src/uxbox/main/geom.cljs index d6f71d42d..c10a4a3db 100644 --- a/frontend/src/uxbox/main/geom.cljs +++ b/frontend/src/uxbox/main/geom.cljs @@ -246,54 +246,37 @@ [(/ (:width final) (:width origin)) (/ (:height final) (:height origin))]) +(defn- get-vid-coords [vid] + (case vid + :top-left [:x2 :y2] + :top-right [:x1 :y2] + :top [:x1 :y2] + :bottom-left [:x2 :y1] + :bottom-right [:x :y ] + :bottom [:x1 :y1] + :right [:x1 :y1] + :left [:x2 :y1])) + (defn generate-resize-matrix "Generate the resize transformation matrix given a corner-id, shape and the scale factor vector. The shape should be of rect-like type. Mainly used by drawarea and shape resize on workspace." [vid shape [scalex scaley]] - (case vid - :top-left + (let [[cor-x cor-y] (get-vid-coords vid) + {:keys [x y width height rotation]} shape + cx (+ x (/ width 2)) + cy (+ y (/ height 2)) + center (gpt/point cx cy) + ] (-> (gmt/matrix) + ;; Correction first otherwise the scale is going to deform the correction + (gmt/translate (gmt/correct-rotation + vid width height scalex scaley rotation)) (gmt/scale (gpt/point scalex scaley) - (gpt/point (+ (:x2 shape)) - (+ (:y2 shape))))) - :top-right - (-> (gmt/matrix) - (gmt/scale (gpt/point scalex scaley) - (gpt/point (+ (:x1 shape)) - (+ (:y2 shape))))) - :top - (-> (gmt/matrix) - (gmt/scale (gpt/point scalex scaley) - (gpt/point (+ (:x1 shape)) - (+ (:y2 shape))))) - :bottom-left - (-> (gmt/matrix) - (gmt/scale (gpt/point scalex scaley) - (gpt/point (+ (:x2 shape)) - (+ (:y1 shape))))) - :bottom-right - (-> (gmt/matrix) - (gmt/scale (gpt/point scalex scaley) - (gpt/point (+ (:x shape)) - (+ (:y shape))))) - :bottom - (-> (gmt/matrix) - (gmt/scale (gpt/point scalex scaley) - (gpt/point (+ (:x1 shape)) - (+ (:y1 shape))))) - :right - (-> (gmt/matrix) - (gmt/scale (gpt/point scalex scaley) - (gpt/point (+ (:x1 shape)) - (+ (:y1 shape))))) - :left - (-> (gmt/matrix) - (gmt/scale (gpt/point scalex scaley) - (gpt/point (+ (:x2 shape)) - (+ (:y1 shape))))))) - + (gpt/point (cor-x shape) + (cor-y shape))) + ))) (defn resize-shape "Apply a resize transformation to a rect-like shape. The shape @@ -304,66 +287,12 @@ with the main objective that on the end of resize have a way a calculte the resize ratio with `calculate-scale-ratio`." [vid shape {:keys [x y] :as point} lock?] - (case vid - :top-left - (let [width (- (:x2 shape) x) - height (- (:y2 shape) y) - proportion (:proportion shape 1)] - (assoc shape - :width width - :height (if lock? (/ width proportion) height))) - :top-right - (let [width (- x (:x1 shape)) - height (- (:y2 shape) y) - proportion (:proportion shape 1)] - (assoc shape - :width width - :height (if lock? (/ width proportion) height))) - - :top - (let [width (- (:x2 shape) (:x1 shape)) - height (- (:y2 shape) y) - proportion (:proportion shape 1)] - (assoc shape - :width width - :height (if lock? (/ width proportion) height))) - - :bottom-left - (let [width (- (:x2 shape) x) - height (- y (:y1 shape)) - proportion (:proportion shape 1)] - (assoc shape - :width width - :height (if lock? (/ width proportion) height))) - - :bottom-right - (let [width (- x (:x shape)) - height (- y (:y shape)) - proportion (:proportion shape 1)] - (assoc shape - :width width - :height (if lock? (/ width proportion) height))) - - :bottom - (let [width (- (:x2 shape) (:x1 shape)) - height (- y (:y1 shape)) - proportion (:proportion shape 1)] - (assoc shape - :width width - :height (if lock? (/ width proportion) height))) - - :left - (let [width (- (:x2 shape) x) - height (- (:y2 shape) (:y1 shape)) - proportion (:proportion shape 1)] - (assoc shape - :width width - :height (if lock? (/ width proportion) height))) - - :right - (let [width (- x (:x1 shape)) - height (- (:y2 shape) (:y1 shape)) + (let [[cor-x cor-y] (get-vid-coords vid)] + (let [final-x (if (#{:top :bottom} vid) (:x2 shape) x) + final-y (if (#{:right :left} vid) (:y2 shape) y) + width (Math/abs (- final-x (cor-x shape))) + height (Math/abs (- final-y (cor-y shape))) proportion (:proportion shape 1)] (assoc shape :width width @@ -657,3 +586,11 @@ (> rx2 sx1) (< ry1 sy2) (> ry2 sy1)))) + +(defn transform-shape [frame shape] + (let [ds-modifier (:displacement-modifier shape) + rz-modifier (:resize-modifier shape)] + (cond-> shape + (gmt/matrix? rz-modifier) (transform rz-modifier) + frame (move (gpt/point (- (:x frame)) (- (:y frame)))) + (gmt/matrix? ds-modifier) (transform ds-modifier)))) diff --git a/frontend/src/uxbox/main/refs.cljs b/frontend/src/uxbox/main/refs.cljs index d3ac1e277..c4abf742e 100644 --- a/frontend/src/uxbox/main/refs.cljs +++ b/frontend/src/uxbox/main/refs.cljs @@ -56,6 +56,13 @@ (get-in % [:workspace-data page-id :objects]))) (l/derive st/state))) +(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)))) + (def selected-shapes (-> (l/key :selected) (l/derive workspace-local))) diff --git a/frontend/src/uxbox/main/ui/shapes/circle.cljs b/frontend/src/uxbox/main/ui/shapes/circle.cljs index 82a424b1e..5361f93c1 100644 --- a/frontend/src/uxbox/main/ui/shapes/circle.cljs +++ b/frontend/src/uxbox/main/ui/shapes/circle.cljs @@ -20,7 +20,7 @@ (declare circle-shape) (mf/defc circle-wrapper - [{:keys [shape] :as props}] + [{:keys [shape frame] :as props}] (let [selected (mf/deref refs/selected-shapes) selected? (contains? selected (:id shape)) on-mouse-down #(common/on-mouse-down % shape) @@ -28,20 +28,13 @@ [:g.shape {:class (when selected? "selected") :on-mouse-down on-mouse-down :on-context-menu on-context-menu} - [:& circle-shape {:shape shape}]])) + [:& circle-shape {:shape (geom/transform-shape frame shape)}]])) ;; --- Circle Shape (mf/defc circle-shape [{:keys [shape] :as props}] - (let [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)) - - {:keys [id cx cy rx ry rotation]} shape + (let [{:keys [id cx cy rx ry rotation]} shape center (gpt/point cx cy) rotation (or rotation 0) diff --git a/frontend/src/uxbox/main/ui/shapes/frame.cljs b/frontend/src/uxbox/main/ui/shapes/frame.cljs index cc100dd3e..f0b4b135e 100644 --- a/frontend/src/uxbox/main/ui/shapes/frame.cljs +++ b/frontend/src/uxbox/main/ui/shapes/frame.cljs @@ -57,48 +57,45 @@ (mf/fnc frame-wrapper {::mf/wrap [wrap-memo-frame]} [{:keys [shape objects] :as props}] - (when (and shape (not (:hidden shape))) - (let [zoom (mf/deref refs/selected-zoom) - inv-zoom (/ 1 zoom) + (let [selected-iref (-> (mf/deps (:id shape)) + (mf/use-memo #(refs/make-selected (:id shape)))) + selected? (mf/deref selected-iref) + zoom (mf/deref refs/selected-zoom)] + (when (and shape (not (:hidden shape))) + (let [on-mouse-down #(common/on-mouse-down % shape) + on-context-menu #(common/on-context-menu % shape) + shape (merge frame-default-props shape) + {:keys [x y width height]} shape + inv-zoom (/ 1 zoom) + childs (mapv #(get objects %) (:shapes shape)) + ds-modifier (:displacement-modifier shape) + label-pos (cond-> (gpt/point x (- y 10)) + (gmt/matrix? ds-modifier) (gpt/transform ds-modifier)) - selected-iref (-> (mf/deps (:id shape)) - (mf/use-memo #(refs/make-selected (:id shape)))) - selected? (mf/deref selected-iref) - on-mouse-down #(common/on-mouse-down % shape) - on-context-menu #(common/on-context-menu % shape) - shape (merge frame-default-props shape) - {:keys [x y width height]} shape - - childs (mapv #(get objects %) (:shapes shape)) - - ds-modifier (:displacement-modifier shape) - label-pos (cond-> (gpt/point x (- y 10)) - (gmt/matrix? ds-modifier) (gpt/transform ds-modifier)) - - on-double-click - (fn [event] - (dom/prevent-default event) - (st/emit! dw/deselect-all - (dw/select-shape (:id shape))))] - [:g {:class (when selected? "selected") - :on-context-menu on-context-menu - :on-double-click on-double-click - :on-mouse-down on-mouse-down} - [:text {:x 0 - :y 0 - :width width - :height 20 - :class-name "workspace-frame-label" - ; Ensure that the label has always the same font size, regardless of zoom - ; https://css-tricks.com/transforms-on-svg-elements/ - :transform (str - "scale(" inv-zoom ", " inv-zoom ") " - "translate(" (* zoom (:x label-pos)) ", " (* zoom (:y label-pos)) ")") - ; User may also select the frame with single click in the label - :on-click on-double-click} - (:name shape)] - [:& (frame-shape shape-wrapper) {:shape shape - :childs childs}]])))) + on-double-click + (fn [event] + (dom/prevent-default event) + (st/emit! dw/deselect-all + (dw/select-shape (:id shape))))] + [:g {:class (when selected? "selected") + :on-context-menu on-context-menu + :on-double-click on-double-click + :on-mouse-down on-mouse-down} + [:text {:x 0 + :y 0 + :width width + :height 20 + :class-name "workspace-frame-label" + ;; Ensure that the label has always the same font size, regardless of zoom + ;; https://css-tricks.com/transforms-on-svg-elements/ + :transform (str + "scale(" inv-zoom ", " inv-zoom ") " + "translate(" (* zoom (:x label-pos)) ", " (* zoom (:y label-pos)) ")") + ;; User may also select the frame with single click in the label + :on-click on-double-click} + (:name shape)] + [:& (frame-shape shape-wrapper) {:shape shape + :childs childs}]]))))) (defn frame-shape [shape-wrapper] (mf/fnc frame-shape @@ -106,7 +103,6 @@ (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)) @@ -124,21 +120,5 @@ [:svg {:x x :y y :width width :height height} [:> "rect" props] (for [item childs] - [:& shape-wrapper {:shape (translate-to-frame item shape) :key (:id item)}])]))) + [:& shape-wrapper {:frame shape :shape item :key (:id item)}])]))) -(defn- translate-to-frame - [shape frame] - (let [pt (gpt/point (- (:x frame)) (- (:y frame))) - frame-ds-modifier (:displacement-modifier frame) - rz-modifier (:resize-modifier shape) - shape (cond-> shape - (gmt/matrix? frame-ds-modifier) - (geom/transform frame-ds-modifier) - - (and (= (:type shape) :group) (gmt/matrix? rz-modifier)) - (geom/transform rz-modifier) - - (and (not= (:type shape) :group) (gmt/matrix? rz-modifier)) - (-> (geom/transform rz-modifier) - (dissoc :resize-modifier)))] - (geom/move shape pt))) diff --git a/frontend/src/uxbox/main/ui/shapes/group.cljs b/frontend/src/uxbox/main/ui/shapes/group.cljs index f5de3924f..d5bb51cab 100644 --- a/frontend/src/uxbox/main/ui/shapes/group.cljs +++ b/frontend/src/uxbox/main/ui/shapes/group.cljs @@ -11,12 +11,12 @@ [uxbox.main.geom :as geom] [uxbox.main.refs :as refs] [uxbox.util.dom :as dom] - [uxbox.util.geom.matrix :as gmt] - [uxbox.util.geom.point :as gpt] [uxbox.util.interop :as itr] [uxbox.main.ui.shapes.common :as common] [uxbox.main.ui.shapes.attrs :as attrs])) +(defonce ^:dynamic *debug* (atom false)) + (declare translate-to-frame) (declare group-shape) @@ -25,71 +25,53 @@ {::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") + frame (unchecked-get props "frame") on-mouse-down #(common/on-mouse-down % shape) on-context-menu #(common/on-context-menu % shape) - objects (-> refs/objects mf/deref) - children (mapv #(get objects %) (:shapes shape)) - frame (get objects (:frame-id shape))] + children (-> (refs/objects-by-id (:shapes shape)) mf/deref) + on-double-click + (fn [event] + (dom/stop-propagation event) + (dom/prevent-default event) + #_(st/emit! (dw/select-inside-group)))] + [:g.shape {:on-mouse-down on-mouse-down - :on-context-menu on-context-menu} - [:& (group-shape shape-wrapper) {:shape shape - :shape-wrapper shape-wrapper - :children children - :frame frame }]]))) + :on-context-menu on-context-menu + :on-double-click on-double-click} + [:& (group-shape shape-wrapper) {:frame frame + :shape (geom/transform-shape frame shape) + :children children}]]))) (defn group-shape [shape-wrapper] (mf/fnc group-shape {::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") + (let [frame (unchecked-get props "frame") + shape (unchecked-get props "shape") children (unchecked-get props "children") - frame (unchecked-get props "frame") - - ds-modifier (:displacement-modifier shape) - rz-modifier (:resize-modifier shape) - - shape (cond-> shape - (and (= "root" (:name frame)) (gmt/matrix? rz-modifier)) (geom/transform rz-modifier) - (gmt/matrix? rz-modifier) (geom/transform ds-modifier)) - - {:keys [id x y width height rotation]} shape + {:keys [id x y width height rotation + displacement-modifier + resize-modifier]} shape transform (when (and rotation (pos? rotation)) (str/format "rotate(%s %s %s)" rotation (+ x (/ width 2)) (+ y (/ height 2))))] - [:g - (for [item (reverse children)] - [:& shape-wrapper {:shape (-> item - (geom/transform rz-modifier) - (assoc :displacement-modifier ds-modifier) - (translate-to-frame frame)) + [:g {:transform transform} + (for [item children] + [:& shape-wrapper {:frame frame + :shape (-> item + (assoc :displacement-modifier displacement-modifier) + (assoc :resize-modifier resize-modifier)) :key (:id item)}]) [:rect {:x x :y y - :fill "red" + :fill (if (deref *debug*) "red" "transparent") :opacity 0.8 - :transform transform :id (str "group-" id) :width width :height height}]]))) -(defn- translate-to-frame - [shape frame] - (let [pt (gpt/point (- (:x frame)) (- (:y frame))) - frame-ds-modifier (:displacement-modifier frame) - rz-modifier (:resize-modifier shape) - shape (cond-> shape - (gmt/matrix? frame-ds-modifier) - (geom/transform frame-ds-modifier) - - (and (= (:type shape) :group) (gmt/matrix? rz-modifier)) - (geom/transform rz-modifier) - - (gmt/matrix? rz-modifier) - (-> (geom/transform rz-modifier) - (dissoc :resize-modifier)))] - (geom/move shape pt))) diff --git a/frontend/src/uxbox/main/ui/shapes/icon.cljs b/frontend/src/uxbox/main/ui/shapes/icon.cljs index 53b20063e..8ce719157 100644 --- a/frontend/src/uxbox/main/ui/shapes/icon.cljs +++ b/frontend/src/uxbox/main/ui/shapes/icon.cljs @@ -12,9 +12,7 @@ [uxbox.main.refs :as refs] [uxbox.main.ui.shapes.attrs :as attrs] [uxbox.main.ui.shapes.common :as common] - [uxbox.util.interop :as itr] - [uxbox.util.geom.matrix :as gmt] - [uxbox.util.geom.point :as gpt])) + [uxbox.util.interop :as itr])) ;; --- Icon Wrapper @@ -22,27 +20,19 @@ (declare icon-shape) (mf/defc icon-wrapper - [{:keys [shape] :as props}] + [{:keys [shape frame] :as props}] (let [selected (mf/deref refs/selected-shapes) selected? (contains? selected (:id shape)) on-mouse-down #(common/on-mouse-down % shape selected)] [:g.shape {:class (when selected? "selected") :on-mouse-down on-mouse-down} - [:& icon-shape {:shape shape}]])) + [:& icon-shape {:shape (geom/transform-shape frame shape)}]])) ;; --- Icon Shape (mf/defc icon-shape [{:keys [shape] :as props}] - (let [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)) - - {:keys [id x y width height metadata rotation content] :as shape} shape - + (let [{:keys [id x y width height metadata rotation content] :as shape} shape transform (when (and rotation (pos? rotation)) (str/format "rotate(%s %s %s)" rotation diff --git a/frontend/src/uxbox/main/ui/shapes/image.cljs b/frontend/src/uxbox/main/ui/shapes/image.cljs index 96e94411e..c9a4cac7f 100644 --- a/frontend/src/uxbox/main/ui/shapes/image.cljs +++ b/frontend/src/uxbox/main/ui/shapes/image.cljs @@ -22,26 +22,19 @@ (declare image-shape) (mf/defc image-wrapper - [{:keys [shape] :as props}] + [{:keys [shape frame] :as props}] (let [selected (mf/deref refs/selected-shapes) selected? (contains? selected (:id shape)) on-mouse-down #(common/on-mouse-down % shape selected)] [:g.shape {:class (when selected? "selected") :on-mouse-down on-mouse-down} - [:& image-shape {:shape shape}]])) + [:& image-shape {:shape (geom/transform-shape frame shape)}]])) ;; --- Image Shape (mf/defc image-shape [{:keys [shape] :as props}] - (let [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)) - - {:keys [id x y width height rotation metadata]} shape + (let [{:keys [id x y width height rotation metadata]} shape transform (when (and rotation (pos? rotation)) (str/format "rotate(%s %s %s)" diff --git a/frontend/src/uxbox/main/ui/shapes/path.cljs b/frontend/src/uxbox/main/ui/shapes/path.cljs index a19f3da53..a08634cf8 100644 --- a/frontend/src/uxbox/main/ui/shapes/path.cljs +++ b/frontend/src/uxbox/main/ui/shapes/path.cljs @@ -22,7 +22,7 @@ (declare path-shape) (mf/defc path-wrapper - [{:keys [shape] :as props}] + [{:keys [shape frame] :as props}] (let [selected (mf/deref refs/selected-shapes) selected? (contains? selected (:id shape)) on-mouse-down #(common/on-mouse-down % shape) @@ -34,7 +34,7 @@ [:g.shape {:on-double-click on-double-click :on-mouse-down on-mouse-down :on-context-menu on-context-menu} - [:& path-shape {:shape shape + [:& path-shape {:shape (geom/transform-shape frame shape) :background? true}]])) ;; --- Path Shape @@ -62,14 +62,7 @@ (mf/defc path-shape [{:keys [shape background?] :as props}] - (let [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)) - - {:keys [id x y width height rotation]} (geom/shape->rect-shape shape) + (let [{:keys [id x y width height rotation]} (geom/shape->rect-shape shape) transform (when (and rotation (pos? rotation)) (str/format "rotate(%s %s %s)" diff --git a/frontend/src/uxbox/main/ui/shapes/rect.cljs b/frontend/src/uxbox/main/ui/shapes/rect.cljs index 38cc03c0b..b7b296751 100644 --- a/frontend/src/uxbox/main/ui/shapes/rect.cljs +++ b/frontend/src/uxbox/main/ui/shapes/rect.cljs @@ -12,9 +12,7 @@ [uxbox.main.refs :as refs] [uxbox.main.ui.shapes.attrs :as attrs] [uxbox.main.ui.shapes.common :as common] - [uxbox.util.interop :as itr] - [uxbox.util.geom.matrix :as gmt] - [uxbox.util.geom.point :as gpt])) + [uxbox.util.interop :as itr])) ;; --- Rect Wrapper @@ -24,11 +22,12 @@ {::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") + frame (unchecked-get props "frame") on-mouse-down #(common/on-mouse-down % shape) on-context-menu #(common/on-context-menu % shape)] [:g.shape {:on-mouse-down on-mouse-down :on-context-menu on-context-menu} - [:& rect-shape {:shape shape}]])) + [:& rect-shape {:shape (geom/transform-shape frame shape) }]])) ;; --- Rect Shape @@ -36,15 +35,7 @@ {::mf/wrap-props false} [props] (let [shape (unchecked-get props "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)) - {:keys [id x y width height rotation]} shape - transform (when (and rotation (pos? rotation)) (str/format "rotate(%s %s %s)" rotation diff --git a/frontend/src/uxbox/main/ui/shapes/shape.cljs b/frontend/src/uxbox/main/ui/shapes/shape.cljs index c04931641..e83591956 100644 --- a/frontend/src/uxbox/main/ui/shapes/shape.cljs +++ b/frontend/src/uxbox/main/ui/shapes/shape.cljs @@ -18,7 +18,8 @@ [uxbox.main.ui.shapes.rect :as rect] [uxbox.main.ui.shapes.text :as text] [uxbox.main.ui.shapes.group :as group] - [uxbox.main.ui.shapes.frame :as frame])) + [uxbox.main.ui.shapes.frame :as frame] + [uxbox.main.refs :as refs])) (defn wrap-memo-shape ([component] @@ -34,19 +35,20 @@ (mf/defc shape-wrapper {::mf/wrap [wrap-memo-shape]} - [{:keys [shape] :as props}] - (when (and shape (not (:hidden shape))) - (case (:type shape) - :group [:& group-wrapper {:shape shape}] - :curve [:& path/path-wrapper {:shape shape}] - :text [:& text/text-wrapper {:shape shape}] - :icon [:& icon/icon-wrapper {:shape shape}] - :rect [:& rect/rect-wrapper {:shape shape}] - :path [:& path/path-wrapper {:shape shape}] - :image [:& image/image-wrapper {:shape shape}] - :circle [:& circle/circle-wrapper {:shape shape}] - :frame [:& frame-wrapper {:shape shape}] - nil))) + [{:keys [shape frame] :as props}] + (let [opts {:shape shape :frame frame}] + (when (and shape (not (:hidden shape))) + (case (:type shape) + :group [:& group-wrapper opts] + :curve [:& path/path-wrapper opts] + :text [:& text/text-wrapper opts] + :icon [:& icon/icon-wrapper opts] + :rect [:& rect/rect-wrapper opts] + :path [:& path/path-wrapper opts] + :image [:& image/image-wrapper opts] + :circle [:& circle/circle-wrapper opts] + :frame [:& frame-wrapper opts] + nil)))) (def group-wrapper (group/group-wrapper shape-wrapper)) (def frame-wrapper (frame/frame-wrapper shape-wrapper)) diff --git a/frontend/src/uxbox/main/ui/shapes/text.cljs b/frontend/src/uxbox/main/ui/shapes/text.cljs index 0ad6000af..43e21f600 100644 --- a/frontend/src/uxbox/main/ui/shapes/text.cljs +++ b/frontend/src/uxbox/main/ui/shapes/text.cljs @@ -37,7 +37,7 @@ (declare text-shape) (mf/defc text-wrapper - [{:keys [shape] :as props}] + [{:keys [shape frame] :as props}] (let [{:keys [id x1 y1 content group]} shape selected (mf/deref refs/selected-shapes) edition (mf/deref refs/selected-edition) @@ -55,8 +55,8 @@ :on-mouse-down on-mouse-down :on-context-menu on-context-menu} (if edition? - [:& text-shape-edit {:shape shape}] - [:& text-shape {:shape shape}])])) + [:& text-shape-edit {:shape (geom/transform-shape frame shape)}] + [:& text-shape {:shape (geom/transform-shape frame shape)}])])) ;; --- Text Styles Helpers @@ -148,15 +148,7 @@ (mf/defc text-shape [{:keys [shape] :as props}] - (let [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)) - - - {:keys [id x y width height rotation content]} shape + (let [{:keys [id x y width height rotation content]} shape transform (when (and rotation (pos? rotation)) (str/format "rotate(%s %s %s)" diff --git a/frontend/src/uxbox/main/ui/workspace/context_menu.cljs b/frontend/src/uxbox/main/ui/workspace/context_menu.cljs index fffdffe1b..4b703b105 100644 --- a/frontend/src/uxbox/main/ui/workspace/context_menu.cljs +++ b/frontend/src/uxbox/main/ui/workspace/context_menu.cljs @@ -58,7 +58,9 @@ do-show-shape #(st/emit! (dw/show-shape (:id shape))) do-hide-shape #(st/emit! (dw/hide-shape (:id shape))) do-lock-shape #(st/emit! (dw/block-shape (:id shape))) - do-unlock-shape #(st/emit! (dw/unblock-shape (:id shape)))] + do-unlock-shape #(st/emit! (dw/unblock-shape (:id shape))) + do-create-group #(st/emit! (dw/create-group)) + do-remove-group #(st/emit! (dw/remove-group))] [:* [:& menu-entry {:title "Copy" :shortcut "Ctrl + c" @@ -83,11 +85,25 @@ :shortcut "Ctrl + Shift + ↓" :on-click do-send-to-back}] [:& menu-separator] + + (when (> (count selected) 1) + [:& menu-entry {:title "Group" + :shortcut "Ctrl + g" + :on-click do-create-group}]) + + (when (and (= (count selected)) (= (:type shape) :group)) + [:& menu-entry {:title "Ungroup" + :shortcut "Ctrl + shift + g" + :on-click do-remove-group}]) + (if (:hidden shape) [:& menu-entry {:title "Show" :on-click do-show-shape}] [:& menu-entry {:title "Hide" :on-click do-hide-shape}]) + + + (if (:blocked shape) [:& menu-entry {:title "Unlock" :on-click do-unlock-shape}] diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs index 176b0a577..4ebfe45dd 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs @@ -33,6 +33,7 @@ (mf/defc element-icon [{:keys [shape] :as props}] (case (:type shape) + :frame i/artboard :icon [:& icon/icon-svg {:shape shape}] :image i/image :line i/line @@ -177,109 +178,7 @@ (when (and (:shapes item) (not collapsed?)) [:ul.element-children (for [[index id] (d/enumerate (:shapes item))] - (let [item (get objects id)] - [:& layer-item - {:item item - :selected selected - :index index - :objects objects - :key (:id item)}]))])])) - -(mf/defc layer-frame-item - {:wrap [#(mf/wrap-memo % =)]} - [{:keys [item selected index objects] :as props}] - (let [selected? (contains? selected (:id item)) - local (mf/use-state {:collapsed false}) - collapsed? (:collapsed @local) - - toggle-collapse - (fn [event] - (dom/stop-propagation event) - (swap! local update :collapsed not)) - - toggle-blocking - (fn [event] - (dom/stop-propagation event) - (if (:blocked item) - (st/emit! (dw/unblock-shape (:id item))) - (st/emit! (dw/block-shape (:id item))))) - - toggle-visibility - (fn [event] - (dom/stop-propagation event) - (if (:hidden item) - (st/emit! (dw/show-frame (:id item))) - (st/emit! (dw/hide-frame (:id item))))) - - select-shape - (fn [event] - (dom/prevent-default event) - (let [id (:id item)] - (cond - (or (:blocked item) - (:hidden item)) - nil - - (.-ctrlKey event) - (st/emit! (dw/select-shape id)) - - (> (count selected) 1) - (st/emit! dw/deselect-all - (dw/select-shape id)) - :else - (st/emit! dw/deselect-all - (dw/select-shape id))))) - - on-context-menu - (fn [event] - (dom/prevent-default event) - (dom/stop-propagation event) - (let [pos (dom/get-client-position event)] - (st/emit! (dw/show-shape-context-menu {:position pos - :shape item})))) - - on-drop - (fn [item monitor] - (st/emit! (dw/commit-shape-order-change (:obj-id item)))) - - on-hover - (fn [item monitor] - (st/emit! (dw/shape-order-change (:obj-id item) index))) - - [dprops dnd-ref] (use-sortable - {:type (str "layer-item" (:frame-id item)) - :data {:obj-id (:id item) - :page-id (:page item) - :index index} - :on-hover on-hover - :on-drop on-drop})] - [:li.group {:ref dnd-ref - :on-context-menu on-context-menu - :class (dom/classnames - :selected selected? - :dragging-TODO (:dragging? dprops))} - [:div.element-list-body {:class (dom/classnames :selected selected?) - :on-click select-shape - :on-double-click #(dom/stop-propagation %)} - [:div.element-icon i/artboard] - [:& layer-name {:shape item}] - - [:div.element-actions - [:div.toggle-element {:class (when (:hidden item) "selected") - :on-click toggle-visibility} - (if (:hidden item) i/eye-closed i/eye)] - [:div.block-element {:class (when (:blocked item) "selected") - :on-click toggle-blocking} - (if (:blocked item) i/lock i/lock-open)]] - - [:span.toggle-content - {:on-click toggle-collapse - :class (when-not collapsed? "inverse")} - i/arrow-slide]] - (when-not collapsed? - [:ul - (for [[index id] (d/enumerate (reverse (:shapes item)))] - (let [item (get objects id)] + (when-let [item (get objects id)] [:& layer-item {:item item :selected selected @@ -297,18 +196,12 @@ [:ul.element-list (for [[index id] (d/enumerate (reverse (:shapes root)))] (let [item (get objects id)] - (if (= (:type item) :frame) - [:& layer-frame-item - {:item item - :key (:id item) - :selected selected - :objects objects - :index index}] - [:& layer-item - {:item item - :selected selected - :index index - :key (:id item)}])))])) + [:& layer-item + {:item item + :selected selected + :index index + :objects objects + :key (:id item)}]))])) ;; --- Layers Toolbox diff --git a/frontend/src/uxbox/util/geom/matrix.cljs b/frontend/src/uxbox/util/geom/matrix.cljs index f99e96c4e..19fa00df8 100644 --- a/frontend/src/uxbox/util/geom/matrix.cljs +++ b/frontend/src/uxbox/util/geom/matrix.cljs @@ -101,3 +101,21 @@ (fn [value] (map->Matrix value)))) +;; Calculates the delta vector to move the figure when scaling after rotation +;; https://math.stackexchange.com/questions/1449672/determine-shift-between-scaled-rotated-object-and-additional-scale-step +(defn correct-rotation [handler lx ly kx ky angle] + (let [[s1 s2 s3] + ;; Different sign configurations change the anchor corner + (cond + (#{:right :bottom :bottom-right} handler) [-1 1 1] + (#{:left :top :top-left} handler) [1 -1 1] + (#{:bottom-left} handler) [-1 -1 -1] + (#{:top-right} handler) [1 1 -1]) + rad (* (or angle 0) (/ Math/PI 180)) + kx' (* (/ (- kx 1.) 2.) lx) + ky' (* (/ (- ky 1.) 2.) ly) + dx (+ (* s3 (* kx' (- 1 (Math/cos rad)))) + (* ky' (Math/sin rad))) + dy (+ (* (- s3) (* ky' (- 1 (Math/cos rad)))) + (* kx' (Math/sin rad)))] + (gpt/point (* s1 dx) (* s2 dy))))