From f3cce1904c41250f53a611f7c08593f14f30bc85 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 12 Nov 2020 10:42:09 +0100 Subject: [PATCH] :sparkles: First version drawing beziers --- common/app/common/geom/shapes/common.cljc | 8 +- common/app/common/geom/shapes/transforms.cljc | 3 +- common/app/common/math.cljc | 2 +- .../app/main/data/workspace/drawing/path.cljs | 133 ++++++++++++++---- .../src/app/main/ui/workspace/drawarea.cljs | 38 ++--- .../app/main/ui/workspace/shapes/path.cljs | 93 +++++++++++- frontend/src/app/util/geom/path.cljs | 34 ++++- 7 files changed, 255 insertions(+), 56 deletions(-) diff --git a/common/app/common/geom/shapes/common.cljc b/common/app/common/geom/shapes/common.cljc index f69a7d3ea..d4b64b481 100644 --- a/common/app/common/geom/shapes/common.cljc +++ b/common/app/common/geom/shapes/common.cljc @@ -19,8 +19,12 @@ (defn center-rect [{:keys [x y width height]}] - (gpt/point (+ x (/ width 2)) - (+ y (/ height 2)))) + (when (and (mth/finite? x) + (mth/finite? y) + (mth/finite? width) + (mth/finite? height)) + (gpt/point (+ x (/ width 2)) + (+ y (/ height 2))))) (defn center-selrect "Calculate the center of the shape." diff --git a/common/app/common/geom/shapes/transforms.cljc b/common/app/common/geom/shapes/transforms.cljc index 94771bbf7..3253e0253 100644 --- a/common/app/common/geom/shapes/transforms.cljc +++ b/common/app/common/geom/shapes/transforms.cljc @@ -23,7 +23,8 @@ "Returns a transformation matrix without changing the shape properties. The result should be used in a `transform` attribute in svg" ([{:keys [x y] :as shape}] - (let [shape-center (gco/center-shape shape)] + (let [shape-center (or (gco/center-shape shape) + (gpt/point 0 0))] (-> (gmt/matrix) (gmt/translate shape-center) (gmt/multiply (:transform shape (gmt/matrix))) diff --git a/common/app/common/math.cljc b/common/app/common/math.cljc index 9125c7c35..92ba19a1a 100644 --- a/common/app/common/math.cljc +++ b/common/app/common/math.cljc @@ -23,7 +23,7 @@ (defn finite? [v] - #?(:cljs (js/isFinite v) + #?(:cljs (and (not (nil? v)) (js/isFinite v)) :clj (Double/isFinite v))) (defn abs diff --git a/frontend/src/app/main/data/workspace/drawing/path.cljs b/frontend/src/app/main/data/workspace/drawing/path.cljs index e9b7382b4..c30441f72 100644 --- a/frontend/src/app/main/data/workspace/drawing/path.cljs +++ b/frontend/src/app/main/data/workspace/drawing/path.cljs @@ -14,15 +14,12 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.main.streams :as ms] - [app.util.geom.path :as path] + [app.util.geom.path :as ugp] [app.main.data.workspace.drawing.common :as common])) (defn finish-event? [{:keys [type shift] :as event}] (or (= event ::end-path-drawing) (= event :interrupt) - #_(and (ms/mouse-event? event) - (or (= type :double-click) - (= type :context-menu))) (and (ms/keyboard-event? event) (= type :down) (= 13 (:key event))))) @@ -78,26 +75,105 @@ (assoc-in [:workspace-drawing :object :last-point] nil) (update-in [:workspace-drawing :object] calculate-selrect))))) +(defn preview-next-point [{:keys [x y]}] + (ptk/reify ::add-node + ptk/UpdateEvent + (update [_ state] + (let [point {:x x :y y} + {:keys [last-point prev-handler]} (get-in state [:workspace-drawing :object]) + + command (cond + (and last-point (not prev-handler)) + {:command :line-to + :params point} + + (and last-point prev-handler) + {:command :curve-to + :params (ugp/make-curve-params point prev-handler)} + + :else + nil) + ] + (-> state + (assoc-in [:workspace-drawing :object :preview] command)))))) + (defn add-node [{:keys [x y]}] (ptk/reify ::add-node ptk/UpdateEvent (update [_ state] (let [point {:x x :y y} - last-point (get-in state [:workspace-drawing :object :last-point]) - command (if last-point + {:keys [last-point prev-handler]} (get-in state [:workspace-drawing :object]) + + command (cond + (and last-point (not prev-handler)) {:command :line-to :params point} + + (and last-point prev-handler) + {:command :curve-to + :params (ugp/make-curve-params point prev-handler)} + + :else {:command :move-to - :params point})] + :params point}) + ] (-> state - (assoc-in [:workspace-drawing :object :last-point] point) - (update-in [:workspace-drawing :object :content] (fnil conj []) command)))))) + (assoc-in [:workspace-drawing :object :last-point] point) + (update-in [:workspace-drawing :object] dissoc :prev-handler) + (update-in [:workspace-drawing :object :content] (fnil conj []) command) + (update-in [:workspace-drawing :object] calculate-selrect)))))) (defn drag-handler [{:keys [x y]}] (ptk/reify ::drag-handler ptk/UpdateEvent (update [_ state] - (-> state)))) + (let [change-handler (fn [content] + (let [last-idx (dec (count content)) + last (get content last-idx nil) + prev (get content (dec last-idx) nil) + {last-x :x last-y :y} (:params last) + opposite (when last (ugp/opposite-handler (gpt/point last-x last-y) (gpt/point x y)))] + + (cond + (and prev (= (:command last) :line-to)) + (-> content + (assoc last-idx {:command :curve-to + :params {:x (-> last :params :x) + :y (-> last :params :y) + :c1x (-> prev :params :x) + :c1y (-> prev :params :y) + :c2x (-> last :params :x) + :c2y (-> last :params :y)}}) + (update-in + [last-idx :params] + #(-> % + (assoc :c2x (:x opposite) + :c2y (:y opposite))))) + + (= (:command last) :curve-to) + (update-in content + [last-idx :params] + #(-> % + (assoc :c2x (:x opposite) + :c2y (:y opposite)))) + :else + content)) + + + ) + handler (gpt/point x y)] + (-> state + (update-in [:workspace-drawing :object :content] change-handler) + (assoc-in [:workspace-drawing :object :drag-handler] handler)))))) + +(defn finish-drag [] + (ptk/reify ::finish-drag + ptk/UpdateEvent + (update [_ state] + (let [handler (get-in state [:workspace-drawing :object :drag-handler])] + (-> state + (update-in [:workspace-drawing :object] dissoc :drag-handler) + (assoc-in [:workspace-drawing :object :prev-handler] handler)))))) (defn make-click-stream [stream down-event] @@ -115,8 +191,9 @@ (rx/map #(drag-handler %)))] (->> (rx/timer 400) (rx/merge-map #(rx/concat - (add-node down-event) - drag-events))))) + (rx/of (add-node down-event)) + drag-events + (rx/of (finish-drag))))))) (defn make-dbl-click-stream [stream down-event] @@ -133,26 +210,32 @@ (watch [_ state stream] ;; clicks stream<[MouseEvent, Position]> - (let [ - - mouse-down (->> stream (rx/filter ms/mouse-down?)) + (let [mouse-down (->> stream (rx/filter ms/mouse-down?)) finish-events (->> stream (rx/filter finish-event?)) - events (->> mouse-down - (rx/take-until finish-events) - (rx/throttle 100) - (rx/with-latest merge ms/mouse-position) + mousemove-events + (->> ms/mouse-position + (rx/take-until finish-events) + (rx/throttle 100) + (rx/map #(preview-next-point %))) - ;; We change to the stream that emits the first event - (rx/switch-map - #(rx/race (make-click-stream stream %) - (make-drag-stream stream %) - (make-dbl-click-stream stream %))))] + mousedown-events + (->> mouse-down + (rx/take-until finish-events) + (rx/throttle 100) + (rx/with-latest merge ms/mouse-position) + + ;; We change to the stream that emits the first event + (rx/switch-map + #(rx/race (make-click-stream stream %) + (make-drag-stream stream %) + (make-dbl-click-stream stream %))))] (rx/concat (rx/of (init-path)) - events + (rx/merge mousemove-events + mousedown-events) (rx/of (finish-path)) (rx/of common/handle-finish-drawing))) diff --git a/frontend/src/app/main/ui/workspace/drawarea.cljs b/frontend/src/app/main/ui/workspace/drawarea.cljs index ae3f9cbb6..0ce96a02c 100644 --- a/frontend/src/app/main/ui/workspace/drawarea.cljs +++ b/frontend/src/app/main/ui/workspace/drawarea.cljs @@ -12,6 +12,7 @@ [app.main.data.workspace.drawing :as dd] [app.main.store :as st] [app.main.ui.workspace.shapes :as shapes] + [app.main.ui.workspace.shapes.path :refer [path-editor]] [app.common.geom.shapes :as gsh] [app.common.data :as d] [app.util.dom :as dom] @@ -22,10 +23,13 @@ (mf/defc draw-area [{:keys [shape zoom] :as props}] - (when (:id shape) - (case (:type shape) - (:path :curve) [:& path-draw-area {:shape shape}] - [:& generic-draw-area {:shape shape :zoom zoom}]))) + + [:g.draw-area + [:& shapes/shape-wrapper {:shape shape}] + + (case (:type shape) + :path [:& path-editor {:shape shape :zoom zoom}] + #_:default [:& generic-draw-area {:shape shape :zoom zoom}])]) (mf/defc generic-draw-area [{:keys [shape zoom]}] @@ -34,19 +38,16 @@ (not (d/nan? x)) (not (d/nan? y))) - [:g - [:& shapes/shape-wrapper {:shape shape}] - [:rect.main {:x x :y y - :width width - :height height - :style {:stroke "#1FDEA7" - :fill "transparent" - :stroke-width (/ 1 zoom)}}]]))) + [:rect.main {:x x :y y + :width width + :height height + :style {:stroke "#1FDEA7" + :fill "transparent" + :stroke-width (/ 1 zoom)}}]))) -(mf/defc path-draw-area +#_(mf/defc path-draw-area [{:keys [shape] :as props}] (let [locale (i18n/use-locale) - on-click (fn [event] (dom/stop-propagation event) @@ -62,14 +63,13 @@ (fn [event] (st/emit! (dw/assign-cursor-tooltip nil)))] - (when-let [{:keys [x y] :as segment} (first (:segments shape))] - [:g - [:& shapes/shape-wrapper {:shape shape}] - (when (not= :curve (:type shape)) + [:g.drawing + [:& shapes/shape-wrapper {:shape shape}] + #_(when (not= :curve (:type shape)) [:circle.close-bezier {:cx x :cy y :r 5 :on-click on-click :on-mouse-enter on-mouse-enter - :on-mouse-leave on-mouse-leave}])]))) + :on-mouse-leave on-mouse-leave}])])) diff --git a/frontend/src/app/main/ui/workspace/shapes/path.cljs b/frontend/src/app/main/ui/workspace/shapes/path.cljs index 66faed116..543d6ae44 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path.cljs @@ -23,7 +23,8 @@ [app.main.ui.shapes.path :as path] [app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.shape :refer [shape-container]] - [app.main.ui.workspace.shapes.common :as common])) + [app.main.ui.workspace.shapes.common :as common] + [app.util.geom.path :as ugp])) (mf/defc path-wrapper {::mf/wrap-props false} @@ -56,3 +57,93 @@ [:& path/path-shape {:shape shape :background? true}]])) + +(mf/defc path-handler [{:keys [point handler zoom selected]}] + (when (and point handler) + (let [{:keys [x y]} handler] + [:g.handler + [:line + {:x1 (:x point) + :y1 (:y point) + :x2 x + :y2 y + :style {:stroke "#B1B2B5" + :stroke-width (/ 1 zoom)}}] + [:rect + {:x (- x (/ 3 zoom)) + :y (- y (/ 3 zoom)) + :width (/ 6 zoom) + :height (/ 6 zoom) + :style {:stroke-width (/ 1 zoom) + :stroke (if selected "#000000" "#1FDEA7") + :fill (if selected "#1FDEA7" "#FFFFFF")}}]]))) + +(mf/defc path-editor + [{:keys [shape zoom]}] + + (let [points (:points shape) + drag-handler (:drag-handler shape) + prev-handler (:prev-handler shape) + last-command (last (:content shape)) + selected false + last-p (last points) + handlers (ugp/extract-handlers (:content shape)) + handlers (if (and prev-handler (not drag-handler)) + (conj handlers {:point last-p :prev prev-handler}) + handlers) + ] + + [:g.path-editor + (when (and (:preview shape) (not (:drag-handler shape))) + [:* + [:path {:style {:fill "transparent" + :stroke "#DB00FF" + :stroke-width (/ 1 zoom)} + :d (ugp/content->path [{:command :move-to + :params {:x (:x last-p) + :y (:y last-p)}} + (:preview shape)])}] + [:circle + {:cx (-> shape :preview :params :x) + :cy (-> shape :preview :params :y) + :r (/ 3 zoom) + :style {:stroke-width (/ 1 zoom) + :stroke "#DB00FF" + :fill "#FFFFFF"}}]]) + + (for [{:keys [point prev next]} handlers] + [:* + [:& path-handler {:point point + :handler prev + :zoom zoom + :type :prev + :selected false}] + [:& path-handler {:point point + :handler next + :zoom zoom + :type :next + :selected false}]]) + + (when drag-handler + [:* + (when (not= :move-to (:command last-command)) + [:& path-handler {:point last-p + :handler (ugp/opposite-handler last-p drag-handler) + :zoom zoom + :type :drag-opposite + :selected false}]) + [:& path-handler {:point last-p + :handler drag-handler + :zoom zoom + :type :drag + :selected false}]]) + + (for [{:keys [x y] :as point} points] + [:circle + {:cx x + :cy y + :r (/ 3 zoom) + :style {:stroke-width (/ 1 zoom) + :stroke (if selected "#000000" "#1FDEA7") + :fill (if selected "#1FDEA7" "#FFFFFF")} + }])])) diff --git a/frontend/src/app/util/geom/path.cljs b/frontend/src/app/util/geom/path.cljs index c50a7ec78..1da9ce3a0 100644 --- a/frontend/src/app/util/geom/path.cljs +++ b/frontend/src/app/util/geom/path.cljs @@ -11,6 +11,7 @@ (:require [cuerdas.core :as str] [app.common.data :as d] + [app.common.geom.point :as gpt] [app.util.geom.path-impl-simplify :as impl-simplify])) (defn simplify @@ -192,10 +193,29 @@ (map command->string) (str/join ""))) -#_(let [path "M.343 15.974a.514.514 0 01-.317-.321c-.023-.07-.026-.23-.026-1.43 0-1.468-.001-1.445.09-1.586.02-.032 1.703-1.724 3.74-3.759a596.805 596.805 0 003.7-3.716c0-.009-.367-.384-.816-.833a29.9 29.9 0 01-.817-.833c0-.01.474-.49 1.054-1.07l1.053-1.053.948.946.947.947 1.417-1.413C12.366.806 12.765.418 12.856.357c.238-.161.52-.28.792-.334.17-.034.586-.03.76.008.801.173 1.41.794 1.57 1.603.03.15.03.569 0 .718a2.227 2.227 0 01-.334.793c-.061.09-.45.49-1.496 1.54L12.734 6.1l.947.948.947.947-1.053 1.054c-.58.58-1.061 1.054-1.07 1.054-.01 0-.384-.368-.833-.817-.45-.45-.824-.817-.834-.817-.009 0-1.68 1.666-3.716 3.701a493.093 493.093 0 01-3.759 3.74c-.14.091-.117.09-1.59.089-1.187 0-1.366-.004-1.43-.027zm6.024-4.633a592.723 592.723 0 003.663-3.68c0-.02-1.67-1.69-1.69-1.69-.01 0-1.666 1.648-3.68 3.663L.996 13.297v.834c0 .627.005.839.02.854.015.014.227.02.854.02h.833l3.664-3.664z" - content (path->content path) - new-path (content->path content) - ] - (prn "path" path) - (.log js/console "?? 1" (clj->js content)) - (prn "?? 2" (= path new-path) new-path)) +(defn make-curve-params + ([point] + (make-curve-params point point point)) + + ([point handler] (make-curve-params point handler point)) + + ([point h1 h2] + {:x (:x point) + :y (:y point) + :c1x (:x h1) + :c1y (:y h1) + :c2x (:x h2) + :c2y (:y h2)})) + +(defn opposite-handler + [point handler] + (let [phv (gpt/to-vec point handler) + opposite (gpt/add point (gpt/negate phv))] + opposite)) + +(defn extract-handlers [content] + (let [extract (fn [{param1 :params :as cmd1} {param2 :params :as cmd2}] + {:point (gpt/point (:x param1) (:y param1)) + :prev (when (:c2x param1) (gpt/point (:c2x param1) (:c2y param1))) + :next (when (:c1x param2) (gpt/point (:c1x param2) (:c1y param2)))})] + (map extract content (d/concat [] (rest content) [nil]))))