Compare commits

...

18 Commits

Author SHA1 Message Date
Alejandro Alonso
b9593bb394 🐛 Fix auto width affects text selection 2026-02-24 16:23:38 +01:00
Alejandro Alonso
5eba070a2c 🎉 Update skia version 2026-02-24 09:49:12 +01:00
Andrey Antukh
9e51fa198a Merge remote-tracking branch 'origin/staging' into staging-render 2026-02-24 00:09:41 +01:00
Andrey Antukh
d176da8012 Add jfr and jcmd to the backend docker image (#8446) 2026-02-24 00:08:14 +01:00
Andrey Antukh
20862c2da3 🐛 Fix incorrect plugin icon resolution 2026-02-24 00:07:30 +01:00
Andrey Antukh
1b8afccba2 Remove usage of multipart body size config on backend 2026-02-23 14:44:44 +01:00
Yamila Moreno
dd856ecf50 ♻️ Deprecate PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE envvar 2026-02-23 13:48:01 +01:00
Elena Torró
f4e79af3cd Merge pull request #8438 from penpot/alotor-fix-flex-layout-issue
🐛 Fix problem with flex layout propagation
2026-02-23 13:09:04 +01:00
alonso.torres
3e758826fe 🐛 Fix problem with flex layout propagation 2026-02-23 12:49:27 +01:00
Aitor Moreno
2cf66c948d Merge pull request #8427 from penpot/superalex-fix-blur-0-artifacts-2
🐛 Fix blur 0 artifacts
2026-02-23 12:25:54 +01:00
Andrey Antukh
145198c148 📎 Use proper version tag on frontend index template 2026-02-23 12:17:58 +01:00
alonso.torres
eddfc4c4b2 🐛 Fix problem with createText in plugins 2026-02-23 09:35:30 +01:00
alonso.torres
e6e34af391 🐛 Show outline on hidden paths 2026-02-23 09:34:50 +01:00
Alejandro Alonso
4ee908fc89 Revert "🐛 Fix stroke artifacts"
This reverts commit bdcf448f3f.
2026-02-23 07:23:41 +01:00
Alejandro Alonso
bdcf448f3f 🐛 Fix stroke artifacts 2026-02-23 07:23:12 +01:00
Alejandro Alonso
a7ab506c5c 🐛 Fix blur 0 artifacts 2026-02-20 13:37:27 +01:00
Andrés Moya
3d41dc276e 🐛 Fix resolve tokens with tokenscript when type is font family 2026-02-20 12:41:17 +01:00
alonso.torres
cee974a906 🐛 Fix problem with tokens in plugins 2026-02-18 17:20:46 +01:00
52 changed files with 4691 additions and 337 deletions

View File

@@ -2,6 +2,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)

View File

@@ -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]}

View File

@@ -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]

View File

@@ -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)

View File

@@ -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

View File

@@ -50,6 +50,7 @@ services:
- 4400:4400
- 4401:4401
- 4402:4402
- 4403:4403
# Plugins
- 4200:4200

View File

@@ -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

View File

@@ -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

View File

@@ -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)"

View File

@@ -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;

View File

@@ -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;

View File

@@ -43,12 +43,13 @@
"clear:wasm": "cargo clean --manifest-path ../render-wasm/Cargo.toml",
"watch": "exit 0",
"watch:app": "pnpm run clear:shadow-cache && pnpm run clear:wasm && pnpm run build:wasm && concurrently --kill-others-on-fail \"pnpm run watch:app:assets\" \"pnpm run watch:app:main\" \"pnpm run watch:app:libs\"",
"watch:storybook": "pnpm run build:storybook:assets && concurrently --kill-others-on-fail \"storybook dev -p 6006 --no-open\" \"node ./scripts/watch-storybook.js\""
"watch:storybook": "pnpm run build:storybook:assets && concurrently --kill-others-on-fail \"storybook dev -p 6006 --no-open\" \"node ./scripts/watch-storybook.js\"",
"postinstall": "(cd ../plugins/libs/plugins-runtime; pnpm install; pnpm run build)"
},
"devDependencies": {
"@penpot/draft-js": "workspace:./packages/draft-js",
"@penpot/mousetrap": "workspace:./packages/mousetrap",
"@penpot/plugins-runtime": "1.4.2",
"@penpot/plugins-runtime": "link:../plugins/dist/plugins-runtime",
"@penpot/svgo": "penpot/svgo#v3.2",
"@penpot/text-editor": "workspace:./text-editor",
"@penpot/tokenscript": "workspace:./packages/tokenscript",

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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\"]]]"
}
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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();

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 140 KiB

View File

@@ -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");
});

View File

