mirror of
https://github.com/penpot/penpot.git
synced 2025-02-13 18:48:37 -05:00
✨ Improves path drawing
This commit is contained in:
parent
7ceb9b4009
commit
5c71601fcf
7 changed files with 460 additions and 210 deletions
|
@ -263,6 +263,7 @@
|
|||
border-radius: 3px;
|
||||
|
||||
svg {
|
||||
pointer-events: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
|
|
@ -9,10 +9,11 @@
|
|||
|
||||
(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]
|
||||
|
@ -23,9 +24,50 @@
|
|||
[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?))
|
||||
|
||||
(s/def ::path-shape
|
||||
(s/keys :req-un [::content]))
|
||||
|
||||
|
||||
;; CONSTANTS
|
||||
(defonce enter-keycode 13)
|
||||
|
||||
(defonce drag-threshold 2)
|
||||
|
||||
;; PRIVATE METHODS
|
||||
|
||||
|
@ -89,26 +131,64 @@
|
|||
(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))))
|
||||
|
||||
;; 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]}]
|
||||
(ptk/reify ::preview-next-point
|
||||
|
@ -118,7 +198,6 @@
|
|||
position (gpt/point x y)
|
||||
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)))))
|
||||
|
||||
|
@ -129,10 +208,13 @@
|
|||
(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))))))
|
||||
(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
|
||||
|
@ -158,7 +240,6 @@
|
|||
(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))
|
||||
|
@ -194,24 +275,15 @@
|
|||
;; 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? %)
|
||||
|
@ -228,25 +300,39 @@
|
|||
|
||||
|
||||
(rx/concat
|
||||
(rx/of (close-path position))
|
||||
(rx/of (add-node position))
|
||||
|
||||
(->> position-stream
|
||||
(rx/filter check-if-dragging)
|
||||
(rx/filter (dragging? start-position zoom))
|
||||
(rx/take 1)
|
||||
(rx/merge-map
|
||||
#(rx/concat
|
||||
(rx/of (start-drag-handler))
|
||||
drag-events-stream
|
||||
(rx/of (finish-drag))))))))))
|
||||
(rx/of (finish-drag))
|
||||
(rx/of (close-path-drag-end)))))
|
||||
(rx/of (finish-path "close-path")))))))
|
||||
|
||||
(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
|
||||
|
@ -261,8 +347,45 @@
|
|||
(rx/concat (rx/of (add-node position))
|
||||
(rx/of (start-drag-handler))
|
||||
drag-events
|
||||
(rx/of (finish-drag))))
|
||||
)))
|
||||
(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
|
||||
|
||||
|
@ -270,38 +393,35 @@
|
|||
[stream down-event]
|
||||
(->> stream
|
||||
(rx/filter ms/mouse-click?)
|
||||
(rx/debounce 200)
|
||||
#_(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
|
||||
(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))
|
||||
|
||||
(->> ms/mouse-position
|
||||
(rx/take-until mouse-up)
|
||||
(rx/filter (dragging? (gpt/point down-event) zoom))
|
||||
(rx/take 1)
|
||||
(rx/merge-map
|
||||
#(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 +438,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
|
||||
(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)
|
||||
|
||||
;; 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,34 +525,12 @@
|
|||
(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)))))))
|
||||
|
@ -445,33 +540,10 @@
|
|||
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 +558,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 +578,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 +605,25 @@
|
|||
[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])
|
||||
|
||||
move-point-stream
|
||||
(fn [] (rx/concat
|
||||
(->> ms/mouse-position
|
||||
(rx/take-until stopper)
|
||||
(rx/map #(move-path-point position %)))
|
||||
(rx/of (apply-content-modifiers))))]
|
||||
|
||||
(->> ms/mouse-position
|
||||
(rx/take-until stopper)
|
||||
(rx/filter (dragging? start-position zoom))
|
||||
(rx/take 1)
|
||||
(rx/merge-map #(move-point-stream)))
|
||||
|
||||
))))
|
||||
|
||||
(defn start-move-handler
|
||||
[index prefix]
|
||||
|
@ -604,7 +687,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 +697,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 +705,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 +713,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 +739,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)))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,35 +138,37 @@
|
|||
|
||||
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)
|
||||
:r (if (or selected? hover?) (/ 3.5 zoom) (/ 3 zoom))
|
||||
:style {:cursor (when (= edit-mode :draw) cur/pen-node)
|
||||
:stroke-width (/ 1 zoom)
|
||||
:stroke (cond (or selected? hover?) black-color
|
||||
|
@ -173,11 +181,21 @@
|
|||
:r (/ 10 zoom)
|
||||
:on-click on-click
|
||||
:on-mouse-down on-mouse-down
|
||||
:on-mouse-enter on-enter
|
||||
:on-mouse-leave on-leave
|
||||
:style {: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 +219,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))
|
||||
|
@ -220,6 +238,8 @@
|
|||
:r (/ 10 zoom)
|
||||
:on-click on-click
|
||||
:on-mouse-down on-mouse-down
|
||||
:on-mouse-enter on-enter
|
||||
:on-mouse-leave on-leave
|
||||
:style {:fill "transparent"}}]])))
|
||||
|
||||
(mf/defc path-preview [{:keys [zoom command from]}]
|
||||
|
@ -239,17 +259,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 +300,41 @@
|
|||
|
||||
(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}])
|
||||
:zoom zoom}])
|
||||
[:& path-handler {:point last-p
|
||||
:handler drag-handler
|
||||
:zoom zoom
|
||||
:selected false}]])]))
|
||||
:zoom zoom}]])]))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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))
|
||||
(-> (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 $))))
|
||||
|
|
Loading…
Add table
Reference in a new issue