diff --git a/CHANGES.md b/CHANGES.md index 022d57bc7..d94ed54fd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ - Add images lock proportions by default [#541](https://github.com/penpot/penpot/discussions/541), [#609](https://github.com/penpot/penpot/issues/609) - Shows a pixel grid when zoom greater than 800% [#519](https://github.com/penpot/penpot/discussions/519) - Increase default deletion delay to 7 days. +- Flip horizontal/vertical - Zstd+nippy based blob storage format - Improved component testing - Add user feedback form @@ -15,6 +16,7 @@ ### Bugs fixed - Make the team deletion defferred (in the same way other objects). +- Problems when transforming path shapes - Fix 500 when requestion password reset - Fix ldap function called on login click - Fix issues when moving shapes outside groups diff --git a/backend/tests/app/tests/test_common_geom_shapes.clj b/backend/tests/app/tests/test_common_geom_shapes.clj index 3048a8903..860f05ea4 100644 --- a/backend/tests/app/tests/test_common_geom_shapes.clj +++ b/backend/tests/app/tests/test_common_geom_shapes.clj @@ -12,6 +12,7 @@ [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] + [app.common.math :refer [close?]] [app.common.pages :refer [make-minimal-shape]] [clojure.test :as t])) @@ -32,7 +33,9 @@ :points points))) (defn add-rect-data [shape] - (let [selrect (gsh/rect->selrect shape) + (let [shape (-> shape + (assoc :width 20 :height 20)) + selrect (gsh/rect->selrect shape) points (gsh/rect->points selrect)] (assoc shape :selrect selrect @@ -64,17 +67,17 @@ shape-after (gsh/transform-shape shape-before)] (t/is (not= shape-before shape-after)) - (t/is (== (get-in shape-before [:selrect :x]) - (- 10 (get-in shape-after [:selrect :x])))) + (t/is (close? (get-in shape-before [:selrect :x]) + (- 10 (get-in shape-after [:selrect :x])))) - (t/is (== (get-in shape-before [:selrect :y]) - (+ 10 (get-in shape-after [:selrect :y])))) + (t/is (close? (get-in shape-before [:selrect :y]) + (+ 10 (get-in shape-after [:selrect :y])))) - (t/is (== (get-in shape-before [:selrect :width]) - (get-in shape-after [:selrect :width]))) + (t/is (close? (get-in shape-before [:selrect :width]) + (get-in shape-after [:selrect :width]))) - (t/is (== (get-in shape-before [:selrect :height]) - (get-in shape-after [:selrect :height]))))) + (t/is (close? (get-in shape-before [:selrect :height]) + (get-in shape-after [:selrect :height]))))) :rect :path)) @@ -84,8 +87,8 @@ shape-before (create-test-shape type {:modifiers modifiers}) shape-after (gsh/transform-shape shape-before)] (t/are [prop] - (t/is (== (get-in shape-before [:selrect prop]) - (get-in shape-after [:selrect prop]))) + (t/is (close? (get-in shape-before [:selrect prop]) + (get-in shape-after [:selrect prop]))) :x :y :width :height :x1 :y1 :x2 :y2)) :rect :path)) @@ -98,17 +101,17 @@ shape-after (gsh/transform-shape shape-before)] (t/is (not= shape-before shape-after)) - (t/is (== (get-in shape-before [:selrect :x]) - (get-in shape-after [:selrect :x]))) + (t/is (close? (get-in shape-before [:selrect :x]) + (get-in shape-after [:selrect :x]))) - (t/is (== (get-in shape-before [:selrect :y]) - (get-in shape-after [:selrect :y]))) + (t/is (close? (get-in shape-before [:selrect :y]) + (get-in shape-after [:selrect :y]))) - (t/is (== (* 2 (get-in shape-before [:selrect :width])) - (get-in shape-after [:selrect :width]))) + (t/is (close? (* 2 (get-in shape-before [:selrect :width])) + (get-in shape-after [:selrect :width]))) - (t/is (== (* 2 (get-in shape-before [:selrect :height])) - (get-in shape-after [:selrect :height])))) + (t/is (close? (* 2 (get-in shape-before [:selrect :height])) + (get-in shape-after [:selrect :height])))) :rect :path)) (t/testing "Transform with empty resize" @@ -119,8 +122,8 @@ shape-before (create-test-shape type {:modifiers modifiers}) shape-after (gsh/transform-shape shape-before)] (t/are [prop] - (t/is (== (get-in shape-before [:selrect prop]) - (get-in shape-after [:selrect prop]))) + (t/is (close? (get-in shape-before [:selrect prop]) + (get-in shape-after [:selrect prop]))) :x :y :width :height :x1 :y1 :x2 :y2)) :rect :path)) @@ -145,13 +148,23 @@ (let [modifiers {:rotation 30} shape-before (create-test-shape type {:modifiers modifiers}) shape-after (gsh/transform-shape shape-before)] + (t/is (not= shape-before shape-after)) - (t/is (not (== (get-in shape-before [:selrect :x]) - (get-in shape-after [:selrect :x])))) + ;; Selrect won't change with a rotation, but points will + (t/is (close? (get-in shape-before [:selrect :x]) + (get-in shape-after [:selrect :x]))) - (t/is (not (== (get-in shape-before [:selrect :y]) - (get-in shape-after [:selrect :y]))))) + (t/is (close? (get-in shape-before [:selrect :y]) + (get-in shape-after [:selrect :y]))) + + (t/is (= (count (:points shape-before)) (count (:points shape-after)))) + + (for [idx (range 0 (count (:point shape-before)))] + (do (t/is (not (close? (get-in shape-before [:points idx :x]) + (get-in shape-after [:points idx :x])))) + (t/is (not (close? (get-in shape-before [:points idx :y]) + (get-in shape-after [:points idx :y]))))))) :rect :path)) (t/testing "Transform shape with rotation = 0 should leave equal selrect" @@ -160,8 +173,8 @@ shape-before (create-test-shape type {:modifiers modifiers}) shape-after (gsh/transform-shape shape-before)] (t/are [prop] - (t/is (== (get-in shape-before [:selrect prop]) - (get-in shape-after [:selrect prop]))) + (t/is (close? (get-in shape-before [:selrect prop]) + (get-in shape-after [:selrect prop]))) :x :y :width :height :x1 :y1 :x2 :y2)) :rect :path)) diff --git a/common/app/common/geom/matrix.cljc b/common/app/common/geom/matrix.cljc index e071e32f3..69921998a 100644 --- a/common/app/common/geom/matrix.cljc +++ b/common/app/common/geom/matrix.cljc @@ -134,3 +134,9 @@ (th-eq m1f m2f)))) (defmethod pp/simple-dispatch Matrix [obj] (pr obj)) + +(defn transform-in [pt mtx] + (-> (matrix) + (translate pt) + (multiply mtx) + (translate (gpt/negate pt)))) diff --git a/common/app/common/geom/shapes/transforms.cljc b/common/app/common/geom/shapes/transforms.cljc index e688a16eb..09f021d1e 100644 --- a/common/app/common/geom/shapes/transforms.cljc +++ b/common/app/common/geom/shapes/transforms.cljc @@ -43,10 +43,13 @@ (let [shape-center (or (gco/center-shape shape) (gpt/point 0 0))] (inverse-transform-matrix shape shape-center))) - ([shape center] + ([{:keys [flip-x flip-y] :as shape} center] (let [] (-> (gmt/matrix) (gmt/translate center) + (cond-> + flip-x (gmt/scale (gpt/point -1 1)) + flip-y (gmt/scale (gpt/point 1 -1))) (gmt/multiply (:transform-inverse shape (gmt/matrix))) (gmt/translate (gpt/negate center)))))) @@ -203,29 +206,7 @@ (gmt/rotate (- rotation-angle)))] [stretch-matrix stretch-matrix-inverse])) - -(defn apply-transform-path - [shape transform] - (let [content (gpa/transform-content (:content shape) transform) - - ;; Calculate the new selrect by "unrotate" the shape - rotation (modif-rotation shape) - center (gpt/transform (gco/center-shape shape) transform) - content-rotated (gpa/transform-content content (gmt/rotate-matrix (- rotation) center)) - selrect (gpa/content->selrect content-rotated) - - ;; Transform the points - points (-> (:points shape) - (transform-points transform))] - (assoc shape - :content content - :points points - :selrect selrect - :transform (gmt/rotate-matrix rotation) - :transform-inverse (gmt/rotate-matrix (- rotation)) - :rotation rotation))) - -(defn apply-transform-rect +(defn apply-transform "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 transform] @@ -246,13 +227,21 @@ (:height points-temp-dim)) rect-points (gpr/rect->points rect-shape) - [matrix matrix-inverse] (calculate-adjust-matrix points-temp rect-points (:flip-x shape) (:flip-y shape))] + [matrix matrix-inverse] (calculate-adjust-matrix points-temp rect-points (:flip-x shape) (:flip-y shape)) + + shape (cond + (= :path (:type shape)) + (-> shape + (update :content #(gpa/transform-content % transform))) + + :else + (-> shape + (merge rect-shape) + (update :x #(mth/precision % 0)) + (update :y #(mth/precision % 0)) + (update :width #(mth/precision % 0)) + (update :height #(mth/precision % 0))))] (as-> shape $ - (merge $ rect-shape) - (update $ :x #(mth/precision % 0)) - (update $ :y #(mth/precision % 0)) - (update $ :width #(mth/precision % 0)) - (update $ :height #(mth/precision % 0)) (update $ :transform #(gmt/multiply (or % (gmt/matrix)) matrix)) (update $ :transform-inverse #(gmt/multiply matrix-inverse (or % (gmt/matrix)))) (assoc $ :points (into [] points)) @@ -260,37 +249,6 @@ (update $ :rotation #(mod (+ (or % 0) (or (get-in $ [:modifiers :rotation]) 0)) 360))))) -(defn apply-transform [shape transform] - (let [apply-transform-fn - (case (:type shape) - :path apply-transform-path - apply-transform-rect)] - (apply-transform-fn shape transform))) - -(defn transform-gradients [shape modifiers] - (let [angle (d/check-num (get modifiers :rotation)) - ;; Gradients are represented with unit vectors so its center is 0.5, 0.5 - center (gpt/point 0.5 0.5) - transform (gmt/rotate-matrix angle center) - transform-gradient - (fn [{:keys [start-x start-y end-x end-y] :as gradient}] - (let [start-point (gpt/point start-x start-y) - end-point (gpt/point end-x end-y) - {start-x :x start-y :y} (gpt/transform start-point transform) - {end-x :x end-y :y} (gpt/transform end-point transform)] - - (assoc gradient - :start-x start-x - :start-y start-y - :end-x end-x - :end-y end-y)))] - (cond-> shape - (:fill-color-gradient shape) - (update :fill-color-gradient transform-gradient) - - (:stroke-color-gradient shape) - (update :stroke-color-gradient transform-gradient)))) - (defn set-flip [shape modifiers] (let [rx (get-in modifiers [:resize-vector :x]) ry (get-in modifiers [:resize-vector :y])] @@ -305,12 +263,13 @@ (-> shape (set-flip (:modifiers shape)) (apply-transform transform) - (transform-gradients (:modifiers shape)) (dissoc :modifiers))) shape))) (defn update-group-selrect [group children] (let [shape-center (gco/center-shape group) + transform (:transform group (gmt/matrix)) + transform-inverse (:transform-inverse group (gmt/matrix)) ;; Points for every shape inside the group points (->> children (mapcat :points)) @@ -330,5 +289,10 @@ (-> group (assoc :selrect new-selrect) (assoc :points new-points) - (apply-transform-rect (gmt/matrix))))) + + ;; We're regenerating the selrect from its children so we + ;; need to remove the flip flags + (assoc :flip-x false) + (assoc :flip-y false) + (apply-transform (gmt/matrix))))) diff --git a/common/app/common/math.cljc b/common/app/common/math.cljc index e9ed34a29..ecb1c2c6f 100644 --- a/common/app/common/math.cljc +++ b/common/app/common/math.cljc @@ -142,3 +142,10 @@ (defn almost-zero? [num] (< (abs num) 1e-8)) + +(defonce float-equal-precision 0.001) + +(defn close? + "Equality for float numbers. Check if the difference is within a range" + [num1 num2] + (<= (abs (- num1 num2)) float-equal-precision)) diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index 898a25a64..46f51b44e 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -4805,5 +4805,19 @@ "es" : "Pulsar para cerrar la ruta" }, "unused" : true + }, + "workspace.shape.menu.flip-horizontal" : { + "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:146" ], + "translations" : { + "en" : "Flip horizontal", + "es" : "Voltear horizontal" + } + }, + "workspace.shape.menu.flip-vertical" : { + "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:143" ], + "translations" : { + "en" : "Flip vertical", + "es" : "Voltear vertical" + } } } diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index f76b8974c..acb58b487 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1799,6 +1799,8 @@ (d/export dwt/set-modifiers) (d/export dwt/apply-modifiers) (d/export dwt/update-dimensions) +(d/export dwt/flip-horizontal-selected) +(d/export dwt/flip-vertical-selected) ;; Persistence diff --git a/frontend/src/app/main/data/workspace/drawing/path.cljs b/frontend/src/app/main/data/workspace/drawing/path.cljs index 7c69909ed..c73014f1e 100644 --- a/frontend/src/app/main/data/workspace/drawing/path.cljs +++ b/frontend/src/app/main/data/workspace/drawing/path.cljs @@ -90,12 +90,15 @@ path))) (defn- points->components [shape content] - (let [rotation (:rotation shape 0) + (let [transform (:transform shape) + transform-inverse (:transform-inverse shape) center (gsh/center-shape shape) - content-rotated (gsh/transform-content content (gmt/rotate-matrix (- rotation) center)) + base-content (gsh/transform-content + content + (gmt/transform-in center transform-inverse)) ;; Calculates the new selrect with points given the old center - points (-> (gsh/content->selrect content-rotated) + points (-> (gsh/content->selrect base-content) (gsh/rect->points) (gsh/transform-points center (:transform shape (gmt/matrix)))) diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index 245645fc2..2eda58376 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -108,6 +108,14 @@ :command (ds/c-mod "k") :fn #(st/emit! dwl/add-component)} + :flip-vertical {:tooltip (ds/shift "V") + :command "shift+v" + :fn #(st/emit! (dw/flip-vertical-selected))} + + :flip-horizontal {:tooltip (ds/shift "V") + :command "shift+h" + :fn #(st/emit! (dw/flip-horizontal-selected))} + :reset-zoom {:tooltip (ds/shift "0") :command "shift+0" :fn #(st/emit! dw/reset-zoom)} diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 519517c04..b102be822 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -82,8 +82,6 @@ {:keys [rotation]} shape shapev (-> (gpt/point width height)) - rotation (if (= :path (:type shape)) 0 rotation) - ;; Vector modifiers depending on the handler handler-modif (let [[x y] (handler-modifiers handler)] (gpt/point x y)) @@ -125,15 +123,7 @@ ;; lock flag that can be activated on element options. (normalize-proportion-lock [[point shift?]] (let [proportion-lock? (:proportion-lock shape)] - [point (or proportion-lock? shift?)])) - - ;; Applies alginment to point if it is currently - ;; activated on the current workspace - ;; (apply-grid-alignment [point] - ;; (if @refs/selected-alignment - ;; (uwrk/align-point point) - ;; (rx/of point))) - ] + [point (or proportion-lock? shift?)]))] (reify ptk/UpdateEvent (update [_ state] @@ -142,8 +132,7 @@ ptk/WatchEvent (watch [_ state stream] - (let [current-pointer @ms/mouse-position - initial-position (merge current-pointer initial) + (let [initial-position @ms/mouse-position stoper (rx/filter ms/mouse-up? stream) layout (:workspace-layout state) page-id (:current-page-id state) @@ -541,3 +530,37 @@ objects (dwc/lookup-page-objects state page-id) ids (d/concat [] ids (mapcat #(cp/get-children % objects) ids))] (rx/of (apply-modifiers ids)))))) + +(defn flip-horizontal-selected [] + (ptk/reify ::flip-horizontal-selected + ptk/WatchEvent + (watch [_ state stream] + (let [objects (dwc/lookup-page-objects state) + selected (get-in state [:workspace-local :selected]) + shapes (map #(get objects %) selected) + selrect (gsh/selection-rect (->> shapes (map gsh/transform-shape))) + origin (gpt/point (:x selrect) (+ (:y selrect) (/ (:height selrect) 2)))] + + (rx/of (set-modifiers selected + {:resize-vector (gpt/point -1.0 1.0) + :resize-origin origin + :displacement (gmt/translate-matrix (gpt/point (- (:width selrect)) 0))} + false) + (apply-modifiers selected)))))) + +(defn flip-vertical-selected [] + (ptk/reify ::flip-vertical-selected + ptk/WatchEvent + (watch [_ state stream] + (let [objects (dwc/lookup-page-objects state) + selected (get-in state [:workspace-local :selected]) + shapes (map #(get objects %) selected) + selrect (gsh/selection-rect (->> shapes (map gsh/transform-shape))) + origin (gpt/point (+ (:x selrect) (/ (:width selrect) 2)) (:y selrect))] + + (rx/of (set-modifiers selected + {:resize-vector (gpt/point 1.0 -1.0) + :resize-origin origin + :displacement (gmt/translate-matrix (gpt/point 0 (- (:height selrect))))} + false) + (apply-modifiers selected)))))) diff --git a/frontend/src/app/main/ui/shapes/gradients.cljs b/frontend/src/app/main/ui/shapes/gradients.cljs index fe10056a9..cd8ab02e0 100644 --- a/frontend/src/app/main/ui/shapes/gradients.cljs +++ b/frontend/src/app/main/ui/shapes/gradients.cljs @@ -20,15 +20,13 @@ (mf/defc linear-gradient [{:keys [id gradient shape]}] (let [{:keys [x y width height]} (:selrect shape) - transform (case (:type shape) - :path (gmt/matrix) - (gsh/inverse-transform-matrix shape (gpt/point 0.5 0.5)))] + transform (when (= :path (:type shape)) (gsh/transform-matrix shape nil (gpt/point 0.5 0.5)))] [:linearGradient {:id id :x1 (:start-x gradient) :y1 (:start-y gradient) :x2 (:end-x gradient) :y2 (:end-y gradient) - :gradient-transform transform} + :gradientTransform transform} (for [{:keys [offset color opacity]} (:stops gradient)] [:stop {:key (str id "-stop-" offset) :offset (or offset 0) @@ -37,9 +35,8 @@ (mf/defc radial-gradient [{:keys [id gradient shape]}] (let [{:keys [x y width height]} (:selrect shape) - transform (case (:type shape) - :path (gmt/matrix) - (gsh/inverse-transform-matrix shape))] + center (gsh/center-shape shape) + transform (when (= :path (:type shape)) (gsh/transform-matrix shape))] (let [[x y] (if (= (:type shape) :frame) [0 0] [x y]) translate-vec (gpt/point (+ x (* width (:start-x gradient))) (+ y (* height (:start-y gradient)))) diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 13d3df116..01c1151d0 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -72,6 +72,8 @@ do-remove-group (st/emitf dw/ungroup-selected) do-mask-group (st/emitf dw/mask-group) do-unmask-group (st/emitf dw/unmask-group) + do-flip-vertical (st/emitf (dw/flip-vertical-selected)) + do-flip-horizontal (st/emitf (dw/flip-horizontal-selected)) do-add-component (st/emitf dwl/add-component) do-detach-component (st/emitf (dwl/detach-component id)) do-reset-component (st/emitf (dwl/reset-component id)) @@ -133,7 +135,18 @@ :on-click do-create-group}] [:& menu-entry {:title (t locale "workspace.shape.menu.mask") :shortcut (sc/get-tooltip :mask) - :on-click do-mask-group}]]) + :on-click do-mask-group}] + [:& menu-separator]]) + + (when (>= (count selected) 1) + [:* + [:& menu-entry {:title (t locale "workspace.shape.menu.flip-vertical") + :shortcut (sc/get-tooltip :flip-vertical) + :on-click do-flip-vertical}] + [:& menu-entry {:title (t locale "workspace.shape.menu.flip-horizontal") + :shortcut (sc/get-tooltip :flip-horizontal) + :on-click do-flip-horizontal}] + [:& menu-separator]]) (when (and (= (count selected) 1) (= (:type shape) :group)) [:* diff --git a/frontend/src/app/main/ui/workspace/gradients.cljs b/frontend/src/app/main/ui/workspace/gradients.cljs index 007b8631d..c88ba43b4 100644 --- a/frontend/src/app/main/ui/workspace/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/gradients.cljs @@ -15,6 +15,7 @@ [beicon.core :as rx] [okulary.core :as l] [app.common.math :as mth] + [app.common.geom.shapes :as gsh] [app.common.geom.point :as gpt] [app.common.geom.matrix :as gmt] [app.util.dom :as dom] @@ -238,16 +239,22 @@ gradient (mf/deref current-gradient-ref) editing-spot (mf/deref editing-spot-ref) + transform (gsh/transform-matrix shape) + transform-inverse (gsh/inverse-transform-matrix shape) + {:keys [x y width height] :as sr} (:selrect shape) [{start-color :color start-opacity :opacity} {end-color :color end-opacity :opacity}] (:stops gradient) - from-p (gpt/point (+ x (* width (:start-x gradient))) - (+ y (* height (:start-y gradient)))) + from-p (-> (gpt/point (+ x (* width (:start-x gradient))) + (+ y (* height (:start-y gradient)))) - to-p (gpt/point (+ x (* width (:end-x gradient))) - (+ y (* height (:end-y gradient)))) + (gpt/transform transform)) + + to-p (-> (gpt/point (+ x (* width (:end-x gradient))) + (+ y (* height (:end-y gradient)))) + (gpt/transform transform)) gradient-vec (gpt/to-vec from-p to-p) gradient-length (gpt/length gradient-vec) @@ -263,14 +270,16 @@ (st/emit! (dc/update-gradient changes))) on-change-start (fn [point] - (let [start-x (/ (- (:x point) x) width) + (let [point (gpt/transform point transform-inverse) + start-x (/ (- (:x point) x) width) start-y (/ (- (:y point) y) height) start-x (mth/precision start-x 2) start-y (mth/precision start-y 2)] (change! {:start-x start-x :start-y start-y}))) on-change-finish (fn [point] - (let [end-x (/ (- (:x point) x) width) + (let [point (gpt/transform point transform-inverse) + end-x (/ (- (:x point) x) width) end-y (/ (- (:y point) y) height) end-x (mth/precision end-x 2)