0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-13 02:28:18 -05:00

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"}
com.sun.mail/jakarta.mail {:mvn/version "2.0.1"}
org.la4j/la4j {:mvn/version "0.6.0"}
;; exception printing
fipp/fipp {:mvn/version "0.6.26"}

View file

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

View file

@ -168,7 +168,7 @@
(dm/export gtr/transform-str)
(dm/export gtr/inverse-transform-matrix)
(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-mask-selrect)
(dm/export gtr/update-bool-selrect)

View file

@ -5,21 +5,24 @@
;; Copyright (c) KALEIDOS INC
(ns app.common.geom.shapes.transforms
#?(:clj (:import (org.la4j Matrix LinearAlgebra))
:cljs (:import goog.math.Matrix))
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.bool :as gshb]
[app.common.geom.shapes.common :as gco]
[app.common.geom.shapes.path :as gpa]
[app.common.geom.shapes.rect :as gpr]
[app.common.math :as mth]
[app.common.pages.helpers :as cph]
[app.common.types.modifiers :as ctm]
[app.common.uuid :as uuid]))
(def ^:dynamic *skip-adjust* false)
#?(:clj (set! *warn-on-reflection* true))
;; --- Relative Movement
@ -76,21 +79,8 @@
dy (- (d/check-num y) (-> shape :selrect :y))]
(move shape (gpt/point dx dy))))
; ---- 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
"Calculates the height of a parallelogram given by the points"
[[p1 _ _ p4]]
@ -104,31 +94,6 @@
(-> (gpt/to-vec p1 p2)
(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
(defn transform-matrix
@ -147,9 +112,12 @@
(cond-> (some? transform)
(gmt/multiply transform))
(cond->
(and (not no-flip) flip-x) (gmt/scale (gpt/point -1 1))
(and (not no-flip) flip-y) (gmt/scale (gpt/point 1 -1)))
(cond-> (and flip-x (not no-flip))
(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)))))
(defn transform-str
@ -186,74 +154,92 @@
(gco/transform-points matrix))]
(gpr/points->rect points)))
(defn calculate-adjust-matrix
"Calculates a matrix that is a series of transformations we have to do to the transformed rectangle so that
after applying them the end result is the `shape-path-temp`.
This is compose of three transformations: skew, resize and rotation"
[points-temp points-rec flip-x flip-y]
(let [center (gco/center-bounds points-temp)
(defn transform-points-matrix
"Calculate the transform matrix to convert from the selrect to the points bounds
TargetM = SourceM * Transform ==> Transform = TargetM * inv(SourceM)"
[{:keys [x1 y1 x2 y2]} [d1 d2 _ d4]]
#?(:clj
;; 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
;; skew-angle (if (neg? (* (:x resize-vector) (:y resize-vector))) (- skew-angle) skew-angle )
skew-angle (if (and (or flip-x flip-y)
(not (and flip-x flip-y))) (- skew-angle) skew-angle )
skew-angle (if (mth/nan? skew-angle) 0 skew-angle)
transform-jvm
(.. target-points-matrix
(multiply source-points-matrix-inv))]
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))
h2 (max 1 (calculate-height (gco/transform-points points-rec center stretch-matrix)))
h3 (if-not (mth/almost-zero? h2) (/ h1 h2) 1)
h3 (if (mth/nan? h3) 1 h3)
:cljs
(let [target-points-matrix
(Matrix. #js [#js [(:x d1) (:x d2) (:x d4)]
#js [(:y d1) (:y d2) (:y d4)]
#js [ 1 1 1]])
w1 (max 1 (calculate-width points-temp))
w2 (max 1 (calculate-width (gco/transform-points points-rec center stretch-matrix)))
w3 (if-not (mth/almost-zero? w2) (/ w1 w2) 1)
w3 (if (mth/nan? w3) 1 w3)
source-points-matrix
(Matrix. #js [#js [x1 x2 x1]
#js [y1 y1 y2]
#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
center
(gco/transform-points points-rec (gco/center-points points-rec) stretch-matrix)
points-temp
flip-x
flip-y)
;; TargetM = SourceM * Transform ==> Transform = TargetM * inv(SourceM)
transform-js
(when source-points-matrix-inv
(.multiply target-points-matrix source-points-matrix-inv))]
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
stretch-matrix-inverse
(gmt/multiply (gmt/scale-matrix (gpt/point (/ 1 w3) (/ 1 h3)))
(gmt/skew-matrix (- skew-angle) 0)
(gmt/rotate-matrix (- rotation-angle)))]
[stretch-matrix stretch-matrix-inverse rotation-angle]))
(defn calculate-geometry
[points]
(let [width (calculate-width points)
height (calculate-height points)
center (gco/center-points points)
sr (gpr/center->selrect center width height)
(defn- adjust-rotated-transform
[{:keys [transform transform-inverse flip-x flip-y]} points]
(let [center (gco/center-bounds points)
points-transform-mtx (transform-points-matrix sr points)
points-temp (cond-> points
(some? transform-inverse)
(gco/transform-points center transform-inverse))
points-temp-dim (calculate-dimensions points-temp)
;; Calculate the transform by move the transformation to the center
transform
(when points-transform-mtx
(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
;; 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)
transform-inverse (when transform (gmt/inverse transform))]
rect-points (gpr/rect->points selrect)
[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)]))
[sr transform transform-inverse]))
(defn- adjust-shape-flips
"After some tranformations the flip-x/flip-y flags can change we need
@ -315,33 +301,36 @@
bool? (= (:type shape) :bool)
path? (= (:type shape) :path)
[selrect transform transform-inverse]
(adjust-rotated-transform shape points)
[selrect transform transform-inverse] (calculate-geometry points)
base-rotation (or (:rotation shape) 0)
modif-rotation (or (get-in shape [:modifiers :rotation]) 0)
rotation (mod (+ base-rotation modif-rotation) 360)]
(-> shape
(cond-> bool?
(update :bool-content gpa/transform-content transform-mtx))
(cond-> path?
(update :content gpa/transform-content transform-mtx))
(cond-> (not path?)
(assoc :x (:x selrect)
:y (:y selrect)
:width (:width selrect)
:height (:height selrect)))
(cond-> transform
(-> (assoc :transform transform)
(assoc :transform-inverse transform-inverse)))
(cond-> (not transform)
(dissoc :transform :transform-inverse))
(cond-> (some? selrect)
(assoc :selrect selrect))
(cond-> (d/not-empty? points)
(assoc :points points))
(assoc :rotation rotation))))
(if-not (and transform transform-inverse)
;; When we cannot calculate the transformation we leave the shape as it was
shape
(-> shape
(cond-> bool?
(update :bool-content gpa/transform-content transform-mtx))
(cond-> path?
(update :content gpa/transform-content transform-mtx))
(cond-> (not path?)
(assoc :x (:x selrect)
:y (:y selrect)
:width (:width selrect)
:height (:height selrect)))
(cond-> transform
(-> (assoc :transform transform)
(assoc :transform-inverse transform-inverse)))
(cond-> (not transform)
(dissoc :transform :transform-inverse))
(cond-> (some? selrect)
(assoc :selrect selrect))
(cond-> (d/not-empty? points)
(assoc :points points))
(assoc :rotation rotation)))))
(defn- apply-transform
"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.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.transforms :as gsht]
[app.common.math :as mth :refer [close?]]
[app.common.types.modifiers :as ctm]
[app.common.types.shape :as cts]
@ -128,13 +129,10 @@
(let [modifiers (ctm/resize-modifiers (gpt/point 0 0) (gpt/point 0 0))
shape-before (create-test-shape type {:modifiers modifiers})
shape-after (gsh/transform-shape shape-before)]
(t/is (> (get-in shape-before [:selrect :width])
(get-in shape-after [:selrect :width])))
(t/is (> (get-in shape-after [:selrect :width]) 0))
(t/is (> (get-in shape-before [:selrect :height])
(get-in shape-after [:selrect :height])))
(t/is (> (get-in shape-after [:selrect :height]) 0)))
(t/is (close? (get-in shape-before [:selrect :width])
(get-in shape-after [:selrect :width])))
(t/is (close? (get-in shape-before [:selrect :height])
(get-in shape-after [:selrect :height]))))
:rect :path))
(t/testing "Transform shape with rotation modifiers"
@ -195,6 +193,50 @@
(t/is (= (:x expect) (:x result)))
(t/is (= (:y expect) (:y 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)
(gsh/transform-points transform))
center (gsh/center-points 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)
[selrect transform transform-inverse] (gsh/calculate-geometry points)]
[shape-transform shape-transform-inv rotation]
(gsh/calculate-adjust-matrix points rect-points (neg? (:a transform)) (neg? (:d transform)))]
(merge rect-shape
{:selrect selrect
:points points
:rotation rotation
:transform shape-transform
:transform-inverse shape-transform-inv})))
{:x (:x selrect)
:y (:y selrect)
:width (:width selrect)
:height (:height selrect)
:selrect selrect
:points points
:transform transform
:transform-inverse transform-inverse}))
(defn create-rect-shape [name frame-id svg-data {:keys [attrs] :as data}]