From a06a8c648ecffcff32969a5fdeb5c09be895e1b7 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 30 Mar 2021 17:13:05 +0200 Subject: [PATCH 01/17] :sparkles: Select path nodes in area --- common/app/common/geom/shapes.cljc | 1 + common/app/common/geom/shapes/intersect.cljc | 5 ++ common/app/common/geom/shapes/rect.cljc | 6 ++ .../app/main/data/workspace/drawing/path.cljs | 71 ++++++++++++++++++- .../app/main/data/workspace/selection.cljs | 5 +- .../main/ui/workspace/shapes/path/editor.cljs | 34 +++++---- .../src/app/main/ui/workspace/viewport.cljs | 2 +- .../main/ui/workspace/viewport/actions.cljs | 14 ++-- 8 files changed, 112 insertions(+), 26 deletions(-) diff --git a/common/app/common/geom/shapes.cljc b/common/app/common/geom/shapes.cljc index de6826249d..28e110b736 100644 --- a/common/app/common/geom/shapes.cljc +++ b/common/app/common/geom/shapes.cljc @@ -253,3 +253,4 @@ ;; Intersection (d/export gin/overlaps?) (d/export gin/has-point?) +(d/export gin/has-point-rect?) diff --git a/common/app/common/geom/shapes/intersect.cljc b/common/app/common/geom/shapes/intersect.cljc index 6b9e3f5be8..0b6fbcd6f5 100644 --- a/common/app/common/geom/shapes/intersect.cljc +++ b/common/app/common/geom/shapes/intersect.cljc @@ -285,6 +285,11 @@ (or (not path?) (overlaps-path? shape rect)) (or (not circle?) (overlaps-ellipse? shape rect)))))) +(defn has-point-rect? + [rect point] + (let [lines (gpr/rect->lines rect)] + (is-point-inside-evenodd? point lines))) + (defn has-point? "Check if the shape contains a point" [shape point] diff --git a/common/app/common/geom/shapes/rect.cljc b/common/app/common/geom/shapes/rect.cljc index be221b1d91..91e7d18a9a 100644 --- a/common/app/common/geom/shapes/rect.cljc +++ b/common/app/common/geom/shapes/rect.cljc @@ -19,6 +19,12 @@ (gpt/point (+ x width) (+ y height)) (gpt/point x (+ y height))]) +(defn rect->lines [{:keys [x y width height]}] + [[(gpt/point x y) (gpt/point (+ x width) y)] + [(gpt/point (+ x width) y) (gpt/point (+ x width) (+ y height))] + [(gpt/point (+ x width) (+ y height)) (gpt/point x (+ y height))] + [(gpt/point x (+ y height)) (gpt/point x y)]]) + (defn points->rect [points] (let [minx (transduce gco/map-x-xf min ##Inf points) diff --git a/frontend/src/app/main/data/workspace/drawing/path.cljs b/frontend/src/app/main/data/workspace/drawing/path.cljs index 494d796af2..b0ed2dcac5 100644 --- a/frontend/src/app/main/data/workspace/drawing/path.cljs +++ b/frontend/src/app/main/data/workspace/drawing/path.cljs @@ -732,7 +732,23 @@ (-> state (update-in [:workspace-local :edit-path id :selected-handlers] (fnil conj #{}) [index type])))))) -(defn select-node [position] +(defn select-node-area [shift?] + (ptk/reify ::select-node-area + ptk/UpdateEvent + (update [_ state] + (let [selrect (get-in state [:workspace-local :selrect]) + id (get-in state [:workspace-local :edition]) + content (get-in state (get-path state :content)) + selected-point? (fn [point] + (gsh/has-point-rect? selrect point)) + positions (into #{} + (comp (map (comp gpt/point :params)) + (filter selected-point?)) + content)] + (-> state + (assoc-in [:workspace-local :edit-path id :selected-points] positions)))))) + +(defn select-node [position shift?] (ptk/reify ::select-node ptk/UpdateEvent (update [_ state] @@ -740,7 +756,7 @@ (-> state (assoc-in [:workspace-local :edit-path id :selected-points] #{position})))))) -(defn deselect-node [position] +(defn deselect-node [position shift?] (ptk/reify ::deselect-node ptk/UpdateEvent (update [_ state] @@ -858,3 +874,54 @@ (rx/filter #(= % :interrupt)) (rx/take 1) (rx/map #(stop-path-edit)))))))) + + +(defn update-area-selection + [selrect] + (ptk/reify ::update-area-selection + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-local :selrect] selrect)))) + +(defn clear-area-selection + [] + (ptk/reify ::clear-area-selection + ptk/UpdateEvent + (update [_ state] + (update state :workspace-local dissoc :selrect)))) + +(defn handle-selection + [shift?] + (letfn [(data->selrect [data] + (let [start (:start data) + stop (:stop data) + start-x (min (:x start) (:x stop)) + start-y (min (:y start) (:y stop)) + end-x (max (:x start) (:x stop)) + end-y (max (:y start) (:y stop))] + {:x start-x + :y start-y + :width (mth/abs (- end-x start-x)) + :height (mth/abs (- end-y start-y))}))] + (ptk/reify ::handle-selection + ptk/WatchEvent + (watch [_ state stream] + (let [stop? (fn [event] (or (dwc/interrupt? event) (ms/mouse-up? event))) + stoper (->> stream (rx/filter stop?))] + (rx/concat + #_(when-not preserve? + (rx/of (deselect-all))) + (->> ms/mouse-position + (rx/scan (fn [data pos] + (if data + (assoc data :stop pos) + {:start pos :stop pos})) + nil) + (rx/map data->selrect) + (rx/filter #(or (> (:width %) 10) + (> (:height %) 10))) + (rx/map update-area-selection) + (rx/take-until stoper)) + (rx/of (select-node-area shift?) + (clear-area-selection)) + #_(rx/of (select-shapes-by-current-selrect preserve?)))))))) diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 8761f0a673..6812a5a9fa 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -60,9 +60,8 @@ (ptk/reify ::handle-selection ptk/WatchEvent (watch [_ state stream] - (let [stoper (rx/filter #(or (dwc/interrupt? %) - (ms/mouse-up? %)) - stream)] + (let [stop? (fn [event] (or (dwc/interrupt? event) (ms/mouse-up? event))) + stoper (->> stream (rx/filter stop?))] (rx/concat (when-not preserve? (rx/of (deselect-all))) 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 494761da8c..b743e42700 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -15,7 +15,9 @@ [app.util.dom :as dom] [app.util.geom.path :as ugp] [goog.events :as events] - [rumext.alpha :as mf]) + [rumext.alpha :as mf] + + [app.util.keyboard :as kbd]) (:import goog.events.EventType)) (mf/defc path-point [{:keys [position zoom edit-mode hover? selected? preview? start-path? last-p?]}] @@ -35,12 +37,13 @@ (dom/stop-propagation event) (dom/prevent-default event) - (cond - (and (= edit-mode :move) (not selected?)) - (st/emit! (drp/select-node position)) + (let [shift? (kbd/shift? event)] + (cond + (and (= edit-mode :move) (not selected?)) + (st/emit! (drp/select-node position shift?)) - (and (= edit-mode :move) selected?) - (st/emit! (drp/deselect-node position))))) + (and (= edit-mode :move) selected?) + (st/emit! (drp/deselect-node position shift?)))))) on-mouse-down @@ -177,23 +180,24 @@ last-p (->> content last ugp/command->point) handlers (ugp/content->handlers content) - handle-click-outside - (fn [event] - (let [current (dom/get-target event) - editor-dom (mf/ref-val editor-ref)] - (when-not (or (.contains editor-dom current) - (dom/class? current "viewport-actions-entry")) - (st/emit! (drp/deselect-all))))) + ;;handle-click-outside + ;;(fn [event] + ;; (let [current (dom/get-target event) + ;; editor-dom (mf/ref-val editor-ref)] + ;; (when-not (or (.contains editor-dom current) + ;; (dom/class? current "viewport-actions-entry")) + ;; (st/emit! (drp/deselect-all))))) handle-double-click-outside (fn [event] (when (= edit-mode :move) - (st/emit! :interrupt)))] + (st/emit! :interrupt))) + ] (mf/use-layout-effect (mf/deps edit-mode) (fn [] - (let [keys [(events/listen (dom/get-root) EventType.CLICK handle-click-outside) + (let [keys [;;(events/listen (dom/get-root) EventType.CLICK handle-click-outside) (events/listen (dom/get-root) EventType.DBLCLICK handle-double-click-outside)]] #(doseq [key keys] (events/unlistenByKey key))))) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index cff98f5880..bd2ccb43c2 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -101,7 +101,7 @@ on-click (actions/on-click hover selected edition drawing-path? drawing-tool) on-context-menu (actions/on-context-menu hover) - on-double-click (actions/on-double-click hover hover-ids drawing-path? objects) + on-double-click (actions/on-double-click hover hover-ids drawing-path? objects edition) on-drag-enter (actions/on-drag-enter) on-drag-over (actions/on-drag-over) on-drop (actions/on-drop file viewport-ref zoom) diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index 1d94d938d7..f2d2d203b3 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -15,6 +15,7 @@ [app.main.store :as st] [app.main.streams :as ms] [app.main.ui.workspace.viewport.utils :as utils] + [app.main.data.workspace.drawing.path :as dwdp] [app.util.dom :as dom] [app.util.dom.dnd :as dnd] [app.util.keyboard :as kbd] @@ -57,13 +58,16 @@ (st/emit! dw/clear-edition-mode)) (when (and (or (not edition) (not= edition id)) (not blocked) (not hidden) (not (#{:comments :path} drawing-tool))) + (not= edition id)) + (not blocked) + (not hidden)) (cond drawing-tool (st/emit! (dd/start-drawing drawing-tool)) (and edit-path (contains? edit-path edition)) - ;; Handle node select-drawing. NOP at the moment - nil + ;; Handle path node area selection + (st/emit! (dwdp/handle-selection shift?)) (or (not id) (and frame? (not selected?))) (st/emit! (dw/handle-selection shift?)) @@ -142,9 +146,9 @@ (st/emit! (dw/select-shape (:id @hover))))))))) (defn on-double-click - [hover hover-ids drawing-path? objects] + [hover hover-ids drawing-path? objects edition] (mf/use-callback - (mf/deps @hover @hover-ids drawing-path?) + (mf/deps @hover @hover-ids drawing-path? edition) (fn [event] (dom/stop-propagation event) (let [ctrl? (kbd/ctrl? event) @@ -170,7 +174,7 @@ (reset! hover-ids (into [] (rest @hover-ids))) (st/emit! (dw/select-shape (:id selected)))) - (or text? path?) + (and (not= id edition) (or text? path?)) (st/emit! (dw/select-shape id) (dw/start-editing-selected)) From c22b4a1de2c4c2563ba53552aa6cedde9726cd84 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 31 Mar 2021 09:12:00 +0200 Subject: [PATCH 02/17] :sparkles: Allows multiple selection and move --- .../app/main/data/workspace/drawing/path.cljs | 61 +++++++++---------- .../ui/workspace/shapes/path/actions.cljs | 6 +- .../main/ui/workspace/shapes/path/editor.cljs | 41 ++++++------- frontend/src/app/util/geom/path.cljs | 20 +++++- 4 files changed, 72 insertions(+), 56 deletions(-) diff --git a/frontend/src/app/main/data/workspace/drawing/path.cljs b/frontend/src/app/main/data/workspace/drawing/path.cljs index b0ed2dcac5..94ff198d79 100644 --- a/frontend/src/app/main/data/workspace/drawing/path.cljs +++ b/frontend/src/app/main/data/workspace/drawing/path.cljs @@ -589,44 +589,43 @@ (= mode :draw) (rx/of :interrupt) :else (rx/of (finish-path "changed-content"))))))) -(defn move-path-point [start-point end-point] - (ptk/reify ::move-point - ptk/UpdateEvent - (update [_ state] - (let [id (get-path-id state) - content (get-in state (get-path state :content)) +(defn move-selected-path-point [from-point to-point] + (letfn [(modify-content-point [content {dx :x dy :y} modifiers point] + (let [point-indices (ugp/point-indices content point) ;; [indices] + handler-indices (ugp/handler-indices content point) ;; [[index prefix]] - {dx :x dy :y} (gpt/subtract end-point start-point) + modify-point + (fn [modifiers index] + (-> modifiers + (update index assoc :x dx :y dy))) - handler-indices (-> (ugp/content->handlers content) - (get start-point)) + modify-handler + (fn [modifiers [index prefix]] + (let [cx (d/prefix-keyword prefix :x) + cy (d/prefix-keyword prefix :y)] + (-> modifiers + (update index assoc cx dx cy dy))))] - command-for-point (fn [[index command]] - (let [point (ugp/command->point command)] - (= point start-point))) + (as-> modifiers $ + (reduce modify-point $ point-indices) + (reduce modify-handler $ handler-indices))))] - point-indices (->> (d/enumerate content) - (filter command-for-point) - (map first)) + (ptk/reify ::move-point + ptk/UpdateEvent + (update [_ state] + (let [id (get-path-id state) + content (get-in state (get-path state :content)) + delta (gpt/subtract to-point from-point) + modifiers-reducer (partial modify-content-point content delta) - point-reducer (fn [modifiers index] - (-> modifiers - (assoc-in [index :x] dx) - (assoc-in [index :y] dy))) + points (get-in state [:workspace-local :edit-path id :selected-points] #{}) - handler-reducer (fn [modifiers [index prefix]] - (let [cx (d/prefix-keyword prefix :x) - cy (d/prefix-keyword prefix :y)] - (-> modifiers - (assoc-in [index cx] dx) - (assoc-in [index cy] dy)))) + modifiers (get-in state [:workspace-local :edit-path id :content-modifiers] {}) + modifiers (->> points + (reduce modifiers-reducer {}))] - modifiers (as-> (get-in state [:workspace-local :edit-path id :content-modifiers] {}) $ - (reduce point-reducer $ point-indices) - (reduce handler-reducer $ handler-indices))] - - (assoc-in state [:workspace-local :edit-path id :content-modifiers] modifiers))))) + (assoc-in state [:workspace-local :edit-path id :content-modifiers] modifiers)))))) (defn start-move-path-point [position] @@ -641,7 +640,7 @@ (rx/concat (->> ms/mouse-position (rx/take-until stopper) - (rx/map #(move-path-point position %))) + (rx/map #(move-selected-path-point start-position %))) (rx/of (apply-content-modifiers)))))))) (defn start-move-handler diff --git a/frontend/src/app/main/ui/workspace/shapes/path/actions.cljs b/frontend/src/app/main/ui/workspace/shapes/path/actions.cljs index 94b98119cf..9a030ddebd 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/actions.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/actions.cljs @@ -23,11 +23,11 @@ [: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-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-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]] @@ -40,5 +40,5 @@ :on-click #(when-not (empty? selected-points) (st/emit! (drp/make-curve)))} i/nodes-curve]] - #_[:div.viewport-actions-group + [: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 b743e42700..a30d29f055 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -33,34 +33,32 @@ on-click (fn [event] - (when-not last-p? - (dom/stop-propagation event) - (dom/prevent-default event) + (dom/stop-propagation event) + (dom/prevent-default event) - (let [shift? (kbd/shift? event)] - (cond - (and (= edit-mode :move) (not selected?)) - (st/emit! (drp/select-node position shift?)) + (let [shift? (kbd/shift? event)] + (cond + (and (= edit-mode :move) (not selected?)) + (st/emit! (drp/select-node position shift?)) - (and (= edit-mode :move) selected?) - (st/emit! (drp/deselect-node position shift?)))))) + (and (= edit-mode :move) selected?) + (st/emit! (drp/deselect-node position shift?))))) on-mouse-down (fn [event] - (when-not last-p? - (dom/stop-propagation event) - (dom/prevent-default event) + (dom/stop-propagation event) + (dom/prevent-default event) - (cond - (= edit-mode :move) - (st/emit! (drp/start-move-path-point position)) + (cond + (= edit-mode :move) + (st/emit! (drp/start-move-path-point position)) - (and (= edit-mode :draw) start-path?) - (st/emit! (drp/start-path-from-point position)) + (and (= edit-mode :draw) start-path?) + (st/emit! (drp/start-path-from-point position)) - (and (= edit-mode :draw) (not start-path?)) - (st/emit! (drp/close-path-drag-start position)))))] + (and (= edit-mode :draw) (not start-path?)) + (st/emit! (drp/close-path-drag-start position))))] [:g.path-point [:circle.path-point @@ -80,8 +78,9 @@ :on-mouse-down on-mouse-down :on-mouse-enter on-enter :on-mouse-leave on-leave - :style {:cursor (cond - (and (not last-p?) (= edit-mode :draw)) cur/pen-node + :style {:pointer-events (when last-p? "none") + :cursor (cond + (= edit-mode :draw) cur/pen-node (= edit-mode :move) cur/pointer-node) :fill "transparent"}}]])) diff --git a/frontend/src/app/util/geom/path.cljs b/frontend/src/app/util/geom/path.cljs index 454b2da203..55f594fb11 100644 --- a/frontend/src/app/util/geom/path.cljs +++ b/frontend/src/app/util/geom/path.cljs @@ -468,7 +468,6 @@ [content] (->> (d/with-prev content) (d/enumerate) - (mapcat (fn [[index [cur-cmd pre-cmd]]] (if (and pre-cmd (= :curve-to (:command cur-cmd))) (let [cur-pos (command->point cur-cmd) @@ -480,6 +479,25 @@ (group-by first) (d/mapm #(mapv second %2)))) +(defn point-indices + [content point] + (->> (d/enumerate content) + (filter (fn [[_ cmd]] (= point (command->point cmd)))) + (mapv (fn [[index _]] index)))) + +(defn handler-indices + [content point] + (->> (d/with-prev content) + (d/enumerate) + (mapcat (fn [[index [cur-cmd pre-cmd]]] + (if (and (some? pre-cmd) (= :curve-to (:command cur-cmd))) + (let [cur-pos (command->point cur-cmd) + pre-pos (command->point pre-cmd)] + (cond-> [] + (= pre-pos point) (conj [index :c1]) + (= cur-pos point) (conj [index :c2]))) + []))))) + (defn opposite-index "Calculate sthe opposite index given a prefix and an index" [content index prefix] From 2e6dacf539c2870916466dbe783eccb8e414bafb Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 31 Mar 2021 20:14:51 +0200 Subject: [PATCH 03/17] :recycle: Refactor path into modules --- frontend/src/app/main/data/workspace.cljs | 2 +- .../src/app/main/data/workspace/common.cljs | 33 +- .../src/app/main/data/workspace/drawing.cljs | 2 +- .../app/main/data/workspace/drawing/path.cljs | 926 ------------------ .../src/app/main/data/workspace/path.cljs | 39 + .../app/main/data/workspace/path/changes.cljs | 71 ++ .../app/main/data/workspace/path/common.cljs | 28 + .../app/main/data/workspace/path/drawing.cljs | 337 +++++++ .../app/main/data/workspace/path/edition.cljs | 216 ++++ .../app/main/data/workspace/path/helpers.cljs | 123 +++ .../main/data/workspace/path/selection.cljs | 157 +++ .../app/main/data/workspace/path/spec.cljs | 52 + .../app/main/data/workspace/path/state.cljs | 32 + .../app/main/data/workspace/path/streams.cljs | 54 + .../app/main/data/workspace/path/tools.cljs | 42 + .../ui/workspace/shapes/path/actions.cljs | 2 +- .../main/ui/workspace/shapes/path/editor.cljs | 96 +- .../main/ui/workspace/viewport/actions.cljs | 2 +- 18 files changed, 1213 insertions(+), 1001 deletions(-) delete mode 100644 frontend/src/app/main/data/workspace/drawing/path.cljs create mode 100644 frontend/src/app/main/data/workspace/path.cljs create mode 100644 frontend/src/app/main/data/workspace/path/changes.cljs create mode 100644 frontend/src/app/main/data/workspace/path/common.cljs create mode 100644 frontend/src/app/main/data/workspace/path/drawing.cljs create mode 100644 frontend/src/app/main/data/workspace/path/edition.cljs create mode 100644 frontend/src/app/main/data/workspace/path/helpers.cljs create mode 100644 frontend/src/app/main/data/workspace/path/selection.cljs create mode 100644 frontend/src/app/main/data/workspace/path/spec.cljs create mode 100644 frontend/src/app/main/data/workspace/path/state.cljs create mode 100644 frontend/src/app/main/data/workspace/path/streams.cljs create mode 100644 frontend/src/app/main/data/workspace/path/tools.cljs diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index b5ce81ae24..5b629986d3 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -24,7 +24,7 @@ [app.main.data.messages :as dm] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.drawing :as dwd] - [app.main.data.workspace.drawing.path :as dwdp] + [app.main.data.workspace.path :as dwdp] [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.notifications :as dwn] diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index afec9744c0..e993573bf9 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -360,25 +360,30 @@ (ptk/reify ::undo ptk/WatchEvent (watch [_ state stream] - (let [undo (:workspace-undo state) - items (:items undo) - index (or (:index undo) (dec (count items)))] - (when-not (or (empty? items) (= index -1)) - (let [changes (get-in items [index :undo-changes])] - (rx/of (materialize-undo changes (dec index)) - (commit-changes changes [] {:save-undo? false})))))))) + (let [edition (get-in state [:workspace-local :edition])] + ;; Editors handle their own undo's + (when-not (some? edition) + (let [undo (:workspace-undo state) + items (:items undo) + index (or (:index undo) (dec (count items)))] + (when-not (or (empty? items) (= index -1)) + (let [changes (get-in items [index :undo-changes])] + (rx/of (materialize-undo changes (dec index)) + (commit-changes changes [] {:save-undo? false})))))))))) (def redo (ptk/reify ::redo ptk/WatchEvent (watch [_ state stream] - (let [undo (:workspace-undo state) - items (:items undo) - index (or (:index undo) (dec (count items)))] - (when-not (or (empty? items) (= index (dec (count items)))) - (let [changes (get-in items [(inc index) :redo-changes])] - (rx/of (materialize-undo changes (inc index)) - (commit-changes changes [] {:save-undo? false})))))))) + (let [edition (get-in state [:workspace-local :edition])] + (when-not (some? edition) + (let [undo (:workspace-undo state) + items (:items undo) + index (or (:index undo) (dec (count items)))] + (when-not (or (empty? items) (= index (dec (count items)))) + (let [changes (get-in items [(inc index) :redo-changes])] + (rx/of (materialize-undo changes (inc index)) + (commit-changes changes [] {:save-undo? false})))))))))) (def reinitialize-undo (ptk/reify ::reset-undo diff --git a/frontend/src/app/main/data/workspace/drawing.cljs b/frontend/src/app/main/data/workspace/drawing.cljs index 9c011c373b..553b310384 100644 --- a/frontend/src/app/main/data/workspace/drawing.cljs +++ b/frontend/src/app/main/data/workspace/drawing.cljs @@ -14,8 +14,8 @@ [app.common.uuid :as uuid] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.selection :as dws] + [app.main.data.workspace.path :as path] [app.main.data.workspace.drawing.common :as common] - [app.main.data.workspace.drawing.path :as path] [app.main.data.workspace.drawing.curve :as curve] [app.main.data.workspace.drawing.box :as box])) diff --git a/frontend/src/app/main/data/workspace/drawing/path.cljs b/frontend/src/app/main/data/workspace/drawing/path.cljs deleted file mode 100644 index 94ff198d79..0000000000 --- a/frontend/src/app/main/data/workspace/drawing/path.cljs +++ /dev/null @@ -1,926 +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.data.workspace.drawing.path - (:require - [app.common.data :as d] - [app.common.geom.matrix :as gmt] - [app.common.geom.point :as gpt] - [app.common.geom.shapes :as gsh] - [app.common.geom.shapes.path :as gsp] - [app.common.math :as mth] - [app.common.pages :as cp] - [app.common.spec :as us] - [app.main.data.workspace.common :as dwc] - [app.main.data.workspace.drawing.common :as common] - [app.main.store :as st] - [app.main.streams :as ms] - [app.util.geom.path :as ugp] - [beicon.core :as rx] - [clojure.spec.alpha :as s] - [potok.core :as ptk])) - -;; SCHEMAS - -(s/def ::command #{:move-to - :line-to - :line-to-horizontal - :line-to-vertical - :curve-to - :smooth-curve-to - :quadratic-bezier-curve-to - :smooth-quadratic-bezier-curve-to - :elliptical-arc - :close-path}) - -(s/def :paths.params/x number?) -(s/def :paths.params/y number?) -(s/def :paths.params/c1x number?) -(s/def :paths.params/c1y number?) -(s/def :paths.params/c2x number?) -(s/def :paths.params/c2y number?) - -(s/def ::relative? boolean?) - -(s/def ::params - (s/keys :req-un [:path.params/x - :path.params/y] - :opt-un [:path.params/c1x - :path.params/c1y - :path.params/c2x - :path.params/c2y])) - -(s/def ::content-entry - (s/keys :req-un [::command] - :req-opt [::params - ::relative?])) -(s/def ::content - (s/coll-of ::content-entry :kind vector?)) - - -;; CONSTANTS -(defonce enter-keycode 13) -(defonce drag-threshold 5) - -;; PRIVATE METHODS - -(defn get-path-id - "Retrieves the currently editing path id" - [state] - (or (get-in state [:workspace-local :edition]) - (get-in state [:workspace-drawing :object :id]))) - -(defn get-path - "Retrieves the location of the path object and additionaly can pass - the arguments. This location can be used in get-in, assoc-in... functions" - [state & path] - (let [edit-id (get-in state [:workspace-local :edition]) - page-id (:current-page-id state)] - (d/concat - (if edit-id - [:workspace-data :pages-index page-id :objects edit-id] - [:workspace-drawing :object]) - path))) - -(defn- points->components [shape content] - (let [transform (:transform shape (gmt/matrix)) - transform-inverse (:transform-inverse shape (gmt/matrix)) - center (gsh/center-shape shape) - base-content (gsh/transform-content - content - (gmt/transform-in center transform-inverse)) - - ;; Calculates the new selrect with points given the old center - points (-> (gsh/content->selrect base-content) - (gsh/rect->points) - (gsh/transform-points center (:transform shape (gmt/matrix)))) - - points-center (gsh/center-points points) - - ;; Points is now the selrect but the center is different so we can create the selrect - ;; through points - selrect (-> points - (gsh/transform-points points-center (:transform-inverse shape (gmt/matrix))) - (gsh/points->selrect))] - [points selrect])) - -(defn update-selrect - "Updates the selrect and points for a path" - [shape] - (if (= (:rotation shape 0) 0) - (let [content (:content shape) - selrect (gsh/content->selrect content) - points (gsh/rect->points selrect)] - (assoc shape :points points :selrect selrect)) - - (let [content (:content shape) - [points selrect] (points->components shape content)] - (assoc shape :points points :selrect selrect)))) - -(defn closest-angle [angle] - (cond - (or (> angle 337.5) (<= angle 22.5)) 0 - (and (> angle 22.5) (<= angle 67.5)) 45 - (and (> angle 67.5) (<= angle 112.5)) 90 - (and (> angle 112.5) (<= angle 157.5)) 135 - (and (> angle 157.5) (<= angle 202.5)) 180 - (and (> angle 202.5) (<= angle 247.5)) 225 - (and (> angle 247.5) (<= angle 292.5)) 270 - (and (> angle 292.5) (<= angle 337.5)) 315)) - -(defn position-fixed-angle [point from-point] - (if (and from-point point) - (let [angle (mod (+ 360 (- (gpt/angle point from-point))) 360) - to-angle (closest-angle angle) - distance (gpt/distance point from-point)] - (gpt/angle->point from-point (mth/radians to-angle) distance)) - point)) - -(defn next-node - "Calculates the next-node to be inserted." - [shape position prev-point prev-handler] - (let [last-command (-> shape :content last :command) - add-line? (and prev-point (not prev-handler) (not= last-command :close-path)) - add-curve? (and prev-point prev-handler (not= last-command :close-path))] - (cond - add-line? {:command :line-to - :params position} - add-curve? {:command :curve-to - :params (ugp/make-curve-params position prev-handler)} - :else {:command :move-to - :params position}))) - -(defn append-node - "Creates a new node in the path. Usualy used when drawing." - [shape position prev-point prev-handler] - (let [command (next-node shape position prev-point prev-handler)] - (-> shape - (update :content (fnil conj []) command) - (update-selrect)))) - -(defn move-handler-modifiers [content index prefix match-opposite? dx dy] - (let [[cx cy] (if (= prefix :c1) [:c1x :c1y] [:c2x :c2y]) - [ocx ocy] (if (= prefix :c1) [:c2x :c2y] [:c1x :c1y]) - opposite-index (ugp/opposite-index content index prefix)] - - (cond-> {} - :always - (update index assoc cx dx cy dy) - - (and match-opposite? opposite-index) - (update opposite-index assoc ocx (- dx) ocy (- dy))))) - -(defn end-path-event? [{:keys [type shift] :as event}] - (or (= (ptk/type event) ::finish-path) - (= (ptk/type event) :esc-pressed) - (= event :interrupt) ;; ESC - (and (ms/mouse-double-click? event)))) - -(defn generate-path-changes [page-id shape old-content new-content] - (us/verify ::content old-content) - (us/verify ::content new-content) - (let [shape-id (:id shape) - [old-points old-selrect] (points->components shape old-content) - [new-points new-selrect] (points->components shape new-content) - - rch [{:type :mod-obj - :id shape-id - :page-id page-id - :operations [{:type :set :attr :content :val new-content} - {:type :set :attr :selrect :val new-selrect} - {:type :set :attr :points :val new-points}]} - {:type :reg-objects - :page-id page-id - :shapes [shape-id]}] - - uch [{:type :mod-obj - :id shape-id - :page-id page-id - :operations [{:type :set :attr :content :val old-content} - {:type :set :attr :selrect :val old-selrect} - {:type :set :attr :points :val old-points}]} - {:type :reg-objects - :page-id page-id - :shapes [shape-id]}]] - [rch uch])) - -(defn clean-edit-state - [state] - (dissoc state :last-point :prev-handler :drag-handler :preview)) - -(defn dragging? [start zoom] - (fn [current] - (>= (gpt/distance start current) (/ drag-threshold zoom)))) - -(defn drag-stream [to-stream] - (let [start @ms/mouse-position - zoom (get-in @st/state [:workspace-local :zoom] 1) - mouse-up (->> st/stream (rx/filter #(ms/mouse-up? %)))] - (->> ms/mouse-position - (rx/take-until mouse-up) - (rx/filter (dragging? start zoom)) - (rx/take 1) - (rx/merge-map (fn [] to-stream))))) - -(defn position-stream [] - (->> ms/mouse-position - (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? %)))))) - -;; EVENTS - -(defn init-path [] - (ptk/reify ::init-path)) - -(defn finish-path [source] - (ptk/reify ::finish-path - ptk/UpdateEvent - (update [_ state] - (let [id (get-path-id state)] - (-> state - (update-in [:workspace-local :edit-path id] clean-edit-state)))))) - -(defn preview-next-point [{:keys [x y shift?]}] - (ptk/reify ::preview-next-point - ptk/UpdateEvent - (update [_ state] - (let [id (get-path-id state) - fix-angle? shift? - last-point (get-in state [:workspace-local :edit-path id :last-point]) - position (cond-> (gpt/point x y) - fix-angle? (position-fixed-angle last-point)) - shape (get-in state (get-path state)) - {:keys [last-point prev-handler]} (get-in state [:workspace-local :edit-path id]) - command (next-node shape position last-point prev-handler)] - (assoc-in state [:workspace-local :edit-path id :preview] command))))) - -(defn add-node [{:keys [x y shift?]}] - (ptk/reify ::add-node - ptk/UpdateEvent - (update [_ state] - (let [id (get-path-id state) - fix-angle? shift? - {:keys [last-point prev-handler]} (get-in state [:workspace-local :edit-path id]) - position (cond-> (gpt/point x y) - fix-angle? (position-fixed-angle last-point))] - (if-not (= last-point position) - (-> state - (assoc-in [:workspace-local :edit-path id :last-point] position) - (update-in [:workspace-local :edit-path id] dissoc :prev-handler) - (update-in [:workspace-local :edit-path id] dissoc :preview) - (update-in (get-path state) append-node position last-point prev-handler)) - state))))) - -(defn start-drag-handler [] - (ptk/reify ::start-drag-handler - ptk/UpdateEvent - (update [_ state] - (let [content (get-in state (get-path state :content)) - index (dec (count content)) - command (get-in state (get-path state :content index :command)) - - make-curve - (fn [command] - (let [params (ugp/make-curve-params - (get-in content [index :params]) - (get-in content [(dec index) :params]))] - (-> command - (assoc :command :curve-to :params params))))] - - (cond-> state - (= command :line-to) - (update-in (get-path state :content index) make-curve)))))) - -(defn drag-handler [{:keys [x y alt? shift?]}] - (ptk/reify ::drag-handler - ptk/UpdateEvent - (update [_ state] - (let [id (get-path-id state) - shape (get-in state (get-path state)) - content (:content shape) - index (dec (count content)) - node-position (ugp/command->point (nth content index)) - handler-position (cond-> (gpt/point x y) - shift? (position-fixed-angle node-position)) - {dx :x dy :y} (gpt/subtract handler-position node-position) - match-opposite? (not alt?) - modifiers (move-handler-modifiers content (inc index) :c1 match-opposite? dx dy)] - (-> state - (update-in [:workspace-local :edit-path id :content-modifiers] merge modifiers) - (assoc-in [:workspace-local :edit-path id :prev-handler] handler-position) - (assoc-in [:workspace-local :edit-path id :drag-handler] handler-position)))))) - -(defn finish-drag [] - (ptk/reify ::finish-drag - ptk/UpdateEvent - (update [_ state] - (let [id (get-path-id state) - modifiers (get-in state [:workspace-local :edit-path id :content-modifiers]) - handler (get-in state [:workspace-local :edit-path id :drag-handler])] - (-> state - (update-in (get-path state :content) ugp/apply-content-modifiers modifiers) - (update-in [:workspace-local :edit-path id] dissoc :drag-handler) - (update-in [:workspace-local :edit-path id] dissoc :content-modifiers) - (assoc-in [:workspace-local :edit-path id :prev-handler] handler) - (update-in (get-path state) update-selrect)))) - - ptk/WatchEvent - (watch [_ state stream] - (let [id (get-path-id state) - handler (get-in state [:workspace-local :edit-path id :prev-handler])] - ;; Update the preview because can be outdated after the dragging - (rx/of (preview-next-point handler)))))) - -(declare close-path-drag-end) - -(defn close-path-drag-start [position] - (ptk/reify ::close-path-drag-start - ptk/WatchEvent - (watch [_ state stream] - (let [id (get-path-id state) - zoom (get-in state [:workspace-local :zoom]) - start-position @ms/mouse-position - - stop-stream - (->> stream (rx/filter #(or (end-path-event? %) - (ms/mouse-up? %)))) - - drag-events-stream - (->> (position-stream) - (rx/take-until stop-stream) - (rx/map #(drag-handler %)))] - - (rx/concat - (rx/of (add-node position)) - (drag-stream - (rx/concat - (rx/of (start-drag-handler)) - drag-events-stream - (rx/of (finish-drag)) - (rx/of (close-path-drag-end)))) - (rx/of (finish-path "close-path"))))))) - -(defn close-path-drag-end [] - (ptk/reify ::close-path-drag-end - ptk/UpdateEvent - (update [_ state] - (let [id (get-path-id state)] - (update-in state [:workspace-local :edit-path id] dissoc :prev-handler))))) - -(defn path-pointer-enter [position] - (ptk/reify ::path-pointer-enter - ptk/UpdateEvent - (update [_ state] - (let [id (get-path-id state)] - (update-in state [:workspace-local :edit-path id :hover-points] (fnil conj #{}) position))))) - -(defn path-pointer-leave [position] - (ptk/reify ::path-pointer-leave - ptk/UpdateEvent - (update [_ state] - (let [id (get-path-id state)] - (update-in state [:workspace-local :edit-path id :hover-points] disj position))))) - -(defn start-path-from-point [position] - (ptk/reify ::start-path-from-point - ptk/WatchEvent - (watch [_ state stream] - (let [start-point @ms/mouse-position - zoom (get-in state [:workspace-local :zoom]) - mouse-up (->> stream (rx/filter #(or (end-path-event? %) - (ms/mouse-up? %)))) - drag-events (->> ms/mouse-position - (rx/take-until mouse-up) - (rx/map #(drag-handler %)))] - - (rx/concat - (rx/of (add-node position)) - (drag-stream - (rx/concat - (rx/of (start-drag-handler)) - drag-events - (rx/of (finish-drag))))))))) - -(defn make-corner [] - (ptk/reify ::make-corner - ptk/WatchEvent - (watch [_ state stream] - (let [id (get-path-id state) - page-id (:current-page-id state) - shape (get-in state (get-path state)) - selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) - new-content (reduce ugp/make-corner-point (:content shape) selected-points) - [rch uch] (generate-path-changes page-id shape (:content shape) new-content)] - (rx/of (dwc/commit-changes rch uch {:commit-local? true})))))) - -(defn make-curve [] - (ptk/reify ::make-curve - ptk/WatchEvent - (watch [_ state stream] - (let [id (get-path-id state) - page-id (:current-page-id state) - shape (get-in state (get-path state)) - selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) - new-content (reduce ugp/make-curve-point (:content shape) selected-points) - [rch uch] (generate-path-changes page-id shape (:content shape) new-content)] - (rx/of (dwc/commit-changes rch uch {:commit-local? true})))))) - -(defn path-handler-enter [index prefix] - (ptk/reify ::path-handler-enter - ptk/UpdateEvent - (update [_ state] - (let [id (get-path-id state)] - (update-in state [:workspace-local :edit-path id :hover-handlers] (fnil conj #{}) [index prefix]))))) - -(defn path-handler-leave [index prefix] - (ptk/reify ::path-handler-leave - ptk/UpdateEvent - (update [_ state] - (let [id (get-path-id state)] - (update-in state [:workspace-local :edit-path id :hover-handlers] disj [index prefix]))))) - -;; EVENT STREAMS - -(defn make-drag-stream - [stream down-event zoom] - (let [mouse-up (->> stream (rx/filter #(or (end-path-event? %) - (ms/mouse-up? %)))) - drag-events (->> (position-stream) - (rx/take-until mouse-up) - (rx/map #(drag-handler %)))] - - (rx/concat - (rx/of (add-node down-event)) - (drag-stream - (rx/concat - (rx/of (start-drag-handler)) - drag-events - (rx/of (finish-drag))))))) - -(defn make-node-events-stream - [stream] - (->> stream - (rx/filter (ptk/type? ::close-path-drag-start)) - (rx/take 1) - (rx/merge-map #(rx/empty)))) - -;; MAIN ENTRIES - -(defn handle-drawing-path - [id] - (ptk/reify ::handle-drawing-path - ptk/UpdateEvent - (update [_ state] - (let [id (get-path-id state)] - (-> state - (assoc-in [:workspace-local :edit-path id :edit-mode] :draw)))) - - ptk/WatchEvent - (watch [_ state stream] - (let [zoom (get-in state [:workspace-local :zoom]) - mouse-down (->> stream (rx/filter ms/mouse-down?)) - end-path-events (->> stream (rx/filter end-path-event?)) - - ;; Mouse move preview - mousemove-events - (->> (position-stream) - (rx/take-until end-path-events) - (rx/map #(preview-next-point %))) - - ;; From mouse down we can have: click, drag and double click - mousedown-events - (->> mouse-down - (rx/take-until end-path-events) - (rx/with-latest merge (position-stream)) - - ;; We change to the stream that emits the first event - (rx/switch-map - #(rx/race (make-node-events-stream stream) - (make-drag-stream stream % zoom))))] - - (rx/concat - (rx/of (init-path)) - (rx/merge mousemove-events - mousedown-events) - (rx/of (finish-path "after-events"))))))) - - - -(defn modify-point [index prefix dx dy] - (ptk/reify ::modify-point - ptk/UpdateEvent - (update [_ state] - (let [id (get-in state [:workspace-local :edition]) - [cx cy] (if (= prefix :c1) [:c1x :c1y] [:c2x :c2y])] - (-> state - (update-in [:workspace-local :edit-path id :content-modifiers (inc index)] assoc - :c1x dx :c1y dy) - (update-in [:workspace-local :edit-path id :content-modifiers index] assoc - :x dx :y dy :c2x dx :c2y dy)))))) - -(defn modify-handler [id index prefix dx dy match-opposite?] - (ptk/reify ::modify-point - ptk/UpdateEvent - (update [_ state] - (let [content (get-in state (get-path state :content)) - [cx cy] (if (= prefix :c1) [:c1x :c1y] [:c2x :c2y]) - [ocx ocy] (if (= prefix :c1) [:c2x :c2y] [:c1x :c1y]) - opposite-index (ugp/opposite-index content index prefix)] - (cond-> state - :always - (update-in [:workspace-local :edit-path id :content-modifiers index] assoc - cx dx cy dy) - - (and match-opposite? opposite-index) - (update-in [:workspace-local :edit-path id :content-modifiers opposite-index] assoc - ocx (- dx) ocy (- dy))))))) - -(defn apply-content-modifiers [] - (ptk/reify ::apply-content-modifiers - ptk/WatchEvent - (watch [_ state stream] - (let [id (get-path-id state) - page-id (:current-page-id state) - shape (get-in state (get-path state)) - content-modifiers (get-in state [:workspace-local :edit-path id :content-modifiers]) - new-content (ugp/apply-content-modifiers (:content shape) content-modifiers) - [rch uch] (generate-path-changes page-id shape (:content shape) new-content)] - - (rx/of (dwc/commit-changes rch uch {:commit-local? true}) - (fn [state] (update-in state [:workspace-local :edit-path id] dissoc :content-modifiers))))))) - -(defn save-path-content [] - (ptk/reify ::save-path-content - ptk/UpdateEvent - (update [_ state] - (let [content (get-in state (get-path state :content)) - content (if (= (-> content last :command) :move-to) - (into [] (take (dec (count content)) content)) - content)] - (assoc-in state (get-path state :content) content))) - - ptk/WatchEvent - (watch [_ state stream] - (let [id (get-in state [:workspace-local :edition]) - old-content (get-in state [:workspace-local :edit-path id :old-content])] - (if (some? old-content) - (let [shape (get-in state (get-path state)) - page-id (:current-page-id state) - [rch uch] (generate-path-changes page-id shape old-content (:content shape))] - (rx/of (dwc/commit-changes rch uch {:commit-local? true}))) - (rx/empty)))))) - -(declare start-draw-mode) -(defn check-changed-content [] - (ptk/reify ::check-changed-content - ptk/WatchEvent - (watch [_ state stream] - (let [id (get-path-id state) - content (get-in state (get-path state :content)) - old-content (get-in state [:workspace-local :edit-path id :old-content]) - mode (get-in state [:workspace-local :edit-path id :edit-mode])] - - (cond - (not= content old-content) (rx/of (save-path-content) - (start-draw-mode)) - (= mode :draw) (rx/of :interrupt) - :else (rx/of (finish-path "changed-content"))))))) - -(defn move-selected-path-point [from-point to-point] - (letfn [(modify-content-point [content {dx :x dy :y} modifiers point] - (let [point-indices (ugp/point-indices content point) ;; [indices] - handler-indices (ugp/handler-indices content point) ;; [[index prefix]] - - modify-point - (fn [modifiers index] - (-> modifiers - (update index assoc :x dx :y dy))) - - modify-handler - (fn [modifiers [index prefix]] - (let [cx (d/prefix-keyword prefix :x) - cy (d/prefix-keyword prefix :y)] - (-> modifiers - (update index assoc cx dx cy dy))))] - - (as-> modifiers $ - (reduce modify-point $ point-indices) - (reduce modify-handler $ handler-indices))))] - - (ptk/reify ::move-point - ptk/UpdateEvent - (update [_ state] - (let [id (get-path-id state) - content (get-in state (get-path state :content)) - delta (gpt/subtract to-point from-point) - - modifiers-reducer (partial modify-content-point content delta) - - points (get-in state [:workspace-local :edit-path id :selected-points] #{}) - - modifiers (get-in state [:workspace-local :edit-path id :content-modifiers] {}) - modifiers (->> points - (reduce modifiers-reducer {}))] - - (assoc-in state [:workspace-local :edit-path id :content-modifiers] modifiers)))))) - -(defn start-move-path-point - [position] - (ptk/reify ::start-move-path-point - ptk/WatchEvent - (watch [_ state stream] - (let [start-position @ms/mouse-position - stopper (->> stream (rx/filter ms/mouse-up?)) - zoom (get-in state [:workspace-local :zoom])] - - (drag-stream - (rx/concat - (->> ms/mouse-position - (rx/take-until stopper) - (rx/map #(move-selected-path-point start-position %))) - (rx/of (apply-content-modifiers)))))))) - -(defn start-move-handler - [index prefix] - (ptk/reify ::start-move-handler - ptk/WatchEvent - (watch [_ state stream] - (let [id (get-in state [:workspace-local :edition]) - cx (d/prefix-keyword prefix :x) - cy (d/prefix-keyword prefix :y) - start-point @ms/mouse-position - modifiers (get-in state [:workspace-local :edit-path id :content-modifiers]) - start-delta-x (get-in modifiers [index cx] 0) - start-delta-y (get-in modifiers [index cy] 0) - - content (get-in state (get-path state :content)) - opposite-index (ugp/opposite-index content index prefix) - opposite-prefix (if (= prefix :c1) :c2 :c1) - opposite-handler (-> content (get opposite-index) (ugp/get-handler opposite-prefix)) - - point (-> content (get (if (= prefix :c1) (dec index) index)) (ugp/command->point)) - handler (-> content (get index) (ugp/get-handler prefix)) - - current-distance (when opposite-handler (gpt/distance (ugp/opposite-handler point handler) opposite-handler)) - match-opposite? (and opposite-handler (mth/almost-zero? current-distance))] - - (drag-stream - (rx/concat - (->> (position-stream) - (rx/take-until (->> stream (rx/filter ms/mouse-up?))) - (rx/map - (fn [{:keys [x y alt? shift?]}] - (let [pos (cond-> (gpt/point x y) - shift? (position-fixed-angle point))] - (modify-handler - id - index - prefix - (+ start-delta-x (- (:x pos) (:x start-point))) - (+ start-delta-y (- (:y pos) (:y start-point))) - (and (not alt?) match-opposite?)))))) - (rx/concat (rx/of (apply-content-modifiers))))))))) - -(defn start-draw-mode [] - (ptk/reify ::start-draw-mode - ptk/UpdateEvent - (update [_ state] - (let [id (get-in state [:workspace-local :edition]) - page-id (:current-page-id state) - old-content (get-in state [:workspace-data :pages-index page-id :objects id :content])] - (-> state - (assoc-in [:workspace-local :edit-path id :old-content] old-content)))) - - ptk/WatchEvent - (watch [_ state stream] - (let [id (get-in state [:workspace-local :edition]) - edit-mode (get-in state [:workspace-local :edit-path id :edit-mode])] - (if (= :draw edit-mode) - (rx/concat - (rx/of (handle-drawing-path id)) - (->> stream - (rx/filter (ptk/type? ::finish-path)) - (rx/take 1) - (rx/merge-map #(rx/of (check-changed-content))))) - (rx/empty)))))) - -(defn change-edit-mode [mode] - (ptk/reify ::change-edit-mode - ptk/UpdateEvent - (update [_ state] - (let [id (get-in state [:workspace-local :edition])] - (cond-> state - id (assoc-in [:workspace-local :edit-path id :edit-mode] mode)))) - - ptk/WatchEvent - (watch [_ state stream] - (let [id (get-path-id state)] - (cond - (and id (= :move mode)) (rx/of (finish-path "change-edit-mode")) - (and id (= :draw mode)) (rx/of (start-draw-mode)) - :else (rx/empty)))))) - -(defn select-handler [index type] - (ptk/reify ::select-handler - ptk/UpdateEvent - (update [_ state] - (let [id (get-in state [:workspace-local :edition])] - (-> state - (update-in [:workspace-local :edit-path id :selected-handlers] (fnil conj #{}) [index type])))))) - -(defn select-node-area [shift?] - (ptk/reify ::select-node-area - ptk/UpdateEvent - (update [_ state] - (let [selrect (get-in state [:workspace-local :selrect]) - id (get-in state [:workspace-local :edition]) - content (get-in state (get-path state :content)) - selected-point? (fn [point] - (gsh/has-point-rect? selrect point)) - positions (into #{} - (comp (map (comp gpt/point :params)) - (filter selected-point?)) - content)] - (-> state - (assoc-in [:workspace-local :edit-path id :selected-points] positions)))))) - -(defn select-node [position shift?] - (ptk/reify ::select-node - ptk/UpdateEvent - (update [_ state] - (let [id (get-in state [:workspace-local :edition])] - (-> state - (assoc-in [:workspace-local :edit-path id :selected-points] #{position})))))) - -(defn deselect-node [position shift?] - (ptk/reify ::deselect-node - ptk/UpdateEvent - (update [_ state] - (let [id (get-in state [:workspace-local :edition])] - (-> state - (update-in [:workspace-local :edit-path id :selected-points] (fnil disj #{}) position)))))) - -(defn add-to-selection-handler [index type] - (ptk/reify ::add-to-selection-handler - ptk/UpdateEvent - (update [_ state] - state))) - -(defn add-to-selection-node [index] - (ptk/reify ::add-to-selection-node - ptk/UpdateEvent - (update [_ state] - state))) - -(defn remove-from-selection-handler [index] - (ptk/reify ::remove-from-selection-handler - ptk/UpdateEvent - (update [_ state] - state))) - -(defn remove-from-selection-node [index] - (ptk/reify ::remove-from-selection-handler - ptk/UpdateEvent - (update [_ state] - state))) - -(defn deselect-all [] - (ptk/reify ::deselect-all - ptk/UpdateEvent - (update [_ state] - (let [id (get-path-id state)] - (-> state - (assoc-in [:workspace-local :edit-path id :selected-handlers] #{}) - (assoc-in [:workspace-local :edit-path id :selected-points] #{})))))) - -(defn setup-frame-path [] - (ptk/reify ::setup-frame-path - ptk/UpdateEvent - (update [_ state] - - (let [objects (dwc/lookup-page-objects state) - content (get-in state [:workspace-drawing :object :content] []) - position (get-in content [0 :params] nil) - frame-id (cp/frame-id-by-position objects position)] - (-> state - (assoc-in [:workspace-drawing :object :frame-id] frame-id)))))) - -(defn handle-new-shape-result [shape-id] - (ptk/reify ::handle-new-shape-result - ptk/UpdateEvent - (update [_ state] - (let [content (get-in state [:workspace-drawing :object :content] [])] - (us/verify ::content content) - (if (> (count content) 1) - (assoc-in state [:workspace-drawing :object :initialized?] true) - state))) - - ptk/WatchEvent - (watch [_ state stream] - (->> (rx/of (setup-frame-path) - common/handle-finish-drawing - (dwc/start-edition-mode shape-id) - (change-edit-mode :draw)))))) - -(defn handle-new-shape - "Creates a new path shape" - [] - (ptk/reify ::handle-new-shape - ptk/WatchEvent - (watch [_ state stream] - (let [shape-id (get-in state [:workspace-drawing :object :id])] - (rx/concat - (rx/of (handle-drawing-path shape-id)) - (->> stream - (rx/filter (ptk/type? ::finish-path)) - (rx/take 1) - (rx/observe-on :async) - (rx/map #(handle-new-shape-result shape-id)))))))) - -(defn stop-path-edit [] - (ptk/reify ::stop-path-edit - ptk/UpdateEvent - (update [_ state] - (let [id (get-in state [:workspace-local :edition])] - (update state :workspace-local dissoc :edit-path id))))) - -(defn start-path-edit - [id] - (ptk/reify ::start-path-edit - ptk/UpdateEvent - (update [_ state] - (let [edit-path (get-in state [:workspace-local :edit-path id])] - - (cond-> state - (or (not edit-path) (= :draw (:edit-mode edit-path))) - (assoc-in [:workspace-local :edit-path id] {:edit-mode :move - :selected #{} - :snap-toggled true}) - - (and (some? edit-path) (= :move (:edit-mode edit-path))) - (assoc-in [:workspace-local :edit-path id :edit-mode] :draw)))) - - ptk/WatchEvent - (watch [_ state stream] - (let [mode (get-in state [:workspace-local :edit-path id :edit-mode])] - (rx/concat - (rx/of (change-edit-mode mode)) - (->> stream - (rx/take-until (->> stream (rx/filter (ptk/type? ::start-path-edit)))) - (rx/filter #(= % :interrupt)) - (rx/take 1) - (rx/map #(stop-path-edit)))))))) - - -(defn update-area-selection - [selrect] - (ptk/reify ::update-area-selection - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-local :selrect] selrect)))) - -(defn clear-area-selection - [] - (ptk/reify ::clear-area-selection - ptk/UpdateEvent - (update [_ state] - (update state :workspace-local dissoc :selrect)))) - -(defn handle-selection - [shift?] - (letfn [(data->selrect [data] - (let [start (:start data) - stop (:stop data) - start-x (min (:x start) (:x stop)) - start-y (min (:y start) (:y stop)) - end-x (max (:x start) (:x stop)) - end-y (max (:y start) (:y stop))] - {:x start-x - :y start-y - :width (mth/abs (- end-x start-x)) - :height (mth/abs (- end-y start-y))}))] - (ptk/reify ::handle-selection - ptk/WatchEvent - (watch [_ state stream] - (let [stop? (fn [event] (or (dwc/interrupt? event) (ms/mouse-up? event))) - stoper (->> stream (rx/filter stop?))] - (rx/concat - #_(when-not preserve? - (rx/of (deselect-all))) - (->> ms/mouse-position - (rx/scan (fn [data pos] - (if data - (assoc data :stop pos) - {:start pos :stop pos})) - nil) - (rx/map data->selrect) - (rx/filter #(or (> (:width %) 10) - (> (:height %) 10))) - (rx/map update-area-selection) - (rx/take-until stoper)) - (rx/of (select-node-area shift?) - (clear-area-selection)) - #_(rx/of (select-shapes-by-current-selrect preserve?)))))))) diff --git a/frontend/src/app/main/data/workspace/path.cljs b/frontend/src/app/main/data/workspace/path.cljs new file mode 100644 index 0000000000..7147751315 --- /dev/null +++ b/frontend/src/app/main/data/workspace/path.cljs @@ -0,0 +1,39 @@ +;; 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.data.workspace.path + (:require + [app.common.data :as d] + [app.main.data.workspace.path.drawing :as drawing] + [app.main.data.workspace.path.edition :as edition] + [app.main.data.workspace.path.selection :as selection] + [app.main.data.workspace.path.tools :as tools])) + +;; Drawing +(d/export drawing/handle-new-shape) +(d/export drawing/start-path-from-point) +(d/export drawing/close-path-drag-start) +(d/export drawing/change-edit-mode) + +;; Edition +(d/export edition/start-move-handler) +(d/export edition/start-move-path-point) +(d/export edition/start-path-edit) + +;; Selection +(d/export selection/select-handler) +(d/export selection/handle-selection) +(d/export selection/path-handler-enter) +(d/export selection/path-handler-leave) +(d/export selection/path-pointer-enter) +(d/export selection/path-pointer-leave) + +;; Path tools +(d/export tools/make-curve) +(d/export tools/make-corner) diff --git a/frontend/src/app/main/data/workspace/path/changes.cljs b/frontend/src/app/main/data/workspace/path/changes.cljs new file mode 100644 index 0000000000..53fdb6c9fc --- /dev/null +++ b/frontend/src/app/main/data/workspace/path/changes.cljs @@ -0,0 +1,71 @@ +;; 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.data.workspace.path.changes + (:require + [app.common.spec :as us] + [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.path.helpers :as helpers] + [app.main.data.workspace.path.spec :as spec] + [app.main.data.workspace.path.state :as st] + [beicon.core :as rx] + [potok.core :as ptk])) + +(defn generate-path-changes + "Generates content changes and the undos for the content given" + [page-id shape old-content new-content] + (us/verify ::spec/content old-content) + (us/verify ::spec/content new-content) + (let [shape-id (:id shape) + [old-points old-selrect] (helpers/content->points+selrect shape old-content) + [new-points new-selrect] (helpers/content->points+selrect shape new-content) + + rch [{:type :mod-obj + :id shape-id + :page-id page-id + :operations [{:type :set :attr :content :val new-content} + {:type :set :attr :selrect :val new-selrect} + {:type :set :attr :points :val new-points}]} + {:type :reg-objects + :page-id page-id + :shapes [shape-id]}] + + uch [{:type :mod-obj + :id shape-id + :page-id page-id + :operations [{:type :set :attr :content :val old-content} + {:type :set :attr :selrect :val old-selrect} + {:type :set :attr :points :val old-points}]} + {:type :reg-objects + :page-id page-id + :shapes [shape-id]}]] + [rch uch])) + +(defn save-path-content [] + (ptk/reify ::save-path-content + ptk/UpdateEvent + (update [_ state] + (let [content (get-in state (st/get-path state :content)) + content (if (= (-> content last :command) :move-to) + (into [] (take (dec (count content)) content)) + content)] + (assoc-in state (st/get-path state :content) content))) + + ptk/WatchEvent + (watch [_ state stream] + (let [id (get-in state [:workspace-local :edition]) + old-content (get-in state [:workspace-local :edit-path id :old-content])] + (if (some? old-content) + (let [shape (get-in state (st/get-path state)) + page-id (:current-page-id state) + [rch uch] (generate-path-changes page-id shape old-content (:content shape))] + (rx/of (dwc/commit-changes rch uch {:commit-local? true}))) + (rx/empty)))))) + + diff --git a/frontend/src/app/main/data/workspace/path/common.cljs b/frontend/src/app/main/data/workspace/path/common.cljs new file mode 100644 index 0000000000..0d28bf984f --- /dev/null +++ b/frontend/src/app/main/data/workspace/path/common.cljs @@ -0,0 +1,28 @@ +;; 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.data.workspace.path.common + (:require + [app.main.data.workspace.path.state :as st] + [potok.core :as ptk])) + +(defn init-path [] + (ptk/reify ::init-path)) + +(defn clean-edit-state + [state] + (dissoc state :last-point :prev-handler :drag-handler :preview)) + +(defn finish-path [source] + (ptk/reify ::finish-path + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state)] + (-> state + (update-in [:workspace-local :edit-path id] clean-edit-state)))))) diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs new file mode 100644 index 0000000000..6f9318ebf2 --- /dev/null +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -0,0 +1,337 @@ +;; 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.data.workspace.path.drawing + (:require + [app.common.geom.point :as gpt] + [app.common.pages :as cp] + [app.common.spec :as us] + [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.drawing.common :as dwdc] + [app.main.data.workspace.path.changes :as changes] + [app.main.data.workspace.path.common :as common] + [app.main.data.workspace.path.helpers :as helpers] + [app.main.data.workspace.path.spec :as spec] + [app.main.data.workspace.path.state :as st] + [app.main.data.workspace.path.streams :as streams] + [app.main.data.workspace.path.tools :as tools] + [app.main.streams :as ms] + [app.util.geom.path :as ugp] + [beicon.core :as rx] + [potok.core :as ptk])) + +(declare change-edit-mode) + +(defn preview-next-point [{:keys [x y shift?]}] + (ptk/reify ::preview-next-point + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state) + fix-angle? shift? + last-point (get-in state [:workspace-local :edit-path id :last-point]) + position (cond-> (gpt/point x y) + fix-angle? (helpers/position-fixed-angle last-point)) + shape (get-in state (st/get-path state)) + {:keys [last-point prev-handler]} (get-in state [:workspace-local :edit-path id]) + command (helpers/next-node shape position last-point prev-handler)] + (assoc-in state [:workspace-local :edit-path id :preview] command))))) + +(defn add-node [{:keys [x y shift?]}] + (ptk/reify ::add-node + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state) + fix-angle? shift? + {:keys [last-point prev-handler]} (get-in state [:workspace-local :edit-path id]) + position (cond-> (gpt/point x y) + fix-angle? (helpers/position-fixed-angle last-point))] + (if-not (= last-point position) + (-> state + (assoc-in [:workspace-local :edit-path id :last-point] position) + (update-in [:workspace-local :edit-path id] dissoc :prev-handler) + (update-in [:workspace-local :edit-path id] dissoc :preview) + (update-in (st/get-path state) helpers/append-node position last-point prev-handler)) + state))))) + +(defn start-drag-handler [] + (ptk/reify ::start-drag-handler + ptk/UpdateEvent + (update [_ state] + (let [content (get-in state (st/get-path state :content)) + index (dec (count content)) + command (get-in state (st/get-path state :content index :command)) + + make-curve + (fn [command] + (let [params (ugp/make-curve-params + (get-in content [index :params]) + (get-in content [(dec index) :params]))] + (-> command + (assoc :command :curve-to :params params))))] + + (cond-> state + (= command :line-to) + (update-in (st/get-path state :content index) make-curve)))))) + +(defn drag-handler [{:keys [x y alt? shift?]}] + (ptk/reify ::drag-handler + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state) + shape (get-in state (st/get-path state)) + content (:content shape) + index (dec (count content)) + node-position (ugp/command->point (nth content index)) + handler-position (cond-> (gpt/point x y) + shift? (helpers/position-fixed-angle node-position)) + {dx :x dy :y} (gpt/subtract handler-position node-position) + match-opposite? (not alt?) + modifiers (helpers/move-handler-modifiers content (inc index) :c1 match-opposite? dx dy)] + (-> state + (update-in [:workspace-local :edit-path id :content-modifiers] merge modifiers) + (assoc-in [:workspace-local :edit-path id :prev-handler] handler-position) + (assoc-in [:workspace-local :edit-path id :drag-handler] handler-position)))))) + +(defn finish-drag [] + (ptk/reify ::finish-drag + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state) + modifiers (get-in state [:workspace-local :edit-path id :content-modifiers]) + handler (get-in state [:workspace-local :edit-path id :drag-handler])] + (-> state + (update-in (st/get-path state :content) ugp/apply-content-modifiers modifiers) + (update-in [:workspace-local :edit-path id] dissoc :drag-handler) + (update-in [:workspace-local :edit-path id] dissoc :content-modifiers) + (assoc-in [:workspace-local :edit-path id :prev-handler] handler) + (update-in (st/get-path state) helpers/update-selrect)))) + + ptk/WatchEvent + (watch [_ state stream] + (let [id (st/get-path-id state) + handler (get-in state [:workspace-local :edit-path id :prev-handler])] + ;; Update the preview because can be outdated after the dragging + (rx/of (preview-next-point handler)))))) + +(declare close-path-drag-end) + +(defn close-path-drag-start [position] + (ptk/reify ::close-path-drag-start + ptk/WatchEvent + (watch [_ state stream] + (let [id (st/get-path-id state) + zoom (get-in state [:workspace-local :zoom]) + start-position @ms/mouse-position + + stop-stream + (->> stream (rx/filter #(or (helpers/end-path-event? %) + (ms/mouse-up? %)))) + + drag-events-stream + (->> (streams/position-stream) + (rx/take-until stop-stream) + (rx/map #(drag-handler %)))] + + (rx/concat + (rx/of (add-node position)) + (streams/drag-stream + (rx/concat + (rx/of (start-drag-handler)) + drag-events-stream + (rx/of (finish-drag)) + (rx/of (close-path-drag-end)))) + (rx/of (common/finish-path "close-path"))))))) + +(defn close-path-drag-end [] + (ptk/reify ::close-path-drag-end + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state)] + (update-in state [:workspace-local :edit-path id] dissoc :prev-handler))))) + +(defn start-path-from-point [position] + (ptk/reify ::start-path-from-point + ptk/WatchEvent + (watch [_ state stream] + (let [start-point @ms/mouse-position + 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 + (rx/take-until mouse-up) + (rx/map #(drag-handler %)))] + + (rx/concat + (rx/of (add-node position)) + (streams/drag-stream + (rx/concat + (rx/of (start-drag-handler)) + drag-events + (rx/of (finish-drag))))))))) + +(defn make-node-events-stream + [stream] + (->> stream + (rx/filter (ptk/type? ::close-path-drag-start)) + (rx/take 1) + (rx/merge-map #(rx/empty)))) + +(defn make-drag-stream + [stream down-event zoom] + (let [mouse-up (->> stream (rx/filter #(or (helpers/end-path-event? %) + (ms/mouse-up? %)))) + drag-events (->> (streams/position-stream) + (rx/take-until mouse-up) + (rx/map #(drag-handler %)))] + + (rx/concat + (rx/of (add-node down-event)) + (streams/drag-stream + (rx/concat + (rx/of (start-drag-handler)) + drag-events + (rx/of (finish-drag))))))) + +(defn handle-drawing-path + [id] + (ptk/reify ::handle-drawing-path + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state)] + (-> state + (assoc-in [:workspace-local :edit-path id :edit-mode] :draw)))) + + ptk/WatchEvent + (watch [_ state stream] + (let [zoom (get-in state [:workspace-local :zoom]) + mouse-down (->> stream (rx/filter ms/mouse-down?)) + end-path-events (->> stream (rx/filter helpers/end-path-event?)) + + ;; Mouse move preview + mousemove-events + (->> (streams/position-stream) + (rx/take-until end-path-events) + (rx/map #(preview-next-point %))) + + ;; From mouse down we can have: click, drag and double click + mousedown-events + (->> mouse-down + (rx/take-until end-path-events) + (rx/with-latest merge (streams/position-stream)) + + ;; We change to the stream that emits the first event + (rx/switch-map + #(rx/race (make-node-events-stream stream) + (make-drag-stream stream % zoom))))] + + (rx/concat + (rx/of (common/init-path)) + (rx/merge mousemove-events + mousedown-events) + (rx/of (common/finish-path "after-events"))))))) + + +(defn setup-frame-path [] + (ptk/reify ::setup-frame-path + ptk/UpdateEvent + (update [_ state] + (let [objects (dwc/lookup-page-objects state) + content (get-in state [:workspace-drawing :object :content] []) + position (get-in content [0 :params] nil) + frame-id (cp/frame-id-by-position objects position)] + (-> state + (assoc-in [:workspace-drawing :object :frame-id] frame-id)))))) + +(defn handle-new-shape-result [shape-id] + (ptk/reify ::handle-new-shape-result + ptk/UpdateEvent + (update [_ state] + (let [content (get-in state [:workspace-drawing :object :content] [])] + (us/verify ::spec/content content) + (if (> (count content) 1) + (assoc-in state [:workspace-drawing :object :initialized?] true) + state))) + + ptk/WatchEvent + (watch [_ state stream] + (->> (rx/of (setup-frame-path) + dwdc/handle-finish-drawing + (dwc/start-edition-mode shape-id) + (change-edit-mode :draw)))))) + +(defn handle-new-shape + "Creates a new path shape" + [] + (ptk/reify ::handle-new-shape + ptk/WatchEvent + (watch [_ state stream] + (let [shape-id (get-in state [:workspace-drawing :object :id])] + (rx/concat + (rx/of (handle-drawing-path shape-id)) + (->> stream + (rx/filter (ptk/type? ::common/finish-path)) + (rx/take 1) + (rx/observe-on :async) + (rx/map #(handle-new-shape-result shape-id)))))))) + +(declare check-changed-content) + +(defn start-draw-mode [] + (ptk/reify ::start-draw-mode + ptk/UpdateEvent + (update [_ state] + (let [id (get-in state [:workspace-local :edition]) + page-id (:current-page-id state) + old-content (get-in state [:workspace-data :pages-index page-id :objects id :content])] + (-> state + (assoc-in [:workspace-local :edit-path id :old-content] old-content)))) + + ptk/WatchEvent + (watch [_ state stream] + (let [id (get-in state [:workspace-local :edition]) + edit-mode (get-in state [:workspace-local :edit-path id :edit-mode])] + (if (= :draw edit-mode) + (rx/concat + (rx/of (handle-drawing-path id)) + (->> stream + (rx/filter (ptk/type? ::common/finish-path)) + (rx/take 1) + (rx/merge-map #(rx/of (check-changed-content))))) + (rx/empty)))))) + +(defn check-changed-content [] + (ptk/reify ::check-changed-content + ptk/WatchEvent + (watch [_ state stream] + (let [id (st/get-path-id state) + content (get-in state (st/get-path state :content)) + old-content (get-in state [:workspace-local :edit-path id :old-content]) + mode (get-in state [:workspace-local :edit-path id :edit-mode])] + + (cond + (not= content old-content) (rx/of (changes/save-path-content) + (start-draw-mode)) + (= mode :draw) (rx/of :interrupt) + :else (rx/of (common/finish-path "changed-content"))))))) + +(defn change-edit-mode [mode] + (ptk/reify ::change-edit-mode + ptk/UpdateEvent + (update [_ state] + (let [id (get-in state [:workspace-local :edition])] + (cond-> state + id (assoc-in [:workspace-local :edit-path id :edit-mode] mode)))) + + ptk/WatchEvent + (watch [_ state stream] + (let [id (st/get-path-id state)] + (cond + (and id (= :move mode)) (rx/of (common/finish-path "change-edit-mode")) + (and id (= :draw mode)) (rx/of (start-draw-mode)) + :else (rx/empty)))))) diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs new file mode 100644 index 0000000000..df71229e6e --- /dev/null +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -0,0 +1,216 @@ +;; 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.data.workspace.path.edition + (:require + [app.common.data :as d] + [app.common.geom.point :as gpt] + [app.common.math :as mth] + [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.path.changes :as changes] + [app.main.data.workspace.path.common :as common] + [app.main.data.workspace.path.helpers :as helpers] + [app.main.data.workspace.path.selection :as selection] + [app.main.data.workspace.path.state :as st] + [app.main.data.workspace.path.streams :as streams] + [app.main.data.workspace.path.drawing :as drawing] + [app.main.streams :as ms] + [app.util.geom.path :as ugp] + [beicon.core :as rx] + [potok.core :as ptk])) + +(defn modify-point [index prefix dx dy] + (ptk/reify ::modify-point + ptk/UpdateEvent + (update [_ state] + (let [id (get-in state [:workspace-local :edition]) + [cx cy] (if (= prefix :c1) [:c1x :c1y] [:c2x :c2y])] + (-> state + (update-in [:workspace-local :edit-path id :content-modifiers (inc index)] assoc + :c1x dx :c1y dy) + (update-in [:workspace-local :edit-path id :content-modifiers index] assoc + :x dx :y dy :c2x dx :c2y dy)))))) + +(defn modify-handler [id index prefix dx dy match-opposite?] + (ptk/reify ::modify-point + ptk/UpdateEvent + (update [_ state] + (let [content (get-in state (st/get-path state :content)) + [cx cy] (if (= prefix :c1) [:c1x :c1y] [:c2x :c2y]) + [ocx ocy] (if (= prefix :c1) [:c2x :c2y] [:c1x :c1y]) + opposite-index (ugp/opposite-index content index prefix)] + (cond-> state + :always + (update-in [:workspace-local :edit-path id :content-modifiers index] assoc + cx dx cy dy) + + (and match-opposite? opposite-index) + (update-in [:workspace-local :edit-path id :content-modifiers opposite-index] assoc + ocx (- dx) ocy (- dy))))))) + +(defn apply-content-modifiers [] + (ptk/reify ::apply-content-modifiers + ptk/WatchEvent + (watch [_ state stream] + (let [id (st/get-path-id state) + page-id (:current-page-id state) + shape (get-in state (st/get-path state)) + content-modifiers (get-in state [:workspace-local :edit-path id :content-modifiers]) + new-content (ugp/apply-content-modifiers (:content shape) content-modifiers) + [rch uch] (changes/generate-path-changes page-id shape (:content shape) new-content)] + + (rx/of (dwc/commit-changes rch uch {:commit-local? true}) + (fn [state] (update-in state [:workspace-local :edit-path id] dissoc :content-modifiers))))))) + +(defn move-selected-path-point [from-point to-point] + (letfn [(modify-content-point [content {dx :x dy :y} modifiers point] + (let [point-indices (ugp/point-indices content point) ;; [indices] + handler-indices (ugp/handler-indices content point) ;; [[index prefix]] + + modify-point + (fn [modifiers index] + (-> modifiers + (update index assoc :x dx :y dy))) + + modify-handler + (fn [modifiers [index prefix]] + (let [cx (d/prefix-keyword prefix :x) + cy (d/prefix-keyword prefix :y)] + (-> modifiers + (update index assoc cx dx cy dy))))] + + (as-> modifiers $ + (reduce modify-point $ point-indices) + (reduce modify-handler $ handler-indices))))] + + (ptk/reify ::move-point + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state) + content (get-in state (st/get-path state :content)) + delta (gpt/subtract to-point from-point) + + modifiers-reducer (partial modify-content-point content delta) + + points (get-in state [:workspace-local :edit-path id :selected-points] #{}) + + modifiers (get-in state [:workspace-local :edit-path id :content-modifiers] {}) + modifiers (->> points + (reduce modifiers-reducer {}))] + + (assoc-in state [:workspace-local :edit-path id :content-modifiers] modifiers)))))) + +(defn start-move-path-point + [position shift?] + (ptk/reify ::start-move-path-point + ptk/WatchEvent + (watch [_ state stream] + (let [start-position @ms/mouse-position + stopper (->> stream (rx/filter ms/mouse-up?)) + zoom (get-in state [:workspace-local :zoom]) + id (get-in state [:workspace-local :edition]) + selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) + selected? (contains? selected-points position) + + mouse-drag-stream + (rx/concat + ;; If we're dragging a selected item we don't change the selection + (if selected? + (rx/empty) + (rx/of (selection/select-node position shift?))) + + ;; This stream checks the consecutive mouse positions to do the draging + (->> ms/mouse-position + (rx/take-until stopper) + (rx/map #(move-selected-path-point start-position %))) + (rx/of (apply-content-modifiers))) + + ;; When there is not drag we select the node + mouse-click-stream + (rx/of (selection/select-node position shift?))] + + (streams/drag-stream mouse-drag-stream + mouse-click-stream))))) + +(defn start-move-handler + [index prefix] + (ptk/reify ::start-move-handler + ptk/WatchEvent + (watch [_ state stream] + (let [id (get-in state [:workspace-local :edition]) + cx (d/prefix-keyword prefix :x) + cy (d/prefix-keyword prefix :y) + start-point @ms/mouse-position + modifiers (get-in state [:workspace-local :edit-path id :content-modifiers]) + start-delta-x (get-in modifiers [index cx] 0) + start-delta-y (get-in modifiers [index cy] 0) + + content (get-in state (st/get-path state :content)) + opposite-index (ugp/opposite-index content index prefix) + opposite-prefix (if (= prefix :c1) :c2 :c1) + opposite-handler (-> content (get opposite-index) (ugp/get-handler opposite-prefix)) + + point (-> content (get (if (= prefix :c1) (dec index) index)) (ugp/command->point)) + handler (-> content (get index) (ugp/get-handler prefix)) + + current-distance (when opposite-handler (gpt/distance (ugp/opposite-handler point handler) opposite-handler)) + match-opposite? (and opposite-handler (mth/almost-zero? current-distance))] + + (streams/drag-stream + (rx/concat + (->> (streams/position-stream) + (rx/take-until (->> stream (rx/filter ms/mouse-up?))) + (rx/map + (fn [{:keys [x y alt? shift?]}] + (let [pos (cond-> (gpt/point x y) + shift? (helpers/position-fixed-angle point))] + (modify-handler + id + index + prefix + (+ start-delta-x (- (:x pos) (:x start-point))) + (+ start-delta-y (- (:y pos) (:y start-point))) + (and (not alt?) match-opposite?)))))) + (rx/concat (rx/of (apply-content-modifiers))))))))) + +(declare stop-path-edit) + +(defn start-path-edit + [id] + (ptk/reify ::start-path-edit + ptk/UpdateEvent + (update [_ state] + (let [edit-path (get-in state [:workspace-local :edit-path id])] + + (cond-> state + (or (not edit-path) (= :draw (:edit-mode edit-path))) + (assoc-in [:workspace-local :edit-path id] {:edit-mode :move + :selected #{} + :snap-toggled true}) + + (and (some? edit-path) (= :move (:edit-mode edit-path))) + (assoc-in [:workspace-local :edit-path id :edit-mode] :draw)))) + + ptk/WatchEvent + (watch [_ state stream] + (let [mode (get-in state [:workspace-local :edit-path id :edit-mode])] + (rx/concat + (rx/of (drawing/change-edit-mode mode)) + (->> stream + (rx/take-until (->> stream (rx/filter (ptk/type? ::start-path-edit)))) + (rx/filter #(= % :interrupt)) + (rx/take 1) + (rx/map #(stop-path-edit)))))))) + +(defn stop-path-edit [] + (ptk/reify ::stop-path-edit + ptk/UpdateEvent + (update [_ state] + (let [id (get-in state [:workspace-local :edition])] + (update state :workspace-local dissoc :edit-path id))))) diff --git a/frontend/src/app/main/data/workspace/path/helpers.cljs b/frontend/src/app/main/data/workspace/path/helpers.cljs new file mode 100644 index 0000000000..f70e89ad9c --- /dev/null +++ b/frontend/src/app/main/data/workspace/path/helpers.cljs @@ -0,0 +1,123 @@ +;; 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.data.workspace.path.helpers + (:require + [app.common.data :as d] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] + [app.common.math :as mth] + [app.main.data.workspace.path.state :refer [get-path]] + [app.main.data.workspace.path.common :as common] + [app.main.streams :as ms] + [app.util.geom.path :as ugp] + [potok.core :as ptk])) + +;; CONSTANTS +(defonce enter-keycode 13) + +(defn end-path-event? [{:keys [type shift] :as event}] + (or (= (ptk/type event) ::common/finish-path) + (= (ptk/type event) :esc-pressed) + (= event :interrupt) ;; ESC + (and (ms/mouse-double-click? event)))) + +(defn content->points+selrect + "Given the content of a shape, calculate its points and selrect" + [shape content] + (let [transform (:transform shape (gmt/matrix)) + transform-inverse (:transform-inverse shape (gmt/matrix)) + center (gsh/center-shape shape) + base-content (gsh/transform-content + content + (gmt/transform-in center transform-inverse)) + + ;; Calculates the new selrect with points given the old center + points (-> (gsh/content->selrect base-content) + (gsh/rect->points) + (gsh/transform-points center (:transform shape (gmt/matrix)))) + + points-center (gsh/center-points points) + + ;; Points is now the selrect but the center is different so we can create the selrect + ;; through points + selrect (-> points + (gsh/transform-points points-center (:transform-inverse shape (gmt/matrix))) + (gsh/points->selrect))] + [points selrect])) + +(defn update-selrect + "Updates the selrect and points for a path" + [shape] + (if (= (:rotation shape 0) 0) + (let [content (:content shape) + selrect (gsh/content->selrect content) + points (gsh/rect->points selrect)] + (assoc shape :points points :selrect selrect)) + + (let [content (:content shape) + [points selrect] (content->points+selrect shape content)] + (assoc shape :points points :selrect selrect)))) + + +(defn closest-angle + [angle] + (cond + (or (> angle 337.5) (<= angle 22.5)) 0 + (and (> angle 22.5) (<= angle 67.5)) 45 + (and (> angle 67.5) (<= angle 112.5)) 90 + (and (> angle 112.5) (<= angle 157.5)) 135 + (and (> angle 157.5) (<= angle 202.5)) 180 + (and (> angle 202.5) (<= angle 247.5)) 225 + (and (> angle 247.5) (<= angle 292.5)) 270 + (and (> angle 292.5) (<= angle 337.5)) 315)) + +(defn position-fixed-angle [point from-point] + (if (and from-point point) + (let [angle (mod (+ 360 (- (gpt/angle point from-point))) 360) + to-angle (closest-angle angle) + distance (gpt/distance point from-point)] + (gpt/angle->point from-point (mth/radians to-angle) distance)) + point)) + +(defn next-node + "Calculates the next-node to be inserted." + [shape position prev-point prev-handler] + (let [last-command (-> shape :content last :command) + add-line? (and prev-point (not prev-handler) (not= last-command :close-path)) + add-curve? (and prev-point prev-handler (not= last-command :close-path))] + (cond + add-line? {:command :line-to + :params position} + add-curve? {:command :curve-to + :params (ugp/make-curve-params position prev-handler)} + :else {:command :move-to + :params position}))) + +(defn append-node + "Creates a new node in the path. Usualy used when drawing." + [shape position prev-point prev-handler] + (let [command (next-node shape position prev-point prev-handler)] + (-> shape + (update :content (fnil conj []) command) + (update-selrect)))) + +(defn move-handler-modifiers + [content index prefix match-opposite? dx dy] + (let [[cx cy] (if (= prefix :c1) [:c1x :c1y] [:c2x :c2y]) + [ocx ocy] (if (= prefix :c1) [:c2x :c2y] [:c1x :c1y]) + opposite-index (ugp/opposite-index content index prefix)] + + (cond-> {} + :always + (update index assoc cx dx cy dy) + + (and match-opposite? opposite-index) + (update opposite-index assoc ocx (- dx) ocy (- dy))))) diff --git a/frontend/src/app/main/data/workspace/path/selection.cljs b/frontend/src/app/main/data/workspace/path/selection.cljs new file mode 100644 index 0000000000..120845fc86 --- /dev/null +++ b/frontend/src/app/main/data/workspace/path/selection.cljs @@ -0,0 +1,157 @@ +;; 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.data.workspace.path.selection + (:require + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] + [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.path.state :as st] + [app.main.streams :as ms] + [beicon.core :as rx] + [potok.core :as ptk])) + +(defn path-pointer-enter [position] + (ptk/reify ::path-pointer-enter + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state)] + (update-in state [:workspace-local :edit-path id :hover-points] (fnil conj #{}) position))))) + +(defn path-pointer-leave [position] + (ptk/reify ::path-pointer-leave + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state)] + (update-in state [:workspace-local :edit-path id :hover-points] disj position))))) + +(defn select-handler [index type] + (ptk/reify ::select-handler + ptk/UpdateEvent + (update [_ state] + (let [id (get-in state [:workspace-local :edition])] + (-> state + (update-in [:workspace-local :edit-path id :selected-handlers] (fnil conj #{}) [index type])))))) + + +(defn path-handler-enter [index prefix] + (ptk/reify ::path-handler-enter + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state)] + (update-in state [:workspace-local :edit-path id :hover-handlers] (fnil conj #{}) [index prefix]))))) + +(defn path-handler-leave [index prefix] + (ptk/reify ::path-handler-leave + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state)] + (update-in state [:workspace-local :edit-path id :hover-handlers] disj [index prefix]))))) + +(defn select-node-area [shift?] + (ptk/reify ::select-node-area + ptk/UpdateEvent + (update [_ state] + (let [selrect (get-in state [:workspace-local :selrect]) + id (get-in state [:workspace-local :edition]) + content (get-in state (st/get-path state :content)) + selected-point? (fn [point] + (gsh/has-point-rect? selrect point)) + positions (into #{} + (comp (map (comp gpt/point :params)) + (filter selected-point?)) + content)] + (cond-> state + (some? id) + (assoc-in [:workspace-local :edit-path id :selected-points] positions)))))) + +(defn select-node [position shift?] + (ptk/reify ::select-node + ptk/UpdateEvent + (update [_ state] + (let [id (get-in state [:workspace-local :edition])] + (cond-> state + (some? id) + (assoc-in [:workspace-local :edit-path id :selected-points] #{position})))))) + +(defn deselect-node [position shift?] + (ptk/reify ::deselect-node + ptk/UpdateEvent + (update [_ state] + (let [id (get-in state [:workspace-local :edition])] + (-> state + (update-in [:workspace-local :edit-path id :selected-points] (fnil disj #{}) position)))))) + +(defn add-to-selection-handler [index type] + (ptk/reify ::add-to-selection-handler + ptk/UpdateEvent + (update [_ state] + state))) + +(defn add-to-selection-node [index] + (ptk/reify ::add-to-selection-node + ptk/UpdateEvent + (update [_ state] + state))) + +(defn remove-from-selection-handler [index] + (ptk/reify ::remove-from-selection-handler + ptk/UpdateEvent + (update [_ state] + state))) + +(defn remove-from-selection-node [index] + (ptk/reify ::remove-from-selection-handler + ptk/UpdateEvent + (update [_ state] + state))) + +(defn deselect-all [] + (ptk/reify ::deselect-all + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state)] + (-> state + (assoc-in [:workspace-local :edit-path id :selected-handlers] #{}) + (assoc-in [:workspace-local :edit-path id :selected-points] #{})))))) + +(defn update-area-selection + [rect] + (ptk/reify ::update-area-selection + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-local :selrect] rect)))) + +(defn clear-area-selection + [] + (ptk/reify ::clear-area-selection + ptk/UpdateEvent + (update [_ state] + (update state :workspace-local dissoc :selrect)))) + +(defn handle-selection + [shift?] + (letfn [(valid-rect? [{width :width height :height}] + (or (> width 10) (> height 10)))] + + (ptk/reify ::handle-selection + ptk/WatchEvent + (watch [_ state stream] + (let [stop? (fn [event] (or (dwc/interrupt? event) (ms/mouse-up? event))) + stoper (->> stream (rx/filter stop?)) + from-p @ms/mouse-position] + (rx/concat + (->> ms/mouse-position + (rx/take-until stoper) + (rx/map #(gsh/points->rect [from-p %])) + (rx/filter valid-rect?) + (rx/map update-area-selection)) + + (rx/of (select-node-area shift?) + (clear-area-selection)))))))) diff --git a/frontend/src/app/main/data/workspace/path/spec.cljs b/frontend/src/app/main/data/workspace/path/spec.cljs new file mode 100644 index 0000000000..759f43dc0c --- /dev/null +++ b/frontend/src/app/main/data/workspace/path/spec.cljs @@ -0,0 +1,52 @@ +;; 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.data.workspace.path.spec + (:require + [clojure.spec.alpha :as s])) + +;; SCHEMAS + +(s/def ::command #{:move-to + :line-to + :line-to-horizontal + :line-to-vertical + :curve-to + :smooth-curve-to + :quadratic-bezier-curve-to + :smooth-quadratic-bezier-curve-to + :elliptical-arc + :close-path}) + +(s/def :paths.params/x number?) +(s/def :paths.params/y number?) +(s/def :paths.params/c1x number?) +(s/def :paths.params/c1y number?) +(s/def :paths.params/c2x number?) +(s/def :paths.params/c2y number?) + +(s/def ::relative? boolean?) + +(s/def ::params + (s/keys :req-un [:path.params/x + :path.params/y] + :opt-un [:path.params/c1x + :path.params/c1y + :path.params/c2x + :path.params/c2y])) + +(s/def ::content-entry + (s/keys :req-un [::command] + :req-opt [::params + ::relative?])) +(s/def ::content + (s/coll-of ::content-entry :kind vector?)) + + + diff --git a/frontend/src/app/main/data/workspace/path/state.cljs b/frontend/src/app/main/data/workspace/path/state.cljs new file mode 100644 index 0000000000..96c4c72213 --- /dev/null +++ b/frontend/src/app/main/data/workspace/path/state.cljs @@ -0,0 +1,32 @@ +;; 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.data.workspace.path.state + (:require + [app.common.data :as d])) + +(defn get-path-id + "Retrieves the currently editing path id" + [state] + (or (get-in state [:workspace-local :edition]) + (get-in state [:workspace-drawing :object :id]))) + +(defn get-path + "Retrieves the location of the path object and additionaly can pass + the arguments. This location can be used in get-in, assoc-in... functions" + [state & path] + (let [edit-id (get-in state [:workspace-local :edition]) + page-id (:current-page-id state)] + (d/concat + (if edit-id + [:workspace-data :pages-index page-id :objects edit-id] + [:workspace-drawing :object]) + path))) + + diff --git a/frontend/src/app/main/data/workspace/path/streams.cljs b/frontend/src/app/main/data/workspace/path/streams.cljs new file mode 100644 index 0000000000..5040b0c344 --- /dev/null +++ b/frontend/src/app/main/data/workspace/path/streams.cljs @@ -0,0 +1,54 @@ +;; 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.data.workspace.path.streams + (:require + [app.main.data.workspace.path.helpers :as helpers] + [app.common.geom.point :as gpt] + [app.main.store :as st] + [app.main.streams :as ms] + [beicon.core :as rx] + [potok.core :as ptk])) + +(defonce drag-threshold 5) + +(defn dragging? [start zoom] + (fn [current] + (>= (gpt/distance start current) (/ drag-threshold zoom)))) + +(defn drag-stream + ([to-stream] + (drag-stream to-stream (rx/empty))) + + ([to-stream not-drag-stream] + (let [start @ms/mouse-position + zoom (get-in @st/state [:workspace-local :zoom] 1) + mouse-up (->> st/stream (rx/filter #(ms/mouse-up? %))) + + position-stream + (->> ms/mouse-position + (rx/take-until mouse-up) + (rx/filter (dragging? start zoom)) + (rx/take 1))] + + (rx/merge + (->> position-stream + (rx/if-empty ::empty) + (rx/merge-map (fn [value] + (if (= value ::empty) + not-drag-stream + (rx/empty))))) + + (->> position-stream + (rx/merge-map (fn [] to-stream))))))) + +(defn position-stream [] + (->> ms/mouse-position + (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 new file mode 100644 index 0000000000..2085ddf79a --- /dev/null +++ b/frontend/src/app/main/data/workspace/path/tools.cljs @@ -0,0 +1,42 @@ +;; 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.data.workspace.path.tools + (:require + [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.path.changes :as changes] + [app.main.data.workspace.path.common :as common] + [app.main.data.workspace.path.state :as st] + [app.util.geom.path :as ugp] + [beicon.core :as rx] + [potok.core :as ptk])) + +(defn make-corner [] + (ptk/reify ::make-corner + ptk/WatchEvent + (watch [_ state stream] + (let [id (st/get-path-id state) + page-id (:current-page-id state) + shape (get-in state (st/get-path state)) + selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) + new-content (reduce ugp/make-corner-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 make-curve [] + (ptk/reify ::make-curve + ptk/WatchEvent + (watch [_ state stream] + (let [id (st/get-path-id state) + page-id (:current-page-id state) + shape (get-in state (st/get-path state)) + selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) + 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})))))) diff --git a/frontend/src/app/main/ui/workspace/shapes/path/actions.cljs b/frontend/src/app/main/ui/workspace/shapes/path/actions.cljs index 9a030ddebd..e71df0316c 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/actions.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/actions.cljs @@ -6,7 +6,7 @@ (ns app.main.ui.workspace.shapes.path.actions (:require - [app.main.data.workspace.drawing.path :as drp] + [app.main.data.workspace.path :as drp] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.icons :as i] 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 a30d29f055..a459dc836a 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -8,7 +8,7 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] - [app.main.data.workspace.drawing.path :as drp] + [app.main.data.workspace.path :as drp] [app.main.store :as st] [app.main.ui.cursors :as cur] [app.main.ui.workspace.shapes.path.common :as pc] @@ -31,34 +31,21 @@ (fn [event] (st/emit! (drp/path-pointer-leave position))) - on-click + on-mouse-down (fn [event] (dom/stop-propagation event) (dom/prevent-default event) (let [shift? (kbd/shift? event)] (cond - (and (= edit-mode :move) (not selected?)) - (st/emit! (drp/select-node position shift?)) + (= edit-mode :move) + (st/emit! (drp/start-move-path-point position shift?)) - (and (= edit-mode :move) selected?) - (st/emit! (drp/deselect-node position shift?))))) + (and (= edit-mode :draw) start-path?) + (st/emit! (drp/start-path-from-point position)) - - on-mouse-down - (fn [event] - (dom/stop-propagation event) - (dom/prevent-default event) - - (cond - (= edit-mode :move) - (st/emit! (drp/start-move-path-point position)) - - (and (= edit-mode :draw) start-path?) - (st/emit! (drp/start-path-from-point position)) - - (and (= edit-mode :draw) (not start-path?)) - (st/emit! (drp/close-path-drag-start position))))] + (and (= edit-mode :draw) (not start-path?)) + (st/emit! (drp/close-path-drag-start position)))))] [:g.path-point [:circle.path-point @@ -74,7 +61,6 @@ [:circle {:cx x :cy y :r (/ 10 zoom) - :on-click on-click :on-mouse-down on-mouse-down :on-mouse-enter on-enter :on-mouse-leave on-leave @@ -179,25 +165,15 @@ last-p (->> content last ugp/command->point) handlers (ugp/content->handlers content) - ;;handle-click-outside - ;;(fn [event] - ;; (let [current (dom/get-target event) - ;; editor-dom (mf/ref-val editor-ref)] - ;; (when-not (or (.contains editor-dom current) - ;; (dom/class? current "viewport-actions-entry")) - ;; (st/emit! (drp/deselect-all))))) - handle-double-click-outside (fn [event] (when (= edit-mode :move) - (st/emit! :interrupt))) - ] + (st/emit! :interrupt)))] (mf/use-layout-effect (mf/deps edit-mode) (fn [] - (let [keys [;;(events/listen (dom/get-root) EventType.CLICK handle-click-outside) - (events/listen (dom/get-root) EventType.DBLCLICK handle-double-click-outside)]] + (let [keys [(events/listen (dom/get-root) EventType.DBLCLICK handle-double-click-outside)]] #(doseq [key keys] (events/unlistenByKey key))))) @@ -208,29 +184,35 @@ :zoom zoom}]) (for [position points] - [:g.path-node - [:g.point-handlers {:pointer-events (when (= edit-mode :draw) "none")} - (for [[index prefix] (get handlers position)] - (let [command (get content index) - x (get-in command [:params (d/prefix-keyword prefix :x)]) - y (get-in command [:params (d/prefix-keyword prefix :y)]) - handler-position (gpt/point x y)] - (when (not= position handler-position) - [:& path-handler {:point position - :handler handler-position - :index index - :prefix prefix - :zoom zoom - :selected? (contains? selected-handlers [index prefix]) - :hover? (contains? hover-handlers [index prefix]) - :edit-mode edit-mode}])))] - [:& path-point {:position position - :zoom zoom - :edit-mode edit-mode - :selected? (contains? selected-points position) - :hover? (contains? hover-points position) - :last-p? (= last-point position) - :start-path? (nil? last-point)}]]) + (let [point-selected? (contains? selected-points position) + point-hover? (contains? hover-points position) + last-p? (= last-point position) + start-p? (some? last-point)] + [:g.path-node + [:g.point-handlers {:pointer-events (when (= edit-mode :draw) "none")} + (for [[index prefix] (get handlers position)] + (let [command (get content index) + x (get-in command [:params (d/prefix-keyword prefix :x)]) + y (get-in command [:params (d/prefix-keyword prefix :y)]) + handler-position (gpt/point x y) + handler-selected? (contains? selected-handlers [index prefix]) + handler-hover? (contains? hover-handlers [index prefix])] + (when (not= position handler-position) + [:& path-handler {:point position + :handler handler-position + :index index + :prefix prefix + :zoom zoom + :selected? handler-selected? + :hover? handler-hover? + :edit-mode edit-mode}])))] + [:& path-point {:position position + :zoom zoom + :edit-mode edit-mode + :selected? point-selected? + :hover? point-hover? + :last-p? last-p? + :start-path? start-p?}]])) (when prev-handler [:g.prev-handler {:pointer-events "none"} diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index f2d2d203b3..82b4277300 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -15,7 +15,7 @@ [app.main.store :as st] [app.main.streams :as ms] [app.main.ui.workspace.viewport.utils :as utils] - [app.main.data.workspace.drawing.path :as dwdp] + [app.main.data.workspace.path :as dwdp] [app.util.dom :as dom] [app.util.dom.dnd :as dnd] [app.util.keyboard :as kbd] From 421b30c1d86b6ef3c53fb757986573b81763da54 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 5 Apr 2021 19:09:42 +0200 Subject: [PATCH 04/17] :sparkles: Snapping on path elements --- .../src/app/main/data/workspace/path.cljs | 7 + .../app/main/data/workspace/path/drawing.cljs | 2 +- .../app/main/data/workspace/path/edition.cljs | 5 +- .../app/main/data/workspace/path/streams.cljs | 11 +- .../app/main/data/workspace/path/tools.cljs | 22 ++ .../ui/workspace/shapes/path/actions.cljs | 44 ---- .../main/ui/workspace/shapes/path/editor.cljs | 2 +- .../src/app/main/ui/workspace/viewport.cljs | 7 +- .../main/ui/workspace/viewport/actions.cljs | 7 +- .../ui/workspace/viewport/path_actions.cljs | 174 ++++++++++++++++ .../main/ui/workspace/viewport/snap_path.cljs | 191 ++++++++++++++++++ .../main/ui/workspace/viewport/widgets.cljs | 2 +- frontend/src/app/util/geom/path.cljs | 44 ++++ 13 files changed, 464 insertions(+), 54 deletions(-) delete mode 100644 frontend/src/app/main/ui/workspace/shapes/path/actions.cljs create mode 100644 frontend/src/app/main/ui/workspace/viewport/path_actions.cljs create mode 100644 frontend/src/app/main/ui/workspace/viewport/snap_path.cljs diff --git a/frontend/src/app/main/data/workspace/path.cljs b/frontend/src/app/main/data/workspace/path.cljs index 7147751315..c6c1dd2df7 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 6f9318ebf2..feadd1a9e9 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 df71229e6e..c40c992596 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 5040b0c344..eb061f0b2a 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 2085ddf79a..445da96cbe 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 e71df0316c..0000000000 --- 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 a459dc836a..85fb3ce867 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 bd2ccb43c2..c9f3779ab2 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 82b4277300..21c7606c78 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 0000000000..cc0caf131e --- /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 0000000000..191756430f --- /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 d810b8a062..6bd73feb84 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 55f594fb11..452c2d3297 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))))) From e81b1b81157fb2fc79d062c0c1730b5128b98f26 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 6 Apr 2021 12:51:03 +0200 Subject: [PATCH 05/17] :sparkles: Adds point to curves tool --- common/app/common/geom/point.cljc | 9 +++++ common/app/common/geom/shapes/path.cljc | 14 +++++++ .../app/main/data/workspace/path/tools.cljs | 35 ++++++++++++++++- frontend/src/app/util/geom/path.cljs | 39 ++++++++++++++++--- 4 files changed, 90 insertions(+), 7 deletions(-) diff --git a/common/app/common/geom/point.cljc b/common/app/common/geom/point.cljc index 0124b142da..7a7dfeee66 100644 --- a/common/app/common/geom/point.cljc +++ b/common/app/common/geom/point.cljc @@ -253,7 +253,16 @@ (and (mth/almost-zero? x) (mth/almost-zero? y))) +(defn line-val + "Given a line with two points p1-p2 and a 'percent'. Returns the point in the vector + generated by these two points. For example: for p1=(0,0) p2=(1,1) and v=0.25 will return + the point (0.25, 0.25)" + [p1 p2 v] + (let [v (-> (to-vec p1 p2) + (scale v))] + (add p1 v))) ;; --- Debug (defmethod pp/simple-dispatch Point [obj] (pr obj)) + diff --git a/common/app/common/geom/shapes/path.cljc b/common/app/common/geom/shapes/path.cljc index 2072ade317..5ab3a340ab 100644 --- a/common/app/common/geom/shapes/path.cljc +++ b/common/app/common/geom/shapes/path.cljc @@ -41,6 +41,20 @@ (gpt/point (coord-v :x) (coord-v :y)))) +(defn curve-split + "Splits a curve into two at the given parametric value `t`. + Calculates the Casteljau's algorithm intermediate points" + [start end h1 h2 t] + + (let [p1 (gpt/line-val start h1 t) + p2 (gpt/line-val h1 h2 t) + p3 (gpt/line-val h2 end t) + p4 (gpt/line-val p1 p2 t) + p5 (gpt/line-val p2 p3 t) + sp (gpt/line-val p4 p5 t)] + [[start sp p1 p4] + [sp end p5 p3]])) + ;; https://pomax.github.io/bezierinfo/#extremities (defn curve-extremities "Given a cubic bezier cube finds its roots in t. This are the extremities diff --git a/frontend/src/app/main/data/workspace/path/tools.cljs b/frontend/src/app/main/data/workspace/path/tools.cljs index 445da96cbe..3fb92be7a3 100644 --- a/frontend/src/app/main/data/workspace/path/tools.cljs +++ b/frontend/src/app/main/data/workspace/path/tools.cljs @@ -14,6 +14,7 @@ [app.main.data.workspace.path.common :as common] [app.main.data.workspace.path.state :as st] [app.util.geom.path :as ugp] + [app.common.geom.point :as gpt] [beicon.core :as rx] [potok.core :as ptk])) @@ -41,8 +42,40 @@ [rch uch] (changes/generate-path-changes page-id shape (:content shape) new-content)] (rx/of (dwc/commit-changes rch uch {:commit-local? true})))))) +(defn split-segments [[start end cmd]] + (case (:command cmd) + :line-to [cmd (ugp/split-line-to start cmd 0.5)] + :curve-to [cmd (ugp/split-curve-to start cmd 0.5)] + :close-path [cmd [(ugp/make-line-to (gpt/line-val start end 0.5)) + cmd]] + nil)) + (defn add-node [] - (ptk/reify ::add-node)) + (ptk/reify ::add-node + ptk/WatchEvent + (watch [_ state stream] + + (let [id (st/get-path-id state) + page-id (:current-page-id state) + shape (get-in state (st/get-path state)) + selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) + content (:content shape) + + + cmd-changes (->> (ugp/get-segments content selected-points) + (into {} + (comp (map split-segments) + (filter (comp not nil?))))) + + process-segments (fn [command] + (if (contains? cmd-changes command) + (get cmd-changes command) + [command])) + + new-content (into [] (mapcat process-segments) content) + + [rch uch] (changes/generate-path-changes page-id shape (:content shape) new-content)] + (rx/of (dwc/commit-changes rch uch {:commit-local? true})))))) (defn remove-node [] (ptk/reify ::remove-node)) diff --git a/frontend/src/app/util/geom/path.cljs b/frontend/src/app/util/geom/path.cljs index 452c2d3297..d573b582cb 100644 --- a/frontend/src/app/util/geom/path.cljs +++ b/frontend/src/app/util/geom/path.cljs @@ -8,6 +8,7 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as gshp] [app.util.a2c :refer [a2c]] [app.util.geom.path-impl-simplify :as impl-simplify] [app.util.svg :as usvg] @@ -64,6 +65,11 @@ (cond-> result (not (empty? current)) (conj current)))))) +(defn command->point [command] + (when-not (nil? command) + (let [{{:keys [x y]} :params} command] + (gpt/point x y)))) + (defn command->param-list [command] (let [params (:params command)] (case (:command command) @@ -387,6 +393,12 @@ (mapv command->string) (str/join ""))) +(defn make-line-to [to] + {:command :line-to + :relative false + :params {:x (:x to) + :y (:y to)}}) + (defn make-curve-params ([point] (make-curve-params point point point)) @@ -401,6 +413,26 @@ :c2x (:x h2) :c2y (:y h2)})) +(defn make-curve-to [to h1 h2] + {:command :curve-to + :relative false + :params (make-curve-params to h1 h2)}) + +(defn split-line-to [from-p cmd val] + (let [to-p (command->point cmd) + sp (gpt/line-val from-p to-p val)] + [(make-line-to sp) cmd])) + +(defn split-curve-to [from-p cmd val] + (let [params (:params cmd) + end (gpt/point (:x params) (:y params)) + h1 (gpt/point (:c1x params) (:c1y params)) + h2 (gpt/point (:c2x params) (:c2y params)) + [[_ to1 h11 h21] + [_ to2 h12 h22]] (gshp/curve-split from-p end h1 h2 val)] + [(make-curve-to to1 h11 h21) + (make-curve-to to2 h12 h22)])) + (defn opposite-handler "Calculates the coordinates of the opposite handler" [point handler] @@ -441,11 +473,6 @@ (let [content (if (vector? content) content (into [] content))] (reduce apply-to-index content modifiers)))) -(defn command->point [command] - (when-not (nil? command) - (let [{{:keys [x y]} :params} command] - (gpt/point x y)))) - (defn content->points [content] (->> content (map #(when (-> % :params :x) (gpt/point (-> % :params :x) (-> % :params :y)))) @@ -638,7 +665,7 @@ segments (cond-> segments is-segment? - (conj [prev-point cur-point]))] + (conj [prev-point cur-point cur-cmd]))] (if (some? cur-cmd) (recur segments From 5361e429760b2f1222516e7d0a60579087d7b88f Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 6 Apr 2021 14:27:58 +0200 Subject: [PATCH 06/17] :sparkles: Path split segments --- .../app/main/data/workspace/path/tools.cljs | 38 +++++++++---------- .../main/ui/workspace/viewport/actions.cljs | 4 +- frontend/src/app/util/geom/path.cljs | 22 +++++++++++ 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/frontend/src/app/main/data/workspace/path/tools.cljs b/frontend/src/app/main/data/workspace/path/tools.cljs index 3fb92be7a3..ad2764ddac 100644 --- a/frontend/src/app/main/data/workspace/path/tools.cljs +++ b/frontend/src/app/main/data/workspace/path/tools.cljs @@ -42,19 +42,22 @@ [rch uch] (changes/generate-path-changes page-id shape (:content shape) new-content)] (rx/of (dwc/commit-changes rch uch {:commit-local? true})))))) -(defn split-segments [[start end cmd]] - (case (:command cmd) - :line-to [cmd (ugp/split-line-to start cmd 0.5)] - :curve-to [cmd (ugp/split-curve-to start cmd 0.5)] - :close-path [cmd [(ugp/make-line-to (gpt/line-val start end 0.5)) - cmd]] - nil)) - (defn add-node [] (ptk/reify ::add-node ptk/WatchEvent (watch [_ state stream] + (let [id (st/get-path-id state) + page-id (:current-page-id state) + shape (get-in state (st/get-path state)) + selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) + new-content (ugp/split-segments (:content shape) selected-points 0.5) + [rch uch] (changes/generate-path-changes page-id shape (:content shape) new-content)] + (rx/of (dwc/commit-changes rch uch {:commit-local? true})))))) +(defn remove-node [] + (ptk/reify ::remove-node + ptk/WatchEvent + (watch [_ state stream] (let [id (st/get-path-id state) page-id (:current-page-id state) shape (get-in state (st/get-path state)) @@ -62,23 +65,16 @@ content (:content shape) - cmd-changes (->> (ugp/get-segments content selected-points) - (into {} - (comp (map split-segments) - (filter (comp not nil?))))) + + - process-segments (fn [command] - (if (contains? cmd-changes command) - (get cmd-changes command) - [command])) - - new-content (into [] (mapcat process-segments) content) + new-content (->> content + (filterv #(not (contains? selected-points (ugp/command->point %))))) [rch uch] (changes/generate-path-changes page-id shape (:content shape) new-content)] - (rx/of (dwc/commit-changes rch uch {:commit-local? true})))))) + (rx/of (dwc/commit-changes rch uch {:commit-local? true})))) -(defn remove-node [] - (ptk/reify ::remove-node)) + )) (defn merge-nodes [] (ptk/reify ::merge-nodes)) diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index 21c7606c78..2cea72660a 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -59,10 +59,10 @@ (when (and (not= edition id) text-editing?) (st/emit! dw/clear-edition-mode)) - (when (and (or (not edition) (not= edition id)) (not blocked) (not hidden) (not (#{:comments :path} drawing-tool))) - (not= edition id)) + (when (and (or (not edition) (not= edition id)) (not blocked) (not hidden) + (not (#{:comments :path} drawing-tool)) (not drawing-path?)) (cond drawing-tool diff --git a/frontend/src/app/util/geom/path.cljs b/frontend/src/app/util/geom/path.cljs index d573b582cb..7ae7bbce51 100644 --- a/frontend/src/app/util/geom/path.cljs +++ b/frontend/src/app/util/geom/path.cljs @@ -675,3 +675,25 @@ (rest content)) segments))))) + +(defn split-segments [content points value] + (let [split-command + (fn [[start end cmd]] + (case (:command cmd) + :line-to [cmd (split-line-to start cmd value)] + :curve-to [cmd (split-curve-to start cmd value)] + :close-path [cmd [(make-line-to (gpt/line-val start end value)) cmd]] + nil)) + + cmd-changes + (->> (get-segments content points) + (into {} (comp (map split-command) + (filter (comp not nil?))))) + + process-segments + (fn [command] + (if (contains? cmd-changes command) + (get cmd-changes command) + [command]))] + + (into [] (mapcat process-segments) content))) From bc3640893c2d64cf76c1344961352505ae171079 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 7 Apr 2021 13:21:05 +0200 Subject: [PATCH 07/17] :sparkles: Remove nodes --- .../app/main/data/workspace/path/tools.cljs | 9 +-- frontend/src/app/util/geom/path.cljs | 75 ++++++++++++++++++- 2 files changed, 75 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/main/data/workspace/path/tools.cljs b/frontend/src/app/main/data/workspace/path/tools.cljs index ad2764ddac..8fc961b7a2 100644 --- a/frontend/src/app/main/data/workspace/path/tools.cljs +++ b/frontend/src/app/main/data/workspace/path/tools.cljs @@ -63,14 +63,7 @@ shape (get-in state (st/get-path state)) selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) content (:content shape) - - - - - - new-content (->> content - (filterv #(not (contains? selected-points (ugp/command->point %))))) - + new-content (ugp/remove-nodes content 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})))) diff --git a/frontend/src/app/util/geom/path.cljs b/frontend/src/app/util/geom/path.cljs index 7ae7bbce51..aa825e1a48 100644 --- a/frontend/src/app/util/geom/path.cljs +++ b/frontend/src/app/util/geom/path.cljs @@ -676,7 +676,9 @@ segments))))) -(defn split-segments [content points value] +(defn split-segments + "Given a content creates splits commands between points with new segments" + [content points value] (let [split-command (fn [[start end cmd]] (case (:command cmd) @@ -697,3 +699,74 @@ [command]))] (into [] (mapcat process-segments) content))) + +(defn remove-nodes + "Removes from content the points given. Will try to reconstruct the paths + to keep everything consistent" + [content points] + + (let [content (d/with-prev content)] + + (loop [result [] + last-handler nil + [cur-cmd prev-cmd] (first content) + content (rest content)] + + (if (nil? cur-cmd) + ;; The result with be an array of arrays were every entry is a subpath + (->> result + ;; remove empty and only 1 node subpaths + (filter #(> (count %) 1)) + ;; flatten array-of-arrays plain array + (flatten) + (into [])) + + (let [move? (= :move-to (:command cur-cmd)) + curve? (= :curve-to (:command cur-cmd)) + + ;; When the old command was a move we start a subpath + result (if move? (conj result []) result) + + subpath (peek result) + + point (command->point cur-cmd) + + old-prev-point (command->point prev-cmd) + new-prev-point (command->point (peek subpath)) + + remove? (contains? points point) + + + ;; We store the first handler for the first curve to be removed to + ;; use it for the first handler of the regenerated path + cur-handler (cond + (and (not last-handler) remove? curve?) + (select-keys (:params cur-cmd) [:c1x :c1y]) + + (not remove?) + nil + + :else + last-handler) + + cur-cmd (cond-> cur-cmd + ;; If we're starting a subpath and it's not a move make it a move + (and (not move?) (empty? subpath)) + (assoc :command :move-to + :params (select-keys (:params cur-cmd) [:x :y])) + + ;; If have a curve the first handler will be relative to the previous + ;; point. We change the handler to the new previous point + (and curve? (not (empty? subpath)) (not= old-prev-point new-prev-point)) + (update :params merge last-handler)) + + head-idx (dec (count result)) + + result (cond-> result + (not remove?) + (update head-idx conj cur-cmd))] + (recur result + cur-handler + (first content) + (rest content))))))) + From fc383664c7f07f957dd80c93debde23c5b240187 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 7 Apr 2021 19:00:29 +0200 Subject: [PATCH 08/17] :sparkles: Adds join, merge, separate nodes --- common/app/common/geom/point.cljc | 4 +- .../app/main/data/workspace/path/tools.cljs | 57 +++---- .../ui/workspace/viewport/path_actions.cljs | 2 +- frontend/src/app/util/geom/path.cljs | 147 +++++++++++++++++- 4 files changed, 168 insertions(+), 42 deletions(-) diff --git a/common/app/common/geom/point.cljc b/common/app/common/geom/point.cljc index 7a7dfeee66..6e656d8889 100644 --- a/common/app/common/geom/point.cljc +++ b/common/app/common/geom/point.cljc @@ -220,7 +220,9 @@ v2-unit (point scalar-projection scalar-projection)))) -(defn center-points [points] +(defn center-points + "Centroid of a group of points" + [points] (let [k (point (count points))] (reduce #(add %1 (divide %2 k)) (point) points))) diff --git a/frontend/src/app/main/data/workspace/path/tools.cljs b/frontend/src/app/main/data/workspace/path/tools.cljs index 8fc961b7a2..850e925e2b 100644 --- a/frontend/src/app/main/data/workspace/path/tools.cljs +++ b/frontend/src/app/main/data/workspace/path/tools.cljs @@ -18,65 +18,44 @@ [beicon.core :as rx] [potok.core :as ptk])) -(defn make-corner [] - (ptk/reify ::make-corner +(defn process-path-tool + "Generic function that executes path transformations with the content and selected nodes" + [tool-fn] + (ptk/reify ::process-path-tool ptk/WatchEvent (watch [_ state stream] (let [id (st/get-path-id state) page-id (:current-page-id state) shape (get-in state (st/get-path state)) selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) - new-content (reduce ugp/make-corner-point (:content shape) selected-points) + new-content (tool-fn (: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 make-corner [] + (process-path-tool + (fn [content points] + (reduce ugp/make-corner-point content points)))) + (defn make-curve [] - (ptk/reify ::make-curve - ptk/WatchEvent - (watch [_ state stream] - (let [id (st/get-path-id state) - page-id (:current-page-id state) - shape (get-in state (st/get-path state)) - selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) - 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})))))) + (process-path-tool + (fn [content points] + (reduce ugp/make-curve-point content points)))) (defn add-node [] - (ptk/reify ::add-node - ptk/WatchEvent - (watch [_ state stream] - (let [id (st/get-path-id state) - page-id (:current-page-id state) - shape (get-in state (st/get-path state)) - selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) - new-content (ugp/split-segments (:content shape) selected-points 0.5) - [rch uch] (changes/generate-path-changes page-id shape (:content shape) new-content)] - (rx/of (dwc/commit-changes rch uch {:commit-local? true})))))) + (process-path-tool (fn [content points] (ugp/split-segments content points 0.5)))) (defn remove-node [] - (ptk/reify ::remove-node - ptk/WatchEvent - (watch [_ state stream] - (let [id (st/get-path-id state) - page-id (:current-page-id state) - shape (get-in state (st/get-path state)) - selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) - content (:content shape) - new-content (ugp/remove-nodes content 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})))) - - )) + (process-path-tool ugp/remove-nodes)) (defn merge-nodes [] - (ptk/reify ::merge-nodes)) + (process-path-tool ugp/merge-nodes)) (defn join-nodes [] - (ptk/reify ::join-nodes)) + (process-path-tool ugp/join-nodes)) (defn separate-nodes [] - (ptk/reify ::separate-nodes)) + (process-path-tool ugp/separate-nodes)) (defn toggle-snap [] (ptk/reify ::toggle-snap diff --git a/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs b/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs index cc0caf131e..8eb346a93a 100644 --- a/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs @@ -28,7 +28,7 @@ :add-node segments-selected? :remove-node points-selected? :merge-nodes segments-selected? - :join-nodes segments-selected? + :join-nodes points-selected? :separate-nodes segments-selected?})) (mf/defc path-actions [{:keys [shape]}] diff --git a/frontend/src/app/util/geom/path.cljs b/frontend/src/app/util/geom/path.cljs index aa825e1a48..c4a9a7249a 100644 --- a/frontend/src/app/util/geom/path.cljs +++ b/frontend/src/app/util/geom/path.cljs @@ -12,7 +12,9 @@ [app.util.a2c :refer [a2c]] [app.util.geom.path-impl-simplify :as impl-simplify] [app.util.svg :as usvg] - [cuerdas.core :as str])) + [cuerdas.core :as str] + [clojure.set :as set] + [app.common.math :as mth])) (defn calculate-opposite-handler "Given a point and its handler, gives the symetric handler" @@ -393,6 +395,12 @@ (mapv command->string) (str/join ""))) +(defn make-move-to [to] + {:command :move-to + :relative false + :params {:x (:x to) + :y (:y to)}}) + (defn make-line-to [to] {:command :line-to :relative false @@ -770,3 +778,140 @@ (first content) (rest content))))))) +(defn join-nodes + "Creates new segments between points that weren't previously" + [content points] + + (let [segments-set (into #{} + (map (fn [[p1 p2 _]] [p1 p2])) + (get-segments content points)) + + create-line-command (fn [point other] + [(make-move-to point) + (make-line-to other)]) + + not-segment? (fn [point other] (and (not (contains? segments-set [point other])) + (not (contains? segments-set [other point])))) + + new-content (->> (d/map-perm create-line-command not-segment? points) + (flatten) + (into []))] + + (d/concat content new-content))) + + +(defn separate-nodes + "Removes the segments between the points given" + [content points] + + (let [content (d/with-prev content)] + (loop [result [] + [cur-cmd prev-cmd] (first content) + content (rest content)] + + (if (nil? cur-cmd) + (->> result + (filter #(> (count %) 1)) + (flatten) + (into [])) + + (let [prev-point (command->point prev-cmd) + cur-point (command->point cur-cmd) + + cur-cmd (cond-> cur-cmd + (and (contains? points prev-point) + (contains? points cur-point)) + + (assoc :command :move-to + :params (select-keys (:params cur-cmd) [:x :y]))) + + move? (= :move-to (:command cur-cmd)) + + result (if move? (conj result []) result) + head-idx (dec (count result)) + + result (-> result + (update head-idx conj cur-cmd))] + (recur result + (first content) + (rest content))))))) + + +(defn- add-to-set + "Given a list of sets adds the value to the target set" + [set-list target value] + (->> set-list + (mapv (fn [it] + (cond-> it + (= it target) (conj value)))))) + +(defn- join-sets + "Given a list of sets join two sets in the list into a new one" + [set-list target other] + (conj (->> set-list + (filterv #(and (not= % target) + (not= % other)))) + (set/union target other))) + +(defn group-segments [segments] + (loop [result [] + [point-a point-b :as segment] (first segments) + segments (rest segments)] + + (if (nil? segment) + result + + (let [set-a (d/seek #(contains? % point-a) result) + set-b (d/seek #(contains? % point-b) result) + + result (cond-> result + (and (nil? set-a) (nil? set-b)) + (conj #{point-a point-b}) + + (and (some? set-a) (nil? set-b)) + (add-to-set set-a point-b) + + (and (nil? set-a) (some? set-b)) + (add-to-set set-b point-a) + + (and (some? set-a) (some? set-b) (not= set-a set-b)) + (join-sets set-a set-b))] + (recur result + (first segments) + (rest segments)))))) + +(defn calculate-merge-points [group-segments points] + (let [index-merge-point (fn [group] (vector group (-> (gpt/center-points group) + (update :x mth/round) + (update :y mth/round)))) + index-group (fn [point] (vector point (d/seek #(contains? % point) group-segments))) + + group->merge-point (into {} (map index-merge-point) group-segments) + point->group (into {} (map index-group) points)] + (d/mapm #(group->merge-point %2) point->group))) + +;; TODO: Improve the replace for curves +(defn replace-points + "Replaces the points in a path for its merge-point" + [content point->merge-point] + (let [replace-command + (fn [cmd] + (let [point (command->point cmd)] + (if (contains? point->merge-point point) + (let [merge-point (get point->merge-point point)] + (-> cmd (update :params assoc :x (:x merge-point) :y (:y merge-point)))) + cmd)))] + (->> content + (mapv replace-command)))) + +(defn merge-nodes + "Reduces the continguous segments in points to a single point" + [content points] + (let [point->merge-point (-> content + (get-segments points) + (group-segments) + (calculate-merge-points points))] + (-> content + (separate-nodes points) + (replace-points point->merge-point)))) + From 6db144e5edd5f56f4db66444b27168f9ea508e68 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 8 Apr 2021 17:48:59 +0200 Subject: [PATCH 09/17] :sparkles: Path-point calculation --- common/app/common/geom/shapes/path.cljc | 89 +++++++++++++++++++ .../app/main/data/workspace/path/drawing.cljs | 29 ++++-- .../app/main/data/workspace/path/edition.cljs | 9 +- .../app/main/data/workspace/path/streams.cljs | 25 ++++-- frontend/src/app/main/snap.cljs | 30 +++++++ .../main/ui/workspace/shapes/path/editor.cljs | 54 +++++++++-- .../src/app/main/ui/workspace/viewport.cljs | 3 +- 7 files changed, 212 insertions(+), 27 deletions(-) diff --git a/common/app/common/geom/shapes/path.cljc b/common/app/common/geom/shapes/path.cljc index 5ab3a340ab..eaa1772544 100644 --- a/common/app/common/geom/shapes/path.cljc +++ b/common/app/common/geom/shapes/path.cljc @@ -225,3 +225,92 @@ point)) (conj result [prev-point last-start])))) + +(defonce path-closest-point-accuracy 0.01) +(defn curve-closest-point + [position start end h1 h2] + (let [d (memoize (fn [t] (gpt/distance position (curve-values start end h1 h2 t))))] + (loop [t1 0 + t2 1] + (if (<= (mth/abs (- t1 t2)) path-closest-point-accuracy) + (curve-values start end h1 h2 t1) + + (let [ht (+ t1 (/ (- t2 t1) 2)) + ht1 (+ t1 (/ (- t2 t1) 4)) + ht2 (+ t1 (/ (* 3 (- t2 t1)) 4)) + + [t1 t2] (cond + (< (d ht1) (d ht2)) + [t1 ht] + + (< (d ht2) (d ht1)) + [ht t2] + + (and (< (d ht) (d t1)) (< (d ht) (d t2))) + [ht1 ht2] + + (< (d t1) (d t2)) + [t1 ht] + + :else + [ht t2])] + (recur t1 t2)))))) + +(defn line-closest-point + "Point on line" + [position from-p to-p] + + (let [{v1x :x v1y :y} from-p + {v2x :x v2y :y} to-p + {px :x py :y} position + + e1 (gpt/point (- v2x v1x) (- v2y v1y)) + e2 (gpt/point (- px v1x) (- py v1y)) + + len2 (+ (mth/sq (:x e1)) (mth/sq (:y e1))) + val-dp (/ (gpt/dot e1 e2) len2)] + + (if (and (>= val-dp 0) + (<= val-dp 1) + (not (mth/almost-zero? len2))) + (gpt/point (+ v1x (* val-dp (:x e1))) + (+ v1y (* val-dp (:y e1)))) + ;; There is no perpendicular projection in the line so the closest + ;; point will be one of the extremes + (if (<= (gpt/distance position from-p) (gpt/distance position to-p)) + from-p + to-p)))) + +(defn path-closest-point + "Given a path and a position" + [shape position] + + (let [point+distance (fn [[cur-cmd prev-cmd]] + (let [point + (case (:command cur-cmd) + :line-to (line-closest-point + position + (command->point prev-cmd) + (command->point cur-cmd)) + :curve-to (curve-closest-point + position + (command->point prev-cmd) + (command->point cur-cmd) + (gpt/point (get-in cur-cmd [:params :c1x]) + (get-in cur-cmd [:params :c1y])) + (gpt/point (get-in cur-cmd [:params :c2x]) + (get-in cur-cmd [:params :c2y]))) + nil)] + (when point + [point (gpt/distance point position)]))) + + find-min-point (fn [[min-p min-dist :as acc] [cur-p cur-dist :as cur]] + (if (and (some? acc) (or (not cur) (<= min-dist cur-dist))) + [min-p min-dist] + [cur-p cur-dist]))] + + (->> (:content shape) + (d/with-prev) + (map point+distance) + (reduce find-min-point) + (first)))) diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index feadd1a9e9..09633ec17c 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -133,8 +133,11 @@ (->> stream (rx/filter #(or (helpers/end-path-event? %) (ms/mouse-up? %)))) + content (get-in state (st/get-path state :content)) + points (ugp/content->points content) + drag-events-stream - (->> (streams/position-stream) + (->> (streams/position-stream points) (rx/take-until stop-stream) (rx/map #(drag-handler %)))] @@ -163,7 +166,10 @@ zoom (get-in state [:workspace-local :zoom]) mouse-up (->> stream (rx/filter #(or (helpers/end-path-event? %) (ms/mouse-up? %)))) - drag-events (->> (streams/position-stream) + content (get-in state (st/get-path state :content)) + points (ugp/content->points content) + + drag-events (->> (streams/position-stream points) (rx/take-until mouse-up) (rx/map #(drag-handler %)))] @@ -183,10 +189,10 @@ (rx/merge-map #(rx/empty)))) (defn make-drag-stream - [stream down-event zoom] + [stream down-event zoom points] (let [mouse-up (->> stream (rx/filter #(or (helpers/end-path-event? %) (ms/mouse-up? %)))) - drag-events (->> (streams/position-stream) + drag-events (->> (streams/position-stream points) (rx/take-until mouse-up) (rx/map #(drag-handler %)))] @@ -213,9 +219,12 @@ mouse-down (->> stream (rx/filter ms/mouse-down?)) end-path-events (->> stream (rx/filter helpers/end-path-event?)) + content (get-in state (st/get-path state :content)) + points (ugp/content->points content) + ;; Mouse move preview mousemove-events - (->> (streams/position-stream) + (->> (streams/position-stream points) (rx/take-until end-path-events) (rx/map #(preview-next-point %))) @@ -223,12 +232,12 @@ mousedown-events (->> mouse-down (rx/take-until end-path-events) - (rx/with-latest merge (streams/position-stream)) + (rx/with-latest merge (streams/position-stream points)) ;; We change to the stream that emits the first event (rx/switch-map #(rx/race (make-node-events-stream stream) - (make-drag-stream stream % zoom))))] + (make-drag-stream stream % zoom points))))] (rx/concat (rx/of (common/init-path)) @@ -269,6 +278,12 @@ "Creates a new path shape" [] (ptk/reify ::handle-new-shape + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state)] + (-> state + (assoc-in [:workspace-local :edit-path id :snap-toggled] true)))) + ptk/WatchEvent (watch [_ state stream] (let [shape-id (get-in state [:workspace-drawing :object :id])] diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index c40c992596..4f723b8fb9 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -118,6 +118,9 @@ selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) selected? (contains? selected-points position) + content (get-in state (st/get-path state :content)) + points (ugp/content->points content) + mouse-drag-stream (rx/concat ;; If we're dragging a selected item we don't change the selection @@ -126,7 +129,7 @@ (rx/of (selection/select-node position shift?))) ;; This stream checks the consecutive mouse positions to do the draging - (->> (streams/position-stream) + (->> (streams/position-stream points) (rx/take-until stopper) (rx/map #(move-selected-path-point start-position %))) (rx/of (apply-content-modifiers))) @@ -151,6 +154,8 @@ start-delta-y (get-in modifiers [index cy] 0) content (get-in state (st/get-path state :content)) + points (ugp/content->points content) + opposite-index (ugp/opposite-index content index prefix) opposite-prefix (if (= prefix :c1) :c2 :c1) opposite-handler (-> content (get opposite-index) (ugp/get-handler opposite-prefix)) @@ -163,7 +168,7 @@ (streams/drag-stream (rx/concat - (->> (streams/position-stream) + (->> (streams/position-stream points) (rx/take-until (->> stream (rx/filter ms/mouse-up?))) (rx/map (fn [{:keys [x y alt? shift?]}] diff --git a/frontend/src/app/main/data/workspace/path/streams.cljs b/frontend/src/app/main/data/workspace/path/streams.cljs index eb061f0b2a..2f5c27a85e 100644 --- a/frontend/src/app/main/data/workspace/path/streams.cljs +++ b/frontend/src/app/main/data/workspace/path/streams.cljs @@ -15,7 +15,8 @@ [app.main.streams :as ms] [beicon.core :as rx] [potok.core :as ptk] - [app.common.math :as mth])) + [app.common.math :as mth] + [app.main.snap :as snap])) (defonce drag-threshold 5) @@ -53,11 +54,17 @@ (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? %)))))) +(defn position-stream + ([points] + (position-stream points #{})) + + ([points selected-points] + (let [zoom (get-in @st/state [:workspace-local :zoom] 1)] + (->> (snap/path-snap ms/mouse-position points selected-points zoom) + (rx/with-latest vector ms/mouse-position) + (rx/map (fn [[{[x] :x [y] :y} position]] + (cond-> position + (some? x) (assoc :x x) + (some? y) (assoc :y y)))) + (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/snap.cljs b/frontend/src/app/main/snap.cljs index e82c67f000..36570576ef 100644 --- a/frontend/src/app/main/snap.cljs +++ b/frontend/src/app/main/snap.cljs @@ -15,6 +15,7 @@ [app.main.refs :as refs] [app.main.worker :as uw] [app.util.geom.snap-points :as sp] + [app.util.range-tree :as rt] [beicon.core :as rx] [clojure.set :as set])) @@ -240,3 +241,32 @@ (rx/reduce gpt/min) (rx/map #(or % (gpt/point 0 0)))))) +(defn path-snap [position-stream points selected-points zoom] + (let [selected-points (or selected-points #{}) + into-tree (fn [coord] + (fn [tree point] + (rt/insert tree (get point coord) point))) + + ranges-x (->> points + (filter (comp not selected-points)) + (reduce (into-tree :x) (rt/make-tree))) + + ranges-y (->> points + (filter (comp not selected-points)) + (reduce (into-tree :y) (rt/make-tree))) + + min-match (fn [matches] + (->> matches + (reduce (fn [[cur-val :as current] [other-val :as other]] + (if (< cur-val other-val) + current + other)))))] + + (->> position-stream + (rx/map + (fn [{:keys [x y]}] + (let [d-pos (/ snap-accuracy zoom) + x-match (rt/range-query ranges-x (- x d-pos) (+ x d-pos)) + y-match (rt/range-query ranges-y (- y d-pos) (+ y d-pos))] + {:x (min-match x-match) + :y (min-match y-match)})))))) 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 85fb3ce867..66fcdf63f6 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -8,16 +8,19 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as gshp] [app.main.data.workspace.path :as drp] + [app.main.snap :as snap] [app.main.store :as st] + [app.main.streams :as ms] [app.main.ui.cursors :as cur] + [app.main.ui.hooks :as hooks] [app.main.ui.workspace.shapes.path.common :as pc] [app.util.dom :as dom] [app.util.geom.path :as ugp] + [app.util.keyboard :as kbd] [goog.events :as events] - [rumext.alpha :as mf] - - [app.util.keyboard :as kbd]) + [rumext.alpha :as mf]) (:import goog.events.EventType)) (mf/defc path-point [{:keys [position zoom edit-mode hover? selected? preview? start-path? last-p?]}] @@ -131,8 +134,9 @@ [:g.preview {:style {:pointer-events "none"}} (when (not= :move-to (:command command)) [:path {:style {:fill "transparent" - :stroke pc/secondary-color - :stroke-width (/ 1 zoom)} + :stroke pc/black-color + :stroke-width (/ 1 zoom) + :stroke-dasharray (/ 4 zoom)} :d (ugp/content->path [{:command :move-to :params {:x (:x from) :y (:y from)}} @@ -141,11 +145,23 @@ :preview? true :zoom zoom}]]) +(mf/defc snap-path-points [{:keys [snaps zoom]}] + [:g.snap-paths + (for [[from to] snaps] + [:line {:x1 (:x from) + :y1 (:y from) + :x2 (:x to) + :y2 (:y to) + :style {:stroke pc/secondary-color + :stroke-width (/ 1 zoom)}}])]) + (mf/defc path-editor [{:keys [shape zoom]}] (let [editor-ref (mf/use-ref nil) edit-path-ref (pc/make-edit-path-ref (:id shape)) + hover-point (mf/use-state nil) + {:keys [edit-mode drag-handler prev-handler @@ -158,9 +174,9 @@ hover-points] :as edit-path} (mf/deref edit-path-ref) - {:keys [content]} shape - content (ugp/apply-content-modifiers content content-modifiers) - points (->> content ugp/content->points (into #{})) + {base-content :content} shape + content (ugp/apply-content-modifiers base-content content-modifiers) + points (mf/use-memo (mf/deps content) #(->> content ugp/content->points (into #{}))) last-command (last content) last-p (->> content last ugp/command->point) handlers (ugp/content->handlers content) @@ -177,12 +193,34 @@ #(doseq [key keys] (events/unlistenByKey key))))) + #_(hooks/use-stream + ms/mouse-position + (mf/deps shape) + (fn [position] + (reset! hover-point (gshp/path-closest-point shape position)))) + + (hooks/use-stream + (mf/use-memo + (mf/deps base-content selected-points zoom) + #(snap/path-snap ms/mouse-position points selected-points zoom)) + + (fn [result] + (prn "??" result))) + [:g.path-editor {:ref editor-ref} + #_[:& snap-points {}] + + (when (and preview (not drag-handler)) [:& path-preview {:command preview :from last-p :zoom zoom}]) + (when @hover-point + [:g.hover-point + [:& path-point {:position @hover-point + :zoom zoom}]]) + (for [position points] (let [point-selected? (contains? selected-points position) point-hover? (contains? hover-points position) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index c9f3779ab2..fda4afe53b 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -171,7 +171,8 @@ :width (:width vport 0) :height (:height vport 0) :view-box (utils/format-viewbox vbox) - :style {:background-color (get options :background "#E8E9EA")}} + :style {:background-color (get options :background "#E8E9EA") + :pointer-events "none"}} [:& (mf/provider muc/embed-ctx) {:value true} ;; Render root shape From 5ce2bc862c5e1e424c59a8f9e49eb0588dd1b7d8 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 12 Apr 2021 16:02:51 +0200 Subject: [PATCH 10/17] :recycle: Move streams refactor --- frontend/deps.edn | 2 +- .../app/main/data/workspace/path/edition.cljs | 5 +- .../app/main/data/workspace/path/streams.cljs | 69 ++++++++--- frontend/src/app/main/snap.cljs | 113 +++++++++++++++++- .../main/ui/workspace/shapes/path/editor.cljs | 2 +- 5 files changed, 172 insertions(+), 19 deletions(-) diff --git a/frontend/deps.edn b/frontend/deps.edn index 51d43a0f60..313e8db1a0 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -11,7 +11,7 @@ danlentz/clj-uuid {:mvn/version "0.1.9"} frankiesardo/linked {:mvn/version "1.3.0"} - funcool/beicon {:mvn/version "2021.04.09-1"} + funcool/beicon {:mvn/version "2021.04.12-1"} funcool/cuerdas {:mvn/version "2020.03.26-3"} funcool/okulary {:mvn/version "2020.04.14-0"} funcool/potok {:mvn/version "3.2.0"} diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index 4f723b8fb9..afc98af60f 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -129,7 +129,8 @@ (rx/of (selection/select-node position shift?))) ;; This stream checks the consecutive mouse positions to do the draging - (->> (streams/position-stream points) + (->> points + (streams/move-points-stream start-position selected-points) (rx/take-until stopper) (rx/map #(move-selected-path-point start-position %))) (rx/of (apply-content-modifiers))) @@ -168,7 +169,7 @@ (streams/drag-stream (rx/concat - (->> (streams/position-stream points) + (->> (streams/move-handler-stream start-point handler points) (rx/take-until (->> stream (rx/filter ms/mouse-up?))) (rx/map (fn [{:keys [x y alt? shift?]}] diff --git a/frontend/src/app/main/data/workspace/path/streams.cljs b/frontend/src/app/main/data/workspace/path/streams.cljs index 2f5c27a85e..9dfd011f4d 100644 --- a/frontend/src/app/main/data/workspace/path/streams.cljs +++ b/frontend/src/app/main/data/workspace/path/streams.cljs @@ -10,13 +10,16 @@ (ns app.main.data.workspace.path.streams (:require [app.main.data.workspace.path.helpers :as helpers] + [app.main.data.workspace.path.state :as state] [app.common.geom.point :as gpt] [app.main.store :as st] [app.main.streams :as ms] [beicon.core :as rx] [potok.core :as ptk] [app.common.math :as mth] - [app.main.snap :as snap])) + [app.main.snap :as snap] + [okulary.core :as l] + [app.util.geom.path :as ugp])) (defonce drag-threshold 5) @@ -54,17 +57,55 @@ (let [k 50] (* (mth/floor (/ num k)) k))) -(defn position-stream - ([points] - (position-stream points #{})) +(defn move-points-stream + [start-point selected-points points] - ([points selected-points] - (let [zoom (get-in @st/state [:workspace-local :zoom] 1)] - (->> (snap/path-snap ms/mouse-position points selected-points zoom) - (rx/with-latest vector ms/mouse-position) - (rx/map (fn [[{[x] :x [y] :y} position]] - (cond-> position - (some? x) (assoc :x x) - (some? y) (assoc :y y)))) - (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? %)))))))) + (let [zoom (get-in @st/state [:workspace-local :zoom] 1) + ranges (snap/create-ranges selected-points points) + d-pos (/ snap/snap-accuracy zoom)] + (->> ms/mouse-position + (rx/map (fn [position] + (let [delta (gpt/subtract position start-point) + moved-points (->> selected-points (mapv #(gpt/add % delta)))] + (gpt/add + position + (snap/get-snap-delta moved-points ranges d-pos))))))) + ) + +(defn move-handler-stream + [start-point handler points] + + (let [zoom (get-in @st/state [:workspace-local :zoom] 1) + ranges (snap/create-ranges points) + d-pos (/ snap/snap-accuracy zoom)] + (->> ms/mouse-position + (rx/map (fn [position] + (let [delta (gpt/subtract position start-point) + handler-position (gpt/add handler delta)] + (gpt/add + position + (snap/get-snap-delta [handler-position] ranges d-pos)))))))) + +(defn position-stream + [points] + (let [zoom (get-in @st/state [:workspace-local :zoom] 1) + ranges (snap/create-ranges points) + d-pos (/ snap/snap-accuracy zoom) + get-content (fn [state] (get-in state (state/get-path state :content))) + + content-stream (-> (l/derived get-content st/state) + (rx/from-atom)) + ] + + (->> content-stream + (rx/map ugp/content->points) + (rx/subs #(prn "????" %))) + + + (->> ms/mouse-position + (rx/tap #(prn "pos" %)) + (rx/map #(let [snap (snap/get-snap-delta [%] ranges d-pos)] + (prn ">>>" snap) + (gpt/add % snap))) + (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/snap.cljs b/frontend/src/app/main/snap.cljs index 36570576ef..3831fb4f6d 100644 --- a/frontend/src/app/main/snap.cljs +++ b/frontend/src/app/main/snap.cljs @@ -241,7 +241,118 @@ (rx/reduce gpt/min) (rx/map #(or % (gpt/point 0 0)))))) -(defn path-snap [position-stream points selected-points zoom] + +;;; PATH SNAP + +(defn create-ranges + ([points] + (create-ranges points #{})) + + ([points selected-points] + (let [selected-points (or selected-points #{}) + + into-tree + (fn [coord] + (fn [tree point] + (rt/insert tree (get point coord) point))) + + make-ranges + (fn [coord] + (->> points + (filter (comp not selected-points)) + (reduce (into-tree coord) (rt/make-tree))))] + + {:x (make-ranges :x) + :y (make-ranges :y)}))) + +(defn query-delta-point [ranges point precision] + (let [query-coord + (fn [coord] + (let [pval (get point coord)] + #_(prn "..." (rt/range-query (get ranges coord) (- pval precision) (+ pval precision))) + + (->> (rt/range-query (get ranges coord) (- pval precision) (+ pval precision)) + + ;; We save the distance to the point and add the matching point to the points + (mapv (fn [[value points]] + #_(prn "!! " value [(mth/abs (- value pval)) + (->> points (mapv #(vector point %)))]) + [(mth/abs (- value pval)) + (->> points (mapv #(vector point %)))])))))] + + {:x (query-coord :x) + :y (query-coord :y)})) + +(defn merge-matches [matches other] + (let [merge-coord + (fn [matches other] + (prn "merge-coord" matches other) + (into {} + (map (fn [key] [key (d/concat [] (get matches key) (get other key))])) + (set/union (keys matches) (keys other))))] + + (-> matches + (update :x merge-matches (:x other)) + (update :y merge-matches (:y other))))) + +(defn min-match + [default matches] + (let [get-min + (fn [[cur-val :as current] [other-val :as other]] + (if (< cur-val other-val) + current + other)) + + min-match-coord + (fn [matches] + (if (and (seq matches) (not (empty? matches))) + (->> matches (reduce get-min)) + default))] + + (-> matches + (update :x min-match-coord) + (update :y min-match-coord)))) + +(defn get-snap-delta-match + [points ranges accuracy] + (assert vector? points) + + (->> points + (mapv #(query-delta-point ranges % accuracy)) + (reduce merge-matches) + (min-match [0 nil]))) + +(defn get-snap-delta + [points ranges accuracy] + (-> (get-snap-delta-match points ranges accuracy) + (update :x first) + (update :y first) + (gpt/point))) + +#_(defn path-snap-points-delta [points-stream selected-points points zoom] + + (let [ranges (create-ranges points selected-points) + d-pos (/ snap-accuracy zoom)] + + (->> points-stream + (rx/map (fn [points] + (get-snap-delta points ranges d-pos))))) + + + ) + +#_(defn path-snap [position-stream points selected-points zoom] + (let [ranges (create-ranges points selected-points) + d-pos (/ snap-accuracy zoom)] + + (->> position-stream + (rx/map (fn [position] + (gpt/add + position + (get-snap-delta position ranges d-pos))))))) + + +#_(defn path-snap [position-stream points selected-points zoom] (let [selected-points (or selected-points #{}) into-tree (fn [coord] (fn [tree point] 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 66fcdf63f6..4299e681be 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -199,7 +199,7 @@ (fn [position] (reset! hover-point (gshp/path-closest-point shape position)))) - (hooks/use-stream + #_(hooks/use-stream (mf/use-memo (mf/deps base-content selected-points zoom) #(snap/path-snap ms/mouse-position points selected-points zoom)) From 5f114163dc737a11f6abcb928970bf5d90497be2 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 12 Apr 2021 16:21:03 +0200 Subject: [PATCH 11/17] :sparkles: Fixes licenses headers --- frontend/src/app/main/data/workspace/path.cljs | 3 --- frontend/src/app/main/data/workspace/path/changes.cljs | 3 --- frontend/src/app/main/data/workspace/path/common.cljs | 3 --- frontend/src/app/main/data/workspace/path/drawing.cljs | 3 --- frontend/src/app/main/data/workspace/path/edition.cljs | 3 --- frontend/src/app/main/data/workspace/path/helpers.cljs | 3 --- frontend/src/app/main/data/workspace/path/selection.cljs | 3 --- frontend/src/app/main/data/workspace/path/spec.cljs | 3 --- frontend/src/app/main/data/workspace/path/state.cljs | 3 --- frontend/src/app/main/data/workspace/path/streams.cljs | 3 --- frontend/src/app/main/data/workspace/path/tools.cljs | 3 --- .../src/app/main/ui/workspace/viewport/path_actions.cljs | 3 --- frontend/src/app/main/ui/workspace/viewport/snap_path.cljs | 5 +---- 13 files changed, 1 insertion(+), 40 deletions(-) diff --git a/frontend/src/app/main/data/workspace/path.cljs b/frontend/src/app/main/data/workspace/path.cljs index c6c1dd2df7..2a02ebe7ff 100644 --- a/frontend/src/app/main/data/workspace/path.cljs +++ b/frontend/src/app/main/data/workspace/path.cljs @@ -2,9 +2,6 @@ ;; 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.data.workspace.path diff --git a/frontend/src/app/main/data/workspace/path/changes.cljs b/frontend/src/app/main/data/workspace/path/changes.cljs index 53fdb6c9fc..84e2de5741 100644 --- a/frontend/src/app/main/data/workspace/path/changes.cljs +++ b/frontend/src/app/main/data/workspace/path/changes.cljs @@ -2,9 +2,6 @@ ;; 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.data.workspace.path.changes diff --git a/frontend/src/app/main/data/workspace/path/common.cljs b/frontend/src/app/main/data/workspace/path/common.cljs index 0d28bf984f..d8e6e19cbf 100644 --- a/frontend/src/app/main/data/workspace/path/common.cljs +++ b/frontend/src/app/main/data/workspace/path/common.cljs @@ -2,9 +2,6 @@ ;; 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.data.workspace.path.common diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index 09633ec17c..781e3fffaa 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -2,9 +2,6 @@ ;; 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.data.workspace.path.drawing diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index afc98af60f..ff14d5d008 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -2,9 +2,6 @@ ;; 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.data.workspace.path.edition diff --git a/frontend/src/app/main/data/workspace/path/helpers.cljs b/frontend/src/app/main/data/workspace/path/helpers.cljs index f70e89ad9c..0fe040d426 100644 --- a/frontend/src/app/main/data/workspace/path/helpers.cljs +++ b/frontend/src/app/main/data/workspace/path/helpers.cljs @@ -2,9 +2,6 @@ ;; 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.data.workspace.path.helpers diff --git a/frontend/src/app/main/data/workspace/path/selection.cljs b/frontend/src/app/main/data/workspace/path/selection.cljs index 120845fc86..99ea74b17a 100644 --- a/frontend/src/app/main/data/workspace/path/selection.cljs +++ b/frontend/src/app/main/data/workspace/path/selection.cljs @@ -2,9 +2,6 @@ ;; 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.data.workspace.path.selection diff --git a/frontend/src/app/main/data/workspace/path/spec.cljs b/frontend/src/app/main/data/workspace/path/spec.cljs index 759f43dc0c..96ad24fa0a 100644 --- a/frontend/src/app/main/data/workspace/path/spec.cljs +++ b/frontend/src/app/main/data/workspace/path/spec.cljs @@ -2,9 +2,6 @@ ;; 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.data.workspace.path.spec diff --git a/frontend/src/app/main/data/workspace/path/state.cljs b/frontend/src/app/main/data/workspace/path/state.cljs index 96c4c72213..6bb59c4c64 100644 --- a/frontend/src/app/main/data/workspace/path/state.cljs +++ b/frontend/src/app/main/data/workspace/path/state.cljs @@ -2,9 +2,6 @@ ;; 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.data.workspace.path.state diff --git a/frontend/src/app/main/data/workspace/path/streams.cljs b/frontend/src/app/main/data/workspace/path/streams.cljs index 9dfd011f4d..99cf0f2980 100644 --- a/frontend/src/app/main/data/workspace/path/streams.cljs +++ b/frontend/src/app/main/data/workspace/path/streams.cljs @@ -2,9 +2,6 @@ ;; 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.data.workspace.path.streams diff --git a/frontend/src/app/main/data/workspace/path/tools.cljs b/frontend/src/app/main/data/workspace/path/tools.cljs index 850e925e2b..90ea6a1913 100644 --- a/frontend/src/app/main/data/workspace/path/tools.cljs +++ b/frontend/src/app/main/data/workspace/path/tools.cljs @@ -2,9 +2,6 @@ ;; 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.data.workspace.path.tools diff --git a/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs b/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs index 8eb346a93a..85e4ff2200 100644 --- a/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs @@ -2,9 +2,6 @@ ;; 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 diff --git a/frontend/src/app/main/ui/workspace/viewport/snap_path.cljs b/frontend/src/app/main/ui/workspace/viewport/snap_path.cljs index 191756430f..22f724c9fe 100644 --- a/frontend/src/app/main/ui/workspace/viewport/snap_path.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/snap_path.cljs @@ -2,10 +2,7 @@ ;; 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 +;; Copyright (c) UXBOX Labs SL (ns app.main.ui.workspace.viewport.snap-path (:require From de8207c5a63ce91cd3d7d1674e389d1bfb57dd52 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 12 Apr 2021 18:22:47 +0200 Subject: [PATCH 12/17] :sparkles: Snap on paths --- .../app/main/data/workspace/path/streams.cljs | 27 +++--- frontend/src/app/main/snap.cljs | 90 +++++-------------- .../main/ui/workspace/shapes/path/editor.cljs | 50 +++++------ 3 files changed, 56 insertions(+), 111 deletions(-) diff --git a/frontend/src/app/main/data/workspace/path/streams.cljs b/frontend/src/app/main/data/workspace/path/streams.cljs index 99cf0f2980..4f0b2a5ef0 100644 --- a/frontend/src/app/main/data/workspace/path/streams.cljs +++ b/frontend/src/app/main/data/workspace/path/streams.cljs @@ -86,23 +86,26 @@ (defn position-stream [points] (let [zoom (get-in @st/state [:workspace-local :zoom] 1) - ranges (snap/create-ranges points) + ;; ranges (snap/create-ranges points) d-pos (/ snap/snap-accuracy zoom) get-content (fn [state] (get-in state (state/get-path state :content))) - content-stream (-> (l/derived get-content st/state) - (rx/from-atom)) - ] - - (->> content-stream - (rx/map ugp/content->points) - (rx/subs #(prn "????" %))) + content-stream + (-> (l/derived get-content st/state) + (rx/from-atom {:emit-current-value? true})) + ranges-stream + (->> content-stream + (rx/map ugp/content->points) + (rx/map snap/create-ranges))] (->> ms/mouse-position - (rx/tap #(prn "pos" %)) - (rx/map #(let [snap (snap/get-snap-delta [%] ranges d-pos)] - (prn ">>>" snap) - (gpt/add % snap))) + (rx/with-latest vector ranges-stream) + (rx/map (fn [[position ranges]] + (let [snap (snap/get-snap-delta [position] ranges d-pos)] + #_(prn ">>>" snap) + (gpt/add position snap)) + )) + (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/snap.cljs b/frontend/src/app/main/snap.cljs index 3831fb4f6d..414cc0a6af 100644 --- a/frontend/src/app/main/snap.cljs +++ b/frontend/src/app/main/snap.cljs @@ -267,33 +267,35 @@ (defn query-delta-point [ranges point precision] (let [query-coord - (fn [coord] + (fn [point coord] (let [pval (get point coord)] - #_(prn "..." (rt/range-query (get ranges coord) (- pval precision) (+ pval precision))) - (->> (rt/range-query (get ranges coord) (- pval precision) (+ pval precision)) - ;; We save the distance to the point and add the matching point to the points (mapv (fn [[value points]] - #_(prn "!! " value [(mth/abs (- value pval)) - (->> points (mapv #(vector point %)))]) [(mth/abs (- value pval)) (->> points (mapv #(vector point %)))])))))] - {:x (query-coord :x) - :y (query-coord :y)})) + {:x (query-coord point :x) + :y (query-coord point :y)})) -(defn merge-matches [matches other] - (let [merge-coord - (fn [matches other] - (prn "merge-coord" matches other) - (into {} - (map (fn [key] [key (d/concat [] (get matches key) (get other key))])) - (set/union (keys matches) (keys other))))] +(defn merge-matches + ([] {:x nil :y nil}) + ([matches other] + (let [merge-coord + (fn [matches other] + + (let [matches (into {} matches) + other (into {} other) + keys (set/union (keys matches) (keys other))] + (into {} + (map (fn [key] + [key + (d/concat [] (get matches key []) (get other key []))])) + keys)))] - (-> matches - (update :x merge-matches (:x other)) - (update :y merge-matches (:y other))))) + (-> matches + (update :x merge-coord (:x other)) + (update :y merge-coord (:y other)))))) (defn min-match [default matches] @@ -329,55 +331,3 @@ (update :y first) (gpt/point))) -#_(defn path-snap-points-delta [points-stream selected-points points zoom] - - (let [ranges (create-ranges points selected-points) - d-pos (/ snap-accuracy zoom)] - - (->> points-stream - (rx/map (fn [points] - (get-snap-delta points ranges d-pos))))) - - - ) - -#_(defn path-snap [position-stream points selected-points zoom] - (let [ranges (create-ranges points selected-points) - d-pos (/ snap-accuracy zoom)] - - (->> position-stream - (rx/map (fn [position] - (gpt/add - position - (get-snap-delta position ranges d-pos))))))) - - -#_(defn path-snap [position-stream points selected-points zoom] - (let [selected-points (or selected-points #{}) - into-tree (fn [coord] - (fn [tree point] - (rt/insert tree (get point coord) point))) - - ranges-x (->> points - (filter (comp not selected-points)) - (reduce (into-tree :x) (rt/make-tree))) - - ranges-y (->> points - (filter (comp not selected-points)) - (reduce (into-tree :y) (rt/make-tree))) - - min-match (fn [matches] - (->> matches - (reduce (fn [[cur-val :as current] [other-val :as other]] - (if (< cur-val other-val) - current - other)))))] - - (->> position-stream - (rx/map - (fn [{:keys [x y]}] - (let [d-pos (/ snap-accuracy zoom) - x-match (rt/range-query ranges-x (- x d-pos) (+ x d-pos)) - y-match (rt/range-query ranges-y (- y d-pos) (+ y d-pos))] - {:x (min-match x-match) - :y (min-match y-match)})))))) 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 4299e681be..90a7547871 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -145,15 +145,19 @@ :preview? true :zoom zoom}]]) -(mf/defc snap-path-points [{:keys [snaps zoom]}] - [:g.snap-paths - (for [[from to] snaps] - [:line {:x1 (:x from) - :y1 (:y from) - :x2 (:x to) - :y2 (:y to) - :style {:stroke pc/secondary-color - :stroke-width (/ 1 zoom)}}])]) +(mf/defc snap-points [{:keys [selected points zoom]}] + (let [ranges (mf/use-memo (mf/deps selected points) #(snap/create-ranges points selected)) + snap-matches (snap/get-snap-delta-match selected ranges (/ 1 zoom)) + matches (d/concat [] (second (:x snap-matches)) (second (:y snap-matches)))] + + [:g.snap-paths + (for [[from to] matches] + [:line {:x1 (:x from) + :y1 (:y from) + :x2 (:x to) + :y2 (:y to) + :style {:stroke pc/secondary-color + :stroke-width (/ 1 zoom)}}])])) (mf/defc path-editor [{:keys [shape zoom]}] @@ -193,28 +197,16 @@ #(doseq [key keys] (events/unlistenByKey key))))) - #_(hooks/use-stream - ms/mouse-position - (mf/deps shape) - (fn [position] - (reset! hover-point (gshp/path-closest-point shape position)))) - - #_(hooks/use-stream - (mf/use-memo - (mf/deps base-content selected-points zoom) - #(snap/path-snap ms/mouse-position points selected-points zoom)) - - (fn [result] - (prn "??" result))) - [:g.path-editor {:ref editor-ref} - #_[:& snap-points {}] - - (when (and preview (not drag-handler)) - [:& path-preview {:command preview - :from last-p - :zoom zoom}]) + [:* + [:& snap-points {:selected #{(ugp/command->point preview)} + :points points + :zoom zoom}] + + [:& path-preview {:command preview + :from last-p + :zoom zoom}]]) (when @hover-point [:g.hover-point From f396ef4fa0c86f2b790d2bd7b96503fb9117ee72 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 13 Apr 2021 17:44:26 +0200 Subject: [PATCH 13/17] :sparkles: Snap for moving path nodes and handlers --- .../src/app/main/data/workspace/path.cljs | 2 +- .../app/main/data/workspace/path/edition.cljs | 20 ++--- .../main/data/workspace/path/selection.cljs | 10 --- .../app/main/data/workspace/path/streams.cljs | 41 +++++----- frontend/src/app/main/snap.cljs | 6 +- .../main/ui/workspace/shapes/path/editor.cljs | 77 +++++++++++-------- .../main/ui/workspace/viewport/actions.cljs | 2 +- 7 files changed, 83 insertions(+), 75 deletions(-) diff --git a/frontend/src/app/main/data/workspace/path.cljs b/frontend/src/app/main/data/workspace/path.cljs index 2a02ebe7ff..e4c04a5a71 100644 --- a/frontend/src/app/main/data/workspace/path.cljs +++ b/frontend/src/app/main/data/workspace/path.cljs @@ -24,8 +24,8 @@ (d/export edition/start-path-edit) ;; Selection -(d/export selection/select-handler) (d/export selection/handle-selection) +(d/export selection/select-node) (d/export selection/path-handler-enter) (d/export selection/path-handler-leave) (d/export selection/path-pointer-enter) diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index ff14d5d008..e5add80115 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -35,17 +35,20 @@ :x dx :y dy :c2x dx :c2y dy)))))) (defn modify-handler [id index prefix dx dy match-opposite?] - (ptk/reify ::modify-point + (ptk/reify ::modify-handler ptk/UpdateEvent (update [_ state] (let [content (get-in state (st/get-path state :content)) [cx cy] (if (= prefix :c1) [:c1x :c1y] [:c2x :c2y]) [ocx ocy] (if (= prefix :c1) [:c2x :c2y] [:c1x :c1y]) + point (gpt/point (+ (get-in content [index :params cx]) dx) + (+ (get-in content [index :params cy]) dy)) opposite-index (ugp/opposite-index content index prefix)] (cond-> state :always - (update-in [:workspace-local :edit-path id :content-modifiers index] assoc - cx dx cy dy) + (-> (update-in [:workspace-local :edit-path id :content-modifiers index] assoc + cx dx cy dy) + (assoc-in [:workspace-local :edit-path id :moving-handler] point)) (and match-opposite? opposite-index) (update-in [:workspace-local :edit-path id :content-modifiers opposite-index] assoc @@ -63,7 +66,7 @@ [rch uch] (changes/generate-path-changes page-id shape (:content shape) new-content)] (rx/of (dwc/commit-changes rch uch {:commit-local? true}) - (fn [state] (update-in state [:workspace-local :edit-path id] dissoc :content-modifiers))))))) + (fn [state] (update-in state [:workspace-local :edit-path id] dissoc :content-modifiers :moving-nodes :moving-handler))))))) (defn move-selected-path-point [from-point to-point] (letfn [(modify-content-point [content {dx :x dy :y} modifiers point] @@ -101,7 +104,9 @@ modifiers (->> points (reduce modifiers-reducer {}))] - (assoc-in state [:workspace-local :edit-path id :content-modifiers] modifiers)))))) + (-> state + (assoc-in [:workspace-local :edit-path id :moving-nodes] true) + (assoc-in [:workspace-local :edit-path id :content-modifiers] modifiers))))))) (defn start-move-path-point [position shift?] @@ -120,11 +125,6 @@ mouse-drag-stream (rx/concat - ;; If we're dragging a selected item we don't change the selection - (if selected? - (rx/empty) - (rx/of (selection/select-node position shift?))) - ;; This stream checks the consecutive mouse positions to do the draging (->> points (streams/move-points-stream start-position selected-points) diff --git a/frontend/src/app/main/data/workspace/path/selection.cljs b/frontend/src/app/main/data/workspace/path/selection.cljs index 99ea74b17a..c9ef72394a 100644 --- a/frontend/src/app/main/data/workspace/path/selection.cljs +++ b/frontend/src/app/main/data/workspace/path/selection.cljs @@ -28,15 +28,6 @@ (let [id (st/get-path-id state)] (update-in state [:workspace-local :edit-path id :hover-points] disj position))))) -(defn select-handler [index type] - (ptk/reify ::select-handler - ptk/UpdateEvent - (update [_ state] - (let [id (get-in state [:workspace-local :edition])] - (-> state - (update-in [:workspace-local :edit-path id :selected-handlers] (fnil conj #{}) [index type])))))) - - (defn path-handler-enter [index prefix] (ptk/reify ::path-handler-enter ptk/UpdateEvent @@ -115,7 +106,6 @@ (update [_ state] (let [id (st/get-path-id state)] (-> state - (assoc-in [:workspace-local :edit-path id :selected-handlers] #{}) (assoc-in [:workspace-local :edit-path id :selected-points] #{})))))) (defn update-area-selection diff --git a/frontend/src/app/main/data/workspace/path/streams.cljs b/frontend/src/app/main/data/workspace/path/streams.cljs index 4f0b2a5ef0..7251096e68 100644 --- a/frontend/src/app/main/data/workspace/path/streams.cljs +++ b/frontend/src/app/main/data/workspace/path/streams.cljs @@ -58,36 +58,39 @@ [start-point selected-points points] (let [zoom (get-in @st/state [:workspace-local :zoom] 1) - ranges (snap/create-ranges selected-points points) - d-pos (/ snap/snap-accuracy zoom)] + ranges (snap/create-ranges points selected-points) + d-pos (/ snap/snap-path-accuracy zoom) + + check-path-snap + (fn [position] + (let [delta (gpt/subtract position start-point) + moved-points (->> selected-points (mapv #(gpt/add % delta))) + snap (snap/get-snap-delta moved-points ranges d-pos)] + (gpt/add position snap)))] (->> ms/mouse-position - (rx/map (fn [position] - (let [delta (gpt/subtract position start-point) - moved-points (->> selected-points (mapv #(gpt/add % delta)))] - (gpt/add - position - (snap/get-snap-delta moved-points ranges d-pos))))))) - ) + (rx/map check-path-snap)))) (defn move-handler-stream [start-point handler points] (let [zoom (get-in @st/state [:workspace-local :zoom] 1) ranges (snap/create-ranges points) - d-pos (/ snap/snap-accuracy zoom)] + d-pos (/ snap/snap-path-accuracy zoom) + + check-path-snap + (fn [position] + (let [delta (gpt/subtract position start-point) + handler-position (gpt/add handler delta) + snap (snap/get-snap-delta [handler-position] ranges d-pos)] + (gpt/add position snap)))] (->> ms/mouse-position - (rx/map (fn [position] - (let [delta (gpt/subtract position start-point) - handler-position (gpt/add handler delta)] - (gpt/add - position - (snap/get-snap-delta [handler-position] ranges d-pos)))))))) + (rx/map check-path-snap)))) (defn position-stream [points] (let [zoom (get-in @st/state [:workspace-local :zoom] 1) ;; ranges (snap/create-ranges points) - d-pos (/ snap/snap-accuracy zoom) + d-pos (/ snap/snap-path-accuracy zoom) get-content (fn [state] (get-in state (state/get-path state :content))) content-stream @@ -103,9 +106,7 @@ (rx/with-latest vector ranges-stream) (rx/map (fn [[position ranges]] (let [snap (snap/get-snap-delta [position] ranges d-pos)] - #_(prn ">>>" snap) - (gpt/add position snap)) - )) + (gpt/add position snap)))) (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/snap.cljs b/frontend/src/app/main/snap.cljs index 414cc0a6af..3e7ebd8709 100644 --- a/frontend/src/app/main/snap.cljs +++ b/frontend/src/app/main/snap.cljs @@ -20,6 +20,7 @@ [clojure.set :as set])) (defonce ^:private snap-accuracy 5) +(defonce ^:private snap-path-accuracy 10) (defonce ^:private snap-distance-accuracy 10) (defn- remove-from-snap-points @@ -272,9 +273,8 @@ (->> (rt/range-query (get ranges coord) (- pval precision) (+ pval precision)) ;; We save the distance to the point and add the matching point to the points (mapv (fn [[value points]] - [(mth/abs (- value pval)) + [(- value pval) (->> points (mapv #(vector point %)))])))))] - {:x (query-coord point :x) :y (query-coord point :y)})) @@ -301,7 +301,7 @@ [default matches] (let [get-min (fn [[cur-val :as current] [other-val :as other]] - (if (< cur-val other-val) + (if (< (mth/abs cur-val) (mth/abs other-val)) current other)) 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 90a7547871..406d4bd58c 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -19,6 +19,7 @@ [app.util.dom :as dom] [app.util.geom.path :as ugp] [app.util.keyboard :as kbd] + [clojure.set :refer [map-invert]] [goog.events :as events] [rumext.alpha :as mf]) (:import goog.events.EventType)) @@ -42,7 +43,10 @@ (let [shift? (kbd/shift? event)] (cond (= edit-mode :move) - (st/emit! (drp/start-move-path-point position shift?)) + ;; If we're dragging a selected item we don't change the selection + (do (when (not selected?) + (st/emit! (drp/select-node position shift?))) + (st/emit! (drp/start-move-path-point position shift?))) (and (= edit-mode :draw) start-path?) (st/emit! (drp/start-path-from-point position)) @@ -84,14 +88,6 @@ (fn [event] (st/emit! (drp/path-handler-leave index prefix))) - on-click - (fn [event] - (dom/stop-propagation event) - (dom/prevent-default event) - (cond - (= edit-mode :move) - (drp/select-handler index prefix))) - on-mouse-down (fn [event] (dom/stop-propagation event) @@ -123,7 +119,6 @@ [:circle {:cx x :cy y :r (/ 10 zoom) - :on-click on-click :on-mouse-down on-mouse-down :on-mouse-enter on-enter :on-mouse-leave on-leave @@ -145,7 +140,7 @@ :preview? true :zoom zoom}]]) -(mf/defc snap-points [{:keys [selected points zoom]}] +(mf/defc path-snap [{:keys [selected points zoom]}] (let [ranges (mf/use-memo (mf/deps selected points) #(snap/create-ranges points selected)) snap-matches (snap/get-snap-delta-match selected ranges (/ 1 zoom)) matches (d/concat [] (second (:x snap-matches)) (second (:y snap-matches)))] @@ -172,19 +167,41 @@ preview content-modifiers last-point - selected-handlers selected-points + moving-nodes + moving-handler hover-handlers hover-points] :as edit-path} (mf/deref edit-path-ref) - {base-content :content} shape + selected-points (or selected-points #{}) + + base-content (:content shape) + base-points (mf/use-memo (mf/deps base-content) #(->> base-content ugp/content->points)) + content (ugp/apply-content-modifiers base-content content-modifiers) - points (mf/use-memo (mf/deps content) #(->> content ugp/content->points (into #{}))) + content-points (mf/use-memo (mf/deps content) #(->> content ugp/content->points)) + + point->base (->> (map hash-map content-points base-points) (reduce merge)) + base->point (map-invert point->base) + + points (into #{} content-points) + last-command (last content) last-p (->> content last ugp/command->point) handlers (ugp/content->handlers content) + [snap-selected snap-points] + (cond + (some? drag-handler) [#{drag-handler} points] + (some? preview) [#{(ugp/command->point preview)} points] + (some? moving-handler) [#{moving-handler} points] + :else + [(->> selected-points (map base->point) (into #{})) + (->> points (remove selected-points) (into #{}))]) + + show-snap? (or (some? drag-handler) (some? preview) (some? moving-handler) moving-nodes) + handle-double-click-outside (fn [event] (when (= edit-mode :move) @@ -199,13 +216,14 @@ [:g.path-editor {:ref editor-ref} (when (and preview (not drag-handler)) - [:* - [:& snap-points {:selected #{(ugp/command->point preview)} - :points points - :zoom zoom}] + [:& path-preview {:command preview + :from last-p + :zoom zoom}]) - [:& path-preview {:command preview - :from last-p + (when drag-handler + [:g.drag-handler {:pointer-events "none"} + [:& path-handler {:point last-p + :handler drag-handler :zoom zoom}]]) (when @hover-point @@ -214,10 +232,11 @@ :zoom zoom}]]) (for [position points] - (let [point-selected? (contains? selected-points position) - point-hover? (contains? hover-points position) - last-p? (= last-point position) + (let [point-selected? (contains? selected-points (get point->base position)) + point-hover? (contains? hover-points (get point->base position)) + last-p? (= last-point (get point->base position)) 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)] @@ -225,7 +244,6 @@ x (get-in command [:params (d/prefix-keyword prefix :x)]) y (get-in command [:params (d/prefix-keyword prefix :y)]) handler-position (gpt/point x y) - handler-selected? (contains? selected-handlers [index prefix]) handler-hover? (contains? hover-handlers [index prefix])] (when (not= position handler-position) [:& path-handler {:point position @@ -233,7 +251,6 @@ :index index :prefix prefix :zoom zoom - :selected? handler-selected? :hover? handler-hover? :edit-mode edit-mode}])))] [:& path-point {:position position @@ -250,9 +267,9 @@ :handler prev-handler :zoom zoom}]]) - (when drag-handler - [:g.drag-handler {:pointer-events "none"} - [:& path-handler {:point last-p - :handler drag-handler - :zoom zoom}]])])) + (when show-snap? + [:g.path-snap {:pointer-events "none"} + [:& path-snap {:selected snap-selected + :points snap-points + :zoom zoom}]])])) diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index 2cea72660a..6b886dbe99 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -59,7 +59,7 @@ (when (and (not= edition id) text-editing?) (st/emit! dw/clear-edition-mode)) - (when (and (or (not edition) (not= edition id)) + (when (and (not text-editing?) (not blocked) (not hidden) (not (#{:comments :path} drawing-tool)) From 74f99f0d48e3cca99829627a3ca9db22d6ed5c6f Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 14 Apr 2021 10:50:30 +0200 Subject: [PATCH 14/17] :sparkles: Toggle snap on button --- .../app/main/data/workspace/path/drawing.cljs | 22 +++++++++---- .../app/main/data/workspace/path/edition.cljs | 8 +++-- .../app/main/data/workspace/path/streams.cljs | 32 +++++++++++-------- .../main/ui/workspace/shapes/path/editor.cljs | 5 +-- 4 files changed, 42 insertions(+), 25 deletions(-) diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index 781e3fffaa..f4621fbc83 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -131,10 +131,11 @@ (ms/mouse-up? %)))) content (get-in state (st/get-path state :content)) + snap-toggled (get-in state [:workspace-local :edit-path id :snap-toggled]) points (ugp/content->points content) drag-events-stream - (->> (streams/position-stream points) + (->> (streams/position-stream snap-toggled points) (rx/take-until stop-stream) (rx/map #(drag-handler %)))] @@ -166,7 +167,10 @@ content (get-in state (st/get-path state :content)) points (ugp/content->points content) - drag-events (->> (streams/position-stream points) + id (st/get-path-id state) + snap-toggled (get-in state [:workspace-local :edit-path id :snap-toggled]) + + drag-events (->> (streams/position-stream snap-toggled points) (rx/take-until mouse-up) (rx/map #(drag-handler %)))] @@ -186,10 +190,11 @@ (rx/merge-map #(rx/empty)))) (defn make-drag-stream - [stream down-event zoom points] + [stream snap-toggled zoom points down-event] (let [mouse-up (->> stream (rx/filter #(or (helpers/end-path-event? %) (ms/mouse-up? %)))) - drag-events (->> (streams/position-stream points) + + drag-events (->> (streams/position-stream snap-toggled points) (rx/take-until mouse-up) (rx/map #(drag-handler %)))] @@ -219,9 +224,12 @@ content (get-in state (st/get-path state :content)) points (ugp/content->points content) + id (st/get-path-id state) + snap-toggled (get-in state [:workspace-local :edit-path id :snap-toggled]) + ;; Mouse move preview mousemove-events - (->> (streams/position-stream points) + (->> (streams/position-stream snap-toggled points) (rx/take-until end-path-events) (rx/map #(preview-next-point %))) @@ -229,12 +237,12 @@ mousedown-events (->> mouse-down (rx/take-until end-path-events) - (rx/with-latest merge (streams/position-stream points)) + (rx/with-latest merge (streams/position-stream snap-toggled points)) ;; We change to the stream that emits the first event (rx/switch-map #(rx/race (make-node-events-stream stream) - (make-drag-stream stream % zoom points))))] + (make-drag-stream stream snap-toggled zoom points %))))] (rx/concat (rx/of (common/init-path)) diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index e5add80115..0443d2b66d 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -118,6 +118,7 @@ zoom (get-in state [:workspace-local :zoom]) id (get-in state [:workspace-local :edition]) selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) + snap-toggled (get-in state [:workspace-local :edit-path id :snap-toggled]) selected? (contains? selected-points position) content (get-in state (st/get-path state :content)) @@ -127,7 +128,7 @@ (rx/concat ;; This stream checks the consecutive mouse positions to do the draging (->> points - (streams/move-points-stream start-position selected-points) + (streams/move-points-stream snap-toggled start-position selected-points) (rx/take-until stopper) (rx/map #(move-selected-path-point start-position %))) (rx/of (apply-content-modifiers))) @@ -162,11 +163,12 @@ handler (-> content (get index) (ugp/get-handler prefix)) current-distance (when opposite-handler (gpt/distance (ugp/opposite-handler point handler) opposite-handler)) - match-opposite? (and opposite-handler (mth/almost-zero? current-distance))] + match-opposite? (and opposite-handler (mth/almost-zero? current-distance)) + snap-toggled (get-in state [:workspace-local :edit-path id :snap-toggled])] (streams/drag-stream (rx/concat - (->> (streams/move-handler-stream start-point handler points) + (->> (streams/move-handler-stream snap-toggled start-point handler points) (rx/take-until (->> stream (rx/filter ms/mouse-up?))) (rx/map (fn [{:keys [x y alt? shift?]}] diff --git a/frontend/src/app/main/data/workspace/path/streams.cljs b/frontend/src/app/main/data/workspace/path/streams.cljs index 7251096e68..f7c7a83c22 100644 --- a/frontend/src/app/main/data/workspace/path/streams.cljs +++ b/frontend/src/app/main/data/workspace/path/streams.cljs @@ -55,7 +55,7 @@ (* (mth/floor (/ num k)) k))) (defn move-points-stream - [start-point selected-points points] + [snap-toggled start-point selected-points points] (let [zoom (get-in @st/state [:workspace-local :zoom] 1) ranges (snap/create-ranges points selected-points) @@ -63,15 +63,17 @@ check-path-snap (fn [position] - (let [delta (gpt/subtract position start-point) - moved-points (->> selected-points (mapv #(gpt/add % delta))) - snap (snap/get-snap-delta moved-points ranges d-pos)] - (gpt/add position snap)))] + (if snap-toggled + (let [delta (gpt/subtract position start-point) + moved-points (->> selected-points (mapv #(gpt/add % delta))) + snap (snap/get-snap-delta moved-points ranges d-pos)] + (gpt/add position snap)) + position))] (->> ms/mouse-position (rx/map check-path-snap)))) (defn move-handler-stream - [start-point handler points] + [snap-toggled start-point handler points] (let [zoom (get-in @st/state [:workspace-local :zoom] 1) ranges (snap/create-ranges points) @@ -79,15 +81,17 @@ check-path-snap (fn [position] - (let [delta (gpt/subtract position start-point) - handler-position (gpt/add handler delta) - snap (snap/get-snap-delta [handler-position] ranges d-pos)] - (gpt/add position snap)))] + (if snap-toggled + (let [delta (gpt/subtract position start-point) + handler-position (gpt/add handler delta) + snap (snap/get-snap-delta [handler-position] ranges d-pos)] + (gpt/add position snap)) + position))] (->> ms/mouse-position (rx/map check-path-snap)))) (defn position-stream - [points] + [snap-toggled points] (let [zoom (get-in @st/state [:workspace-local :zoom] 1) ;; ranges (snap/create-ranges points) d-pos (/ snap/snap-path-accuracy zoom) @@ -105,8 +109,10 @@ (->> ms/mouse-position (rx/with-latest vector ranges-stream) (rx/map (fn [[position ranges]] - (let [snap (snap/get-snap-delta [position] ranges d-pos)] - (gpt/add position snap)))) + (if snap-toggled + (let [snap (snap/get-snap-delta [position] ranges d-pos)] + (gpt/add position snap)) + position))) (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/ui/workspace/shapes/path/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs index 406d4bd58c..3c8e37e9ad 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -171,7 +171,8 @@ moving-nodes moving-handler hover-handlers - hover-points] + hover-points + snap-toggled] :as edit-path} (mf/deref edit-path-ref) selected-points (or selected-points #{}) @@ -200,7 +201,7 @@ [(->> selected-points (map base->point) (into #{})) (->> points (remove selected-points) (into #{}))]) - show-snap? (or (some? drag-handler) (some? preview) (some? moving-handler) moving-nodes) + show-snap? (and snap-toggled (or (some? drag-handler) (some? preview) (some? moving-handler) moving-nodes)) handle-double-click-outside (fn [event] From 48ba80c6e2948f699ed95588df66553f10c595e7 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 14 Apr 2021 10:57:13 +0200 Subject: [PATCH 15/17] :books: Updated changelog --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 7efcc54743..3eb09081cf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ - Add integration with gitpod.io (an online IDE) [#807](https://github.com/penpot/penpot/pull/807) - Allow to group assets (components and graphics) [Taiga #1289](https://tree.taiga.io/project/penpot/us/1289) - Internal: refactor of http client, replace internal xhr usage with more modern Fetch API. +- New features for paths: snap points on edition, add/remove nodes, merge/join/split nodes. [Taiga #907](https://tree.taiga.io/project/penpot/us/907) ### :bug: Bugs fixed From 07799d9b019a3825a37e541534ed83074d6760d1 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 14 Apr 2021 11:04:00 +0200 Subject: [PATCH 16/17] :sparkles: Path improvements --- .../src/app/main/ui/workspace/viewport.cljs | 6 - .../main/ui/workspace/viewport/snap_path.cljs | 188 ------------------ 2 files changed, 194 deletions(-) delete mode 100644 frontend/src/app/main/ui/workspace/viewport/snap_path.cljs diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index fda4afe53b..3a0c422806 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -27,7 +27,6 @@ [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] @@ -284,11 +283,6 @@ :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 diff --git a/frontend/src/app/main/ui/workspace/viewport/snap_path.cljs b/frontend/src/app/main/ui/workspace/viewport/snap_path.cljs deleted file mode 100644 index 22f724c9fe..0000000000 --- a/frontend/src/app/main/ui/workspace/viewport/snap_path.cljs +++ /dev/null @@ -1,188 +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.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}]))) From 74a09301a70fe661496eb997c788e259ae3a84e4 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 14 Apr 2021 13:24:55 +0200 Subject: [PATCH 17/17] :sparkles: Shift+select path nodes --- .../app/main/data/workspace/path/edition.cljs | 56 ++++++++++++------- .../main/data/workspace/path/selection.cljs | 35 ++++++++++-- .../main/ui/workspace/shapes/path/editor.cljs | 4 +- 3 files changed, 66 insertions(+), 29 deletions(-) diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index 0443d2b66d..0d07756ba2 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -62,10 +62,18 @@ page-id (:current-page-id state) shape (get-in state (st/get-path state)) content-modifiers (get-in state [:workspace-local :edit-path id :content-modifiers]) - new-content (ugp/apply-content-modifiers (:content shape) content-modifiers) + + content (:content shape) + new-content (ugp/apply-content-modifiers content content-modifiers) + + old-points (->> content ugp/content->points) + new-points (->> new-content ugp/content->points) + point-change (->> (map hash-map old-points new-points) (reduce merge)) + [rch uch] (changes/generate-path-changes page-id shape (:content shape) new-content)] (rx/of (dwc/commit-changes rch uch {:commit-local? true}) + (selection/update-selection point-change) (fn [state] (update-in state [:workspace-local :edit-path id] dissoc :content-modifiers :moving-nodes :moving-handler))))))) (defn move-selected-path-point [from-point to-point] @@ -108,36 +116,44 @@ (assoc-in [:workspace-local :edit-path id :moving-nodes] true) (assoc-in [:workspace-local :edit-path id :content-modifiers] modifiers))))))) +(declare drag-selected-points) + (defn start-move-path-point [position shift?] (ptk/reify ::start-move-path-point ptk/WatchEvent (watch [_ state stream] - (let [start-position @ms/mouse-position - stopper (->> stream (rx/filter ms/mouse-up?)) + (let [id (get-in state [:workspace-local :edition]) + selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) + selected? (contains? selected-points position)] + (streams/drag-stream + (rx/of + (when-not selected? (selection/select-node position shift? "drag")) + (drag-selected-points @ms/mouse-position)) + (rx/of (selection/select-node position shift? "click"))))))) + +(defn drag-selected-points + [start-position] + (ptk/reify ::drag-selected-points + ptk/WatchEvent + (watch [_ state stream] + (let [stopper (->> stream (rx/filter ms/mouse-up?)) zoom (get-in state [:workspace-local :zoom]) id (get-in state [:workspace-local :edition]) - selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) snap-toggled (get-in state [:workspace-local :edit-path id :snap-toggled]) - selected? (contains? selected-points position) + + selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) content (get-in state (st/get-path state :content)) - points (ugp/content->points content) + points (ugp/content->points content)] - mouse-drag-stream - (rx/concat - ;; This stream checks the consecutive mouse positions to do the draging - (->> points - (streams/move-points-stream snap-toggled start-position selected-points) - (rx/take-until stopper) - (rx/map #(move-selected-path-point start-position %))) - (rx/of (apply-content-modifiers))) - - ;; When there is not drag we select the node - mouse-click-stream - (rx/of (selection/select-node position shift?))] - - (streams/drag-stream mouse-drag-stream mouse-click-stream))))) + (rx/concat + ;; This stream checks the consecutive mouse positions to do the draging + (->> points + (streams/move-points-stream snap-toggled start-position selected-points) + (rx/take-until stopper) + (rx/map #(move-selected-path-point start-position %))) + (rx/of (apply-content-modifiers))))))) (defn start-move-handler [index prefix] diff --git a/frontend/src/app/main/data/workspace/path/selection.cljs b/frontend/src/app/main/data/workspace/path/selection.cljs index c9ef72394a..903954da38 100644 --- a/frontend/src/app/main/data/workspace/path/selection.cljs +++ b/frontend/src/app/main/data/workspace/path/selection.cljs @@ -49,9 +49,11 @@ (let [selrect (get-in state [:workspace-local :selrect]) id (get-in state [:workspace-local :edition]) content (get-in state (st/get-path state :content)) - selected-point? (fn [point] - (gsh/has-point-rect? selrect point)) - positions (into #{} + selected-point? #(gsh/has-point-rect? selrect %) + + selected-points (get-in state [:workspace-local :edit-path id :selected-points]) + + positions (into (if shift? selected-points #{}) (comp (map (comp gpt/point :params)) (filter selected-point?)) content)] @@ -59,14 +61,24 @@ (some? id) (assoc-in [:workspace-local :edit-path id :selected-points] positions)))))) -(defn select-node [position shift?] +(defn select-node [position shift? kk] (ptk/reify ::select-node ptk/UpdateEvent (update [_ state] - (let [id (get-in state [:workspace-local :edition])] + (let [id (get-in state [:workspace-local :edition]) + selected-points (or (get-in state [:workspace-local :edit-path id :selected-points]) #{}) + selected-points (cond + (and shift? (contains? selected-points position)) + (disj selected-points position) + + shift? + (conj selected-points position) + + :else + #{position})] (cond-> state (some? id) - (assoc-in [:workspace-local :edit-path id :selected-points] #{position})))))) + (assoc-in [:workspace-local :edit-path id :selected-points] selected-points)))))) (defn deselect-node [position shift?] (ptk/reify ::deselect-node @@ -142,3 +154,14 @@ (rx/of (select-node-area shift?) (clear-area-selection)))))))) + +(defn update-selection + [point-change] + (ptk/reify ::update-selection + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state) + selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) + selected-points (into #{} (map point-change) selected-points)] + (-> state + (assoc-in [:workspace-local :edit-path id :selected-points] selected-points)))))) 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 3c8e37e9ad..150e66b312 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -44,9 +44,7 @@ (cond (= edit-mode :move) ;; If we're dragging a selected item we don't change the selection - (do (when (not selected?) - (st/emit! (drp/select-node position shift?))) - (st/emit! (drp/start-move-path-point position shift?))) + (st/emit! (drp/start-move-path-point position shift?)) (and (= edit-mode :draw) start-path?) (st/emit! (drp/start-path-from-point position))