diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 633c0a140..388261fe1 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 aaef1ca47..befd509b9 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 7e53523af..43b5d61fc 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 4a34220ae..9693ca464 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 35ec32486..38b33e466 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 db5f85af2..8e2c702e6 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 000000000..d6a744794 --- /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 000000000..cdda9bb42 --- /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 000000000..72eeae5d8 --- /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 f0e20c777..4634615df 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 000000000..5fb4a3657 --- /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)))