diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index 4ca786b2c..07ee421cd 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -260,6 +260,7 @@ (def ^:private sql:team-shared-files "select f.id, + f.revn, f.project_id, f.created_at, f.modified_at, @@ -330,6 +331,7 @@ (def sql:team-recent-files "with recent_files as ( select f.id, + f.revn, f.project_id, f.created_at, f.modified_at, 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/matrix.cljc b/common/src/app/common/geom/matrix.cljc index c04c05bb3..d262e3d77 100644 --- a/common/src/app/common/geom/matrix.cljc +++ b/common/src/app/common/geom/matrix.cljc @@ -36,6 +36,33 @@ (apply matrix params))) (defn multiply + ([m1 m2] + (let [m1a (.-a m1) + m1b (.-b m1) + m1c (.-c m1) + m1d (.-d m1) + m1e (.-e m1) + m1f (.-f m1) + + m2a (.-a m2) + m2b (.-b m2) + m2c (.-c m2) + m2d (.-d m2) + m2e (.-e m2) + m2f (.-f m2)] + + (Matrix. + (+ (* m1a m2a) (* m1c m2b)) + (+ (* m1b m2a) (* m1d m2b)) + (+ (* m1a m2c) (* m1c m2d)) + (+ (* m1b m2c) (* m1d m2d)) + (+ (* m1a m2e) (* m1c m2f) m1e) + (+ (* m1b m2e) (* m1d m2f) m1f)))) + + ([m1 m2 & others] + (reduce multiply (multiply m1 m2) others))) + +(defn -old-multiply ([{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f} {m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f}] (Matrix. @@ -46,20 +73,15 @@ (+ (* m1a m2e) (* m1c m2f) m1e) (+ (* m1b m2e) (* m1d m2f) m1f))) ([m1 m2 & others] - (reduce multiply (multiply m1 m2) others))) + (reduce multiply (-old-multiply m1 m2) others))) (defn add-translate "Given two TRANSLATE matrixes (only e and f have significative values), combine them. Quicker than multiplying them, for this precise case." ([{m1e :e m1f :f} {m2e :e m2f :f}] - (Matrix. - 1 - 0 - 0 - 1 - (+ m1e m2e) - (+ m1f m2f))) + (Matrix. 1 0 0 1 (+ m1e m2e) (+ m1f m2f))) + ([m1 m2 & others] (reduce add-translate (add-translate m1 m2) others))) diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index 82d5dd6c7..6df3f748e 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) @@ -179,3 +184,4 @@ ;; Bool (d/export gsb/update-bool-selrect) +(d/export gsb/calc-bool-content) diff --git a/common/src/app/common/geom/shapes/bool.cljc b/common/src/app/common/geom/shapes/bool.cljc index 93b7ccc72..4d5bdb401 100644 --- a/common/src/app/common/geom/shapes/bool.cljc +++ b/common/src/app/common/geom/shapes/bool.cljc @@ -6,21 +6,31 @@ (ns app.common.geom.shapes.bool (:require + [app.common.data :as d] [app.common.geom.shapes.path :as gsp] [app.common.geom.shapes.rect :as gpr] [app.common.geom.shapes.transforms :as gtr] [app.common.path.bool :as pb] [app.common.path.shapes-to-path :as stp])) +(defn calc-bool-content + [shape objects] + + (let [extract-content-xf + (comp (map (d/getf objects)) + (filter (comp not :hidden)) + (map #(stp/convert-to-path % objects)) + (map :content)) + + shapes-content + (into [] extract-content-xf (:shapes shape))] + (pb/content-bool (:bool-type shape) shapes-content))) + (defn update-bool-selrect "Calculates the selrect+points for the boolean shape" [shape children objects] - (let [content (->> children - (map #(stp/convert-to-path % objects)) - (mapv :content) - (pb/content-bool (:bool-type shape))) - + (let [content (calc-bool-content shape objects) [points selrect] (if (empty? content) (let [selrect (gtr/selection-rect children) @@ -29,4 +39,6 @@ (gsp/content->points+selrect shape content))] (-> shape (assoc :selrect selrect) - (assoc :points points)))) + (assoc :points points) + (assoc :bool-content content)))) + 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..062f4ea46 100644 --- a/common/src/app/common/geom/shapes/path.cljc +++ b/common/src/app/common/geom/shapes/path.cljc @@ -279,11 +279,19 @@ (filterv #(and (>= % 0) (<= % 1))))))) (defn command->point - ([command] (command->point command nil)) - ([{params :params} coord] - (let [prefix (if coord (name coord) "") - xkey (keyword (str prefix "x")) - ykey (keyword (str prefix "y")) + ([command] + (command->point command nil)) + + ([command coord] + (let [params (:params command) + xkey (case coord + :c1 :c1x + :c2 :c2x + :x) + ykey (case coord + :c1 :c1y + :c2 :c2y + :y) x (get params xkey) y (get params ykey)] (when (and (some? x) (some? y)) @@ -322,7 +330,7 @@ (command->point command :c1) (command->point command :c2)]] (->> (curve-extremities curve) - (map #(curve-values curve %))))) + (mapv #(curve-values curve %))))) []) selrect (gpr/points->selrect points)] (-> selrect @@ -361,25 +369,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 +400,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] @@ -675,8 +684,6 @@ (curve-roots c2' :y))) - - (defn ray-line-intersect [point [a b :as line]] @@ -707,20 +714,19 @@ [[l1-t] [l2-t]]))) (defn ray-curve-intersect - [ray-line c2] + [ray-line curve] - (let [;; ray-line [point (gpt/point (inc (:x point)) (:y point))] - curve-ts (->> (line-curve-crossing ray-line c2) - (filterv #(let [curve-v (curve-values c2 %) - curve-tg (curve-tangent c2 %) + (let [curve-ts (->> (line-curve-crossing ray-line curve) + (filterv #(let [curve-v (curve-values curve %) + curve-tg (curve-tangent curve %) curve-tg-angle (gpt/angle curve-tg) ray-t (get-line-tval ray-line curve-v)] (and (> ray-t 0) (> (mth/abs (- curve-tg-angle 180)) 0.01) (> (mth/abs (- curve-tg-angle 0)) 0.01)) )))] (->> curve-ts - (mapv #(vector (curve-values c2 %) - (curve-windup c2 %)))))) + (mapv #(vector (curve-values curve %) + (curve-windup curve %)))))) (defn line-curve-intersect [l1 c2] @@ -816,32 +822,58 @@ (->> content (some inside-border?)))) -(defn is-point-in-content? - [point content] - (let [selrect (content->selrect content) - ray-line [point (gpt/point (inc (:x point)) (:y point))] +(defn close-content + [content] + (into [] + (comp (filter sp/is-closed?) + (mapcat :data)) + (->> content + (sp/close-subpaths) + (sp/get-subpaths)))) - closed-content - (into [] - (comp (filter sp/is-closed?) - (mapcat :data)) - (->> content - (sp/close-subpaths) - (sp/get-subpaths))) + +(defn ray-overlaps? + [ray-point {selrect :selrect}] + (and (>= (:y ray-point) (:y1 selrect)) + (<= (:y ray-point) (:y2 selrect)))) + +(defn content->geom-data + [content] + + (->> content + (close-content) + (filter #(not= (= :line-to (:command %)) + (= :curve-to (:command %)))) + (mapv (fn [segment] + {:command (:command segment) + :segment segment + :geom (if (= :line-to (:command segment)) + (command->line segment) + (command->bezier segment)) + :selrect (command->selrect segment)})))) + +(defn is-point-in-geom-data? + [point content-geom] + + (let [ray-line [point (gpt/point (inc (:x point)) (:y point))] cast-ray - (fn [cmd] - (case (:command cmd) - :line-to (ray-line-intersect point (command->line cmd)) - :curve-to (ray-curve-intersect ray-line (command->bezier cmd)) - #_:else []))] + (fn [data] + (case (:command data) + :line-to + (ray-line-intersect point (:geom data)) - (and (gpr/contains-point? selrect point) - (->> closed-content - (mapcat cast-ray) - (map second) - (reduce +) - (not= 0))))) + :curve-to + (ray-curve-intersect ray-line (:geom data)) + + #_:default []))] + + (->> content-geom + (filter (partial ray-overlaps? point)) + (mapcat cast-ray) + (map second) + (reduce +) + (not= 0)))) (defn split-line-to "Given a point and a line-to command will create a two new line-to commands 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 9cec2339f..44cc73b06 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -14,20 +14,26 @@ [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] + (when (and (some? selrect) (some? pt)) + (let [dx (.-x pt) + dy (.-y pt) + {:keys [x y x1 y1 x2 y2 width height]} selrect] + {:x (if (some? x) (+ dx x) x) + :y (if (some? y) (+ dy y) y) + :x1 (if (some? x1) (+ dx x1) x1) + :y1 (if (some? y1) (+ dy y1) y1) + :x2 (if (some? x2) (+ dx x2) x2) + :y2 (if (some? y2) (+ dy y2) y2) + :width width + :height height}))) (defn- move-points [points move-vec] (->> points @@ -46,6 +52,8 @@ (update :points move-points move-vec) (d/update-when :x + dx) (d/update-when :y + dy) + (cond-> (= :bool (:type shape)) + (update :bool-content gpa/move-content move-vec)) (cond-> (= :path (:type shape)) (update :content gpa/move-content move-vec))))) @@ -171,9 +179,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 +221,76 @@ 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) + bool? (= (:type shape) :bool) + 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-> bool? + (update :bool-content gpa/transform-content transform-mtx)) + (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 +424,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,50 +515,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 @@ -556,177 +587,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/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index e1bd60bff..65dddbbdd 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -46,13 +46,14 @@ (defn get-root-shape "Get the root shape linked to a component for this shape, if any" [shape objects] - (if-not (:shape-ref shape) - nil - (if (:component-root? shape) - shape - (if-let [parent-id (:parent-id shape)] - (get-root-shape (get objects parent-id) objects) - nil)))) + + (cond + (some? (:component-root? shape)) + shape + + (some? (:shape-ref shape)) + (recur (get objects (:parent-id shape)) + objects))) (defn make-container [page-or-component type] diff --git a/common/src/app/common/path/bool.cljc b/common/src/app/common/path/bool.cljc index bb5253138..a04e65bbb 100644 --- a/common/src/app/common/path/bool.cljc +++ b/common/src/app/common/path/bool.cljc @@ -91,55 +91,55 @@ :else [[] []])) -(defn split - [seg-1 seg-2] - (let [r1 (gsp/command->selrect seg-1) - r2 (gsp/command->selrect seg-2)] - (if (not (gpr/overlaps-rects? r1 r2)) - [[seg-1] [seg-2]] - (let [[ts-seg-1 ts-seg-2] (split-ts seg-1 seg-2)] - [(-> (split-command seg-1 ts-seg-1) (add-previous (:prev seg-1))) - (-> (split-command seg-2 ts-seg-2) (add-previous (:prev seg-2)))])))) - (defn content-intersect-split - [content-a content-b] + [content-a content-b sr-a sr-b] - (let [cache (atom {})] - (letfn [(split-cache [seg-1 seg-2] - (cond - (contains? @cache [seg-1 seg-2]) - (first (get @cache [seg-1 seg-2])) + (let [command->selrect (memoize gsp/command->selrect)] - (contains? @cache [seg-2 seg-1]) - (second (get @cache [seg-2 seg-1])) + (letfn [(overlap-segment-selrect? + [segment selrect] + (if (= :move-to (:command segment)) + false + (let [r1 (command->selrect segment)] + (gpr/overlaps-rects? r1 selrect)))) - :else - (let [value (split seg-1 seg-2)] - (swap! cache assoc [seg-1 seg-2] value) - (first value)))) + (overlap-segments? + [seg-1 seg-2] + (if (or (= :move-to (:command seg-1)) + (= :move-to (:command seg-2))) + false + (let [r1 (command->selrect seg-1) + r2 (command->selrect seg-2)] + (gpr/overlaps-rects? r1 r2)))) + + (split + [seg-1 seg-2] + (if (not (overlap-segments? seg-1 seg-2)) + [seg-1] + (let [[ts-seg-1 _] (split-ts seg-1 seg-2)] + (-> (split-command seg-1 ts-seg-1) + (add-previous (:prev seg-1)))))) (split-segment-on-content - [segment content] + [segment content content-sr] - (loop [current (first content) - content (rest content) - result [segment]] - - (if (nil? current) - result - (let [result (->> result (into [] (mapcat #(split-cache % current))))] - (recur (first content) - (rest content) - result))))) + (if (overlap-segment-selrect? segment content-sr) + (->> content + (filter #(overlap-segments? segment %)) + (reduce + (fn [result current] + (into [] (mapcat #(split % current)) result)) + [segment])) + [segment])) (split-content - [content-a content-b] + [content-a content-b sr-b] (into [] - (mapcat #(split-segment-on-content % content-b)) + (mapcat #(split-segment-on-content % content-b sr-b)) content-a))] - [(split-content content-a content-b) - (split-content content-b content-a)]))) + [(split-content content-a content-b sr-b) + (split-content content-b content-a sr-a)]))) (defn is-segment? [cmd] @@ -147,7 +147,7 @@ (contains? #{:line-to :curve-to} (:command cmd)))) (defn contains-segment? - [segment content] + [segment content content-sr content-geom] (let [point (case (:command segment) :line-to (-> (gsp/command->line segment) @@ -156,11 +156,13 @@ :curve-to (-> (gsp/command->bezier segment) (gsp/curve-values 0.5)))] - (or (gsp/is-point-in-content? point content) - (gsp/is-point-in-border? point content)))) + (and (gpr/contains-point? content-sr point) + (or + (gsp/is-point-in-geom-data? point content-geom) + (gsp/is-point-in-border? point content))))) (defn inside-segment? - [segment content] + [segment content-sr content-geom] (let [point (case (:command segment) :line-to (-> (gsp/command->line segment) (gsp/line-values 0.5)) @@ -168,7 +170,8 @@ :curve-to (-> (gsp/command->bezier segment) (gsp/curve-values 0.5)))] - (gsp/is-point-in-content? point content))) + (and (gpr/contains-point? content-sr point) + (gsp/is-point-in-geom-data? point content-geom)))) (defn overlap-segment? "Finds if the current segment is overlapping against other @@ -209,49 +212,59 @@ (d/seek overlap-single?) (some?)))) -(defn create-union [content-a content-a-split content-b content-b-split] +(defn create-union [content-a content-a-split content-b content-b-split sr-a sr-b] ;; Pick all segments in content-a that are not inside content-b ;; Pick all segments in content-b that are not inside content-a - (let [content + (let [content-a-geom (gsp/content->geom-data content-a) + content-b-geom (gsp/content->geom-data content-b) + + content (concat - (->> content-a-split (filter #(not (contains-segment? % content-b)))) - (->> content-b-split (filter #(not (contains-segment? % content-a))))) + (->> content-a-split (filter #(not (contains-segment? % content-b sr-b content-b-geom)))) + (->> content-b-split (filter #(not (contains-segment? % content-a sr-a content-a-geom))))) + + content-geom (gsp/content->geom-data content) + + content-sr (gsp/content->selrect content) ;; Overlapping segments should be added when they are part of the border border-content (->> content-b-split - (filter #(and (contains-segment? % content-a) + (filter #(and (contains-segment? % content-a sr-a content-a-geom) (overlap-segment? % content-a-split) - (not (inside-segment? % content)))))] + (not (inside-segment? % content-sr content-geom)))))] ;; Ensure that the output is always a vector (d/concat-vec content border-content))) -(defn create-difference [content-a content-a-split content-b content-b-split] +(defn create-difference [content-a content-a-split content-b content-b-split sr-a sr-b] ;; Pick all segments in content-a that are not inside content-b ;; Pick all segments in content b that are inside content-a ;; removing overlapping - (d/concat-vec - (->> content-a-split (filter #(not (contains-segment? % content-b)))) + (let [content-a-geom (gsp/content->geom-data content-a) + content-b-geom (gsp/content->geom-data content-b)] + (d/concat-vec + (->> content-a-split (filter #(not (contains-segment? % content-b sr-b content-b-geom)))) - ;; Reverse second content so we can have holes inside other shapes - (->> content-b-split - (filter #(and (contains-segment? % content-a) - (not (overlap-segment? % content-a-split))))))) + ;; Reverse second content so we can have holes inside other shapes + (->> content-b-split + (filter #(and (contains-segment? % content-a sr-a content-a-geom) + (not (overlap-segment? % content-a-split)))))))) -(defn create-intersection [content-a content-a-split content-b content-b-split] +(defn create-intersection [content-a content-a-split content-b content-b-split sr-a sr-b] ;; Pick all segments in content-a that are inside content-b ;; Pick all segments in content-b that are inside content-a - (d/concat-vec - (->> content-a-split (filter #(contains-segment? % content-b))) - (->> content-b-split (filter #(contains-segment? % content-a))))) + (let [content-a-geom (gsp/content->geom-data content-a) + content-b-geom (gsp/content->geom-data content-b)] + (d/concat-vec + (->> content-a-split (filter #(contains-segment? % content-b sr-b content-b-geom))) + (->> content-b-split (filter #(contains-segment? % content-a sr-a content-a-geom)))))) (defn create-exclusion [content-a content-b] ;; Pick all segments (d/concat-vec content-a content-b)) - (defn fix-move-to [content] ;; Remove the field `:prev` and makes the necessaries `move-to` @@ -284,16 +297,19 @@ (ups/reverse-content)) (add-previous)) + sr-a (gsp/content->selrect content-a) + sr-b (gsp/content->selrect content-b) + ;; Split content in new segments in the intersection with the other path - [content-a-split content-b-split] (content-intersect-split content-a content-b) + [content-a-split content-b-split] (content-intersect-split content-a content-b sr-a sr-b) content-a-split (->> content-a-split add-previous (filter is-segment?)) content-b-split (->> content-b-split add-previous (filter is-segment?)) bool-content (case bool-type - :union (create-union content-a content-a-split content-b content-b-split) - :difference (create-difference content-a content-a-split content-b content-b-split) - :intersection (create-intersection content-a content-a-split content-b content-b-split) + :union (create-union content-a content-a-split content-b content-b-split sr-a sr-b) + :difference (create-difference content-a content-a-split content-b content-b-split sr-a sr-b) + :intersection (create-intersection content-a content-a-split content-b content-b-split sr-a sr-b) :exclude (create-exclusion content-a-split content-b-split))] (->> (fix-move-to bool-content) 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/common/test/app/common/geom_shapes_test.cljc b/common/test/app/common/geom_shapes_test.cljc index 49d303611..72eb0e262 100644 --- a/common/test/app/common/geom_shapes_test.cljc +++ b/common/test/app/common/geom_shapes_test.cljc @@ -181,9 +181,11 @@ shape-before (-> (create-test-shape type {:modifiers modifiers}) (assoc :selrect selrect)) shape-after (gsh/transform-shape shape-before {:round-coords? false})] - (= (:selrect shape-before) (:selrect shape-after))) - :rect {:x 0 :y 0 :width ##Inf :height ##Inf} - :path {:x 0 :y 0 :width ##Inf :height ##Inf} + (= (:selrect shape-before) + (:selrect shape-after))) + + :rect {:x 0 :y 0 :x1 0 :y1 0 :x2 ##Inf :y2 ##Inf :width ##Inf :height ##Inf} + :path {:x 0 :y 0 :x1 0 :y1 0 :x2 ##Inf :y2 ##Inf :width ##Inf :height ##Inf} :rect nil :path nil))) diff --git a/frontend/resources/styles/main/partials/dashboard-grid.scss b/frontend/resources/styles/main/partials/dashboard-grid.scss index bbf80ec67..31a7a0fdb 100644 --- a/frontend/resources/styles/main/partials/dashboard-grid.scss +++ b/frontend/resources/styles/main/partials/dashboard-grid.scss @@ -311,6 +311,9 @@ width: 100%; background-color: $color-canvas; + display: flex; + justify-content: center; + flex-direction: row; .img-th { height: auto; @@ -321,6 +324,10 @@ height: 100%; width: 100%; } + + svg#loader-pencil { + fill: $color-gray-20; + } } } 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/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index 63bd8c34b..0503dc5f1 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -252,7 +252,6 @@ (ptk/reify ::select-next-frame ptk/WatchEvent (watch [_ state _] - (prn "select-next-frame") (let [route (:route state) pparams (:path-params route) qparams (:query-params route) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 68a40ab7e..f26ded73f 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -24,10 +24,11 @@ [app.config :as cfg] [app.main.data.events :as ev] [app.main.data.messages :as dm] - [app.main.data.workspace.booleans :as dwb] + [app.main.data.workspace.bool :as dwb] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.drawing :as dwd] + [app.main.data.workspace.fix-bool-contents :as fbc] [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.interactions :as dwi] [app.main.data.workspace.libraries :as dwl] @@ -213,8 +214,11 @@ (or (not ignore-until) (> (:modified-at %) ignore-until))) libraries)] - (when needs-update? - (rx/of (dwl/notify-sync-file file-id))))))) + (rx/merge + (rx/of (fbc/fix-bool-contents)) + (if needs-update? + (rx/of (dwl/notify-sync-file file-id)) + (rx/empty))))))) (defn finalize-file [_project-id file-id] @@ -307,7 +311,7 @@ [page-id] (ptk/reify ::duplicate-page ptk/WatchEvent - (watch [this state _] + (watch [it state _] (let [id (uuid/next) pages (get-in state [:workspace-data :pages-index]) unames (dwc/retrieve-used-names pages) @@ -322,7 +326,7 @@ :id id}] (rx/of (dch/commit-changes {:redo-changes [rchange] :undo-changes [uchange] - :origin this})))))) + :origin it})))))) (s/def ::rename-page (s/keys :req-un [::id ::name])) diff --git a/frontend/src/app/main/data/workspace/booleans.cljs b/frontend/src/app/main/data/workspace/bool.cljs similarity index 89% rename from frontend/src/app/main/data/workspace/booleans.cljs rename to frontend/src/app/main/data/workspace/bool.cljs index 4475eb61a..ab0ec4fdd 100644 --- a/frontend/src/app/main/data/workspace/booleans.cljs +++ b/frontend/src/app/main/data/workspace/bool.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.data.workspace.booleans +(ns app.main.data.workspace.bool (:require [app.common.colors :as clr] [app.common.data :as d] @@ -38,17 +38,20 @@ (and (contains? head :svg-attrs) (nil? (:fill-color head))) (assoc :fill-color clr/black)) - head-data (select-keys head stp/style-properties)] - [(-> {:id (uuid/next) - :type :bool - :bool-type bool-type - :frame-id (:frame-id head) - :parent-id (:parent-id head) - :name name - :shapes []} - (merge head-data) - (gsh/update-bool-selrect shapes objects)) - (cp/position-on-parent (:id head) objects)])) + head-data (select-keys head stp/style-properties) + + bool-shape + (-> {:id (uuid/next) + :type :bool + :bool-type bool-type + :frame-id (:frame-id head) + :parent-id (:parent-id head) + :name name + :shapes (->> shapes (mapv :id))} + (merge head-data) + (gsh/update-bool-selrect shapes objects))] + + [bool-shape (cp/position-on-parent (:id head) objects)])) (defn group->bool [group bool-type objects] diff --git a/frontend/src/app/main/data/workspace/fix_bool_contents.cljs b/frontend/src/app/main/data/workspace/fix_bool_contents.cljs new file mode 100644 index 000000000..b59bc401a --- /dev/null +++ b/frontend/src/app/main/data/workspace/fix_bool_contents.cljs @@ -0,0 +1,94 @@ +;; 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.main.data.workspace.fix-bool-contents + (:require + [app.common.data :as d] + [app.common.geom.shapes :as gsh] + [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.state-helpers :as wsh] + [beicon.core :as rx] + [potok.core :as ptk])) + +;; This event will update the file so the boolean data has a pre-generated path data +;; to increase performance. +;; For new shapes this will be generated in the :reg-objects but we need to do this for +;; old files. + +;; FIXME: Remove me after June 2022 + +(defn fix-bool-contents + "This event will calculate the bool content and update the page. This is kind of a 'addhoc' migration + to fill the optional value 'bool-content'" + [] + + (letfn [(should-migrate-shape? [shape] + (and (= :bool (:type shape)) (not (contains? shape :bool-content)))) + + (should-migrate-component? [component] + (->> (:objects component) + (vals) + (d/seek should-migrate-shape?))) + + (update-shape [shape objects] + (cond-> shape + (should-migrate-shape? shape) + (assoc :bool-content (gsh/calc-bool-content shape objects)))) + + (migrate-component [component] + (-> component + (update + :objects + (fn [objects] + (d/mapm #(update-shape %2 objects) objects))))) + + (update-library + [library] + (-> library + (d/update-in-when + [:data :components] + (fn [components] + (d/mapm #(migrate-component %2) components)))))] + + (ptk/reify ::fix-bool-contents + ptk/UpdateEvent + (update [_ state] + ;; Update (only-local) the imported libraries + (-> state + (d/update-when + :workspace-libraries + (fn [libraries] (d/mapm #(update-library %2) libraries))))) + + ptk/WatchEvent + (watch [it state _] + (let [objects (wsh/lookup-page-objects state) + + ids (into #{} + (comp (filter should-migrate-shape?) (map :id)) + (vals objects)) + + components (->> (wsh/lookup-local-components state) + (vals) + (filter should-migrate-component?)) + + component-changes + (into [] + (map (fn [component] + {:type :mod-component + :id (:id component) + :objects (-> component migrate-component :objects)})) + components)] + + (rx/of (dch/update-shapes ids #(update-shape % objects) {:reg-objects? false + :save-undo? false + :ignore-tree true})) + + (if (empty? component-changes) + (rx/empty) + (rx/of (dch/commit-changes {:origin it + :redo-changes component-changes + :undo-changes [] + :save-undo? false})))))))) diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index 70d77f3bb..d69c99ec3 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -269,19 +269,19 @@ :type "keyup" :fn #(st/emit! (dw/toggle-distances-display false))} - :boolean-union {:tooltip (ds/meta (ds/alt "U")) + :bool-union {:tooltip (ds/meta (ds/alt "U")) :command (ds/c-mod "alt+u") :fn #(st/emit! (dw/create-bool :union))} - :boolean-difference {:tooltip (ds/meta (ds/alt "D")) + :bool-difference {:tooltip (ds/meta (ds/alt "D")) :command (ds/c-mod "alt+d") :fn #(st/emit! (dw/create-bool :difference))} - :boolean-intersection {:tooltip (ds/meta (ds/alt "I")) + :bool-intersection {:tooltip (ds/meta (ds/alt "I")) :command (ds/c-mod "alt+i") :fn #(st/emit! (dw/create-bool :intersection))} - :boolean-exclude {:tooltip (ds/meta (ds/alt "E")) + :bool-exclude {:tooltip (ds/meta (ds/alt "E")) :command (ds/c-mod "alt+e") :fn #(st/emit! (dw/create-bool :exclude))} diff --git a/frontend/src/app/main/data/workspace/state_helpers.cljs b/frontend/src/app/main/data/workspace/state_helpers.cljs index 0710aba55..f144670ba 100644 --- a/frontend/src/app/main/data/workspace/state_helpers.cljs +++ b/frontend/src/app/main/data/workspace/state_helpers.cljs @@ -25,6 +25,10 @@ ([state component-id] (get-in state [:workspace-data :components component-id :objects]))) +(defn lookup-local-components + ([state] + (get-in state [:workspace-data :components]))) + (defn lookup-selected ([state] (lookup-selected state nil)) diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index f48ad1e43..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? (d/without-keys 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..04da9d9d9 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 @@ -241,8 +237,7 @@ [:& wrapper {:shape frame :view-box vbox}]])) (mf/defc component-svg - {::mf/wrap [mf/memo - #(mf/deferred % ts/idle-then-raf)]} + {::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]} [{:keys [objects group zoom] :or {zoom 1} :as props}] (let [modifier (-> (gpt/point (:x group) (:y group)) (gpt/negate) @@ -253,17 +248,21 @@ include-metadata? (mf/use-ctx use/include-metadata-ctx) modifier-ids (concat [group-id] (cp/get-children group-id objects)) + update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier) - objects (reduce update-fn objects modifier-ids) - group (assoc-in group [:modifiers :displacement] modifier) + modifiers (reduce update-fn {} modifier-ids) + objects (gsh/merge-modifiers objects modifiers) + + group (get objects group-id) width (* (:width group) zoom) height (* (:height group) zoom) vbox (str "0 0 " (:width group 0) " " (:height group 0)) - wrapper (mf/use-memo - (mf/deps objects) - #(group-wrapper-factory objects))] + group-wrapper + (mf/use-memo + (mf/deps objects) + #(group-wrapper-factory objects))] [:svg {:view-box vbox :width width @@ -273,7 +272,7 @@ :xmlnsXlink "http://www.w3.org/1999/xlink" :xmlns:penpot (when include-metadata? "https://penpot.app/xmlns")} [:> shape-container {:shape group} - [:& wrapper {:shape group :view-box vbox}]]])) + [:& group-wrapper {:shape group :view-box vbox}]]])) (mf/defc component-symbol [{:keys [id data] :as props}] diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index f98d98be2..96432175a 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -116,6 +116,14 @@ :show-distances?]) workspace-local =)) +(def interactions-data + (l/derived #(select-keys % [:editing-interaction-index + :draw-interaction-to + :draw-interaction-to-frame + :move-overlay-to + :move-overlay-index]) + workspace-local =)) + (def local-displacement (l/derived #(select-keys % [:modifiers :selected]) workspace-local =)) @@ -232,23 +240,12 @@ (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)] + (into [] (keep (d/getf objects)) ids)))] + (l/derived selector st/state =))) (defn- set-content-modifiers [state] (fn [id shape] @@ -257,21 +254,11 @@ (update shape :content upc/apply-content-modifiers content-modifiers) shape)))) -(defn select-children [id] +(defn select-bool-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))] - + modifiers (:workspace-modifiers state)] (as-> (cp/select-children id objects) $ (gsh/merge-modifiers $ modifiers) (d/mapm (set-content-modifiers state) $))))] diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index f158785e4..d9ecd0bc3 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -22,6 +22,7 @@ [app.util.dom.dnd :as dnd] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] + [app.util.storage :as stg] [app.util.time :as dt] [app.util.timers :as ts] [app.util.webapi :as wapi] @@ -30,22 +31,55 @@ ;; --- Grid Item Thumbnail +(defn use-thumbnail-cache + "Creates some hooks to handle the files thumbnails cache" + [file] + + (let [get-thumbnail + (mf/use-callback + (mf/deps file) + (fn [] + (let [[revn thumb-data] (get-in @stg/storage [:thumbnails (:id file)])] + (when (= revn (:revn file)) + thumb-data)))) + + cache-thumbnail + (mf/use-callback + (mf/deps file) + (fn [thumb-data] + (swap! stg/storage #(assoc-in % [:thumbnails (:id file)] [(:revn file) thumb-data])))) + + generate-thumbnail + (mf/use-callback + (mf/deps file) + (fn [] + (let [thumb-data (get-thumbnail)] + (if (some? thumb-data) + (rx/of thumb-data) + (->> (wrk/ask! {:cmd :thumbnails/generate + :file-id (:id file) + :page-id (get-in file [:data :pages 0])}) + (rx/tap cache-thumbnail))))))] + + generate-thumbnail)) + (mf/defc grid-item-thumbnail {::mf/wrap [mf/memo]} [{:keys [file] :as props}] - (let [container (mf/use-ref)] + (let [container (mf/use-ref) + generate-thumbnail (use-thumbnail-cache file)] + (mf/use-effect - (mf/deps (:id file)) + (mf/deps file) (fn [] - (->> (wrk/ask! {:cmd :thumbnails/generate - :file-id (:id file) - :page-id (get-in file [:data :pages 0])}) + (->> (generate-thumbnail) (rx/subs (fn [{:keys [svg fonts]}] (run! fonts/ensure-loaded! fonts) (when-let [node (mf/ref-val container)] (set! (.-innerHTML ^js node) svg))))))) [:div.grid-item-th {:style {:background-color (get-in file [:data :options :background])} - :ref container}])) + :ref container} + i/loader-pencil])) ;; --- Grid Item diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index 2a2c7b5d8..0230fa055 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -29,11 +29,11 @@ (def auto-fix (icon-xref :auto-fix)) (def auto-height (icon-xref :auto-height)) (def auto-width (icon-xref :auto-width)) -(def boolean-difference (icon-xref :boolean-difference)) -(def boolean-exclude (icon-xref :boolean-exclude)) -(def boolean-flatten (icon-xref :boolean-flatten)) -(def boolean-intersection (icon-xref :boolean-intersection)) -(def boolean-union (icon-xref :boolean-union)) +(def bool-difference (icon-xref :boolean-difference)) +(def bool-exclude (icon-xref :boolean-exclude)) +(def bool-flatten (icon-xref :boolean-flatten)) +(def bool-intersection (icon-xref :boolean-intersection)) +(def bool-union (icon-xref :boolean-union)) (def box (icon-xref :box)) (def chain (icon-xref :chain)) (def chat (icon-xref :chat)) diff --git a/frontend/src/app/main/ui/shapes/bool.cljs b/frontend/src/app/main/ui/shapes/bool.cljs index fd9b58f94..9d3d41851 100644 --- a/frontend/src/app/main/ui/shapes/bool.cljs +++ b/frontend/src/app/main/ui/shapes/bool.cljs @@ -8,107 +8,41 @@ (: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]] [app.main.ui.shapes.export :as use] [app.main.ui.shapes.path :refer [path-shape]] [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))))))] + (cond + (some? (:bool-content shape)) + (:bool-content shape) + + (some? childs) + (->> childs + (d/mapm #(gsh/transform-shape %2)) + (gsh/calc-bool-content shape)))))] [:* - [:& path-shape {:shape (assoc shape :content bool-content)}] + (when (some? bool-content) + [:& path-shape {:shape (assoc shape :content bool-content)}]) (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/viewer/handoff/selection_feedback.cljs b/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs index f0ee419c2..a93fbbdea 100644 --- a/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs @@ -57,8 +57,7 @@ (let [{:keys [hover selected zoom]} local hover-shape (-> (or (first (resolve-shapes objects [hover])) frame) (gsh/translate-to-frame frame)) - selected-shapes (->> (resolve-shapes objects selected) - (map #(gsh/translate-to-frame % frame))) + selected-shapes (->> (resolve-shapes objects selected)) selrect (gsh/selection-rect selected-shapes) bounds (frame->bounds frame)] diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 31f6689b0..926642099 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -250,16 +250,16 @@ (or multiple? (and single? (or is-group? is-bool?)))) [:& menu-entry {:title (tr "workspace.shape.menu.path")} [:& menu-entry {:title (tr "workspace.shape.menu.union") - :shortcut (sc/get-tooltip :boolean-union) + :shortcut (sc/get-tooltip :bool-union) :on-click (set-bool :union)}] [:& menu-entry {:title (tr "workspace.shape.menu.difference") - :shortcut (sc/get-tooltip :boolean-difference) + :shortcut (sc/get-tooltip :bool-difference) :on-click (set-bool :difference)}] [:& menu-entry {:title (tr "workspace.shape.menu.intersection") - :shortcut (sc/get-tooltip :boolean-intersection) + :shortcut (sc/get-tooltip :bool-intersection) :on-click (set-bool :intersection)}] [:& menu-entry {:title (tr "workspace.shape.menu.exclude") - :shortcut (sc/get-tooltip :boolean-exclude) + :shortcut (sc/get-tooltip :bool-exclude) :on-click (set-bool :exclude)}] (when (and single? is-bool? (not disable-flatten?)) diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index 2a260de3c..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] @@ -77,20 +76,16 @@ :key (:id item)}]))])) (mf/defc shape-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 (obj/get props "shape") - frame (obj/get props "frame") - shape (-> (geom/transform-shape shape {:round-coords? false}) - (geom/translate-to-frame 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..96234a0dd 100644 --- a/frontend/src/app/main/ui/workspace/shapes/bool.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/bool.cljs @@ -27,21 +27,26 @@ [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") + (let [shape (unchecked-get props "shape") + child-sel-ref (mf/use-memo + (mf/deps (:id shape)) + #(refs/is-child-selected? (:id shape))) - childs-ref (mf/use-memo - (mf/deps (:id shape)) - #(refs/select-children (:id shape))) + childs-ref (mf/use-memo + (mf/deps (:id shape)) + #(refs/select-bool-children (:id shape))) - childs (mf/deref childs-ref)] + child-sel? (mf/deref child-sel-ref) + childs (mf/deref childs-ref) + + shape (cond-> shape + child-sel? + (dissoc :bool-content))] [:> shape-container {:shape shape} - [:& shape-component - {:frame frame - :shape shape - :childs childs}]])))) + [:& shape-component {: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 5009d926f..f3fad5640 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -6,7 +6,7 @@ (ns app.main.ui.workspace.shapes.frame (:require - [app.common.geom.shapes :as gsh] + [app.common.data :as d] [app.common.pages :as cp] [app.main.ui.hooks :as hooks] [app.main.ui.shapes.frame :as frame] @@ -101,8 +101,7 @@ objects (unchecked-get props "objects") thumbnail? (unchecked-get props "thumbnail?") - shape (gsh/transform-shape shape) - children (-> (mapv #(get objects %) (:shapes shape)) + children (-> (mapv (d/getf objects) (:shapes shape)) (hooks/use-equal-memo)) all-children (-> (cp/get-children-objects (:id shape) objects) 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/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index f23e7ab1f..e18c0ae06 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -42,10 +42,10 @@ i/mask i/folder)) :bool (case (:bool-type shape) - :difference i/boolean-difference - :exclude i/boolean-exclude - :intersection i/boolean-intersection - #_:default i/boolean-union) + :difference i/bool-difference + :exclude i/bool-exclude + :intersection i/bool-intersection + #_:default i/bool-union) :svg-raw i/file-svg nil)) @@ -308,6 +308,7 @@ :bool-type])) (defn- strip-objects + "Remove unnecesary data from objects map" [objects] (persistent! (->> objects @@ -320,8 +321,11 @@ {::mf/wrap-props false ::mf/wrap [mf/memo #(mf/throttle % 200)]} [props] - (let [objects (obj/get props "objects") - objects (strip-objects objects)] + (let [objects (-> (obj/get props "objects") + (hooks/use-equal-memo)) + objects (mf/use-memo + (mf/deps objects) + #(strip-objects objects))] [:& layers-tree {:objects objects}])) ;; --- Layers Toolbox diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index 9e5523345..0d9f45264 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -12,7 +12,7 @@ [app.main.ui.components.tab-container :refer [tab-container tab-element]] [app.main.ui.context :as ctx] [app.main.ui.workspace.sidebar.options.menus.align :refer [align-options]] - [app.main.ui.workspace.sidebar.options.menus.booleans :refer [booleans-options]] + [app.main.ui.workspace.sidebar.options.menus.bool :refer [bool-options]] [app.main.ui.workspace.sidebar.options.menus.exports :refer [exports-menu]] [app.main.ui.workspace.sidebar.options.menus.interactions :refer [interactions-menu]] [app.main.ui.workspace.sidebar.options.page :as page] @@ -63,7 +63,7 @@ :title (tr "workspace.options.design")} [:div.element-options [:& align-options] - [:& booleans-options] + [:& bool-options] (case (count selected) 0 [:& page/options] 1 [:& shape-options {:shape (first shapes) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.cljs similarity index 90% rename from frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs rename to frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.cljs index 187c884fd..3b76be0c2 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.workspace.sidebar.options.menus.booleans +(ns app.main.ui.workspace.sidebar.options.menus.bool (:require [app.main.data.workspace :as dw] [app.main.data.workspace.shortcuts :as sc] @@ -15,7 +15,7 @@ [app.util.i18n :as i18n :refer [tr]] [rumext.alpha :as mf])) -(mf/defc booleans-options +(mf/defc bool-options [] (let [selected (mf/deref refs/selected-objects) selected-with-children (mf/deref refs/selected-shapes-with-children) @@ -52,37 +52,37 @@ [:div.align-options [:div.align-group [:div.align-button.tooltip.tooltip-bottom - {:alt (str (tr "workspace.shape.menu.union") " (" (sc/get-tooltip :boolean-union) ")") + {:alt (str (tr "workspace.shape.menu.union") " (" (sc/get-tooltip :bool-union) ")") :class (dom/classnames :disabled disabled-bool-btns :selected (= head-bool-type :union)) :on-click (set-bool :union)} - i/boolean-union] + i/bool-union] [:div.align-button.tooltip.tooltip-bottom - {:alt (str (tr "workspace.shape.menu.difference") " (" (sc/get-tooltip :boolean-difference) ")") + {:alt (str (tr "workspace.shape.menu.difference") " (" (sc/get-tooltip :bool-difference) ")") :class (dom/classnames :disabled disabled-bool-btns :selected (= head-bool-type :difference)) :on-click (set-bool :difference)} - i/boolean-difference] + i/bool-difference] [:div.align-button.tooltip.tooltip-bottom - {:alt (str (tr "workspace.shape.menu.intersection") " (" (sc/get-tooltip :boolean-intersection) ")") + {:alt (str (tr "workspace.shape.menu.intersection") " (" (sc/get-tooltip :bool-intersection) ")") :class (dom/classnames :disabled disabled-bool-btns :selected (= head-bool-type :intersection)) :on-click (set-bool :intersection)} - i/boolean-intersection] + i/bool-intersection] [:div.align-button.tooltip.tooltip-bottom - {:alt (str (tr "workspace.shape.menu.exclude") " (" (sc/get-tooltip :boolean-exclude) ")") + {:alt (str (tr "workspace.shape.menu.exclude") " (" (sc/get-tooltip :bool-exclude) ")") :class (dom/classnames :disabled disabled-bool-btns :selected (= head-bool-type :exclude)) :on-click (set-bool :exclude)} - i/boolean-exclude]] + i/bool-exclude]] [:div.align-group [:div.align-button.tooltip.tooltip-bottom {:alt (tr "workspace.shape.menu.flatten") :class (dom/classnames :disabled disabled-flatten) :on-click (st/emitf (dw/convert-selected-to-path))} - i/boolean-flatten]]])) + i/bool-flatten]]])) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index c87f346b8..51c288e33 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? @@ -61,11 +60,11 @@ ;; DEREFS drawing (mf/deref refs/workspace-drawing) options (mf/deref refs/workspace-page-options) - objects (mf/deref refs/workspace-page-objects) - object-modifiers (mf/deref refs/workspace-modifiers) - objects (mf/use-memo - (mf/deps objects object-modifiers) - #(gsh/merge-modifiers objects object-modifiers)) + base-objects (mf/deref refs/workspace-page-objects) + 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,21 @@ drawing-tool (:tool drawing) drawing-obj (:object drawing) - selected-shapes (into [] - (comp (map #(get objects %)) - (filter some?)) - selected) + selected-shapes (into [] (keep (d/getf objects-modified)) 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,15 +149,15 @@ (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 - [:& wtr/frame-renderer {:objects objects + [:& wtr/frame-renderer {:objects base-objects :background background}] (when show-comments? @@ -184,7 +178,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 +195,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 +227,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 +252,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 +266,7 @@ (when show-prototypes? [:& widgets/frame-flows {:flows (:flows options) - :objects objects + :objects base-objects :selected selected :zoom zoom :modifiers modifiers @@ -310,7 +303,7 @@ :zoom zoom :page-id page-id :selected selected - :objects objects + :objects base-objects :modifiers modifiers}]) (when show-snap-distance? @@ -335,6 +328,9 @@ (when show-prototypes? [:& interactions/interactions {:selected selected + :zoom zoom + :objects objects-modified + :current-transform transform :hover-disabled? hover-disabled?}]) (when show-selrect? diff --git a/frontend/src/app/main/ui/workspace/viewport/drawarea.cljs b/frontend/src/app/main/ui/workspace/viewport/drawarea.cljs index 97fdf6168..2ab39b52d 100644 --- a/frontend/src/app/main/ui/workspace/viewport/drawarea.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/drawarea.cljs @@ -22,7 +22,7 @@ [:g.draw-area [:g {:style {:pointer-events "none"}} - [:& shapes/shape-wrapper {:shape shape}]] + [:& shapes/shape-wrapper {:shape (gsh/transform-shape shape)}]] (case tool :path [:& path-editor {:shape shape :zoom zoom}] 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/interactions.cljs b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs index de2e50a88..90f120024 100644 --- a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs @@ -8,6 +8,7 @@ "Visually show shape interactions in workspace" (:require [app.common.data :as d] + [app.common.geom.shapes :as gsh] [app.common.pages :as cp] [app.common.types.interactions :as cti] [app.main.data.workspace :as dw] @@ -235,18 +236,23 @@ :fill "var(--color-primary)"}]])))) (mf/defc interactions - [{:keys [selected hover-disabled?] :as props}] - (let [local (mf/deref refs/workspace-local) - zoom (mf/deref refs/selected-zoom) - current-transform (:transform local) - objects (mf/deref refs/workspace-page-objects) - active-shapes (filter #(seq (:interactions %)) (vals objects)) - selected-shapes (map #(get objects %) selected) - editing-interaction-index (:editing-interaction-index local) - draw-interaction-to (:draw-interaction-to local) - draw-interaction-to-frame (:draw-interaction-to-frame local) - move-overlay-to (:move-overlay-to local) - move-overlay-index (:move-overlay-index local) + [{:keys [current-transform objects zoom selected hover-disabled?] :as props}] + (let [active-shapes (into [] + (comp (filter #(seq (:interactions %))) + (map gsh/transform-shape)) + (vals objects)) + + selected-shapes (into [] + (comp (map (d/getf objects)) + (map gsh/transform-shape)) + selected) + + {:keys [editing-interaction-index + draw-interaction-to + draw-interaction-to-frame + move-overlay-to + move-overlay-index]} (mf/deref refs/interactions-data) + first-selected (first selected-shapes) calc-level (fn [index interactions] diff --git a/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs b/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs index 1373a151e..3c09e952f 100644 --- a/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs @@ -6,13 +6,14 @@ (ns app.main.ui.workspace.viewport.pixel-overlay (:require + [app.common.data :as d] [app.common.uuid :as uuid] [app.main.data.modal :as modal] [app.main.data.workspace.colors :as dwc] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.cursors :as cur] - [app.main.ui.workspace.shapes :refer [shape-wrapper frame-wrapper]] + [app.main.ui.workspace.shapes :as shapes] [app.util.dom :as dom] [app.util.keyboard :as kbd] [app.util.object :as obj] @@ -36,16 +37,16 @@ (let [data (mf/deref refs/workspace-page) objects (:objects data) root (get objects uuid/zero) - shapes (->> (:shapes root) (map #(get objects %)))] - [:* - [:g.shapes - (for [item shapes] - (if (= (:type item) :frame) - [:& frame-wrapper {:shape item - :key (:id item) - :objects objects}] - [:& shape-wrapper {:shape item - :key (:id item)}]))]])) + shapes (->> (:shapes root) + (map (d/getf objects)))] + [:g.shapes + (for [item shapes] + (if (= (:type item) :frame) + [:& shapes/frame-wrapper {:shape item + :key (:id item) + :objects objects}] + [:& shapes/shape-wrapper {:shape item + :key (:id item)}]))])) (mf/defc pixel-overlay {::mf/wrap-props false} 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 2f6d65561..460985b7d 100644 --- a/frontend/src/debug.cljs +++ b/frontend/src/debug.cljs @@ -5,10 +5,14 @@ ;; Copyright (c) UXBOX Labs SL (ns debug + #_(:import [goog.math AffineTransform]) (:require [app.common.data :as d] + #_[app.common.geom.matrix :as gmt] [app.common.math :as mth] [app.common.pages :as cp] + #_[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] @@ -17,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 @@ -106,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)) @@ -209,3 +251,41 @@ (not (debug-exclude-events (ptk/type s)))))) (rx/subs #(println "[stream]: " (ptk/repr-event %)))))) +#_(defn ^:export bench-matrix + [] + (let [iterations 1000000 + + good (gmt/multiply (gmt/matrix 1 2 3 4 5 6) + (gmt/matrix 1 2 3 4 5 6)) + + k1 (perf/start) + _ (dotimes [_ iterations] + (when-not (= good (gmt/-old-multiply (gmt/matrix 1 2 3 4 5 6) + (gmt/matrix 1 2 3 4 5 6))) + (throw "ERROR"))) + m1 (perf/measure k1) + + k2 (perf/start) + _ (dotimes [_ iterations] + (when-not (= good (gmt/multiply (gmt/matrix 1 2 3 4 5 6) + (gmt/matrix 1 2 3 4 5 6))) + (throw "ERROR"))) + m2 (perf/measure k2) + + k3 (perf/start) + _ (dotimes [_ iterations] + (let [res (.concatenate (AffineTransform. 1 2 3 4 5 6) + (AffineTransform. 1 2 3 4 5 6)) + res (gmt/matrix (.-m00_ res) (.-m10_ res) (.-m01_ res) (.-m11_ res) (.-m02_ res) (.-m12_ res))] + + (when-not (= good res) + (throw "ERROR")))) + m3 (perf/measure k3) + ] + + (println "Clojure matrix. Total: " m1 " (" (/ m1 iterations) ")") + (println "Clojure matrix (NEW). Total: " m2 " (" (/ m2 iterations) ")") + (println "Affine transform (with new). Total: " m3 " (" (/ m3 iterations) ")"))) + + +