0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-13 16:21:57 -05:00

Merge pull request #851 from penpot/advanced-path-options

Advanced path options
This commit is contained in:
Andrés Moya 2021-04-22 16:02:09 +02:00 committed by GitHub
commit 43f7750658
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 2023 additions and 1238 deletions

View file

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

View file

@ -38,7 +38,7 @@
([v]
(cond
(point? v)
v
(Point. (:x v) (:y v))
(number? v)
(point v v)
@ -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."
@ -213,12 +215,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 +266,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))

View file

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

View file

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

View file

@ -305,10 +305,9 @@
}
&.is-disabled {
opacity: 0.3;
&:hover svg {
fill: initial;
cursor: initial;
svg {
fill: $color-gray-20;
}
}

View file

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

View file

@ -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) (not-empty 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) (not-empty 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

View file

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

View file

@ -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)
@ -22,6 +23,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)
@ -41,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)

View file

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

View file

@ -18,8 +18,10 @@
[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]
[app.util.path.commands :as upc]
[app.util.path.geom :as upg]
[beicon.core :as rx]
[potok.core :as ptk]))
@ -56,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 (ugp/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 (ugp/command->point (nth content index))
handler-position (cond-> (gpt/point x y)
shift? (helpers/position-fixed-angle node-position))
{dx :x dy :y} (gpt/subtract handler-position node-position)
match-opposite? (not alt?)
modifiers (helpers/move-handler-modifiers content (inc index) :c1 match-opposite? dx dy)]
(-> state
(update-in [:workspace-local :edit-path id :content-modifiers] merge modifiers)
(assoc-in [:workspace-local :edit-path id :prev-handler] handler-position)
(assoc-in [:workspace-local :edit-path id :drag-handler] handler-position))))))
old-handler (upc/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) ugp/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)
@ -114,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)
@ -132,18 +132,23 @@
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)
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))))
@ -165,7 +170,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])
@ -178,7 +183,6 @@
(rx/of (add-node position))
(streams/drag-stream
(rx/concat
(rx/of (start-drag-handler))
drag-events
(rx/of (finish-drag)))))))))
@ -202,7 +206,6 @@
(rx/of (add-node down-event))
(streams/drag-stream
(rx/concat
(rx/of (start-drag-handler))
drag-events
(rx/of (finish-drag)))))))
@ -222,7 +225,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])
@ -245,6 +248,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)

View file

@ -17,42 +17,31 @@
[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]
[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]))
(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 (ugp/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
@ -64,10 +53,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)]
@ -78,8 +67,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]
@ -145,7 +134,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
@ -169,22 +158,19 @@
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-prefix (if (= prefix :c1) :c2 :c1)
opposite-handler (-> content (get opposite-index) (ugp/get-handler opposite-prefix))
point (-> content (get (if (= prefix :c1) (dec index) index)) (upc/command->point))
handler (-> content (get index) (upc/get-handler prefix))
point (-> content (get (if (= prefix :c1) (dec index) index)) (ugp/command->point))
handler (-> content (get index) (ugp/get-handler prefix))
[op-idx op-prefix] (upc/opposite-index content index prefix)
opposite (upc/handler->point content op-idx op-prefix)
current-distance (when opposite-handler (gpt/distance (ugp/opposite-handler point handler) opposite-handler))
match-opposite? (and opposite-handler (mth/almost-zero? current-distance))
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?]}]
@ -196,7 +182,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)
@ -221,6 +207,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))))
@ -234,3 +221,18 @@
(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)
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 {:preserve-move-to true})))))

View file

@ -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
@ -87,14 +87,15 @@
(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
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})))
@ -106,15 +107,65 @@
(update :content (fnil conj []) command)
(update-selrect))))
(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 (gpt/angle-sign v1 v2)
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 (ugp/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] (upc/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 (upc/handler->node content index prefix)
handler (upc/handler->point content index prefix)
opposite (upc/handler->point content op-idx 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)
(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)))))))

View file

@ -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 #{})

View file

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

View file

@ -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)
@ -72,22 +72,49 @@
(->> 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 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] (get-angle node handler 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)
[rot-angle rot-sign] (get-angle node handler opposite)
snap-opposite-angle?
(and (some? rot-angle)
(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/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/map check-path-snap))))
(defn position-stream
@ -103,7 +130,7 @@
ranges-stream
(->> content-stream
(rx/map ugp/content->points)
(rx/map upg/content->points)
(rx/map snap/create-ranges))]
(->> ms/mouse-position
@ -113,6 +140,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? %)))))))

View file

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

View file

@ -0,0 +1,162 @@
;; 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.data.workspace.path.changes :as changes]
[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))
: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 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
:old-content old-content))))
(defn undo-path []
(ptk/reify ::undo-path
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)))))
ptk/WatchEvent
(watch [_ state stream]
(rx/of (changes/save-path-content {:preserve-move-to true})))))
(defn redo-path []
(ptk/reify ::redo-path
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))))
ptk/WatchEvent
(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
(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/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))))))))))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,14 +17,16 @@
[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]
[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 +42,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)
@ -75,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
@ -103,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))
@ -130,10 +145,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}]])
@ -152,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]}]
@ -176,10 +203,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)
@ -187,19 +214,25 @@
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 #{}))
(->> 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
(or (some? drag-handler)
(some? preview)
(some? moving-handler)
moving-nodes))
handle-double-click-outside
(fn [event]
@ -213,6 +246,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,30 +268,36 @@
(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")}
(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
@ -269,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}]])]))

View file

@ -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,15 +133,17 @@
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)
(hooks/setup-viewport-modifiers modifiers selected objects render-ref)
(hooks/setup-shortcuts path-editing? drawing-path?)
[:div.viewport
[:div.viewport-overlays
@ -224,7 +227,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

View file

@ -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]
@ -58,23 +61,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))))))
@ -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)))

View file

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

View file

@ -8,16 +8,17 @@
(: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.geom.path :as ugp]
[app.util.i18n :as i18n :refer [tr]]
[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?
@ -107,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]]]))

View file

@ -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* #{}))

View file

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

View file

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

View file

@ -0,0 +1,206 @@
;; 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 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 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 :command])))
(-> (assoc-in [index :command] :curve-to)
(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))
(: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
"Return an index where the key is the positions and the values the handlers"
[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))))
point->handlers (content->handlers content)
handlers (->> point
(point->handlers )
(filter (fn [[ci cp]] (and (not= index ci) (not= prefix cp)) )))]
(cond
(= (count handlers) 1)
(->> handlers first)
(and (= :c1 prefix) (= (count content) index))
[(dec index) :c2]
:else nil)))
(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)))))
(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))))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,439 @@
;; 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 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 [make-curve-cmd (fn [cmd h1 h2]
(-> cmd
(update :params assoc
:c1x (:x h1) :c1y (:y h1)
:c2x (:x h2) :c2y (:y h2))))
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)
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)))))
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
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))))

View file

@ -1,11 +1,25 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR Free Software Foundation, Inc.
# FIRST AUTHOR <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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"

View file

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