diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index f8c18fcd62..011356929e 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -18,6 +18,7 @@ app.util.services/defmethod hooks.export/service-defmethod app.common.record/defrecord hooks.export/penpot-defrecord app.db/with-atomic hooks.export/penpot-with-atomic + rumext.v2/fnc hooks.export/rumext-fnc }} :output diff --git a/.clj-kondo/hooks/export.clj b/.clj-kondo/hooks/export.clj index e56d2f559b..02c304fa20 100644 --- a/.clj-kondo/hooks/export.clj +++ b/.clj-kondo/hooks/export.clj @@ -58,6 +58,19 @@ (api/vector-node [params params])] body))}))) +(defn rumext-fnc + [{:keys [node]}] + (let [[cname mdata params & body] (rest (:children node)) + [params body] (if (api/vector-node? mdata) + [mdata (cons params body)] + [params body])] + (let [result (api/list-node + (into [(api/token-node 'fn) + params] + (cons mdata body)))] + {:node result}))) + + (defn penpot-defrecord [{:keys [:node]}] (let [[rnode rtype rparams & other] (:children node) diff --git a/backend/src/app/srepl/helpers.clj b/backend/src/app/srepl/helpers.clj index 94fe563713..d0eb2abdb6 100644 --- a/backend/src/app/srepl/helpers.clj +++ b/backend/src/app/srepl/helpers.clj @@ -164,7 +164,7 @@ ffeat/*wrap-with-pointer-map-fn* (if (contains? (:features file) "storage/pointer-map") pmap/wrap identity) ffeat/*wrap-with-objects-map-fn* - (if (contains? (:features file) "storage/objectd-map") omap/wrap identity)] + (if (contains? (:features file) "storage/objects-map") omap/wrap identity)] (try (on-file file) (catch Throwable cause diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 96dd03c8fd..42611a84a6 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -538,8 +538,9 @@ ;; Implemented with transients for performance. 30~50% better (letfn [(process-shape [objects [id shape]] (let [frame-id (if (= :frame (:type shape)) id (:frame-id shape)) - cur (-> (or (get objects frame-id) (transient {})) - (assoc! id shape))] + cur (-> (or (get objects frame-id) + (transient {})) + (assoc! id shape))] (assoc! objects frame-id cur)))] (update-vals (->> objects diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index e2fb5a79ef..2cefb77a8c 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -311,8 +311,8 @@ [id] (l/derived (fn [objects] - (let [children-ids (get-in objects [id :shapes])] - (into [] (keep (d/getf objects)) children-ids))) + (->> (dm/get-in objects [id :shapes]) + (into [] (keep (d/getf objects))))) workspace-page-objects =)) (defn all-children-objects diff --git a/frontend/src/app/main/ui/shapes/bool.cljs b/frontend/src/app/main/ui/shapes/bool.cljs index d725a00aeb..7f378244f1 100644 --- a/frontend/src/app/main/ui/shapes/bool.cljs +++ b/frontend/src/app/main/ui/shapes/bool.cljs @@ -8,39 +8,41 @@ (:require [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] - [app.main.ui.hooks :refer [use-equal-memo]] + [app.main.ui.hooks :as h] [app.main.ui.shapes.export :as use] [app.main.ui.shapes.path :refer [path-shape]] - [app.util.object :as obj] [rumext.v2 :as mf])) (defn bool-shape [shape-wrapper] (mf/fnc bool-shape - {::mf/wrap-props false} - [props] - (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) + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + children (unchecked-get props "childs") + children (h/use-equal-memo children) - bool-content - (mf/use-memo - (mf/deps shape childs) - (fn [] - (cond - (some? (:bool-content shape)) - (:bool-content shape) + metadata? (mf/use-ctx use/include-metadata-ctx) + content (mf/with-memo [shape children] + (let [content (:bool-content shape)] + (cond + (some? content) + content - (some? childs) - (gsh/calc-bool-content shape childs))))] + (some? children) + (gsh/calc-bool-content shape children)))) - [:* - (when (some? bool-content) - [:& path-shape {:shape (assoc shape :content bool-content)}]) + shape (mf/with-memo [shape content] + (assoc shape :content content))] - (when include-metadata? - [:> "penpot:bool" {} - (for [item (->> (:shapes shape) (mapv #(get childs %)))] - [:& shape-wrapper {:shape item - :key (dm/str (:id item))}])])]))) + [:* + (when (some? content) + [:& path-shape {:shape shape}]) + + (when metadata? + ;; FIXME: get children looks wrong + [:> "penpot:bool" {} + (for [item (map #(get children %) (:shapes shape))] + [:& shape-wrapper + {:shape item + :key (dm/str (dm/get-prop item :id))}])])]))) diff --git a/frontend/src/app/main/ui/shapes/circle.cljs b/frontend/src/app/main/ui/shapes/circle.cljs index 4501098ba0..50a947252b 100644 --- a/frontend/src/app/main/ui/shapes/circle.cljs +++ b/frontend/src/app/main/ui/shapes/circle.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.shapes.circle (:require + [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]] @@ -16,21 +17,22 @@ {::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") - {:keys [x y width height]} shape - transform (gsh/transform-str shape) - cx (+ x (/ width 2)) - cy (+ y (/ height 2)) - rx (/ width 2) - ry (/ height 2) + x (dm/get-prop shape :x) + y (dm/get-prop shape :y) + w (dm/get-prop shape :width) + h (dm/get-prop shape :height) - props (-> (attrs/extract-style-attrs shape) - (obj/merge! - #js {:cx cx - :cy cy - :rx rx - :ry ry - :transform transform}))] + t (gsh/transform-str shape) + + cx (+ x (/ w 2)) + cy (+ y (/ h 2)) + rx (/ w 2) + ry (/ h 2) + + props (mf/with-memo [shape] + (-> (attrs/extract-style-attrs shape) + (obj/merge! #js {:cx cx :cy cy :rx rx :ry ry :transform t})))] [:& shape-custom-strokes {:shape shape} [:> :ellipse props]])) diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index 4b46446a58..e0c31dc614 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -20,32 +20,39 @@ [debug :refer [debug?]] [rumext.v2 :as mf])) -(defn frame-clip-id +(defn- frame-clip-id [shape render-id] - (dm/str "frame-clip-" (:id shape) "-" render-id)) + (dm/str "frame-clip-" (dm/get-prop shape :id) "-" render-id)) -(defn frame-clip-url +(defn- frame-clip-url [shape render-id] - (when (= :frame (:type shape)) - (dm/str "url(#" (frame-clip-id shape render-id) ")"))) + (dm/str "url(#" (frame-clip-id shape render-id) ")")) (mf/defc frame-clip-def - [{:keys [shape render-id]}] - (when (and (= :frame (:type shape)) (not (:show-content shape))) - (let [{:keys [x y width height]} shape - transform (gsh/transform-str shape) - props (-> (attrs/extract-style-attrs shape) - (obj/merge! - #js {:x x - :y y - :width width - :height height - :transform transform})) - path? (some? (.-d props))] - [:clipPath.frame-clip-def {:id (frame-clip-id shape render-id) :class "frame-clip"} - (if ^boolean path? - [:> :path props] - [:> :rect props])]))) + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape")] + (when (and ^boolean (cph/frame-shape? shape) + (not ^boolean (:show-content shape))) + + (let [render-id (unchecked-get props "render-id") + x (dm/get-prop shape :x) + y (dm/get-prop shape :y) + w (dm/get-prop shape :width) + h (dm/get-prop shape :height) + t (gsh/transform-str shape) + + props (mf/with-memo [shape] + (-> (attrs/extract-style-attrs shape) + (obj/merge! #js {:x x :y y :width w :height h :transform t}))) + + path? (some? (.-d props))] + + [:clipPath {:id (frame-clip-id shape render-id) + :class "frame-clip frame-clip-def"} + (if ^boolean path? + [:> :path props] + [:> :rect props])])))) ;; Wrapper around the frame that will handle things such as strokes and other properties ;; we wrap the proper frames and also the thumbnails @@ -53,29 +60,38 @@ {::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - children (unchecked-get props "children") + (let [shape (unchecked-get props "shape") + children (unchecked-get props "children") - {:keys [x y width height show-content]} shape - transform (gsh/transform-str shape) + render-id (mf/use-ctx muc/render-id) - render-id (mf/use-ctx muc/render-id) + x (dm/get-prop shape :x) + y (dm/get-prop shape :y) + w (dm/get-prop shape :width) + h (dm/get-prop shape :height) + transform (gsh/transform-str shape) + + show-content? (get shape :show-content) + + props (mf/with-memo [shape render-id] + (-> (attrs/extract-style-attrs shape render-id) + (obj/merge! + #js {:x x + :y y + :width w + :height h + :transform transform + :className "frame-background"}))) + path? (some? (.-d props))] - props (-> (attrs/extract-style-attrs shape render-id) - (obj/merge! - #js {:x x - :y y - :transform transform - :width width - :height height - :className "frame-background"})) - path? (some? (.-d props))] [:* - [:g {:clip-path (when (not show-content) (frame-clip-url shape render-id)) - :fill "none"} ;; A frame sets back normal fill behavior (default transparent). It may have - ;; been changed to default black if a shape coming from an imported SVG file - ;; is rendered. See main.ui.shapes.attrs/add-style-attrs. - [:& frame-clip-def {:shape shape :render-id render-id}] + [:g {:clip-path (when-not ^boolean show-content? + (frame-clip-url shape render-id)) + ;; A frame sets back normal fill behavior (default + ;; transparent). It may have been changed to default black + ;; if a shape coming from an imported SVG file is + ;; rendered. See main.ui.shapes.attrs/add-style-attrs. + :fill "none"} [:& shape-fills {:shape shape} (if ^boolean path? @@ -94,34 +110,43 @@ {::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") - bounds (or (unchecked-get props "bounds") - (grc/points->rect (:points shape))) + bounds (unchecked-get props "bounds") + + shape-id (dm/get-prop shape :id) + points (dm/get-prop shape :points) + + bounds (mf/with-memo [bounds points] + (or bounds (grc/points->rect points))) - shape-id (:id shape) thumb (:thumbnail shape) debug? (debug? :thumbnails) - safari? (cf/check-browser? :safari)] + safari? (cf/check-browser? :safari) + + ;; FIXME: ensure bounds is always a rect instance and + ;; dm/get-prop for static attr access + bx (:x bounds) + by (:y bounds) + bh (:height bounds) + bw (:width bounds)] [:* [:image.frame-thumbnail {:id (dm/str "thumbnail-" shape-id) :href thumb :decoding "async" - ;; FIXME: ensure bounds is always a rect instance and - ;; dm/get-prop for static attr access - :x (:x bounds) - :y (:y bounds) - :width (:width bounds) - :height (:height bounds) + :x bx + :y by + :width bw + :height bh :style {:filter (when (and (not ^boolean safari?) ^boolean debug?) "sepia(1)")}}] ;; Safari don't support filters so instead we add a rectangle around the thumbnail (when (and ^boolean safari? ^boolean debug?) - [:rect {:x (+ (:x bounds) 4) - :y (+ (:y bounds) 4) - :width (- (:width bounds) 8) - :height (- (:height bounds) 8) + [:rect {:x (+ bx 4) + :y (+ by 4) + :width (- bw 8) + :height (- bh 8) :stroke "red" :stroke-width 2}])])) @@ -138,17 +163,22 @@ (mf/fnc frame-shape {::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - childs (unchecked-get props "childs") - childs (cond-> childs - (ctl/any-layout? shape) - (cph/sort-layout-children-z-index)) - is-component? (mf/use-ctx muc/is-component?)] + (let [shape (unchecked-get props "shape") + childs (unchecked-get props "childs") + is-component? (mf/use-ctx muc/is-component?) + childs (cond-> childs + (ctl/any-layout? shape) + (cph/sort-layout-children-z-index)) + ] + [:> frame-container props [:g.frame-children {:opacity (:opacity shape)} - (for [{:keys [id] :as item} childs] - (when (some? id) - [:& shape-wrapper {:key (dm/str (:id item)) :shape item}]))] - (when (and is-component? (empty? childs)) + (for [item childs] + (let [id (dm/get-prop item :id)] + (when (some? id) + [:& shape-wrapper {:key (dm/str id) :shape item}])))] + + (when (and ^boolean is-component? + ^boolean (empty? childs)) [:& grid-layout-viewer {:shape shape :childs childs}])]))) diff --git a/frontend/src/app/main/ui/shapes/gradients.cljs b/frontend/src/app/main/ui/shapes/gradients.cljs index 11b92af6d9..2eb3b16cc0 100644 --- a/frontend/src/app/main/ui/shapes/gradients.cljs +++ b/frontend/src/app/main/ui/shapes/gradients.cljs @@ -10,108 +10,130 @@ [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] + [app.common.pages.helpers :as cph] [app.main.ui.context :as muc] [app.main.ui.shapes.export :as ed] [app.util.object :as obj] [rumext.v2 :as mf])) -(defn add-metadata [props gradient] +(defn- add-metadata! + [props gradient] (-> props (obj/set! "penpot:gradient" "true") (obj/set! "penpot:start-x" (:start-x gradient)) - (obj/set! "penpot:start-x" (:start-x gradient)) (obj/set! "penpot:start-y" (:start-y gradient)) (obj/set! "penpot:end-x" (:end-x gradient)) (obj/set! "penpot:end-y" (:end-y gradient)) (obj/set! "penpot:width" (:width gradient)))) -(mf/defc linear-gradient [{:keys [id gradient shape]}] - (let [transform (when (= :path (:type shape)) - (gsh/transform-matrix shape nil (gpt/point 0.5 0.5))) +(mf/defc linear-gradient + {::mf/wrap-props false} + [{:keys [id gradient shape]}] + (let [transform (mf/with-memo [shape] + (when (cph/frame-shape? shape) + (gsh/transform-matrix shape nil (gpt/point 0.5 0.5)))) - base-props #js {:id id - :x1 (:start-x gradient) - :y1 (:start-y gradient) - :x2 (:end-x gradient) - :y2 (:end-y gradient) - :gradientTransform (dm/str transform)} + metadata? (mf/use-ctx ed/include-metadata-ctx) + props #js {:id id + :x1 (:start-x gradient) + :y1 (:start-y gradient) + :x2 (:end-x gradient) + :y2 (:end-y gradient) + :gradientTransform (dm/str transform)}] - include-metadata? (mf/use-ctx ed/include-metadata-ctx) - - props (cond-> base-props - include-metadata? - (add-metadata gradient))] + (when ^boolean metadata? + (add-metadata! props gradient)) [:> :linearGradient props (for [{:keys [offset color opacity]} (:stops gradient)] [:stop {:key (dm/str id "-stop-" offset) - :offset (or offset 0) + :offset (d/nilv offset 0) :stop-color color :stop-opacity opacity}])])) -(mf/defc radial-gradient [{:keys [id gradient shape]}] - (let [path? (= :path (:type shape)) - shape-transform (or (when path? (:transform shape)) (gmt/matrix)) - shape-transform-inv (or (when path? (:transform-inverse shape)) (gmt/matrix)) +(mf/defc radial-gradient + {::mf/wrap-props false} + [{:keys [id gradient shape]}] + (let [path? (cph/path-shape? shape) + + transform (when ^boolean path? + (dm/get-prop shape :transform)) + transform (d/nilv transform gmt/base) + + transform-inv (when ^boolean path? + (dm/get-prop shape :transform-inverse)) + transform-inv (d/nilv transform-inv gmt/base) {:keys [start-x start-y end-x end-y] gwidth :width} gradient - gradient-vec (gpt/to-vec (gpt/point start-x start-y) - (gpt/point end-x end-y)) + gstart-pt (gpt/point start-x start-y) + gend-pt (gpt/point end-x end-y) + gradient-vec (gpt/to-vec gstart-pt gend-pt) - angle (+ (gpt/angle gradient-vec) 90) + angle (+ (gpt/angle gradient-vec) 90) - bb-shape (gsh/shapes->rect [shape]) + points (dm/get-prop shape :points) + bounds (mf/with-memo [points] + (grc/points->rect points)) + selrect (dm/get-prop shape :selrect) - ;; Paths don't have a transform in SVG because we transform the points - ;; we need to compensate the difference between the original rectangle - ;; and the transformed one. This factor is that calculation. - factor (if path? - (/ (:height (:selrect shape)) (:height bb-shape)) - 1.0) + ;; Paths don't have a transform in SVG because we transform + ;; the points we need to compensate the difference between the + ;; original rectangle and the transformed one. This factor is + ;; that calculation. + factor (if ^boolean path? + (/ (dm/get-prop selrect :height) + (dm/get-prop bounds :height)) + 1.0) - transform (-> (gmt/matrix) - (gmt/translate (gpt/point start-x start-y)) - (gmt/multiply shape-transform) - (gmt/rotate angle) - (gmt/scale (gpt/point gwidth factor)) - (gmt/multiply shape-transform-inv) - (gmt/translate (gpt/negate (gpt/point start-x start-y)))) + transform (mf/with-memo [gradient transform transform-inv factor] + (-> (gmt/matrix) + (gmt/translate gstart-pt) + (gmt/multiply transform) + (gmt/rotate angle) + (gmt/scale (gpt/point gwidth factor)) + (gmt/multiply transform-inv) + (gmt/translate (gpt/negate gstart-pt)))) - gradient-radius (gpt/length gradient-vec) - base-props #js {:id id - :cx start-x - :cy start-y - :r gradient-radius - :gradientTransform transform} + metadata? (mf/use-ctx ed/include-metadata-ctx) - include-metadata? (mf/use-ctx ed/include-metadata-ctx) + props #js {:id id + :cx start-x + :cy start-y + :r (gpt/length gradient-vec) + :gradientTransform transform}] + + (when ^boolean metadata? + (add-metadata! props gradient)) - props (cond-> base-props - include-metadata? - (add-metadata gradient))] [:> :radialGradient props (for [{:keys [offset color opacity]} (:stops gradient)] [:stop {:key (dm/str id "-stop-" offset) - :offset (or offset 0) + :offset (d/nilv offset 0) :stop-color color :stop-opacity opacity}])])) (mf/defc gradient {::mf/wrap-props false} [props] - (let [attr (obj/get props "attr") - shape (obj/get props "shape") - id (obj/get props "id") - id' (mf/use-ctx muc/render-id) - id (or id (dm/str (name attr) "_" id')) + (let [attr (unchecked-get props "attr") + shape (unchecked-get props "shape") + id (unchecked-get props "id") + rid (mf/use-ctx muc/render-id) + + id (if (some? id) + id + (dm/str (name attr) "_" rid)) + gradient (get shape attr) - gradient-props #js {:id id - :gradient gradient - :shape shape}] - (when gradient - (case (d/name (:type gradient)) - "linear" [:> linear-gradient gradient-props] - "radial" [:> radial-gradient gradient-props] + props #js {:id id + :gradient gradient + :shape shape}] + + (when (some? gradient) + (case (:type gradient) + :linear [:> linear-gradient props] + :radial [:> radial-gradient props] nil)))) diff --git a/frontend/src/app/main/ui/shapes/group.cljs b/frontend/src/app/main/ui/shapes/group.cljs index 2be8a46ab1..8b3ad40ce8 100644 --- a/frontend/src/app/main/ui/shapes/group.cljs +++ b/frontend/src/app/main/ui/shapes/group.cljs @@ -9,7 +9,6 @@ [app.common.data.macros :as dm] [app.main.ui.context :as muc] [app.main.ui.shapes.mask :refer [mask-url clip-url mask-factory]] - [app.util.object :as obj] [rumext.v2 :as mf])) (defn group-shape @@ -18,41 +17,40 @@ (mf/fnc group-shape {::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - childs (unchecked-get props "childs") - objects (unchecked-get props "objects") - render-id (mf/use-ctx muc/render-id) - masked-group? (:masked-group shape) + (let [shape (unchecked-get props "shape") + childs (unchecked-get props "childs") + render-id (mf/use-ctx muc/render-id) + masked-group? (:masked-group shape) - [mask childs] (if masked-group? - [(first childs) (rest childs)] - [nil childs]) + mask (if ^boolean masked-group? + (first childs) + nil) + childs (if ^boolean masked-group? + (rest childs) + childs) - ;; We need to separate mask and clip into two because a bug in Firefox - ;; breaks when the group has clip+mask+foreignObject - ;; Clip and mask separated will work in every platform - ; Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1734805 - [clip-wrapper clip-props] - (if masked-group? - ["g" (-> (obj/create) - (obj/set! "clipPath" (clip-url render-id mask)))] - [mf/Fragment nil]) + wrapper (if ^boolean masked-group? "g" mf/Fragment) + clip-props (if ^boolean masked-group? + #js {:clipPath (clip-url render-id mask)} + #js {}) - [mask-wrapper mask-props] - (if masked-group? - ["g" (-> (obj/create) - (obj/set! "mask" (mask-url render-id mask)))] - [mf/Fragment nil])] + mask-props (if ^boolean masked-group? + #js {:mask (mask-url render-id mask)} + #js {})] - [:> clip-wrapper clip-props - [:> mask-wrapper mask-props - (when masked-group? - [:> render-mask #js {:mask mask - :objects objects}]) + ;; We need to separate mask and clip into two because a bug in + ;; Firefox breaks when the group has clip+mask+foreignObject + ;; Clip and mask separated will work in every platform Firefox + ;; bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1734805 + [:> wrapper clip-props + [:> wrapper mask-props + (when ^boolean masked-group? + [:& render-mask {:mask mask}]) (for [item childs] - [:& shape-wrapper {:shape item - :key (dm/str (:id item))}])]])))) + [:& shape-wrapper + {:shape item + :key (dm/str (dm/get-prop item :id))}])]])))) diff --git a/frontend/src/app/main/ui/shapes/mask.cljs b/frontend/src/app/main/ui/shapes/mask.cljs index 14c0dd22ef..2f81478153 100644 --- a/frontend/src/app/main/ui/shapes/mask.cljs +++ b/frontend/src/app/main/ui/shapes/mask.cljs @@ -9,27 +9,28 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.rect :as grc] + [app.common.pages.helpers :as cph] [app.main.ui.context :as muc] [cuerdas.core :as str] [rumext.v2 :as mf])) (defn mask-id [render-id mask] - (str render-id "-" (:id mask) "-mask")) + (dm/str render-id "-" (:id mask) "-mask")) (defn mask-url [render-id mask] - (str "url(#" (mask-id render-id mask) ")")) + (dm/str "url(#" (mask-id render-id mask) ")")) (defn clip-id [render-id mask] - (str render-id "-" (:id mask) "-clip")) + (dm/str render-id "-" (:id mask) "-clip")) (defn clip-url [render-id mask] - (str "url(#" (clip-id render-id mask) ")")) + (dm/str "url(#" (clip-id render-id mask) ")")) (defn filter-id [render-id mask] - (str render-id "-" (:id mask) "-filter")) + (dm/str render-id "-" (:id mask) "-filter")) (defn filter-url [render-id mask] - (str "url(#" (filter-id render-id mask) ")")) + (dm/str "url(#" (filter-id render-id mask) ")")) (defn set-white-fill [shape] @@ -42,17 +43,39 @@ (d/update-when :position-data #(mapv update-color %)) (assoc :stroke-color "#FFFFFF" :stroke-opacity 1)))) +(defn- point->str + [point] + (dm/str (dm/get-prop point :x) "," (dm/get-prop point :y))) + (defn mask-factory [shape-wrapper] (mf/fnc mask-shape {::mf/wrap-props false} [props] - (let [mask (unchecked-get props "mask") - render-id (mf/use-ctx muc/render-id) - svg-text? (and (= :text (:type mask)) (some? (:position-data mask))) + (let [mask (unchecked-get props "mask") + render-id (mf/use-ctx muc/render-id) + + svg-text? (and ^boolean (cph/text-shape? mask) + ^boolean (some? (:position-data mask))) + + points (dm/get-prop mask :points) + points-str (mf/with-memo [points] + (->> (map point->str points) + (str/join " "))) + + bounds (mf/with-memo [points] + (grc/points->rect points)) + + bx (dm/get-prop bounds :x) + by (dm/get-prop bounds :y) + bw (dm/get-prop bounds :width) + bh (dm/get-prop bounds :height) + + shape (mf/with-memo [mask] + (-> mask + (dissoc :shadow :blur) + (assoc :is-mask? true)))] - mask-bb (:points mask) - mask-bb-rect (grc/points->rect mask-bb)] [:defs [:filter {:id (filter-id render-id mask)} [:feFlood {:flood-color "white" @@ -66,26 +89,26 @@ ;; we cannot use clips instead of mask because clips can only be simple shapes [:clipPath {:class "mask-clip-path" :id (clip-id render-id mask)} - [:polyline {:points (->> mask-bb - (map #(dm/str (:x %) "," (:y %))) - (str/join " "))}]] + [:polyline {:points points-str}]] ;; When te shape is a text we pass to the shape the info and disable the filter. ;; There is a bug in Firefox with filters and texts. We change the text to white at shape level [:mask {:class "mask-shape" :id (mask-id render-id mask) - :x (:x mask-bb-rect) - :y (:y mask-bb-rect) - :width (:width mask-bb-rect) - :height (:height mask-bb-rect) + :x bx + :y by + :width bw + :height bh ;; This is necesary to prevent a race condition in the dynamic-modifiers whether the modifier ;; triggers afte the render - :data-old-x (:x mask-bb-rect) - :data-old-y (:y mask-bb-rect) - :data-old-width (:width mask-bb-rect) - :data-old-height (:height mask-bb-rect) + :data-old-x bx + :data-old-y by + :data-old-width bw + :data-old-height bh :mask-units "userSpaceOnUse"} - [:g {:filter (when-not svg-text? (filter-url render-id mask))} - [:& shape-wrapper {:shape (-> mask (dissoc :shadow :blur) (assoc :is-mask? true))}]]]]))) + + [:g {:filter (when-not ^boolean svg-text? + (filter-url render-id mask))} + [:& shape-wrapper {:shape shape}]]]]))) diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index 26fd703cb0..45a3bc0feb 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -54,7 +54,7 @@ children (unchecked-get props "children") pointer-events (unchecked-get props "pointer-events") disable-shadows? (unchecked-get props "disable-shadows?") - shape-id (:id shape) + shape-id (dm/get-prop shape :id) preview-blend-mode-ref (mf/with-memo [shape-id] (refs/workspace-preview-blend-by-id shape-id)) @@ -62,7 +62,7 @@ blend-mode (-> (mf/deref preview-blend-mode-ref) (or (:blend-mode shape))) - type (:type shape) + type (dm/get-prop shape :type) render-id (mf/use-id) filter-id (dm/str "filter_" render-id) styles (-> (obj/create) diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index 083713ddbc..fb4563e709 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -50,23 +50,14 @@ (let [objects (obj/get props "objects") active-frames (obj/get props "active-frames") shapes (cph/get-immediate-children objects) - - ;; We group the objects together per frame-id so if an object of a different - ;; frame changes won't affect the rendering frame - frame-objects - (mf/with-memo [objects] - (cph/objects-by-frame objects)) - - vbox (mf/use-ctx ctx/current-vbox) - shapes - (mf/with-memo [shapes vbox] - (if (some? vbox) - (->> shapes - (filterv (fn [shape] - (grc/overlaps-rects? vbox (dm/get-prop shape :selrect))))) - shapes))] + shapes (mf/with-memo [shapes vbox] + (if (some? vbox) + (filter (fn [shape] + (grc/overlaps-rects? vbox (dm/get-prop shape :selrect))) + shapes) + shapes))] [:g {:id (dm/str "shape-" uuid/zero)} [:& (mf/provider ctx/active-frames) {:value active-frames} @@ -83,17 +74,11 @@ (if ^boolean (cph/frame-shape? shape) [:& root-frame-wrapper {:shape shape - :objects (get frame-objects (dm/get-prop shape :id)) :thumbnail? (not (contains? active-frames (dm/get-prop shape :id)))}] [:& shape-wrapper {:shape shape}])])]]])) -(defn- check-shape-wrapper-props - [np op] - (frame/check-shape (unchecked-get np "shape") - (unchecked-get op "shape"))) - (mf/defc shape-wrapper - {::mf/wrap [#(mf/memo' % check-shape-wrapper-props)] + {::mf/wrap [#(mf/memo' % common/check-shape-props)] ::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") @@ -109,26 +94,27 @@ (and (some? active-frames) (not (contains? active-frames shape-id))) - opts #js {:shape shape :thumbnail? thumbnail?} + props #js {:shape shape :thumbnail? thumbnail?} - [wrapper wrapper-props] - (if (= :svg-raw shape-type) - [mf/Fragment nil] - ["g" #js {:className "workspace-shape-wrapper"}])] + rawsvg? (= :svg-raw shape-type) + wrapper-elem (if ^boolean rawsvg? mf/Fragment "g") + wrapper-props (if ^boolean rawsvg? + #js {:className "workspace-shape-wrapper"} + #js {})] (when (and (some? shape) (not ^boolean (:hidden shape))) - [:> wrapper wrapper-props + [:> wrapper-elem wrapper-props (case shape-type - :path [:> path/path-wrapper opts] - :text [:> text/text-wrapper opts] - :group [:> group-wrapper opts] - :rect [:> rect-wrapper opts] - :image [:> image-wrapper opts] - :circle [:> circle-wrapper opts] - :svg-raw [:> svg-raw-wrapper opts] - :bool [:> bool-wrapper opts] - :frame [:> nested-frame-wrapper opts] + :path [:> path/path-wrapper props] + :text [:> text/text-wrapper props] + :group [:> group-wrapper props] + :rect [:> rect-wrapper props] + :image [:> image-wrapper props] + :circle [:> circle-wrapper props] + :svg-raw [:> svg-raw-wrapper props] + :bool [:> bool-wrapper props] + :frame [:> nested-frame-wrapper props] nil)]))) diff --git a/frontend/src/app/main/ui/workspace/shapes/bool.cljs b/frontend/src/app/main/ui/workspace/shapes/bool.cljs index 62cc6e8212..6a07eeed85 100644 --- a/frontend/src/app/main/ui/workspace/shapes/bool.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/bool.cljs @@ -6,47 +6,37 @@ (ns app.main.ui.workspace.shapes.bool (:require - [app.main.data.workspace :as dw] + [app.common.data.macros :as dm] [app.main.refs :as refs] - [app.main.store :as st] - [app.main.streams :as ms] [app.main.ui.shapes.bool :as bool] [app.main.ui.shapes.shape :refer [shape-container]] - [app.util.dom :as dom] + [app.main.ui.workspace.shapes.common :refer [check-shape-props]] [rumext.v2 :as mf])) -(defn use-double-click [{:keys [id]}] - (mf/use-callback - (mf/deps id) - (fn [event] - (dom/stop-propagation event) - (dom/prevent-default event) - (st/emit! (dw/select-inside-group id @ms/mouse-position))))) - (defn bool-wrapper-factory [shape-wrapper] - (let [shape-component (bool/bool-shape shape-wrapper)] + (let [bool-shape (bool/bool-shape shape-wrapper)] (mf/fnc bool-wrapper - {::mf/wrap [#(mf/memo' % (mf/check-props ["shape"]))] + {::mf/wrap [#(mf/memo' % check-shape-props)] ::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - child-sel-ref (mf/use-memo - (mf/deps (:id shape)) - #(refs/is-child-selected? (:id shape))) + (let [shape (unchecked-get props "shape") + shape-id (dm/get-prop shape :id) - childs-ref (mf/use-memo - (mf/deps (:id shape)) - #(refs/select-bool-children (:id shape))) + child-sel* (mf/with-memo [shape-id] + (refs/is-child-selected? shape-id)) - child-sel? (mf/deref child-sel-ref) - childs (mf/deref childs-ref) + childs* (mf/with-memo [shape-id] + (refs/select-bool-children shape-id)) - shape (cond-> shape - child-sel? - (dissoc :bool-content))] + child-sel? (mf/deref child-sel*) + childs (mf/deref childs*) + + shape (cond-> shape + ^boolean child-sel? + (dissoc :bool-content))] [:> shape-container {:shape shape} - [:& shape-component {:shape shape - :childs childs}]])))) + [:& bool-shape {:shape shape + :childs childs}]])))) diff --git a/frontend/src/app/main/ui/workspace/shapes/common.cljs b/frontend/src/app/main/ui/workspace/shapes/common.cljs index cccc1cb7ef..c407b8db8f 100644 --- a/frontend/src/app/main/ui/workspace/shapes/common.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/common.cljs @@ -6,13 +6,30 @@ (ns app.main.ui.workspace.shapes.common (:require + [app.common.record :as cr] [app.main.ui.shapes.shape :refer [shape-container]] [rumext.v2 :as mf])) +(def ^:private excluded-attrs + #{:blocked + :hide-fill-on-export + :collapsed + :remote-synced + :exports}) + +(defn check-shape + [new-shape old-shape] + (cr/-equiv-with-exceptions old-shape new-shape excluded-attrs)) + +(defn check-shape-props + [np op] + (check-shape (unchecked-get np "shape") + (unchecked-get op "shape"))) + (defn generic-wrapper-factory [component] (mf/fnc generic-wrapper - {::mf/wrap [#(mf/memo' % (mf/check-props ["shape"]))] + {::mf/wrap [#(mf/memo' % check-shape-props)] ::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape")] diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index 24c0882257..0fc57d3e60 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -9,7 +9,6 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.pages.helpers :as cph] - [app.common.record :as cr] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.thumbnails :as dwt] [app.main.refs :as refs] @@ -20,33 +19,18 @@ [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.shapes.text.fontfaces :as ff] + [app.main.ui.workspace.shapes.common :refer [check-shape-props]] [app.main.ui.workspace.shapes.frame.dynamic-modifiers :as fdm] [app.main.ui.workspace.shapes.frame.node-store :as fns] [app.main.ui.workspace.shapes.frame.thumbnail-render :as ftr] [beicon.core :as rx] [rumext.v2 :as mf])) -(def ^:private excluded-attrs - #{:blocked - :hide-fill-on-export - :collapsed - :remote-synced - :exports}) - -(defn check-shape - [new-shape old-shape] - (cr/-equiv-with-exceptions old-shape new-shape excluded-attrs)) - -(defn check-frame-props - [np op] - (check-shape (unchecked-get np "shape") - (unchecked-get op "shape"))) - (defn frame-shape-factory [shape-wrapper] (let [frame-shape (frame/frame-shape shape-wrapper)] (mf/fnc frame-shape-inner - {::mf/wrap [#(mf/memo' % check-frame-props)] + {::mf/wrap [#(mf/memo' % check-shape-props)] ::mf/wrap-props false ::mf/forward-ref true} [props ref] @@ -66,8 +50,7 @@ [new-props old-props] (and (= (unchecked-get new-props "thumbnail?") (unchecked-get old-props "thumbnail?")) - (check-shape (unchecked-get new-props "shape") - (unchecked-get old-props "shape")))) + (check-shape-props new-props old-props))) (defn nested-frame-wrapper-factory [shape-wrapper] @@ -77,16 +60,18 @@ {::mf/wrap [#(mf/memo' % check-props)] ::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - frame-id (:id shape) - objects (wsh/lookup-page-objects @st/state) - node-ref (mf/use-ref nil) - modifiers-ref (mf/use-memo (mf/deps frame-id) #(refs/workspace-modifiers-by-frame-id frame-id)) - modifiers (mf/deref modifiers-ref)] + (let [shape (unchecked-get props "shape") + objects (wsh/lookup-page-objects @st/state) + + frame-id (dm/get-prop shape :id) + + node-ref (mf/use-ref nil) + modifiers* (mf/with-memo [frame-id] + (refs/workspace-modifiers-by-frame-id frame-id)) + modifiers (mf/deref modifiers*)] (fdm/use-dynamic-modifiers objects (mf/ref-val node-ref) modifiers) - (let [shape (unchecked-get props "shape")] - [:& frame-shape {:shape shape :ref node-ref}]))))) + [:& frame-shape {:shape shape :ref node-ref}])))) (defn root-frame-wrapper-factory @@ -166,12 +151,9 @@ :key "frame-container" :ref on-frame-load :opacity (when (:hidden shape) 0)} - [:& ff/fontfaces-style {:fonts fonts}] + [:& ff/fontfaces-style {:fonts fonts}] [:g.frame-thumbnail-wrapper {:id (dm/str "thumbnail-container-" frame-id) ;; Hide the thumbnail when not displaying :opacity (when-not thumbnail? 0)} - children]] - - ])))) - + children]]])))) diff --git a/frontend/src/app/main/ui/workspace/shapes/group.cljs b/frontend/src/app/main/ui/workspace/shapes/group.cljs index dd9a1a47e8..5fbd2ad16b 100644 --- a/frontend/src/app/main/ui/workspace/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/group.cljs @@ -6,33 +6,26 @@ (ns app.main.ui.workspace.shapes.group (:require - [app.main.data.workspace :as dw] + [app.common.data.macros :as dm] [app.main.refs :as refs] - [app.main.store :as st] - [app.main.streams :as ms] [app.main.ui.shapes.group :as group] [app.main.ui.shapes.shape :refer [shape-container]] - [app.util.dom :as dom] + [app.main.ui.workspace.shapes.common :refer [check-shape-props]] [rumext.v2 :as mf])) -(defn use-double-click [{:keys [id]}] - (mf/use-callback - (mf/deps id) - (fn [event] - (dom/stop-propagation event) - (dom/prevent-default event) - (st/emit! (dw/select-inside-group id @ms/mouse-position))))) - (defn group-wrapper-factory [shape-wrapper] (let [group-shape (group/group-shape shape-wrapper)] (mf/fnc group-wrapper - {::mf/wrap [#(mf/memo' % (mf/check-props ["shape"]))] + {::mf/wrap [#(mf/memo' % check-shape-props)] ::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - childs-ref (mf/use-memo (mf/deps (:id shape)) #(refs/children-objects (:id shape))) - childs (mf/deref childs-ref)] + (let [shape (unchecked-get props "shape") + shape-id (dm/get-prop shape :id) + + childs* (mf/with-memo [shape-id] + (refs/children-objects shape-id)) + childs (mf/deref childs*)] [:> shape-container {:shape shape} [:& group-shape