From d8ab3473bf1d495667d435bf4e60ebcf4ef5832e Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 12 Nov 2020 19:50:04 +0100 Subject: [PATCH] :sparkles: Calculate selrect for bezier curves --- common/app/common/geom/point.cljc | 5 +- common/app/common/geom/shapes.cljc | 5 + common/app/common/geom/shapes/common.cljc | 1 - common/app/common/geom/shapes/path.cljc | 123 +++++++++++++++++- common/app/common/geom/shapes/rect.cljc | 1 - common/app/common/geom/shapes/transforms.cljc | 113 ++++++++-------- common/app/common/math.cljc | 3 + .../app/main/data/workspace/drawing/path.cljs | 11 +- .../app/main/ui/workspace/shapes/path.cljs | 4 +- .../src/app/main/ui/workspace/viewport.cljs | 25 ++-- 10 files changed, 212 insertions(+), 79 deletions(-) diff --git a/common/app/common/geom/point.cljc b/common/app/common/geom/point.cljc index 65b453b56..32112f9e1 100644 --- a/common/app/common/geom/point.cljc +++ b/common/app/common/geom/point.cljc @@ -39,7 +39,10 @@ :else (throw (ex-info "Invalid arguments" {:v v})))) - ([x y] (Point. x y))) + ([x y] + ;;(assert (not (nil? x))) + ;;(assert (not (nil? y))) + (Point. x y))) (defn add "Returns the addition of the supplied value to both diff --git a/common/app/common/geom/shapes.cljc b/common/app/common/geom/shapes.cljc index 8cf607315..b45b7beed 100644 --- a/common/app/common/geom/shapes.cljc +++ b/common/app/common/geom/shapes.cljc @@ -16,6 +16,7 @@ [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.transforms :as gtr] [app.common.geom.shapes.rect :as gpr] + [app.common.geom.shapes.path :as gsp] [app.common.math :as mth] [app.common.data :as d])) @@ -299,3 +300,7 @@ (defn transform-matrix [shape] (gtr/transform-matrix shape)) (defn transform-point-center [point center transform] (gtr/transform-point-center point center transform)) (defn transform-rect [rect mtx] (gtr/transform-rect rect mtx)) + +;; PATHS +(defn content->points [content] (gsp/content->points content)) +(defn content->selrect [content] (gsp/content->selrect content)) diff --git a/common/app/common/geom/shapes/common.cljc b/common/app/common/geom/shapes/common.cljc index d4b64b481..164266372 100644 --- a/common/app/common/geom/shapes/common.cljc +++ b/common/app/common/geom/shapes/common.cljc @@ -13,7 +13,6 @@ [app.common.spec :as us] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] - [app.common.geom.shapes.path :as gpa] [app.common.math :as mth] [app.common.data :as d])) diff --git a/common/app/common/geom/shapes/path.cljc b/common/app/common/geom/shapes/path.cljc index 10b6fa0e4..ab77dfda6 100644 --- a/common/app/common/geom/shapes/path.cljc +++ b/common/app/common/geom/shapes/path.cljc @@ -13,6 +13,7 @@ [app.common.spec :as us] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.shapes.rect :as gpr] [app.common.math :as mth] [app.common.data :as d])) @@ -20,4 +21,124 @@ segments) (defn content->points [content] - (map #(gpt/point (-> % :param :x) (-> % :param :y)) content)) + (mapv #(gpt/point (-> % :params :x) (-> % :params :y)) content)) + +;; https://medium.com/@Acegikmo/the-ever-so-lovely-b%C3%A9zier-curve-eb27514da3bf +;; https://en.wikipedia.org/wiki/Bernstein_polynomial +(defn curve-values + "Parametric equation for cubic beziers. Given a start and end and + two intermediate points returns points for values of t. + If you draw t on a plane you got the bezier cube" + [start end h1 h2 t] + + (let [t2 (* t t) ;; t square + t3 (* t2 t) ;; t cube + + start-v (+ (- t3) (* 3 t2) (* -3 t) 1) + h1-v (+ (* 3 t3) (* -6 t2) (* 3 t)) + h2-v (+ (* -3 t3) (* 3 t2)) + end-v t3 + + coord-v (fn [coord] + (+ (* (coord start) start-v) + (* (coord h1) h1-v) + (* (coord h2) h2-v) + (* (coord end) end-v)))] + + (gpt/point (coord-v :x) (coord-v :y)))) + +;; https://pomax.github.io/bezierinfo/#extremities +(defn curve-extremities + "Given a cubic bezier cube finds its roots in t. This are the extremities + if we calculate its values for x, y we can find a bounding box for the curve." + [start end h1 h2] + + (let [coords [[(:x start) (:x h1) (:x h2) (:x end)] + [(:y start) (:y h1) (:y h2) (:y end)]] + + coord->tvalue + (fn [[c0 c1 c2 c3]] + + (let [a (+ (* -3 c0) (* 9 c1) (* -9 c2) (* 3 c3)) + b (+ (* 6 c0) (* -12 c1) (* 6 c2)) + c (+ (* 3 c1) (* -3 c0)) + + sqrt-b2-4ac (mth/sqrt (- (* b b) (* 4 a c)))] + + (cond + (and (mth/almost-zero? a) + (not (mth/almost-zero? b))) + ;; When the term a is close to zero we have a linear equation + [(/ (- c) b)] + + ;; If a is not close to zero return the two roots for a cuadratic + (not (mth/almost-zero? a)) + [(/ (+ (- b) sqrt-b2-4ac) + (* 2 a)) + (/ (- (- b) sqrt-b2-4ac) + (* 2 a))] + + ;; If a and b close to zero we can't find a root for a constant term + :else + [])))] + (->> coords + (mapcat coord->tvalue) + + ;; Only values in the range [0, 1] are valid + (filter #(and (>= % 0) (<= % 1))) + + ;; Pass t-values to actual points + (map #(curve-values start end h1 h2 %))) + )) + +(defn command->point + ([command] (command->point command nil)) + ([{params :params} coord] + (let [prefix (if coord (name coord) "") + xkey (keyword (str prefix "x")) + ykey (keyword (str prefix "y")) + x (get params xkey) + y (get params ykey)] + (gpt/point x y)))) + +(defn content->selrect [content] + (let [calc-extremities + (fn [command prev] + (case (:command command) + :move-to [(command->point command)] + + ;; If it's a line we add the beginning point and endpoint + :line-to [(command->point prev) + (command->point command)] + + ;; We return the bezier extremities + :curve-to (d/concat + [(command->point prev) + (command->point command)] + (curve-extremities (command->point prev) + (command->point command) + (command->point command :c1) + (command->point command :c2))))) + + extremities (mapcat calc-extremities + content + (d/concat [nil] content))] + + (gpr/points->selrect extremities))) + +(defn transform-content [content transform] + (let [set-tr (fn [params px py] + (let [tr-point (-> (gpt/point (get params px) (get params py)) + (gpt/transform transform))] + (assoc params + px (:x tr-point) + py (:y tr-point)))) + + transform-params + (fn [{:keys [x y c1x c1y c2x c2y] :as params}] + (cond-> params + (not (nil? x)) (set-tr :x :y) + (not (nil? c1x)) (set-tr :c1x :c1y) + (not (nil? c2x)) (set-tr :c2x :c2y)))] + + (mapv #(update % :params transform-params) content))) diff --git a/common/app/common/geom/shapes/rect.cljc b/common/app/common/geom/shapes/rect.cljc index e5204a5e9..89da516a6 100644 --- a/common/app/common/geom/shapes/rect.cljc +++ b/common/app/common/geom/shapes/rect.cljc @@ -13,7 +13,6 @@ [app.common.spec :as us] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] - [app.common.geom.shapes.path :as gpa] [app.common.geom.shapes.common :as gco] [app.common.math :as mth] [app.common.data :as d])) diff --git a/common/app/common/geom/shapes/transforms.cljc b/common/app/common/geom/shapes/transforms.cljc index 3253e0253..cd819cca5 100644 --- a/common/app/common/geom/shapes/transforms.cljc +++ b/common/app/common/geom/shapes/transforms.cljc @@ -89,39 +89,43 @@ :else scale)) -(defn modifiers->transform [current-transform center modifiers] - (let [ds-modifier (:displacement modifiers (gmt/matrix)) - {res-x :x res-y :y} (:resize-vector modifiers (gpt/point 1 1)) +(defn modifiers->transform + ([center modifiers] + (modifiers->transform (gmt/matrix) center modifiers)) - ;; Normalize x/y vector coordinates because scale by 0 is infinite - res-x (normalize-scale res-x) - res-y (normalize-scale res-y) - resize (gpt/point res-x res-y) + ([current-transform center modifiers] + (let [ds-modifier (:displacement modifiers (gmt/matrix)) + {res-x :x res-y :y} (:resize-vector modifiers (gpt/point 1 1)) - origin (:resize-origin modifiers (gpt/point 0 0)) + ;; Normalize x/y vector coordinates because scale by 0 is infinite + res-x (normalize-scale res-x) + res-y (normalize-scale res-y) + resize (gpt/point res-x res-y) - resize-transform (:resize-transform modifiers (gmt/matrix)) - resize-transform-inverse (:resize-transform-inverse modifiers (gmt/matrix)) - rt-modif (or (:rotation modifiers) 0) + origin (:resize-origin modifiers (gpt/point 0 0)) - transform (-> (gmt/matrix) + resize-transform (:resize-transform modifiers (gmt/matrix)) + resize-transform-inverse (:resize-transform-inverse modifiers (gmt/matrix)) + rt-modif (or (:rotation modifiers) 0) - ;; Applies the current resize transformation - (gmt/translate origin) - (gmt/multiply resize-transform) - (gmt/scale resize) - (gmt/multiply resize-transform-inverse) - (gmt/translate (gpt/negate origin)) + transform (-> (gmt/matrix) - ;; Applies the stacked transformations - (gmt/translate center) - (gmt/multiply (gmt/rotate-matrix rt-modif)) - #_(gmt/multiply current-transform) - (gmt/translate (gpt/negate center)) + ;; Applies the current resize transformation + (gmt/translate origin) + (gmt/multiply resize-transform) + (gmt/scale resize) + (gmt/multiply resize-transform-inverse) + (gmt/translate (gpt/negate origin)) - ;; Displacement - (gmt/multiply ds-modifier))] - transform)) + ;; Applies the stacked transformations + (gmt/translate center) + (gmt/multiply (gmt/rotate-matrix rt-modif)) + #_(gmt/multiply current-transform) + (gmt/translate (gpt/negate center)) + + ;; Displacement + (gmt/multiply ds-modifier))] + transform))) (defn- calculate-skew-angle "Calculates the skew angle of the paralelogram given by the points" @@ -210,28 +214,31 @@ [stretch-matrix stretch-matrix-inverse])) -(defn set-points-path - [shape points] - (let [shape (reduce (fn [acc [idx {:keys [x y]}]] - (-> acc - (assoc-in [:content idx :params :x] x) - (assoc-in [:content idx :params :y] y))) shape (d/enumerate points)) +(defn apply-transform-path + [shape transform] + (let [content (gpa/transform-content (:content shape) transform) + points (gpa/content->points content) + rotation (mod (+ (:rotation shape 0) + (or (get-in shape [:modifiers :rotation]) 0)) + 360) + selrect (gpa/content->selrect content)] + (assoc shape + :content content + :points points + :selrect selrect + :rotation rotation))) - shape (assoc shape - :points points - :selrect (gpr/points->selrect points))] - shape)) - -(defn set-points-curve - [shape points] +(defn apply-transform-curve + [shape transform] shape) -(defn set-points-rect +(defn apply-transform-rect "Given a new set of points transformed, set up the rectangle so it keeps its properties. We adjust de x,y,width,height and create a custom transform" - [shape points] + [shape transform] ;; - (let [center (gco/center-points points) + (let [points (-> shape :points (transform-points transform)) + center (gco/center-points points) ;; Reverse the current transformation stack to get the base rectangle tr-inverse (:transform-inverse shape (gmt/matrix)) @@ -259,18 +266,18 @@ (update $ :transform #(gmt/multiply (or % (gmt/matrix)) matrix)) (update $ :transform-inverse #(gmt/multiply matrix-inverse (or % (gmt/matrix)))) (assoc $ :points (into [] points)) - (assoc $ :selrect (gpr/rect->selrect rect-shape) #_(gpr/points->selrect points)) + (assoc $ :selrect (gpr/rect->selrect rect-shape)) (update $ :rotation #(mod (+ (or % 0) (or (get-in $ [:modifiers :rotation]) 0)) 360)))] new-shape)) -(defn set-points [shape points] - (let [set-points-fn +(defn apply-transform [shape transform] + (let [apply-transform-fn (case (:type shape) - :path set-points-path - :curve set-points-curve - set-points-rect)] - (set-points-fn shape points))) + :path apply-transform-path + :curve apply-transform-curve + apply-transform-rect)] + (apply-transform-fn shape transform))) (defn set-flip [shape modifiers] (cond-> shape @@ -279,13 +286,11 @@ (defn transform-shape [shape] (if (:modifiers shape) - (let [points (:points shape (shape->points shape)) - center (gco/center-points points) - transform (modifiers->transform (:transform shape (gmt/matrix)) center (:modifiers shape)) - tr-points (transform-points points transform)] + (let [center (gco/center-shape shape) + transform (modifiers->transform (:transform shape (gmt/matrix)) center (:modifiers shape))] (-> shape (set-flip (:modifiers shape)) - (set-points tr-points) + (apply-transform transform) (dissoc :modifiers))) shape)) diff --git a/common/app/common/math.cljc b/common/app/common/math.cljc index 92ba19a1a..f9baa3387 100644 --- a/common/app/common/math.cljc +++ b/common/app/common/math.cljc @@ -135,3 +135,6 @@ (if (< num from) from (if (> num to) to num))) + +(defn almost-zero? [num] + (< (abs num) 1e-8)) diff --git a/frontend/src/app/main/data/workspace/drawing/path.cljs b/frontend/src/app/main/data/workspace/drawing/path.cljs index c30441f72..0fb022ba5 100644 --- a/frontend/src/app/main/data/workspace/drawing/path.cljs +++ b/frontend/src/app/main/data/workspace/drawing/path.cljs @@ -50,14 +50,9 @@ (defn calculate-selrect [shape] - (let [points (->> shape - :content - (mapv #(gpt/point - (-> % :params :x) - (-> % :params :y))))] - (assoc shape - :points points - :selrect (gsh/points->selrect points)))) + (assoc shape + :points (gsh/content->points (:content shape)) + :selrect (gsh/content->selrect (:content shape)))) (defn init-path [] (ptk/reify ::init-path diff --git a/frontend/src/app/main/ui/workspace/shapes/path.cljs b/frontend/src/app/main/ui/workspace/shapes/path.cljs index 543d6ae44..b844ffbdb 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path.cljs @@ -24,7 +24,8 @@ [app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.workspace.shapes.common :as common] - [app.util.geom.path :as ugp])) + [app.util.geom.path :as ugp] + [app.common.geom.shapes.path :as gsp])) (mf/defc path-wrapper {::mf/wrap-props false} @@ -53,7 +54,6 @@ :on-double-click on-double-click :on-mouse-down on-mouse-down :on-context-menu on-context-menu} - [:& path/path-shape {:shape shape :background? true}]])) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 80619fa28..a2e7461ac 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -204,11 +204,14 @@ picking-color?]} local page-id (mf/use-ctx ctx/current-page-id) - selrect-orig (->> (mf/deref refs/selected-objects) - (gsh/selection-rect)) - selrect (-> selrect-orig - (assoc :modifiers (:modifiers local)) - (gsh/transform-shape)) + + selected-objects (mf/deref refs/selected-objects) + selrect-orig (->> selected-objects + (gsh/selection-rect)) + selrect (->> selected-objects + (map #(assoc % :modifiers (:modifiers local))) + (map gsh/transform-shape) + (gsh/selection-rect)) alt? (mf/use-state false) viewport-ref (mf/use-ref nil) @@ -266,18 +269,18 @@ on-pointer-down (mf/use-callback - (fn [event] + (fn [event] (let [target (dom/get-target event)] - ; Capture mouse pointer to detect the movements even if cursor - ; leaves the viewport or the browser itself - ; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture + ; Capture mouse pointer to detect the movements even if cursor + ; leaves the viewport or the browser itself + ; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture (.setPointerCapture target (.-pointerId event))))) on-pointer-up (mf/use-callback - (fn [event] + (fn [event] (let [target (dom/get-target event)] - ; Release pointer on mouse up + ; Release pointer on mouse up (.releasePointerCapture target (.-pointerId event))))) on-click