mirror of
https://github.com/penpot/penpot.git
synced 2025-01-25 16:09:17 -05:00
🎉 Multiple selection rotation
This commit is contained in:
parent
e59b20fb6e
commit
d86b5237c1
5 changed files with 146 additions and 104 deletions
|
@ -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?
|
||||
|
|
|
@ -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))))))
|
||||
|
|
|
@ -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]))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
|
|
Loading…
Add table
Reference in a new issue