diff --git a/common/app/common/pages.cljc b/common/app/common/pages.cljc index ae5a91983..491b1fc78 100644 --- a/common/app/common/pages.cljc +++ b/common/app/common/pages.cljc @@ -611,8 +611,7 @@ :stroke-alignment :center :stroke-width 2 :stroke-color "#000000" - :stroke-opacity 1 - :segments []} + :stroke-opacity 1} {:type :frame :name "Artboard" @@ -632,8 +631,7 @@ :stroke-alignment :center :stroke-width 2 :stroke-color "#000000" - :stroke-opacity 1 - :segments []} + :stroke-opacity 1} {:type :text :name "Text" @@ -646,22 +644,24 @@ (ex/raise :type :assertion :code :shape-type-not-implemented :context {:type type})) - (assoc shape - :id (uuid/next) - :x 0 - :y 0 - :width 1 - :height 1 - :selrect {:x 0 - :x1 0 - :x2 1 - :y 0 - :y1 0 - :y2 1 - :width 1 - :height 1} - :points [] - :segments []))) + + (cond-> shape + :always + (assoc :id (uuid/next)) + + (not #{:path :curve}) + (assoc :x 0 + :y 0 + :width 1 + :height 1 + :selrect {:x 0 + :y 0 + :x1 0 + :y1 0 + :x2 1 + :y2 1 + :width 1 + :height 1})))) (defn make-minimal-group [frame-id selection-rect group-name] diff --git a/frontend/resources/images/cursors/pen-node.svg b/frontend/resources/images/cursors/pen-node.svg new file mode 100644 index 000000000..ba03c12c6 --- /dev/null +++ b/frontend/resources/images/cursors/pen-node.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/images/cursors/pointer-move.svg b/frontend/resources/images/cursors/pointer-move.svg new file mode 100644 index 000000000..895bbd8ee --- /dev/null +++ b/frontend/resources/images/cursors/pointer-move.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/images/cursors/pointer-node.svg b/frontend/resources/images/cursors/pointer-node.svg new file mode 100644 index 000000000..185862c1d --- /dev/null +++ b/frontend/resources/images/cursors/pointer-node.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/resources/images/icons/pen.svg b/frontend/resources/images/icons/pen.svg new file mode 100644 index 000000000..9cd783355 --- /dev/null +++ b/frontend/resources/images/icons/pen.svg @@ -0,0 +1,56 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/frontend/resources/images/icons/pointer-inner.svg b/frontend/resources/images/icons/pointer-inner.svg new file mode 100644 index 000000000..50798578b --- /dev/null +++ b/frontend/resources/images/icons/pointer-inner.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/styles/main/partials/workspace.scss b/frontend/resources/styles/main/partials/workspace.scss index 69282ab40..d6119cd4a 100644 --- a/frontend/resources/styles/main/partials/workspace.scss +++ b/frontend/resources/styles/main/partials/workspace.scss @@ -253,13 +253,17 @@ } .viewport-actions-entry { - width: 27px; - height: 20px; + width: 28px; + height: 28px; margin: 0 0.25rem; cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + border-radius: 3px; svg { - width: 27px; + width: 20px; height: 20px; } @@ -267,13 +271,21 @@ fill: $color-primary; } - &.disabled { + &.is-disabled { opacity: 0.3; &:hover svg { fill: initial; } } + + &.is-toggled { + background: $color-black; + + svg { + fill: $color-primary; + } + } } .viewport-actions-entry-wide { diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 112f98afa..a8fc65d4d 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1632,13 +1632,24 @@ (defn start-path-edit [id] (dwdp/start-path-edit id)) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; 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 (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 + (deselect-all true)) + (rx/empty)))))) + (def shortcuts {"ctrl+i" #(st/emit! (toggle-layout-flags :assets)) "ctrl+l" #(st/emit! (toggle-layout-flags :sitemap :layers)) @@ -1671,7 +1682,7 @@ "ctrl+c" #(st/emit! copy-selected) "ctrl+v" #(st/emit! paste) "ctrl+x" #(st/emit! copy-selected delete-selected) - "escape" #(st/emit! :interrupt (deselect-all true)) + "escape" #(st/emit! (esc-pressed)) "del" #(st/emit! delete-selected) "backspace" #(st/emit! delete-selected) "ctrl+up" #(st/emit! (vertical-order-selected :up)) diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index 3275799b5..594c00f03 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -461,7 +461,14 @@ (ptk/reify ::start-edition-mode ptk/UpdateEvent (update [_ state] - (assoc-in state [:workspace-local :edition] id)) + (let [page-id (:current-page-id state) + objects (get-in state [:workspace-data :pages-index page-id :objects])] + ;; Can only edit objects that exist + (if (contains? objects id) + (-> state + (assoc-in [:workspace-local :selected] #{id}) + (assoc-in [:workspace-local :edition] id)) + state))) ptk/WatchEvent (watch [_ state stream] @@ -486,7 +493,7 @@ (let [page-id (:current-page-id state) objects (lookup-page-objects state page-id) - id (uuid/next) + id (or (:id attrs) (uuid/next)) shape (gpr/setup-proportions attrs) unames (retrieve-used-names objects) diff --git a/frontend/src/app/main/data/workspace/drawing.cljs b/frontend/src/app/main/data/workspace/drawing.cljs index 7987488a2..9e792a797 100644 --- a/frontend/src/app/main/data/workspace/drawing.cljs +++ b/frontend/src/app/main/data/workspace/drawing.cljs @@ -45,8 +45,9 @@ (rx/of (start-drawing :path))) ;; NOTE: comments are a special case and they manage they - ;; own interrupt cycle. - (when (not= tool :comments) + ;; own interrupt cycle.q + (when (and (not= tool :comments) + (not= tool :path)) (->> stream (rx/filter dwc/interrupt?) (rx/take 1) @@ -90,7 +91,7 @@ (watch [_ state stream] (rx/of (case type :path - (path/handle-drawing-path) + (path/handle-new-shape) :curve (curve/handle-drawing-curve) diff --git a/frontend/src/app/main/data/workspace/drawing/path.cljs b/frontend/src/app/main/data/workspace/drawing/path.cljs index 4d956d7b6..2ac40bf29 100644 --- a/frontend/src/app/main/data/workspace/drawing/path.cljs +++ b/frontend/src/app/main/data/workspace/drawing/path.cljs @@ -14,38 +14,48 @@ [app.common.math :as mth] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] - [app.util.data :as d] + [app.util.data :as ud] + [app.common.data :as cd] [app.util.geom.path :as ugp] [app.main.streams :as ms] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.drawing.common :as common] [app.common.geom.shapes.path :as gsp])) -;;;; +;; CONSTANTS +(defonce enter-keycode 13) -(def close-path-distance 5) -(defn seek-start-path [content] +;; PRIVATE METHODS + +(defn get-path-id [state] + (or (get-in state [:workspace-local :edition]) + (get-in state [:workspace-drawing :object :id]))) + +(defn get-path [state & path] + (let [edit-id (get-in state [:workspace-local :edition]) + page-id (:current-page-id state)] + (cd/concat + (if edit-id + [:workspace-data :pages-index page-id :objects edit-id] + [:workspace-drawing :object]) + path))) + +(defn last-start-path [content] (->> content reverse - (d/seek (fn [{cmd :command}] (= cmd :move-to))) + (cd/seek (fn [{cmd :command}] (= cmd :move-to))) :params)) (defn next-node "Calculates the next-node to be inserted." [shape position prev-point prev-handler] (let [last-command (-> shape :content last :command) - start-point (-> shape :content seek-start-path) + start-point (-> shape :content last-start-path) 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)) - close-path? (and start-point - (< (mth/abs (gpt/distance (gpt/point start-point) - (gpt/point position))) - close-path-distance))] + add-curve? (and prev-point prev-handler (not= last-command :close-path))] (cond - close-path? {:command :close-path - :params []} add-line? {:command :line-to :params position} add-curve? {:command :curve-to @@ -56,19 +66,24 @@ (defn append-node "Creates a new node in the path. Usualy used when drawing." [shape position prev-point prev-handler] - (let [command (next-node shape position prev-point prev-handler)] - (as-> shape $ - (update $ :content (fnil conj []) command) - (update $ :selrect (gsh/content->selrect (:content $)))))) + (let [command (next-node shape position prev-point prev-handler) + content (:content shape []) + content (conj content command)] + (-> shape + (assoc :content content) + (assoc :selrect (gsh/content->selrect content)) + ;; TODO: REMOVE POINTS + (assoc :points (gsh/content->points content))))) -(defn suffix-keyword [kw suffix] +(defn suffix-keyword + [kw suffix] (let [strkw (if kw (name kw) "")] (keyword (str strkw suffix)))) -;; handler-type => :prev :next -(defn move-handler [shape index handler-type match-opposite? position] +(defn move-handler + [shape index handler-type match-opposite? position] (let [content (:content shape) - [command next-command] (-> (d/with-next content) (nth index)) + [command next-command] (-> (ud/with-next content) (nth index)) update-command (fn [{cmd :command params :params :as command} param-prefix prev-command] @@ -86,9 +101,11 @@ update-content (fn [shape index prefix] (if (contains? (:content shape) index) - (let [prev-command (get-in shape [:content (dec index)])] - (update-in shape [:content index] update-command prefix prev-command)) - + (let [prev-command (get-in shape [:content (dec index)]) + content (-> shape :content (update index update-command prefix prev-command))] + (-> shape + (assoc :content content) + (assoc :selrect (gsh/content->selrect content)))) shape))] (cond-> shape @@ -106,102 +123,94 @@ (ugp/opposite-handler (gpt/point (:params command)) (gpt/point position)))))) - -;;;; -(defn finish-event? [{:keys [type shift] :as event}] - (or (= event ::end-path-drawing) - (= event :interrupt) +(defn end-path-event? [{:keys [type shift] :as event}] + (or (= event ::end-path) + (= (ptk/type event) :esc-pressed) + (= event :interrupt) ;; ESC (and (ms/keyboard-event? event) (= type :down) - (= 13 (:key event))))) - -#_(defn init-path [] - (fn [state] - (update-in state [:workspace-drawing :object] - assoc :content [] - :initialized? true))) - -#_(defn add-path-command [command] - (fn [state] - (update-in state [:workspace-drawing :object :content] conj command))) - -#_(defn update-point-segment [state index point] - (let [segments (count (get-in state [:workspace-drawing :object :segments])) - exists? (< -1 index segments)] - (cond-> state - exists? (assoc-in [:workspace-drawing :object :segments index] point)))) - -#_(defn finish-drawing-path [] - (fn [state] - (update-in - state [:workspace-drawing :object] - (fn [shape] (-> shape - (update :segments #(vec (butlast %))) - (gsh/update-path-selrect)))))) - + ;; TODO: Enter now finish path but can finish drawing/editing as well + (= enter-keycode (:key event))))) (defn calculate-selrect [shape] (assoc shape :points (gsh/content->points (:content shape)) :selrect (gsh/content->selrect (:content shape)))) -(defn init-path [] + +;; EVENTS + +(defn init-path [id] (ptk/reify ::init-path ptk/UpdateEvent (update [_ state] (-> state - (assoc-in [:workspace-drawing :object :initialized?] true) - (assoc-in [:workspace-local :edit-path :last-point] nil))))) + #_(assoc-in [:workspace-drawing :object :initialized?] true) + #_(assoc-in [:workspace-local :edit-path :last-point] nil))))) -(defn finish-path [] +(defn finish-path [id] (ptk/reify ::finish-path ptk/UpdateEvent (update [_ state] (-> state - (update :workspace-local dissoc :edit-path) - (update-in [:workspace-drawing :object] calculate-selrect))))) + (update-in [:workspace-local :edit-path id] dissoc :last-point :prev-handler :drag-handler :preview))))) (defn preview-next-point [{:keys [x y]}] - (ptk/reify ::add-node + (ptk/reify ::preview-next-point ptk/UpdateEvent (update [_ state] - (let [position (gpt/point x y) - {:keys [last-point prev-handler] :as shape} (get-in state [:workspace-local :edit-path]) + (let [id (get-path-id state) + 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 :preview] command))))) + (assoc-in state [:workspace-local :edit-path id :preview] command))))) (defn add-node [{:keys [x y]}] (ptk/reify ::add-node ptk/UpdateEvent (update [_ state] - - (let [position (gpt/point x y) - {:keys [last-point prev-handler]} (get-in state [:workspace-local :edit-path])] + (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 :last-point] position) - (update-in [:workspace-local :edit-path] dissoc :prev-handler) - (update-in [:workspace-drawing :object] append-node position last-point prev-handler)))))) + (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)))))) (defn drag-handler [{:keys [x y]}] (ptk/reify ::drag-handler ptk/UpdateEvent (update [_ state] - (let [position (gpt/point x y) - shape (get-in state [:workspace-drawing :object]) + (let [id (get-path-id state) + position (gpt/point x y) + shape (get-in state (get-path state)) index (dec (count (:content shape)))] (-> state - (update-in [:workspace-drawing :object] move-handler index :next true position) - (assoc-in [:workspace-local :edit-path :drag-handler] position)))))) + (update-in (get-path state) move-handler index :next true position) + (assoc-in [:workspace-local :edit-path id :prev-handler] position) + (assoc-in [:workspace-local :edit-path id :drag-handler] position)))))) (defn finish-drag [] (ptk/reify ::finish-drag ptk/UpdateEvent (update [_ state] - (let [handler (get-in state [:workspace-local :edit-path :drag-handler])] + (let [id (get-path-id state) + handler (get-in state [:workspace-local :edit-path id :drag-handler])] (-> state - (update-in [:workspace-local :edit-path] dissoc :drag-handler) - (assoc-in [:workspace-local :edit-path :prev-handler] handler)))))) + (update-in [:workspace-local :edit-path id] dissoc :drag-handler) + (assoc-in [:workspace-local :edit-path id :prev-handler] handler)))) + + ptk/WatchEvent + (watch [_ state stream] + (let [id (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)))))) + +;; EVENT STREAMS (defn make-click-stream [stream down-event] @@ -230,26 +239,31 @@ (rx/first) (rx/merge-map #(rx/of (add-node down-event) - ::end-path-drawing)))) + ::end-path)))) -(defn handle-drawing-path [] +;; MAIN ENTRIES + +(defn handle-drawing-path + [id] (ptk/reify ::handle-drawing-path ptk/WatchEvent (watch [_ state stream] (let [mouse-down (->> stream (rx/filter ms/mouse-down?)) - finish-events (->> stream (rx/filter finish-event?)) + end-path-events (->> stream (rx/filter end-path-event?)) + ;; Mouse move preview mousemove-events (->> ms/mouse-position - (rx/take-until finish-events) - (rx/throttle 100) + (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 finish-events) - (rx/throttle 100) + (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 @@ -258,13 +272,13 @@ (make-drag-stream stream %) (make-dbl-click-stream stream %))))] + (->> (rx/concat + (rx/of (init-path id)) + (rx/merge mousemove-events + mousedown-events) + (rx/of (finish-path id)))))))) + - (rx/concat - (rx/of (init-path)) - (rx/merge mousemove-events - mousedown-events) - (rx/of (finish-path)) - (rx/of common/handle-finish-drawing)))))) #_(def handle-drawing-path (ptk/reify ::handle-drawing-path @@ -326,29 +340,25 @@ (rx/of finish-drawing-path common/handle-finish-drawing))))))) -#_(defn close-drawing-path [] - (ptk/reify ::close-drawing-path - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-drawing :object :close?] true)) - - ptk/WatchEvent - (watch [_ state stream] - (rx/of ::end-path-drawing)))) - (defn stop-path-edit [] (ptk/reify ::stop-path-edit ptk/UpdateEvent (update [_ state] - (update state :workspace-local dissoc :edit-path)))) + (let [id (get-in state [:workspace-local :edition])] + (update state :workspace-local dissoc :edit-path id))))) (defn start-path-edit [id] (ptk/reify ::start-path-edit ptk/UpdateEvent (update [_ state] - (assoc-in state [:workspace-local :edit-path] {})) + ;; Only edit if the object has been created + (if-let [id (get-in state [:workspace-local :edition])] + (assoc-in state [:workspace-local :edit-path id] {:edit-mode :move + :selected #{} + :snap-toggled true}) + state)) ptk/WatchEvent (watch [_ state stream] @@ -362,40 +372,37 @@ ptk/UpdateEvent (update [_ state] - - (-> state - (update-in [:workspace-local :edit-path :content-modifiers (inc index)] assoc - :c1x dx :c1y dy) - (update-in [:workspace-local :edit-path :content-modifiers index] assoc - :x dx :y dy :c2x dx :c2y dy) - )))) + (let [id (get-in state [:workspace-local :edition])] + (-> 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 [index type dx dy] (ptk/reify ::modify-point ptk/UpdateEvent (update [_ state] - (let [s1 (if (= type :prev) -1 1) - s2 (if (= type :prev) 1 -1)] - (-> state - (update-in [:workspace-local :edit-path :content-modifiers (inc index)] assoc - :c1x (* s1 dx) :c1y (* s1 dy)) - (update-in [:workspace-local :edit-path :content-modifiers index] assoc - :c2x (* s2 dx) :c2y (* s2 dy) )) - )))) + (let [id (get-in state [:workspace-local :edition])] + (let [s1 (if (= type :prev) -1 1) + s2 (if (= type :prev) 1 -1)] + (-> state + (update-in [:workspace-local :edit-path id :content-modifiers (inc index)] assoc + :c1x (* s1 dx) :c1y (* s1 dy)) + (update-in [:workspace-local :edit-path id :content-modifiers index] assoc + :c2x (* s2 dx) :c2y (* s2 dy) )) + ))))) (defn apply-content-modifiers [] (ptk/reify ::apply-content-modifiers - ;;ptk/UpdateEvent - ;;(update [_ state] - ;; (update-in state [:workspace-local :edit-path] dissoc :content-modifiers)) - ptk/WatchEvent (watch [_ state stream] (let [id (get-in state [:workspace-local :edition]) page-id (:current-page-id state) old-content (get-in state [:workspace-data :pages-index page-id :objects id :content]) old-selrect (get-in state [:workspace-data :pages-index page-id :objects id :selrect]) - content-modifiers (get-in state [:workspace-local :edit-path :content-modifiers]) + content-modifiers (get-in state [:workspace-local :edit-path id :content-modifiers]) new-content (gsp/apply-content-modifiers old-content content-modifiers) new-selrect (gsh/content->selrect new-content) rch [{:type :mod-obj @@ -411,16 +418,59 @@ {:type :set :attr :selrect :val old-selrect}]}]] (rx/of (dwc/commit-changes rch uch {:commit-local? true}) - (fn [state] (update-in state [:workspace-local :edit-path] dissoc :content-modifiers))))))) + (fn [state] (update-in state [:workspace-local :edit-path id] dissoc :content-modifiers))))))) + +(defn save-path-content [] + (ptk/reify ::save-path-content + ptk/WatchEvent + (watch [_ state stream] + (let [id (get-in state [:workspace-local :edition]) + 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) + new-content (get-in state [:workspace-data :pages-index page-id :objects id :content]) + new-selrect (get-in state [:workspace-data :pages-index page-id :objects id :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}]}] + + 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}]}]] + + (rx/of (dwc/commit-changes rch uch {:commit-local? true})))))) + +(declare start-draw-mode) +(defn check-changed-content [] + (ptk/reify ::check-changed-content + ptk/WatchEvent + (watch [_ state stream] + (let [id (get-path-id state) + content (get-in state (get-path state :content)) + old-content (get-in state [:workspace-local :edit-path id :old-content]) + mode (get-in state [:workspace-local :edit-path id :edit-mode])] + + + (cond + (not= content old-content) (rx/of (save-path-content) + (start-draw-mode)) + (= mode :draw) (rx/of :interrupt) + :else (rx/of (finish-path id))))))) (defn start-move-path-point [index] (ptk/reify ::start-move-path-point ptk/WatchEvent (watch [_ state stream] - (let [start-point @ms/mouse-position - start-delta-x (get-in state [:workspace-local :edit-path :content-modifiers index :x] 0) - start-delta-y (get-in state [:workspace-local :edit-path :content-modifiers index :y] 0)] + (let [id (get-in state [:workspace-local :edition]) + start-point @ms/mouse-position + start-delta-x (get-in state [:workspace-local :edit-path id :content-modifiers index :x] 0) + start-delta-y (get-in state [:workspace-local :edit-path id :content-modifiers index :y] 0)] (rx/concat (->> ms/mouse-position (rx/take-until (->> stream (rx/filter ms/mouse-up?))) @@ -436,12 +486,13 @@ (ptk/reify ::start-move-handler ptk/WatchEvent (watch [_ state stream] - (let [[cx cy] (if (= :prev type) [:c2x :c2y] [:c1x :c1y]) + (let [id (get-in state [:workspace-local :edition]) + [cx cy] (if (= :prev type) [:c2x :c2y] [:c1x :c1y]) cidx (if (= :prev type) index (inc index)) start-point @ms/mouse-position - start-delta-x (get-in state [:workspace-local :edit-path :content-modifiers cidx cx] 0) - start-delta-y (get-in state [:workspace-local :edit-path :content-modifiers cidx cy] 0)] + start-delta-x (get-in state [:workspace-local :edit-path id :content-modifiers cidx cx] 0) + start-delta-y (get-in state [:workspace-local :edit-path id :content-modifiers cidx cy] 0)] (rx/concat (->> ms/mouse-position @@ -453,3 +504,114 @@ (+ start-delta-y (- (:y %) (:y start-point))))) ) (rx/concat (rx/of (apply-content-modifiers)))))))) + +(defn start-draw-mode [] + (ptk/reify ::start-draw-mode + ptk/UpdateEvent + (update [_ state] + (let [id (get-in state [:workspace-local :edition]) + page-id (:current-page-id state) + old-content (get-in state [:workspace-data :pages-index page-id :objects id :content])] + (-> state + (assoc-in [:workspace-local :edit-path id :old-content] old-content)))) + + ptk/WatchEvent + (watch [_ state stream] + (let [id (get-in state [:workspace-local :edition]) + edit-mode (get-in state [:workspace-local :edit-path id :edit-mode])] + (if (= :draw edit-mode) + (rx/concat + (rx/of (handle-drawing-path id)) + (->> stream + (rx/filter (ptk/type? ::finish-path)) + (rx/take 1) + (rx/merge-map #(rx/of (check-changed-content))))) + (rx/empty)))))) + +(defn change-edit-mode [mode] + (ptk/reify ::change-edit-mode + ptk/UpdateEvent + (update [_ state] + (let [id (get-in state [:workspace-local :edition])] + (cond-> state + id (assoc-in [:workspace-local :edit-path id :edit-mode] mode)))) + + ptk/WatchEvent + (watch [_ state stream] + (let [id (get-path-id state)] + (cond + (and id (= :move mode)) (rx/of ::end-path) + (and id (= :draw mode)) (rx/of (start-draw-mode)) + :else (rx/empty)))))) + +(defn select-handler [index type] + (ptk/reify ::select-handler + ptk/UpdateEvent + (update [_ state] + (let [id (get-in state [:workspace-local :edition])] + (-> state + (update-in [:workspace-local :edit-path id :selected] (fnil conj #{}) [index type])))))) + +(defn select-node [index] + (ptk/reify ::select-node + ptk/UpdateEvent + (update [_ state] + (let [id (get-in state [:workspace-local :edition])] + (-> state + (update-in [:workspace-local :edit-path id :selected] (fnil conj #{}) index)))))) + +(defn add-to-selection-handler [index type] + (ptk/reify ::add-to-selection-handler + ptk/UpdateEvent + (update [_ state] + state))) + +(defn add-to-selection-node [index] + (ptk/reify ::add-to-selection-node + ptk/UpdateEvent + (update [_ state] + state))) + +(defn remove-from-selection-handler [index] + (ptk/reify ::remove-from-selection-handler + ptk/UpdateEvent + (update [_ state] + state))) + +(defn remove-from-selection-node [index] + (ptk/reify ::remove-from-selection-handler + ptk/UpdateEvent + (update [_ state] + state))) + +(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] [])] + (if (> (count content) 1) + (assoc-in state [:workspace-drawing :object :initialized?] true) + state))) + + ptk/WatchEvent + (watch [_ state stream] + (->> (rx/of common/handle-finish-drawing + (dwc/start-edition-mode shape-id) + (start-path-edit shape-id) + (change-edit-mode :draw)))))) + +(defn handle-new-shape + "Creates a new path shape" + [] + (ptk/reify ::handle-new-shape + ptk/WatchEvent + (watch [_ state stream] + (let [shape-id (get-in state [:workspace-drawing :object :id])] + (rx/concat + (rx/of (handle-drawing-path shape-id)) + (->> stream + (rx/filter (ptk/type? ::finish-path)) + (rx/take 1) + (rx/observe-on :async) + (rx/map #(handle-new-shape-result shape-id))) + ))))) diff --git a/frontend/src/app/main/ui/cursors.clj b/frontend/src/app/main/ui/cursors.clj index 414297d11..d7ed83b71 100644 --- a/frontend/src/app/main/ui/cursors.clj +++ b/frontend/src/app/main/ui/cursors.clj @@ -19,6 +19,7 @@ (def default-hotspot-x 12) (def default-hotspot-y 12) (def default-rotation 0) +(def default-height 20) (defn parse-svg [svg-data] (-> svg-data @@ -53,25 +54,27 @@ (str/replace #"\s+$" ""))) (defn encode-svg-cursor - [id rotation x y] + [id rotation x y height] (let [svg-path (str cursor-folder "/" (name id) ".svg") data (-> svg-path io/resource slurp parse-svg uri/percent-encode) transform (if rotation (str " transform='rotate(" rotation ")'") "") data (clojure.pprint/cl-format nil - "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='20px' height='20px'~A%3E~A%3C/svg%3E\") ~A ~A, auto" - transform data x y)] + "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='20px' height='~Apx'~A%3E~A%3C/svg%3E\") ~A ~A, auto" + height transform data x y )] data)) (defmacro cursor-ref "Creates a static cursor given its name, rotation and x/y hotspot" - ([id] (encode-svg-cursor id default-rotation default-hotspot-x default-hotspot-y)) - ([id rotation] (encode-svg-cursor id rotation default-hotspot-x default-hotspot-y)) - ([id rotation x y] (encode-svg-cursor id rotation x y))) + ([id] (encode-svg-cursor id default-rotation default-hotspot-x default-hotspot-y default-height)) + ([id rotation] (encode-svg-cursor id rotation default-hotspot-x default-hotspot-y default-height)) + ([id rotation x y] (encode-svg-cursor id rotation x y default-height)) + ([id rotation x y height] (encode-svg-cursor id rotation x y height)) + ) (defmacro cursor-fn "Creates a dynamic cursor that can be rotated in runtime" [id initial] - (let [cursor (encode-svg-cursor id "{{rotation}}" default-hotspot-x default-hotspot-y)] + (let [cursor (encode-svg-cursor id "{{rotation}}" default-hotspot-x default-hotspot-y default-height)] `(fn [rot#] (str/replace ~cursor "{{rotation}}" (+ ~initial rot#))))) diff --git a/frontend/src/app/main/ui/cursors.cljs b/frontend/src/app/main/ui/cursors.cljs index f7dd21fed..bc60deba4 100644 --- a/frontend/src/app/main/ui/cursors.cljs +++ b/frontend/src/app/main/ui/cursors.cljs @@ -8,8 +8,7 @@ ;; Copyright (c) 2020 UXBOX Labs SL (ns app.main.ui.cursors - (:require-macros [app.main.ui.cursors :refer [cursor-ref - cursor-fn]]) + (:require-macros [app.main.ui.cursors :refer [cursor-ref cursor-fn]]) (:require [rumext.alpha :as mf] [cuerdas.core :as str] [app.util.timers :as ts])) @@ -33,6 +32,9 @@ (def rotate (cursor-fn :rotate 90)) (def text (cursor-ref :text)) (def picker (cursor-ref :picker 0 0 24)) +(def pointer-node (cursor-ref :pointer-node 0 0 10 32)) +(def pointer-move (cursor-ref :pointer-move 0 0 10 42)) +(def pen-node (cursor-ref :pen-node 0 0 10 36)) (mf/defc debug-preview {::mf/wrap-props false} @@ -49,8 +51,11 @@ [:div {:style {:width "100px" :height "100px" :background-image (-> value (str/replace #"(url\(.*\)).*" "$1")) - :background-size "cover" + :background-size "contain" + :background-repeat "no-repeat" + :background-position "center" :cursor value}}] [:span {:style {:white-space "nowrap" :margin-right "1rem"}} (pr-str key)]])))])) + diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index 0550c4a4a..f620afd84 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -136,6 +136,8 @@ (def nodes-remove (icon-xref :nodes-remove)) (def nodes-separate (icon-xref :nodes-separate)) (def nodes-snap (icon-xref :nodes-snap)) +(def pen (icon-xref :pen)) +(def pointer-inner (icon-xref :pointer-inner)) (def loader-pencil (mf/html diff --git a/frontend/src/app/main/ui/workspace/selection.cljs b/frontend/src/app/main/ui/workspace/selection.cljs index 5f9708a58..cdd104091 100644 --- a/frontend/src/app/main/ui/workspace/selection.cljs +++ b/frontend/src/app/main/ui/workspace/selection.cljs @@ -368,8 +368,8 @@ :zoom zoom :color color}] - (= (= type :path) - (= edition (:id shape))) + (and (= type :path) + (= edition (:id shape))) [:& path-editor {:zoom zoom :shape shape}] diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index d1665317b..1d84fa620 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -110,7 +110,6 @@ (mf/use-callback (mf/deps (:id shape)) (fn [event] - (prn "?? FRAME") (dom/prevent-default event) (st/emit! (dw/deselect-all) (dw/select-shape (:id shape))))) diff --git a/frontend/src/app/main/ui/workspace/shapes/path.cljs b/frontend/src/app/main/ui/workspace/shapes/path.cljs index 338a45f79..9396aca1f 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path.cljs @@ -39,11 +39,25 @@ (def white-color "#FFFFFF") (def gray-color "#B1B2B5") -(def edit-path-ref - (l/derived :edit-path refs/workspace-local)) -(def content-modifiers-ref - (l/derived :content-modifiers edit-path-ref)) + +(def current-edit-path-ref + (let [selfn (fn [local] + (let [id (:edition local)] + (get-in local [:edit-path id])))] + (l/derived selfn refs/workspace-local))) + +(defn make-edit-path-ref [id] + (mf/use-memo + (mf/deps id) + (let [selfn #(get-in % [:edit-path id])] + #(l/derived selfn refs/workspace-local)))) + +(defn make-content-modifiers-ref [id] + (mf/use-memo + (mf/deps id) + (let [selfn #(get-in % [:edit-path id :content-modifiers])] + #(l/derived selfn refs/workspace-local)))) (mf/defc path-wrapper {::mf/wrap-props false} @@ -67,11 +81,14 @@ (dom/prevent-default event) (st/emit! (dw/start-edition-mode (:id shape)) (dw/start-path-edit (:id shape))))))) - + content-modifiers-ref (make-content-modifiers-ref (:id shape)) content-modifiers (mf/deref content-modifiers-ref) + editing-id (mf/deref refs/selected-edition) + editing? (= editing-id (:id shape)) shape (update shape :content gsp/apply-content-modifiers content-modifiers)] [:> shape-container {:shape shape + :pointer-events (when editing? "none") :on-double-click on-double-click :on-mouse-down on-mouse-down :on-context-menu on-context-menu} @@ -79,22 +96,30 @@ :background? true}]])) (mf/defc path-actions [{:keys [shape]}] - [:div.path-actions - [:div.viewport-actions-group - [:div.viewport-actions-entry i/nodes-add] - [:div.viewport-actions-entry i/nodes-remove]] + (let [id (mf/deref refs/selected-edition) + {:keys [edit-mode selected 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") + :on-click #(st/emit! (drp/change-edit-mode :draw))} i/pen] + [: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-entry {:class "is-disabled"} i/nodes-add] + [:div.viewport-actions-entry {:class "is-disabled"} i/nodes-remove]] - [:div.viewport-actions-group - [:div.viewport-actions-entry i/nodes-merge] - [:div.viewport-actions-entry i/nodes-join] - [:div.viewport-actions-entry i/nodes-separate]] + [: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 i/nodes-corner] - [:div.viewport-actions-entry i/nodes-curve]] + [: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-group - [:div.viewport-actions-entry i/nodes-snap]]]) + [:div.viewport-actions-group + [:div.viewport-actions-entry {:class (when snap-toggled "is-toggled")} i/nodes-snap]]])) (mf/defc path-preview [{:keys [zoom command from]}] @@ -107,36 +132,58 @@ :y (:y from)}} command])}])) -(mf/defc path-point [{:keys [index position stroke-color fill-color zoom]}] +(mf/defc path-point [{:keys [index position stroke-color fill-color zoom edit-mode selected]}] (let [{:keys [x y]} position on-click (fn [event] - (dom/stop-propagation event) - (dom/prevent-default event)) - on-mouse-down (fn [event] - (dom/stop-propagation event) - (dom/prevent-default event) - (st/emit! (drp/start-move-path-point index)))] - [:circle - {:cx x - :cy y - :r (/ 3 zoom) - :on-click on-click - :on-mouse-down on-mouse-down - :style {:cursor cur/resize-alt - :stroke-width (/ 1 zoom) - :stroke (or stroke-color black-color) - :fill (or fill-color white-color)}}])) + (cond + (= edit-mode :move) + (do + (dom/stop-propagation event) + (dom/prevent-default event) + (st/emit! (drp/select-node index))))) -(mf/defc path-handler [{:keys [index point handler zoom selected type]}] + on-mouse-down (fn [event] + (cond + (= edit-mode :move) + (do + (dom/stop-propagation event) + (dom/prevent-default event) + (st/emit! (drp/start-move-path-point index)))))] + [:g.path-point + [:circle.path-point + {:cx x + :cy y + :r (/ 3 zoom) + :style { ;; :cursor cur/resize-alt + :stroke-width (/ 1 zoom) + :stroke (or stroke-color black-color) + :fill (or fill-color white-color)}}] + [:circle {:cx x + :cy y + :r (/ 10 zoom) + :on-click on-click + :on-mouse-down on-mouse-down + :style {:fill "transparent"}}]] + )) + +(mf/defc path-handler [{:keys [index point handler zoom selected type edit-mode]}] (when (and point handler) (let [{:keys [x y]} handler on-click (fn [event] - (dom/stop-propagation event) - (dom/prevent-default event)) + (cond + (= edit-mode :move) + (do + (dom/stop-propagation event) + (dom/prevent-default event) + (drp/select-handler index type)))) + on-mouse-down (fn [event] - (dom/stop-propagation event) - (dom/prevent-default event) - (st/emit! (drp/start-move-handler index type)))] + (cond + (= edit-mode :move) + (do + (dom/stop-propagation event) + (dom/prevent-default event) + (st/emit! (drp/start-move-handler index type)))))] [:g.handler {:class (name type)} [:line {:x1 (:x point) @@ -150,23 +197,30 @@ :y (- y (/ 3 zoom)) :width (/ 6 zoom) :height (/ 6 zoom) - :on-click on-click - :on-mouse-down on-mouse-down - :style {:cursor cur/resize-alt + + :style {;; :cursor cur/resize-alt :stroke-width (/ 1 zoom) :stroke (if selected black-color primary-color) - :fill (if selected primary-color white-color)}}]]))) + :fill (if selected primary-color white-color)}}] + + [:circle {:cx x + :cy y + :r (/ 10 zoom) + :on-click on-click + :on-mouse-down on-mouse-down + :style {:fill "transparent"}}]]))) (mf/defc path-editor [{:keys [shape zoom]}] (let [{:keys [content]} shape - {:keys [drag-handler prev-handler preview content-modifiers]} (mf/deref edit-path-ref) + edit-path-ref (make-edit-path-ref (:id shape)) + {:keys [edit-mode selected drag-handler prev-handler preview content-modifiers]} (mf/deref edit-path-ref) + selected (or selected #{}) content (gsp/apply-content-modifiers content content-modifiers) points (gsp/content->points content) last-command (last content) - last-p (last points) - selected false] + last-p (last points)] [:g.path-editor (when (and preview (not drag-handler)) @@ -187,7 +241,8 @@ :zoom zoom :type :prev :index index - :selected false}]) + :selected (selected [index :prev]) + :edit-mode edit-mode}]) (when (= :curve-to (:command next)) [:& path-handler {:point point @@ -195,7 +250,8 @@ :zoom zoom :type :next :index index - :selected false}]) + :selected (selected [index :next]) + :edit-mode edit-mode}]) (when (and (= index (dec (count content))) prev-handler (not drag-handler)) @@ -204,13 +260,15 @@ :zoom zoom :type :prev :index index - :selected false}]) + :selected (selected index) + :edit-mode edit-mode}]) [:& path-point {:position point - :stroke-color (when-not selected primary-color) - :fill-color (when selected primary-color) + :stroke-color (when-not (selected index) primary-color) + :fill-color (when (selected index) primary-color) :index index - :zoom zoom}]])) + :zoom zoom + :edit-mode edit-mode}]])) (when drag-handler [:g.drag-handler diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 547ceb36c..fc07ba6db 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -199,6 +199,7 @@ vport vbox edition + edit-path tooltip selected panning @@ -221,6 +222,7 @@ drawing (mf/deref refs/workspace-drawing) drawing-tool (:tool drawing) drawing-obj (:object drawing) + drawing-path? (and edition (= :draw (get-in edit-path [edition :edit-mode]))) zoom (or zoom 1) on-mouse-down @@ -292,7 +294,7 @@ on-double-click (mf/use-callback - (mf/deps edition) + (mf/deps edition edit-path) (fn [event] (dom/stop-propagation event) (let [ctrl? (kbd/ctrl? event) @@ -300,7 +302,7 @@ alt? (kbd/alt? event)] (st/emit! (ms/->MouseEvent :double-click ctrl? shift? alt?)) - (if edition + (if (not drawing-path?) (st/emit! dw/clear-edition-mode))))) on-key-down @@ -431,6 +433,7 @@ final-x (- (:x viewport-coord) (/ (:width shape) 2)) final-y (- (:y viewport-coord) (/ (:height shape) 2))] (st/emit! (dw/add-shape (-> shape + (assoc :id (uuid/next)) (assoc :x final-x) (assoc :y final-y))))) @@ -537,7 +540,7 @@ (= drawing-tool :frame) cur/create-artboard (= drawing-tool :rect) cur/create-rectangle (= drawing-tool :circle) cur/create-ellipse - (= drawing-tool :path) cur/pen + (or (= drawing-tool :path) drawing-path?) cur/pen (= drawing-tool :curve)cur/pencil drawing-tool cur/create-shape :else cur/pointer-inner)