0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-13 16:21:57 -05:00

Improved snap to grids

This commit is contained in:
alonso.torres 2022-03-15 15:33:01 +01:00
parent b5e965cf1a
commit aec68c52ab
15 changed files with 220 additions and 119 deletions

View file

@ -9,7 +9,7 @@
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.math :as mth :refer [close?]]
[app.common.pages :refer [make-minimal-shape]]
[clojure.test :as t]))

View file

@ -7,6 +7,7 @@
(ns app.common.geom-test
(:require
[clojure.test :as t]
[app.common.math :as mth]
[app.common.geom.point :as gpt]
[app.common.geom.matrix :as gmt]))
@ -67,7 +68,7 @@
p2 (gpt/point 10 10)
angle (gpt/angle-with-other p1 p2)]
(t/is (number? angle))
(t/is (= angle 45.0)))))
(t/is (mth/close? angle 45.0)))))
(t/deftest matrix-constructors-test
(let [m (gmt/matrix)]

View file

@ -678,7 +678,9 @@
(rx/of (finish-transform))
(rx/concat
(->> position
;; We ask for the snap position but we continue even if the result is not available
(rx/with-latest vector snap-delta)
;; We try to use the previous snap so we don't have to wait for the result of the new
(rx/map snap/correct-snap-point)
(rx/map #(hash-map :displacement (gmt/translate-matrix %)))
(rx/map (partial set-modifiers ids))

View file

@ -55,10 +55,6 @@
(and (d/not-empty? focus)
(not (cp/is-in-focus? objects focus id)))))))
(defn- flatten-to-points
[query-result]
(mapcat (fn [[_ data]] (map :pt data)) query-result))
(defn- calculate-distance [query-result point coord]
(->> query-result
(map (fn [[value _]] [(mth/abs (- value (coord point))) [(coord point) value]]))))
@ -90,8 +86,7 @@
:axis coord
:ranges [[(- value (/ 0.5 zoom)) (+ value (/ 0.5 zoom))]]})
(rx/take 1)
(rx/map (remove-from-snap-points remove-snap?))
(rx/map flatten-to-points))))
(rx/map (remove-from-snap-points remove-snap?)))))
(defn- search-snap
[page-id frame-id points coord remove-snap? zoom]
@ -369,7 +364,12 @@
0)
dy (if (not= 0 (:y snap-delta))
(- (+ (:y snap-pos) (:y snap-delta)) (:y position))
0)]
0)
;; If the deltas (dx,dy) are bigger than the snap-accuracy means the stored snap
;; is not valid, so we change to 0
dx (if (> (mth/abs dx) snap-accuracy) 0 dx)
dy (if (> (mth/abs dy) snap-accuracy) 0 dy)]
(-> position
(update :x + dx)
(update :y + dy)))

View file

