mirror of
https://github.com/penpot/penpot.git
synced 2025-05-31 02:06:10 +02:00
✨ Calculate selrect for bezier curves
This commit is contained in:
parent
f3cce1904c
commit
d8ab3473bf
10 changed files with 212 additions and 79 deletions
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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]))
|
||||
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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]))
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -135,3 +135,6 @@
|
|||
(if (< num from)
|
||||
from
|
||||
(if (> num to) to num)))
|
||||
|
||||
(defn almost-zero? [num]
|
||||
(< (abs num) 1e-8))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}]]))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue