diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index 95c5cddf55..6ccb29a9c4 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -22,6 +22,7 @@ [app.util.path.commands :as upc] [app.util.path.geom :as upg] [app.util.path.tools :as upt] + [app.util.path.subpaths :as ups] [beicon.core :as rx] [potok.core :as ptk])) @@ -35,9 +36,7 @@ modifiers (helpers/move-handler-modifiers content index prefix false match-opposite? dx dy) [cx cy] (if (= prefix :c1) [:c1x :c1y] [:c2x :c2y]) point (gpt/point (+ (get-in content [index :params cx]) dx) - (+ (get-in content [index :params cy]) dy)) - - ] + (+ (get-in content [index :params cy]) dy))] (-> state (update-in [:workspace-local :edit-path id :content-modifiers] merge modifiers) @@ -192,8 +191,8 @@ (ptk/reify ::start-path-edit ptk/UpdateEvent (update [_ state] - (let [edit-path (get-in state [:workspace-local :edit-path id])] - + (let [edit-path (get-in state [:workspace-local :edit-path id]) + state (update-in state (st/get-path state :content) ups/close-subpaths)] (cond-> state (or (not edit-path) (= :draw (:edit-mode edit-path))) (assoc-in [:workspace-local :edit-path id] {:edit-mode :move diff --git a/frontend/src/app/main/data/workspace/path/helpers.cljs b/frontend/src/app/main/data/workspace/path/helpers.cljs index 17c4edb012..79c120d4a1 100644 --- a/frontend/src/app/main/data/workspace/path/helpers.cljs +++ b/frontend/src/app/main/data/workspace/path/helpers.cljs @@ -11,10 +11,11 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.math :as mth] - [app.main.data.workspace.path.state :refer [get-path]] [app.main.data.workspace.path.common :as common] + [app.main.data.workspace.path.state :refer [get-path]] [app.main.streams :as ms] [app.util.path.commands :as upc] + [app.util.path.subpaths :as ups] [potok.core :as ptk])) ;; CONSTANTS @@ -105,6 +106,7 @@ (let [command (next-node shape position prev-point prev-handler)] (-> shape (update :content (fnil conj []) command) + (update :content ups/close-subpaths) (update-selrect)))) (defn angle-points [common p1 p2] diff --git a/frontend/src/app/main/data/workspace/path/shortcuts.cljs b/frontend/src/app/main/data/workspace/path/shortcuts.cljs index 25eac7a4d9..7c28926f68 100644 --- a/frontend/src/app/main/data/workspace/path/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/path/shortcuts.cljs @@ -40,8 +40,8 @@ :command "p" :fn #(st/emit! (drp/change-edit-mode :draw))} - :add-node {:tooltip (ds/meta "+") - :command (ds/c-mod "+") + :add-node {:tooltip "+" + :command "+" :fn #(st/emit! (drp/add-node))} :delete-node {:tooltip (ds/supr) @@ -52,20 +52,20 @@ :command (ds/c-mod "j") :fn #(st/emit! (drp/merge-nodes))} - :join-nodes {:tooltip (ds/meta-shift "J") - :command (ds/c-mod "shift+j") + :join-nodes {:tooltip "J" + :command "j" :fn #(st/emit! (drp/join-nodes))} - :separate-nodes {:tooltip (ds/meta "K") - :command (ds/c-mod "k") + :separate-nodes {:tooltip "K" + :command "k" :fn #(st/emit! (drp/separate-nodes))} - :make-corner {:tooltip (ds/meta "B") - :command (ds/c-mod "b") + :make-corner {:tooltip "B" + :command "b" :fn #(st/emit! (drp/make-corner))} - :make-curve {:tooltip (ds/meta-shift "B") - :command (ds/c-mod "shift+b") + :make-curve {:tooltip (ds/meta "B") + :command (ds/c-mod "b") :fn #(st/emit! (drp/make-curve))} :snap-nodes {:tooltip (ds/meta "'") diff --git a/frontend/src/app/main/data/workspace/path/tools.cljs b/frontend/src/app/main/data/workspace/path/tools.cljs index 059636565a..303e504282 100644 --- a/frontend/src/app/main/data/workspace/path/tools.cljs +++ b/frontend/src/app/main/data/workspace/path/tools.cljs @@ -11,33 +11,46 @@ [app.main.data.workspace.path.common :as common] [app.main.data.workspace.path.state :as st] [app.util.path.tools :as upt] + [app.util.path.subpaths :as ups] [app.common.geom.point :as gpt] [beicon.core :as rx] [potok.core :as ptk])) (defn process-path-tool "Generic function that executes path transformations with the content and selected nodes" - [tool-fn] - (ptk/reify ::process-path-tool - ptk/WatchEvent - (watch [_ state stream] - (let [id (st/get-path-id state) - page-id (:current-page-id state) - shape (get-in state (st/get-path state)) - selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) - new-content (tool-fn (:content shape) selected-points) - [rch uch] (changes/generate-path-changes page-id shape (:content shape) new-content)] - (rx/of (dwc/commit-changes rch uch {:commit-local? true})))))) + ([tool-fn] + (process-path-tool nil tool-fn)) + ([points tool-fn] + (ptk/reify ::process-path-tool + ptk/WatchEvent + (watch [_ state stream] + (let [id (st/get-path-id state) + page-id (:current-page-id state) + shape (get-in state (st/get-path state)) + selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) + points (or points selected-points) + new-content (-> (tool-fn (:content shape) points) + (ups/close-subpaths)) + [rch uch] (changes/generate-path-changes page-id shape (:content shape) new-content)] + (rx/of (dwc/commit-changes rch uch {:commit-local? true}))))))) -(defn make-corner [] - (process-path-tool - (fn [content points] - (reduce upt/make-corner-point content points)))) +(defn make-corner + ([] + (make-corner nil)) + ([point] + (process-path-tool + #{point} + (fn [content points] + (reduce upt/make-corner-point content points))))) -(defn make-curve [] - (process-path-tool - (fn [content points] - (reduce upt/make-curve-point content points)))) +(defn make-curve + ([] + (make-curve nil)) + ([point] + (process-path-tool + #{point} + (fn [content points] + (reduce upt/make-curve-point content points))))) (defn add-node [] (process-path-tool (fn [content points] (upt/split-segments content points 0.5)))) 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 0d2e805cd3..16101fecaf 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -26,7 +26,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? new-point?]}] +(mf/defc path-point [{:keys [position zoom edit-mode hover? selected? preview? start-path? last-p? new-point? curve?]}] (let [{:keys [x y]} position on-enter @@ -45,8 +45,15 @@ (when (and new-point? (some? (meta position))) (st/emit! (drp/create-node-at-position (meta position)))) - (let [shift? (kbd/shift? event)] + (let [shift? (kbd/shift? event) + ctrl? (kbd/ctrl? event)] (cond + (and (= edit-mode :move) ctrl? (not curve?)) + (st/emit! (drp/make-curve position)) + + (and (= edit-mode :move) ctrl? curve?) + (st/emit! (drp/make-corner position)) + (= edit-mode :move) ;; If we're dragging a selected item we don't change the selection (st/emit! (drp/start-move-path-point position shift?)) @@ -274,37 +281,42 @@ :zoom zoom}]]) (for [position points] - (let [point-selected? (contains? selected-points (get point->base position)) + (let [show-handler? + (fn [[index prefix]] + (let [handler-position (upc/handler->point content index prefix)] + (not= position handler-position))) + + pos-handlers (get handlers position) + 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))] + last-p? (= last-point (get point->base position)) + + pos-handlers (->> pos-handlers (filter show-handler?)) + curve? (not (empty? pos-handlers))] [:g.path-node [:g.point-handlers {:pointer-events (when (= edit-mode :draw) "none")} - (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}]))))] + (for [[index prefix] pos-handlers] + (let [handler-position (upc/handler->point content index prefix) + handler-hover? (contains? hover-handlers [index prefix]) + moving-handler? (= handler-position moving-handler) + matching-handler? (matching-handler? content position pos-handlers)] + [:& 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 :selected? point-selected? :hover? point-hover? :last-p? last-p? - :start-path? start-p?}]])) + :start-path? start-p? + :curve? curve?}]])) (when prev-handler [:g.prev-handler {:pointer-events "none"} 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 b53dcc2176..d504faf56b 100644 --- a/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs @@ -19,6 +19,7 @@ (defn check-enabled [content selected-points] (let [segments (upt/get-segments content selected-points) + num-points (count selected-points) points-selected? (not (empty? selected-points)) segments-selected? (not (empty? segments))] {:make-corner points-selected? @@ -26,7 +27,7 @@ :add-node segments-selected? :remove-node points-selected? :merge-nodes segments-selected? - :join-nodes points-selected? + :join-nodes (and points-selected? (>= num-points 2)) :separate-nodes segments-selected?})) (mf/defc path-actions [{:keys [shape]}] diff --git a/frontend/src/app/util/path/format.cljs b/frontend/src/app/util/path/format.cljs index 8728033b87..a79a5a7e50 100644 --- a/frontend/src/app/util/path/format.cljs +++ b/frontend/src/app/util/path/format.cljs @@ -7,13 +7,9 @@ (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] + [app.util.path.commands :as upc] [cuerdas.core :as str] - [clojure.set :as set] - [app.common.math :as mth] - )) + [app.util.path.subpaths :as ups])) (defn command->param-list [command] (let [params (:params command)] @@ -69,6 +65,20 @@ (defn format-path [content] - (->> content - (mapv command->string) - (str/join ""))) + (with-out-str + (loop [last-move nil + current (first content) + content (rest content)] + + (when (some? current) + (let [point (upc/command->point current) + current-move? (= :move-to (:command current)) + last-move (if current-move? point last-move)] + (print (command->string current)) + + (when (and (not current-move?) (= last-move point)) + (print "Z")) + + (recur last-move + (first content) + (rest content))))))) diff --git a/frontend/src/app/util/path/subpaths.cljs b/frontend/src/app/util/path/subpaths.cljs new file mode 100644 index 0000000000..3337b6c884 --- /dev/null +++ b/frontend/src/app/util/path/subpaths.cljs @@ -0,0 +1,133 @@ +;; 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.subpaths + (:require + [app.common.data :as d] + [app.util.path.commands :as upc])) + +(defn make-subpath + "Creates a subpath either from a single command or with all the data" + ([command] + (let [p (upc/command->point command)] + (make-subpath p p [command]))) + ([from to data] + {:from from + :to to + :data data})) + +(defn add-subpath-command + "Adds a command to the subpath" + [subpath command] + (let [p (upc/command->point command)] + (-> subpath + (assoc :to p) + (update :data conj command)))) + +(defn reverse-command + "Reverses a single command" + [command prev] + + (let [{:keys [x y]} (:params prev) + {:keys [c1x c1y c2x c2y]} (:params command)] + + (-> command + (update :params assoc :x x :y y) + + (cond-> (= :curve-to (:command command)) + (update :params assoc + :c1x c2x :c1y c2y + :c2x c1x :c2y c1y))))) + +(defn reverse-subpath + "Reverses a subpath starting with move-to" + [subpath] + + (let [reverse-commands + (fn [result [command prev]] + (if (some? prev) + (conj result (reverse-command command prev)) + result)) + + new-data (->> subpath :data d/with-prev reverse + (reduce reverse-commands [(upc/make-move-to (:to subpath))]))] + + (make-subpath (:to subpath) (:from subpath) new-data))) + +(defn get-subpaths + "Retrieves every subpath inside the current content" + [content] + (let [reduce-subpath + (fn [subpaths current] + (let [is-move? (= :move-to (:command current)) + last-idx (dec (count subpaths))] + (if is-move? + (conj subpaths (make-subpath current)) + (update subpaths last-idx add-subpath-command current))))] + (->> content + (reduce reduce-subpath [])))) + +(defn subpaths-join + "Join two subpaths together when the first finish where the second starts" + [subpath other] + (assert (= (:to subpath) (:from other))) + (-> subpath + (update :data d/concat (rest (:data other))) + (assoc :to (:to other)))) + +(defn- merge-paths + "Tries to merge into candidate the subpaths. Will return the candidate with the subpaths merged + and removed from subpaths the subpaths merged" + [candidate subpaths] + (let [merge-with-candidate + (fn [[candidate result] current] + (cond + (= (:to current) (:from current)) + [candidate (conj result current)] + + (= (:to candidate) (:from current)) + [(subpaths-join candidate current) result] + + (= (:to candidate) (:to current)) + [(subpaths-join candidate (reverse-subpath current)) result] + + :else + [candidate (conj result current)]))] + + (->> subpaths + (reduce merge-with-candidate [candidate []])))) + +(defn close-subpaths + "Searches a path for posible supaths that can create closed loops and merge them" + [content] + (let [subpaths (get-subpaths content) + closed-subpaths + (loop [result [] + current (first subpaths) + subpaths (rest subpaths)] + + (if (some? current) + (let [[new-current new-subpaths] + (if (= (:from current) (:to current)) + [current subpaths] + (merge-paths current subpaths))] + + (if (= current new-current) + ;; If equal we haven't found any matching subpaths we advance + (recur (conj result new-current) + (first new-subpaths) + (rest new-subpaths)) + + ;; If different we need to pass again the merge to check for additional + ;; subpaths to join + (recur result + new-current + new-subpaths))) + result))] + + (->> closed-subpaths + (mapcat :data) + (into []))))