mirror of
https://github.com/penpot/penpot.git
synced 2025-06-15 00:31:38 +02:00
🎉 Add space distribution of shapes
This commit is contained in:
parent
a0c5f32a42
commit
e9d60913d0
8 changed files with 250 additions and 55 deletions
|
@ -72,9 +72,11 @@
|
|||
(def shape-halign-left (icon-xref :shape-halign-left))
|
||||
(def shape-halign-center (icon-xref :shape-halign-center))
|
||||
(def shape-halign-right (icon-xref :shape-halign-right))
|
||||
(def shape-hdistribute (icon-xref :shape-hdistribute))
|
||||
(def shape-valign-top (icon-xref :shape-valign-top))
|
||||
(def shape-valign-center (icon-xref :shape-valign-center))
|
||||
(def shape-valign-bottom (icon-xref :shape-valign-bottom))
|
||||
(def shape-vdistribute (icon-xref :shape-vdistribute))
|
||||
(def size-horiz (icon-xref :size-horiz))
|
||||
(def size-vert (icon-xref :size-vert))
|
||||
(def stroke (icon-xref :stroke))
|
||||
|
|
|
@ -1445,14 +1445,14 @@
|
|||
(rx/of (commit-changes [rchange] [uchange]))))))
|
||||
|
||||
|
||||
;; --- Shape / Selection Alignment
|
||||
;; --- Shape / Selection Alignment and Distribution
|
||||
|
||||
(declare align-object-to-frame)
|
||||
(declare align-objects-list)
|
||||
|
||||
(defn align-objects
|
||||
[axis]
|
||||
(us/verify ::geom/axis axis)
|
||||
(us/verify ::geom/align-axis axis)
|
||||
(ptk/reify :align-objects
|
||||
IBatchedChange
|
||||
ptk/UpdateEvent
|
||||
|
@ -1478,6 +1478,21 @@
|
|||
rect (geom/selection-rect selected-objs)]
|
||||
(map #(geom/align-to-rect % rect axis) selected-objs)))
|
||||
|
||||
(defn distribute-objects
|
||||
[axis]
|
||||
(us/verify ::geom/dist-axis axis)
|
||||
(ptk/reify :align-objects
|
||||
IBatchedChange
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [page-id (::page-id state)
|
||||
objects (get-in state [:workspace-data page-id :objects])
|
||||
selected (get-in state [:workspace-local :selected])
|
||||
selected-objs (map #(get objects %) selected)
|
||||
moved-objs (geom/distribute-space selected-objs axis)
|
||||
updated-objs (merge objects (d/index-by :id moved-objs))]
|
||||
(assoc-in state [:workspace-data page-id :objects] updated-objs)))))
|
||||
|
||||
|
||||
;; --- Temportal displacement for Shape / Selection
|
||||
|
||||
|
|
|
@ -107,7 +107,6 @@
|
|||
|
||||
;; --- Size
|
||||
|
||||
(declare size-rect)
|
||||
(declare size-circle)
|
||||
(declare size-path)
|
||||
|
||||
|
@ -141,6 +140,39 @@
|
|||
(merge shape {:width (* rx 2)
|
||||
:height (* ry 2)}))
|
||||
|
||||
;; --- Center
|
||||
|
||||
(declare center-rect)
|
||||
(declare center-circle)
|
||||
(declare center-path)
|
||||
|
||||
(defn center
|
||||
"Calculate the center of the shape."
|
||||
[shape]
|
||||
(case (:type shape)
|
||||
:circle (center-circle shape)
|
||||
:curve (center-path shape)
|
||||
:path (center-path shape)
|
||||
(center-rect shape)))
|
||||
|
||||
(defn- center-rect
|
||||
[{:keys [x y width height] :as shape}]
|
||||
(gpt/point (+ x (/ width 2)) (+ y (/ height 2))))
|
||||
|
||||
(defn- center-circle
|
||||
[{:keys [cx cy] :as shape}]
|
||||
(gpt/point cx cy))
|
||||
|
||||
(defn- center-path
|
||||
[{:keys [segments x1 y1 x2 y2] :as shape}]
|
||||
(if (and x1 y1 x2 y2)
|
||||
(gpt/point (/ (+ x1 x2) 2) (/ (+ y1 y2) 2))
|
||||
(let [minx (apply min (map :x segments))
|
||||
miny (apply min (map :y segments))
|
||||
maxx (apply max (map :x segments))
|
||||
maxy (apply max (map :y segments))]
|
||||
(gpt/point (/ (+ minx maxx) 2) (/ (+ miny maxy) 2)))))
|
||||
|
||||
;; --- Proportions
|
||||
|
||||
(declare assign-proportions-path)
|
||||
|
@ -566,10 +598,9 @@
|
|||
[shape {:keys [x y] :as frame}]
|
||||
(move shape (gpt/point (+ x) (+ y))))
|
||||
|
||||
|
||||
;; --- Alignment
|
||||
|
||||
(s/def ::axis #{:hleft :hcenter :hright :vtop :vcenter :vbottom})
|
||||
(s/def ::align-axis #{:hleft :hcenter :hright :vtop :vcenter :vbottom})
|
||||
|
||||
(declare calc-align-pos)
|
||||
|
||||
|
@ -612,6 +643,51 @@
|
|||
{:x (:x wrapper-rect)
|
||||
:y (- bottom (:height wrapper-rect))})))
|
||||
|
||||
;; --- Distribute
|
||||
|
||||
(s/def ::dist-axis #{:horizontal :vertical})
|
||||
|
||||
(defn distribute-space
|
||||
"Distribute equally the space between shapes in the given axis. If
|
||||
there is no space enough, it does nothing. It takes into account
|
||||
the form of the shape and the rotation, what is distributed is
|
||||
the wrapping recangles of the shapes."
|
||||
[shapes axis]
|
||||
(let [coord (if (= axis :horizontal) :x :y)
|
||||
other-coord (if (= axis :horizontal) :y :x)
|
||||
size (if (= axis :horizontal) :width :height)
|
||||
; The rectangle that wraps the whole selection
|
||||
wrapper-rect (selection-rect shapes)
|
||||
; Sort shapes by the center point in the given axis
|
||||
sorted-shapes (sort-by #(coord (center %)) shapes)
|
||||
; Each shape wrapped in its own rectangle
|
||||
wrapped-shapes (map #(selection-rect [%]) sorted-shapes)
|
||||
; The total space between shapes
|
||||
space (reduce - (size wrapper-rect) (map size wrapped-shapes))]
|
||||
|
||||
(if (<= space 0)
|
||||
shapes
|
||||
(let [unit-space (/ space (- (count wrapped-shapes) 1))
|
||||
; Calculate the distance we need to move each shape.
|
||||
; The new position of each one is the position of the
|
||||
; previous one plus its size plus the unit space.
|
||||
deltas (loop [shapes' wrapped-shapes
|
||||
start-pos (coord wrapper-rect)
|
||||
deltas []]
|
||||
|
||||
(let [first-shape (first shapes')
|
||||
delta (- start-pos (coord first-shape))
|
||||
new-pos (+ start-pos (size first-shape) unit-space)]
|
||||
|
||||
(if (= (count shapes') 1)
|
||||
(conj deltas delta)
|
||||
(recur (rest shapes')
|
||||
new-pos
|
||||
(conj deltas delta)))))]
|
||||
|
||||
(map #(move %1 {coord %2 other-coord 0}) sorted-shapes deltas)))))
|
||||
|
||||
|
||||
;; --- Helpers
|
||||
|
||||
(defn contained-in?
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
[uxbox.main.refs :as refs]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.data.workspace :as dw]
|
||||
[uxbox.util.i18n :as i18n :refer [t]]
|
||||
[uxbox.util.uuid :as uuid]))
|
||||
|
||||
(mf/defc align-options
|
||||
|
@ -25,26 +26,67 @@
|
|||
:else
|
||||
(= uuid/zero (:frame-id (get objects (first selected)))))
|
||||
|
||||
disabled-distribute (cond
|
||||
(empty? selected) true
|
||||
(< (count selected) 2) true
|
||||
:else false)
|
||||
|
||||
locale (i18n/use-locale)
|
||||
|
||||
on-align-button-clicked
|
||||
(fn [axis] (when-not disabled (st/emit! (dw/align-objects axis))))]
|
||||
(fn [axis] (when-not disabled (st/emit! (dw/align-objects axis))))
|
||||
|
||||
on-distribute-button-clicked
|
||||
(fn [axis] (when-not disabled-distribute (st/emit! (dw/distribute-objects axis))))]
|
||||
|
||||
[:div.align-options
|
||||
[:div.align-button {:class (when disabled "disabled")
|
||||
:on-click #(on-align-button-clicked :hleft)}
|
||||
i/shape-halign-left]
|
||||
[:div.align-button {:class (when disabled "disabled")
|
||||
:on-click #(on-align-button-clicked :hcenter)}
|
||||
i/shape-halign-center]
|
||||
[:div.align-button {:class (when disabled "disabled")
|
||||
:on-click #(on-align-button-clicked :hright)}
|
||||
i/shape-halign-right]
|
||||
[:div.align-button {:class (when disabled "disabled")
|
||||
:on-click #(on-align-button-clicked :vtop)}
|
||||
i/shape-valign-top]
|
||||
[:div.align-button {:class (when disabled "disabled")
|
||||
:on-click #(on-align-button-clicked :vcenter)}
|
||||
i/shape-valign-center]
|
||||
[:div.align-button {:class (when disabled "disabled")
|
||||
:on-click #(on-align-button-clicked :vbottom)}
|
||||
i/shape-valign-bottom]]))
|
||||
[:div.align-group
|
||||
[:div.align-button.tooltip.tooltip-bottom
|
||||
{:alt (t locale "workspace.align.hleft")
|
||||
:class (when disabled "disabled")
|
||||
:on-click #(on-align-button-clicked :hleft)}
|
||||
i/shape-halign-left]
|
||||
|
||||
[:div.align-button.tooltip.tooltip-bottom
|
||||
{:alt (t locale "workspace.align.hcenter")
|
||||
:class (when disabled "disabled")
|
||||
:on-click #(on-align-button-clicked :hcenter)}
|
||||
i/shape-halign-center]
|
||||
|
||||
[:div.align-button.tooltip.tooltip-bottom
|
||||
{:alt (t locale "workspace.align.hright")
|
||||
:class (when disabled "disabled")
|
||||
:on-click #(on-align-button-clicked :hright)}
|
||||
i/shape-halign-right]
|
||||
|
||||
[:div.align-button.tooltip.tooltip-bottom
|
||||
{:alt (t locale "workspace.align.hdistribute")
|
||||
:class (when disabled-distribute "disabled")
|
||||
:on-click #(on-distribute-button-clicked :horizontal)}
|
||||
i/shape-hdistribute]]
|
||||
|
||||
[:div.align-group
|
||||
[:div.align-button.tooltip.tooltip-bottom
|
||||
{:alt (t locale "workspace.align.vtop")
|
||||
:class (when disabled "disabled")
|
||||
:on-click #(on-align-button-clicked :vtop)}
|
||||
i/shape-valign-top]
|
||||
|
||||
[:div.align-button.tooltip.tooltip-bottom
|
||||
{:alt (t locale "workspace.align.vcenter")
|
||||
:class (when disabled "disabled")
|
||||
:on-click #(on-align-button-clicked :vcenter)}
|
||||
i/shape-valign-center]
|
||||
|
||||
[:div.align-button.tooltip.tooltip-bottom
|
||||
{:alt (t locale "workspace.align.vbottom")
|
||||
:class (when disabled "disabled")
|
||||
:on-click #(on-align-button-clicked :vbottom)}
|
||||
i/shape-valign-bottom]
|
||||
|
||||
[:div.align-button.tooltip.tooltip-bottom
|
||||
{:alt (t locale "workspace.align.vdistribute")
|
||||
:class (when disabled-distribute "disabled")
|
||||
:on-click #(on-distribute-button-clicked :vertical)}
|
||||
i/shape-vdistribute]]]))
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue