From b2211aec5913f4fbbc2f2ba02058d192edd616dc Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 21 Dec 2021 12:32:04 +0100 Subject: [PATCH] :zap: Change resize to use DOM transformations --- common/src/app/common/file_builder.cljc | 11 +- common/src/app/common/geom/shapes.cljc | 9 +- common/src/app/common/geom/shapes/common.cljc | 16 + .../app/common/geom/shapes/constraints.cljc | 182 ++++++++ .../src/app/common/geom/shapes/intersect.cljc | 10 +- common/src/app/common/geom/shapes/path.cljc | 37 +- common/src/app/common/geom/shapes/rect.cljc | 8 + .../app/common/geom/shapes/transforms.cljc | 422 ++++++------------ common/src/app/common/perf.cljc | 14 +- frontend/src/app/main.cljs | 3 +- .../app/main/data/workspace/transforms.cljs | 161 +++---- frontend/src/app/main/exports.cljs | 24 +- frontend/src/app/main/refs.cljs | 39 +- frontend/src/app/main/ui/shapes/bool.cljs | 87 +--- frontend/src/app/main/ui/shapes/embed.cljs | 7 +- frontend/src/app/main/ui/shapes/filters.cljs | 35 +- frontend/src/app/main/ui/shapes/frame.cljs | 25 +- frontend/src/app/main/ui/shapes/group.cljs | 8 +- frontend/src/app/main/ui/shapes/mask.cljs | 18 +- frontend/src/app/main/ui/shapes/shape.cljs | 32 +- frontend/src/app/main/ui/shapes/svg_raw.cljs | 7 +- .../src/app/main/ui/workspace/shapes.cljs | 13 +- .../app/main/ui/workspace/shapes/bool.cljs | 7 +- .../app/main/ui/workspace/shapes/frame.cljs | 1 - .../app/main/ui/workspace/shapes/group.cljs | 9 +- .../app/main/ui/workspace/shapes/svg_raw.cljs | 10 +- .../src/app/main/ui/workspace/viewport.cljs | 44 +- .../app/main/ui/workspace/viewport/hooks.cljs | 34 +- .../main/ui/workspace/viewport/selection.cljs | 4 +- .../ui/workspace/viewport/snap_points.cljs | 2 +- .../viewport/thumbnail_renderer.cljs | 12 +- .../app/main/ui/workspace/viewport/utils.cljs | 94 +++- frontend/src/app/main/worker.cljs | 16 +- frontend/src/app/util/http.cljs | 34 +- frontend/src/app/util/perf.cljs | 26 ++ frontend/src/app/worker/export.cljs | 3 + frontend/src/app/worker/import.cljs | 22 +- frontend/src/debug.cljs | 70 ++- 38 files changed, 839 insertions(+), 717 deletions(-) create mode 100644 common/src/app/common/geom/shapes/constraints.cljc diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/file_builder.cljc index 4cf883a97..6fac15a10 100644 --- a/common/src/app/common/file_builder.cljc +++ b/common/src/app/common/file_builder.cljc @@ -51,7 +51,7 @@ (get-in file [:data :components (:current-component-id file) :objects]) (get-in file [:data :pages-index (:current-page-id file) :objects])))) -(defn- lookup-shape [file shape-id] +(defn lookup-shape [file shape-id] (-> (lookup-objects file) (get shape-id))) @@ -321,16 +321,11 @@ (update :parent-stack pop)))) (defn create-shape [file type data] - (let [frame-id (:current-frame-id file) - frame (when-not (= frame-id root-frame) - (lookup-shape file frame-id)) - obj (-> (init/make-minimal-shape type) + (let [obj (-> (init/make-minimal-shape type) (merge data) (check-name file :type) (setup-selrect) - (d/without-nils)) - obj (cond-> obj - frame (gsh/translate-from-frame frame))] + (d/without-nils))] (-> file (commit-shape obj) (assoc :last-id (:id obj)) diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index 82d5dd6c7..8d0e2cd91 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -10,6 +10,7 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes.bool :as gsb] [app.common.geom.shapes.common :as gco] + [app.common.geom.shapes.constraints :as gct] [app.common.geom.shapes.intersect :as gin] [app.common.geom.shapes.path :as gsp] [app.common.geom.shapes.rect :as gpr] @@ -163,8 +164,12 @@ (d/export gtr/rotation-modifiers) (d/export gtr/merge-modifiers) (d/export gtr/transform-shape) -(d/export gtr/calc-transformed-parent-rect) -(d/export gtr/calc-child-modifiers) +(d/export gtr/transform-selrect) +(d/export gtr/modifiers->transform) +(d/export gtr/empty-modifiers?) + +;; Constratins +(d/export gct/calc-child-modifiers) ;; PATHS (d/export gsp/content->selrect) diff --git a/common/src/app/common/geom/shapes/common.cljc b/common/src/app/common/geom/shapes/common.cljc index 00eec4386..aa0a655ec 100644 --- a/common/src/app/common/geom/shapes/common.cljc +++ b/common/src/app/common/geom/shapes/common.cljc @@ -50,6 +50,22 @@ :width width :height height}) +(defn make-centered-selrect + "Creates a rect given a center and a width and height" + [center width height] + (let [x1 (- (:x center) (/ width 2.0)) + y1 (- (:y center) (/ height 2.0)) + x2 (+ x1 width) + y2 (+ y1 height)] + {:x x1 + :y y1 + :x1 x1 + :x2 x2 + :y1 y1 + :y2 y2 + :width width + :height height})) + (defn transform-points ([points matrix] (transform-points points nil matrix)) diff --git a/common/src/app/common/geom/shapes/constraints.cljc b/common/src/app/common/geom/shapes/constraints.cljc new file mode 100644 index 000000000..4ead68b4e --- /dev/null +++ b/common/src/app/common/geom/shapes/constraints.cljc @@ -0,0 +1,182 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.common.geom.shapes.constraints + (:require + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.common :as gco] + [app.common.geom.shapes.transforms :as gtr] + [app.common.math :as mth] + [app.common.pages.spec :as spec])) + +;; Auxiliary methods to work in an specifica axis +(defn get-delta-start [axis rect tr-rect] + (if (= :x axis) + (- (:x1 tr-rect) (:x1 rect)) + (- (:y1 tr-rect) (:y1 rect)))) + +(defn get-delta-end [axis rect tr-rect] + (if (= :x axis) + (- (:x2 tr-rect) (:x2 rect)) + (- (:y2 tr-rect) (:y2 rect)))) + +(defn get-delta-size [axis rect tr-rect] + (if (= :x axis) + (- (:width tr-rect) (:width rect)) + (- (:height tr-rect) (:height rect)))) + +(defn get-delta-center [axis center tr-center] + (if (= :x axis) + (- (:x tr-center) (:x center)) + (- (:y tr-center) (:y center)))) + +(defn get-displacement + ([axis delta] + (get-displacement axis delta 0 0)) + + ([axis delta init-x init-y] + (if (= :x axis) + (gpt/point (+ init-x delta) init-y) + (gpt/point init-x (+ init-y delta))))) + +(defn get-scale [axis scale] + (if (= :x axis) + (gpt/point scale 1) + (gpt/point 1 scale))) + +(defn get-size [axis rect] + (if (= :x axis) + (:width rect) + (:height rect))) + +;; Constraint function definitions + +(defmulti constraint-modifier (fn [type & _] type)) + +(defmethod constraint-modifier :start + [_ axis parent _ _ transformed-parent-rect] + + (let [parent-rect (:selrect parent) + delta-start (get-delta-start axis parent-rect transformed-parent-rect)] + (if-not (mth/almost-zero? delta-start) + {:displacement (get-displacement axis delta-start)} + {}))) + +(defmethod constraint-modifier :end + [_ axis parent _ _ transformed-parent-rect] + (let [parent-rect (:selrect parent) + delta-end (get-delta-end axis parent-rect transformed-parent-rect)] + (if-not (mth/almost-zero? delta-end) + {:displacement (get-displacement axis delta-end)} + {}))) + +(defmethod constraint-modifier :fixed + [_ axis parent child _ transformed-parent-rect] + (let [parent-rect (:selrect parent) + child-rect (:selrect child) + + delta-start (get-delta-start axis parent-rect transformed-parent-rect) + delta-size (get-delta-size axis parent-rect transformed-parent-rect) + child-size (get-size axis child-rect) + child-center (gco/center-rect child-rect)] + (if (or (not (mth/almost-zero? delta-start)) + (not (mth/almost-zero? delta-size))) + + {:displacement (get-displacement axis delta-start) + :resize-origin (-> (get-displacement axis delta-start (:x1 child-rect) (:y1 child-rect)) + (gtr/transform-point-center child-center (:transform child (gmt/matrix)))) + :resize-vector (get-scale axis (/ (+ child-size delta-size) child-size))} + {}))) + +(defmethod constraint-modifier :center + [_ axis parent _ _ transformed-parent-rect] + (let [parent-rect (:selrect parent) + parent-center (gco/center-rect parent-rect) + transformed-parent-center (gco/center-rect transformed-parent-rect) + delta-center (get-delta-center axis parent-center transformed-parent-center)] + (if-not (mth/almost-zero? delta-center) + {:displacement (get-displacement axis delta-center)} + {}))) + +(defmethod constraint-modifier :scale + [_ axis _ _ modifiers _] + (let [{:keys [resize-vector resize-vector-2 displacement]} modifiers] + (cond-> {} + (and (some? resize-vector) + (not (mth/close? (axis resize-vector) 1))) + (assoc :resize-origin (:resize-origin modifiers) + :resize-vector (if (= :x axis) + (gpt/point (:x resize-vector) 1) + (gpt/point 1 (:y resize-vector)))) + + (and (= :y axis) (some? resize-vector-2) + (not (mth/close? (:y resize-vector-2) 1))) + (assoc :resize-origin (:resize-origin-2 modifiers) + :resize-vector (gpt/point 1 (:y resize-vector-2))) + + (some? displacement) + (assoc :displacement + (get-displacement axis (-> (gpt/point 0 0) + (gpt/transform displacement) + (gpt/transform (:resize-transform-inverse modifiers (gmt/matrix))) + axis)))))) + +(defmethod constraint-modifier :default [_ _ _ _ _] + {}) + +(def const->type+axis + {:left :start + :top :start + :right :end + :bottom :end + :leftright :fixed + :topbottom :fixed + :center :center + :scale :scale}) + +(defn calc-child-modifiers + [parent child modifiers ignore-constraints transformed-parent-rect] + (let [constraints-h + (if-not ignore-constraints + (:constraints-h child (spec/default-constraints-h child)) + :scale) + + constraints-v + (if-not ignore-constraints + (:constraints-v child (spec/default-constraints-v child)) + :scale) + + modifiers-h (constraint-modifier (constraints-h const->type+axis) :x parent child modifiers transformed-parent-rect) + modifiers-v (constraint-modifier (constraints-v const->type+axis) :y parent child modifiers transformed-parent-rect)] + + ;; Build final child modifiers. Apply transform again to the result, to get the + ;; real modifiers that need to be applied to the child, including rotation as needed. + (cond-> {} + (or (contains? modifiers-h :displacement) + (contains? modifiers-v :displacement)) + (assoc :displacement (cond-> (gpt/point (get-in modifiers-h [:displacement :x] 0) + (get-in modifiers-v [:displacement :y] 0)) + (some? (:resize-transform modifiers)) + (gpt/transform (:resize-transform modifiers)) + + :always + (gmt/translate-matrix))) + + (:resize-vector modifiers-h) + (assoc :resize-origin (:resize-origin modifiers-h) + :resize-vector (gpt/point (get-in modifiers-h [:resize-vector :x] 1) + (get-in modifiers-h [:resize-vector :y] 1))) + + (:resize-vector modifiers-v) + (assoc :resize-origin-2 (:resize-origin modifiers-v) + :resize-vector-2 (gpt/point (get-in modifiers-v [:resize-vector :x] 1) + (get-in modifiers-v [:resize-vector :y] 1))) + + (:resize-transform modifiers) + (assoc :resize-transform (:resize-transform modifiers) + :resize-transform-inverse (:resize-transform-inverse modifiers))))) + diff --git a/common/src/app/common/geom/shapes/intersect.cljc b/common/src/app/common/geom/shapes/intersect.cljc index bffd72486..7b8925dc0 100644 --- a/common/src/app/common/geom/shapes/intersect.cljc +++ b/common/src/app/common/geom/shapes/intersect.cljc @@ -196,8 +196,8 @@ [point {:keys [cx cy rx ry transform]}] (let [center (gpt/point cx cy) - transform (gmt/transform-in center transform) - {px :x py :y} (gpt/transform point transform) + transform (when (some? transform) (gmt/transform-in center transform)) + {px :x py :y} (if (some? transform) (gpt/transform point transform) point) ;; Ellipse inequality formula ;; https://en.wikipedia.org/wiki/Ellipse#Shifted_ellipse v (+ (/ (mth/sq (- px cx)) @@ -256,10 +256,10 @@ "Checks if a set of lines intersect with an ellipse in any point" [rect-lines {:keys [cx cy transform] :as ellipse-data}] (let [center (gpt/point cx cy) - transform (gmt/transform-in center transform)] + transform (when (some? transform) (gmt/transform-in center transform))] (some (fn [[p1 p2]] - (let [p1 (gpt/transform p1 transform) - p2 (gpt/transform p2 transform)] + (let [p1 (if (some? transform) (gpt/transform p1 transform) p1) + p2 (if (some? transform) (gpt/transform p2 transform) p2)] (intersects-line-ellipse? [p1 p2] ellipse-data))) rect-lines))) (defn overlaps-ellipse? diff --git a/common/src/app/common/geom/shapes/path.cljc b/common/src/app/common/geom/shapes/path.cljc index c777da983..3f28a0861 100644 --- a/common/src/app/common/geom/shapes/path.cljc +++ b/common/src/app/common/geom/shapes/path.cljc @@ -361,25 +361,24 @@ (update :height #(if (mth/almost-zero? %) 1 %))))) (defn move-content [content move-vec] - (let [set-tr (fn [params px py] - (let [tr-point (-> (gpt/point (get params px) (get params py)) - (gpt/add move-vec))] - (assoc params - px (:x tr-point) - py (:y tr-point)))) + (let [dx (:x move-vec) + dy (:y move-vec) + + set-tr + (fn [params px py] + (assoc params + px (+ (get params px) dx) + py (+ (get params py) dy))) transform-params - (fn [{:keys [x c1x c2x] :as params}] + (fn [params] (cond-> params - (not (nil? x)) (set-tr :x :y) - (not (nil? c1x)) (set-tr :c1x :c1y) - (not (nil? c2x)) (set-tr :c2x :c2y)))] + (contains? params :x) (set-tr :x :y) + (contains? params :c1x) (set-tr :c1x :c1y) + (contains? params :c2x) (set-tr :c2x :c2y)))] (->> content - (mapv (fn [cmd] - (cond-> cmd - (map? cmd) - (update :params transform-params))))))) + (mapv #(d/update-when % :params transform-params))))) (defn transform-content [content transform] @@ -393,11 +392,13 @@ transform-params (fn [{:keys [x c1x c2x] :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)))] + (some? x) (set-tr :x :y) + (some? c1x) (set-tr :c1x :c1y) + (some? c2x) (set-tr :c2x :c2y)))] - (mapv #(update % :params transform-params) content))) + (into [] + (map #(update % :params transform-params)) + content))) (defn segments->content ([segments] diff --git a/common/src/app/common/geom/shapes/rect.cljc b/common/src/app/common/geom/shapes/rect.cljc index e80dae7a9..740c91677 100644 --- a/common/src/app/common/geom/shapes/rect.cljc +++ b/common/src/app/common/geom/shapes/rect.cljc @@ -129,3 +129,11 @@ (<= (:x2 sr2) (:x2 sr1)) (>= (:y1 sr2) (:y1 sr1)) (<= (:y2 sr2) (:y2 sr1)))) + +(defn round-selrect + [selrect] + (-> selrect + (update :x mth/round) + (update :y mth/round) + (update :width mth/round) + (update :height mth/round))) diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc index fcec6fa97..af748616b 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -14,20 +14,23 @@ [app.common.geom.shapes.path :as gpa] [app.common.geom.shapes.rect :as gpr] [app.common.math :as mth] - [app.common.pages.spec :as spec] [app.common.spec :as us] [app.common.text :as txt])) +(def ^:dynamic *skip-adjust* false) + ;; --- Relative Movement -(defn- move-selrect [selrect {dx :x dy :y}] - (-> selrect - (d/update-when :x + dx) - (d/update-when :y + dy) - (d/update-when :x1 + dx) - (d/update-when :y1 + dy) - (d/update-when :x2 + dx) - (d/update-when :y2 + dy))) +(defn- move-selrect [selrect pt] + (let [dx (.-x pt) + dy (.-y pt)] + (-> selrect + (update :x + dx) + (update :y + dy) + (update :x1 + dx) + (update :y1 + dy) + (update :x2 + dx) + (update :y2 + dy)))) (defn- move-points [points move-vec] (->> points @@ -171,9 +174,11 @@ (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-pathn-temp`. + 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] (calculate-adjust-matrix points-temp points-rec false false)) + ([points-temp points-rec] + (calculate-adjust-matrix points-temp points-rec false false)) + ([points-temp points-rec flip-x flip-y] (let [center (gco/center-points points-temp) @@ -211,64 +216,73 @@ stretch-matrix (gmt/multiply (gmt/rotate-matrix rotation-angle) stretch-matrix) ;; This is the inverse to be able to remove the transformation - stretch-matrix-inverse (-> (gmt/matrix) - (gmt/scale (gpt/point (/ 1 w3) (/ 1 h3))) - (gmt/skew (- skew-angle) 0) - (gmt/rotate (- rotation-angle)))] + 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- 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 round-coords?] - ;; FIXME: Improve performance - (let [points (-> shape :points (gco/transform-points transform)) - center (gco/center-points points) +(defn is-rotated? + [[a b _c _d]] + ;; true if either a-b or c-d are parallel to the axis + (not (mth/close? (:y a) (:y b)))) - ;; Reverse the current transformation stack to get the base rectangle - tr-inverse (:transform-inverse shape (gmt/matrix)) +(defn- adjust-rotated-transform + [{:keys [transform transform-inverse flip-x flip-y]} points] + (let [center (gco/center-points points) - points-temp (gco/transform-points points center tr-inverse) + points-temp (cond-> points + (some? transform-inverse) + (gco/transform-points center transform-inverse)) points-temp-dim (calculate-dimensions points-temp) ;; This rectangle is the new data for the current rectangle. We want to change our rectangle ;; to have this width, height, x, y - rect-shape (-> (gco/make-centered-rect - center - (:width points-temp-dim) - (:height points-temp-dim)) - (update :width max 1) - (update :height max 1)) + new-width (max 1 (:width points-temp-dim)) + new-height (max 1 (:height points-temp-dim)) + selrect (gco/make-centered-selrect center new-width new-height) - rect-points (gpr/rect->points rect-shape) + rect-points (gpr/rect->points selrect) + [matrix matrix-inverse] (calculate-adjust-matrix points-temp rect-points flip-x flip-y)] - [matrix matrix-inverse] (calculate-adjust-matrix points-temp rect-points (:flip-x shape) (:flip-y shape)) + [selrect + (if transform (gmt/multiply transform matrix) matrix) + (if transform-inverse (gmt/multiply matrix-inverse transform-inverse) matrix-inverse)])) - rect-shape (cond-> rect-shape - round-coords? - (-> (update :x mth/round) - (update :y mth/round) - (update :width mth/round) - (update :height mth/round))) +(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-mtx round-coords?] - shape (cond - (= :path (:type shape)) - (-> shape - (update :content #(gpa/transform-content % transform))) + (let [points' (:points shape) + points (gco/transform-points points' transform-mtx) + path? (= (:type shape) :path) + rotated? (is-rotated? points) - :else - (-> shape - (merge rect-shape))) + [selrect transform transform-inverse] + (if (not rotated?) + [(gpr/points->selrect points) nil nil] + (adjust-rotated-transform shape points)) + selrect (cond-> selrect + round-coords? gpr/round-selrect) + + ;; Redondear los points? 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)] - (as-> shape $ - (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)) - (assoc $ :rotation (mod (+ base-rotation modif-rotation) 360))))) + (-> shape + (cond-> path? + (update :content gpa/transform-content transform-mtx)) + (cond-> (not path?) + (-> (merge (select-keys selrect [:x :y :width :height])))) + (cond-> transform + (-> (assoc :transform transform) + (assoc :transform-inverse transform-inverse))) + (assoc :selrect selrect) + (assoc :points points) + (assoc :rotation rotation)))) (defn- update-group-viewbox "Updates the viewbox for groups imported from SVG's" @@ -402,53 +416,54 @@ (def merge-modifiers (memoize merge-modifiers*)) -(defn- modifiers->transform - [center modifiers] - (let [ds-modifier (:displacement modifiers (gmt/matrix)) - {res-x :x res-y :y} (:resize-vector modifiers (gpt/point 1 1)) - {res-x-2 :x res-y-2 :y} (:resize-vector-2 modifiers (gpt/point 1 1)) +(defn modifiers->transform + ([modifiers] + (modifiers->transform nil 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) + ([center modifiers] + (let [displacement (:displacement modifiers) + resize-v1 (:resize-vector modifiers) + resize-v2 (:resize-vector-2 modifiers) + origin-1 (:resize-origin modifiers (gpt/point)) + origin-2 (:resize-origin-2 modifiers (gpt/point)) - res-x-2 (normalize-scale res-x-2) - res-y-2 (normalize-scale res-y-2) - resize-2 (gpt/point res-x-2 res-y-2) + ;; Normalize x/y vector coordinates because scale by 0 is infinite + resize-1 (when (some? resize-v1) + (gpt/point (normalize-scale (:x resize-v1)) + (normalize-scale (:y resize-v1)))) - origin (:resize-origin modifiers (gpt/point 0 0)) - origin-2 (:resize-origin-2 modifiers (gpt/point 0 0)) + resize-2 (when (some? resize-v2) + (gpt/point (normalize-scale (:x resize-v2)) + (normalize-scale (:y resize-v2)))) - resize-transform (:resize-transform modifiers (gmt/matrix)) - resize-transform-inverse (:resize-transform-inverse modifiers (gmt/matrix)) - rt-modif (or (:rotation modifiers) 0) - center (gpt/transform center ds-modifier) + resize-transform (:resize-transform modifiers (gmt/matrix)) + resize-transform-inverse (:resize-transform-inverse modifiers (gmt/matrix)) - transform (-> (gmt/matrix) + rt-modif (:rotation modifiers)] - ;; 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)) + (cond-> (gmt/matrix) + (some? displacement) + (gmt/multiply displacement) - (gmt/translate origin-2) - (gmt/multiply resize-transform) - (gmt/scale resize-2) - (gmt/multiply resize-transform-inverse) - (gmt/translate (gpt/negate origin-2)) + (some? resize-1) + (-> (gmt/translate origin-1) + (gmt/multiply resize-transform) + (gmt/scale resize-1) + (gmt/multiply resize-transform-inverse) + (gmt/translate (gpt/negate origin-1))) - ;; Applies the stacked transformations - (gmt/translate center) - (gmt/multiply (gmt/rotate-matrix rt-modif)) - (gmt/translate (gpt/negate center)) + (some? resize-2) + (-> (gmt/translate origin-2) + (gmt/multiply resize-transform) + (gmt/scale resize-2) + (gmt/multiply resize-transform-inverse) + (gmt/translate (gpt/negate origin-2))) - ;; Displacement - (gmt/multiply ds-modifier))] - transform)) + (some? rt-modif) + (-> (gmt/translate center) + (gmt/multiply (gmt/rotate-matrix rt-modif)) + (gmt/translate (gpt/negate center))))))) (defn- set-flip [shape modifiers] (let [rx (or (get-in modifiers [:resize-vector :x]) @@ -492,51 +507,58 @@ %))) shape)) -(defn -transform-shape - [shape {:keys [round-coords?] - :or {round-coords? true}}] - (if (and (contains? shape :modifiers) (empty-modifiers? (:modifiers shape))) - (dissoc shape :modifiers) - (let [shape (apply-displacement shape) - center (gco/center-shape shape) - modifiers (:modifiers shape)] - (if (and (not (empty-modifiers? modifiers)) center) - (let [transform (modifiers->transform center modifiers)] - (-> shape - (set-flip modifiers) - (apply-transform transform round-coords?) - (apply-text-resize modifiers) - (dissoc :modifiers))) - shape)))) - -(def transform-shape* (memoize -transform-shape)) +(defn apply-modifiers + [shape modifiers round-coords?] + (let [center (gco/center-shape shape) + transform (modifiers->transform center modifiers)] + (apply-transform shape transform round-coords?))) (defn transform-shape ([shape] - (transform-shape* shape nil)) - ([shape options] - (transform-shape* shape options))) + (transform-shape shape nil)) -(defn calc-transformed-parent-rect - [{:keys [selrect] :as shape} {:keys [displacement resize-transform-inverse resize-vector resize-origin resize-vector-2 resize-origin-2]}] + ([shape {:keys [round-coords?] :or {round-coords? true}}] + (let [modifiers (:modifiers shape)] + (cond + (nil? modifiers) + shape + (empty-modifiers? modifiers) + (dissoc shape :modifiers) + + :else + (let [shape (apply-displacement shape) + modifiers (:modifiers shape)] + (cond-> shape + (not (empty-modifiers? modifiers)) + (-> (set-flip modifiers) + (apply-modifiers modifiers round-coords?) + (apply-text-resize modifiers)) + + :always + (dissoc :modifiers))))))) + +(defn transform-selrect + [selrect {:keys [displacement resize-transform-inverse resize-vector resize-origin resize-vector-2 resize-origin-2]}] + ;; FIXME: Improve Performance (let [resize-transform-inverse (or resize-transform-inverse (gmt/matrix)) displacement (when (some? displacement) - (-> (gpt/point 0 0) + (gmt/multiply resize-transform-inverse displacement) + #_(-> (gpt/point 0 0) (gpt/transform displacement) (gpt/transform resize-transform-inverse) (gmt/translate-matrix))) resize-origin (when (some? resize-origin) - (transform-point-center resize-origin (gco/center-shape shape) resize-transform-inverse)) + (transform-point-center resize-origin (gco/center-selrect selrect) resize-transform-inverse)) resize-origin-2 (when (some? resize-origin-2) - (transform-point-center resize-origin-2 (gco/center-shape shape) resize-transform-inverse))] + (transform-point-center resize-origin-2 (gco/center-selrect selrect) resize-transform-inverse))] (if (and (nil? displacement) (nil? resize-origin) (nil? resize-origin-2)) selrect @@ -557,177 +579,11 @@ :always (gpr/points->selrect))))) -(defn calc-child-modifiers - "Given the modifiers to apply to the parent, calculate the corresponding - modifiers for the child, depending on the child constraints." - ([parent child parent-modifiers ignore-constraints] - (let [transformed-parent-rect (calc-transformed-parent-rect parent parent-modifiers )] - (calc-child-modifiers parent child parent-modifiers ignore-constraints transformed-parent-rect))) - - ([parent child parent-modifiers ignore-constraints transformed-parent-rect] - (let [parent-rect (:selrect parent) - child-rect (:selrect child) - - ;; Apply the modifiers to the parent's selrect, to check the difference with - ;; the original, and calculate child transformations from this. - ;; - ;; Note that a shape's selrect is always "horizontal" (i.e. without applying - ;; the shape transform, that may include some rotation and skew). Thus, to - ;; apply the modifiers, we first apply to them the transform-inverse. - - ;; Calculate the modifiers in the horizontal and vertical directions - ;; depending on the child constraints. - constraints-h (if-not ignore-constraints - (get child :constraints-h (spec/default-constraints-h child)) - :scale) - constraints-v (if-not ignore-constraints - (get child :constraints-v (spec/default-constraints-v child)) - :scale) - - modifiers-h (case constraints-h - :left - (let [delta-left (- (:x1 transformed-parent-rect) (:x1 parent-rect))] - - (if-not (mth/almost-zero? delta-left) - {:displacement (gpt/point delta-left 0)} ;; we convert to matrix below - {})) - - :right - (let [delta-right (- (:x2 transformed-parent-rect) (:x2 parent-rect))] - (if-not (mth/almost-zero? delta-right) - {:displacement (gpt/point delta-right 0)} - {})) - - :leftright - (let [delta-left (- (:x1 transformed-parent-rect) (:x1 parent-rect)) - delta-width (- (:width transformed-parent-rect) (:width parent-rect))] - (if (or (not (mth/almost-zero? delta-left)) - (not (mth/almost-zero? delta-width))) - {:displacement (gpt/point delta-left 0) - :resize-origin (-> (gpt/point (+ (:x1 child-rect) delta-left) - (:y1 child-rect)) - (transform-point-center - (gco/center-rect child-rect) - (:transform child (gmt/matrix)))) - :resize-vector (gpt/point (/ (+ (:width child-rect) delta-width) - (:width child-rect)) 1)} - {})) - - :center - (let [parent-center (gco/center-rect parent-rect) - transformed-parent-center (gco/center-rect transformed-parent-rect) - delta-center (- (:x transformed-parent-center) (:x parent-center))] - (if-not (mth/almost-zero? delta-center) - {:displacement (gpt/point delta-center 0)} - {})) - - :scale - (cond-> {} - (and (:resize-vector parent-modifiers) - (not (mth/close? (:x (:resize-vector parent-modifiers)) 1))) - (assoc :resize-origin (:resize-origin parent-modifiers) - :resize-vector (gpt/point (:x (:resize-vector parent-modifiers)) 1)) - - ;; resize-vector-2 is always for vertical modifiers, so no need to - ;; check it here. - - (:displacement parent-modifiers) - (assoc :displacement - (gpt/point (-> (gpt/point 0 0) - (gpt/transform (:displacement parent-modifiers)) - (gpt/transform (:resize-transform-inverse parent-modifiers (gmt/matrix))) - (:x)) - 0))) - {}) - - modifiers-v (case constraints-v - :top - (let [delta-top (- (:y1 transformed-parent-rect) (:y1 parent-rect))] - (if-not (mth/almost-zero? delta-top) - {:displacement (gpt/point 0 delta-top)} ;; we convert to matrix below - {})) - - :bottom - (let [delta-bottom (- (:y2 transformed-parent-rect) (:y2 parent-rect))] - (if-not (mth/almost-zero? delta-bottom) - {:displacement (gpt/point 0 delta-bottom)} - {})) - - :topbottom - (let [delta-top (- (:y1 transformed-parent-rect) (:y1 parent-rect)) - delta-height (- (:height transformed-parent-rect) (:height parent-rect))] - (if (or (not (mth/almost-zero? delta-top)) - (not (mth/almost-zero? delta-height))) - {:displacement (gpt/point 0 delta-top) - :resize-origin (-> (gpt/point (:x1 child-rect) - (+ (:y1 child-rect) delta-top)) - (transform-point-center - (gco/center-rect child-rect) - (:transform child (gmt/matrix)))) - :resize-vector (gpt/point 1 (/ (+ (:height child-rect) delta-height) - (:height child-rect)))} - {})) - - :center - (let [parent-center (gco/center-rect parent-rect) - transformed-parent-center (gco/center-rect transformed-parent-rect) - delta-center (- (:y transformed-parent-center) (:y parent-center))] - (if-not (mth/almost-zero? delta-center) - {:displacement (gpt/point 0 delta-center)} - {})) - - :scale - (cond-> {} - (and (:resize-vector parent-modifiers) - (not (mth/close? (:y (:resize-vector parent-modifiers)) 1))) - (assoc :resize-origin (:resize-origin parent-modifiers) - :resize-vector (gpt/point 1 (:y (:resize-vector parent-modifiers)))) - - ;; If there is a resize-vector-2, this means that we come from a recursive - ;; call, and the resize-vector has no vertical data, so we may override it. - (and (:resize-vector-2 parent-modifiers) - (not (mth/close? (:y (:resize-vector-2 parent-modifiers)) 1))) - (assoc :resize-origin (:resize-origin-2 parent-modifiers) - :resize-vector (gpt/point 1 (:y (:resize-vector-2 parent-modifiers)))) - - (:displacement parent-modifiers) - (assoc :displacement - (gpt/point 0 (-> (gpt/point 0 0) - (gpt/transform (:displacement parent-modifiers)) - (gpt/transform (:resize-transform-inverse parent-modifiers (gmt/matrix))) - (:y))))) - {})] - - ;; Build final child modifiers. Apply transform again to the result, to get the - ;; real modifiers that need to be applied to the child, including rotation as needed. - (cond-> {} - (or (:displacement modifiers-h) (:displacement modifiers-v)) - (assoc :displacement (gmt/translate-matrix - (-> (gpt/point (get (:displacement modifiers-h) :x 0) - (get (:displacement modifiers-v) :y 0)) - (gpt/transform - (:resize-transform parent-modifiers (gmt/matrix)))))) - - (:resize-vector modifiers-h) - (assoc :resize-origin (:resize-origin modifiers-h) - :resize-vector (gpt/point (get (:resize-vector modifiers-h) :x 1) - (get (:resize-vector modifiers-h) :y 1))) - - (:resize-vector modifiers-v) - (assoc :resize-origin-2 (:resize-origin modifiers-v) - :resize-vector-2 (gpt/point (get (:resize-vector modifiers-v) :x 1) - (get (:resize-vector modifiers-v) :y 1))) - - (:resize-transform parent-modifiers) - (assoc :resize-transform (:resize-transform parent-modifiers) - :resize-transform-inverse (:resize-transform-inverse parent-modifiers)))))) - (defn selection-rect "Returns a rect that contains all the shapes and is aware of the rotation of each shape. Mainly used for multiple selection." [shapes] (->> shapes - (transform-shape) - (map (comp gpr/points->selrect :points)) + (map (comp gpr/points->selrect :points transform-shape)) (gpr/join-selrects))) diff --git a/common/src/app/common/perf.cljc b/common/src/app/common/perf.cljc index 6b528e0ba..eb1841171 100644 --- a/common/src/app/common/perf.cljc +++ b/common/src/app/common/perf.cljc @@ -21,14 +21,15 @@ [key] (- (timestamp) (get @measures key))) -#?(:cljs - (defn benchmark - "A helper function for perform a unitari benchmark on JS/CLJS. It +(defn benchmark + "A helper function for perform a unitari benchmark on JS/CLJS. It uses browser native api so it only suitable to be executed in browser." - [& {:keys [f iterations name] - :or {iterations 10000}}] - (let [end-mark (str name ":end")] + [& _options] + #?(:cljs + (let [{:keys [f iterations name] + :or {iterations 10000}} _options + end-mark (str name ":end")] (println "=> benchmarking:" name) (println "--> warming up:" iterations) (loop [i iterations] @@ -51,3 +52,4 @@ #js {:duration duration :avg avg})))) + diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 1339369b1..79af0a906 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -18,7 +18,7 @@ [app.main.ui.confirm] [app.main.ui.modal :refer [modal]] [app.main.ui.routes :as rt] - [app.main.worker] + [app.main.worker :as worker] [app.util.dom :as dom] [app.util.i18n :as i18n] [app.util.theme :as theme] @@ -60,6 +60,7 @@ (defn ^:export init [] + (worker/init!) (sentry/init!) (i18n/init! cf/translations) (theme/init! cf/themes) diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 6cbc044bc..24598b1a6 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -106,9 +106,9 @@ ;; apply-modifiers event is done, that consolidates all modifiers into the base ;; geometric attributes of the shapes. -(declare set-modifiers-recursive) -(declare set-local-displacement) (declare clear-local-transform) +(declare set-modifiers-recursive) +(declare get-ignore-tree) (defn- set-modifiers ([ids] (set-modifiers ids nil false)) @@ -119,21 +119,17 @@ ptk/UpdateEvent (update [_ state] (let [modifiers (or modifiers (get-in state [:workspace-local :modifiers] {})) - page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - ids (into #{} (remove #(get-in objects [% :blocked] false)) ids)] + page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + ids (into #{} (remove #(get-in objects [% :blocked] false)) ids) - (reduce (fn [state id] - (update state :workspace-modifiers - #(set-modifiers-recursive % - objects - (get objects id) - modifiers - nil - nil - ignore-constraints))) - state - ids)))))) + setup-modifiers + (fn [state id] + (let [shape (get objects id)] + (update state :workspace-modifiers + #(set-modifiers-recursive % objects shape modifiers ignore-constraints))))] + + (reduce setup-modifiers state ids)))))) ;; Rotation use different algorithm to calculate children modifiers (and do not use child constraints). (defn- set-rotation-modifiers @@ -169,15 +165,14 @@ children-ids (->> ids (mapcat #(cp/get-children % objects))) ids-with-children (d/concat-vec children-ids ids) object-modifiers (get state :workspace-modifiers) - ignore-tree (d/mapm #(get-in %2 [:modifiers :ignore-geometry?]) object-modifiers)] + ignore-tree (get-ignore-tree object-modifiers objects ids)] (rx/of (dwu/start-undo-transaction) (dch/update-shapes ids-with-children (fn [shape] - (-> shape - (merge (get object-modifiers (:id shape))) - (gsh/transform-shape))) + (let [modif (get object-modifiers (:id shape))] + (gsh/transform-shape (merge shape modif)))) {:reg-objects? true :ignore-tree ignore-tree ;; Attributes that can change in the transform. This way we don't have to check @@ -198,62 +193,51 @@ "If the shape is a component instance, check its relative position respect the root of the component, and see if it changes after applying a transformation." [shape root transformed-shape transformed-root objects] - (let [root (cond - (:component-root? shape) - shape + (let [root + (cond + (:component-root? shape) + shape - (nil? root) - (cp/get-root-shape shape objects) + (nil? root) + (cp/get-root-shape shape objects) - :else root) + :else root) - transformed-root (cond - (:component-root? transformed-shape) - transformed-shape + transformed-root + (cond + (:component-root? transformed-shape) + transformed-shape - (nil? transformed-root) - (cp/get-root-shape transformed-shape objects) + (nil? transformed-root) + (cp/get-root-shape transformed-shape objects) - :else transformed-root) + :else transformed-root) - shape-delta (when root - (gpt/point (- (:x shape) (:x root)) - (- (:y shape) (:y root)))) + shape-delta + (when root + (gpt/point (- (:x shape) (:x root)) + (- (:y shape) (:y root)))) - transformed-shape-delta (when transformed-root - (gpt/point (- (:x transformed-shape) (:x transformed-root)) - (- (:y transformed-shape) (:y transformed-root)))) + transformed-shape-delta + (when transformed-root + (gpt/point (- (:x transformed-shape) (:x transformed-root)) + (- (:y transformed-shape) (:y transformed-root)))) ignore-geometry? (= shape-delta transformed-shape-delta)] [root transformed-root ignore-geometry?])) (defn- set-modifiers-recursive - [modif-tree objects shape modifiers root transformed-root ignore-constraints] + [modif-tree objects shape modifiers ignore-constraints] (let [children (map (d/getf objects) (:shapes shape)) - - transformed-shape (gsh/transform-shape (assoc shape :modifiers modifiers)) - - [root transformed-root ignore-geometry?] - (check-delta shape root transformed-shape transformed-root objects) - - modifiers (assoc modifiers :ignore-geometry? ignore-geometry?) - - transformed-rect (gsh/calc-transformed-parent-rect shape modifiers) + transformed-rect (gsh/transform-selrect (:selrect shape) modifiers) set-child (fn [modif-tree child] - (let [child-modifiers - (gsh/calc-child-modifiers shape child modifiers ignore-constraints transformed-rect)] - + (let [child-modifiers (gsh/calc-child-modifiers shape child modifiers ignore-constraints transformed-rect)] (cond-> modif-tree - (d/not-empty? (dissoc child-modifiers :ignore-geometry?)) - (set-modifiers-recursive objects - child - child-modifiers - root - transformed-root - ignore-constraints)))) + (not (gsh/empty-modifiers? child-modifiers)) + (set-modifiers-recursive objects child child-modifiers ignore-constraints)))) modif-tree (-> modif-tree @@ -261,13 +245,27 @@ (reduce set-child modif-tree children))) -(defn- set-local-displacement [point] - (ptk/reify ::start-local-displacement - ptk/UpdateEvent - (update [_ state] - (let [mtx (gmt/translate-matrix point)] - (-> state - (assoc-in [:workspace-local :modifiers] {:displacement mtx})))))) +(defn- get-ignore-tree + "Retrieves a map with the flag `ignore-tree` given a tree of modifiers" + ([modif-tree objects shape] + (get-ignore-tree modif-tree objects shape nil nil {})) + + ([modif-tree objects shape root transformed-root ignore-tree] + (let [children (map (d/getf objects) (:shapes shape)) + + shape-id (:id shape) + transformed-shape (gsh/transform-shape (merge shape (get modif-tree shape-id))) + + [root transformed-root ignore-geometry?] + (check-delta shape root transformed-shape transformed-root objects) + + ignore-tree (assoc ignore-tree shape-id ignore-geometry?) + + set-child + (fn [modif-tree child] + (get-ignore-tree modif-tree objects child root transformed-root ignore-tree))] + + (reduce set-child ignore-tree children)))) (defn- clear-local-transform [] (ptk/reify ::clear-local-transform @@ -275,7 +273,7 @@ (update [_ state] (-> state (dissoc :workspace-modifiers) - (update :workspace-local dissoc :modifiers :current-move-selected))))) + (update :workspace-local dissoc :current-move-selected))))) ;; -- Resize -------------------------------------------------------- @@ -410,26 +408,13 @@ (let [shape (get objects id) modifiers (gsh/resize-modifiers shape attr value)] (update state :workspace-modifiers - #(set-modifiers-recursive % - objects - shape - modifiers - nil - nil - false)))) + #(set-modifiers-recursive % objects shape modifiers false)))) state ids))) ptk/WatchEvent - (watch [_ state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - - ;; TODO: looks completly redundant operation because - ;; apply-modifiers already finds all children. - ids (d/concat-vec ids (mapcat #(cp/get-children % objects) ids))] - (rx/of (apply-modifiers ids)))))) - + (watch [_ _ _] + (rx/of (apply-modifiers ids))))) ;; -- Rotate -------------------------------------------------------- @@ -579,11 +564,11 @@ (->> position (rx/with-latest vector snap-delta) (rx/map snap/correct-snap-point) - (rx/map set-local-displacement) + (rx/map #(hash-map :displacement (gmt/translate-matrix %))) + (rx/map (partial set-modifiers ids)) (rx/take-until stopper)) - (rx/of (set-modifiers ids) - (apply-modifiers ids) + (rx/of (apply-modifiers ids) (calculate-frame-for-move ids) (finish-transform))))))))) @@ -626,11 +611,11 @@ (->> move-events (rx/take-until stopper) (rx/scan #(gpt/add %1 mov-vec) (gpt/point 0 0)) - (rx/map set-local-displacement)) + (rx/map #(hash-map :displacement (gmt/translate-matrix %))) + (rx/map (partial set-modifiers selected))) (rx/of (move-selected direction shift?))) - (rx/of (set-modifiers selected) - (apply-modifiers selected) + (rx/of (apply-modifiers selected) (finish-transform)))) (rx/empty)))))) diff --git a/frontend/src/app/main/exports.cljs b/frontend/src/app/main/exports.cljs index 5917852e9..85d3fd954 100644 --- a/frontend/src/app/main/exports.cljs +++ b/frontend/src/app/main/exports.cljs @@ -31,6 +31,7 @@ [app.util.object :as obj] [app.util.timers :as ts] [cuerdas.core :as str] + [debug :refer [debug?]] [rumext.alpha :as mf])) (def ^:private default-color clr/canvas) @@ -76,10 +77,9 @@ (let [shape-wrapper (shape-wrapper-factory objects) group-shape (group/group-shape shape-wrapper)] (mf/fnc group-wrapper - [{:keys [shape frame] :as props}] + [{:keys [shape] :as props}] (let [childs (mapv #(get objects %) (:shapes shape))] - [:& group-shape {:frame frame - :shape shape + [:& group-shape {:shape shape :is-child-selected? true :childs childs}])))) @@ -88,11 +88,10 @@ (let [shape-wrapper (shape-wrapper-factory objects) bool-shape (bool/bool-shape shape-wrapper)] (mf/fnc bool-wrapper - [{:keys [shape frame] :as props}] + [{:keys [shape] :as props}] (let [childs (->> (cp/get-children (:id shape) objects) (select-keys objects))] - [:& bool-shape {:frame frame - :shape shape + [:& bool-shape {:shape shape :childs childs}])))) (defn svg-raw-wrapper-factory @@ -100,18 +99,16 @@ (let [shape-wrapper (shape-wrapper-factory objects) svg-raw-shape (svg-raw/svg-raw-shape shape-wrapper)] (mf/fnc svg-raw-wrapper - [{:keys [shape frame] :as props}] + [{:keys [shape] :as props}] (let [childs (mapv #(get objects %) (:shapes shape))] (if (and (map? (:content shape)) (or (= :svg (get-in shape [:content :tag])) (contains? shape :svg-attrs))) [:> shape-container {:shape shape} - [:& svg-raw-shape {:frame frame - :shape shape + [:& svg-raw-shape {:shape shape :childs childs}]] - [:& svg-raw-shape {:frame frame - :shape shape + [:& svg-raw-shape {:shape shape :childs childs}]))))) (defn shape-wrapper-factory @@ -123,8 +120,7 @@ bool-wrapper (mf/use-memo (mf/deps objects) #(bool-wrapper-factory objects)) frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects))] (when (and shape (not (:hidden shape))) - (let [shape (-> (gsh/transform-shape shape) - (gsh/translate-to-frame frame)) + (let [shape (gsh/transform-shape shape) opts #js {:shape shape} svg-raw? (= :svg-raw (:type shape))] (if-not svg-raw? @@ -198,7 +194,7 @@ :width (:width item) :height (:height item) ;; DEBUG - ;; :style {:filter "sepia(1)"} + :style {:filter (when (debug? :thumbnails) "sepia(1)")} }] frame? [:& frame-wrapper {:shape item diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index f98d98be2..7eff78ef5 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -8,7 +8,6 @@ "A collection of derived refs." (:require [app.common.data :as d] - [app.common.geom.shapes :as gsh] [app.common.pages :as cp] [app.common.path.commands :as upc] [app.main.data.workspace.state-helpers :as wsh] @@ -232,23 +231,13 @@ (l/derived #(get % id) workspace-page-objects)) (defn objects-by-id - ([ids] - (objects-by-id ids nil)) - - ([ids {:keys [with-modifiers?] - :or { with-modifiers? false }}] - (let [selector - (fn [state] - (let [objects (wsh/lookup-page-objects state) - modifiers (:workspace-modifiers state) - ;; FIXME: Improve performance - objects (cond-> objects - with-modifiers? - (gsh/merge-modifiers modifiers)) - xform (comp (map (d/getf objects)) - (remove nil?))] - (into [] xform ids)))] - (l/derived selector st/state =)))) + [ids] + (let [selector + (fn [state] + (let [objects (wsh/lookup-page-objects state) + xform (comp (map (d/getf objects)) (remove nil?))] + (into [] xform ids)))] + (l/derived selector st/state =))) (defn- set-content-modifiers [state] (fn [id shape] @@ -260,20 +249,8 @@ (defn select-children [id] (let [selector (fn [state] - (let [objects (wsh/lookup-page-objects state) - - modifiers (-> (:workspace-modifiers state)) - {selected :selected disp-modifiers :modifiers} - (-> (:workspace-local state) - (select-keys [:modifiers :selected])) - - modifiers - (d/deep-merge - modifiers - (into {} (map #(vector % {:modifiers disp-modifiers})) selected))] - + (let [objects (wsh/lookup-page-objects state)] (as-> (cp/select-children id objects) $ - (gsh/merge-modifiers $ modifiers) (d/mapm (set-content-modifiers state) $))))] (l/derived selector st/state =))) diff --git a/frontend/src/app/main/ui/shapes/bool.cljs b/frontend/src/app/main/ui/shapes/bool.cljs index fd9b58f94..88a48feef 100644 --- a/frontend/src/app/main/ui/shapes/bool.cljs +++ b/frontend/src/app/main/ui/shapes/bool.cljs @@ -6,9 +6,6 @@ (ns app.main.ui.shapes.bool (:require - [app.common.data :as d] - [app.common.geom.shapes :as gsh] - [app.common.geom.shapes.path :as gsp] [app.common.path.bool :as pb] [app.common.path.shapes-to-path :as stp] [app.main.ui.hooks :refer [use-equal-memo]] @@ -17,87 +14,26 @@ [app.util.object :as obj] [rumext.alpha :as mf])) -(mf/defc debug-bool - {::mf/wrap-props false} - [props] - - (let [frame (obj/get props "frame") - shape (obj/get props "shape") - childs (obj/get props "childs") - - [content-a content-b] - (mf/use-memo - (mf/deps shape childs) - (fn [] - (let [childs (d/mapm #(-> %2 (gsh/translate-to-frame frame) gsh/transform-shape) childs) - [content-a content-b] - (->> (:shapes shape) - (map #(get childs %)) - (filter #(not (:hidden %))) - (map #(stp/convert-to-path % childs)) - (map :content) - (map pb/close-paths) - (map pb/add-previous))] - (pb/content-intersect-split content-a content-b))))] - [:g.debug-bool - [:g.shape-a - [:& path-shape {:shape (-> shape - (assoc :type :path) - (assoc :stroke-color "blue") - (assoc :stroke-opacity 1) - (assoc :stroke-width 1) - (assoc :stroke-style :solid) - (dissoc :fill-color :fill-opacity) - (assoc :content content-b)) - :frame frame}] - (for [{:keys [x y]} (gsp/content->points (pb/close-paths content-b))] - [:circle {:cx x - :cy y - :r 2.5 - :style {:fill "blue"}}])] - - [:g.shape-b - [:& path-shape {:shape (-> shape - (assoc :type :path) - (assoc :stroke-color "red") - (assoc :stroke-opacity 1) - (assoc :stroke-width 0.5) - (assoc :stroke-style :solid) - (dissoc :fill-color :fill-opacity) - (assoc :content content-a)) - :frame frame}] - (for [{:keys [x y]} (gsp/content->points (pb/close-paths content-a))] - [:circle {:cx x - :cy y - :r 1.25 - :style {:fill "red"}}])]]) - ) - - (defn bool-shape [shape-wrapper] (mf/fnc bool-shape {::mf/wrap-props false} [props] - (let [frame (obj/get props "frame") - shape (obj/get props "shape") + (let [shape (obj/get props "shape") childs (obj/get props "childs") - childs (use-equal-memo childs) - include-metadata? (mf/use-ctx use/include-metadata-ctx) bool-content (mf/use-memo (mf/deps shape childs) (fn [] - (let [childs (d/mapm #(-> %2 gsh/transform-shape (gsh/translate-to-frame frame)) childs)] - (->> (:shapes shape) - (map #(get childs %)) - (filter #(not (:hidden %))) - (map #(stp/convert-to-path % childs)) - (mapv :content) - (pb/content-bool (:bool-type shape))))))] + (->> (:shapes shape) + (map #(get childs %)) + (filter #(not (:hidden %))) + (map #(stp/convert-to-path % childs)) + (mapv :content) + (pb/content-bool (:bool-type shape)))))] [:* [:& path-shape {:shape (assoc shape :content bool-content)}] @@ -105,10 +41,5 @@ (when include-metadata? [:> "penpot:bool" {} (for [item (->> (:shapes shape) (mapv #(get childs %)))] - [:& shape-wrapper {:frame frame - :shape item - :key (:id item)}])]) - - #_[:& debug-bool {:frame frame - :shape shape - :childs childs}]]))) + [:& shape-wrapper {:shape item + :key (:id item)}])])]))) diff --git a/frontend/src/app/main/ui/shapes/embed.cljs b/frontend/src/app/main/ui/shapes/embed.cljs index 34b9f7d1a..0c4fbd259 100644 --- a/frontend/src/app/main/ui/shapes/embed.cljs +++ b/frontend/src/app/main/ui/shapes/embed.cljs @@ -25,7 +25,12 @@ (let [;; When not active the embedding we return the URI url-mapping (fn [obs] (if embed? - (rx/merge-map http/fetch-data-uri obs) + (->> obs + (rx/merge-map + (fn [uri] + (->> (http/fetch-data-uri uri true) + ;; If fetching give an error we store the URI as its `data-uri` + (rx/catch #(rx/of (hash-map uri uri))))))) (rx/map identity obs))) sub (->> (rx/from urls) diff --git a/frontend/src/app/main/ui/shapes/filters.cljs b/frontend/src/app/main/ui/shapes/filters.cljs index 1fb272c69..1bf804cce 100644 --- a/frontend/src/app/main/ui/shapes/filters.cljs +++ b/frontend/src/app/main/ui/shapes/filters.cljs @@ -113,7 +113,7 @@ filter-x (min x (+ x offset-x (- spread) (- blur) -5)) filter-y (min y (+ y offset-y (- spread) (- blur) -5)) filter-width (+ width (mth/abs offset-x) (* spread 2) (* blur 2) 10) - filter-height (+ height (mth/abs offset-x) (* spread 2) (* blur 2) 10)] + filter-height (+ height (mth/abs offset-y) (* spread 2) (* blur 2) 10)] {:x1 filter-x :y1 filter-y :x2 (+ filter-x filter-width) @@ -208,26 +208,31 @@ margin (gsh/shape-stroke-margin shape stroke-width)] (+ stroke-width margin))) +(defn change-filter-in + "Adds the previous filter as `filter-in` parameter" + [filters] + (map #(assoc %1 :filter-in %2) filters (cons nil (map :id filters)))) + (mf/defc filters [{:keys [filter-id shape]}] - (let [filters (shape->filters shape) - - ;; Adds the previous filter as `filter-in` parameter - filters (map #(assoc %1 :filter-in %2) filters (cons nil (map :id filters))) - bounds (get-filters-bounds shape filters (or (-> shape :blur :value) 0)) - padding (calculate-padding shape)] - + (let [filters (-> shape shape->filters change-filter-in) + bounds (get-filters-bounds shape filters (or (-> shape :blur :value) 0)) + padding (calculate-padding shape) + selrect (:selrect shape) + filter-x (/ (- (:x bounds) (:x selrect) padding) (:width selrect)) + filter-y (/ (- (:y bounds) (:y selrect) padding) (:height selrect)) + filter-width (/ (+ (:width bounds) (* 2 padding)) (:width selrect)) + filter-height (/ (+ (:height bounds) (* 2 padding)) (:height selrect))] [:* (when (> (count filters) 2) - [:filter {:id filter-id - :x (- (:x bounds) padding) - :y (- (:y bounds) padding) - :width (+ (:width bounds) (* 2 padding)) - :height (+ (:height bounds) (* 2 padding)) - :filterUnits "userSpaceOnUse" + [:filter {:id filter-id + :x filter-x + :y filter-y + :width filter-width + :height filter-height + :filterUnits "objectBoundingBox" :color-interpolation-filters "sRGB"} - (for [entry filters] [:& filter-entry {:entry entry}])])])) diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index d9e95c135..bf49ca432 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -10,6 +10,22 @@ [app.util.object :as obj] [rumext.alpha :as mf])) +(defn frame-clip-id + [shape render-id] + (str "frame-clip-" (:id shape) "-" render-id)) + +(defn frame-clip-url + [shape render-id] + (when (= :frame (:type shape)) + (str "url(#" (frame-clip-id shape render-id) ")"))) + +(mf/defc frame-clip-def + [{:keys [shape render-id]}] + (when (= :frame (:type shape)) + (let [{:keys [x y width height]} shape] + [:clipPath {:id (frame-clip-id shape render-id) :class "frame-clip"} + [:rect {:x x :y y :width width :height height}]]))) + (defn frame-shape [shape-wrapper] (mf/fnc frame-shape @@ -17,7 +33,7 @@ [props] (let [childs (unchecked-get props "childs") shape (unchecked-get props "shape") - {:keys [width height]} shape + {:keys [x y width height]} shape has-background? (or (some? (:fill-color shape)) (some? (:fill-color-gradient shape))) @@ -25,8 +41,8 @@ props (-> (attrs/extract-style-attrs shape) (obj/merge! - #js {:x 0 - :y 0 + #js {:x x + :y y :width width :height height :className "frame-background"}))] @@ -34,7 +50,6 @@ (when (or has-background? has-stroke?) [:> :rect props]) (for [item childs] - [:& shape-wrapper {:frame shape - :shape item + [:& shape-wrapper {:shape item :key (:id item)}])]))) diff --git a/frontend/src/app/main/ui/shapes/group.cljs b/frontend/src/app/main/ui/shapes/group.cljs index 49a628350..69bc4ee58 100644 --- a/frontend/src/app/main/ui/shapes/group.cljs +++ b/frontend/src/app/main/ui/shapes/group.cljs @@ -17,8 +17,7 @@ (mf/fnc group-shape {::mf/wrap-props false} [props] - (let [frame (unchecked-get props "frame") - shape (unchecked-get props "shape") + (let [shape (unchecked-get props "shape") childs (unchecked-get props "childs") render-id (mf/use-ctx muc/render-ctx) masked-group? (:masked-group? shape) @@ -46,11 +45,10 @@ [:> clip-wrapper clip-props [:> mask-wrapper mask-props (when masked-group? - [:> render-mask #js {:frame frame :mask mask}]) + [:> render-mask #js {:mask mask}]) (for [item childs] - [:& shape-wrapper {:frame frame - :shape item + [:& shape-wrapper {:shape item :key (:id item)}])]])))) diff --git a/frontend/src/app/main/ui/shapes/mask.cljs b/frontend/src/app/main/ui/shapes/mask.cljs index 7220aceaa..30a5ed70f 100644 --- a/frontend/src/app/main/ui/shapes/mask.cljs +++ b/frontend/src/app/main/ui/shapes/mask.cljs @@ -34,13 +34,9 @@ (mf/fnc mask-shape {::mf/wrap-props false} [props] - (let [frame (unchecked-get props "frame") - mask (unchecked-get props "mask") + (let [mask (unchecked-get props "mask") render-id (mf/use-ctx muc/render-ctx) - - mask' (-> mask - (gsh/transform-shape) - (gsh/translate-to-frame frame))] + mask' (gsh/transform-shape mask)] [:defs [:filter {:id (filter-id render-id mask)} [:feFlood {:flood-color "white" @@ -52,13 +48,13 @@ ;; Clip path is necessary so the elements inside the mask won't affect ;; the events outside. Clip hides the elements but mask doesn't (like display vs visibility) ;; we cannot use clips instead of mask because clips can only be simple shapes - [:clipPath {:id (clip-id render-id mask)} + [:clipPath {:class "mask-clip-path" + :id (clip-id render-id mask)} [:polyline {:points (->> (:points mask') (map #(str (:x %) "," (:y %))) (str/join " "))}]] - [:mask {:id (mask-id render-id mask)} + [:mask {:class "mask-shape" + :id (mask-id render-id mask)} [:g {:filter (filter-url render-id mask)} - [:& shape-wrapper {:frame frame - :shape (-> mask - (dissoc :shadow :blur))}]]]]))) + [:& shape-wrapper {:shape (dissoc mask :shadow :blur)}]]]]))) diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index 5b68b7ee9..961a30e29 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -14,6 +14,7 @@ [app.main.ui.shapes.export :as ed] [app.main.ui.shapes.fill-image :as fim] [app.main.ui.shapes.filters :as filters] + [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.gradients :as grad] [app.main.ui.shapes.svg-defs :as defs] [app.util.object :as obj] @@ -26,6 +27,8 @@ (let [shape (obj/get props "shape") children (obj/get props "children") pointer-events (obj/get props "pointer-events") + + type (:type shape) render-id (mf/use-memo #(str (uuid/next))) filter-id (str "filter_" render-id) styles (-> (obj/new) @@ -34,10 +37,6 @@ (cond-> (and (:blend-mode shape) (not= (:blend-mode shape) :normal)) (obj/set! "mixBlendMode" (d/name (:blend-mode shape))))) - {:keys [x y width height type]} shape - frame? (= :frame type) - group? (= :group type) - include-metadata? (mf/use-ctx ed/include-metadata-ctx) wrapper-props @@ -50,26 +49,14 @@ wrapper-props (cond-> wrapper-props - frame? - (-> (obj/set! "x" x) - (obj/set! "y" y) - (obj/set! "width" width) - (obj/set! "height" height) - (obj/set! "xmlns" "http://www.w3.org/2000/svg") - (obj/set! "xmlnsXlink" "http://www.w3.org/1999/xlink") - (cond-> - include-metadata? - (obj/set! "xmlns:penpot" "https://penpot.app/xmlns")))) + (= :frame type) + (obj/set! "clipPath" (frame/frame-clip-url shape render-id)) - wrapper-props - (cond-> wrapper-props - group? - (attrs/add-style-attrs shape)) - - wrapper-tag (if frame? "svg" "g")] + (= :group type) + (attrs/add-style-attrs shape))] [:& (mf/provider muc/render-ctx) {:value render-id} - [:> wrapper-tag wrapper-props + [:> :g wrapper-props (when include-metadata? [:& ed/export-data {:shape shape}]) @@ -79,5 +66,6 @@ [:& grad/gradient {:shape shape :attr :fill-color-gradient}] [:& grad/gradient {:shape shape :attr :stroke-color-gradient}] [:& fim/fill-image-pattern {:shape shape :render-id render-id}] - [:& cs/stroke-defs {:shape shape :render-id render-id}]] + [:& cs/stroke-defs {:shape shape :render-id render-id}] + [:& frame/frame-clip-def {:shape shape :render-id render-id}]] children]])) diff --git a/frontend/src/app/main/ui/shapes/svg_raw.cljs b/frontend/src/app/main/ui/shapes/svg_raw.cljs index 407f5096d..42b797be8 100644 --- a/frontend/src/app/main/ui/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/shapes/svg_raw.cljs @@ -88,8 +88,7 @@ {::mf/wrap-props false} [props] - (let [frame (unchecked-get props "frame") - shape (unchecked-get props "shape") + (let [shape (unchecked-get props "shape") childs (unchecked-get props "childs") {:keys [content]} shape @@ -103,12 +102,12 @@ svg-root? [:& svg-root {:shape shape} (for [item childs] - [:& shape-wrapper {:frame frame :shape item :key (:id item)}])] + [:& shape-wrapper {:shape item :key (:id item)}])] svg-tag? [:& svg-element {:shape shape} (for [item childs] - [:& shape-wrapper {:frame frame :shape item :key (:id item)}])] + [:& shape-wrapper {:shape item :key (:id item)}])] svg-leaf? content diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index a6c205658..315d697a9 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -12,7 +12,6 @@ others are defined using a generic wrapper implemented in common." (:require - [app.common.geom.shapes :as geom] [app.common.pages :as cp] [app.common.uuid :as uuid] [app.main.refs :as refs] @@ -81,16 +80,12 @@ ::mf/wrap-props false} [props] (let [shape (obj/get props "shape") - frame (obj/get props "frame") - shape (geom/translate-to-frame shape frame) - - opts #js {:shape shape - :frame frame} + opts #js {:shape shape} svg-element? (and (= (:type shape) :svg-raw) (not= :svg (get-in shape [:content :tag])))] - (when (and shape (not (:hidden shape))) + (when (and (some? shape) (not (:hidden shape))) [:* (if-not svg-element? (case (:type shape) @@ -104,7 +99,7 @@ :bool [:> bool-wrapper opts] ;; Only used when drawing a new frame. - :frame [:> frame-wrapper {:shape shape}] + :frame [:> frame-wrapper opts] nil) @@ -112,7 +107,7 @@ [:> svg-raw-wrapper opts]) (when (debug? :bounding-boxes) - [:& bounding-box {:shape shape :frame frame}])]))) + [:> bounding-box opts])]))) (def group-wrapper (group/group-wrapper-factory shape-wrapper)) (def svg-raw-wrapper (svg-raw/svg-raw-wrapper-factory shape-wrapper)) diff --git a/frontend/src/app/main/ui/workspace/shapes/bool.cljs b/frontend/src/app/main/ui/workspace/shapes/bool.cljs index 2cc8f6e00..416aed505 100644 --- a/frontend/src/app/main/ui/workspace/shapes/bool.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/bool.cljs @@ -27,12 +27,10 @@ [shape-wrapper] (let [shape-component (bool/bool-shape shape-wrapper)] (mf/fnc bool-wrapper - {::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "frame"]))] + {::mf/wrap [#(mf/memo' % (mf/check-props ["shape"]))] ::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") - frame (unchecked-get props "frame") - childs-ref (mf/use-memo (mf/deps (:id shape)) #(refs/select-children (:id shape))) @@ -41,7 +39,6 @@ [:> shape-container {:shape shape} [:& shape-component - {:frame frame - :shape shape + {:shape shape :childs childs}]])))) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index c13e95a25..f3fad5640 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -7,7 +7,6 @@ (ns app.main.ui.workspace.shapes.frame (:require [app.common.data :as d] - [app.common.geom.shapes :as gsh] [app.common.pages :as cp] [app.main.ui.hooks :as hooks] [app.main.ui.shapes.frame :as frame] diff --git a/frontend/src/app/main/ui/workspace/shapes/group.cljs b/frontend/src/app/main/ui/workspace/shapes/group.cljs index 745523a1b..18c1237ae 100644 --- a/frontend/src/app/main/ui/workspace/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/group.cljs @@ -27,18 +27,15 @@ [shape-wrapper] (let [group-shape (group/group-shape shape-wrapper)] (mf/fnc group-wrapper - {::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "frame"]))] + {::mf/wrap [#(mf/memo' % (mf/check-props ["shape"]))] ::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") - frame (unchecked-get props "frame") - - childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape) {:with-modifiers? true})) + childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape))) childs (mf/deref childs-ref)] [:> shape-container {:shape shape} [:& group-shape - {:frame frame - :shape shape + {:shape shape :childs childs}]])))) diff --git a/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs b/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs index dda2e33b3..22919126a 100644 --- a/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs @@ -15,12 +15,10 @@ [shape-wrapper] (let [svg-raw-shape (svg-raw/svg-raw-shape shape-wrapper)] (mf/fnc svg-raw-wrapper - {::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "frame"]))] + {::mf/wrap [#(mf/memo' % (mf/check-props ["shape"]))] ::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") - frame (unchecked-get props "frame") - childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape))) childs (mf/deref childs-ref)] @@ -28,11 +26,9 @@ (if (or (= (get-in shape [:content :tag]) :svg) (and (contains? shape :svg-attrs) (map? (:content shape)))) [:> shape-container {:shape shape} - [:& svg-raw-shape {:frame frame - :shape shape + [:& svg-raw-shape {:shape shape :childs childs}]] - [:& svg-raw-shape {:frame frame - :shape shape + [:& svg-raw-shape {:shape shape :childs childs}]))))) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index bf605d83d..52bbb14b0 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -43,7 +43,6 @@ ;; that the new parameter is sent {:keys [edit-path edition - modifiers options-mode panning picking-color? @@ -62,10 +61,10 @@ drawing (mf/deref refs/workspace-drawing) options (mf/deref refs/workspace-page-options) base-objects (mf/deref refs/workspace-page-objects) - object-modifiers (mf/deref refs/workspace-modifiers) - objects (mf/use-memo - (mf/deps base-objects object-modifiers) - #(gsh/merge-modifiers base-objects object-modifiers)) + modifiers (mf/deref refs/workspace-modifiers) + objects-modified (mf/use-memo + (mf/deps base-objects modifiers) + #(gsh/merge-modifiers base-objects modifiers)) background (get options :background clr/canvas) ;; STATE @@ -81,7 +80,6 @@ ;; REFS viewport-ref (mf/use-ref nil) - render-ref (mf/use-ref nil) ;; VARS disable-paste (mf/use-var false) @@ -94,25 +92,22 @@ drawing-tool (:tool drawing) drawing-obj (:object drawing) - selected-shapes (into [] - (comp (map #(get objects %)) - (filter some?)) - selected) + xf-select-shape (comp (map (d/getf objects-modified)) (filter some?)) + selected-shapes (into [] xf-select-shape selected) selected-frames (into #{} (map :frame-id) selected-shapes) ;; Only when we have all the selected shapes in one frame - selected-frame (when (= (count selected-frames) 1) (get objects (first selected-frames))) - + selected-frame (when (= (count selected-frames) 1) (get base-objects (first selected-frames))) create-comment? (= :comments drawing-tool) drawing-path? (or (and edition (= :draw (get-in edit-path [edition :edit-mode]))) (and (some? drawing-obj) (= :path (:type drawing-obj)))) - node-editing? (and edition (not= :text (get-in objects [edition :type]))) - text-editing? (and edition (= :text (get-in objects [edition :type]))) + node-editing? (and edition (not= :text (get-in base-objects [edition :type]))) + text-editing? (and edition (= :text (get-in base-objects [edition :type]))) on-click (actions/on-click hover selected edition drawing-path? drawing-tool) on-context-menu (actions/on-context-menu hover) - on-double-click (actions/on-double-click hover hover-ids drawing-path? objects edition) + on-double-click (actions/on-double-click hover hover-ids drawing-path? base-objects edition) on-drag-enter (actions/on-drag-enter) on-drag-over (actions/on-drag-over) on-drop (actions/on-drop file viewport-ref zoom) @@ -155,10 +150,10 @@ (hooks/setup-cursor cursor alt? panning drawing-tool drawing-path? node-editing?) (hooks/setup-resize layout viewport-ref) (hooks/setup-keyboard alt? ctrl? space?) - (hooks/setup-hover-shapes page-id move-stream objects transform selected ctrl? hover hover-ids @hover-disabled? zoom) - (hooks/setup-viewport-modifiers modifiers selected objects render-ref) + (hooks/setup-hover-shapes page-id move-stream base-objects transform selected ctrl? hover hover-ids @hover-disabled? zoom) + (hooks/setup-viewport-modifiers modifiers base-objects) (hooks/setup-shortcuts node-editing? drawing-path?) - (hooks/setup-active-frames objects vbox hover active-frames) + (hooks/setup-active-frames base-objects vbox hover active-frames) [:div.viewport [:div.viewport-overlays @@ -184,7 +179,6 @@ [:& widgets/viewport-actions]] [:svg.render-shapes {:id "render" - :ref render-ref :xmlns "http://www.w3.org/2000/svg" :xmlnsXlink "http://www.w3.org/1999/xlink" :xmlns:penpot "https://penpot.app/xmlns" @@ -202,7 +196,7 @@ [:& (mf/provider embed/context) {:value true} ;; Render root shape [:& shapes/root-shape {:key page-id - :objects objects + :objects base-objects :active-frames @active-frames}]]]] [:svg.viewport-controls @@ -234,7 +228,7 @@ [:g {:style {:pointer-events (if disable-events? "none" "auto")}} (when show-outlines? [:& outline/shape-outlines - {:objects objects + {:objects base-objects :selected selected :hover (when (not= :frame (:type @hover)) #{(or @frame-hover (:id @hover))}) @@ -259,10 +253,10 @@ :zoom zoom}]) (when text-editing? - [:& editor/text-shape-edit {:shape (get objects edition)}]) + [:& editor/text-shape-edit {:shape (get base-objects edition)}]) [:& widgets/frame-titles - {:objects objects + {:objects objects-modified :selected selected :zoom zoom :modifiers modifiers @@ -273,7 +267,7 @@ (when show-prototypes? [:& widgets/frame-flows {:flows (:flows options) - :objects objects + :objects base-objects :selected selected :zoom zoom :modifiers modifiers @@ -310,7 +304,7 @@ :zoom zoom :page-id page-id :selected selected - :objects objects + :objects base-objects :modifiers modifiers}]) (when show-snap-distance? diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index c1869680b..2ff6e1744 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.workspace.viewport.hooks (:require + [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.common.pages :as cp] [app.main.data.shortcuts :as dsc] @@ -169,22 +170,31 @@ (reset! hover hover-shape) (reset! hover-ids ids)))))) -(defn setup-viewport-modifiers [modifiers selected objects render-ref] - (let [roots (mf/use-memo - (mf/deps objects selected) - (fn [] - (let [roots-ids (cp/clean-loops objects selected)] - (->> roots-ids (mapv #(get objects %))))))] +(defn setup-viewport-modifiers + [modifiers objects] + (let [transforms + (mf/use-memo + (mf/deps modifiers) + (fn [] + (d/mapm (fn [id {modifiers :modifiers}] + (let [center (gsh/center-shape (get objects id))] + (gsh/modifiers->transform center modifiers))) + modifiers))) + + shapes + (mf/use-memo + (mf/deps transforms) + (fn [] + (->> (keys transforms) + (mapv (d/getf objects)))))] ;; Layout effect is important so the code is executed before the modifiers ;; are applied to the shape (mf/use-layout-effect - (mf/deps modifiers roots) - - #(when-let [render-node (mf/ref-val render-ref)] - (if modifiers - (utils/update-transform render-node roots modifiers) - (utils/remove-transform render-node roots)))))) + (mf/deps transforms) + (fn [] + (utils/update-transform shapes transforms modifiers) + #(utils/remove-transform shapes))))) (defn inside-vbox [vbox objects frame-id] (let [frame (get objects frame-id)] diff --git a/frontend/src/app/main/ui/workspace/viewport/selection.cljs b/frontend/src/app/main/ui/workspace/viewport/selection.cljs index 71a5c1d80..cf9ed2d8c 100644 --- a/frontend/src/app/main/ui/workspace/viewport/selection.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/selection.cljs @@ -320,7 +320,9 @@ (let [shape-id (:id shape) shape (geom/transform-shape shape {:round-coords? false}) - shape' (if (debug? :simple-selection) (geom/setup {:type :rect} (geom/selection-rect [shape])) shape) + shape' (if (debug? :simple-selection) + (geom/setup {:type :rect} (geom/selection-rect [shape])) + shape) on-resize (fn [current-position _initial-position event] diff --git a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs index 331f25cc6..ec95528ee 100644 --- a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs @@ -58,7 +58,7 @@ (->> shapes (first))) shape (if modifiers - (-> shape (assoc :modifiers modifiers) gsh/transform-shape) + (-> shape (merge (get modifiers (:id shape))) gsh/transform-shape) shape) frame-id (snap/snap-frame-id shapes)] diff --git a/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs b/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs index 85a9ab316..a3a992e6e 100644 --- a/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs @@ -32,8 +32,16 @@ loading-node (when frame-node (dom/query frame-node "[data-loading=\"true\"]"))] (if (and (some? frame-node) (not (some? loading-node))) - (let [xml (-> (js/XMLSerializer.) - (.serializeToString frame-node) + (let [frame-html (-> (js/XMLSerializer.) + (.serializeToString frame-node)) + + ;; We need to wrap the group node into a SVG with a viewbox that matches the selrect of the frame + svg-node (.createElementNS js/document "http://www.w3.org/2000/svg" "svg") + _ (.setAttribute svg-node "version" "1.1") + _ (.setAttribute svg-node "viewBox" (str (:x shape) " " (:y shape) " " (:width shape) " " (:height shape))) + _ (unchecked-set svg-node "innerHTML" frame-html) + xml (-> (js/XMLSerializer.) + (.serializeToString svg-node) js/encodeURIComponent js/unescape js/btoa) diff --git a/frontend/src/app/main/ui/workspace/viewport/utils.cljs b/frontend/src/app/main/ui/workspace/viewport/utils.cljs index 384f38117..a7e15507c 100644 --- a/frontend/src/app/main/ui/workspace/viewport/utils.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/utils.cljs @@ -7,35 +7,89 @@ (ns app.main.ui.workspace.viewport.utils (:require [app.common.data :as d] + [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.main.ui.cursors :as cur] [app.util.dom :as dom] [cuerdas.core :as str])) -;; TODO: looks like first argument is not necessary. -(defn update-transform [_node shapes modifiers] - (doseq [{:keys [id type]} shapes] - (let [shape-node (dom/get-element (str "shape-" id)) +(defn- text-corrected-transform + "If we apply a scale directly to the texts it will show deformed so we need to create this + correction matrix to \"undo\" the resize but keep the other transformations." + [{:keys [points transform transform-inverse]} current-transform modifiers] - ;; When the shape is a frame we maybe need to move its thumbnail - thumb-node (dom/get-element (str "thumbnail-" id))] - (when-let [node (cond - (and (some? shape-node) (= :frame type)) - (.-parentNode shape-node) + (let [corner-pt (first points) + transform (or transform (gmt/matrix)) + transform-inverse (or transform-inverse (gmt/matrix)) - (and (some? thumb-node) (= :frame type)) - (.-parentNode thumb-node) + current-transform + (if (some? (:resize-vector modifiers)) + (gmt/multiply + current-transform + transform + (gmt/scale-matrix (gpt/inverse (:resize-vector modifiers)) (gpt/transform corner-pt transform-inverse)) + transform-inverse) + current-transform) - :else - shape-node)] - (dom/set-attribute node "transform" (str (:displacement modifiers))))))) + current-transform + (if (some? (:resize-vector-2 modifiers)) + (gmt/multiply + current-transform + transform + (gmt/scale-matrix (gpt/inverse (:resize-vector-2 modifiers)) (gpt/transform corner-pt transform-inverse)) + transform-inverse) + current-transform)] + current-transform)) -;; TODO: looks like first argument is not necessary. -(defn remove-transform [_node shapes] - (doseq [{:keys [id type]} shapes] - (when-let [node (dom/get-element (str "shape-" id))] - (let [node (if (= :frame type) (.-parentNode node) node)] - (dom/remove-attribute node "transform"))))) +(defn get-nodes + "Retrieve the DOM nodes to apply the matrix transformation" + [{:keys [id type masked-group?]}] + (let [shape-node (dom/get-element (str "shape-" id)) + + frame? (= :frame type) + group? (= :group type) + mask? (and group? masked-group?) + + ;; When the shape is a frame we maybe need to move its thumbnail + thumb-node (when frame? (dom/get-element (str "thumbnail-" id)))] + (cond + (some? thumb-node) + [(.-parentNode thumb-node)] + + (and (some? shape-node) frame?) + [(dom/query shape-node ".frame-background") + (dom/query shape-node ".frame-clip")] + + ;; For groups we don't want to transform the whole group but only + ;; its filters/masks + (and (some? shape-node) mask?) + [(dom/query shape-node ".mask-clip-path") + (dom/query shape-node ".mask-shape")] + + group? + [] + + :else + [shape-node]))) + +(defn update-transform [shapes transforms modifiers] + (doseq [{id :id :as shape} shapes] + (when-let [nodes (get-nodes shape)] + (let [transform (get transforms id) + modifiers (get-in modifiers [id :modifiers]) + transform (case type + :text (text-corrected-transform shape transform modifiers) + transform)] + (doseq [node nodes] + (when (and (some? transform) (some? node)) + (dom/set-attribute node "transform" (str transform)))))))) + +(defn remove-transform [shapes] + (doseq [shape shapes] + (when-let [nodes (get-nodes shape)] + (doseq [node nodes] + (when (some? node) + (dom/remove-attribute node "transform")))))) (defn format-viewbox [vbox] (str/join " " [(+ (:x vbox 0) (:left-offset vbox 0)) diff --git a/frontend/src/app/main/worker.cljs b/frontend/src/app/main/worker.cljs index c9004a9f0..401d2822d 100644 --- a/frontend/src/app/main/worker.cljs +++ b/frontend/src/app/main/worker.cljs @@ -13,18 +13,22 @@ [error] (js/console.error "Error on worker" error)) -(defonce instance - (when (not= *target* "nodejs") - (uw/init cfg/worker-uri on-error))) +(defonce instance (atom nil)) + +(defn init! + [] + (reset! + instance + (uw/init cfg/worker-uri on-error))) (defn ask! [message] - (uw/ask! instance message)) + (when @instance (uw/ask! @instance message))) (defn ask-buffered! [message] - (uw/ask-buffered! instance message)) + (when @instance (uw/ask-buffered! @instance message))) (defn ask-many! [message] - (uw/ask-many! instance message)) + (when @instance (uw/ask-many! @instance message))) diff --git a/frontend/src/app/util/http.cljs b/frontend/src/app/util/http.cljs index 78c4817f4..151311994 100644 --- a/frontend/src/app/util/http.cljs +++ b/frontend/src/app/util/http.cljs @@ -162,16 +162,30 @@ (->> (rx/take 1 observable) (rx/subs resolve reject))))) -(defn fetch-data-uri [uri] - (c/with-cache {:key uri :max-age (dt/duration {:hours 4})} - (->> (send! {:method :get - :uri uri - :response-type :blob - :omit-default-headers true}) - (rx/filter #(= 200 (:status %))) - (rx/map :body) - (rx/mapcat wapi/read-file-as-data-url) - (rx/map #(hash-map uri %))))) +(defn fetch-data-uri + ([uri] + (fetch-data-uri uri false)) + + ([uri throw-err?] + (c/with-cache {:key uri :max-age (dt/duration {:hours 4})} + (let [request-stream + (send! {:method :get + :uri uri + :response-type :blob + :omit-default-headers true}) + + request-stream + (if throw-err? + (rx/tap #(when-not (and (>= (:status %) 200) (< (:status %) 300)) + ;; HTTP ERRROR + (throw (js/Error. "Error fetching data uri" #js {:cause (clj->js %)}))) + request-stream) + (rx/filter #(= 200 (:status %)) + request-stream))] + (->> request-stream + (rx/map :body) + (rx/mapcat wapi/read-file-as-data-url) + (rx/map #(hash-map uri %))))))) (defn fetch-text [url] (c/with-cache {:key url :max-age (dt/duration {:hours 4})} diff --git a/frontend/src/app/util/perf.cljs b/frontend/src/app/util/perf.cljs index 8db428e04..87a0273be 100644 --- a/frontend/src/app/util/perf.cljs +++ b/frontend/src/app/util/perf.cljs @@ -105,3 +105,29 @@ :onRender on-render} children] children))) + +(defn benchmark + [& {:keys [f warmup iterations name] + :or {iterations 10000}}] + (let [end-mark (str name ":end")] + (println "=> benchmarking:" name) + (println "--> warming up:" iterations) + (loop [i iterations] + (when (pos? i) + (f) + (recur (dec i)))) + (println "--> benchmarking:" iterations) + (js/performance.mark name) + (loop [i iterations] + (when (pos? i) + (f) + (recur (dec i)))) + (js/performance.measure end-mark name) + (let [[result] (js/performance.getEntriesByName end-mark) + duration (mth/precision (.-duration ^js result) 4) + avg (mth/precision (/ duration iterations) 4)] + (println "--> TOTAL:" (str duration "ms") "AVG:" (str avg "ms")) + (js/performance.clearMarks name) + (js/performance.clearMeasures end-mark) + #js {:duration duration + :avg avg}))) diff --git a/frontend/src/app/worker/export.cljs b/frontend/src/app/worker/export.cljs index ba4b01cef..1718cbbdb 100644 --- a/frontend/src/app/worker/export.cljs +++ b/frontend/src/app/worker/export.cljs @@ -19,6 +19,8 @@ [beicon.core :as rx] [cuerdas.core :as str])) +(def ^:const current-version 2) + (defn create-manifest "Creates a manifest entry for the given files" [team-id file-id export-type files] @@ -41,6 +43,7 @@ :shared is-shared :pages pages :pagesIndex index + :version current-version :libraries (->> (:libraries file) (into #{}) (mapv str)) :exportType (d/name export-type) :hasComponents (d/not-empty? (get-in file [:data :components])) diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index 04c9521f3..5849ca22c 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -9,6 +9,8 @@ (:require [app.common.data :as d] [app.common.file-builder :as fb] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as gpa] [app.common.logging :as log] [app.common.pages :as cp] [app.common.text :as ct] @@ -229,6 +231,21 @@ (cond-> (= type :text) (d/update-when :content resolve-text-content context))))) +(defn- translate-frame + [data type file] + (let [frame-id (:current-frame-id file) + frame (when (and (some? frame-id) (not= frame-id uuid/zero)) + (fb/lookup-shape file frame-id))] + + (if (some? frame) + (-> data + (d/update-when :x + (:x frame)) + (d/update-when :y + (:y frame)) + (cond-> (= :path type) + (update :content gpa/move-content (gpt/point (:x frame) (:y frame))))) + + data))) + (defn process-import-node [context file node] @@ -250,7 +267,9 @@ data (-> (cip/parse-data type node) (resolve-data-ids type context) (cond-> (some? old-id) - (assoc :id (resolve old-id)))) + (assoc :id (resolve old-id))) + (cond-> (< (:version context 1) 2) + (translate-frame type file))) file (case type :frame (fb/add-artboard file data) @@ -463,6 +482,7 @@ (rx/flat-map (fn [context] (->> (create-file context) + (rx/tap #(.log js/console "create-file" (clj->js %))) (rx/map #(vector % (first (get data (:file-id context))))))))) (->> (rx/from files) diff --git a/frontend/src/debug.cljs b/frontend/src/debug.cljs index d3c0e8dbf..460985b7d 100644 --- a/frontend/src/debug.cljs +++ b/frontend/src/debug.cljs @@ -5,13 +5,14 @@ ;; Copyright (c) UXBOX Labs SL (ns debug - (:import [goog.math AffineTransform]) + #_(:import [goog.math AffineTransform]) (:require [app.common.data :as d] - [app.common.geom.matrix :as gmt] + #_[app.common.geom.matrix :as gmt] [app.common.math :as mth] [app.common.pages :as cp] - [app.common.perf :as perf] + #_[app.common.perf :as perf] + [app.common.uuid :as uuid] [app.main.store :as st] [app.util.object :as obj] [app.util.timers :as timers] @@ -20,7 +21,32 @@ [cuerdas.core :as str] [potok.core :as ptk])) -(def debug-options #{:bounding-boxes :group :events :rotation-handler :resize-handler :selection-center :export :import #_:simple-selection}) +(def debug-options + #{;; Displays the bounding box for the shapes + :bounding-boxes + + ;; Displays an overlay over the groups + :group + + ;; Displays in the console log the events through the application + :events + + ;; Display the boxes that represent the rotation handlers + :rotation-handler + + ;; Display the boxes that represent the resize handlers + :resize-handler + + ;; Displays the center of a selection + :selection-center + + ;; When active the single selection will not take into account previous transformations + ;; this is useful to debug transforms + :simple-selection + + ;; When active the thumbnails will be displayed with a sepia filter + :thumbnails + }) ;; These events are excluded when we activate the :events flag (def debug-exclude-events @@ -109,27 +135,40 @@ (do-thing))) (defn ^:export dump-state [] - (logjs "state" @st/state)) + (logjs "state" @st/state) + nil) (defn ^:export dump-buffer [] - (logjs "state" @st/last-events)) + (logjs "state" @st/last-events) + nil) (defn ^:export get-state [str-path] (let [path (->> (str/split str-path " ") (map d/read-string))] - (clj->js (get-in @st/state path)))) + (clj->js (get-in @st/state path))) + nil) (defn ^:export dump-objects [] - (let [page-id (get @st/state :current-page-id)] - (logjs "state" (get-in @st/state [:workspace-data :pages-index page-id :objects])))) + (let [page-id (get @st/state :current-page-id) + objects (get-in @st/state [:workspace-data :pages-index page-id :objects])] + (logjs "objects" objects) + nil)) (defn ^:export dump-object [name] (let [page-id (get @st/state :current-page-id) objects (get-in @st/state [:workspace-data :pages-index page-id :objects]) - target (or (d/seek (fn [[_ shape]] (= name (:name shape))) objects) - (get objects (uuid name)))] - (->> target - (logjs "state")))) + result (or (d/seek (fn [[_ shape]] (= name (:name shape))) objects) + (get objects (uuid/uuid name)))] + (logjs name result) + nil)) + +(defn ^:export dump-selected [] + (let [page-id (get @st/state :current-page-id) + objects (get-in @st/state [:workspace-data :pages-index page-id :objects]) + selected (get-in @st/state [:workspace-local :selected]) + result (->> selected (map (d/getf objects)))] + (logjs "selected" result) + nil)) (defn ^:export dump-tree ([] (dump-tree false false)) @@ -212,7 +251,7 @@ (not (debug-exclude-events (ptk/type s)))))) (rx/subs #(println "[stream]: " (ptk/repr-event %)))))) -(defn ^:export bench-matrix +#_(defn ^:export bench-matrix [] (let [iterations 1000000 @@ -247,3 +286,6 @@ (println "Clojure matrix. Total: " m1 " (" (/ m1 iterations) ")") (println "Clojure matrix (NEW). Total: " m2 " (" (/ m2 iterations) ")") (println "Affine transform (with new). Total: " m3 " (" (/ m3 iterations) ")"))) + + +