mirror of
https://github.com/penpot/penpot.git
synced 2026-09-10 14:09:17 +00:00
Merge remote-tracking branch 'origin/staging-render' into develop
This commit is contained in:
commit
bcc755b0be
@ -22,6 +22,9 @@
|
|||||||
|
|
||||||
## 2.14.0 (Unreleased)
|
## 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
|
### :sparkles: New features & Enhancements
|
||||||
|
|
||||||
- Access to design tokens in Penpot Plugins [Taiga #8990](https://tree.taiga.io/project/penpot/us/8990)
|
- Access to design tokens in Penpot Plugins [Taiga #8990](https://tree.taiga.io/project/penpot/us/8990)
|
||||||
|
|||||||
@ -28,8 +28,8 @@
|
|||||||
com.google.guava/guava {:mvn/version "33.4.8-jre"}
|
com.google.guava/guava {:mvn/version "33.4.8-jre"}
|
||||||
|
|
||||||
funcool/yetti
|
funcool/yetti
|
||||||
{:git/tag "v11.8"
|
{:git/tag "v11.9"
|
||||||
:git/sha "1d1b33f"
|
:git/sha "5fad7a9"
|
||||||
:git/url "https://github.com/funcool/yetti.git"
|
:git/url "https://github.com/funcool/yetti.git"
|
||||||
:exclusions [org.slf4j/slf4j-api]}
|
:exclusions [org.slf4j/slf4j-api]}
|
||||||
|
|
||||||
|
|||||||
@ -98,7 +98,6 @@
|
|||||||
[:http-server-port {:optional true} ::sm/int]
|
[:http-server-port {:optional true} ::sm/int]
|
||||||
[:http-server-host {:optional true} :string]
|
[:http-server-host {:optional true} :string]
|
||||||
[:http-server-max-body-size {:optional true} ::sm/int]
|
[: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-io-threads {:optional true} ::sm/int]
|
||||||
[:http-server-max-worker-threads {:optional true} ::sm/int]
|
[:http-server-max-worker-threads {:optional true} ::sm/int]
|
||||||
|
|
||||||
|
|||||||
@ -42,8 +42,8 @@
|
|||||||
(def default-params
|
(def default-params
|
||||||
{::port 6060
|
{::port 6060
|
||||||
::host "0.0.0.0"
|
::host "0.0.0.0"
|
||||||
::max-body-size 31457280 ; default 30 MiB
|
::max-body-size 367001600 ; default 350 MiB
|
||||||
::max-multipart-body-size 367001600}) ; default 350 MiB
|
})
|
||||||
|
|
||||||
(defmethod ig/expand-key ::server
|
(defmethod ig/expand-key ::server
|
||||||
[k v]
|
[k v]
|
||||||
@ -56,7 +56,6 @@
|
|||||||
[::io-threads {:optional true} ::sm/int]
|
[::io-threads {:optional true} ::sm/int]
|
||||||
[::max-worker-threads {:optional true} ::sm/int]
|
[::max-worker-threads {:optional true} ::sm/int]
|
||||||
[::max-body-size {: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?]]
|
[::router {:optional true} [:fn r/router?]]
|
||||||
[::handler {:optional true} ::sm/fn]])
|
[::handler {:optional true} ::sm/fn]])
|
||||||
|
|
||||||
@ -79,7 +78,7 @@
|
|||||||
{:http/port port
|
{:http/port port
|
||||||
:http/host host
|
:http/host host
|
||||||
:http/max-body-size (::max-body-size cfg)
|
: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/direct-buffers false
|
||||||
:xnio/io-threads (::io-threads cfg)
|
:xnio/io-threads (::io-threads cfg)
|
||||||
:xnio/max-worker-threads (::max-worker-threads cfg)
|
:xnio/max-worker-threads (::max-worker-threads cfg)
|
||||||
|
|||||||
@ -226,11 +226,10 @@
|
|||||||
::http/server
|
::http/server
|
||||||
{::http/port (cf/get :http-server-port)
|
{::http/port (cf/get :http-server-port)
|
||||||
::http/host (cf/get :http-server-host)
|
::http/host (cf/get :http-server-host)
|
||||||
::http/router (ig/ref ::http/router)
|
|
||||||
::http/io-threads (cf/get :http-server-io-threads)
|
::http/io-threads (cf/get :http-server-io-threads)
|
||||||
::http/max-worker-threads (cf/get :http-server-max-worker-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-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)}
|
::mtx/metrics (ig/ref ::mtx/metrics)}
|
||||||
|
|
||||||
::ldap/provider
|
::ldap/provider
|
||||||
|
|||||||
@ -605,31 +605,31 @@
|
|||||||
add-undo-change-shape
|
add-undo-change-shape
|
||||||
(fn [change-set id]
|
(fn [change-set id]
|
||||||
(let [shape (get objects id)]
|
(let [shape (get objects id)]
|
||||||
(conj
|
(cond-> change-set
|
||||||
change-set
|
(some? shape)
|
||||||
{:type :add-obj
|
(conj {:type :add-obj
|
||||||
:id id
|
:id id
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
:parent-id (:parent-id shape)
|
:parent-id (:parent-id shape)
|
||||||
:frame-id (:frame-id shape)
|
:frame-id (:frame-id shape)
|
||||||
:index (cfh/get-position-on-parent objects id)
|
:index (cfh/get-position-on-parent objects id)
|
||||||
:obj (cond-> shape
|
:obj (cond-> shape
|
||||||
(contains? shape :shapes)
|
(contains? shape :shapes)
|
||||||
(assoc :shapes []))})))
|
(assoc :shapes []))}))))
|
||||||
|
|
||||||
add-undo-change-parent
|
add-undo-change-parent
|
||||||
(fn [change-set id]
|
(fn [change-set id]
|
||||||
(let [shape (get objects id)
|
(let [shape (get objects id)
|
||||||
prev-sibling (cfh/get-prev-sibling objects (:id shape))]
|
prev-sibling (cfh/get-prev-sibling objects (:id shape))]
|
||||||
(conj
|
(cond-> change-set
|
||||||
change-set
|
(some? shape)
|
||||||
{:type :mov-objects
|
(conj {:type :mov-objects
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
:parent-id (:parent-id shape)
|
:parent-id (:parent-id shape)
|
||||||
:shapes [id]
|
:shapes [id]
|
||||||
:after-shape prev-sibling
|
:after-shape prev-sibling
|
||||||
:index 0
|
:index 0
|
||||||
:ignore-touched true})))]
|
:ignore-touched true}))))]
|
||||||
|
|
||||||
(-> changes
|
(-> changes
|
||||||
(update :redo-changes #(reduce add-redo-change % ids))
|
(update :redo-changes #(reduce add-redo-change % ids))
|
||||||
@ -1150,3 +1150,24 @@
|
|||||||
[changes]
|
[changes]
|
||||||
(::page-id (meta 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))))
|
||||||
|
|||||||
@ -68,7 +68,7 @@ RUN set -eux; \
|
|||||||
--no-header-files \
|
--no-header-files \
|
||||||
--no-man-pages \
|
--no-man-pages \
|
||||||
--strip-debug \
|
--strip-debug \
|
||||||
--add-modules java.base,jdk.management.agent,java.se,jdk.compiler,jdk.javadoc,jdk.attach,jdk.unsupported \
|
--add-modules java.base,jdk.management.agent,java.se,jdk.compiler,jdk.javadoc,jdk.attach,jdk.unsupported,jdk.jfr,jdk.jcmd \
|
||||||
--output /opt/jre;
|
--output /opt/jre;
|
||||||
|
|
||||||
FROM ubuntu:24.04 AS image
|
FROM ubuntu:24.04 AS image
|
||||||
|
|||||||
@ -30,11 +30,9 @@ x-uri: &penpot-public-uri
|
|||||||
PENPOT_PUBLIC_URI: http://localhost:9001
|
PENPOT_PUBLIC_URI: http://localhost:9001
|
||||||
|
|
||||||
x-body-size: &penpot-http-body-size
|
x-body-size: &penpot-http-body-size
|
||||||
# Max body size (30MiB); Used for plain requests, should never be
|
# Max body size
|
||||||
# greater than multi-part size
|
PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 367001600
|
||||||
PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 31457280
|
# Deprecation warning: this variable is deprecated. Use PENPOT_HTTP_SERVER_MAX_BODY (defaults to 367001600)
|
||||||
|
|
||||||
# Max multipart body size (350MiB)
|
|
||||||
PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE: 367001600
|
PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE: 367001600
|
||||||
|
|
||||||
## Penpot SECRET KEY. It serves as a master key from which other keys for subsystems
|
## Penpot SECRET KEY. It serves as a master key from which other keys for subsystems
|
||||||
|
|||||||
@ -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_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060}
|
||||||
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061}
|
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061}
|
||||||
export PENPOT_NITRATE_URI=${PENPOT_NITRATE_URI:-http://penpot-nitrate:3000}
|
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
|
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_MULTIPART_BODY_SIZE" \
|
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_HTTP_SERVER_MAX_BODY_SIZE" \
|
||||||
< /tmp/nginx.conf.template > /etc/nginx/nginx.conf
|
< /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)"
|
PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)"
|
||||||
|
|||||||
@ -76,7 +76,7 @@ http {
|
|||||||
listen [::]:8080 default_server;
|
listen [::]:8080 default_server;
|
||||||
server_name _;
|
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;
|
charset utf-8;
|
||||||
|
|
||||||
etag off;
|
etag off;
|
||||||
|
|||||||
@ -188,8 +188,8 @@ server {
|
|||||||
server_name penpot.mycompany.com;
|
server_name penpot.mycompany.com;
|
||||||
|
|
||||||
# This value should be in sync with the corresponding in the docker-compose.yml
|
# This value should be in sync with the corresponding in the docker-compose.yml
|
||||||
# PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 31457280
|
# PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 367001600
|
||||||
client_max_body_size 31457280;
|
client_max_body_size 367001600;
|
||||||
|
|
||||||
# Logs: Configure your logs following the best practices inside your company
|
# Logs: Configure your logs following the best practices inside your company
|
||||||
access_log /path/to/penpot.access.log;
|
access_log /path/to/penpot.access.log;
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"~:features": {
|
|
||||||
"~#set": [
|
|
||||||
"variants/v1",
|
|
||||||
"layout/grid",
|
|
||||||
"styles/v2",
|
|
||||||
"fdata/pointer-map",
|
|
||||||
"fdata/objects-map",
|
|
||||||
"components/v2",
|
|
||||||
"fdata/shape-data-type"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"~:permissions": {
|
|
||||||
"~:type": "~:membership",
|
|
||||||
"~:is-owner": true,
|
|
||||||
"~:is-admin": true,
|
|
||||||
"~:can-edit": true
|
|
||||||
},
|
|
||||||
"~:name": "Default",
|
|
||||||
"~:modified-at": "~m1713533116375",
|
|
||||||
"~:id": "~uc7ce0794-0992-8105-8004-38e630f7920a",
|
|
||||||
"~:created-at": "~m1713533116375",
|
|
||||||
"~:is-default": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"~:email": "foo@example.com",
|
|
||||||
"~:is-demo": false,
|
|
||||||
"~:auth-backend": "penpot",
|
|
||||||
"~:fullname": "Princesa Leia",
|
|
||||||
"~:modified-at": "~m1713533116365",
|
|
||||||
"~:is-active": true,
|
|
||||||
"~:default-project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
|
|
||||||
"~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
|
|
||||||
"~:is-muted": false,
|
|
||||||
"~:default-team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
|
|
||||||
"~:created-at": "~m1713533116365",
|
|
||||||
"~:is-blocked": false,
|
|
||||||
"~:props": {
|
|
||||||
"~:nudge": {
|
|
||||||
"~:big": 10,
|
|
||||||
"~:small": 1
|
|
||||||
},
|
|
||||||
"~:v2-info-shown": true,
|
|
||||||
"~:viewed-tutorial?": false,
|
|
||||||
"~:viewed-walkthrough?": false,
|
|
||||||
"~:onboarding-viewed": true,
|
|
||||||
"~:builtin-templates-collapsed-status":
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1161
frontend/playwright/data/render-wasm/get-solid-shadows.json
Normal file
1161
frontend/playwright/data/render-wasm/get-solid-shadows.json
Normal file
File diff suppressed because it is too large
Load Diff
2826
frontend/playwright/data/render-wasm/get-solid-strokes-shadows.json
Normal file
2826
frontend/playwright/data/render-wasm/get-solid-strokes-shadows.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"~:file-id": "~u3a4d7ec7-c391-8146-8007-9a05c41da6b9",
|
||||||
|
"~:id": "~u3a4d7ec7-c391-8146-8007-9dd6c998fbc4",
|
||||||
|
"~:created-at": "~m1771846681191",
|
||||||
|
"~:modified-at": "~m1771846681191",
|
||||||
|
"~:type": "fragment",
|
||||||
|
"~:backend": "db",
|
||||||
|
"~:data": {
|
||||||
|
"~:id": "~u95b23c15-79f9-81ba-8007-99d81b5290dd",
|
||||||
|
"~:name": "Page 1",
|
||||||
|
"~:objects": {
|
||||||
|
"~#penpot/objects-map/v2": {
|
||||||
|
"~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u95b23c15-79f9-81ba-8007-99d81b5290dd\",\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^I\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~ucfb31a9c-83c2-806f-8007-9dbf43043bdf\"]]]",
|
||||||
|
"~ucfb31a9c-83c2-806f-8007-9dbf43043be0": "[\"~#shape\",[\"^ \",\"~:y\",-218.99999605032087,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",5,\"~:p2\",5,\"~:p3\",5,\"~:p4\",5],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:nowrap\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"Container\",\"~:layout-align-items\",\"~:start\",\"~:width\",431.99994866329087,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",608.9999813066789,\"~:y\",-218.99999605032087]],[\"^J\",[\"^ \",\"~:x\",1040.9999299699698,\"~:y\",-218.99999605032087]],[\"^J\",[\"^ \",\"~:x\",1040.9999299699698,\"~:y\",-177.00001533586985]],[\"^J\",[\"^ \",\"~:x\",608.9999813066789,\"~:y\",-177.00001533586985]]],\"~:show-content\",true,\"~:layout-item-h-sizing\",\"~:fill\",\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",4,\"~:column-gap\",4],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u95b23c15-79f9-81ba-8007-99d81b5290dd\",\"~:layout-item-v-sizing\",\"~:auto\",\"~:layout-justify-content\",\"^C\",\"~:constraints-v\",\"~:top\",\"~:constraints-h\",\"~:left\",\"~:id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be0\",\"~:parent-id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043bdf\",\"~:layout-flex-dir\",\"~:column\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043bdf\",\"~:strokes\",[],\"~:x\",608.9999813066788,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",608.9999813066788,\"~:y\",-218.99999605032087,\"^D\",431.99994866329087,\"~:height\",41.99998071445103,\"~:x1\",608.9999813066788,\"~:y1\",-218.99999605032087,\"~:x2\",1040.9999299699698,\"~:y2\",-177.00001533586985]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#ffc0cb\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^1:\",41.99998071445103,\"~:flip-y\",null,\"~:shapes\",[\"~ucfb31a9c-83c2-806f-8007-9dbf43043be2\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be3\"]]]",
|
||||||
|
"~ucfb31a9c-83c2-806f-8007-9dbf43043be2": "[\"~#shape\",[\"^ \",\"~:y\",-178.00000568505413,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:hide-in-viewer\",false,\"~:name\",\"show / hide me\",\"~:width\",99.98206911702209,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",614.0000002576337,\"~:y\",-178.00000568505413]],[\"^:\",[\"^ \",\"~:x\",713.9820693746558,\"~:y\",-178.00000568505413]],[\"^:\",[\"^ \",\"~:x\",713.9820693746558,\"~:y\",-148.0000135081636]],[\"^:\",[\"^ \",\"~:x\",614.0000002576337,\"~:y\",-148.0000135081636]]],\"~:r2\",0,\"~:layout-item-h-sizing\",\"~:fix\",\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:layout-item-v-sizing\",\"^=\",\"~:r3\",0,\"~:constraints-v\",\"~:top\",\"~:constraints-h\",\"~:left\",\"~:r1\",0,\"~:hidden\",true,\"~:id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be2\",\"~:parent-id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be0\",\"~:frame-id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be0\",\"~:strokes\",[],\"~:x\",614.0000002576337,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",614.0000002576337,\"~:y\",-178.00000568505413,\"^6\",99.98206911702209,\"~:height\",29.999992176890544,\"~:x1\",614.0000002576337,\"~:y1\",-178.00000568505413,\"~:x2\",713.9820693746558,\"~:y2\",-148.0000135081636]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^P\",29.999992176890544,\"~:flip-y\",null]]",
|
||||||
|
"~ucfb31a9c-83c2-806f-8007-9dbf43043be3": "[\"~#shape\",[\"^ \",\"~:y\",-213.99999587313152,\"~:hide-fill-on-export\",false,\"~:rx\",8,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:hide-in-viewer\",true,\"~:name\",\"Full width\",\"~:width\",422.00001200500014,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",613.9999939062393,\"~:y\",-213.99999587313152]],[\"^<\",[\"^ \",\"~:x\",1036.0000059112394,\"~:y\",-213.99999587313152]],[\"^<\",[\"^ \",\"~:x\",1036.0000059112394,\"~:y\",-182.00001303926604]],[\"^<\",[\"^ \",\"~:x\",613.9999939062393,\"~:y\",-182.00001303926604]]],\"~:r2\",8,\"~:show-content\",true,\"~:layout-item-h-sizing\",\"~:fix\",\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^4\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u95b23c15-79f9-81ba-8007-99d81b5290dd\",\"~:layout-item-v-sizing\",\"^@\",\"~:r3\",8,\"~:r1\",8,\"~:id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be3\",\"~:parent-id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be0\",\"~:frame-id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be0\",\"~:strokes\",[],\"~:x\",613.9999939062393,\"~:proportion\",1,\"~:r4\",8,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",613.9999939062393,\"~:y\",-213.99999587313152,\"^8\",422.00001200500014,\"~:height\",31.999982833865488,\"~:x1\",613.9999939062393,\"~:y1\",-213.99999587313152,\"~:x2\",1036.0000059112394,\"~:y2\",-182.00001303926604]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#212426\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"~:ry\",8,\"^O\",31.999982833865488,\"~:flip-y\",null,\"~:shapes\",[]]]",
|
||||||
|
"~ucfb31a9c-83c2-806f-8007-9dbf43043bdf": "[\"~#shape\",[\"^ \",\"~:y\",-228.99999763039506,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",10,\"~:p2\",10,\"~:p3\",10,\"~:p4\",10],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:nowrap\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"Parent\",\"~:layout-align-items\",\"~:start\",\"~:width\",451.999905143128,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",599.0000149607649,\"~:y\",-228.99999763039506]],[\"^J\",[\"^ \",\"~:x\",1050.999920103893,\"~:y\",-228.99999763039506]],[\"^J\",[\"^ \",\"~:x\",1050.999920103893,\"~:y\",-167.0000160450801]],[\"^J\",[\"^ \",\"~:x\",599.0000149607649,\"~:y\",-167.0000160450801]]],\"~:r2\",0,\"~:show-content\",true,\"~:layout-item-h-sizing\",\"~:fix\",\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",10,\"~:column-gap\",8],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u95b23c15-79f9-81ba-8007-99d81b5290dd\",\"~:layout-item-v-sizing\",\"~:auto\",\"~:r3\",0,\"~:layout-justify-content\",\"^C\",\"~:r1\",0,\"~:id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043bdf\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:layout-flex-dir\",\"~:column\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",599.0000149607649,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",599.0000149607649,\"~:y\",-228.99999763039506,\"^D\",451.999905143128,\"~:height\",61.99998158531497,\"~:x1\",599.0000149607649,\"~:y1\",-228.99999763039506,\"~:x2\",1050.999920103893,\"~:y2\",-167.0000160450801]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#000000\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^1:\",61.99998158531497,\"~:flip-y\",null,\"~:shapes\",[\"~ucfb31a9c-83c2-806f-8007-9dbf43043be0\"]]]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
131
frontend/playwright/data/workspace/get-file-13468.json
Normal file
131
frontend/playwright/data/workspace/get-file-13468.json
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
{
|
||||||
|
"~:features": {
|
||||||
|
"~#set": [
|
||||||
|
"fdata/path-data",
|
||||||
|
"design-tokens/v1",
|
||||||
|
"variants/v1",
|
||||||
|
"layout/grid",
|
||||||
|
"fdata/pointer-map",
|
||||||
|
"fdata/objects-map",
|
||||||
|
"components/v2",
|
||||||
|
"fdata/shape-data-type"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"~:team-id": "~ud715d0a5-a44e-8056-8005-a79999e18b64",
|
||||||
|
"~:permissions": {
|
||||||
|
"~:type": "~:membership",
|
||||||
|
"~:is-owner": true,
|
||||||
|
"~:is-admin": true,
|
||||||
|
"~:can-edit": true,
|
||||||
|
"~:can-read": true,
|
||||||
|
"~:is-logged": true
|
||||||
|
},
|
||||||
|
"~:has-media-trimmed": false,
|
||||||
|
"~:comment-thread-seqn": 0,
|
||||||
|
"~:name": "test-bug-flex",
|
||||||
|
"~:revn": 114,
|
||||||
|
"~:modified-at": "~m1771846681183",
|
||||||
|
"~:vern": 0,
|
||||||
|
"~:id": "~u3a4d7ec7-c391-8146-8007-9a05c41da6b9",
|
||||||
|
"~:is-shared": false,
|
||||||
|
"~:migrations": {
|
||||||
|
"~#ordered-set": [
|
||||||
|
"legacy-2",
|
||||||
|
"legacy-3",
|
||||||
|
"legacy-5",
|
||||||
|
"legacy-6",
|
||||||
|
"legacy-7",
|
||||||
|
"legacy-8",
|
||||||
|
"legacy-9",
|
||||||
|
"legacy-10",
|
||||||
|
"legacy-11",
|
||||||
|
"legacy-12",
|
||||||
|
"legacy-13",
|
||||||
|
"legacy-14",
|
||||||
|
"legacy-16",
|
||||||
|
"legacy-17",
|
||||||
|
"legacy-18",
|
||||||
|
"legacy-19",
|
||||||
|
"legacy-25",
|
||||||
|
"legacy-26",
|
||||||
|
"legacy-27",
|
||||||
|
"legacy-28",
|
||||||
|
"legacy-29",
|
||||||
|
"legacy-31",
|
||||||
|
"legacy-32",
|
||||||
|
"legacy-33",
|
||||||
|
"legacy-34",
|
||||||
|
"legacy-36",
|
||||||
|
"legacy-37",
|
||||||
|
"legacy-38",
|
||||||
|
"legacy-39",
|
||||||
|
"legacy-40",
|
||||||
|
"legacy-41",
|
||||||
|
"legacy-42",
|
||||||
|
"legacy-43",
|
||||||
|
"legacy-44",
|
||||||
|
"legacy-45",
|
||||||
|
"legacy-46",
|
||||||
|
"legacy-47",
|
||||||
|
"legacy-48",
|
||||||
|
"legacy-49",
|
||||||
|
"legacy-50",
|
||||||
|
"legacy-51",
|
||||||
|
"legacy-52",
|
||||||
|
"legacy-53",
|
||||||
|
"legacy-54",
|
||||||
|
"legacy-55",
|
||||||
|
"legacy-56",
|
||||||
|
"legacy-57",
|
||||||
|
"legacy-59",
|
||||||
|
"legacy-62",
|
||||||
|
"legacy-65",
|
||||||
|
"legacy-66",
|
||||||
|
"legacy-67",
|
||||||
|
"0001-remove-tokens-from-groups",
|
||||||
|
"0002-normalize-bool-content-v2",
|
||||||
|
"0002-clean-shape-interactions",
|
||||||
|
"0003-fix-root-shape",
|
||||||
|
"0003-convert-path-content-v2",
|
||||||
|
"0005-deprecate-image-type",
|
||||||
|
"0006-fix-old-texts-fills",
|
||||||
|
"0008-fix-library-colors-v4",
|
||||||
|
"0009-clean-library-colors",
|
||||||
|
"0009-add-partial-text-touched-flags",
|
||||||
|
"0010-fix-swap-slots-pointing-non-existent-shapes",
|
||||||
|
"0011-fix-invalid-text-touched-flags",
|
||||||
|
"0012-fix-position-data",
|
||||||
|
"0013-fix-component-path",
|
||||||
|
"0013-clear-invalid-strokes-and-fills",
|
||||||
|
"0014-fix-tokens-lib-duplicate-ids",
|
||||||
|
"0014-clear-components-nil-objects",
|
||||||
|
"0015-fix-text-attrs-blank-strings",
|
||||||
|
"0015-clean-shadow-color",
|
||||||
|
"0016-copy-fills-from-position-data-to-text-node"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"~:version": 67,
|
||||||
|
"~:project-id": "~u76eab896-accf-81a5-8007-2b264ebe7817",
|
||||||
|
"~:created-at": "~m1771590560885",
|
||||||
|
"~:backend": "legacy-db",
|
||||||
|
"~:data": {
|
||||||
|
"~:pages": [
|
||||||
|
"~u95b23c15-79f9-81ba-8007-99d81b5290dd"
|
||||||
|
],
|
||||||
|
"~:pages-index": {
|
||||||
|
"~u95b23c15-79f9-81ba-8007-99d81b5290dd": {
|
||||||
|
"~#penpot/pointer": [
|
||||||
|
"~u3a4d7ec7-c391-8146-8007-9dd6c998fbc4",
|
||||||
|
{
|
||||||
|
"~:created-at": "~m1771846681187"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:id": "~u3a4d7ec7-c391-8146-8007-9a05c41da6b9",
|
||||||
|
"~:options": {
|
||||||
|
"~:components-v2": true,
|
||||||
|
"~:base-font-size": "16px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,8 @@
|
|||||||
export class BasePage {
|
export class BasePage {
|
||||||
|
static async init(page) {
|
||||||
|
await BasePage.mockConfigFlags(page, []);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mocks multiple RPC calls in a single call.
|
* Mocks multiple RPC calls in a single call.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -2,13 +2,8 @@ import { MockWebSocketHelper } from "../../helpers/MockWebSocketHelper";
|
|||||||
import BasePage from "./BasePage";
|
import BasePage from "./BasePage";
|
||||||
|
|
||||||
export class BaseWebSocketPage extends BasePage {
|
export class BaseWebSocketPage extends BasePage {
|
||||||
/**
|
static async init(page) {
|
||||||
* This should be called on `test.beforeEach`.
|
await super.init(page);
|
||||||
*
|
|
||||||
* @param {Page} page
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
static async initWebSockets(page) {
|
|
||||||
await MockWebSocketHelper.init(page);
|
await MockWebSocketHelper.init(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,54 +3,62 @@ import { BaseWebSocketPage } from "./BaseWebSocketPage";
|
|||||||
|
|
||||||
export class DashboardPage extends BaseWebSocketPage {
|
export class DashboardPage extends BaseWebSocketPage {
|
||||||
static async init(page) {
|
static async init(page) {
|
||||||
await BaseWebSocketPage.initWebSockets(page);
|
await super.init(page);
|
||||||
|
|
||||||
await BaseWebSocketPage.mockRPC(
|
await super.mockConfigFlags(page, ["disable-onboarding"]);
|
||||||
|
|
||||||
|
await super.mockRPC(
|
||||||
page,
|
page,
|
||||||
"get-teams",
|
"get-teams",
|
||||||
"logged-in-user/get-teams-default.json",
|
"logged-in-user/get-teams-default.json",
|
||||||
);
|
);
|
||||||
await BaseWebSocketPage.mockRPC(
|
await super.mockRPC(
|
||||||
page,
|
page,
|
||||||
"get-font-variants?team-id=*",
|
"get-font-variants?team-id=*",
|
||||||
"workspace/get-font-variants-empty.json",
|
"workspace/get-font-variants-empty.json",
|
||||||
);
|
);
|
||||||
|
|
||||||
await BaseWebSocketPage.mockRPC(
|
await super.mockRPC(
|
||||||
page,
|
page,
|
||||||
"get-projects?team-id=*",
|
"get-projects?team-id=*",
|
||||||
"logged-in-user/get-projects-default.json",
|
"logged-in-user/get-projects-default.json",
|
||||||
);
|
);
|
||||||
await BaseWebSocketPage.mockRPC(
|
await super.mockRPC(
|
||||||
page,
|
page,
|
||||||
"get-team-members?team-id=*",
|
"get-team-members?team-id=*",
|
||||||
"logged-in-user/get-team-members-your-penpot.json",
|
"logged-in-user/get-team-members-your-penpot.json",
|
||||||
);
|
);
|
||||||
await BaseWebSocketPage.mockRPC(
|
await super.mockRPC(
|
||||||
page,
|
page,
|
||||||
"get-team-users?team-id=*",
|
"get-team-users?team-id=*",
|
||||||
"logged-in-user/get-team-users-single-user.json",
|
"logged-in-user/get-team-users-single-user.json",
|
||||||
);
|
);
|
||||||
await BaseWebSocketPage.mockRPC(
|
await super.mockRPC(
|
||||||
page,
|
page,
|
||||||
"get-unread-comment-threads?team-id=*",
|
"get-unread-comment-threads?team-id=*",
|
||||||
"logged-in-user/get-team-users-single-user.json",
|
"logged-in-user/get-team-users-single-user.json",
|
||||||
);
|
);
|
||||||
await BaseWebSocketPage.mockRPC(
|
await super.mockRPC(
|
||||||
page,
|
page,
|
||||||
"get-team-recent-files?team-id=*",
|
"get-team-recent-files?team-id=*",
|
||||||
"logged-in-user/get-team-recent-files-empty.json",
|
"logged-in-user/get-team-recent-files-empty.json",
|
||||||
);
|
);
|
||||||
await BaseWebSocketPage.mockRPC(
|
await super.mockRPC(
|
||||||
page,
|
page,
|
||||||
"get-profiles-for-file-comments",
|
"get-profiles-for-file-comments",
|
||||||
"workspace/get-profile-for-file-comments.json",
|
"workspace/get-profile-for-file-comments.json",
|
||||||
);
|
);
|
||||||
await BaseWebSocketPage.mockRPC(
|
await super.mockRPC(
|
||||||
page,
|
page,
|
||||||
"get-builtin-templates",
|
"get-builtin-templates",
|
||||||
"logged-in-user/get-built-in-templates-empty.json",
|
"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";
|
static anyTeamId = "c7ce0794-0992-8105-8004-38e630f40f6d";
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import { BasePage } from "./BasePage";
|
import { BasePage } from "./BasePage";
|
||||||
|
|
||||||
export class LoginPage extends BasePage {
|
export class LoginPage extends BasePage {
|
||||||
|
static async init(page) {
|
||||||
|
await super.init(page);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(page) {
|
constructor(page) {
|
||||||
super(page);
|
super(page);
|
||||||
this.loginButton = page.getByRole("button", { name: "Continue" });
|
this.loginButton = page.getByRole("button", { name: "Continue" });
|
||||||
|
|||||||
@ -29,8 +29,13 @@ export class RegisterPage extends BasePage {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async init(page) {
|
||||||
|
await BasePage.init(page);
|
||||||
|
}
|
||||||
|
|
||||||
static async initWithLoggedOutUser(page) {
|
static async initWithLoggedOutUser(page) {
|
||||||
await this.mockRPC(page, "get-profile", "get-profile-anonymous.json");
|
await BasePage.init(page);
|
||||||
|
await BasePage.mockRPC(page, "get-profile", "get-profile-anonymous.json");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,9 @@ import { DashboardPage } from "./DashboardPage";
|
|||||||
|
|
||||||
export class SubscriptionProfilePage extends DashboardPage {
|
export class SubscriptionProfilePage extends DashboardPage {
|
||||||
static async init(page) {
|
static async init(page) {
|
||||||
await DashboardPage.initWebSockets(page);
|
await super.init(page);
|
||||||
|
|
||||||
await DashboardPage.mockRPC(
|
await super.mockRPC(
|
||||||
page,
|
page,
|
||||||
"get-subscription-usage",
|
"get-subscription-usage",
|
||||||
"subscription/get-subscription-usage.json",
|
"subscription/get-subscription-usage.json",
|
||||||
|
|||||||
@ -4,16 +4,6 @@ export class ViewerPage extends BaseWebSocketPage {
|
|||||||
static anyFileId = "c7ce0794-0992-8105-8004-38f280443849";
|
static anyFileId = "c7ce0794-0992-8105-8004-38f280443849";
|
||||||
static anyPageId = "c7ce0794-0992-8105-8004-38f28044384a";
|
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() {
|
async setupLoggedInUser() {
|
||||||
await this.mockRPC(
|
await this.mockRPC(
|
||||||
"get-profile",
|
"get-profile",
|
||||||
|
|||||||
@ -45,24 +45,27 @@ export class WorkspacePage extends BaseWebSocketPage {
|
|||||||
return this.waitForEditor();
|
return this.waitForEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
stopEditing() {
|
async stopEditing() {
|
||||||
return this.page.keyboard.press("Escape");
|
await this.page.keyboard.press("Escape");
|
||||||
}
|
}
|
||||||
|
|
||||||
async moveToLeft(amount = 0) {
|
async moveToLeft(amount = 0) {
|
||||||
for (let i = 0; i < amount; i++) {
|
for (let i = 0; i < amount; i++) {
|
||||||
await this.page.keyboard.press("ArrowLeft");
|
await this.page.keyboard.press("ArrowLeft");
|
||||||
}
|
}
|
||||||
|
await this.waitForIdle();
|
||||||
}
|
}
|
||||||
|
|
||||||
async moveToRight(amount = 0) {
|
async moveToRight(amount = 0) {
|
||||||
for (let i = 0; i < amount; i++) {
|
for (let i = 0; i < amount; i++) {
|
||||||
await this.page.keyboard.press("ArrowRight");
|
await this.page.keyboard.press("ArrowRight");
|
||||||
}
|
}
|
||||||
|
await this.waitForIdle();
|
||||||
}
|
}
|
||||||
|
|
||||||
async moveFromStart(offset = 0) {
|
async moveFromStart(offset = 0) {
|
||||||
await this.page.keyboard.press("Home");
|
await this.page.keyboard.press("Home");
|
||||||
|
await this.waitForIdle();
|
||||||
await this.moveToRight(offset);
|
await this.moveToRight(offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,6 +106,10 @@ export class WorkspacePage extends BaseWebSocketPage {
|
|||||||
changeLetterSpacing(newValue) {
|
changeLetterSpacing(newValue) {
|
||||||
return this.changeNumericInput(this.letterSpacing, newValue);
|
return this.changeNumericInput(this.letterSpacing, newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async waitForIdle() {
|
||||||
|
await this.page.evaluate(() => new Promise((resolve) => globalThis.requestIdleCallback(resolve)));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -112,9 +119,9 @@ export class WorkspacePage extends BaseWebSocketPage {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static async init(page) {
|
static async init(page) {
|
||||||
await BaseWebSocketPage.initWebSockets(page);
|
await super.init(page);
|
||||||
|
|
||||||
await BaseWebSocketPage.mockRPCs(page, {
|
await super.mockRPCs(page, {
|
||||||
"get-profile": "logged-in-user/get-profile-logged-in.json",
|
"get-profile": "logged-in-user/get-profile-logged-in.json",
|
||||||
"get-team-users?file-id=*":
|
"get-team-users?file-id=*":
|
||||||
"logged-in-user/get-team-users-single-user.json",
|
"logged-in-user/get-team-users-single-user.json",
|
||||||
|
|||||||
@ -243,6 +243,46 @@ test("Renders a file with a closed path shape with multiple segments using strok
|
|||||||
await expect(workspace.canvas).toHaveScreenshot();
|
await expect(workspace.canvas).toHaveScreenshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Renders solid shadows after select all and zoom to selected", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const workspace = new WasmWorkspacePage(page);
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.mockGetFile("render-wasm/get-solid-shadows.json");
|
||||||
|
|
||||||
|
await workspace.goToWorkspace({
|
||||||
|
id: "93113137-fe66-80fb-8007-99ca9fd96841",
|
||||||
|
pageId: "93113137-fe66-80fb-8007-99ca9fd96842",
|
||||||
|
});
|
||||||
|
await workspace.waitForFirstRender();
|
||||||
|
|
||||||
|
await workspace.viewport.click();
|
||||||
|
await page.keyboard.press("ControlOrMeta+A");
|
||||||
|
const previousRenderCount = await workspace.getRenderCount();
|
||||||
|
await page.keyboard.press("f");
|
||||||
|
await workspace.waitForNextRender(previousRenderCount);
|
||||||
|
|
||||||
|
await workspace.hideUI();
|
||||||
|
await expect(workspace.canvas).toHaveScreenshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Renders strokes with solid shadows", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const workspace = new WasmWorkspacePage(page);
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.mockGetFile("render-wasm/get-solid-strokes-shadows.json");
|
||||||
|
|
||||||
|
await workspace.goToWorkspace({
|
||||||
|
id: "93113137-fe66-80fb-8007-99cfd5cbf361",
|
||||||
|
pageId: "93113137-fe66-80fb-8007-99cfd5cbf362",
|
||||||
|
});
|
||||||
|
await workspace.waitForFirstRender();
|
||||||
|
|
||||||
|
await workspace.hideUI();
|
||||||
|
await expect(workspace.canvas).toHaveScreenshot();
|
||||||
|
});
|
||||||
|
|
||||||
test("Renders a file with paths and svg attrs", async ({ page }) => {
|
test("Renders a file with paths and svg attrs", async ({ page }) => {
|
||||||
const workspace = new WasmWorkspacePage(page);
|
const workspace = new WasmWorkspacePage(page);
|
||||||
await workspace.setupEmptyFile();
|
await workspace.setupEmptyFile();
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 260 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 140 KiB |
@ -3,11 +3,6 @@ import DashboardPage from "../pages/DashboardPage";
|
|||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await DashboardPage.init(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", () => {
|
test.describe("Dashboard Deleted Page", () => {
|
||||||
|
|||||||
@ -3,11 +3,6 @@ import DashboardPage from "../pages/DashboardPage";
|
|||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await DashboardPage.init(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 }) => {
|
test("BUG 10421 - Fix libraries context menu", async ({ page }) => {
|
||||||
|
|||||||
@ -3,11 +3,6 @@ import DashboardPage from "../pages/DashboardPage";
|
|||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await DashboardPage.init(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 ({
|
test("BUG 12359 - Selected invitations count is not pluralized", async ({
|
||||||
|
|||||||
@ -3,11 +3,7 @@ import DashboardPage from "../pages/DashboardPage";
|
|||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await DashboardPage.init(page);
|
await DashboardPage.init(page);
|
||||||
await DashboardPage.mockRPC(
|
|
||||||
page,
|
|
||||||
"get-profile",
|
|
||||||
"logged-in-user/get-profile-logged-in-no-onboarding.json",
|
|
||||||
);
|
|
||||||
await DashboardPage.mockRPC(
|
await DashboardPage.mockRPC(
|
||||||
page,
|
page,
|
||||||
"get-teams",
|
"get-teams",
|
||||||
|
|||||||
@ -3,11 +3,6 @@ import DashboardPage from "../pages/DashboardPage";
|
|||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await DashboardPage.init(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 }) => {
|
test("Dashboad page has title ", async ({ page }) => {
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { test, expect } from "@playwright/test";
|
|||||||
import { LoginPage } from "../pages/LoginPage";
|
import { LoginPage } from "../pages/LoginPage";
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await LoginPage.init(page);
|
||||||
|
|
||||||
const login = new LoginPage(page);
|
const login = new LoginPage(page);
|
||||||
await login.initWithLoggedOutUser();
|
await login.initWithLoggedOutUser();
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import OnboardingPage from "../pages/OnboardingPage";
|
|||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await DashboardPage.init(page);
|
await DashboardPage.init(page);
|
||||||
|
await DashboardPage.mockConfigFlags(page, ["enable-onboarding"]);
|
||||||
await DashboardPage.mockRPC(
|
await DashboardPage.mockRPC(
|
||||||
page,
|
page,
|
||||||
"get-profile",
|
"get-profile",
|
||||||
|
|||||||
@ -3,11 +3,6 @@ import DashboardPage from "../pages/DashboardPage";
|
|||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await DashboardPage.init(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 }) => {
|
test("Navigate to penpot changelog from profile menu", async ({ page }) => {
|
||||||
|
|||||||
@ -2,8 +2,6 @@ import { test, expect } from "@playwright/test";
|
|||||||
import { Clipboard } from "../../helpers/Clipboard";
|
import { Clipboard } from "../../helpers/Clipboard";
|
||||||
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
|
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
|
||||||
|
|
||||||
const timeToWait = 100;
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page, context }) => {
|
test.beforeEach(async ({ page, context }) => {
|
||||||
await Clipboard.enable(context, Clipboard.Permission.ALL);
|
await Clipboard.enable(context, Clipboard.Permission.ALL);
|
||||||
|
|
||||||
@ -37,11 +35,13 @@ test("Create a new text shape from pasting text", async ({ page, context }) => {
|
|||||||
await workspace.setupEmptyFile();
|
await workspace.setupEmptyFile();
|
||||||
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||||
await workspace.goToWorkspace();
|
await workspace.goToWorkspace();
|
||||||
|
await workspace.moveButton.click();
|
||||||
|
|
||||||
await Clipboard.writeText(page, textToPaste);
|
await Clipboard.writeText(page, textToPaste);
|
||||||
|
|
||||||
await workspace.clickAt(190, 150);
|
await workspace.clickAt(190, 150);
|
||||||
await workspace.paste("keyboard");
|
await workspace.paste("keyboard");
|
||||||
|
|
||||||
await workspace.textEditor.stopEditing();
|
await workspace.textEditor.stopEditing();
|
||||||
|
|
||||||
await expect(workspace.layers.getByText(textToPaste)).toBeVisible();
|
await expect(workspace.layers.getByText(textToPaste)).toBeVisible();
|
||||||
@ -57,6 +57,7 @@ test("Create a new text shape from pasting text using context menu", async ({
|
|||||||
});
|
});
|
||||||
await workspace.setupEmptyFile();
|
await workspace.setupEmptyFile();
|
||||||
await workspace.goToWorkspace();
|
await workspace.goToWorkspace();
|
||||||
|
await workspace.moveButton.click();
|
||||||
|
|
||||||
await Clipboard.writeText(page, textToPaste);
|
await Clipboard.writeText(page, textToPaste);
|
||||||
|
|
||||||
@ -138,7 +139,7 @@ test("Update a new text shape appending text by pasting text", async ({
|
|||||||
await workspace.paste("keyboard");
|
await workspace.paste("keyboard");
|
||||||
await workspace.textEditor.stopEditing();
|
await workspace.textEditor.stopEditing();
|
||||||
await workspace.waitForSelectedShapeName("Lorem ipsum dolor sit amet");
|
await workspace.waitForSelectedShapeName("Lorem ipsum dolor sit amet");
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip("Update a new text shape prepending text by pasting text", async ({
|
test.skip("Update a new text shape prepending text by pasting text", async ({
|
||||||
page,
|
page,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
|
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
|
||||||
import { BaseWebSocketPage } from "../pages/BaseWebSocketPage";
|
import { BaseWebSocketPage } from "../pages/BaseWebSocketPage";
|
||||||
import { Clipboard } from "../../helpers/Clipboard";
|
import { Clipboard } from "../../helpers/Clipboard";
|
||||||
|
|
||||||
@ -7,7 +7,7 @@ test.beforeEach(async ({ page, context }) => {
|
|||||||
await Clipboard.enable(context, Clipboard.Permission.ALL);
|
await Clipboard.enable(context, Clipboard.Permission.ALL);
|
||||||
|
|
||||||
await WasmWorkspacePage.init(page);
|
await WasmWorkspacePage.init(page);
|
||||||
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-variants.json");
|
await WasmWorkspacePage.mockConfigFlags(page, ["enable-feature-variants"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterEach(async ({ context }) => {
|
test.afterEach(async ({ context }) => {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
|
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
|
||||||
import { presenceFixture } from "../../data/workspace/ws-notifications";
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await WasmWorkspacePage.init(page);
|
await WasmWorkspacePage.init(page);
|
||||||
@ -106,7 +105,7 @@ test("BUG 11006 - Fix history panel shortcut", async ({ page }) => {
|
|||||||
|
|
||||||
await workspacePage.goToWorkspace();
|
await workspacePage.goToWorkspace();
|
||||||
|
|
||||||
await page.keyboard.press("Control+Alt+h");
|
await page.keyboard.press("ControlOrMeta+Alt+h");
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
workspacePage.rightSidebar.getByText("There are no versions yet"),
|
workspacePage.rightSidebar.getByText("There are no versions yet"),
|
||||||
|
|||||||
@ -55,3 +55,31 @@ test("BUG 13382 - Fix problem with flex layout", async ({ page }) => {
|
|||||||
await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("340");
|
await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("340");
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("BUG 13468 - Fix problem with flex propagation", async ({ page }) => {
|
||||||
|
const workspacePage = new WasmWorkspacePage(page);
|
||||||
|
await workspacePage.setupEmptyFile();
|
||||||
|
await workspacePage.mockGetFile("workspace/get-file-13468.json");
|
||||||
|
|
||||||
|
await workspacePage.mockRPC(
|
||||||
|
"get-file-fragment?file-id=*&fragment-id=*",
|
||||||
|
"workspace/get-file-13468-fragment.json",
|
||||||
|
);
|
||||||
|
|
||||||
|
await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-empty.json");
|
||||||
|
|
||||||
|
await workspacePage.goToWorkspace({
|
||||||
|
fileId: "3a4d7ec7-c391-8146-8007-9a05c41da6b9",
|
||||||
|
pageId: "95b23c15-79f9-81ba-8007-99d81b5290dd",
|
||||||
|
});
|
||||||
|
0
|
||||||
|
await workspacePage.clickToggableLayer("Parent");
|
||||||
|
await workspacePage.clickToggableLayer("Container");
|
||||||
|
|
||||||
|
await workspacePage.sidebar.getByRole('button', { name: 'Show' }).click();
|
||||||
|
|
||||||
|
await workspacePage.clickLeafLayer("Container");
|
||||||
|
await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("76");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -3,11 +3,6 @@ import DashboardPage from "../pages/DashboardPage";
|
|||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await DashboardPage.init(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 }) => {
|
test("User goes to an empty dashboard", async ({ page }) => {
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { test, expect } from "@playwright/test";
|
|||||||
import { LoginPage } from "../pages/LoginPage";
|
import { LoginPage } from "../pages/LoginPage";
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await LoginPage.init(page);
|
||||||
|
|
||||||
const login = new LoginPage(page);
|
const login = new LoginPage(page);
|
||||||
await login.initWithLoggedOutUser();
|
await login.initWithLoggedOutUser();
|
||||||
await login.page.goto("/#/auth/login");
|
await login.page.goto("/#/auth/login");
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
<meta name="twitter:creator" content="@penpotapp">
|
<meta name="twitter:creator" content="@penpotapp">
|
||||||
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
|
<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 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}}
|
{{#isDebug}}
|
||||||
<link href="css/debug.css?version={{& version_tag}}" rel="stylesheet" type="text/css" />
|
<link href="css/debug.css?version={{& version_tag}}" rel="stylesheet" type="text/css" />
|
||||||
{{/isDebug}}
|
{{/isDebug}}
|
||||||
|
|||||||
@ -4,4 +4,9 @@ TARGET=${1:-app};
|
|||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
exec pnpm run watch:$TARGET
|
rm -rf node_modules;
|
||||||
|
|
||||||
|
corepack enable;
|
||||||
|
corepack install;
|
||||||
|
pnpm install;
|
||||||
|
pnpm run watch:$TARGET
|
||||||
|
|||||||
@ -79,7 +79,7 @@
|
|||||||
Structured tokens are non-primitive token types like `typography` or `box-shadow`."
|
Structured tokens are non-primitive token types like `typography` or `box-shadow`."
|
||||||
[^js token-symbol]
|
[^js token-symbol]
|
||||||
(if (instance? js/Array (.-value 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)))]
|
(let [entries (es6-iterator-seq (.entries (.-value token-symbol)))]
|
||||||
(into {} (map (fn [[k v :as V]]
|
(into {} (map (fn [[k v :as V]]
|
||||||
[(keyword k) (tokenscript-symbols->penpot-unit v)])
|
[(keyword k) (tokenscript-symbols->penpot-unit v)])
|
||||||
@ -88,7 +88,7 @@
|
|||||||
(defn tokenscript-symbols->penpot-unit [^js v]
|
(defn tokenscript-symbols->penpot-unit [^js v]
|
||||||
(cond
|
(cond
|
||||||
(structured-token? v) (structured-token->penpot-map v)
|
(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"))
|
(color-symbol? v) (.-value (.to v "hex"))
|
||||||
(rem-number-with-unit? v) (rem->px v)
|
(rem-number-with-unit? v) (rem->px v)
|
||||||
:else (.-value v)))
|
:else (.-value v)))
|
||||||
|
|||||||
@ -197,11 +197,12 @@
|
|||||||
objects (:objects page)
|
objects (:objects page)
|
||||||
|
|
||||||
undo-id (or (:undo-id options) (js/Symbol))
|
undo-id (or (:undo-id options) (js/Symbol))
|
||||||
[all-parents changes] (-> (pcb/empty-changes it (:id page))
|
[all-parents changes]
|
||||||
(cls/generate-delete-shapes fdata page objects ids
|
(-> (pcb/empty-changes it (:id page))
|
||||||
{:ignore-touched (:allow-altering-copies options)
|
(cls/generate-delete-shapes fdata page objects ids
|
||||||
:undo-group (:undo-group options)
|
{:ignore-touched (:allow-altering-copies options)
|
||||||
:undo-id undo-id}))]
|
:undo-group (:undo-group options)
|
||||||
|
:undo-id undo-id}))]
|
||||||
|
|
||||||
(rx/of (dwu/start-undo-transaction undo-id)
|
(rx/of (dwu/start-undo-transaction undo-id)
|
||||||
(dc/detach-comment-thread ids)
|
(dc/detach-comment-thread ids)
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
[app.common.attrs :as attrs]
|
[app.common.attrs :as attrs]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.files.changes-builder :as pcb]
|
||||||
[app.common.files.helpers :as cfh]
|
[app.common.files.helpers :as cfh]
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
@ -19,6 +20,7 @@
|
|||||||
[app.common.types.shape.layout :as ctl]
|
[app.common.types.shape.layout :as ctl]
|
||||||
[app.common.types.text :as txt]
|
[app.common.types.text :as txt]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
|
[app.main.data.changes :as dch]
|
||||||
[app.main.data.event :as ev]
|
[app.main.data.event :as ev]
|
||||||
[app.main.data.helpers :as dsh]
|
[app.main.data.helpers :as dsh]
|
||||||
[app.main.data.workspace.common :as dwc]
|
[app.main.data.workspace.common :as dwc]
|
||||||
@ -916,11 +918,11 @@
|
|||||||
(update-in state [:workspace-text-modifier shape-id] {:position-data position-data}))))
|
(update-in state [:workspace-text-modifier shape-id] {:position-data position-data}))))
|
||||||
|
|
||||||
(defn v2-update-text-shape-content
|
(defn v2-update-text-shape-content
|
||||||
[id content & {:keys [update-name? name finalize? save-undo?]
|
[id content & {:keys [update-name? name finalize? save-undo? original-content]
|
||||||
:or {update-name? false name nil finalize? false save-undo? true}}]
|
:or {update-name? false name nil finalize? false save-undo? true original-content nil}}]
|
||||||
(ptk/reify ::v2-update-text-shape-content
|
(ptk/reify ::v2-update-text-shape-content
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state _]
|
(watch [it state _]
|
||||||
(if (features/active-feature? state "render-wasm/v1")
|
(if (features/active-feature? state "render-wasm/v1")
|
||||||
(let [objects (dsh/lookup-page-objects state)
|
(let [objects (dsh/lookup-page-objects state)
|
||||||
shape (get objects id)
|
shape (get objects id)
|
||||||
@ -950,11 +952,11 @@
|
|||||||
new-shape))
|
new-shape))
|
||||||
{:save-undo? save-undo? :undo-group (when new-shape? id)})
|
{:save-undo? save-undo? :undo-group (when new-shape? id)})
|
||||||
|
|
||||||
(let [modifiers (dwwt/resize-wasm-text-modifiers shape content)
|
(when-let [modifiers (dwwt/resize-wasm-text-modifiers shape content)]
|
||||||
options {:undo-group (when new-shape? id)}]
|
(let [options {:undo-group (when new-shape? id)}]
|
||||||
(if (and (not= :fixed (:grow-type shape)) finalize?)
|
(if (and (not= :fixed (:grow-type shape)) finalize?)
|
||||||
(dwm/apply-wasm-modifiers modifiers options)
|
(dwm/apply-wasm-modifiers modifiers options)
|
||||||
(dwm/set-wasm-modifiers modifiers options))))
|
(dwm/set-wasm-modifiers modifiers options)))))
|
||||||
|
|
||||||
(when finalize?
|
(when finalize?
|
||||||
(rx/concat
|
(rx/concat
|
||||||
@ -970,7 +972,13 @@
|
|||||||
{:save-undo? false}))
|
{:save-undo? false}))
|
||||||
(dws/deselect-shape id)
|
(dws/deselect-shape id)
|
||||||
(dwsh/delete-shapes #{id})))
|
(dwsh/delete-shapes #{id})))
|
||||||
(rx/of (dwt/finish-transform))))))
|
(rx/of
|
||||||
|
;; This commit is necesary for undo and component propagation
|
||||||
|
;; on finalization
|
||||||
|
(dch/commit-changes
|
||||||
|
(-> (pcb/empty-changes it (:current-page-id state))
|
||||||
|
(pcb/set-text-content id content original-content)))
|
||||||
|
(dwt/finish-transform))))))
|
||||||
|
|
||||||
(let [objects (dsh/lookup-page-objects state)
|
(let [objects (dsh/lookup-page-objects state)
|
||||||
shape (get objects id)
|
shape (get objects id)
|
||||||
|
|||||||
@ -27,27 +27,28 @@
|
|||||||
(resize-wasm-text-modifiers shape (:content shape)))
|
(resize-wasm-text-modifiers shape (:content shape)))
|
||||||
|
|
||||||
([{:keys [id points selrect grow-type] :as shape} content]
|
([{:keys [id points selrect grow-type] :as shape} content]
|
||||||
(wasm.api/use-shape id)
|
(when id
|
||||||
(wasm.api/set-shape-text-content id content)
|
(wasm.api/use-shape id)
|
||||||
(wasm.api/set-shape-text-images id content)
|
(wasm.api/set-shape-text-content id content)
|
||||||
|
(wasm.api/set-shape-text-images id content)
|
||||||
|
|
||||||
(let [dimension (wasm.api/get-text-dimensions)
|
(let [dimension (wasm.api/get-text-dimensions)
|
||||||
width-scale (if (#{:fixed :auto-height} grow-type)
|
width-scale (if (#{:fixed :auto-height} grow-type)
|
||||||
1.0
|
1.0
|
||||||
(/ (:width dimension) (:width selrect)))
|
(/ (:width dimension) (:width selrect)))
|
||||||
height-scale (if (= :fixed grow-type)
|
height-scale (if (= :fixed grow-type)
|
||||||
1.0
|
1.0
|
||||||
(/ (:height dimension) (:height selrect)))
|
(/ (:height dimension) (:height selrect)))
|
||||||
resize-v (gpt/point width-scale height-scale)
|
resize-v (gpt/point width-scale height-scale)
|
||||||
origin (first points)]
|
origin (first points)]
|
||||||
|
|
||||||
{id
|
{id
|
||||||
{:modifiers
|
{:modifiers
|
||||||
(ctm/resize-modifiers
|
(ctm/resize-modifiers
|
||||||
resize-v
|
resize-v
|
||||||
origin
|
origin
|
||||||
(:transform shape (gmt/matrix))
|
(:transform shape (gmt/matrix))
|
||||||
(:transform-inverse shape (gmt/matrix)))}})))
|
(:transform-inverse shape (gmt/matrix)))}}))))
|
||||||
|
|
||||||
(defn resize-wasm-text
|
(defn resize-wasm-text
|
||||||
"Resize a single text shape (auto-width/auto-height) by id.
|
"Resize a single text shape (auto-width/auto-height) by id.
|
||||||
|
|||||||
@ -118,7 +118,8 @@
|
|||||||
:update-name? update-name?
|
:update-name? update-name?
|
||||||
:name generated-name
|
:name generated-name
|
||||||
:finalize? true
|
:finalize? true
|
||||||
:save-undo? false))))
|
:save-undo? false
|
||||||
|
:original-content original-content))))
|
||||||
|
|
||||||
(let [container-node (mf/ref-val container-ref)]
|
(let [container-node (mf/ref-val container-ref)]
|
||||||
(dom/set-style! container-node "opacity" 0)))
|
(dom/set-style! container-node "opacity" 0)))
|
||||||
|
|||||||
@ -540,7 +540,7 @@
|
|||||||
[:values schema:layout-item-props-schema]
|
[:values schema:layout-item-props-schema]
|
||||||
[:applied-tokens [:maybe [:map-of :keyword :string]]]
|
[:applied-tokens [:maybe [:map-of :keyword :string]]]
|
||||||
[:ids [::sm/vec ::sm/uuid]]
|
[:ids [::sm/vec ::sm/uuid]]
|
||||||
[:v-sizing {:optional true} [:maybe [:= :fill]]]])
|
[:v-sizing {:optional true} [:maybe [:enum :fill :fix :auto]]]])
|
||||||
|
|
||||||
(mf/defc layout-size-constraints*
|
(mf/defc layout-size-constraints*
|
||||||
{::mf/private true
|
{::mf/private true
|
||||||
|
|||||||
@ -111,11 +111,6 @@
|
|||||||
:modifier modifier
|
:modifier modifier
|
||||||
:zoom zoom}]))))
|
:zoom zoom}]))))
|
||||||
|
|
||||||
(defn- show-outline?
|
|
||||||
[shape]
|
|
||||||
(and (not (:hidden shape))
|
|
||||||
(not (:blocked shape))))
|
|
||||||
|
|
||||||
(mf/defc shape-outlines
|
(mf/defc shape-outlines
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
[props]
|
[props]
|
||||||
@ -133,8 +128,7 @@
|
|||||||
|
|
||||||
shapes (-> #{}
|
shapes (-> #{}
|
||||||
(into (comp (remove edition?)
|
(into (comp (remove edition?)
|
||||||
(keep lookup)
|
(keep lookup))
|
||||||
(filter show-outline?))
|
|
||||||
(set/union selected hover))
|
(set/union selected hover))
|
||||||
(into (comp (remove edition?)
|
(into (comp (remove edition?)
|
||||||
(keep lookup))
|
(keep lookup))
|
||||||
|
|||||||
@ -224,8 +224,9 @@
|
|||||||
show-gradient-handlers? (= (count selected) 1)
|
show-gradient-handlers? (= (count selected) 1)
|
||||||
show-grids? (contains? layout :display-guides)
|
show-grids? (contains? layout :display-guides)
|
||||||
|
|
||||||
show-frame-outline? (= transform :move)
|
show-frame-outline? (and (= transform :move) (not panning))
|
||||||
show-outlines? (and (nil? transform)
|
show-outlines? (and (nil? transform)
|
||||||
|
(not panning)
|
||||||
(not edition)
|
(not edition)
|
||||||
(not drawing-obj)
|
(not drawing-obj)
|
||||||
(not (#{:comments :path :curve} drawing-tool)))
|
(not (#{:comments :path :curve} drawing-tool)))
|
||||||
@ -561,7 +562,7 @@
|
|||||||
:shift? @shift?}])
|
:shift? @shift?}])
|
||||||
|
|
||||||
[:> widgets/frame-titles*
|
[:> widgets/frame-titles*
|
||||||
{:objects (with-meta objects-modified nil)
|
{:objects objects-modified
|
||||||
:selected selected
|
:selected selected
|
||||||
:zoom zoom
|
:zoom zoom
|
||||||
:is-show-artboard-names show-artboard-names?
|
:is-show-artboard-names show-artboard-names?
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
[app.main.data.workspace.media :as dwm]
|
[app.main.data.workspace.media :as dwm]
|
||||||
[app.main.data.workspace.selection :as dws]
|
[app.main.data.workspace.selection :as dws]
|
||||||
[app.main.data.workspace.wasm-text :as dwwt]
|
[app.main.data.workspace.wasm-text :as dwwt]
|
||||||
|
[app.main.features :as features]
|
||||||
[app.main.fonts :refer [fetch-font-css]]
|
[app.main.fonts :refer [fetch-font-css]]
|
||||||
[app.main.router :as rt]
|
[app.main.router :as rt]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
@ -365,8 +366,10 @@
|
|||||||
(cb/add-object shape))]
|
(cb/add-object shape))]
|
||||||
|
|
||||||
(st/emit! (ch/commit-changes changes)
|
(st/emit! (ch/commit-changes changes)
|
||||||
(se/event plugin-id "create-shape" :type :text)
|
(se/event plugin-id "create-shape" :type :text))
|
||||||
(dwwt/resize-wasm-text-debounce (:id shape)))
|
|
||||||
|
(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)))))
|
(shape/shape-proxy plugin-id (:id shape)))))
|
||||||
|
|
||||||
|
|||||||
@ -62,7 +62,7 @@
|
|||||||
origin
|
origin
|
||||||
(if (= vers 1)
|
(if (= vers 1)
|
||||||
(-> plugin-url
|
(-> plugin-url
|
||||||
(assoc :path "/")
|
(assoc :path "")
|
||||||
(str))
|
(str))
|
||||||
(-> plugin-url
|
(-> plugin-url
|
||||||
(u/join ".")
|
(u/join ".")
|
||||||
|
|||||||
@ -39,6 +39,7 @@ pub use images::*;
|
|||||||
const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 3;
|
const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 3;
|
||||||
const MAX_BLOCKING_TIME_MS: i32 = 32;
|
const MAX_BLOCKING_TIME_MS: i32 = 32;
|
||||||
const NODE_BATCH_THRESHOLD: i32 = 3;
|
const NODE_BATCH_THRESHOLD: i32 = 3;
|
||||||
|
const BLUR_DOWNSCALE_THRESHOLD: f32 = 8.0;
|
||||||
|
|
||||||
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
|
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
|
||||||
|
|
||||||
@ -641,6 +642,7 @@ impl RenderState {
|
|||||||
apply_to_current_surface: bool,
|
apply_to_current_surface: bool,
|
||||||
offset: Option<(f32, f32)>,
|
offset: Option<(f32, f32)>,
|
||||||
parent_shadows: Option<Vec<skia_safe::Paint>>,
|
parent_shadows: Option<Vec<skia_safe::Paint>>,
|
||||||
|
spread: Option<f32>,
|
||||||
) {
|
) {
|
||||||
let surface_ids = fills_surface_id as u32
|
let surface_ids = fills_surface_id as u32
|
||||||
| strokes_surface_id as u32
|
| strokes_surface_id as u32
|
||||||
@ -699,7 +701,14 @@ impl RenderState {
|
|||||||
canvas.translate(translation);
|
canvas.translate(translation);
|
||||||
});
|
});
|
||||||
|
|
||||||
fills::render(self, shape, &shape.fills, antialias, SurfaceId::Current);
|
fills::render(
|
||||||
|
self,
|
||||||
|
shape,
|
||||||
|
&shape.fills,
|
||||||
|
antialias,
|
||||||
|
SurfaceId::Current,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
// Pass strokes in natural order; stroke merging handles top-most ordering internally.
|
// Pass strokes in natural order; stroke merging handles top-most ordering internally.
|
||||||
let visible_strokes: Vec<&Stroke> = shape.visible_strokes().collect();
|
let visible_strokes: Vec<&Stroke> = shape.visible_strokes().collect();
|
||||||
@ -709,6 +718,7 @@ impl RenderState {
|
|||||||
&visible_strokes,
|
&visible_strokes,
|
||||||
Some(SurfaceId::Current),
|
Some(SurfaceId::Current),
|
||||||
antialias,
|
antialias,
|
||||||
|
spread,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.surfaces.apply_mut(SurfaceId::Current as u32, |s| {
|
self.surfaces.apply_mut(SurfaceId::Current as u32, |s| {
|
||||||
@ -787,6 +797,25 @@ impl RenderState {
|
|||||||
shape.to_mut().set_blur(None);
|
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 center = shape.center();
|
||||||
let mut matrix = shape.transform;
|
let mut matrix = shape.transform;
|
||||||
matrix.post_translate(center);
|
matrix.post_translate(center);
|
||||||
@ -1005,6 +1034,24 @@ impl RenderState {
|
|||||||
s.canvas().concat(&matrix);
|
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;
|
let shape = &shape;
|
||||||
|
|
||||||
if shape.fills.is_empty()
|
if shape.fills.is_empty()
|
||||||
@ -1017,10 +1064,24 @@ impl RenderState {
|
|||||||
{
|
{
|
||||||
if let Some(fills_to_render) = self.nested_fills.last() {
|
if let Some(fills_to_render) = self.nested_fills.last() {
|
||||||
let fills_to_render = fills_to_render.clone();
|
let fills_to_render = fills_to_render.clone();
|
||||||
fills::render(self, shape, &fills_to_render, antialias, fills_surface_id);
|
fills::render(
|
||||||
|
self,
|
||||||
|
shape,
|
||||||
|
&fills_to_render,
|
||||||
|
antialias,
|
||||||
|
fills_surface_id,
|
||||||
|
spread,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fills::render(self, shape, &shape.fills, antialias, fills_surface_id);
|
fills::render(
|
||||||
|
self,
|
||||||
|
shape,
|
||||||
|
&shape.fills,
|
||||||
|
antialias,
|
||||||
|
fills_surface_id,
|
||||||
|
spread,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip stroke rendering for clipped frames - they are drawn in render_shape_exit
|
// Skip stroke rendering for clipped frames - they are drawn in render_shape_exit
|
||||||
@ -1035,6 +1096,7 @@ impl RenderState {
|
|||||||
&visible_strokes,
|
&visible_strokes,
|
||||||
Some(strokes_surface_id),
|
Some(strokes_surface_id),
|
||||||
antialias,
|
antialias,
|
||||||
|
spread,
|
||||||
);
|
);
|
||||||
if !fast_mode {
|
if !fast_mode {
|
||||||
for stroke in &visible_strokes {
|
for stroke in &visible_strokes {
|
||||||
@ -1057,7 +1119,12 @@ impl RenderState {
|
|||||||
innershadows_surface_id,
|
innershadows_surface_id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// bools::debug_render_bool_paths(self, shape, shapes, modifiers, structure);
|
|
||||||
|
if blur_filter_for_layers.is_some() {
|
||||||
|
self.surfaces.canvas(innershadows_surface_id).restore();
|
||||||
|
self.surfaces.canvas(strokes_surface_id).restore();
|
||||||
|
self.surfaces.canvas(fills_surface_id).restore();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1149,6 +1216,11 @@ impl RenderState {
|
|||||||
let _start = performance::begin_timed_log!("render_preview");
|
let _start = performance::begin_timed_log!("render_preview");
|
||||||
performance::begin_measure!("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
|
// Skip tile rebuilding during preview - we'll do it at the end
|
||||||
// Just rebuild tiles for touched shapes and render synchronously
|
// Just rebuild tiles for touched shapes and render synchronously
|
||||||
self.rebuild_touched_tiles(tree);
|
self.rebuild_touched_tiles(tree);
|
||||||
@ -1156,6 +1228,8 @@ impl RenderState {
|
|||||||
// Use the sync render path
|
// Use the sync render path
|
||||||
self.start_render_loop(None, tree, timestamp, true)?;
|
self.start_render_loop(None, tree, timestamp, true)?;
|
||||||
|
|
||||||
|
self.options.set_fast_mode(current_fast_mode);
|
||||||
|
|
||||||
performance::end_measure!("render_preview");
|
performance::end_measure!("render_preview");
|
||||||
performance::end_timed_log!("render_preview", _start);
|
performance::end_timed_log!("render_preview", _start);
|
||||||
|
|
||||||
@ -1326,11 +1400,16 @@ impl RenderState {
|
|||||||
paint.set_blend_mode(element.blend_mode().into());
|
paint.set_blend_mode(element.blend_mode().into());
|
||||||
paint.set_alpha_f(element.opacity());
|
paint.set_alpha_f(element.opacity());
|
||||||
|
|
||||||
if let Some(frame_blur) = Self::frame_clip_layer_blur(element) {
|
// Skip frame-level blur in fast mode (pan/zoom)
|
||||||
let scale = self.get_scale();
|
if !self.options.is_fast_mode() {
|
||||||
let sigma = frame_blur.value * scale;
|
if let Some(frame_blur) = Self::frame_clip_layer_blur(element) {
|
||||||
if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) {
|
let scale = self.get_scale();
|
||||||
paint.set_image_filter(filter);
|
let sigma = frame_blur.value * scale;
|
||||||
|
if let Some(filter) =
|
||||||
|
skia::image_filters::blur((sigma, sigma), None, None, None)
|
||||||
|
{
|
||||||
|
paint.set_image_filter(filter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1417,6 +1496,7 @@ impl RenderState {
|
|||||||
true,
|
true,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1517,9 +1597,7 @@ impl RenderState {
|
|||||||
Self::combine_blur_values(self.combined_layer_blur(shape.blur), extra_layer_blur);
|
Self::combine_blur_values(self.combined_layer_blur(shape.blur), extra_layer_blur);
|
||||||
let blur_filter = combined_blur
|
let blur_filter = combined_blur
|
||||||
.and_then(|blur| skia::image_filters::blur((blur.value, blur.value), None, None, None));
|
.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();
|
let use_low_zoom_path = scale <= 1.0 && combined_blur.is_none();
|
||||||
|
|
||||||
if use_low_zoom_path {
|
if use_low_zoom_path {
|
||||||
@ -1573,14 +1651,10 @@ impl RenderState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if use_low_zoom_path {
|
// blur=0 at high zoom: draw directly on DropShadows with geometric spread (no filter).
|
||||||
let mut shadow_paint = skia::Paint::default();
|
if scale > 1.0 && shadow.blur <= 0.0 {
|
||||||
shadow_paint.set_image_filter(drop_filter);
|
|
||||||
shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
|
|
||||||
|
|
||||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint);
|
|
||||||
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
||||||
drop_canvas.save_layer(&layer_rec);
|
drop_canvas.save();
|
||||||
drop_canvas.scale((scale, scale));
|
drop_canvas.scale((scale, scale));
|
||||||
drop_canvas.translate(translation);
|
drop_canvas.translate(translation);
|
||||||
|
|
||||||
@ -1595,6 +1669,7 @@ impl RenderState {
|
|||||||
false,
|
false,
|
||||||
Some(shadow.offset),
|
Some(shadow.offset),
|
||||||
None,
|
None,
|
||||||
|
Some(shadow.spread),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1602,20 +1677,75 @@ impl RenderState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter_result =
|
// Create filter with blur only (no offset, no spread - handled geometrically)
|
||||||
filters::render_into_filter_surface(self, bounds, |state, temp_surface| {
|
let blur_only_filter = if transformed_shadow.blur > 0.0 {
|
||||||
{
|
Some(skia::image_filters::blur(
|
||||||
let canvas = state.surfaces.canvas(temp_surface);
|
(transformed_shadow.blur, transformed_shadow.blur),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let mut shadow_paint = skia::Paint::default();
|
let mut shadow_paint = skia::Paint::default();
|
||||||
shadow_paint.set_image_filter(drop_filter.clone());
|
if let Some(blur_filter) = blur_only_filter {
|
||||||
shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
|
shadow_paint.set_image_filter(blur_filter);
|
||||||
|
}
|
||||||
|
shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
|
||||||
|
|
||||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint);
|
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint);
|
||||||
canvas.save_layer(&layer_rec);
|
|
||||||
}
|
// Low zoom path: use blur filter but apply offset and spread geometrically
|
||||||
|
if use_low_zoom_path {
|
||||||
|
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
||||||
|
drop_canvas.save_layer(&layer_rec);
|
||||||
|
drop_canvas.scale((scale, scale));
|
||||||
|
drop_canvas.translate(translation);
|
||||||
|
|
||||||
|
self.with_nested_blurs_suppressed(|state| {
|
||||||
|
state.render_shape(
|
||||||
|
&plain_shape,
|
||||||
|
clip_bounds,
|
||||||
|
SurfaceId::DropShadows,
|
||||||
|
SurfaceId::DropShadows,
|
||||||
|
SurfaceId::DropShadows,
|
||||||
|
SurfaceId::DropShadows,
|
||||||
|
false,
|
||||||
|
Some(shadow.offset), // Offset is geometric
|
||||||
|
None,
|
||||||
|
Some(shadow.spread), // Spread is geometric
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adaptive downscale for large blur values (lossless GPU optimization).
|
||||||
|
// Bounds above were computed from the original sigma so filter surface coverage is correct.
|
||||||
|
// Maximum downscale is 1/BLUR_DOWNSCALE_THRESHOLD (i.e. 8x): beyond that the
|
||||||
|
// filter surface becomes too small and quality degrades noticeably.
|
||||||
|
const MIN_BLUR_DOWNSCALE: f32 = 1.0 / BLUR_DOWNSCALE_THRESHOLD;
|
||||||
|
let blur_downscale = if shadow.blur > BLUR_DOWNSCALE_THRESHOLD {
|
||||||
|
(BLUR_DOWNSCALE_THRESHOLD / shadow.blur).max(MIN_BLUR_DOWNSCALE)
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// High zoom with blur: use render_into_filter_surface to ensure blur has enough space
|
||||||
|
// Apply spread geometrically to avoid dilate filter rounding issues
|
||||||
|
let filter_result = filters::render_into_filter_surface(
|
||||||
|
self,
|
||||||
|
bounds,
|
||||||
|
blur_downscale,
|
||||||
|
|state, temp_surface| {
|
||||||
|
let canvas = state.surfaces.canvas(temp_surface);
|
||||||
|
canvas.save_layer(&layer_rec);
|
||||||
|
|
||||||
state.with_nested_blurs_suppressed(|state| {
|
state.with_nested_blurs_suppressed(|state| {
|
||||||
|
// Apply offset and spread geometrically
|
||||||
state.render_shape(
|
state.render_shape(
|
||||||
&plain_shape,
|
&plain_shape,
|
||||||
clip_bounds,
|
clip_bounds,
|
||||||
@ -1624,16 +1754,15 @@ impl RenderState {
|
|||||||
temp_surface,
|
temp_surface,
|
||||||
temp_surface,
|
temp_surface,
|
||||||
false,
|
false,
|
||||||
Some(shadow.offset),
|
Some(shadow.offset), // Offset is geometric
|
||||||
None,
|
None,
|
||||||
|
Some(shadow.spread), // Spread is geometric
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
state.surfaces.canvas(temp_surface).restore();
|
||||||
let canvas = state.surfaces.canvas(temp_surface);
|
},
|
||||||
canvas.restore();
|
);
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some((mut surface, filter_scale)) = filter_result {
|
if let Some((mut surface, filter_scale)) = filter_result {
|
||||||
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
||||||
@ -1720,6 +1849,7 @@ impl RenderState {
|
|||||||
if shadow_shape.hidden {
|
if shadow_shape.hidden {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let nested_clip_bounds =
|
let nested_clip_bounds =
|
||||||
node_render_state.get_nested_shadow_clip_bounds(element, shadow);
|
node_render_state.get_nested_shadow_clip_bounds(element, shadow);
|
||||||
|
|
||||||
@ -1767,6 +1897,7 @@ impl RenderState {
|
|||||||
true,
|
true,
|
||||||
None,
|
None,
|
||||||
Some(vec![new_shadow_paint.clone()]),
|
Some(vec![new_shadow_paint.clone()]),
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||||
@ -1780,7 +1911,6 @@ impl RenderState {
|
|||||||
self.surfaces
|
self.surfaces
|
||||||
.canvas(SurfaceId::DropShadows)
|
.canvas(SurfaceId::DropShadows)
|
||||||
.draw_paint(&paint);
|
.draw_paint(&paint);
|
||||||
|
|
||||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1979,6 +2109,7 @@ impl RenderState {
|
|||||||
true,
|
true,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.surfaces
|
self.surfaces
|
||||||
|
|||||||
@ -97,6 +97,7 @@ pub fn render(
|
|||||||
fills: &[Fill],
|
fills: &[Fill],
|
||||||
antialias: bool,
|
antialias: bool,
|
||||||
surface_id: SurfaceId,
|
surface_id: SurfaceId,
|
||||||
|
spread: Option<f32>,
|
||||||
) {
|
) {
|
||||||
if fills.is_empty() {
|
if fills.is_empty() {
|
||||||
return;
|
return;
|
||||||
@ -107,7 +108,7 @@ pub fn render(
|
|||||||
let has_image_fills = fills.iter().any(|f| matches!(f, Fill::Image(_)));
|
let has_image_fills = fills.iter().any(|f| matches!(f, Fill::Image(_)));
|
||||||
if has_image_fills {
|
if has_image_fills {
|
||||||
for fill in fills.iter().rev() {
|
for fill in fills.iter().rev() {
|
||||||
render_single_fill(render_state, shape, fill, antialias, surface_id);
|
render_single_fill(render_state, shape, fill, antialias, surface_id, spread);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -124,7 +125,7 @@ pub fn render(
|
|||||||
|state, temp_surface| {
|
|state, temp_surface| {
|
||||||
let mut filtered_paint = paint.clone();
|
let mut filtered_paint = paint.clone();
|
||||||
filtered_paint.set_image_filter(image_filter.clone());
|
filtered_paint.set_image_filter(image_filter.clone());
|
||||||
draw_fill_to_surface(state, shape, temp_surface, &filtered_paint);
|
draw_fill_to_surface(state, shape, temp_surface, &filtered_paint, spread);
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@ -133,7 +134,7 @@ pub fn render(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
draw_fill_to_surface(render_state, shape, surface_id, &paint);
|
draw_fill_to_surface(render_state, shape, surface_id, &paint, spread);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws a single paint (with a merged shader) to the appropriate surface
|
/// Draws a single paint (with a merged shader) to the appropriate surface
|
||||||
@ -143,18 +144,23 @@ fn draw_fill_to_surface(
|
|||||||
shape: &Shape,
|
shape: &Shape,
|
||||||
surface_id: SurfaceId,
|
surface_id: SurfaceId,
|
||||||
paint: &Paint,
|
paint: &Paint,
|
||||||
|
spread: Option<f32>,
|
||||||
) {
|
) {
|
||||||
match &shape.shape_type {
|
match &shape.shape_type {
|
||||||
Type::Rect(_) | Type::Frame(_) => {
|
Type::Rect(_) | Type::Frame(_) => {
|
||||||
render_state.surfaces.draw_rect_to(surface_id, shape, paint);
|
render_state
|
||||||
|
.surfaces
|
||||||
|
.draw_rect_to(surface_id, shape, paint, spread);
|
||||||
}
|
}
|
||||||
Type::Circle => {
|
Type::Circle => {
|
||||||
render_state
|
render_state
|
||||||
.surfaces
|
.surfaces
|
||||||
.draw_circle_to(surface_id, shape, paint);
|
.draw_circle_to(surface_id, shape, paint, spread);
|
||||||
}
|
}
|
||||||
Type::Path(_) | Type::Bool(_) => {
|
Type::Path(_) | Type::Bool(_) => {
|
||||||
render_state.surfaces.draw_path_to(surface_id, shape, paint);
|
render_state
|
||||||
|
.surfaces
|
||||||
|
.draw_path_to(surface_id, shape, paint, spread);
|
||||||
}
|
}
|
||||||
Type::Group(_) => {}
|
Type::Group(_) => {}
|
||||||
_ => unreachable!("This shape should not have fills"),
|
_ => unreachable!("This shape should not have fills"),
|
||||||
@ -167,6 +173,7 @@ fn render_single_fill(
|
|||||||
fill: &Fill,
|
fill: &Fill,
|
||||||
antialias: bool,
|
antialias: bool,
|
||||||
surface_id: SurfaceId,
|
surface_id: SurfaceId,
|
||||||
|
spread: Option<f32>,
|
||||||
) {
|
) {
|
||||||
let mut paint = fill.to_paint(&shape.selrect, antialias);
|
let mut paint = fill.to_paint(&shape.selrect, antialias);
|
||||||
if let Some(image_filter) = shape.image_filter(1.) {
|
if let Some(image_filter) = shape.image_filter(1.) {
|
||||||
@ -185,6 +192,7 @@ fn render_single_fill(
|
|||||||
antialias,
|
antialias,
|
||||||
temp_surface,
|
temp_surface,
|
||||||
&filtered_paint,
|
&filtered_paint,
|
||||||
|
spread,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@ -194,7 +202,15 @@ fn render_single_fill(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
draw_single_fill_to_surface(render_state, shape, fill, antialias, surface_id, &paint);
|
draw_single_fill_to_surface(
|
||||||
|
render_state,
|
||||||
|
shape,
|
||||||
|
fill,
|
||||||
|
antialias,
|
||||||
|
surface_id,
|
||||||
|
&paint,
|
||||||
|
spread,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_single_fill_to_surface(
|
fn draw_single_fill_to_surface(
|
||||||
@ -204,6 +220,7 @@ fn draw_single_fill_to_surface(
|
|||||||
antialias: bool,
|
antialias: bool,
|
||||||
surface_id: SurfaceId,
|
surface_id: SurfaceId,
|
||||||
paint: &Paint,
|
paint: &Paint,
|
||||||
|
spread: Option<f32>,
|
||||||
) {
|
) {
|
||||||
match (fill, &shape.shape_type) {
|
match (fill, &shape.shape_type) {
|
||||||
(Fill::Image(image_fill), _) => {
|
(Fill::Image(image_fill), _) => {
|
||||||
@ -217,15 +234,19 @@ fn draw_single_fill_to_surface(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
(_, Type::Rect(_) | Type::Frame(_)) => {
|
(_, Type::Rect(_) | Type::Frame(_)) => {
|
||||||
render_state.surfaces.draw_rect_to(surface_id, shape, paint);
|
render_state
|
||||||
|
.surfaces
|
||||||
|
.draw_rect_to(surface_id, shape, paint, spread);
|
||||||
}
|
}
|
||||||
(_, Type::Circle) => {
|
(_, Type::Circle) => {
|
||||||
render_state
|
render_state
|
||||||
.surfaces
|
.surfaces
|
||||||
.draw_circle_to(surface_id, shape, paint);
|
.draw_circle_to(surface_id, shape, paint, spread);
|
||||||
}
|
}
|
||||||
(_, Type::Path(_)) | (_, Type::Bool(_)) => {
|
(_, Type::Path(_)) | (_, Type::Bool(_)) => {
|
||||||
render_state.surfaces.draw_path_to(surface_id, shape, paint);
|
render_state
|
||||||
|
.surfaces
|
||||||
|
.draw_path_to(surface_id, shape, paint, spread);
|
||||||
}
|
}
|
||||||
(_, Type::Group(_)) => {
|
(_, Type::Group(_)) => {
|
||||||
// Groups can have fills but they propagate them to their children
|
// Groups can have fills but they propagate them to their children
|
||||||
|
|||||||
@ -40,7 +40,9 @@ pub fn render_with_filter_surface<F>(
|
|||||||
where
|
where
|
||||||
F: FnOnce(&mut RenderState, SurfaceId),
|
F: FnOnce(&mut RenderState, SurfaceId),
|
||||||
{
|
{
|
||||||
if let Some((mut surface, scale)) = render_into_filter_surface(render_state, bounds, draw_fn) {
|
if let Some((mut surface, scale)) =
|
||||||
|
render_into_filter_surface(render_state, bounds, 1.0, draw_fn)
|
||||||
|
{
|
||||||
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
|
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
|
// If we scaled down, we need to scale the source rect and adjust the destination
|
||||||
@ -69,9 +71,15 @@ where
|
|||||||
/// down so that everything fits; the returned `scale` tells the caller how much the
|
/// 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
|
/// 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`.
|
/// 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>(
|
pub fn render_into_filter_surface<F>(
|
||||||
render_state: &mut RenderState,
|
render_state: &mut RenderState,
|
||||||
bounds: Rect,
|
bounds: Rect,
|
||||||
|
extra_downscale: f32,
|
||||||
draw_fn: F,
|
draw_fn: F,
|
||||||
) -> Option<(skia::Surface, f32)>
|
) -> Option<(skia::Surface, f32)>
|
||||||
where
|
where
|
||||||
@ -86,16 +94,28 @@ where
|
|||||||
let bounds_width = bounds.width().ceil().max(1.0) as i32;
|
let bounds_width = bounds.width().ceil().max(1.0) as i32;
|
||||||
let bounds_height = bounds.height().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
|
// Calculate scale factor if bounds exceed filter surface size
|
||||||
let scale = if bounds_width > filter_width || bounds_height > filter_height {
|
let fit_scale = if bounds_width > filter_width || bounds_height > filter_height {
|
||||||
let scale_x = filter_width as f32 / bounds_width as f32;
|
let scale_x = filter_width as f32 / bounds_width as f32;
|
||||||
let scale_y = filter_height as f32 / bounds_height as f32;
|
let scale_y = filter_height as f32 / bounds_height as f32;
|
||||||
// Use the smaller scale to ensure everything fits
|
// Use the smaller scale to ensure everything fits
|
||||||
scale_x.min(scale_y).max(0.1) // Clamp to minimum 0.1 to avoid extreme scaling
|
scale_x.min(scale_y).max(MIN_FIT_SCALE)
|
||||||
} else {
|
} else {
|
||||||
1.0
|
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);
|
let canvas = render_state.surfaces.canvas(filter_id);
|
||||||
canvas.clear(skia::Color::TRANSPARENT);
|
canvas.clear(skia::Color::TRANSPARENT);
|
||||||
|
|||||||
@ -47,6 +47,7 @@ pub fn render_stroke_inner_shadows(
|
|||||||
Some(surface_id),
|
Some(surface_id),
|
||||||
filter.as_ref(),
|
filter.as_ref(),
|
||||||
antialias,
|
antialias,
|
||||||
|
None, // Inner shadows don't use spread
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,15 +107,19 @@ fn render_shadow_paint(
|
|||||||
) {
|
) {
|
||||||
match &shape.shape_type {
|
match &shape.shape_type {
|
||||||
Type::Rect(_) | Type::Frame(_) => {
|
Type::Rect(_) | Type::Frame(_) => {
|
||||||
render_state.surfaces.draw_rect_to(surface_id, shape, paint);
|
render_state
|
||||||
|
.surfaces
|
||||||
|
.draw_rect_to(surface_id, shape, paint, None);
|
||||||
}
|
}
|
||||||
Type::Circle => {
|
Type::Circle => {
|
||||||
render_state
|
render_state
|
||||||
.surfaces
|
.surfaces
|
||||||
.draw_circle_to(surface_id, shape, paint);
|
.draw_circle_to(surface_id, shape, paint, None);
|
||||||
}
|
}
|
||||||
Type::Path(_) | Type::Bool(_) => {
|
Type::Path(_) | Type::Bool(_) => {
|
||||||
render_state.surfaces.draw_path_to(surface_id, shape, paint);
|
render_state
|
||||||
|
.surfaces
|
||||||
|
.draw_path_to(surface_id, shape, paint, None);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -526,6 +526,7 @@ pub fn render(
|
|||||||
strokes: &[&Stroke],
|
strokes: &[&Stroke],
|
||||||
surface_id: Option<SurfaceId>,
|
surface_id: Option<SurfaceId>,
|
||||||
antialias: bool,
|
antialias: bool,
|
||||||
|
spread: Option<f32>,
|
||||||
) {
|
) {
|
||||||
if strokes.is_empty() {
|
if strokes.is_empty() {
|
||||||
return;
|
return;
|
||||||
@ -540,6 +541,10 @@ pub fn render(
|
|||||||
// edges semi-transparent and revealing strokes underneath.
|
// edges semi-transparent and revealing strokes underneath.
|
||||||
if let Some(image_filter) = shape.image_filter(1.) {
|
if let Some(image_filter) = shape.image_filter(1.) {
|
||||||
let mut content_bounds = shape.selrect;
|
let mut content_bounds = shape.selrect;
|
||||||
|
// Expand for spread if provided
|
||||||
|
if let Some(s) = spread.filter(|&s| s > 0.0) {
|
||||||
|
content_bounds.outset((s, s));
|
||||||
|
}
|
||||||
let max_margin = strokes
|
let max_margin = strokes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| s.bounds_width(shape.is_open()))
|
.map(|s| s.bounds_width(shape.is_open()))
|
||||||
@ -583,6 +588,7 @@ pub fn render(
|
|||||||
antialias,
|
antialias,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
|
spread,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -595,12 +601,28 @@ pub fn render(
|
|||||||
|
|
||||||
// No blur or filter surface unavailable — draw strokes individually.
|
// No blur or filter surface unavailable — draw strokes individually.
|
||||||
for stroke in strokes.iter().rev() {
|
for stroke in strokes.iter().rev() {
|
||||||
render_single(render_state, shape, stroke, surface_id, None, antialias);
|
render_single(
|
||||||
|
render_state,
|
||||||
|
shape,
|
||||||
|
stroke,
|
||||||
|
surface_id,
|
||||||
|
None,
|
||||||
|
antialias,
|
||||||
|
spread,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
render_merged(render_state, shape, strokes, surface_id, antialias, false);
|
render_merged(
|
||||||
|
render_state,
|
||||||
|
shape,
|
||||||
|
strokes,
|
||||||
|
surface_id,
|
||||||
|
antialias,
|
||||||
|
false,
|
||||||
|
spread,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn strokes_share_geometry(strokes: &[&Stroke]) -> bool {
|
fn strokes_share_geometry(strokes: &[&Stroke]) -> bool {
|
||||||
@ -620,6 +642,7 @@ fn render_merged(
|
|||||||
surface_id: Option<SurfaceId>,
|
surface_id: Option<SurfaceId>,
|
||||||
antialias: bool,
|
antialias: bool,
|
||||||
bypass_filter: bool,
|
bypass_filter: bool,
|
||||||
|
spread: Option<f32>,
|
||||||
) {
|
) {
|
||||||
let representative = *strokes
|
let representative = *strokes
|
||||||
.last()
|
.last()
|
||||||
@ -635,6 +658,10 @@ fn render_merged(
|
|||||||
if !bypass_filter {
|
if !bypass_filter {
|
||||||
if let Some(image_filter) = blur_filter.clone() {
|
if let Some(image_filter) = blur_filter.clone() {
|
||||||
let mut content_bounds = shape.selrect;
|
let mut content_bounds = shape.selrect;
|
||||||
|
// Expand for spread if provided
|
||||||
|
if let Some(s) = spread.filter(|&s| s > 0.0) {
|
||||||
|
content_bounds.outset((s, s));
|
||||||
|
}
|
||||||
let stroke_margin = representative.bounds_width(shape.is_open());
|
let stroke_margin = representative.bounds_width(shape.is_open());
|
||||||
if stroke_margin > 0.0 {
|
if stroke_margin > 0.0 {
|
||||||
content_bounds.inset((-stroke_margin, -stroke_margin));
|
content_bounds.inset((-stroke_margin, -stroke_margin));
|
||||||
@ -660,7 +687,15 @@ fn render_merged(
|
|||||||
canvas.save_layer(&layer_rec);
|
canvas.save_layer(&layer_rec);
|
||||||
});
|
});
|
||||||
|
|
||||||
render_merged(state, shape, strokes, Some(temp_surface), antialias, true);
|
render_merged(
|
||||||
|
state,
|
||||||
|
shape,
|
||||||
|
strokes,
|
||||||
|
Some(temp_surface),
|
||||||
|
antialias,
|
||||||
|
true,
|
||||||
|
spread,
|
||||||
|
);
|
||||||
|
|
||||||
state.surfaces.apply_mut(temp_surface as u32, |surface| {
|
state.surfaces.apply_mut(temp_surface as u32, |surface| {
|
||||||
surface.canvas().restore();
|
surface.canvas().restore();
|
||||||
@ -676,11 +711,19 @@ fn render_merged(
|
|||||||
// via SrcOver), matching the non-merged path where strokes[0] is drawn last (on top).
|
// via SrcOver), matching the non-merged path where strokes[0] is drawn last (on top).
|
||||||
let fills: Vec<Fill> = strokes.iter().map(|s| s.fill.clone()).collect();
|
let fills: Vec<Fill> = strokes.iter().map(|s| s.fill.clone()).collect();
|
||||||
|
|
||||||
let merged = merge_fills(&fills, shape.selrect);
|
// Expand selrect if spread is provided
|
||||||
|
let selrect = if let Some(s) = spread.filter(|&s| s > 0.0) {
|
||||||
|
let mut r = shape.selrect;
|
||||||
|
r.outset((s, s));
|
||||||
|
r
|
||||||
|
} else {
|
||||||
|
shape.selrect
|
||||||
|
};
|
||||||
|
|
||||||
|
let merged = merge_fills(&fills, selrect);
|
||||||
let scale = render_state.get_scale();
|
let scale = render_state.get_scale();
|
||||||
let target_surface = surface_id.unwrap_or(SurfaceId::Strokes);
|
let target_surface = surface_id.unwrap_or(SurfaceId::Strokes);
|
||||||
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
|
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
|
||||||
let selrect = shape.selrect;
|
|
||||||
let svg_attrs = shape.svg_attrs.as_ref();
|
let svg_attrs = shape.svg_attrs.as_ref();
|
||||||
let path_transform = shape.to_path_transform();
|
let path_transform = shape.to_path_transform();
|
||||||
|
|
||||||
@ -747,6 +790,7 @@ pub fn render_single(
|
|||||||
surface_id: Option<SurfaceId>,
|
surface_id: Option<SurfaceId>,
|
||||||
shadow: Option<&ImageFilter>,
|
shadow: Option<&ImageFilter>,
|
||||||
antialias: bool,
|
antialias: bool,
|
||||||
|
spread: Option<f32>,
|
||||||
) {
|
) {
|
||||||
render_single_internal(
|
render_single_internal(
|
||||||
render_state,
|
render_state,
|
||||||
@ -757,6 +801,7 @@ pub fn render_single(
|
|||||||
antialias,
|
antialias,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
spread,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -770,10 +815,15 @@ fn render_single_internal(
|
|||||||
antialias: bool,
|
antialias: bool,
|
||||||
bypass_filter: bool,
|
bypass_filter: bool,
|
||||||
skip_blur: bool,
|
skip_blur: bool,
|
||||||
|
spread: Option<f32>,
|
||||||
) {
|
) {
|
||||||
if !bypass_filter {
|
if !bypass_filter {
|
||||||
if let Some(image_filter) = shape.image_filter(1.) {
|
if let Some(image_filter) = shape.image_filter(1.) {
|
||||||
let mut content_bounds = shape.selrect;
|
let mut content_bounds = shape.selrect;
|
||||||
|
// Expand for spread if provided
|
||||||
|
if let Some(s) = spread.filter(|&s| s > 0.0) {
|
||||||
|
content_bounds.outset((s, s));
|
||||||
|
}
|
||||||
let stroke_margin = stroke.bounds_width(shape.is_open());
|
let stroke_margin = stroke.bounds_width(shape.is_open());
|
||||||
if stroke_margin > 0.0 {
|
if stroke_margin > 0.0 {
|
||||||
content_bounds.inset((-stroke_margin, -stroke_margin));
|
content_bounds.inset((-stroke_margin, -stroke_margin));
|
||||||
@ -799,6 +849,7 @@ fn render_single_internal(
|
|||||||
antialias,
|
antialias,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
|
spread,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@ -867,7 +918,21 @@ fn render_single_internal(
|
|||||||
shape_type @ (Type::Path(_) | Type::Bool(_)) => {
|
shape_type @ (Type::Path(_) | Type::Bool(_)) => {
|
||||||
if let Some(path) = shape_type.path() {
|
if let Some(path) = shape_type.path() {
|
||||||
let is_open = path.is_open();
|
let is_open = path.is_open();
|
||||||
let paint = stroke.to_stroked_paint(is_open, &selrect, svg_attrs, antialias);
|
let mut paint =
|
||||||
|
stroke.to_stroked_paint(is_open, &selrect, svg_attrs, antialias);
|
||||||
|
// Apply spread by increasing stroke width
|
||||||
|
if let Some(s) = spread.filter(|&s| s > 0.0) {
|
||||||
|
let current_width = paint.stroke_width();
|
||||||
|
// Path stroke kinds are built differently:
|
||||||
|
// - Center uses the stroke width directly.
|
||||||
|
// - Inner/Outer use a doubled width plus clipping/clearing logic.
|
||||||
|
// Compensate spread so visual growth is comparable across kinds.
|
||||||
|
let spread_growth = match stroke.render_kind(is_open) {
|
||||||
|
StrokeKind::Center => s * 2.0,
|
||||||
|
StrokeKind::Inner | StrokeKind::Outer => s * 4.0,
|
||||||
|
};
|
||||||
|
paint.set_stroke_width(current_width + spread_growth);
|
||||||
|
}
|
||||||
draw_stroke_on_path(
|
draw_stroke_on_path(
|
||||||
canvas,
|
canvas,
|
||||||
stroke,
|
stroke,
|
||||||
|
|||||||
@ -74,7 +74,7 @@ impl Surfaces {
|
|||||||
let margins = skia::ISize::new(extra_tile_dims.width / 4, extra_tile_dims.height / 4);
|
let margins = skia::ISize::new(extra_tile_dims.width / 4, extra_tile_dims.height / 4);
|
||||||
|
|
||||||
let target = gpu_state.create_target_surface(width, height);
|
let target = gpu_state.create_target_surface(width, height);
|
||||||
let filter = gpu_state.create_surface_with_dimensions("filter".to_string(), width, height);
|
let filter = gpu_state.create_surface_with_isize("filter".to_string(), extra_tile_dims);
|
||||||
let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height);
|
let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height);
|
||||||
let current = gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims);
|
let current = gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims);
|
||||||
let drop_shadows =
|
let drop_shadows =
|
||||||
@ -355,24 +355,62 @@ impl Surfaces {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw_rect_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) {
|
pub fn draw_rect_to(
|
||||||
|
&mut self,
|
||||||
|
id: SurfaceId,
|
||||||
|
shape: &Shape,
|
||||||
|
paint: &Paint,
|
||||||
|
spread: Option<f32>,
|
||||||
|
) {
|
||||||
|
let rect = if let Some(s) = spread.filter(|&s| s > 0.0) {
|
||||||
|
let mut r = shape.selrect;
|
||||||
|
r.outset((s, s));
|
||||||
|
r
|
||||||
|
} else {
|
||||||
|
shape.selrect
|
||||||
|
};
|
||||||
if let Some(corners) = shape.shape_type.corners() {
|
if let Some(corners) = shape.shape_type.corners() {
|
||||||
let rrect = RRect::new_rect_radii(shape.selrect, &corners);
|
let rrect = RRect::new_rect_radii(rect, &corners);
|
||||||
self.canvas_and_mark_dirty(id).draw_rrect(rrect, paint);
|
self.canvas_and_mark_dirty(id).draw_rrect(rrect, paint);
|
||||||
} else {
|
} else {
|
||||||
self.canvas_and_mark_dirty(id)
|
self.canvas_and_mark_dirty(id).draw_rect(rect, paint);
|
||||||
.draw_rect(shape.selrect, paint);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw_circle_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) {
|
pub fn draw_circle_to(
|
||||||
self.canvas_and_mark_dirty(id)
|
&mut self,
|
||||||
.draw_oval(shape.selrect, paint);
|
id: SurfaceId,
|
||||||
|
shape: &Shape,
|
||||||
|
paint: &Paint,
|
||||||
|
spread: Option<f32>,
|
||||||
|
) {
|
||||||
|
let rect = if let Some(s) = spread.filter(|&s| s > 0.0) {
|
||||||
|
let mut r = shape.selrect;
|
||||||
|
r.outset((s, s));
|
||||||
|
r
|
||||||
|
} else {
|
||||||
|
shape.selrect
|
||||||
|
};
|
||||||
|
self.canvas_and_mark_dirty(id).draw_oval(rect, paint);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw_path_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) {
|
pub fn draw_path_to(
|
||||||
|
&mut self,
|
||||||
|
id: SurfaceId,
|
||||||
|
shape: &Shape,
|
||||||
|
paint: &Paint,
|
||||||
|
spread: Option<f32>,
|
||||||
|
) {
|
||||||
if let Some(path) = shape.get_skia_path() {
|
if let Some(path) = shape.get_skia_path() {
|
||||||
self.canvas_and_mark_dirty(id).draw_path(&path, paint);
|
let canvas = self.canvas_and_mark_dirty(id);
|
||||||
|
if let Some(s) = spread.filter(|&s| s > 0.0) {
|
||||||
|
// Draw path as a thick stroke to get outset (expanded) silhouette
|
||||||
|
let mut stroke_paint = paint.clone();
|
||||||
|
stroke_paint.set_stroke_width(s * 2.0);
|
||||||
|
canvas.draw_path(&path, &stroke_paint);
|
||||||
|
} else {
|
||||||
|
canvas.draw_path(&path, paint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -300,20 +300,7 @@ fn propagate_reflow(
|
|||||||
Type::Frame(Frame {
|
Type::Frame(Frame {
|
||||||
layout: Some(_), ..
|
layout: Some(_), ..
|
||||||
}) => {
|
}) => {
|
||||||
let mut skip_reflow = false;
|
layout_reflows.insert(*id);
|
||||||
if shape.is_layout_horizontal_fill() || shape.is_layout_vertical_fill() {
|
|
||||||
if let Some(parent_id) = shape.parent_id {
|
|
||||||
if parent_id != Uuid::nil() && !reflown.contains(&parent_id) {
|
|
||||||
// If this is a fill layout but the parent has not been reflown yet
|
|
||||||
// we wait for the next iteration for reflow
|
|
||||||
skip_reflow = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !skip_reflow {
|
|
||||||
layout_reflows.insert(*id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Type::Group(Group { masked: true }) => {
|
Type::Group(Group { masked: true }) => {
|
||||||
let children_ids = shape.children_ids(true);
|
let children_ids = shape.children_ids(true);
|
||||||
|
|||||||
@ -14,7 +14,7 @@ pushd $_SCRIPT_DIR;
|
|||||||
|
|
||||||
cargo watch \
|
cargo watch \
|
||||||
--why \
|
--why \
|
||||||
-i "_tmp*"
|
-i "_tmp*" \
|
||||||
-x "build $CARGO_PARAMS" \
|
-x "build $CARGO_PARAMS" \
|
||||||
-s "./build" \
|
-s "./build" \
|
||||||
-s "echo 'DONE\n'";
|
-s "echo 'DONE\n'";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user