diff --git a/CHANGES.md b/CHANGES.md index 0bf134bc1..f3e14a352 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,7 @@ - Add actions to go to main component context menu option [Taiga #2053](https://tree.taiga.io/project/penpot/us/2053). - Add contrast between component select color and shape select color [Taiga #2121](https://tree.taiga.io/project/penpot/issue/2121). - Add animations in interactions [Taiga #2244](https://tree.taiga.io/project/penpot/us/2244). +- Add performance improvements on .penpot file import process [Taiga 2497](https://tree.taiga.io/project/penpot/us/2497). ### :bug: Bugs fixed @@ -69,6 +70,7 @@ - Avoid modifying component when moving into a group [Taiga #2534](https://tree.taiga.io/project/penpot/issue/2534) - Show correctly group types label in handoff [Taiga #2482](https://tree.taiga.io/project/penpot/issue/2482) - Display view mode buttons always centered in viewer [#Taiga 2466](https://tree.taiga.io/project/penpot/issue/2466) +- Fix default profile image generation issue [Taiga #2601](https://tree.taiga.io/project/penpot/issue/2601) ### :arrow_up: Deps updates diff --git a/backend/scripts/repl b/backend/scripts/repl index 3ab0e1ca3..71fb65071 100755 --- a/backend/scripts/repl +++ b/backend/scripts/repl @@ -8,7 +8,7 @@ export OPTIONS=" -J-Dlog4j2.configurationFile=log4j2-devenv.xml \ -J-XX:+UseZGC \ -J-XX:-OmitStackTraceInFastThrow \ - -J-Xms50m -J-Xmx512m \ + -J-Xms50m -J-Xmx1024m \ -J-Djdk.attach.allowAttachSelf \ -J-XX:+UnlockDiagnosticVMOptions \ -J-XX:+DebugNonSafepoints"; diff --git a/backend/src/app/rpc/mutations/files.clj b/backend/src/app/rpc/mutations/files.clj index 4e787d43d..c145e2ceb 100644 --- a/backend/src/app/rpc/mutations/files.clj +++ b/backend/src/app/rpc/mutations/files.clj @@ -414,7 +414,9 @@ [conn project-id] (:team-id (db/get-by-id conn :project project-id {:columns [:team-id]}))) -;; TEMPORARY FILE CREATION +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TEMPORARY FILES (behaves differently) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (s/def ::create-temp-file ::create-file) @@ -424,6 +426,23 @@ (proj/check-edition-permissions! conn profile-id project-id) (create-file conn (assoc params :deleted-at (dt/in-future {:days 1}))))) +(s/def ::update-temp-file + (s/keys :req-un [::changes ::revn ::session-id ::id])) + +(sv/defmethod ::update-temp-file + [{:keys [pool] :as cfg} {:keys [profile-id session-id id revn changes] :as params}] + (db/with-atomic [conn pool] + (db/insert! conn :file-change + {:id (uuid/next) + :session-id session-id + :profile-id profile-id + :created-at (dt/now) + :file-id id + :revn revn + :data nil + :changes (blob/encode changes)}) + nil)) + (s/def ::persist-temp-file (s/keys :req-un [::id ::profile-id])) @@ -431,6 +450,25 @@ [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] (db/with-atomic [conn pool] (files/check-edition-permissions! conn profile-id id) - (db/update! conn :file - {:deleted-at nil} - {:id id}))) + (let [file (db/get-by-id conn :file id) + revs (db/query conn :file-change + {:file-id id} + {:order-by [[:revn :asc]]}) + revn (count revs)] + + (when (nil? (:deleted-at file)) + (ex/raise :type :validation + :code :cant-persist-already-persisted-file)) + + (loop [revs (seq revs) + data (blob/decode (:data file))] + (if-let [rev (first revs)] + (recur (rest revs) + (->> rev :changes blob/decode (cp/process-changes data))) + (db/update! conn :file + {:deleted-at nil + :revn revn + :data (blob/encode data)} + {:id id}))) + + nil))) diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index 5495de9f8..ea6832aa2 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -6,6 +6,8 @@ (ns app.rpc.queries.files (:require + [app.common.data :as d] + [app.common.pages :as cp] [app.common.pages.migrations :as pmg] [app.common.spec :as us] [app.common.uuid :as uuid] @@ -215,8 +217,66 @@ (some-> (retrieve-file cfg id) (assoc :permissions perms))))) -(defn remove-thumbnails-frames - "Removes from data the children for frames that have a thumbnail set up" +(declare trim-file-data) + +(s/def ::page-id ::us/uuid) +(s/def ::object-id ::us/uuid) + +(s/def ::trimmed-file + (s/keys :req-un [::profile-id ::id ::object-id ::page-id])) + +(sv/defmethod ::trimmed-file + "Retrieve a file by its ID and trims all unnecesary content from + it. It is mainly used for rendering a concrete object, so we don't + need force download all shapes when only a small subset is + necesseary." + [{:keys [pool] :as cfg} {:keys [profile-id id] :as params}] + (db/with-atomic [conn pool] + (let [cfg (assoc cfg :conn conn) + perms (get-permissions conn profile-id id)] + (check-read-permissions! perms) + (some-> (retrieve-file cfg id) + (trim-file-data params) + (assoc :permissions perms))))) + +(defn- trim-file-data + [file {:keys [page-id object-id]}] + (let [page (get-in file [:data :pages-index page-id]) + objects (->> (:objects page) + (cp/get-object-with-children object-id) + (map #(dissoc % :thumbnail))) + + objects (d/index-by :id objects) + page (assoc page :objects objects)] + + (-> file + (update :data assoc :pages-index {page-id page}) + (assoc :pages [page-id])))) + +(declare strip-frames-with-thumbnails) + +(s/def ::strip-frames-with-thumbnails ::us/boolean) + +(s/def ::page + (s/keys :req-un [::profile-id ::file-id] + :opt-un [::strip-frames-with-thumbnails])) + +(sv/defmethod ::page + "Retrieves the first page of the file. Used mainly for render + thumbnails on dashboard." + [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as props}] + (db/with-atomic [conn pool] + (check-read-permissions! conn profile-id file-id) + + (let [cfg (assoc cfg :conn conn) + file (retrieve-file cfg file-id) + page-id (get-in file [:data :pages 0])] + (cond-> (get-in file [:data :pages-index page-id]) + (true? (:strip-frames-with-thumbnails props)) + (strip-frames-with-thumbnails))))) + +(defn strip-frames-with-thumbnails + "Remove unnecesary shapes from frames that have thumbnail." [data] (let [filter-shape? (fn [objects [id shape]] @@ -242,22 +302,6 @@ (update data :objects update-objects))) -(s/def ::page - (s/keys :req-un [::profile-id ::file-id])) - -(sv/defmethod ::page - "Retrieves the first page of the file. Used mainly for render - thumbnails on dashboard." - [{:keys [pool] :as cfg} {:keys [profile-id file-id strip-thumbnails]}] - (db/with-atomic [conn pool] - (check-read-permissions! conn profile-id file-id) - - (let [cfg (assoc cfg :conn conn) - file (retrieve-file cfg file-id) - page-id (get-in file [:data :pages 0])] - (cond-> (get-in file [:data :pages-index page-id]) - strip-thumbnails - (remove-thumbnails-frames))))) ;; --- Query: Shared Library Files diff --git a/common/src/app/common/geom/align.cljc b/common/src/app/common/geom/align.cljc index fc678e20e..11c8422c6 100644 --- a/common/src/app/common/geom/align.cljc +++ b/common/src/app/common/geom/align.cljc @@ -6,7 +6,9 @@ (ns app.common.geom.align (:require + [app.common.data :as d] [app.common.geom.shapes :as gsh] + [app.common.pages.helpers :refer [get-children]] [clojure.spec.alpha :as s])) ;; --- Alignment @@ -15,23 +17,13 @@ (declare calc-align-pos) -;; TODO: revisit on how to reuse code and dont have this function -;; duplicated because the implementation right now differs from the -;; original function. - -;; Duplicated from pages/helpers to remove cyclic dependencies -(defn- get-children [id objects] - (let [shapes (vec (get-in objects [id :shapes]))] - (if shapes - (into shapes (mapcat #(get-children % objects)) shapes) - []))) - (defn- recursive-move "Move the shape and all its recursive children." [shape dpoint objects] - (let [children-ids (get-children (:id shape) objects) - children (map #(get objects %) children-ids)] - (map #(gsh/move % dpoint) (cons shape children)))) + (->> (get-children (:id shape) objects) + (map (d/getf objects)) + (cons shape) + (map #(gsh/move % dpoint)))) (defn align-to-rect "Move the shape so that it is aligned with the given rectangle diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc index 7ff2aec82..56010f196 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -412,7 +412,7 @@ {:rotation angle :displacement displacement})) -(defn merge-modifiers* +(defn merge-modifiers [objects modifiers] (let [set-modifier @@ -422,8 +422,6 @@ (->> modifiers (reduce set-modifier objects)))) -(def merge-modifiers (memoize merge-modifiers*)) - (defn modifiers->transform ([modifiers] (modifiers->transform nil modifiers)) diff --git a/common/src/app/common/logging.cljc b/common/src/app/common/logging.cljc index 033de85cb..6800253c3 100644 --- a/common/src/app/common/logging.cljc +++ b/common/src/app/common/logging.cljc @@ -168,7 +168,7 @@ `(write-log! ~(or logger (str *ns*)) ~level ~cause - ~(dissoc props :level :cause ::logger ::raw)) + (or ~raw ~(dissoc props :level :cause ::logger ::raw))) (let [props (dissoc props :level :cause ::logger ::async ::raw) logger (or logger (str *ns*)) logger-sym (gensym "log") diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 65dddbbdd..4fd822eca 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -99,37 +99,10 @@ [component] (get-in component [:objects (:id component)])) -;; Implemented with transient for performance -(defn get-children* - "Retrieve all children ids recursively for a given object. The - children's order will be breadth first." - [id objects] - (loop [result (transient []) - pending (transient []) - next id] - (let [children (get-in objects [next :shapes] []) - [result pending] - ;; Iterate through children and add them to the result - ;; also add them in pending to check for their children - (loop [result result - pending pending - current (first children) - children (rest children)] - (if current - (recur (conj! result current) - (conj! pending current) - (first children) - (rest children)) - [result pending])) - - ;; If we have still pending, advance the iterator - length (count pending)] - (if (pos? length) - (let [next (get pending (dec length))] - (recur result (pop! pending) next)) - (persistent! result))))) - -(def get-children (memoize get-children*)) +(defn get-children [id objects] + (if-let [shapes (-> (get objects id) :shapes (some-> vec))] + (into shapes (mapcat #(get-children % objects)) shapes) + [])) (defn get-children-objects "Retrieve all children objects recursively for a given object" @@ -175,7 +148,7 @@ shape (get objects (:frame-id shape)))) -(defn clean-loops* +(defn clean-loops "Clean a list of ids from circular references." [objects ids] @@ -192,8 +165,6 @@ (reduce add-element (d/ordered-set) ids))) -(def clean-loops (memoize clean-loops*)) - (defn calculate-invalid-targets [shape-id objects] (let [result #{shape-id} diff --git a/exporter/scripts/build b/exporter/scripts/build index 1df9a1707..a4c9df9dc 100755 --- a/exporter/scripts/build +++ b/exporter/scripts/build @@ -2,17 +2,21 @@ set -ex +CURRENT_VERSION=$1; + yarn install rm -rf target export NODE_ENV=production; # Build the application -clojure -M:dev:shadow-cljs release main +clojure -M:dev:shadow-cljs release main; # Remove source -rm -rf target/app +rm -rf target/app; # Copy package*.json files -cp yarn.lock target/ -cp package.json target/ +cp yarn.lock target/; +cp package.json target/; + +sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/app.js; diff --git a/exporter/src/app/browser.cljs b/exporter/src/app/browser.cljs index 051a34422..b16ad95cf 100644 --- a/exporter/src/app/browser.cljs +++ b/exporter/src/app/browser.cljs @@ -12,9 +12,9 @@ [app.common.logging :as l] [app.common.uuid :as uuid] [app.config :as cf] + [app.util.object :as obj] [promesa.core :as p])) - (l/set-level! :trace) ;; --- BROWSER API @@ -35,7 +35,8 @@ [page {:keys [timeout cookie user-agent viewport]}] (let [timeout (or timeout default-timeout) user-agent (or user-agent default-user-agent) - viewport (d/merge default-viewport viewport)] + viewport (merge default-viewport viewport)] + (p/do! (.setViewport ^js page #js {:width (:width viewport) :height (:height viewport) @@ -63,29 +64,27 @@ (defn screenshot ([frame] (screenshot frame nil)) ([frame {:keys [full-page? omit-background? type] - :or {full-page? false - type "png" + :or {type "png" + full-page? false omit-background? false}}] - (.screenshot ^js frame #js {:fullPage full-page? - :type (name type) - :omitBackground omit-background?}))) + (let [options (-> (obj/new) + (obj/set! "type" (name type)) + (obj/set! "omitBackground" omit-background?) + (cond-> full-page? (-> (obj/set! "fullPage" true) + (obj/set! "clip" nil))))] + (.screenshot ^js frame options)))) (defn pdf ([page] (pdf page nil)) - ([page {:keys [viewport omit-background? prefer-css-page-size? save-path] - :or {viewport {} - omit-background? true - prefer-css-page-size? true - save-path nil}}] - (let [viewport (d/merge default-viewport viewport)] + ([page {:keys [viewport save-path]}] + (p/let [viewport (d/merge default-viewport viewport)] + (.emulateMediaType ^js page "screen") (.pdf ^js page #js {:path save-path :width (:width viewport) :height (:height viewport) :scale (:scale viewport) - :omitBackground omit-background? - :printBackground (not omit-background?) - :preferCSSPageSize prefer-css-page-size?})))) - + :printBackground true + :preferCSSPageSize true})))) (defn eval! [frame f] (.evaluate ^js frame f)) @@ -104,10 +103,17 @@ (defonce pool (atom nil)) (defonce pool-browser-id (atom 1)) +(def default-chrome-args + #js ["--no-sandbox" + "--font-render-hinting=none" + "--disable-setuid-sandbox" + "--disable-accelerated-2d-canvas" + "--disable-gpu"]) + (def browser-pool-factory (letfn [(create [] (let [path (cf/get :browser-executable-path "/usr/bin/google-chrome")] - (-> (pp/launch #js {:executablePath path :args #js ["--no-sandbox" "--font-render-hinting=none"]}) + (-> (pp/launch #js {:executablePath path :args default-chrome-args}) (p/then (fn [browser] (let [id (deref pool-browser-id)] (l/info :origin "factory" :action "create" :browser-id id) diff --git a/exporter/src/app/config.cljs b/exporter/src/app/config.cljs index 307019171..f1abb6352 100644 --- a/exporter/src/app/config.cljs +++ b/exporter/src/app/config.cljs @@ -71,9 +71,7 @@ (atom (prepare-config))) (def version - (atom (v/parse (or (some-> (ex/ignoring (fs/readFileSync "version.txt")) - (str/trim)) - "%version%")))) + (atom (v/parse "%version%"))) (defn get "A configuration getter." diff --git a/exporter/src/app/renderer/pdf.cljs b/exporter/src/app/renderer/pdf.cljs index 4a579bf00..b421befa0 100644 --- a/exporter/src/app/renderer/pdf.cljs +++ b/exporter/src/app/renderer/pdf.cljs @@ -31,20 +31,26 @@ (let [path (str "/render-object/" file-id "/" page-id "/" object-id) uri (-> (u/uri (cf/get :public-uri)) (assoc :path "/") + (assoc :query "essential=t") (assoc :fragment path)) + cookie (create-cookie uri token)] (pdf-from page (str uri) cookie))) (pdf-from [page uri cookie] (l/info :uri uri) - (let [options {:cookie cookie}] - (p/do! - (bw/configure-page! page options) - (bw/navigate! page uri) - (bw/wait-for page "#screenshot") - (if save-path - (bw/pdf page {:save-path save-path}) - (bw/pdf page)))))] + (p/let [options {:cookie cookie}] + (bw/configure-page! page options) + (bw/navigate! page uri) + (bw/wait-for page "#screenshot") + ;; taking png screenshot before pdf, helps to make the + ;; pdf rendering works as expected. + (p/let [dom (bw/select page "#screenshot")] + (bw/screenshot dom {:full-page? true})) + + (if save-path + (bw/pdf page {:save-path save-path}) + (bw/pdf page))))] (bw/exec! handle))) diff --git a/exporter/src/app/util/object.cljs b/exporter/src/app/util/object.cljs new file mode 100644 index 000000000..35dcaeccc --- /dev/null +++ b/exporter/src/app/util/object.cljs @@ -0,0 +1,86 @@ +;; 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.util.object + "A collection of helpers for work with javascript objects." + (:refer-clojure :exclude [set! get get-in merge clone contains?]) + (:require + [cuerdas.core :as str])) + +(defn new [] #js {}) + +(defn get + ([obj k] + (when-not (nil? obj) + (unchecked-get obj k))) + ([obj k default] + (let [result (get obj k)] + (if (undefined? result) default result)))) + +(defn contains? + [obj k] + (some? (unchecked-get obj k))) + +(defn get-keys + [obj] + (js/Object.keys ^js obj)) + +(defn get-in + ([obj keys] + (get-in obj keys nil)) + + ([obj keys default] + (loop [key (first keys) + keys (rest keys) + res obj] + (if (or (nil? key) (nil? res)) + (or res default) + (recur (first keys) + (rest keys) + (unchecked-get res key)))))) + +(defn clone + [a] + (js/Object.assign #js {} a)) + +(defn merge! + ([a b] + (js/Object.assign a b)) + ([a b & more] + (reduce merge! (merge! a b) more))) + +(defn merge + ([a b] + (js/Object.assign #js {} a b)) + ([a b & more] + (reduce merge! (merge a b) more))) + +(defn set! + [obj key value] + (unchecked-set obj key value) + obj) + +(defn update! + [obj key f & args] + (let [found (get obj key ::not-found)] + (if-not (identical? ::not-found found) + (do (unchecked-set obj key (apply f found args)) + obj) + obj))) + +(defn- props-key-fn + [key] + (if (or (= key :class) (= key :class-name)) + "className" + (str/camel (name key)))) + +(defn clj->props + [props] + (clj->js props :keyword-fn props-key-fn)) + +(defn ^boolean in? + [obj prop] + (js* "~{} in ~{}" prop obj)) diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 79af0a906..50e0f18db 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -20,9 +20,11 @@ [app.main.ui.routes :as rt] [app.main.worker :as worker] [app.util.dom :as dom] + [app.util.globals :as glob] [app.util.i18n :as i18n] [app.util.theme :as theme] [beicon.core :as rx] + [cuerdas.core :as str] [debug] [potok.core :as ptk] [rumext.alpha :as mf])) @@ -58,12 +60,18 @@ (rx/take 1) (rx/map #(rt/init-routes))))))) +(def essential-only? + (let [href (.-href ^js glob/location)] + (str/includes? href "essential=t"))) + (defn ^:export init [] - (worker/init!) - (sentry/init!) - (i18n/init! cf/translations) - (theme/init! cf/themes) + (when-not essential-only? + (worker/init!) + (sentry/init!) + (i18n/init! cf/translations) + (theme/init! cf/themes)) + (init-ui) (st/emit! (initialize))) diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index eb31598aa..b76f47859 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -101,10 +101,12 @@ bool-shape (bool/bool-shape shape-wrapper)] (mf/fnc bool-wrapper [{:keys [shape] :as props}] - (let [childs (->> (cp/get-children (:id shape) objects) - (select-keys objects))] - [:& bool-shape {:shape shape - :childs childs}])))) + (let [childs (mf/use-memo + (mf/deps (:id shape) objects) + (fn [] + (->> (cp/get-children (:id shape) objects) + (select-keys objects))))] + [:& bool-shape {:shape shape :childs childs}])))) (defn svg-raw-wrapper-factory [objects] @@ -221,26 +223,39 @@ (mf/defc frame-svg {::mf/wrap [mf/memo]} [{:keys [objects frame zoom] :or {zoom 1} :as props}] - (let [modifier (-> (gpt/point (:x frame) (:y frame)) - (gpt/negate) - (gmt/translate-matrix)) - - frame-id (:id frame) - + (let [frame-id (:id frame) include-metadata? (mf/use-ctx export/include-metadata-ctx) - modifier-ids (concat [frame-id] (cp/get-children frame-id objects)) - update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier) - objects (reduce update-fn objects modifier-ids) - frame (assoc-in frame [:modifiers :displacement] modifier) + modifier + (mf/use-memo + (mf/deps (:x frame) (:y frame)) + (fn [] + (-> (gpt/point (:x frame) (:y frame)) + (gpt/negate) + (gmt/translate-matrix)))) + + objects + (mf/use-memo + (mf/deps frame-id objects modifier) + (fn [] + (let [update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)] + (->> (cp/get-children frame-id objects) + (into [frame-id]) + (reduce update-fn objects))))) + + frame + (mf/use-memo + (mf/deps modifier) + (fn [] (assoc-in frame [:modifiers :displacement] modifier))) + + wrapper + (mf/use-memo + (mf/deps objects) + (fn [] (frame-wrapper-factory objects))) width (* (:width frame) zoom) height (* (:height frame) zoom) - vbox (format-viewbox {:width (:width frame 0) :height (:height frame 0)}) - - wrapper (mf/use-memo - (mf/deps objects) - #(frame-wrapper-factory objects))] + vbox (format-viewbox {:width (:width frame 0) :height (:height frame 0)})] [:svg {:view-box vbox :width (ust/format-precision width viewbox-decimal-precision) @@ -254,19 +269,25 @@ (mf/defc component-svg {::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) - (gmt/translate-matrix)) - - group-id (:id group) - + (let [group-id (:id group) include-metadata? (mf/use-ctx export/include-metadata-ctx) - modifier-ids (concat [group-id] (cp/get-children group-id objects)) + modifier + (mf/use-memo + (mf/deps (:x group) (:y group)) + (fn [] + (-> (gpt/point (:x group) (:y group)) + (gpt/negate) + (gmt/translate-matrix)))) - update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier) - modifiers (reduce update-fn {} modifier-ids) - objects (gsh/merge-modifiers objects modifiers) + objects + (mf/use-memo + (mf/deps modifier objects group-id) + (fn [] + (let [modifier-ids (concat [group-id] (cp/get-children group-id objects)) + update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier) + modifiers (reduce update-fn {} modifier-ids)] + (gsh/merge-modifiers objects modifiers)))) group (get objects group-id) width (* (:width group) zoom) @@ -276,7 +297,7 @@ group-wrapper (mf/use-memo (mf/deps objects) - #(group-wrapper-factory objects))] + (fn [] (group-wrapper-factory objects)))] [:svg {:view-box vbox :width (ust/format-precision width viewbox-decimal-precision) @@ -285,31 +306,51 @@ :xmlns "http://www.w3.org/2000/svg" :xmlnsXlink "http://www.w3.org/1999/xlink" :xmlns:penpot (when include-metadata? "https://penpot.app/xmlns")} + [:> shape-container {:shape group} [:& group-wrapper {:shape group :view-box vbox}]]])) (mf/defc component-symbol - [{:keys [id data] :as props}] + {::mf/wrap-props false} + [props] + (let [id (obj/get props "id") + data (obj/get props "data") + name (:name data) + path (:path data) + objects (:objects data) + root (get objects id) + selrect (:selrect root) - (let [{:keys [name path objects]} data - root (get objects id) + vbox + (format-viewbox + {:width (:width selrect) + :height (:height selrect)}) - {:keys [width height]} (:selrect root) - vbox (format-viewbox {:width width :height height}) + modifier + (mf/use-memo + (mf/deps (:x root) (:y root)) + (fn [] + (-> (gpt/point (:x root) (:y root)) + (gpt/negate) + (gmt/translate-matrix)))) - modifier (-> (gpt/point (:x root) (:y root)) - (gpt/negate) - (gmt/translate-matrix)) + objects + (mf/use-memo + (mf/deps modifier id objects) + (fn [] + (let [modifier-ids (concat [id] (cp/get-children id objects)) + update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)] + (reduce update-fn objects modifier-ids)))) - modifier-ids (concat [id] (cp/get-children id objects)) - update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier) - objects (reduce update-fn objects modifier-ids) - root (assoc-in root [:modifiers :displacement] modifier) + root + (mf/use-memo + (mf/deps modifier root) + (fn [] (assoc-in root [:modifiers :displacement] modifier))) group-wrapper (mf/use-memo (mf/deps objects) - #(group-wrapper-factory objects))] + (fn [] (group-wrapper-factory objects)))] [:> "symbol" #js {:id (str id) :viewBox vbox @@ -321,10 +362,9 @@ (mf/defc components-sprite-svg {::mf/wrap-props false} [props] - - (let [data (obj/get props "data") - children (obj/get props "children") - embed? (obj/get props "embed?") + (let [data (obj/get props "data") + children (obj/get props "children") + embed? (obj/get props "embed?") include-metadata? (obj/get props "include-metadata?")] [:& (mf/provider embed/context) {:value embed?} [:& (mf/provider export/include-metadata-ctx) {:value include-metadata?} diff --git a/frontend/src/app/main/ui/dashboard/export.cljs b/frontend/src/app/main/ui/dashboard/export.cljs index 6b353dab9..5a1425ba6 100644 --- a/frontend/src/app/main/ui/dashboard/export.cljs +++ b/frontend/src/app/main/ui/dashboard/export.cljs @@ -7,7 +7,6 @@ (ns app.main.ui.dashboard.export (:require [app.common.data :as d] - [app.main.data.events :as ev] [app.main.data.modal :as modal] [app.main.store :as st] [app.main.ui.icons :as i] @@ -15,7 +14,6 @@ [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [beicon.core :as rx] - [potok.core :as ptk] [rumext.alpha :as mf])) (def ^:const options [:all :merge :detach]) @@ -60,10 +58,6 @@ start-export (fn [] - (st/emit! (ptk/event ::ev/event {::ev/name "export-files" - :num-files (count (:files @state)) - :option @selected-option})) - (swap! state assoc :status :exporting) (->> (uw/ask-many! {:cmd :export-file diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index 28eff99bb..dba16d15b 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -162,6 +162,7 @@ (mf/deps files current-team-id) (fn [_] (st/emit! (ptk/event ::ev/event {::ev/name "export-files" + ::ev/origin "dashboard" :num-files (count files)})) (->> (rx/from files) (rx/flat-map diff --git a/frontend/src/app/main/ui/render.cljs b/frontend/src/app/main/ui/render.cljs index 2ce3ddf24..d5909a9ed 100644 --- a/frontend/src/app/main/ui/render.cljs +++ b/frontend/src/app/main/ui/render.cljs @@ -153,7 +153,7 @@ (fn [] (->> (rx/zip (repo/query! :font-variants {:file-id file-id}) - (repo/query! :file {:id file-id})) + (repo/query! :trimmed-file {:id file-id :page-id page-id :object-id object-id})) (rx/subs (fn [[fonts {:keys [data]}]] (when (seq fonts) diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs index 052dce5ab..1ea58e5ed 100644 --- a/frontend/src/app/main/ui/shapes/export.cljs +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -199,6 +199,18 @@ :penpot:suffix suffix :penpot:scale (str scale)}]))) +(defn str->style + [style-str] + (if (string? style-str) + (->> (str/split style-str ";") + (map str/trim) + (map #(str/split % ":")) + (group-by first) + (map (fn [[key val]] + (vector (keyword key) (second (first val))))) + (into {})) + style-str)) + (defn style->str [style] (->> style @@ -229,7 +241,8 @@ [:& render-xml {:xml def-xml}]])])) (when (= (:type shape) :svg-raw) - (let [props + (let [shape (-> shape (d/update-in-when [:content :attrs :style] str->style)) + props (-> (obj/new) (obj/set! "penpot:x" (:x shape)) (obj/set! "penpot:y" (:y shape)) @@ -263,7 +276,6 @@ (mf/defc export-data [{:keys [shape]}] (let [props (-> (obj/new) (add-data shape) (add-library-refs shape))] - (js/console.log props) [:> "penpot:shape" props (export-shadow-data shape) (export-blur-data shape) diff --git a/frontend/src/app/main/ui/workspace/header.cljs b/frontend/src/app/main/ui/workspace/header.cljs index afbf84945..ca418b63f 100644 --- a/frontend/src/app/main/ui/workspace/header.cljs +++ b/frontend/src/app/main/ui/workspace/header.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.math :as mth] [app.config :as cf] + [app.main.data.events :as ev] [app.main.data.messages :as dm] [app.main.data.modal :as modal] [app.main.data.workspace :as dw] @@ -25,6 +26,7 @@ [app.util.router :as rt] [beicon.core :as rx] [okulary.core :as l] + [potok.core :as ptk] [rumext.alpha :as mf])) ;; --- Zoom Widget @@ -152,6 +154,10 @@ (mf/use-callback (mf/deps file team-id) (fn [_] + (st/emit! (ptk/event ::ev/event {::ev/name "export-files" + ::ev/origin "workspace" + :num-files 1})) + (->> (rx/of file) (rx/flat-map (fn [file] @@ -251,7 +257,7 @@ (tr "workspace.header.menu.hide-palette") (tr "workspace.header.menu.show-palette"))] [:span.shortcut (sc/get-tooltip :toggle-palette)]] - + [:li {:on-click #(st/emit! (dw/toggle-layout-flags :display-artboard-names))} [:span (if (contains? layout :display-artboard-names) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index 3651012c0..fe79dd29e 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -96,18 +96,21 @@ ::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - objects (unchecked-get props "objects") - thumbnail? (unchecked-get props "thumbnail?") + (when-let [shape (unchecked-get props "shape")] + (let [objects (unchecked-get props "objects") + thumbnail? (unchecked-get props "thumbnail?") - children (-> (mapv (d/getf objects) (:shapes shape)) - (hooks/use-equal-memo)) - all-children (-> (cp/get-children-objects (:id shape) objects) - (hooks/use-equal-memo)) + children + (-> (mapv (d/getf objects) (:shapes shape)) + (hooks/use-equal-memo)) - show-thumbnail? (and thumbnail? (some? (:thumbnail shape)))] + all-children + (-> (cp/get-children-objects (:id shape) objects) + (hooks/use-equal-memo)) + + show-thumbnail? + (and thumbnail? (some? (:thumbnail shape)))] - (when (some? shape) [:g.frame-wrapper {:display (when (:hidden shape) "none")} [:> shape-container {:shape shape} [:& ff/fontfaces-style {:shapes all-children}] diff --git a/frontend/src/app/main/worker.cljs b/frontend/src/app/main/worker.cljs index 401d2822d..8bd60284f 100644 --- a/frontend/src/app/main/worker.cljs +++ b/frontend/src/app/main/worker.cljs @@ -11,7 +11,7 @@ (defn on-error [error] - (js/console.error "Error on worker" error)) + (js/console.error "Error on worker" (pr-str error))) (defonce instance (atom nil)) diff --git a/frontend/src/app/util/avatars.cljs b/frontend/src/app/util/avatars.cljs index e7fdc0b25..efe1ec07e 100644 --- a/frontend/src/app/util/avatars.cljs +++ b/frontend/src/app/util/avatars.cljs @@ -11,7 +11,8 @@ (defn generate* [{:keys [name color size] - :or {color "var(--color-black)" size 128}}] + :or {color "#000000" size 128}}] + (let [parts (str/words (str/upper name)) letters (if (= 1 (count parts)) (ffirst parts) @@ -27,7 +28,7 @@ (obj/set! context "font" (str (/ size 2) "px Arial")) (obj/set! context "textAlign" "center") - (obj/set! context "fillStyle" "var(--color-white)") + (obj/set! context "fillStyle" "#ffffff") (.fillText context letters (/ size 2) (/ size 1.5)) (.toDataURL canvas))) diff --git a/frontend/src/app/util/import/parser.cljs b/frontend/src/app/util/import/parser.cljs index 4f1431b48..dec08ba7c 100644 --- a/frontend/src/app/util/import/parser.cljs +++ b/frontend/src/app/util/import/parser.cljs @@ -210,7 +210,8 @@ svg-node (if (= :svg tag) (->> node :content last :content last) - (->> node :content last))] + (->> node :content last)) + svg-node (d/update-in-when svg-node [:attrs :style] parse-style)] (merge (add-attrs {} (:attrs svg-node)) node-attrs)) (= type :bool) @@ -633,7 +634,8 @@ (defn add-svg-content [props node] (let [svg-content (get-data node :penpot:svg-content) - attrs (-> (:attrs svg-content) (without-penpot-prefix)) + attrs (-> (:attrs svg-content) (without-penpot-prefix) + (d/update-when :style parse-style)) tag (-> svg-content :attrs :penpot:tag keyword) node-content @@ -641,13 +643,17 @@ (= tag :svg) (->> node :content last :content last :content fix-style-attr) - (= tag :text) - (-> node :content last :content))] - (assoc - props :content - {:attrs attrs - :tag tag - :content node-content}))) + (some? (:content svg-content)) + (->> (:content svg-content) + (filter #(= :penpot:svg-child (:tag %))) + (mapv :content) + (first)))] + (-> props + (assoc + :content + {:attrs attrs + :tag tag + :content node-content})))) (defn add-frame-data [props node] (let [grids (parse-grids node)] diff --git a/frontend/src/app/worker.cljs b/frontend/src/app/worker.cljs index ea54c3931..52bb72451 100644 --- a/frontend/src/app/worker.cljs +++ b/frontend/src/app/worker.cljs @@ -53,7 +53,7 @@ (post {:payload result})) (reply-error [err] - (.error js/console "error" err) + (.error js/console "error" (pr-str err)) (post {:error {:data (ex-data err) :message (ex-message err)}})) diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index 5dcfa3f52..983e5d83d 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -141,38 +141,30 @@ (rx/map #(hash-map :file-id file-id :library-id %)) (rx/flat-map (partial rp/mutation :link-file-to-library))))) -(defn persist-file [file] - (rp/mutation :persist-temp-file {:id (:id file)})) - (defn send-changes "Creates batches of changes to be sent to the backend" [context file] - (let [revn (atom (:revn file)) - file-id (:id file) + (let [file-id (:id file) session-id (uuid/next) - changes-batches - (->> (fb/generate-changes file) - (partition change-batch-size change-batch-size nil) - (mapv vec)) + batches (->> (fb/generate-changes file) + (partition change-batch-size change-batch-size nil) + (mapv vec)) - current (atom 0) - total (count changes-batches)] + processed (atom 0) + total (count batches)] (rx/concat - (->> (rx/from changes-batches) - (rx/mapcat - (fn [change-batch] - (->> (rp/mutation :update-file + (->> (rx/from (d/enumerate batches)) + (rx/merge-map + (fn [[i change-batch]] + (->> (rp/mutation :update-temp-file {:id file-id :session-id session-id - :revn @revn + :revn i :changes change-batch}) - (rx/tap #(do (swap! current inc) - (progress! context - :upload-data @current total)))))) - + (rx/tap #(do (swap! processed inc) + (progress! context :upload-data @processed total)))))) (rx/map first) - (rx/tap #(reset! revn (:revn %))) (rx/ignore)) (->> (rp/mutation :persist-temp-file {:id file-id}) @@ -340,7 +332,7 @@ file (-> file (fb/add-page page-data))] (->> (rx/from nodes) (rx/filter cip/shape?) - (rx/mapcat (partial resolve-media context file-id)) + (rx/merge-map (partial resolve-media context file-id)) (rx/reduce (partial process-import-node context) file) (rx/map (comp fb/close-page setup-interactions))))) @@ -362,7 +354,7 @@ (rx/filter cip/shape?) (rx/skip 1) (rx/skip-last 1) - (rx/mapcat (partial resolve-media context file-id)) + (rx/merge-map (partial resolve-media context file-id)) (rx/reduce (partial process-import-node context) file) (rx/map fb/finish-component)))) @@ -382,6 +374,7 @@ (fn [[page-id page-name]] (->> (get-file context :page page-id) (rx/map (fn [page-data] [page-id page-name page-data]))))) + (rx/concat-reduce (partial import-page context) file)))) (defn process-library-colors @@ -420,7 +413,7 @@ (let [resolve (:resolve context)] (->> (get-file context :media-list) (rx/flat-map (comp d/kebab-keys cip/string->uuid)) - (rx/mapcat + (rx/merge-map (fn [[id media]] (let [media (assoc media :id (resolve id))] (->> (get-file context :media id media) @@ -432,12 +425,11 @@ :content content :is-local false}))) (rx/tap #(progress! context :upload-media (:name %))) - (rx/flat-map #(rp/mutation! :upload-file-media-object %)) + (rx/merge-map #(rp/mutation! :upload-file-media-object %)) (rx/map (constantly media)) (rx/catch #(do (.error js/console (str "Error uploading media: " (:name media)) ) (rx/empty))))))) (rx/reduce fb/add-library-media file))) - (rx/of file))) (defn process-library-components @@ -511,7 +503,7 @@ (fn [[file data]] (->> (uz/load-from-url (:uri data)) (rx/map #(-> context (assoc :zip %) (merge data))) - (rx/flat-map + (rx/merge-map (fn [context] ;; process file retrieves a stream that will emit progress notifications ;; and other that will emit the files once imported diff --git a/frontend/src/app/worker/thumbnails.cljs b/frontend/src/app/worker/thumbnails.cljs index a6da0d73c..f5ad5c13d 100644 --- a/frontend/src/app/worker/thumbnails.cljs +++ b/frontend/src/app/worker/thumbnails.cljs @@ -34,7 +34,7 @@ (let [uri (u/join (cfg/get-public-uri) "api/rpc/query/page") params {:file-id file-id :id page-id - :strip-thumbnails true}] + :strip-frames-with-thumbnails true}] (->> (http/send! {:method :get :uri uri