Snap to square grid

This commit is contained in:
alonso.torres 2020-05-15 15:00:40 +02:00 committed by Andrés Moya
parent 3308d762f1
commit d2229f43c7
12 changed files with 159 additions and 171 deletions

View file

@ -66,7 +66,8 @@
:layers
:element-options
:rules
:dynamic-alignment})
:dynamic-alignment
:layouts})
(s/def ::options-mode #{:design :prototype})
@ -1525,6 +1526,17 @@
state
(assoc-in [:workspace-data pid :objects frame-id :layouts index] data))))))
(defn set-default-layout [type params]
(ptk/reify ::set-default-layout
dwc/IBatchedChange
;; TODO: Save into the backend
ptk/UpdateEvent
(update [_ state]
(->
state
(assoc-in [:workspace-page :options :saved-layouts type] params)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Exports
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -42,6 +42,12 @@
(def workspace-page
(l/derived :workspace-page st/state))
(def workspace-page-options
(l/derived :options workspace-page))
(def workspace-saved-layouts
(l/derived :saved-layouts workspace-page-options))
(def workspace-page-id
(l/derived :id workspace-page))
@ -71,6 +77,9 @@
(def workspace-objects
(l/derived :objects workspace-data))
(def workspace-frames
(l/derived cp/select-frames workspace-objects))
(defn object-by-id
[id]
(letfn [(selector [state]

View file

@ -73,7 +73,8 @@
[:& viewport {:page page
:key (:id page)
:file file
:local local}]]]
:local local
:layout layout}]]]
[:& left-toolbar {:page page :layout layout}]

View file

@ -11,13 +11,15 @@
(:require
[rumext.alpha :as mf]
[uxbox.main.refs :as refs]
[uxbox.common.pages :as cp]
[uxbox.util.geom.shapes :as gsh]
[uxbox.util.geom.layout :as ula]))
(mf/defc grid-layout [{:keys [frame zoom layout] :as props}]
(let [{:keys [color size] :as params} (-> layout :params)
{color-value :value color-opacity :opacity} color
{color-value :value color-opacity :opacity} (-> layout :params :color)
{frame-width :width frame-height :height :keys [x y]} frame]
[:g.grid
[:g.layout
[:*
(for [xs (range size frame-width size)]
[:line {:key (str (:id frame) "-y-" xs)
@ -38,20 +40,20 @@
:stroke-opacity color-opacity
:stroke-width (str (/ 1 zoom))}}])]]))
(mf/defc flex-layout [{:keys [frame zoom layout]}]
(mf/defc flex-layout [{:keys [key frame zoom layout]}]
(let [{color-value :value color-opacity :opacity} (-> layout :params :color)]
(for [{:keys [x y width height]} (ula/layout-rects frame layout)]
[:rect {:x x
:y y
:width width
:height height
:style {:pointer-events "none"
:fill color-value
:opacity color-opacity}}])))
[:g.layout
(for [{:keys [x y width height]} (ula/layout-rects frame layout)]
[:rect {:key (str key "-" x "-" y)
:x x
:y y
:width width
:height height
:style {:fill color-value
:opacity color-opacity}}])]))
(mf/defc layout-display [{:keys [frame]}]
(let [zoom (mf/deref refs/selected-zoom)
layouts (:layouts frame)]
(mf/defc layout-display-frame [{:keys [frame zoom]}]
(let [layouts (:layouts frame)]
(for [[index {:keys [type display] :as layout}] (map-indexed vector layouts)]
(let [props #js {:key (str (:id frame) "-layout-" index)
:frame frame
@ -62,3 +64,12 @@
:square [:> grid-layout props]
:column [:> flex-layout props]
:row [:> flex-layout props]))))))
(mf/defc layout-display [{:keys [zoom]}]
(let [frames (mf/deref refs/workspace-frames)]
[:g.layout-display {:style {:pointer-events "none"}}
(for [frame frames]
[:& layout-display-frame {:key (str "layout-" (:id frame))
:zoom zoom
:frame (gsh/transform-shape frame)}])]))

View file

@ -1,79 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
;; Copyright (c) 2015-2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.main.ui.workspace.ruler
(:require
[rumext.alpha :as mf]
[uxbox.main.constants :as c]
[uxbox.main.data.workspace :as udw]
[uxbox.main.store :as st]
[uxbox.util.dom :as dom]
[uxbox.util.geom.point :as gpt]
[uxbox.util.math :as mth]))
(mf/defc ruler-text
[{:keys [zoom ruler] :as props}]
#_(let [{:keys [start end]} ruler
distance (-> (gpt/distance (gpt/divide end zoom)
(gpt/divide start zoom))
(mth/precision 2))
angle (-> (gpt/angle end start)
(mth/precision 2))
transform1 (str "translate(" (+ (:x end) 35) "," (- (:y end) 10) ")")
transform2 (str "translate(" (+ (:x end) 25) "," (- (:y end) 30) ")")]
[:g
[:rect {:fill "black"
:fill-opacity "0.4"
:rx "3"
:ry "3"
:width "90"
:height "50"
:transform transform2}]
[:text {:transform transform1
:fill "white"}
[:tspan {:x "0"}
(str distance " px")]
[:tspan {:x "0" :y "20"}
(str angle "°")]]]))
(mf/defc ruler-line
[{:keys [zoom ruler] :as props}]
#_(let [{:keys [start end]} ruler]
[:line {:x1 (:x start)
:y1 (:y start)
:x2 (:x end)
:y2 (:y end)
:style {:cursor "cell"}
:stroke-width "1"
:stroke "red"}]))
(mf/defc ruler
[{:keys [ruler zoom] :as props}]
#_(letfn [(on-mouse-down [event]
(dom/stop-propagation event)
(st/emit! :interrupt
(udw/assign-cursor-tooltip nil)
(udw/start-ruler)))
(on-mouse-up [event]
(dom/stop-propagation event)
(st/emit! :interrupt))
(on-unmount []
(st/emit! :interrupt
(udw/clear-ruler)))]
(mf/use-effect (constantly on-unmount))
[:svg {:on-mouse-down on-mouse-down
:on-mouse-up on-mouse-up}
[:rect {:style {:fill "transparent"
:stroke "transparent"
:cursor "cell"}
:width c/viewport-width
:height c/viewport-height}]
(when ruler
[:g
[:& ruler-line {:ruler ruler}]
[:& ruler-text {:ruler ruler :zoom zoom}]])]))

