Merge pull request #2653 from penpot/alotor-poc-improve-transform

♻️ Changed transform calculation
This commit is contained in:
Andrey Antukh 2022-12-13 12:37:18 +01:00 committed by GitHub
commit fe7b4331d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 179 additions and 150 deletions

View file

@ -33,6 +33,7 @@
funcool/datoteka {:mvn/version "3.0.66"} funcool/datoteka {:mvn/version "3.0.66"}
com.sun.mail/jakarta.mail {:mvn/version "2.0.1"} com.sun.mail/jakarta.mail {:mvn/version "2.0.1"}
org.la4j/la4j {:mvn/version "0.6.0"}
;; exception printing ;; exception printing
fipp/fipp {:mvn/version "0.6.26"} fipp/fipp {:mvn/version "0.6.26"}

View file

@ -288,14 +288,15 @@
(defn inverse (defn inverse
"Gets the inverse of the affinity transform `mtx`" "Gets the inverse of the affinity transform `mtx`"
[{:keys [a b c d e f] :as mtx}] [{:keys [a b c d e f] :as mtx}]
(let [det (determinant mtx) (let [det (determinant mtx)]
a' (/ d det) (when-not (mth/almost-zero? det)
(let [a' (/ d det)
b' (/ (- b) det) b' (/ (- b) det)
c' (/ (- c) det) c' (/ (- c) det)
d' (/ a det) d' (/ a det)
e' (/ (- (* c f) (* d e)) det) e' (/ (- (* c f) (* d e)) det)
f' (/ (- (* b e) (* a f)) det)] f' (/ (- (* b e) (* a f)) det)]
(Matrix. a' b' c' d' e' f'))) (Matrix. a' b' c' d' e' f')))))
(defn round (defn round
[mtx] [mtx]

View file

@ -168,7 +168,7 @@
(dm/export gtr/transform-str) (dm/export gtr/transform-str)
(dm/export gtr/inverse-transform-matrix) (dm/export gtr/inverse-transform-matrix)
(dm/export gtr/transform-rect) (dm/export gtr/transform-rect)
(dm/export gtr/calculate-adjust-matrix) (dm/export gtr/calculate-geometry)
(dm/export gtr/update-group-selrect) (dm/export gtr/update-group-selrect)
(dm/export gtr/update-mask-selrect) (dm/export gtr/update-mask-selrect)
(dm/export gtr/update-bool-selrect) (dm/export gtr/update-bool-selrect)

View file

@ -5,21 +5,24 @@
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.common.geom.shapes.transforms (ns app.common.geom.shapes.transforms
#?(:clj (:import (org.la4j Matrix LinearAlgebra))
:cljs (:import goog.math.Matrix))
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.geom.matrix :as gmt] [app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes.bool :as gshb] [app.common.geom.shapes.bool :as gshb]
[app.common.geom.shapes.common :as gco] [app.common.geom.shapes.common :as gco]
[app.common.geom.shapes.path :as gpa] [app.common.geom.shapes.path :as gpa]
[app.common.geom.shapes.rect :as gpr] [app.common.geom.shapes.rect :as gpr]
[app.common.math :as mth]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.common.types.modifiers :as ctm] [app.common.types.modifiers :as ctm]
[app.common.uuid :as uuid])) [app.common.uuid :as uuid]))
(def ^:dynamic *skip-adjust* false) #?(:clj (set! *warn-on-reflection* true))
;; --- Relative Movement ;; --- Relative Movement
@ -76,21 +79,8 @@
dy (- (d/check-num y) (-> shape :selrect :y))] dy (- (d/check-num y) (-> shape :selrect :y))]
(move shape (gpt/point dx dy)))) (move shape (gpt/point dx dy))))
; ---- Geometric operations ; ---- Geometric operations
(defn- calculate-skew-angle
"Calculates the skew angle of the parallelogram given by the points"
[[p1 _ p3 p4]]
(let [v1 (gpt/to-vec p3 p4)
v2 (gpt/to-vec p4 p1)]
;; If one of the vectors is zero it's a rectangle with 0 height or width
;; We don't skew these
(if (or (gpt/almost-zero? v1)
(gpt/almost-zero? v2))
0
(- 90 (gpt/angle-with-other v1 v2)))))
(defn- calculate-height (defn- calculate-height
"Calculates the height of a parallelogram given by the points" "Calculates the height of a parallelogram given by the points"
[[p1 _ _ p4]] [[p1 _ _ p4]]
@ -104,31 +94,6 @@
(-> (gpt/to-vec p1 p2) (-> (gpt/to-vec p1 p2)
(gpt/length))) (gpt/length)))
(defn- calculate-rotation
"Calculates the rotation between two shapes given the resize vector direction"
[center points-shape1 points-shape2 flip-x flip-y]
(let [idx-1 0
idx-2 (cond (and flip-x (not flip-y)) 1
(and flip-x flip-y) 2
(and (not flip-x) flip-y) 3
:else 0)
p1 (nth points-shape1 idx-1)
p2 (nth points-shape2 idx-2)
v1 (gpt/to-vec center p1)
v2 (gpt/to-vec center p2)
rot-angle (gpt/angle-with-other v1 v2)
rot-sign (gpt/angle-sign v1 v2)]
(* rot-sign rot-angle)))
(defn- calculate-dimensions
[[p1 p2 p3 _]]
(let [width (gpt/distance p1 p2)
height (gpt/distance p2 p3)]
{:width width :height height}))
;; --- Transformation matrix operations ;; --- Transformation matrix operations
(defn transform-matrix (defn transform-matrix
@ -147,9 +112,12 @@
(cond-> (some? transform) (cond-> (some? transform)
(gmt/multiply transform)) (gmt/multiply transform))
(cond-> (cond-> (and flip-x (not no-flip))
(and (not no-flip) flip-x) (gmt/scale (gpt/point -1 1)) (gmt/scale (gpt/point -1 1)))
(and (not no-flip) flip-y) (gmt/scale (gpt/point 1 -1)))
(cond-> (and flip-y (not no-flip))
(gmt/scale (gpt/point 1 -1)))
(gmt/translate (gpt/negate shape-center))))) (gmt/translate (gpt/negate shape-center)))))
(defn transform-str (defn transform-str
@ -186,74 +154,92 @@
(gco/transform-points matrix))] (gco/transform-points matrix))]
(gpr/points->rect points))) (gpr/points->rect points)))
(defn calculate-adjust-matrix (defn transform-points-matrix
"Calculates a matrix that is a series of transformations we have to do to the transformed rectangle so that "Calculate the transform matrix to convert from the selrect to the points bounds
after applying them the end result is the `shape-path-temp`. TargetM = SourceM * Transform ==> Transform = TargetM * inv(SourceM)"
This is compose of three transformations: skew, resize and rotation" [{:keys [x1 y1 x2 y2]} [d1 d2 _ d4]]
[points-temp points-rec flip-x flip-y] #?(:clj
(let [center (gco/center-bounds points-temp) ;; NOTE: the source matrix may not be invertible we can't
;; calculate the transform, so on exception we return `nil`
(ex/ignoring
(let [target-points-matrix
(->> (list (:x d1) (:x d2) (:x d4)
(:y d1) (:y d2) (:y d4)
1 1 1 )
(into-array Double/TYPE)
(Matrix/from1DArray 3 3))
stretch-matrix (gmt/matrix) source-points-matrix
(->> (list x1 x2 x1
y1 y1 y2
1 1 1)
(into-array Double/TYPE)
(Matrix/from1DArray 3 3))
skew-angle (calculate-skew-angle points-temp) ;; May throw an exception if the matrix is not invertible
source-points-matrix-inv
(.. source-points-matrix
(withInverter LinearAlgebra/GAUSS_JORDAN)
(inverse))
;; When one of the axis is flipped we have to reverse the skew transform-jvm
;; skew-angle (if (neg? (* (:x resize-vector) (:y resize-vector))) (- skew-angle) skew-angle ) (.. target-points-matrix
skew-angle (if (and (or flip-x flip-y) (multiply source-points-matrix-inv))]
(not (and flip-x flip-y))) (- skew-angle) skew-angle )
skew-angle (if (mth/nan? skew-angle) 0 skew-angle)
stretch-matrix (gmt/multiply stretch-matrix (gmt/skew-matrix skew-angle 0)) (gmt/matrix (.get transform-jvm 0 0)
(.get transform-jvm 1 0)
(.get transform-jvm 0 1)
(.get transform-jvm 1 1)
(.get transform-jvm 0 2)
(.get transform-jvm 1 2))))
h1 (max 1 (calculate-height points-temp)) :cljs
h2 (max 1 (calculate-height (gco/transform-points points-rec center stretch-matrix))) (let [target-points-matrix
h3 (if-not (mth/almost-zero? h2) (/ h1 h2) 1) (Matrix. #js [#js [(:x d1) (:x d2) (:x d4)]
h3 (if (mth/nan? h3) 1 h3) #js [(:y d1) (:y d2) (:y d4)]
#js [ 1 1 1]])
w1 (max 1 (calculate-width points-temp)) source-points-matrix
w2 (max 1 (calculate-width (gco/transform-points points-rec center stretch-matrix))) (Matrix. #js [#js [x1 x2 x1]
w3 (if-not (mth/almost-zero? w2) (/ w1 w2) 1) #js [y1 y1 y2]
w3 (if (mth/nan? w3) 1 w3) #js [ 1 1 1]])
stretch-matrix (gmt/multiply stretch-matrix (gmt/scale-matrix (gpt/point w3 h3))) ;; returns nil if not invertible
source-points-matrix-inv (.getInverse source-points-matrix)
rotation-angle (calculate-rotation ;; TargetM = SourceM * Transform ==> Transform = TargetM * inv(SourceM)
center transform-js
(gco/transform-points points-rec (gco/center-points points-rec) stretch-matrix) (when source-points-matrix-inv
points-temp (.multiply target-points-matrix source-points-matrix-inv))]
flip-x
flip-y)
stretch-matrix (gmt/multiply (gmt/rotate-matrix rotation-angle) stretch-matrix) (when transform-js
(gmt/matrix (.getValueAt transform-js 0 0)
(.getValueAt transform-js 1 0)
(.getValueAt transform-js 0 1)
(.getValueAt transform-js 1 1)
(.getValueAt transform-js 0 2)
(.getValueAt transform-js 1 2))))))
;; This is the inverse to be able to remove the transformation (defn calculate-geometry
stretch-matrix-inverse [points]
(gmt/multiply (gmt/scale-matrix (gpt/point (/ 1 w3) (/ 1 h3))) (let [width (calculate-width points)
(gmt/skew-matrix (- skew-angle) 0) height (calculate-height points)
(gmt/rotate-matrix (- rotation-angle)))] center (gco/center-points points)
[stretch-matrix stretch-matrix-inverse rotation-angle])) sr (gpr/center->selrect center width height)
(defn- adjust-rotated-transform points-transform-mtx (transform-points-matrix sr points)
[{:keys [transform transform-inverse flip-x flip-y]} points]
(let [center (gco/center-bounds points)
points-temp (cond-> points ;; Calculate the transform by move the transformation to the center
(some? transform-inverse) transform
(gco/transform-points center transform-inverse)) (when points-transform-mtx
points-temp-dim (calculate-dimensions points-temp) (gmt/multiply
(gmt/translate-matrix (gpt/negate center))
points-transform-mtx
(gmt/translate-matrix center)))
;; This rectangle is the new data for the current rectangle. We want to change our rectangle transform-inverse (when transform (gmt/inverse transform))]
;; to have this width, height, x, y
new-width (max 0.01 (:width points-temp-dim))
new-height (max 0.01 (:height points-temp-dim))
selrect (gpr/center->selrect center new-width new-height)
rect-points (gpr/rect->points selrect) [sr transform transform-inverse]))
[matrix matrix-inverse] (calculate-adjust-matrix points-temp rect-points flip-x flip-y)]
[selrect
(if transform (gmt/multiply transform matrix) matrix)
(if transform-inverse (gmt/multiply matrix-inverse transform-inverse) matrix-inverse)]))
(defn- adjust-shape-flips (defn- adjust-shape-flips
"After some tranformations the flip-x/flip-y flags can change we need "After some tranformations the flip-x/flip-y flags can change we need
@ -315,12 +301,15 @@
bool? (= (:type shape) :bool) bool? (= (:type shape) :bool)
path? (= (:type shape) :path) path? (= (:type shape) :path)
[selrect transform transform-inverse] [selrect transform transform-inverse] (calculate-geometry points)
(adjust-rotated-transform shape points)
base-rotation (or (:rotation shape) 0) base-rotation (or (:rotation shape) 0)
modif-rotation (or (get-in shape [:modifiers :rotation]) 0) modif-rotation (or (get-in shape [:modifiers :rotation]) 0)
rotation (mod (+ base-rotation modif-rotation) 360)] rotation (mod (+ base-rotation modif-rotation) 360)]
(if-not (and transform transform-inverse)
;; When we cannot calculate the transformation we leave the shape as it was
shape
(-> shape (-> shape
(cond-> bool? (cond-> bool?
(update :bool-content gpa/transform-content transform-mtx)) (update :bool-content gpa/transform-content transform-mtx))
@ -341,7 +330,7 @@
(cond-> (d/not-empty? points) (cond-> (d/not-empty? points)
(assoc :points points)) (assoc :points points))
(assoc :rotation rotation)))) (assoc :rotation rotation)))))
(defn- apply-transform (defn- apply-transform
"Given a new set of points transformed, set up the rectangle so it keeps "Given a new set of points transformed, set up the rectangle so it keeps

View file

@ -9,6 +9,7 @@
[app.common.geom.matrix :as gmt] [app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.geom.shapes.transforms :as gsht]
[app.common.math :as mth :refer [close?]] [app.common.math :as mth :refer [close?]]
[app.common.types.modifiers :as ctm] [app.common.types.modifiers :as ctm]
[app.common.types.shape :as cts] [app.common.types.shape :as cts]
@ -128,13 +129,10 @@
(let [modifiers (ctm/resize-modifiers (gpt/point 0 0) (gpt/point 0 0)) (let [modifiers (ctm/resize-modifiers (gpt/point 0 0) (gpt/point 0 0))
shape-before (create-test-shape type {:modifiers modifiers}) shape-before (create-test-shape type {:modifiers modifiers})
shape-after (gsh/transform-shape shape-before)] shape-after (gsh/transform-shape shape-before)]
(t/is (> (get-in shape-before [:selrect :width]) (t/is (close? (get-in shape-before [:selrect :width])
(get-in shape-after [:selrect :width]))) (get-in shape-after [:selrect :width])))
(t/is (> (get-in shape-after [:selrect :width]) 0)) (t/is (close? (get-in shape-before [:selrect :height])
(get-in shape-after [:selrect :height]))))
(t/is (> (get-in shape-before [:selrect :height])
(get-in shape-after [:selrect :height])))
(t/is (> (get-in shape-after [:selrect :height]) 0)))
:rect :path)) :rect :path))
(t/testing "Transform shape with rotation modifiers" (t/testing "Transform shape with rotation modifiers"
@ -195,6 +193,50 @@
(t/is (= (:x expect) (:x result))) (t/is (= (:x expect) (:x result)))
(t/is (= (:y expect) (:y result))) (t/is (= (:y expect) (:y result)))
(t/is (= (:width expect) (:width result))) (t/is (= (:width expect) (:width result)))
(t/is (= (:height expect) (:height result))) (t/is (= (:height expect) (:height result)))))
))
(def g45 (mth/radians 45))
(t/deftest points-transform-matrix
(t/testing "Transform matrix"
(t/are [selrect points expected]
(let [result (gsht/transform-points-matrix selrect points)]
(t/is (gmt/close? expected result)))
;; No transformation
(gsh/make-selrect 0 0 10 10)
(-> (gsh/make-selrect 0 0 10 10)
(gsh/rect->points))
(gmt/matrix)
;; Displacement
(gsh/make-selrect 0 0 10 10)
(-> (gsh/make-selrect 20 20 10 10)
(gsh/rect->points ))
(gmt/matrix 1 0 0 1 20 20)
;; Resize
(gsh/make-selrect 0 0 10 10)
(-> (gsh/make-selrect 0 0 20 40)
(gsh/rect->points))
(gmt/matrix 2 0 0 4 0 0)
;; Displacement+Resize
(gsh/make-selrect 0 0 10 10)
(-> (gsh/make-selrect 10 10 20 40)
(gsh/rect->points))
(gmt/matrix 2 0 0 4 10 10)
;; Rotation
(gsh/make-selrect 0 0 10 10)
(-> (gsh/make-selrect 0 0 10 10)
(gsh/rect->points)
(gsh/transform-points (gmt/rotate-matrix 45)))
(gmt/matrix (mth/cos g45) (mth/sin g45) (- (mth/sin g45)) (mth/cos g45) 0 0)
;; Rotation + Resize
(gsh/make-selrect 0 0 10 10)
(-> (gsh/make-selrect 0 0 20 40)
(gsh/rect->points)
(gsh/transform-points (gmt/rotate-matrix 45)))
(gmt/matrix (* (mth/cos g45) 2) (* (mth/sin g45) 2) (* (- (mth/sin g45)) 4) (* (mth/cos g45) 4) 0 0))))

View file

@ -249,20 +249,16 @@
(let [points (-> (gsh/rect->points rect-data) (let [points (-> (gsh/rect->points rect-data)
(gsh/transform-points transform)) (gsh/transform-points transform))
center (gsh/center-points points) [selrect transform transform-inverse] (gsh/calculate-geometry points)]
rect-shape (gsh/center->rect center (:width rect-data) (:height rect-data))
selrect (gsh/rect->selrect rect-shape)
rect-points (gsh/rect->points rect-shape)
[shape-transform shape-transform-inv rotation] {:x (:x selrect)
(gsh/calculate-adjust-matrix points rect-points (neg? (:a transform)) (neg? (:d transform)))] :y (:y selrect)
:width (:width selrect)
(merge rect-shape :height (:height selrect)
{:selrect selrect :selrect selrect
:points points :points points
:rotation rotation :transform transform
:transform shape-transform :transform-inverse transform-inverse}))
:transform-inverse shape-transform-inv})))
(defn create-rect-shape [name frame-id svg-data {:keys [attrs] :as data}] (defn create-rect-shape [name frame-id svg-data {:keys [attrs] :as data}]