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'";