Add huge optimization to the transform-points-matrix

it reduces the 90% overhead of this function; in an relative
comparison the same execution is reduced from 350ms to 18ms
This commit is contained in:
Andrey Antukh 2023-06-20 11:43:07 +02:00
parent 4d3064ba6d
commit a15a2010b6

View file

@ -5,10 +5,7 @@
;; 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
#?(:clj [app.common.exceptions :as ex])
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.geom.matrix :as gmt] [app.common.geom.matrix :as gmt]
@ -19,8 +16,8 @@
[app.common.geom.shapes.path :as gpa] [app.common.geom.shapes.path :as gpa]
[app.common.math :as mth] [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.record :as cr]
[app.common.uuid :as uuid])) [app.common.types.modifiers :as ctm]))
#?(:clj (set! *warn-on-reflection* true)) #?(:clj (set! *warn-on-reflection* true))
@ -40,10 +37,6 @@
y (dm/get-prop selrect :y) y (dm/get-prop selrect :y)
w (dm/get-prop selrect :width) w (dm/get-prop selrect :width)
h (dm/get-prop selrect :height) h (dm/get-prop selrect :height)
x1 (dm/get-prop selrect :x1)
y1 (dm/get-prop selrect :y1)
x2 (dm/get-prop selrect :x2)
y2 (dm/get-prop selrect :y2)
dx (dm/get-prop pt :x) dx (dm/get-prop pt :x)
dy (dm/get-prop pt :y)] dy (dm/get-prop pt :y)]
@ -60,19 +53,32 @@
(mapv #(gpt/add % move-vec) points) (mapv #(gpt/add % move-vec) points)
points)) points))
;; FIXME: revisit performance ;; FIXME: deprecated
(defn move-position-data (defn move-position-data
([position-data {:keys [x y]}] [position-data delta]
(move-position-data position-data x y))
([position-data dx dy]
(when (some? position-data) (when (some? position-data)
(cond->> position-data (let [dx (dm/get-prop delta :x)
(d/num? dx dy) dy (dm/get-prop delta :y)]
(if (d/num? dx dy)
(mapv #(-> % (mapv #(-> %
(update :x + dx) (update :x + dx)
(update :y + dy))))))) (update :y + dy))
position-data)
position-data))))
(defn transform-position-data
[position-data transform]
(when (some? position-data)
(let [dx (dm/get-prop transform :e)
dy (dm/get-prop transform :f)]
(if (d/num? dx dy)
(mapv #(-> %
(update :x + dx)
(update :y + dy))
position-data)
position-data))))
;; FIXME: revist usage of mutability
(defn move (defn move
"Move the shape relatively to its current "Move the shape relatively to its current
position applying the provided delta." position applying the provided delta."
@ -89,7 +95,7 @@
(update :points move-points mvec) (update :points move-points mvec)
(d/update-when :x d/safe+ dx) (d/update-when :x d/safe+ dx)
(d/update-when :y d/safe+ dy) (d/update-when :y d/safe+ dy)
(d/update-when :position-data move-position-data dx dy) (d/update-when :position-data move-position-data mvec)
(cond-> (= :bool type) (update :bool-content gpa/move-content mvec)) (cond-> (= :bool type) (update :bool-content gpa/move-content mvec))
(cond-> (= :path type) (update :content gpa/move-content mvec))))) (cond-> (= :path type) (update :content gpa/move-content mvec)))))
@ -100,28 +106,13 @@
[shape pos] [shape pos]
(let [x (dm/get-prop pos :x) (let [x (dm/get-prop pos :x)
y (dm/get-prop pos :y) y (dm/get-prop pos :y)
sr (dm/get-prop shape selrect) sr (dm/get-prop shape :selrect)
px (dm/get-prop sr :x) px (dm/get-prop sr :x)
py (dm/get-prop sr :y) py (dm/get-prop sr :y)
dx (- (d/check-num x) px) dx (- (d/check-num x) px)
dy (- (d/check-num y) py)] dy (- (d/check-num y) py)]
(move shape (gpt/point dx dy)))) (move shape (gpt/point dx dy))))
; ---- Geometric operations
(defn- calculate-height
"Calculates the height of a parallelogram given by the points"
[[p1 _ _ p4]]
(-> (gpt/to-vec p4 p1)
(gpt/length)))
(defn- calculate-width
"Calculates the width of a parallelogram given by the points"
[[p1 p2 _ _]]
(-> (gpt/to-vec p1 p2)
(gpt/length)))
;; --- Transformation matrix operations ;; --- Transformation matrix operations
(defn transform-matrix (defn transform-matrix
@ -160,6 +151,7 @@
(dm/str (transform-matrix shape params)) (dm/str (transform-matrix shape params))
""))) "")))
;; FIXME: performance
(defn inverse-transform-matrix (defn inverse-transform-matrix
([shape] ([shape]
(let [shape-center (or (gco/shape->center shape) (let [shape-center (or (gco/shape->center shape)
@ -174,6 +166,7 @@
(gmt/multiply (:transform-inverse shape (gmt/matrix))) (gmt/multiply (:transform-inverse shape (gmt/matrix)))
(gmt/translate (gpt/negate center))))) (gmt/translate (gpt/negate center)))))
;; FIXME: move to geom rect?
(defn transform-rect (defn transform-rect
"Transform a rectangles and changes its attributes" "Transform a rectangles and changes its attributes"
[rect matrix] [rect matrix]
@ -183,137 +176,127 @@
(grc/points->rect points))) (grc/points->rect points)))
(defn transform-points-matrix (defn transform-points-matrix
"Calculate the transform matrix to convert from the selrect to the points bounds [selrect [d1 d2 _ d4]]
TargetM = SourceM * Transform ==> Transform = TargetM * inv(SourceM)"
[{:keys [x1 y1 x2 y2]} [d1 d2 _ d4]]
;; If the coordinates are very close to zero (but not zero) the rounding can mess with the ;; If the coordinates are very close to zero (but not zero) the rounding can mess with the
;; transforms. So we round to zero the values ;; transforms. So we round to zero the values
(let [x1 (mth/round-to-zero x1) (let [x1 (mth/round-to-zero (dm/get-prop selrect :x1))
y1 (mth/round-to-zero y1) y1 (mth/round-to-zero (dm/get-prop selrect :y1))
x2 (mth/round-to-zero x2) x2 (mth/round-to-zero (dm/get-prop selrect :x2))
y2 (mth/round-to-zero y2) y2 (mth/round-to-zero (dm/get-prop selrect :y2))
d1x (mth/round-to-zero (:x d1))
d1y (mth/round-to-zero (:y d1))
d2x (mth/round-to-zero (:x d2))
d2y (mth/round-to-zero (:y d2))
d4x (mth/round-to-zero (:x d4))
d4y (mth/round-to-zero (:y 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 d1x d2x d4x
d1y d2y d4y
1 1 1)
(into-array Double/TYPE)
(Matrix/from1DArray 3 3))
source-points-matrix det (+ (- (* (- y1 y2) x1)
(->> (list x1 x2 x1 (* (- y1 y2) x2))
y1 y1 y2 (* (- y1 y1) x1))]
1 1 1)
(into-array Double/TYPE)
(Matrix/from1DArray 3 3))
;; May throw an exception if the matrix is not invertible (when-not (zero? det)
source-points-matrix-inv (let [ma0 (mth/round-to-zero (dm/get-prop d1 :x))
(.. source-points-matrix ma1 (mth/round-to-zero (dm/get-prop d2 :x))
(withInverter LinearAlgebra/GAUSS_JORDAN) ma2 (mth/round-to-zero (dm/get-prop d4 :x))
(inverse)) ma3 (mth/round-to-zero (dm/get-prop d1 :y))
ma4 (mth/round-to-zero (dm/get-prop d2 :y))
ma5 (mth/round-to-zero (dm/get-prop d4 :y))
transform-jvm mb0 (/ (- y1 y2) det)
(.. target-points-matrix mb1 (/ (- x1 x2) det)
(multiply source-points-matrix-inv))] mb2 (/ (- (* x2 y2) (* x1 y1)) det)
mb3 (/ (- y2 y1) det)
mb4 (/ (- x1 x1) det)
mb5 (/ (- (* x1 y1) (* x1 y2)) det)
mb6 (/ (- y1 y1) det)
mb7 (/ (- x2 x1) det)
mb8 (/ (- (* x1 y1) (* x2 y1)) det)]
(gmt/matrix (.get transform-jvm 0 0) (gmt/matrix (+ (* ma0 mb0)
(.get transform-jvm 1 0) (* ma1 mb3)
(.get transform-jvm 0 1) (* ma2 mb6))
(.get transform-jvm 1 1) (+ (* ma3 mb0)
(.get transform-jvm 0 2) (* ma4 mb3)
(.get transform-jvm 1 2)))) (* ma5 mb6))
(+ (* ma0 mb1)
(* ma1 mb4)
(* ma2 mb7))
(+ (* ma3 mb1)
(* ma4 mb4)
(* ma5 mb7))
(+ (* ma0 mb2)
(* ma1 mb5)
(* ma2 mb8))
(+ (* ma3 mb2)
(* ma4 mb5)
(* ma5 mb8)))))))
:cljs (defn calculate-selrect
(let [target-points-matrix [points center]
(Matrix. #js [#js [d1x d2x d4x]
#js [d1y d2y d4y]
#js [ 1 1 1]])
source-points-matrix (let [p1 (nth points 0)
(Matrix. #js [#js [x1 x2 x1] p2 (nth points 1)
#js [y1 y1 y2] p4 (nth points 3)
#js [ 1 1 1]])
;; returns nil if not invertible width (mth/hypot
source-points-matrix-inv (.getInverse source-points-matrix) (- (dm/get-prop p2 :x)
(dm/get-prop p1 :x))
(- (dm/get-prop p2 :y)
(dm/get-prop p1 :y)))
;; TargetM = SourceM * Transform ==> Transform = TargetM * inv(SourceM) height (mth/hypot
transform-js (- (dm/get-prop p1 :x)
(when source-points-matrix-inv (dm/get-prop p4 :x))
(.multiply target-points-matrix source-points-matrix-inv))] (- (dm/get-prop p1 :y)
(dm/get-prop p4 :y)))]
(when transform-js (grc/center->rect center width height)))
(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)))))))
(defn calculate-geometry (defn calculate-transform
[points] [points center selrect]
(let [width (calculate-width points) (let [transform (transform-points-matrix selrect points)
height (calculate-height points)
;; FIXME: looks redundant, we can convert points to rect directly
center (gco/points->center points)
sr (grc/center->rect center width height)
points-transform-mtx (transform-points-matrix sr points)
;; Calculate the transform by move the transformation to the center ;; Calculate the transform by move the transformation to the center
transform transform
(when points-transform-mtx (when (some? transform)
(gmt/multiply (-> (gmt/translate-matrix-neg center)
(gmt/translate-matrix (gpt/negate center)) (gmt/multiply! transform)
points-transform-mtx (gmt/multiply! (gmt/translate-matrix center))))]
(gmt/translate-matrix center)))
transform-inverse (when transform (gmt/inverse transform))
;; There is a rounding error when the matrix returned have float point values ;; There is a rounding error when the matrix returned have float point values
;; when the matrix is unit we return a "pure" matrix so we don't accumulate ;; when the matrix is unit we return a "pure" matrix so we don't accumulate
;; rounding problems ;; rounding problems
[transform transform-inverse] (when ^boolean (gmt/matrix? transform)
(if (gmt/unit? transform) (if ^boolean (gmt/unit? transform)
[(gmt/matrix) (gmt/matrix)] gmt/base
[transform transform-inverse])] transform))))
[sr transform transform-inverse])) (defn calculate-geometry
[points]
(let [center (gco/points->center points)
selrect (calculate-selrect points center)
transform (calculate-transform points center selrect)]
[selrect transform (when (some? transform) (gmt/inverse transform))]))
(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
to check this before adjusting the selrect" to check this before adjusting the selrect"
[shape points] [shape points]
(let [points' (dm/get-prop shape :points)
p0' (nth points' 0)
p0 (nth points 0)
(let [points' (:points shape) ;; FIXME: unroll and remove point allocation here
xv1 (gpt/to-vec p0' (nth points' 1))
xv1 (gpt/to-vec (nth points' 0) (nth points' 1)) xv2 (gpt/to-vec p0 (nth points 1))
xv2 (gpt/to-vec (nth points 0) (nth points 1))
dot-x (gpt/dot xv1 xv2) dot-x (gpt/dot xv1 xv2)
yv1 (gpt/to-vec (nth points' 0) (nth points' 3)) yv1 (gpt/to-vec p0' (nth points' 3))
yv2 (gpt/to-vec (nth points 0) (nth points 3)) yv2 (gpt/to-vec p0 (nth points 3))
dot-y (gpt/dot yv1 yv2)] dot-y (gpt/dot yv1 yv2)]
(cond-> shape (cond-> shape
(neg? dot-x) (neg? dot-x)
(-> (update :flip-x not) (-> (update :flip-x not)
(update :rotation -)) (cr/update! :rotation -))
(neg? dot-y) (neg? dot-y)
(-> (update :flip-y not) (-> (update :flip-y not)
(update :rotation -))))) (cr/update! :rotation -)))))
(defn- apply-transform-move (defn- apply-transform-move
"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
@ -386,7 +369,7 @@
"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
its properties. We adjust de x,y,width,height and create a custom transform" its properties. We adjust de x,y,width,height and create a custom transform"
[shape transform-mtx] [shape transform-mtx]
(if (gmt/move? transform-mtx) (if ^boolean (gmt/move? transform-mtx)
(apply-transform-move shape transform-mtx) (apply-transform-move shape transform-mtx)
(apply-transform-generic shape transform-mtx))) (apply-transform-generic shape transform-mtx)))
@ -477,21 +460,16 @@
(transform-shape modifiers)))) (transform-shape modifiers))))
([shape modifiers] ([shape modifiers]
(letfn [(apply-modifiers (if (and (some? modifiers) (not (ctm/empty? modifiers)))
[shape modifiers]
(if (ctm/empty? modifiers)
shape
(let [transform (ctm/modifiers->transform modifiers)] (let [transform (ctm/modifiers->transform modifiers)]
(cond-> shape (cond-> shape
(and (some? transform) (not= uuid/zero (:id shape))) ;; Never transform the root frame (and (some? transform)
(not (cph/root? shape)))
(apply-transform transform) (apply-transform transform)
(ctm/has-structure? modifiers) (ctm/has-structure? modifiers)
(ctm/apply-structure-modifiers modifiers)))))] (ctm/apply-structure-modifiers modifiers)))
shape)))
(cond-> shape
(and (some? modifiers) (not (ctm/empty? modifiers)))
(apply-modifiers modifiers)))))
(defn apply-objects-modifiers (defn apply-objects-modifiers
([objects modifiers] ([objects modifiers]
@ -532,7 +510,6 @@
(gco/transform-points mtx) (gco/transform-points mtx)
(grc/points->rect))) (grc/points->rect)))
(declare apply-group-modifiers) (declare apply-group-modifiers)
(defn apply-children-modifiers (defn apply-children-modifiers