@ -31,6 +31,7 @@
on-blur (obj/get props "onBlur")
title (obj/get props "title")
default-val (obj/get props "default")
nillable (obj/get props "nillable")
;; We need a ref pointing to the input dom element, but the user
;; of this component may provide one (that is forwarded here).
@ -71,21 +72,27 @@
parse-value
(mf/use-callback
(mf/deps ref min-val max-val value)
(mf/deps ref min-val max-val value nillable default-val)
(fn []
(let [input-node (mf/ref-val ref)
new-value (-> (dom/get-value input-node)
(sm/expr-eval value))]
(when (d/num? new-value)
(cond
(d/num? new-value)
(-> new-value
(cljs.core/max us/min-safe-int)
(cljs.core/min us/max-safe-int)
(cond->
(d/num? min-val)
(d/num? min-val)
(cljs.core/max min-val)
(d/num? max-val)
(cljs.core/min max-val)))))))
(cljs.core/min max-val)))
nillable
default-val
:else value))))
update-input
(mf/use-callback
@ -109,8 +116,14 @@
(fn [event up? down?]
(let [current-value (parse-value)]
(when current-value
(let [increment (if (kbd/shift? event)
(let [increment (cond
(kbd/shift? event)
(if up? (* step-val 10) (* step-val -10))
(kbd/alt? event)
(if up? (* step-val 0.1) (* step-val -0.1))
:else
(if up? step-val (- step-val)))
new-value (+ current-value increment)
@ -154,14 +167,19 @@
(mf/use-callback
(mf/deps set-delta)
(fn [event]
(set-delta event (< (.-deltaY event) 0) (> (.-deltaY event) 0))))
(let [input-node (mf/ref-val ref)]
(when (dom/active? input-node)
(let [event (.getBrowserEvent ^js event)]
(dom/prevent-default event)
(dom/stop-propagation event)
(set-delta event (< (.-deltaY event) 0) (> (.-deltaY event) 0)))))))
handle-blur
(mf/use-callback
(mf/deps parse-value apply-value update-input on-blur)
(fn [_]
(let [new-value (or (parse-value) default-val)]
(if new-value
(if (or nillable new-value)
(apply-value new-value)
(update-input new-value)))
(when on-blur (on-blur))))
@ -176,13 +194,12 @@
(dom/blur! current)))))))
props (-> props
(obj/without ["value" "onChange"])
(obj/without ["value" "onChange" "nillable"])
(obj/set! "className" "input-text")
(obj/set! "type" "text")
(obj/set! "ref" ref)
(obj/set! "defaultValue" (fmt/format-number value))
(obj/set! "title" title)
(obj/set! "onWheel" handle-mouse-wheel)
(obj/set! "onKeyDown" handle-key-down)
(obj/set! "onBlur" handle-blur))]
@ -203,6 +220,13 @@
(let [handle-blur (:fn (mf/ref-val handle-blur-ref))]
(handle-blur)))))
(mf/use-layout-effect
(mf/deps handle-mouse-wheel)
(fn []
(let [keys [(events/listen (mf/ref-val ref) EventType.WHEEL handle-mouse-wheel #js {:pasive false})]]
#(doseq [key keys]
(events/unlistenByKey key)))))
(mf/use-layout-effect
(fn []
(let [keys [(events/listen globals/window EventType.POINTERDOWN on-click)
@ -212,4 +236,3 @@
(events/unlistenByKey key)))))
[:> :input props]))

View file

@ -205,7 +205,7 @@
[:> numeric-input
{:placeholder "Auto"
:value (or (:item-length params) "")
:default nil
:nillable true
:on-change handle-change-item-length}]])
(when (#{:row :column} type)
@ -214,11 +214,15 @@
:class "pixels"
:value (:gutter params)
:min 0
:nillable true
:default 0
:placeholder "0"
:on-change (handle-change :params :gutter)}]
[:& input-row {:label (tr "workspace.options.grid.params.margin")
:class "pixels"
:min 0
:nillable true
:default 0
:placeholder "0"
:value (:margin params)
:on-change (handle-change :params :margin)}]])

View file

