🎉 Add space distribution of shapes

This commit is contained in:
Andrés Moya 2020-04-09 09:57:24 +02:00 committed by Alonso Torres
parent a0c5f32a42
commit e9d60913d0
8 changed files with 250 additions and 55 deletions

View file

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

View file

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

View file

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

View file

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