mirror of
https://github.com/penpot/penpot.git
synced 2025-02-13 18:48:37 -05:00
🎉 Dynamic alignment brute-force method
This commit is contained in:
parent
37d0f20b7e
commit
ffd0c95760
11 changed files with 297 additions and 30 deletions
|
@ -33,6 +33,7 @@
|
|||
[uxbox.util.geom.matrix :as gmt]
|
||||
[uxbox.util.geom.point :as gpt]
|
||||
[uxbox.util.geom.shapes :as geom]
|
||||
[uxbox.util.geom.snap :as snap]
|
||||
[uxbox.util.math :as mth]
|
||||
[uxbox.util.router :as rt]
|
||||
[uxbox.util.transit :as t]
|
||||
|
@ -146,7 +147,8 @@
|
|||
(-> state
|
||||
(assoc :current-page-id page-id ; mainly used by events
|
||||
: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)))))
|
||||
|
||||
ptk/WatchEvent
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
[uxbox.common.spec :as us]
|
||||
[uxbox.common.uuid :as uuid]
|
||||
[uxbox.main.worker :as uw]
|
||||
[uxbox.util.geom.shapes :as geom]))
|
||||
[uxbox.util.geom.shapes :as geom]
|
||||
[uxbox.util.geom.snap :as snap]))
|
||||
|
||||
;; --- Protocols
|
||||
|
||||
|
@ -19,6 +20,7 @@
|
|||
|
||||
(declare setup-selection-index)
|
||||
(declare update-selection-index)
|
||||
(declare update-snap-data)
|
||||
(declare reset-undo)
|
||||
(declare append-undo)
|
||||
|
||||
|
@ -51,7 +53,8 @@
|
|||
(let [page (:workspace-page state)
|
||||
uidx (get-in state [:workspace-local :undo-index] ::not-found)]
|
||||
(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))
|
||||
(rx/of (reset-undo uidx)))
|
||||
|
@ -138,6 +141,16 @@
|
|||
:page-id page-id
|
||||
: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])]
|
||||
(println "Update snap data")
|
||||
(-> state
|
||||
(assoc :workspace-snap-data (snap/initialize-snap-data objects)))))))
|
||||
|
||||
;; --- Common Helpers & Events
|
||||
|
||||
|
|
|
@ -3,16 +3,19 @@
|
|||
(:require
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]
|
||||
[uxbox.common.data :as d]
|
||||
[uxbox.common.spec :as us]
|
||||
[uxbox.main.data.helpers :as helpers]
|
||||
[uxbox.main.data.workspace.common :as dwc]
|
||||
[uxbox.main.refs :as refs]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.streams :as ms]
|
||||
[uxbox.util.geom.matrix :as gmt]
|
||||
[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
|
||||
|
||||
|
@ -64,10 +67,9 @@
|
|||
|
||||
;; -- RESIZE
|
||||
(defn start-resize
|
||||
[handler ids shape objects]
|
||||
[handler ids shape]
|
||||
(letfn [(resize [shape initial [point lock?]]
|
||||
(let [frame (get objects (:frame-id shape))
|
||||
{:keys [width height rotation]} shape
|
||||
(let [{:keys [width height rotation]} shape
|
||||
|
||||
shapev (-> (gpt/point width height))
|
||||
|
||||
|
@ -171,6 +173,9 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [selected (get-in state [:workspace-local :selected])
|
||||
page-id (get state :current-page-id)
|
||||
shapes (mapv #(get-in state [:workspace-data page-id :objects %]) selected)
|
||||
snap-data (get state :workspace-snap-data)
|
||||
stoper (rx/filter ms/mouse-up? stream)
|
||||
zero-point? #(= % (gpt/point 0 0))
|
||||
initial (apply-zoom @ms/mouse-position)
|
||||
|
@ -180,7 +185,8 @@
|
|||
(->> ms/mouse-position
|
||||
(rx/map apply-zoom)
|
||||
(rx/filter (complement zero-point?))
|
||||
(rx/map #(gpt/subtract % initial))
|
||||
(rx/map #(gpt/to-vec initial %))
|
||||
(rx/map (snap/closest-snap snap-data shapes))
|
||||
(rx/map gmt/translate-matrix)
|
||||
(rx/filter #(not (gmt/base? %)))
|
||||
(rx/map #(set-modifiers selected {:displacement %}))
|
||||
|
@ -301,7 +307,6 @@
|
|||
[ids]
|
||||
(us/verify (s/coll-of uuid?) ids)
|
||||
(ptk/reify ::apply-modifiers
|
||||
|
||||
dwc/IUpdateGroup
|
||||
(get-ids [_] ids)
|
||||
|
||||
|
|
|
@ -52,6 +52,9 @@
|
|||
(def workspace-presence
|
||||
(l/derived :workspace-presence st/state))
|
||||
|
||||
(def workspace-snap-data
|
||||
(l/derived :workspace-snap-data st/state))
|
||||
|
||||
(def workspace-data
|
||||
(-> #(let [page-id (get-in % [:workspace-page :id])]
|
||||
(get-in % [:workspace-data page-id]))
|
||||
|
|
|
@ -8,11 +8,13 @@
|
|||
(:require
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.util.geom.shapes :as geom]
|
||||
[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.point :as gpt]
|
||||
[uxbox.util.debug :refer [debug?]]))
|
||||
[uxbox.util.debug :refer [debug?]]
|
||||
["randomcolor" :as rdcolor]))
|
||||
|
||||
(defn fix [num]
|
||||
(when num (.toFixed num 2)))
|
||||
|
@ -26,7 +28,8 @@
|
|||
selrect (-> shape
|
||||
(geom/selection-rect-shape)
|
||||
(geom/translate-to-frame frame))
|
||||
shape-center (geom/center selrect)]
|
||||
shape-center (geom/center selrect)
|
||||
line-color (rdcolor #js {:seed (str (:id shape))})]
|
||||
[:g
|
||||
[:text {:x (:x selrect)
|
||||
:y (- (:y selrect) 5)
|
||||
|
@ -44,4 +47,28 @@
|
|||
:fill "transparent"
|
||||
:stroke-width "1px"
|
||||
: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}]]
|
||||
))
|
||||
])))
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
[uxbox.util.object :as obj]
|
||||
[uxbox.util.geom.point :as gpt]
|
||||
[uxbox.util.geom.matrix :as gmt]
|
||||
[uxbox.main.ui.workspace.snap-feedback :refer [snap-feedback]]
|
||||
[uxbox.util.debug :refer [debug?]]))
|
||||
|
||||
;; --- Controls (Component)
|
||||
|
@ -191,12 +192,12 @@
|
|||
:fill "transparent"}}]]))
|
||||
|
||||
(mf/defc multiple-selection-handlers
|
||||
[{:keys [shapes selected zoom objects] :as props}]
|
||||
[{:keys [shapes selected zoom] :as props}]
|
||||
(let [shape (geom/selection-rect shapes)
|
||||
shape-center (geom/center shape)
|
||||
|
||||
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 %)
|
||||
(st/emit! (dw/start-rotate shapes)))]
|
||||
|
@ -206,36 +207,37 @@
|
|||
:zoom zoom
|
||||
:on-resize on-resize
|
||||
:on-rotate on-rotate}]
|
||||
[:& snap-feedback {:shapes shapes}]
|
||||
(when (debug? :selection-center)
|
||||
[:circle {:cx (:x shape-center) :cy (:y shape-center) :r 5 :fill "yellow"}])]))
|
||||
|
||||
(mf/defc single-selection-handlers
|
||||
[{:keys [shape zoom objects] :as props}]
|
||||
[{:keys [shape zoom] :as props}]
|
||||
(let [shape-id (:id shape)
|
||||
shape (geom/transform-shape shape)
|
||||
shape' (if (debug? :simple-selection) (geom/selection-rect [shape]) shape)
|
||||
|
||||
on-resize
|
||||
#(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
|
||||
#(do (dom/stop-propagation %)
|
||||
(st/emit! (dw/start-rotate [shape])))]
|
||||
[:& controls {:shape shape'
|
||||
:zoom zoom
|
||||
:on-rotate on-rotate
|
||||
:on-resize on-resize}]))
|
||||
|
||||
[:*
|
||||
[:& controls {:shape shape'
|
||||
:zoom zoom
|
||||
:on-rotate on-rotate
|
||||
:on-resize on-resize}]
|
||||
[:& snap-feedback {:shapes [shape]}]]))
|
||||
|
||||
(mf/defc selection-handlers
|
||||
[{:keys [selected edition zoom] :as props}]
|
||||
(let [data (mf/deref refs/workspace-data)
|
||||
objects (:objects data)
|
||||
|
||||
;; We need remove posible nil values because on shape
|
||||
(let [;; We need remove posible nil values because on shape
|
||||
;; deletion many shape will reamin selected and deleted
|
||||
;; in the same time for small instant of time
|
||||
shapes (->> (map #(get objects %) selected)
|
||||
shapes (->> (mf/deref (refs/objects-by-id selected))
|
||||
(remove nil?))
|
||||
num (count shapes)
|
||||
{:keys [id type] :as shape} (first shapes)]
|
||||
|
@ -246,7 +248,6 @@
|
|||
(> num 1)
|
||||
[:& multiple-selection-handlers {:shapes shapes
|
||||
:selected selected
|
||||
:objects objects
|
||||
:zoom zoom}]
|
||||
|
||||
(and (= type :text)
|
||||
|
@ -261,5 +262,4 @@
|
|||
|
||||
:else
|
||||
[:& single-selection-handlers {:shape shape
|
||||
:zoom zoom
|
||||
:objects objects}])))
|
||||
:zoom zoom}])))
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
alotor@bloodraven.68367:1587963441
|
48
frontend/src/uxbox/main/ui/workspace/snap_feedback.cljs
Normal file
48
frontend/src/uxbox/main/ui/workspace/snap_feedback.cljs
Normal file
|
@ -0,0 +1,48 @@
|
|||
(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-feedback
|
||||
[{:keys [shapes] :as props}]
|
||||
(let [snap-data (mf/deref refs/workspace-snap-data)]
|
||||
(for [shape shapes]
|
||||
(for [point (snap/shape-snap-points shape)]
|
||||
(let [frame-id (:frame-id shape)
|
||||
shape-id (:id shape)
|
||||
|
||||
snaps-x (snap/get-snap-points snap-data frame-id shape-id point :x)
|
||||
snaps-y (snap/get-snap-points snap-data frame-id shape-id point :y)]
|
||||
(if (or (not-empty snaps-x) (not-empty snaps-y))
|
||||
[:* {:key (str "point-" (:id shape) "-" (:x point) "-" (:y point))}
|
||||
[:circle {:cx (:x point)
|
||||
:cy (:y point)
|
||||
:r 2
|
||||
:fill line-color}]
|
||||
|
||||
(for [snap (concat snaps-x snaps-y)]
|
||||
[:*
|
||||
[:circle {:cx (:x snap)
|
||||
:cy (:y snap)
|
||||
:r 2
|
||||
:fill line-color}]
|
||||
[:line {:x1 (:x snap)
|
||||
:y1 (:y snap)
|
||||
:x2 (:x point)
|
||||
:y2 (:y point)
|
||||
:style {:stroke line-color :stroke-width "1"}
|
||||
:opacity 0.4}]])
|
||||
|
||||
#_(when is-snap-y?
|
||||
[:line {:x1 -10000
|
||||
:y1 (:y point)
|
||||
:x2 10000
|
||||
:y2 (:y point)
|
||||
:style {:stroke line-color :stroke-width "1"}
|
||||
:opacity 0.4}])]))))))
|
||||
|
25
frontend/src/uxbox/util/geom/range_tree.cljs
Normal file
25
frontend/src/uxbox/util/geom/range_tree.cljs
Normal 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)]}
|
||||
|
||||
|
||||
|
||||
;;
|
|
@ -687,7 +687,7 @@
|
|||
transform
|
||||
(gmt/translate-matrix (gpt/negate shape-center)))))))
|
||||
|
||||
(defn- transform-apply-modifiers
|
||||
(defn transform-apply-modifiers
|
||||
[shape]
|
||||
(let [modifiers (:modifiers shape)
|
||||
ds-modifier (:displacement modifiers (gmt/matrix))
|
||||
|
|
143
frontend/src/uxbox/util/geom/snap.cljs
Normal file
143
frontend/src/uxbox/util/geom/snap.cljs
Normal file
|
@ -0,0 +1,143 @@
|
|||
;; 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]
|
||||
[uxbox.util.math :as mth]
|
||||
[uxbox.common.uuid :refer [zero]]
|
||||
[uxbox.util.geom.shapes :as gsh]
|
||||
[uxbox.util.geom.point :as gpt]
|
||||
[uxbox.util.debug :refer [logjs]]))
|
||||
|
||||
(def ^:private snap-accuracy 5)
|
||||
|
||||
(defn mapm
|
||||
"Map over the values of a map"
|
||||
[mfn coll]
|
||||
(into {} (map (fn [[key val]] [key (mfn val)]) coll)))
|
||||
|
||||
(defn shape-snap-points [shape]
|
||||
(let [modified-path (gsh/transform-apply-modifiers shape)
|
||||
shape-center (gsh/center modified-path)]
|
||||
(into #{shape-center} (:segments modified-path))))
|
||||
|
||||
(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 (group-by :frame-id (filter (comp not nil? :frame-id) shapes))]
|
||||
(logjs "snap-data"
|
||||
(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] (partial closest-snap snap-data shapes))
|
||||
([snap-data shapes trans-vec]
|
||||
(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 moving
|
||||
remove-shapes (into #{} (map :id shapes))
|
||||
|
||||
shapes-points (->> shapes
|
||||
;; Unroll all the possible snap-points
|
||||
(mapcat shape-snap-points)
|
||||
|
||||
;; Move the points in the translation vector
|
||||
(map #(gpt/add % trans-vec)))
|
||||
|
||||
;; 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 get-snap-points [snap-data frame-id shape-id point coord]
|
||||
(let [value (coord point)
|
||||
|
||||
;; Search for values within 1 pixel
|
||||
snap-matches (-> (get-in snap-data [frame-id coord])
|
||||
(range-query (- value 0.5) (+ value 0.5))
|
||||
(remove-from-snap-points #{shape-id}))
|
||||
|
||||
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 0.25) (+ value 0.25))]
|
||||
(some (fn [[point other-shape-id]] (not (= shape-id other-shape-id))) snap-points)))
|
Loading…
Add table
Reference in a new issue