0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-15 11:38:24 -05:00

Merge pull request #194 from uxbox/201/dynamic_alignment

Dynamic alignment (brute force)
This commit is contained in:
Andrey Antukh 2020-05-05 11:23:36 +02:00 committed by GitHub
commit e92688f37e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 574 additions and 103 deletions

View file

@ -33,6 +33,7 @@
[uxbox.util.geom.matrix :as gmt] [uxbox.util.geom.matrix :as gmt]
[uxbox.util.geom.point :as gpt] [uxbox.util.geom.point :as gpt]
[uxbox.util.geom.shapes :as geom] [uxbox.util.geom.shapes :as geom]
[uxbox.util.geom.snap :as snap]
[uxbox.util.math :as mth] [uxbox.util.math :as mth]
[uxbox.util.router :as rt] [uxbox.util.router :as rt]
[uxbox.util.transit :as t] [uxbox.util.transit :as t]
@ -146,7 +147,8 @@
(-> state (-> state
(assoc :current-page-id page-id ; mainly used by events (assoc :current-page-id page-id ; mainly used by events
:workspace-local local :workspace-local local
:workspace-page (dissoc page :data)) :workspace-page (dissoc page :data)
:workspace-snap-data (snap/initialize-snap-data (get-in page [:data :objects])))
(assoc-in [:workspace-data page-id] (:data page))))) (assoc-in [:workspace-data page-id] (:data page)))))
ptk/WatchEvent ptk/WatchEvent
@ -691,7 +693,6 @@
:left (gpt/point (- 1) 0) :left (gpt/point (- 1) 0)
:right (gpt/point 1 0))) :right (gpt/point 1 0)))
(s/def ::direction #{:up :down :right :left})
(s/def ::loc #{:up :down :bottom :top}) (s/def ::loc #{:up :down :bottom :top})
;; --- Delete Selected ;; --- Delete Selected

View file

@ -9,7 +9,8 @@
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.common.uuid :as uuid] [uxbox.common.uuid :as uuid]
[uxbox.main.worker :as uw] [uxbox.main.worker :as uw]
[uxbox.util.geom.shapes :as geom])) [uxbox.util.geom.shapes :as geom]
[uxbox.util.geom.snap :as snap]))
;; --- Protocols ;; --- Protocols
@ -19,6 +20,7 @@
(declare setup-selection-index) (declare setup-selection-index)
(declare update-selection-index) (declare update-selection-index)
(declare update-snap-data)
(declare reset-undo) (declare reset-undo)
(declare append-undo) (declare append-undo)
@ -51,7 +53,8 @@
(let [page (:workspace-page state) (let [page (:workspace-page state)
uidx (get-in state [:workspace-local :undo-index] ::not-found)] uidx (get-in state [:workspace-local :undo-index] ::not-found)]
(rx/concat (rx/concat
(rx/of (update-selection-index (:id page))) (rx/of (update-selection-index (:id page))
(update-snap-data (:id page)))
(when (and save-undo? (not= uidx ::not-found)) (when (and save-undo? (not= uidx ::not-found))
(rx/of (reset-undo uidx))) (rx/of (reset-undo uidx)))
@ -138,6 +141,15 @@
:page-id page-id :page-id page-id
:objects objects}))))) :objects objects})))))
(defn update-snap-data
[page-id]
(ptk/reify ::update-snap-data
ptk/UpdateEvent
(update [_ state]
(let [page (get-in state [:workspace-pages page-id])
objects (get-in page [:data :objects])]
(-> state
(assoc :workspace-snap-data (snap/initialize-snap-data objects)))))))
;; --- Common Helpers & Events ;; --- Common Helpers & Events

View file

