From ffd0c9576068f88793ba2ece5c9b9f8e0fadcd31 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 30 Apr 2020 11:37:36 +0200 Subject: [PATCH 1/3] :tada: Dynamic alignment brute-force method --- frontend/src/uxbox/main/data/workspace.cljs | 4 +- .../src/uxbox/main/data/workspace/common.cljs | 17 ++- .../uxbox/main/data/workspace/transforms.cljs | 17 ++- frontend/src/uxbox/main/refs.cljs | 3 + .../uxbox/main/ui/shapes/bounding_box.cljs | 35 ++++- .../uxbox/main/ui/workspace/selection.cljs | 32 ++-- .../workspace/sidebar/options/.#circle.cljs | 1 + .../main/ui/workspace/snap_feedback.cljs | 48 ++++++ frontend/src/uxbox/util/geom/range_tree.cljs | 25 +++ frontend/src/uxbox/util/geom/shapes.cljs | 2 +- frontend/src/uxbox/util/geom/snap.cljs | 143 ++++++++++++++++++ 11 files changed, 297 insertions(+), 30 deletions(-) create mode 120000 frontend/src/uxbox/main/ui/workspace/sidebar/options/.#circle.cljs create mode 100644 frontend/src/uxbox/main/ui/workspace/snap_feedback.cljs create mode 100644 frontend/src/uxbox/util/geom/range_tree.cljs create mode 100644 frontend/src/uxbox/util/geom/snap.cljs diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 633c0a140d..388261fe1f 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -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 diff --git a/frontend/src/uxbox/main/data/workspace/common.cljs b/frontend/src/uxbox/main/data/workspace/common.cljs index aaef1ca470..befd509b94 100644 --- a/frontend/src/uxbox/main/data/workspace/common.cljs +++ b/frontend/src/uxbox/main/data/workspace/common.cljs @@ -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 diff --git a/frontend/src/uxbox/main/data/workspace/transforms.cljs b/frontend/src/uxbox/main/data/workspace/transforms.cljs index 7e53523af2..43b5d61fc7 100644 --- a/frontend/src/uxbox/main/data/workspace/transforms.cljs +++ b/frontend/src/uxbox/main/data/workspace/transforms.cljs @@ -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) diff --git a/frontend/src/uxbox/main/refs.cljs b/frontend/src/uxbox/main/refs.cljs index 4a34220ae1..9693ca4644 100644 --- a/frontend/src/uxbox/main/refs.cljs +++ b/frontend/src/uxbox/main/refs.cljs @@ -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])) diff --git a/frontend/src/uxbox/main/ui/shapes/bounding_box.cljs b/frontend/src/uxbox/main/ui/shapes/bounding_box.cljs index 35ec324862..38b33e4666 100644 --- a/frontend/src/uxbox/main/ui/shapes/bounding_box.cljs +++ b/frontend/src/uxbox/main/ui/shapes/bounding_box.cljs @@ -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}]] + )) + ]))) diff --git a/frontend/src/uxbox/main/ui/workspace/selection.cljs b/frontend/src/uxbox/main/ui/workspace/selection.cljs index db5f85af2d..8e2c702e6f 100644 --- a/frontend/src/uxbox/main/ui/workspace/selection.cljs +++ b/frontend/src/uxbox/main/ui/workspace/selection.cljs @@ -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}]))) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options/.#circle.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options/.#circle.cljs new file mode 120000 index 0000000000..d6a7447944 --- /dev/null +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options/.#circle.cljs @@ -0,0 +1 @@ +alotor@bloodraven.68367:1587963441 \ No newline at end of file diff --git a/frontend/src/uxbox/main/ui/workspace/snap_feedback.cljs b/frontend/src/uxbox/main/ui/workspace/snap_feedback.cljs new file mode 100644 index 0000000000..cdda9bb428 --- /dev/null +++ b/frontend/src/uxbox/main/ui/workspace/snap_feedback.cljs @@ -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}])])))))) + diff --git a/frontend/src/uxbox/util/geom/range_tree.cljs b/frontend/src/uxbox/util/geom/range_tree.cljs new file mode 100644 index 0000000000..72eeae5d80 --- /dev/null +++ b/frontend/src/uxbox/util/geom/range_tree.cljs @@ -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)]} + + + +;; diff --git a/frontend/src/uxbox/util/geom/shapes.cljs b/frontend/src/uxbox/util/geom/shapes.cljs index f0e20c7772..4634615df8 100644 --- a/frontend/src/uxbox/util/geom/shapes.cljs +++ b/frontend/src/uxbox/util/geom/shapes.cljs @@ -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)) diff --git a/frontend/src/uxbox/util/geom/snap.cljs b/frontend/src/uxbox/util/geom/snap.cljs new file mode 100644 index 0000000000..5fb4a3657a --- /dev/null +++ b/frontend/src/uxbox/util/geom/snap.cljs @@ -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))) From 8cbc12ef94a2d2f637d140affdf309844b865176 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 1 May 2020 13:02:42 +0200 Subject: [PATCH 2/3] :sparkles: More functionality to dynamic alignment --- .../uxbox/main/data/workspace/transforms.cljs | 43 ++++++++++--- frontend/src/uxbox/main/refs.cljs | 12 ++++ .../src/uxbox/main/ui/workspace/drawarea.cljs | 14 ++++- .../uxbox/main/ui/workspace/selection.cljs | 43 +++++++------ .../workspace/sidebar/options/.#circle.cljs | 1 - .../main/ui/workspace/snap_feedback.cljs | 61 +++++++++---------- .../src/uxbox/main/ui/workspace/viewport.cljs | 3 + frontend/src/uxbox/util/geom/point.cljs | 12 +++- frontend/src/uxbox/util/geom/shapes.cljs | 10 +++ frontend/src/uxbox/util/geom/snap.cljs | 35 ++++++++--- 10 files changed, 159 insertions(+), 75 deletions(-) delete mode 120000 frontend/src/uxbox/main/ui/workspace/sidebar/options/.#circle.cljs diff --git a/frontend/src/uxbox/main/data/workspace/transforms.cljs b/frontend/src/uxbox/main/data/workspace/transforms.cljs index 43b5d61fc7..e051e236cf 100644 --- a/frontend/src/uxbox/main/data/workspace/transforms.cljs +++ b/frontend/src/uxbox/main/data/workspace/transforms.cljs @@ -65,19 +65,21 @@ :bottom-left [ex sy])] (gpt/point x y))) +(defn finish-transform [state] + (update state :workspace-local dissoc :transform)) + ;; -- RESIZE (defn start-resize [handler ids shape] - (letfn [(resize [shape initial [point lock?]] + (letfn [(resize [shape initial resizing-shapes snap-data [point lock?]] (let [{:keys [width height rotation]} shape - shapev (-> (gpt/point width height)) ;; Vector modifiers depending on the handler handler-modif (let [[x y] (handler-modifiers handler)] (gpt/point x y)) ;; Difference between the origin point in the coordinate system of the rotation - deltav (-> (gpt/subtract point initial) + deltav (-> (snap/closest-snap snap-data resizing-shapes (gpt/to-vec initial point)) (gpt/transform (gmt/rotate-matrix (- rotation))) (gpt/multiply handler-modif)) @@ -115,27 +117,41 @@ ;; (rx/of point))) ] (reify + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc-in [:workspace-local :transform] :resize))) + ptk/WatchEvent (watch [_ state stream] (let [initial (apply-zoom @ms/mouse-position) shape (gsh/shape->rect-shape shape) - 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 (->> ms/mouse-position (rx/map apply-zoom) ;; (rx/mapcat apply-grid-alignment) (rx/with-latest vector ms/mouse-position-ctrl) (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/empty) - (rx/of (apply-modifiers ids)))))))) + (rx/of (apply-modifiers ids) + finish-transform))))))) ;; -- ROTATE (defn start-rotate [shapes] (ptk/reify ::start-rotate + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc-in [:workspace-local :transform] :rotate))) + ptk/WatchEvent (watch [_ state stream] (let [stoper (rx/filter ms/mouse-up? stream) @@ -163,13 +179,19 @@ (let [delta-angle (calculate-angle pos ctrl?)] (set-rotation delta-angle shapes group-center)))) (rx/take-until stoper)) - (rx/of (apply-modifiers (map :id shapes)))))))) + (rx/of (apply-modifiers (map :id shapes)) + finish-transform)))))) ;; -- MOVE (defn start-move-selected [] (ptk/reify ::start-move-selected + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc-in [:workspace-local :transform] :move))) + ptk/WatchEvent (watch [_ state stream] (let [selected (get-in state [:workspace-local :selected]) @@ -192,11 +214,14 @@ (rx/map #(set-modifiers selected {:displacement %})) (rx/tap #(vswap! counter inc)) (rx/take-until stoper)) - (->> (rx/create (fn [sink] (sink @counter))) + (->> (rx/create (fn [sink] (sink (reduced @counter)))) (rx/mapcat (fn [n] (if (zero? n) (rx/empty) - (rx/of (apply-modifiers selected))))))))))) + (rx/of (apply-modifiers selected)))))) + + (rx/of finish-transform) + ))))) (defn- get-displacement-with-grid "Retrieve the correct displacement delta point for the diff --git a/frontend/src/uxbox/main/refs.cljs b/frontend/src/uxbox/main/refs.cljs index 9693ca4644..1f06e87600 100644 --- a/frontend/src/uxbox/main/refs.cljs +++ b/frontend/src/uxbox/main/refs.cljs @@ -93,6 +93,15 @@ (def selected-shapes (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 [id] (l/derived #(contains? % id) selected-shapes)) @@ -105,3 +114,6 @@ (def selected-edition (l/derived :edition workspace-local)) + +(def current-transform + (l/derived :transform workspace-local)) diff --git a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs index 7c43e52f4a..23c07512de 100644 --- a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs +++ b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs @@ -119,8 +119,8 @@ (let [shape (get-in state [:workspace-local :drawing]) shape (geom/setup shape {:x (:x point) :y (:y point) - :width 10 - :height 10})] + :width 1 + :height 1})] (assoc-in state [:workspace-local :drawing] (assoc shape ::initialized? true)))) (resize-shape [{:keys [x y] :as shape} initial point lock?] @@ -280,8 +280,16 @@ (rx/concat (rx/of dw/clear-drawing) (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) + (update :width #(max shape-min-width %)) + (update :height #(max shape-min-height %)) (dissoc shape ::initialized?))] ;; Add & select the created shape to the workspace (rx/of dw/deselect-all diff --git a/frontend/src/uxbox/main/ui/workspace/selection.cljs b/frontend/src/uxbox/main/ui/workspace/selection.cljs index 8e2c702e6f..b67ba6d2a7 100644 --- a/frontend/src/uxbox/main/ui/workspace/selection.cljs +++ b/frontend/src/uxbox/main/ui/workspace/selection.cljs @@ -22,7 +22,6 @@ [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) @@ -97,7 +96,7 @@ zoom (obj/get props "zoom") 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,23 +111,26 @@ :bottom-right [(+ x width) (+ y height)]}] [:g.controls - [: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 :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"}}]) - (for [[position [cx cy]] resize-handlers] - (let [tp (gpt/transform (gpt/point cx cy) transform)] - [:* {:key (name position)} - [:& rotation-handler {:cx (:x tp) - :cy (:y tp) - :position position - :rotation (:rotation shape) - :zoom zoom - :on-mouse-down on-rotate}] + (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) + :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 %) @@ -195,7 +197,6 @@ [{: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))) @@ -207,7 +208,6 @@ :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"}])])) @@ -229,8 +229,7 @@ [:& controls {:shape shape' :zoom zoom :on-rotate on-rotate - :on-resize on-resize}] - [:& snap-feedback {:shapes [shape]}]])) + :on-resize on-resize}]])) (mf/defc selection-handlers [{:keys [selected edition zoom] :as props}] diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options/.#circle.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options/.#circle.cljs deleted file mode 120000 index d6a7447944..0000000000 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/options/.#circle.cljs +++ /dev/null @@ -1 +0,0 @@ -alotor@bloodraven.68367:1587963441 \ No newline at end of file diff --git a/frontend/src/uxbox/main/ui/workspace/snap_feedback.cljs b/frontend/src/uxbox/main/ui/workspace/snap_feedback.cljs index cdda9bb428..f40016eadc 100644 --- a/frontend/src/uxbox/main/ui/workspace/snap_feedback.cljs +++ b/frontend/src/uxbox/main/ui/workspace/snap_feedback.cljs @@ -5,44 +5,43 @@ [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) +(mf/defc snap-feedback [] + (let [selected (mf/deref refs/selected-shapes) + shapes (mf/deref (refs/objects-by-id selected)) + 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}] - 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) + (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}] - [:line {:x1 (:x snap) + :fill line-color}]) + + (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}]]) - - #_(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}])])))))) + :opacity 0.4}])]))))))) diff --git a/frontend/src/uxbox/main/ui/workspace/viewport.cljs b/frontend/src/uxbox/main/ui/workspace/viewport.cljs index c01bbe909b..91a3d81c14 100644 --- a/frontend/src/uxbox/main/ui/workspace/viewport.cljs +++ b/frontend/src/uxbox/main/ui/workspace/viewport.cljs @@ -29,6 +29,7 @@ [uxbox.main.ui.workspace.ruler :refer [ruler]] [uxbox.main.ui.workspace.selection :refer [selection-handlers]] [uxbox.main.ui.workspace.presence :as presence] + [uxbox.main.ui.workspace.snap-feedback :refer [snap-feedback]] [uxbox.util.dom :as dom] [uxbox.util.geom.point :as gpt] [uxbox.util.perf :as perf] @@ -305,6 +306,8 @@ :zoom zoom :edition edition}]) + [:& snap-feedback] + (when-let [drawing-shape (:drawing local)] [:& draw-area {:shape drawing-shape :zoom zoom diff --git a/frontend/src/uxbox/util/geom/point.cljs b/frontend/src/uxbox/util/geom/point.cljs index 0e2d9b7f39..de1b3744d3 100644 --- a/frontend/src/uxbox/util/geom/point.cljs +++ b/frontend/src/uxbox/util/geom/point.cljs @@ -8,8 +8,9 @@ ;; Copyright (c) 2015-2020 Andrey Antukh (ns uxbox.util.geom.point - (:refer-clojure :exclude [divide]) + (:refer-clojure :exclude [divide min max]) (:require + [cljs.core :as c] [cuerdas.core :as str] [uxbox.util.math :as mth] [cognitect.transit :as t])) @@ -70,6 +71,15 @@ (assert (point? other)) (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 [{:keys [x y] :as p}] (assert (point? p)) diff --git a/frontend/src/uxbox/util/geom/shapes.cljs b/frontend/src/uxbox/util/geom/shapes.cljs index 4634615df8..6755f9a3ed 100644 --- a/frontend/src/uxbox/util/geom/shapes.cljs +++ b/frontend/src/uxbox/util/geom/shapes.cljs @@ -738,6 +738,15 @@ (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))))) +(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 [shape] (let [;; Apply modifiers to the rect as a path so we have the end shape expected @@ -785,6 +794,7 @@ (merge rec) (update :x #(mth/precision % 2)) (update :y #(mth/precision % 2)) + (fix-invalid-rect-values) (update :transform #(gmt/multiply (or % (gmt/matrix)) stretch-matrix)) (update :transform-inverse #(gmt/multiply stretch-matrix-inverse (or % (gmt/matrix)))))] diff --git a/frontend/src/uxbox/util/geom/snap.cljs b/frontend/src/uxbox/util/geom/snap.cljs index 5fb4a3657a..4a6c074aa8 100644 --- a/frontend/src/uxbox/util/geom/snap.cljs +++ b/frontend/src/uxbox/util/geom/snap.cljs @@ -16,17 +16,29 @@ [uxbox.util.geom.point :as gpt] [uxbox.util.debug :refer [logjs]])) -(def ^:private snap-accuracy 5) +(def ^:private snap-accuracy 8) (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 shape-snap-points [shape] - (let [modified-path (gsh/transform-apply-modifiers shape) - shape-center (gsh/center modified-path)] - (into #{shape-center} (:segments modified-path)))) + (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 create-coord-data [shapes coord] (let [process-shape @@ -42,7 +54,14 @@ "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))] + 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))] (logjs "snap-data" (mapm (fn [shapes] {:x (create-coord-data shapes :x) :y (create-coord-data shapes :y)}) @@ -125,13 +144,13 @@ (gpt/add trans-vec snapv)))) -(defn get-snap-points [snap-data frame-id shape-id point coord] +(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)) - (remove-from-snap-points #{shape-id})) + (remove-from-snap-points filter-shapes)) snap-points (mapcat (fn [[v data]] (map (fn [[point _]] point) data)) snap-matches)] snap-points)) @@ -139,5 +158,5 @@ (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))] + 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))) From c5dce559cd6b5116f1d51d9373f79e82264fcc77 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 5 May 2020 10:33:14 +0200 Subject: [PATCH 3/3] :sparkles: First dynamic alignment version --- frontend/src/uxbox/main/data/workspace.cljs | 1 - .../src/uxbox/main/data/workspace/common.cljs | 1 - .../uxbox/main/data/workspace/transforms.cljs | 129 ++++++++++++------ frontend/src/uxbox/main/refs.cljs | 3 + .../src/uxbox/main/ui/workspace/drawarea.cljs | 37 +++-- .../uxbox/main/ui/workspace/selection.cljs | 29 ++-- .../main/ui/workspace/snap_feedback.cljs | 77 ++++++----- .../src/uxbox/main/ui/workspace/viewport.cljs | 3 +- frontend/src/uxbox/util/geom/snap.cljs | 110 ++++++++++----- 9 files changed, 255 insertions(+), 135 deletions(-) diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 388261fe1f..02081c30c1 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 befd509b94..69bfbd51cb 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 e051e236cf..54dccc1940 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 1f06e87600..c3d2ff7162 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 23c07512de..d0e6c025a6 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 b67ba6d2a7..073695d656 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 f40016eadc..e87013f036 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 91a3d81c14..5e5fc597fa 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 4a6c074aa8..291abf3f32 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)]