From e2cf3a5a981c2e1f431501a5b530660f7d636986 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 14 Apr 2021 17:23:16 +0200 Subject: [PATCH 1/7] :sparkles: Adds path new-point edition --- common/app/common/geom/shapes/path.cljc | 51 +++++++++---------- .../src/app/main/data/workspace/path.cljs | 1 + .../app/main/data/workspace/path/edition.cljs | 12 +++++ .../main/ui/workspace/shapes/path/editor.cljs | 24 +++++++-- .../src/app/main/ui/workspace/viewport.cljs | 6 ++- .../app/main/ui/workspace/viewport/hooks.cljs | 24 ++++----- 6 files changed, 74 insertions(+), 44 deletions(-) diff --git a/common/app/common/geom/shapes/path.cljc b/common/app/common/geom/shapes/path.cljc index eaa1772544..33c041cee4 100644 --- a/common/app/common/geom/shapes/path.cljc +++ b/common/app/common/geom/shapes/path.cljc @@ -233,7 +233,9 @@ (loop [t1 0 t2 1] (if (<= (mth/abs (- t1 t2)) path-closest-point-accuracy) - (curve-values start end h1 h2 t1) + (-> (curve-values start end h1 h2 t1) + ;; store the segment info + (with-meta {:t t1 :from-p start :to-p end})) (let [ht (+ t1 (/ (- t2 t1) 2)) ht1 (+ t1 (/ (- t2 t1) 4)) @@ -260,21 +262,18 @@ "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)) + (let [e1 (gpt/to-vec from-p to-p ) + e2 (gpt/to-vec from-p position) len2 (+ (mth/sq (:x e1)) (mth/sq (:y e1))) - val-dp (/ (gpt/dot e1 e2) len2)] + t (/ (gpt/dot e1 e2) len2)] + + (if (and (>= t 0) (<= t 1) (not (mth/almost-zero? len2))) + (-> (gpt/add from-p (gpt/scale e1 t)) + (with-meta {:t t + :from-p from-p + :to-p to-p})) - (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)) @@ -286,20 +285,20 @@ [shape position] (let [point+distance (fn [[cur-cmd prev-cmd]] - (let [point + (let [from-p (command->point prev-cmd) + to-p (command->point cur-cmd) + h1 (gpt/point (get-in cur-cmd [:params :c1x]) + (get-in cur-cmd [:params :c1y])) + h2 (gpt/point (get-in cur-cmd [:params :c2x]) + (get-in cur-cmd [:params :c2y])) + 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]))) + :line-to + (line-closest-point position from-p to-p) + + :curve-to + (curve-closest-point position from-p to-p h1 h2) + nil)] (when point [point (gpt/distance point position)]))) diff --git a/frontend/src/app/main/data/workspace/path.cljs b/frontend/src/app/main/data/workspace/path.cljs index e4c04a5a71..7dd1a6d5d5 100644 --- a/frontend/src/app/main/data/workspace/path.cljs +++ b/frontend/src/app/main/data/workspace/path.cljs @@ -22,6 +22,7 @@ (d/export edition/start-move-handler) (d/export edition/start-move-path-point) (d/export edition/start-path-edit) +(d/export edition/create-node-at-position) ;; Selection (d/export selection/handle-selection) diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index 0d07756ba2..60335af17e 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -234,3 +234,15 @@ (update [_ state] (let [id (get-in state [:workspace-local :edition])] (update state :workspace-local dissoc :edit-path id))))) + +(defn create-node-at-position + [{:keys [from-p to-p t]}] + (ptk/reify ::create-node-at-position + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state)] + (update-in state (st/get-path state :content) ugp/split-segments #{from-p to-p} t))) + + ptk/WatchEvent + (watch [_ state stream] + (rx/of (changes/save-path-content))))) 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 150e66b312..ea81ec0371 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -24,7 +24,7 @@ [rumext.alpha :as mf]) (:import goog.events.EventType)) -(mf/defc path-point [{:keys [position zoom edit-mode hover? selected? preview? start-path? last-p?]}] +(mf/defc path-point [{:keys [position zoom edit-mode hover? selected? preview? start-path? last-p? new-point?]}] (let [{:keys [x y]} position on-enter @@ -40,6 +40,9 @@ (dom/stop-propagation event) (dom/prevent-default event) + (when (and new-point? (some? (meta position))) + (st/emit! (drp/create-node-at-position (meta position)))) + (let [shift? (kbd/shift? event)] (cond (= edit-mode :move) @@ -190,6 +193,8 @@ last-p (->> content last ugp/command->point) handlers (ugp/content->handlers content) + start-p? (not (some? last-point)) + [snap-selected snap-points] (cond (some? drag-handler) [#{drag-handler} points] @@ -199,7 +204,9 @@ [(->> selected-points (map base->point) (into #{})) (->> points (remove selected-points) (into #{}))]) - show-snap? (and snap-toggled (or (some? drag-handler) (some? preview) (some? moving-handler) moving-nodes)) + show-snap? (and snap-toggled + (empty? hover-points) + (or (some? drag-handler) (some? preview) (some? moving-handler) moving-nodes)) handle-double-click-outside (fn [event] @@ -213,6 +220,13 @@ #(doseq [key keys] (events/unlistenByKey key))))) + (hooks/use-stream + ms/mouse-position + (mf/deps shape zoom) + (fn [position] + (when-let [point (gshp/path-closest-point shape position)] + (reset! hover-point (when (< (gpt/distance position point) (/ 10 zoom)) point))))) + [:g.path-editor {:ref editor-ref} (when (and preview (not drag-handler)) [:& path-preview {:command preview @@ -228,13 +242,15 @@ (when @hover-point [:g.hover-point [:& path-point {:position @hover-point + :edit-mode edit-mode + :new-point? true + :start-path? start-p? :zoom zoom}]]) (for [position points] (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))] + last-p? (= last-point (get point->base position))] [:g.path-node [:g.point-handlers {:pointer-events (when (= edit-mode :draw) "none")} diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 3a0c422806..fd8b79170b 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -98,6 +98,7 @@ drawing-path? (or (and edition (= :draw (get-in edit-path [edition :edit-mode]))) (and (some? drawing-obj) (= :path (:type drawing-obj)))) text-editing? (and edition (= :text (get-in objects [edition :type]))) + path-editing? (and edition (= :path (get-in objects [edition :type]))) on-click (actions/on-click hover selected edition drawing-path? drawing-tool) on-context-menu (actions/on-context-menu hover) @@ -132,11 +133,12 @@ show-snap-distance? (and (contains? layout :dynamic-alignment) (= transform :move) (not (empty? selected))) show-snap-points? (and (contains? layout :dynamic-alignment) (or drawing-obj transform)) show-selrect? (and selrect (empty? drawing)) + show-measures? (and (not transform) (not path-editing?) show-distances?) ] (hooks/setup-dom-events viewport-ref zoom disable-paste in-viewport?) (hooks/setup-viewport-size viewport-ref) - (hooks/setup-cursor cursor alt? panning drawing-tool drawing-path?) + (hooks/setup-cursor cursor alt? panning drawing-tool drawing-path? path-editing?) (hooks/setup-resize layout viewport-ref) (hooks/setup-keyboard alt? ctrl?) (hooks/setup-hover-shapes page-id move-stream selected objects transform selected ctrl? hover hover-ids) @@ -224,7 +226,7 @@ :disable-handlers (or drawing-tool edition) :on-move-selected on-move-selected}]) - (when (and (not transform) show-distances?) + (when show-measures? [:& msr/measurement {:bounds vbox :selected-shapes selected-shapes diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index f2c3ae75b4..12abb98535 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -58,23 +58,23 @@ ;; We schedule the event so it fires after `initialize-page` event (timers/schedule #(st/emit! (dw/initialize-viewport size))))))) -(defn setup-cursor [cursor alt? panning drawing-tool drawing-path?] +(defn setup-cursor [cursor alt? panning drawing-tool drawing-path? path-editing?] (mf/use-effect - (mf/deps @cursor @alt? panning drawing-tool drawing-path?) + (mf/deps @cursor @alt? panning drawing-tool drawing-path? path-editing?) (fn [] (let [new-cursor (cond - panning (utils/get-cursor :hand) - (= drawing-tool :comments) (utils/get-cursor :comments) - (= drawing-tool :frame) (utils/get-cursor :create-artboard) - (= drawing-tool :rect) (utils/get-cursor :create-rectangle) - (= drawing-tool :circle) (utils/get-cursor :create-ellipse) + panning (utils/get-cursor :hand) + (= drawing-tool :comments) (utils/get-cursor :comments) + (= drawing-tool :frame) (utils/get-cursor :create-artboard) + (= drawing-tool :rect) (utils/get-cursor :create-rectangle) + (= drawing-tool :circle) (utils/get-cursor :create-ellipse) (or (= drawing-tool :path) - drawing-path?) (utils/get-cursor :pen) - (= drawing-tool :curve) (utils/get-cursor :pencil) - drawing-tool (utils/get-cursor :create-shape) - @alt? (utils/get-cursor :duplicate) - :else (utils/get-cursor :pointer-inner))] + drawing-path?) (utils/get-cursor :pen) + (= drawing-tool :curve) (utils/get-cursor :pencil) + drawing-tool (utils/get-cursor :create-shape) + (and @alt? (not path-editing?)) (utils/get-cursor :duplicate) + :else (utils/get-cursor :pointer-inner))] (when (not= @cursor new-cursor) (reset! cursor new-cursor)))))) From 0455aaa4cdb2ede4851c619b58d721f25b8dce7a Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 16 Apr 2021 11:13:20 +0200 Subject: [PATCH 2/7] :sparkles: Undo/redo paths --- common/app/common/data/undo_stack.cljc | 60 ++++++++ .../src/app/main/data/workspace/common.cljs | 12 +- .../app/main/data/workspace/path/drawing.cljs | 2 + .../app/main/data/workspace/path/edition.cljs | 2 + .../app/main/data/workspace/path/undo.cljs | 140 ++++++++++++++++++ .../main/ui/workspace/shapes/path/editor.cljs | 6 +- frontend/src/app/util/debug.cljs | 2 +- 7 files changed, 217 insertions(+), 7 deletions(-) create mode 100644 common/app/common/data/undo_stack.cljc create mode 100644 frontend/src/app/main/data/workspace/path/undo.cljs diff --git a/common/app/common/data/undo_stack.cljc b/common/app/common/data/undo_stack.cljc new file mode 100644 index 0000000000..57f71d1286 --- /dev/null +++ b/common/app/common/data/undo_stack.cljc @@ -0,0 +1,60 @@ +;; 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.common.data.undo-stack + (:refer-clojure :exclude [peek]) + (:require + #?(:cljs [cljs.core :as core] + :clj [clojure.core :as core]))) + +(defonce MAX-UNDO-SIZE 100) + +(defn make-stack + [] + {:index -1 + :items []}) + +(defn peek + [{index :index items :items :as stack}] + (when (and (>= index 0) (< index (count items))) + (nth items index))) + +(defn append + [{index :index items :items :as stack} value] + + (if (and (some? stack) (not= value (peek stack))) + (let [items (cond-> items + (> index 0) + (subvec 0 (inc index)) + + (> (+ index 2) MAX-UNDO-SIZE) + (subvec 1 (inc index)) + + :always + (conj value)) + + index (min (dec MAX-UNDO-SIZE) (inc index))] + {:index index + :items items}) + stack)) + +(defn fixup + [{index :index :as stack} value] + (assoc-in stack [:items index] value)) + +(defn undo + [{index :index items :items :as stack}] + (update stack :index dec)) + +(defn redo + [{index :index items :items :as stack}] + (cond-> stack + (< index (dec (count items))) + (update :index inc))) + +(defn size + [{index :index items :items :as stack}] + (inc index)) diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index d479b5b83d..9c3807563b 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -356,13 +356,15 @@ (>= index 0) (accumulate-undo-entry (get-in state [:workspace-undo :items index])) (>= index 0) (update-in [:workspace-undo :index] dec)))))) +;; If these functions change modules review /src/app/main/data/workspace/path/undo.cljs (def undo (ptk/reify ::undo ptk/WatchEvent (watch [_ state stream] - (let [edition (get-in state [:workspace-local :edition])] + (let [edition (get-in state [:workspace-local :edition]) + drawing (get state :workspace-drawing)] ;; Editors handle their own undo's - (when-not (some? edition) + (when-not (or (some? edition) (some? drawing)) (let [undo (:workspace-undo state) items (:items undo) index (or (:index undo) (dec (count items)))] @@ -375,8 +377,9 @@ (ptk/reify ::redo ptk/WatchEvent (watch [_ state stream] - (let [edition (get-in state [:workspace-local :edition])] - (when-not (some? edition) + (let [edition (get-in state [:workspace-local :edition]) + drawing (get state :workspace-drawing)] + (when-not (or (some? edition) (some? drawing)) (let [undo (:workspace-undo state) items (:items undo) index (or (:index undo) (dec (count items)))] @@ -543,6 +546,7 @@ (rx/take 1) (rx/map (constantly clear-edition-mode))))))) +;; If these event change modules review /src/app/main/data/workspace/path/undo.cljs (def clear-edition-mode (ptk/reify ::clear-edition-mode ptk/UpdateEvent diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index f4621fbc83..c0945a33bc 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -18,6 +18,7 @@ [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.data.workspace.path.undo :as undo] [app.main.streams :as ms] [app.util.geom.path :as ugp] [beicon.core :as rx] @@ -245,6 +246,7 @@ (make-drag-stream stream snap-toggled zoom points %))))] (rx/concat + (rx/of (undo/start-path-undo)) (rx/of (common/init-path)) (rx/merge mousemove-events mousedown-events) diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index 60335af17e..f11e6c86d3 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -17,6 +17,7 @@ [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.data.workspace.path.undo :as undo] [app.main.streams :as ms] [app.util.geom.path :as ugp] [beicon.core :as rx] @@ -221,6 +222,7 @@ (watch [_ state stream] (let [mode (get-in state [:workspace-local :edit-path id :edit-mode])] (rx/concat + (rx/of (undo/start-path-undo)) (rx/of (drawing/change-edit-mode mode)) (->> stream (rx/take-until (->> stream (rx/filter (ptk/type? ::start-path-edit)))) diff --git a/frontend/src/app/main/data/workspace/path/undo.cljs b/frontend/src/app/main/data/workspace/path/undo.cljs new file mode 100644 index 0000000000..2c490ecba5 --- /dev/null +++ b/frontend/src/app/main/data/workspace/path/undo.cljs @@ -0,0 +1,140 @@ +;; 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.path.undo + (:require + [app.common.data :as d] + [app.common.data.undo-stack :as u] + [app.common.uuid :as uuid] + [app.main.data.workspace.path.state :as st] + [app.main.store :as store] + [beicon.core :as rx] + [okulary.core :as l] + [potok.core :as ptk])) + +(defn undo-event? + [event] + (= :app.main.data.workspace.common/undo (ptk/type event))) + +(defn redo-event? + [event] + (= :app.main.data.workspace.common/redo (ptk/type event))) + +(defn- make-entry [state] + (let [id (st/get-path-id state)] + {:content (get-in state (st/get-path state :content)) + :preview (get-in state [:workspace-local :edit-path id :preview]) + :last-point (get-in state [:workspace-local :edit-path id :last-point]) + :prev-handler (get-in state [:workspace-local :edit-path id :prev-handler])})) + +(defn- load-entry [state {:keys [content preview last-point prev-handler]}] + (let [id (st/get-path-id state)] + (-> state + (d/assoc-in-when (st/get-path state :content) content) + (d/update-in-when + [:workspace-local :edit-path id] + assoc + :preview preview + :last-point last-point + :prev-handler prev-handler)))) + +(defn undo [] + (ptk/reify ::undo + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state) + undo-stack (-> (get-in state [:workspace-local :edit-path id :undo-stack]) + (u/undo)) + entry (u/peek undo-stack)] + (cond-> state + (some? entry) + (-> (load-entry entry) + (d/assoc-in-when + [:workspace-local :edit-path id :undo-stack] + undo-stack))))))) + +(defn redo [] + (ptk/reify ::redo + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state) + undo-stack (-> (get-in state [:workspace-local :edit-path id :undo-stack]) + (u/redo)) + entry (u/peek undo-stack)] + (-> state + (load-entry entry) + (d/assoc-in-when + [:workspace-local :edit-path id :undo-stack] + undo-stack)))))) + +(defn add-undo-entry [] + (ptk/reify ::add-undo-entry + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state) + entry (make-entry state)] + (-> state + (d/update-in-when + [:workspace-local :edit-path id :undo-stack] + u/append entry)))))) + +(defn end-path-undo + [] + (ptk/reify ::end-path-undo + ptk/UpdateEvent + (update [_ state] + (-> state + (d/update-in-when + [:workspace-local :edit-path (st/get-path-id state)] + dissoc :undo-lock :undo-stack))))) + +(defn- stop-undo? [event] + (= :app.main.data.workspace.common/clear-edition-mode (ptk/type event))) + +(def path-content-ref + (letfn [(selector [state] + (get-in state (st/get-path state :content)))] + (l/derived selector store/state))) + +(defn start-path-undo + [] + (let [lock (uuid/next)] + (ptk/reify ::start-path-undo + ptk/UpdateEvent + (update [_ state] + (let [undo-lock (get-in state [:workspace-local :edit-path (st/get-path-id state) :undo-lock])] + (cond-> state + (not undo-lock) + (update-in [:workspace-local :edit-path (st/get-path-id state)] + assoc + :undo-lock lock + :undo-stack (u/make-stack))))) + + ptk/WatchEvent + (watch [_ state stream] + (let [undo-lock (get-in state [:workspace-local :edit-path (st/get-path-id state) :undo-lock])] + (when (= undo-lock lock) + (let [stop-undo-stream (->> stream + (rx/filter stop-undo?) + (rx/take 1))] + (rx/concat + (->> (rx/merge + (->> (rx/from-atom path-content-ref {:emit-current-value? true}) + (rx/filter (comp not nil?)) + (rx/map #(add-undo-entry))) + + (->> stream + (rx/filter undo-event?) + (rx/map #(undo))) + + (->> stream + (rx/filter redo-event?) + (rx/map #(redo)))) + + (rx/take-until stop-undo-stream)) + + (rx/of (end-path-undo)))))))))) + 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 ea81ec0371..cb471cad79 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -205,8 +205,10 @@ (->> points (remove selected-points) (into #{}))]) show-snap? (and snap-toggled - (empty? hover-points) - (or (some? drag-handler) (some? preview) (some? moving-handler) moving-nodes)) + (or (some? drag-handler) + (some? preview) + (some? moving-handler) + moving-nodes)) handle-double-click-outside (fn [event] diff --git a/frontend/src/app/util/debug.cljs b/frontend/src/app/util/debug.cljs index 80fdc47831..d83a3e0543 100644 --- a/frontend/src/app/util/debug.cljs +++ b/frontend/src/app/util/debug.cljs @@ -13,7 +13,7 @@ #{:app.main.data.workspace.notifications/handle-pointer-update :app.main.data.workspace.selection/change-hover-state}) -(defonce ^:dynamic *debug* (atom #{})) +(defonce ^:dynamic *debug* (atom #{#_:events})) (defn debug-all! [] (reset! *debug* debug-options)) (defn debug-none! [] (reset! *debug* #{})) From de11e85d2b906033d5cff50225fb694b9a76e1e1 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 16 Apr 2021 14:06:32 +0200 Subject: [PATCH 3/7] :recycle: Refactor path utils --- .../main/data/workspace/drawing/curve.cljs | 5 +- .../app/main/data/workspace/path/drawing.cljs | 15 +- .../app/main/data/workspace/path/edition.cljs | 32 +- .../app/main/data/workspace/path/helpers.cljs | 6 +- .../app/main/data/workspace/path/streams.cljs | 4 +- .../app/main/data/workspace/path/tools.cljs | 16 +- .../app/main/data/workspace/svg_upload.cljs | 4 +- frontend/src/app/main/ui/shapes/path.cljs | 5 +- .../app/main/ui/workspace/shapes/path.cljs | 4 +- .../main/ui/workspace/shapes/path/editor.cljs | 24 +- .../main/ui/workspace/viewport/outline.cljs | 4 +- .../ui/workspace/viewport/path_actions.cljs | 5 +- frontend/src/app/util/geom/path.cljs | 917 ------------------ .../app/util/{a2c.js => path/arc_to_curve.js} | 4 +- frontend/src/app/util/path/commands.cljs | 147 +++ frontend/src/app/util/path/format.cljs | 74 ++ frontend/src/app/util/path/geom.cljs | 60 ++ frontend/src/app/util/path/parser.cljs | 317 ++++++ .../util/{geom => path}/path_impl_simplify.js | 4 +- .../src/app/util/path/simplify_curve.cljs | 24 + frontend/src/app/util/path/tools.cljs | 385 ++++++++ 21 files changed, 1076 insertions(+), 980 deletions(-) delete mode 100644 frontend/src/app/util/geom/path.cljs rename frontend/src/app/util/{a2c.js => path/arc_to_curve.js} (98%) create mode 100644 frontend/src/app/util/path/commands.cljs create mode 100644 frontend/src/app/util/path/format.cljs create mode 100644 frontend/src/app/util/path/geom.cljs create mode 100644 frontend/src/app/util/path/parser.cljs rename frontend/src/app/util/{geom => path}/path_impl_simplify.js (96%) create mode 100644 frontend/src/app/util/path/simplify_curve.cljs create mode 100644 frontend/src/app/util/path/tools.cljs diff --git a/frontend/src/app/main/data/workspace/drawing/curve.cljs b/frontend/src/app/main/data/workspace/drawing/curve.cljs index 02c7bb92d8..8808b0d0f7 100644 --- a/frontend/src/app/main/data/workspace/drawing/curve.cljs +++ b/frontend/src/app/main/data/workspace/drawing/curve.cljs @@ -12,7 +12,7 @@ [app.common.geom.shapes :as gsh] [app.common.geom.shapes.path :as gsp] [app.main.streams :as ms] - [app.util.geom.path :as path] + [app.util.path.simplify-curve :as ups] [app.main.data.workspace.drawing.common :as common] [app.main.data.workspace.common :as dwc] [app.common.pages :as cp])) @@ -67,7 +67,7 @@ state [:workspace-drawing :object] (fn [shape] (-> shape - (update :segments #(path/simplify % simplify-tolerance)) + (update :segments #(ups/simplify % simplify-tolerance)) (curve-to-path))))) (defn handle-drawing-curve [] @@ -85,3 +85,4 @@ (rx/of (setup-frame-curve) finish-drawing-curve common/handle-finish-drawing)))))) + diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index c0945a33bc..9f359c3afb 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -20,7 +20,8 @@ [app.main.data.workspace.path.tools :as tools] [app.main.data.workspace.path.undo :as undo] [app.main.streams :as ms] - [app.util.geom.path :as ugp] + [app.util.path.commands :as upc] + [app.util.path.geom :as upg] [beicon.core :as rx] [potok.core :as ptk])) @@ -67,7 +68,7 @@ make-curve (fn [command] - (let [params (ugp/make-curve-params + (let [params (upc/make-curve-params (get-in content [index :params]) (get-in content [(dec index) :params]))] (-> command @@ -85,7 +86,7 @@ shape (get-in state (st/get-path state)) content (:content shape) index (dec (count content)) - node-position (ugp/command->point (nth content index)) + node-position (upc/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) @@ -104,7 +105,7 @@ 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 (st/get-path state :content) upc/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) @@ -133,7 +134,7 @@ 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) + points (upg/content->points content) drag-events-stream (->> (streams/position-stream snap-toggled points) @@ -166,7 +167,7 @@ mouse-up (->> 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) + points (upg/content->points content) id (st/get-path-id state) snap-toggled (get-in state [:workspace-local :edit-path id :snap-toggled]) @@ -223,7 +224,7 @@ end-path-events (->> stream (rx/filter helpers/end-path-event?)) content (get-in state (st/get-path state :content)) - points (ugp/content->points content) + points (upg/content->points content) id (st/get-path-id state) snap-toggled (get-in state [:workspace-local :edit-path id :snap-toggled]) diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index f11e6c86d3..57162153f0 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -19,7 +19,9 @@ [app.main.data.workspace.path.drawing :as drawing] [app.main.data.workspace.path.undo :as undo] [app.main.streams :as ms] - [app.util.geom.path :as ugp] + [app.util.path.commands :as upc] + [app.util.path.geom :as upg] + [app.util.path.tools :as upt] [beicon.core :as rx] [potok.core :as ptk])) @@ -44,7 +46,7 @@ [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)] + opposite-index (upc/opposite-index content index prefix)] (cond-> state :always (-> (update-in [:workspace-local :edit-path id :content-modifiers index] assoc @@ -65,10 +67,10 @@ content-modifiers (get-in state [:workspace-local :edit-path id :content-modifiers]) content (:content shape) - new-content (ugp/apply-content-modifiers content content-modifiers) + new-content (upc/apply-content-modifiers content content-modifiers) - old-points (->> content ugp/content->points) - new-points (->> new-content ugp/content->points) + old-points (->> content upg/content->points) + new-points (->> new-content upg/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)] @@ -79,8 +81,8 @@ (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]] + (let [point-indices (upc/point-indices content point) ;; [indices] + handler-indices (upc/handler-indices content point) ;; [[index prefix]] modify-point (fn [modifiers index] @@ -146,7 +148,7 @@ 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 (upg/content->points content)] (rx/concat ;; This stream checks the consecutive mouse positions to do the draging @@ -170,16 +172,16 @@ start-delta-y (get-in modifiers [index cy] 0) content (get-in state (st/get-path state :content)) - points (ugp/content->points content) + points (upg/content->points content) - opposite-index (ugp/opposite-index content index prefix) + opposite-index (upc/opposite-index content index prefix) opposite-prefix (if (= prefix :c1) :c2 :c1) - opposite-handler (-> content (get opposite-index) (ugp/get-handler opposite-prefix)) + opposite-handler (-> content (get opposite-index) (upc/get-handler opposite-prefix)) - point (-> content (get (if (= prefix :c1) (dec index) index)) (ugp/command->point)) - handler (-> content (get index) (ugp/get-handler prefix)) + point (-> content (get (if (= prefix :c1) (dec index) index)) (upc/command->point)) + handler (-> content (get index) (upc/get-handler prefix)) - current-distance (when opposite-handler (gpt/distance (ugp/opposite-handler point handler) opposite-handler)) + current-distance (when opposite-handler (gpt/distance (upg/opposite-handler point handler) opposite-handler)) match-opposite? (and opposite-handler (mth/almost-zero? current-distance)) snap-toggled (get-in state [:workspace-local :edit-path id :snap-toggled])] @@ -243,7 +245,7 @@ ptk/UpdateEvent (update [_ state] (let [id (st/get-path-id state)] - (update-in state (st/get-path state :content) ugp/split-segments #{from-p to-p} t))) + (update-in state (st/get-path state :content) upt/split-segments #{from-p to-p} t))) ptk/WatchEvent (watch [_ state stream] diff --git a/frontend/src/app/main/data/workspace/path/helpers.cljs b/frontend/src/app/main/data/workspace/path/helpers.cljs index 0fe040d426..57b31b9088 100644 --- a/frontend/src/app/main/data/workspace/path/helpers.cljs +++ b/frontend/src/app/main/data/workspace/path/helpers.cljs @@ -14,7 +14,7 @@ [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] + [app.util.path.commands :as upc] [potok.core :as ptk])) ;; CONSTANTS @@ -94,7 +94,7 @@ add-line? {:command :line-to :params position} add-curve? {:command :curve-to - :params (ugp/make-curve-params position prev-handler)} + :params (upc/make-curve-params position prev-handler)} :else {:command :move-to :params position}))) @@ -110,7 +110,7 @@ [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)] + opposite-index (upc/opposite-index content index prefix)] (cond-> {} :always diff --git a/frontend/src/app/main/data/workspace/path/streams.cljs b/frontend/src/app/main/data/workspace/path/streams.cljs index f7c7a83c22..d61d1a82aa 100644 --- a/frontend/src/app/main/data/workspace/path/streams.cljs +++ b/frontend/src/app/main/data/workspace/path/streams.cljs @@ -16,7 +16,7 @@ [app.common.math :as mth] [app.main.snap :as snap] [okulary.core :as l] - [app.util.geom.path :as ugp])) + [app.util.path.geom :as upg])) (defonce drag-threshold 5) @@ -103,7 +103,7 @@ ranges-stream (->> content-stream - (rx/map ugp/content->points) + (rx/map upg/content->points) (rx/map snap/create-ranges))] (->> ms/mouse-position diff --git a/frontend/src/app/main/data/workspace/path/tools.cljs b/frontend/src/app/main/data/workspace/path/tools.cljs index 90ea6a1913..059636565a 100644 --- a/frontend/src/app/main/data/workspace/path/tools.cljs +++ b/frontend/src/app/main/data/workspace/path/tools.cljs @@ -10,7 +10,7 @@ [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] + [app.util.path.tools :as upt] [app.common.geom.point :as gpt] [beicon.core :as rx] [potok.core :as ptk])) @@ -32,27 +32,27 @@ (defn make-corner [] (process-path-tool (fn [content points] - (reduce ugp/make-corner-point content points)))) + (reduce upt/make-corner-point content points)))) (defn make-curve [] (process-path-tool (fn [content points] - (reduce ugp/make-curve-point content points)))) + (reduce upt/make-curve-point content points)))) (defn add-node [] - (process-path-tool (fn [content points] (ugp/split-segments content points 0.5)))) + (process-path-tool (fn [content points] (upt/split-segments content points 0.5)))) (defn remove-node [] - (process-path-tool ugp/remove-nodes)) + (process-path-tool upt/remove-nodes)) (defn merge-nodes [] - (process-path-tool ugp/merge-nodes)) + (process-path-tool upt/merge-nodes)) (defn join-nodes [] - (process-path-tool ugp/join-nodes)) + (process-path-tool upt/join-nodes)) (defn separate-nodes [] - (process-path-tool ugp/separate-nodes)) + (process-path-tool upt/separate-nodes)) (defn toggle-snap [] (ptk/reify ::toggle-snap diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index f2dca9d4ef..1b3a479bf0 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -16,7 +16,7 @@ [app.main.data.workspace.common :as dwc] [app.main.repo :as rp] [app.util.color :as uc] - [app.util.geom.path :as ugp] + [app.util.path.parser :as upp] [app.util.object :as obj] [app.util.svg :as usvg] [app.util.uri :as uu] @@ -163,7 +163,7 @@ (defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}] (when (and (contains? attrs :d) (not (empty? (:d attrs)) )) (let [svg-transform (usvg/parse-transform (:transform attrs)) - path-content (ugp/path->content (:d attrs)) + path-content (upp/parse-path (:d attrs)) content (cond-> path-content svg-transform (gsh/transform-content svg-transform)) diff --git a/frontend/src/app/main/ui/shapes/path.cljs b/frontend/src/app/main/ui/shapes/path.cljs index 98643cb7fe..c5de929e14 100644 --- a/frontend/src/app/main/ui/shapes/path.cljs +++ b/frontend/src/app/main/ui/shapes/path.cljs @@ -11,7 +11,7 @@ [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]] [app.util.object :as obj] - [app.util.geom.path :as ugp])) + [app.util.path.format :as upf])) ;; --- Path Shape @@ -22,7 +22,7 @@ background? (unchecked-get props "background?") {:keys [id x y width height]} (:selrect shape) content (:content shape) - pdata (mf/use-memo (mf/deps content) #(ugp/content->path content)) + pdata (mf/use-memo (mf/deps content) #(upf/format-path content)) props (-> (attrs/extract-style-attrs shape) (obj/merge! #js {:d pdata}))] @@ -39,3 +39,4 @@ :base-props props :elem-name "path"}]))) + diff --git a/frontend/src/app/main/ui/workspace/shapes/path.cljs b/frontend/src/app/main/ui/workspace/shapes/path.cljs index 33de97772b..22232ca011 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path.cljs @@ -13,7 +13,7 @@ [app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.workspace.shapes.path.common :as pc] [app.util.dom :as dom] - [app.util.geom.path :as ugp] + [app.util.path.commands :as upc] [rumext.alpha :as mf])) (mf/defc path-wrapper @@ -24,7 +24,7 @@ content-modifiers (mf/deref content-modifiers-ref) editing-id (mf/deref refs/selected-edition) editing? (= editing-id (:id shape)) - shape (update shape :content ugp/apply-content-modifiers content-modifiers)] + shape (update shape :content upc/apply-content-modifiers content-modifiers)] [:> shape-container {:shape shape :pointer-events (when editing? "none")} 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 cb471cad79..7457e35d7a 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -17,7 +17,9 @@ [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.path.geom :as upg] + [app.util.path.commands :as upc] + [app.util.path.format :as upf] [app.util.keyboard :as kbd] [clojure.set :refer [map-invert]] [goog.events :as events] @@ -133,10 +135,10 @@ :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)}} - command])}]) + :d (upf/format-path [{:command :move-to + :params {:x (:x from) + :y (:y from)}} + command])}]) [:& path-point {:position (:params command) :preview? true :zoom zoom}]]) @@ -179,10 +181,10 @@ selected-points (or selected-points #{}) base-content (:content shape) - base-points (mf/use-memo (mf/deps base-content) #(->> base-content ugp/content->points)) + base-points (mf/use-memo (mf/deps base-content) #(->> base-content upg/content->points)) - content (ugp/apply-content-modifiers base-content content-modifiers) - content-points (mf/use-memo (mf/deps content) #(->> content ugp/content->points)) + content (upc/apply-content-modifiers base-content content-modifiers) + content-points (mf/use-memo (mf/deps content) #(->> content upg/content->points)) point->base (->> (map hash-map content-points base-points) (reduce merge)) base->point (map-invert point->base) @@ -190,15 +192,15 @@ points (into #{} content-points) last-command (last content) - last-p (->> content last ugp/command->point) - handlers (ugp/content->handlers content) + last-p (->> content last upc/command->point) + handlers (upc/content->handlers content) start-p? (not (some? last-point)) [snap-selected snap-points] (cond (some? drag-handler) [#{drag-handler} points] - (some? preview) [#{(ugp/command->point preview)} points] + (some? preview) [#{(upc/command->point preview)} points] (some? moving-handler) [#{moving-handler} points] :else [(->> selected-points (map base->point) (into #{})) diff --git a/frontend/src/app/main/ui/workspace/viewport/outline.cljs b/frontend/src/app/main/ui/workspace/viewport/outline.cljs index 11af2cd59a..e292b16dcd 100644 --- a/frontend/src/app/main/ui/workspace/viewport/outline.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/outline.cljs @@ -9,7 +9,7 @@ [app.common.geom.shapes :as gsh] [app.common.pages :as cp] [app.main.refs :as refs] - [app.util.geom.path :as ugp] + [app.util.path.format :as upf] [app.util.object :as obj] [clojure.set :as set] [rumext.alpha :as mf] @@ -27,7 +27,7 @@ path-data (mf/use-memo (mf/deps shape) - #(when path? (ugp/content->path (:content shape)))) + #(when path? (upf/format-path (:content shape)))) {:keys [id x y width height]} shape 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 85e4ff2200..640eb5cffe 100644 --- a/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs @@ -12,12 +12,11 @@ [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] + [app.util.path.tools :as upt] [rumext.alpha :as mf])) (defn check-enabled [content selected-points] - (let [segments (ugp/get-segments content selected-points) - + (let [segments (upt/get-segments content selected-points) points-selected? (not (empty? selected-points)) segments-selected? (not (empty? segments))] {:make-corner points-selected? diff --git a/frontend/src/app/util/geom/path.cljs b/frontend/src/app/util/geom/path.cljs deleted file mode 100644 index c4a9a7249a..0000000000 --- a/frontend/src/app/util/geom/path.cljs +++ /dev/null @@ -1,917 +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.util.geom.path - (: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] - [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" - [point handler] - (let [handler-vector (gpt/to-vec point handler)] - (gpt/add point (gpt/negate handler-vector)))) - -(defn simplify - "Simplifies a drawing done with the pen tool" - ([points] - (simplify points 0.1)) - ([points tolerance] - (let [points (into-array points)] - (into [] (impl-simplify/simplify points tolerance true))))) - -;; -(def commands-regex #"(?i)[mzlhvcsqta][^mzlhvcsqta]*") - -;; Matches numbers for path values allows values like... -.01, 10, +12.22 -;; 0 and 1 are special because can refer to flags -(def num-regex #"[+-]?(\d+(\.\d+)?|\.\d+)(e[+-]?\d+)?") - -(def flag-regex #"[01]") - -(defn extract-params [cmd-str extract-commands] - (loop [result [] - extract-idx 0 - current {} - remain (-> cmd-str (subs 1) (str/trim))] - - (let [[param type] (nth extract-commands extract-idx) - regex (case type - :flag flag-regex - #_:number num-regex) - match (re-find regex remain)] - - (if match - (let [value (-> match first usvg/fix-dot-number d/read-string) - remain (str/replace-first remain regex "") - current (assoc current param value) - extract-idx (inc extract-idx) - [result current extract-idx] - (if (>= extract-idx (count extract-commands)) - [(conj result current) {} 0] - [result current extract-idx])] - (recur result - extract-idx - current - remain)) - (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) - (:move-to :line-to :smooth-quadratic-bezier-curve-to) - (str (:x params) "," - (:y params)) - - :close-path - "" - - (:line-to-horizontal :line-to-vertical) - (str (:value params)) - - :curve-to - (str (:c1x params) "," - (:c1y params) "," - (:c2x params) "," - (:c2y params) "," - (:x params) "," - (:y params)) - - (:smooth-curve-to :quadratic-bezier-curve-to) - (str (:cx params) "," - (:cy params) "," - (:x params) "," - (:y params)) - - :elliptical-arc - (str (:rx params) "," - (:ry params) "," - (:x-axis-rotation params) "," - (:large-arc-flag params) "," - (:sweep-flag params) "," - (:x params) "," - (:y params))))) - -;; Path specification -;; https://www.w3.org/TR/SVG11/paths.html -(defmulti parse-command (comp str/upper first)) - -(defmethod parse-command "M" [cmd] - (let [relative (str/starts-with? cmd "m") - param-list (extract-params cmd [[:x :number] - [:y :number]])] - - (d/concat [{:command :move-to - :relative relative - :params (first param-list)}] - - (for [params (rest param-list)] - {:command :line-to - :relative relative - :params params})))) - -(defmethod parse-command "Z" [cmd] - [{:command :close-path}]) - -(defmethod parse-command "L" [cmd] - (let [relative (str/starts-with? cmd "l") - param-list (extract-params cmd [[:x :number] - [:y :number]])] - (for [params param-list] - {:command :line-to - :relative relative - :params params}))) - -(defmethod parse-command "H" [cmd] - (let [relative (str/starts-with? cmd "h") - param-list (extract-params cmd [[:value :number]])] - (for [params param-list] - {:command :line-to-horizontal - :relative relative - :params params}))) - -(defmethod parse-command "V" [cmd] - (let [relative (str/starts-with? cmd "v") - param-list (extract-params cmd [[:value :number]])] - (for [params param-list] - {:command :line-to-vertical - :relative relative - :params params}))) - -(defmethod parse-command "C" [cmd] - (let [relative (str/starts-with? cmd "c") - param-list (extract-params cmd [[:c1x :number] - [:c1y :number] - [:c2x :number] - [:c2y :number] - [:x :number] - [:y :number]]) - ] - (for [params param-list] - {:command :curve-to - :relative relative - :params params}))) - -(defmethod parse-command "S" [cmd] - (let [relative (str/starts-with? cmd "s") - param-list (extract-params cmd [[:cx :number] - [:cy :number] - [:x :number] - [:y :number]])] - (for [params param-list] - {:command :smooth-curve-to - :relative relative - :params params}))) - -(defmethod parse-command "Q" [cmd] - (let [relative (str/starts-with? cmd "q") - param-list (extract-params cmd [[:cx :number] - [:cy :number] - [:x :number] - [:y :number]])] - (for [params param-list] - {:command :quadratic-bezier-curve-to - :relative relative - :params params}))) - -(defmethod parse-command "T" [cmd] - (let [relative (str/starts-with? cmd "t") - param-list (extract-params cmd [[:x :number] - [:y :number]])] - (for [params param-list] - {:command :smooth-quadratic-bezier-curve-to - :relative relative - :params params}))) - -(defmethod parse-command "A" [cmd] - (let [relative (str/starts-with? cmd "a") - param-list (extract-params cmd [[:rx :number] - [:ry :number] - [:x-axis-rotation :number] - [:large-arc-flag :flag] - [:sweep-flag :flag] - [:x :number] - [:y :number]])] - (for [params param-list] - {:command :elliptical-arc - :relative relative - :params params}))) - -(defn command->string [{:keys [command relative params] :as entry}] - (let [command-str (case command - :move-to "M" - :close-path "Z" - :line-to "L" - :line-to-horizontal "H" - :line-to-vertical "V" - :curve-to "C" - :smooth-curve-to "S" - :quadratic-bezier-curve-to "Q" - :smooth-quadratic-bezier-curve-to "T" - :elliptical-arc "A") - command-str (if relative (str/lower command-str) command-str) - param-list (command->param-list entry)] - (str command-str param-list))) - -(defn cmd-pos [prev-pos {:keys [relative params]}] - (let [{:keys [x y] :or {x (:x prev-pos) y (:y prev-pos)}} params] - (if relative - (-> prev-pos (update :x + x) (update :y + y)) - (gpt/point x y)))) - -(defn arc->beziers [from-p command] - (let [to-command - (fn [[_ _ c1x c1y c2x c2y x y]] - {:command :curve-to - :relative (:relative command) - :params {:c1x c1x :c1y c1y - :c2x c2x :c2y c2y - :x x :y y}}) - - {from-x :x from-y :y} from-p - {:keys [rx ry x-axis-rotation large-arc-flag sweep-flag x y]} (:params command) - result (a2c from-x from-y x y large-arc-flag sweep-flag rx ry x-axis-rotation)] - - (mapv to-command result))) - -(defn smooth->curve - [{:keys [params]} pos handler] - (let [{c1x :x c1y :y} (calculate-opposite-handler pos handler)] - {:c1x c1x - :c1y c1y - :c2x (:cx params) - :c2y (:cy params)})) - -(defn quadratic->curve - [sp ep cp] - (let [cp1 (-> (gpt/to-vec sp cp) - (gpt/scale (/ 2 3)) - (gpt/add sp)) - - cp2 (-> (gpt/to-vec ep cp) - (gpt/scale (/ 2 3)) - (gpt/add ep))] - - {:c1x (:x cp1) - :c1y (:y cp1) - :c2x (:x cp2) - :c2y (:y cp2)})) - -(defn simplify-commands - "Removes some commands and convert relative to absolute coordinates" - [commands] - (let [simplify-command - ;; prev-pos : previous position for the current path. Necesary for relative commands - ;; prev-start : previous move-to necesary for Z commands - ;; prev-cc : previous command control point for cubic beziers - ;; prev-qc : previous command control point for quadratic curves - (fn [[result prev-pos prev-start prev-cc prev-qc] [command prev]] - (let [command (assoc command :prev-pos prev-pos) - - command - (cond-> command - (:relative command) - (-> (assoc :relative false) - (d/update-in-when [:params :c1x] + (:x prev-pos)) - (d/update-in-when [:params :c1y] + (:y prev-pos)) - - (d/update-in-when [:params :c2x] + (:x prev-pos)) - (d/update-in-when [:params :c2y] + (:y prev-pos)) - - (d/update-in-when [:params :cx] + (:x prev-pos)) - (d/update-in-when [:params :cy] + (:y prev-pos)) - - (d/update-in-when [:params :x] + (:x prev-pos)) - (d/update-in-when [:params :y] + (:y prev-pos)) - - (cond-> - (= :line-to-horizontal (:command command)) - (d/update-in-when [:params :value] + (:x prev-pos)) - - (= :line-to-vertical (:command command)) - (d/update-in-when [:params :value] + (:y prev-pos))))) - - params (:params command) - orig-command command - - command - (cond-> command - (= :line-to-horizontal (:command command)) - (-> (assoc :command :line-to) - (update :params dissoc :value) - (assoc-in [:params :x] (:value params)) - (assoc-in [:params :y] (:y prev-pos))) - - (= :line-to-vertical (:command command)) - (-> (assoc :command :line-to) - (update :params dissoc :value) - (assoc-in [:params :y] (:value params)) - (assoc-in [:params :x] (:x prev-pos))) - - (= :smooth-curve-to (:command command)) - (-> (assoc :command :curve-to) - (update :params dissoc :cx :cy) - (update :params merge (smooth->curve command prev-pos prev-cc))) - - (= :quadratic-bezier-curve-to (:command command)) - (-> (assoc :command :curve-to) - (update :params dissoc :cx :cy) - (update :params merge (quadratic->curve prev-pos (gpt/point params) (gpt/point (:cx params) (:cy params))))) - - (= :smooth-quadratic-bezier-curve-to (:command command)) - (-> (assoc :command :curve-to) - (update :params merge (quadratic->curve prev-pos (gpt/point params) (calculate-opposite-handler prev-pos prev-qc))))) - - result (if (= :elliptical-arc (:command command)) - (d/concat result (arc->beziers prev-pos command)) - (conj result command)) - - next-cc (case (:command orig-command) - :smooth-curve-to - (gpt/point (get-in orig-command [:params :cx]) (get-in orig-command [:params :cy])) - - :curve-to - (gpt/point (get-in orig-command [:params :c2x]) (get-in orig-command [:params :c2y])) - - (:line-to-horizontal :line-to-vertical) - (gpt/point (get-in command [:params :x]) (get-in command [:params :y])) - - (gpt/point (get-in orig-command [:params :x]) (get-in orig-command [:params :y]))) - - next-qc (case (:command orig-command) - :quadratic-bezier-curve-to - (gpt/point (get-in orig-command [:params :cx]) (get-in orig-command [:params :cy])) - - :smooth-quadratic-bezier-curve-to - (calculate-opposite-handler prev-pos prev-qc) - - (gpt/point (get-in orig-command [:params :x]) (get-in orig-command [:params :y]))) - - next-pos (if (= :close-path (:command command)) - prev-start - (cmd-pos prev-pos command)) - - next-start (if (= :move-to (:command command)) next-pos prev-start)] - - [result next-pos next-start next-cc next-qc])) - - start (first commands) - start-pos (gpt/point (:params start))] - - (->> (map vector (rest commands) commands) - (reduce simplify-command [[start] start-pos start-pos start-pos start-pos]) - (first)))) - -(defn path->content [path-str] - (let [clean-path-str - (-> path-str - (str/trim) - ;; Change "commas" for spaces - (str/replace #"," " ") - ;; Remove all consecutive spaces - (str/replace #"\s+" " ")) - commands (re-seq commands-regex clean-path-str)] - (-> (mapcat parse-command commands) - (simplify-commands)))) - -(defn content->path [content] - (->> content - (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 - :params {:x (:x to) - :y (:y to)}}) - -(defn make-curve-params - ([point] - (make-curve-params point point point)) - - ([point handler] (make-curve-params point handler point)) - - ([point h1 h2] - {:x (:x point) - :y (:y point) - :c1x (:x h1) - :c1y (:y h1) - :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] - (let [phv (gpt/to-vec point handler)] - (gpt/add point (gpt/negate phv)))) - -(defn opposite-handler-keep-distance - "Calculates the coordinates of the opposite handler but keeping the old distance" - [point handler old-opposite] - (let [old-distance (gpt/distance point old-opposite) - phv (gpt/to-vec point handler) - phv2 (gpt/multiply - (gpt/unit (gpt/negate phv)) - (gpt/point old-distance))] - (gpt/add point phv2))) - -(defn apply-content-modifiers [content modifiers] - (letfn [(apply-to-index [content [index params]] - (if (contains? content index) - (cond-> content - (and - (or (:c1x params) (:c1y params) (:c2x params) (:c2y params)) - (= :line-to (get-in content [index :params :command]))) - (-> (assoc-in [index :command] :curve-to) - (assoc-in [index :params] :curve-to) (make-curve-params - (get-in content [index :params]) - (get-in content [(dec index) :params]))) - - (:x params) (update-in [index :params :x] + (:x params)) - (:y params) (update-in [index :params :y] + (:y params)) - - (:c1x params) (update-in [index :params :c1x] + (:c1x params)) - (:c1y params) (update-in [index :params :c1y] + (:c1y params)) - - (:c2x params) (update-in [index :params :c2x] + (:c2x params)) - (:c2y params) (update-in [index :params :c2y] + (:c2y params))) - content))] - (let [content (if (vector? content) content (into [] content))] - (reduce apply-to-index content modifiers)))) - -(defn content->points [content] - (->> content - (map #(when (-> % :params :x) (gpt/point (-> % :params :x) (-> % :params :y)))) - (remove nil?) - (into []))) - -(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))))) - -(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 pre-cmd)] - (-> [[pre-pos [index :c1]] - [cur-pos [index :c2]]])) - []))) - - (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] - (let [point (if (= prefix :c2) - (command->point (nth content index)) - (command->point (nth content (dec index)))) - - handlers (-> (content->handlers content) - (get point)) - - opposite-prefix (if (= prefix :c1) :c2 :c1)] - (when (<= (count handlers) 2) - (->> handlers - (d/seek (fn [[index prefix]] (= prefix opposite-prefix))) - (first))))) - -(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) (= next 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 - (d/update-when index make-curve prev) - (d/update-when index update-handler :c2 handler) - (d/update-when (inc index) make-curve command) - (d/update-when (inc index) update-handler :c1 handler-opposite))) - - content))] - (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 cur-cmd]))] - - (if (some? cur-cmd) - (recur segments - cur-point - start-point - (first content) - (rest content)) - - segments))))) - -(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) - :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))) - -(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))))))) - -(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)))) - diff --git a/frontend/src/app/util/a2c.js b/frontend/src/app/util/path/arc_to_curve.js similarity index 98% rename from frontend/src/app/util/a2c.js rename to frontend/src/app/util/path/arc_to_curve.js index 62c3dcae91..a4aa02f853 100644 --- a/frontend/src/app/util/a2c.js +++ b/frontend/src/app/util/path/arc_to_curve.js @@ -10,11 +10,11 @@ "use strict"; -goog.provide("app.util.a2c"); +goog.provide("app.util.path.arc_to_curve"); // https://raw.githubusercontent.com/fontello/svgpath/master/lib/a2c.js goog.scope(function() { - const self = app.util.a2c; + const self = app.util.path.arc_to_curve; var TAU = Math.PI * 2; diff --git a/frontend/src/app/util/path/commands.cljs b/frontend/src/app/util/path/commands.cljs new file mode 100644 index 0000000000..7d933991ad --- /dev/null +++ b/frontend/src/app/util/path/commands.cljs @@ -0,0 +1,147 @@ +;; 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.util.path.commands + (:require + [app.common.data :as d] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as gshp] + [app.util.svg :as usvg] + [cuerdas.core :as str] + [clojure.set :as set] + [app.common.math :as mth])) + +(defn command->point + ([prev-pos {:keys [relative params] :as command}] + (let [{:keys [x y] :or {x (:x prev-pos) y (:y prev-pos)}} params] + (if relative + (-> prev-pos (update :x + x) (update :y + y)) + (command->point command)))) + + ([command] + (when-not (nil? command) + (let [{{:keys [x y]} :params} command] + (gpt/point x y))))) + + +(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 + :params {:x (:x to) + :y (:y to)}}) + +(defn make-curve-params + ([point] + (make-curve-params point point point)) + + ([point handler] (make-curve-params point handler point)) + + ([point h1 h2] + {:x (:x point) + :y (:y point) + :c1x (:x h1) + :c1y (:y h1) + :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 apply-content-modifiers [content modifiers] + (letfn [(apply-to-index [content [index params]] + (if (contains? content index) + (cond-> content + (and + (or (:c1x params) (:c1y params) (:c2x params) (:c2y params)) + (= :line-to (get-in content [index :params :command]))) + (-> (assoc-in [index :command] :curve-to) + (assoc-in [index :params] :curve-to) (make-curve-params + (get-in content [index :params]) + (get-in content [(dec index) :params]))) + + (:x params) (update-in [index :params :x] + (:x params)) + (:y params) (update-in [index :params :y] + (:y params)) + + (:c1x params) (update-in [index :params :c1x] + (:c1x params)) + (:c1y params) (update-in [index :params :c1y] + (:c1y params)) + + (:c2x params) (update-in [index :params :c2x] + (:c2x params)) + (:c2y params) (update-in [index :params :c2y] + (:c2y params))) + content))] + (let [content (if (vector? content) content (into [] content))] + (reduce apply-to-index content modifiers)))) + + +(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))))) + +(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 pre-cmd)] + (-> [[pre-pos [index :c1]] + [cur-pos [index :c2]]])) + []))) + + (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] + (let [point (if (= prefix :c2) + (command->point (nth content index)) + (command->point (nth content (dec index)))) + + handlers (-> (content->handlers content) + (get point)) + + opposite-prefix (if (= prefix :c1) :c2 :c1)] + (when (<= (count handlers) 2) + (->> handlers + (d/seek (fn [[index prefix]] (= prefix opposite-prefix))) + (first))))) + diff --git a/frontend/src/app/util/path/format.cljs b/frontend/src/app/util/path/format.cljs new file mode 100644 index 0000000000..8728033b87 --- /dev/null +++ b/frontend/src/app/util/path/format.cljs @@ -0,0 +1,74 @@ +;; 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.util.path.format + (:require + [app.common.data :as d] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as gshp] + [app.util.svg :as usvg] + [cuerdas.core :as str] + [clojure.set :as set] + [app.common.math :as mth] + )) + +(defn command->param-list [command] + (let [params (:params command)] + (case (:command command) + (:move-to :line-to :smooth-quadratic-bezier-curve-to) + (str (:x params) "," + (:y params)) + + :close-path + "" + + (:line-to-horizontal :line-to-vertical) + (str (:value params)) + + :curve-to + (str (:c1x params) "," + (:c1y params) "," + (:c2x params) "," + (:c2y params) "," + (:x params) "," + (:y params)) + + (:smooth-curve-to :quadratic-bezier-curve-to) + (str (:cx params) "," + (:cy params) "," + (:x params) "," + (:y params)) + + :elliptical-arc + (str (:rx params) "," + (:ry params) "," + (:x-axis-rotation params) "," + (:large-arc-flag params) "," + (:sweep-flag params) "," + (:x params) "," + (:y params))))) + +(defn command->string [{:keys [command relative params] :as entry}] + (let [command-str (case command + :move-to "M" + :close-path "Z" + :line-to "L" + :line-to-horizontal "H" + :line-to-vertical "V" + :curve-to "C" + :smooth-curve-to "S" + :quadratic-bezier-curve-to "Q" + :smooth-quadratic-bezier-curve-to "T" + :elliptical-arc "A") + command-str (if relative (str/lower command-str) command-str) + param-list (command->param-list entry)] + (str command-str param-list))) + + +(defn format-path [content] + (->> content + (mapv command->string) + (str/join ""))) diff --git a/frontend/src/app/util/path/geom.cljs b/frontend/src/app/util/path/geom.cljs new file mode 100644 index 0000000000..af99972ee2 --- /dev/null +++ b/frontend/src/app/util/path/geom.cljs @@ -0,0 +1,60 @@ +;; 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.util.path.geom + (:require + [app.common.data :as d] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as gshp] + [app.util.svg :as usvg] + [cuerdas.core :as str] + [clojure.set :as set] + [app.common.math :as mth] + [app.util.path.commands :as upc])) + +(defn calculate-opposite-handler + "Given a point and its handler, gives the symetric handler" + [point handler] + (let [handler-vector (gpt/to-vec point handler)] + (gpt/add point (gpt/negate handler-vector)))) + +(defn split-line-to [from-p cmd val] + (let [to-p (upc/command->point cmd) + sp (gpt/line-val from-p to-p val)] + [(upc/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)] + [(upc/make-curve-to to1 h11 h21) + (upc/make-curve-to to2 h12 h22)])) + +(defn opposite-handler + "Calculates the coordinates of the opposite handler" + [point handler] + (let [phv (gpt/to-vec point handler)] + (gpt/add point (gpt/negate phv)))) + +(defn opposite-handler-keep-distance + "Calculates the coordinates of the opposite handler but keeping the old distance" + [point handler old-opposite] + (let [old-distance (gpt/distance point old-opposite) + phv (gpt/to-vec point handler) + phv2 (gpt/multiply + (gpt/unit (gpt/negate phv)) + (gpt/point old-distance))] + (gpt/add point phv2))) + +(defn content->points [content] + (->> content + (map #(when (-> % :params :x) (gpt/point (-> % :params :x) (-> % :params :y)))) + (remove nil?) + (into []))) + diff --git a/frontend/src/app/util/path/parser.cljs b/frontend/src/app/util/path/parser.cljs new file mode 100644 index 0000000000..09f491555a --- /dev/null +++ b/frontend/src/app/util/path/parser.cljs @@ -0,0 +1,317 @@ +;; 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.util.path.parser + (:require + [app.common.data :as d] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as gshp] + [app.util.path.arc-to-curve :refer [a2c]] + [app.util.path.commands :as upc] + [app.util.svg :as usvg] + [cuerdas.core :as str] + [clojure.set :as set] + [app.common.math :as mth] + [app.util.path.geom :as upg] + )) + +;; +(def commands-regex #"(?i)[mzlhvcsqta][^mzlhvcsqta]*") + +;; Matches numbers for path values allows values like... -.01, 10, +12.22 +;; 0 and 1 are special because can refer to flags +(def num-regex #"[+-]?(\d+(\.\d+)?|\.\d+)(e[+-]?\d+)?") + +(def flag-regex #"[01]") + +(defn extract-params [cmd-str extract-commands] + (loop [result [] + extract-idx 0 + current {} + remain (-> cmd-str (subs 1) (str/trim))] + + (let [[param type] (nth extract-commands extract-idx) + regex (case type + :flag flag-regex + #_:number num-regex) + match (re-find regex remain)] + + (if match + (let [value (-> match first usvg/fix-dot-number d/read-string) + remain (str/replace-first remain regex "") + current (assoc current param value) + extract-idx (inc extract-idx) + [result current extract-idx] + (if (>= extract-idx (count extract-commands)) + [(conj result current) {} 0] + [result current extract-idx])] + (recur result + extract-idx + current + remain)) + (cond-> result + (not (empty? current)) (conj current)))))) + +;; Path specification +;; https://www.w3.org/TR/SVG11/paths.html +(defmulti parse-command (comp str/upper first)) + +(defmethod parse-command "M" [cmd] + (let [relative (str/starts-with? cmd "m") + param-list (extract-params cmd [[:x :number] + [:y :number]])] + + (d/concat [{:command :move-to + :relative relative + :params (first param-list)}] + + (for [params (rest param-list)] + {:command :line-to + :relative relative + :params params})))) + +(defmethod parse-command "Z" [cmd] + [{:command :close-path}]) + +(defmethod parse-command "L" [cmd] + (let [relative (str/starts-with? cmd "l") + param-list (extract-params cmd [[:x :number] + [:y :number]])] + (for [params param-list] + {:command :line-to + :relative relative + :params params}))) + +(defmethod parse-command "H" [cmd] + (let [relative (str/starts-with? cmd "h") + param-list (extract-params cmd [[:value :number]])] + (for [params param-list] + {:command :line-to-horizontal + :relative relative + :params params}))) + +(defmethod parse-command "V" [cmd] + (let [relative (str/starts-with? cmd "v") + param-list (extract-params cmd [[:value :number]])] + (for [params param-list] + {:command :line-to-vertical + :relative relative + :params params}))) + +(defmethod parse-command "C" [cmd] + (let [relative (str/starts-with? cmd "c") + param-list (extract-params cmd [[:c1x :number] + [:c1y :number] + [:c2x :number] + [:c2y :number] + [:x :number] + [:y :number]]) + ] + (for [params param-list] + {:command :curve-to + :relative relative + :params params}))) + +(defmethod parse-command "S" [cmd] + (let [relative (str/starts-with? cmd "s") + param-list (extract-params cmd [[:cx :number] + [:cy :number] + [:x :number] + [:y :number]])] + (for [params param-list] + {:command :smooth-curve-to + :relative relative + :params params}))) + +(defmethod parse-command "Q" [cmd] + (let [relative (str/starts-with? cmd "q") + param-list (extract-params cmd [[:cx :number] + [:cy :number] + [:x :number] + [:y :number]])] + (for [params param-list] + {:command :quadratic-bezier-curve-to + :relative relative + :params params}))) + +(defmethod parse-command "T" [cmd] + (let [relative (str/starts-with? cmd "t") + param-list (extract-params cmd [[:x :number] + [:y :number]])] + (for [params param-list] + {:command :smooth-quadratic-bezier-curve-to + :relative relative + :params params}))) + +(defmethod parse-command "A" [cmd] + (let [relative (str/starts-with? cmd "a") + param-list (extract-params cmd [[:rx :number] + [:ry :number] + [:x-axis-rotation :number] + [:large-arc-flag :flag] + [:sweep-flag :flag] + [:x :number] + [:y :number]])] + (for [params param-list] + {:command :elliptical-arc + :relative relative + :params params}))) + +(defn smooth->curve + [{:keys [params]} pos handler] + (let [{c1x :x c1y :y} (upg/calculate-opposite-handler pos handler)] + {:c1x c1x + :c1y c1y + :c2x (:cx params) + :c2y (:cy params)})) + +(defn quadratic->curve + [sp ep cp] + (let [cp1 (-> (gpt/to-vec sp cp) + (gpt/scale (/ 2 3)) + (gpt/add sp)) + + cp2 (-> (gpt/to-vec ep cp) + (gpt/scale (/ 2 3)) + (gpt/add ep))] + + {:c1x (:x cp1) + :c1y (:y cp1) + :c2x (:x cp2) + :c2y (:y cp2)})) + +(defn arc->beziers [from-p command] + (let [to-command + (fn [[_ _ c1x c1y c2x c2y x y]] + {:command :curve-to + :relative (:relative command) + :params {:c1x c1x :c1y c1y + :c2x c2x :c2y c2y + :x x :y y}}) + + {from-x :x from-y :y} from-p + {:keys [rx ry x-axis-rotation large-arc-flag sweep-flag x y]} (:params command) + result (a2c from-x from-y x y large-arc-flag sweep-flag rx ry x-axis-rotation)] + (mapv to-command result))) + +(defn simplify-commands + "Removes some commands and convert relative to absolute coordinates" + [commands] + (let [simplify-command + ;; prev-pos : previous position for the current path. Necesary for relative commands + ;; prev-start : previous move-to necesary for Z commands + ;; prev-cc : previous command control point for cubic beziers + ;; prev-qc : previous command control point for quadratic curves + (fn [[result prev-pos prev-start prev-cc prev-qc] [command prev]] + (let [command (assoc command :prev-pos prev-pos) + + command + (cond-> command + (:relative command) + (-> (assoc :relative false) + (d/update-in-when [:params :c1x] + (:x prev-pos)) + (d/update-in-when [:params :c1y] + (:y prev-pos)) + + (d/update-in-when [:params :c2x] + (:x prev-pos)) + (d/update-in-when [:params :c2y] + (:y prev-pos)) + + (d/update-in-when [:params :cx] + (:x prev-pos)) + (d/update-in-when [:params :cy] + (:y prev-pos)) + + (d/update-in-when [:params :x] + (:x prev-pos)) + (d/update-in-when [:params :y] + (:y prev-pos)) + + (cond-> + (= :line-to-horizontal (:command command)) + (d/update-in-when [:params :value] + (:x prev-pos)) + + (= :line-to-vertical (:command command)) + (d/update-in-when [:params :value] + (:y prev-pos))))) + + params (:params command) + orig-command command + + command + (cond-> command + (= :line-to-horizontal (:command command)) + (-> (assoc :command :line-to) + (update :params dissoc :value) + (assoc-in [:params :x] (:value params)) + (assoc-in [:params :y] (:y prev-pos))) + + (= :line-to-vertical (:command command)) + (-> (assoc :command :line-to) + (update :params dissoc :value) + (assoc-in [:params :y] (:value params)) + (assoc-in [:params :x] (:x prev-pos))) + + (= :smooth-curve-to (:command command)) + (-> (assoc :command :curve-to) + (update :params dissoc :cx :cy) + (update :params merge (smooth->curve command prev-pos prev-cc))) + + (= :quadratic-bezier-curve-to (:command command)) + (-> (assoc :command :curve-to) + (update :params dissoc :cx :cy) + (update :params merge (quadratic->curve prev-pos (gpt/point params) (gpt/point (:cx params) (:cy params))))) + + (= :smooth-quadratic-bezier-curve-to (:command command)) + (-> (assoc :command :curve-to) + (update :params merge (quadratic->curve prev-pos (gpt/point params) (upg/calculate-opposite-handler prev-pos prev-qc))))) + + result (if (= :elliptical-arc (:command command)) + (d/concat result (arc->beziers prev-pos command)) + (conj result command)) + + next-cc (case (:command orig-command) + :smooth-curve-to + (gpt/point (get-in orig-command [:params :cx]) (get-in orig-command [:params :cy])) + + :curve-to + (gpt/point (get-in orig-command [:params :c2x]) (get-in orig-command [:params :c2y])) + + (:line-to-horizontal :line-to-vertical) + (gpt/point (get-in command [:params :x]) (get-in command [:params :y])) + + (gpt/point (get-in orig-command [:params :x]) (get-in orig-command [:params :y]))) + + next-qc (case (:command orig-command) + :quadratic-bezier-curve-to + (gpt/point (get-in orig-command [:params :cx]) (get-in orig-command [:params :cy])) + + :smooth-quadratic-bezier-curve-to + (upg/calculate-opposite-handler prev-pos prev-qc) + + (gpt/point (get-in orig-command [:params :x]) (get-in orig-command [:params :y]))) + + next-pos (if (= :close-path (:command command)) + prev-start + (upc/command->point prev-pos command)) + + next-start (if (= :move-to (:command command)) next-pos prev-start)] + + [result next-pos next-start next-cc next-qc])) + + start (first commands) + start-pos (gpt/point (:params start))] + + (->> (map vector (rest commands) commands) + (reduce simplify-command [[start] start-pos start-pos start-pos start-pos]) + (first)))) + + +(defn parse-path [path-str] + (let [clean-path-str + (-> path-str + (str/trim) + ;; Change "commas" for spaces + (str/replace #"," " ") + ;; Remove all consecutive spaces + (str/replace #"\s+" " ")) + commands (re-seq commands-regex clean-path-str)] + (-> (mapcat parse-command commands) + (simplify-commands)))) + diff --git a/frontend/src/app/util/geom/path_impl_simplify.js b/frontend/src/app/util/path/path_impl_simplify.js similarity index 96% rename from frontend/src/app/util/geom/path_impl_simplify.js rename to frontend/src/app/util/path/path_impl_simplify.js index 12fe81c8db..2ceb4a3782 100644 --- a/frontend/src/app/util/geom/path_impl_simplify.js +++ b/frontend/src/app/util/path/path_impl_simplify.js @@ -11,10 +11,10 @@ "use strict"; -goog.provide("app.util.geom.path_impl_simplify"); +goog.provide("app.util.path.path_impl_simplify"); goog.scope(function() { - const self = app.util.geom.path_impl_simplify; + const self = app.util.path.path_impl_simplify; // square distance between 2 points function getSqDist(p1, p2) { diff --git a/frontend/src/app/util/path/simplify_curve.cljs b/frontend/src/app/util/path/simplify_curve.cljs new file mode 100644 index 0000000000..c3a400a118 --- /dev/null +++ b/frontend/src/app/util/path/simplify_curve.cljs @@ -0,0 +1,24 @@ +;; 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.util.path.simplify-curve + (:require + [app.common.data :as d] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as gshp] + [app.util.path.path-impl-simplify :as impl-simplify] + [app.util.svg :as usvg] + [cuerdas.core :as str] + [clojure.set :as set] + [app.common.math :as mth])) + +(defn simplify + "Simplifies a drawing done with the pen tool" + ([points] + (simplify points 0.1)) + ([points tolerance] + (let [points (into-array points)] + (into [] (impl-simplify/simplify points tolerance true))))) diff --git a/frontend/src/app/util/path/tools.cljs b/frontend/src/app/util/path/tools.cljs new file mode 100644 index 0000000000..8224d4c9c9 --- /dev/null +++ b/frontend/src/app/util/path/tools.cljs @@ -0,0 +1,385 @@ +;; 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.util.path.tools + (:require + [app.common.data :as d] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as gshp] + [app.util.svg :as usvg] + [cuerdas.core :as str] + [clojure.set :as set] + [app.common.math :as mth] + [app.util.path.commands :as upc] + [app.util.path.geom :as upg] + )) + +(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 (upc/command->point command) + pre-point (upc/command->point prev) + handler-c1 (upc/get-handler command :c1) + handler-c2 (upc/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 (-> (upc/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 (upc/command->point command) + pre-point (upc/command->point previous)] + (-> command + (assoc :command :curve-to) + (assoc :params (upc/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) (= next 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 (upc/command->point command)) + (let [prev-point (if (= :move-to (:command command)) nil (upc/command->point prev)) + next-point (if (= :move-to (:command next)) nil (upc/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 + (d/update-when index make-curve prev) + (d/update-when index update-handler :c2 handler) + (d/update-when (inc index) make-curve command) + (d/update-when (inc index) update-handler :c1 handler-opposite))) + + content))] + (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 + (upc/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 cur-cmd]))] + + (if (some? cur-cmd) + (recur segments + cur-point + start-point + (first content) + (rest content)) + + segments))))) + +(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) + :line-to [cmd (upg/split-line-to start cmd value)] + :curve-to [cmd (upg/split-curve-to start cmd value)] + :close-path [cmd [(upc/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))) + +(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 (upc/command->point cur-cmd) + + old-prev-point (upc/command->point prev-cmd) + new-prev-point (upc/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))))))) + +(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] + [(upc/make-move-to point) + (upc/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 (upc/command->point prev-cmd) + cur-point (upc/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 (upc/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 c7683dfd80165f6f80f7ab3def8fbb9d65adf6c6 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 20 Apr 2021 21:22:15 +0200 Subject: [PATCH 4/7] :sparkles: Improved make curve options --- common/app/common/geom/point.cljc | 37 ++++- .../app/main/data/workspace/path/drawing.cljs | 79 +++++----- .../app/main/data/workspace/path/edition.cljs | 38 ++--- .../app/main/data/workspace/path/helpers.cljs | 91 +++++++++-- .../app/main/data/workspace/path/streams.cljs | 4 +- frontend/src/app/main/ui/shapes/path.cljs | 1 - frontend/src/app/util/path/commands.cljs | 54 +++++-- frontend/src/app/util/path/tools.cljs | 142 ++++++++++++------ 8 files changed, 305 insertions(+), 141 deletions(-) diff --git a/common/app/common/geom/point.cljc b/common/app/common/geom/point.cljc index 6e656d8889..9752f32be8 100644 --- a/common/app/common/geom/point.cljc +++ b/common/app/common/geom/point.cljc @@ -213,12 +213,12 @@ (let [v-length (length v)] (divide v (point v-length v-length)))) -(defn project [v1 v2] +(defn project + "V1 perpendicular projection on vector V2" + [v1 v2] (let [v2-unit (unit v2) - scalar-projection (dot v1 (unit v2))] - (multiply - v2-unit - (point scalar-projection scalar-projection)))) + scalar-proj (dot v1 v2-unit)] + (scale v2-unit scalar-proj))) (defn center-points "Centroid of a group of points" @@ -264,7 +264,34 @@ (scale v))] (add p1 v))) + +(defn rotate + "Rotates the point around center with an angle" + [{px :x py :y} {cx :x cy :y} angle] + (let [angle (mth/radians angle) + + x (+ (* (mth/cos angle) (- px cx)) + (* (mth/sin angle) (- py cy) -1) + cx) + + y (+ (* (mth/sin angle) (- px cx)) + (* (mth/cos angle) (- py cy)) + cy)] + (point x y))) + + +(defn scale-from + "Moves a point in the vector that creates with center with a scale + value" + [point center value] + (add point + (-> (to-vec center point) + (unit) + (scale value)))) + + ;; --- Debug (defmethod pp/simple-dispatch Point [obj] (pr obj)) + diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index 9f359c3afb..08de690f78 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -58,54 +58,51 @@ (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)) +(defn drag-handler + ([{:keys [x y alt? shift?] :as position}] + (drag-handler nil nil :c1 position)) - make-curve - (fn [command] - (let [params (upc/make-curve-params - (get-in content [index :params]) - (get-in content [(dec index) :params]))] - (-> command - (assoc :command :curve-to :params params))))] + ([position index prefix {:keys [x y alt? shift?]}] + (ptk/reify ::drag-handler + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state) + content (get-in state (st/get-path state :content)) - (cond-> state - (= command :line-to) - (update-in (st/get-path state :content index) make-curve)))))) + index (or index (count content)) + prefix (or prefix :c1) + position (or position (upc/command->point (nth content (dec index)))) -(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 (upc/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)))))) + old-handler (helpers/handler->point content index prefix) + + handler-position (cond-> (gpt/point x y) + shift? (helpers/position-fixed-angle position)) + + {dx :x dy :y} (if (some? old-handler) + (gpt/add (gpt/to-vec old-handler position) + (gpt/to-vec position handler-position)) + (gpt/to-vec position handler-position)) + + match-opposite? (not alt?) + + modifiers (helpers/move-handler-modifiers content index prefix match-opposite? match-opposite? dx dy)] + (-> state + (update-in [:workspace-local :edit-path id :content-modifiers] merge modifiers) + (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]) + content (-> (get-in state (st/get-path state :content)) + (upc/apply-content-modifiers modifiers)) + handler (get-in state [:workspace-local :edit-path id :drag-handler])] (-> state - (update-in (st/get-path state :content) upc/apply-content-modifiers modifiers) + (assoc-in (st/get-path state :content) content) (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) @@ -135,17 +132,21 @@ content (get-in state (st/get-path state :content)) snap-toggled (get-in state [:workspace-local :edit-path id :snap-toggled]) points (upg/content->points content) + + handlers (-> (upc/content->handlers content) + (get position)) + + [idx prefix] (when (= (count handlers) 1) (first handlers)) drag-events-stream (->> (streams/position-stream snap-toggled points) (rx/take-until stop-stream) - (rx/map #(drag-handler %)))] + (rx/map #(drag-handler position idx prefix %)))] (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)))) @@ -180,7 +181,6 @@ (rx/of (add-node position)) (streams/drag-stream (rx/concat - (rx/of (start-drag-handler)) drag-events (rx/of (finish-drag))))))))) @@ -204,7 +204,6 @@ (rx/of (add-node down-event)) (streams/drag-stream (rx/concat - (rx/of (start-drag-handler)) drag-events (rx/of (finish-drag))))))) diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index 57162153f0..eb6a63eb2b 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -25,37 +25,23 @@ [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-handler ptk/UpdateEvent (update [_ state] + (let [content (get-in state (st/get-path state :content)) + + modifiers (helpers/move-handler-modifiers content index prefix false match-opposite? dx dy) [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 (upc/opposite-index content index prefix)] - (cond-> state - :always - (-> (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 - ocx (- dx) ocy (- dy))))))) + ] + + (-> state + (update-in [:workspace-local :edit-path id :content-modifiers] merge modifiers) + (assoc-in [:workspace-local :edit-path id :moving-handler] point)))))) (defn apply-content-modifiers [] (ptk/reify ::apply-content-modifiers @@ -174,15 +160,9 @@ content (get-in state (st/get-path state :content)) points (upg/content->points content) - opposite-index (upc/opposite-index content index prefix) - opposite-prefix (if (= prefix :c1) :c2 :c1) - opposite-handler (-> content (get opposite-index) (upc/get-handler opposite-prefix)) - point (-> content (get (if (= prefix :c1) (dec index) index)) (upc/command->point)) handler (-> content (get index) (upc/get-handler prefix)) - current-distance (when opposite-handler (gpt/distance (upg/opposite-handler point handler) opposite-handler)) - 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 @@ -199,7 +179,7 @@ prefix (+ start-delta-x (- (:x pos) (:x start-point))) (+ start-delta-y (- (:y pos) (:y start-point))) - (and (not alt?) match-opposite?)))))) + (not alt?)))))) (rx/concat (rx/of (apply-content-modifiers))))))))) (declare stop-path-edit) diff --git a/frontend/src/app/main/data/workspace/path/helpers.cljs b/frontend/src/app/main/data/workspace/path/helpers.cljs index 57b31b9088..a1f81e51d3 100644 --- a/frontend/src/app/main/data/workspace/path/helpers.cljs +++ b/frontend/src/app/main/data/workspace/path/helpers.cljs @@ -106,15 +106,88 @@ (update :content (fnil conj []) command) (update-selrect)))) +(defn prefix->coords [prefix] + (case prefix + :c1 [:c1x :c1y] + :c2 [:c2x :c2y] + nil)) + +(defn handler->point [content index prefix] + (when (and (some? index) + (some? prefix) + (contains? content index)) + (let [[cx cy :as coords] (prefix->coords prefix)] + (if (= :curve-to (get-in content [index :command])) + (gpt/point (get-in content [index :params cx]) + (get-in content [index :params cy])) + + (gpt/point (get-in content [index :params :x]) + (get-in content [index :params :y])))))) + +(defn handler->node [content index prefix] + (if (= prefix :c1) + (upc/command->point (get content (dec index))) + (upc/command->point (get content index)))) + +(defn angle-points [common p1 p2] + (mth/abs + (gpt/angle-with-other + (gpt/to-vec common p1) + (gpt/to-vec common p2)))) + +(defn calculate-opposite-delta [node handler opposite match-angle? match-distance? dx dy] + (when (and (some? handler) (some? opposite)) + (let [;; To match the angle, the angle should be matching (angle between points 180deg) + angle-handlers (angle-points node handler opposite) + + match-angle? (and match-angle? (<= (mth/abs (- 180 angle-handlers) ) 0.1)) + + ;; To match distance the distance should be matching + match-distance? (and match-distance? (mth/almost-zero? (- (gpt/distance node handler) + (gpt/distance node opposite)))) + + new-handler (-> handler (update :x + dx) (update :y + dy)) + + v1 (gpt/to-vec node handler) + v2 (gpt/to-vec node new-handler) + + delta-angle (gpt/angle-with-other v1 v2) + delta-sign (if (> (* (:y v1) (:x v2)) (* (:x v1) (:y v2))) -1 1) + + distance-scale (/ (gpt/distance node handler) + (gpt/distance node new-handler)) + + new-opposite (cond-> opposite + match-angle? + (gpt/rotate node (* delta-sign delta-angle)) + + match-distance? + (gpt/scale-from node distance-scale))] + [(- (:x new-opposite) (:x opposite)) + (- (:y new-opposite) (:y opposite))]))) + (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 (upc/opposite-index content index prefix)] + [content index prefix match-distance? match-angle? dx dy] - (cond-> {} - :always - (update index assoc cx dx cy dy) + (let [[cx cy] (prefix->coords prefix) + [op-idx op-prefix] (upc/opposite-index content index prefix) - (and match-opposite? opposite-index) - (update opposite-index assoc ocx (- dx) ocy (- dy))))) + node (handler->node content index prefix) + handler (handler->point content index prefix) + opposite (handler->point content op-idx op-prefix) + + [ocx ocy] (prefix->coords op-prefix) + [odx ody] (calculate-opposite-delta node handler opposite match-angle? match-distance? dx dy) + + hnv (if (some? handler) + (gpt/to-vec node (-> handler (update :x + dx) (update :y + dy))) + (gpt/point dx dy))] + + (-> {} + (update index assoc cx dx cy dy) + + (cond-> (and (some? op-idx) (not= opposite node)) + (update op-idx assoc ocx odx ocy ody) + + (and (some? op-idx) (= opposite node) match-distance? match-angle?) + (update op-idx assoc ocx (- (:x hnv)) ocy (- (:y hnv))))))) diff --git a/frontend/src/app/main/data/workspace/path/streams.cljs b/frontend/src/app/main/data/workspace/path/streams.cljs index d61d1a82aa..8912a1141e 100644 --- a/frontend/src/app/main/data/workspace/path/streams.cljs +++ b/frontend/src/app/main/data/workspace/path/streams.cljs @@ -88,7 +88,9 @@ (gpt/add position snap)) position))] (->> ms/mouse-position - (rx/map check-path-snap)))) + (rx/map check-path-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? %))))))) (defn position-stream [snap-toggled points] diff --git a/frontend/src/app/main/ui/shapes/path.cljs b/frontend/src/app/main/ui/shapes/path.cljs index c5de929e14..08b716e152 100644 --- a/frontend/src/app/main/ui/shapes/path.cljs +++ b/frontend/src/app/main/ui/shapes/path.cljs @@ -39,4 +39,3 @@ :base-props props :elem-name "path"}]))) - diff --git a/frontend/src/app/util/path/commands.cljs b/frontend/src/app/util/path/commands.cljs index 7d933991ad..e899cf85ef 100644 --- a/frontend/src/app/util/path/commands.cljs +++ b/frontend/src/app/util/path/commands.cljs @@ -53,22 +53,43 @@ :c2x (:x h2) :c2y (:y h2)})) -(defn make-curve-to [to h1 h2] +(defn update-curve-to + [command h1 h2] + (-> command + (assoc :command :curve-to) + (assoc-in [:params :c1x] (:x h1)) + (assoc-in [:params :c1y] (:y h1)) + (assoc-in [:params :c2x] (:x h2)) + (assoc-in [:params :c2y] (:y h2)))) + +(defn make-curve-to + [to h1 h2] {:command :curve-to :relative false :params (make-curve-params to h1 h2)}) -(defn apply-content-modifiers [content modifiers] +(defn update-handler + [command prefix point] + (let [[cox coy] (if (= prefix :c1) [:c1x :c1y] [:c2x :c2y])] + (-> command + (assoc-in [:params cox] (:x point)) + (assoc-in [:params coy] (:y point))))) + +(defn apply-content-modifiers + "Apply to content a map with point translations" + [content modifiers] (letfn [(apply-to-index [content [index params]] (if (contains? content index) (cond-> content (and (or (:c1x params) (:c1y params) (:c2x params) (:c2y params)) - (= :line-to (get-in content [index :params :command]))) + (= :line-to (get-in content [index :command]))) + (-> (assoc-in [index :command] :curve-to) - (assoc-in [index :params] :curve-to) (make-curve-params - (get-in content [index :params]) - (get-in content [(dec index) :params]))) + (assoc-in [index :params] + (make-curve-params + (get-in content [index :params]) + (get-in content [(dec index) :params])))) (:x params) (update-in [index :params :x] + (:x params)) (:y params) (update-in [index :params :y] + (:y params)) @@ -117,6 +138,7 @@ (mapv (fn [[index _]] index)))) (defn handler-indices + "Return an index where the key is the positions and the values the handlers" [content point] (->> (d/with-prev content) (d/enumerate) @@ -132,16 +154,24 @@ (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)))) - handlers (-> (content->handlers content) - (get point)) + point->handlers (content->handlers content) - opposite-prefix (if (= prefix :c1) :c2 :c1)] - (when (<= (count handlers) 2) + handlers (->> point + (point->handlers ) + (filter (fn [[ci cp]] (and (not= index ci) (not= prefix cp)) )))] + + (when (= (count handlers) 1) (->> handlers - (d/seek (fn [[index prefix]] (= prefix opposite-prefix))) - (first))))) + first)))) + +(defn get-commands + "Returns the commands involving a point with its indices" + [content point] + (->> (d/enumerate content) + (filterv (fn [[idx cmd]] (= (command->point cmd) point))))) diff --git a/frontend/src/app/util/path/tools.cljs b/frontend/src/app/util/path/tools.cljs index 8224d4c9c9..ff81960274 100644 --- a/frontend/src/app/util/path/tools.cljs +++ b/frontend/src/app/util/path/tools.cljs @@ -54,59 +54,113 @@ (reduce change-content $ handlers) (remove-line-curves $)))) +(defn line->curve + [from-p cmd] + + (let [to-p (upc/command->point cmd) + + v (gpt/to-vec from-p to-p) + d (gpt/distance from-p to-p) + + dv1 (-> (gpt/normal-left v) + (gpt/scale (/ d 3))) + + h1 (gpt/add from-p dv1) + + dv2 (-> (gpt/to-vec to-p h1) + (gpt/unit) + (gpt/scale (/ d 3))) + + h2 (gpt/add to-p dv2)] + (-> cmd + (assoc :command :curve-to) + (assoc-in [:params :c1x] (:x h1)) + (assoc-in [:params :c1y] (:y h1)) + (assoc-in [:params :c2x] (:x h2)) + (assoc-in [:params :c2y] (:y h2))))) + (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 (upc/command->point command) - pre-point (upc/command->point previous)] - (-> command - (assoc :command :curve-to) - (assoc :params (upc/make-curve-params cur-point pre-point)))) - command)) + (let [make-curve-cmd (fn [cmd h1 h2] + (-> cmd + (update :params assoc + :c1x (:x h1) :c1y (:y h1) + :c2x (:x h2) :c2y (:y h2)))) - 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)) + indices (upc/point-indices content point) + vectors (->> indices (mapv (fn [index] + (let [cmd (nth content index) + prev-i (dec index) + prev (when (not (= :move-to (:command cmd))) + (get content prev-i)) + next-i (inc index) + next (get content next-i) - calculate-vector - (fn [point next 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))] - (-> base-vector - (gpt/unit) - (gpt/multiply (gpt/point 100))))) + next (when (not (= :move-to (:command next))) + next)] + (hash-map :index index + :prev-i (when (some? prev) prev-i) + :prev-c prev + :prev-p (upc/command->point prev) + :next-i (when (some? next) next-i) + :next-c next + :next-p (upc/command->point next) + :command cmd))))) - redfn (fn [content [index [command prev next]]] - (if (= point (upc/command->point command)) - (let [prev-point (if (= :move-to (:command command)) nil (upc/command->point prev)) - next-point (if (= :move-to (:command next)) nil (upc/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 - (d/update-when index make-curve prev) - (d/update-when index update-handler :c2 handler) - (d/update-when (inc index) make-curve command) - (d/update-when (inc index) update-handler :c1 handler-opposite))) - content))] - (as-> content $ - (reduce redfn $ content-next) - (remove-line-curves $)))) + points (->> vectors (mapcat #(vector (:next-p %) (:prev-p %))) (remove nil?) (into #{}))] + + (cond + (= (count points) 2) + ;; + (let [v1 (gpt/to-vec (first points) point) + v2 (gpt/to-vec (first points) (second points)) + vp (gpt/project v1 v2) + vh (gpt/subtract v1 vp) + + add-curve + (fn [content {:keys [index prev-p next-p next-i]}] + (let [cur-cmd (get content index) + next-cmd (get content next-i) + + ;; New handlers for prev-point and next-point + prev-h (when (some? prev-p) (gpt/add prev-p vh)) + next-h (when (some? next-p) (gpt/add next-p vh)) + + ;; Correct 1/3 to the point improves the curve + prev-correction (when (some? prev-h) (gpt/scale (gpt/to-vec prev-h point) (/ 1 3))) + next-correction (when (some? next-h) (gpt/scale (gpt/to-vec next-h point) (/ 1 3))) + + prev-h (when (some? prev-h) (gpt/add prev-h prev-correction)) + next-h (when (some? next-h) (gpt/add next-h next-correction)) + ] + (cond-> content + (and (= :line-to (:command cur-cmd)) (some? prev-p)) + (update index upc/update-curve-to prev-p prev-h) + + (and (= :line-to (:command next-cmd)) (some? next-p)) + (update next-i upc/update-curve-to next-h next-p) + + (and (= :curve-to (:command cur-cmd)) (some? prev-p)) + (update index upc/update-handler :c2 prev-h) + + (and (= :curve-to (:command next-cmd)) (some? next-p)) + (update next-i upc/update-handler :c1 next-h))))] + (->> vectors (reduce add-curve content))) + + :else + (let [add-curve + (fn [content {:keys [index command prev-p next-c next-i]}] + (cond-> content + (and (= :line-to (:command command))) + (update index #(line->curve prev-p %)) + + (and (= :line-to (:command next-c))) + (update next-i #(line->curve point %))))] + (->> vectors (reduce add-curve content)))))) (defn get-segments "Given a content and a set of points return all the segments in the path From 961a7a2e03a323bde869c8393e9f396bef84bae6 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 21 Apr 2021 14:31:26 +0200 Subject: [PATCH 5/7] :sparkles: Additional changes to path editing --- common/app/common/geom/point.cljc | 2 +- .../src/app/main/data/workspace/common.cljs | 4 +- .../app/main/data/workspace/path/changes.cljs | 43 +++++++++++-------- .../app/main/data/workspace/path/drawing.cljs | 5 ++- .../app/main/data/workspace/path/edition.cljs | 9 ++-- .../app/main/data/workspace/path/helpers.cljs | 3 +- .../main/data/workspace/path/selection.cljs | 1 - .../app/main/data/workspace/path/undo.cljs | 25 ++++++++--- frontend/src/app/util/path/commands.cljs | 11 +++-- 9 files changed, 66 insertions(+), 37 deletions(-) diff --git a/common/app/common/geom/point.cljc b/common/app/common/geom/point.cljc index 9752f32be8..6d78d283a2 100644 --- a/common/app/common/geom/point.cljc +++ b/common/app/common/geom/point.cljc @@ -38,7 +38,7 @@ ([v] (cond (point? v) - v + (Point. (:x v) (:y v)) (number? v) (point v v) diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index 9c3807563b..c8f4c3393f 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -364,7 +364,7 @@ (let [edition (get-in state [:workspace-local :edition]) drawing (get state :workspace-drawing)] ;; Editors handle their own undo's - (when-not (or (some? edition) (some? drawing)) + (when-not (or (some? edition) (not-empty drawing)) (let [undo (:workspace-undo state) items (:items undo) index (or (:index undo) (dec (count items)))] @@ -379,7 +379,7 @@ (watch [_ state stream] (let [edition (get-in state [:workspace-local :edition]) drawing (get state :workspace-drawing)] - (when-not (or (some? edition) (some? drawing)) + (when-not (or (some? edition) (not-empty drawing)) (let [undo (:workspace-undo state) items (:items undo) index (or (:index undo) (dec (count items)))] diff --git a/frontend/src/app/main/data/workspace/path/changes.cljs b/frontend/src/app/main/data/workspace/path/changes.cljs index 84e2de5741..2d046fe6df 100644 --- a/frontend/src/app/main/data/workspace/path/changes.cljs +++ b/frontend/src/app/main/data/workspace/path/changes.cljs @@ -44,25 +44,30 @@ :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))) +(defn save-path-content + ([] + (save-path-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)))))) + ([{:keys [preserve-move-to] :or {preserve-move-to false}}] + (ptk/reify ::save-path-content + ptk/UpdateEvent + (update [_ state] + (let [content (get-in state (st/get-path state :content)) + content (if (and (not preserve-move-to) + (= (-> 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/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index 08de690f78..80ed8631d0 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -136,8 +136,9 @@ handlers (-> (upc/content->handlers content) (get position)) - [idx prefix] (when (= (count handlers) 1) (first handlers)) - + [idx prefix] (when (= (count handlers) 1) + (first handlers)) + drag-events-stream (->> (streams/position-stream snap-toggled points) (rx/take-until stop-stream) diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index eb6a63eb2b..bea6948162 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -224,9 +224,12 @@ (ptk/reify ::create-node-at-position ptk/UpdateEvent (update [_ state] - (let [id (st/get-path-id state)] - (update-in state (st/get-path state :content) upt/split-segments #{from-p to-p} t))) + (let [id (st/get-path-id state) + old-content (get-in state (st/get-path state :content))] + (-> state + (assoc-in [:workspace-local :edit-path id :old-content] old-content) + (update-in (st/get-path state :content) upt/split-segments #{from-p to-p} t)))) ptk/WatchEvent (watch [_ state stream] - (rx/of (changes/save-path-content))))) + (rx/of (changes/save-path-content {:preserve-move-to true}))))) diff --git a/frontend/src/app/main/data/workspace/path/helpers.cljs b/frontend/src/app/main/data/workspace/path/helpers.cljs index a1f81e51d3..85d0c5870b 100644 --- a/frontend/src/app/main/data/workspace/path/helpers.cljs +++ b/frontend/src/app/main/data/workspace/path/helpers.cljs @@ -87,7 +87,8 @@ (defn next-node "Calculates the next-node to be inserted." [shape position prev-point prev-handler] - (let [last-command (-> shape :content last :command) + (let [position (select-keys position [:x :y]) + 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 diff --git a/frontend/src/app/main/data/workspace/path/selection.cljs b/frontend/src/app/main/data/workspace/path/selection.cljs index 903954da38..93bb59e665 100644 --- a/frontend/src/app/main/data/workspace/path/selection.cljs +++ b/frontend/src/app/main/data/workspace/path/selection.cljs @@ -50,7 +50,6 @@ id (get-in state [:workspace-local :edition]) content (get-in state (st/get-path state :content)) 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 #{}) diff --git a/frontend/src/app/main/data/workspace/path/undo.cljs b/frontend/src/app/main/data/workspace/path/undo.cljs index 2c490ecba5..0301b47d3c 100644 --- a/frontend/src/app/main/data/workspace/path/undo.cljs +++ b/frontend/src/app/main/data/workspace/path/undo.cljs @@ -10,6 +10,7 @@ [app.common.data.undo-stack :as u] [app.common.uuid :as uuid] [app.main.data.workspace.path.state :as st] + [app.main.data.workspace.path.changes :as changes] [app.main.store :as store] [beicon.core :as rx] [okulary.core :as l] @@ -26,20 +27,26 @@ (defn- make-entry [state] (let [id (st/get-path-id state)] {:content (get-in state (st/get-path state :content)) + :selrect (get-in state (st/get-path state :selrect)) + :points (get-in state (st/get-path state :points)) :preview (get-in state [:workspace-local :edit-path id :preview]) :last-point (get-in state [:workspace-local :edit-path id :last-point]) :prev-handler (get-in state [:workspace-local :edit-path id :prev-handler])})) -(defn- load-entry [state {:keys [content preview last-point prev-handler]}] - (let [id (st/get-path-id state)] +(defn- load-entry [state {:keys [content selrect points preview last-point prev-handler]}] + (let [id (st/get-path-id state) + old-content (get-in state (st/get-path state :content))] (-> state (d/assoc-in-when (st/get-path state :content) content) + (d/assoc-in-when (st/get-path state :selrect) selrect) + (d/assoc-in-when (st/get-path state :points) points) (d/update-in-when [:workspace-local :edit-path id] assoc :preview preview :last-point last-point - :prev-handler prev-handler)))) + :prev-handler prev-handler + :old-content old-content)))) (defn undo [] (ptk/reify ::undo @@ -54,7 +61,11 @@ (-> (load-entry entry) (d/assoc-in-when [:workspace-local :edit-path id :undo-stack] - undo-stack))))))) + undo-stack))))) + + ptk/WatchEvent + (watch [_ state stream] + (rx/of (changes/save-path-content))))) (defn redo [] (ptk/reify ::redo @@ -68,7 +79,11 @@ (load-entry entry) (d/assoc-in-when [:workspace-local :edit-path id :undo-stack] - undo-stack)))))) + undo-stack)))) + + ptk/WatchEvent + (watch [_ state stream] + (rx/of (changes/save-path-content))))) (defn add-undo-entry [] (ptk/reify ::add-undo-entry diff --git a/frontend/src/app/util/path/commands.cljs b/frontend/src/app/util/path/commands.cljs index e899cf85ef..9800b77724 100644 --- a/frontend/src/app/util/path/commands.cljs +++ b/frontend/src/app/util/path/commands.cljs @@ -165,9 +165,14 @@ (point->handlers ) (filter (fn [[ci cp]] (and (not= index ci) (not= prefix cp)) )))] - (when (= (count handlers) 1) - (->> handlers - first)))) + (cond + (= (count handlers) 1) + (->> handlers first) + + (and (= :c1 prefix) (= (count content) index)) + [(dec index) :c2] + + :else nil))) (defn get-commands From 6331dff484f60c00680ce4ddf226add184180461 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 21 Apr 2021 17:39:47 +0200 Subject: [PATCH 6/7] :sparkles: Snapping 180 angle with opposites handlers --- common/app/common/geom/point.cljc | 2 + common/app/common/geom/shapes/transforms.cljc | 2 +- .../app/main/data/workspace/path/drawing.cljs | 2 +- .../app/main/data/workspace/path/edition.cljs | 5 +- .../app/main/data/workspace/path/helpers.cljs | 35 ++--------- .../app/main/data/workspace/path/streams.cljs | 34 ++++++++--- .../main/ui/workspace/shapes/path/editor.cljs | 60 +++++++++++++------ frontend/src/app/util/path/commands.cljs | 24 ++++++++ 8 files changed, 108 insertions(+), 56 deletions(-) diff --git a/common/app/common/geom/point.cljc b/common/app/common/geom/point.cljc index 6d78d283a2..0d3feeb063 100644 --- a/common/app/common/geom/point.cljc +++ b/common/app/common/geom/point.cljc @@ -162,6 +162,8 @@ (mth/precision 6))] (if (mth/nan? d) 0 d))))) +(defn angle-sign [v1 v2] + (if (> (* (:y v1) (:x v2)) (* (:x v1) (:y v2))) -1 1)) (defn update-angle "Update the angle of the point." diff --git a/common/app/common/geom/shapes/transforms.cljc b/common/app/common/geom/shapes/transforms.cljc index 2f80ffb958..78f78dc880 100644 --- a/common/app/common/geom/shapes/transforms.cljc +++ b/common/app/common/geom/shapes/transforms.cljc @@ -161,7 +161,7 @@ v2 (gpt/to-vec center p2) rot-angle (gpt/angle-with-other v1 v2) - rot-sign (if (> (* (:y v1) (:x v2)) (* (:x v1) (:y v2))) -1 1)] + rot-sign (gpt/angle-sign v1 v2)] (* rot-sign rot-angle))) (defn- calculate-dimensions diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index 80ed8631d0..1a3d79d27e 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -73,7 +73,7 @@ prefix (or prefix :c1) position (or position (upc/command->point (nth content (dec index)))) - old-handler (helpers/handler->point content index prefix) + old-handler (upc/handler->point content index prefix) handler-position (cond-> (gpt/point x y) shift? (helpers/position-fixed-angle position)) diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index bea6948162..95c5cddf55 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -163,11 +163,14 @@ point (-> content (get (if (= prefix :c1) (dec index) index)) (upc/command->point)) handler (-> content (get index) (upc/get-handler prefix)) + [op-idx op-prefix] (upc/opposite-index content index prefix) + opposite (upc/handler->point content op-idx op-prefix) + snap-toggled (get-in state [:workspace-local :edit-path id :snap-toggled])] (streams/drag-stream (rx/concat - (->> (streams/move-handler-stream snap-toggled start-point handler points) + (->> (streams/move-handler-stream snap-toggled start-point point handler opposite 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/helpers.cljs b/frontend/src/app/main/data/workspace/path/helpers.cljs index 85d0c5870b..17c4edb012 100644 --- a/frontend/src/app/main/data/workspace/path/helpers.cljs +++ b/frontend/src/app/main/data/workspace/path/helpers.cljs @@ -107,29 +107,6 @@ (update :content (fnil conj []) command) (update-selrect)))) -(defn prefix->coords [prefix] - (case prefix - :c1 [:c1x :c1y] - :c2 [:c2x :c2y] - nil)) - -(defn handler->point [content index prefix] - (when (and (some? index) - (some? prefix) - (contains? content index)) - (let [[cx cy :as coords] (prefix->coords prefix)] - (if (= :curve-to (get-in content [index :command])) - (gpt/point (get-in content [index :params cx]) - (get-in content [index :params cy])) - - (gpt/point (get-in content [index :params :x]) - (get-in content [index :params :y])))))) - -(defn handler->node [content index prefix] - (if (= prefix :c1) - (upc/command->point (get content (dec index))) - (upc/command->point (get content index)))) - (defn angle-points [common p1 p2] (mth/abs (gpt/angle-with-other @@ -153,7 +130,7 @@ v2 (gpt/to-vec node new-handler) delta-angle (gpt/angle-with-other v1 v2) - delta-sign (if (> (* (:y v1) (:x v2)) (* (:x v1) (:y v2))) -1 1) + delta-sign (gpt/angle-sign v1 v2) distance-scale (/ (gpt/distance node handler) (gpt/distance node new-handler)) @@ -170,14 +147,14 @@ (defn move-handler-modifiers [content index prefix match-distance? match-angle? dx dy] - (let [[cx cy] (prefix->coords prefix) + (let [[cx cy] (upc/prefix->coords prefix) [op-idx op-prefix] (upc/opposite-index content index prefix) - node (handler->node content index prefix) - handler (handler->point content index prefix) - opposite (handler->point content op-idx op-prefix) + node (upc/handler->node content index prefix) + handler (upc/handler->point content index prefix) + opposite (upc/handler->point content op-idx op-prefix) - [ocx ocy] (prefix->coords op-prefix) + [ocx ocy] (upc/prefix->coords op-prefix) [odx ody] (calculate-opposite-delta node handler opposite match-angle? match-distance? dx dy) hnv (if (some? handler) diff --git a/frontend/src/app/main/data/workspace/path/streams.cljs b/frontend/src/app/main/data/workspace/path/streams.cljs index 8912a1141e..4dbe0e846e 100644 --- a/frontend/src/app/main/data/workspace/path/streams.cljs +++ b/frontend/src/app/main/data/workspace/path/streams.cljs @@ -73,24 +73,45 @@ (rx/map check-path-snap)))) (defn move-handler-stream - [snap-toggled start-point handler points] + [snap-toggled start-point node handler opposite points] (let [zoom (get-in @st/state [:workspace-local :zoom] 1) ranges (snap/create-ranges points) d-pos (/ snap/snap-path-accuracy zoom) + initial-angle (gpt/angle-with-other (gpt/to-vec node handler) + (gpt/to-vec node opposite)) + check-path-snap (fn [position] (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)) + handler (gpt/add handler delta) + + v1 (gpt/to-vec node opposite) + v2 (gpt/to-vec node handler) + + rot-angle (gpt/angle-with-other v1 v2) + rot-sign (gpt/angle-sign v1 v2) + + snap-opposite-angle? + (and (or (:alt? position) (> (- 180 initial-angle) 0.1)) + (<= (- 180 rot-angle) 5))] + + (cond + snap-opposite-angle? + (let [rot-handler (gpt/rotate handler node (- 180 (* rot-sign rot-angle))) + snap (gpt/to-vec handler rot-handler)] + (merge position (gpt/add position snap))) + + :else + (let [snap (snap/get-snap-delta [handler] ranges d-pos)] + (merge position (gpt/add position snap))))) position))] (->> ms/mouse-position - (rx/map check-path-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? %))))))) + (rx/with-latest merge (->> ms/mouse-position-alt (rx/map #(hash-map :alt? %)))) + (rx/map check-path-snap)))) (defn position-stream [snap-toggled points] @@ -115,6 +136,5 @@ (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 7457e35d7a..0d2e805cd3 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -80,7 +80,7 @@ (= edit-mode :move) cur/pointer-node) :fill "transparent"}}]])) -(mf/defc path-handler [{:keys [index prefix point handler zoom selected? hover? edit-mode]}] +(mf/defc path-handler [{:keys [index prefix point handler zoom selected? hover? edit-mode snap-angle?]}] (when (and point handler) (let [{:keys [x y]} handler on-enter @@ -108,6 +108,16 @@ :y2 y :style {:stroke (if hover? pc/black-color pc/gray-color) :stroke-width (/ 1 zoom)}}] + + (when snap-angle? + [:line + {:x1 (:x point) + :y1 (:y point) + :x2 x + :y2 y + :style {:stroke pc/secondary-color + :stroke-width (/ 1 zoom)}}]) + [:rect {:x (- x (/ 3 zoom)) :y (- y (/ 3 zoom)) @@ -157,6 +167,18 @@ :style {:stroke pc/secondary-color :stroke-width (/ 1 zoom)}}])])) +(defn matching-handler? [content node handlers] + (when (= 2 (count handlers)) + (let [[[i1 p1] [i2 p2]] handlers + p1 (upc/handler->point content i1 p1) + p2 (upc/handler->point content i2 p2) + + v1 (gpt/to-vec node p1) + v2 (gpt/to-vec node p2) + + angle (gpt/angle-with-other v1 v2)] + (<= (- 180 angle) 0.1)))) + (mf/defc path-editor [{:keys [shape zoom]}] @@ -258,20 +280,24 @@ [: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-hover? (contains? hover-handlers [index prefix])] - (when (not= position handler-position) - [:& path-handler {:point position - :handler handler-position - :index index - :prefix prefix - :zoom zoom - :hover? handler-hover? - :edit-mode edit-mode}])))] + (let [pos-handlers (get handlers position)] + (for [[index prefix] pos-handlers] + (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-hover? (contains? hover-handlers [index prefix]) + moving-handler? (= handler-position moving-handler) + matching-handler? (matching-handler? content position pos-handlers)] + (when (not= position handler-position) + [:& path-handler {:point position + :handler handler-position + :index index + :prefix prefix + :zoom zoom + :hover? handler-hover? + :snap-angle? (and moving-handler? matching-handler?) + :edit-mode edit-mode}]))))] [:& path-point {:position position :zoom zoom :edit-mode edit-mode @@ -289,6 +315,6 @@ (when show-snap? [:g.path-snap {:pointer-events "none"} [:& path-snap {:selected snap-selected - :points snap-points - :zoom zoom}]])])) + :points snap-points + :zoom zoom}]])])) diff --git a/frontend/src/app/util/path/commands.cljs b/frontend/src/app/util/path/commands.cljs index 9800b77724..f284a457c5 100644 --- a/frontend/src/app/util/path/commands.cljs +++ b/frontend/src/app/util/path/commands.cljs @@ -180,3 +180,27 @@ [content point] (->> (d/enumerate content) (filterv (fn [[idx cmd]] (= (command->point cmd) point))))) + + +(defn prefix->coords [prefix] + (case prefix + :c1 [:c1x :c1y] + :c2 [:c2x :c2y] + nil)) + +(defn handler->point [content index prefix] + (when (and (some? index) + (some? prefix) + (contains? content index)) + (let [[cx cy :as coords] (prefix->coords prefix)] + (if (= :curve-to (get-in content [index :command])) + (gpt/point (get-in content [index :params cx]) + (get-in content [index :params cy])) + + (gpt/point (get-in content [index :params :x]) + (get-in content [index :params :y])))))) + +(defn handler->node [content index prefix] + (if (= prefix :c1) + (command->point (get content (dec index))) + (command->point (get content index)))) From 65ad46ab38fdb146a9cfbd322a0aab5e2d5d2dad Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 22 Apr 2021 14:06:11 +0200 Subject: [PATCH 7/7] :sparkles: Shortcuts for paths --- .../styles/main/partials/workspace.scss | 7 +- frontend/src/app/main/data/shortcuts.cljs | 57 +++++++---- .../src/app/main/data/workspace/path.cljs | 7 +- .../app/main/data/workspace/path/drawing.cljs | 3 +- .../main/data/workspace/path/shortcuts.cljs | 94 +++++++++++++++++++ .../app/main/data/workspace/path/streams.cljs | 20 ++-- .../app/main/data/workspace/path/undo.cljs | 45 +++++---- .../app/main/data/workspace/shortcuts.cljs | 22 +---- frontend/src/app/main/ui/hooks.cljs | 15 +-- frontend/src/app/main/ui/workspace.cljs | 38 ++++---- .../src/app/main/ui/workspace/viewport.cljs | 1 + .../app/main/ui/workspace/viewport/hooks.cljs | 15 +++ .../ui/workspace/viewport/path_actions.cljs | 32 +++++-- frontend/translations/en.po | 50 +++++++++- frontend/translations/es.po | 30 ++++++ 15 files changed, 320 insertions(+), 116 deletions(-) create mode 100644 frontend/src/app/main/data/workspace/path/shortcuts.cljs diff --git a/frontend/resources/styles/main/partials/workspace.scss b/frontend/resources/styles/main/partials/workspace.scss index 65965da270..d8e3b69ef7 100644 --- a/frontend/resources/styles/main/partials/workspace.scss +++ b/frontend/resources/styles/main/partials/workspace.scss @@ -305,10 +305,9 @@ } &.is-disabled { - opacity: 0.3; - - &:hover svg { - fill: initial; + cursor: initial; + svg { + fill: $color-gray-20; } } diff --git a/frontend/src/app/main/data/shortcuts.cljs b/frontend/src/app/main/data/shortcuts.cljs index 00701f830c..850380517c 100644 --- a/frontend/src/app/main/data/shortcuts.cljs +++ b/frontend/src/app/main/data/shortcuts.cljs @@ -6,15 +6,13 @@ (ns app.main.data.shortcuts (:require - [app.main.data.workspace.colors :as mdc] - [app.main.data.workspace.transforms :as dwt] - [app.main.store :as st] - [app.util.dom :as dom] - [potok.core :as ptk] - [beicon.core :as rx] - [app.config :as cfg]) + ["mousetrap" :as mousetrap] + [app.config :as cfg] + [app.util.logging :as log]) (:refer-clojure :exclude [meta])) +(log/set-level! :warn) + (def mac-command "\u2318") (def mac-option "\u2325") (def mac-delete "\u232B") @@ -46,20 +44,41 @@ [shortcut] (c-mod (a-mod shortcut))) -(defn bind-shortcuts [shortcuts bind-fn cb-fn] - (doseq [[key {:keys [command disabled fn type]}] shortcuts] - (when-not disabled - (if (vector? command) - (doseq [cmd (seq command)] - (bind-fn cmd (cb-fn key fn) type)) - (bind-fn command (cb-fn key fn) type))))) +(defn bind-shortcuts + ([shortcuts-config] + (bind-shortcuts + shortcuts-config + mousetrap/bind + (fn [key cb] + (fn [event] + (log/debug :msg (str "Shortcut" key)) + (.preventDefault event) + (cb event))))) + + ([shortcuts-config bind-fn cb-fn] + (doseq [[key {:keys [command disabled fn type]}] shortcuts-config] + (when-not disabled + (if (vector? command) + (doseq [cmd (seq command)] + (bind-fn cmd (cb-fn key fn) type)) + (bind-fn command (cb-fn key fn) type)))))) + +(defn remove-shortcuts + [] + (mousetrap/reset)) (defn meta [key] - (str - (if (cfg/check-platform? :macos) - mac-command - "Ctrl+") - key)) + ;; If the key is "+" we need to surround with quotes + ;; otherwise will not be very readable + (let [key (if (and (not (cfg/check-platform? :macos)) + (= key "+")) + "\"+\"" + key)] + (str + (if (cfg/check-platform? :macos) + mac-command + "Ctrl+") + key))) (defn shift [key] (str diff --git a/frontend/src/app/main/data/workspace/path.cljs b/frontend/src/app/main/data/workspace/path.cljs index 7dd1a6d5d5..ed3d3c8ee2 100644 --- a/frontend/src/app/main/data/workspace/path.cljs +++ b/frontend/src/app/main/data/workspace/path.cljs @@ -10,7 +10,8 @@ [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])) + [app.main.data.workspace.path.tools :as tools] + [app.main.data.workspace.path.undo :as undo])) ;; Drawing (d/export drawing/handle-new-shape) @@ -42,3 +43,7 @@ (d/export tools/separate-nodes) (d/export tools/toggle-snap) +;; Undo/redo +(d/export undo/undo-path) +(d/export undo/redo-path) +(d/export undo/merge-head) diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index 1a3d79d27e..08fdad18ee 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -113,7 +113,8 @@ (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)))))) + (rx/of (preview-next-point handler) + (undo/merge-head)))))) (declare close-path-drag-end) diff --git a/frontend/src/app/main/data/workspace/path/shortcuts.cljs b/frontend/src/app/main/data/workspace/path/shortcuts.cljs new file mode 100644 index 0000000000..25eac7a4d9 --- /dev/null +++ b/frontend/src/app/main/data/workspace/path/shortcuts.cljs @@ -0,0 +1,94 @@ +;; 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.path.shortcuts + (:require + [app.main.data.shortcuts :as ds] + [app.main.data.workspace :as dw] + [app.main.data.workspace.path :as drp] + [app.main.store :as st] + [beicon.core :as rx] + [potok.core :as ptk])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Shortcuts +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Shortcuts format https://github.com/ccampbell/mousetrap + +(defn esc-pressed [] + (ptk/reify :esc-pressed + ptk/WatchEvent + (watch [_ state stream] + ;; Not interrupt when we're editing a path + (let [edition-id (or (get-in state [:workspace-drawing :object :id]) + (get-in state [:workspace-local :edition])) + path-edit-mode (get-in state [:workspace-local :edit-path edition-id :edit-mode])] + (if-not (= :draw path-edit-mode) + (rx/of :interrupt (dw/deselect-all true)) + (rx/empty)))))) + +(def shortcuts + {:move-nodes {:tooltip "V" + :command "v" + :fn #(st/emit! (drp/change-edit-mode :move))} + + :draw-nodes {:tooltip "P" + :command "p" + :fn #(st/emit! (drp/change-edit-mode :draw))} + + :add-node {:tooltip (ds/meta "+") + :command (ds/c-mod "+") + :fn #(st/emit! (drp/add-node))} + + :delete-node {:tooltip (ds/supr) + :command ["del" "backspace"] + :fn #(st/emit! (drp/remove-node))} + + :merge-nodes {:tooltip (ds/meta "J") + :command (ds/c-mod "j") + :fn #(st/emit! (drp/merge-nodes))} + + :join-nodes {:tooltip (ds/meta-shift "J") + :command (ds/c-mod "shift+j") + :fn #(st/emit! (drp/join-nodes))} + + :separate-nodes {:tooltip (ds/meta "K") + :command (ds/c-mod "k") + :fn #(st/emit! (drp/separate-nodes))} + + :make-corner {:tooltip (ds/meta "B") + :command (ds/c-mod "b") + :fn #(st/emit! (drp/make-corner))} + + :make-curve {:tooltip (ds/meta-shift "B") + :command (ds/c-mod "shift+b") + :fn #(st/emit! (drp/make-curve))} + + :snap-nodes {:tooltip (ds/meta "'") + :command (ds/c-mod "'") + :fn #(st/emit! (drp/toggle-snap))} + + :escape {:tooltip (ds/esc) + :command "escape" + :fn #(st/emit! (esc-pressed))} + + :start-editing {:tooltip (ds/enter) + :command "enter" + :fn #(st/emit! (dw/start-editing-selected))} + + :undo {:tooltip (ds/meta "Z") + :command (ds/c-mod "z") + :fn #(st/emit! (drp/undo-path))} + + :redo {:tooltip (ds/meta "Y") + :command [(ds/c-mod "shift+z") (ds/c-mod "y")] + :fn #(st/emit! (drp/redo-path))} + }) + +(defn get-tooltip [shortcut] + (assert (contains? shortcuts shortcut) (str shortcut)) + (get-in shortcuts [shortcut :tooltip])) diff --git a/frontend/src/app/main/data/workspace/path/streams.cljs b/frontend/src/app/main/data/workspace/path/streams.cljs index 4dbe0e846e..3bf10d31e8 100644 --- a/frontend/src/app/main/data/workspace/path/streams.cljs +++ b/frontend/src/app/main/data/workspace/path/streams.cljs @@ -72,6 +72,14 @@ (->> ms/mouse-position (rx/map check-path-snap)))) +(defn get-angle [node handler opposite] + (when (and (some? node) (some? handler) (some? opposite)) + (let [v1 (gpt/to-vec node opposite) + v2 (gpt/to-vec node handler) + rot-angle (gpt/angle-with-other v1 v2) + rot-sign (gpt/angle-sign v1 v2)] + [rot-angle rot-sign]))) + (defn move-handler-stream [snap-toggled start-point node handler opposite points] @@ -79,8 +87,7 @@ ranges (snap/create-ranges points) d-pos (/ snap/snap-path-accuracy zoom) - initial-angle (gpt/angle-with-other (gpt/to-vec node handler) - (gpt/to-vec node opposite)) + [initial-angle] (get-angle node handler opposite) check-path-snap (fn [position] @@ -88,14 +95,11 @@ (let [delta (gpt/subtract position start-point) handler (gpt/add handler delta) - v1 (gpt/to-vec node opposite) - v2 (gpt/to-vec node handler) - - rot-angle (gpt/angle-with-other v1 v2) - rot-sign (gpt/angle-sign v1 v2) + [rot-angle rot-sign] (get-angle node handler opposite) snap-opposite-angle? - (and (or (:alt? position) (> (- 180 initial-angle) 0.1)) + (and (some? rot-angle) + (or (:alt? position) (> (- 180 initial-angle) 0.1)) (<= (- 180 rot-angle) 5))] (cond diff --git a/frontend/src/app/main/data/workspace/path/undo.cljs b/frontend/src/app/main/data/workspace/path/undo.cljs index 0301b47d3c..061de999d8 100644 --- a/frontend/src/app/main/data/workspace/path/undo.cljs +++ b/frontend/src/app/main/data/workspace/path/undo.cljs @@ -48,8 +48,8 @@ :prev-handler prev-handler :old-content old-content)))) -(defn undo [] - (ptk/reify ::undo +(defn undo-path [] + (ptk/reify ::undo-path ptk/UpdateEvent (update [_ state] (let [id (st/get-path-id state) @@ -65,10 +65,10 @@ ptk/WatchEvent (watch [_ state stream] - (rx/of (changes/save-path-content))))) + (rx/of (changes/save-path-content {:preserve-move-to true}))))) -(defn redo [] - (ptk/reify ::redo +(defn redo-path [] + (ptk/reify ::redo-path ptk/UpdateEvent (update [_ state] (let [id (st/get-path-id state) @@ -85,6 +85,23 @@ (watch [_ state stream] (rx/of (changes/save-path-content))))) +(defn merge-head + "Joins the head with the previous undo in one. This is done so when the user changes a + node handlers after adding it the undo merges both in one operation only" + [] + (ptk/reify ::add-undo-entry + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state) + entry (make-entry state) + stack (get-in state [:workspace-local :edit-path id :undo-stack]) + head (u/peek stack) + stack (-> stack (u/undo) (u/fixup head))] + (-> state + (d/assoc-in-when + [:workspace-local :edit-path id :undo-stack] + stack)))))) + (defn add-undo-entry [] (ptk/reify ::add-undo-entry ptk/UpdateEvent @@ -136,20 +153,10 @@ (rx/filter stop-undo?) (rx/take 1))] (rx/concat - (->> (rx/merge - (->> (rx/from-atom path-content-ref {:emit-current-value? true}) - (rx/filter (comp not nil?)) - (rx/map #(add-undo-entry))) - - (->> stream - (rx/filter undo-event?) - (rx/map #(undo))) - - (->> stream - (rx/filter redo-event?) - (rx/map #(redo)))) - - (rx/take-until stop-undo-stream)) + (->> (rx/from-atom path-content-ref {:emit-current-value? true}) + (rx/take-until stop-undo-stream) + (rx/filter (comp not nil?)) + (rx/map #(add-undo-entry))) (rx/of (end-path-undo)))))))))) diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index f87abcabda..f10934f16f 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -6,10 +6,9 @@ (ns app.main.data.workspace.shortcuts (:require - [app.config :as cfg] - [app.main.data.workspace.colors :as mdc] [app.main.data.shortcuts :as ds] [app.main.data.workspace :as dw] + [app.main.data.workspace.colors :as mdc] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.drawing :as dwd] [app.main.data.workspace.libraries :as dwl] @@ -17,28 +16,13 @@ [app.main.data.workspace.transforms :as dwt] [app.main.store :as st] [app.util.dom :as dom] - [beicon.core :as rx] [potok.core :as ptk])) -;; \u2318P - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Shortcuts ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Shortcuts impl https://github.com/ccampbell/mousetrap - -(defn esc-pressed [] - (ptk/reify :esc-pressed - ptk/WatchEvent - (watch [_ state stream] - ;; Not interrupt when we're editing a path - (let [edition-id (or (get-in state [:workspace-drawing :object :id]) - (get-in state [:workspace-local :edition])) - path-edit-mode (get-in state [:workspace-local :edit-path edition-id :edit-mode])] - (if-not (= :draw path-edit-mode) - (rx/of :interrupt (dw/deselect-all true)) - (rx/empty)))))) +;; Shortcuts format https://github.com/ccampbell/mousetrap (def shortcuts {:toggle-layers {:tooltip (ds/alt "L") @@ -252,7 +236,7 @@ :escape {:tooltip (ds/esc) :command "escape" - :fn #(st/emit! (esc-pressed))} + :fn #(st/emit! :interrupt (dw/deselect-all true))} :start-editing {:tooltip (ds/enter) :command "enter" diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index 01e7fa5cdc..bc2a8fd2df 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -7,9 +7,8 @@ (ns app.main.ui.hooks "A collection of general purpose react hooks." (:require - ["mousetrap" :as mousetrap] [app.common.spec :as us] - [app.main.data.shortcuts :refer [bind-shortcuts]] + [app.main.data.shortcuts :as dsc] [app.util.dom :as dom] [app.util.object :as obj] [app.util.dom.dnd :as dnd] @@ -39,16 +38,8 @@ [shortcuts] (mf/use-effect (fn [] - (bind-shortcuts - shortcuts - mousetrap/bind - (fn [key cb] - (fn [event] - (log/debug :msg (str "Shortcut" key)) - (.preventDefault event) - (cb event)))) - (fn [] (mousetrap/reset)))) - nil) + (dsc/bind-shortcuts shortcuts) + (fn [] (dsc/remove-shortcuts))))) (defn invisible-image [] diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 0ed6b0a778..fa6226f918 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -11,7 +11,6 @@ [app.main.data.history :as udh] [app.main.data.messages :as dm] [app.main.data.workspace :as dw] - [app.main.data.workspace.shortcuts :as sc] [app.main.refs :as refs] [app.main.store :as st] [app.main.streams :as ms] @@ -21,13 +20,13 @@ [app.main.ui.workspace.colorpalette :refer [colorpalette]] [app.main.ui.workspace.colorpicker] [app.main.ui.workspace.context-menu :refer [context-menu]] + [app.main.ui.workspace.coordinates :as coordinates] [app.main.ui.workspace.header :refer [header]] [app.main.ui.workspace.left-toolbar :refer [left-toolbar]] [app.main.ui.workspace.libraries] [app.main.ui.workspace.rules :refer [horizontal-rule vertical-rule]] [app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]] [app.main.ui.workspace.viewport :refer [viewport]] - [app.main.ui.workspace.coordinates :as coordinates] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] @@ -114,30 +113,29 @@ (mf/defc workspace {::mf/wrap [mf/memo]} [{:keys [project-id file-id page-id layout-name] :as props}] - (mf/use-effect - (mf/deps layout-name) - #(st/emit! (dw/initialize-layout layout-name))) - - (mf/use-effect - (mf/deps project-id file-id) - (fn [] - (st/emit! (dw/initialize-file project-id file-id)) - (st/emitf (dw/finalize-file project-id file-id)))) - - (mf/use-effect - (fn [] - ;; Close any non-modal dialog that may be still open - (st/emitf dm/hide))) - - (hooks/use-shortcuts sc/shortcuts) (let [file (mf/deref refs/workspace-file) project (mf/deref refs/workspace-project) layout (mf/deref refs/workspace-layout)] (mf/use-effect - (mf/deps file) - #(dom/set-html-title (tr "title.workspace" (:name file)))) + (mf/deps layout-name) + #(st/emit! (dw/initialize-layout layout-name))) + + (mf/use-effect + (mf/deps project-id file-id) + (fn [] + (st/emit! (dw/initialize-file project-id file-id)) + (st/emitf (dw/finalize-file project-id file-id)))) + + (mf/use-effect + (fn [] + ;; Close any non-modal dialog that may be still open + (st/emitf dm/hide))) + + (mf/use-effect + (mf/deps file) + #(dom/set-html-title (tr "title.workspace" (:name file)))) [:& (mf/provider ctx/current-file-id) {:value (:id file)} [:& (mf/provider ctx/current-team-id) {:value (:team-id project)} diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index fd8b79170b..6e73069a5f 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -143,6 +143,7 @@ (hooks/setup-keyboard alt? ctrl?) (hooks/setup-hover-shapes page-id move-stream selected objects transform selected ctrl? hover hover-ids) (hooks/setup-viewport-modifiers modifiers selected objects render-ref) + (hooks/setup-shortcuts path-editing? drawing-path?) [:div.viewport [:div.viewport-overlays diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 12abb98535..2c0a08f8c4 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -9,7 +9,10 @@ [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.common.pages :as cp] + [app.main.data.shortcuts :as dsc] [app.main.data.workspace :as dw] + [app.main.data.workspace.path.shortcuts :as psc] + [app.main.data.workspace.shortcuts :as wsc] [app.main.store :as st] [app.main.streams :as ms] [app.main.ui.hooks :as hooks] @@ -148,3 +151,15 @@ (if modifiers (utils/update-transform render-node roots modifiers) (utils/remove-transform render-node roots)))))) + +(defn setup-shortcuts [path-editing? drawing-path?] + (mf/use-effect + (mf/deps path-editing? drawing-path?) + (fn [] + (cond + (or drawing-path? path-editing?) + (dsc/bind-shortcuts psc/shortcuts) + + :else + (dsc/bind-shortcuts wsc/shortcuts)) + dsc/remove-shortcuts))) 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 640eb5cffe..b53dcc2176 100644 --- a/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs @@ -8,10 +8,12 @@ (:require [app.main.data.workspace.path :as drp] [app.main.data.workspace.path.helpers :as wph] + [app.main.data.workspace.path.shortcuts :as sc] [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.i18n :as i18n :refer [tr]] [app.util.path.tools :as upt] [rumext.alpha :as mf])) @@ -106,65 +108,75 @@ [:div.viewport-actions-group ;; Draw Mode - [:div.viewport-actions-entry + [:div.viewport-actions-entry.tooltip.tooltip-bottom {:class (when (= edit-mode :draw) "is-toggled") + :alt (tr "workspace.path.actions.move-nodes" (sc/get-tooltip :move-nodes)) :on-click on-select-draw-mode} i/pen] ;; Edit mode - [:div.viewport-actions-entry + [:div.viewport-actions-entry.tooltip.tooltip-bottom {:class (when (= edit-mode :move) "is-toggled") + :alt (tr "workspace.path.actions.draw-nodes" (sc/get-tooltip :draw-nodes)) :on-click on-select-edit-mode} i/pointer-inner]] [:div.viewport-actions-group ;; Add Node - [:div.viewport-actions-entry + [:div.viewport-actions-entry.tooltip.tooltip-bottom {:class (when-not (:add-node enabled-buttons) "is-disabled") + :alt (tr "workspace.path.actions.add-node" (sc/get-tooltip :add-node)) :on-click on-add-node} i/nodes-add] ;; Remove node - [:div.viewport-actions-entry + [:div.viewport-actions-entry.tooltip.tooltip-bottom {:class (when-not (:remove-node enabled-buttons) "is-disabled") + :alt (tr "workspace.path.actions.delete-node" (sc/get-tooltip :delete-node)) :on-click on-remove-node} i/nodes-remove]] [:div.viewport-actions-group ;; Merge Nodes - [:div.viewport-actions-entry + [:div.viewport-actions-entry.tooltip.tooltip-bottom {:class (when-not (:merge-nodes enabled-buttons) "is-disabled") + :alt (tr "workspace.path.actions.merge-nodes" (sc/get-tooltip :merge-nodes)) :on-click on-merge-nodes} i/nodes-merge] ;; Join Nodes - [:div.viewport-actions-entry + [:div.viewport-actions-entry.tooltip.tooltip-bottom {:class (when-not (:join-nodes enabled-buttons) "is-disabled") + :alt (tr "workspace.path.actions.join-nodes" (sc/get-tooltip :join-nodes)) :on-click on-join-nodes} i/nodes-join] ;; Separate Nodes - [:div.viewport-actions-entry + [:div.viewport-actions-entry.tooltip.tooltip-bottom {:class (when-not (:separate-nodes enabled-buttons) "is-disabled") + :alt (tr "workspace.path.actions.separate-nodes" (sc/get-tooltip :separate-nodes)) :on-click on-separate-nodes} i/nodes-separate]] ;; Make Corner [:div.viewport-actions-group - [:div.viewport-actions-entry + [:div.viewport-actions-entry.tooltip.tooltip-bottom {:class (when-not (:make-corner enabled-buttons) "is-disabled") + :alt (tr "workspace.path.actions.make-corner" (sc/get-tooltip :make-corner)) :on-click on-make-corner} i/nodes-corner] ;; Make Curve - [:div.viewport-actions-entry + [:div.viewport-actions-entry.tooltip.tooltip-bottom {:class (when-not (:make-curve enabled-buttons) "is-disabled") + :alt (tr "workspace.path.actions.make-curve" (sc/get-tooltip :make-curve)) :on-click on-make-curve} i/nodes-curve]] ;; Toggle snap [:div.viewport-actions-group - [:div.viewport-actions-entry + [:div.viewport-actions-entry.tooltip.tooltip-bottom {:class (when snap-toggled "is-toggled") + :alt (tr "workspace.path.actions.snap-nodes" (sc/get-tooltip :snap-nodes)) :on-click on-toggle-snap} i/nodes-snap]]])) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index a47d706640..eb794a40a4 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1,11 +1,25 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR Free Software Foundation, Inc. +# FIRST AUTHOR , YEAR. +# +#, fuzzy msgid "" msgstr "" -"Language: en\n" +"Project-Id-Version: PACKAGE VERSION\n" +"PO-Revision-Date: 2021-04-22 13:43+0200\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" +"Content-Type: text/plain; charset=iso-8859-1\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +# ~ msgid "" +# ~ msgstr "" +# ~ "Language: en\n" +# ~ "MIME-Version: 1.0\n" +# ~ "Content-Type: text/plain; charset=utf-8\n" +# ~ "Content-Transfer-Encoding: 8bit\n" +# ~ "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" msgstr "Already have an account?" @@ -2169,6 +2183,36 @@ msgstr "Vertical align" msgid "workspace.options.use-play-button" msgstr "Use the play button at the header to run the prototype view." +msgid "workspace.path.actions.add-node" +msgstr "Add node (%s)" + +msgid "workspace.path.actions.delete-node" +msgstr "Delete node (%s)" + +msgid "workspace.path.actions.draw-nodes" +msgstr "Draw nodes (%s)" + +msgid "workspace.path.actions.join-nodes" +msgstr "Join nodes (%s)" + +msgid "workspace.path.actions.make-corner" +msgstr "To corner (%s)" + +msgid "workspace.path.actions.make-curve" +msgstr "To curve (%s)" + +msgid "workspace.path.actions.merge-nodes" +msgstr "Merge nodes (%s)" + +msgid "workspace.path.actions.move-nodes" +msgstr "Move nodes (%s)" + +msgid "workspace.path.actions.separate-nodes" +msgstr "Separate nodes (%s)" + +msgid "workspace.path.actions.snap-nodes" +msgstr "Snap nodes (%s)" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.back" msgstr "Send to back" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index b104624773..5286588b4e 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -2153,6 +2153,36 @@ msgstr "Alineación vertical" msgid "workspace.options.use-play-button" msgstr "Usa el botón de play de la cabecera para arrancar la vista de prototipo." +msgid "workspace.path.actions.add-node" +msgstr "Añadir nodo (%s)" + +msgid "workspace.path.actions.delete-node" +msgstr "Borrar nodos (%s)" + +msgid "workspace.path.actions.draw-nodes" +msgstr "Dibujar nodos (%s)" + +msgid "workspace.path.actions.join-nodes" +msgstr "Unir nodos (%s)" + +msgid "workspace.path.actions.make-corner" +msgstr "Convertir en esquina (%s)" + +msgid "workspace.path.actions.make-curve" +msgstr "Convertir en curva (%s)" + +msgid "workspace.path.actions.merge-nodes" +msgstr "Fusionar nodos (%s)" + +msgid "workspace.path.actions.move-nodes" +msgstr "Mover nodes (%s)" + +msgid "workspace.path.actions.separate-nodes" +msgstr "Separar nodos (%s)" + +msgid "workspace.path.actions.snap-nodes" +msgstr "Alinear nodos (%s)" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.back" msgstr "Enviar al fondo"