From 5c71601fcfb69758fd1a99484398ed1493677c5a Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 25 Nov 2020 17:10:39 +0100 Subject: [PATCH 1/3] :sparkles: Improves path drawing --- .../styles/main/partials/workspace.scss | 1 + .../app/main/data/workspace/drawing/path.cljs | 349 +++++++++++------- frontend/src/app/main/exports.cljs | 27 +- .../app/main/ui/workspace/shapes/path.cljs | 145 +++++--- .../src/app/main/ui/workspace/viewport.cljs | 2 +- frontend/src/app/util/dom.cljs | 4 + frontend/src/app/util/geom/path.cljs | 142 ++++++- 7 files changed, 460 insertions(+), 210 deletions(-) diff --git a/frontend/resources/styles/main/partials/workspace.scss b/frontend/resources/styles/main/partials/workspace.scss index d6119cd4a..3861a5d33 100644 --- a/frontend/resources/styles/main/partials/workspace.scss +++ b/frontend/resources/styles/main/partials/workspace.scss @@ -263,6 +263,7 @@ border-radius: 3px; svg { + pointer-events: none; width: 20px; height: 20px; } diff --git a/frontend/src/app/main/data/workspace/drawing/path.cljs b/frontend/src/app/main/data/workspace/drawing/path.cljs index 024e6baee..aed97a83e 100644 --- a/frontend/src/app/main/data/workspace/drawing/path.cljs +++ b/frontend/src/app/main/data/workspace/drawing/path.cljs @@ -9,10 +9,11 @@ (ns app.main.data.workspace.drawing.path (:require + [clojure.spec.alpha :as s] + [app.common.spec :as us] [beicon.core :as rx] [potok.core :as ptk] [app.common.math :as mth] - [app.common.data :as d] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.util.data :as ud] @@ -23,9 +24,50 @@ [app.main.data.workspace.drawing.common :as common] [app.common.geom.shapes.path :as gsp])) +;; 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?)) + +(s/def ::path-shape + (s/keys :req-un [::content])) + + ;; CONSTANTS (defonce enter-keycode 13) - +(defonce drag-threshold 2) ;; PRIVATE METHODS @@ -89,26 +131,64 @@ (update opposite-index assoc ocx (- dx) ocy (- dy))))) (defn end-path-event? [{:keys [type shift] :as event}] - (or (= event ::end-path) + (or (= (ptk/type event) ::finish-path) (= (ptk/type event) :esc-pressed) (= event :interrupt) ;; ESC + (and (ms/mouse-double-click? event)) (and (ms/keyboard-event? event) (= type :down) ;; TODO: Enter now finish path but can finish drawing/editing as well (= enter-keycode (:key event))))) +(defn generate-path-changes [page-id shape-id old-content new-content] + (us/verify ::content old-content) + (us/verify ::content new-content) + (let [old-selrect (gsh/content->selrect old-content) + old-points (gsh/rect->points old-selrect) + new-selrect (gsh/content->selrect new-content) + new-points (gsh/rect->points new-selrect) + + 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)))) ;; EVENTS -(defn init-path [id] +(defn init-path [] (ptk/reify ::init-path)) -(defn finish-path [id] +(defn finish-path [source] (ptk/reify ::finish-path ptk/UpdateEvent (update [_ state] - (-> state - (update-in [:workspace-local :edit-path id] dissoc :last-point :prev-handler :drag-handler :preview))))) + (let [id (get-path-id state)] + (-> state + (update-in [:workspace-local :edit-path id] clean-edit-state)))))) (defn preview-next-point [{:keys [x y]}] (ptk/reify ::preview-next-point @@ -118,7 +198,6 @@ position (gpt/point x y) 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))))) @@ -129,10 +208,13 @@ (let [id (get-path-id state) position (gpt/point x y) {:keys [last-point prev-handler]} (get-in state [:workspace-local :edit-path id])] - (-> state - (assoc-in [:workspace-local :edit-path id :last-point] position) - (update-in [:workspace-local :edit-path id] dissoc :prev-handler) - (update-in (get-path state) append-node position last-point prev-handler)))))) + (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 @@ -158,7 +240,6 @@ (ptk/reify ::drag-handler ptk/UpdateEvent (update [_ state] - (let [id (get-path-id state) handler-position (gpt/point x y) shape (get-in state (get-path state)) @@ -194,24 +275,15 @@ ;; Update the preview because can be outdated after the dragging (rx/of (preview-next-point handler)))))) -(defn close-path [position] - (ptk/reify ::close-path - ptk/WatchEvent - (watch [_ state stream] - (rx/of (add-node position) - ::end-path)))) +(declare close-path-drag-end) (defn close-path-drag-start [position] (ptk/reify ::close-path-drag-start ptk/WatchEvent (watch [_ state stream] - (let [zoom (get-in state [:workspace-local :zoom]) - threshold (/ 5 zoom) - check-if-dragging - (fn [current-position] - (let [start (gpt/point position) - current (gpt/point current-position)] - (>= (gpt/distance start current) 100))) + (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? %) @@ -228,25 +300,39 @@ (rx/concat - (rx/of (close-path position)) + (rx/of (add-node position)) (->> position-stream - (rx/filter check-if-dragging) + (rx/filter (dragging? start-position zoom)) (rx/take 1) (rx/merge-map #(rx/concat (rx/of (start-drag-handler)) drag-events-stream - (rx/of (finish-drag)))))))))) + (rx/of (finish-drag)) + (rx/of (close-path-drag-end))))) + (rx/of (finish-path "close-path"))))))) -(defn close-path-drag-end [position] - (ptk/reify ::close-path-drag-end)) +(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/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/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 @@ -261,8 +347,45 @@ (rx/concat (rx/of (add-node position)) (rx/of (start-drag-handler)) drag-events - (rx/of (finish-drag)))) - ))) + (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 id (: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 id (: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 @@ -270,38 +393,35 @@ [stream down-event] (->> stream (rx/filter ms/mouse-click?) - (rx/debounce 200) + #_(rx/debounce 200) (rx/first) (rx/map #(add-node down-event)))) (defn make-drag-stream - [stream down-event] + [stream down-event zoom] (let [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/timer 400) - (rx/merge-map #(rx/concat - (rx/of (add-node down-event)) - (rx/of (start-drag-handler)) - drag-events - (rx/of (finish-drag))))))) -(defn make-dbl-click-stream - [stream down-event] - (->> stream - (rx/filter ms/mouse-double-click?) - (rx/first) - (rx/merge-map - #(rx/of (add-node down-event) - ::end-path)))) + (rx/concat + (rx/of (add-node down-event)) + + (->> ms/mouse-position + (rx/take-until mouse-up) + (rx/filter (dragging? (gpt/point down-event) zoom)) + (rx/take 1) + (rx/merge-map + #(rx/concat + (rx/of (start-drag-handler)) + drag-events + (rx/of (finish-drag)))))))) (defn make-node-events-stream [stream] - (->> (rx/merge - (->> stream (rx/filter (ptk/type? ::close-path))) - (->> stream (rx/filter (ptk/type? ::close-path-drag-start)))) + (->> stream + (rx/filter (ptk/type? ::close-path-drag-start)) (rx/take 1) (rx/merge-map #(rx/empty)))) @@ -318,35 +438,32 @@ ptk/WatchEvent (watch [_ state stream] - (let [mouse-down (->> stream (rx/filter ms/mouse-down?)) + (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 (->> ms/mouse-position (rx/take-until end-path-events) - (rx/throttle 50) (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/throttle 50) (rx/with-latest merge ms/mouse-position) ;; We change to the stream that emits the first event (rx/switch-map #(rx/race (make-node-events-stream stream) - (make-click-stream stream %) - (make-drag-stream stream %) - (make-dbl-click-stream stream %))))] + (make-drag-stream stream % zoom))))] (rx/concat - (rx/of (init-path id)) + (rx/of (init-path)) (rx/merge mousemove-events mousedown-events) - (rx/of (finish-path id))))))) + (rx/of (finish-path "after-events"))))))) (defn stop-path-edit [] (ptk/reify ::stop-path-edit @@ -408,34 +525,12 @@ (ptk/reify ::apply-content-modifiers ptk/WatchEvent (watch [_ state stream] - (let [id (get-in state [:workspace-local :edition]) + (let [id (get-path-id state) page-id (:current-page-id state) - shape (get-in state [:workspace-data :pages-index page-id :objects id]) - {old-content :content old-selrect :selrect old-points :points} shape - content-modifiers (get-in state [:workspace-local :edit-path id :content-modifiers] {}) - new-content (ugp/apply-content-modifiers old-content content-modifiers) - new-selrect (gsh/content->selrect new-content) - new-points (gsh/rect->points new-selrect) - - rch [{:type :mod-obj - :id 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 [id]}] - - uch [{:type :mod-obj - :id 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 [id]}]] + 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 (: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))))))) @@ -445,33 +540,10 @@ ptk/WatchEvent (watch [_ state stream] (let [id (get-in state [:workspace-local :edition]) + shape (get-in state (get-path state)) page-id (:current-page-id state) old-content (get-in state [:workspace-local :edit-path id :old-content]) - old-selrect (gsh/content->selrect old-content) - old-points (gsh/rect->points old-content) - shape (get-in state [:workspace-data :pages-index page-id :objects id]) - {new-content :content new-selrect :selrect new-points :points} shape - - rch [{:type :mod-obj - :id 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 [id]}] - - uch [{:type :mod-obj - :id 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 [id]}]] - + [rch uch] (generate-path-changes page-id id old-content (:content shape))] (rx/of (dwc/commit-changes rch uch {:commit-local? true})))))) (declare start-draw-mode) @@ -486,9 +558,9 @@ (cond (not= content old-content) (rx/of (save-path-content) - (start-draw-mode)) + (start-draw-mode)) (= mode :draw) (rx/of :interrupt) - :else (rx/of (finish-path id))))))) + :else (rx/of (finish-path "changed-content"))))))) (defn move-path-point [start-point end-point] (ptk/reify ::move-point @@ -506,7 +578,7 @@ (let [point (ugp/command->point command)] (= point start-point))) - point-indices (->> (d/enumerate content) + point-indices (->> (cd/enumerate content) (filter command-for-point) (map first)) @@ -533,14 +605,25 @@ [position] (ptk/reify ::start-move-path-point ptk/WatchEvent - ;; TODO REWRITE (watch [_ state stream] - (let [stopper (->> stream (rx/filter ms/mouse-up?))] - (rx/concat - (->> ms/mouse-position - (rx/take-until stopper) - (rx/map #(move-path-point position %))) - (rx/of (apply-content-modifiers))))))) + (let [start-position @ms/mouse-position + stopper (->> stream (rx/filter ms/mouse-up?)) + zoom (get-in state [:workspace-local :zoom]) + + move-point-stream + (fn [] (rx/concat + (->> ms/mouse-position + (rx/take-until stopper) + (rx/map #(move-path-point position %))) + (rx/of (apply-content-modifiers))))] + + (->> ms/mouse-position + (rx/take-until stopper) + (rx/filter (dragging? start-position zoom)) + (rx/take 1) + (rx/merge-map #(move-point-stream))) + + )))) (defn start-move-handler [index prefix] @@ -604,7 +687,7 @@ (watch [_ state stream] (let [id (get-path-id state)] (cond - (and id (= :move mode)) (rx/of ::end-path) + (and id (= :move mode)) (rx/of (finish-path "change-edit-mode")) (and id (= :draw mode)) (rx/of (start-draw-mode)) :else (rx/empty)))))) @@ -614,7 +697,7 @@ (update [_ state] (let [id (get-in state [:workspace-local :edition])] (-> state - (update-in [:workspace-local :edit-path id :selected] (fnil conj #{}) [index type])))))) + (update-in [:workspace-local :edit-path id :selected-handlers] (fnil conj #{}) [index type])))))) (defn select-node [position] (ptk/reify ::select-node @@ -622,7 +705,7 @@ (update [_ state] (let [id (get-in state [:workspace-local :edition])] (-> state - (update-in [:workspace-local :edit-path id :selected-node] (fnil conj #{}) position)))))) + (assoc-in [:workspace-local :edit-path id :selected-points] #{position})))))) (defn deselect-node [position] (ptk/reify ::deselect-node @@ -630,7 +713,7 @@ (update [_ state] (let [id (get-in state [:workspace-local :edition])] (-> state - (update-in [:workspace-local :edit-path id :selected-node] (fnil disj #{}) position)))))) + (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 @@ -656,11 +739,21 @@ (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 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))) diff --git a/frontend/src/app/main/exports.cljs b/frontend/src/app/main/exports.cljs index 38721a84c..6a85274e6 100644 --- a/frontend/src/app/main/exports.cljs +++ b/frontend/src/app/main/exports.cljs @@ -11,6 +11,7 @@ "The main logic for SVG export functionality." (:require [rumext.alpha :as mf] + [cuerdas.core :as str] [app.common.uuid :as uuid] [app.common.pages :as cp] [app.common.pages-helpers :as cph] @@ -42,10 +43,15 @@ (defn- calculate-dimensions [{:keys [objects] :as data} vport] - (let [shapes (cph/select-toplevel-shapes objects {:include-frames? true})] - (->> (gsh/selection-rect shapes) - (gal/adjust-to-viewport vport) - #_(gsh/fix-invalid-rect-values)))) + (let [shapes (cph/select-toplevel-shapes objects {:include-frames? true}) + to-finite (fn [val fallback] (if (not (mth/finite? val)) fallback val)) + rect (->> (gsh/selection-rect shapes) + (gal/adjust-to-viewport vport))] + (-> rect + (update :x to-finite 0) + (update :y to-finite 0) + (update :width to-finite 10000) + (update :height to-finite 10000)))) (declare shape-wrapper-factory) @@ -93,21 +99,20 @@ :group [:> group-wrapper {:shape shape :frame frame}] nil)]))))) +(defn get-viewbox [{:keys [x y width height] :or {x 0 y 0 width 100 height 100}}] + (str/fmt "%s %s %s %s" x y width height)) + (mf/defc page-svg {::mf/wrap [mf/memo]} [{:keys [data width height] :as props}] (let [objects (:objects data) - vport {:width width :height height} - - dim (calculate-dimensions data vport) root (get objects uuid/zero) shapes (->> (:shapes root) (map #(get objects %))) - vbox (str (:x dim 0) " " - (:y dim 0) " " - (:width dim 100) " " - (:height dim 100)) + vport {:width width :height height} + dim (calculate-dimensions data vport) + vbox (get-viewbox dim) background-color (get-in data [:options :background] default-color) frame-wrapper (mf/use-memo diff --git a/frontend/src/app/main/ui/workspace/shapes/path.cljs b/frontend/src/app/main/ui/workspace/shapes/path.cljs index d7330361e..9106df8ae 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path.cljs @@ -10,6 +10,7 @@ (ns app.main.ui.workspace.shapes.path (:require [rumext.alpha :as mf] + [goog.events :as events] [okulary.core :as l] [app.util.data :as d] [app.util.dom :as dom] @@ -30,7 +31,8 @@ [app.util.geom.path :as ugp] [app.common.geom.point :as gpt] [app.main.ui.cursors :as cur] - [app.main.ui.icons :as i])) + [app.main.ui.icons :as i]) + (:import goog.events.EventType)) (def primary-color "#1FDEA7") (def secondary-color "#DB00FF") @@ -94,7 +96,7 @@ (mf/defc path-actions [{:keys [shape]}] (let [id (mf/deref refs/selected-edition) - {:keys [edit-mode selected snap-toggled] :as all} (mf/deref current-edit-path-ref)] + {:keys [edit-mode selected-points snap-toggled] :as all} (mf/deref current-edit-path-ref)] [:div.path-actions [:div.viewport-actions-group [:div.viewport-actions-entry {:class (when (= edit-mode :draw) "is-toggled") @@ -102,24 +104,28 @@ [: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]] [:div.viewport-actions-group - [:div.viewport-actions-entry {:class "is-disabled"} i/nodes-corner] - [:div.viewport-actions-entry {:class "is-disabled"} i/nodes-curve]] + [: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-group [:div.viewport-actions-entry {:class (when snap-toggled "is-toggled")} i/nodes-snap]]])) -(mf/defc path-point [{:keys [position zoom edit-mode hover? selected? preview? start-path?]}] +(mf/defc path-point [{:keys [position zoom edit-mode hover? selected? preview? start-path? last-p?]}] (let [{:keys [x y]} position on-enter @@ -132,35 +138,37 @@ on-click (fn [event] - (dom/stop-propagation event) - (dom/prevent-default event) + (when-not last-p? + (do (dom/stop-propagation event) + (dom/prevent-default event) - (cond - (and (= edit-mode :move) (not selected?)) - (st/emit! (drp/select-node position)) + (cond + (and (= edit-mode :move) (not selected?)) + (st/emit! (drp/select-node position)) - (and (= edit-mode :move) selected?) - (st/emit! (drp/deselect-node position)))) + (and (= edit-mode :move) selected?) + (st/emit! (drp/deselect-node position)))))) on-mouse-down (fn [event] - (dom/stop-propagation event) - (dom/prevent-default event) + (when-not last-p? + (do (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 {:cx x :cy y - :r (/ 3 zoom) + :r (if (or selected? hover?) (/ 3.5 zoom) (/ 3 zoom)) :style {:cursor (when (= edit-mode :draw) cur/pen-node) :stroke-width (/ 1 zoom) :stroke (cond (or selected? hover?) black-color @@ -173,11 +181,21 @@ :r (/ 10 zoom) :on-click on-click :on-mouse-down on-mouse-down + :on-mouse-enter on-enter + :on-mouse-leave on-leave :style {:fill "transparent"}}]])) (mf/defc path-handler [{:keys [index prefix point handler zoom selected? hover? edit-mode]}] (when (and point handler) (let [{:keys [x y]} handler + on-enter + (fn [event] + (st/emit! (drp/path-handler-enter index prefix))) + + on-leave + (fn [event] + (st/emit! (drp/path-handler-leave index prefix))) + on-click (fn [event] (dom/stop-propagation event) @@ -201,7 +219,7 @@ :y1 (:y point) :x2 x :y2 y - :style {:stroke gray-color + :style {:stroke (if hover? black-color gray-color) :stroke-width (/ 1 zoom)}}] [:rect {:x (- x (/ 3 zoom)) @@ -220,6 +238,8 @@ :r (/ 10 zoom) :on-click on-click :on-mouse-down on-mouse-down + :on-mouse-enter on-enter + :on-mouse-leave on-leave :style {:fill "transparent"}}]]))) (mf/defc path-preview [{:keys [zoom command from]}] @@ -239,17 +259,40 @@ (mf/defc path-editor [{:keys [shape zoom]}] - (let [edit-path-ref (make-edit-path-ref (:id shape)) - {:keys [edit-mode selected drag-handler prev-handler preview content-modifiers last-point]} (mf/deref edit-path-ref) + (let [editor-ref (mf/use-ref nil) + edit-path-ref (make-edit-path-ref (:id shape)) + {:keys [edit-mode + drag-handler + prev-handler + preview + content-modifiers + last-point + selected-handlers + selected-points + hover-handlers + hover-points]} (mf/deref edit-path-ref) {:keys [content]} shape - selected (or selected #{}) content (ugp/apply-content-modifiers content content-modifiers) points (->> content ugp/content->points (into #{})) last-command (last content) last-p (->> content last ugp/command->point) - handlers (ugp/content->handlers content)] + handlers (ugp/content->handlers content) - [:g.path-editor + 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)))))] + + (mf/use-layout-effect + (fn [] + (let [keys [(events/listen (dom/get-root) EventType.CLICK handle-click-outside)]] + #(doseq [key keys] + (events/unlistenByKey key))))) + + [:g.path-editor {:ref editor-ref} (when (and preview (not drag-handler)) [:& path-preview {:command preview :from last-p @@ -257,43 +300,41 @@ (for [position points] [:g.path-node - [:& path-point {:position position - :selected? false - :zoom zoom - :edit-mode edit-mode - :start-path? (nil? last-point)}] - [: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)] - [:& path-handler {:point position - :handler handler-position - :index index - :prefix prefix - :zoom zoom - :selected? false - :hover? false - :preview? false - :edit-mode edit-mode}]))]]) + (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)}]]) (when prev-handler [:g.prev-handler {:pointer-events "none"} [:& path-handler {:point last-p :handler prev-handler - :zoom zoom - :selected false}]]) + :zoom zoom}]]) (when drag-handler [:g.drag-handler {:pointer-events "none"} (when (not= :move-to (:command last-command)) [:& path-handler {:point last-p :handler (ugp/opposite-handler last-p drag-handler) - :zoom zoom - :selected false}]) + :zoom zoom}]) [:& path-handler {:point last-p :handler drag-handler - :zoom zoom - :selected false}]])])) + :zoom zoom}]])])) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index c469e240b..de145248a 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -294,7 +294,7 @@ on-double-click (mf/use-callback - (mf/deps edition edit-path) + (mf/deps drawing-path?) (fn [event] (dom/stop-propagation event) (let [ctrl? (kbd/ctrl? event) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index c5cdc9c09..11057bc29 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -236,3 +236,7 @@ (defn get-root [] (query js/document "#app")) + +(defn ^boolean class? [node class-name] + (let [class-list (.-classList ^js node)] + (.contains ^js class-list class-name))) diff --git a/frontend/src/app/util/geom/path.cljs b/frontend/src/app/util/geom/path.cljs index 601c62874..4e150f7a5 100644 --- a/frontend/src/app/util/geom/path.cljs +++ b/frontend/src/app/util/geom/path.cljs @@ -10,6 +10,7 @@ (ns app.util.geom.path (:require [cuerdas.core :as str] + [app.common.data :as cd] [app.util.data :as d] [app.common.data :as cd] [app.common.geom.point :as gpt] @@ -247,8 +248,10 @@ content))] (reduce apply-to-index content modifiers))) -(defn command->point [{{:keys [x y]} :params}] - (gpt/point x y)) +(defn command->point [command] + (when-not (nil? command) + (let [{{:keys [x y]} :params} command] + (gpt/point x y)))) (defn content->points [content] (->> content @@ -256,23 +259,37 @@ (remove nil?) (into []))) -(defn content->handlers [content] - (->> (d/with-prev content) ;; [cmd, prev] - (d/enumerate) ;; [idx [cmd, prev]] +(defn get-handler [{:keys [params] :as command} prefix] + (let [cx (d/prefix-keyword prefix :x) + cy (d/prefix-keyword prefix :y)] + (when (and command + (contains? params cx) + (contains? params cy)) + (gpt/point (get params cx) + (get params cy))))) - (mapcat (fn [[index [cur-cmd prev-cmd]]] - (if (and prev-cmd - (= :curve-to (:command cur-cmd))) +(defn content->handlers + "Retrieve a map where for every point will retrieve a list of + the handlers that are associated with that point. + point -> [[index, prefix]]" + [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) - pre-pos (command->point prev-cmd)] - [[pre-pos [index :c1]] - [cur-pos [index :c2]]]) + pre-pos (command->point pre-cmd)] + (-> [[pre-pos [index :c1]] + [cur-pos [index :c2]]])) []))) (group-by first) (cd/mapm #(mapv second %2)))) -(defn opposite-index [content index prefix] +(defn opposite-index + "Calculate sthe opposite index given a prefix and an index" + [content index prefix] (let [point (if (= prefix :c2) (command->point (nth content index)) (command->point (nth content (dec index)))) @@ -280,10 +297,99 @@ handlers (-> (content->handlers content) (get point)) - opposite-prefix (if (= prefix :c1) :c2 :c1) + opposite-prefix (if (= prefix :c1) :c2 :c1)] + (when (<= (count handlers) 2) + (->> handlers + (d/seek (fn [[index prefix]] (= prefix opposite-prefix))) + (first))))) - result (when (<= (count handlers) 2) - (->> handlers - (d/seek (fn [[index prefix]] (= prefix opposite-prefix))) - (first)))] - result)) +(defn remove-line-curves + "Remove all curves that have both handlers in the same position that the + beggining and end points. This makes them really line-to commands" + [content] + (let [with-prev (d/enumerate (d/with-prev content)) + process-command + (fn [content [index [command prev]]] + + (let [cur-point (command->point command) + pre-point (command->point prev) + handler-c1 (get-handler command :c1) + handler-c2 (get-handler command :c2)] + (if (and (= :curve-to (:command command)) + (= cur-point handler-c2) + (= pre-point handler-c1)) + (assoc content index {:command :line-to + :params cur-point}) + content)))] + + (reduce process-command content with-prev))) + +(defn make-corner-point + "Changes the content to make a point a 'corner'" + [content point] + (let [handlers (-> (content->handlers content) + (get point)) + change-content + (fn [content [index prefix]] + (let [cx (d/prefix-keyword prefix :x) + cy (d/prefix-keyword prefix :y)] + (-> content + (assoc-in [index :params cx] (:x point)) + (assoc-in [index :params cy] (:y point)))))] + (as-> content $ + (reduce change-content $ handlers) + (remove-line-curves $)))) + +(defn make-curve-point + "Changes the content to make the point a 'curve'. The handlers will be positioned + in the same vector that results from te previous->next points but with fixed length." + [content point] + (let [content-next (d/enumerate (d/with-prev-next content)) + + make-curve + (fn [command previous] + (if (= :line-to (:command command)) + (let [cur-point (command->point command) + pre-point (command->point previous)] + (-> command + (assoc :command :curve-to) + (assoc :params (make-curve-params cur-point pre-point)))) + command)) + + update-handler + (fn [command prefix handler] + (if (= :curve-to (:command command)) + (let [cx (d/prefix-keyword prefix :x) + cy (d/prefix-keyword prefix :y)] + (-> command + (assoc-in [:params cx] (:x handler)) + (assoc-in [:params cy] (:y handler)))) + command)) + + calculate-vector + (fn [point next prev] + (let [base-vector (if (or (nil? next) (nil? prev)) + (-> (gpt/to-vec point (or next prev)) + (gpt/normal-left)) + (gpt/to-vec next prev))] + (-> base-vector + (gpt/unit) + (gpt/multiply (gpt/point 100))))) + + redfn (fn [content [index [command prev next]]] + (if (= point (command->point command)) + (let [prev-point (if (= :move-to (:command command)) nil (command->point prev)) + next-point (if (= :move-to (:command next)) nil (command->point next)) + handler-vector (calculate-vector point next-point prev-point) + handler (gpt/add point handler-vector) + handler-opposite (gpt/add point (gpt/negate handler-vector))] + (-> content + (cd/update-when index make-curve prev) + (cd/update-when index update-handler :c2 handler) + (cd/update-when (inc index) make-curve command) + (cd/update-when (inc index) update-handler :c1 handler-opposite))) + + content))] + (as-> content $ + (reduce redfn $ content-next) + (remove-line-curves $)))) From 89f0f24707f41d704c273c6e9653d066571b2b39 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 25 Nov 2020 22:08:04 +0100 Subject: [PATCH 2/3] :sparkles: Fixed angle with shift in paths --- common/app/common/geom/point.cljc | 5 + .../app/main/data/workspace/drawing/path.cljs | 164 ++++++++++-------- .../app/main/ui/workspace/left_toolbar.cljs | 2 +- .../app/main/ui/workspace/shapes/path.cljs | 14 +- 4 files changed, 107 insertions(+), 78 deletions(-) diff --git a/common/app/common/geom/point.cljc b/common/app/common/geom/point.cljc index 1f97b3174..ef0139f21 100644 --- a/common/app/common/geom/point.cljc +++ b/common/app/common/geom/point.cljc @@ -55,6 +55,11 @@ ;;(assert (not (nil? y))) (Point. x y))) +(defn angle->point [{:keys [x y]} angle distance] + (point + (+ x (* distance (mth/cos angle))) + (- y (* distance (mth/sin angle))))) + (defn add "Returns the addition of the supplied value to both coordinates of the point as a new point." diff --git a/frontend/src/app/main/data/workspace/drawing/path.cljs b/frontend/src/app/main/data/workspace/drawing/path.cljs index aed97a83e..9e31ee460 100644 --- a/frontend/src/app/main/data/workspace/drawing/path.cljs +++ b/frontend/src/app/main/data/workspace/drawing/path.cljs @@ -20,6 +20,7 @@ [app.common.data :as cd] [app.util.geom.path :as ugp] [app.main.streams :as ms] + [app.main.store :as st] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.drawing.common :as common] [app.common.geom.shapes.path :as gsp])) @@ -67,7 +68,7 @@ ;; CONSTANTS (defonce enter-keycode 13) -(defonce drag-threshold 2) +(defonce drag-threshold 5) ;; PRIVATE METHODS @@ -96,6 +97,25 @@ points (gsh/rect->points selrect)] (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] @@ -177,6 +197,21 @@ (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 [] @@ -190,24 +225,31 @@ (-> state (update-in [:workspace-local :edit-path id] clean-edit-state)))))) -(defn preview-next-point [{:keys [x y]}] +(defn preview-next-point [{:keys [x y shift?]}] (ptk/reify ::preview-next-point ptk/UpdateEvent (update [_ state] (let [id (get-path-id state) - position (gpt/point x y) + 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]}] +(defn add-node [{:keys [x y shift?]}] (ptk/reify ::add-node ptk/UpdateEvent (update [_ state] (let [id (get-path-id state) - position (gpt/point x y) - {:keys [last-point prev-handler]} (get-in state [:workspace-local :edit-path id])] + 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) @@ -301,16 +343,12 @@ (rx/concat (rx/of (add-node position)) - - (->> position-stream - (rx/filter (dragging? start-position zoom)) - (rx/take 1) - (rx/merge-map - #(rx/concat - (rx/of (start-drag-handler)) - drag-events-stream - (rx/of (finish-drag)) - (rx/of (close-path-drag-end))))) + (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 [] @@ -338,16 +376,21 @@ (ptk/reify ::start-path-from-point ptk/WatchEvent (watch [_ state stream] - (let [mouse-up (->> stream (rx/filter #(or (end-path-event? %) + (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)) - (rx/of (start-drag-handler)) - drag-events - (rx/of (finish-drag))))))) + (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 @@ -389,14 +432,6 @@ ;; EVENT STREAMS -(defn make-click-stream - [stream down-event] - (->> stream - (rx/filter ms/mouse-click?) - #_(rx/debounce 200) - (rx/first) - (rx/map #(add-node down-event)))) - (defn make-drag-stream [stream down-event zoom] (let [mouse-up (->> stream (rx/filter #(or (end-path-event? %) @@ -407,16 +442,11 @@ (rx/concat (rx/of (add-node down-event)) - - (->> ms/mouse-position - (rx/take-until mouse-up) - (rx/filter (dragging? (gpt/point down-event) zoom)) - (rx/take 1) - (rx/merge-map - #(rx/concat - (rx/of (start-drag-handler)) - drag-events - (rx/of (finish-drag)))))))) + (drag-stream + (rx/concat + (rx/of (start-drag-handler)) + drag-events + (rx/of (finish-drag))))))) (defn make-node-events-stream [stream] @@ -444,7 +474,7 @@ ;; Mouse move preview mousemove-events - (->> ms/mouse-position + (->> (position-stream) (rx/take-until end-path-events) (rx/map #(preview-next-point %))) @@ -452,7 +482,7 @@ mousedown-events (->> mouse-down (rx/take-until end-path-events) - (rx/with-latest merge ms/mouse-position) + (rx/with-latest merge (position-stream)) ;; We change to the stream that emits the first event (rx/switch-map @@ -608,22 +638,14 @@ (watch [_ state stream] (let [start-position @ms/mouse-position stopper (->> stream (rx/filter ms/mouse-up?)) - zoom (get-in state [:workspace-local :zoom]) + zoom (get-in state [:workspace-local :zoom])] - move-point-stream - (fn [] (rx/concat - (->> ms/mouse-position - (rx/take-until stopper) - (rx/map #(move-path-point position %))) - (rx/of (apply-content-modifiers))))] - - (->> ms/mouse-position - (rx/take-until stopper) - (rx/filter (dragging? start-position zoom)) - (rx/take 1) - (rx/merge-map #(move-point-stream))) - - )))) + (drag-stream + (rx/concat + (->> ms/mouse-position + (rx/take-until stopper) + (rx/map #(move-path-point position %))) + (rx/of (apply-content-modifiers)))))))) (defn start-move-handler [index prefix] @@ -636,21 +658,21 @@ start-delta-x (get-in state [:workspace-local :edit-path id :content-modifiers index cx] 0) start-delta-y (get-in state [:workspace-local :edit-path id :content-modifiers index cy] 0)] - (rx/concat - (->> ms/mouse-position - (rx/take-until (->> stream (rx/filter ms/mouse-up?))) - (rx/with-latest vector ms/mouse-position-alt) - (rx/map - (fn [[pos alt?]] - (modify-handler - id - index - prefix - (+ start-delta-x (- (:x pos) (:x start-point))) - (+ start-delta-y (- (:y pos) (:y start-point))) - (not alt?)))) - ) - (rx/concat (rx/of (apply-content-modifiers)))))))) + (drag-stream + (rx/concat + (->> ms/mouse-position + (rx/take-until (->> stream (rx/filter ms/mouse-up?))) + (rx/with-latest vector ms/mouse-position-alt) + (rx/map + (fn [[pos alt?]] + (modify-handler + id + index + prefix + (+ start-delta-x (- (:x pos) (:x start-point))) + (+ start-delta-y (- (:y pos) (:y start-point))) + (not alt?))))) + (rx/concat (rx/of (apply-content-modifiers))))))))) (defn start-draw-mode [] (ptk/reify ::start-draw-mode diff --git a/frontend/src/app/main/ui/workspace/left_toolbar.cljs b/frontend/src/app/main/ui/workspace/left_toolbar.cljs index d67f40e14..9308a5a1e 100644 --- a/frontend/src/app/main/ui/workspace/left_toolbar.cljs +++ b/frontend/src/app/main/ui/workspace/left_toolbar.cljs @@ -93,7 +93,7 @@ {:alt (t locale "workspace.toolbar.path") :class (when (= selected-drawtool :path) "selected") :on-click (partial select-drawtool :path)} - i/curve] + i/pen] [:li.tooltip.tooltip-right {:alt (t locale "workspace.toolbar.comments") diff --git a/frontend/src/app/main/ui/workspace/shapes/path.cljs b/frontend/src/app/main/ui/workspace/shapes/path.cljs index 9106df8ae..72a327f36 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path.cljs @@ -169,8 +169,7 @@ {:cx x :cy y :r (if (or selected? hover?) (/ 3.5 zoom) (/ 3 zoom)) - :style {:cursor (when (= edit-mode :draw) cur/pen-node) - :stroke-width (/ 1 zoom) + :style {:stroke-width (/ 1 zoom) :stroke (cond (or selected? hover?) black-color preview? secondary-color :else primary-color) @@ -183,7 +182,10 @@ :on-mouse-down on-mouse-down :on-mouse-enter on-enter :on-mouse-leave on-leave - :style {:fill "transparent"}}]])) + :style {:cursor (cond + (and (not last-p?) (= edit-mode :draw)) cur/pen-node + (= edit-mode :move) cur/pointer-node) + :fill "transparent"}}]])) (mf/defc path-handler [{:keys [index prefix point handler zoom selected? hover? edit-mode]}] (when (and point handler) @@ -227,8 +229,7 @@ :width (/ 6 zoom) :height (/ 6 zoom) - :style {:cursor cur/pointer-move - :stroke-width (/ 1 zoom) + :style {:stroke-width (/ 1 zoom) :stroke (cond (or selected? hover?) black-color :else primary-color) :fill (cond selected? primary-color @@ -240,7 +241,8 @@ :on-mouse-down on-mouse-down :on-mouse-enter on-enter :on-mouse-leave on-leave - :style {:fill "transparent"}}]]))) + :style {:cursor (when (= edit-mode :move) cur/pointer-move) + :fill "transparent"}}]]))) (mf/defc path-preview [{:keys [zoom command from]}] [:g.preview {:style {:pointer-events "none"}} From f8cf7103ca55085993d6dc81698460868712c9c3 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 26 Nov 2020 10:50:24 +0100 Subject: [PATCH 3/3] :sparkles: Improved opposite handler management --- .../app/main/data/workspace/drawing/path.cljs | 72 +++++++++++-------- .../app/main/ui/workspace/shapes/path.cljs | 4 -- frontend/src/app/util/geom/path.cljs | 2 +- 3 files changed, 44 insertions(+), 34 deletions(-) diff --git a/frontend/src/app/main/data/workspace/drawing/path.cljs b/frontend/src/app/main/data/workspace/drawing/path.cljs index 9e31ee460..4599e903b 100644 --- a/frontend/src/app/main/data/workspace/drawing/path.cljs +++ b/frontend/src/app/main/data/workspace/drawing/path.cljs @@ -62,9 +62,6 @@ (s/def ::content (s/coll-of ::content-entry :kind vector?)) -(s/def ::path-shape - (s/keys :req-un [::content])) - ;; CONSTANTS (defonce enter-keycode 13) @@ -234,7 +231,6 @@ 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)] @@ -278,21 +274,22 @@ (= command :line-to) (update-in (get-path state :content index) make-curve)))))) -(defn drag-handler [{:keys [x y]}] +(defn drag-handler [{:keys [x y alt? shift?]}] (ptk/reify ::drag-handler ptk/UpdateEvent (update [_ state] (let [id (get-path-id state) - handler-position (gpt/point x y) 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? true + match-opposite? (not alt?) modifiers (move-handler-modifiers content (inc index) :c1 match-opposite? dx dy)] (-> state - (assoc-in [:workspace-local :edit-path id :content-modifiers] modifiers) + (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)))))) @@ -331,16 +328,11 @@ (->> stream (rx/filter #(or (end-path-event? %) (ms/mouse-up? %)))) - position-stream - (->> ms/mouse-position - (rx/take-until stop-stream) - (rx/throttle 50)) - drag-events-stream - (->> position-stream + (->> (position-stream) + (rx/take-until stop-stream) (rx/map #(drag-handler %)))] - (rx/concat (rx/of (add-node position)) (drag-stream @@ -436,7 +428,7 @@ [stream down-event zoom] (let [mouse-up (->> stream (rx/filter #(or (end-path-event? %) (ms/mouse-up? %)))) - drag-events (->> ms/mouse-position + drag-events (->> (position-stream) (rx/take-until mouse-up) (rx/map #(drag-handler %)))] @@ -567,6 +559,14 @@ (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]) @@ -653,25 +653,39 @@ ptk/WatchEvent (watch [_ state stream] (let [id (get-in state [:workspace-local :edition]) - [cx cy] (if (= prefix :c1) [:c1x :c1y] [:c2x :c2y]) + cx (ud/prefix-keyword prefix :x) + cy (ud/prefix-keyword prefix :y) start-point @ms/mouse-position - start-delta-x (get-in state [:workspace-local :edit-path id :content-modifiers index cx] 0) - start-delta-y (get-in state [:workspace-local :edit-path id :content-modifiers index cy] 0)] + 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 (gpt/distance (ugp/opposite-handler point handler) opposite-handler) + match-opposite? (mth/almost-zero? current-distance)] (drag-stream (rx/concat - (->> ms/mouse-position + (->> (position-stream) (rx/take-until (->> stream (rx/filter ms/mouse-up?))) - (rx/with-latest vector ms/mouse-position-alt) (rx/map - (fn [[pos alt?]] - (modify-handler - id - index - prefix - (+ start-delta-x (- (:x pos) (:x start-point))) - (+ start-delta-y (- (:y pos) (:y start-point))) - (not alt?))))) + (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 [] diff --git a/frontend/src/app/main/ui/workspace/shapes/path.cljs b/frontend/src/app/main/ui/workspace/shapes/path.cljs index 72a327f36..09a3ab9b2 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path.cljs @@ -333,10 +333,6 @@ (when drag-handler [:g.drag-handler {:pointer-events "none"} - (when (not= :move-to (:command last-command)) - [:& path-handler {:point last-p - :handler (ugp/opposite-handler last-p drag-handler) - :zoom zoom}]) [:& path-handler {:point last-p :handler drag-handler :zoom zoom}]])])) diff --git a/frontend/src/app/util/geom/path.cljs b/frontend/src/app/util/geom/path.cljs index 4e150f7a5..5357563a7 100644 --- a/frontend/src/app/util/geom/path.cljs +++ b/frontend/src/app/util/geom/path.cljs @@ -368,7 +368,7 @@ calculate-vector (fn [point next prev] - (let [base-vector (if (or (nil? next) (nil? prev)) + (let [base-vector (if (or (nil? next) (nil? prev) (= next prev)) (-> (gpt/to-vec point (or next prev)) (gpt/normal-left)) (gpt/to-vec next prev))]