@ -41,12 +41,6 @@
:svg-raw #{:size :position :rotation}
:text #{:size :position :rotation}})
(defn- attr->string [attr values]
(let [value (attr values)]
(if (= value :multiple)
""
(str (-> value (d/coalesce 0))))))
(declare +size-presets+)
;; -- User/drawing coords
@ -241,7 +235,7 @@
:placeholder "--"
:on-click select-all
:on-change on-width-change
:value (attr->string :width values)}]]
:value (:width values)}]]
[:div.input-element.height {:title (tr "workspace.options.height")}
[:> numeric-input {:min 0.01
@ -249,7 +243,7 @@
:placeholder "--"
:on-click select-all
:on-change on-height-change
:value (attr->string :height values)}]]
:value (:height values)}]]
[:div.lock-size {:class (dom/classnames
:selected (true? proportion-lock)
@ -268,13 +262,13 @@
:placeholder "--"
:on-click select-all
:on-change on-pos-x-change
:value (attr->string :x values)}]]
:value (:x values)}]]
[:div.input-element.Yaxis {:title (tr "workspace.options.y")}
[:> numeric-input {:no-validate true
:placeholder "--"
:on-click select-all
:on-change on-pos-y-change
:value (attr->string :y values)}]]])
:value (:y values)}]]])
;; ROTATION
(when (options :rotation)
@ -285,19 +279,12 @@
{:no-validate true
:min 0
:max 359
:default 0
:data-wrap true
:placeholder "--"
:on-click select-all
:on-change on-rotation-change
:value (attr->string :rotation values)}]]
#_[:input.slidebar
{:type "range"
:min "0"
:max "359"
:step "10"
:no-validate true
:on-change on-rotation-change
:value (attr->string :rotation values)}]])
:value (:rotation values)}]]])
;; RADIUS
(when (options :radius)
@ -325,13 +312,14 @@
:min 0
:on-click select-all
:on-change on-radius-1-change
:value (attr->string :rx values)}]]
:value (:rx values)}]]
@radius-multi?
[:div.input-element.mini {:title (tr "workspace.options.radius")}
[:input.input-text
{:type "number"
:placeholder "--"
:min 0
:on-click select-all
:on-change on-radius-multi-change
:value ""}]]
@ -344,7 +332,7 @@
:min 0
:on-click select-all
:on-change on-radius-r1-change
:value (attr->string :r1 values)}]]
:value (:r1 values)}]]
[:div.input-element.mini {:title (tr "workspace.options.radius")}
[:> numeric-input
@ -352,7 +340,7 @@
:min 0
:on-click select-all
:on-change on-radius-r2-change
:value (attr->string :r2 values)}]]
:value (:r2 values)}]]
[:div.input-element.mini {:title (tr "workspace.options.radius")}
[:> numeric-input
@ -360,7 +348,7 @@
:min 0
:on-click select-all
:on-change on-radius-r3-change
:value (attr->string :r3 values)}]]
:value (:r3 values)}]]
[:div.input-element.mini {:title (tr "workspace.options.radius")}
[:> numeric-input
@ -368,7 +356,7 @@
:min 0
:on-click select-all
:on-change on-radius-r4-change
:value (attr->string :r4 values)}]]])])]]]))
:value (:r4 values)}]]])])]]]))
(def +size-presets+
[{:name "APPLE"}

View file

@ -12,7 +12,7 @@
[app.util.object :as obj]
[rumext.alpha :as mf]))
(mf/defc input-row [{:keys [label options value class min max on-change type placeholder]}]
(mf/defc input-row [{:keys [label options value class min max on-change type placeholder default nillable]}]
[:div.row-flex.input-row
[:span.element-set-subtitle label]
[:div.input-element {:class class}
@ -43,6 +43,8 @@
{:placeholder placeholder
:min min
:max max
:default default
:nillable nillable
:on-change on-change
:value (or value "")}])]])

View file