View file

@ -22,8 +22,7 @@
[uxbox.util.geom.shapes :as geom]
[uxbox.util.dom :as dom]
[uxbox.main.streams :as ms]
[uxbox.util.timers :as ts]
[uxbox.main.ui.workspace.layout-display :refer [layout-display]]))
[uxbox.util.timers :as ts]))
(defn- frame-wrapper-factory-equals?
[np op]
@ -105,6 +104,5 @@
(:name shape)]
[:& frame-shape
{:shape shape
:childs childs}]
[:& layout-display {:frame shape}]])))))
:childs childs}]])))))

View file

@ -12,7 +12,9 @@
[rumext.alpha :as mf]
[uxbox.util.dom :as dom]
[uxbox.util.data :as d]
[uxbox.common.data :refer [parse-integer]]
[uxbox.main.store :as st]
[uxbox.main.refs :as refs]
[uxbox.main.data.workspace :as dw]
[uxbox.main.ui.icons :as i]
[uxbox.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]]
@ -29,27 +31,8 @@
[:div.advanced-options {}
children]]))
(defonce ^:private default-params
{:square {:size 16
:color {:value "#59B9E2"
:opacity 0.9}}
:column {:size 12
:type :stretch
:item-width nil
:gutter 8
:margin 0
:color {:value "#DE4762"
:opacity 0.1}}
:row {:size 12
:type :stretch
:item-height nil
:gutter 8
:margin 0
:color {:value "#DE4762"
:opacity 0.1}}})
(mf/defc layout-options [{:keys [layout on-change on-remove]}]
(mf/defc layout-options [{:keys [layout default-layout-params on-change on-remove]}]
(prn "(render) layout" layout)
(let [state (mf/use-state {:show-advanced-options false
:changes {}})
{:keys [type display params] :as layout} (d/deep-merge layout (:changes @state))
@ -61,17 +44,18 @@
18 12 10 8 6 4 3 2]
emit-changes! (fn [update-fn]
(swap! state update :changes update-fn)
(when on-change (on-change (d/deep-merge layout (-> @state :changes update-fn)))))
(swap! state update :changes update-fn)
(prn "(event) layout" (d/deep-merge layout (-> @state :changes update-fn)))
(when on-change (on-change (d/deep-merge layout (-> @state :changes update-fn)))))
handle-toggle-visibility (fn [event]
(emit-changes! #(update % :display not)))
(emit-changes! (fn [changes] (update changes :display #(if (nil? %) false (not %))))))
handle-remove-layout (fn [event]
(when on-remove (on-remove)))
handle-change-type (fn [type]
(let [defaults (type default-params)
(let [defaults (type default-layout-params)
params (merge
defaults
(select-keys (keys defaults) (-> @state :changes params)))
@ -85,7 +69,7 @@
handle-change-event (fn [& keys]
(fn [event]
(let [change-fn (apply handle-change keys)]
(-> event dom/get-target dom/get-value change-fn))))
(-> event dom/get-target dom/get-value parse-integer change-fn))))
]
[:div.grid-option
@ -169,20 +153,45 @@
[:button.btn-options "Use default"]
[:button.btn-options "Set as default"]]]]))
(defonce ^:private default-layout-params
{:square {:size 16
:color {:value "#59B9E2"
:opacity 0.9}}
:column {:size 12
:type :stretch
:item-width nil
:gutter 8
:margin 0
:color {:value "#DE4762"
:opacity 0.1}}
:row {:size 12
:type :stretch
:item-height nil
:gutter 8
:margin 0
:color {:value "#DE4762"
:opacity 0.1}}})
(mf/defc frame-layouts [{:keys [shape]}]
(let [id (:id shape)
default-layout-params (merge default-layout-params (mf/deref refs/workspace-saved-layouts))
handle-create-layout #(st/emit! (dw/add-frame-layout id))
handle-remove-layout (fn [index] #(st/emit! (dw/remove-frame-layout id index)))
handle-edit-layout (fn [index] #(st/emit! (dw/set-frame-layout id index %)))]
handle-edit-layout (fn [index] #(st/emit! (dw/set-frame-layout id index %)))
handle-save-layout (fn [layout] (st/emit! (dw/set-default-layout (:type layout) (:params layout))))]
[:div.element-set
[:div.element-set-title
[:span "Grid & Layout"]
[:div.add-page {:on-click handle-create-layout} i/close]]
[:div.element-set-content
(for [[index layout] (map-indexed vector (:layouts shape))]
[:& layout-options {:key (str (:id shape) "-" index)
:layout layout
:on-change (handle-edit-layout index)
:on-remove (handle-remove-layout index)}])]]))
(when (not (empty? (:layouts shape)))
[:div.element-set-content
(for [[index layout] (map-indexed vector (:layouts shape))]
[:& layout-options {:key (str (:id shape) "-" index)
:layout layout
:default-layout-params default-layout-params
:on-change (handle-edit-layout index)
:on-remove (handle-remove-layout index)
:on-save-layout handle-save-layout}])])]))

View file

@ -26,11 +26,10 @@
[uxbox.main.ui.workspace.shapes :refer [shape-wrapper frame-wrapper]]
[uxbox.main.ui.workspace.shapes.interactions :refer [interactions]]
[uxbox.main.ui.workspace.drawarea :refer [draw-area start-drawing]]
[uxbox.main.ui.workspace.grid :refer [grid]]
[uxbox.main.ui.workspace.ruler :refer [ruler]]
[uxbox.main.ui.workspace.selection :refer [selection-handlers]]
[uxbox.main.ui.workspace.presence :as presence]
[uxbox.main.ui.workspace.snap-feedback :refer [snap-feedback]]
[uxbox.main.ui.workspace.layout-display :refer [layout-display]]
[uxbox.util.math :as mth]
[uxbox.util.dom :as dom]
[uxbox.util.object :as obj]
@ -127,7 +126,7 @@
:key (:id item)}]))]))
(mf/defc viewport
[{:keys [page local] :as props}]
[{:keys [page local layout] :as props}]
(let [{:keys [drawing-tool
options-mode
zoom
@ -354,6 +353,7 @@
]
(mf/use-effect on-mount)
[:svg.viewport
{:preserveAspectRatio "xMidYMid meet"
:width (:width vport 0)
@ -381,22 +381,18 @@
:zoom zoom
:edition edition}])
(when-let [drawing-shape (:drawing local)]
[:& draw-area {:shape drawing-shape
:zoom zoom
:modifiers (:modifiers local)}])
(when (contains? layout :layouts)
[:& layout-display {:zoom zoom}])
[:& snap-feedback]
(when (contains? flags :grid)
[:& grid])]
(when tooltip
[:& cursor-tooltip {:zoom zoom :tooltip tooltip}])
(when (contains? flags :ruler)
[:& ruler {:zoom zoom :ruler (:ruler local)}])
(when tooltip
[:& cursor-tooltip {:zoom zoom :tooltip tooltip}])]
[:& presence/active-cursors {:page page}]
[:& selection-rect {:data (:selrect local)}]

View file

@ -7,7 +7,9 @@
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.util.geom.layout)
(ns uxbox.util.geom.layout
(:require
[uxbox.util.geom.point :as gpt]))
(defn calculate-column-layout [{:keys [width height x y] :as frame} {:keys [size gutter margin item-width type] :as params}]
(let [parts (/ width size)
@ -20,7 +22,7 @@
gutter (if (= :stretch type) (/ (- width (* item-width size) (* margin 2)) (dec size)) gutter)
next-x (fn [cur-val] (+ initial-offset x (* (+ item-width gutter) cur-val)))
next-y (fn [cur-val] y)]
[item-width item-height next-x next-y]))
[size item-width item-height next-x next-y]))
(defn calculate-row-layout [{:keys [width height x y] :as frame} {:keys [size gutter margin item-height type] :as params}]
(let [{:keys [width height x y]} frame
@ -34,16 +36,51 @@
gutter (if (= :stretch type) (/ (- height (* item-height size) (* margin 2)) (dec size)) gutter)
next-x (fn [cur-val] x)
next-y (fn [cur-val] (+ initial-offset y (* (+ item-height gutter) cur-val)))]
[item-width item-height next-x next-y]))
[size item-width item-height next-x next-y]))
(defn calculate-grid-layout [{:keys [width height x y] :as frame} {:keys [size] :as params}]
(let [col-size (quot width size)
row-size (quot height size)
as-row-col (fn [value] [(quot value col-size) (rem value col-size)])
next-x (fn [cur-val]
(let [[_ col] (as-row-col cur-val)] (+ x (* col size))))
next-y (fn [cur-val]
(let [[row _] (as-row-col cur-val)] (+ y (* row size))))]
[(* col-size row-size) size size next-x next-y]))
(defn layout-rects [frame layout]
(let [[item-width item-height next-x next-y]
(case (-> layout :type)
:column (calculate-column-layout frame (-> layout :params))
:row (calculate-row-layout frame (-> layout :params)))]
(let [layout-fn (case (-> layout :type)
:column calculate-column-layout
:row calculate-row-layout
:square calculate-grid-layout)
[num-items item-width item-height next-x next-y] (layout-fn frame (-> layout :params))]
(->>
(range 0 (-> layout :params :size))
(range 0 num-items)
(map #(hash-map :x (next-x %)
:y (next-y %)
:width item-width
:height item-height)))))
(defn- layout-rect-points [{:keys [x y width height]}]
[(gpt/point x y)
(gpt/point (+ x width) y)
(gpt/point (+ x width) (+ y height))
(gpt/point x (+ y height))])
(defn- layout-snap-points
([shape coord] (mapcat #(layout-snap-points shape % coord) (:layouts shape)))
([shape {:keys [type display params] :as layout} coord]
(case type
:square (let [{:keys [x y width height]} shape
size (-> params :size)]
(if (= coord :x)
(mapcat #(vector (gpt/point (+ x %) y)
(gpt/point (+ x %) (+ y height))) (range size width size))
(mapcat #(vector (gpt/point x (+ y %))
(gpt/point (+ x width) (+ y %))) (range size height size))))
:column (when (= coord :x) (->> (layout-rects shape layout)
(mapcat layout-rect-points)))
:row (when (= coord :y) (->> (layout-rects shape layout)
(mapcat layout-rect-points))))))