@@ -20,8 +20,8 @@ importers:
specifier: workspace:./packages/mousetrap
version: link:packages/mousetrap
'@penpot/plugins-runtime':
specifier: 1.4.2
version: 1.4.2
specifier: link:../plugins/dist/plugins-runtime
version: link:../plugins/dist/plugins-runtime
'@penpot/svgo':
specifier: penpot/svgo#v3.2
version: svgo@https://codeload.github.com/penpot/svgo/tar.gz/8c9b0e32e9cb5f106085260bd9375f3c91a5010b
@@ -581,15 +581,6 @@ packages:
'@dabh/diagnostics@2.0.8':
resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==}
'@endo/cache-map@1.1.0':
resolution: {integrity: sha512-owFGshs/97PDw9oguZqU/px8Lv1d0KjAUtDUiPwKHNXRVUE/jyettEbRoTbNJR1OaI8biMn6bHr9kVJsOh6dXw==}
'@endo/env-options@1.1.11':
resolution: {integrity: sha512-p9OnAPsdqoX4YJsE98e3NBVhIr2iW9gNZxHhAI2/Ul5TdRfoOViItzHzTqrgUVopw6XxA1u1uS6CykLMDUxarA==}
'@endo/immutable-arraybuffer@1.1.2':
resolution: {integrity: sha512-u+NaYB2aqEugQ3u7w3c5QNkPogf8q/xGgsPaqdY6pUiGWtYiTiFspKFcha6+oeZhWXWQ23rf0KrUq0kfuzqYyQ==}
'@esbuild/aix-ppc64@0.21.5':
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
engines: {node: '>=12'}
@@ -1258,12 +1249,6 @@ packages:
resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==}
engines: {node: '>= 10.0.0'}
'@penpot/plugin-types@1.4.2':
resolution: {integrity: sha512-O8wU6RSYE8bIVU7g8cSTYi32ppxs3R13dq7X3Nn9tmDaJjBOKOBpVLuoRPIp3fJC65fv8/7om0sdrtFoL5v19g==}
'@penpot/plugins-runtime@1.4.2':
resolution: {integrity: sha512-y9TDZOnb96JBW9E33dHKpmTMeAPXLtHDIZruUVjtM8hBJWZK7RCv+vAGDGxeoZJC/OB2YAHrCZG+mukePBzcuQ==}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@@ -4636,9 +4621,6 @@ packages:
resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
engines: {node: '>= 18'}
ses@1.14.0:
resolution: {integrity: sha512-T07hNgOfVRTLZGwSS50RnhqrG3foWP+rM+Q5Du4KUQyMLFI3A8YA4RKl0jjZzhihC1ZvDGrWi/JMn4vqbgr/Jg==}
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@@ -5499,9 +5481,6 @@ packages:
peerDependencies:
zod: ^3.25.0 || ^4.0.0
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
@@ -5775,12 +5754,6 @@ snapshots:
enabled: 2.0.0
kuler: 2.0.0
'@endo/cache-map@1.1.0': {}
'@endo/env-options@1.1.11': {}
'@endo/immutable-arraybuffer@1.1.2': {}
'@esbuild/aix-ppc64@0.21.5':
optional: true
@@ -6297,14 +6270,6 @@ snapshots:
'@parcel/watcher-win32-x64': 2.5.6
optional: true
'@penpot/plugin-types@1.4.2': {}
'@penpot/plugins-runtime@1.4.2':
dependencies:
'@penpot/plugin-types': 1.4.2
ses: 1.14.0
zod: 3.25.76
'@pkgjs/parseargs@0.11.0':
optional: true
@@ -10000,12 +9965,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
ses@1.14.0:
dependencies:
'@endo/cache-map': 1.1.0
'@endo/env-options': 1.1.11
'@endo/immutable-arraybuffer': 1.1.2
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4
@@ -10974,6 +10933,4 @@ snapshots:
dependencies:
zod: 4.3.6
zod@3.25.76: {}
zod@4.3.6: {}

View File

@@ -18,7 +18,7 @@
<meta name="twitter:creator" content="@penpotapp">
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
<link id="theme" href="css/main.css?version={{& version_tag}}" rel="stylesheet" type="text/css" />
<link href="css/ui.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
<link href="css/ui.css?ts={{& version_tag}}" rel="stylesheet" type="text/css" />
{{#isDebug}}
<link href="css/debug.css?version={{& version_tag}}" rel="stylesheet" type="text/css" />
{{/isDebug}}

View File

@@ -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

View File

@@ -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)))

View File

@@ -73,12 +73,12 @@
}
.grow-type-auto-width {
[data-itype="inline"],
[data-itype="span"],
[data-itype="paragraph"] {
white-space: nowrap;
}
[data-itype="inline"] {
[data-itype="span"] {
white-space-collapse: preserve;
}
}

View File

@@ -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))

