mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
884 lines
34 KiB
Clojure
884 lines
34 KiB
Clojure
;; 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) KALEIDOS INC
|
|
|
|
(ns app.main.data.workspace.transforms
|
|
"Events related with shapes transformations"
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.data.macros :as dm]
|
|
[app.common.files.changes-builder :as pcb]
|
|
[app.common.files.helpers :as cfh]
|
|
[app.common.geom.matrix :as gmt]
|
|
[app.common.geom.modifiers :as gm]
|
|
[app.common.geom.point :as gpt]
|
|
[app.common.geom.rect :as grc]
|
|
[app.common.geom.shapes :as gsh]
|
|
[app.common.geom.shapes.flex-layout :as gslf]
|
|
[app.common.geom.shapes.grid-layout :as gslg]
|
|
[app.common.logic.shapes :as cls]
|
|
[app.common.math :as mth]
|
|
[app.common.types.component :as ctk]
|
|
[app.common.types.container :as ctn]
|
|
[app.common.types.modifiers :as ctm]
|
|
[app.common.types.shape-tree :as ctst]
|
|
[app.common.types.shape.layout :as ctl]
|
|
[app.main.data.changes :as dch]
|
|
[app.main.data.workspace.collapse :as dwc]
|
|
[app.main.data.workspace.modifiers :as dwm]
|
|
[app.main.data.workspace.selection :as dws]
|
|
[app.main.data.workspace.state-helpers :as wsh]
|
|
[app.main.data.workspace.undo :as dwu]
|
|
[app.main.snap :as snap]
|
|
[app.main.streams :as ms]
|
|
[app.util.array :as array]
|
|
[app.util.dom :as dom]
|
|
[app.util.keyboard :as kbd]
|
|
[app.util.mouse :as mse]
|
|
[beicon.v2.core :as rx]
|
|
[potok.v2.core :as ptk]))
|
|
|
|
;; -- Helpers --------------------------------------------------------
|
|
|
|
;; For each of the 8 handlers gives the multiplier for resize
|
|
;; for example, right will only grow in the x coordinate and left
|
|
;; will grow in the inverse of the x coordinate
|
|
(defn get-handler-multiplier
|
|
[handler]
|
|
(case handler
|
|
:right (gpt/point 1 0)
|
|
:bottom (gpt/point 0 1)
|
|
:left (gpt/point -1 0)
|
|
:top (gpt/point 0 -1)
|
|
:top-right (gpt/point 1 -1)
|
|
:top-left (gpt/point -1 -1)
|
|
:bottom-right (gpt/point 1 1)
|
|
:bottom-left (gpt/point -1 1)))
|
|
|
|
(defn- get-handler-resize-origin
|
|
"Given a handler, return the coordinate origin for resizes.
|
|
This is the opposite of the handler so for right we want the
|
|
left side as origin of the resize.
|
|
|
|
sx, sy => start x/y
|
|
mx, my => middle x/y
|
|
ex, ey => end x/y
|
|
"
|
|
[selrect handler]
|
|
(let [sx (dm/get-prop selrect :x)
|
|
sy (dm/get-prop selrect :y)
|
|
width (dm/get-prop selrect :width)
|
|
height (dm/get-prop selrect :height)
|
|
mx (+ sx (/ width 2))
|
|
my (+ sy (/ height 2))
|
|
ex (+ sx width)
|
|
ey (+ sy height)]
|
|
(case handler
|
|
:right (gpt/point sx my)
|
|
:bottom (gpt/point mx sy)
|
|
:left (gpt/point ex my)
|
|
:top (gpt/point mx ey)
|
|
:top-right (gpt/point sx ey)
|
|
:top-left (gpt/point ex ey)
|
|
:bottom-right (gpt/point sx sy)
|
|
:bottom-left (gpt/point ex sy))))
|
|
|
|
(defn- fix-init-point
|
|
"Fix the initial point so the resizes are accurate"
|
|
[initial handler shape]
|
|
(let [selrect (dm/get-prop shape :selrect)
|
|
x (dm/get-prop selrect :x)
|
|
y (dm/get-prop selrect :y)
|
|
width (dm/get-prop selrect :width)
|
|
height (dm/get-prop selrect :height)]
|
|
|
|
(case handler
|
|
:left
|
|
(assoc initial :x x)
|
|
|
|
:top
|
|
(assoc initial :y y)
|
|
|
|
:top-left
|
|
(-> initial
|
|
(assoc :x x)
|
|
(assoc :y y))
|
|
|
|
:bottom-left
|
|
(-> initial
|
|
(assoc :x x)
|
|
(assoc :y (+ y height)))
|
|
|
|
:right
|
|
(assoc initial :x (+ x width))
|
|
|
|
:top-right
|
|
(-> initial
|
|
(assoc :x (+ x width))
|
|
(assoc :y y))
|
|
|
|
:bottom-right
|
|
(-> initial
|
|
(assoc :x (+ x width))
|
|
(assoc :y (+ y height)))
|
|
|
|
:bottom
|
|
(assoc initial :y (+ y height)))))
|
|
|
|
(defn finish-transform []
|
|
(ptk/reify ::finish-transform
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(update state :workspace-local dissoc :transform :duplicate-move-started? false))))
|
|
|
|
;; -- Resize --------------------------------------------------------
|
|
|
|
(defn start-resize
|
|
"Enter mouse resize mode, until mouse button is released."
|
|
[handler ids shape]
|
|
(letfn [(resize [shape initial layout [point lock? center? point-snap]]
|
|
(let [selrect (dm/get-prop shape :selrect)
|
|
width (dm/get-prop selrect :width)
|
|
height (dm/get-prop selrect :height)
|
|
rotation (dm/get-prop shape :rotation)
|
|
|
|
shape-center (gsh/shape->center shape)
|
|
shape-transform (:transform shape)
|
|
shape-transform-inverse (:transform-inverse shape)
|
|
|
|
rotation (or rotation 0)
|
|
|
|
initial (gmt/transform-point-center initial shape-center shape-transform-inverse)
|
|
initial (fix-init-point initial handler shape)
|
|
|
|
point (gmt/transform-point-center (if (= rotation 0) point-snap point)
|
|
shape-center shape-transform-inverse)
|
|
|
|
shapev (-> (gpt/point width height))
|
|
|
|
scale-text (contains? layout :scale-text)
|
|
|
|
;; Force lock if the scale text mode is active
|
|
lock? (or ^boolean lock?
|
|
^boolean scale-text)
|
|
|
|
;; Difference between the origin point in the
|
|
;; coordinate system of the rotation
|
|
deltav (-> (gpt/to-vec initial point)
|
|
;; Vector modifiers depending on the handler
|
|
(gpt/multiply (get-handler-multiplier handler)))
|
|
|
|
;; Resize vector
|
|
scalev (-> (gpt/divide (gpt/add shapev deltav) shapev)
|
|
(gpt/no-zeros))
|
|
|
|
scalev (if ^boolean lock?
|
|
(let [v (cond
|
|
(or (= handler :right)
|
|
(= handler :left))
|
|
(dm/get-prop scalev :x)
|
|
|
|
(or (= handler :top)
|
|
(= handler :bottom))
|
|
(dm/get-prop scalev :y)
|
|
|
|
:else
|
|
(mth/max (dm/get-prop scalev :x)
|
|
(dm/get-prop scalev :y)))]
|
|
(gpt/point v v))
|
|
scalev)
|
|
|
|
;; Resize origin point given the selected handler
|
|
selrect (dm/get-prop shape :selrect)
|
|
handler-origin (get-handler-resize-origin selrect handler)
|
|
|
|
;; If we want resize from center, displace the shape
|
|
;; so it is still centered after resize.
|
|
displacement (when ^boolean center?
|
|
(-> shape-center
|
|
(gpt/subtract handler-origin)
|
|
(gpt/multiply scalev)
|
|
(gpt/add handler-origin)
|
|
(gpt/subtract shape-center)
|
|
(gpt/multiply (gpt/point -1 -1))
|
|
(gpt/transform shape-transform)))
|
|
|
|
resize-origin (gmt/transform-point-center handler-origin shape-center shape-transform)
|
|
resize-origin (if (some? displacement)
|
|
(gpt/add resize-origin displacement)
|
|
resize-origin)
|
|
|
|
;; When the horizontal/vertical scale a flex children with auto/fill
|
|
;; we change it too fixed
|
|
set-fix-width?
|
|
(not (mth/close? (dm/get-prop scalev :x) 1))
|
|
|
|
set-fix-height?
|
|
(not (mth/close? (dm/get-prop scalev :y) 1))
|
|
|
|
modifiers (cond-> (ctm/empty)
|
|
(some? displacement)
|
|
(ctm/move displacement)
|
|
|
|
:always
|
|
(ctm/resize scalev resize-origin shape-transform shape-transform-inverse)
|
|
|
|
^boolean set-fix-width?
|
|
(ctm/change-property :layout-item-h-sizing :fix)
|
|
|
|
^boolean set-fix-height?
|
|
(ctm/change-property :layout-item-v-sizing :fix)
|
|
|
|
^boolean scale-text
|
|
(ctm/scale-content (dm/get-prop scalev :x)))
|
|
|
|
modif-tree (dwm/create-modif-tree ids modifiers)]
|
|
|
|
(rx/of (dwm/set-modifiers modif-tree scale-text))))
|
|
|
|
;; Unifies the instantaneous proportion lock modifier
|
|
;; activated by Shift key and the shapes own proportion
|
|
;; lock flag that can be activated on element options.
|
|
(normalize-proportion-lock [[point shift? alt?]]
|
|
(let [proportion-lock? (:proportion-lock shape)]
|
|
[point
|
|
(or ^boolean proportion-lock?
|
|
^boolean shift?)
|
|
alt?]))]
|
|
(reify
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(-> state
|
|
(assoc-in [:workspace-local :transform] :resize)))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state stream]
|
|
(let [initial-position @ms/mouse-position
|
|
|
|
stopper (mse/drag-stopper stream)
|
|
layout (:workspace-layout state)
|
|
page-id (:current-page-id state)
|
|
focus (:workspace-focus-selected state)
|
|
zoom (dm/get-in state [:workspace-local :zoom] 1)
|
|
objects (wsh/lookup-page-objects state page-id)
|
|
shapes (map (d/getf objects) ids)]
|
|
|
|
(rx/concat
|
|
(->> ms/mouse-position
|
|
(rx/filter some?)
|
|
(rx/with-latest-from ms/mouse-position-shift ms/mouse-position-alt)
|
|
(rx/map normalize-proportion-lock)
|
|
(rx/switch-map (fn [[point _ _ :as current]]
|
|
(->> (snap/closest-snap-point page-id shapes objects layout zoom focus point)
|
|
(rx/map #(conj current %)))))
|
|
(rx/mapcat (partial resize shape initial-position layout))
|
|
(rx/take-until stopper))
|
|
(rx/of (dwm/apply-modifiers)
|
|
(finish-transform))))))))
|
|
|
|
(defn trigger-bounding-box-cloaking
|
|
"Trigger the bounding box cloaking (with default timer of 1sec)
|
|
|
|
Used to hide bounding-box of shape after changes in sidebar->measures."
|
|
[ids]
|
|
(dm/assert!
|
|
"expected valid coll of uuids"
|
|
(every? uuid? ids))
|
|
|
|
(ptk/reify ::trigger-bounding-box-cloaking
|
|
ptk/WatchEvent
|
|
(watch [_ _ stream]
|
|
(rx/concat
|
|
(rx/of #(assoc-in % [:workspace-local :transform] :move))
|
|
(->> (rx/timer 1000)
|
|
(rx/map (fn []
|
|
#(assoc-in % [:workspace-local :transform] nil)))
|
|
(rx/take-until
|
|
(rx/filter (ptk/type? ::trigger-bounding-box-cloaking) stream)))))))
|
|
|
|
(defn update-dimensions
|
|
"Change size of shapes, from the sideber options form.
|
|
Will ignore pixel snap used in the options side panel"
|
|
[ids attr value]
|
|
(dm/assert! (number? value))
|
|
(dm/assert!
|
|
"expected valid coll of uuids"
|
|
(every? uuid? ids))
|
|
(dm/assert!
|
|
"expected valid attr"
|
|
(contains? #{:width :height} attr))
|
|
(ptk/reify ::update-dimensions
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [objects (wsh/lookup-page-objects state)
|
|
get-modifier
|
|
(fn [shape] (ctm/change-dimensions-modifiers shape attr value))
|
|
|
|
modif-tree
|
|
(-> (dwm/build-modif-tree ids objects get-modifier)
|
|
(gm/set-objects-modifiers objects))]
|
|
|
|
(assoc state :workspace-modifiers modif-tree)))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(rx/of (dwm/apply-modifiers)))))
|
|
|
|
(defn change-orientation
|
|
"Change orientation of shapes, from the sidebar options form.
|
|
Will ignore pixel snap used in the options side panel"
|
|
[ids orientation]
|
|
(dm/assert!
|
|
"expected valid coll of uuids"
|
|
(every? uuid? ids))
|
|
(dm/assert!
|
|
"expected valid orientation"
|
|
(contains? #{:horiz :vert} orientation))
|
|
|
|
(ptk/reify ::change-orientation
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [objects (wsh/lookup-page-objects state)
|
|
|
|
get-modifier
|
|
(fn [shape] (ctm/change-orientation-modifiers shape orientation))
|
|
|
|
modif-tree
|
|
(-> (dwm/build-modif-tree ids objects get-modifier)
|
|
(gm/set-objects-modifiers objects))]
|
|
|
|
(assoc state :workspace-modifiers modif-tree)))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(rx/of (dwm/apply-modifiers)))))
|
|
|
|
;; -- Rotate --------------------------------------------------------
|
|
|
|
(defn start-rotate
|
|
"Enter mouse rotate mode, until mouse button is released."
|
|
[shapes]
|
|
(ptk/reify ::start-rotate
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(-> state
|
|
(assoc-in [:workspace-local :transform] :rotate)))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ _ stream]
|
|
(let [stopper (mse/drag-stopper stream)
|
|
group (gsh/shapes->rect shapes)
|
|
group-center (grc/rect->center group)
|
|
initial-angle (gpt/angle @ms/mouse-position group-center)
|
|
|
|
calculate-angle
|
|
(fn [pos mod? shift?]
|
|
(let [angle (- (gpt/angle pos group-center) initial-angle)
|
|
angle (if (neg? angle) (+ 360 angle) angle)
|
|
angle (if (= angle 360)
|
|
0
|
|
angle)
|
|
angle (if mod?
|
|
(* (mth/floor (/ angle 45)) 45)
|
|
angle)
|
|
angle (if shift?
|
|
(* (mth/floor (/ angle 15)) 15)
|
|
angle)]
|
|
angle))]
|
|
(rx/concat
|
|
(->> ms/mouse-position
|
|
(rx/with-latest-from ms/mouse-position-mod ms/mouse-position-shift)
|
|
(rx/map
|
|
(fn [[pos mod? shift?]]
|
|
(let [delta-angle (calculate-angle pos mod? shift?)]
|
|
(dwm/set-rotation-modifiers delta-angle shapes group-center))))
|
|
(rx/take-until stopper))
|
|
(rx/of (dwm/apply-modifiers)
|
|
(finish-transform)))))))
|
|
|
|
(defn increase-rotation
|
|
"Rotate shapes a fixed angle, from a keyboard action."
|
|
[ids rotation]
|
|
(ptk/reify ::increase-rotation
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
|
|
(let [page-id (:current-page-id state)
|
|
objects (wsh/lookup-page-objects state page-id)
|
|
shapes (->> ids (map #(get objects %)))]
|
|
(rx/concat
|
|
(rx/of (dwm/set-delta-rotation-modifiers rotation shapes))
|
|
(rx/of (dwm/apply-modifiers)))))))
|
|
|
|
|
|
;; -- Move ----------------------------------------------------------
|
|
|
|
(declare start-move)
|
|
(declare start-move-duplicate)
|
|
(declare move-shapes-to-frame)
|
|
(declare get-displacement)
|
|
|
|
(defn start-move-selected
|
|
"Enter mouse move mode, until mouse button is released."
|
|
([]
|
|
(start-move-selected nil false))
|
|
|
|
([id shift?]
|
|
(ptk/reify ::start-move-selected
|
|
ptk/WatchEvent
|
|
(watch [_ state stream]
|
|
(let [initial (deref ms/mouse-position)
|
|
|
|
stopper (mse/drag-stopper stream)
|
|
zoom (get-in state [:workspace-local :zoom] 1)
|
|
|
|
;; We toggle the selection so we don't have to wait for the event
|
|
selected
|
|
(cond-> (wsh/lookup-selected state {:omit-blocked? true})
|
|
(some? id)
|
|
(d/toggle-selection id shift?))]
|
|
|
|
;; Take the first mouse position and start a move or a duplicate
|
|
(when (or (d/not-empty? selected) (some? id))
|
|
(->> ms/mouse-position
|
|
(rx/map #(gpt/to-vec initial %))
|
|
(rx/map #(gpt/length %))
|
|
(rx/filter #(> % (/ 10 zoom)))
|
|
(rx/take 1)
|
|
(rx/with-latest-from ms/mouse-position-alt)
|
|
(rx/mapcat
|
|
(fn [[_ alt?]]
|
|
(rx/concat
|
|
(if (some? id)
|
|
(rx/of (dws/select-shape id shift?))
|
|
(rx/empty))
|
|
|
|
(if alt?
|
|
;; When alt is down we start a duplicate+move
|
|
(rx/of (start-move-duplicate initial)
|
|
(dws/duplicate-selected false true))
|
|
|
|
;; Otherwise just plain old move
|
|
(rx/of (start-move initial selected))))))
|
|
(rx/take-until stopper))))))))
|
|
|
|
(defn- start-move-duplicate
|
|
[from-position]
|
|
(ptk/reify ::start-move-duplicate
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(-> state
|
|
(assoc-in [:workspace-local :transform] :move)
|
|
(assoc-in [:workspace-local :duplicate-move-started?] true)))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ _ stream]
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::dws/duplicate-selected))
|
|
(rx/take 1)
|
|
(rx/map #(start-move from-position))))))
|
|
|
|
(defn set-ghost-displacement
|
|
[move-vector]
|
|
(ptk/reify ::set-ghost-displacement
|
|
ptk/EffectEvent
|
|
(effect [_ _ _]
|
|
(when-let [node (dom/get-element-by-class "ghost-outline")]
|
|
(dom/set-property! node "transform" (gmt/translate-matrix move-vector))))))
|
|
|
|
(defn start-move
|
|
([from-position] (start-move from-position nil))
|
|
([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 (:current-page-id state)
|
|
objects (wsh/lookup-page-objects state page-id)
|
|
selected (wsh/lookup-selected state {:omit-blocked? true})
|
|
ids (if (nil? ids) selected ids)
|
|
shapes (into []
|
|
(comp (map (d/getf objects))
|
|
(remove #(let [parent (get objects (:parent-id %))]
|
|
(and (ctk/in-component-copy? parent)
|
|
(ctl/any-layout? parent)))))
|
|
ids)
|
|
|
|
duplicate-move-started? (get-in state [:workspace-local :duplicate-move-started?] false)
|
|
|
|
stopper (mse/drag-stopper stream)
|
|
layout (get state :workspace-layout)
|
|
zoom (get-in state [:workspace-local :zoom] 1)
|
|
focus (:workspace-focus-selected state)
|
|
|
|
exclude-frames
|
|
(into #{}
|
|
(filter (partial cfh/frame-shape? objects))
|
|
(cfh/selected-with-children objects selected))
|
|
|
|
exclude-frames-siblings
|
|
(into exclude-frames
|
|
(comp (mapcat (partial cfh/get-siblings-ids objects))
|
|
(filter (partial ctl/any-layout-immediate-child-id? objects)))
|
|
selected)
|
|
|
|
position (->> ms/mouse-position
|
|
(rx/map #(gpt/to-vec from-position %)))
|
|
|
|
snap-delta (rx/concat
|
|
;; We send the nil first so the stream is not waiting for the first value
|
|
(rx/of nil)
|
|
(->> position
|
|
;; FIXME: performance throttle
|
|
(rx/throttle 20)
|
|
(rx/switch-map
|
|
(fn [pos]
|
|
(->> (snap/closest-snap-move page-id shapes objects layout zoom focus pos)
|
|
(rx/map #(array pos %)))))))]
|
|
(if (empty? shapes)
|
|
(rx/of (finish-transform))
|
|
(let [move-stream
|
|
(->> position
|
|
;; We ask for the snap position but we continue even if the result is not available
|
|
(rx/with-latest-from 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/with-latest-from ms/mouse-position-mod)
|
|
|
|
(rx/map
|
|
(fn [[move-vector mod?]]
|
|
(let [position (gpt/add from-position move-vector)
|
|
exclude-frames (if mod? exclude-frames exclude-frames-siblings)
|
|
target-frame (ctst/top-nested-frame objects position exclude-frames)
|
|
[target-frame _] (ctn/find-valid-parent-and-frame-ids target-frame objects shapes)
|
|
flex-layout? (ctl/flex-layout? objects target-frame)
|
|
grid-layout? (ctl/grid-layout? objects target-frame)
|
|
drop-index (when flex-layout? (gslf/get-drop-index target-frame objects position))
|
|
cell-data (when (and grid-layout? (not mod?)) (gslg/get-drop-cell target-frame objects position))]
|
|
(array move-vector target-frame drop-index cell-data))))
|
|
|
|
(rx/take-until stopper))]
|
|
|
|
(rx/merge
|
|
;; Temporary modifiers stream
|
|
(->> move-stream
|
|
(rx/with-latest-from array/conj ms/mouse-position-shift)
|
|
(rx/map
|
|
(fn [[move-vector target-frame drop-index cell-data shift?]]
|
|
(let [x-disp? (> (mth/abs (:x move-vector)) (mth/abs (:y move-vector)))
|
|
[move-vector snap-ignore-axis]
|
|
(cond
|
|
(and shift? x-disp?)
|
|
[(assoc move-vector :y 0) :y]
|
|
|
|
shift?
|
|
[(assoc move-vector :x 0) :x]
|
|
|
|
:else
|
|
[move-vector nil])]
|
|
|
|
(-> (dwm/create-modif-tree ids (ctm/move-modifiers move-vector))
|
|
(dwm/build-change-frame-modifiers objects selected target-frame drop-index cell-data)
|
|
(dwm/set-modifiers false false {:snap-ignore-axis snap-ignore-axis}))))))
|
|
|
|
(->> move-stream
|
|
(rx/with-latest-from ms/mouse-position-alt)
|
|
(rx/filter (fn [[_ alt?]] alt?))
|
|
(rx/take 1)
|
|
(rx/mapcat
|
|
(fn [[_ alt?]]
|
|
(if (and (not duplicate-move-started?) alt?)
|
|
(rx/of (start-move-duplicate from-position)
|
|
(dws/duplicate-selected false true))
|
|
(rx/empty)))))
|
|
|
|
(->> move-stream
|
|
(rx/map (comp set-ghost-displacement first)))
|
|
|
|
;; Last event will write the modifiers creating the changes
|
|
(->> move-stream
|
|
(rx/last)
|
|
(rx/mapcat
|
|
(fn [[_ target-frame drop-index drop-cell]]
|
|
(let [undo-id (js/Symbol)]
|
|
(rx/of (dwu/start-undo-transaction undo-id)
|
|
(dwm/apply-modifiers {:undo-transation? false})
|
|
(move-shapes-to-frame ids target-frame drop-index drop-cell)
|
|
(finish-transform)
|
|
(dwu/commit-undo-transaction undo-id))))))))))))))
|
|
|
|
(def valid-directions
|
|
#{:up :down :right :left})
|
|
|
|
(defn reorder-selected-layout-child
|
|
[direction]
|
|
(ptk/reify ::reorder-layout-child
|
|
ptk/WatchEvent
|
|
(watch [it state _]
|
|
(let [selected (wsh/lookup-selected state {:omit-blocked? true})
|
|
objects (wsh/lookup-page-objects state)
|
|
page-id (:current-page-id state)
|
|
|
|
get-move-to-index
|
|
(fn [parent-id position]
|
|
(let [parent (get objects parent-id)]
|
|
(if (or (and (ctl/reverse? parent)
|
|
(or (= direction :left)
|
|
(= direction :up)))
|
|
(and (not (ctl/reverse? parent))
|
|
(or (= direction :right)
|
|
(= direction :down))))
|
|
(dec position)
|
|
(+ position 2))))
|
|
|
|
move-flex-children
|
|
(fn [changes parent-id children]
|
|
(->> children
|
|
;; Add the position to move the children
|
|
(map (fn [id]
|
|
(let [position (cfh/get-position-on-parent objects id)]
|
|
[id (get-move-to-index parent-id position)])))
|
|
(sort-by second >)
|
|
(reduce (fn [changes [child-id index]]
|
|
(pcb/change-parent changes parent-id [(get objects child-id)] index))
|
|
changes)))
|
|
|
|
move-grid-children
|
|
(fn [changes parent-id children]
|
|
(let [parent (get objects parent-id)
|
|
|
|
key-prop (case direction
|
|
(:up :down) :row
|
|
(:right :left) :column)
|
|
key-comp (case direction
|
|
(:up :left) <
|
|
(:down :right) >)
|
|
|
|
{:keys [layout-grid-cells]}
|
|
(->> children
|
|
(remove #(ctk/in-component-copy-not-head? (get objects %)))
|
|
(keep #(ctl/get-cell-by-shape-id parent %))
|
|
(sort-by key-prop key-comp)
|
|
(reduce (fn [parent {:keys [id row column row-span column-span]}]
|
|
(let [[next-row next-column]
|
|
(case direction
|
|
:up [(dec row) column]
|
|
:right [row (+ column column-span)]
|
|
:down [(+ row row-span) column]
|
|
:left [row (dec column)])
|
|
next-cell (ctl/get-cell-by-position parent next-row next-column)]
|
|
(cond-> parent
|
|
(some? next-cell)
|
|
(ctl/swap-shapes id (:id next-cell)))))
|
|
parent))]
|
|
(-> changes
|
|
(pcb/update-shapes
|
|
[(:id parent)]
|
|
(fn [shape]
|
|
(-> shape
|
|
(assoc :layout-grid-cells layout-grid-cells)
|
|
;; We want the previous objects value
|
|
(ctl/assign-cells objects))))
|
|
(pcb/reorder-grid-children [(:id parent)]))))
|
|
|
|
changes
|
|
(->> selected
|
|
(group-by #(dm/get-in objects [% :parent-id]))
|
|
(reduce
|
|
(fn [changes [parent-id children]]
|
|
(cond-> changes
|
|
(ctl/flex-layout? objects parent-id)
|
|
(move-flex-children parent-id children)
|
|
|
|
(ctl/grid-layout? objects parent-id)
|
|
(move-grid-children parent-id children)))
|
|
|
|
(-> (pcb/empty-changes it page-id)
|
|
(pcb/with-objects objects))))
|
|
|
|
undo-id (js/Symbol)]
|
|
|
|
(rx/of
|
|
(dwu/start-undo-transaction undo-id)
|
|
(dch/commit-changes changes)
|
|
(ptk/data-event :layout/update {:ids selected})
|
|
(dwu/commit-undo-transaction undo-id))))))
|
|
|
|
(defn nudge-selected-shapes
|
|
"Move shapes a fixed increment in one direction, from a keyboard action."
|
|
[direction shift?]
|
|
|
|
(let [same-event (js/Symbol "same-event")]
|
|
(ptk/reify ::nudge-selected-shapes
|
|
IDeref
|
|
(-deref [_] direction)
|
|
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(if (nil? (get state ::current-move-selected))
|
|
(-> state
|
|
(assoc-in [:workspace-local :transform] :move)
|
|
(assoc ::current-move-selected same-event))
|
|
state))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state stream]
|
|
(if (= same-event (get state ::current-move-selected))
|
|
(let [selected (wsh/lookup-selected state {:omit-blocked? true})
|
|
nudge (get-in state [:profile :props :nudge] {:big 10 :small 1})
|
|
move-events (->> stream
|
|
(rx/filter (ptk/type? ::nudge-selected-shapes))
|
|
(rx/filter #(= direction (deref %))))
|
|
|
|
stopper
|
|
(->> move-events
|
|
;; We stop when there's been 1s without movement or after 250ms after a key-up
|
|
(rx/switch-map #(rx/merge
|
|
(rx/timer 1000)
|
|
(->> stream
|
|
(rx/filter kbd/keyboard-event?)
|
|
(rx/filter kbd/key-up-event?)
|
|
(rx/delay 250))))
|
|
(rx/take 1))
|
|
|
|
scale (if shift? (gpt/point (or (:big nudge) 10)) (gpt/point (or (:small nudge) 1)))
|
|
mov-vec (gpt/multiply (get-displacement direction) scale)]
|
|
|
|
(rx/concat
|
|
(rx/merge
|
|
(->> move-events
|
|
(rx/scan #(gpt/add %1 mov-vec) (gpt/point 0 0))
|
|
(rx/map #(dwm/create-modif-tree selected (ctm/move-modifiers %)))
|
|
(rx/map #(dwm/set-modifiers % false true))
|
|
(rx/take-until stopper))
|
|
(rx/of (nudge-selected-shapes direction shift?)))
|
|
|
|
(rx/of (dwm/apply-modifiers)
|
|
(finish-transform))))
|
|
(rx/empty))))))
|
|
|
|
(defn move-selected
|
|
"Move shapes a fixed increment in one direction, from a keyboard action."
|
|
[direction shift?]
|
|
(dm/assert! (contains? valid-directions direction))
|
|
(dm/assert! (boolean? shift?))
|
|
|
|
(ptk/reify ::move-selected
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [objects (wsh/lookup-page-objects state)
|
|
selected (wsh/lookup-selected state {:omit-blocked? true})
|
|
selected-shapes (->> selected (map (d/getf objects)))]
|
|
(if (every? #(and (ctl/any-layout-immediate-child? objects %)
|
|
(not (ctl/position-absolute? %)))
|
|
selected-shapes)
|
|
(rx/of (reorder-selected-layout-child direction))
|
|
(rx/of (nudge-selected-shapes direction shift?)))))))
|
|
|
|
(defn update-position
|
|
"Move shapes to a new position"
|
|
[id position]
|
|
(dm/assert! (uuid? id))
|
|
|
|
(ptk/reify ::update-position
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [page-id (:current-page-id state)
|
|
objects (wsh/lookup-page-objects state page-id)
|
|
shape (get objects id)
|
|
|
|
;; FIXME: performance rect
|
|
bbox (-> shape :points grc/points->rect)
|
|
|
|
cpos (gpt/point (:x bbox) (:y bbox))
|
|
pos (gpt/point (or (:x position) (:x bbox))
|
|
(or (:y position) (:y bbox)))
|
|
|
|
delta (gpt/subtract pos cpos)
|
|
|
|
modif-tree (dwm/create-modif-tree [id] (ctm/move-modifiers delta))]
|
|
|
|
(rx/of (dwm/apply-modifiers {:modifiers modif-tree
|
|
:ignore-constraints false
|
|
:ignore-snap-pixel true}))))))
|
|
|
|
(defn position-shapes
|
|
[shapes]
|
|
(ptk/reify ::position-shapes
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [objects (wsh/lookup-page-objects state)
|
|
shapes (d/index-by :id shapes)
|
|
|
|
modif-tree
|
|
(dwm/build-modif-tree
|
|
(keys shapes)
|
|
objects
|
|
(fn [cshape]
|
|
(let [oshape (get shapes (:id cshape))
|
|
cpos (-> cshape :points first gpt/point)
|
|
opos (-> oshape :points first gpt/point)]
|
|
(ctm/move-modifiers (gpt/subtract opos cpos)))))]
|
|
|
|
(rx/of (dwm/apply-modifiers {:modifiers modif-tree
|
|
:ignore-constraints false
|
|
:ignore-snap-pixel true}))))))
|
|
|
|
(defn move-shapes-to-frame
|
|
[ids frame-id drop-index cell]
|
|
(ptk/reify ::move-shapes-to-frame
|
|
ptk/WatchEvent
|
|
(watch [it state _]
|
|
(let [page-id (:current-page-id state)
|
|
objects (wsh/lookup-page-objects state page-id)
|
|
changes (cls/generate-move-shapes-to-frame (pcb/empty-changes it) ids frame-id page-id objects drop-index cell)]
|
|
|
|
(when (and (some? frame-id) (d/not-empty? changes))
|
|
(rx/of (dch/commit-changes changes)
|
|
(dwc/expand-collapse frame-id)))))))
|
|
|
|
(defn- get-displacement
|
|
"Retrieve the correct displacement delta point for the
|
|
provided direction speed and distances thresholds."
|
|
[direction]
|
|
(case direction
|
|
:up (gpt/point 0 (- 1))
|
|
:down (gpt/point 0 1)
|
|
:left (gpt/point (- 1) 0)
|
|
:right (gpt/point 1 0)))
|
|
|
|
|
|
;; -- Flip ----------------------------------------------------------
|
|
|
|
(defn flip-horizontal-selected []
|
|
(ptk/reify ::flip-horizontal-selected
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [objects (wsh/lookup-page-objects state)
|
|
selected (wsh/lookup-selected state {:omit-blocked? true})
|
|
shapes (map #(get objects %) selected)
|
|
selrect (gsh/shapes->rect shapes)
|
|
center (grc/rect->center selrect)
|
|
modifiers (dwm/create-modif-tree selected (ctm/resize-modifiers (gpt/point -1.0 1.0) center))]
|
|
(rx/of (dwm/apply-modifiers {:modifiers modifiers :ignore-snap-pixel true}))))))
|
|
|
|
(defn flip-vertical-selected []
|
|
(ptk/reify ::flip-vertical-selected
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [objects (wsh/lookup-page-objects state)
|
|
selected (wsh/lookup-selected state {:omit-blocked? true})
|
|
shapes (map #(get objects %) selected)
|
|
selrect (gsh/shapes->rect shapes)
|
|
center (grc/rect->center selrect)
|
|
modifiers (dwm/create-modif-tree selected (ctm/resize-modifiers (gpt/point 1.0 -1.0) center))]
|
|
(rx/of (dwm/apply-modifiers {:modifiers modifiers :ignore-snap-pixel true}))))))
|