Compare commits

..

7 Commits

Author SHA1 Message Date
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
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
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
85 changed files with 878 additions and 12874 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

@@ -605,31 +605,31 @@
add-undo-change-shape
(fn [change-set id]
(let [shape (get objects id)]
(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 []))}))))
(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 []))})))
add-undo-change-parent
(fn [change-set id]
(let [shape (get objects id)
prev-sibling (cfh/get-prev-sibling objects (:id shape))]
(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}))))]
(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})))]
(-> changes
(update :redo-changes #(reduce add-redo-change % ids))
@@ -1150,24 +1150,3 @@
[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))))

View File

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

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

@@ -40,9 +40,8 @@
"watch:app:libs": "node ./scripts/build-libs.js --watch",
"watch:app:main": "clojure -M:dev:shadow-cljs watch main worker storybook",
"clear:shadow-cache": "rm -rf .shadow-cljs",
"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:app": "pnpm run clear:shadow-cache && 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\""
},
"devDependencies": {

View File

@@ -0,0 +1,26 @@
[
{
"~: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
}
]

View File

@@ -0,0 +1,26 @@
{
"~: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
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +0,0 @@
{
"~:file-id": "~u52c4e771-3853-8190-8007-9506c70e8100",
"~:id": "~u4173a29d-4020-80b4-8007-96527ba9d8af",
"~:created-at": "~m1771342236330",
"~:modified-at": "~m1771342236330",
"~:type": "fragment",
"~:backend": "db",
"~:data": {
"~:id": "~uecb0cfd0-0f0b-81f7-8007-950628f9665b",
"~:name": "Page 1",
"~:objects": {
"~#penpot/objects-map/v2": {
"~ude9c6736-45ce-80a1-8007-950643da554d": "[\"~#shape\",[\"^ \",\"~:y\",383.99998939037323,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"R1\",\"~:width\",74,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",831.999992787838,\"~:y\",383.99998939037323]],[\"^<\",[\"^ \",\"~:x\",905.999992787838,\"~:y\",383.99998939037323]],[\"^<\",[\"^ \",\"~:x\",905.999992787838,\"~:y\",429.99998664855957]],[\"^<\",[\"^ \",\"~:x\",831.999992787838,\"~:y\",429.99998664855957]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~uecb0cfd0-0f0b-81f7-8007-950628f9665b\",\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~ude9c6736-45ce-80a1-8007-950643da554d\",\"~:parent-id\",\"~ude9c6736-45ce-80a1-8007-95063a202e52\",\"~:frame-id\",\"~ude9c6736-45ce-80a1-8007-95063a202e52\",\"~:strokes\",[],\"~:x\",831.999992787838,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",831.999992787838,\"~:y\",383.99998939037323,\"^8\",74,\"~:height\",45.99999725818634,\"~:x1\",831.999992787838,\"~:y1\",383.99998939037323,\"~:x2\",905.999992787838,\"~:y2\",429.99998664855957]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^K\",45.99999725818634,\"~:flip-y\",null]]",
"~ude9c6736-45ce-80a1-8007-95065b4599ea": "[\"~#shape\",[\"^ \",\"~:y\",439.999969256975,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"R2\",\"~:width\",300.00007388362076,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",832.0000015918991,\"~:y\",439.99996925697496]],[\"^<\",[\"^ \",\"~:x\",1132.00007547552,\"~:y\",439.99996925697496]],[\"^<\",[\"^ \",\"~:x\",1132.00007547552,\"~:y\",589.9999658711585]],[\"^<\",[\"^ \",\"~:x\",832.0000015918991,\"~:y\",589.9999658711585]]],\"~: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]],\"~:page-id\",\"~uecb0cfd0-0f0b-81f7-8007-950628f9665b\",\"~:layout-item-v-sizing\",\"^?\",\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~ude9c6736-45ce-80a1-8007-95065b4599ea\",\"~:parent-id\",\"~ude9c6736-45ce-80a1-8007-95063a202e52\",\"~:frame-id\",\"~ude9c6736-45ce-80a1-8007-95063a202e52\",\"~:strokes\",[],\"~:x\",832.0000015918993,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",832.0000015918993,\"~:y\",439.999969256975,\"^8\",300.00007388362076,\"~:height\",149.9999966141835,\"~:x1\",832.0000015918993,\"~:y1\",439.999969256975,\"~:x2\",1132.0000754755201,\"~:y2\",589.9999658711586]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^N\",149.9999966141835,\"~:flip-y\",null]]",
"~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\",\"~uecb0cfd0-0f0b-81f7-8007-950628f9665b\",\"~: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\",[\"~ude9c6736-45ce-80a1-8007-95062aa41d6b\"]]]",
"~ude9c6736-45ce-80a1-8007-95062aa41d6b": "[\"~#shape\",[\"^ \",\"~:y\",342.0000208153068,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",0,\"~:p2\",0,\"~:p3\",0,\"~:p4\",0],\"~: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\",\"~:grow-type\",\"~:fixed\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",false,\"~:name\",\"A\",\"~:layout-align-items\",\"~:start\",\"~:width\",392.9999889135361,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",787.9999763965607,\"~:y\",342.0000208153068]],[\"^L\",[\"^ \",\"~:x\",1180.9999653100967,\"~:y\",342.0000208153068]],[\"^L\",[\"^ \",\"~:x\",1180.9999653100967,\"~:y\",704.000018450968]],[\"^L\",[\"^ \",\"~:x\",787.9999763965607,\"~:y\",704.000018450968]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",0,\"~:column-gap\",0],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~uecb0cfd0-0f0b-81f7-8007-950628f9665b\",\"~:layout-item-v-sizing\",\"~:fix\",\"~:r3\",0,\"~:layout-justify-content\",\"^E\",\"~:r1\",0,\"~:id\",\"~ude9c6736-45ce-80a1-8007-95062aa41d6b\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:layout-flex-dir\",\"~:column\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",787.9999763965607,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",787.9999763965607,\"~:y\",342.0000208153068,\"^F\",392.9999889135361,\"~:height\",361.9999976356612,\"~:x1\",787.9999763965607,\"~:y1\",342.0000208153068,\"~:x2\",1180.9999653100967,\"~:y2\",704.000018450968]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^19\",361.9999976356612,\"~:flip-y\",null,\"~:shapes\",[\"~ude9c6736-45ce-80a1-8007-95062fafbb88\"]]]",
"~ude9c6736-45ce-80a1-8007-95063a202e52": "[\"~#shape\",[\"^ \",\"~:y\",374.00001145409465,\"~: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\",\"~:grow-type\",\"~:fixed\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"C\",\"~:layout-align-items\",\"~:start\",\"~:width\",320.00009969013945,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",821.9999915631233,\"~:y\",374.0000114540946]],[\"^L\",[\"^ \",\"~:x\",1142.0000912532628,\"~:y\",374.0000114540946]],[\"^L\",[\"^ \",\"~:x\",1142.0000912532628,\"~:y\",600.0000518384436]],[\"^L\",[\"^ \",\"~:x\",821.9999915631233,\"~:y\",600.0000518384436]]],\"~:r2\",0,\"~:layout-item-h-sizing\",\"~:auto\",\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",10,\"~:column-gap\",0],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~uecb0cfd0-0f0b-81f7-8007-950628f9665b\",\"~:layout-item-v-sizing\",\"^O\",\"~:r3\",0,\"~:layout-justify-content\",\"^E\",\"~:r1\",0,\"~:id\",\"~ude9c6736-45ce-80a1-8007-95063a202e52\",\"~:parent-id\",\"~ude9c6736-45ce-80a1-8007-95062fafbb88\",\"~:layout-flex-dir\",\"~:column\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~ude9c6736-45ce-80a1-8007-95062fafbb88\",\"~:strokes\",[[\"^ \",\"~:stroke-alignment\",\"~:inner\",\"~:stroke-style\",\"~:solid\",\"~:stroke-color\",\"#000000\",\"~:stroke-opacity\",1,\"~:stroke-width\",1]],\"~:x\",821.9999915631233,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",821.9999915631233,\"~:y\",374.00001145409465,\"^F\",320.00009969013945,\"~:height\",226.000040384349,\"~:x1\",821.9999915631233,\"~:y1\",374.00001145409465,\"~:x2\",1142.0000912532628,\"~:y2\",600.0000518384436]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^1A\",226.000040384349,\"~:flip-y\",null,\"~:shapes\",[\"~ude9c6736-45ce-80a1-8007-95065b4599ea\",\"~ude9c6736-45ce-80a1-8007-950643da554d\"]]]",
"~ude9c6736-45ce-80a1-8007-95062fafbb88": "[\"~#shape\",[\"^ \",\"~:y\",342.0000083402533,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",31.999953508377075,\"~:p2\",34.000026052944804,\"~:p3\",31.999953508377075,\"~:p4\",34.000026052944804],\"~: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\",\"~:grow-type\",\"~:fixed\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"B\",\"~:layout-align-items\",\"~:start\",\"~:width\",392.9999979687366,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",788.0000295450811,\"~:y\",342.00000834025326]],[\"^L\",[\"^ \",\"~:x\",1181.0000275138177,\"~:y\",342.00000834025326]],[\"^L\",[\"^ \",\"~:x\",1181.0000275138177,\"~:y\",631.9999659712]],[\"^L\",[\"^ \",\"~:x\",788.0000295450811,\"~:y\",631.9999659712]]],\"~:r2\",0,\"~:layout-item-h-sizing\",\"~:fill\",\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",0,\"~:column-gap\",0],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~uecb0cfd0-0f0b-81f7-8007-950628f9665b\",\"~:layout-item-v-sizing\",\"~:auto\",\"~:r3\",0,\"~:layout-justify-content\",\"^E\",\"~:r1\",0,\"~:id\",\"~ude9c6736-45ce-80a1-8007-95062fafbb88\",\"~:parent-id\",\"~ude9c6736-45ce-80a1-8007-95062aa41d6b\",\"~:layout-flex-dir\",\"~:column\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~ude9c6736-45ce-80a1-8007-95062aa41d6b\",\"~:strokes\",[[\"^ \",\"~:stroke-alignment\",\"~:inner\",\"~:stroke-style\",\"~:solid\",\"~:stroke-color\",\"#000000\",\"~:stroke-opacity\",1,\"~:stroke-width\",1]],\"~:x\",788.0000295450811,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",788.0000295450811,\"~:y\",342.0000083402533,\"^F\",392.9999979687366,\"~:height\",289.99995763094677,\"~:x1\",788.0000295450811,\"~:y1\",342.0000083402533,\"~:x2\",1181.0000275138177,\"~:y2\",631.9999659712]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^1B\",289.99995763094677,\"~:flip-y\",null,\"~:shapes\",[\"~ude9c6736-45ce-80a1-8007-95063a202e52\"]]]"
}
}
}
}

View File

@@ -1,131 +0,0 @@
{
"~: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": "bug flex",
"~:revn": 33,
"~:modified-at": "~m1771342236324",
"~:vern": 0,
"~:id": "~u52c4e771-3853-8190-8007-9506c70e8100",
"~: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": "~m1771255281717",
"~:backend": "legacy-db",
"~:data": {
"~:pages": [
"~uecb0cfd0-0f0b-81f7-8007-950628f9665b"
],
"~:pages-index": {
"~uecb0cfd0-0f0b-81f7-8007-950628f9665b": {
"~#penpot/pointer": [
"~u4173a29d-4020-80b4-8007-96527ba9d8af",
{
"~:created-at": "~m1771342236327"
}
]
}
},
"~:id": "~u52c4e771-3853-8190-8007-9506c70e8100",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@@ -1,136 +0,0 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"variants/v1",
"layout/grid",
"styles/v2",
"fdata/objects-map",
"render-wasm/v1",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~u99e49e93-362f-80ef-8007-3450ea52c9a4",
"~: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": "BUG 13385",
"~:revn": 3,
"~:modified-at": "~m1771254407745",
"~:vern": 1173241426,
"~:id": "~u3ea49ce0-9d99-8197-8007-950361d24e43",
"~: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": "~ucd8f7672-e5d1-810f-8007-87e124eda82a",
"~:created-at": "~m1771254391625",
"~:backend": "legacy-db",
"~:data": {
"~:pages": [
"~u3ea49ce0-9d99-8197-8007-950361d24e44"
],
"~:pages-index": {
"~u3ea49ce0-9d99-8197-8007-950361d24e44": {
"~: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]],\"~: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,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~ue0e81bed-4dc2-805c-8007-95036a4a3131\",\"~ue0e81bed-4dc2-805c-8007-95036c27428b\"]]]",
"~ue0e81bed-4dc2-805c-8007-95036a4a3131": "[\"~#shape\",[\"^ \",\"~:y\",252,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",177,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",156,\"~:y\",252]],[\"^<\",[\"^ \",\"~:x\",333,\"~:y\",252]],[\"^<\",[\"^ \",\"~:x\",333,\"~:y\",389]],[\"^<\",[\"^ \",\"~:x\",156,\"~:y\",389]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~ue0e81bed-4dc2-805c-8007-95036a4a3131\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",156,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",156,\"~:y\",252,\"^8\",177,\"~:height\",137,\"~:x1\",156,\"~:y1\",252,\"~:x2\",333,\"~:y2\",389]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",137,\"~:flip-y\",null]]",
"~ue0e81bed-4dc2-805c-8007-95036c27428b": "[\"~#shape\",[\"^ \",\"~:y\",250,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Ellipse\",\"~:width\",148,\"~:type\",\"~:circle\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",362,\"~:y\",250]],[\"^<\",[\"^ \",\"~:x\",510,\"~:y\",250]],[\"^<\",[\"^ \",\"~:x\",510,\"~:y\",389]],[\"^<\",[\"^ \",\"~:x\",362,\"~:y\",389]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~ue0e81bed-4dc2-805c-8007-95036c27428b\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",362,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",362,\"~:y\",250,\"^8\",148,\"~:height\",139,\"~:x1\",362,\"~:y1\",250,\"~:x2\",510,\"~:y2\",389]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^F\",139,\"~:flip-y\",null]]"
}
},
"~:id": "~u3ea49ce0-9d99-8197-8007-950361d24e44",
"~:name": "Page 1"
}
},
"~:id": "~u3ea49ce0-9d99-8197-8007-950361d24e43",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@@ -1,135 +0,0 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"variants/v1",
"layout/grid",
"styles/v2",
"fdata/objects-map",
"render-wasm/v1",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~u99e49e93-362f-80ef-8007-3450ea52c9a4",
"~: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": "BUG 13385",
"~:revn": 2,
"~:modified-at": "~m1771254464312",
"~:vern": 0,
"~:id": "~u3ea49ce0-9d99-8197-8007-950361d24e43",
"~: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": "~ucd8f7672-e5d1-810f-8007-87e124eda82a",
"~:created-at": "~m1771254391625",
"~:backend": "legacy-db",
"~:data": {
"~:pages": [
"~u3ea49ce0-9d99-8197-8007-950361d24e44"
],
"~:pages-index": {
"~u3ea49ce0-9d99-8197-8007-950361d24e44": {
"~: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]],\"~: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,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~ue0e81bed-4dc2-805c-8007-95036a4a3131\"]]]",
"~ue0e81bed-4dc2-805c-8007-95036a4a3131": "[\"~#shape\",[\"^ \",\"~:y\",252,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",177,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",156,\"~:y\",252]],[\"^<\",[\"^ \",\"~:x\",333,\"~:y\",252]],[\"^<\",[\"^ \",\"~:x\",333,\"~:y\",389]],[\"^<\",[\"^ \",\"~:x\",156,\"~:y\",389]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~ue0e81bed-4dc2-805c-8007-95036a4a3131\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",156,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",156,\"~:y\",252,\"^8\",177,\"~:height\",137,\"~:x1\",156,\"~:y1\",252,\"~:x2\",333,\"~:y2\",389]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",137,\"~:flip-y\",null]]"
}
},
"~:id": "~u3ea49ce0-9d99-8197-8007-950361d24e44",
"~:name": "Page 1"
}
},
"~:id": "~u3ea49ce0-9d99-8197-8007-950361d24e43",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@@ -1,135 +0,0 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"variants/v1",
"layout/grid",
"styles/v2",
"fdata/objects-map",
"render-wasm/v1",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~u99e49e93-362f-80ef-8007-3450ea52c9a4",
"~: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": "BUG 13415",
"~:revn": 14,
"~:modified-at": "~m1771334256704",
"~:vern": 0,
"~:id": "~u0472abff-2573-8186-8007-961793e53f45",
"~: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": "~ucd8f7672-e5d1-810f-8007-87e124eda82a",
"~:created-at": "~m1771326794644",
"~:backend": "legacy-db",
"~:data": {
"~:pages": [
"~u0472abff-2573-8186-8007-961793e53f46"
],
"~:pages-index": {
"~u0472abff-2573-8186-8007-961793e53f46": {
"~: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]],\"~: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,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~uaef184da-e9c1-80f8-8007-961cf253d534\"]]]",
"~uaef184da-e9c1-80f8-8007-961cf253d534": "[\"~#shape\",[\"^ \",\"~:y\",286,\"~:layout-grid-columns\",[[\"^ \",\"~:type\",\"~:flex\",\"~:value\",1],[\"^ \",\"^2\",\"^3\",\"^4\",1]],\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",0,\"~:p2\",0,\"~:p3\",0,\"~:p4\",0],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:layout\",\"~:grid\",\"~:hide-in-viewer\",false,\"~:name\",\"Board\",\"~:layout-align-items\",\"~:start\",\"~:width\",298,\"~:layout-grid-cells\",[\"^ \",\"~uaef184da-e9c1-80f8-8007-961cf50d67b4\",[\"^ \",\"~:justify-self\",\"~:auto\",\"~:column\",1,\"~:id\",\"~uaef184da-e9c1-80f8-8007-961cf50d67b4\",\"~:position\",\"^L\",\"~:column-span\",1,\"~:align-self\",\"^L\",\"~:row\",1,\"~:row-span\",1,\"~:shapes\",[]],\"~uaef184da-e9c1-80f8-8007-961cf50d67b5\",[\"^ \",\"^K\",\"^L\",\"^M\",2,\"^N\",\"~uaef184da-e9c1-80f8-8007-961cf50d67b5\",\"^O\",\"^L\",\"^P\",1,\"^Q\",\"^L\",\"^R\",1,\"^S\",1,\"^T\",[]],\"~uaef184da-e9c1-80f8-8007-961cf50d67b6\",[\"^ \",\"^K\",\"^L\",\"^M\",1,\"^N\",\"~uaef184da-e9c1-80f8-8007-961cf50d67b6\",\"^O\",\"^L\",\"^P\",1,\"^Q\",\"^L\",\"^R\",2,\"^S\",1,\"^T\",[]],\"~uaef184da-e9c1-80f8-8007-961cf50d67b7\",[\"^ \",\"^K\",\"^L\",\"^M\",2,\"^N\",\"~uaef184da-e9c1-80f8-8007-961cf50d67b7\",\"^O\",\"^L\",\"^P\",1,\"^Q\",\"^L\",\"^R\",2,\"^S\",1,\"^T\",[]]],\"~:layout-padding-type\",\"~:simple\",\"^2\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",322,\"~:y\",286]],[\"^10\",[\"^ \",\"~:x\",620,\"~:y\",286]],[\"^10\",[\"^ \",\"~:x\",620,\"~:y\",552]],[\"^10\",[\"^ \",\"~:x\",322,\"~:y\",552]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",0,\"~:column-gap\",0],\"~:transform-inverse\",[\"^>\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:layout-justify-content\",\"~:stretch\",\"~:r1\",0,\"^N\",\"~uaef184da-e9c1-80f8-8007-961cf253d534\",\"~:layout-justify-items\",\"^G\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:layout-align-content\",\"^19\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",322,\"~:proportion\",1,\"~:r4\",0,\"~:layout-grid-rows\",[[\"^ \",\"^2\",\"^3\",\"^4\",1],[\"^ \",\"^2\",\"^3\",\"^4\",1]],\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",322,\"~:y\",286,\"^H\",298,\"~:height\",266,\"~:x1\",322,\"~:y1\",286,\"~:x2\",620,\"~:y2\",552]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:layout-grid-dir\",\"^R\",\"~:flip-x\",null,\"^1E\",266,\"~:flip-y\",null,\"^T\",[]]]"
}
},
"~:id": "~u0472abff-2573-8186-8007-961793e53f46",
"~:name": "Page 1"
}
},
"~:id": "~u0472abff-2573-8186-8007-961793e53f45",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@@ -1,23 +0,0 @@
[
{
"~:id": "~u3ea49ce0-9d99-8197-8007-95037190405b",
"~:label": "Version 000",
"~:revn": 1,
"~:version": 67,
"~:created-at": "~m1771254407745",
"~:modified-at": "~m1771254407745",
"~:created-by": "user",
"~:profile-id": "~u99e49e93-362f-80ef-8007-3450ea5204aa"
},
{
"~:revn": 0,
"~:modified-at": "~m1771254406526",
"~:deleted-at": "~m1771340806524",
"~:created-by": "system",
"~:label": "internal/snapshot/0",
"~:id": "~u3ea49ce0-9d99-8197-8007-9503705f8b9b",
"~:profile-id": "~u99e49e93-362f-80ef-8007-3450ea5204aa",
"~:version": 67,
"~:created-at": "~m1771254406526"
}
]

View File

@@ -1,9 +0,0 @@
[
{
"~:id": "~u99e49e93-362f-80ef-8007-3450ea5204aa",
"~:email": "belen@example.com",
"~:name": "Belén Albeza",
"~:fullname": "Belén Albeza",
"~:is-active": true
}
]

View File

@@ -1,18 +0,0 @@
{
"~:revn": 14,
"~:lagged": [
{
"~:id": "~u0472abff-2573-8186-8007-96347d525f65",
"~:revn": 15,
"~:file-id": "~u0472abff-2573-8186-8007-961793e53f45",
"~:session-id": "~uf25e6d2f-d10c-8021-8007-96344433f08d",
"~:changes": [
{
"~:type": "~:del-obj",
"~:page-id": "~u0472abff-2573-8186-8007-961793e53f46",
"~:id": "~uaef184da-e9c1-80f8-8007-961cf253d534"
}
]
}
]
}

View File

@@ -1,8 +1,4 @@
export class BasePage {
static async init(page) {
await BasePage.mockConfigFlags(page, []);
}
/**
* Mocks multiple RPC calls in a single call.
*
@@ -26,7 +22,7 @@ export class BasePage {
* @param {*} options
* @returns {Promise<void>}
*/
static async mockRPC(page, path, jsonFilename = "", options = {}) {
static async mockRPC(page, path, jsonFilename, options) {
if (!page) {
throw new TypeError("Invalid page argument. Must be a Playwright page.");
}
@@ -45,7 +41,7 @@ export class BasePage {
return page.route(url, (route) =>
route.fulfill({
...interceptConfig,
path: jsonFilename ? `playwright/data/${jsonFilename}` : undefined,
path: `playwright/data/${jsonFilename}`,
}),
);
}

View File

@@ -2,8 +2,13 @@ import { MockWebSocketHelper } from "../../helpers/MockWebSocketHelper";
import BasePage from "./BasePage";
export class BaseWebSocketPage extends BasePage {
static async init(page) {
await super.init(page);
/**
* This should be called on `test.beforeEach`.
*
* @param {Page} page
* @returns
*/
static async initWebSockets(page) {
await MockWebSocketHelper.init(page);
}

View File

@@ -3,62 +3,54 @@ import { BaseWebSocketPage } from "./BaseWebSocketPage";
export class DashboardPage extends BaseWebSocketPage {
static async init(page) {
await super.init(page);
await BaseWebSocketPage.initWebSockets(page);
await super.mockConfigFlags(page, ["disable-onboarding"]);
await super.mockRPC(
await BaseWebSocketPage.mockRPC(
page,
"get-teams",
"logged-in-user/get-teams-default.json",
);
await super.mockRPC(
await BaseWebSocketPage.mockRPC(
page,
"get-font-variants?team-id=*",
"workspace/get-font-variants-empty.json",
);
await super.mockRPC(
await BaseWebSocketPage.mockRPC(
page,
"get-projects?team-id=*",
"logged-in-user/get-projects-default.json",
);
await super.mockRPC(
await BaseWebSocketPage.mockRPC(
page,
"get-team-members?team-id=*",
"logged-in-user/get-team-members-your-penpot.json",
);
await super.mockRPC(
await BaseWebSocketPage.mockRPC(
page,
"get-team-users?team-id=*",
"logged-in-user/get-team-users-single-user.json",
);
await super.mockRPC(
await BaseWebSocketPage.mockRPC(
page,
"get-unread-comment-threads?team-id=*",
"logged-in-user/get-team-users-single-user.json",
);
await super.mockRPC(
await BaseWebSocketPage.mockRPC(
page,
"get-team-recent-files?team-id=*",
"logged-in-user/get-team-recent-files-empty.json",
);
await super.mockRPC(
await BaseWebSocketPage.mockRPC(
page,
"get-profiles-for-file-comments",
"workspace/get-profile-for-file-comments.json",
);
await super.mockRPC(
await BaseWebSocketPage.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";

View File

@@ -1,10 +1,6 @@
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" });

View File

@@ -29,13 +29,8 @@ export class RegisterPage extends BasePage {
);
}
static async init(page) {
await BasePage.init(page);
}
static async initWithLoggedOutUser(page) {
await BasePage.init(page);
await BasePage.mockRPC(page, "get-profile", "get-profile-anonymous.json");
await this.mockRPC(page, "get-profile", "get-profile-anonymous.json");
}
}

View File

@@ -3,9 +3,9 @@ import { DashboardPage } from "./DashboardPage";
export class SubscriptionProfilePage extends DashboardPage {
static async init(page) {
await super.init(page);
await DashboardPage.initWebSockets(page);
await super.mockRPC(
await DashboardPage.mockRPC(
page,
"get-subscription-usage",
"subscription/get-subscription-usage.json",

View File

@@ -4,6 +4,16 @@ 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",

View File

@@ -35,8 +35,8 @@ export class WasmWorkspacePage extends WorkspacePage {
return WasmWorkspacePage.mockConfigFlags(this.page, flags);
}
constructor(page, options) {
super(page, options);
constructor(page) {
super(page);
this.canvas = page.getByTestId("canvas-wasm-shapes");
}
@@ -54,19 +54,6 @@ export class WasmWorkspacePage extends WorkspacePage {
await this.hideUI();
}
async getRenderCount() {
return this.page.evaluate(() => window.wasmRenderCount || 0);
}
async waitForNextRender(previousCount = null) {
const baseCount =
previousCount === null ? await this.getRenderCount() : previousCount;
await this.page.waitForFunction(
(count) => (window.wasmRenderCount || 0) > count,
baseCount,
);
}
async hideUI() {
await this.page.keyboard.press("\\");
await expect(this.pageName).not.toBeVisible();

View File

@@ -35,9 +35,45 @@ export class WorkspacePage extends BaseWebSocketPage {
}
async waitForEditor() {
const typographyInput =
this.workspacePage.rightSidebar.getByLabel("Font Size");
await expect(typographyInput).toBeVisible();
return this.page.waitForSelector('[data-itype="editor"]');
}
async waitForRoot() {
return this.page.waitForSelector('[data-itype="root"]');
}
async waitForParagraph(nth) {
if (!nth) {
return this.page.waitForSelector('[data-itype="paragraph"]');
}
return this.page.waitForSelector(
`[data-itype="paragraph"]:nth-child(${nth})`,
);
}
async waitForParagraphStyle(nth, styleName) {
const paragraph = await this.waitForParagraph(nth);
return this.waitForStyle(paragraph, styleName);
}
async waitForTextSpan(nth = 0) {
if (!nth) {
return this.page.waitForSelector('[data-itype="span"]');
}
return this.page.waitForSelector(
`[data-itype="span"]:nth-child(${nth})`,
);
}
async waitForTextSpanContent(nth = 0) {
const textSpan = await this.waitForTextSpan(nth);
const textContent = await textSpan.textContent();
return textContent;
}
async waitForTextSpanStyle(nth, styleName) {
const textSpan = await this.waitForTextSpan(nth);
return this.waitForStyle(textSpan, styleName);
}
async startEditing() {
@@ -45,27 +81,24 @@ export class WorkspacePage extends BaseWebSocketPage {
return this.waitForEditor();
}
async stopEditing() {
await this.page.keyboard.press("Escape");
stopEditing() {
return 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.page.keyboard.press("ArrowLeft");
await this.moveToRight(offset);
}
@@ -92,7 +125,7 @@ export class WorkspacePage extends BaseWebSocketPage {
await expect(locator).toBeVisible();
await locator.focus();
await locator.fill(`${newValue}`);
await this.page.keyboard.press("Enter");
await locator.blur();
}
changeFontSize(newValue) {
@@ -106,10 +139,6 @@ 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)));
}
};
/**
@@ -119,9 +148,9 @@ export class WorkspacePage extends BaseWebSocketPage {
* @returns
*/
static async init(page) {
await super.init(page);
await BaseWebSocketPage.initWebSockets(page);
await super.mockRPCs(page, {
await BaseWebSocketPage.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",
@@ -288,6 +317,7 @@ export class WorkspacePage extends BaseWebSocketPage {
body,
}),
);
// await this.mockRPC(/get\-file\?/, jsonFile);
}
async mockGetAsset(regex, asset) {
@@ -361,12 +391,10 @@ export class WorkspacePage extends BaseWebSocketPage {
const timeToWait = options?.timeToWait ?? 100;
await this.page.keyboard.press("T");
await this.page.waitForTimeout(timeToWait);
const layersCountBefore = await this.layers.getByTestId("layer-row").count();
await this.clickAndMove(x1, y1, x2, y2);
await expect(this.page.getByTestId("text-editor")).toBeVisible();
if (initialText) {
await this.waitForSelectedShapeName("Text");
await this.page.keyboard.type(initialText);
}
}
@@ -466,23 +494,10 @@ export class WorkspacePage extends BaseWebSocketPage {
async expectSelectedLayer(name) {
await expect(
this.layers.getByRole("checkbox", { name, checked: true }),
).toBeVisible();
}
async getSelectedShapeName() {
const selectedLayer = this.layers
.getByRole("checkbox", { checked: true })
.first();
await selectedLayer.waitFor({ state: "visible" });
return (await selectedLayer.innerText()).trim();
}
async waitForSelectedShapeName(expectedName) {
const selectedLayer = this.layers
.getByRole("checkbox", { checked: true })
.first();
await expect(selectedLayer).toHaveText(expectedName);
this.layers
.getByTestId("layer-row")
.filter({ has: this.page.getByText(name) }),
).toHaveClass(/selected/);
}
async expectHiddenToolbarOptions() {

View File

@@ -356,39 +356,3 @@ test("Renders shapes with multiple fills and blur", async ({
await expect(workspace.canvas).toHaveScreenshot();
});
test("Keeps component visible when focusing after creating it", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockRPC(/get\-file\?/, "workspace/get-file-not-empty.json");
await workspace.mockRPC(
"update-file?id=*",
"workspace/update-file-create-rect.json",
);
await workspace.goToWorkspace({
fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374",
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375",
});
await workspace.waitForFirstRender();
await workspace.clickLayers();
await workspace.clickLeafLayer("Rectangle");
await page.keyboard.press("ControlOrMeta+k");
const componentLayer = workspace.layers
.getByTestId("layer-row")
.filter({ has: page.getByTestId("icon-component") })
.first();
await expect(componentLayer).toBeVisible();
await componentLayer.click();
const previousRenderCount = await workspace.getRenderCount();
await page.keyboard.press("f");
await workspace.waitForNextRender(previousRenderCount);
await workspace.hideUI();
await expect(workspace.canvas).toHaveScreenshot();
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 KiB

After

Width:  |  Height:  |  Size: 348 KiB

View File

@@ -3,6 +3,11 @@ 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", () => {

View File

@@ -3,6 +3,11 @@ 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 }) => {

View File

@@ -3,6 +3,11 @@ 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 ({

View File

@@ -3,7 +3,11 @@ 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",

View File

@@ -3,6 +3,11 @@ 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 }) => {

View File

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

View File

@@ -4,7 +4,6 @@ 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",

View File

@@ -3,6 +3,11 @@ 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 }) => {

View File

@@ -1,12 +1,14 @@
import { test, expect } from "@playwright/test";
import { Clipboard } from "../../helpers/Clipboard";
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
import { WorkspacePage } from "../pages/WorkspacePage";
const timeToWait = 100;
test.beforeEach(async ({ page, context }) => {
await Clipboard.enable(context, Clipboard.Permission.ALL);
await WasmWorkspacePage.init(page);
await WasmWorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]);
await WorkspacePage.init(page);
await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]);
});
test.afterEach(async ({ context }) => {
@@ -15,36 +17,39 @@ test.afterEach(async ({ context }) => {
test("Create a new text shape", async ({ page }) => {
const initialText = "Lorem ipsum";
const workspace = new WasmWorkspacePage(page, {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
await workspace.goToWorkspace();
await workspace.createTextShape(190, 150, 300, 200, initialText);
await workspace.textEditor.stopEditing();
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe(initialText);
await workspace.waitForSelectedShapeName(initialText);
await workspace.textEditor.stopEditing();
});
test("Create a new text shape from pasting text", async ({ page, context }) => {
const textToPaste = "Lorem ipsum";
const workspace = new WasmWorkspacePage(page, {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
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 page.waitForTimeout(timeToWait);
await expect(workspace.layers.getByText(textToPaste)).toBeVisible();
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe(textToPaste);
await workspace.textEditor.stopEditing();
});
test("Create a new text shape from pasting text using context menu", async ({
@@ -52,26 +57,27 @@ test("Create a new text shape from pasting text using context menu", async ({
context,
}) => {
const textToPaste = "Lorem ipsum";
const workspace = new WasmWorkspacePage(page, {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
await workspace.goToWorkspace();
await workspace.moveButton.click();
await Clipboard.writeText(page, textToPaste);
await workspace.clickAt(190, 150);
await workspace.paste("context-menu");
await workspace.textEditor.stopEditing();
await expect(workspace.layers.getByText(textToPaste)).toBeVisible();
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe(textToPaste);
await workspace.textEditor.stopEditing();
});
test("Update an already created text shape by appending text", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page, {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
@@ -81,14 +87,15 @@ test("Update an already created text shape by appending text", async ({
await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromEnd(0);
await page.keyboard.type(" dolor sit amet");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lorem ipsum dolor sit amet");
await workspace.textEditor.stopEditing();
await workspace.waitForSelectedShapeName("Lorem ipsum dolor sit amet");
});
test("Update an already created text shape by prepending text", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page, {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
@@ -98,14 +105,15 @@ test("Update an already created text shape by prepending text", async ({
await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromStart(0);
await page.keyboard.type("Dolor sit amet ");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Dolor sit amet Lorem ipsum");
await workspace.textEditor.stopEditing();
await workspace.waitForSelectedShapeName("Dolor sit amet Lorem ipsum");
});
test.skip("Update an already created text shape by inserting text in between", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page, {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
@@ -115,8 +123,9 @@ test.skip("Update an already created text shape by inserting text in between", a
await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromStart(5);
await page.keyboard.type(" dolor sit amet");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lorem dolor sit amet ipsum");
await workspace.textEditor.stopEditing();
await workspace.waitForSelectedShapeName("Lorem dolor sit amet ipsum");
});
test("Update a new text shape appending text by pasting text", async ({
@@ -124,7 +133,7 @@ test("Update a new text shape appending text by pasting text", async ({
context,
}) => {
const textToPaste = " dolor sit amet";
const workspace = new WasmWorkspacePage(page, {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
@@ -137,8 +146,9 @@ test("Update a new text shape appending text by pasting text", async ({
await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromEnd();
await workspace.paste("keyboard");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lorem ipsum dolor sit amet");
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 ({
@@ -146,7 +156,7 @@ test.skip("Update a new text shape prepending text by pasting text", async ({
context,
}) => {
const textToPaste = "Dolor sit amet ";
const workspace = new WasmWorkspacePage(page, {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
@@ -159,17 +169,16 @@ test.skip("Update a new text shape prepending text by pasting text", async ({
await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromStart();
await workspace.paste("keyboard");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Dolor sit amet Lorem ipsum");
await workspace.textEditor.stopEditing();
await workspace.hideUI();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Update a new text shape replacing (starting) text with pasted text", async ({
page,
}) => {
const textToPaste = "Dolor sit amet";
const workspace = new WasmWorkspacePage(page, {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
@@ -183,15 +192,17 @@ test("Update a new text shape replacing (starting) text with pasted text", async
await workspace.paste("keyboard");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Dolor sit amet ipsum");
await workspace.textEditor.stopEditing();
await workspace.waitForSelectedShapeName("Dolor sit amet ipsum");
});
test("Update a new text shape replacing (ending) text with pasted text", async ({
page,
}) => {
const textToPaste = "dolor sit amet";
const workspace = new WasmWorkspacePage(page, {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
@@ -205,15 +216,17 @@ test("Update a new text shape replacing (ending) text with pasted text", async (
await workspace.paste("keyboard");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lorem dolor sit amet");
await workspace.textEditor.stopEditing();
await workspace.waitForSelectedShapeName("Lorem dolor sit amet");
});
test("Update a new text shape replacing (in between) text with pasted text", async ({
page,
}) => {
const textToPaste = "dolor sit amet";
const workspace = new WasmWorkspacePage(page, {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
@@ -227,14 +240,16 @@ test("Update a new text shape replacing (in between) text with pasted text", asy
await workspace.paste("keyboard");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lordolor sit ametsum");
await workspace.textEditor.stopEditing();
await workspace.waitForSelectedShapeName("Lordolor sit ametsum");
});
test("Update text font size selecting a part of it (starting)", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page, {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
@@ -245,16 +260,18 @@ test("Update text font size selecting a part of it (starting)", async ({
await workspace.textEditor.startEditing();
await workspace.textEditor.selectFromStart(5);
await workspace.textEditor.changeFontSize(36);
await workspace.textEditor.stopEditing();
await workspace.hideUI();
await expect(workspace.canvas).toHaveScreenshot();
const textContent1 = await workspace.textEditor.waitForTextSpanContent(1);
expect(textContent1).toBe("Lorem");
const textContent2 = await workspace.textEditor.waitForTextSpanContent(2);
expect(textContent2).toBe(" ipsum");
await workspace.textEditor.stopEditing();
});
test("Update text line height selecting a part of it (starting)", async ({
test.skip("Update text line height selecting a part of it (starting)", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page, {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
@@ -264,17 +281,24 @@ test("Update text line height selecting a part of it (starting)", async ({
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.selectFromStart(5);
await workspace.textEditor.changeLineHeight(4.4);
await workspace.textEditor.stopEditing();
await workspace.textEditor.changeLineHeight(1.4);
await workspace.hideUI();
await expect(workspace.canvas).toHaveScreenshot();
const lineHeight = await workspace.textEditor.waitForParagraphStyle(
1,
"line-height",
);
expect(lineHeight).toBe("1.4");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lorem ipsum");
await workspace.textEditor.stopEditing();
});
test.skip("Update text letter spacing selecting a part of it (starting)", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page, {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
@@ -285,14 +309,16 @@ test.skip("Update text letter spacing selecting a part of it (starting)", async
await workspace.textEditor.startEditing();
await workspace.textEditor.selectFromStart(5);
await workspace.textEditor.changeLetterSpacing(10);
await workspace.textEditor.stopEditing();
await workspace.hideUI();
await expect(workspace.canvas).toHaveScreenshot();
const textContent1 = await workspace.textEditor.waitForTextSpanContent(1);
expect(textContent1).toBe("Lorem");
const textContent2 = await workspace.textEditor.waitForTextSpanContent(2);
expect(textContent2).toBe(" ipsum");
await workspace.textEditor.stopEditing();
});
test("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
const workspace = new WorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-11552.json");
await workspace.mockRPC(

View File

@@ -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 WasmWorkspacePage.mockConfigFlags(page, ["enable-feature-variants"]);
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-variants.json");
});
test.afterEach(async ({ context }) => {

View File

@@ -1,5 +1,6 @@
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);
@@ -105,37 +106,9 @@ test("BUG 11006 - Fix history panel shortcut", async ({ page }) => {
await workspacePage.goToWorkspace();
await page.keyboard.press("ControlOrMeta+Alt+h");
await page.keyboard.press("Control+Alt+h");
await expect(
workspacePage.rightSidebar.getByText("There are no versions yet"),
).toBeVisible();
});
test("BUG 13385 - Fix viewport not updating when restoring version", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockGetFile("workspace/get-file-13385.json");
await workspacePage.mockRPC("get-profiles-for-file-comments?file-id=*", "workspace/get-profiles-for-file-comments-13385.json");
// navigate to workspace and check that the circle shape is not there
await workspacePage.goToWorkspace();
await expect(workspacePage.layers.getByText("Ellipse")).not.toBeVisible();
// mock network requests to restore the version
await workspacePage.mockGetFile("workspace/get-file-13385-2.json");
await workspacePage.mockRPC("get-file-snapshots?file-id=*", "workspace/get-file-snapshots-13385.json");
await workspacePage.mockRPC("restore-file-snapshot", "", {
status: 204,
});
// request to restore the version
await workspacePage.rightSidebar.getByRole("button", { name: "History" }).click();
await workspacePage.rightSidebar.getByRole("button", { name: "Open version menu" }).click();
await workspacePage.rightSidebar.getByRole("button", { name: "Restore" }).click();
// confirm modal
await workspacePage.page.getByRole("button", { name: /Restore/i }).click();
// assert that the circle shape exists
await expect(workspacePage.layers.getByText("Ellipse")).toBeVisible();
});

View File

@@ -23,35 +23,4 @@ test("BUG 13305 - Fix resize board to fit content", async ({ page }) => {
await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("630");
await expect(workspacePage.rightSidebar.getByTitle("X axis").getByRole("textbox")).toHaveValue("110");
await expect(workspacePage.rightSidebar.getByTitle("Y axis").getByRole("textbox")).toHaveValue("110");
});
test("BUG 13382 - Fix problem with flex layout", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockGetFile("workspace/get-file-13382.json");
await workspacePage.mockRPC(
"get-file-fragment?file-id=*&fragment-id=*",
"workspace/get-file-13382-fragment.json",
);
await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-empty.json");
await workspacePage.goToWorkspace({
fileId: "52c4e771-3853-8190-8007-9506c70e8100",
pageId: "ecb0cfd0-0f0b-81f7-8007-950628f9665b",
});
await workspacePage.clickToggableLayer("A");
await workspacePage.clickToggableLayer("B");
await workspacePage.clickToggableLayer("C");
await workspacePage.clickLeafLayer("R2");
const heightText = workspacePage.rightSidebar.getByTitle("Height").getByPlaceholder('--');
await heightText.fill("200");
await heightText.press("Enter");
await workspacePage.clickLeafLayer("B");
await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("340");
});
});

View File

@@ -483,25 +483,3 @@ test("Bug 8371 - Flatten option is not visible in context menu", async ({
.filter({ visible: true }),
).toBeVisible();
});
test("BUG 13415 - Grid layout overlay is not removed when deleting a board", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.mockGetFile("workspace/get-file-13415.json");
await workspacePage.mockRPC(
"update-file?id=*",
"workspace/update-file-13415.json",
);
await workspacePage.goToWorkspace();
await workspacePage.clickLeafLayer("Board");
const currentRenderCount = await workspacePage.getRenderCount();
await workspacePage.page.keyboard.press("Delete");
await workspacePage.waitForNextRender(currentRenderCount);
await workspacePage.hideUI();
await expect(workspacePage.canvas).toHaveScreenshot();
});

View File

@@ -3,6 +3,11 @@ 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 }) => {

View File

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

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

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

@@ -222,16 +222,9 @@
ptk/UpdateEvent
(update [_ state]
(let [pending-version-id (:workspace-pending-file-version-id state)
state (-> state
(assoc :thumbnails thumbnails)
(update :files assoc file-id file)
(dissoc :workspace-pending-file-version-id))]
(cond-> state
(some? pending-version-id)
(assoc :workspace-file-version-id pending-version-id)
(nil? pending-version-id)
(dissoc :workspace-file-version-id))))))
(-> state
(assoc :thumbnails thumbnails)
(update :files assoc file-id file)))))
(defn zoom-to-frame
[]
@@ -287,197 +280,192 @@
(wasm.api/process-object shape))))))
(defn initialize-workspace
([team-id file-id]
(initialize-workspace team-id file-id nil))
([team-id file-id version-id]
(assert (uuid? team-id) "expected valud uuid for `team-id`")
(assert (uuid? file-id) "expected valud uuid for `file-id`")
[team-id file-id]
(assert (uuid? team-id) "expected valud uuid for `team-id`")
(assert (uuid? file-id) "expected valud uuid for `file-id`")
(ptk/reify ::initialize-workspace
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc :recent-colors (:recent-colors storage/user))
(assoc :recent-fonts (:recent-fonts storage/user))
(assoc :current-file-id file-id)
(assoc :workspace-presence {})
;; Store pending version-id; bundle-fetched will set workspace-file-version-id
;; when the new bundle is applied so the viewport re-inits with new data
(assoc :workspace-pending-file-version-id version-id)))
(ptk/reify ::initialize-workspace
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc :recent-colors (:recent-colors storage/user))
(assoc :recent-fonts (:recent-fonts storage/user))
(assoc :current-file-id file-id)
(assoc :workspace-presence {})))
ptk/WatchEvent
(watch [_ state stream]
(let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
rparams (rt/get-params state)
features (features/get-enabled-features state team-id)
render-wasm? (contains? features "render-wasm/v1")]
ptk/WatchEvent
(watch [_ state stream]
(let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
rparams (rt/get-params state)
features (features/get-enabled-features state team-id)
render-wasm? (contains? features "render-wasm/v1")]
(log/debug :hint "initialize-workspace"
:team-id (dm/str team-id)
:file-id (dm/str file-id))
(log/debug :hint "initialize-workspace"
:team-id (dm/str team-id)
:file-id (dm/str file-id))
(->> (rx/merge
(rx/concat
;; Fetch all essential data that should be loaded before the file
(rx/merge
(if ^boolean render-wasm?
(->> (rx/from @wasm/module)
(rx/filter true?)
(rx/tap (fn [_]
(let [event (ug/event "penpot:wasm:loaded")]
(ug/dispatch! event))))
(rx/ignore))
(rx/empty))
(->> (rx/merge
(rx/concat
;; Fetch all essential data that should be loaded before the file
(rx/merge
(if ^boolean render-wasm?
(->> (rx/from @wasm/module)
(rx/filter true?)
(rx/tap (fn [_]
(let [event (ug/event "penpot:wasm:loaded")]
(ug/dispatch! event))))
(rx/ignore))
(rx/empty))
(->> stream
(rx/filter (ptk/type? ::df/fonts-loaded))
(rx/take 1)
(rx/ignore))
(->> stream
(rx/filter (ptk/type? ::df/fonts-loaded))
(rx/take 1)
(rx/ignore))
(rx/of (ntf/hide)
(dcmt/retrieve-comment-threads file-id)
(dcmt/fetch-profiles)
(df/fetch-fonts team-id)))
(rx/of (ntf/hide)
(dcmt/retrieve-comment-threads file-id)
(dcmt/fetch-profiles)
(df/fetch-fonts team-id)))
;; Once the essential data is fetched, lets proceed to
;; fetch teh file bunldle
(rx/of (fetch-bundle file-id features)))
;; Once the essential data is fetched, lets proceed to
;; fetch teh file bunldle
(rx/of (fetch-bundle file-id features)))
(->> stream
(rx/filter (ptk/type? ::bundle-fetched))
(rx/take 1)
(rx/map deref)
(rx/mapcat
(fn [{:keys [file]}]
(log/debug :hint "bundle fetched"
:team-id (dm/str team-id)
:file-id (dm/str file-id))
(->> stream
(rx/filter (ptk/type? ::bundle-fetched))
(rx/take 1)
(rx/map deref)
(rx/mapcat
(fn [{:keys [file]}]
(log/debug :hint "bundle fetched"
:team-id (dm/str team-id)
:file-id (dm/str file-id))
(rx/of (dpj/initialize-project (:project-id file))
(dwn/initialize team-id file-id)
(dwsl/initialize-shape-layout)
(fetch-libraries file-id features)
(-> (workspace-initialized file-id)
(with-meta {:team-id team-id
:file-id file-id}))))))
(rx/of (dpj/initialize-project (:project-id file))
(dwn/initialize team-id file-id)
(dwsl/initialize-shape-layout)
(fetch-libraries file-id features)
(-> (workspace-initialized file-id)
(with-meta {:team-id team-id
:file-id file-id}))))))
;; Install dev perf observers once the workspace is ready
(when (contains? cf/flags :perf-logs)
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
(rx/take 1)
(rx/tap (fn [_] (perf/setup)))))
;; Install dev perf observers once the workspace is ready
(when (contains? cf/flags :perf-logs)
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
(rx/take 1)
(rx/tap (fn [_] (perf/setup)))))
(->> stream
(rx/filter (ptk/type? ::dps/persistence-notification))
(rx/take 1)
(rx/map dwc/set-workspace-visited))
(->> stream
(rx/filter (ptk/type? ::dps/persistence-notification))
(rx/take 1)
(rx/map dwc/set-workspace-visited))
(when-let [component-id (some-> rparams :component-id uuid/parse)]
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
(rx/observe-on :async)
(rx/take 1)
(rx/map #(dwl/go-to-local-component :id component-id :update-layout? (:update-layout rparams)))))
(when-let [component-id (some-> rparams :component-id uuid/parse)]
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
(rx/observe-on :async)
(rx/take 1)
(rx/map #(dwl/go-to-local-component :id component-id :update-layout? (:update-layout rparams)))))
(when (:board-id rparams)
(->> stream
(rx/filter (ptk/type? ::dwv/initialize-viewport))
(rx/take 1)
(rx/map zoom-to-frame)))
(when (:board-id rparams)
(->> stream
(rx/filter (ptk/type? ::dwv/initialize-viewport))
(rx/take 1)
(rx/map zoom-to-frame)))
(when-let [comment-id (some-> rparams :comment-id uuid/parse)]
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
(rx/observe-on :async)
(rx/take 1)
(rx/map #(dwcm/navigate-to-comment-id comment-id))))
(when-let [comment-id (some-> rparams :comment-id uuid/parse)]
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
(rx/observe-on :async)
(rx/take 1)
(rx/map #(dwcm/navigate-to-comment-id comment-id))))
(when render-wasm?
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/mapcat
(fn [{:keys [redo-changes]}]
(let [added (->> redo-changes
(filter #(= (:type %) :add-obj))
(map :id))]
(->> (rx/from added)
(rx/map process-wasm-object)))))))
(when render-wasm?
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/mapcat
(fn [{:keys [redo-changes]}]
(let [added (->> redo-changes
(filter #(= (:type %) :add-obj))
(map :id))]
(->> (rx/from added)
(rx/map process-wasm-object)))))))
(when render-wasm?
(let [local-commits-s
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/filter #(and (= :local (:source %))
(not (contains? (:tags %) :position-data))))
(rx/filter (complement empty?)))
(when render-wasm?
(let [local-commits-s
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/filter #(and (= :local (:source %))
(not (contains? (:tags %) :position-data))))
(rx/filter (complement empty?)))
notifier-s
(rx/merge
(->> local-commits-s (rx/debounce 1000))
(->> stream (rx/filter dps/force-persist?)))
notifier-s
(rx/merge
(->> local-commits-s (rx/debounce 1000))
(->> stream (rx/filter dps/force-persist?)))
objects-s
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
objects-s
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
current-page-id-s
(rx/from-atom refs/current-page-id {:emit-current-value? true})]
current-page-id-s
(rx/from-atom refs/current-page-id {:emit-current-value? true})]
(->> local-commits-s
(rx/buffer-until notifier-s)
(rx/with-latest-from objects-s)
(rx/map
(fn [[commits objects]]
(->> commits
(mapcat :redo-changes)
(filter #(contains? #{:mod-obj :add-obj} (:type %)))
(filter #(cfh/text-shape? objects (:id %)))
(map #(vector
(:id %)
(wasm.api/calculate-position-data (get objects (:id %))))))))
(->> local-commits-s
(rx/buffer-until notifier-s)
(rx/with-latest-from objects-s)
(rx/map
(fn [[commits objects]]
(->> commits
(mapcat :redo-changes)
(filter #(contains? #{:mod-obj :add-obj} (:type %)))
(filter #(cfh/text-shape? objects (:id %)))
(map #(vector
(:id %)
(wasm.api/calculate-position-data (get objects (:id %))))))))
(rx/with-latest-from current-page-id-s)
(rx/map
(fn [[text-position-data page-id]]
(let [changes
(->> text-position-data
(mapv (fn [[id position-data]]
{:type :mod-obj
:id id
:page-id page-id
:operations
[{:type :set
:attr :position-data
:val position-data
:ignore-touched true
:ignore-geometry true}]})))]
(when (d/not-empty? changes)
(dch/commit-changes
{:redo-changes changes :undo-changes []
:save-undo? false
:tags #{:position-data}})))))
(rx/take-until stoper-s))))
(rx/with-latest-from current-page-id-s)
(rx/map
(fn [[text-position-data page-id]]
(let [changes
(->> text-position-data
(mapv (fn [[id position-data]]
{:type :mod-obj
:id id
:page-id page-id
:operations
[{:type :set
:attr :position-data
:val position-data
:ignore-touched true
:ignore-geometry true}]})))]
(when (d/not-empty? changes)
(dch/commit-changes
{:redo-changes changes :undo-changes []
:save-undo? false
:tags #{:position-data}})))))
(rx/take-until stoper-s))))
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/mapcat
(fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo?]}]
(if (and save-undo? (seq undo-changes))
(let [entry {:undo-changes undo-changes
:redo-changes redo-changes
:undo-group undo-group
:tags tags}]
(rx/of (dwu/append-undo entry stack-undo?)))
(rx/empty))))))
(rx/take-until stoper-s))))
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/mapcat
(fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo?]}]
(if (and save-undo? (seq undo-changes))
(let [entry {:undo-changes undo-changes
:redo-changes redo-changes
:undo-group undo-group
:tags tags}]
(rx/of (dwu/append-undo entry stack-undo?)))
(rx/empty))))))
(rx/take-until stoper-s))))
ptk/EffectEvent
(effect [_ _ _]
(let [name (dm/str "workspace-" file-id)]
(unchecked-set ug/global "name" name))))))
ptk/EffectEvent
(effect [_ _ _]
(let [name (dm/str "workspace-" file-id)]
(unchecked-set ug/global "name" name)))))
(defn finalize-workspace
[_team-id file-id]

View File

@@ -197,12 +197,11 @@
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)

View File

@@ -10,7 +10,6 @@
[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]
@@ -20,7 +19,6 @@
[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]
@@ -918,11 +916,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? original-content]
:or {update-name? false name nil finalize? false save-undo? true original-content nil}}]
[id content & {:keys [update-name? name finalize? save-undo?]
:or {update-name? false name nil finalize? false save-undo? true}}]
(ptk/reify ::v2-update-text-shape-content
ptk/WatchEvent
(watch [it state _]
(watch [_ state _]
(if (features/active-feature? state "render-wasm/v1")
(let [objects (dsh/lookup-page-objects state)
shape (get objects id)
@@ -952,11 +950,11 @@
new-shape))
{:save-undo? save-undo? :undo-group (when new-shape? id)})
(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)))))
(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 finalize?
(rx/concat
@@ -972,13 +970,7 @@
{:save-undo? false}))
(dws/deselect-shape id)
(dwsh/delete-shapes #{id})))
(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))))))
(rx/of (dwt/finish-transform))))))
(let [objects (dsh/lookup-page-objects state)
shape (get objects id)

View File

@@ -108,7 +108,7 @@
(rx/take 1)
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
(rx/tap #(th/clear-queue!))
(rx/map #(dw/initialize-workspace team-id file-id id)))
(rx/map #(dw/initialize-workspace team-id file-id)))
(case origin
:version
(rx/of (ptk/event ::ev/event {::ev/name "restore-pin-version"}))
@@ -231,7 +231,7 @@
(rx/filter #(or (nil? %) (= :saved %)))
(rx/take 1)
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
(rx/map #(dw/initialize-workspace team-id file-id id)))
(rx/map #(dw/initialize-workspace team-id file-id)))
(->> (rx/of 1)
(rx/tap resolve)

View File

@@ -27,28 +27,27 @@
(resize-wasm-text-modifiers shape (:content shape)))
([{:keys [id points selrect grow-type] :as shape} content]
(when id
(wasm.api/use-shape id)
(wasm.api/set-shape-text-content id content)
(wasm.api/set-shape-text-images id content)
(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.

View File

@@ -256,9 +256,6 @@
(def workspace-layout
(l/derived :workspace-layout st/state))
(def workspace-file-version-id
(l/derived :workspace-file-version-id st/state))
(def snap-pixel?
(l/derived #(contains? % :snap-pixel-grid) workspace-layout))

View File

@@ -50,7 +50,7 @@
(mf/defc workspace-content*
{::mf/private true}
[{:keys [file layout page wglobal file-version-id]}]
[{:keys [file layout page wglobal]}]
(let [palete-size (mf/use-state nil)
selected (mf/deref refs/selected-shapes)
@@ -109,7 +109,6 @@
:wglobal wglobal
:selected selected
:layout layout
:file-version-id file-version-id
:palete-size
(when (and (or colorpalette? textpalette?) (not hide-ui?))
@palete-size)}]]]
@@ -169,7 +168,7 @@
(mf/defc workspace-inner*
{::mf/private true}
[{:keys [page-id file-id file layout wglobal file-version-id]}]
[{:keys [page-id file-id file layout wglobal]}]
(let [page-ref (mf/with-memo [file-id page-id]
(make-page-ref file-id page-id))
page (mf/deref page-ref)]
@@ -188,8 +187,7 @@
[:> workspace-content* {:file file
:page page
:wglobal wglobal
:layout layout
:file-version-id file-version-id}]
:layout layout}]
[:> workspace-loader*])))
(mf/defc workspace*
@@ -201,7 +199,6 @@
layout (mf/deref refs/workspace-layout)
wglobal (mf/deref refs/workspace-global)
file-version-id (mf/deref refs/workspace-file-version-id)
team-ref (mf/with-memo [team-id]
(make-team-ref team-id))
@@ -277,8 +274,7 @@
:file-id file-id
:file file
:wglobal wglobal
:layout layout
:file-version-id file-version-id}])
:layout layout}])
(when (or (not (and file-loaded? page-id))
;; in wasm renderer, extend the pixel loader until the first frame is rendered
;; but do not apply it when switching pages

View File

@@ -118,8 +118,7 @@
:update-name? update-name?
:name generated-name
:finalize? true
:save-undo? false
:original-content original-content))))
:save-undo? false))))
(let [container-node (mf/ref-val container-ref)]
(dom/set-style! container-node "opacity" 0)))

View File

@@ -84,8 +84,6 @@
:on-click on-select-shape
:on-context-menu on-context-menu
:data-testid "layer-row"
:role "checkbox"
:aria-checked selected?
:class (stl/css-case
:layer-row true
:highlight highlighted?

View File

@@ -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 [:enum :fill :fix :auto]]]])
[:v-sizing {:optional true} [:maybe [:= :fill]]]])
(mf/defc layout-size-constraints*
{::mf/private true

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

@@ -73,7 +73,7 @@
objects)))
(mf/defc viewport*
[{:keys [selected wglobal wlocal layout file page palete-size file-version-id]}]
[{:keys [selected wglobal wlocal layout file page palete-size]}]
(let [;; When adding data from workspace-local revisit `app.main.ui.workspace` to check
;; that the new parameter is sent
{:keys [edit-path
@@ -141,7 +141,6 @@
canvas-ref (mf/use-ref nil)
text-editor-ref (mf/use-ref nil)
last-file-version-id-ref (mf/use-ref nil)
;; STATE REFS
disable-paste-ref (mf/use-ref false)
@@ -224,9 +223,8 @@
show-gradient-handlers? (= (count selected) 1)
show-grids? (contains? layout :display-guides)
show-frame-outline? (and (= transform :move) (not panning))
show-frame-outline? (= transform :move)
show-outlines? (and (nil? transform)
(not panning)
(not edition)
(not drawing-obj)
(not (#{:comments :path :curve} drawing-tool)))
@@ -346,18 +344,11 @@
(when (and @canvas-init? preview-blend)
(wasm.api/request-render "with-effect")))
(mf/with-effect [@canvas-init? file-version-id zoom vbox background]
(when @canvas-init?
(if (not @initialized?)
(do
(wasm.api/clear-canvas-pixels)
(wasm.api/initialize-viewport base-objects zoom vbox background)
(reset! initialized? true)
(mf/set-ref-val! last-file-version-id-ref file-version-id))
(when (and (some? file-version-id)
(not= file-version-id (mf/ref-val last-file-version-id-ref)))
(wasm.api/initialize-viewport base-objects zoom vbox background)
(mf/set-ref-val! last-file-version-id-ref file-version-id)))))
(mf/with-effect [@canvas-init? zoom vbox background]
(when (and @canvas-init? (not @initialized?))
(wasm.api/clear-canvas-pixels)
(wasm.api/initialize-viewport base-objects zoom vbox background)
(reset! initialized? true)))
(mf/with-effect [focus]
(when (and @canvas-init? @initialized?)
@@ -562,7 +553,7 @@
:shift? @shift?}])
[:> widgets/frame-titles*
{:objects objects-modified
{:objects (with-meta objects-modified nil)
:selected selected
:zoom zoom
:is-show-artboard-names show-artboard-names?

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

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

@@ -642,15 +642,13 @@ export class SelectionController extends EventTarget {
} else {
this.#anchorNode = anchorNode;
this.#anchorOffset = anchorOffset;
this.#focusNode = focusNode;
this.#focusOffset = focusOffset;
// setPosition() collapses the selection to a single caret. We must only use it
// when anchorOffset === focusOffset. When both points are in the same node but
// offsets differ (e.g. selecting "hola" in "hola adios"), we need setBaseAndExtent()
// to preserve the range so we don't incorrectly collapse ranges and lose the selection.
if (anchorNode === focusNode && anchorOffset === focusOffset) {
if (anchorNode === focusNode) {
this.#focusNode = this.#anchorNode;
this.#focusOffset = this.#anchorOffset;
this.#selection.setPosition(anchorNode, anchorOffset);
} else {
this.#focusNode = focusNode;
this.#focusOffset = focusOffset;
this.#selection.setBaseAndExtent(
anchorNode,
anchorOffset,

View File

@@ -412,9 +412,6 @@ fn set_children_set(entries: Vec<Uuid>) {
for id in entries {
state.touch_shape(id);
if let Some(children_shape) = state.shapes.get_mut(&id) {
children_shape.set_deleted(false);
}
}
});

View File

@@ -39,7 +39,6 @@ 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<Corners>, Matrix)>;
@@ -642,7 +641,6 @@ impl RenderState {
apply_to_current_surface: bool,
offset: Option<(f32, f32)>,
parent_shadows: Option<Vec<skia_safe::Paint>>,
outset: Option<f32>,
) {
let surface_ids = fills_surface_id as u32
| strokes_surface_id as u32
@@ -701,7 +699,7 @@ impl RenderState {
canvas.translate(translation);
});
fills::render(self, shape, &shape.fills, antialias, SurfaceId::Current, outset);
fills::render(self, shape, &shape.fills, antialias, SurfaceId::Current);
// Pass strokes in natural order; stroke merging handles top-most ordering internally.
let visible_strokes: Vec<&Stroke> = shape.visible_strokes().collect();
@@ -711,7 +709,6 @@ impl RenderState {
&visible_strokes,
Some(SurfaceId::Current),
antialias,
outset,
);
self.surfaces.apply_mut(SurfaceId::Current as u32, |s| {
@@ -790,25 +787,6 @@ 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<f32> = 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);
@@ -853,8 +831,6 @@ impl RenderState {
let text_content = text_content.new_bounds(shape.selrect());
let count_inner_strokes = shape.count_visible_inner_strokes();
let text_stroke_blur_outset =
Stroke::max_bounds_width(shape.visible_strokes(), false);
let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None);
let mut stroke_paragraphs_list = shape
.visible_strokes()
@@ -882,7 +858,7 @@ impl RenderState {
);
for stroke_paragraphs in stroke_paragraphs_list.iter_mut() {
text::render_with_bounds_outset(
text::render(
Some(self),
None,
&shape,
@@ -890,7 +866,6 @@ impl RenderState {
Some(strokes_surface_id),
None,
None,
text_stroke_blur_outset,
);
}
} else {
@@ -982,7 +957,7 @@ impl RenderState {
// 4. Stroke fills
for stroke_paragraphs in stroke_paragraphs_list.iter_mut() {
text::render_with_bounds_outset(
text::render(
Some(self),
None,
&shape,
@@ -990,7 +965,6 @@ impl RenderState {
Some(strokes_surface_id),
None,
blur_filter.as_ref(),
text_stroke_blur_outset,
);
}
@@ -1027,24 +1001,6 @@ 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<skia::ImageFilter> = 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()
@@ -1057,24 +1013,10 @@ 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,
outset,
);
fills::render(self, shape, &fills_to_render, antialias, fills_surface_id);
}
} else {
fills::render(
self,
shape,
&shape.fills,
antialias,
fills_surface_id,
outset,
);
fills::render(self, shape, &shape.fills, antialias, fills_surface_id);
}
// Skip stroke rendering for clipped frames - they are drawn in render_shape_exit
@@ -1089,7 +1031,6 @@ impl RenderState {
&visible_strokes,
Some(strokes_surface_id),
antialias,
outset,
);
if !fast_mode {
for stroke in &visible_strokes {
@@ -1112,12 +1053,7 @@ impl RenderState {
innershadows_surface_id,
);
}
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();
}
// bools::debug_render_bool_paths(self, shape, shapes, modifiers, structure);
}
};
@@ -1209,11 +1145,6 @@ 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);
@@ -1221,8 +1152,6 @@ 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);
@@ -1393,16 +1322,11 @@ impl RenderState {
paint.set_blend_mode(element.blend_mode().into());
paint.set_alpha_f(element.opacity());
// 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);
}
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);
}
}
@@ -1489,7 +1413,6 @@ impl RenderState {
true,
None,
None,
None,
);
}
@@ -1590,7 +1513,9 @@ 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 {
@@ -1666,7 +1591,6 @@ impl RenderState {
false,
Some(shadow.offset),
None,
None,
);
});
@@ -1674,27 +1598,13 @@ impl RenderState {
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. 8×): 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
};
let filter_result = filters::render_into_filter_surface(
self,
bounds,
blur_downscale,
|state, temp_surface| {
let filter_result =
filters::render_into_filter_surface(self, bounds, |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_image_filter(drop_filter.clone());
shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint);
@@ -1712,7 +1622,6 @@ impl RenderState {
false,
Some(shadow.offset),
None,
None,
);
});
@@ -1720,8 +1629,7 @@ impl RenderState {
let canvas = state.surfaces.canvas(temp_surface);
canvas.restore();
}
},
);
});
if let Some((mut surface, filter_scale)) = filter_result {
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
@@ -1795,20 +1703,13 @@ impl RenderState {
);
if !matches!(element.shape_type, Type::Bool(_)) {
let shadow_children = if element.is_recursive() {
get_simplified_children(tree, element)
} else {
Vec::new()
};
for shadow_shape_id in shadow_children.iter() {
for shadow_shape_id in element.children.iter() {
let Some(shadow_shape) = tree.get(shadow_shape_id) else {
continue;
};
if shadow_shape.hidden {
continue;
}
let nested_clip_bounds =
node_render_state.get_nested_shadow_clip_bounds(element, shadow);
@@ -1856,7 +1757,6 @@ impl RenderState {
true,
None,
Some(vec![new_shadow_paint.clone()]),
None,
);
});
self.surfaces.canvas(SurfaceId::DropShadows).restore();
@@ -1870,6 +1770,7 @@ impl RenderState {
self.surfaces
.canvas(SurfaceId::DropShadows)
.draw_paint(&paint);
self.surfaces.canvas(SurfaceId::DropShadows).restore();
}
@@ -1995,12 +1896,10 @@ impl RenderState {
}
}
let can_flatten = element.can_flatten() && !self.focus_mode.should_focus(&element.id);
// Skip render_shape_enter/exit for flattened containers
// If a container was flattened, it doesn't affect children visually, so we skip
// the expensive enter/exit operations and process children directly
if !can_flatten {
if !element.can_flatten() {
// Enter focus early so shadow_before_layer can run (it needs focus_mode.is_active())
self.focus_mode.enter(&element.id);
@@ -2068,7 +1967,6 @@ impl RenderState {
true,
None,
None,
None,
);
self.surfaces
@@ -2080,7 +1978,7 @@ impl RenderState {
// Skip nested state updates for flattened containers
// Flattened containers don't affect children, so we don't need to track their state
if !can_flatten {
if !element.can_flatten() {
match element.shape_type {
Type::Frame(_) if Self::frame_clip_layer_blur(element).is_some() => {
self.nested_blurs.push(None);
@@ -2105,7 +2003,7 @@ impl RenderState {
let children_clip_bounds =
node_render_state.get_children_clip_bounds(element, None);
let children_ids: Vec<_> = if can_flatten {
let children_ids: Vec<_> = if element.can_flatten() {
// Container was flattened: get simplified children (which skip this level)
get_simplified_children(tree, element)
} else {

View File

@@ -2,15 +2,7 @@ use skia_safe::{self as skia, Paint, RRect};
use super::{filters, RenderState, SurfaceId};
use crate::render::get_source_rect;
use crate::shapes::{merge_fills, Fill, Frame, ImageFill, Rect, Shape, StrokeKind, Type};
/// True when the shape has at least one visible inner stroke.
fn has_inner_stroke(shape: &Shape) -> bool {
let is_open = shape.is_open();
shape
.visible_strokes()
.any(|s| s.render_kind(is_open) == StrokeKind::Inner)
}
use crate::shapes::{merge_fills, Fill, Frame, ImageFill, Rect, Shape, Type};
fn draw_image_fill(
render_state: &mut RenderState,
@@ -105,7 +97,6 @@ pub fn render(
fills: &[Fill],
antialias: bool,
surface_id: SurfaceId,
outset: Option<f32>,
) {
if fills.is_empty() {
return;
@@ -115,14 +106,8 @@ pub fn render(
// and sampling options that get_fill_shader (used by merge_fills) lacks.
let has_image_fills = fills.iter().any(|f| matches!(f, Fill::Image(_)));
if has_image_fills {
let scale = render_state.get_scale().max(1e-6);
let inset = if has_inner_stroke(shape) {
Some(1.0 / scale)
} else {
None
};
for fill in fills.iter().rev() {
render_single_fill(render_state, shape, fill, antialias, surface_id, outset, inset);
render_single_fill(render_state, shape, fill, antialias, surface_id);
}
return;
}
@@ -130,13 +115,6 @@ pub fn render(
let mut paint = merge_fills(fills, shape.selrect);
paint.set_anti_alias(antialias);
let scale = render_state.get_scale().max(1e-6);
let inset = if has_inner_stroke(shape) {
Some(1.0 / scale)
} else {
None
};
if let Some(image_filter) = shape.image_filter(1.) {
let bounds = image_filter.compute_fast_bounds(shape.selrect);
if filters::render_with_filter_surface(
@@ -146,7 +124,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, outset, inset);
draw_fill_to_surface(state, shape, temp_surface, &filtered_paint);
},
) {
return;
@@ -155,35 +133,28 @@ pub fn render(
}
}
draw_fill_to_surface(render_state, shape, surface_id, &paint, outset, inset);
draw_fill_to_surface(render_state, shape, surface_id, &paint);
}
/// Draws a single paint (with a merged shader) to the appropriate surface
/// based on the shape type.
/// When `inset` is Some(eps), the fill is inset by eps (e.g. to avoid seam with inner strokes).
fn draw_fill_to_surface(
render_state: &mut RenderState,
shape: &Shape,
surface_id: SurfaceId,
paint: &Paint,
outset: Option<f32>,
inset: Option<f32>,
) {
match &shape.shape_type {
Type::Rect(_) | Type::Frame(_) => {
render_state
.surfaces
.draw_rect_to(surface_id, shape, paint, outset, inset);
render_state.surfaces.draw_rect_to(surface_id, shape, paint);
}
Type::Circle => {
render_state
.surfaces
.draw_circle_to(surface_id, shape, paint, outset, inset);
.draw_circle_to(surface_id, shape, paint);
}
Type::Path(_) | Type::Bool(_) => {
render_state
.surfaces
.draw_path_to(surface_id, shape, paint, outset, inset);
render_state.surfaces.draw_path_to(surface_id, shape, paint);
}
Type::Group(_) => {}
_ => unreachable!("This shape should not have fills"),
@@ -196,8 +167,6 @@ fn render_single_fill(
fill: &Fill,
antialias: bool,
surface_id: SurfaceId,
outset: Option<f32>,
inset: Option<f32>,
) {
let mut paint = fill.to_paint(&shape.selrect, antialias);
if let Some(image_filter) = shape.image_filter(1.) {
@@ -216,8 +185,6 @@ fn render_single_fill(
antialias,
temp_surface,
&filtered_paint,
outset,
inset,
);
},
) {
@@ -227,16 +194,7 @@ fn render_single_fill(
}
}
draw_single_fill_to_surface(
render_state,
shape,
fill,
antialias,
surface_id,
&paint,
outset,
inset,
);
draw_single_fill_to_surface(render_state, shape, fill, antialias, surface_id, &paint);
}
fn draw_single_fill_to_surface(
@@ -246,8 +204,6 @@ fn draw_single_fill_to_surface(
antialias: bool,
surface_id: SurfaceId,
paint: &Paint,
outset: Option<f32>,
inset: Option<f32>,
) {
match (fill, &shape.shape_type) {
(Fill::Image(image_fill), _) => {
@@ -261,19 +217,15 @@ fn draw_single_fill_to_surface(
);
}
(_, Type::Rect(_) | Type::Frame(_)) => {
render_state
.surfaces
.draw_rect_to(surface_id, shape, paint, outset, inset);
render_state.surfaces.draw_rect_to(surface_id, shape, paint);
}
(_, Type::Circle) => {
render_state
.surfaces
.draw_circle_to(surface_id, shape, paint, outset, inset);
.draw_circle_to(surface_id, shape, paint);
}
(_, Type::Path(_)) | (_, Type::Bool(_)) => {
render_state
.surfaces
.draw_path_to(surface_id, shape, paint, outset, inset);
render_state.surfaces.draw_path_to(surface_id, shape, paint);
}
(_, Type::Group(_)) => {
// Groups can have fills but they propagate them to their children

View File

@@ -40,9 +40,7 @@ pub fn render_with_filter_surface<F>(
where
F: FnOnce(&mut RenderState, SurfaceId),
{
if let Some((mut surface, scale)) =
render_into_filter_surface(render_state, bounds, 1.0, draw_fn)
{
if let Some((mut surface, scale)) = render_into_filter_surface(render_state, bounds, 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
@@ -71,15 +69,9 @@ 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<F>(
render_state: &mut RenderState,
bounds: Rect,
extra_downscale: f32,
draw_fn: F,
) -> Option<(skia::Surface, f32)>
where
@@ -94,28 +86,16 @@ 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 fit_scale = if bounds_width > filter_width || bounds_height > filter_height {
let 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(MIN_FIT_SCALE)
scale_x.min(scale_y).max(0.1) // Clamp to minimum 0.1 to avoid extreme scaling
} 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);

View File

@@ -47,7 +47,6 @@ pub fn render_stroke_inner_shadows(
Some(surface_id),
filter.as_ref(),
antialias,
None,
)
}
}
@@ -107,19 +106,15 @@ fn render_shadow_paint(
) {
match &shape.shape_type {
Type::Rect(_) | Type::Frame(_) => {
render_state
.surfaces
.draw_rect_to(surface_id, shape, paint, None, None);
render_state.surfaces.draw_rect_to(surface_id, shape, paint);
}
Type::Circle => {
render_state
.surfaces
.draw_circle_to(surface_id, shape, paint, None, None);
.draw_circle_to(surface_id, shape, paint);
}
Type::Path(_) | Type::Bool(_) => {
render_state
.surfaces
.draw_path_to(surface_id, shape, paint, None, None);
render_state.surfaces.draw_path_to(surface_id, shape, paint);
}
_ => {}
}

View File

@@ -526,7 +526,6 @@ pub fn render(
strokes: &[&Stroke],
surface_id: Option<SurfaceId>,
antialias: bool,
outset: Option<f32>,
) {
if strokes.is_empty() {
return;
@@ -541,10 +540,6 @@ 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 outset if provided
if let Some(s) = outset.filter(|&s| s > 0.0) {
content_bounds.outset((s, s));
}
let max_margin = strokes
.iter()
.map(|s| s.bounds_width(shape.is_open()))
@@ -588,7 +583,6 @@ pub fn render(
antialias,
true,
true,
outset,
);
}
@@ -601,28 +595,12 @@ 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,
outset,
);
render_single(render_state, shape, stroke, surface_id, None, antialias);
}
return;
}
render_merged(
render_state,
shape,
strokes,
surface_id,
antialias,
false,
outset,
);
render_merged(render_state, shape, strokes, surface_id, antialias, false);
}
fn strokes_share_geometry(strokes: &[&Stroke]) -> bool {
@@ -642,7 +620,6 @@ fn render_merged(
surface_id: Option<SurfaceId>,
antialias: bool,
bypass_filter: bool,
outset: Option<f32>,
) {
let representative = *strokes
.last()
@@ -658,10 +635,6 @@ fn render_merged(
if !bypass_filter {
if let Some(image_filter) = blur_filter.clone() {
let mut content_bounds = shape.selrect;
// Expand for outset if provided
if let Some(s) = outset.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));
@@ -687,15 +660,7 @@ fn render_merged(
canvas.save_layer(&layer_rec);
});
render_merged(
state,
shape,
strokes,
Some(temp_surface),
antialias,
true,
outset,
);
render_merged(state, shape, strokes, Some(temp_surface), antialias, true);
state.surfaces.apply_mut(temp_surface as u32, |surface| {
surface.canvas().restore();
@@ -711,16 +676,7 @@ 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();
// Expand selrect if outset is provided
let selrect = if let Some(s) = outset.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 merged = merge_fills(&fills, shape.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);
@@ -791,7 +747,6 @@ pub fn render_single(
surface_id: Option<SurfaceId>,
shadow: Option<&ImageFilter>,
antialias: bool,
outset: Option<f32>,
) {
render_single_internal(
render_state,
@@ -802,7 +757,6 @@ pub fn render_single(
antialias,
false,
false,
outset,
);
}
@@ -816,15 +770,10 @@ fn render_single_internal(
antialias: bool,
bypass_filter: bool,
skip_blur: bool,
outset: Option<f32>,
) {
if !bypass_filter {
if let Some(image_filter) = shape.image_filter(1.) {
let mut content_bounds = shape.selrect;
// Expand for outset if provided
if let Some(s) = outset.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));
@@ -850,7 +799,6 @@ fn render_single_internal(
antialias,
true,
true,
outset,
);
},
) {
@@ -919,21 +867,7 @@ fn render_single_internal(
shape_type @ (Type::Path(_) | Type::Bool(_)) => {
if let Some(path) = shape_type.path() {
let is_open = path.is_open();
let mut paint =
stroke.to_stroked_paint(is_open, &selrect, svg_attrs, antialias);
// Apply outset by increasing stroke width
if let Some(s) = outset.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 outset so visual growth is comparable across kinds.
let outset_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 + outset_growth);
}
let paint = stroke.to_stroked_paint(is_open, &selrect, svg_attrs, antialias);
draw_stroke_on_path(
canvas,
stroke,

View File

@@ -355,91 +355,24 @@ impl Surfaces {
));
}
pub fn draw_rect_to(
&mut self,
id: SurfaceId,
shape: &Shape,
paint: &Paint,
outset: Option<f32>,
inset: Option<f32>,
) {
let mut rect = if let Some(s) = outset.filter(|&s| s > 0.0) {
let mut r = shape.selrect;
r.outset((s, s));
r
} else {
shape.selrect
};
if let Some(eps) = inset.filter(|&e| e > 0.0) {
rect.inset((eps, eps));
}
pub fn draw_rect_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) {
if let Some(corners) = shape.shape_type.corners() {
let corners = if let Some(eps) = inset.filter(|&e| e > 0.0) {
let mut c = corners;
for r in c.iter_mut() {
r.x = (r.x - eps).max(0.0);
r.y = (r.y - eps).max(0.0);
}
c
} else {
corners
};
let rrect = RRect::new_rect_radii(rect, &corners);
let rrect = RRect::new_rect_radii(shape.selrect, &corners);
self.canvas_and_mark_dirty(id).draw_rrect(rrect, paint);
} else {
self.canvas_and_mark_dirty(id).draw_rect(rect, paint);
self.canvas_and_mark_dirty(id)
.draw_rect(shape.selrect, paint);
}
}
pub fn draw_circle_to(
&mut self,
id: SurfaceId,
shape: &Shape,
paint: &Paint,
outset: Option<f32>,
inset: Option<f32>,
) {
let mut rect = if let Some(s) = outset.filter(|&s| s > 0.0) {
let mut r = shape.selrect;
r.outset((s, s));
r
} else {
shape.selrect
};
if let Some(eps) = inset.filter(|&e| e > 0.0) {
rect.inset((eps, eps));
}
self.canvas_and_mark_dirty(id).draw_oval(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_path_to(
&mut self,
id: SurfaceId,
shape: &Shape,
paint: &Paint,
outset: Option<f32>,
inset: Option<f32>,
) {
pub fn draw_path_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) {
if let Some(path) = shape.get_skia_path() {
let canvas = self.canvas_and_mark_dirty(id);
if let Some(s) = outset.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_style(skia::PaintStyle::Stroke);
stroke_paint.set_stroke_width(s * 2.0);
canvas.draw_path(&path, &stroke_paint);
} else {
canvas.draw_path(&path, paint);
// Inset: avoid seam with inner strokes by clearing a thin border from the fill
if let Some(eps) = inset.filter(|&e| e > 0.0) {
let mut clear_paint = skia::Paint::default();
clear_paint.set_style(skia::PaintStyle::Stroke);
clear_paint.set_stroke_width(eps * 2.0);
clear_paint.set_blend_mode(skia::BlendMode::Clear);
clear_paint.set_anti_alias(paint.is_anti_alias());
canvas.draw_path(&path, &clear_paint);
}
}
self.canvas_and_mark_dirty(id).draw_path(&path, paint);
}
}

View File

@@ -156,8 +156,7 @@ fn get_text_stroke_paints(
paints
}
#[allow(clippy::too_many_arguments)]
pub fn render_with_bounds_outset(
pub fn render(
render_state: Option<&mut RenderState>,
canvas: Option<&Canvas>,
shape: &Shape,
@@ -165,20 +164,12 @@ pub fn render_with_bounds_outset(
surface_id: Option<SurfaceId>,
shadow: Option<&Paint>,
blur: Option<&ImageFilter>,
stroke_bounds_outset: f32,
) {
if let Some(render_state) = render_state {
let target_surface = surface_id.unwrap_or(SurfaceId::Fills);
if let Some(blur_filter) = blur {
let mut text_bounds = shape
.get_text_content()
.calculate_bounds(shape, false)
.to_rect();
if stroke_bounds_outset > 0.0 {
text_bounds.inset((-stroke_bounds_outset, -stroke_bounds_outset));
}
let bounds = blur_filter.compute_fast_bounds(text_bounds);
let bounds = blur_filter.compute_fast_bounds(shape.selrect);
if bounds.is_finite() && bounds.width() > 0.0 && bounds.height() > 0.0 {
let blur_filter_clone = blur_filter.clone();
if filters::render_with_filter_surface(
@@ -211,27 +202,6 @@ pub fn render_with_bounds_outset(
}
}
pub fn render(
render_state: Option<&mut RenderState>,
canvas: Option<&Canvas>,
shape: &Shape,
paragraph_builders: &mut [Vec<ParagraphBuilder>],
surface_id: Option<SurfaceId>,
shadow: Option<&Paint>,
blur: Option<&ImageFilter>,
) {
render_with_bounds_outset(
render_state,
canvas,
shape,
paragraph_builders,
surface_id,
shadow,
blur,
0.0,
);
}
fn render_text_on_canvas(
canvas: &Canvas,
shape: &Shape,

View File

@@ -45,10 +45,6 @@ pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) {
continue;
}
if shape.deleted() {
continue;
}
if let Some(shape) = shapes.get(&shape.id) {
grid_layout::render_overlay(zoom, canvas, shape, shapes);
}

View File

@@ -196,7 +196,6 @@ pub struct Shape {
pub extrect_cache: RefCell<Option<(math::Rect, u32)>>,
pub svg_transform: Option<Matrix>,
pub ignore_constraints: bool,
deleted: bool,
}
// Returns all ancestor shapes of this shape, traversing up the parent hierarchy
@@ -285,7 +284,6 @@ impl Shape {
extrect_cache: RefCell::new(None),
svg_transform: None,
ignore_constraints: false,
deleted: false,
}
}
@@ -443,14 +441,6 @@ impl Shape {
self.svg_transform
}
pub fn set_deleted(&mut self, value: bool) {
self.deleted = value;
}
pub fn deleted(&self) -> bool {
self.deleted
}
// FIXME: These arguments could be grouped or simplified
#[allow(clippy::too_many_arguments)]
pub fn set_flex_layout_child_data(

View File

@@ -441,18 +441,12 @@ pub fn propagate_modifiers(
db.cmp(&da)
});
// This temporary bounds is necesary so the layouts can be calculated
// correctly but will be discarded before the next iteration for the
// bounds to be calculated properly with the modifiers.
let mut bounds_temp = bounds.clone();
for id in &layout_reflows_vec {
if reflown.contains(id) {
continue;
}
reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds_temp);
reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds);
}
layout_reflows = HashSet::new();
}
#[allow(dead_code)]

View File

@@ -157,10 +157,6 @@ impl State {
self.render_state.tiles.remove_shape_at(tile, shape.id);
}
}
if let Some(shape_to_delete) = self.shapes.get_mut(&id) {
shape_to_delete.set_deleted(true);
}
}
}

View File

@@ -13,8 +13,6 @@ copy_shared_artifact;
pushd $_SCRIPT_DIR;
cargo watch \
--why \
-i "_tmp*" \
-x "build $CARGO_PARAMS" \
-s "./build" \
-s "echo 'DONE\n'";