0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-23 23:18:48 -05:00

Merge pull request #399 from penpot/bezier-fixes

Bezier improvements
This commit is contained in:
Andrey Antukh 2020-11-26 14:12:19 +01:00 committed by GitHub
commit 24ea6a63c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 564 additions and 275 deletions

View file

@ -55,6 +55,11 @@
;;(assert (not (nil? y)))
(Point. x y)))
(defn angle->point [{:keys [x y]} angle distance]
(point
(+ x (* distance (mth/cos angle)))
(- y (* distance (mth/sin angle)))))
(defn add
"Returns the addition of the supplied value to both
coordinates of the point as a new point."

View file

@ -263,6 +263,7 @@
border-radius: 3px;
svg {
pointer-events: none;
width: 20px;
height: 20px;
}

View file

@ -9,23 +9,63 @@
(ns app.main.data.workspace.drawing.path
(:require
[clojure.spec.alpha :as s]
[app.common.spec :as us]
[beicon.core :as rx]
[potok.core :as ptk]
[app.common.math :as mth]
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.util.data :as ud]
[app.common.data :as cd]
[app.util.geom.path :as ugp]
[app.main.streams :as ms]
[app.main.store :as st]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.drawing.common :as common]
[app.common.geom.shapes.path :as gsp]))
;; SCHEMAS
(s/def ::command #{:move-to
:line-to
:line-to-horizontal
:line-to-vertical
:curve-to
:smooth-curve-to
:quadratic-bezier-curve-to
:smooth-quadratic-bezier-curve-to
:elliptical-arc
:close-path})
(s/def :paths.params/x number?)
(s/def :paths.params/y number?)
(s/def :paths.params/c1x number?)
(s/def :paths.params/c1y number?)
(s/def :paths.params/c2x number?)
(s/def :paths.params/c2y number?)
(s/def ::relative? boolean?)
(s/def ::params
(s/keys :req-un [:path.params/x
:path.params/y]
:opt-un [:path.params/c1x
:path.params/c1y
:path.params/c2x
:path.params/c2y]))
(s/def ::content-entry
(s/keys :req-un [::command]
:req-opt [::params
::relative?]))
(s/def ::content
(s/coll-of ::content-entry :kind vector?))
;; CONSTANTS
(defonce enter-keycode 13)
(defonce drag-threshold 5)
;; PRIVATE METHODS
@ -54,6 +94,25 @@
points (gsh/rect->points selrect)]
(assoc shape :points points :selrect selrect)))
(defn closest-angle [angle]
(cond
(or (> angle 337.5) (<= angle 22.5)) 0
(and (> angle 22.5) (<= angle 67.5)) 45
(and (> angle 67.5) (<= angle 112.5)) 90
(and (> angle 112.5) (<= angle 157.5)) 135
(and (> angle 157.5) (<= angle 202.5)) 180
(and (> angle 202.5) (<= angle 247.5)) 225
(and (> angle 247.5) (<= angle 292.5)) 270
(and (> angle 292.5) (<= angle 337.5)) 315))
(defn position-fixed-angle [point from-point]
(if (and from-point point)
(let [angle (mod (+ 360 (- (gpt/angle point from-point))) 360)
to-angle (closest-angle angle)
distance (gpt/distance point from-point)]
(gpt/angle->point from-point (mth/radians to-angle) distance))
point))
(defn next-node
"Calculates the next-node to be inserted."
[shape position prev-point prev-handler]
@ -89,50 +148,111 @@
(update opposite-index assoc ocx (- dx) ocy (- dy)))))
(defn end-path-event? [{:keys [type shift] :as event}]
(or (= event ::end-path)
(or (= (ptk/type event) ::finish-path)
(= (ptk/type event) :esc-pressed)
(= event :interrupt) ;; ESC
(and (ms/mouse-double-click? event))
(and (ms/keyboard-event? event)
(= type :down)
;; TODO: Enter now finish path but can finish drawing/editing as well
(= enter-keycode (:key event)))))
(defn generate-path-changes [page-id shape-id old-content new-content]
(us/verify ::content old-content)
(us/verify ::content new-content)
(let [old-selrect (gsh/content->selrect old-content)
old-points (gsh/rect->points old-selrect)
new-selrect (gsh/content->selrect new-content)
new-points (gsh/rect->points new-selrect)
rch [{:type :mod-obj
:id shape-id
:page-id page-id
:operations [{:type :set :attr :content :val new-content}
{:type :set :attr :selrect :val new-selrect}
{:type :set :attr :points :val new-points}]}
{:type :reg-objects
:page-id page-id
:shapes [shape-id]}]
uch [{:type :mod-obj
:id shape-id
:page-id page-id
:operations [{:type :set :attr :content :val old-content}
{:type :set :attr :selrect :val old-selrect}
{:type :set :attr :points :val old-points}]}
{:type :reg-objects
:page-id page-id
:shapes [shape-id]}]]
[rch uch]))
(defn clean-edit-state
[state]
(dissoc state :last-point :prev-handler :drag-handler :preview))
(defn dragging? [start zoom]
(fn [current]
(>= (gpt/distance start current) (/ drag-threshold zoom))))
(defn drag-stream [to-stream]
(let [start @ms/mouse-position
zoom (get-in @st/state [:workspace-local :zoom] 1)
mouse-up (->> st/stream (rx/filter #(ms/mouse-up? %)))]
(->> ms/mouse-position
(rx/take-until mouse-up)
(rx/filter (dragging? start zoom))
(rx/take 1)
(rx/merge-map (fn [] to-stream)))))
(defn position-stream []
(->> ms/mouse-position
(rx/with-latest merge (->> ms/mouse-position-shift (rx/map #(hash-map :shift? %))))
(rx/with-latest merge (->> ms/mouse-position-alt (rx/map #(hash-map :alt? %))))))
;; EVENTS
(defn init-path [id]
(defn init-path []
(ptk/reify ::init-path))
(defn finish-path [id]
(defn finish-path [source]
(ptk/reify ::finish-path
ptk/UpdateEvent
(update [_ state]
(-> state
(update-in [:workspace-local :edit-path id] dissoc :last-point :prev-handler :drag-handler :preview)))))
(let [id (get-path-id state)]
(-> state
(update-in [:workspace-local :edit-path id] clean-edit-state))))))
(defn preview-next-point [{:keys [x y]}]
(defn preview-next-point [{:keys [x y shift?]}]
(ptk/reify ::preview-next-point
ptk/UpdateEvent
(update [_ state]
(let [id (get-path-id state)
position (gpt/point x y)
fix-angle? shift?
last-point (get-in state [:workspace-local :edit-path id :last-point])
position (cond-> (gpt/point x y)
fix-angle? (position-fixed-angle last-point))
shape (get-in state (get-path state))
{:keys [last-point prev-handler]} (get-in state [:workspace-local :edit-path id])
command (next-node shape position last-point prev-handler)]
(assoc-in state [:workspace-local :edit-path id :preview] command)))))
(defn add-node [{:keys [x y]}]
(defn add-node [{:keys [x y shift?]}]
(ptk/reify ::add-node
ptk/UpdateEvent
(update [_ state]
(let [id (get-path-id state)
position (gpt/point x y)
{:keys [last-point prev-handler]} (get-in state [:workspace-local :edit-path id])]
(-> state
(assoc-in [:workspace-local :edit-path id :last-point] position)
(update-in [:workspace-local :edit-path id] dissoc :prev-handler)
(update-in (get-path state) append-node position last-point prev-handler))))))
fix-angle? shift?
{:keys [last-point prev-handler]} (get-in state [:workspace-local :edit-path id])
position (cond-> (gpt/point x y)
fix-angle? (position-fixed-angle last-point))
]
(if-not (= last-point position)
(-> state
(assoc-in [:workspace-local :edit-path id :last-point] position)
(update-in [:workspace-local :edit-path id] dissoc :prev-handler)
(update-in [:workspace-local :edit-path id] dissoc :preview)
(update-in (get-path state) append-node position last-point prev-handler))
state)))))
(defn start-drag-handler []
(ptk/reify ::start-drag-handler
@ -154,22 +274,22 @@
(= command :line-to)
(update-in (get-path state :content index) make-curve))))))
(defn drag-handler [{:keys [x y]}]
(defn drag-handler [{:keys [x y alt? shift?]}]
(ptk/reify ::drag-handler
ptk/UpdateEvent
(update [_ state]
(let [id (get-path-id state)
handler-position (gpt/point x y)
shape (get-in state (get-path state))
content (:content shape)
index (dec (count content))
node-position (ugp/command->point (nth content index))
handler-position (cond-> (gpt/point x y)
shift? (position-fixed-angle node-position))
{dx :x dy :y} (gpt/subtract handler-position node-position)
match-opposite? true
match-opposite? (not alt?)
modifiers (move-handler-modifiers content (inc index) :c1 match-opposite? dx dy)]
(-> state
(assoc-in [:workspace-local :edit-path id :content-modifiers] modifiers)
(update-in [:workspace-local :edit-path id :content-modifiers] merge modifiers)
(assoc-in [:workspace-local :edit-path id :prev-handler] handler-position)
(assoc-in [:workspace-local :edit-path id :drag-handler] handler-position))))))
@ -194,114 +314,136 @@
;; Update the preview because can be outdated after the dragging
(rx/of (preview-next-point handler))))))
(defn close-path [position]
(ptk/reify ::close-path
ptk/WatchEvent
(watch [_ state stream]
(rx/of (add-node position)
::end-path))))
(declare close-path-drag-end)
(defn close-path-drag-start [position]
(ptk/reify ::close-path-drag-start
ptk/WatchEvent
(watch [_ state stream]
(let [zoom (get-in state [:workspace-local :zoom])
threshold (/ 5 zoom)
check-if-dragging
(fn [current-position]
(let [start (gpt/point position)
current (gpt/point current-position)]
(>= (gpt/distance start current) 100)))
(let [id (get-path-id state)
zoom (get-in state [:workspace-local :zoom])
start-position @ms/mouse-position
stop-stream
(->> stream (rx/filter #(or (end-path-event? %)
(ms/mouse-up? %))))
position-stream
(->> ms/mouse-position
(rx/take-until stop-stream)
(rx/throttle 50))
drag-events-stream
(->> position-stream
(->> (position-stream)
(rx/take-until stop-stream)
(rx/map #(drag-handler %)))]
(rx/concat
(rx/of (close-path position))
(rx/of (add-node position))
(drag-stream
(rx/concat
(rx/of (start-drag-handler))
drag-events-stream
(rx/of (finish-drag))
(rx/of (close-path-drag-end))))
(rx/of (finish-path "close-path")))))))
(->> position-stream
(rx/filter check-if-dragging)
(rx/take 1)
(rx/merge-map
#(rx/concat
(rx/of (start-drag-handler))
drag-events-stream
(rx/of (finish-drag))))))))))
(defn close-path-drag-end [position]
(ptk/reify ::close-path-drag-end))
(defn close-path-drag-end []
(ptk/reify ::close-path-drag-end
ptk/UpdateEvent
(update [_ state]
(let [id (get-path-id state)]
(update-in state [:workspace-local :edit-path id] dissoc :prev-handler)))))
(defn path-pointer-enter [position]
(ptk/reify ::path-pointer-enter))
(ptk/reify ::path-pointer-enter
ptk/UpdateEvent
(update [_ state]
(let [id (get-path-id state)]
(update-in state [:workspace-local :edit-path id :hover-points] (fnil conj #{}) position)))))
(defn path-pointer-leave [position]
(ptk/reify ::path-pointer-leave))
(ptk/reify ::path-pointer-leave
ptk/UpdateEvent
(update [_ state]
(let [id (get-path-id state)]
(update-in state [:workspace-local :edit-path id :hover-points] disj position)))))
(defn start-path-from-point [position]
(ptk/reify ::start-path-from-point
ptk/WatchEvent
(watch [_ state stream]
(let [mouse-up (->> stream (rx/filter #(or (end-path-event? %)
(let [start-point @ms/mouse-position
zoom (get-in state [:workspace-local :zoom])
mouse-up (->> stream (rx/filter #(or (end-path-event? %)
(ms/mouse-up? %))))
drag-events (->> ms/mouse-position
(rx/take-until mouse-up)
(rx/map #(drag-handler %)))]
(rx/concat (rx/of (add-node position))
(rx/of (start-drag-handler))
drag-events
(rx/of (finish-drag))))
)))
(rx/concat
(rx/of (add-node position))
(drag-stream
(rx/concat
(rx/of (start-drag-handler))
drag-events
(rx/of (finish-drag)))))))))
(defn make-corner []
(ptk/reify ::make-corner
ptk/WatchEvent
(watch [_ state stream]
(let [id (get-path-id state)
page-id (:current-page-id state)
shape (get-in state (get-path state))
selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{})
new-content (reduce ugp/make-corner-point (:content shape) selected-points)
[rch uch] (generate-path-changes page-id id (:content shape) new-content)]
(rx/of (dwc/commit-changes rch uch {:commit-local? true}))))))
(defn make-curve []
(ptk/reify ::make-curve
ptk/WatchEvent
(watch [_ state stream]
(let [id (get-path-id state)
page-id (:current-page-id state)
shape (get-in state (get-path state))
selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{})
new-content (reduce ugp/make-curve-point (:content shape) selected-points)
[rch uch] (generate-path-changes page-id id (:content shape) new-content)]
(rx/of (dwc/commit-changes rch uch {:commit-local? true}))))))
(defn path-handler-enter [index prefix]
(ptk/reify ::path-handler-enter
ptk/UpdateEvent
(update [_ state]
(let [id (get-path-id state)]
(update-in state [:workspace-local :edit-path id :hover-handlers] (fnil conj #{}) [index prefix])))))
(defn path-handler-leave [index prefix]
(ptk/reify ::path-handler-leave
ptk/UpdateEvent
(update [_ state]
(let [id (get-path-id state)]
(update-in state [:workspace-local :edit-path id :hover-handlers] disj [index prefix])))))
;; EVENT STREAMS
(defn make-click-stream
[stream down-event]
(->> stream
(rx/filter ms/mouse-click?)
(rx/debounce 200)
(rx/first)
(rx/map #(add-node down-event))))
(defn make-drag-stream
[stream down-event]
[stream down-event zoom]
(let [mouse-up (->> stream (rx/filter #(or (end-path-event? %)
(ms/mouse-up? %))))
drag-events (->> ms/mouse-position
drag-events (->> (position-stream)
(rx/take-until mouse-up)
(rx/map #(drag-handler %)))]
(->> (rx/timer 400)
(rx/merge-map #(rx/concat
(rx/of (add-node down-event))
(rx/of (start-drag-handler))
drag-events
(rx/of (finish-drag)))))))
(defn make-dbl-click-stream
[stream down-event]
(->> stream
(rx/filter ms/mouse-double-click?)
(rx/first)
(rx/merge-map
#(rx/of (add-node down-event)
::end-path))))
(rx/concat
(rx/of (add-node down-event))
(drag-stream
(rx/concat
(rx/of (start-drag-handler))
drag-events
(rx/of (finish-drag)))))))
(defn make-node-events-stream
[stream]
(->> (rx/merge
(->> stream (rx/filter (ptk/type? ::close-path)))
(->> stream (rx/filter (ptk/type? ::close-path-drag-start))))
(->> stream
(rx/filter (ptk/type? ::close-path-drag-start))
(rx/take 1)
(rx/merge-map #(rx/empty))))
@ -318,35 +460,32 @@
ptk/WatchEvent
(watch [_ state stream]
(let [mouse-down (->> stream (rx/filter ms/mouse-down?))
(let [zoom (get-in state [:workspace-local :zoom])
mouse-down (->> stream (rx/filter ms/mouse-down?))
end-path-events (->> stream (rx/filter end-path-event?))
;; Mouse move preview
mousemove-events
(->> ms/mouse-position
(->> (position-stream)
(rx/take-until end-path-events)
(rx/throttle 50)
(rx/map #(preview-next-point %)))
;; From mouse down we can have: click, drag and double click
mousedown-events
(->> mouse-down
(rx/take-until end-path-events)
(rx/throttle 50)
(rx/with-latest merge ms/mouse-position)
(rx/with-latest merge (position-stream))
;; We change to the stream that emits the first event
(rx/switch-map
#(rx/race (make-node-events-stream stream)
(make-click-stream stream %)
(make-drag-stream stream %)
(make-dbl-click-stream stream %))))]
(make-drag-stream stream % zoom))))]
(rx/concat
(rx/of (init-path id))
(rx/of (init-path))
(rx/merge mousemove-events
mousedown-events)
(rx/of (finish-path id)))))))
(rx/of (finish-path "after-events")))))))
(defn stop-path-edit []
(ptk/reify ::stop-path-edit
@ -408,70 +547,33 @@
(ptk/reify ::apply-content-modifiers
ptk/WatchEvent
(watch [_ state stream]
(let [id (get-in state [:workspace-local :edition])
(let [id (get-path-id state)
page-id (:current-page-id state)
shape (get-in state [:workspace-data :pages-index page-id :objects id])
{old-content :content old-selrect :selrect old-points :points} shape
content-modifiers (get-in state [:workspace-local :edit-path id :content-modifiers] {})
new-content (ugp/apply-content-modifiers old-content content-modifiers)
new-selrect (gsh/content->selrect new-content)
new-points (gsh/rect->points new-selrect)
rch [{:type :mod-obj
:id id
:page-id page-id
:operations [{:type :set :attr :content :val new-content}
{:type :set :attr :selrect :val new-selrect}
{:type :set :attr :points :val new-points}]}
{:type :reg-objects
:page-id page-id
:shapes [id]}]
uch [{:type :mod-obj
:id id
:page-id page-id
:operations [{:type :set :attr :content :val old-content}
{:type :set :attr :selrect :val old-selrect}
{:type :set :attr :points :val old-points}]}
{:type :reg-objects
:page-id page-id
:shapes [id]}]]
shape (get-in state (get-path state))
content-modifiers (get-in state [:workspace-local :edit-path id :content-modifiers])
new-content (ugp/apply-content-modifiers (:content shape) content-modifiers)
[rch uch] (generate-path-changes page-id (:id shape) (:content shape) new-content)]
(rx/of (dwc/commit-changes rch uch {:commit-local? true})
(fn [state] (update-in state [:workspace-local :edit-path id] dissoc :content-modifiers)))))))
(defn save-path-content []
(ptk/reify ::save-path-content
ptk/UpdateEvent
(update [_ state]
(let [content (get-in state (get-path state :content))
content (if (= (-> content last :command) :move-to)
(into [] (take (dec (count content)) content))
content)]
(assoc-in state (get-path state :content) content)))
ptk/WatchEvent
(watch [_ state stream]
(let [id (get-in state [:workspace-local :edition])
shape (get-in state (get-path state))
page-id (:current-page-id state)
old-content (get-in state [:workspace-local :edit-path id :old-content])
old-selrect (gsh/content->selrect old-content)
old-points (gsh/rect->points old-content)
shape (get-in state [:workspace-data :pages-index page-id :objects id])
{new-content :content new-selrect :selrect new-points :points} shape
rch [{:type :mod-obj
:id id
:page-id page-id
:operations [{:type :set :attr :content :val new-content}
{:type :set :attr :selrect :val new-selrect}
{:type :set :attr :points :val new-points}]}
{:type :reg-objects
:page-id page-id
:shapes [id]}]
uch [{:type :mod-obj
:id id
:page-id page-id
:operations [{:type :set :attr :content :val old-content}
{:type :set :attr :selrect :val old-selrect}
{:type :set :attr :points :val old-points}]}
{:type :reg-objects
:page-id page-id
:shapes [id]}]]
[rch uch] (generate-path-changes page-id id old-content (:content shape))]
(rx/of (dwc/commit-changes rch uch {:commit-local? true}))))))
(declare start-draw-mode)
@ -486,9 +588,9 @@
(cond
(not= content old-content) (rx/of (save-path-content)
(start-draw-mode))
(start-draw-mode))
(= mode :draw) (rx/of :interrupt)
:else (rx/of (finish-path id)))))))
:else (rx/of (finish-path "changed-content")))))))
(defn move-path-point [start-point end-point]
(ptk/reify ::move-point
@ -506,7 +608,7 @@
(let [point (ugp/command->point command)]
(= point start-point)))
point-indices (->> (d/enumerate content)
point-indices (->> (cd/enumerate content)
(filter command-for-point)
(map first))
@ -533,14 +635,17 @@
[position]
(ptk/reify ::start-move-path-point
ptk/WatchEvent
;; TODO REWRITE
(watch [_ state stream]
(let [stopper (->> stream (rx/filter ms/mouse-up?))]
(rx/concat
(->> ms/mouse-position
(rx/take-until stopper)
(rx/map #(move-path-point position %)))
(rx/of (apply-content-modifiers)))))))
(let [start-position @ms/mouse-position
stopper (->> stream (rx/filter ms/mouse-up?))
zoom (get-in state [:workspace-local :zoom])]
(drag-stream
(rx/concat
(->> ms/mouse-position
(rx/take-until stopper)
(rx/map #(move-path-point position %)))
(rx/of (apply-content-modifiers))))))))
(defn start-move-handler
[index prefix]
@ -548,26 +653,40 @@
ptk/WatchEvent
(watch [_ state stream]
(let [id (get-in state [:workspace-local :edition])
[cx cy] (if (= prefix :c1) [:c1x :c1y] [:c2x :c2y])
cx (ud/prefix-keyword prefix :x)
cy (ud/prefix-keyword prefix :y)
start-point @ms/mouse-position
start-delta-x (get-in state [:workspace-local :edit-path id :content-modifiers index cx] 0)
start-delta-y (get-in state [:workspace-local :edit-path id :content-modifiers index cy] 0)]
modifiers (get-in state [:workspace-local :edit-path id :content-modifiers])
start-delta-x (get-in modifiers [index cx] 0)
start-delta-y (get-in modifiers [index cy] 0)
(rx/concat
(->> ms/mouse-position
(rx/take-until (->> stream (rx/filter ms/mouse-up?)))
(rx/with-latest vector ms/mouse-position-alt)
(rx/map
(fn [[pos alt?]]
(modify-handler
id
index
prefix
(+ start-delta-x (- (:x pos) (:x start-point)))
(+ start-delta-y (- (:y pos) (:y start-point)))
(not alt?))))
)
(rx/concat (rx/of (apply-content-modifiers))))))))
content (get-in state (get-path state :content))
opposite-index (ugp/opposite-index content index prefix)
opposite-prefix (if (= prefix :c1) :c2 :c1)
opposite-handler (-> content (get opposite-index) (ugp/get-handler opposite-prefix))
point (-> content (get (if (= prefix :c1) (dec index) index)) (ugp/command->point))
handler (-> content (get index) (ugp/get-handler prefix))
current-distance (gpt/distance (ugp/opposite-handler point handler) opposite-handler)
match-opposite? (mth/almost-zero? current-distance)]
(drag-stream
(rx/concat
(->> (position-stream)
(rx/take-until (->> stream (rx/filter ms/mouse-up?)))
(rx/map
(fn [{:keys [x y alt? shift?]}]
(let [pos (cond-> (gpt/point x y)
shift? (position-fixed-angle point))]
(modify-handler
id
index
prefix
(+ start-delta-x (- (:x pos) (:x start-point)))
(+ start-delta-y (- (:y pos) (:y start-point)))
(and (not alt?) match-opposite?))))))
(rx/concat (rx/of (apply-content-modifiers)))))))))
(defn start-draw-mode []
(ptk/reify ::start-draw-mode
@ -604,7 +723,7 @@
(watch [_ state stream]
(let [id (get-path-id state)]
(cond
(and id (= :move mode)) (rx/of ::end-path)
(and id (= :move mode)) (rx/of (finish-path "change-edit-mode"))
(and id (= :draw mode)) (rx/of (start-draw-mode))
:else (rx/empty))))))
@ -614,7 +733,7 @@
(update [_ state]
(let [id (get-in state [:workspace-local :edition])]
(-> state
(update-in [:workspace-local :edit-path id :selected] (fnil conj #{}) [index type]))))))
(update-in [:workspace-local :edit-path id :selected-handlers] (fnil conj #{}) [index type]))))))
(defn select-node [position]
(ptk/reify ::select-node
@ -622,7 +741,7 @@
(update [_ state]
(let [id (get-in state [:workspace-local :edition])]
(-> state
(update-in [:workspace-local :edit-path id :selected-node] (fnil conj #{}) position))))))
(assoc-in [:workspace-local :edit-path id :selected-points] #{position}))))))
(defn deselect-node [position]
(ptk/reify ::deselect-node
@ -630,7 +749,7 @@
(update [_ state]
(let [id (get-in state [:workspace-local :edition])]
(-> state
(update-in [:workspace-local :edit-path id :selected-node] (fnil disj #{}) position))))))
(update-in [:workspace-local :edit-path id :selected-points] (fnil disj #{}) position))))))
(defn add-to-selection-handler [index type]
(ptk/reify ::add-to-selection-handler
@ -656,11 +775,21 @@
(update [_ state]
state)))
(defn deselect-all []
(ptk/reify ::deselect-all
ptk/UpdateEvent
(update [_ state]
(let [id (get-path-id state)]
(-> state
(assoc-in [:workspace-local :edit-path id :selected-handlers] #{})
(assoc-in [:workspace-local :edit-path id :selected-points] #{}))))))
(defn handle-new-shape-result [shape-id]
(ptk/reify ::handle-new-shape-result
ptk/UpdateEvent
(update [_ state]
(let [content (get-in state [:workspace-drawing :object :content] [])]
(us/verify ::content content)
(if (> (count content) 1)
(assoc-in state [:workspace-drawing :object :initialized?] true)
state)))

View file

@ -11,6 +11,7 @@
"The main logic for SVG export functionality."
(:require
[rumext.alpha :as mf]
[cuerdas.core :as str]
[app.common.uuid :as uuid]
[app.common.pages :as cp]
[app.common.pages-helpers :as cph]
@ -42,10 +43,15 @@
(defn- calculate-dimensions
[{:keys [objects] :as data} vport]
(let [shapes (cph/select-toplevel-shapes objects {:include-frames? true})]
(->> (gsh/selection-rect shapes)
(gal/adjust-to-viewport vport)
#_(gsh/fix-invalid-rect-values))))
(let [shapes (cph/select-toplevel-shapes objects {:include-frames? true})
to-finite (fn [val fallback] (if (not (mth/finite? val)) fallback val))
rect (->> (gsh/selection-rect shapes)
(gal/adjust-to-viewport vport))]
(-> rect
(update :x to-finite 0)
(update :y to-finite 0)
(update :width to-finite 10000)
(update :height to-finite 10000))))
(declare shape-wrapper-factory)
@ -93,21 +99,20 @@
:group [:> group-wrapper {:shape shape :frame frame}]
nil)])))))
(defn get-viewbox [{:keys [x y width height] :or {x 0 y 0 width 100 height 100}}]
(str/fmt "%s %s %s %s" x y width height))
(mf/defc page-svg
{::mf/wrap [mf/memo]}
[{:keys [data width height] :as props}]
(let [objects (:objects data)
vport {:width width :height height}
dim (calculate-dimensions data vport)
root (get objects uuid/zero)
shapes (->> (:shapes root)
(map #(get objects %)))
vbox (str (:x dim 0) " "
(:y dim 0) " "
(:width dim 100) " "
(:height dim 100))
vport {:width width :height height}
dim (calculate-dimensions data vport)
vbox (get-viewbox dim)
background-color (get-in data [:options :background] default-color)
frame-wrapper
(mf/use-memo

View file

@ -93,7 +93,7 @@
{:alt (t locale "workspace.toolbar.path")
:class (when (= selected-drawtool :path) "selected")
:on-click (partial select-drawtool :path)}
i/curve]
i/pen]
[:li.tooltip.tooltip-right
{:alt (t locale "workspace.toolbar.comments")

View file

@ -10,6 +10,7 @@
(ns app.main.ui.workspace.shapes.path
(:require
[rumext.alpha :as mf]
[goog.events :as events]
[okulary.core :as l]
[app.util.data :as d]
[app.util.dom :as dom]
@ -30,7 +31,8 @@
[app.util.geom.path :as ugp]
[app.common.geom.point :as gpt]
[app.main.ui.cursors :as cur]
[app.main.ui.icons :as i]))
[app.main.ui.icons :as i])
(:import goog.events.EventType))
(def primary-color "#1FDEA7")
(def secondary-color "#DB00FF")
@ -94,7 +96,7 @@
(mf/defc path-actions [{:keys [shape]}]
(let [id (mf/deref refs/selected-edition)
{:keys [edit-mode selected snap-toggled] :as all} (mf/deref current-edit-path-ref)]
{:keys [edit-mode selected-points snap-toggled] :as all} (mf/deref current-edit-path-ref)]
[:div.path-actions
[:div.viewport-actions-group
[:div.viewport-actions-entry {:class (when (= edit-mode :draw) "is-toggled")
@ -102,24 +104,28 @@
[:div.viewport-actions-entry {:class (when (= edit-mode :move) "is-toggled")
:on-click #(st/emit! (drp/change-edit-mode :move))} i/pointer-inner]]
[:div.viewport-actions-group
#_[:div.viewport-actions-group
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-add]
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-remove]]
[:div.viewport-actions-group
#_[:div.viewport-actions-group
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-merge]
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-join]
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-separate]]
[:div.viewport-actions-group
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-corner]
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-curve]]
[:div.viewport-actions-entry {:class (when (empty? selected-points) "is-disabled")
:on-click #(when-not (empty? selected-points)
(st/emit! (drp/make-corner)))} i/nodes-corner]
[:div.viewport-actions-entry {:class (when (empty? selected-points) "is-disabled")
:on-click #(when-not (empty? selected-points)
(st/emit! (drp/make-curve)))} i/nodes-curve]]
[:div.viewport-actions-group
#_[:div.viewport-actions-group
[:div.viewport-actions-entry {:class (when snap-toggled "is-toggled")} i/nodes-snap]]]))
(mf/defc path-point [{:keys [position zoom edit-mode hover? selected? preview? start-path?]}]
(mf/defc path-point [{:keys [position zoom edit-mode hover? selected? preview? start-path? last-p?]}]
(let [{:keys [x y]} position
on-enter
@ -132,37 +138,38 @@
on-click
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(when-not last-p?
(do (dom/stop-propagation event)
(dom/prevent-default event)
(cond
(and (= edit-mode :move) (not selected?))
(st/emit! (drp/select-node position))
(cond
(and (= edit-mode :move) (not selected?))
(st/emit! (drp/select-node position))
(and (= edit-mode :move) selected?)
(st/emit! (drp/deselect-node position))))
(and (= edit-mode :move) selected?)
(st/emit! (drp/deselect-node position))))))
on-mouse-down
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(when-not last-p?
(do (dom/stop-propagation event)
(dom/prevent-default event)
(cond
(= edit-mode :move)
(st/emit! (drp/start-move-path-point position))
(cond
(= edit-mode :move)
(st/emit! (drp/start-move-path-point position))
(and (= edit-mode :draw) start-path?)
(st/emit! (drp/start-path-from-point position))
(and (= edit-mode :draw) start-path?)
(st/emit! (drp/start-path-from-point position))
(and (= edit-mode :draw) (not start-path?))
(st/emit! (drp/close-path-drag-start position))))]
(and (= edit-mode :draw) (not start-path?))
(st/emit! (drp/close-path-drag-start position))))))]
[:g.path-point
[:circle.path-point
{:cx x
:cy y
:r (/ 3 zoom)
:style {:cursor (when (= edit-mode :draw) cur/pen-node)
:stroke-width (/ 1 zoom)
:r (if (or selected? hover?) (/ 3.5 zoom) (/ 3 zoom))
:style {:stroke-width (/ 1 zoom)
:stroke (cond (or selected? hover?) black-color
preview? secondary-color
:else primary-color)
@ -173,11 +180,24 @@
:r (/ 10 zoom)
:on-click on-click
:on-mouse-down on-mouse-down
:style {:fill "transparent"}}]]))
:on-mouse-enter on-enter
:on-mouse-leave on-leave
:style {:cursor (cond
(and (not last-p?) (= edit-mode :draw)) cur/pen-node
(= edit-mode :move) cur/pointer-node)
:fill "transparent"}}]]))
(mf/defc path-handler [{:keys [index prefix point handler zoom selected? hover? edit-mode]}]
(when (and point handler)
(let [{:keys [x y]} handler
on-enter
(fn [event]
(st/emit! (drp/path-handler-enter index prefix)))
on-leave
(fn [event]
(st/emit! (drp/path-handler-leave index prefix)))
on-click
(fn [event]
(dom/stop-propagation event)
@ -201,7 +221,7 @@
:y1 (:y point)
:x2 x
:y2 y
:style {:stroke gray-color
:style {:stroke (if hover? black-color gray-color)
:stroke-width (/ 1 zoom)}}]
[:rect
{:x (- x (/ 3 zoom))
@ -209,8 +229,7 @@
:width (/ 6 zoom)
:height (/ 6 zoom)
:style {:cursor cur/pointer-move
:stroke-width (/ 1 zoom)
:style {:stroke-width (/ 1 zoom)
:stroke (cond (or selected? hover?) black-color
:else primary-color)
:fill (cond selected? primary-color
@ -220,7 +239,10 @@
:r (/ 10 zoom)
:on-click on-click
:on-mouse-down on-mouse-down
:style {:fill "transparent"}}]])))
:on-mouse-enter on-enter
:on-mouse-leave on-leave
:style {:cursor (when (= edit-mode :move) cur/pointer-move)
:fill "transparent"}}]])))
(mf/defc path-preview [{:keys [zoom command from]}]
[:g.preview {:style {:pointer-events "none"}}
@ -239,17 +261,40 @@
(mf/defc path-editor
[{:keys [shape zoom]}]
(let [edit-path-ref (make-edit-path-ref (:id shape))
{:keys [edit-mode selected drag-handler prev-handler preview content-modifiers last-point]} (mf/deref edit-path-ref)
(let [editor-ref (mf/use-ref nil)
edit-path-ref (make-edit-path-ref (:id shape))
{:keys [edit-mode
drag-handler
prev-handler
preview
content-modifiers
last-point
selected-handlers
selected-points
hover-handlers
hover-points]} (mf/deref edit-path-ref)
{:keys [content]} shape
selected (or selected #{})
content (ugp/apply-content-modifiers content content-modifiers)
points (->> content ugp/content->points (into #{}))
last-command (last content)
last-p (->> content last ugp/command->point)
handlers (ugp/content->handlers content)]
handlers (ugp/content->handlers content)
[:g.path-editor
handle-click-outside
(fn [event]
(let [current (dom/get-target event)
editor-dom (mf/ref-val editor-ref)]
(when-not (or (.contains editor-dom current)
(dom/class? current "viewport-actions-entry"))
(st/emit! (drp/deselect-all)))))]
(mf/use-layout-effect
(fn []
(let [keys [(events/listen (dom/get-root) EventType.CLICK handle-click-outside)]]
#(doseq [key keys]
(events/unlistenByKey key)))))
[:g.path-editor {:ref editor-ref}
(when (and preview (not drag-handler))
[:& path-preview {:command preview
:from last-p
@ -257,43 +302,37 @@
(for [position points]
[:g.path-node
[:& path-point {:position position
:selected? false
:zoom zoom
:edit-mode edit-mode
:start-path? (nil? last-point)}]
[:g.point-handlers {:pointer-events (when (= edit-mode :draw) "none")}
(for [[index prefix] (get handlers position)]
(let [command (get content index)
x (get-in command [:params (d/prefix-keyword prefix :x)])
y (get-in command [:params (d/prefix-keyword prefix :y)])
handler-position (gpt/point x y)]
[:& path-handler {:point position
:handler handler-position
:index index
:prefix prefix
:zoom zoom
:selected? false
:hover? false
:preview? false
:edit-mode edit-mode}]))]])
(when (not= position handler-position)
[:& path-handler {:point position
:handler handler-position
:index index
:prefix prefix
:zoom zoom
:selected? (contains? selected-handlers [index prefix])
:hover? (contains? hover-handlers [index prefix])
:edit-mode edit-mode}])))]
[:& path-point {:position position
:zoom zoom
:edit-mode edit-mode
:selected? (contains? selected-points position)
:hover? (contains? hover-points position)
:last-p? (= last-point position)
:start-path? (nil? last-point)}]])
(when prev-handler
[:g.prev-handler {:pointer-events "none"}
[:& path-handler {:point last-p
:handler prev-handler
:zoom zoom
:selected false}]])
:zoom zoom}]])
(when drag-handler
[:g.drag-handler {:pointer-events "none"}
(when (not= :move-to (:command last-command))
[:& path-handler {:point last-p
:handler (ugp/opposite-handler last-p drag-handler)
:zoom zoom
:selected false}])
[:& path-handler {:point last-p
:handler drag-handler
:zoom zoom
:selected false}]])]))
:zoom zoom}]])]))

View file

@ -294,7 +294,7 @@
on-double-click
(mf/use-callback
(mf/deps edition edit-path)
(mf/deps drawing-path?)
(fn [event]
(dom/stop-propagation event)
(let [ctrl? (kbd/ctrl? event)

View file

@ -236,3 +236,7 @@
(defn get-root []
(query js/document "#app"))
(defn ^boolean class? [node class-name]
(let [class-list (.-classList ^js node)]
(.contains ^js class-list class-name)))

View file

@ -10,6 +10,7 @@
(ns app.util.geom.path
(:require
[cuerdas.core :as str]
[app.common.data :as cd]
[app.util.data :as d]
[app.common.data :as cd]
[app.common.geom.point :as gpt]
@ -247,8 +248,10 @@
content))]
(reduce apply-to-index content modifiers)))
(defn command->point [{{:keys [x y]} :params}]
(gpt/point x y))
(defn command->point [command]
(when-not (nil? command)
(let [{{:keys [x y]} :params} command]
(gpt/point x y))))
(defn content->points [content]
(->> content
@ -256,23 +259,37 @@
(remove nil?)
(into [])))
(defn content->handlers [content]
(->> (d/with-prev content) ;; [cmd, prev]
(d/enumerate) ;; [idx [cmd, prev]]
(defn get-handler [{:keys [params] :as command} prefix]
(let [cx (d/prefix-keyword prefix :x)
cy (d/prefix-keyword prefix :y)]
(when (and command
(contains? params cx)
(contains? params cy))
(gpt/point (get params cx)
(get params cy)))))
(mapcat (fn [[index [cur-cmd prev-cmd]]]
(if (and prev-cmd
(= :curve-to (:command cur-cmd)))
(defn content->handlers
"Retrieve a map where for every point will retrieve a list of
the handlers that are associated with that point.
point -> [[index, prefix]]"
[content]
(->> (d/with-prev content)
(d/enumerate)
(mapcat (fn [[index [cur-cmd pre-cmd]]]
(if (and pre-cmd (= :curve-to (:command cur-cmd)))
(let [cur-pos (command->point cur-cmd)
pre-pos (command->point prev-cmd)]
[[pre-pos [index :c1]]
[cur-pos [index :c2]]])
pre-pos (command->point pre-cmd)]
(-> [[pre-pos [index :c1]]
[cur-pos [index :c2]]]))
[])))
(group-by first)
(cd/mapm #(mapv second %2))))
(defn opposite-index [content index prefix]
(defn opposite-index
"Calculate sthe opposite index given a prefix and an index"
[content index prefix]
(let [point (if (= prefix :c2)
(command->point (nth content index))
(command->point (nth content (dec index))))
@ -280,10 +297,99 @@
handlers (-> (content->handlers content)
(get point))
opposite-prefix (if (= prefix :c1) :c2 :c1)
opposite-prefix (if (= prefix :c1) :c2 :c1)]
(when (<= (count handlers) 2)
(->> handlers
(d/seek (fn [[index prefix]] (= prefix opposite-prefix)))
(first)))))
result (when (<= (count handlers) 2)
(->> handlers
(d/seek (fn [[index prefix]] (= prefix opposite-prefix)))
(first)))]
result))
(defn remove-line-curves
"Remove all curves that have both handlers in the same position that the
beggining and end points. This makes them really line-to commands"
[content]
(let [with-prev (d/enumerate (d/with-prev content))
process-command
(fn [content [index [command prev]]]
(let [cur-point (command->point command)
pre-point (command->point prev)
handler-c1 (get-handler command :c1)
handler-c2 (get-handler command :c2)]
(if (and (= :curve-to (:command command))
(= cur-point handler-c2)
(= pre-point handler-c1))
(assoc content index {:command :line-to
:params cur-point})
content)))]
(reduce process-command content with-prev)))
(defn make-corner-point
"Changes the content to make a point a 'corner'"
[content point]
(let [handlers (-> (content->handlers content)
(get point))
change-content
(fn [content [index prefix]]
(let [cx (d/prefix-keyword prefix :x)
cy (d/prefix-keyword prefix :y)]
(-> content
(assoc-in [index :params cx] (:x point))
(assoc-in [index :params cy] (:y point)))))]
(as-> content $
(reduce change-content $ handlers)
(remove-line-curves $))))
(defn make-curve-point
"Changes the content to make the point a 'curve'. The handlers will be positioned
in the same vector that results from te previous->next points but with fixed length."
[content point]
(let [content-next (d/enumerate (d/with-prev-next content))
make-curve
(fn [command previous]
(if (= :line-to (:command command))
(let [cur-point (command->point command)
pre-point (command->point previous)]
(-> command
(assoc :command :curve-to)
(assoc :params (make-curve-params cur-point pre-point))))
command))
update-handler
(fn [command prefix handler]
(if (= :curve-to (:command command))
(let [cx (d/prefix-keyword prefix :x)
cy (d/prefix-keyword prefix :y)]
(-> command
(assoc-in [:params cx] (:x handler))
(assoc-in [:params cy] (:y handler))))
command))
calculate-vector
(fn [point next prev]
(let [base-vector (if (or (nil? next) (nil? prev) (= 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
(cd/update-when index make-curve prev)
(cd/update-when index update-handler :c2 handler)
(cd/update-when (inc index) make-curve command)
(cd/update-when (inc index) update-handler :c1 handler-opposite)))
content))]
(as-> content $
(reduce redfn $ content-next)
(remove-line-curves $))))