0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-05 19:41:27 -05:00

Merge pull request #163 from uxbox/group_shape

Group shapes
This commit is contained in:
Andrey Antukh 2020-04-06 12:29:56 +02:00 committed by GitHub
commit c10c6cf149
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 305 additions and 441 deletions

View file

@ -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]

View file

@ -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)))

View file

@ -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))

View file

@ -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))))

View file

@ -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)))

View file

@ -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)

View file

@ -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)))

View file

@ -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)))

View file

@ -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

View file

@ -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)"

View file

@ -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)"

View file

@ -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

View file

@ -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))

View file

@ -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)"

View file

@ -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}]

View file

@ -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

View file

@ -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))))