diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 0902e1ac3..d27f78807 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -1592,7 +1592,6 @@ (rx/of (diff-and-commit-changes page-id) (rehash-shape-frame-relationship ids)))))) - (defn apply-displacement-in-bulk "Apply the same displacement delta to all shapes identified by the set if ids." @@ -1696,6 +1695,54 @@ (update-in state [:workspace-data page-id :objects] merge shapes))))) +(defn apply-rotation + [delta-rotation shapes] + (ptk/reify ::apply-rotation + ptk/UpdateEvent + (update [_ state] + (let [group (geom/selection-rect shapes) + group-center (gpt/center group) + calculate-displacement + (fn [shape angle] + (let [shape-rect (geom/shape->rect-shape shape) + shape-center (gpt/center shape-rect)] + (-> (gmt/matrix) + (gmt/rotate angle group-center) + (gmt/rotate (- angle) shape-center)))) + + page-id (::page-id state) + rotate-shape + (fn [state shape] + (let [path [:workspace-data page-id :objects (:id shape)] + ds (calculate-displacement shape delta-rotation)] + (-> state + (assoc-in (conj path :rotation-modifier) delta-rotation) + (assoc-in (conj path :displacement-modifier) ds))))] + (reduce rotate-shape state shapes))))) + +(defn materialize-rotation + [shapes] + (ptk/reify ::materialize-rotation + IBatchedChange + + ptk/UpdateEvent + (update [_ state] + (let [apply-rotation + (fn [shape] + (let [ds-modifier (or (:displacement-modifier shape) (gmt/matrix))] + (-> shape + (update :rotation #(mod (+ % (:rotation-modifier shape)) 360)) + (geom/transform ds-modifier) + (dissoc :rotation-modifier) + (dissoc :displacement-modifier)))) + + materialize-shape + (fn [state shape] + (let [path [:workspace-data (::page-id state) :objects (:id shape)]] + (update-in state path apply-rotation)))] + + (reduce materialize-shape state shapes))))) + (defn commit-changes ([changes undo-changes] (commit-changes changes undo-changes {})) ([changes undo-changes {:keys [save-undo? diff --git a/frontend/src/uxbox/main/geom.cljs b/frontend/src/uxbox/main/geom.cljs index 11e318fcc..d73a83376 100644 --- a/frontend/src/uxbox/main/geom.cljs +++ b/frontend/src/uxbox/main/geom.cljs @@ -275,8 +275,7 @@ vid width height scalex scaley rotation)) (gmt/scale (gpt/point scalex scaley) (gpt/point (cor-x shape) - (cor-y shape))) - ))) + (cor-y shape)))))) (defn resize-shape "Apply a resize transformation to a rect-like shape. The shape @@ -517,13 +516,16 @@ (transform shape (rotation-matrix shape))) (defn resolve-modifier - [{:keys [resize-modifier displacement-modifier] :as shape}] + [{:keys [resize-modifier displacement-modifier rotation-modifier] :as shape}] (cond-> shape (gmt/matrix? resize-modifier) (transform resize-modifier) (gmt/matrix? displacement-modifier) - (transform displacement-modifier))) + (transform displacement-modifier) + + rotation-modifier + (update :rotation #(+ (or % 0) rotation-modifier)))) ;; NOTE: we need apply `shape->rect-shape` 3 times because we need to ;; update the x1 x2 y1 y2 attributes on each step; this is because @@ -652,9 +654,11 @@ ([frame shape] (let [ds-modifier (:displacement-modifier shape) rz-modifier (:resize-modifier shape) - frame-ds-modifier (:displacement-modifier frame)] + frame-ds-modifier (:displacement-modifier frame) + rt-modifier (:rotation-modifier shape)] (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))))) + (gmt/matrix? ds-modifier) (transform ds-modifier) + rt-modifier (update :rotation #(+ (or % 0) rt-modifier)))))) diff --git a/frontend/src/uxbox/main/ui/shapes/rect.cljs b/frontend/src/uxbox/main/ui/shapes/rect.cljs index b7b296751..d0210f0a7 100644 --- a/frontend/src/uxbox/main/ui/shapes/rect.cljs +++ b/frontend/src/uxbox/main/ui/shapes/rect.cljs @@ -8,6 +8,8 @@ (:require [rumext.alpha :as mf] [cuerdas.core :as str] + [uxbox.util.geom.matrix :as gmt] + [uxbox.util.geom.point :as gpt] [uxbox.main.geom :as geom] [uxbox.main.refs :as refs] [uxbox.main.ui.shapes.attrs :as attrs] @@ -36,12 +38,10 @@ [props] (let [shape (unchecked-get props "shape") {:keys [id x y width height rotation]} shape - transform (when (and rotation (pos? rotation)) - (str/format "rotate(%s %s %s)" - rotation - (+ x (/ width 2)) - (+ y (/ height 2)))) - + center (gpt/center shape) + transform (when (pos? rotation) + (str (-> (gmt/matrix) + (gmt/rotate rotation center)))) props (-> (attrs/extract-style-attrs shape) (itr/obj-assign! #js {:x x @@ -50,5 +50,4 @@ :id (str "shape-" id) :width width :height height}))] - [:> "rect" props])) diff --git a/frontend/src/uxbox/main/ui/workspace/selection.cljs b/frontend/src/uxbox/main/ui/workspace/selection.cljs index eb093f0d2..743f7edc8 100644 --- a/frontend/src/uxbox/main/ui/workspace/selection.cljs +++ b/frontend/src/uxbox/main/ui/workspace/selection.cljs @@ -68,35 +68,39 @@ (rx/of (dw/materialize-resize-modifier-in-bulk ids)))))))) (defn start-rotate - [shape] + [shapes] (ptk/reify ::start-rotate ptk/WatchEvent (watch [_ state stream] - (let [shape (geom/shape->rect-shape shape) - stoper (rx/filter ms/mouse-up? stream) - center (gpt/point (+ (:x shape) (/ (:width shape) 2)) - (+ (:y shape) (/ (:height shape) 2)))] - + (let [stoper (rx/filter ms/mouse-up? stream) + group (geom/selection-rect shapes) + group-center (gpt/center group) + initial-angle (gpt/angle @ms/mouse-position group-center) + calculate-angle (fn [pos ctrl?] + (let [angle (- (gpt/angle pos group-center) initial-angle) + angle (if (neg? angle) (+ 360 angle) angle) + modval (mod angle 90) + angle (if ctrl? + (if (< 50 modval) + (+ angle (- 90 modval)) + (- angle modval)) + angle) + angle (if (= angle 360) + 0 + angle)] + angle))] (rx/concat (->> ms/mouse-position (rx/map apply-zoom) (rx/with-latest vector ms/mouse-position-ctrl) (rx/map (fn [[pos ctrl?]] - (let [angle (+ (gpt/angle pos center) 90) - angle (if (neg? angle) - (+ 360 angle) - angle) - modval (mod angle 90) - angle (if ctrl? - (if (< 50 modval) - (+ angle (- 90 modval)) - (- angle modval)) - angle) - angle (if (= angle 360) - 0 - angle)] - (dw/update-shape (:id shape) {:rotation angle})))) - (rx/take-until stoper))))))) + (let [delta-angle (calculate-angle pos ctrl?)] + (dw/apply-rotation delta-angle shapes)))) + + + (rx/take-until stoper)) + (rx/of (dw/materialize-rotation shapes)) + ))))) ;; --- Controls (Component) @@ -120,78 +124,67 @@ :cx cx :cy cy}]) +(def ^:private rotate-cursor-svg "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20px' height='20px' transform='rotate(%s)' viewBox='0 0 132.292 132.006'%3E%3Cpath d='M85.225 3.48c.034 4.989-.093 9.852-.533 14.78-29.218 5.971-54.975 27.9-63.682 56.683-1.51 2.923-1.431 7.632-3.617 9.546-5.825.472-11.544.5-17.393.45 11.047 15.332 20.241 32.328 32.296 46.725 5.632 1.855 7.155-5.529 10.066-8.533 8.12-12.425 17.252-24.318 24.269-37.482-6.25-.86-12.564-.88-18.857-1.057 5.068-17.605 19.763-31.81 37.091-37.122.181 6.402.206 12.825 1.065 19.184 15.838-9.05 30.899-19.617 45.601-30.257 2.985-4.77-3.574-7.681-6.592-9.791C111.753 17.676 98.475 8.889 85.23.046l-.005 3.435z'/%3E%3Cpath fill='%23fff' d='M92.478 23.995s-1.143.906-6.714 1.923c-29.356 5.924-54.352 30.23-59.717 59.973-.605 3.728-1.09 5.49-1.09 5.49l-11.483-.002s7.84 10.845 10.438 15.486c3.333 4.988 6.674 9.971 10.076 14.912a2266.92 2266.92 0 0019.723-29.326c-5.175-.16-10.35-.343-15.522-.572 3.584-27.315 26.742-50.186 53.91-54.096.306 5.297.472 10.628.631 15.91a2206.462 2206.462 0 0029.333-19.726c-9.75-6.7-19.63-13.524-29.483-20.12z'/%3E%3C/svg%3E\") 10 10, auto") + +(mf/defc rotation-handler + [{:keys [cx cy position on-mouse-down rotation]}] + (when (#{:top-left :top-right :bottom-left :bottom-right} position) + (let [size 20 + rotation (or rotation 0) + x (- cx (if (#{:top-left :bottom-left} position) size 0)) + y (- cy (if (#{:top-left :top-right} position) size 0)) + angle (case position + :top-left 0 + :top-right 90 + :bottom-right 180 + :bottom-left 270)] + [:rect {:style {:cursor (str/format rotate-cursor-svg (str (+ rotation angle)))} + :x x + :y y + :width size + :height size + :fill "transparent" + :on-mouse-down (or on-mouse-down (fn []))}]))) + (mf/defc controls [{:keys [shape zoom on-resize on-rotate] :as props}] (let [{:keys [x y width height rotation] :as shape} (geom/shape->rect-shape shape) radius (if (> (max width height) handler-size-threshold) 6.0 4.0) - transform (geom/rotation-matrix shape)] + transform (geom/rotation-matrix shape) + + resize-handlers {:top [(+ x (/ width 2 )) (- y 2)] + :right [(+ x width 1) (+ y (/ height 2))] + :bottom [(+ x (/ width 2)) (+ y height 2)] + :left [(- x 3) (+ y (/ height 2))] + :top-left [x y] + :top-right [(+ x width) y] + :bottom-left [x (+ y height)] + :bottom-right [(+ x width) (+ y height)]}] + [:g.controls {:transform transform} [:rect.main {:x x :y y :width width :height height :stroke-dasharray (str (/ 8.0 zoom) "," (/ 5 zoom)) - :style {:stroke "#31EFB8" :fill "transparent" + :style {:stroke "#31EFB8" + :fill "transparent" :stroke-opacity "1"}}] - (when (and (fn? on-rotate) - (not= :frame (:type shape))) - [:* - [:path {:stroke "#31EFB8" - :stroke-opacity "1" - :stroke-dasharray (str (/ 8.0 zoom) "," (/ 5 zoom)) - :fill "transparent" - :d (str/format "M %s %s L %s %s" - (+ x (/ width 2)) - y - (+ x (/ width 2)) - (- y 30))}] + (for [[position [cx cy]] resize-handlers] + [:* {:key (str "fragment-" (name position))} + [:& rotation-handler {:key (str "rotation-" (name position)) + :cx cx + :cy cy + :position position + :rotation (:rotation shape) + :on-mouse-down on-rotate}] - [:& control-item {:class "rotate" + [:& control-item {:key (str "resize-" (name position)) + :class (name position) + :on-click #(on-resize position %) :r (/ radius zoom) - :cx (+ x (/ width 2)) - :on-click on-rotate - :cy (- y 30)}]]) - - [:& control-item {:class "top" - :on-click #(on-resize :top %) - :r (/ radius zoom) - :cx (+ x (/ width 2)) - :cy (- y 2)}] - [:& control-item {:on-click #(on-resize :right %) - :r (/ radius zoom) - :cy (+ y (/ height 2)) - :cx (+ x width 1) - :class "right"}] - [:& control-item {:on-click #(on-resize :bottom %) - :r (/ radius zoom) - :cx (+ x (/ width 2)) - :cy (+ y height 2) - :class "bottom"}] - [:& control-item {:on-click #(on-resize :left %) - :r (/ radius zoom) - :cy (+ y (/ height 2)) - :cx (- x 3) - :class "left"}] - [:& control-item {:on-click #(on-resize :top-left %) - :r (/ radius zoom) - :cx x - :cy y - :class "top-left"}] - [:& control-item {:on-click #(on-resize :top-right %) - :r (/ radius zoom) - :cx (+ x width) - :cy y - :class "top-right"}] - [:& control-item {:on-click #(on-resize :bottom-left %) - :r (/ radius zoom) - :cx x - :cy (+ y height) - :class "bottom-left"}] - [:& control-item {:on-click #(on-resize :bottom-right %) - :r (/ radius zoom) - :cx (+ x width) - :cy (+ y height) - :class "bottom-right"}]])) + :cx cx + :cy cy}]])])) ;; --- Selection Handlers (Component) @@ -253,7 +246,7 @@ (st/emit! (start-resize %1 selected shape))) on-rotate #(do (dom/stop-propagation %) - (println "ROTATE!"))] + (st/emit! (start-rotate shapes)))] [:& controls {:shape shape :zoom zoom @@ -262,17 +255,11 @@ (mf/defc single-selection-handlers [{:keys [shape zoom objects] :as props}] - (let [on-resize #(do (dom/stop-propagation %2) + (let [shape (geom/transform-shape shape) + on-resize #(do (dom/stop-propagation %2) (st/emit! (start-resize %1 #{(:id shape)} shape))) on-rotate #(do (dom/stop-propagation %) - (st/emit! (start-rotate shape))) - - ds-modifier (:displacement-modifier shape) - rz-modifier (:resize-modifier shape) - ;; shape (geom/resolve-shape objects shape) - shape (cond-> (geom/shape->rect-shape shape) - (gmt/matrix? rz-modifier) (geom/transform rz-modifier) - (gmt/matrix? ds-modifier) (geom/transform ds-modifier))] + (st/emit! (start-rotate [shape])))] [:& controls {:shape shape :zoom zoom diff --git a/frontend/src/uxbox/util/geom/point.cljs b/frontend/src/uxbox/util/geom/point.cljs index 91beeaa37..96638c409 100644 --- a/frontend/src/uxbox/util/geom/point.cljs +++ b/frontend/src/uxbox/util/geom/point.cljs @@ -36,6 +36,11 @@ (throw (ex-info "Invalid arguments" {:v v})))) ([x y] (Point. x y))) +(defn center + [{:keys [x y width height]}] + (point (+ x (/ width 2)) + (+ y (/ height 2)))) + (defn add "Returns the addition of the supplied value to both coordinates of the point as a new point."