View file

@ -12,17 +12,7 @@
[cljs.spec.alpha :as s]
[clojure.set :as set]
[uxbox.util.geom.shapes :as gsh]
[uxbox.util.geom.point :as gpt]
[uxbox.util.geom.layout :as gla]))
(defn- layout-rect-snaps [{:keys [x y width height]}]
#{(gpt/point x y)
(gpt/point (+ x width) y)
(gpt/point (+ x width) (+ y height))
(gpt/point x (+ y height))})
(defn- layout-snap-points [frame {:keys [type] :as layout}]
(mapcat layout-rect-snaps (gla/layout-rects frame layout)))
[uxbox.util.geom.point :as gpt]))
(defn- frame-snap-points [{:keys [x y width height layouts] :as frame}]
(into #{(gpt/point x y)
@ -32,11 +22,7 @@
(gpt/point (+ x width) (+ y height))
(gpt/point (+ x (/ width 2)) (+ y height))
(gpt/point x (+ y height))
(gpt/point x (+ y (/ height 2)))}
(->>
layouts
(filter #(and (not= :grid (:type %)) (:display %)))
(mapcat #(layout-snap-points frame %)))))
(gpt/point x (+ y (/ height 2)))}))
(defn shape-snap-points
[shape]

View file

@ -14,7 +14,8 @@
[uxbox.common.pages :as cp]
[uxbox.worker.impl :as impl]
[uxbox.util.range-tree :as rt]
[uxbox.util.geom.snap-points :as snap]))
[uxbox.util.geom.snap-points :as snap]
[uxbox.util.geom.layout :as gla]))
(defonce state (l/atom {}))
@ -23,8 +24,11 @@
[shapes coord]
(let [process-shape (fn [coord]
(fn [shape]
(let [points (snap/shape-snap-points shape)]
(map #(vector % (:id shape)) points))))
(concat
(let [points (snap/shape-snap-points shape)]
(map #(vector % (:id shape)) points))
(let [points (gla/layout-snap-points shape coord)]
(map #(vector % :layout) points)))))
into-tree (fn [tree [point _ :as data]]
(rt/insert tree (coord point) data))]
(->> shapes