@ -3,16 +3,19 @@
(:require (:require
[beicon.core :as rx] [beicon.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[beicon.core :as rx]
[potok.core :as ptk] [potok.core :as ptk]
[uxbox.common.data :as d] [uxbox.common.data :as d]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.main.data.helpers :as helpers] [uxbox.main.data.helpers :as helpers]
[uxbox.main.data.workspace.common :as dwc] [uxbox.main.data.workspace.common :as dwc]
[uxbox.main.refs :as refs] [uxbox.main.refs :as refs]
[uxbox.main.store :as st]
[uxbox.main.streams :as ms] [uxbox.main.streams :as ms]
[uxbox.util.geom.matrix :as gmt] [uxbox.util.geom.matrix :as gmt]
[uxbox.util.geom.point :as gpt] [uxbox.util.geom.point :as gpt]
[uxbox.util.geom.shapes :as gsh])) [uxbox.util.geom.shapes :as gsh]
[uxbox.util.geom.snap :as snap]))
;; -- Declarations ;; -- Declarations
@ -62,23 +65,38 @@
:bottom-left [ex sy])] :bottom-left [ex sy])]
(gpt/point x y))) (gpt/point x y)))
(defn finish-transform [state]
(update state :workspace-local dissoc :transform))
(defn handler->initial-point [{:keys [x1 y1 x2 y2] :as shape} handler]
(let [[x y] (case handler
:top-left [x1 y1]
:top [x1 y1]
:top-right [x2 y1]
:right [x2 y1]
:bottom-right [x2 y2]
:bottom [x2 y2]
:bottom-left [x1 y2]
:left [x1 y2])]
(gpt/point x y)))
;; -- RESIZE ;; -- RESIZE
(defn start-resize (defn start-resize
[handler ids shape objects] [handler ids shape]
(letfn [(resize [shape initial [point lock?]] (letfn [(resize [shape initial resizing-shapes snap-data [point lock?]]
(let [frame (get objects (:frame-id shape)) (let [{:keys [width height rotation]} shape
{:keys [width height rotation]} shape
shapev (-> (gpt/point width height)) shapev (-> (gpt/point width height))
;; Vector modifiers depending on the handler ;; Vector modifiers depending on the handler
handler-modif (let [[x y] (handler-modifiers handler)] (gpt/point x y)) handler-modif (let [[x y] (handler-modifiers handler)] (gpt/point x y))
point-snap (snap/closest-snap-point snap-data resizing-shapes point)
;; Difference between the origin point in the coordinate system of the rotation ;; Difference between the origin point in the coordinate system of the rotation
deltav (-> (gpt/subtract point initial) deltav (-> (gpt/to-vec initial (if (= rotation 0) point-snap point))
(gpt/transform (gmt/rotate-matrix (- rotation))) (gpt/transform (gmt/rotate-matrix (- rotation)))
(gpt/multiply handler-modif)) (gpt/multiply handler-modif))
;; Resize vector ;; Resize vector
scalev (gpt/divide (gpt/add shapev deltav) shapev) scalev (gpt/divide (gpt/add shapev deltav) shapev)
@ -113,27 +131,41 @@
;; (rx/of point))) ;; (rx/of point)))
] ]
(reify (reify
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-local :transform] :resize)))
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [initial (apply-zoom @ms/mouse-position) (let [shape (gsh/shape->rect-shape shape)
shape (gsh/shape->rect-shape shape) initial (handler->initial-point shape handler)
stoper (rx/filter ms/mouse-up? stream)] stoper (rx/filter ms/mouse-up? stream)
snap-data (get state :workspace-snap-data)
page-id (get state :current-page-id)
resizing-shapes (map #(get-in state [:workspace-data page-id :objects %]) ids)]
(rx/concat (rx/concat
(->> ms/mouse-position (->> ms/mouse-position
(rx/map apply-zoom) (rx/map apply-zoom)
;; (rx/mapcat apply-grid-alignment) ;; (rx/mapcat apply-grid-alignment)
(rx/with-latest vector ms/mouse-position-ctrl) (rx/with-latest vector ms/mouse-position-ctrl)
(rx/map normalize-proportion-lock) (rx/map normalize-proportion-lock)
(rx/mapcat (partial resize shape initial)) (rx/mapcat (partial resize shape initial resizing-shapes snap-data))
(rx/take-until stoper)) (rx/take-until stoper))
#_(rx/empty) #_(rx/empty)
(rx/of (apply-modifiers ids)))))))) (rx/of (apply-modifiers ids)
finish-transform)))))))
;; -- ROTATE ;; -- ROTATE
(defn start-rotate (defn start-rotate
[shapes] [shapes]
(ptk/reify ::start-rotate (ptk/reify ::start-rotate
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-local :transform] :rotate)))
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [stoper (rx/filter ms/mouse-up? stream) (let [stoper (rx/filter ms/mouse-up? stream)
@ -161,36 +193,55 @@
(let [delta-angle (calculate-angle pos ctrl?)] (let [delta-angle (calculate-angle pos ctrl?)]
(set-rotation delta-angle shapes group-center)))) (set-rotation delta-angle shapes group-center))))
(rx/take-until stoper)) (rx/take-until stoper))
(rx/of (apply-modifiers (map :id shapes)))))))) (rx/of (apply-modifiers (map :id shapes))
finish-transform))))))
;; -- MOVE ;; -- MOVE
(declare start-move)
(defn start-move-selected (defn start-move-selected
[] []
(ptk/reify ::start-move-selected (ptk/reify ::start-move-selected
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [selected (get-in state [:workspace-local :selected]) (let [initial (apply-zoom @ms/mouse-position)
stoper (rx/filter ms/mouse-up? stream) selected (get-in state [:workspace-local :selected])
zero-point? #(= % (gpt/point 0 0)) stopper (rx/filter ms/mouse-up? stream)]
initial (apply-zoom @ms/mouse-position) (->> ms/mouse-position
position @ms/mouse-position (rx/take-until stopper)
counter (volatile! 0)] (rx/map apply-zoom)
(rx/map #(gpt/to-vec initial %))
(rx/map #(gpt/length %))
(rx/filter #(> % 0.5))
(rx/take 1)
(rx/map #(start-move initial selected)))))))
(defn start-move
[from-position ids]
(ptk/reify ::start-move
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-local :transform] :move)))
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (get state :current-page-id)
shapes (mapv #(get-in state [:workspace-data page-id :objects %]) ids)
snap-data (get state :workspace-snap-data)
stopper (rx/filter ms/mouse-up? stream)]
(rx/concat (rx/concat
(->> ms/mouse-position (->> ms/mouse-position
(rx/take-until stopper)
(rx/map apply-zoom) (rx/map apply-zoom)
(rx/filter (complement zero-point?)) (rx/map #(gpt/to-vec from-position %))
(rx/map #(gpt/subtract % initial)) (rx/map (snap/closest-snap-move snap-data shapes))
(rx/map gmt/translate-matrix) (rx/map gmt/translate-matrix)
(rx/filter #(not (gmt/base? %))) (rx/map #(set-modifiers ids {:displacement %})))
(rx/map #(set-modifiers selected {:displacement %}))
(rx/tap #(vswap! counter inc)) (rx/of (apply-modifiers ids)
(rx/take-until stoper)) finish-transform))))))
(->> (rx/create (fn [sink] (sink @counter)))
(rx/mapcat (fn [n]
(if (zero? n)
(rx/empty)
(rx/of (apply-modifiers selected)))))))))))
(defn- get-displacement-with-grid (defn- get-displacement-with-grid
"Retrieve the correct displacement delta point for the "Retrieve the correct displacement delta point for the
@ -209,31 +260,60 @@
(defn- get-displacement (defn- get-displacement
"Retrieve the correct displacement delta point for the "Retrieve the correct displacement delta point for the
provided direction speed and distances thresholds." provided direction speed and distances thresholds."
[shape direction] [direction]
(case direction (case direction
:up (gpt/point 0 (- 1)) :up (gpt/point 0 (- 1))
:down (gpt/point 0 1) :down (gpt/point 0 1)
:left (gpt/point (- 1) 0) :left (gpt/point (- 1) 0)
:right (gpt/point 1 0))) :right (gpt/point 1 0)))
(s/def ::direction #{:up :down :right :left})
(defn move-selected (defn move-selected
[direction align?] [direction align?]
(us/verify ::direction direction) (us/verify ::direction direction)
(us/verify boolean? align?) (us/verify boolean? align?)
(ptk/reify ::move-selected (let [same-event (js/Symbol "same-event")]
ptk/WatchEvent (ptk/reify ::move-selected
(watch [_ state stream] IDeref
(let [pid (:current-page-id state) (-deref [_] direction)
selected (get-in state [:workspace-local :selected])
options (get-in state [:workspace-data pid :options]) ptk/UpdateEvent
shapes (map #(get-in state [:workspace-data pid :objects %]) selected) (update [_ state]
shape (gsh/shapes->rect-shape shapes) (if (nil? (get-in state [:workspace-local :current-move-selected]))
displacement (if align? (-> state
(get-displacement-with-grid shape direction options) (assoc-in [:workspace-local :transform] :move)
(get-displacement shape direction))] (assoc-in [:workspace-local :current-move-selected] same-event))
(rx/of (set-modifiers selected displacement) state))
(apply-modifiers selected))))))
ptk/WatchEvent
(watch [_ state stream]
(if (= same-event (get-in state [:workspace-local :current-move-selected]))
(let [selected (get-in state [:workspace-local :selected])
move-events (->> stream
(rx/filter (ptk/type? ::move-selected))
(rx/filter #(= direction (deref %))))
stopper (->> move-events
(rx/debounce 100)
(rx/first))
mov-vec (get-displacement direction)]
(rx/concat
(rx/merge
(->> move-events
(rx/take-until stopper)
(rx/scan #(gpt/add %1 mov-vec) (gpt/point 0 0))
(rx/map #(set-modifiers selected {:displacement (gmt/translate-matrix %)})))
(rx/of (move-selected direction align?)))
(rx/of (apply-modifiers selected)
(fn [state] (-> state
(update :workspace-local dissoc :current-move-selected))))
(->>
(rx/timer 100)
(rx/map (fn [] finish-transform)))))
(rx/empty))))))
;; -- Apply modifiers ;; -- Apply modifiers
@ -301,7 +381,6 @@
[ids] [ids]
(us/verify (s/coll-of uuid?) ids) (us/verify (s/coll-of uuid?) ids)
(ptk/reify ::apply-modifiers (ptk/reify ::apply-modifiers
dwc/IUpdateGroup dwc/IUpdateGroup
(get-ids [_] ids) (get-ids [_] ids)

View file

@ -52,6 +52,9 @@
(def workspace-presence (def workspace-presence
(l/derived :workspace-presence st/state)) (l/derived :workspace-presence st/state))
(def workspace-snap-data
(l/derived :workspace-snap-data st/state))
(def workspace-data (def workspace-data
(-> #(let [page-id (get-in % [:workspace-page :id])] (-> #(let [page-id (get-in % [:workspace-page :id])]
(get-in % [:workspace-data page-id])) (get-in % [:workspace-data page-id]))
@ -90,6 +93,15 @@
(def selected-shapes (def selected-shapes
(l/derived :selected workspace-local)) (l/derived :selected workspace-local))
(def selected-shapes-with-children
(letfn [(selector [state]
(let [selected (get-in state [:workspace-local :selected])
page-id (get-in state [:workspace-page :id])
objects (get-in state [:workspace-data page-id :objects])
children (mapcat #(helpers/get-children % objects) selected)]
(into selected children)))]
(l/derived selector st/state)))
(defn make-selected (defn make-selected
[id] [id]
(l/derived #(contains? % id) selected-shapes)) (l/derived #(contains? % id) selected-shapes))
@ -100,5 +112,11 @@
(def selected-drawing-tool (def selected-drawing-tool
(l/derived :drawing-tool workspace-local)) (l/derived :drawing-tool workspace-local))
(def current-drawing-shape
(l/derived :drawing workspace-local))
(def selected-edition (def selected-edition
(l/derived :edition workspace-local)) (l/derived :edition workspace-local))
(def current-transform
(l/derived :transform workspace-local))

View file

@ -8,11 +8,13 @@
(:require (:require
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.alpha :as mf] [rumext.alpha :as mf]
[uxbox.util.geom.shapes :as geom]
[uxbox.util.debug :as debug] [uxbox.util.debug :as debug]
[uxbox.util.geom.shapes :as geom]
[uxbox.util.geom.snap :as snap]
[uxbox.util.geom.matrix :as gmt] [uxbox.util.geom.matrix :as gmt]
[uxbox.util.geom.point :as gpt] [uxbox.util.geom.point :as gpt]
[uxbox.util.debug :refer [debug?]])) [uxbox.util.debug :refer [debug?]]
["randomcolor" :as rdcolor]))
(defn fix [num] (defn fix [num]
(when num (.toFixed num 2))) (when num (.toFixed num 2)))
@ -26,7 +28,8 @@
selrect (-> shape selrect (-> shape
(geom/selection-rect-shape) (geom/selection-rect-shape)
(geom/translate-to-frame frame)) (geom/translate-to-frame frame))
shape-center (geom/center selrect)] shape-center (geom/center selrect)
line-color (rdcolor #js {:seed (str (:id shape))})]
[:g [:g
[:text {:x (:x selrect) [:text {:x (:x selrect)
:y (- (:y selrect) 5) :y (- (:y selrect) 5)
@ -44,4 +47,28 @@
:fill "transparent" :fill "transparent"
:stroke-width "1px" :stroke-width "1px"
:stroke-opacity 0.5 :stroke-opacity 0.5
:pointer-events "none"}}]]))) :pointer-events "none"}}]
#_(for [point (snap/shape-snap-points shape)]
(let [point (gpt/subtract point (gpt/point (:x frame) (:y frame)))]
[:* {:key (str "point-" (:id shape) "-" (:x point) "-" (:y point))}
[:circle {:cx (:x point)
:cy (:y point)
:r 4
:fill line-color}]
[:line {:x1 (:x point)
:y1 -10000
:x2 (:x point)
:y2 10000
:style {:stroke line-color :stroke-width "1"}
:opacity 0.4}]
[:line {:x1 -10000
:y1 (:y point)
:x2 10000
:y2 (:y point)
:style {:stroke line-color :stroke-width "1"}
:opacity 0.4}]]
))
])))

View file

@ -24,6 +24,7 @@
[uxbox.util.geom.path :as path] [uxbox.util.geom.path :as path]
[uxbox.util.geom.point :as gpt] [uxbox.util.geom.point :as gpt]
[uxbox.util.i18n :as i18n :refer [t]] [uxbox.util.i18n :as i18n :refer [t]]
[uxbox.util.geom.snap :as snap]
[uxbox.common.uuid :as uuid])) [uxbox.common.uuid :as uuid]))
;; --- Events ;; --- Events
@ -115,28 +116,30 @@
(rx/of handle-drawing-generic))))) (rx/of handle-drawing-generic)))))
(def handle-drawing-generic (def handle-drawing-generic
(letfn [(initialize-drawing [state point] (letfn [(initialize-drawing [state point frame-id]
(let [shape (get-in state [:workspace-local :drawing]) (let [shape (get-in state [:workspace-local :drawing])
shape (geom/setup shape {:x (:x point) shape (geom/setup shape {:x (:x point)
:y (:y point) :y (:y point)
:width 10 :width 1
:height 10})] :height 1})]
(assoc-in state [:workspace-local :drawing] (assoc shape ::initialized? true)))) (assoc-in state [:workspace-local :drawing] (-> shape
(assoc :frame-id frame-id)
(assoc ::initialized? true)))))
(resize-shape [{:keys [x y] :as shape} initial point lock?] (resize-shape [{:keys [x y] :as shape} initial snap-data point lock?]
(let [shape' (geom/shape->rect-shape shape) (let [shape' (geom/shape->rect-shape shape)
shapev (gpt/point (:width shape') (:height shape')) shapev (gpt/point (:width shape') (:height shape'))
deltav (gpt/subtract point initial) point-snap (snap/closest-snap-point snap-data [shape] point)
deltav (gpt/to-vec initial point-snap)
scalev (gpt/divide (gpt/add shapev deltav) shapev) scalev (gpt/divide (gpt/add shapev deltav) shapev)
scalev (if lock? (let [v (max (:x scalev) (:y scalev))] (gpt/point v v)) scalev)] scalev (if lock? (let [v (max (:x scalev) (:y scalev))] (gpt/point v v)) scalev)]
(-> shape (-> shape
(assoc-in [:modifiers :resize-vector] scalev) (assoc-in [:modifiers :resize-vector] scalev)
(assoc-in [:modifiers :resize-origin] (gpt/point x y)) (assoc-in [:modifiers :resize-origin] (gpt/point x y))
(assoc-in [:modifiers :resize-rotation] 0)))) (assoc-in [:modifiers :resize-rotation] 0))))
(update-drawing [state initial point lock?] (update-drawing [state initial snap-data point lock?]
(update-in state [:workspace-local :drawing] resize-shape initial point lock?))] (update-in state [:workspace-local :drawing] resize-shape initial snap-data point lock?))]
(ptk/reify ::handle-drawing-generic (ptk/reify ::handle-drawing-generic
ptk/WatchEvent ptk/WatchEvent
@ -145,15 +148,29 @@
stoper? #(or (ms/mouse-up? %) (= % :interrupt)) stoper? #(or (ms/mouse-up? %) (= % :interrupt))
stoper (rx/filter stoper? stream) stoper (rx/filter stoper? stream)
initial @ms/mouse-position initial @ms/mouse-position
snap-data (get state :workspace-snap-data)
mouse (->> ms/mouse-position mouse (->> ms/mouse-position
(rx/map #(gpt/divide % (gpt/point zoom))))] (rx/map #(gpt/divide % (gpt/point zoom))))
page-id (get state :current-page-id)
objects (get-in state [:workspace-data page-id :objects])
frames (->> objects
vals
(filter (comp #{:frame} :type))
(remove #(= (:id %) uuid/zero) ))
frame-id (->> frames
(filter #(geom/has-point? % initial))
first
:id)]
(rx/concat (rx/concat
(->> mouse (->> mouse
(rx/take 1) (rx/take 1)
(rx/map (fn [pt] #(initialize-drawing % pt)))) (rx/map (fn [pt] #(initialize-drawing % pt frame-id))))
(->> mouse (->> mouse
(rx/with-latest vector ms/mouse-position-ctrl) (rx/with-latest vector ms/mouse-position-ctrl)
(rx/map (fn [[pt ctrl?]] #(update-drawing % initial pt ctrl?))) (rx/map (fn [[pt ctrl?]] #(update-drawing % initial snap-data pt ctrl?)))
(rx/take-until stoper)) (rx/take-until stoper))
(rx/of handle-finish-drawing))))))) (rx/of handle-finish-drawing)))))))
@ -280,8 +297,16 @@
(rx/concat (rx/concat
(rx/of dw/clear-drawing) (rx/of dw/clear-drawing)
(when (::initialized? shape) (when (::initialized? shape)
(let [shape (-> shape (let [shape-min-width (case (:type shape)
:text 20
5)
shape-min-height (case (:type shape)
:text 16
5)
shape (-> shape
(geom/transform-shape) (geom/transform-shape)
(update :width #(max shape-min-width %))
(update :height #(max shape-min-height %))
(dissoc shape ::initialized?))] (dissoc shape ::initialized?))]
;; Add & select the created shape to the workspace ;; Add & select the created shape to the workspace
(rx/of dw/deselect-all (rx/of dw/deselect-all

View file

@ -96,6 +96,7 @@
zoom (obj/get props "zoom") zoom (obj/get props "zoom")
on-resize (obj/get props "on-resize") on-resize (obj/get props "on-resize")
on-rotate (obj/get props "on-rotate") on-rotate (obj/get props "on-rotate")
current-transform (mf/deref refs/current-transform)
{:keys [x y width height rotation] :as shape} (geom/shape->rect-shape shape) {:keys [x y width height rotation] :as shape} (geom/shape->rect-shape shape)
@ -111,32 +112,33 @@
:bottom-right [(+ x width) (+ y height)]}] :bottom-right [(+ x width) (+ y height)]}]
[:g.controls [:g.controls
[:rect.main {:transform transform (when (not (#{:move :rotate :resize} current-transform))
[:rect.main {:transform transform
:x (- x 1) :y (- y 1) :x (- x 1) :y (- y 1)
:width (+ width 2) :width (+ width 2)
:height (+ height 2) :height (+ height 2)
:style {:stroke "#1FDEA7" :style {:stroke "#1FDEA7"
:stroke-width "1" :stroke-width "1"
:fill "transparent"}}] :fill "transparent"}}])
(for [[position [cx cy]] resize-handlers] (when (not (#{:move :rotate} current-transform))
(let [tp (gpt/transform (gpt/point cx cy) transform)] (for [[position [cx cy]] resize-handlers]
[:* {:key (name position)} (let [tp (gpt/transform (gpt/point cx cy) transform)]
[:& rotation-handler {:cx (:x tp) [:* {:key (name position)}
:cy (:y tp) [:& rotation-handler {:cx (:x tp)
:position position :cy (:y tp)
:rotation (:rotation shape) :position position
:zoom zoom :rotation (:rotation shape)
:on-mouse-down on-rotate}] :zoom zoom
:on-mouse-down on-rotate}]
[:& control-item {:class (name position) [:& control-item {:class (name position)
:on-click #(on-resize position %) :on-click #(on-resize position %)
:r (/ radius zoom) :r (/ radius zoom)
:cx (:x tp) :cx (:x tp)
:cy (:y tp)}]]))])) :cy (:y tp)}]])))]))
;; --- Selection Handlers (Component) ;; --- Selection Handlers (Component)
(mf/defc path-edition-selection-handlers (mf/defc path-edition-selection-handlers
[{:keys [shape modifiers zoom] :as props}] [{:keys [shape modifiers zoom] :as props}]
(letfn [(on-mouse-down [event index] (letfn [(on-mouse-down [event index]
@ -191,12 +193,11 @@
:fill "transparent"}}]])) :fill "transparent"}}]]))
(mf/defc multiple-selection-handlers (mf/defc multiple-selection-handlers
[{:keys [shapes selected zoom objects] :as props}] [{:keys [shapes selected zoom] :as props}]
(let [shape (geom/selection-rect shapes) (let [shape (geom/selection-rect shapes)
shape-center (geom/center shape) shape-center (geom/center shape)
on-resize #(do (dom/stop-propagation %2) on-resize #(do (dom/stop-propagation %2)
(st/emit! (dw/start-resize %1 selected shape objects))) (st/emit! (dw/start-resize %1 selected shape)))
on-rotate #(do (dom/stop-propagation %) on-rotate #(do (dom/stop-propagation %)
(st/emit! (dw/start-rotate shapes)))] (st/emit! (dw/start-rotate shapes)))]
@ -210,32 +211,31 @@
[:circle {:cx (:x shape-center) :cy (:y shape-center) :r 5 :fill "yellow"}])])) [:circle {:cx (:x shape-center) :cy (:y shape-center) :r 5 :fill "yellow"}])]))
(mf/defc single-selection-handlers (mf/defc single-selection-handlers
[{:keys [shape zoom objects] :as props}] [{:keys [shape zoom] :as props}]
(let [shape-id (:id shape) (let [shape-id (:id shape)
shape (geom/transform-shape shape) shape (geom/transform-shape shape)
shape' (if (debug? :simple-selection) (geom/selection-rect [shape]) shape) shape' (if (debug? :simple-selection) (geom/selection-rect [shape]) shape)
on-resize on-resize
#(do (dom/stop-propagation %2) #(do (dom/stop-propagation %2)
(st/emit! (dw/start-resize %1 #{shape-id} shape' objects))) (st/emit! (dw/start-resize %1 #{shape-id} shape')))
on-rotate on-rotate
#(do (dom/stop-propagation %) #(do (dom/stop-propagation %)
(st/emit! (dw/start-rotate [shape])))] (st/emit! (dw/start-rotate [shape])))]
[:& controls {:shape shape'
:zoom zoom [:*
:on-rotate on-rotate [:& controls {:shape shape'
:on-resize on-resize}])) :zoom zoom
:on-rotate on-rotate
:on-resize on-resize}]]))
(mf/defc selection-handlers (mf/defc selection-handlers
[{:keys [selected edition zoom] :as props}] [{:keys [selected edition zoom] :as props}]
(let [data (mf/deref refs/workspace-data) (let [;; We need remove posible nil values because on shape
objects (:objects data)
;; We need remove posible nil values because on shape
;; deletion many shape will reamin selected and deleted ;; deletion many shape will reamin selected and deleted
;; in the same time for small instant of time ;; in the same time for small instant of time
shapes (->> (map #(get objects %) selected) shapes (->> (mf/deref (refs/objects-by-id selected))
(remove nil?)) (remove nil?))
num (count shapes) num (count shapes)
{:keys [id type] :as shape} (first shapes)] {:keys [id type] :as shape} (first shapes)]
@ -246,7 +246,6 @@
(> num 1) (> num 1)
[:& multiple-selection-handlers {:shapes shapes [:& multiple-selection-handlers {:shapes shapes
:selected selected :selected selected
:objects objects
:zoom zoom}] :zoom zoom}]
(and (= type :text) (and (= type :text)
@ -261,5 +260,4 @@
:else :else
[:& single-selection-handlers {:shape shape [:& single-selection-handlers {:shape shape
:zoom zoom :zoom zoom}])))
:objects objects}])))

View file

@ -0,0 +1,62 @@
(ns uxbox.main.ui.workspace.snap-feedback
(:require
[rumext.alpha :as mf]
[uxbox.main.refs :as refs]
[uxbox.util.geom.snap :as snap]
[uxbox.util.geom.point :as gpt]))
(def ^:private line-color "#D383DA")
(mf/defc snap-point [{:keys [point]}]
(let [{:keys [x y]} point
cross-width 3]
[:g
[:line {:x1 (- x cross-width)
:y1 (- y cross-width)
:x2 (+ x cross-width)
:y2 (+ y cross-width)
:style {:stroke line-color :stroke-width "1"}}]
[:line {:x1 (- x cross-width)
:y1 (+ y cross-width)
:x2 (+ x cross-width)
:y2 (- y cross-width)
:style {:stroke line-color :stroke-width "1"}}]]))
(mf/defc snap-line [{:keys [snap point]}]
[:line {:x1 (:x snap)
:y1 (:y snap)
:x2 (:x point)
:y2 (:y point)
:style {:stroke line-color :stroke-width "1"}
:opacity 0.4}])
(mf/defc snap-feedback []
(let [selected (mf/deref refs/selected-shapes)
selected-shapes (mf/deref (refs/objects-by-id selected))
drawing (mf/deref refs/current-drawing-shape)
filter-shapes (mf/deref refs/selected-shapes-with-children)
current-transform (mf/deref refs/current-transform)
snap-data (mf/deref refs/workspace-snap-data)
shapes (if drawing [drawing] selected-shapes)]
(when (or drawing current-transform)
(for [shape shapes]
(for [point (snap/shape-snap-points shape)]
(let [frame-id (:frame-id shape)
shape-id (:id shape)
snaps (into #{}
(concat
(snap/get-snap-points snap-data frame-id filter-shapes point :x)
(snap/get-snap-points snap-data frame-id filter-shapes point :y)))]
(if (not-empty snaps)
[:* {:key (str "point-" (:id shape) "-" (:x point) "-" (:y point))}
[:& snap-point {:point point}]
(for [snap snaps]
[:& snap-point {:key (str "snap-" (:id shape) "-" (:x point) "-" (:y point) "-" (:x snap) "-" (:y snap))
:point snap}])
(for [snap snaps]
[:& snap-line {:key (str "line-" (:id shape) "-" (:x point) "-" (:y point) "-" (:x snap) "-" (:y snap))
:snap snap
:point point}])])))))))

View file

@ -29,6 +29,7 @@
[uxbox.main.ui.workspace.ruler :refer [ruler]] [uxbox.main.ui.workspace.ruler :refer [ruler]]
[uxbox.main.ui.workspace.selection :refer [selection-handlers]] [uxbox.main.ui.workspace.selection :refer [selection-handlers]]
[uxbox.main.ui.workspace.presence :as presence] [uxbox.main.ui.workspace.presence :as presence]
[uxbox.main.ui.workspace.snap-feedback :refer [snap-feedback]]
[uxbox.util.dom :as dom] [uxbox.util.dom :as dom]
[uxbox.util.geom.point :as gpt] [uxbox.util.geom.point :as gpt]
[uxbox.util.perf :as perf] [uxbox.util.perf :as perf]
@ -305,11 +306,14 @@
:zoom zoom :zoom zoom
:edition edition}]) :edition edition}])
(when-let [drawing-shape (:drawing local)] (when-let [drawing-shape (:drawing local)]
[:& draw-area {:shape drawing-shape [:& draw-area {:shape drawing-shape
:zoom zoom :zoom zoom
:modifiers (:modifiers local)}]) :modifiers (:modifiers local)}])
[:& snap-feedback]
(if (contains? flags :grid) (if (contains? flags :grid)
[:& grid])] [:& grid])]

