Merge pull request #864 from penpot/fix/close-paths

Close paths
This commit is contained in:
Andrés Moya 2021-04-23 13:30:39 +02:00 committed by GitHub
commit 9e39e53488
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 238 additions and 68 deletions

View File

@ -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

View File

@ -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]

View File

@ -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 "'")

View File

@ -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))))

View File

@ -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"}

View File

@ -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]}]

View File

@ -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)))))))

View File

@ -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 []))))