@ -6,7 +6,9 @@
(ns app.main.ui.workspace.viewport.frame-grid
(:require
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.uuid :as uuid]
[app.main.refs :as refs]
[app.util.geom.grid :as gg]
@ -41,27 +43,71 @@
:fill (str "url(#" grid-id ")")}]]))
(mf/defc layout-grid
[{:keys [key frame grid]}]
[{:keys [key frame grid zoom]}]
(let [{color-value :color color-opacity :opacity} (-> grid :params :color)
;; Support for old color format
color-value (or color-value (:value (get-in grid [:params :color :value])))
gutter (-> grid :params :gutter)
gutter? (and (not (nil? gutter)) (not= gutter 0))
gutter (gg/grid-gutter frame grid)
gutter? (and (not (nil? gutter)) (not (mth/almost-zero? gutter)))]
style (if gutter?
#js {:fill color-value
:opacity color-opacity}
#js {:stroke color-value
:strokeOpacity color-opacity
:fill "none"})]
[:g.grid
(for [{:keys [x y width height] :as area} (gg/grid-areas frame grid)]
[:rect {:key (str key "-" x "-" y)
:x x
:y y
:width (- (+ x width) x)
:height (- (+ y height) y)
:style style}])]))
(for [[idx {:keys [x y width height] :as area}] (d/enumerate (gg/grid-areas frame grid))]
(cond
gutter?
[:rect {:key (str key "-" x "-" y)
:x x
:y y
:width (- (+ x width) x)
:height (- (+ y height) y)
:style {:fill color-value
:stroke-width 0
:opacity color-opacity}}]
(and (not gutter?) (= :column (:type grid)))
[:*
(when (= idx 0)
[:line {:key (str key "-" x "-" y "-start")
:x1 x
:y1 y
:x2 x
:y2 (+ y height)
:style {:stroke color-value
:stroke-width (/ 1 zoom)
:strokeOpacity color-opacity
:fill "none"}}])
[:line {:key (str key "-" x "-" y "-end")
:x1 (+ x width)
:y1 y
:x2 (+ x width)
:y2 (+ y height)
:style {:stroke color-value
:stroke-width (/ 1 zoom)
:strokeOpacity color-opacity
:fill "none"}}]]
(and (not gutter?) (= :row (:type grid)))
[:*
(when (= idx 0)
[:line {:key (str key "-" x "-" y "-start")
:x1 x
:y1 y
:x2 (+ x width)
:y2 y
:style {:stroke color-value
:stroke-width (/ 1 zoom)
:strokeOpacity color-opacity
:fill "none"}}])
[:line {:key (str key "-" x "-" y "-end")
:x1 x
:y1 (+ y height)
:x2 (+ x width)
:y2 (+ y height)
:style {:stroke color-value
:stroke-width (/ 1 zoom)
:strokeOpacity color-opacity
:fill "none"}}]]))]))
(mf/defc grid-display-frame
[{:keys [frame zoom]}]

View file

@ -69,6 +69,8 @@
(rx/flat-map
(fn [[frame-id point]]
(->> (snap/get-snap-points page-id frame-id remove-snap? zoom point coord)
(rx/map #(mapcat second %))
(rx/map #(map :pt %))
(rx/map #(vector point % coord)))))
(rx/reduce conj []))))
@ -157,22 +159,29 @@
(mf/defc snap-points
{::mf/wrap [mf/memo]}
[{:keys [layout zoom objects selected page-id drawing transform modifiers focus] :as props}]
[{:keys [layout zoom objects selected page-id drawing modifiers focus] :as props}]
(us/assert set? selected)
(let [shapes (into [] (keep (d/getf objects)) selected)
filter-shapes
(into selected (mapcat #(cph/get-children-ids objects %)) selected)
remove-snap?
remove-snap-base?
(mf/with-memo [layout filter-shapes objects focus]
(snap/make-remove-snap layout filter-shapes objects focus))
shapes (if drawing [drawing] shapes)]
(when (or drawing transform)
[:& snap-feedback {:shapes shapes
:page-id page-id
:remove-snap? remove-snap?
:zoom zoom
:modifiers modifiers}])))
remove-snap?
(mf/use-callback
(mf/deps remove-snap-base?)
(fn [{:keys [type grid] :as snap}]
(or (remove-snap-base? snap)
(and (= type :layout) (= grid :square))
(= type :guide))))
shapes (if drawing [drawing] shapes)]
[:& snap-feedback {:shapes shapes
:page-id page-id
:remove-snap? remove-snap?
:zoom zoom
:modifiers modifiers}]))

View file

@ -168,3 +168,10 @@
nm (str/trim (str/lower name))]
(str/includes? nm st))))
(defn tap
"Works like rx/tap but for normal collections"
;; Signature for transducer use
([f]
(map #(do (f %) %)))
([f coll]
(map #(do (f %) %) coll)))

View file

@ -46,7 +46,7 @@
next-v (fn [cur-val]
(+ offset v (* (+ width' gutter) cur-val)))]
[size width' next-v]))
[size width' next-v gutter]))
(defn- calculate-column-grid
[{:keys [width height x y] :as frame} params]
@ -70,6 +70,20 @@
[(* col-size row-size) size size next-x next-y]))
(defn grid-gutter
[{:keys [x y width height]} {:keys [type params] :as grid}]
(case type
:column
(let [[_ _ _ gutter] (calculate-generic-grid x width params)]
gutter)
:row
(let [[_ _ _ gutter] (calculate-generic-grid y height params)]
gutter)
nil))
(defn grid-areas
"Given a frame and the grid parameters returns the areas defined on the grid"
[frame grid]
@ -93,28 +107,25 @@
(defn grid-snap-points
"Returns the snap points for a given grid"
([shape coord]
(mapcat #(grid-snap-points shape % coord) (:grids shape)))
[shape {:keys [type params] :as grid} coord]
(when (:display grid)
(case type
:square
(let [{:keys [x y width height]} shape
size (-> params :size)]
(when (> size 0)
(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)))))
([shape {:keys [type params] :as grid} coord]
(when (:display grid)
(case type
:square
(let [{:keys [x y width height]} shape
size (-> params :size)]
(when (> size 0)
(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)
(->> (grid-areas shape grid)
(mapcat grid-area-points)))
:column
(when (= coord :x)
(->> (grid-areas shape grid)
(mapcat grid-area-points)))
:row
(when (= coord :y)
(->> (grid-areas shape grid)
(mapcat grid-area-points)))))))
:row
(when (= coord :y)
(->> (grid-areas shape grid)
(mapcat grid-area-points))))))

View file

@ -9,25 +9,28 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]))
(defn selrect-snap-points [{:keys [x y width height]}]
(defn selrect-snap-points [{:keys [x y width height] :as selrect}]
#{(gpt/point x y)
(gpt/point (+ x width) y)
(gpt/point (+ x width) (+ y height))
(gpt/point x (+ y height))})
(gpt/point x (+ y height))
(gsh/center-selrect selrect)})
(defn frame-snap-points [{:keys [x y width height] :as selrect}]
(into (selrect-snap-points selrect)
#{(gpt/point (+ x (/ width 2)) y)
(gpt/point (+ x width) (+ y (/ height 2)))
(gpt/point (+ x (/ width 2)) (+ y height))
(gpt/point x (+ y (/ height 2)))}))
(defn frame-snap-points [{:keys [x y width height blocked hidden] :as selrect}]
(when (and (not blocked) (not hidden))
(into (selrect-snap-points selrect)
#{(gpt/point (+ x (/ width 2)) y)
(gpt/point (+ x width) (+ y (/ height 2)))
(gpt/point (+ x (/ width 2)) (+ y height))
(gpt/point x (+ y (/ height 2)))})))
(defn shape-snap-points
[shape]
(let [shape (gsh/transform-shape shape)]
(case (:type shape)
:frame (-> shape :selrect frame-snap-points)
(into #{(gsh/center-shape shape)} (:points shape)))))
[{:keys [hidden blocked] :as shape}]
(when (and (not blocked) (not hidden))
(let [shape (gsh/transform-shape shape)]
(case (:type shape)
:frame (-> shape :selrect frame-snap-points)
(into #{(gsh/center-shape shape)} (:points shape))))))
(defn guide-snap-points
[guide]

View file

@ -53,6 +53,19 @@
(assoc-in [frame-id :x] (rt/make-tree))
(assoc-in [frame-id :y] (rt/make-tree)))))
(defn get-grids-snap-points
[frame coord]
(let [grid->snap (fn [[grid-type position]]
{:type :layout
:id (:id frame)
:grid grid-type
:pt position})]
(->> (:grids frame)
(mapcat (fn [grid]
(->> (gg/grid-snap-points frame grid coord)
(mapv #(vector (:type grid) %)))))
(mapv grid->snap))))
(defn- add-frame
[page-data frame]
(let [frame-id (:id frame)
@ -61,17 +74,8 @@
(mapv #(array-map :type :shape
:id frame-id
:pt %)))
grid-x-data (->> (gg/grid-snap-points frame :x)
(mapv #(array-map :type :layout
:id frame-id
:pt %)))
grid-y-data (->> (gg/grid-snap-points frame :y)
(mapv #(array-map :type :layout
:id frame-id
:pt %)))]
grid-x-data (get-grids-snap-points frame :x)
grid-y-data (get-grids-snap-points frame :y)]
(-> page-data
;; Update root frame information
(assoc-in [uuid/zero :objects-data frame-id] frame-data)
@ -107,6 +111,7 @@
(mapv #(array-map
:type :guide
:id (:id guide)
:axis (:axis guide)
:frame-id (:frame-id guide)
:pt %)))]
(if-let [frame-id (:frame-id guide)]

View file

@ -70,7 +70,7 @@
(let [page (current-page state)
frame (cph/get-frame (:objects page))
shape (-> (cp/make-minimal-shape type)
(gsh/setup {:x 0 :y 0 :width 1 :height 1})
(cp/setup-shape {:x 0 :y 0 :width 1 :height 1})
(merge props))]
(swap! idmap assoc label (:id shape))
(update state :workspace-data