View file

@ -8,8 +8,9 @@
;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.util.geom.point (ns uxbox.util.geom.point
(:refer-clojure :exclude [divide]) (:refer-clojure :exclude [divide min max])
(:require (:require
[cljs.core :as c]
[cuerdas.core :as str] [cuerdas.core :as str]
[uxbox.util.math :as mth] [uxbox.util.math :as mth]
[cognitect.transit :as t])) [cognitect.transit :as t]))
@ -70,6 +71,15 @@
(assert (point? other)) (assert (point? other))
(Point. (/ x ox) (/ y oy))) (Point. (/ x ox) (/ y oy)))
(defn min
[{x1 :x y1 :y :as p1} {x2 :x y2 :y :as p2}]
(Point. (c/min x1 x2) (c/min y1 y2)))
(defn max
[{x1 :x y1 :y :as p1} {x2 :x y2 :y :as p2}]
(Point. (c/max x1 x2) (c/max y1 y2)))
(defn inverse (defn inverse
[{:keys [x y] :as p}] [{:keys [x y] :as p}]
(assert (point? p)) (assert (point? p))

View file

@ -0,0 +1,25 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.util.geom.range-tree
(:require
[cljs.spec.alpha :as s]))
(defn make-tree [objects])
(defn add-shape [shape])
(defn remove-shape [shape])
(defn update-shape [old-shape new-shape])
(defn query [point match-dist]) ;; Return {:x => [(point, distance, shape-id)]}
;;

View file

@ -687,7 +687,7 @@
transform transform
(gmt/translate-matrix (gpt/negate shape-center))))))) (gmt/translate-matrix (gpt/negate shape-center)))))))
(defn- transform-apply-modifiers (defn transform-apply-modifiers
[shape] [shape]
(let [modifiers (:modifiers shape) (let [modifiers (:modifiers shape)
ds-modifier (:displacement modifiers (gmt/matrix)) ds-modifier (:displacement modifiers (gmt/matrix))
@ -738,6 +738,15 @@
(gpt/divide (gpt/point (:width shape-path-temp-rec) (:height shape-path-temp-rec)) (gpt/divide (gpt/point (:width shape-path-temp-rec) (:height shape-path-temp-rec))
(gpt/point (:width shape-path-temp-dim) (:height shape-path-temp-dim))))) (gpt/point (:width shape-path-temp-dim) (:height shape-path-temp-dim)))))
(defn- fix-invalid-rect-values [rect-shape]
(letfn [(check [num] (if (or (nil? num) (mth/nan? num)) 0 num))
(to-positive [num] (if (< num 1) 1 num))]
(-> rect-shape
(update :x check)
(update :y check)
(update :width (comp to-positive check))
(update :height (comp to-positive check)))))
(defn transform-rect-shape (defn transform-rect-shape
[shape] [shape]
(let [;; Apply modifiers to the rect as a path so we have the end shape expected (let [;; Apply modifiers to the rect as a path so we have the end shape expected
@ -785,6 +794,7 @@
(merge rec) (merge rec)
(update :x #(mth/precision % 2)) (update :x #(mth/precision % 2))
(update :y #(mth/precision % 2)) (update :y #(mth/precision % 2))
(fix-invalid-rect-values)
(update :transform #(gmt/multiply (or % (gmt/matrix)) stretch-matrix)) (update :transform #(gmt/multiply (or % (gmt/matrix)) stretch-matrix))
(update :transform-inverse #(gmt/multiply stretch-matrix-inverse (or % (gmt/matrix)))))] (update :transform-inverse #(gmt/multiply stretch-matrix-inverse (or % (gmt/matrix)))))]

View file

@ -0,0 +1,200 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.util.geom.snap
(:require
[cljs.spec.alpha :as s]
[clojure.set :as set]
[uxbox.util.math :as mth]
[uxbox.common.uuid :refer [zero]]
[uxbox.util.geom.shapes :as gsh]
[uxbox.util.geom.point :as gpt]))
(def ^:private snap-accuracy 20)
(defn mapm
"Map over the values of a map"
[mfn coll]
(into {} (map (fn [[key val]] [key (mfn val)]) coll)))
(defn- frame-snap-points [{:keys [x y width height]}]
#{(gpt/point x y)
(gpt/point (+ x (/ width 2)) y)
(gpt/point (+ x width) y)
(gpt/point (+ x width) (+ y (/ height 2)))
(gpt/point (+ x width) (+ y height))
(gpt/point (+ x (/ width 2)) (+ y height))
(gpt/point x (+ y height))
(gpt/point x (+ y (/ height 2)))})
(defn- frame-snap-points-resize [{:keys [x y width height]} handler]
(case handler
:top-left (gpt/point x y)
:top (gpt/point (+ x (/ width 2)) y)
:top-right (gpt/point (+ x width) y)
:right (gpt/point (+ x width) (+ y (/ height 2)))
:bottom-right (gpt/point (+ x width) (+ y height))
:bottom (gpt/point (+ x (/ width 2)) (+ y height))
:bottom-left (gpt/point x (+ y height))
:left (gpt/point x (+ y (/ height 2)))))
(def ^:private handler->point-idx
{:top-left 0
:top 0
:top-right 1
:right 1
:bottom-right 2
:bottom 2
:bottom-left 3
:left 3})
(defn shape-snap-points-resize
[handler shape]
(let [modified-path (gsh/transform-apply-modifiers shape)
point-idx (handler->point-idx handler)]
#{(case (:type shape)
:frame (frame-snap-points-resize shape handler)
(:path :curve) (-> modified-path gsh/shape->rect-shape :segments (nth point-idx))
(-> modified-path :segments (nth point-idx)))}))
(defn shape-snap-points
[shape]
(let [modified-path (gsh/transform-apply-modifiers shape)
shape-center (gsh/center modified-path)]
(case (:type shape)
:frame (frame-snap-points shape)
(:path :curve) (into #{shape-center} (-> modified-path gsh/shape->rect-shape :segments))
(into #{shape-center} (-> modified-path :segments)))))
(defn create-coord-data [shapes coord]
(let [process-shape
(fn [coord]
(fn [shape]
(let [points (shape-snap-points shape)]
(map #(vector % (:id shape)) points))))]
(->> shapes
(mapcat (process-shape coord))
(group-by (comp coord first)))))
(defn initialize-snap-data
"Initialize the snap information with the current workspace information"
[objects]
(let [shapes (vals objects)
frame-shapes (->> shapes
(filter (comp not nil? :frame-id))
(group-by :frame-id))
frame-shapes (->> shapes
(filter #(= :frame (:type %)))
(remove #(= zero (:id %)))
(reduce #(update %1 (:id %2) conj %2) frame-shapes))]
(mapm (fn [shapes] {:x (create-coord-data shapes :x)
:y (create-coord-data shapes :y)})
frame-shapes)))
(defn range-query
"Queries the snap-data within a range of values"
[snap-data from-value to-value]
(filter (fn [[value _]] (and (>= value from-value)
(<= value to-value)))
snap-data))
(defn remove-from-snap-points [snap-points ids-to-remove]
(->> snap-points
(map (fn [[value data]] [value (remove (comp ids-to-remove second) data)]))
(filter (fn [[_ data]] (not (empty? data))))))
(defn search-snap-point
"Search snap for a single point"
[point coord snap-data filter-shapes]
(let [coord-value (get point coord)
;; This gives a list of [value [[point1 uuid1] [point2 uuid2] ...] we need to remove
;; the shapes in filter shapes
candidates (-> snap-data
(range-query (- coord-value snap-accuracy) (+ coord-value snap-accuracy))
(remove-from-snap-points filter-shapes))
;; Now return with the distance and the from-to pair that we'll return if this is the chosen
point-snaps (map (fn [[cand-value data]] [(mth/abs (- coord-value cand-value)) [coord-value cand-value]]) candidates)]
point-snaps))
(defn search-snap
"Search a snap point in one axis `snap-data` contains the information to make the snap.
`points` are the points that we need to search for a snap and `filter-shapes` is a set of uuids
containgin the shapes that should be ignored to get a snap (usually because they are being moved)"
[points coord snap-data filter-shapes]
(let [snap-points (mapcat #(search-snap-point % coord snap-data filter-shapes) points)
result (->> snap-points (apply min-key first) second)]
(or result [0 0])))
(defn snap-frame-id [shapes]
(let [frames (into #{} (map :frame-id shapes))]
(cond
;; Only shapes from one frame. The common is the only one
(= 0 (count frames)) (first frames)
;; Frames doesn't contain zero. So we take the first frame
(not (frames zero)) (-> shapes first :frame-id)
;; Otherwise the root frame is the common
:else zero)))
(defn- closest-snap
[snap-data shapes trans-vec shapes-points]
(let [;; Get the common frame-id to make the snap
frame-id (snap-frame-id shapes)
;; We don't want to snap to the shapes currently transformed
remove-shapes (into #{} (map :id shapes))
;; The snap is a tuple. The from is the point in the current moving shape
;; the "to" is the point where we'll snap. So we need to create a vector
;; snap-from --> snap-to and move the position in that vector
[snap-from-x snap-to-x] (search-snap shapes-points :x (get-in snap-data [frame-id :x]) remove-shapes)
[snap-from-y snap-to-y] (search-snap shapes-points :y (get-in snap-data [frame-id :y]) remove-shapes)
snapv (gpt/to-vec (gpt/point snap-from-x snap-from-y)
(gpt/point snap-to-x snap-to-y))]
(gpt/add trans-vec snapv)))
(defn closest-snap-point
[snap-data shapes point]
(closest-snap snap-data shapes point [point]))
(defn closest-snap-move
([snap-data shapes] (partial closest-snap-move snap-data shapes))
([snap-data shapes trans-vec]
(let [shapes-points (->> shapes
;; Unroll all the possible snap-points
(mapcat (partial shape-snap-points))
;; Move the points in the translation vector
(map #(gpt/add % trans-vec)))]
(closest-snap snap-data shapes trans-vec shapes-points))))
(defn get-snap-points [snap-data frame-id filter-shapes point coord]
(let [value (coord point)
;; Search for values within 1 pixel
snap-matches (-> (get-in snap-data [frame-id coord])
(range-query (- value 1) (+ value 1))
(remove-from-snap-points filter-shapes))
snap-points (mapcat (fn [[v data]] (map (fn [[point _]] point) data)) snap-matches)]
snap-points))
(defn is-snapping? [snap-data frame-id shape-id point coord]
(let [value (coord point)
;; Search for values within 1 pixel
snap-points (range-query (get-in snap-data [frame-id coord]) (- value 1.0) (+ value 1.0))]
(some (fn [[point other-shape-id]] (not (= shape-id other-shape-id))) snap-points)))