View File

@@ -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)))))

View File

@@ -58,7 +58,7 @@
origin
(if (= vers 1)
(-> plugin-url
(assoc :path "/")
(assoc :path "")
(str))
(-> plugin-url
(u/join ".")

View File

@@ -1305,7 +1305,8 @@
tokens)))}
:applyToken
{:schema [:tuple
{:enumerable false
:schema [:tuple
[:fn token-proxy?]
[:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
:fn (fn [token attrs]

View File

@@ -144,14 +144,16 @@
(st/emit! (dwtl/delete-token set-id id)))
:applyToShapes
{:schema [:tuple
{:enumerable false
:schema [:tuple
[:vector [:fn shape-proxy?]]
[:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
:fn (fn [shapes attrs]
(apply-token-to-shapes file-id set-id id (map #(obj/get % "$id") shapes) attrs))}
:applyToSelected
{:schema [:tuple [:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
{:enumerable false
:schema [:tuple [:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
:fn (fn [attrs]
(let [selected (get-in @st/state [:workspace-local :selected])]
(apply-token-to-shapes file-id set-id id selected attrs)))}))
@@ -236,14 +238,16 @@
(apply array))))}
:getTokenById
{:schema [:tuple ::sm/uuid]
{:enumerable false
:schema [:tuple ::sm/uuid]
:fn (fn [token-id]
(let [token (u/locate-token file-id id token-id)]
(when (some? token)
(token-proxy plugin-id file-id id token-id))))}
:addToken
{:schema (fn [args]
{:enumerable false
:schema (fn [args]
[:tuple (-> (cfo/make-token-schema
(-> (u/locate-tokens-lib file-id) (ctob/get-tokens id))
(cto/dtcg-token-type->token-type (-> args (first) (get "type"))))
@@ -353,13 +357,15 @@
{:this true :get (fn [_])}
:addSet
{:schema [:tuple [:fn token-set-proxy?]]
{:enumerable false
:schema [:tuple [:fn token-set-proxy?]]
:fn (fn [token-set]
(let [theme (u/locate-token-theme file-id id)]
(st/emit! (dwtl/update-token-theme id (ctob/enable-set theme (obj/get token-set :name))))))}
:removeSet
{:schema [:tuple [:fn token-set-proxy?]]
{:enumerable false
:schema [:tuple [:fn token-set-proxy?]]
:fn (fn [token-set]
(let [theme (u/locate-token-theme file-id id)]
(st/emit! (dwtl/update-token-theme id (ctob/disable-set theme (obj/get token-set :name))))))}
@@ -406,7 +412,8 @@
(apply array (map #(token-set-proxy plugin-id file-id (ctob/get-id %)) sets))))}
:addTheme
{:schema (fn [attrs]
{:enumerable false
:schema (fn [attrs]
[:tuple (-> (sm/schema (cfo/make-token-theme-schema
(u/locate-tokens-lib file-id)
(or (obj/get attrs "group") "")
@@ -419,7 +426,8 @@
(token-theme-proxy plugin-id file-id (:id theme))))}
:addSet
{:schema [:tuple (-> (sm/schema (cfo/make-token-set-schema
{:enumerable false
:schema [:tuple (-> (sm/schema (cfo/make-token-set-schema
(u/locate-tokens-lib file-id)
nil))
(sm/dissoc-key :id))] ;; We don't allow plugins to set the id
@@ -431,14 +439,16 @@
(token-set-proxy plugin-id file-id (ctob/get-id set))))}
:getThemeById
{:schema [:tuple ::sm/uuid]
{:enumerable false
:schema [:tuple ::sm/uuid]
:fn (fn [theme-id]
(let [theme (u/locate-token-theme file-id theme-id)]
(when (some? theme)
(token-theme-proxy plugin-id file-id theme-id))))}
:getSetById
{:schema [:tuple ::sm/uuid]
{:enumerable false
:schema [:tuple ::sm/uuid]
:fn (fn [set-id]
(let [set (u/locate-token-set file-id set-id)]
(when (some? set)

View File

@@ -6,8 +6,8 @@
"ses": "^1.1.0",
"zod": "^3.22.4"
},
"module": "./index.mjs",
"typings": "./index.d.ts",
"module": "./dist/index.js",
"typings": "./dist/index.d.ts",
"type": "module",
"scripts": {
"build": "vite build",

86
render-wasm/Cargo.lock generated
View File

@@ -202,9 +202,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "hashbrown"
version = "0.15.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "heck"
@@ -214,9 +214,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.7.1"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown",
@@ -253,12 +253,6 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.161"
@@ -468,18 +462,27 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.210"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.210"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@@ -500,11 +503,11 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "0.6.8"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
dependencies = [
"serde",
"serde_core",
]
[[package]]
@@ -515,9 +518,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "skia-bindings"
version = "0.87.0"
version = "0.93.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "704242769235d2ffe66a2a0a3002661262fc4af08d32807c362d7b0160ee703c"
checksum = "2359f7e30c9da3f322f8ca3d4ec0abbc12a40035ce758309db0cdab07b5d4476"
dependencies = [
"bindgen",
"cc",
@@ -532,13 +535,12 @@ dependencies = [
[[package]]
name = "skia-safe"
version = "0.87.0"
version = "0.93.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f7d94f3e7537c71ad4cf132eb26e3be8c8a886ed3649c4525c089041fc312b2"
checksum = "7f9e837ea9d531c9efee8f980bfcdb7226b21db0285b0c3171d8be745829f940"
dependencies = [
"base64",
"bitflags",
"lazy_static",
"percent-encoding",
"skia-bindings",
"skia-svg-macros",
@@ -579,38 +581,43 @@ dependencies = [
[[package]]
name = "toml"
version = "0.8.19"
version = "1.0.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c"
dependencies = [
"serde",
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_edit",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
version = "1.0.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
dependencies = [
"serde",
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.22.22"
name = "toml_parser"
version = "1.0.9+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
[[package]]
name = "unicode-ident"
version = "1.0.13"
@@ -775,12 +782,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.20"
version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
dependencies = [
"memchr",
]
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
[[package]]
name = "xattr"

View File

@@ -25,7 +25,7 @@ gl = "0.14.0"
glam = "0.24.2"
indexmap = "2.7.1"
macros = { path = "macros" }
skia-safe = { version = "0.87.0", default-features = false, features = [
skia-safe = { version = "0.93.1", default-features = false, features = [
"gl",
"svg",
"textlayout",

View File

@@ -10,7 +10,7 @@ fi
export BUILD_NAME="${BUILD_NAME:-render-wasm}"
export CARGO_BUILD_TARGET=${CARGO_BUILD_TARGET:-"wasm32-unknown-emscripten"};
export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.87.0/skia-binaries-e551f334ad5cbdf43abf-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp.tar.gz"}
export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp.tar.gz"}
# 256 MB of initial heap to perform less
# initial calls to memory grow.

View File

@@ -11,7 +11,7 @@ fi
. ./_build_env
export CARGO_BUILD_TARGET=${CARGO_BUILD_TARGET:-"wasm32-unknown-emscripten"};
export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.87.0/skia-binaries-e551f334ad5cbdf43abf-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp.tar.gz"}
export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp.tar.gz"}
ALLOWED_RULES="-D static_mut_refs"

View File

@@ -356,7 +356,7 @@ impl Bounds {
}
pub fn from_rect(r: &Rect) -> Self {
let [nw, ne, se, sw] = r.to_quad();
let [nw, ne, se, sw] = r.to_quad(None);
Self::new(nw, ne, se, sw)
}

View File

@@ -477,30 +477,32 @@ pub fn debug_render_bool_paths(
paint.set_alpha_f(1.0);
paint.set_style(skia::PaintStyle::Stroke);
let mut path = skia::Path::default();
path.move_to((b.1.start.x as f32, b.1.start.y as f32));
match b.1.handles {
BezierHandles::Linear => {
path.line_to((b.1.end.x as f32, b.1.end.y as f32));
let path = {
let mut pb = skia::PathBuilder::new();
pb.move_to((b.1.start.x as f32, b.1.start.y as f32));
match b.1.handles {
BezierHandles::Linear => {
pb.line_to((b.1.end.x as f32, b.1.end.y as f32));
}
BezierHandles::Quadratic { handle } => {
pb.quad_to(
(handle.x as f32, handle.y as f32),
(b.1.end.x as f32, b.1.end.y as f32),
);
}
BezierHandles::Cubic {
handle_start,
handle_end,
} => {
pb.cubic_to(
(handle_start.x as f32, handle_start.y as f32),
(handle_end.x as f32, handle_end.y as f32),
(b.1.end.x as f32, b.1.end.y as f32),
);
}
}
BezierHandles::Quadratic { handle } => {
path.quad_to(
(handle.x as f32, handle.y as f32),
(b.1.end.x as f32, b.1.end.y as f32),
);
}
BezierHandles::Cubic {
handle_start,
handle_end,
} => {
path.cubic_to(
(handle_start.x as f32, handle_start.y as f32),
(handle_end.x as f32, handle_end.y as f32),
(b.1.end.x as f32, b.1.end.y as f32),
);
}
}
pb.detach()
};
canvas.draw_path(&path, &paint);
let mut v1 = b.1.normal(TValue::Parametric(1.0));

View File

@@ -642,6 +642,7 @@ impl RenderState {
apply_to_current_surface: bool,
offset: Option<(f32, f32)>,
parent_shadows: Option<Vec<skia_safe::Paint>>,
spread: Option<f32>,
) {
let surface_ids = fills_surface_id as u32
| strokes_surface_id as u32
@@ -700,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();
@@ -710,6 +718,7 @@ impl RenderState {
&visible_strokes,
Some(SurfaceId::Current),
antialias,
spread,
);
self.surfaces.apply_mut(SurfaceId::Current as u32, |s| {
@@ -1055,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
@@ -1073,6 +1096,7 @@ impl RenderState {
&visible_strokes,
Some(strokes_surface_id),
antialias,
spread,
);
if !fast_mode {
for stroke in &visible_strokes {
@@ -1472,6 +1496,7 @@ impl RenderState {
true,
None,
None,
None,
);
}
@@ -1626,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);
@@ -1648,6 +1669,53 @@ impl RenderState {
false,
Some(shadow.offset),
None,
Some(shadow.spread),
);
});
self.surfaces.canvas(SurfaceId::DropShadows).restore();
return;
}
// 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();
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);
// 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
);
});
@@ -1657,7 +1725,7 @@ impl RenderState {
// 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. 8×): beyond that the
// 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 {
@@ -1666,23 +1734,18 @@ impl RenderState {
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);
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);
canvas.save_layer(&layer_rec);
}
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,
@@ -1691,15 +1754,13 @@ 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();
},
);
@@ -1836,6 +1897,7 @@ impl RenderState {
true,
None,
Some(vec![new_shadow_paint.clone()]),
None,
);
});
self.surfaces.canvas(SurfaceId::DropShadows).restore();
@@ -2047,6 +2109,7 @@ impl RenderState {
true,
None,
None,
None,
);
self.surfaces

View File

@@ -51,15 +51,18 @@ fn draw_image_fill(
canvas.clip_rect(container, skia::ClipOp::Intersect, antialias);
}
Type::Circle => {
let mut oval_path = skia::Path::new();
oval_path.add_oval(container, None);
let oval_path = {
let mut pb = skia::PathBuilder::new();
pb.add_oval(container, None, None);
pb.detach()
};
canvas.clip_path(&oval_path, skia::ClipOp::Intersect, antialias);
}
shape_type @ (Type::Path(_) | Type::Bool(_)) => {
if let Some(path) = shape_type.path() {
if let Some(path_transform) = path_transform {
canvas.clip_path(
path.to_skia_path().transform(&path_transform),
&path.to_skia_path().make_transform(&path_transform),
skia::ClipOp::Intersect,
antialias,
);
@@ -97,6 +100,7 @@ pub fn render(
fills: &[Fill],
antialias: bool,
surface_id: SurfaceId,
spread: Option<f32>,
) {
if fills.is_empty() {
return;
@@ -107,7 +111,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 +128,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 +137,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 +147,23 @@ fn draw_fill_to_surface(
shape: &Shape,
surface_id: SurfaceId,
paint: &Paint,
spread: Option<f32>,
) {
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 +176,7 @@ fn render_single_fill(
fill: &Fill,
antialias: bool,
surface_id: SurfaceId,
spread: Option<f32>,
) {
let mut paint = fill.to_paint(&shape.selrect, antialias);
if let Some(image_filter) = shape.image_filter(1.) {
@@ -185,6 +195,7 @@ fn render_single_fill(
antialias,
temp_surface,
&filtered_paint,
spread,
);
},
) {
@@ -194,7 +205,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 +223,7 @@ fn draw_single_fill_to_surface(
antialias: bool,
surface_id: SurfaceId,
paint: &Paint,
spread: Option<f32>,
) {
match (fill, &shape.shape_type) {
(Fill::Image(image_fill), _) => {
@@ -217,15 +237,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

View File

@@ -24,7 +24,11 @@ pub fn render_overlay(zoom: f32, canvas: &skia::Canvas, shape: &Shape, shapes: S
cell.anchor + hv + vv,
cell.anchor + vv,
];
let polygon = skia::Path::polygon(&points, true, None, None);
let polygon = {
let mut pb = skia::PathBuilder::new();
pb.add_polygon(&points, true);
pb.detach()
};
canvas.draw_path(&polygon, &paint);
}
}

View File

@@ -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);
}
_ => {}
}

View File

@@ -83,8 +83,11 @@ fn draw_stroke_on_circle(
if let Some(clip_op) = stroke.clip_op() {
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
canvas.save_layer(&layer_rec);
let mut clip_path = skia::Path::new();
clip_path.add_oval(rect, None);
let clip_path = {
let mut pb = skia::PathBuilder::new();
pb.add_oval(rect, None, None);
pb.detach()
};
canvas.clip_path(&clip_path, clip_op, antialias);
canvas.draw_oval(stroke_rect, &paint);
canvas.restore();
@@ -153,8 +156,9 @@ fn draw_stroke_on_path(
blur: Option<&ImageFilter>,
antialias: bool,
) {
let mut skia_path = path.to_skia_path();
skia_path.transform(path_transform.unwrap_or(&Matrix::default()));
let skia_path = path
.to_skia_path()
.make_transform(path_transform.unwrap_or(&Matrix::default()));
let is_open = path.is_open();
@@ -174,15 +178,7 @@ fn draw_stroke_on_path(
}
}
handle_stroke_caps(
&mut skia_path,
stroke,
canvas,
is_open,
paint,
blur,
antialias,
);
handle_stroke_caps(&skia_path, stroke, canvas, is_open, paint, blur, antialias);
}
fn handle_stroke_cap(
@@ -224,7 +220,7 @@ fn handle_stroke_cap(
#[allow(clippy::too_many_arguments)]
fn handle_stroke_caps(
path: &mut skia::Path,
path: &skia::Path,
stroke: &Stroke,
canvas: &skia::Canvas,
is_open: bool,
@@ -232,8 +228,7 @@ fn handle_stroke_caps(
blur: Option<&ImageFilter>,
_antialias: bool,
) {
let mut points = vec![Point::default(); path.count_points()];
path.get_points(&mut points);
let mut points = path.points().to_vec();
// Curves can have duplicated points, so let's remove consecutive duplicated points
points.dedup();
let c_points = points.len();
@@ -304,13 +299,16 @@ fn draw_square_cap(
let mut transformed_points = points;
matrix.map_points(&mut transformed_points, &points);
let mut path = skia::Path::new();
path.move_to(Point::new(center.x, center.y));
path.move_to(transformed_points[0]);
path.line_to(transformed_points[1]);
path.line_to(transformed_points[2]);
path.line_to(transformed_points[3]);
path.close();
let path = {
let mut pb = skia::PathBuilder::new();
pb.move_to(Point::new(center.x, center.y));
pb.move_to(transformed_points[0]);
pb.line_to(transformed_points[1]);
pb.line_to(transformed_points[2]);
pb.line_to(transformed_points[3]);
pb.close();
pb.detach()
};
canvas.draw_path(&path, paint);
}
@@ -338,13 +336,15 @@ fn draw_arrow_cap(
let mut transformed_points = points;
matrix.map_points(&mut transformed_points, &points);
let mut path = skia::Path::new();
path.move_to(transformed_points[1]);
path.line_to(transformed_points[0]);
path.line_to(transformed_points[2]);
path.move_to(Point::new(center.x, center.y));
path.line_to(transformed_points[0]);
let path = {
let mut pb = skia::PathBuilder::new();
pb.move_to(transformed_points[1]);
pb.line_to(transformed_points[0]);
pb.line_to(transformed_points[2]);
pb.move_to(Point::new(center.x, center.y));
pb.line_to(transformed_points[0]);
pb.detach()
};
canvas.draw_path(&path, paint);
}
@@ -372,12 +372,14 @@ fn draw_triangle_cap(
let mut transformed_points = points;
matrix.map_points(&mut transformed_points, &points);
let mut path = skia::Path::new();
path.move_to(transformed_points[0]);
path.line_to(transformed_points[1]);
path.line_to(transformed_points[2]);
path.close();
let path = {
let mut pb = skia::PathBuilder::new();
pb.move_to(transformed_points[0]);
pb.line_to(transformed_points[1]);
pb.line_to(transformed_points[2]);
pb.close();
pb.detach()
};
canvas.draw_path(&path, paint);
}
@@ -441,8 +443,7 @@ fn draw_image_stroke_in_container(
shape_type @ (Type::Path(_) | Type::Bool(_)) => {
if let Some(p) = shape_type.path() {
canvas.save();
let mut path = p.to_skia_path();
path.transform(&path_transform.unwrap());
let path = p.to_skia_path().make_transform(&path_transform.unwrap());
let stroke_kind = stroke.render_kind(p.is_open());
match stroke_kind {
StrokeKind::Inner => {
@@ -464,7 +465,7 @@ fn draw_image_stroke_in_container(
canvas.draw_path(&path, &thin_paint);
}
handle_stroke_caps(
&mut path,
&path,
stroke,
canvas,
is_open,
@@ -504,8 +505,7 @@ fn draw_image_stroke_in_container(
// Clear outer stroke for paths if necessary. When adding an outer stroke we need to empty the stroke added too in the inner area.
if let Type::Path(p) = &shape.shape_type {
if stroke.render_kind(p.is_open()) == StrokeKind::Outer {
let mut path = p.to_skia_path();
path.transform(&path_transform.unwrap());
let path = p.to_skia_path().make_transform(&path_transform.unwrap());
let mut clear_paint = skia::Paint::default();
clear_paint.set_blend_mode(skia::BlendMode::Clear);
clear_paint.set_anti_alias(antialias);
@@ -526,6 +526,7 @@ pub fn render(
strokes: &[&Stroke],
surface_id: Option<SurfaceId>,
antialias: bool,
spread: Option<f32>,
) {
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<SurfaceId>,
antialias: bool,
bypass_filter: bool,
spread: Option<f32>,
) {
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<Fill> = 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<SurfaceId>,
shadow: Option<&ImageFilter>,
antialias: bool,
spread: Option<f32>,
) {
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<f32>,
) {
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,

View File

@@ -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<f32>,
) {
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<f32>,
) {
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<f32>,
) {
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);
}
}
}
@@ -419,9 +457,9 @@ impl Surfaces {
);
let snapshot = self.current.image_snapshot();
let mut direct_context = self.current.direct_context();
let props = skia::image::RequiredProperties::default();
let tile_image_opt = snapshot
.make_subset(direct_context.as_mut(), rect)
.make_subset(None, rect, props)
.or_else(|| self.current.image_snapshot_with_bounds(rect));
if let Some(tile_image) = tile_image_opt {

View File

@@ -1336,7 +1336,7 @@ impl Shape {
if let Some(path) = self.shape_type.path() {
let mut skia_path = path.to_skia_path();
if let Some(path_transform) = self.to_path_transform() {
skia_path.transform(&path_transform);
skia_path = skia_path.make_transform(&path_transform);
}
if let Some(svg_attrs) = &self.svg_attrs {
if svg_attrs.fill_rule == FillRule::Evenodd {

View File

@@ -51,10 +51,10 @@ impl Gradient {
rect.left + self.end.0 * rect.width(),
rect.top + self.end.1 * rect.height(),
);
skia::shader::Shader::linear_gradient(
skia::gradient_shader::linear(
(start, end),
self.colors.as_slice(),
self.offsets.as_slice(),
Some(self.offsets.as_slice()),
skia::TileMode::Clamp,
None,
None,
@@ -83,11 +83,11 @@ impl Gradient {
transform.pre_scale((self.width * rect.width() / rect.height(), 1.), None);
transform.pre_translate((-center.x, -center.y));
skia::shader::Shader::radial_gradient(
skia::gradient_shader::radial(
center,
distance,
self.colors.as_slice(),
self.offsets.as_slice(),
Some(self.offsets.as_slice()),
skia::TileMode::Clamp,
None,
Some(&transform),

View File

@@ -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);

View File

@@ -29,40 +29,28 @@ impl Default for Path {
}
}
fn to_verb(v: u8) -> skia::path::Verb {
match v {
0 => skia::path::Verb::Move,
1 => skia::path::Verb::Line,
2 => skia::path::Verb::Quad,
3 => skia::path::Verb::Conic,
4 => skia::path::Verb::Cubic,
5 => skia::path::Verb::Close,
_ => skia::path::Verb::Done,
}
}
impl Path {
pub fn new(segments: Vec<Segment>) -> Self {
let mut skia_path = skia::Path::new();
let mut pb = skia::PathBuilder::new();
let mut start = None;
for segment in segments.iter() {
let destination = match *segment {
Segment::MoveTo(xy) => {
start = Some(xy);
skia_path.move_to(xy);
pb.move_to(xy);
None
}
Segment::LineTo(xy) => {
skia_path.line_to(xy);
pb.line_to(xy);
Some(xy)
}
Segment::CurveTo((c1, c2, xy)) => {
skia_path.cubic_to(c1, c2, xy);
pb.cubic_to(c1, c2, xy);
Some(xy)
}
Segment::Close => {
skia_path.close();
pb.close();
None
}
};
@@ -71,11 +59,12 @@ impl Path {
if math::is_close_to(destination.0, start.0)
&& math::is_close_to(destination.1, start.1)
{
skia_path.close();
pb.close();
}
}
}
let skia_path = pb.detach();
let open = subpaths::is_open_path(&segments);
Self {
@@ -86,38 +75,31 @@ impl Path {
}
pub fn from_skia_path(path: skia::Path) -> Self {
let nv = path.count_verbs();
let mut verbs = vec![0; nv];
path.get_verbs(&mut verbs);
let np = path.count_points();
let mut points = Vec::with_capacity(np);
points.resize(np, skia::Point::default());
path.get_points(&mut points);
let verbs = path.verbs();
let points = path.points();
let mut segments = Vec::new();
let mut current_point = 0;
for verb in verbs {
let verb = to_verb(verb);
match verb {
skia::path::Verb::Move => {
skia::PathVerb::Move => {
let p = points[current_point];
segments.push(Segment::MoveTo((p.x, p.y)));
current_point += 1;
}
skia::path::Verb::Line => {
skia::PathVerb::Line => {
let p = points[current_point];
segments.push(Segment::LineTo((p.x, p.y)));
current_point += 1;
}
skia::path::Verb::Quad => {
skia::PathVerb::Quad => {
let p1 = points[current_point];
let p2 = points[current_point + 1];
segments.push(Segment::CurveTo(((p1.x, p1.y), (p1.x, p1.y), (p2.x, p2.y))));
current_point += 2;
}
skia::path::Verb::Conic => {
skia::PathVerb::Conic => {
// TODO: There is no way currently to access the conic weight
// to transform this correctly
let p1 = points[current_point];
@@ -125,17 +107,14 @@ impl Path {
segments.push(Segment::CurveTo(((p1.x, p1.y), (p1.x, p1.y), (p2.x, p2.y))));
current_point += 2;
}
skia::path::Verb::Cubic => {
skia::PathVerb::Cubic => {
let p1 = points[current_point];
let p2 = points[current_point + 1];
let p3 = points[current_point + 2];
segments.push(Segment::CurveTo(((p1.x, p1.y), (p2.x, p2.y), (p3.x, p3.y))));
current_point += 3;
}
skia::path::Verb::Close => {
segments.push(Segment::Close);
}
skia::path::Verb::Done => {
skia::PathVerb::Close => {
segments.push(Segment::Close);
}
}
@@ -184,7 +163,7 @@ impl Path {
_ => {}
});
self.skia_path.transform(mtx);
self.skia_path = self.skia_path.make_transform(mtx);
}
pub fn segments(&self) -> &Vec<Segment> {

View File

@@ -225,13 +225,16 @@ impl Stroke {
if self.style != StrokeStyle::Solid {
let path_effect = match self.style {
StrokeStyle::Dotted => {
let mut circle_path = skia::Path::new();
let width = match self.kind {
StrokeKind::Inner => self.width,
StrokeKind::Center => self.width / 2.0,
StrokeKind::Outer => self.width,
};
circle_path.add_circle((0.0, 0.0), width, None);
let circle_path = {
let mut pb = skia::PathBuilder::new();
pb.add_circle((0.0, 0.0), width, None);
pb.detach()
};
let advance = self.width + 5.0;
skia::PathEffect::path_1d(
&circle_path,

View File

@@ -101,7 +101,6 @@ impl TextPaths {
if let Some((text_blob_path, text_blob_bounds)) =
Self::get_text_blob_path(span_text, font, blob_offset_x, blob_offset_y)
{
let mut text_path = text_blob_path.clone();
let text_width = font.measure_text(span_text, None).0;
let decoration = style_metric.text_style.decoration();
@@ -111,16 +110,20 @@ impl TextPaths {
let blob_top = blob_offset_y;
let blob_height = text_blob_bounds.height();
if let Some(decoration_rect) = self.calculate_text_decoration_rect(
decoration.ty,
font_metrics,
blob_left,
blob_top,
text_width,
blob_height,
) {
text_path.add_rect(decoration_rect, None);
}
let text_path = {
let mut pb = skia::PathBuilder::new_path(&text_blob_path);
if let Some(decoration_rect) = self.calculate_text_decoration_rect(
decoration.ty,
font_metrics,
blob_left,
blob_top,
text_width,
blob_height,
) {
pb.add_rect(decoration_rect, None, None);
}
pb.detach()
};
let mut paint = style_metric.text_style.foreground();
paint.set_anti_alias(antialias);

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -x
export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.87.0/skia-binaries-e551f334ad5cbdf43abf-x86_64-unknown-linux-gnu-gl-svg-textlayout-binary-cache-webp.tar.gz"}
export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-x86_64-unknown-linux-gnu-gl-svg-textlayout-binary-cache-webp.tar.gz"}
export CARGO_BUILD_TARGET=${CARGO_BUILD_TARGET:-"x86_64-unknown-linux-gnu"};
_SCRIPT_DIR=$(dirname $0);

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
_SCRIPT_DIR=$(dirname $0);
export SKIA_BINARIES_URL="https://github.com/penpot/skia-binaries/releases/download/0.87.0/skia-binaries-e551f334ad5cbdf43abf-x86_64-unknown-linux-gnu-gl-svg-textlayout-binary-cache-webp.tar.gz"
export SKIA_BINARIES_URL="https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-x86_64-unknown-linux-gnu-gl-svg-textlayout-binary-cache-webp.tar.gz"
pushd $_SCRIPT_DIR;
cargo watch -x "test --bin render_wasm -- --show-output"