diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 388261fe1..02081c30c 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -693,7 +693,6 @@ :left (gpt/point (- 1) 0) :right (gpt/point 1 0))) -(s/def ::direction #{:up :down :right :left}) (s/def ::loc #{:up :down :bottom :top}) ;; --- Delete Selected diff --git a/frontend/src/uxbox/main/data/workspace/common.cljs b/frontend/src/uxbox/main/data/workspace/common.cljs index befd509b9..69bfbd51c 100644 --- a/frontend/src/uxbox/main/data/workspace/common.cljs +++ b/frontend/src/uxbox/main/data/workspace/common.cljs @@ -148,7 +148,6 @@ (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))))))) diff --git a/frontend/src/uxbox/main/data/workspace/transforms.cljs b/frontend/src/uxbox/main/data/workspace/transforms.cljs index e051e236c..54dccc194 100644 --- a/frontend/src/uxbox/main/data/workspace/transforms.cljs +++ b/frontend/src/uxbox/main/data/workspace/transforms.cljs @@ -68,6 +68,18 @@ (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 (defn start-resize [handler ids shape] @@ -78,11 +90,13 @@ ;; Vector modifiers depending on the handler 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 - deltav (-> (snap/closest-snap snap-data resizing-shapes (gpt/to-vec initial point)) + deltav (-> (gpt/to-vec initial (if (= rotation 0) point-snap point)) (gpt/transform (gmt/rotate-matrix (- rotation))) (gpt/multiply handler-modif)) - + ;; Resize vector scalev (gpt/divide (gpt/add shapev deltav) shapev) @@ -124,8 +138,8 @@ ptk/WatchEvent (watch [_ state stream] - (let [initial (apply-zoom @ms/mouse-position) - shape (gsh/shape->rect-shape shape) + (let [shape (gsh/shape->rect-shape shape) + initial (handler->initial-point shape handler) stoper (rx/filter ms/mouse-up? stream) snap-data (get state :workspace-snap-data) page-id (get state :current-page-id) @@ -184,9 +198,28 @@ ;; -- MOVE +(declare start-move) + (defn start-move-selected [] (ptk/reify ::start-move-selected + ptk/WatchEvent + (watch [_ state stream] + (let [initial (apply-zoom @ms/mouse-position) + selected (get-in state [:workspace-local :selected]) + stopper (rx/filter ms/mouse-up? stream)] + (->> ms/mouse-position + (rx/take-until stopper) + (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 @@ -194,34 +227,21 @@ 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) + (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) - stoper (rx/filter ms/mouse-up? stream) - zero-point? #(= % (gpt/point 0 0)) - initial (apply-zoom @ms/mouse-position) - position @ms/mouse-position - counter (volatile! 0)] + stopper (rx/filter ms/mouse-up? stream)] (rx/concat (->> ms/mouse-position + (rx/take-until stopper) (rx/map apply-zoom) - (rx/filter (complement zero-point?)) - (rx/map #(gpt/to-vec initial %)) - (rx/map (snap/closest-snap snap-data shapes)) + (rx/map #(gpt/to-vec from-position %)) + (rx/map (snap/closest-snap-move snap-data shapes)) (rx/map gmt/translate-matrix) - (rx/filter #(not (gmt/base? %))) - (rx/map #(set-modifiers selected {:displacement %})) - (rx/tap #(vswap! counter inc)) - (rx/take-until stoper)) - (->> (rx/create (fn [sink] (sink (reduced @counter)))) - (rx/mapcat (fn [n] - (if (zero? n) - (rx/empty) - (rx/of (apply-modifiers selected)))))) + (rx/map #(set-modifiers ids {:displacement %}))) - (rx/of finish-transform) - ))))) + (rx/of (apply-modifiers ids) + finish-transform)))))) (defn- get-displacement-with-grid "Retrieve the correct displacement delta point for the @@ -240,31 +260,60 @@ (defn- get-displacement "Retrieve the correct displacement delta point for the provided direction speed and distances thresholds." - [shape direction] + [direction] (case direction :up (gpt/point 0 (- 1)) :down (gpt/point 0 1) :left (gpt/point (- 1) 0) :right (gpt/point 1 0))) +(s/def ::direction #{:up :down :right :left}) + (defn move-selected [direction align?] (us/verify ::direction direction) (us/verify boolean? align?) - (ptk/reify ::move-selected - ptk/WatchEvent - (watch [_ state stream] - (let [pid (:current-page-id state) - selected (get-in state [:workspace-local :selected]) - options (get-in state [:workspace-data pid :options]) - shapes (map #(get-in state [:workspace-data pid :objects %]) selected) - shape (gsh/shapes->rect-shape shapes) - displacement (if align? - (get-displacement-with-grid shape direction options) - (get-displacement shape direction))] - (rx/of (set-modifiers selected displacement) - (apply-modifiers selected)))))) + (let [same-event (js/Symbol "same-event")] + (ptk/reify ::move-selected + IDeref + (-deref [_] direction) + + ptk/UpdateEvent + (update [_ state] + (if (nil? (get-in state [:workspace-local :current-move-selected])) + (-> state + (assoc-in [:workspace-local :transform] :move) + (assoc-in [:workspace-local :current-move-selected] same-event)) + state)) + + 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 diff --git a/frontend/src/uxbox/main/refs.cljs b/frontend/src/uxbox/main/refs.cljs index 1f06e8760..c3d2ff716 100644 --- a/frontend/src/uxbox/main/refs.cljs +++ b/frontend/src/uxbox/main/refs.cljs @@ -112,6 +112,9 @@ (def selected-drawing-tool (l/derived :drawing-tool workspace-local)) +(def current-drawing-shape + (l/derived :drawing workspace-local)) + (def selected-edition (l/derived :edition workspace-local)) diff --git a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs index 23c07512d..d0e6c025a 100644 --- a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs +++ b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs @@ -24,6 +24,7 @@ [uxbox.util.geom.path :as path] [uxbox.util.geom.point :as gpt] [uxbox.util.i18n :as i18n :refer [t]] + [uxbox.util.geom.snap :as snap] [uxbox.common.uuid :as uuid])) ;; --- Events @@ -115,28 +116,30 @@ (rx/of 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]) shape (geom/setup shape {:x (:x point) :y (:y point) :width 1 :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) 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 (if lock? (let [v (max (:x scalev) (:y scalev))] (gpt/point v v)) scalev)] - (-> shape (assoc-in [:modifiers :resize-vector] scalev) (assoc-in [:modifiers :resize-origin] (gpt/point x y)) (assoc-in [:modifiers :resize-rotation] 0)))) - (update-drawing [state initial point lock?] - (update-in state [:workspace-local :drawing] resize-shape initial point lock?))] + (update-drawing [state initial snap-data point lock?] + (update-in state [:workspace-local :drawing] resize-shape initial snap-data point lock?))] (ptk/reify ::handle-drawing-generic ptk/WatchEvent @@ -145,15 +148,29 @@ stoper? #(or (ms/mouse-up? %) (= % :interrupt)) stoper (rx/filter stoper? stream) initial @ms/mouse-position + snap-data (get state :workspace-snap-data) 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 (->> mouse (rx/take 1) - (rx/map (fn [pt] #(initialize-drawing % pt)))) + (rx/map (fn [pt] #(initialize-drawing % pt frame-id)))) (->> mouse (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/of handle-finish-drawing))))))) diff --git a/frontend/src/uxbox/main/ui/workspace/selection.cljs b/frontend/src/uxbox/main/ui/workspace/selection.cljs index b67ba6d2a..073695d65 100644 --- a/frontend/src/uxbox/main/ui/workspace/selection.cljs +++ b/frontend/src/uxbox/main/ui/workspace/selection.cljs @@ -97,6 +97,7 @@ on-resize (obj/get props "on-resize") 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) radius (if (> (max width height) handler-size-threshold) 4.0 4.0) @@ -112,34 +113,32 @@ [:g.controls (when (not (#{:move :rotate :resize} current-transform)) - [:rect.main {:transform transform - :x (- x 1) :y (- y 1) - :width (+ width 2) - :height (+ height 2) - :style {:stroke "#1FDEA7" - :stroke-width "1" - :fill "transparent"}}]) + [:rect.main {:transform transform + :x (- x 1) :y (- y 1) + :width (+ width 2) + :height (+ height 2) + :style {:stroke "#1FDEA7" + :stroke-width "1" + :fill "transparent"}}]) (when (not (#{:move :rotate} current-transform)) (for [[position [cx cy]] resize-handlers] (let [tp (gpt/transform (gpt/point cx cy) transform)] [:* {:key (name position)} - [:& rotation-handler {:key (str "rotation-" (name position)) - :cx (:x tp) + [:& rotation-handler {:cx (:x tp) :cy (:y tp) :position position :rotation (:rotation shape) :zoom zoom :on-mouse-down on-rotate}] - [:& control-item {:class (name position) - :on-click #(on-resize position %) - :r (/ radius zoom) - :cx (:x tp) - :cy (:y tp)}]]))])) + [:& control-item {:class (name position) + :on-click #(on-resize position %) + :r (/ radius zoom) + :cx (:x tp) + :cy (:y tp)}]])))])) ;; --- Selection Handlers (Component) - (mf/defc path-edition-selection-handlers [{:keys [shape modifiers zoom] :as props}] (letfn [(on-mouse-down [event index] diff --git a/frontend/src/uxbox/main/ui/workspace/snap_feedback.cljs b/frontend/src/uxbox/main/ui/workspace/snap_feedback.cljs index f40016ead..e87013f03 100644 --- a/frontend/src/uxbox/main/ui/workspace/snap_feedback.cljs +++ b/frontend/src/uxbox/main/ui/workspace/snap_feedback.cljs @@ -7,41 +7,56 @@ (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) - shapes (mf/deref (refs/objects-by-id selected)) + 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)] - (when (not (nil? 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))} - [:circle {:cx (:x point) - :cy (:y point) - :r 2 - :fill line-color}] + 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] - [:circle {:key (str "snap-" (:id shape) "-" (:x point) "-" (:y point) "-" (:x snap) "-" (:y snap)) - :cx (:x snap) - :cy (:y snap) - :r 2 - :fill line-color}]) + (for [snap snaps] + [:& snap-point {:key (str "snap-" (:id shape) "-" (:x point) "-" (:y point) "-" (:x snap) "-" (:y snap)) + :point snap}]) - (for [snap snaps] - [:line {:key (str "line-" (:id shape) "-" (:x point) "-" (:y point) "-" (:x snap) "-" (:y snap)) - :x1 (:x snap) - :y1 (:y snap) - :x2 (:x point) - :y2 (:y point) - :style {:stroke line-color :stroke-width "1"} - :opacity 0.4}])]))))))) + (for [snap snaps] + [:& snap-line {:key (str "line-" (:id shape) "-" (:x point) "-" (:y point) "-" (:x snap) "-" (:y snap)) + :snap snap + :point point}])]))))))) diff --git a/frontend/src/uxbox/main/ui/workspace/viewport.cljs b/frontend/src/uxbox/main/ui/workspace/viewport.cljs index 91a3d81c1..5e5fc597f 100644 --- a/frontend/src/uxbox/main/ui/workspace/viewport.cljs +++ b/frontend/src/uxbox/main/ui/workspace/viewport.cljs @@ -306,13 +306,14 @@ :zoom zoom :edition edition}]) - [:& snap-feedback] (when-let [drawing-shape (:drawing local)] [:& draw-area {:shape drawing-shape :zoom zoom :modifiers (:modifiers local)}]) + [:& snap-feedback] + (if (contains? flags :grid) [:& grid])] diff --git a/frontend/src/uxbox/util/geom/snap.cljs b/frontend/src/uxbox/util/geom/snap.cljs index 4a6c074aa..291abf3f3 100644 --- a/frontend/src/uxbox/util/geom/snap.cljs +++ b/frontend/src/uxbox/util/geom/snap.cljs @@ -10,13 +10,13 @@ (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] - [uxbox.util.debug :refer [logjs]])) + [uxbox.util.geom.point :as gpt])) -(def ^:private snap-accuracy 8) +(def ^:private snap-accuracy 20) (defn mapm "Map over the values of a map" @@ -33,12 +33,44 @@ (gpt/point x (+ y height)) (gpt/point x (+ y (/ height 2)))}) -(defn shape-snap-points [shape] - (if (= :frame (:type shape)) - (frame-snap-points shape) - (let [modified-path (gsh/transform-apply-modifiers shape) - shape-center (gsh/center modified-path)] - (into #{shape-center} (:segments modified-path))))) +(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 @@ -62,10 +94,9 @@ (filter #(= :frame (:type %))) (remove #(= zero (:id %))) (reduce #(update %1 (:id %2) conj %2) frame-shapes))] - (logjs "snap-data" - (mapm (fn [shapes] {:x (create-coord-data shapes :x) - :y (create-coord-data shapes :y)}) - 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" @@ -117,39 +148,46 @@ ;; Otherwise the root frame is the common :else zero))) -(defn closest-snap - ([snap-data shapes] (partial closest-snap snap-data shapes)) +(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 [;; 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 + (let [shapes-points (->> shapes ;; Unroll all the possible snap-points - (mapcat shape-snap-points) + (mapcat (partial 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)))) + (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 0.5) (+ value 0.5)) + (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)]