diff --git a/CHANGES.md b/CHANGES.md index 9a5f21621c..73f14701a3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,6 +22,9 @@ ## 2.14.0 (Unreleased) +### :boom: Breaking changes & Deprecations +- Deprecate `PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE` in favour of `PENPOT_HTTP_SERVER_MAX_BODY_SIZE`. + ### :sparkles: New features & Enhancements - Access to design tokens in Penpot Plugins [Taiga #8990](https://tree.taiga.io/project/penpot/us/8990) diff --git a/backend/deps.edn b/backend/deps.edn index 27d052c510..af73aecbc8 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -28,8 +28,8 @@ com.google.guava/guava {:mvn/version "33.4.8-jre"} funcool/yetti - {:git/tag "v11.8" - :git/sha "1d1b33f" + {:git/tag "v11.9" + :git/sha "5fad7a9" :git/url "https://github.com/funcool/yetti.git" :exclusions [org.slf4j/slf4j-api]} diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 6b3f784170..d0a80f6515 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -98,7 +98,6 @@ [:http-server-port {:optional true} ::sm/int] [:http-server-host {:optional true} :string] [:http-server-max-body-size {:optional true} ::sm/int] - [:http-server-max-multipart-body-size {:optional true} ::sm/int] [:http-server-io-threads {:optional true} ::sm/int] [:http-server-max-worker-threads {:optional true} ::sm/int] diff --git a/backend/src/app/http.clj b/backend/src/app/http.clj index 0191589b05..1578138d05 100644 --- a/backend/src/app/http.clj +++ b/backend/src/app/http.clj @@ -42,8 +42,8 @@ (def default-params {::port 6060 ::host "0.0.0.0" - ::max-body-size 31457280 ; default 30 MiB - ::max-multipart-body-size 367001600}) ; default 350 MiB + ::max-body-size 367001600 ; default 350 MiB + }) (defmethod ig/expand-key ::server [k v] @@ -56,7 +56,6 @@ [::io-threads {:optional true} ::sm/int] [::max-worker-threads {:optional true} ::sm/int] [::max-body-size {:optional true} ::sm/int] - [::max-multipart-body-size {:optional true} ::sm/int] [::router {:optional true} [:fn r/router?]] [::handler {:optional true} ::sm/fn]]) @@ -79,7 +78,7 @@ {:http/port port :http/host host :http/max-body-size (::max-body-size cfg) - :http/max-multipart-body-size (::max-multipart-body-size cfg) + :http/max-multipart-body-size (::max-body-size cfg) :xnio/direct-buffers false :xnio/io-threads (::io-threads cfg) :xnio/max-worker-threads (::max-worker-threads cfg) diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index a5027bd190..693752080a 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -226,11 +226,10 @@ ::http/server {::http/port (cf/get :http-server-port) ::http/host (cf/get :http-server-host) - ::http/router (ig/ref ::http/router) ::http/io-threads (cf/get :http-server-io-threads) ::http/max-worker-threads (cf/get :http-server-max-worker-threads) ::http/max-body-size (cf/get :http-server-max-body-size) - ::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size) + ::http/router (ig/ref ::http/router) ::mtx/metrics (ig/ref ::mtx/metrics)} ::ldap/provider diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index 2fb3ddfa53..93ac58d03b 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -605,31 +605,31 @@ add-undo-change-shape (fn [change-set id] (let [shape (get objects id)] - (conj - change-set - {:type :add-obj - :id id - :page-id page-id - :parent-id (:parent-id shape) - :frame-id (:frame-id shape) - :index (cfh/get-position-on-parent objects id) - :obj (cond-> shape - (contains? shape :shapes) - (assoc :shapes []))}))) + (cond-> change-set + (some? shape) + (conj {:type :add-obj + :id id + :page-id page-id + :parent-id (:parent-id shape) + :frame-id (:frame-id shape) + :index (cfh/get-position-on-parent objects id) + :obj (cond-> shape + (contains? shape :shapes) + (assoc :shapes []))})))) add-undo-change-parent (fn [change-set id] (let [shape (get objects id) prev-sibling (cfh/get-prev-sibling objects (:id shape))] - (conj - change-set - {:type :mov-objects - :page-id page-id - :parent-id (:parent-id shape) - :shapes [id] - :after-shape prev-sibling - :index 0 - :ignore-touched true})))] + (cond-> change-set + (some? shape) + (conj {:type :mov-objects + :page-id page-id + :parent-id (:parent-id shape) + :shapes [id] + :after-shape prev-sibling + :index 0 + :ignore-touched true}))))] (-> changes (update :redo-changes #(reduce add-redo-change % ids)) @@ -1150,3 +1150,24 @@ [changes] (::page-id (meta changes))) + +(defn set-text-content + [changes id content prev-content] + (assert-page-id! changes) + (let [page-id (::page-id (meta changes)) + + redo-change + {:type :mod-obj + :page-id page-id + :id id + :operations [{:type :set :attr :content :val content}]} + + undo-change + {:type :mod-obj + :page-id page-id + :id id + :operations [{:type :set :attr :content :val prev-content}]}] + + (-> changes + (update :redo-changes conj redo-change) + (update :undo-changes conj undo-change)))) diff --git a/docker/images/Dockerfile.backend b/docker/images/Dockerfile.backend index 9d5500ecc8..c3d08916a6 100644 --- a/docker/images/Dockerfile.backend +++ b/docker/images/Dockerfile.backend @@ -68,7 +68,7 @@ RUN set -eux; \ --no-header-files \ --no-man-pages \ --strip-debug \ - --add-modules java.base,jdk.management.agent,java.se,jdk.compiler,jdk.javadoc,jdk.attach,jdk.unsupported \ + --add-modules java.base,jdk.management.agent,java.se,jdk.compiler,jdk.javadoc,jdk.attach,jdk.unsupported,jdk.jfr,jdk.jcmd \ --output /opt/jre; FROM ubuntu:24.04 AS image diff --git a/docker/images/docker-compose.yaml b/docker/images/docker-compose.yaml index 8076064b94..5d3b84d09c 100644 --- a/docker/images/docker-compose.yaml +++ b/docker/images/docker-compose.yaml @@ -30,11 +30,9 @@ x-uri: &penpot-public-uri PENPOT_PUBLIC_URI: http://localhost:9001 x-body-size: &penpot-http-body-size - # Max body size (30MiB); Used for plain requests, should never be - # greater than multi-part size - PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 31457280 - - # Max multipart body size (350MiB) + # Max body size + PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 367001600 + # Deprecation warning: this variable is deprecated. Use PENPOT_HTTP_SERVER_MAX_BODY (defaults to 367001600) PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE: 367001600 ## Penpot SECRET KEY. It serves as a master key from which other keys for subsystems diff --git a/docker/images/files/nginx-entrypoint.sh b/docker/images/files/nginx-entrypoint.sh index 39546de336..4512d06495 100644 --- a/docker/images/files/nginx-entrypoint.sh +++ b/docker/images/files/nginx-entrypoint.sh @@ -30,8 +30,8 @@ update_flags /var/www/app/js/config.js export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060} export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061} export PENPOT_NITRATE_URI=${PENPOT_NITRATE_URI:-http://penpot-nitrate:3000} -export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE:-367001600} # Default to 350MiB -envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \ +export PENPOT_HTTP_SERVER_MAX_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_BODY_SIZE:-367001600} # Default to 350MiB +envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_HTTP_SERVER_MAX_BODY_SIZE" \ < /tmp/nginx.conf.template > /etc/nginx/nginx.conf PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)" diff --git a/docker/images/files/nginx.conf.template b/docker/images/files/nginx.conf.template index a623408dcd..d0b7bc3b1f 100644 --- a/docker/images/files/nginx.conf.template +++ b/docker/images/files/nginx.conf.template @@ -76,7 +76,7 @@ http { listen [::]:8080 default_server; server_name _; - client_max_body_size $PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE; + client_max_body_size $PENPOT_HTTP_SERVER_MAX_BODY_SIZE; charset utf-8; etag off; diff --git a/docs/technical-guide/getting-started/docker.md b/docs/technical-guide/getting-started/docker.md index 2e82d8d629..53560235d3 100644 --- a/docs/technical-guide/getting-started/docker.md +++ b/docs/technical-guide/getting-started/docker.md @@ -188,8 +188,8 @@ server { server_name penpot.mycompany.com; # This value should be in sync with the corresponding in the docker-compose.yml - # PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 31457280 - client_max_body_size 31457280; + # PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 367001600 + client_max_body_size 367001600; # Logs: Configure your logs following the best practices inside your company access_log /path/to/penpot.access.log; diff --git a/frontend/playwright/data/get-teams-variants.json b/frontend/playwright/data/get-teams-variants.json deleted file mode 100644 index 50b323d047..0000000000 --- a/frontend/playwright/data/get-teams-variants.json +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "~:features": { - "~#set": [ - "variants/v1", - "layout/grid", - "styles/v2", - "fdata/pointer-map", - "fdata/objects-map", - "components/v2", - "fdata/shape-data-type" - ] - }, - "~:permissions": { - "~:type": "~:membership", - "~:is-owner": true, - "~:is-admin": true, - "~:can-edit": true - }, - "~:name": "Default", - "~:modified-at": "~m1713533116375", - "~:id": "~uc7ce0794-0992-8105-8004-38e630f7920a", - "~:created-at": "~m1713533116375", - "~:is-default": true - } -] \ No newline at end of file diff --git a/frontend/playwright/data/logged-in-user/get-profile-logged-in-no-onboarding.json b/frontend/playwright/data/logged-in-user/get-profile-logged-in-no-onboarding.json deleted file mode 100644 index 0b416835f7..0000000000 --- a/frontend/playwright/data/logged-in-user/get-profile-logged-in-no-onboarding.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "~:email": "foo@example.com", - "~:is-demo": false, - "~:auth-backend": "penpot", - "~:fullname": "Princesa Leia", - "~:modified-at": "~m1713533116365", - "~:is-active": true, - "~:default-project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", - "~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", - "~:is-muted": false, - "~:default-team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", - "~:created-at": "~m1713533116365", - "~:is-blocked": false, - "~:props": { - "~:nudge": { - "~:big": 10, - "~:small": 1 - }, - "~:v2-info-shown": true, - "~:viewed-tutorial?": false, - "~:viewed-walkthrough?": false, - "~:onboarding-viewed": true, - "~:builtin-templates-collapsed-status": - true - } -} diff --git a/frontend/playwright/data/render-wasm/get-solid-shadows.json b/frontend/playwright/data/render-wasm/get-solid-shadows.json new file mode 100644 index 0000000000..85e5f2d855 --- /dev/null +++ b/frontend/playwright/data/render-wasm/get-solid-shadows.json @@ -0,0 +1,1161 @@ +{ + "~:features": { + "~#set": [ + "fdata/path-data", + "plugins/runtime", + "design-tokens/v1", + "variants/v1", + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "render-wasm/v1", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:team-id": "~ueba8fa2e-4140-8084-8005-448635d7a724", + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "New File 19", + "~:revn": 1, + "~:modified-at": "~m1771575061911", + "~:vern": 0, + "~:id": "~u93113137-fe66-80fb-8007-99ca9fd96841", + "~:is-shared": false, + "~:migrations": { + "~#ordered-set": [ + "legacy-2", + "legacy-3", + "legacy-5", + "legacy-6", + "legacy-7", + "legacy-8", + "legacy-9", + "legacy-10", + "legacy-11", + "legacy-12", + "legacy-13", + "legacy-14", + "legacy-16", + "legacy-17", + "legacy-18", + "legacy-19", + "legacy-25", + "legacy-26", + "legacy-27", + "legacy-28", + "legacy-29", + "legacy-31", + "legacy-32", + "legacy-33", + "legacy-34", + "legacy-36", + "legacy-37", + "legacy-38", + "legacy-39", + "legacy-40", + "legacy-41", + "legacy-42", + "legacy-43", + "legacy-44", + "legacy-45", + "legacy-46", + "legacy-47", + "legacy-48", + "legacy-49", + "legacy-50", + "legacy-51", + "legacy-52", + "legacy-53", + "legacy-54", + "legacy-55", + "legacy-56", + "legacy-57", + "legacy-59", + "legacy-62", + "legacy-65", + "legacy-66", + "legacy-67", + "0001-remove-tokens-from-groups", + "0002-normalize-bool-content-v2", + "0002-clean-shape-interactions", + "0003-fix-root-shape", + "0003-convert-path-content-v2", + "0005-deprecate-image-type", + "0006-fix-old-texts-fills", + "0008-fix-library-colors-v4", + "0009-clean-library-colors", + "0009-add-partial-text-touched-flags", + "0010-fix-swap-slots-pointing-non-existent-shapes", + "0011-fix-invalid-text-touched-flags", + "0012-fix-position-data", + "0013-fix-component-path", + "0013-clear-invalid-strokes-and-fills", + "0014-fix-tokens-lib-duplicate-ids", + "0014-clear-components-nil-objects", + "0015-fix-text-attrs-blank-strings", + "0015-clean-shadow-color", + "0016-copy-fills-from-position-data-to-text-node" + ] + }, + "~:version": 67, + "~:project-id": "~ueba8fa2e-4140-8084-8005-448635da32b4", + "~:created-at": "~m1771575057253", + "~:backend": "legacy-db", + "~:data": { + "~:pages": [ + "~u93113137-fe66-80fb-8007-99ca9fd96842" + ], + "~:pages-index": { + "~u93113137-fe66-80fb-8007-99ca9fd96842": { + "~:objects": { + "~u00000000-0000-0000-0000-000000000000": { + "~#shape": { + "~:y": 0, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:name": "Root Frame", + "~:width": 0.01, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 0, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.01 + } + }, + { + "~#point": { + "~:x": 0, + "~:y": 0.01 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:r3": 0, + "~:r1": 0, + "~:id": "~u00000000-0000-0000-0000-000000000000", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 0, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 0, + "~:y": 0, + "~:width": 0.01, + "~:height": 0.01, + "~:x1": 0, + "~:y1": 0, + "~:x2": 0.01, + "~:y2": 0.01 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 0.01, + "~:flip-y": null, + "~:shapes": [ + "~uf6d6a19f-9b44-8006-8007-99caa15c11d0", + "~uf6d6a19f-9b44-8006-8007-99caa15c11d1", + "~uf6d6a19f-9b44-8006-8007-99caa15c11d2", + "~uf6d6a19f-9b44-8006-8007-99caa15c11d3", + "~uf6d6a19f-9b44-8006-8007-99caa15c11d4", + "~uf6d6a19f-9b44-8006-8007-99caa15c11d5", + "~uf6d6a19f-9b44-8006-8007-99caa15c11d6", + "~uf6d6a19f-9b44-8006-8007-99caa15c11d7", + "~uf6d6a19f-9b44-8006-8007-99caa15c11d8" + ] + } + }, + "~uf6d6a19f-9b44-8006-8007-99caa15c11d3": { + "~#shape": { + "~:y": 316, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 24.000014555722, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 376.999997388182, + "~:y": 316 + } + }, + { + "~#point": { + "~:x": 401.000011943904, + "~:y": 316 + } + }, + { + "~#point": { + "~:x": 401.000011943904, + "~:y": 340.000015617202 + } + }, + { + "~#point": { + "~:x": 376.999997388182, + "~:y": 340.000015617202 + } + } + ], + "~:r2": 10, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:r3": 10, + "~:r1": 10, + "~:id": "~uf6d6a19f-9b44-8006-8007-99caa15c11d3", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 376.999997388182, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~ua28e6acc-48c7-80d6-8007-988d7ce4c3ba", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#de0808", + "~:opacity": 1 + }, + "~:offset-x": 30, + "~:offset-y": 0, + "~:blur": 2, + "~:spread": 0, + "~:hidden": false + } + ], + "~:r4": 10, + "~:selrect": { + "~#rect": { + "~:x": 376.999997388182, + "~:y": 316, + "~:width": 24.000014555722, + "~:height": 24.0000156172018, + "~:x1": 376.999997388182, + "~:y1": 316, + "~:x2": 401.000011943904, + "~:y2": 340.000015617202 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 24.0000156172018, + "~:flip-y": null + } + }, + "~uf6d6a19f-9b44-8006-8007-99caa15c11d2": { + "~#shape": { + "~:y": 372.999997713929, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 24.0000145557221, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 310.999955443065, + "~:y": 372.999997713929 + } + }, + { + "~#point": { + "~:x": 334.999969998787, + "~:y": 372.999997713929 + } + }, + { + "~#point": { + "~:x": 334.999969998787, + "~:y": 396.99998162146 + } + }, + { + "~#point": { + "~:x": 310.999955443065, + "~:y": 396.99998162146 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:r3": 0, + "~:r1": 0, + "~:id": "~uf6d6a19f-9b44-8006-8007-99caa15c11d2", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 310.999955443065, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~ua28e6acc-48c7-80d6-8007-988d7ce4c3ba", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#de0808", + "~:opacity": 1 + }, + "~:offset-x": 30, + "~:offset-y": 0, + "~:blur": 0, + "~:spread": 4, + "~:hidden": false + } + ], + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 310.999955443065, + "~:y": 372.999997713929, + "~:width": 24.0000145557221, + "~:height": 23.9999839075303, + "~:x1": 310.999955443065, + "~:y1": 372.999997713929, + "~:x2": 334.999969998787, + "~:y2": 396.99998162146 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 23.9999839075303, + "~:flip-y": null + } + }, + "~uf6d6a19f-9b44-8006-8007-99caa15c11d1": { + "~#shape": { + "~:y": 343.00000969424, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 24.0000145557221, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 310.999978889336, + "~:y": 343.00000969424 + } + }, + { + "~#point": { + "~:x": 334.999993445058, + "~:y": 343.00000969424 + } + }, + { + "~#point": { + "~:x": 334.999993445058, + "~:y": 367.000025311442 + } + }, + { + "~#point": { + "~:x": 310.999978889336, + "~:y": 367.000025311442 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:r3": 0, + "~:r1": 0, + "~:id": "~uf6d6a19f-9b44-8006-8007-99caa15c11d1", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 310.999978889336, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~ua28e6acc-48c7-80d6-8007-988d7ce4c3ba", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#de0808", + "~:opacity": 1 + }, + "~:offset-x": 30, + "~:offset-y": 0, + "~:blur": 0, + "~:spread": 0, + "~:hidden": false + } + ], + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 310.999978889336, + "~:y": 343.00000969424, + "~:width": 24.0000145557221, + "~:height": 24.0000156172019, + "~:x1": 310.999978889336, + "~:y1": 343.00000969424, + "~:x2": 334.999993445058, + "~:y2": 367.000025311442 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 24.0000156172019, + "~:flip-y": null + } + }, + "~uf6d6a19f-9b44-8006-8007-99caa15c11d0": { + "~#shape": { + "~:y": 316.99997126302, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 24.000014555722, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 310.999980822839, + "~:y": 316.99997126302 + } + }, + { + "~#point": { + "~:x": 334.999995378561, + "~:y": 316.99997126302 + } + }, + { + "~#point": { + "~:x": 334.999995378561, + "~:y": 340.99995517055 + } + }, + { + "~#point": { + "~:x": 310.999980822839, + "~:y": 340.99995517055 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:r3": 0, + "~:r1": 0, + "~:id": "~uf6d6a19f-9b44-8006-8007-99caa15c11d0", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 310.999980822839, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~ua28e6acc-48c7-80d6-8007-988d7ce4c3ba", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#de0808", + "~:opacity": 1 + }, + "~:offset-x": 30, + "~:offset-y": 0, + "~:blur": 2, + "~:spread": 0, + "~:hidden": false + } + ], + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 310.999980822839, + "~:y": 316.99997126302, + "~:width": 24.000014555722, + "~:height": 23.9999839075304, + "~:x1": 310.999980822839, + "~:y1": 316.99997126302, + "~:x2": 334.999995378561, + "~:y2": 340.99995517055 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 23.9999839075304, + "~:flip-y": null + } + }, + "~uf6d6a19f-9b44-8006-8007-99caa15c11d7": { + "~#shape": { + "~:y": 430.000005806352, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Ellipse", + "~:width": 24.0000147255407, + "~:type": "~:circle", + "~:points": [ + { + "~#point": { + "~:x": 310.999970438785, + "~:y": 430.000005806352 + } + }, + { + "~#point": { + "~:x": 334.999985164326, + "~:y": 430.000005806352 + } + }, + { + "~#point": { + "~:x": 334.999985164326, + "~:y": 453.999988623858 + } + }, + { + "~#point": { + "~:x": 310.999970438785, + "~:y": 453.999988623858 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:r3": 0, + "~:r1": 0, + "~:id": "~uf6d6a19f-9b44-8006-8007-99caa15c11d7", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 310.999970438785, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u9c6321b5-aeab-809f-8007-971f9e232191", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#de0808", + "~:opacity": 1 + }, + "~:offset-x": 30, + "~:offset-y": 0, + "~:blur": 0, + "~:spread": 0, + "~:hidden": false + } + ], + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 310.999970438785, + "~:y": 430.000005806352, + "~:width": 24.0000147255407, + "~:height": 23.9999828175062, + "~:x1": 310.999970438785, + "~:y1": 430.000005806352, + "~:x2": 334.999985164326, + "~:y2": 453.999988623858 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 23.9999828175062, + "~:flip-y": null + } + }, + "~uf6d6a19f-9b44-8006-8007-99caa15c11d6": { + "~#shape": { + "~:y": 403.999982791028, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Ellipse", + "~:width": 24.0000147255407, + "~:type": "~:circle", + "~:points": [ + { + "~#point": { + "~:x": 310, + "~:y": 403.999982791028 + } + }, + { + "~#point": { + "~:x": 334.000014725541, + "~:y": 403.999982791028 + } + }, + { + "~#point": { + "~:x": 334.000014725541, + "~:y": 428.000027358944 + } + }, + { + "~#point": { + "~:x": 310, + "~:y": 428.000027358944 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:r3": 0, + "~:r1": 0, + "~:id": "~uf6d6a19f-9b44-8006-8007-99caa15c11d6", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 310, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u9c6321b5-aeab-809f-8007-971f9e232191", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#de0808", + "~:opacity": 1 + }, + "~:offset-x": 30, + "~:offset-y": 0, + "~:blur": 2, + "~:spread": 0, + "~:hidden": false + } + ], + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 310, + "~:y": 403.999982791028, + "~:width": 24.0000147255407, + "~:height": 24.0000445679165, + "~:x1": 310, + "~:y1": 403.999982791028, + "~:x2": 334.000014725541, + "~:y2": 428.000027358944 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 24.0000445679165, + "~:flip-y": null + } + }, + "~uf6d6a19f-9b44-8006-8007-99caa15c11d5": { + "~#shape": { + "~:y": 373.000002389236, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 23.9999544742418, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 376.999977821987, + "~:y": 373.000002389236 + } + }, + { + "~#point": { + "~:x": 400.999932296229, + "~:y": 373.000002389236 + } + }, + { + "~#point": { + "~:x": 400.999932296229, + "~:y": 396.999986296766 + } + }, + { + "~#point": { + "~:x": 376.999977821987, + "~:y": 396.999986296766 + } + } + ], + "~:r2": 10, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:r3": 10, + "~:r1": 10, + "~:id": "~uf6d6a19f-9b44-8006-8007-99caa15c11d5", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 376.999977821987, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~ua28e6acc-48c7-80d6-8007-988d7ce4c3ba", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#de0808", + "~:opacity": 1 + }, + "~:offset-x": 30, + "~:offset-y": 0, + "~:blur": 0, + "~:spread": 3, + "~:hidden": false + } + ], + "~:r4": 10, + "~:selrect": { + "~#rect": { + "~:x": 376.999977821987, + "~:y": 373.000002389236, + "~:width": 23.9999544742418, + "~:height": 23.9999839075304, + "~:x1": 376.999977821987, + "~:y1": 373.000002389236, + "~:x2": 400.999932296229, + "~:y2": 396.999986296766 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 23.9999839075304, + "~:flip-y": null + } + }, + "~uf6d6a19f-9b44-8006-8007-99caa15c11d4": { + "~#shape": { + "~:y": 343.000003602484, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 23.9999605115606, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 376.999989391033, + "~:y": 343.000003602484 + } + }, + { + "~#point": { + "~:x": 400.999949902594, + "~:y": 343.000003602484 + } + }, + { + "~#point": { + "~:x": 400.999949902594, + "~:y": 366.999987510014 + } + }, + { + "~#point": { + "~:x": 376.999989391033, + "~:y": 366.999987510014 + } + } + ], + "~:r2": 10, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:r3": 10, + "~:r1": 10, + "~:id": "~uf6d6a19f-9b44-8006-8007-99caa15c11d4", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 376.999989391033, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~ua28e6acc-48c7-80d6-8007-988d7ce4c3ba", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#de0808", + "~:opacity": 1 + }, + "~:offset-x": 30, + "~:offset-y": 0, + "~:blur": 0, + "~:spread": 0, + "~:hidden": false + } + ], + "~:r4": 10, + "~:selrect": { + "~#rect": { + "~:x": 376.999989391033, + "~:y": 343.000003602484, + "~:width": 23.9999605115606, + "~:height": 23.9999839075304, + "~:x1": 376.999989391033, + "~:y1": 343.000003602484, + "~:x2": 400.999949902594, + "~:y2": 366.999987510014 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 23.9999839075304, + "~:flip-y": null + } + }, + "~uf6d6a19f-9b44-8006-8007-99caa15c11d8": { + "~#shape": { + "~:y": 462.000000956476, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Ellipse", + "~:width": 24.0000147255407, + "~:type": "~:circle", + "~:points": [ + { + "~#point": { + "~:x": 312.999977686074, + "~:y": 462.000000956476 + } + }, + { + "~#point": { + "~:x": 336.999992411615, + "~:y": 462.000000956476 + } + }, + { + "~#point": { + "~:x": 336.999992411615, + "~:y": 486.000015483653 + } + }, + { + "~#point": { + "~:x": 312.999977686074, + "~:y": 486.000015483653 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:r3": 0, + "~:r1": 0, + "~:id": "~uf6d6a19f-9b44-8006-8007-99caa15c11d8", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 312.999977686074, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u9c6321b5-aeab-809f-8007-971f9e232191", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#de0808", + "~:opacity": 1 + }, + "~:offset-x": 30, + "~:offset-y": 0, + "~:blur": 0, + "~:spread": 3, + "~:hidden": false + } + ], + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 312.999977686074, + "~:y": 462.000000956476, + "~:width": 24.0000147255407, + "~:height": 24.0000145271764, + "~:x1": 312.999977686074, + "~:y1": 462.000000956476, + "~:x2": 336.999992411615, + "~:y2": 486.000015483653 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 24.0000145271764, + "~:flip-y": null + } + } + }, + "~:id": "~u93113137-fe66-80fb-8007-99ca9fd96842", + "~:name": "Page 1" + } + }, + "~:id": "~u93113137-fe66-80fb-8007-99ca9fd96841", + "~:options": { + "~:components-v2": true, + "~:base-font-size": "16px" + } + } +} \ No newline at end of file diff --git a/frontend/playwright/data/render-wasm/get-solid-strokes-shadows.json b/frontend/playwright/data/render-wasm/get-solid-strokes-shadows.json new file mode 100644 index 0000000000..070e659b01 --- /dev/null +++ b/frontend/playwright/data/render-wasm/get-solid-strokes-shadows.json @@ -0,0 +1,2826 @@ +{ + "~:features": { + "~#set": [ + "fdata/path-data", + "plugins/runtime", + "design-tokens/v1", + "variants/v1", + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "render-wasm/v1", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:team-id": "~ueba8fa2e-4140-8084-8005-448635d7a724", + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "New File 21", + "~:revn": 1, + "~:modified-at": "~m1771576427744", + "~:vern": 0, + "~:id": "~u93113137-fe66-80fb-8007-99cfd5cbf361", + "~:is-shared": false, + "~:migrations": { + "~#ordered-set": [ + "legacy-2", + "legacy-3", + "legacy-5", + "legacy-6", + "legacy-7", + "legacy-8", + "legacy-9", + "legacy-10", + "legacy-11", + "legacy-12", + "legacy-13", + "legacy-14", + "legacy-16", + "legacy-17", + "legacy-18", + "legacy-19", + "legacy-25", + "legacy-26", + "legacy-27", + "legacy-28", + "legacy-29", + "legacy-31", + "legacy-32", + "legacy-33", + "legacy-34", + "legacy-36", + "legacy-37", + "legacy-38", + "legacy-39", + "legacy-40", + "legacy-41", + "legacy-42", + "legacy-43", + "legacy-44", + "legacy-45", + "legacy-46", + "legacy-47", + "legacy-48", + "legacy-49", + "legacy-50", + "legacy-51", + "legacy-52", + "legacy-53", + "legacy-54", + "legacy-55", + "legacy-56", + "legacy-57", + "legacy-59", + "legacy-62", + "legacy-65", + "legacy-66", + "legacy-67", + "0001-remove-tokens-from-groups", + "0002-normalize-bool-content-v2", + "0002-clean-shape-interactions", + "0003-fix-root-shape", + "0003-convert-path-content-v2", + "0005-deprecate-image-type", + "0006-fix-old-texts-fills", + "0008-fix-library-colors-v4", + "0009-clean-library-colors", + "0009-add-partial-text-touched-flags", + "0010-fix-swap-slots-pointing-non-existent-shapes", + "0011-fix-invalid-text-touched-flags", + "0012-fix-position-data", + "0013-fix-component-path", + "0013-clear-invalid-strokes-and-fills", + "0014-fix-tokens-lib-duplicate-ids", + "0014-clear-components-nil-objects", + "0015-fix-text-attrs-blank-strings", + "0015-clean-shadow-color", + "0016-copy-fills-from-position-data-to-text-node" + ] + }, + "~:version": 67, + "~:project-id": "~ueba8fa2e-4140-8084-8005-448635da32b4", + "~:created-at": "~m1771576423215", + "~:backend": "legacy-db", + "~:data": { + "~:pages": [ + "~u93113137-fe66-80fb-8007-99cfd5cbf362" + ], + "~:pages-index": { + "~u93113137-fe66-80fb-8007-99cfd5cbf362": { + "~:objects": { + "~u00000000-0000-0000-0000-000000000000": { + "~#shape": { + "~:y": 0, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:name": "Root Frame", + "~:width": 0.01, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 0, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.01 + } + }, + { + "~#point": { + "~:x": 0, + "~:y": 0.01 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:r3": 0, + "~:r1": 0, + "~:id": "~u00000000-0000-0000-0000-000000000000", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 0, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 0, + "~:y": 0, + "~:width": 0.01, + "~:height": 0.01, + "~:x1": 0, + "~:y1": 0, + "~:x2": 0.01, + "~:y2": 0.01 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 0.01, + "~:flip-y": null, + "~:shapes": [ + "~ue52394cc-813d-80b9-8007-99cfd71e02c8", + "~ue52394cc-813d-80b9-8007-99cfd71e02c9", + "~ue52394cc-813d-80b9-8007-99cfd71e02ca", + "~ue52394cc-813d-80b9-8007-99cfd71e02cb", + "~ue52394cc-813d-80b9-8007-99cfd71e02cc", + "~ue52394cc-813d-80b9-8007-99cfd71e02cd", + "~ue52394cc-813d-80b9-8007-99cfd71e02ce", + "~ue52394cc-813d-80b9-8007-99cfd71e02cf", + "~ue52394cc-813d-80b9-8007-99cfd71e02d0", + "~ue52394cc-813d-80b9-8007-99cfd71e02d1", + "~ue52394cc-813d-80b9-8007-99cfd71e02d2", + "~ue52394cc-813d-80b9-8007-99cfd71e02d3", + "~ue52394cc-813d-80b9-8007-99cfd71e02d4", + "~ue52394cc-813d-80b9-8007-99cfd71e02d5", + "~ue52394cc-813d-80b9-8007-99cfd71e02d6", + "~ue52394cc-813d-80b9-8007-99cfd71e02d7", + "~ue52394cc-813d-80b9-8007-99cfd71e02d8", + "~ue52394cc-813d-80b9-8007-99cfd71e02d9", + "~ue52394cc-813d-80b9-8007-99cfd71e02da", + "~ue52394cc-813d-80b9-8007-99cfd71e02db", + "~ue52394cc-813d-80b9-8007-99cfd71e02dc", + "~ue52394cc-813d-80b9-8007-99cfd71e02dd", + "~ue52394cc-813d-80b9-8007-99cfd71e02de", + "~ue52394cc-813d-80b9-8007-99cfd71e02df" + ] + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02da": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAABcMBFAABQRAIAAAAAAAAAAAAAAAAAAAAAAAAAAPjTRQCAukMCAAAAAAAAAAAAAAAAAAAAAAAAAADQ1EX//4tE" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 6158.00012798179, + "~:y": 373 + } + }, + { + "~#point": { + "~:x": 6810.00012798179, + "~:y": 373 + } + }, + { + "~#point": { + "~:x": 6810.00012798179, + "~:y": 1120 + } + }, + { + "~#point": { + "~:x": 6158.00012798179, + "~:y": 1120 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02da", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:outer", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 0, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 6158.00012798179, + "~:y": 373, + "~:width": 652, + "~:height": 747, + "~:x1": 6158.00012798179, + "~:y1": 373, + "~:x2": 6810.00012798179, + "~:y2": 1120 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02db": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 0.998897352755765, + "~:b": -0.0469476161005346, + "~:c": 0.0469476161005349, + "~:d": 0.998897352755766, + "~:e": 4.54747350886464e-13, + "~:f": 0 + } + }, + "~:rotation": 357.309110550245, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAACmVsBF1TfxRAMAAABYJsJFLZ/rRIC/3UUBgLFEgL/dRQGAsUQCAAAAAAAAAAAAAAAAAAAAAAAAADj63kVZtRtFAgAAAAAAAAAAAAAAAAAAAAAAAACmVsBF1TfxRA==" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 6133.00018961173, + "~:y": 1465.25750392213 + } + }, + { + "~#point": { + "~:x": 7095.93725783569, + "~:y": 1420.00000105336 + } + }, + { + "~#point": { + "~:x": 7146.26510334951, + "~:y": 2490.81798563435 + } + }, + { + "~#point": { + "~:x": 6183.32803512555, + "~:y": 2536.07548850312 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 0.998897352755766, + "~:b": 0.0469476161005346, + "~:c": -0.0469476161005349, + "~:d": 0.998897352755766, + "~:e": -4.54245924973187e-13, + "~:f": -2.13493040521528e-14 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02db", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:outer", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 0, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 6157.63263638579, + "~:y": 1442.03773355246, + "~:width": 964.000020189657, + "~:height": 1072.00002245157, + "~:x1": 6157.63263638579, + "~:y1": 1442.03773355246, + "~:x2": 7121.63265657545, + "~:y2": 2514.03775600403 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02d8": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAD/B+1FAQBcRAIAAAAAAAAAAAAAAAAAAAAAAAAA/0cARgCA0kMCAAAAAAAAAAAAAAAAAAAAAAAAAP+zAEb//5FE" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 7584.99974837637, + "~:y": 421.000030517579 + } + }, + { + "~#point": { + "~:x": 8236.99974837637, + "~:y": 421.000030517579 + } + }, + { + "~#point": { + "~:x": 8236.99974837637, + "~:y": 1168.00003051758 + } + }, + { + "~#point": { + "~:x": 7584.99974837637, + "~:y": 1168.00003051758 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02d8", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:outer", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 0, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 7584.99974837637, + "~:y": 421.000030517579, + "~:width": 652, + "~:height": 747, + "~:x1": 7584.99974837637, + "~:y1": 421.000030517579, + "~:x2": 8236.99974837637, + "~:y2": 1168.00003051758 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02d9": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAD/B+1F/x/nRAMAAAD/5+5F/9/hRP+TBUb//6xE/5MFRv//rEQCAAAAAAAAAAAAAAAAAAAAAAAAAP9nBUb/fxlFAgAAAAAAAAAAAAAAAAAAAAAAAAD/B+1F/x/nRA==" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 7584.99974837637, + "~:y": 1384 + } + }, + { + "~#point": { + "~:x": 8548.99974837637, + "~:y": 1384 + } + }, + { + "~#point": { + "~:x": 8548.99974837637, + "~:y": 2456 + } + }, + { + "~#point": { + "~:x": 7584.99974837637, + "~:y": 2456 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02d9", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:outer", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 0, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 7584.99974837637, + "~:y": 1384, + "~:width": 964, + "~:height": 1072, + "~:x1": 7584.99974837637, + "~:y1": 1384, + "~:x2": 8548.99974837637, + "~:y2": 2456 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02de": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAABcMBFALBMRQIAAAAAAAAAAAAAAAAAAAAAAAAAAPjTRf//L0UCAAAAAAAAAAAAAAAAAAAAAAAAAADQ1EUAsF5F" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 6158.00012557245, + "~:y": 2815.99998762242 + } + }, + { + "~#point": { + "~:x": 6810.00012557245, + "~:y": 2815.99998762242 + } + }, + { + "~#point": { + "~:x": 6810.00012557245, + "~:y": 3562.99998762242 + } + }, + { + "~#point": { + "~:x": 6158.00012557245, + "~:y": 3562.99998762242 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02de", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:outer", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 40, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 6158.00012557245, + "~:y": 2815.99998762242, + "~:width": 652, + "~:height": 747, + "~:x1": 6158.00012557245, + "~:y1": 2815.99998762242, + "~:x2": 6810.00012557245, + "~:y2": 3562.99998762242 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02df": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAABcMBFAPCHRQMAAAABUMJFAKCGRQCQ3kUA0HJFAJDeRQDQckUCAAAAAAAAAAAAAAAAAAAAAAAAAAA43kUA6JpFAgAAAAAAAAAAAAAAAAAAAAAAAAABcMBFAPCHRQ==" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 6158.00012557245, + "~:y": 3884.99995710485 + } + }, + { + "~#point": { + "~:x": 7122.00012557245, + "~:y": 3884.99995710485 + } + }, + { + "~#point": { + "~:x": 7122.00012557245, + "~:y": 4956.99995710485 + } + }, + { + "~#point": { + "~:x": 6158.00012557245, + "~:y": 4956.99995710485 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02df", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:outer", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 40, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 6158.00012557245, + "~:y": 3884.99995710485, + "~:width": 964, + "~:height": 1072, + "~:x1": 6158.00012557245, + "~:y1": 3884.99995710485, + "~:x2": 7122.00012557245, + "~:y2": 4956.99995710485 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02dc": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAD/B+1FALBPRQIAAAAAAAAAAAAAAAAAAAAAAAAA/0cARv//MkUCAAAAAAAAAAAAAAAAAAAAAAAAAP+zAEYAsGFF" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 7584.99974596703, + "~:y": 2864.00001814 + } + }, + { + "~#point": { + "~:x": 8236.99974596703, + "~:y": 2864.00001814 + } + }, + { + "~#point": { + "~:x": 8236.99974596703, + "~:y": 3611.00001814 + } + }, + { + "~#point": { + "~:x": 7584.99974596703, + "~:y": 3611.00001814 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02dc", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:outer", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 40, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 7584.99974596703, + "~:y": 2864.00001814, + "~:width": 652, + "~:height": 747, + "~:x1": 7584.99974596703, + "~:y1": 2864.00001814, + "~:x2": 8236.99974596703, + "~:y2": 3611.00001814 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02dd": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAD/B+1FACCGRQMAAAD/5+5FANCERf+TBUYAMG9F/5MFRgAwb0UCAAAAAAAAAAAAAAAAAAAAAAAAAP9nBUYAGJlFAgAAAAAAAAAAAAAAAAAAAAAAAAD/B+1FACCGRQ==" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 7584.99974596703, + "~:y": 3826.99989606969 + } + }, + { + "~#point": { + "~:x": 8548.99974596703, + "~:y": 3826.99989606969 + } + }, + { + "~#point": { + "~:x": 8548.99974596703, + "~:y": 4898.99989606969 + } + }, + { + "~#point": { + "~:x": 7584.99974596703, + "~:y": 4898.99989606969 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02dd", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:outer", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 40, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 7584.99974596703, + "~:y": 3826.99989606969, + "~:width": 964, + "~:height": 1072, + "~:x1": 7584.99974596703, + "~:y1": 3826.99989606969, + "~:x2": 8548.99974596703, + "~:y2": 4898.99989606969 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02d2": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAAAMExF/79hRAIAAAAAAAAAAAAAAAAAAAAAAAAAAEBzRf//3UMCAAAAAAAAAAAAAAAAAAAAAAAAAADwdEX/35RE" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 3266.99998057661, + "~:y": 443.999986701422 + } + }, + { + "~#point": { + "~:x": 3918.99998057661, + "~:y": 443.999986701422 + } + }, + { + "~#point": { + "~:x": 3918.99998057661, + "~:y": 1190.99998670142 + } + }, + { + "~#point": { + "~:x": 3266.99998057661, + "~:y": 1190.99998670142 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02d2", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:center", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 0, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 3266.99998057661, + "~:y": 443.999986701422, + "~:width": 652, + "~:height": 747, + "~:x1": 3266.99998057661, + "~:y1": 443.999986701422, + "~:x2": 3918.99998057661, + "~:y2": 1190.99998670142 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02d3": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 0.998897352755765, + "~:b": -0.0469476161005346, + "~:c": 0.0469476161005349, + "~:d": 0.998897352755766, + "~:e": 4.54747350886464e-13, + "~:f": 0 + } + }, + "~:rotation": 357.309110550245, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAABL/UtF1hf6RAMAAACvnE9FLn/0RH9ng0UBYLpEf2eDRQFgukQCAAAAAAAAAAAAAAAAAAAAAAAAADeihEVaJSBFAgAAAAAAAAAAAAAAAAAAAAAAAABL/UtF1hf6RA==" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 3241.9999811714, + "~:y": 1536.25758217629 + } + }, + { + "~#point": { + "~:x": 4204.93704939535, + "~:y": 1491.00007930752 + } + }, + { + "~#point": { + "~:x": 4255.26489490917, + "~:y": 2561.81806388851 + } + }, + { + "~#point": { + "~:x": 3292.32782668522, + "~:y": 2607.07556675728 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 0.998897352755766, + "~:b": 0.0469476161005346, + "~:c": -0.0469476161005349, + "~:d": 0.998897352755766, + "~:e": -4.54245924973187e-13, + "~:f": -2.13493040521528e-14 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02d3", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:center", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 0, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 3266.63242794546, + "~:y": 1513.03781180661, + "~:width": 964.000020189657, + "~:height": 1072.00002245157, + "~:x1": 3266.63242794546, + "~:y1": 1513.03781180661, + "~:x2": 4230.63244813512, + "~:y2": 2585.03783425818 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02d0": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAAAMIxFAMBhRAIAAAAAAAAAAAAAAAAAAAAAAAAAALifRf//3UMCAAAAAAAAAAAAAAAAAAAAAAAAAACQoEUA4JRE" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 4486.00005873486, + "~:y": 443.999986701423 + } + }, + { + "~#point": { + "~:x": 5138.00005873486, + "~:y": 443.999986701423 + } + }, + { + "~#point": { + "~:x": 5138.00005873486, + "~:y": 1190.99998670142 + } + }, + { + "~#point": { + "~:x": 4486.00005873486, + "~:y": 1190.99998670142 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02d0", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:center", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 0, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 4486.00005873486, + "~:y": 443.999986701423, + "~:width": 652, + "~:height": 747, + "~:x1": 4486.00005873486, + "~:y1": 443.999986701423, + "~:x2": 5138.00005873486, + "~:y2": 1190.99998670142 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02d1": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAAAMIxFAADqRAMAAAAAEI5FAMDkRABQqkUA4K9EAFCqRQDgr0QCAAAAAAAAAAAAAAAAAAAAAAAAAAD4qUUA8BpFAgAAAAAAAAAAAAAAAAAAAAAAAAAAMIxFAADqRA==" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 4486.00005873486, + "~:y": 1407.00004773658 + } + }, + { + "~#point": { + "~:x": 5450.00005873486, + "~:y": 1407.00004773658 + } + }, + { + "~#point": { + "~:x": 5450.00005873486, + "~:y": 2479.00004773658 + } + }, + { + "~#point": { + "~:x": 4486.00005873486, + "~:y": 2479.00004773658 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02d1", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:center", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 0, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 4486.00005873486, + "~:y": 1407.00004773658, + "~:width": 964, + "~:height": 1072, + "~:x1": 4486.00005873486, + "~:y1": 1407.00004773658, + "~:x2": 5450.00005873486, + "~:y2": 2479.00004773658 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02d6": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAAAMExFACBRRQIAAAAAAAAAAAAAAAAAAAAAAAAAAEBzRQBwNEUCAAAAAAAAAAAAAAAAAAAAAAAAAADwdEUAIGNF" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 3266.99997816727, + "~:y": 2887.00009639416 + } + }, + { + "~#point": { + "~:x": 3918.99997816727, + "~:y": 2887.00009639416 + } + }, + { + "~#point": { + "~:x": 3918.99997816727, + "~:y": 3634.00009639416 + } + }, + { + "~#point": { + "~:x": 3266.99997816727, + "~:y": 3634.00009639416 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02d6", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:center", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 40, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 3266.99997816727, + "~:y": 2887.00009639416, + "~:width": 652, + "~:height": 747, + "~:x1": 3266.99997816727, + "~:y1": 2887.00009639416, + "~:x2": 3918.99997816727, + "~:y2": 3634.00009639416 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02d7": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAAAMExFACiKRQMAAAAA8E9FANiIRQA4hEUAQHdFADiERQBAd0UCAAAAAAAAAAAAAAAAAAAAAAAAAADgg0UAIJ1FAgAAAAAAAAAAAAAAAAAAAAAAAAAAMExFACiKRQ==" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 3266.99997816727, + "~:y": 3956.00006587658 + } + }, + { + "~#point": { + "~:x": 4230.99997816727, + "~:y": 3956.00006587658 + } + }, + { + "~#point": { + "~:x": 4230.99997816727, + "~:y": 5028.00006587658 + } + }, + { + "~#point": { + "~:x": 3266.99997816727, + "~:y": 5028.00006587658 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02d7", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:center", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 40, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 3266.99997816727, + "~:y": 3956.00006587658, + "~:width": 964, + "~:height": 1072, + "~:x1": 3266.99997816727, + "~:y1": 3956.00006587658, + "~:x2": 4230.99997816727, + "~:y2": 5028.00006587658 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02d4": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAAAMIxFACBRRQIAAAAAAAAAAAAAAAAAAAAAAAAAALifRQBwNEUCAAAAAAAAAAAAAAAAAAAAAAAAAACQoEUAIGNF" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 4486.00005632553, + "~:y": 2887.00009639416 + } + }, + { + "~#point": { + "~:x": 5138.00005632553, + "~:y": 2887.00009639416 + } + }, + { + "~#point": { + "~:x": 5138.00005632553, + "~:y": 3634.00009639416 + } + }, + { + "~#point": { + "~:x": 4486.00005632553, + "~:y": 3634.00009639416 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02d4", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:center", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 40, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 4486.00005632553, + "~:y": 2887.00009639416, + "~:width": 652, + "~:height": 747, + "~:x1": 4486.00005632553, + "~:y1": 2887.00009639416, + "~:x2": 5138.00005632553, + "~:y2": 3634.00009639416 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02d5": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAAAMIxFANiGRQMAAAAAEI5FAIiFRQBQqkUAoHBFAFCqRQCgcEUCAAAAAAAAAAAAAAAAAAAAAAAAAAD4qUUA0JlFAgAAAAAAAAAAAAAAAAAAAAAAAAAAMIxFANiGRQ==" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 4486.00005632553, + "~:y": 3850.00009639416 + } + }, + { + "~#point": { + "~:x": 5450.00005632553, + "~:y": 3850.00009639416 + } + }, + { + "~#point": { + "~:x": 5450.00005632553, + "~:y": 4922.00009639416 + } + }, + { + "~#point": { + "~:x": 4486.00005632553, + "~:y": 4922.00009639416 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02d5", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:center", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 40, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 4486.00005632553, + "~:y": 3850.00009639416, + "~:width": 964, + "~:height": 1072, + "~:x1": 4486.00005632553, + "~:y1": 3850.00009639416, + "~:x2": 5450.00005632553, + "~:y2": 4922.00009639416 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02ca": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAD//wpEAAB2RAIAAAAAAAAAAAAAAAAAAAAAAAAAAKCTRP8/A0QCAAAAAAAAAAAAAAAAAAAAAAAAAAAAl0QAAJ9E" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 555.999938370053, + "~:y": 524.999995655115 + } + }, + { + "~#point": { + "~:x": 1207.99993837005, + "~:y": 524.999995655115 + } + }, + { + "~#point": { + "~:x": 1207.99993837005, + "~:y": 1271.99999565511 + } + }, + { + "~#point": { + "~:x": 555.999938370053, + "~:y": 1271.99999565511 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02ca", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:inner", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 0, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 555.999938370053, + "~:y": 524.999995655115, + "~:width": 652, + "~:height": 747, + "~:x1": 555.999938370053, + "~:y1": 524.999995655115, + "~:x2": 1207.99993837005, + "~:y2": 1271.99999565511 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02cb": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 0.998897352755765, + "~:b": -0.0469476161005346, + "~:c": 0.0469476161005349, + "~:d": 0.998897352755766, + "~:e": 4.54747350886464e-13, + "~:f": 0 + } + }, + "~:rotation": 357.309110550245, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAArNQpE6xsCRQMAAAC7shhELp/+RP29ukQAgMRE/b26RACAxEQCAAAAAAAAAAAAAAAAAAAAAAAAAN6ov0RZNSVFAgAAAAAAAAAAAAAAAAAAAAAAAAArNQpE6xsCRQ==" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 531, + "~:y": 1617.25753009482 + } + }, + { + "~#point": { + "~:x": 1493.93706822395, + "~:y": 1572.00002722605 + } + }, + { + "~#point": { + "~:x": 1544.26491373777, + "~:y": 2642.81801180705 + } + }, + { + "~#point": { + "~:x": 581.327845513821, + "~:y": 2688.07551467582 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 0.998897352755766, + "~:b": 0.0469476161005346, + "~:c": -0.0469476161005349, + "~:d": 0.998897352755766, + "~:e": -4.54245924973187e-13, + "~:f": -2.13493040521528e-14 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02cb", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:inner", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 0, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 555.632446774058, + "~:y": 1594.03775972515, + "~:width": 964.000020189657, + "~:height": 1072.00002245157, + "~:x1": 555.632446774058, + "~:y1": 1594.03775972515, + "~:x2": 1519.63246696372, + "~:y2": 2666.03778217672 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02c8": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAAA4N1EAAB2RAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAWRf8/A0QCAAAAAAAAAAAAAAAAAAAAAAAAAACwF0UAAJ9E" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 1774.99989445799, + "~:y": 524.999995655115 + } + }, + { + "~#point": { + "~:x": 2426.99989445799, + "~:y": 524.999995655115 + } + }, + { + "~#point": { + "~:x": 2426.99989445799, + "~:y": 1271.99999565512 + } + }, + { + "~#point": { + "~:x": 1774.99989445799, + "~:y": 1271.99999565512 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02c8", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:inner", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 0, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 1774.99989445799, + "~:y": 524.999995655115, + "~:width": 652, + "~:height": 747, + "~:x1": 1774.99989445799, + "~:y1": 524.999995655115, + "~:x2": 2426.99989445799, + "~:y2": 1271.99999565512 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02c9": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAAA4N1EACD0RAMAAAAAYOVEAODuRAAwK0UAALpEADArRQAAukQCAAAAAAAAAAAAAAAAAAAAAAAAAACAKkUAACBFAgAAAAAAAAAAAAAAAAAAAAAAAAAA4N1EACD0RA==" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 1774.99989445799, + "~:y": 1487.99999565512 + } + }, + { + "~#point": { + "~:x": 2738.99989445799, + "~:y": 1487.99999565512 + } + }, + { + "~#point": { + "~:x": 2738.99989445799, + "~:y": 2559.99999565512 + } + }, + { + "~#point": { + "~:x": 1774.99989445799, + "~:y": 2559.99999565512 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02c9", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:inner", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 0, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 1774.99989445799, + "~:y": 1487.99999565512, + "~:width": 964, + "~:height": 1072, + "~:x1": 1774.99989445799, + "~:y1": 1487.99999565512, + "~:x2": 2738.99989445799, + "~:y2": 2559.99999565512 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02ce": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAD//wpEADBWRQIAAAAAAAAAAAAAAAAAAAAAAAAAAKCTRACAOUUCAAAAAAAAAAAAAAAAAAAAAAAAAAAAl0QAMGhF" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 555.999935960719, + "~:y": 2967.99998327754 + } + }, + { + "~#point": { + "~:x": 1207.99993596072, + "~:y": 2967.99998327754 + } + }, + { + "~#point": { + "~:x": 1207.99993596072, + "~:y": 3714.99998327754 + } + }, + { + "~#point": { + "~:x": 555.999935960719, + "~:y": 3714.99998327754 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02ce", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:inner", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 40, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 555.999935960719, + "~:y": 2967.99998327754, + "~:width": 652, + "~:height": 747, + "~:x1": 555.999935960719, + "~:y1": 2967.99998327754, + "~:x2": 1207.99993596072, + "~:y2": 3714.99998327754 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02cf": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAD//wpEALCMRQMAAAD//xlEAGCLRQAAvkQAUHxFAAC+RABQfEUCAAAAAAAAAAAAAAAAAAAAAAAAAACgvEQAqJ9FAgAAAAAAAAAAAAAAAAAAAAAAAAD//wpEALCMRQ==" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 555.999935960719, + "~:y": 4036.99995275996 + } + }, + { + "~#point": { + "~:x": 1519.99993596072, + "~:y": 4036.99995275996 + } + }, + { + "~#point": { + "~:x": 1519.99993596072, + "~:y": 5108.99995275996 + } + }, + { + "~#point": { + "~:x": 555.999935960719, + "~:y": 5108.99995275996 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02cf", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:inner", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 40, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 555.999935960719, + "~:y": 4036.99995275996, + "~:width": 964, + "~:height": 1072, + "~:x1": 555.999935960719, + "~:y1": 4036.99995275996, + "~:x2": 1519.99993596072, + "~:y2": 5108.99995275996 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02cc": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAAA4N1EADBWRQIAAAAAAAAAAAAAAAAAAAAAAAAAAAAWRQCAOUUCAAAAAAAAAAAAAAAAAAAAAAAAAACwF0UAMGhF" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 1774.99989204866, + "~:y": 2967.99998327754 + } + }, + { + "~#point": { + "~:x": 2426.99989204866, + "~:y": 2967.99998327754 + } + }, + { + "~#point": { + "~:x": 2426.99989204866, + "~:y": 3714.99998327754 + } + }, + { + "~#point": { + "~:x": 1774.99989204866, + "~:y": 3714.99998327754 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02cc", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:inner", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 40, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 1774.99989204866, + "~:y": 2967.99998327754, + "~:width": 652, + "~:height": 747, + "~:x1": 1774.99989204866, + "~:y1": 2967.99998327754, + "~:x2": 2426.99989204866, + "~:y2": 3714.99998327754 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ue52394cc-813d-80b9-8007-99cfd71e02cd": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAAA4N1EAGCJRQMAAAAAYOVEABCIRQAwK0UAsHVFADArRQCwdUUCAAAAAAAAAAAAAAAAAAAAAAAAAACAKkUAWJxFAgAAAAAAAAAAAAAAAAAAAAAAAAAA4N1EAGCJRQ==" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 1774.99989204866, + "~:y": 3930.99998327754 + } + }, + { + "~#point": { + "~:x": 2738.99989204866, + "~:y": 3930.99998327754 + } + }, + { + "~#point": { + "~:x": 2738.99989204866, + "~:y": 5002.99998327754 + } + }, + { + "~#point": { + "~:x": 1774.99989204866, + "~:y": 5002.99998327754 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ue52394cc-813d-80b9-8007-99cfd71e02cd", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:inner", + "~:stroke-width": 50, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u660e606c-c745-80a4-8007-988b6dd9ceb1", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#ff0000", + "~:opacity": 1 + }, + "~:offset-x": 100, + "~:offset-y": 100, + "~:blur": 40, + "~:spread": 40, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 1774.99989204866, + "~:y": 3930.99998327754, + "~:width": 964, + "~:height": 1072, + "~:x1": 1774.99989204866, + "~:y1": 3930.99998327754, + "~:x2": 2738.99989204866, + "~:y2": 5002.99998327754 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + } + }, + "~:id": "~u93113137-fe66-80fb-8007-99cfd5cbf362", + "~:name": "Page 1" + } + }, + "~:id": "~u93113137-fe66-80fb-8007-99cfd5cbf361", + "~:options": { + "~:components-v2": true, + "~:base-font-size": "16px" + } + } + } \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-file-13468-fragment.json b/frontend/playwright/data/workspace/get-file-13468-fragment.json new file mode 100644 index 0000000000..94a22cb794 --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-13468-fragment.json @@ -0,0 +1,21 @@ +{ + "~:file-id": "~u3a4d7ec7-c391-8146-8007-9a05c41da6b9", + "~:id": "~u3a4d7ec7-c391-8146-8007-9dd6c998fbc4", + "~:created-at": "~m1771846681191", + "~:modified-at": "~m1771846681191", + "~:type": "fragment", + "~:backend": "db", + "~:data": { + "~:id": "~u95b23c15-79f9-81ba-8007-99d81b5290dd", + "~:name": "Page 1", + "~:objects": { + "~#penpot/objects-map/v2": { + "~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u95b23c15-79f9-81ba-8007-99d81b5290dd\",\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^I\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~ucfb31a9c-83c2-806f-8007-9dbf43043bdf\"]]]", + "~ucfb31a9c-83c2-806f-8007-9dbf43043be0": "[\"~#shape\",[\"^ \",\"~:y\",-218.99999605032087,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",5,\"~:p2\",5,\"~:p3\",5,\"~:p4\",5],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:nowrap\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"Container\",\"~:layout-align-items\",\"~:start\",\"~:width\",431.99994866329087,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",608.9999813066789,\"~:y\",-218.99999605032087]],[\"^J\",[\"^ \",\"~:x\",1040.9999299699698,\"~:y\",-218.99999605032087]],[\"^J\",[\"^ \",\"~:x\",1040.9999299699698,\"~:y\",-177.00001533586985]],[\"^J\",[\"^ \",\"~:x\",608.9999813066789,\"~:y\",-177.00001533586985]]],\"~:show-content\",true,\"~:layout-item-h-sizing\",\"~:fill\",\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",4,\"~:column-gap\",4],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u95b23c15-79f9-81ba-8007-99d81b5290dd\",\"~:layout-item-v-sizing\",\"~:auto\",\"~:layout-justify-content\",\"^C\",\"~:constraints-v\",\"~:top\",\"~:constraints-h\",\"~:left\",\"~:id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be0\",\"~:parent-id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043bdf\",\"~:layout-flex-dir\",\"~:column\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043bdf\",\"~:strokes\",[],\"~:x\",608.9999813066788,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",608.9999813066788,\"~:y\",-218.99999605032087,\"^D\",431.99994866329087,\"~:height\",41.99998071445103,\"~:x1\",608.9999813066788,\"~:y1\",-218.99999605032087,\"~:x2\",1040.9999299699698,\"~:y2\",-177.00001533586985]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#ffc0cb\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^1:\",41.99998071445103,\"~:flip-y\",null,\"~:shapes\",[\"~ucfb31a9c-83c2-806f-8007-9dbf43043be2\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be3\"]]]", + "~ucfb31a9c-83c2-806f-8007-9dbf43043be2": "[\"~#shape\",[\"^ \",\"~:y\",-178.00000568505413,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:hide-in-viewer\",false,\"~:name\",\"show / hide me\",\"~:width\",99.98206911702209,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",614.0000002576337,\"~:y\",-178.00000568505413]],[\"^:\",[\"^ \",\"~:x\",713.9820693746558,\"~:y\",-178.00000568505413]],[\"^:\",[\"^ \",\"~:x\",713.9820693746558,\"~:y\",-148.0000135081636]],[\"^:\",[\"^ \",\"~:x\",614.0000002576337,\"~:y\",-148.0000135081636]]],\"~:r2\",0,\"~:layout-item-h-sizing\",\"~:fix\",\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:layout-item-v-sizing\",\"^=\",\"~:r3\",0,\"~:constraints-v\",\"~:top\",\"~:constraints-h\",\"~:left\",\"~:r1\",0,\"~:hidden\",true,\"~:id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be2\",\"~:parent-id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be0\",\"~:frame-id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be0\",\"~:strokes\",[],\"~:x\",614.0000002576337,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",614.0000002576337,\"~:y\",-178.00000568505413,\"^6\",99.98206911702209,\"~:height\",29.999992176890544,\"~:x1\",614.0000002576337,\"~:y1\",-178.00000568505413,\"~:x2\",713.9820693746558,\"~:y2\",-148.0000135081636]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^P\",29.999992176890544,\"~:flip-y\",null]]", + "~ucfb31a9c-83c2-806f-8007-9dbf43043be3": "[\"~#shape\",[\"^ \",\"~:y\",-213.99999587313152,\"~:hide-fill-on-export\",false,\"~:rx\",8,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:hide-in-viewer\",true,\"~:name\",\"Full width\",\"~:width\",422.00001200500014,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",613.9999939062393,\"~:y\",-213.99999587313152]],[\"^<\",[\"^ \",\"~:x\",1036.0000059112394,\"~:y\",-213.99999587313152]],[\"^<\",[\"^ \",\"~:x\",1036.0000059112394,\"~:y\",-182.00001303926604]],[\"^<\",[\"^ \",\"~:x\",613.9999939062393,\"~:y\",-182.00001303926604]]],\"~:r2\",8,\"~:show-content\",true,\"~:layout-item-h-sizing\",\"~:fix\",\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^4\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u95b23c15-79f9-81ba-8007-99d81b5290dd\",\"~:layout-item-v-sizing\",\"^@\",\"~:r3\",8,\"~:r1\",8,\"~:id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be3\",\"~:parent-id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be0\",\"~:frame-id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be0\",\"~:strokes\",[],\"~:x\",613.9999939062393,\"~:proportion\",1,\"~:r4\",8,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",613.9999939062393,\"~:y\",-213.99999587313152,\"^8\",422.00001200500014,\"~:height\",31.999982833865488,\"~:x1\",613.9999939062393,\"~:y1\",-213.99999587313152,\"~:x2\",1036.0000059112394,\"~:y2\",-182.00001303926604]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#212426\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"~:ry\",8,\"^O\",31.999982833865488,\"~:flip-y\",null,\"~:shapes\",[]]]", + "~ucfb31a9c-83c2-806f-8007-9dbf43043bdf": "[\"~#shape\",[\"^ \",\"~:y\",-228.99999763039506,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",10,\"~:p2\",10,\"~:p3\",10,\"~:p4\",10],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:nowrap\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"Parent\",\"~:layout-align-items\",\"~:start\",\"~:width\",451.999905143128,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",599.0000149607649,\"~:y\",-228.99999763039506]],[\"^J\",[\"^ \",\"~:x\",1050.999920103893,\"~:y\",-228.99999763039506]],[\"^J\",[\"^ \",\"~:x\",1050.999920103893,\"~:y\",-167.0000160450801]],[\"^J\",[\"^ \",\"~:x\",599.0000149607649,\"~:y\",-167.0000160450801]]],\"~:r2\",0,\"~:show-content\",true,\"~:layout-item-h-sizing\",\"~:fix\",\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",10,\"~:column-gap\",8],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u95b23c15-79f9-81ba-8007-99d81b5290dd\",\"~:layout-item-v-sizing\",\"~:auto\",\"~:r3\",0,\"~:layout-justify-content\",\"^C\",\"~:r1\",0,\"~:id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043bdf\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:layout-flex-dir\",\"~:column\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",599.0000149607649,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",599.0000149607649,\"~:y\",-228.99999763039506,\"^D\",451.999905143128,\"~:height\",61.99998158531497,\"~:x1\",599.0000149607649,\"~:y1\",-228.99999763039506,\"~:x2\",1050.999920103893,\"~:y2\",-167.0000160450801]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#000000\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^1:\",61.99998158531497,\"~:flip-y\",null,\"~:shapes\",[\"~ucfb31a9c-83c2-806f-8007-9dbf43043be0\"]]]" + } + } + } +} diff --git a/frontend/playwright/data/workspace/get-file-13468.json b/frontend/playwright/data/workspace/get-file-13468.json new file mode 100644 index 0000000000..7f1be239ca --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-13468.json @@ -0,0 +1,131 @@ +{ + "~:features": { + "~#set": [ + "fdata/path-data", + "design-tokens/v1", + "variants/v1", + "layout/grid", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:team-id": "~ud715d0a5-a44e-8056-8005-a79999e18b64", + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "test-bug-flex", + "~:revn": 114, + "~:modified-at": "~m1771846681183", + "~:vern": 0, + "~:id": "~u3a4d7ec7-c391-8146-8007-9a05c41da6b9", + "~:is-shared": false, + "~:migrations": { + "~#ordered-set": [ + "legacy-2", + "legacy-3", + "legacy-5", + "legacy-6", + "legacy-7", + "legacy-8", + "legacy-9", + "legacy-10", + "legacy-11", + "legacy-12", + "legacy-13", + "legacy-14", + "legacy-16", + "legacy-17", + "legacy-18", + "legacy-19", + "legacy-25", + "legacy-26", + "legacy-27", + "legacy-28", + "legacy-29", + "legacy-31", + "legacy-32", + "legacy-33", + "legacy-34", + "legacy-36", + "legacy-37", + "legacy-38", + "legacy-39", + "legacy-40", + "legacy-41", + "legacy-42", + "legacy-43", + "legacy-44", + "legacy-45", + "legacy-46", + "legacy-47", + "legacy-48", + "legacy-49", + "legacy-50", + "legacy-51", + "legacy-52", + "legacy-53", + "legacy-54", + "legacy-55", + "legacy-56", + "legacy-57", + "legacy-59", + "legacy-62", + "legacy-65", + "legacy-66", + "legacy-67", + "0001-remove-tokens-from-groups", + "0002-normalize-bool-content-v2", + "0002-clean-shape-interactions", + "0003-fix-root-shape", + "0003-convert-path-content-v2", + "0005-deprecate-image-type", + "0006-fix-old-texts-fills", + "0008-fix-library-colors-v4", + "0009-clean-library-colors", + "0009-add-partial-text-touched-flags", + "0010-fix-swap-slots-pointing-non-existent-shapes", + "0011-fix-invalid-text-touched-flags", + "0012-fix-position-data", + "0013-fix-component-path", + "0013-clear-invalid-strokes-and-fills", + "0014-fix-tokens-lib-duplicate-ids", + "0014-clear-components-nil-objects", + "0015-fix-text-attrs-blank-strings", + "0015-clean-shadow-color", + "0016-copy-fills-from-position-data-to-text-node" + ] + }, + "~:version": 67, + "~:project-id": "~u76eab896-accf-81a5-8007-2b264ebe7817", + "~:created-at": "~m1771590560885", + "~:backend": "legacy-db", + "~:data": { + "~:pages": [ + "~u95b23c15-79f9-81ba-8007-99d81b5290dd" + ], + "~:pages-index": { + "~u95b23c15-79f9-81ba-8007-99d81b5290dd": { + "~#penpot/pointer": [ + "~u3a4d7ec7-c391-8146-8007-9dd6c998fbc4", + { + "~:created-at": "~m1771846681187" + } + ] + } + }, + "~:id": "~u3a4d7ec7-c391-8146-8007-9a05c41da6b9", + "~:options": { + "~:components-v2": true, + "~:base-font-size": "16px" + } + } +} diff --git a/frontend/playwright/ui/pages/BasePage.js b/frontend/playwright/ui/pages/BasePage.js index 2e7380b2a4..bb95840664 100644 --- a/frontend/playwright/ui/pages/BasePage.js +++ b/frontend/playwright/ui/pages/BasePage.js @@ -1,4 +1,8 @@ export class BasePage { + static async init(page) { + await BasePage.mockConfigFlags(page, []); + } + /** * Mocks multiple RPC calls in a single call. * diff --git a/frontend/playwright/ui/pages/BaseWebSocketPage.js b/frontend/playwright/ui/pages/BaseWebSocketPage.js index 21855312ee..e5de194537 100644 --- a/frontend/playwright/ui/pages/BaseWebSocketPage.js +++ b/frontend/playwright/ui/pages/BaseWebSocketPage.js @@ -2,13 +2,8 @@ import { MockWebSocketHelper } from "../../helpers/MockWebSocketHelper"; import BasePage from "./BasePage"; export class BaseWebSocketPage extends BasePage { - /** - * This should be called on `test.beforeEach`. - * - * @param {Page} page - * @returns - */ - static async initWebSockets(page) { + static async init(page) { + await super.init(page); await MockWebSocketHelper.init(page); } diff --git a/frontend/playwright/ui/pages/DashboardPage.js b/frontend/playwright/ui/pages/DashboardPage.js index 111913e5a3..f7e4df2582 100644 --- a/frontend/playwright/ui/pages/DashboardPage.js +++ b/frontend/playwright/ui/pages/DashboardPage.js @@ -3,54 +3,62 @@ import { BaseWebSocketPage } from "./BaseWebSocketPage"; export class DashboardPage extends BaseWebSocketPage { static async init(page) { - await BaseWebSocketPage.initWebSockets(page); + await super.init(page); - await BaseWebSocketPage.mockRPC( + await super.mockConfigFlags(page, ["disable-onboarding"]); + + await super.mockRPC( page, "get-teams", "logged-in-user/get-teams-default.json", ); - await BaseWebSocketPage.mockRPC( + await super.mockRPC( page, "get-font-variants?team-id=*", "workspace/get-font-variants-empty.json", ); - await BaseWebSocketPage.mockRPC( + await super.mockRPC( page, "get-projects?team-id=*", "logged-in-user/get-projects-default.json", ); - await BaseWebSocketPage.mockRPC( + await super.mockRPC( page, "get-team-members?team-id=*", "logged-in-user/get-team-members-your-penpot.json", ); - await BaseWebSocketPage.mockRPC( + await super.mockRPC( page, "get-team-users?team-id=*", "logged-in-user/get-team-users-single-user.json", ); - await BaseWebSocketPage.mockRPC( + await super.mockRPC( page, "get-unread-comment-threads?team-id=*", "logged-in-user/get-team-users-single-user.json", ); - await BaseWebSocketPage.mockRPC( + await super.mockRPC( page, "get-team-recent-files?team-id=*", "logged-in-user/get-team-recent-files-empty.json", ); - await BaseWebSocketPage.mockRPC( + await super.mockRPC( page, "get-profiles-for-file-comments", "workspace/get-profile-for-file-comments.json", ); - await BaseWebSocketPage.mockRPC( + await super.mockRPC( page, "get-builtin-templates", "logged-in-user/get-built-in-templates-empty.json", ); + + await super.mockRPC( + page, + "get-profile", + "logged-in-user/get-profile-logged-in.json", + ); } static anyTeamId = "c7ce0794-0992-8105-8004-38e630f40f6d"; diff --git a/frontend/playwright/ui/pages/LoginPage.js b/frontend/playwright/ui/pages/LoginPage.js index 80f471aa43..bb2efc68c6 100644 --- a/frontend/playwright/ui/pages/LoginPage.js +++ b/frontend/playwright/ui/pages/LoginPage.js @@ -1,6 +1,10 @@ import { BasePage } from "./BasePage"; export class LoginPage extends BasePage { + static async init(page) { + await super.init(page); + } + constructor(page) { super(page); this.loginButton = page.getByRole("button", { name: "Continue" }); diff --git a/frontend/playwright/ui/pages/RegisterPage.js b/frontend/playwright/ui/pages/RegisterPage.js index 097bbefb97..8d3633e678 100644 --- a/frontend/playwright/ui/pages/RegisterPage.js +++ b/frontend/playwright/ui/pages/RegisterPage.js @@ -29,8 +29,13 @@ export class RegisterPage extends BasePage { ); } + static async init(page) { + await BasePage.init(page); + } + static async initWithLoggedOutUser(page) { - await this.mockRPC(page, "get-profile", "get-profile-anonymous.json"); + await BasePage.init(page); + await BasePage.mockRPC(page, "get-profile", "get-profile-anonymous.json"); } } diff --git a/frontend/playwright/ui/pages/SubscriptionProfilePage.js b/frontend/playwright/ui/pages/SubscriptionProfilePage.js index b0e349a81a..b9769cfa52 100644 --- a/frontend/playwright/ui/pages/SubscriptionProfilePage.js +++ b/frontend/playwright/ui/pages/SubscriptionProfilePage.js @@ -3,9 +3,9 @@ import { DashboardPage } from "./DashboardPage"; export class SubscriptionProfilePage extends DashboardPage { static async init(page) { - await DashboardPage.initWebSockets(page); + await super.init(page); - await DashboardPage.mockRPC( + await super.mockRPC( page, "get-subscription-usage", "subscription/get-subscription-usage.json", diff --git a/frontend/playwright/ui/pages/ViewerPage.js b/frontend/playwright/ui/pages/ViewerPage.js index cf97f80fc6..05ba705bc5 100644 --- a/frontend/playwright/ui/pages/ViewerPage.js +++ b/frontend/playwright/ui/pages/ViewerPage.js @@ -4,16 +4,6 @@ export class ViewerPage extends BaseWebSocketPage { static anyFileId = "c7ce0794-0992-8105-8004-38f280443849"; static anyPageId = "c7ce0794-0992-8105-8004-38f28044384a"; - /** - * This should be called on `test.beforeEach`. - * - * @param {Page} page - * @returns - */ - static async init(page) { - await BaseWebSocketPage.initWebSockets(page); - } - async setupLoggedInUser() { await this.mockRPC( "get-profile", diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js index 18da0810bd..a4624ce37c 100644 --- a/frontend/playwright/ui/pages/WorkspacePage.js +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -45,24 +45,27 @@ export class WorkspacePage extends BaseWebSocketPage { return this.waitForEditor(); } - stopEditing() { - return this.page.keyboard.press("Escape"); + async stopEditing() { + await this.page.keyboard.press("Escape"); } async moveToLeft(amount = 0) { for (let i = 0; i < amount; i++) { await this.page.keyboard.press("ArrowLeft"); } + await this.waitForIdle(); } async moveToRight(amount = 0) { for (let i = 0; i < amount; i++) { await this.page.keyboard.press("ArrowRight"); } + await this.waitForIdle(); } async moveFromStart(offset = 0) { await this.page.keyboard.press("Home"); + await this.waitForIdle(); await this.moveToRight(offset); } @@ -103,6 +106,10 @@ export class WorkspacePage extends BaseWebSocketPage { changeLetterSpacing(newValue) { return this.changeNumericInput(this.letterSpacing, newValue); } + + async waitForIdle() { + await this.page.evaluate(() => new Promise((resolve) => globalThis.requestIdleCallback(resolve))); + } }; /** @@ -112,9 +119,9 @@ export class WorkspacePage extends BaseWebSocketPage { * @returns */ static async init(page) { - await BaseWebSocketPage.initWebSockets(page); + await super.init(page); - await BaseWebSocketPage.mockRPCs(page, { + await super.mockRPCs(page, { "get-profile": "logged-in-user/get-profile-logged-in.json", "get-team-users?file-id=*": "logged-in-user/get-team-users-single-user.json", diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js index 11253425c6..9a4b26809b 100644 --- a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js +++ b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js @@ -243,6 +243,46 @@ test("Renders a file with a closed path shape with multiple segments using strok await expect(workspace.canvas).toHaveScreenshot(); }); +test("Renders solid shadows after select all and zoom to selected", async ({ + page, +}) => { + const workspace = new WasmWorkspacePage(page); + await workspace.setupEmptyFile(); + await workspace.mockGetFile("render-wasm/get-solid-shadows.json"); + + await workspace.goToWorkspace({ + id: "93113137-fe66-80fb-8007-99ca9fd96841", + pageId: "93113137-fe66-80fb-8007-99ca9fd96842", + }); + await workspace.waitForFirstRender(); + + await workspace.viewport.click(); + await page.keyboard.press("ControlOrMeta+A"); + const previousRenderCount = await workspace.getRenderCount(); + await page.keyboard.press("f"); + await workspace.waitForNextRender(previousRenderCount); + + await workspace.hideUI(); + await expect(workspace.canvas).toHaveScreenshot(); +}); + +test("Renders strokes with solid shadows", async ({ + page, +}) => { + const workspace = new WasmWorkspacePage(page); + await workspace.setupEmptyFile(); + await workspace.mockGetFile("render-wasm/get-solid-strokes-shadows.json"); + + await workspace.goToWorkspace({ + id: "93113137-fe66-80fb-8007-99cfd5cbf361", + pageId: "93113137-fe66-80fb-8007-99cfd5cbf362", + }); + await workspace.waitForFirstRender(); + + await workspace.hideUI(); + await expect(workspace.canvas).toHaveScreenshot(); +}); + test("Renders a file with paths and svg attrs", async ({ page }) => { const workspace = new WasmWorkspacePage(page); await workspace.setupEmptyFile(); diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-solid-shadows-after-select-all-and-zoom-to-selected-1.png b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-solid-shadows-after-select-all-and-zoom-to-selected-1.png new file mode 100644 index 0000000000..cb3c5e6135 Binary files /dev/null and b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-solid-shadows-after-select-all-and-zoom-to-selected-1.png differ diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-strokes-with-solid-shadows-1.png b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-strokes-with-solid-shadows-1.png new file mode 100644 index 0000000000..e91a91e8ab Binary files /dev/null and b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-strokes-with-solid-shadows-1.png differ diff --git a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-group-with-text-with-inherited-shadows-1.png b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-group-with-text-with-inherited-shadows-1.png index f8affcad8c..c8ff437984 100644 Binary files a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-group-with-text-with-inherited-shadows-1.png and b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-group-with-text-with-inherited-shadows-1.png differ diff --git a/frontend/playwright/ui/specs/dashboard-deleted.spec.js b/frontend/playwright/ui/specs/dashboard-deleted.spec.js index bacf85ae75..062d6d1bfa 100644 --- a/frontend/playwright/ui/specs/dashboard-deleted.spec.js +++ b/frontend/playwright/ui/specs/dashboard-deleted.spec.js @@ -3,11 +3,6 @@ import DashboardPage from "../pages/DashboardPage"; test.beforeEach(async ({ page }) => { await DashboardPage.init(page); - await DashboardPage.mockRPC( - page, - "get-profile", - "logged-in-user/get-profile-logged-in-no-onboarding.json", - ); }); test.describe("Dashboard Deleted Page", () => { diff --git a/frontend/playwright/ui/specs/dashboard-libraries.spec.js b/frontend/playwright/ui/specs/dashboard-libraries.spec.js index d30f6e82c0..efb258aef4 100644 --- a/frontend/playwright/ui/specs/dashboard-libraries.spec.js +++ b/frontend/playwright/ui/specs/dashboard-libraries.spec.js @@ -3,11 +3,6 @@ import DashboardPage from "../pages/DashboardPage"; test.beforeEach(async ({ page }) => { await DashboardPage.init(page); - await DashboardPage.mockRPC( - page, - "get-profile", - "logged-in-user/get-profile-logged-in-no-onboarding.json", - ); }); test("BUG 10421 - Fix libraries context menu", async ({ page }) => { diff --git a/frontend/playwright/ui/specs/dashboard-teams.spec.js b/frontend/playwright/ui/specs/dashboard-teams.spec.js index 8a95cf8e12..31153ac4cb 100644 --- a/frontend/playwright/ui/specs/dashboard-teams.spec.js +++ b/frontend/playwright/ui/specs/dashboard-teams.spec.js @@ -3,11 +3,6 @@ import DashboardPage from "../pages/DashboardPage"; test.beforeEach(async ({ page }) => { await DashboardPage.init(page); - await DashboardPage.mockRPC( - page, - "get-profile", - "logged-in-user/get-profile-logged-in-no-onboarding.json", - ); }); test("BUG 12359 - Selected invitations count is not pluralized", async ({ diff --git a/frontend/playwright/ui/specs/dashboard-viewer-role.spec.js b/frontend/playwright/ui/specs/dashboard-viewer-role.spec.js index 55197be037..0ab4ef8446 100644 --- a/frontend/playwright/ui/specs/dashboard-viewer-role.spec.js +++ b/frontend/playwright/ui/specs/dashboard-viewer-role.spec.js @@ -3,11 +3,7 @@ import DashboardPage from "../pages/DashboardPage"; test.beforeEach(async ({ page }) => { await DashboardPage.init(page); - await DashboardPage.mockRPC( - page, - "get-profile", - "logged-in-user/get-profile-logged-in-no-onboarding.json", - ); + await DashboardPage.mockRPC( page, "get-teams", diff --git a/frontend/playwright/ui/specs/dashboard.spec.js b/frontend/playwright/ui/specs/dashboard.spec.js index 768106634e..23e3b7a669 100644 --- a/frontend/playwright/ui/specs/dashboard.spec.js +++ b/frontend/playwright/ui/specs/dashboard.spec.js @@ -3,11 +3,6 @@ import DashboardPage from "../pages/DashboardPage"; test.beforeEach(async ({ page }) => { await DashboardPage.init(page); - await DashboardPage.mockRPC( - page, - "get-profile", - "logged-in-user/get-profile-logged-in-no-onboarding.json", - ); }); test("Dashboad page has title ", async ({ page }) => { diff --git a/frontend/playwright/ui/specs/login.spec.js b/frontend/playwright/ui/specs/login.spec.js index 4a2604f4b1..254e205b75 100644 --- a/frontend/playwright/ui/specs/login.spec.js +++ b/frontend/playwright/ui/specs/login.spec.js @@ -2,6 +2,8 @@ import { test, expect } from "@playwright/test"; import { LoginPage } from "../pages/LoginPage"; test.beforeEach(async ({ page }) => { + await LoginPage.init(page); + const login = new LoginPage(page); await login.initWithLoggedOutUser(); diff --git a/frontend/playwright/ui/specs/onboarding.spec.js b/frontend/playwright/ui/specs/onboarding.spec.js index a14b0abe42..87a8da65b7 100644 --- a/frontend/playwright/ui/specs/onboarding.spec.js +++ b/frontend/playwright/ui/specs/onboarding.spec.js @@ -4,6 +4,7 @@ import OnboardingPage from "../pages/OnboardingPage"; test.beforeEach(async ({ page }) => { await DashboardPage.init(page); + await DashboardPage.mockConfigFlags(page, ["enable-onboarding"]); await DashboardPage.mockRPC( page, "get-profile", diff --git a/frontend/playwright/ui/specs/profile-menu.spec.js b/frontend/playwright/ui/specs/profile-menu.spec.js index fdb35e28e6..e86a79a826 100644 --- a/frontend/playwright/ui/specs/profile-menu.spec.js +++ b/frontend/playwright/ui/specs/profile-menu.spec.js @@ -3,11 +3,6 @@ import DashboardPage from "../pages/DashboardPage"; test.beforeEach(async ({ page }) => { await DashboardPage.init(page); - await DashboardPage.mockRPC( - page, - "get-profile", - "logged-in-user/get-profile-logged-in-no-onboarding.json", - ); }); test("Navigate to penpot changelog from profile menu", async ({ page }) => { diff --git a/frontend/playwright/ui/specs/text-editor-v2.spec.js b/frontend/playwright/ui/specs/text-editor-v2.spec.js index e32e0536a3..197a19c743 100644 --- a/frontend/playwright/ui/specs/text-editor-v2.spec.js +++ b/frontend/playwright/ui/specs/text-editor-v2.spec.js @@ -2,8 +2,6 @@ import { test, expect } from "@playwright/test"; import { Clipboard } from "../../helpers/Clipboard"; import { WasmWorkspacePage } from "../pages/WasmWorkspacePage"; -const timeToWait = 100; - test.beforeEach(async ({ page, context }) => { await Clipboard.enable(context, Clipboard.Permission.ALL); @@ -37,11 +35,13 @@ test("Create a new text shape from pasting text", async ({ page, context }) => { await workspace.setupEmptyFile(); await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json"); await workspace.goToWorkspace(); + await workspace.moveButton.click(); await Clipboard.writeText(page, textToPaste); await workspace.clickAt(190, 150); await workspace.paste("keyboard"); + await workspace.textEditor.stopEditing(); await expect(workspace.layers.getByText(textToPaste)).toBeVisible(); @@ -57,6 +57,7 @@ test("Create a new text shape from pasting text using context menu", async ({ }); await workspace.setupEmptyFile(); await workspace.goToWorkspace(); + await workspace.moveButton.click(); await Clipboard.writeText(page, textToPaste); @@ -138,7 +139,7 @@ test("Update a new text shape appending text by pasting text", async ({ await workspace.paste("keyboard"); await workspace.textEditor.stopEditing(); await workspace.waitForSelectedShapeName("Lorem ipsum dolor sit amet"); - }); +}); test.skip("Update a new text shape prepending text by pasting text", async ({ page, diff --git a/frontend/playwright/ui/specs/variants.spec.js b/frontend/playwright/ui/specs/variants.spec.js index b053a2ca7d..9cbe4ca440 100644 --- a/frontend/playwright/ui/specs/variants.spec.js +++ b/frontend/playwright/ui/specs/variants.spec.js @@ -1,5 +1,5 @@ import { test, expect } from "@playwright/test"; -import { WasmWorkspacePage } from "../pages/WasmWorkspacePage"; +import { WasmWorkspacePage } from "../pages/WasmWorkspacePage"; import { BaseWebSocketPage } from "../pages/BaseWebSocketPage"; import { Clipboard } from "../../helpers/Clipboard"; @@ -7,7 +7,7 @@ test.beforeEach(async ({ page, context }) => { await Clipboard.enable(context, Clipboard.Permission.ALL); await WasmWorkspacePage.init(page); - await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-variants.json"); + await WasmWorkspacePage.mockConfigFlags(page, ["enable-feature-variants"]); }); test.afterEach(async ({ context }) => { diff --git a/frontend/playwright/ui/specs/versions.spec.js b/frontend/playwright/ui/specs/versions.spec.js index d3d566efd1..22fbdc08be 100644 --- a/frontend/playwright/ui/specs/versions.spec.js +++ b/frontend/playwright/ui/specs/versions.spec.js @@ -1,6 +1,5 @@ import { test, expect } from "@playwright/test"; import { WasmWorkspacePage } from "../pages/WasmWorkspacePage"; -import { presenceFixture } from "../../data/workspace/ws-notifications"; test.beforeEach(async ({ page }) => { await WasmWorkspacePage.init(page); @@ -106,7 +105,7 @@ test("BUG 11006 - Fix history panel shortcut", async ({ page }) => { await workspacePage.goToWorkspace(); - await page.keyboard.press("Control+Alt+h"); + await page.keyboard.press("ControlOrMeta+Alt+h"); await expect( workspacePage.rightSidebar.getByText("There are no versions yet"), diff --git a/frontend/playwright/ui/specs/workspace-modifers.spec.js b/frontend/playwright/ui/specs/workspace-modifers.spec.js index de1f4ef23f..448b620330 100644 --- a/frontend/playwright/ui/specs/workspace-modifers.spec.js +++ b/frontend/playwright/ui/specs/workspace-modifers.spec.js @@ -55,3 +55,31 @@ test("BUG 13382 - Fix problem with flex layout", async ({ page }) => { await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("340"); }); + +test("BUG 13468 - Fix problem with flex propagation", async ({ page }) => { + const workspacePage = new WasmWorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.mockGetFile("workspace/get-file-13468.json"); + + await workspacePage.mockRPC( + "get-file-fragment?file-id=*&fragment-id=*", + "workspace/get-file-13468-fragment.json", + ); + + await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-empty.json"); + + await workspacePage.goToWorkspace({ + fileId: "3a4d7ec7-c391-8146-8007-9a05c41da6b9", + pageId: "95b23c15-79f9-81ba-8007-99d81b5290dd", + }); +0 + await workspacePage.clickToggableLayer("Parent"); + await workspacePage.clickToggableLayer("Container"); + + await workspacePage.sidebar.getByRole('button', { name: 'Show' }).click(); + + await workspacePage.clickLeafLayer("Container"); + await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("76"); +}); + + diff --git a/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js b/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js index 50ed19787f..5e3f1a5eff 100644 --- a/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js +++ b/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js @@ -3,11 +3,6 @@ import DashboardPage from "../pages/DashboardPage"; test.beforeEach(async ({ page }) => { await DashboardPage.init(page); - await DashboardPage.mockRPC( - page, - "get-profile", - "logged-in-user/get-profile-logged-in-no-onboarding.json", - ); }); test("User goes to an empty dashboard", async ({ page }) => { diff --git a/frontend/playwright/ui/visual-specs/visual-login.spec.js b/frontend/playwright/ui/visual-specs/visual-login.spec.js index b3b63a0c56..5ee1ba7a2b 100644 --- a/frontend/playwright/ui/visual-specs/visual-login.spec.js +++ b/frontend/playwright/ui/visual-specs/visual-login.spec.js @@ -2,6 +2,8 @@ import { test, expect } from "@playwright/test"; import { LoginPage } from "../pages/LoginPage"; test.beforeEach(async ({ page }) => { + await LoginPage.init(page); + const login = new LoginPage(page); await login.initWithLoggedOutUser(); await login.page.goto("/#/auth/login"); diff --git a/frontend/resources/templates/index.mustache b/frontend/resources/templates/index.mustache index 129d9c57a1..f80b7e7759 100644 --- a/frontend/resources/templates/index.mustache +++ b/frontend/resources/templates/index.mustache @@ -18,7 +18,7 @@ - + {{#isDebug}} {{/isDebug}} diff --git a/frontend/scripts/watch b/frontend/scripts/watch index 3af4e45684..78de971cba 100755 --- a/frontend/scripts/watch +++ b/frontend/scripts/watch @@ -4,4 +4,9 @@ TARGET=${1:-app}; set -ex -exec pnpm run watch:$TARGET +rm -rf node_modules; + +corepack enable; +corepack install; +pnpm install; +pnpm run watch:$TARGET diff --git a/frontend/src/app/main/data/tokenscript.cljs b/frontend/src/app/main/data/tokenscript.cljs index cfd8003430..83a09d35e8 100644 --- a/frontend/src/app/main/data/tokenscript.cljs +++ b/frontend/src/app/main/data/tokenscript.cljs @@ -79,7 +79,7 @@ Structured tokens are non-primitive token types like `typography` or `box-shadow`." [^js token-symbol] (if (instance? js/Array (.-value token-symbol)) - (mapv structured-token->penpot-map (.-value token-symbol)) + (mapv tokenscript-symbols->penpot-unit (.-value token-symbol)) (let [entries (es6-iterator-seq (.entries (.-value token-symbol)))] (into {} (map (fn [[k v :as V]] [(keyword k) (tokenscript-symbols->penpot-unit v)]) @@ -88,7 +88,7 @@ (defn tokenscript-symbols->penpot-unit [^js v] (cond (structured-token? v) (structured-token->penpot-map v) - (list-symbol? v) (tokenscript-symbols->penpot-unit (.nth 1 v)) + (list-symbol? v) (structured-token->penpot-map v) (color-symbol? v) (.-value (.to v "hex")) (rem-number-with-unit? v) (rem->px v) :else (.-value v))) diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 06981f7bb9..b93258a869 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -197,11 +197,12 @@ objects (:objects page) undo-id (or (:undo-id options) (js/Symbol)) - [all-parents changes] (-> (pcb/empty-changes it (:id page)) - (cls/generate-delete-shapes fdata page objects ids - {:ignore-touched (:allow-altering-copies options) - :undo-group (:undo-group options) - :undo-id undo-id}))] + [all-parents changes] + (-> (pcb/empty-changes it (:id page)) + (cls/generate-delete-shapes fdata page objects ids + {:ignore-touched (:allow-altering-copies options) + :undo-group (:undo-group options) + :undo-id undo-id}))] (rx/of (dwu/start-undo-transaction undo-id) (dc/detach-comment-thread ids) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 8874115443..421b29d4d0 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -10,6 +10,7 @@ [app.common.attrs :as attrs] [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.changes-builder :as pcb] [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] @@ -19,6 +20,7 @@ [app.common.types.shape.layout :as ctl] [app.common.types.text :as txt] [app.common.uuid :as uuid] + [app.main.data.changes :as dch] [app.main.data.event :as ev] [app.main.data.helpers :as dsh] [app.main.data.workspace.common :as dwc] @@ -916,11 +918,11 @@ (update-in state [:workspace-text-modifier shape-id] {:position-data position-data})))) (defn v2-update-text-shape-content - [id content & {:keys [update-name? name finalize? save-undo?] - :or {update-name? false name nil finalize? false save-undo? true}}] + [id content & {:keys [update-name? name finalize? save-undo? original-content] + :or {update-name? false name nil finalize? false save-undo? true original-content nil}}] (ptk/reify ::v2-update-text-shape-content ptk/WatchEvent - (watch [_ state _] + (watch [it state _] (if (features/active-feature? state "render-wasm/v1") (let [objects (dsh/lookup-page-objects state) shape (get objects id) @@ -950,11 +952,11 @@ new-shape)) {:save-undo? save-undo? :undo-group (when new-shape? id)}) - (let [modifiers (dwwt/resize-wasm-text-modifiers shape content) - options {:undo-group (when new-shape? id)}] - (if (and (not= :fixed (:grow-type shape)) finalize?) - (dwm/apply-wasm-modifiers modifiers options) - (dwm/set-wasm-modifiers modifiers options)))) + (when-let [modifiers (dwwt/resize-wasm-text-modifiers shape content)] + (let [options {:undo-group (when new-shape? id)}] + (if (and (not= :fixed (:grow-type shape)) finalize?) + (dwm/apply-wasm-modifiers modifiers options) + (dwm/set-wasm-modifiers modifiers options))))) (when finalize? (rx/concat @@ -970,7 +972,13 @@ {:save-undo? false})) (dws/deselect-shape id) (dwsh/delete-shapes #{id}))) - (rx/of (dwt/finish-transform)))))) + (rx/of + ;; This commit is necesary for undo and component propagation + ;; on finalization + (dch/commit-changes + (-> (pcb/empty-changes it (:current-page-id state)) + (pcb/set-text-content id content original-content))) + (dwt/finish-transform)))))) (let [objects (dsh/lookup-page-objects state) shape (get objects id) diff --git a/frontend/src/app/main/data/workspace/wasm_text.cljs b/frontend/src/app/main/data/workspace/wasm_text.cljs index ee262123cf..594a657105 100644 --- a/frontend/src/app/main/data/workspace/wasm_text.cljs +++ b/frontend/src/app/main/data/workspace/wasm_text.cljs @@ -27,27 +27,28 @@ (resize-wasm-text-modifiers shape (:content shape))) ([{:keys [id points selrect grow-type] :as shape} content] - (wasm.api/use-shape id) - (wasm.api/set-shape-text-content id content) - (wasm.api/set-shape-text-images id content) + (when id + (wasm.api/use-shape id) + (wasm.api/set-shape-text-content id content) + (wasm.api/set-shape-text-images id content) - (let [dimension (wasm.api/get-text-dimensions) - width-scale (if (#{:fixed :auto-height} grow-type) - 1.0 - (/ (:width dimension) (:width selrect))) - height-scale (if (= :fixed grow-type) - 1.0 - (/ (:height dimension) (:height selrect))) - resize-v (gpt/point width-scale height-scale) - origin (first points)] + (let [dimension (wasm.api/get-text-dimensions) + width-scale (if (#{:fixed :auto-height} grow-type) + 1.0 + (/ (:width dimension) (:width selrect))) + height-scale (if (= :fixed grow-type) + 1.0 + (/ (:height dimension) (:height selrect))) + resize-v (gpt/point width-scale height-scale) + origin (first points)] - {id - {:modifiers - (ctm/resize-modifiers - resize-v - origin - (:transform shape (gmt/matrix)) - (:transform-inverse shape (gmt/matrix)))}}))) + {id + {:modifiers + (ctm/resize-modifiers + resize-v + origin + (:transform shape (gmt/matrix)) + (:transform-inverse shape (gmt/matrix)))}})))) (defn resize-wasm-text "Resize a single text shape (auto-width/auto-height) by id. diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs index ef452cd663..fd33cdcb45 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs @@ -118,7 +118,8 @@ :update-name? update-name? :name generated-name :finalize? true - :save-undo? false)))) + :save-undo? false + :original-content original-content)))) (let [container-node (mf/ref-val container-ref)] (dom/set-style! container-node "opacity" 0))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs index dd6c661030..f5bc637567 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs @@ -540,7 +540,7 @@ [:values schema:layout-item-props-schema] [:applied-tokens [:maybe [:map-of :keyword :string]]] [:ids [::sm/vec ::sm/uuid]] - [:v-sizing {:optional true} [:maybe [:= :fill]]]]) + [:v-sizing {:optional true} [:maybe [:enum :fill :fix :auto]]]]) (mf/defc layout-size-constraints* {::mf/private true diff --git a/frontend/src/app/main/ui/workspace/viewport/outline.cljs b/frontend/src/app/main/ui/workspace/viewport/outline.cljs index a5ba8aba4f..853addb761 100644 --- a/frontend/src/app/main/ui/workspace/viewport/outline.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/outline.cljs @@ -111,11 +111,6 @@ :modifier modifier :zoom zoom}])))) -(defn- show-outline? - [shape] - (and (not (:hidden shape)) - (not (:blocked shape)))) - (mf/defc shape-outlines {::mf/wrap-props false} [props] @@ -133,8 +128,7 @@ shapes (-> #{} (into (comp (remove edition?) - (keep lookup) - (filter show-outline?)) + (keep lookup)) (set/union selected hover)) (into (comp (remove edition?) (keep lookup)) diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index 0cc4dec336..b120658a5b 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -224,8 +224,9 @@ show-gradient-handlers? (= (count selected) 1) show-grids? (contains? layout :display-guides) - show-frame-outline? (= transform :move) + show-frame-outline? (and (= transform :move) (not panning)) show-outlines? (and (nil? transform) + (not panning) (not edition) (not drawing-obj) (not (#{:comments :path :curve} drawing-tool))) @@ -561,7 +562,7 @@ :shift? @shift?}]) [:> widgets/frame-titles* - {:objects (with-meta objects-modified nil) + {:objects objects-modified :selected selected :zoom zoom :is-show-artboard-names show-artboard-names? diff --git a/frontend/src/app/plugins/api.cljs b/frontend/src/app/plugins/api.cljs index 00a59b0011..14effac100 100644 --- a/frontend/src/app/plugins/api.cljs +++ b/frontend/src/app/plugins/api.cljs @@ -27,6 +27,7 @@ [app.main.data.workspace.media :as dwm] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.wasm-text :as dwwt] + [app.main.features :as features] [app.main.fonts :refer [fetch-font-css]] [app.main.router :as rt] [app.main.store :as st] @@ -365,8 +366,10 @@ (cb/add-object shape))] (st/emit! (ch/commit-changes changes) - (se/event plugin-id "create-shape" :type :text) - (dwwt/resize-wasm-text-debounce (:id shape))) + (se/event plugin-id "create-shape" :type :text)) + + (when (features/active-feature? @st/state "render-wasm/v1") + (st/emit! (dwwt/resize-wasm-text-debounce (:id shape)))) (shape/shape-proxy plugin-id (:id shape))))) diff --git a/frontend/src/app/plugins/register.cljs b/frontend/src/app/plugins/register.cljs index 87456dbc3f..3ea013347c 100644 --- a/frontend/src/app/plugins/register.cljs +++ b/frontend/src/app/plugins/register.cljs @@ -62,7 +62,7 @@ origin (if (= vers 1) (-> plugin-url - (assoc :path "/") + (assoc :path "") (str)) (-> plugin-url (u/join ".") diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 35e0e78721..2265ef4de7 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -39,6 +39,7 @@ pub use images::*; const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 3; const MAX_BLOCKING_TIME_MS: i32 = 32; const NODE_BATCH_THRESHOLD: i32 = 3; +const BLUR_DOWNSCALE_THRESHOLD: f32 = 8.0; type ClipStack = Vec<(Rect, Option, Matrix)>; @@ -641,6 +642,7 @@ impl RenderState { apply_to_current_surface: bool, offset: Option<(f32, f32)>, parent_shadows: Option>, + spread: Option, ) { let surface_ids = fills_surface_id as u32 | strokes_surface_id as u32 @@ -699,7 +701,14 @@ impl RenderState { canvas.translate(translation); }); - fills::render(self, shape, &shape.fills, antialias, SurfaceId::Current); + fills::render( + self, + shape, + &shape.fills, + antialias, + SurfaceId::Current, + None, + ); // Pass strokes in natural order; stroke merging handles top-most ordering internally. let visible_strokes: Vec<&Stroke> = shape.visible_strokes().collect(); @@ -709,6 +718,7 @@ impl RenderState { &visible_strokes, Some(SurfaceId::Current), antialias, + spread, ); self.surfaces.apply_mut(SurfaceId::Current as u32, |s| { @@ -787,6 +797,25 @@ impl RenderState { shape.to_mut().set_blur(None); } + // For non-text, non-SVG shapes in the normal rendering path, apply blur + // via a single save_layer on each render surface + // Clip correctness is preserved + let blur_sigma_for_layers: Option = if !fast_mode + && apply_to_current_surface + && fills_surface_id == SurfaceId::Fills + && !matches!(shape.shape_type, Type::Text(_)) + && !matches!(shape.shape_type, Type::SVGRaw(_)) + { + if let Some(blur) = shape.blur.filter(|b| !b.hidden) { + shape.to_mut().set_blur(None); + Some(blur.value) + } else { + None + } + } else { + None + }; + let center = shape.center(); let mut matrix = shape.transform; matrix.post_translate(center); @@ -1005,6 +1034,24 @@ impl RenderState { s.canvas().concat(&matrix); }); + // Wrap ALL fill/stroke/shadow rendering so a single GPU blur pass calls + let blur_filter_for_layers: Option = blur_sigma_for_layers + .and_then(|sigma| skia::image_filters::blur((sigma, sigma), None, None, None)); + if let Some(ref filter) = blur_filter_for_layers { + let mut layer_paint = skia::Paint::default(); + layer_paint.set_image_filter(filter.clone()); + let layer_rec = skia::canvas::SaveLayerRec::default().paint(&layer_paint); + self.surfaces + .canvas(fills_surface_id) + .save_layer(&layer_rec); + self.surfaces + .canvas(strokes_surface_id) + .save_layer(&layer_rec); + self.surfaces + .canvas(innershadows_surface_id) + .save_layer(&layer_rec); + } + let shape = &shape; if shape.fills.is_empty() @@ -1017,10 +1064,24 @@ impl RenderState { { if let Some(fills_to_render) = self.nested_fills.last() { let fills_to_render = fills_to_render.clone(); - fills::render(self, shape, &fills_to_render, antialias, fills_surface_id); + fills::render( + self, + shape, + &fills_to_render, + antialias, + fills_surface_id, + spread, + ); } } else { - fills::render(self, shape, &shape.fills, antialias, fills_surface_id); + fills::render( + self, + shape, + &shape.fills, + antialias, + fills_surface_id, + spread, + ); } // Skip stroke rendering for clipped frames - they are drawn in render_shape_exit @@ -1035,6 +1096,7 @@ impl RenderState { &visible_strokes, Some(strokes_surface_id), antialias, + spread, ); if !fast_mode { for stroke in &visible_strokes { @@ -1057,7 +1119,12 @@ impl RenderState { innershadows_surface_id, ); } - // bools::debug_render_bool_paths(self, shape, shapes, modifiers, structure); + + if blur_filter_for_layers.is_some() { + self.surfaces.canvas(innershadows_surface_id).restore(); + self.surfaces.canvas(strokes_surface_id).restore(); + self.surfaces.canvas(fills_surface_id).restore(); + } } }; @@ -1149,6 +1216,11 @@ impl RenderState { let _start = performance::begin_timed_log!("render_preview"); performance::begin_measure!("render_preview"); + // Enable fast_mode during preview to skip expensive effects (blur, shadows). + // Restore the previous state afterward so the final render is full quality. + let current_fast_mode = self.options.is_fast_mode(); + self.options.set_fast_mode(true); + // Skip tile rebuilding during preview - we'll do it at the end // Just rebuild tiles for touched shapes and render synchronously self.rebuild_touched_tiles(tree); @@ -1156,6 +1228,8 @@ impl RenderState { // Use the sync render path self.start_render_loop(None, tree, timestamp, true)?; + self.options.set_fast_mode(current_fast_mode); + performance::end_measure!("render_preview"); performance::end_timed_log!("render_preview", _start); @@ -1326,11 +1400,16 @@ impl RenderState { paint.set_blend_mode(element.blend_mode().into()); paint.set_alpha_f(element.opacity()); - if let Some(frame_blur) = Self::frame_clip_layer_blur(element) { - let scale = self.get_scale(); - let sigma = frame_blur.value * scale; - if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) { - paint.set_image_filter(filter); + // Skip frame-level blur in fast mode (pan/zoom) + if !self.options.is_fast_mode() { + if let Some(frame_blur) = Self::frame_clip_layer_blur(element) { + let scale = self.get_scale(); + let sigma = frame_blur.value * scale; + if let Some(filter) = + skia::image_filters::blur((sigma, sigma), None, None, None) + { + paint.set_image_filter(filter); + } } } @@ -1417,6 +1496,7 @@ impl RenderState { true, None, None, + None, ); } @@ -1517,9 +1597,7 @@ impl RenderState { Self::combine_blur_values(self.combined_layer_blur(shape.blur), extra_layer_blur); let blur_filter = combined_blur .and_then(|blur| skia::image_filters::blur((blur.value, blur.value), None, None, None)); - // Legacy path is only stable up to 1.0 zoom: the canvas is scaled and the shadow - // filter is evaluated in that scaled space, so for scale > 1 it over-inflates blur/spread. - // We also disable it when combined layer blur is present to avoid incorrect composition. + let use_low_zoom_path = scale <= 1.0 && combined_blur.is_none(); if use_low_zoom_path { @@ -1573,14 +1651,10 @@ impl RenderState { return; } - if use_low_zoom_path { - let mut shadow_paint = skia::Paint::default(); - shadow_paint.set_image_filter(drop_filter); - shadow_paint.set_blend_mode(skia::BlendMode::SrcOver); - - let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint); + // blur=0 at high zoom: draw directly on DropShadows with geometric spread (no filter). + if scale > 1.0 && shadow.blur <= 0.0 { let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows); - drop_canvas.save_layer(&layer_rec); + drop_canvas.save(); drop_canvas.scale((scale, scale)); drop_canvas.translate(translation); @@ -1595,6 +1669,7 @@ impl RenderState { false, Some(shadow.offset), None, + Some(shadow.spread), ); }); @@ -1602,20 +1677,75 @@ impl RenderState { return; } - let filter_result = - filters::render_into_filter_surface(self, bounds, |state, temp_surface| { - { - let canvas = state.surfaces.canvas(temp_surface); + // Create filter with blur only (no offset, no spread - handled geometrically) + let blur_only_filter = if transformed_shadow.blur > 0.0 { + Some(skia::image_filters::blur( + (transformed_shadow.blur, transformed_shadow.blur), + None, + None, + None, + )) + } else { + None + }; - let mut shadow_paint = skia::Paint::default(); - shadow_paint.set_image_filter(drop_filter.clone()); - shadow_paint.set_blend_mode(skia::BlendMode::SrcOver); + let mut shadow_paint = skia::Paint::default(); + if let Some(blur_filter) = blur_only_filter { + shadow_paint.set_image_filter(blur_filter); + } + shadow_paint.set_blend_mode(skia::BlendMode::SrcOver); - let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint); - canvas.save_layer(&layer_rec); - } + let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint); + + // Low zoom path: use blur filter but apply offset and spread geometrically + if use_low_zoom_path { + let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows); + drop_canvas.save_layer(&layer_rec); + drop_canvas.scale((scale, scale)); + drop_canvas.translate(translation); + + self.with_nested_blurs_suppressed(|state| { + state.render_shape( + &plain_shape, + clip_bounds, + SurfaceId::DropShadows, + SurfaceId::DropShadows, + SurfaceId::DropShadows, + SurfaceId::DropShadows, + false, + Some(shadow.offset), // Offset is geometric + None, + Some(shadow.spread), // Spread is geometric + ); + }); + + self.surfaces.canvas(SurfaceId::DropShadows).restore(); + return; + } + + // Adaptive downscale for large blur values (lossless GPU optimization). + // Bounds above were computed from the original sigma so filter surface coverage is correct. + // Maximum downscale is 1/BLUR_DOWNSCALE_THRESHOLD (i.e. 8x): beyond that the + // filter surface becomes too small and quality degrades noticeably. + const MIN_BLUR_DOWNSCALE: f32 = 1.0 / BLUR_DOWNSCALE_THRESHOLD; + let blur_downscale = if shadow.blur > BLUR_DOWNSCALE_THRESHOLD { + (BLUR_DOWNSCALE_THRESHOLD / shadow.blur).max(MIN_BLUR_DOWNSCALE) + } else { + 1.0 + }; + + // High zoom with blur: use render_into_filter_surface to ensure blur has enough space + // Apply spread geometrically to avoid dilate filter rounding issues + let filter_result = filters::render_into_filter_surface( + self, + bounds, + blur_downscale, + |state, temp_surface| { + let canvas = state.surfaces.canvas(temp_surface); + canvas.save_layer(&layer_rec); state.with_nested_blurs_suppressed(|state| { + // Apply offset and spread geometrically state.render_shape( &plain_shape, clip_bounds, @@ -1624,16 +1754,15 @@ impl RenderState { temp_surface, temp_surface, false, - Some(shadow.offset), + Some(shadow.offset), // Offset is geometric None, + Some(shadow.spread), // Spread is geometric ); }); - { - let canvas = state.surfaces.canvas(temp_surface); - canvas.restore(); - } - }); + state.surfaces.canvas(temp_surface).restore(); + }, + ); if let Some((mut surface, filter_scale)) = filter_result { let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows); @@ -1720,6 +1849,7 @@ impl RenderState { if shadow_shape.hidden { continue; } + let nested_clip_bounds = node_render_state.get_nested_shadow_clip_bounds(element, shadow); @@ -1767,6 +1897,7 @@ impl RenderState { true, None, Some(vec![new_shadow_paint.clone()]), + None, ); }); self.surfaces.canvas(SurfaceId::DropShadows).restore(); @@ -1780,7 +1911,6 @@ impl RenderState { self.surfaces .canvas(SurfaceId::DropShadows) .draw_paint(&paint); - self.surfaces.canvas(SurfaceId::DropShadows).restore(); } @@ -1979,6 +2109,7 @@ impl RenderState { true, None, None, + None, ); self.surfaces diff --git a/render-wasm/src/render/fills.rs b/render-wasm/src/render/fills.rs index 0875fdd649..f6f8a2e4ea 100644 --- a/render-wasm/src/render/fills.rs +++ b/render-wasm/src/render/fills.rs @@ -97,6 +97,7 @@ pub fn render( fills: &[Fill], antialias: bool, surface_id: SurfaceId, + spread: Option, ) { if fills.is_empty() { return; @@ -107,7 +108,7 @@ pub fn render( let has_image_fills = fills.iter().any(|f| matches!(f, Fill::Image(_))); if has_image_fills { for fill in fills.iter().rev() { - render_single_fill(render_state, shape, fill, antialias, surface_id); + render_single_fill(render_state, shape, fill, antialias, surface_id, spread); } return; } @@ -124,7 +125,7 @@ pub fn render( |state, temp_surface| { let mut filtered_paint = paint.clone(); filtered_paint.set_image_filter(image_filter.clone()); - draw_fill_to_surface(state, shape, temp_surface, &filtered_paint); + draw_fill_to_surface(state, shape, temp_surface, &filtered_paint, spread); }, ) { return; @@ -133,7 +134,7 @@ pub fn render( } } - draw_fill_to_surface(render_state, shape, surface_id, &paint); + draw_fill_to_surface(render_state, shape, surface_id, &paint, spread); } /// Draws a single paint (with a merged shader) to the appropriate surface @@ -143,18 +144,23 @@ fn draw_fill_to_surface( shape: &Shape, surface_id: SurfaceId, paint: &Paint, + spread: Option, ) { match &shape.shape_type { Type::Rect(_) | Type::Frame(_) => { - render_state.surfaces.draw_rect_to(surface_id, shape, paint); + render_state + .surfaces + .draw_rect_to(surface_id, shape, paint, spread); } Type::Circle => { render_state .surfaces - .draw_circle_to(surface_id, shape, paint); + .draw_circle_to(surface_id, shape, paint, spread); } Type::Path(_) | Type::Bool(_) => { - render_state.surfaces.draw_path_to(surface_id, shape, paint); + render_state + .surfaces + .draw_path_to(surface_id, shape, paint, spread); } Type::Group(_) => {} _ => unreachable!("This shape should not have fills"), @@ -167,6 +173,7 @@ fn render_single_fill( fill: &Fill, antialias: bool, surface_id: SurfaceId, + spread: Option, ) { let mut paint = fill.to_paint(&shape.selrect, antialias); if let Some(image_filter) = shape.image_filter(1.) { @@ -185,6 +192,7 @@ fn render_single_fill( antialias, temp_surface, &filtered_paint, + spread, ); }, ) { @@ -194,7 +202,15 @@ fn render_single_fill( } } - draw_single_fill_to_surface(render_state, shape, fill, antialias, surface_id, &paint); + draw_single_fill_to_surface( + render_state, + shape, + fill, + antialias, + surface_id, + &paint, + spread, + ); } fn draw_single_fill_to_surface( @@ -204,6 +220,7 @@ fn draw_single_fill_to_surface( antialias: bool, surface_id: SurfaceId, paint: &Paint, + spread: Option, ) { match (fill, &shape.shape_type) { (Fill::Image(image_fill), _) => { @@ -217,15 +234,19 @@ fn draw_single_fill_to_surface( ); } (_, Type::Rect(_) | Type::Frame(_)) => { - render_state.surfaces.draw_rect_to(surface_id, shape, paint); + render_state + .surfaces + .draw_rect_to(surface_id, shape, paint, spread); } (_, Type::Circle) => { render_state .surfaces - .draw_circle_to(surface_id, shape, paint); + .draw_circle_to(surface_id, shape, paint, spread); } (_, Type::Path(_)) | (_, Type::Bool(_)) => { - render_state.surfaces.draw_path_to(surface_id, shape, paint); + render_state + .surfaces + .draw_path_to(surface_id, shape, paint, spread); } (_, Type::Group(_)) => { // Groups can have fills but they propagate them to their children diff --git a/render-wasm/src/render/filters.rs b/render-wasm/src/render/filters.rs index 557f92bb75..149c598e94 100644 --- a/render-wasm/src/render/filters.rs +++ b/render-wasm/src/render/filters.rs @@ -40,7 +40,9 @@ pub fn render_with_filter_surface( where F: FnOnce(&mut RenderState, SurfaceId), { - if let Some((mut surface, scale)) = render_into_filter_surface(render_state, bounds, draw_fn) { + if let Some((mut surface, scale)) = + render_into_filter_surface(render_state, bounds, 1.0, draw_fn) + { let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface); // If we scaled down, we need to scale the source rect and adjust the destination @@ -69,9 +71,15 @@ where /// down so that everything fits; the returned `scale` tells the caller how much the /// content was reduced so it can be re-scaled on compositing. The `draw_fn` should /// render the untransformed shape (i.e. in document coordinates) onto `SurfaceId::Filter`. +/// +/// `extra_downscale` is an additional scale factor applied on top of the overflow-fit scale. +/// Use values < 1.0 to pre-downscale before applying Gaussian blur filters, which dramatically +/// reduces GPU kernel work for large blur sigmas (Gaussian blur is scale-equivariant, so the +/// caller must also reduce the sigma proportionally). Pass 1.0 for no extra downscale. pub fn render_into_filter_surface( render_state: &mut RenderState, bounds: Rect, + extra_downscale: f32, draw_fn: F, ) -> Option<(skia::Surface, f32)> where @@ -86,16 +94,28 @@ where let bounds_width = bounds.width().ceil().max(1.0) as i32; let bounds_height = bounds.height().ceil().max(1.0) as i32; + // Minimum scale floor for fit_scale alone; prevents extreme downscaling when + // the shape is much larger than the filter surface. + const MIN_FIT_SCALE: f32 = 0.1; + // Absolute minimum for the combined scale (fit × extra_downscale). Below this + // the offscreen surface would have sub-pixel dimensions and produce artifacts or + // crashes. At 0.03 a shape must be at least ~34 px wide to render as a single + // pixel, which is a safe lower bound in practice. + const MIN_COMBINED_SCALE: f32 = 0.03; + // Calculate scale factor if bounds exceed filter surface size - let scale = if bounds_width > filter_width || bounds_height > filter_height { + let fit_scale = if bounds_width > filter_width || bounds_height > filter_height { let scale_x = filter_width as f32 / bounds_width as f32; let scale_y = filter_height as f32 / bounds_height as f32; // Use the smaller scale to ensure everything fits - scale_x.min(scale_y).max(0.1) // Clamp to minimum 0.1 to avoid extreme scaling + scale_x.min(scale_y).max(MIN_FIT_SCALE) } else { 1.0 }; + // Combine overflow-fit scale with caller-requested extra downscale + let scale = (fit_scale * extra_downscale).max(MIN_COMBINED_SCALE); + { let canvas = render_state.surfaces.canvas(filter_id); canvas.clear(skia::Color::TRANSPARENT); diff --git a/render-wasm/src/render/shadows.rs b/render-wasm/src/render/shadows.rs index 9a0862cbff..4906714389 100644 --- a/render-wasm/src/render/shadows.rs +++ b/render-wasm/src/render/shadows.rs @@ -47,6 +47,7 @@ pub fn render_stroke_inner_shadows( Some(surface_id), filter.as_ref(), antialias, + None, // Inner shadows don't use spread ) } } @@ -106,15 +107,19 @@ fn render_shadow_paint( ) { match &shape.shape_type { Type::Rect(_) | Type::Frame(_) => { - render_state.surfaces.draw_rect_to(surface_id, shape, paint); + render_state + .surfaces + .draw_rect_to(surface_id, shape, paint, None); } Type::Circle => { render_state .surfaces - .draw_circle_to(surface_id, shape, paint); + .draw_circle_to(surface_id, shape, paint, None); } Type::Path(_) | Type::Bool(_) => { - render_state.surfaces.draw_path_to(surface_id, shape, paint); + render_state + .surfaces + .draw_path_to(surface_id, shape, paint, None); } _ => {} } diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index ff61502d7c..d48a41bfa9 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -526,6 +526,7 @@ pub fn render( strokes: &[&Stroke], surface_id: Option, antialias: bool, + spread: Option, ) { if strokes.is_empty() { return; @@ -540,6 +541,10 @@ pub fn render( // edges semi-transparent and revealing strokes underneath. if let Some(image_filter) = shape.image_filter(1.) { let mut content_bounds = shape.selrect; + // Expand for spread if provided + if let Some(s) = spread.filter(|&s| s > 0.0) { + content_bounds.outset((s, s)); + } let max_margin = strokes .iter() .map(|s| s.bounds_width(shape.is_open())) @@ -583,6 +588,7 @@ pub fn render( antialias, true, true, + spread, ); } @@ -595,12 +601,28 @@ pub fn render( // No blur or filter surface unavailable — draw strokes individually. for stroke in strokes.iter().rev() { - render_single(render_state, shape, stroke, surface_id, None, antialias); + render_single( + render_state, + shape, + stroke, + surface_id, + None, + antialias, + spread, + ); } return; } - render_merged(render_state, shape, strokes, surface_id, antialias, false); + render_merged( + render_state, + shape, + strokes, + surface_id, + antialias, + false, + spread, + ); } fn strokes_share_geometry(strokes: &[&Stroke]) -> bool { @@ -620,6 +642,7 @@ fn render_merged( surface_id: Option, antialias: bool, bypass_filter: bool, + spread: Option, ) { let representative = *strokes .last() @@ -635,6 +658,10 @@ fn render_merged( if !bypass_filter { if let Some(image_filter) = blur_filter.clone() { let mut content_bounds = shape.selrect; + // Expand for spread if provided + if let Some(s) = spread.filter(|&s| s > 0.0) { + content_bounds.outset((s, s)); + } let stroke_margin = representative.bounds_width(shape.is_open()); if stroke_margin > 0.0 { content_bounds.inset((-stroke_margin, -stroke_margin)); @@ -660,7 +687,15 @@ fn render_merged( canvas.save_layer(&layer_rec); }); - render_merged(state, shape, strokes, Some(temp_surface), antialias, true); + render_merged( + state, + shape, + strokes, + Some(temp_surface), + antialias, + true, + spread, + ); state.surfaces.apply_mut(temp_surface as u32, |surface| { surface.canvas().restore(); @@ -676,11 +711,19 @@ fn render_merged( // via SrcOver), matching the non-merged path where strokes[0] is drawn last (on top). let fills: Vec = strokes.iter().map(|s| s.fill.clone()).collect(); - let merged = merge_fills(&fills, shape.selrect); + // Expand selrect if spread is provided + let selrect = if let Some(s) = spread.filter(|&s| s > 0.0) { + let mut r = shape.selrect; + r.outset((s, s)); + r + } else { + shape.selrect + }; + + let merged = merge_fills(&fills, selrect); let scale = render_state.get_scale(); let target_surface = surface_id.unwrap_or(SurfaceId::Strokes); let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface); - let selrect = shape.selrect; let svg_attrs = shape.svg_attrs.as_ref(); let path_transform = shape.to_path_transform(); @@ -747,6 +790,7 @@ pub fn render_single( surface_id: Option, shadow: Option<&ImageFilter>, antialias: bool, + spread: Option, ) { render_single_internal( render_state, @@ -757,6 +801,7 @@ pub fn render_single( antialias, false, false, + spread, ); } @@ -770,10 +815,15 @@ fn render_single_internal( antialias: bool, bypass_filter: bool, skip_blur: bool, + spread: Option, ) { if !bypass_filter { if let Some(image_filter) = shape.image_filter(1.) { let mut content_bounds = shape.selrect; + // Expand for spread if provided + if let Some(s) = spread.filter(|&s| s > 0.0) { + content_bounds.outset((s, s)); + } let stroke_margin = stroke.bounds_width(shape.is_open()); if stroke_margin > 0.0 { content_bounds.inset((-stroke_margin, -stroke_margin)); @@ -799,6 +849,7 @@ fn render_single_internal( antialias, true, true, + spread, ); }, ) { @@ -867,7 +918,21 @@ fn render_single_internal( shape_type @ (Type::Path(_) | Type::Bool(_)) => { if let Some(path) = shape_type.path() { let is_open = path.is_open(); - let paint = stroke.to_stroked_paint(is_open, &selrect, svg_attrs, antialias); + let mut paint = + stroke.to_stroked_paint(is_open, &selrect, svg_attrs, antialias); + // Apply spread by increasing stroke width + if let Some(s) = spread.filter(|&s| s > 0.0) { + let current_width = paint.stroke_width(); + // Path stroke kinds are built differently: + // - Center uses the stroke width directly. + // - Inner/Outer use a doubled width plus clipping/clearing logic. + // Compensate spread so visual growth is comparable across kinds. + let spread_growth = match stroke.render_kind(is_open) { + StrokeKind::Center => s * 2.0, + StrokeKind::Inner | StrokeKind::Outer => s * 4.0, + }; + paint.set_stroke_width(current_width + spread_growth); + } draw_stroke_on_path( canvas, stroke, diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index 86a0f0422e..56d26a48c7 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -74,7 +74,7 @@ impl Surfaces { let margins = skia::ISize::new(extra_tile_dims.width / 4, extra_tile_dims.height / 4); let target = gpu_state.create_target_surface(width, height); - let filter = gpu_state.create_surface_with_dimensions("filter".to_string(), width, height); + let filter = gpu_state.create_surface_with_isize("filter".to_string(), extra_tile_dims); let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height); let current = gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims); let drop_shadows = @@ -355,24 +355,62 @@ impl Surfaces { )); } - pub fn draw_rect_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) { + pub fn draw_rect_to( + &mut self, + id: SurfaceId, + shape: &Shape, + paint: &Paint, + spread: Option, + ) { + let rect = if let Some(s) = spread.filter(|&s| s > 0.0) { + let mut r = shape.selrect; + r.outset((s, s)); + r + } else { + shape.selrect + }; if let Some(corners) = shape.shape_type.corners() { - let rrect = RRect::new_rect_radii(shape.selrect, &corners); + let rrect = RRect::new_rect_radii(rect, &corners); self.canvas_and_mark_dirty(id).draw_rrect(rrect, paint); } else { - self.canvas_and_mark_dirty(id) - .draw_rect(shape.selrect, paint); + self.canvas_and_mark_dirty(id).draw_rect(rect, paint); } } - pub fn draw_circle_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) { - self.canvas_and_mark_dirty(id) - .draw_oval(shape.selrect, paint); + pub fn draw_circle_to( + &mut self, + id: SurfaceId, + shape: &Shape, + paint: &Paint, + spread: Option, + ) { + let rect = if let Some(s) = spread.filter(|&s| s > 0.0) { + let mut r = shape.selrect; + r.outset((s, s)); + r + } else { + shape.selrect + }; + self.canvas_and_mark_dirty(id).draw_oval(rect, paint); } - pub fn draw_path_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) { + pub fn draw_path_to( + &mut self, + id: SurfaceId, + shape: &Shape, + paint: &Paint, + spread: Option, + ) { if let Some(path) = shape.get_skia_path() { - self.canvas_and_mark_dirty(id).draw_path(&path, paint); + let canvas = self.canvas_and_mark_dirty(id); + if let Some(s) = spread.filter(|&s| s > 0.0) { + // Draw path as a thick stroke to get outset (expanded) silhouette + let mut stroke_paint = paint.clone(); + stroke_paint.set_stroke_width(s * 2.0); + canvas.draw_path(&path, &stroke_paint); + } else { + canvas.draw_path(&path, paint); + } } } diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index 36cde42bf0..0f44d69ae5 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -300,20 +300,7 @@ fn propagate_reflow( Type::Frame(Frame { layout: Some(_), .. }) => { - let mut skip_reflow = false; - if shape.is_layout_horizontal_fill() || shape.is_layout_vertical_fill() { - if let Some(parent_id) = shape.parent_id { - if parent_id != Uuid::nil() && !reflown.contains(&parent_id) { - // If this is a fill layout but the parent has not been reflown yet - // we wait for the next iteration for reflow - skip_reflow = true; - } - } - } - - if !skip_reflow { - layout_reflows.insert(*id); - } + layout_reflows.insert(*id); } Type::Group(Group { masked: true }) => { let children_ids = shape.children_ids(true); diff --git a/render-wasm/watch b/render-wasm/watch index 5ead50c188..90c08c9ffd 100755 --- a/render-wasm/watch +++ b/render-wasm/watch @@ -14,7 +14,7 @@ pushd $_SCRIPT_DIR; cargo watch \ --why \ - -i "_tmp*" + -i "_tmp*" \ -x "build $CARGO_PARAMS" \ -s "./build" \ -s "echo 'DONE\n'";