Calculate selrect for bezier curves

This commit is contained in:
alonso.torres 2020-11-12 19:50:04 +01:00
parent f3cce1904c
commit d8ab3473bf
10 changed files with 212 additions and 79 deletions

View file

@ -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

View file

@ -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))

View file

@ -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]))

View file

@ -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)))

View file

@ -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]))

View file

@ -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))

View file

@ -135,3 +135,6 @@
(if (< num from)
from
(if (> num to) to num)))
(defn almost-zero? [num]
(< (abs num) 1e-8))

View file

@ -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

View file

@ -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}]]))

View file

@ -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