diff --git a/frontend/src/app/main/data/workspace/path.cljs b/frontend/src/app/main/data/workspace/path.cljs index 714775131..c6c1dd2df 100644 --- a/frontend/src/app/main/data/workspace/path.cljs +++ b/frontend/src/app/main/data/workspace/path.cljs @@ -37,3 +37,10 @@ ;; Path tools (d/export tools/make-curve) (d/export tools/make-corner) +(d/export tools/add-node) +(d/export tools/remove-node) +(d/export tools/merge-nodes) +(d/export tools/join-nodes) +(d/export tools/separate-nodes) +(d/export tools/toggle-snap) + diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index 6f9318ebf..feadd1a9e 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -163,7 +163,7 @@ zoom (get-in state [:workspace-local :zoom]) mouse-up (->> stream (rx/filter #(or (helpers/end-path-event? %) (ms/mouse-up? %)))) - drag-events (->> ms/mouse-position + drag-events (->> (streams/position-stream) (rx/take-until mouse-up) (rx/map #(drag-handler %)))] diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index df71229e6..c40c99259 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -126,7 +126,7 @@ (rx/of (selection/select-node position shift?))) ;; This stream checks the consecutive mouse positions to do the draging - (->> ms/mouse-position + (->> (streams/position-stream) (rx/take-until stopper) (rx/map #(move-selected-path-point start-position %))) (rx/of (apply-content-modifiers))) @@ -135,8 +135,7 @@ mouse-click-stream (rx/of (selection/select-node position shift?))] - (streams/drag-stream mouse-drag-stream - mouse-click-stream))))) + (streams/drag-stream mouse-drag-stream mouse-click-stream))))) (defn start-move-handler [index prefix] diff --git a/frontend/src/app/main/data/workspace/path/streams.cljs b/frontend/src/app/main/data/workspace/path/streams.cljs index 5040b0c34..eb061f0b2 100644 --- a/frontend/src/app/main/data/workspace/path/streams.cljs +++ b/frontend/src/app/main/data/workspace/path/streams.cljs @@ -14,7 +14,8 @@ [app.main.store :as st] [app.main.streams :as ms] [beicon.core :as rx] - [potok.core :as ptk])) + [potok.core :as ptk] + [app.common.math :as mth])) (defonce drag-threshold 5) @@ -48,7 +49,15 @@ (->> position-stream (rx/merge-map (fn [] to-stream))))))) +(defn to-dec [num] + (let [k 50] + (* (mth/floor (/ num k)) k))) + (defn position-stream [] (->> ms/mouse-position + ;; TODO: Prueba para el snap + #_(rx/map #(-> % + (update :x to-dec) + (update :y to-dec))) (rx/with-latest merge (->> ms/mouse-position-shift (rx/map #(hash-map :shift? %)))) (rx/with-latest merge (->> ms/mouse-position-alt (rx/map #(hash-map :alt? %)))))) diff --git a/frontend/src/app/main/data/workspace/path/tools.cljs b/frontend/src/app/main/data/workspace/path/tools.cljs index 2085ddf79..445da96cb 100644 --- a/frontend/src/app/main/data/workspace/path/tools.cljs +++ b/frontend/src/app/main/data/workspace/path/tools.cljs @@ -40,3 +40,25 @@ new-content (reduce ugp/make-curve-point (:content shape) selected-points) [rch uch] (changes/generate-path-changes page-id shape (:content shape) new-content)] (rx/of (dwc/commit-changes rch uch {:commit-local? true})))))) + +(defn add-node [] + (ptk/reify ::add-node)) + +(defn remove-node [] + (ptk/reify ::remove-node)) + +(defn merge-nodes [] + (ptk/reify ::merge-nodes)) + +(defn join-nodes [] + (ptk/reify ::join-nodes)) + +(defn separate-nodes [] + (ptk/reify ::separate-nodes)) + +(defn toggle-snap [] + (ptk/reify ::toggle-snap + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state)] + (update-in state [:workspace-local :edit-path id :snap-toggled] not))))) diff --git a/frontend/src/app/main/ui/workspace/shapes/path/actions.cljs b/frontend/src/app/main/ui/workspace/shapes/path/actions.cljs deleted file mode 100644 index e71df0316..000000000 --- a/frontend/src/app/main/ui/workspace/shapes/path/actions.cljs +++ /dev/null @@ -1,44 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) UXBOX Labs SL - -(ns app.main.ui.workspace.shapes.path.actions - (:require - [app.main.data.workspace.path :as drp] - [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.icons :as i] - [app.main.ui.workspace.shapes.path.common :as pc] - [rumext.alpha :as mf])) - -(mf/defc path-actions [{:keys [shape]}] - (let [id (mf/deref refs/selected-edition) - {:keys [edit-mode selected-points snap-toggled] :as all} (mf/deref pc/current-edit-path-ref)] - [:div.path-actions - [:div.viewport-actions-group - [:div.viewport-actions-entry {:class (when (= edit-mode :draw) "is-toggled") - :on-click #(st/emit! (drp/change-edit-mode :draw))} i/pen] - [:div.viewport-actions-entry {:class (when (= edit-mode :move) "is-toggled") - :on-click #(st/emit! (drp/change-edit-mode :move))} i/pointer-inner]] - - [:div.viewport-actions-group - [:div.viewport-actions-entry {:class "is-disabled"} i/nodes-add] - [:div.viewport-actions-entry {:class "is-disabled"} i/nodes-remove]] - - [:div.viewport-actions-group - [:div.viewport-actions-entry {:class "is-disabled"} i/nodes-merge] - [:div.viewport-actions-entry {:class "is-disabled"} i/nodes-join] - [:div.viewport-actions-entry {:class "is-disabled"} i/nodes-separate]] - - [:div.viewport-actions-group - [:div.viewport-actions-entry {:class (when (empty? selected-points) "is-disabled") - :on-click #(when-not (empty? selected-points) - (st/emit! (drp/make-corner)))} i/nodes-corner] - [:div.viewport-actions-entry {:class (when (empty? selected-points) "is-disabled") - :on-click #(when-not (empty? selected-points) - (st/emit! (drp/make-curve)))} i/nodes-curve]] - - [:div.viewport-actions-group - [:div.viewport-actions-entry {:class (when snap-toggled "is-toggled")} i/nodes-snap]]])) diff --git a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs index a459dc836..85fb3ce86 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -187,7 +187,7 @@ (let [point-selected? (contains? selected-points position) point-hover? (contains? hover-points position) last-p? (= last-point position) - start-p? (some? last-point)] + start-p? (not (some? last-point))] [:g.path-node [:g.point-handlers {:pointer-events (when (= edit-mode :draw) "none")} (for [[index prefix] (get handlers position)] diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index bd2ccb43c..c9f3779ab 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -27,6 +27,7 @@ [app.main.ui.workspace.viewport.selection :as selection] [app.main.ui.workspace.viewport.snap-distances :as snap-distances] [app.main.ui.workspace.viewport.snap-points :as snap-points] + [app.main.ui.workspace.viewport.snap-path :as snap-path] [app.main.ui.workspace.viewport.utils :as utils] [app.main.ui.workspace.viewport.widgets :as widgets] [beicon.core :as rx] @@ -282,12 +283,16 @@ :selected selected :page-id page-id}]) + [:& snap-path/snap-path + {:zoom zoom + :edition edition + :edit-path edit-path}] + (when show-cursor-tooltip? [:& widgets/cursor-tooltip {:zoom zoom :tooltip tooltip}]) - (when show-presence? [:& presence/active-cursors {:page-id page-id}]) diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index 82b427730..21c7606c7 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -45,7 +45,9 @@ middle-click? (= 2 (.-which event)) frame? (= :frame type) - selected? (contains? selected id)] + selected? (contains? selected id) + + drawing-path? (= :draw (get-in edit-path [edition :edit-mode]))] (when middle-click? (dom/prevent-default bevent) @@ -60,7 +62,8 @@ (when (and (or (not edition) (not= edition id)) (not blocked) (not hidden) (not (#{:comments :path} drawing-tool))) (not= edition id)) (not blocked) - (not hidden)) + (not hidden) + (not drawing-path?)) (cond drawing-tool (st/emit! (dd/start-drawing drawing-tool)) diff --git a/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs b/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs new file mode 100644 index 000000000..cc0caf131 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs @@ -0,0 +1,174 @@ +;; 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) UXBOX Labs SL + +(ns app.main.ui.workspace.viewport.path-actions + (:require + [app.main.data.workspace.path :as drp] + [app.main.data.workspace.path.helpers :as wph] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.icons :as i] + [app.main.ui.workspace.shapes.path.common :as pc] + [app.util.geom.path :as ugp] + [rumext.alpha :as mf])) + +(defn check-enabled [content selected-points] + (let [segments (ugp/get-segments content selected-points) + + points-selected? (not (empty? selected-points)) + segments-selected? (not (empty? segments))] + {:make-corner points-selected? + :make-curve points-selected? + :add-node segments-selected? + :remove-node points-selected? + :merge-nodes segments-selected? + :join-nodes segments-selected? + :separate-nodes segments-selected?})) + +(mf/defc path-actions [{:keys [shape]}] + (let [id (mf/deref refs/selected-edition) + {:keys [edit-mode selected-points snap-toggled] :as all} (mf/deref pc/current-edit-path-ref) + content (:content shape) + + enabled-buttons + (mf/use-memo + (mf/deps content selected-points) + #(check-enabled content selected-points)) + + on-select-draw-mode + (mf/use-callback + (fn [event] + (st/emit! (drp/change-edit-mode :draw)))) + + on-select-edit-mode + (mf/use-callback + (fn [event] + (st/emit! (drp/change-edit-mode :move)))) + + on-add-node + (mf/use-callback + (mf/deps (:add-node enabled-buttons)) + (fn [event] + (when (:add-node enabled-buttons) + (st/emit! (drp/add-node))))) + + on-remove-node + (mf/use-callback + (mf/deps (:remove-node enabled-buttons)) + (fn [event] + (when (:remove-node enabled-buttons) + (st/emit! (drp/remove-node))))) + + on-merge-nodes + (mf/use-callback + (mf/deps (:merge-nodes enabled-buttons)) + (fn [event] + (when (:merge-nodes enabled-buttons) + (st/emit! (drp/merge-nodes))))) + + on-join-nodes + (mf/use-callback + (mf/deps (:join-nodes enabled-buttons)) + (fn [event] + (when (:join-nodes enabled-buttons) + (st/emit! (drp/join-nodes))))) + + on-separate-nodes + (mf/use-callback + (mf/deps (:separate-nodes enabled-buttons)) + (fn [event] + (when (:separate-nodes enabled-buttons) + (st/emit! (drp/separate-nodes))))) + + on-make-corner + (mf/use-callback + (mf/deps (:make-corner enabled-buttons)) + (fn [event] + (when (:make-corner enabled-buttons) + (st/emit! (drp/make-corner))))) + + on-make-curve + (mf/use-callback + (mf/deps (:make-curve enabled-buttons)) + (fn [event] + (when (:make-curve enabled-buttons) + (st/emit! (drp/make-curve))))) + + on-toggle-snap + (mf/use-callback + (fn [event] + (st/emit! (drp/toggle-snap)))) + + ] + [:div.path-actions + [:div.viewport-actions-group + + ;; Draw Mode + [:div.viewport-actions-entry + {:class (when (= edit-mode :draw) "is-toggled") + :on-click on-select-draw-mode} + i/pen] + + ;; Edit mode + [:div.viewport-actions-entry + {:class (when (= edit-mode :move) "is-toggled") + :on-click on-select-edit-mode} + i/pointer-inner]] + + [:div.viewport-actions-group + ;; Add Node + [:div.viewport-actions-entry + {:class (when-not (:add-node enabled-buttons) "is-disabled") + :on-click on-add-node} + i/nodes-add] + + ;; Remove node + [:div.viewport-actions-entry + {:class (when-not (:remove-node enabled-buttons) "is-disabled") + :on-click on-remove-node} + i/nodes-remove]] + + [:div.viewport-actions-group + ;; Merge Nodes + [:div.viewport-actions-entry + {:class (when-not (:merge-nodes enabled-buttons) "is-disabled") + :on-click on-merge-nodes} + i/nodes-merge] + + ;; Join Nodes + [:div.viewport-actions-entry + {:class (when-not (:join-nodes enabled-buttons) "is-disabled") + :on-click on-join-nodes} + i/nodes-join] + + ;; Separate Nodes + [:div.viewport-actions-entry + {:class (when-not (:separate-nodes enabled-buttons) "is-disabled") + :on-click on-separate-nodes} + i/nodes-separate]] + + ;; Make Corner + [:div.viewport-actions-group + [:div.viewport-actions-entry + {:class (when-not (:make-corner enabled-buttons) "is-disabled") + :on-click on-make-corner} + i/nodes-corner] + + ;; Make Curve + [:div.viewport-actions-entry + {:class (when-not (:make-curve enabled-buttons) "is-disabled") + :on-click on-make-curve} + i/nodes-curve]] + + ;; Toggle snap + [:div.viewport-actions-group + [:div.viewport-actions-entry + {:class (when snap-toggled "is-toggled") + :on-click on-toggle-snap} + i/nodes-snap]]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/snap_path.cljs b/frontend/src/app/main/ui/workspace/viewport/snap_path.cljs new file mode 100644 index 000000000..191756430 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/viewport/snap_path.cljs @@ -0,0 +1,191 @@ +;; 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 app.main.ui.workspace.viewport.snap-path + (:require + #_[app.common.math :as mth] + #_[app.common.data :as d] + #_[app.common.geom.point :as gpt] + #_[app.common.geom.shapes :as gsh] + [app.main.refs :as refs] + #_[app.main.snap :as snap] + #_[app.util.geom.snap-points :as sp] + [app.util.geom.path :as ugp] + #_[beicon.core :as rx] + [rumext.alpha :as mf])) + +#_(def ^:private line-color "#D383DA") +#_(def ^:private line-opacity 0.6) +#_(def ^:private line-width 1) + +;; Configuration for debug +;; (def ^:private line-color "red") +;; (def ^:private line-opacity 1 ) +;; (def ^:private line-width 2) + +#_(mf/defc snap-point + [{:keys [point zoom]}] + (let [{:keys [x y]} point + x (mth/round x) + y (mth/round y) + cross-width (/ 3 zoom)] + [:g + [:line {:x1 (- x cross-width) + :y1 (- y cross-width) + :x2 (+ x cross-width) + :y2 (+ y cross-width) + :style {:stroke line-color :stroke-width (str (/ line-width zoom))}}] + [:line {:x1 (- x cross-width) + :y1 (+ y cross-width) + :x2 (+ x cross-width) + :y2 (- y cross-width) + :style {:stroke line-color :stroke-width (str (/ line-width zoom))}}]])) + +#_(mf/defc snap-line + [{:keys [snap point zoom]}] + [:line {:x1 (mth/round (:x snap)) + :y1 (mth/round (:y snap)) + :x2 (mth/round (:x point)) + :y2 (mth/round (:y point)) + :style {:stroke line-color :stroke-width (str (/ line-width zoom))} + :opacity line-opacity}]) + +#_(defn get-snap + [coord {:keys [shapes page-id filter-shapes modifiers]}] + (let [shape (if (> (count shapes) 1) + (->> shapes (map gsh/transform-shape) gsh/selection-rect (gsh/setup {:type :rect})) + (->> shapes (first))) + + shape (if modifiers + (-> shape (assoc :modifiers modifiers) gsh/transform-shape) + shape) + + frame-id (snap/snap-frame-id shapes)] + + (->> (rx/of shape) + (rx/flat-map (fn [shape] + (->> (sp/shape-snap-points shape) + (map #(vector frame-id %))))) + (rx/flat-map (fn [[frame-id point]] + (->> (snap/get-snap-points page-id frame-id filter-shapes point coord) + (rx/map #(vector point % coord))))) + (rx/reduce conj [])))) + +#_(defn- flip + "Function that reverses the x/y coordinates to their counterpart" + [coord] + (if (= coord :x) :y :x)) + +#_(defn add-point-to-snaps + [[point snaps coord]] + (let [normalize-coord #(assoc % coord (get point coord))] + (cons point (map normalize-coord snaps)))) + + +#_(defn- process-snap-lines + "Gets the snaps for a coordinate and creates lines with a fixed coordinate" + [snaps coord] + (->> snaps + ;; only snap on the `coord` coordinate + (filter #(= (nth % 2) coord)) + ;; we add the point so the line goes from the point to the snap + (mapcat add-point-to-snaps) + ;; We flatten because it's a list of from-to points + (flatten) + ;; Put together the points of the coordinate + (group-by coord) + ;; Keep only the other coordinate + (d/mapm #(map (flip coord) %2)) + ;; Finally get the max/min and this will define the line to draw + (d/mapm #(vector (apply min %2) (apply max %2))) + ;; Change the structure to retrieve a list of lines from/todo + (map (fn [[fixedv [minv maxv]]] [(hash-map coord fixedv (flip coord) minv) + (hash-map coord fixedv (flip coord) maxv)])))) + +#_(mf/defc snap-feedback + [{:keys [shapes page-id filter-shapes zoom modifiers] :as props}] + (let [state (mf/use-state []) + subject (mf/use-memo #(rx/subject)) + + ;; We use sets to store points/lines so there are no points/lines repeated + ;; can cause problems with react keys + snap-points (into #{} (mapcat add-point-to-snaps) @state) + + snap-lines (->> (into (process-snap-lines @state :x) + (process-snap-lines @state :y)) + (into #{}))] + + (mf/use-effect + (fn [] + (let [sub (->> subject + (rx/switch-map #(rx/combine-latest + d/concat + (get-snap :y %) + (get-snap :x %))) + (rx/subs #(let [rs (filter (fn [[_ snaps _]] (> (count snaps) 0)) %)] + (reset! state rs))))] + + ;; On unmount callback + #(rx/dispose! sub)))) + + (mf/use-effect + (mf/deps shapes modifiers) + (fn [] + (rx/push! subject props))) + + [:g.snap-feedback + (for [[from-point to-point] snap-lines] + [:& snap-line {:key (str "line-" (:x from-point) + "-" (:y from-point) + "-" (:x to-point) + "-" (:y to-point) "-") + :snap from-point + :point to-point + :zoom zoom}]) + (for [point snap-points] + [:& snap-point {:key (str "point-" (:x point) + "-" (:y point)) + :point point + :zoom zoom}])])) + +#_(mf/defc snap-points + {::mf/wrap [mf/memo]} + [{:keys [layout zoom selected page-id drawing transform modifiers] :as props}] + (let [shapes (mf/deref (refs/objects-by-id selected)) + filter-shapes (mf/deref refs/selected-shapes-with-children) + filter-shapes (fn [id] + (if (= id :layout) + (or (not (contains? layout :display-grid)) + (not (contains? layout :snap-grid))) + (or (filter-shapes id) + (not (contains? layout :dynamic-alignment))))) + shapes (if drawing [drawing] shapes)] + (when (or drawing transform) + [:& snap-feedback {:shapes shapes + :page-id page-id + :filter-shapes filter-shapes + :zoom zoom + :modifiers modifiers}]))) + +(mf/defc snap-feedback []) + + +(mf/defc snap-path + {::mf/wrap [mf/memo]} + [{:keys [edition edit-path zoom]}] + (let [{:keys [content]} (mf/deref (refs/object-by-id edition)) + {:keys [drag-handler preview snap-toggled]} (get edit-path edition) + + position (or drag-handler + (ugp/command->point preview))] + + (when snap-toggled + [:& snap-feedback {:content content + :position position + :zoom zoom}]))) diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index d810b8a06..6bd73feb8 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -14,7 +14,7 @@ [app.main.store :as st] [app.main.streams :as ms] [app.main.ui.hooks :as hooks] - [app.main.ui.workspace.shapes.path.actions :refer [path-actions]] + [app.main.ui.workspace.viewport.path-actions :refer [path-actions]] [app.util.dom :as dom] [app.util.object :as obj] [rumext.alpha :as mf])) diff --git a/frontend/src/app/util/geom/path.cljs b/frontend/src/app/util/geom/path.cljs index 55f594fb1..452c2d329 100644 --- a/frontend/src/app/util/geom/path.cljs +++ b/frontend/src/app/util/geom/path.cljs @@ -604,3 +604,47 @@ (as-> content $ (reduce redfn $ content-next) (remove-line-curves $)))) + +(defn get-segments + "Given a content and a set of points return all the segments in the path + that uses the points" + [content points] + (let [point-set (set points)] + + (loop [segments [] + prev-point nil + start-point nil + cur-cmd (first content) + content (rest content)] + + (let [;; Close-path makes a segment from the last point to the initial path point + cur-point (if (= :close-path (:command cur-cmd)) + start-point + (command->point cur-cmd)) + + ;; If there is a move-to we don't have a segment + prev-point (if (= :move-to (:command cur-cmd)) + nil + prev-point) + + ;; We update the start point + start-point (if (= :move-to (:command cur-cmd)) + cur-point + start-point) + + is-segment? (and (some? prev-point) + (contains? point-set prev-point) + (contains? point-set cur-point)) + + segments (cond-> segments + is-segment? + (conj [prev-point cur-point]))] + + (if (some? cur-cmd) + (recur segments + cur-point + start-point + (first content) + (rest content)) + + segments)))))