diff --git a/CHANGES.md b/CHANGES.md index e8d958e95d..ce68dda7ab 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -52,6 +52,7 @@ - Fix missing text color token from selected shapes in selected colors list [Taiga #12956](https://tree.taiga.io/project/penpot/issue/12956) - Fix dropdown option width in Guides columns dropdown [Taiga #12959](https://tree.taiga.io/project/penpot/issue/12959) - Fix typos on download modal [Taiga #12865](https://tree.taiga.io/project/penpot/issue/12865) +- Fix problem with text editor maintaining previous styles [Taiga #12835](https://tree.taiga.io/project/penpot/issue/12835) ## 2.12.1 diff --git a/frontend/playwright/data/render-wasm/get-file-frame-nested-clipping.json b/frontend/playwright/data/render-wasm/get-file-frame-nested-clipping.json index 1a4d016d8f..6e68edf16b 100644 --- a/frontend/playwright/data/render-wasm/get-file-frame-nested-clipping.json +++ b/frontend/playwright/data/render-wasm/get-file-frame-nested-clipping.json @@ -1,1089 +1,1996 @@ { - "~:features": { - "~#set": [ - "fdata/path-data", - "plugins/runtime", - "design-tokens/v1", - "variants/v1", - "layout/grid", - "styles/v2", - "fdata/pointer-map", - "fdata/objects-map", - "render-wasm/v1", - "components/v2", - "fdata/shape-data-type" - ] - }, - "~:team-id": "~ueba8fa2e-4140-8084-8005-448635d7a724", - "~:permissions": { - "~:type": "~:membership", - "~:is-owner": true, - "~:is-admin": true, - "~:can-edit": true, - "~:can-read": true, - "~:is-logged": true - }, - "~:has-media-trimmed": false, - "~:comment-thread-seqn": 0, - "~:name": "Nested clipping", - "~:revn": 44, - "~:modified-at": "~m1764151542189", - "~:vern": 0, - "~:id": "~u44471494-966a-8178-8006-c5bd93f0fe72", - "~:is-shared": false, - "~:migrations": { - "~#ordered-set": [ - "legacy-2", - "legacy-3", - "legacy-5", - "legacy-6", - "legacy-7", - "legacy-8", - "legacy-9", - "legacy-10", - "legacy-11", - "legacy-12", - "legacy-13", - "legacy-14", - "legacy-16", - "legacy-17", - "legacy-18", - "legacy-19", - "legacy-25", - "legacy-26", - "legacy-27", - "legacy-28", - "legacy-29", - "legacy-31", - "legacy-32", - "legacy-33", - "legacy-34", - "legacy-36", - "legacy-37", - "legacy-38", - "legacy-39", - "legacy-40", - "legacy-41", - "legacy-42", - "legacy-43", - "legacy-44", - "legacy-45", - "legacy-46", - "legacy-47", - "legacy-48", - "legacy-49", - "legacy-50", - "legacy-51", - "legacy-52", - "legacy-53", - "legacy-54", - "legacy-55", - "legacy-56", - "legacy-57", - "legacy-59", - "legacy-62", - "legacy-65", - "legacy-66", - "legacy-67", - "0001-remove-tokens-from-groups", - "0002-normalize-bool-content-v2", - "0002-clean-shape-interactions", - "0003-fix-root-shape", - "0003-convert-path-content-v2", - "0005-deprecate-image-type", - "0006-fix-old-texts-fills", - "0008-fix-library-colors-v4", - "0009-clean-library-colors", - "0009-add-partial-text-touched-flags", - "0010-fix-swap-slots-pointing-non-existent-shapes", - "0011-fix-invalid-text-touched-flags", - "0012-fix-position-data", - "0013-fix-component-path", - "0013-clear-invalid-strokes-and-fills", - "0014-fix-tokens-lib-duplicate-ids", - "0014-clear-components-nil-objects", - "0015-fix-text-attrs-blank-strings", - "0015-clean-shadow-color", - "0016-copy-fills-from-position-data-to-text-node" - ] - }, - "~:version": 67, - "~:project-id": "~ueba8fa2e-4140-8084-8005-448635da32b4", - "~:created-at": "~m1764144613130", - "~:backend": "legacy-db", - "~:data": { - "~:pages": [ - "~u44471494-966a-8178-8006-c5bd93f0fe73" - ], - "~:pages-index": { - "~u44471494-966a-8178-8006-c5bd93f0fe73": { - "~:objects": { - "~u00000000-0000-0000-0000-000000000000": { - "~#shape": { - "~:y": 0, - "~:hide-fill-on-export": false, - "~:transform": { - "~#matrix": { - "~:a": 1, - "~:b": 0, - "~:c": 0, - "~:d": 1, - "~:e": 0, - "~:f": 0 - } - }, - "~:rotation": 0, - "~:name": "Root Frame", - "~:width": 0.01, - "~:type": "~:frame", - "~:points": [ - { - "~#point": { - "~:x": 0, - "~:y": 0 - } - }, - { - "~#point": { - "~:x": 0.01, - "~:y": 0 - } - }, - { - "~#point": { - "~:x": 0.01, - "~:y": 0.01 - } - }, - { - "~#point": { - "~:x": 0, - "~:y": 0.01 - } - } - ], - "~:r2": 0, - "~:proportion-lock": false, - "~:transform-inverse": { - "~#matrix": { - "~:a": 1, - "~:b": 0, - "~:c": 0, - "~:d": 1, - "~:e": 0, - "~:f": 0 - } - }, - "~:r3": 0, - "~:r1": 0, - "~:id": "~u00000000-0000-0000-0000-000000000000", - "~:parent-id": "~u00000000-0000-0000-0000-000000000000", - "~:frame-id": "~u00000000-0000-0000-0000-000000000000", - "~:strokes": [], - "~:x": 0, - "~:proportion": 1, - "~:r4": 0, - "~:selrect": { - "~#rect": { + "~: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": "~ueba8fa2e-4140-8084-8005-448635d7a724", + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "Nested clipping", + "~:revn": 44, + "~:modified-at": "~m1764151542189", + "~:vern": 0, + "~:id": "~u44471494-966a-8178-8006-c5bd93f0fe72", + "~:is-shared": false, + "~:migrations": { + "~#ordered-set": [ + "legacy-2", + "legacy-3", + "legacy-5", + "legacy-6", + "legacy-7", + "legacy-8", + "legacy-9", + "legacy-10", + "legacy-11", + "legacy-12", + "legacy-13", + "legacy-14", + "legacy-16", + "legacy-17", + "legacy-18", + "legacy-19", + "legacy-25", + "legacy-26", + "legacy-27", + "legacy-28", + "legacy-29", + "legacy-31", + "legacy-32", + "legacy-33", + "legacy-34", + "legacy-36", + "legacy-37", + "legacy-38", + "legacy-39", + "legacy-40", + "legacy-41", + "legacy-42", + "legacy-43", + "legacy-44", + "legacy-45", + "legacy-46", + "legacy-47", + "legacy-48", + "legacy-49", + "legacy-50", + "legacy-51", + "legacy-52", + "legacy-53", + "legacy-54", + "legacy-55", + "legacy-56", + "legacy-57", + "legacy-59", + "legacy-62", + "legacy-65", + "legacy-66", + "legacy-67", + "0001-remove-tokens-from-groups", + "0002-normalize-bool-content-v2", + "0002-clean-shape-interactions", + "0003-fix-root-shape", + "0003-convert-path-content-v2", + "0005-deprecate-image-type", + "0006-fix-old-texts-fills", + "0008-fix-library-colors-v4", + "0009-clean-library-colors", + "0009-add-partial-text-touched-flags", + "0010-fix-swap-slots-pointing-non-existent-shapes", + "0011-fix-invalid-text-touched-flags", + "0012-fix-position-data", + "0013-fix-component-path", + "0013-clear-invalid-strokes-and-fills", + "0014-fix-tokens-lib-duplicate-ids", + "0014-clear-components-nil-objects", + "0015-fix-text-attrs-blank-strings", + "0015-clean-shadow-color", + "0016-copy-fills-from-position-data-to-text-node" + ] + }, + "~:version": 67, + "~:project-id": "~ueba8fa2e-4140-8084-8005-448635da32b4", + "~:created-at": "~m1768375757989", + "~:backend": "legacy-db", + "~:data": { + "~:pages": [ + "~u44471494-966a-8178-8006-c5bd93f0fe73" + ], + "~:pages-index": { + "~u44471494-966a-8178-8006-c5bd93f0fe73": { + "~:id": "~ubf963303-bcd6-8000-8007-6a0bc8b60ccb", + "~:name": "Page 1", + "~:objects": { + "~u00000000-0000-0000-0000-000000000000": { + "~#shape": { + "~:y": 0, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:name": "Root Frame", + "~:width": 0.01, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { "~:x": 0, - "~:y": 0, - "~:width": 0.01, - "~:height": 0.01, - "~:x1": 0, - "~:y1": 0, - "~:x2": 0.01, - "~:y2": 0.01 + "~:y": 0 } }, - "~:fills": [ - { - "~:fill-color": "#FFFFFF", - "~:fill-opacity": 1 - } - ], - "~:flip-x": null, - "~:height": 0.01, - "~:flip-y": null, - "~:shapes": [ - "~u571478fd-6386-8085-8007-2b11cd2fc79a", - "~u1a629c22-3d11-80b1-8007-2b2bf3d82765", - "~u1a629c22-3d11-80b1-8007-2b2c061d3786" - ] - } - }, - "~u571478fd-6386-8085-8007-2b11c3aa600f": { - "~#shape": { - "~:y": 440, - "~:transform": { - "~#matrix": { - "~:a": 1, - "~:b": 0, - "~:c": 0, - "~:d": 1, - "~:e": 0, - "~:f": 0 + { + "~#point": { + "~:x": 0.01, + "~:y": 0 } }, - "~:rotation": 0, - "~:grow-type": "~:fixed", - "~:hide-in-viewer": false, - "~:name": "Rectangle", - "~:width": 456, - "~:type": "~:rect", - "~:points": [ - { - "~#point": { - "~:x": 669, - "~:y": 440 - } - }, - { - "~#point": { - "~:x": 1125, - "~:y": 440 - } - }, - { - "~#point": { - "~:x": 1125, - "~:y": 609 - } - }, - { - "~#point": { - "~:x": 669, - "~:y": 609 - } - } - ], - "~:r2": 0, - "~:proportion-lock": false, - "~:transform-inverse": { - "~#matrix": { - "~:a": 1, - "~:b": 0, - "~:c": 0, - "~:d": 1, - "~:e": 0, - "~:f": 0 + { + "~#point": { + "~:x": 0.01, + "~:y": 0.01 } }, - "~:r3": 0, - "~:constraints-v": "~:top", - "~:constraints-h": "~:left", - "~:r1": 0, - "~:id": "~u571478fd-6386-8085-8007-2b11c3aa600f", - "~:parent-id": "~u571478fd-6386-8085-8007-2b11bf4e9c11", - "~:frame-id": "~u571478fd-6386-8085-8007-2b11bf4e9c11", - "~:strokes": [], - "~:x": 669, - "~:proportion": 1, - "~:r4": 0, - "~:selrect": { - "~#rect": { - "~:x": 669, - "~:y": 440, - "~:width": 456, - "~:height": 169, - "~:x1": 669, - "~:y1": 440, - "~:x2": 1125, - "~:y2": 609 + { + "~#point": { + "~:x": 0, + "~:y": 0.01 } - }, - "~:fills": [ - { - "~:fill-color": "#B1B2B5", - "~:fill-opacity": 1 - } - ], - "~:flip-x": null, - "~:height": 169, - "~:flip-y": null - } - }, - "~u571478fd-6386-8085-8007-2b11cd2fc79a": { - "~#shape": { - "~:y": 204, - "~:hide-fill-on-export": false, - "~:transform": { - "~#matrix": { - "~:a": 1, - "~:b": 0, - "~:c": 0, - "~:d": 1, - "~:e": 0, - "~:f": 0 - } - }, - "~:rotation": 0, - "~:grow-type": "~:fixed", - "~:hide-in-viewer": false, - "~:name": "Board", - "~:width": 535, - "~:type": "~:frame", - "~:points": [ - { - "~#point": { - "~:x": 333, - "~:y": 204 - } - }, - { - "~#point": { - "~:x": 868, - "~:y": 204 - } - }, - { - "~#point": { - "~:x": 868, - "~:y": 851 - } - }, - { - "~#point": { - "~:x": 333, - "~:y": 851 - } - } - ], - "~:r2": 0, - "~:proportion-lock": false, - "~:transform-inverse": { - "~#matrix": { - "~:a": 1, - "~:b": 0, - "~:c": 0, - "~:d": 1, - "~:e": 0, - "~:f": 0 - } - }, - "~:r3": 0, - "~:r1": 0, - "~:id": "~u571478fd-6386-8085-8007-2b11cd2fc79a", - "~:parent-id": "~u00000000-0000-0000-0000-000000000000", - "~:frame-id": "~u00000000-0000-0000-0000-000000000000", - "~:strokes": [], - "~:x": 333, - "~:proportion": 1, - "~:r4": 0, - "~:selrect": { - "~#rect": { - "~:x": 333, - "~:y": 204, - "~:width": 535, - "~:height": 647, - "~:x1": 333, - "~:y1": 204, - "~:x2": 868, - "~:y2": 851 - } - }, - "~:fills": [ - { - "~:fill-color": "#FFFFFF", - "~:fill-opacity": 1 - } - ], - "~:flip-x": null, - "~:height": 647, - "~:flip-y": null, - "~:shapes": [ - "~u571478fd-6386-8085-8007-2b11bf4e9c11" - ] - } - }, - "~u1a629c22-3d11-80b1-8007-2b2c061d3788": { - "~#shape": { - "~:y": 1173, - "~:transform": { - "~#matrix": { - "~:a": 1, - "~:b": 0, - "~:c": 0, - "~:d": 1, - "~:e": 0, - "~:f": 0 - } - }, - "~:rotation": 0, - "~:grow-type": "~:fixed", - "~:hide-in-viewer": false, - "~:name": "Rectangle", - "~:width": 456, - "~:type": "~:rect", - "~:points": [ - { - "~#point": { - "~:x": 1254, - "~:y": 1173 - } - }, - { - "~#point": { - "~:x": 1710, - "~:y": 1173 - } - }, - { - "~#point": { - "~:x": 1710, - "~:y": 1342 - } - }, - { - "~#point": { - "~:x": 1254, - "~:y": 1342 - } - } - ], - "~:r2": 0, - "~:proportion-lock": false, - "~:transform-inverse": { - "~#matrix": { - "~:a": 1, - "~:b": 0, - "~:c": 0, - "~:d": 1, - "~:e": 0, - "~:f": 0 - } - }, - "~:r3": 0, - "~:constraints-v": "~:top", - "~:constraints-h": "~:left", - "~:r1": 0, - "~:id": "~u1a629c22-3d11-80b1-8007-2b2c061d3788", - "~:parent-id": "~u1a629c22-3d11-80b1-8007-2b2c061d3787", - "~:frame-id": "~u1a629c22-3d11-80b1-8007-2b2c061d3787", - "~:strokes": [], - "~:x": 1254, - "~:proportion": 1, - "~:r4": 0, - "~:selrect": { - "~#rect": { - "~:x": 1254, - "~:y": 1173, - "~:width": 456, - "~:height": 169, - "~:x1": 1254, - "~:y1": 1173, - "~:x2": 1710, - "~:y2": 1342 - } - }, - "~:fills": [ - { - "~:fill-color": "#B1B2B5", - "~:fill-opacity": 1 - } - ], - "~:flip-x": null, - "~:height": 169, - "~:flip-y": null - } - }, - "~u1a629c22-3d11-80b1-8007-2b2c061d3787": { - "~#shape": { - "~:y": 1042, - "~:hide-fill-on-export": false, - "~:transform": { - "~#matrix": { - "~:a": 1, - "~:b": 0, - "~:c": 0, - "~:d": 1, - "~:e": 0, - "~:f": 0 - } - }, - "~:rotation": 0, - "~:grow-type": "~:fixed", - "~:hide-in-viewer": true, - "~:name": "Board", - "~:width": 518, - "~:type": "~:frame", - "~:points": [ - { - "~#point": { - "~:x": 1106, - "~:y": 1042 - } - }, - { - "~#point": { - "~:x": 1624, - "~:y": 1042 - } - }, - { - "~#point": { - "~:x": 1624, - "~:y": 1466 - } - }, - { - "~#point": { - "~:x": 1106, - "~:y": 1466 - } - } - ], - "~:r2": 0, - "~:proportion-lock": false, - "~:transform-inverse": { - "~#matrix": { - "~:a": 1, - "~:b": 0, - "~:c": 0, - "~:d": 1, - "~:e": 0, - "~:f": 0 - } - }, - "~:r3": 0, - "~:constraints-v": "~:top", - "~:constraints-h": "~:left", - "~:r1": 0, - "~:id": "~u1a629c22-3d11-80b1-8007-2b2c061d3787", - "~:parent-id": "~u1a629c22-3d11-80b1-8007-2b2c061d3786", - "~:frame-id": "~u1a629c22-3d11-80b1-8007-2b2c061d3786", - "~:strokes": [], - "~:x": 1106, - "~:proportion": 1, - "~:r4": 0, - "~:selrect": { - "~#rect": { - "~:x": 1106, - "~:y": 1042, - "~:width": 518, - "~:height": 424, - "~:x1": 1106, - "~:y1": 1042, - "~:x2": 1624, - "~:y2": 1466 - } - }, - "~:fills": [ - { - "~:fill-color": "#dc0606", - "~:fill-opacity": 1 - } - ], - "~:flip-x": null, - "~:height": 424, - "~:flip-y": null, - "~:shapes": [ - "~u1a629c22-3d11-80b1-8007-2b2c061d3788" - ] - } - }, - "~u571478fd-6386-8085-8007-2b11bf4e9c11": { - "~#shape": { - "~:y": 309, - "~:hide-fill-on-export": false, - "~:transform": { - "~#matrix": { - "~:a": 1, - "~:b": 0, - "~:c": 0, - "~:d": 1, - "~:e": 0, - "~:f": 0 - } - }, - "~:rotation": 0, - "~:grow-type": "~:fixed", - "~:hide-in-viewer": true, - "~:name": "Board", - "~:width": 518, - "~:type": "~:frame", - "~:points": [ - { - "~#point": { - "~:x": 521, - "~:y": 309 - } - }, - { - "~#point": { - "~:x": 1039, - "~:y": 309 - } - }, - { - "~#point": { - "~:x": 1039, - "~:y": 733 - } - }, - { - "~#point": { - "~:x": 521, - "~:y": 733 - } - } - ], - "~:r2": 0, - "~:proportion-lock": false, - "~:transform-inverse": { - "~#matrix": { - "~:a": 1, - "~:b": 0, - "~:c": 0, - "~:d": 1, - "~:e": 0, - "~:f": 0 - } - }, - "~:r3": 0, - "~:constraints-v": "~:top", - "~:constraints-h": "~:left", - "~:r1": 0, - "~:id": "~u571478fd-6386-8085-8007-2b11bf4e9c11", - "~:parent-id": "~u571478fd-6386-8085-8007-2b11cd2fc79a", - "~:frame-id": "~u571478fd-6386-8085-8007-2b11cd2fc79a", - "~:strokes": [], - "~:x": 521, - "~:proportion": 1, - "~:r4": 0, - "~:selrect": { - "~#rect": { - "~:x": 521, - "~:y": 309, - "~:width": 518, - "~:height": 424, - "~:x1": 521, - "~:y1": 309, - "~:x2": 1039, - "~:y2": 733 - } - }, - "~:fills": [ - { - "~:fill-color": "#dc0606", - "~:fill-opacity": 1 - } - ], - "~:flip-x": null, - "~:height": 424, - "~:flip-y": null, - "~:shapes": [ - "~u571478fd-6386-8085-8007-2b11c3aa600f" - ] - } - }, - "~u1a629c22-3d11-80b1-8007-2b2c061d3786": { - "~#shape": { - "~:y": 937, - "~:hide-fill-on-export": false, - "~:transform": { - "~#matrix": { - "~:a": 1, - "~:b": 0, - "~:c": 0, - "~:d": 1, - "~:e": 0, - "~:f": 0 - } - }, - "~:rotation": 0, - "~:grow-type": "~:fixed", - "~:hide-in-viewer": false, - "~:name": "Board", - "~:width": 535, - "~:type": "~:frame", - "~:points": [ - { - "~#point": { - "~:x": 918, - "~:y": 937 - } - }, - { - "~#point": { - "~:x": 1453, - "~:y": 937 - } - }, - { - "~#point": { - "~:x": 1453, - "~:y": 1584 - } - }, - { - "~#point": { - "~:x": 918, - "~:y": 1584 - } - } - ], - "~:r2": 0, - "~:proportion-lock": false, - "~:transform-inverse": { - "~#matrix": { - "~:a": 1, - "~:b": 0, - "~:c": 0, - "~:d": 1, - "~:e": 0, - "~:f": 0 - } - }, - "~:r3": 0, - "~:blur": { - "~:id": "~u1a629c22-3d11-80b1-8007-2b2c031523df", - "~:type": "~:layer-blur", - "~:value": 4, - "~:hidden": false - }, - "~:r1": 0, - "~:id": "~u1a629c22-3d11-80b1-8007-2b2c061d3786", - "~:parent-id": "~u00000000-0000-0000-0000-000000000000", - "~:frame-id": "~u00000000-0000-0000-0000-000000000000", - "~:strokes": [], - "~:x": 918, - "~:proportion": 1, - "~:shadow": [ - { - "~:id": "~u1a629c22-3d11-80b1-8007-2b2c0899422b", - "~:style": "~:drop-shadow", - "~:color": { - "~:color": "#000000", - "~:opacity": 1 - }, - "~:offset-x": 40, - "~:offset-y": 40, - "~:blur": 4, - "~:spread": 0, - "~:hidden": false - } - ], - "~:r4": 0, - "~:selrect": { - "~#rect": { - "~:x": 918, - "~:y": 937, - "~:width": 535, - "~:height": 647, - "~:x1": 918, - "~:y1": 937, - "~:x2": 1453, - "~:y2": 1584 - } - }, - "~:fills": [ - { - "~:fill-color": "#FFFFFF", - "~:fill-opacity": 1 - } - ], - "~:flip-x": null, - "~:height": 647, - "~:flip-y": null, - "~:shapes": [ - "~u1a629c22-3d11-80b1-8007-2b2c061d3787" - ] - } - }, - "~u1a629c22-3d11-80b1-8007-2b2bf3d82765": { - "~#shape": { - "~:y": 937, - "~:hide-fill-on-export": false, - "~:transform": { - "~#matrix": { - "~:a": 1, - "~:b": 0, - "~:c": 0, - "~:d": 1, - "~:e": 0, - "~:f": 0 - } - }, - "~:rotation": 0, - "~:grow-type": "~:fixed", - "~:hide-in-viewer": false, - "~:name": "Board", - "~:width": 535, - "~:type": "~:frame", - "~:points": [ - { - "~#point": { - "~:x": 333, - "~:y": 937 - } - }, - { - "~#point": { - "~:x": 868, - "~:y": 937 - } - }, - { - "~#point": { - "~:x": 868, - "~:y": 1584 - } - }, - { - "~#point": { - "~:x": 333, - "~:y": 1584 - } - } - ], - "~:r2": 0, - "~:proportion-lock": false, - "~:transform-inverse": { - "~#matrix": { - "~:a": 1, - "~:b": 0, - "~:c": 0, - "~:d": 1, - "~:e": 0, - "~:f": 0 - } - }, - "~:r3": 0, - "~:blur": { - "~:id": "~u1a629c22-3d11-80b1-8007-2b2c031523df", - "~:type": "~:layer-blur", - "~:value": 4, - "~:hidden": false - }, - "~:r1": 0, - "~:id": "~u1a629c22-3d11-80b1-8007-2b2bf3d82765", - "~:parent-id": "~u00000000-0000-0000-0000-000000000000", - "~:frame-id": "~u00000000-0000-0000-0000-000000000000", - "~:strokes": [], - "~:x": 333, - "~:proportion": 1, - "~:r4": 0, - "~:selrect": { - "~#rect": { - "~:x": 333, - "~:y": 937, - "~:width": 535, - "~:height": 647, - "~:x1": 333, - "~:y1": 937, - "~:x2": 868, - "~:y2": 1584 - } - }, - "~:fills": [ - { - "~:fill-color": "#FFFFFF", - "~:fill-opacity": 1 - } - ], - "~:flip-x": null, - "~:height": 647, - "~:flip-y": null, - "~:shapes": [ - "~u1a629c22-3d11-80b1-8007-2b2bf3d82766" - ] - } - }, - "~u1a629c22-3d11-80b1-8007-2b2bf3d82766": { - "~#shape": { - "~:y": 1042, - "~:hide-fill-on-export": false, - "~:transform": { - "~#matrix": { - "~:a": 1, - "~:b": 0, - "~:c": 0, - "~:d": 1, - "~:e": 0, - "~:f": 0 - } - }, - "~:rotation": 0, - "~:grow-type": "~:fixed", - "~:hide-in-viewer": true, - "~:name": "Board", - "~:width": 518, - "~:type": "~:frame", - "~:points": [ - { - "~#point": { - "~:x": 521, - "~:y": 1042 - } - }, - { - "~#point": { - "~:x": 1039, - "~:y": 1042 - } - }, - { - "~#point": { - "~:x": 1039, - "~:y": 1466 - } - }, - { - "~#point": { - "~:x": 521, - "~:y": 1466 - } - } - ], - "~:r2": 0, - "~:proportion-lock": false, - "~:transform-inverse": { - "~#matrix": { - "~:a": 1, - "~:b": 0, - "~:c": 0, - "~:d": 1, - "~:e": 0, - "~:f": 0 - } - }, - "~:r3": 0, - "~:constraints-v": "~:top", - "~:constraints-h": "~:left", - "~:r1": 0, - "~:id": "~u1a629c22-3d11-80b1-8007-2b2bf3d82766", - "~:parent-id": "~u1a629c22-3d11-80b1-8007-2b2bf3d82765", - "~:frame-id": "~u1a629c22-3d11-80b1-8007-2b2bf3d82765", - "~:strokes": [], - "~:x": 521, - "~:proportion": 1, - "~:r4": 0, - "~:selrect": { - "~#rect": { - "~:x": 521, - "~:y": 1042, - "~:width": 518, - "~:height": 424, - "~:x1": 521, - "~:y1": 1042, - "~:x2": 1039, - "~:y2": 1466 - } - }, - "~:fills": [ - { - "~:fill-color": "#dc0606", - "~:fill-opacity": 1 - } - ], - "~:flip-x": null, - "~:height": 424, - "~:flip-y": null, - "~:shapes": [ - "~u1a629c22-3d11-80b1-8007-2b2bf3d82767" - ] - } - }, - "~u1a629c22-3d11-80b1-8007-2b2bf3d82767": { - "~#shape": { - "~:y": 1173, - "~:transform": { - "~#matrix": { - "~:a": 1, - "~:b": 0, - "~:c": 0, - "~:d": 1, - "~:e": 0, - "~:f": 0 - } - }, - "~:rotation": 0, - "~:grow-type": "~:fixed", - "~:hide-in-viewer": false, - "~:name": "Rectangle", - "~:width": 456, - "~:type": "~:rect", - "~:points": [ - { - "~#point": { - "~:x": 669, - "~:y": 1173 - } - }, - { - "~#point": { - "~:x": 1125, - "~:y": 1173 - } - }, - { - "~#point": { - "~:x": 1125, - "~:y": 1342 - } - }, - { - "~#point": { - "~:x": 669, - "~:y": 1342 - } - } - ], - "~:r2": 0, - "~:proportion-lock": false, - "~:transform-inverse": { - "~#matrix": { - "~:a": 1, - "~:b": 0, - "~:c": 0, - "~:d": 1, - "~:e": 0, - "~:f": 0 - } - }, - "~:r3": 0, - "~:constraints-v": "~:top", - "~:constraints-h": "~:left", - "~:r1": 0, - "~:id": "~u1a629c22-3d11-80b1-8007-2b2bf3d82767", - "~:parent-id": "~u1a629c22-3d11-80b1-8007-2b2bf3d82766", - "~:frame-id": "~u1a629c22-3d11-80b1-8007-2b2bf3d82766", - "~:strokes": [], - "~:x": 669, - "~:proportion": 1, - "~:r4": 0, - "~:selrect": { - "~#rect": { - "~:x": 669, - "~:y": 1173, - "~:width": 456, - "~:height": 169, - "~:x1": 669, - "~:y1": 1173, - "~:x2": 1125, - "~:y2": 1342 - } - }, - "~:fills": [ - { - "~:fill-color": "#B1B2B5", - "~:fill-opacity": 1 - } - ], - "~:flip-x": null, - "~:height": 169, - "~:flip-y": null - } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:page-id": "~ubf963303-bcd6-8000-8007-6a0bc8b60ccb", + "~:r3": 0, + "~:r1": 0, + "~:id": "~u00000000-0000-0000-0000-000000000000", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 0, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 0, + "~:y": 0, + "~:width": 0.01, + "~:height": 0.01, + "~:x1": 0, + "~:y1": 0, + "~:x2": 0.01, + "~:y2": 0.01 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 0.01, + "~:flip-y": null, + "~:shapes": [ + "~ue5199cd7-69e2-8001-8007-6a1bc188c72e", + "~ue5199cd7-69e2-8001-8007-6a1bccaf0d96", + "~ue5199cd7-69e2-8001-8007-6a1bc188c731", + "~ue5199cd7-69e2-8001-8007-6a1beb087a2f", + "~ue5199cd7-69e2-8001-8007-6a1bc188c734", + "~ue5199cd7-69e2-8001-8007-6a1beb087a2c" + ] } }, - "~:id": "~u1dc9717a-2217-80f7-8007-2b11bac2823f", - "~:name": "Page 1" + "~ue5199cd7-69e2-8001-8007-6a1beb087a2d": { + "~#shape": { + "~:y": 1864.99995803833, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": true, + "~:name": "Board", + "~:width": 518, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 1006, + "~:y": 1864.99995803833 + } + }, + { + "~#point": { + "~:x": 1524, + "~:y": 1864.99995803833 + } + }, + { + "~#point": { + "~:x": 1524, + "~:y": 2288.99995803833 + } + }, + { + "~#point": { + "~:x": 1006, + "~:y": 2288.99995803833 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:page-id": "~ubf963303-bcd6-8000-8007-6a0bc8b60ccb", + "~:r3": 0, + "~:constraints-v": "~:top", + "~:constraints-h": "~:left", + "~:r1": 0, + "~:id": "~ue5199cd7-69e2-8001-8007-6a1beb087a2d", + "~:parent-id": "~ue5199cd7-69e2-8001-8007-6a1beb087a2c", + "~:frame-id": "~ue5199cd7-69e2-8001-8007-6a1beb087a2c", + "~:strokes": [ + { + "~:stroke-alignment": "~:inner", + "~:stroke-style": "~:solid", + "~:stroke-color": "#000000", + "~:stroke-opacity": 1, + "~:stroke-width": 10 + } + ], + "~:x": 1006, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 1006, + "~:y": 1864.99995803833, + "~:width": 518, + "~:height": 424, + "~:x1": 1006, + "~:y1": 1864.99995803833, + "~:x2": 1524, + "~:y2": 2288.99995803833 + } + }, + "~:fills": [ + { + "~:fill-color": "#dc0606", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 424, + "~:flip-y": null, + "~:shapes": [ + "~ue5199cd7-69e2-8001-8007-6a1beb087a2e" + ] + } + }, + "~ue5199cd7-69e2-8001-8007-6a1beb087a2c": { + "~#shape": { + "~:y": 1759.99995803833, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Board", + "~:width": 535, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 818, + "~:y": 1759.99995803833 + } + }, + { + "~#point": { + "~:x": 1353, + "~:y": 1759.99995803833 + } + }, + { + "~#point": { + "~:x": 1353, + "~:y": 2406.99995803833 + } + }, + { + "~#point": { + "~:x": 818, + "~:y": 2406.99995803833 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:page-id": "~ubf963303-bcd6-8000-8007-6a0bc8b60ccb", + "~:r3": 0, + "~:blur": { + "~:id": "~u1a629c22-3d11-80b1-8007-2b2c031523df", + "~:type": "~:layer-blur", + "~:value": 4, + "~:hidden": false + }, + "~:r1": 0, + "~:id": "~ue5199cd7-69e2-8001-8007-6a1beb087a2c", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 818, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u1a629c22-3d11-80b1-8007-2b2c0899422b", + "~:style": "~:drop-shadow", + "~:color": { + "~:opacity": 1, + "~:color": "#000000" + }, + "~:offset-x": 40, + "~:offset-y": 40, + "~:blur": 4, + "~:spread": 0, + "~:hidden": false + } + ], + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 818, + "~:y": 1759.99995803833, + "~:width": 535, + "~:height": 647, + "~:x1": 818, + "~:y1": 1759.99995803833, + "~:x2": 1353, + "~:y2": 2406.99995803833 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 647, + "~:flip-y": null, + "~:shapes": [ + "~ue5199cd7-69e2-8001-8007-6a1beb087a2d" + ] + } + }, + "~ue5199cd7-69e2-8001-8007-6a1beb087a2f": { + "~#shape": { + "~:y": 1759.99995803833, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Board", + "~:width": 535, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 233, + "~:y": 1759.99995803833 + } + }, + { + "~#point": { + "~:x": 768, + "~:y": 1759.99995803833 + } + }, + { + "~#point": { + "~:x": 768, + "~:y": 2406.99995803833 + } + }, + { + "~#point": { + "~:x": 233, + "~:y": 2406.99995803833 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:page-id": "~ubf963303-bcd6-8000-8007-6a0bc8b60ccb", + "~:r3": 0, + "~:blur": { + "~:id": "~u1a629c22-3d11-80b1-8007-2b2c031523df", + "~:type": "~:layer-blur", + "~:value": 4, + "~:hidden": false + }, + "~:r1": 0, + "~:id": "~ue5199cd7-69e2-8001-8007-6a1beb087a2f", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 233, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 233, + "~:y": 1759.99995803833, + "~:width": 535, + "~:height": 647, + "~:x1": 233, + "~:y1": 1759.99995803833, + "~:x2": 768, + "~:y2": 2406.99995803833 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 647, + "~:flip-y": null, + "~:shapes": [ + "~ue5199cd7-69e2-8001-8007-6a1beb087a30" + ] + } + }, + "~ue5199cd7-69e2-8001-8007-6a1bc188c72f": { + "~#shape": { + "~:y": 375, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": true, + "~:name": "Board", + "~:width": 518, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 421, + "~:y": 375 + } + }, + { + "~#point": { + "~:x": 939, + "~:y": 375 + } + }, + { + "~#point": { + "~:x": 939, + "~:y": 799 + } + }, + { + "~#point": { + "~:x": 421, + "~:y": 799 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:page-id": "~ubf963303-bcd6-8000-8007-6a0bc8b60ccb", + "~:r3": 0, + "~:constraints-v": "~:top", + "~:constraints-h": "~:left", + "~:r1": 0, + "~:id": "~ue5199cd7-69e2-8001-8007-6a1bc188c72f", + "~:parent-id": "~ue5199cd7-69e2-8001-8007-6a1bc188c72e", + "~:frame-id": "~ue5199cd7-69e2-8001-8007-6a1bc188c72e", + "~:strokes": [], + "~:x": 421, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 421, + "~:y": 375, + "~:width": 518, + "~:height": 424, + "~:x1": 421, + "~:y1": 375, + "~:x2": 939, + "~:y2": 799 + } + }, + "~:fills": [ + { + "~:fill-color": "#dc0606", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 424, + "~:flip-y": null, + "~:shapes": [ + "~ue5199cd7-69e2-8001-8007-6a1bc188c730" + ] + } + }, + "~ue5199cd7-69e2-8001-8007-6a1beb087a2e": { + "~#shape": { + "~:y": 1995.99995803833, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 456, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 1154, + "~:y": 1995.99995803833 + } + }, + { + "~#point": { + "~:x": 1610, + "~:y": 1995.99995803833 + } + }, + { + "~#point": { + "~:x": 1610, + "~:y": 2164.99995803833 + } + }, + { + "~#point": { + "~:x": 1154, + "~:y": 2164.99995803833 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:page-id": "~ubf963303-bcd6-8000-8007-6a0bc8b60ccb", + "~:r3": 0, + "~:constraints-v": "~:top", + "~:constraints-h": "~:left", + "~:r1": 0, + "~:id": "~ue5199cd7-69e2-8001-8007-6a1beb087a2e", + "~:parent-id": "~ue5199cd7-69e2-8001-8007-6a1beb087a2d", + "~:frame-id": "~ue5199cd7-69e2-8001-8007-6a1beb087a2d", + "~:strokes": [], + "~:x": 1154, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 1154, + "~:y": 1995.99995803833, + "~:width": 456, + "~:height": 169, + "~:x1": 1154, + "~:y1": 1995.99995803833, + "~:x2": 1610, + "~:y2": 2164.99995803833 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 169, + "~:flip-y": null + } + }, + "~ue5199cd7-69e2-8001-8007-6a1bc188c72e": { + "~#shape": { + "~:y": 270, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Board", + "~:width": 535, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 233, + "~:y": 270 + } + }, + { + "~#point": { + "~:x": 768, + "~:y": 270 + } + }, + { + "~#point": { + "~:x": 768, + "~:y": 917 + } + }, + { + "~#point": { + "~:x": 233, + "~:y": 917 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:page-id": "~ubf963303-bcd6-8000-8007-6a0bc8b60ccb", + "~:r3": 0, + "~:r1": 0, + "~:id": "~ue5199cd7-69e2-8001-8007-6a1bc188c72e", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 233, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 233, + "~:y": 270, + "~:width": 535, + "~:height": 647, + "~:x1": 233, + "~:y1": 270, + "~:x2": 768, + "~:y2": 917 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 647, + "~:flip-y": null, + "~:shapes": [ + "~ue5199cd7-69e2-8001-8007-6a1bc188c72f" + ] + } + }, + "~ue5199cd7-69e2-8001-8007-6a1bccaf0d98": { + "~#shape": { + "~:y": 506, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 456, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 1154, + "~:y": 506 + } + }, + { + "~#point": { + "~:x": 1610, + "~:y": 506 + } + }, + { + "~#point": { + "~:x": 1610, + "~:y": 675 + } + }, + { + "~#point": { + "~:x": 1154, + "~:y": 675 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:page-id": "~ubf963303-bcd6-8000-8007-6a0bc8b60ccb", + "~:r3": 0, + "~:constraints-v": "~:top", + "~:constraints-h": "~:left", + "~:r1": 0, + "~:id": "~ue5199cd7-69e2-8001-8007-6a1bccaf0d98", + "~:parent-id": "~ue5199cd7-69e2-8001-8007-6a1bccaf0d97", + "~:frame-id": "~ue5199cd7-69e2-8001-8007-6a1bccaf0d97", + "~:strokes": [], + "~:x": 1154, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 1154, + "~:y": 506, + "~:width": 456, + "~:height": 169, + "~:x1": 1154, + "~:y1": 506, + "~:x2": 1610, + "~:y2": 675 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 169, + "~:flip-y": null + } + }, + "~ue5199cd7-69e2-8001-8007-6a1bc188c735": { + "~#shape": { + "~:y": 1108, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": true, + "~:name": "Board", + "~:width": 518, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 1006, + "~:y": 1108 + } + }, + { + "~#point": { + "~:x": 1524, + "~:y": 1108 + } + }, + { + "~#point": { + "~:x": 1524, + "~:y": 1532 + } + }, + { + "~#point": { + "~:x": 1006, + "~:y": 1532 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:page-id": "~ubf963303-bcd6-8000-8007-6a0bc8b60ccb", + "~:r3": 0, + "~:constraints-v": "~:top", + "~:constraints-h": "~:left", + "~:r1": 0, + "~:id": "~ue5199cd7-69e2-8001-8007-6a1bc188c735", + "~:parent-id": "~ue5199cd7-69e2-8001-8007-6a1bc188c734", + "~:frame-id": "~ue5199cd7-69e2-8001-8007-6a1bc188c734", + "~:strokes": [], + "~:x": 1006, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 1006, + "~:y": 1108, + "~:width": 518, + "~:height": 424, + "~:x1": 1006, + "~:y1": 1108, + "~:x2": 1524, + "~:y2": 1532 + } + }, + "~:fills": [ + { + "~:fill-color": "#dc0606", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 424, + "~:flip-y": null, + "~:shapes": [ + "~ue5199cd7-69e2-8001-8007-6a1bc188c736" + ] + } + }, + "~ue5199cd7-69e2-8001-8007-6a1bc188c734": { + "~#shape": { + "~:y": 1003, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Board", + "~:width": 535, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 818, + "~:y": 1003 + } + }, + { + "~#point": { + "~:x": 1353, + "~:y": 1003 + } + }, + { + "~#point": { + "~:x": 1353, + "~:y": 1650 + } + }, + { + "~#point": { + "~:x": 818, + "~:y": 1650 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:page-id": "~ubf963303-bcd6-8000-8007-6a0bc8b60ccb", + "~:r3": 0, + "~:blur": { + "~:id": "~u1a629c22-3d11-80b1-8007-2b2c031523df", + "~:type": "~:layer-blur", + "~:value": 4, + "~:hidden": false + }, + "~:r1": 0, + "~:id": "~ue5199cd7-69e2-8001-8007-6a1bc188c734", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 818, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u1a629c22-3d11-80b1-8007-2b2c0899422b", + "~:style": "~:drop-shadow", + "~:color": { + "~:opacity": 1, + "~:color": "#000000" + }, + "~:offset-x": 40, + "~:offset-y": 40, + "~:blur": 4, + "~:spread": 0, + "~:hidden": false + } + ], + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 818, + "~:y": 1003, + "~:width": 535, + "~:height": 647, + "~:x1": 818, + "~:y1": 1003, + "~:x2": 1353, + "~:y2": 1650 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 647, + "~:flip-y": null, + "~:shapes": [ + "~ue5199cd7-69e2-8001-8007-6a1bc188c735" + ] + } + }, + "~ue5199cd7-69e2-8001-8007-6a1bccaf0d97": { + "~#shape": { + "~:y": 375, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": true, + "~:name": "Board", + "~:width": 518, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 1006, + "~:y": 375 + } + }, + { + "~#point": { + "~:x": 1524, + "~:y": 375 + } + }, + { + "~#point": { + "~:x": 1524, + "~:y": 799 + } + }, + { + "~#point": { + "~:x": 1006, + "~:y": 799 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:page-id": "~ubf963303-bcd6-8000-8007-6a0bc8b60ccb", + "~:r3": 0, + "~:constraints-v": "~:top", + "~:constraints-h": "~:left", + "~:r1": 0, + "~:id": "~ue5199cd7-69e2-8001-8007-6a1bccaf0d97", + "~:parent-id": "~ue5199cd7-69e2-8001-8007-6a1bccaf0d96", + "~:frame-id": "~ue5199cd7-69e2-8001-8007-6a1bccaf0d96", + "~:strokes": [ + { + "~:stroke-alignment": "~:inner", + "~:stroke-style": "~:solid", + "~:stroke-color": "#000000", + "~:stroke-opacity": 1, + "~:stroke-width": 10 + } + ], + "~:x": 1006, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 1006, + "~:y": 375, + "~:width": 518, + "~:height": 424, + "~:x1": 1006, + "~:y1": 375, + "~:x2": 1524, + "~:y2": 799 + } + }, + "~:fills": [ + { + "~:fill-color": "#dc0606", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 424, + "~:flip-y": null, + "~:shapes": [ + "~ue5199cd7-69e2-8001-8007-6a1bccaf0d98" + ] + } + }, + "~ue5199cd7-69e2-8001-8007-6a1bc188c736": { + "~#shape": { + "~:y": 1239, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 456, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 1154, + "~:y": 1239 + } + }, + { + "~#point": { + "~:x": 1610, + "~:y": 1239 + } + }, + { + "~#point": { + "~:x": 1610, + "~:y": 1408 + } + }, + { + "~#point": { + "~:x": 1154, + "~:y": 1408 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:page-id": "~ubf963303-bcd6-8000-8007-6a0bc8b60ccb", + "~:r3": 0, + "~:constraints-v": "~:top", + "~:constraints-h": "~:left", + "~:r1": 0, + "~:id": "~ue5199cd7-69e2-8001-8007-6a1bc188c736", + "~:parent-id": "~ue5199cd7-69e2-8001-8007-6a1bc188c735", + "~:frame-id": "~ue5199cd7-69e2-8001-8007-6a1bc188c735", + "~:strokes": [], + "~:x": 1154, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 1154, + "~:y": 1239, + "~:width": 456, + "~:height": 169, + "~:x1": 1154, + "~:y1": 1239, + "~:x2": 1610, + "~:y2": 1408 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 169, + "~:flip-y": null + } + }, + "~ue5199cd7-69e2-8001-8007-6a1bccaf0d96": { + "~#shape": { + "~:y": 270, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Board", + "~:width": 535, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 818, + "~:y": 270 + } + }, + { + "~#point": { + "~:x": 1353, + "~:y": 270 + } + }, + { + "~#point": { + "~:x": 1353, + "~:y": 917 + } + }, + { + "~#point": { + "~:x": 818, + "~:y": 917 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:page-id": "~ubf963303-bcd6-8000-8007-6a0bc8b60ccb", + "~:r3": 0, + "~:r1": 0, + "~:id": "~ue5199cd7-69e2-8001-8007-6a1bccaf0d96", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 818, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 818, + "~:y": 270, + "~:width": 535, + "~:height": 647, + "~:x1": 818, + "~:y1": 270, + "~:x2": 1353, + "~:y2": 917 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 647, + "~:flip-y": null, + "~:shapes": [ + "~ue5199cd7-69e2-8001-8007-6a1bccaf0d97" + ] + } + }, + "~ue5199cd7-69e2-8001-8007-6a1beb087a31": { + "~#shape": { + "~:y": 1995.99995803833, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 456, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 569, + "~:y": 1995.99995803833 + } + }, + { + "~#point": { + "~:x": 1025, + "~:y": 1995.99995803833 + } + }, + { + "~#point": { + "~:x": 1025, + "~:y": 2164.99995803833 + } + }, + { + "~#point": { + "~:x": 569, + "~:y": 2164.99995803833 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:page-id": "~ubf963303-bcd6-8000-8007-6a0bc8b60ccb", + "~:r3": 0, + "~:constraints-v": "~:top", + "~:constraints-h": "~:left", + "~:r1": 0, + "~:id": "~ue5199cd7-69e2-8001-8007-6a1beb087a31", + "~:parent-id": "~ue5199cd7-69e2-8001-8007-6a1beb087a30", + "~:frame-id": "~ue5199cd7-69e2-8001-8007-6a1beb087a30", + "~:strokes": [], + "~:x": 569, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 569, + "~:y": 1995.99995803833, + "~:width": 456, + "~:height": 169, + "~:x1": 569, + "~:y1": 1995.99995803833, + "~:x2": 1025, + "~:y2": 2164.99995803833 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 169, + "~:flip-y": null + } + }, + "~ue5199cd7-69e2-8001-8007-6a1bc188c731": { + "~#shape": { + "~:y": 1003, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Board", + "~:width": 535, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 233, + "~:y": 1003 + } + }, + { + "~#point": { + "~:x": 768, + "~:y": 1003 + } + }, + { + "~#point": { + "~:x": 768, + "~:y": 1650 + } + }, + { + "~#point": { + "~:x": 233, + "~:y": 1650 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:page-id": "~ubf963303-bcd6-8000-8007-6a0bc8b60ccb", + "~:r3": 0, + "~:blur": { + "~:id": "~u1a629c22-3d11-80b1-8007-2b2c031523df", + "~:type": "~:layer-blur", + "~:value": 4, + "~:hidden": false + }, + "~:r1": 0, + "~:id": "~ue5199cd7-69e2-8001-8007-6a1bc188c731", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 233, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 233, + "~:y": 1003, + "~:width": 535, + "~:height": 647, + "~:x1": 233, + "~:y1": 1003, + "~:x2": 768, + "~:y2": 1650 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 647, + "~:flip-y": null, + "~:shapes": [ + "~ue5199cd7-69e2-8001-8007-6a1bc188c732" + ] + } + }, + "~ue5199cd7-69e2-8001-8007-6a1beb087a30": { + "~#shape": { + "~:y": 1864.99995803833, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": true, + "~:name": "Board", + "~:width": 518, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 421, + "~:y": 1864.99995803833 + } + }, + { + "~#point": { + "~:x": 939, + "~:y": 1864.99995803833 + } + }, + { + "~#point": { + "~:x": 939, + "~:y": 2288.99995803833 + } + }, + { + "~#point": { + "~:x": 421, + "~:y": 2288.99995803833 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:page-id": "~ubf963303-bcd6-8000-8007-6a0bc8b60ccb", + "~:r3": 0, + "~:constraints-v": "~:top", + "~:constraints-h": "~:left", + "~:r1": 0, + "~:id": "~ue5199cd7-69e2-8001-8007-6a1beb087a30", + "~:parent-id": "~ue5199cd7-69e2-8001-8007-6a1beb087a2f", + "~:frame-id": "~ue5199cd7-69e2-8001-8007-6a1beb087a2f", + "~:strokes": [ + { + "~:stroke-alignment": "~:inner", + "~:stroke-style": "~:solid", + "~:stroke-color": "#000000", + "~:stroke-opacity": 1, + "~:stroke-width": 10 + } + ], + "~:x": 421, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 421, + "~:y": 1864.99995803833, + "~:width": 518, + "~:height": 424, + "~:x1": 421, + "~:y1": 1864.99995803833, + "~:x2": 939, + "~:y2": 2288.99995803833 + } + }, + "~:fills": [ + { + "~:fill-color": "#dc0606", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 424, + "~:flip-y": null, + "~:shapes": [ + "~ue5199cd7-69e2-8001-8007-6a1beb087a31" + ] + } + }, + "~ue5199cd7-69e2-8001-8007-6a1bc188c730": { + "~#shape": { + "~:y": 506, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 456, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 569, + "~:y": 506 + } + }, + { + "~#point": { + "~:x": 1025, + "~:y": 506 + } + }, + { + "~#point": { + "~:x": 1025, + "~:y": 675 + } + }, + { + "~#point": { + "~:x": 569, + "~:y": 675 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:page-id": "~ubf963303-bcd6-8000-8007-6a0bc8b60ccb", + "~:r3": 0, + "~:constraints-v": "~:top", + "~:constraints-h": "~:left", + "~:r1": 0, + "~:id": "~ue5199cd7-69e2-8001-8007-6a1bc188c730", + "~:parent-id": "~ue5199cd7-69e2-8001-8007-6a1bc188c72f", + "~:frame-id": "~ue5199cd7-69e2-8001-8007-6a1bc188c72f", + "~:strokes": [], + "~:x": 569, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 569, + "~:y": 506, + "~:width": 456, + "~:height": 169, + "~:x1": 569, + "~:y1": 506, + "~:x2": 1025, + "~:y2": 675 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 169, + "~:flip-y": null + } + }, + "~ue5199cd7-69e2-8001-8007-6a1bc188c733": { + "~#shape": { + "~:y": 1239, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 456, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 569, + "~:y": 1239 + } + }, + { + "~#point": { + "~:x": 1025, + "~:y": 1239 + } + }, + { + "~#point": { + "~:x": 1025, + "~:y": 1408 + } + }, + { + "~#point": { + "~:x": 569, + "~:y": 1408 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:page-id": "~ubf963303-bcd6-8000-8007-6a0bc8b60ccb", + "~:r3": 0, + "~:constraints-v": "~:top", + "~:constraints-h": "~:left", + "~:r1": 0, + "~:id": "~ue5199cd7-69e2-8001-8007-6a1bc188c733", + "~:parent-id": "~ue5199cd7-69e2-8001-8007-6a1bc188c732", + "~:frame-id": "~ue5199cd7-69e2-8001-8007-6a1bc188c732", + "~:strokes": [], + "~:x": 569, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 569, + "~:y": 1239, + "~:width": 456, + "~:height": 169, + "~:x1": 569, + "~:y1": 1239, + "~:x2": 1025, + "~:y2": 1408 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 169, + "~:flip-y": null + } + }, + "~ue5199cd7-69e2-8001-8007-6a1bc188c732": { + "~#shape": { + "~:y": 1108, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": true, + "~:name": "Board", + "~:width": 518, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 421, + "~:y": 1108 + } + }, + { + "~#point": { + "~:x": 939, + "~:y": 1108 + } + }, + { + "~#point": { + "~:x": 939, + "~:y": 1532 + } + }, + { + "~#point": { + "~:x": 421, + "~:y": 1532 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:page-id": "~ubf963303-bcd6-8000-8007-6a0bc8b60ccb", + "~:r3": 0, + "~:constraints-v": "~:top", + "~:constraints-h": "~:left", + "~:r1": 0, + "~:id": "~ue5199cd7-69e2-8001-8007-6a1bc188c732", + "~:parent-id": "~ue5199cd7-69e2-8001-8007-6a1bc188c731", + "~:frame-id": "~ue5199cd7-69e2-8001-8007-6a1bc188c731", + "~:strokes": [], + "~:x": 421, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 421, + "~:y": 1108, + "~:width": 518, + "~:height": 424, + "~:x1": 421, + "~:y1": 1108, + "~:x2": 939, + "~:y2": 1532 + } + }, + "~:fills": [ + { + "~:fill-color": "#dc0606", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 424, + "~:flip-y": null, + "~:shapes": [ + "~ue5199cd7-69e2-8001-8007-6a1bc188c733" + ] + } + } } - }, - "~:id": "~u44471494-966a-8178-8006-c5bd93f0fe72", - "~:options": { - "~:components-v2": true, - "~:base-font-size": "16px" } + }, + "~:id": "~u44471494-966a-8178-8006-c5bd93f0fe72", + "~:options": { + "~:components-v2": true, + "~:base-font-size": "16px" } - } \ No newline at end of file + } +} \ No newline at end of file diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-a-file-with-nested-clipping-frames-1.png b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-a-file-with-nested-clipping-frames-1.png index c0821c53f8..67c2af4f41 100644 Binary files a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-a-file-with-nested-clipping-frames-1.png and b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-a-file-with-nested-clipping-frames-1.png differ diff --git a/frontend/resources/wasm-playground/clips.html b/frontend/resources/wasm-playground/clips.html index e4f8bef576..ba1951e56a 100644 --- a/frontend/resources/wasm-playground/clips.html +++ b/frontend/resources/wasm-playground/clips.html @@ -23,7 +23,7 @@ - \ No newline at end of file + diff --git a/frontend/src/app/main/ui/ds/z-index.scss b/frontend/src/app/main/ui/ds/z-index.scss index 4d0e6c592e..3cdb47366d 100644 --- a/frontend/src/app/main/ui/ds/z-index.scss +++ b/frontend/src/app/main/ui/ds/z-index.scss @@ -10,6 +10,7 @@ $z-index-200: 200; $z-index-300: 300; $z-index-400: 400; $z-index-500: 500; +$z-index-600: 600; :global(:root) { --z-index-auto: #{$z-index-auto}; // Index for elements such as workspace, rulers ... @@ -18,4 +19,5 @@ $z-index-500: 500; --z-index-set: #{$z-index-300}; // Index for configuration elements like modals, color picker, grid edition elements --z-index-dropdown: #{$z-index-400}; // Index for dropdown like elements, selects, menus, dropdowns --z-index-notifications: #{$z-index-500}; // Index for notification + --z-index-loaders: #{$z-index-600}; // Index for loaders } diff --git a/frontend/src/app/main/ui/static.cljs b/frontend/src/app/main/ui/static.cljs index 1140ee319b..96ec92fd8b 100644 --- a/frontend/src/app/main/ui/static.cljs +++ b/frontend/src/app/main/ui/static.cljs @@ -308,6 +308,16 @@ [:div {:class (stl/css :sign-info)} [:button {:on-click on-click} (tr "labels.retry")]]])) +(mf/defc webgl-context-lost* + [] + (let [on-reload (mf/use-fn #(js/location.reload))] + [:> error-container* {} + [:div {:class (stl/css :main-message)} (tr "labels.webgl-context-lost.main-message")] + [:div {:class (stl/css :desc-message)} (tr "labels.webgl-context-lost.desc-message")] + [:div {:class (stl/css :buttons-container)} + [:> button* {:variant "primary" :on-click on-reload} + (tr "labels.reload-page")]]])) + (defn- generate-report [data] (try @@ -437,6 +447,7 @@ (rx/of default) (rx/throw cause))))))) + (mf/defc exception-section* {::mf/private true} [{:keys [data route] :as props}] @@ -469,6 +480,9 @@ :service-unavailable [:> service-unavailable*] + :webgl-context-lost + [:> webgl-context-lost*] + [:> internal-error* props]))) (mf/defc context-wrapper* diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 06519c5d75..0a5a6059d3 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -218,6 +218,10 @@ design-tokens? (features/use-feature "design-tokens/v1") + wasm-renderer-enabled? (features/use-feature "render-wasm/v1") + + first-frame-rendered? (mf/use-state false) + background-color (:background-color wglobal)] (mf/with-effect [] @@ -242,6 +246,17 @@ (when (and file-loaded? (not page-id)) (st/emit! (dcm/go-to-workspace :file-id file-id ::rt/replace true)))) + (mf/with-effect [file-id page-id] + (reset! first-frame-rendered? false)) + + (mf/with-effect [] + (let [handle-wasm-render + (fn [_] + (reset! first-frame-rendered? true)) + listener-key (events/listen globals/document "penpot:wasm:render" handle-wasm-render)] + (fn [] + (events/unlistenByKey listener-key)))) + [:> (mf/provider ctx/current-project-id) {:value project-id} [:> (mf/provider ctx/current-file-id) {:value file-id} [:> (mf/provider ctx/current-page-id) {:value page-id} @@ -250,15 +265,18 @@ [:> modal-container*] [:section {:class (stl/css :workspace) :style {:background-color background-color - :touch-action "none"}} + :touch-action "none" + :position "relative"}} [:> context-menu*] - (if (and file-loaded? page-id) + (when (and file-loaded? page-id) [:> workspace-inner* {:page-id page-id :file-id file-id :file file :wglobal wglobal - :layout layout}] + :layout layout}]) + (when (or (not (and file-loaded? page-id)) + (and wasm-renderer-enabled? (not @first-frame-rendered?))) [:> workspace-loader*])]]]]]])) (mf/defc workspace-page* diff --git a/frontend/src/app/main/ui/workspace.scss b/frontend/src/app/main/ui/workspace.scss index d6c21429dd..5cd617bab4 100644 --- a/frontend/src/app/main/ui/workspace.scss +++ b/frontend/src/app/main/ui/workspace.scss @@ -20,7 +20,13 @@ } .workspace-loader { - grid-area: viewport; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: var(--z-index-loaders); + background-color: var(--color-background-primary); } .workspace-content { diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index f2e1d133a4..3059462cd6 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -10,6 +10,7 @@ ["react-dom/server" :as rds] [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.exceptions :as ex] [app.common.files.helpers :as cfh] [app.common.logging :as log] [app.common.math :as mth] @@ -21,7 +22,6 @@ [app.common.types.text :as txt] [app.common.uuid :as uuid] [app.config :as cf] - [app.main.data.render-wasm :as drw] [app.main.refs :as refs] [app.main.render :as render] [app.main.store :as st] @@ -1236,7 +1236,8 @@ (dom/prevent-default event) (reset! wasm/context-lost? true) (log/warn :hint "WebGL context lost") - (st/emit! (drw/context-lost))) + (ex/raise :type :webgl-context-lost + :hint "WebGL context lost")) (defn init-canvas-context [canvas] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index b26964cb23..e647dd095b 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -2693,7 +2693,12 @@ msgstr "Release notes" msgid "labels.reload-file" msgstr "Reload file" +msgid "labels.reload-page" +msgstr "Reload page" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:167, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:406 +#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +#, unused msgid "labels.remove" msgstr "Remove" @@ -8726,6 +8731,12 @@ msgstr "" msgid "workspace.versions.warning.text" msgstr "Autosaved versions will be kept for %s days." +msgid "labels.webgl-context-lost.main-message" +msgstr "Oops! The canvas context was lost" + +msgid "labels.webgl-context-lost.desc-message" +msgstr "WebGL has stopped working. Please reload the page to reset it" + #, unused msgid "workspace.viewport.click-to-close-path" msgstr "Click to close the path" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 330f22fc73..1f155d0265 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -2658,7 +2658,12 @@ msgstr "Notas de versión" msgid "labels.reload-file" msgstr "Recargar archivo" +msgid "labels.reload-page" +msgstr "Recargar página" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:167, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:406 +#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +#, unused msgid "labels.remove" msgstr "Quitar" @@ -8576,3 +8581,9 @@ msgstr "Los autoguardados duran %s días." #, unused msgid "workspace.viewport.click-to-close-path" msgstr "Pulsar para cerrar la ruta" + +msgid "labels.webgl-context-lost.main-message" +msgstr "Ups! Se ha perdido el contexto del canvas" + +msgid "labels.webgl-context-lost.desc-message" +msgstr "WebGL ha dejado de funcionar. Por favor, recarga la página para restaurarlo" \ No newline at end of file diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index a0763e6d40..1d80780bb1 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -10,7 +10,6 @@ mod shadows; mod strokes; mod surfaces; pub mod text; - mod ui; use skia_safe::{self as skia, Matrix, RRect, Rect}; @@ -53,6 +52,25 @@ pub struct NodeRenderState { mask: bool, } +/// Get simplified children of a container, flattening nested flattened containers +fn get_simplified_children<'a>(tree: ShapesPoolRef<'a>, shape: &'a Shape) -> Vec { + let mut result = Vec::new(); + + for child_id in shape.children_ids_iter(false) { + if let Some(child) = tree.get(child_id) { + if child.can_flatten() { + // Child is flattened: recursively get its simplified children + result.extend(get_simplified_children(tree, child)); + } else { + // Child is not flattened: add it directly + result.push(*child_id); + } + } + } + + result +} + impl NodeRenderState { pub fn is_root(&self) -> bool { self.id.is_nil() @@ -398,12 +416,7 @@ impl RenderState { } fn frame_clip_layer_blur(shape: &Shape) -> Option { - match shape.shape_type { - Type::Frame(_) if shape.clip() => shape.blur.filter(|blur| { - !blur.hidden && blur.blur_type == BlurType::LayerBlur && blur.value > 0. - }), - _ => None, - } + shape.frame_clip_layer_blur() } /// Runs `f` with `ignore_nested_blurs` temporarily forced to `true`. @@ -521,38 +534,59 @@ impl RenderState { let paint = skia::Paint::default(); - self.surfaces - .draw_into(SurfaceId::TextDropShadows, SurfaceId::Current, Some(&paint)); + // Only draw surfaces that have content (dirty flag optimization) + if self.surfaces.is_dirty(SurfaceId::TextDropShadows) { + self.surfaces + .draw_into(SurfaceId::TextDropShadows, SurfaceId::Current, Some(&paint)); + } - self.surfaces - .draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint)); + if self.surfaces.is_dirty(SurfaceId::Fills) { + self.surfaces + .draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint)); + } let mut render_overlay_below_strokes = false; if let Some(shape) = shape { render_overlay_below_strokes = shape.has_fills(); } - if render_overlay_below_strokes { + if render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) { self.surfaces .draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint)); } - self.surfaces - .draw_into(SurfaceId::Strokes, SurfaceId::Current, Some(&paint)); + if self.surfaces.is_dirty(SurfaceId::Strokes) { + self.surfaces + .draw_into(SurfaceId::Strokes, SurfaceId::Current, Some(&paint)); + } - if !render_overlay_below_strokes { + if !render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) { self.surfaces .draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint)); } - let surface_ids = SurfaceId::Strokes as u32 - | SurfaceId::Fills as u32 - | SurfaceId::InnerShadows as u32 - | SurfaceId::TextDropShadows as u32; + // Build mask of dirty surfaces that need clearing + let mut dirty_surfaces_to_clear = 0u32; + if self.surfaces.is_dirty(SurfaceId::Strokes) { + dirty_surfaces_to_clear |= SurfaceId::Strokes as u32; + } + if self.surfaces.is_dirty(SurfaceId::Fills) { + dirty_surfaces_to_clear |= SurfaceId::Fills as u32; + } + if self.surfaces.is_dirty(SurfaceId::InnerShadows) { + dirty_surfaces_to_clear |= SurfaceId::InnerShadows as u32; + } + if self.surfaces.is_dirty(SurfaceId::TextDropShadows) { + dirty_surfaces_to_clear |= SurfaceId::TextDropShadows as u32; + } - self.surfaces.apply_mut(surface_ids, |s| { - s.canvas().clear(skia::Color::TRANSPARENT); - }); + if dirty_surfaces_to_clear != 0 { + self.surfaces.apply_mut(dirty_surfaces_to_clear, |s| { + s.canvas().clear(skia::Color::TRANSPARENT); + }); + // Clear dirty flags for surfaces we just cleared + self.surfaces.clear_dirty(dirty_surfaces_to_clear); + } } pub fn clear_focus_mode(&mut self) { @@ -605,11 +639,90 @@ impl RenderState { | strokes_surface_id as u32 | innershadows_surface_id as u32 | text_drop_shadows_surface_id as u32; - self.surfaces.apply_mut(surface_ids, |s| { - s.canvas().save(); - }); + + // Only save canvas state if we have clipping or transforms + // For simple shapes without clipping, skip expensive save/restore + let needs_save = + clip_bounds.is_some() || offset.is_some() || !shape.transform.is_identity(); + + if needs_save { + self.surfaces.apply_mut(surface_ids, |s| { + s.canvas().save(); + }); + } let antialias = shape.should_use_antialias(self.get_scale()); + let fast_mode = self.options.is_fast_mode(); + let has_nested_fills = self + .nested_fills + .last() + .is_some_and(|fills| !fills.is_empty()); + let has_inherited_blur = !self.ignore_nested_blurs + && self.nested_blurs.iter().flatten().any(|blur| { + !blur.hidden && blur.blur_type == BlurType::LayerBlur && blur.value > 0.0 + }); + let can_render_directly = apply_to_current_surface + && clip_bounds.is_none() + && offset.is_none() + && parent_shadows.is_none() + && !shape.needs_layer() + && shape.blur.is_none() + && !has_inherited_blur + && shape.shadows.is_empty() + && shape.transform.is_identity() + && matches!( + shape.shape_type, + Type::Rect(_) | Type::Circle | Type::Path(_) | Type::Bool(_) + ) + && !(shape.fills.is_empty() && has_nested_fills) + && !shape + .svg_attrs + .as_ref() + .is_some_and(|attrs| attrs.fill_none); + + if can_render_directly { + let scale = self.get_scale(); + let translation = self + .surfaces + .get_render_context_translation(self.render_area, scale); + self.surfaces.apply_mut(SurfaceId::Current as u32, |s| { + let canvas = s.canvas(); + canvas.save(); + canvas.scale((scale, scale)); + canvas.translate(translation); + }); + + for fill in shape.fills().rev() { + fills::render(self, shape, fill, antialias, SurfaceId::Current); + } + + for stroke in shape.visible_strokes().rev() { + strokes::render( + self, + shape, + stroke, + Some(SurfaceId::Current), + None, + antialias, + ); + } + + self.surfaces.apply_mut(SurfaceId::Current as u32, |s| { + s.canvas().restore(); + }); + + if self.options.is_debug_visible() { + let shape_selrect_bounds = self.get_shape_selrect_bounds(shape); + debug::render_debug_shape(self, Some(shape_selrect_bounds), None); + } + + if needs_save { + self.surfaces.apply_mut(surface_ids, |s| { + s.canvas().restore(); + }); + } + return; + } // set clipping if let Some(clips) = clip_bounds.as_ref() { @@ -666,6 +779,9 @@ impl RenderState { } else if shape_has_blur { shape.to_mut().set_blur(None); } + if fast_mode { + shape.to_mut().set_blur(None); + } let center = shape.center(); let mut matrix = shape.transform; @@ -683,16 +799,18 @@ impl RenderState { matrix.pre_concat(&svg_transform); } - self.surfaces.canvas(fills_surface_id).concat(&matrix); + self.surfaces + .canvas_and_mark_dirty(fills_surface_id) + .concat(&matrix); if let Some(svg) = shape.svg.as_ref() { - svg.render(self.surfaces.canvas(fills_surface_id)) + svg.render(self.surfaces.canvas_and_mark_dirty(fills_surface_id)); } else { let font_manager = skia::FontMgr::from(self.fonts().font_provider().clone()); let dom_result = skia::svg::Dom::from_str(&sr.content, font_manager); match dom_result { Ok(dom) => { - dom.render(self.surfaces.canvas(fills_surface_id)); + dom.render(self.surfaces.canvas_and_mark_dirty(fills_surface_id)); shape.to_mut().set_svg(dom); } Err(e) => { @@ -708,18 +826,8 @@ impl RenderState { }); let text_content = text_content.new_bounds(shape.selrect()); - let mut drop_shadows = shape.drop_shadow_paints(); - - if let Some(inherited_shadows) = self.get_inherited_drop_shadows() { - drop_shadows.extend(inherited_shadows); - } - - let inner_shadows = shape.inner_shadow_paints(); - let blur_filter = shape.image_filter(1.); let count_inner_strokes = shape.count_visible_inner_strokes(); let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None); - let mut paragraphs_with_shadows = - text_content.paragraph_builder_group_from_text(Some(true)); let mut stroke_paragraphs_list = shape .visible_strokes() .rev() @@ -733,62 +841,8 @@ impl RenderState { ) }) .collect::>(); - - let mut stroke_paragraphs_with_shadows_list = shape - .visible_strokes() - .rev() - .map(|stroke| { - text::stroke_paragraph_builder_group_from_text( - &text_content, - stroke, - &shape.selrect(), - count_inner_strokes, - Some(true), - ) - }) - .collect::>(); - - if let Some(parent_shadows) = parent_shadows { - if !shape.has_visible_strokes() { - for shadow in parent_shadows { - text::render( - Some(self), - None, - &shape, - &mut paragraphs_with_shadows, - text_drop_shadows_surface_id.into(), - Some(&shadow), - blur_filter.as_ref(), - ); - } - } else { - shadows::render_text_shadows( - self, - &shape, - &mut paragraphs_with_shadows, - &mut stroke_paragraphs_with_shadows_list, - text_drop_shadows_surface_id.into(), - &parent_shadows, - &blur_filter, - ); - } - } else { - // 1. Text drop shadows - if !shape.has_visible_strokes() { - for shadow in &drop_shadows { - text::render( - Some(self), - None, - &shape, - &mut paragraphs_with_shadows, - text_drop_shadows_surface_id.into(), - Some(shadow), - blur_filter.as_ref(), - ); - } - } - - // 2. Text fills + if fast_mode { + // Fast path: render fills and strokes only (skip shadows/blur). text::render( Some(self), None, @@ -796,21 +850,9 @@ impl RenderState { &mut paragraph_builders, Some(fills_surface_id), None, - blur_filter.as_ref(), + None, ); - // 3. Stroke drop shadows - shadows::render_text_shadows( - self, - &shape, - &mut paragraphs_with_shadows, - &mut stroke_paragraphs_with_shadows_list, - text_drop_shadows_surface_id.into(), - &drop_shadows, - &blur_filter, - ); - - // 4. Stroke fills for stroke_paragraphs in stroke_paragraphs_list.iter_mut() { text::render( Some(self), @@ -819,34 +861,134 @@ impl RenderState { stroke_paragraphs, Some(strokes_surface_id), None, - blur_filter.as_ref(), + None, ); } + } else { + let mut drop_shadows = shape.drop_shadow_paints(); - // 5. Stroke inner shadows - shadows::render_text_shadows( - self, - &shape, - &mut paragraphs_with_shadows, - &mut stroke_paragraphs_with_shadows_list, - Some(innershadows_surface_id), - &inner_shadows, - &blur_filter, - ); + if let Some(inherited_shadows) = self.get_inherited_drop_shadows() { + drop_shadows.extend(inherited_shadows); + } - // 6. Fill Inner shadows - if !shape.has_visible_strokes() { - for shadow in &inner_shadows { + let inner_shadows = shape.inner_shadow_paints(); + let blur_filter = shape.image_filter(1.); + let mut paragraphs_with_shadows = + text_content.paragraph_builder_group_from_text(Some(true)); + let mut stroke_paragraphs_with_shadows_list = shape + .visible_strokes() + .rev() + .map(|stroke| { + text::stroke_paragraph_builder_group_from_text( + &text_content, + stroke, + &shape.selrect(), + count_inner_strokes, + Some(true), + ) + }) + .collect::>(); + + if let Some(parent_shadows) = parent_shadows { + if !shape.has_visible_strokes() { + for shadow in parent_shadows { + text::render( + Some(self), + None, + &shape, + &mut paragraphs_with_shadows, + text_drop_shadows_surface_id.into(), + Some(&shadow), + blur_filter.as_ref(), + ); + } + } else { + shadows::render_text_shadows( + self, + &shape, + &mut paragraphs_with_shadows, + &mut stroke_paragraphs_with_shadows_list, + text_drop_shadows_surface_id.into(), + &parent_shadows, + &blur_filter, + ); + } + } else { + // 1. Text drop shadows + if !shape.has_visible_strokes() { + for shadow in &drop_shadows { + text::render( + Some(self), + None, + &shape, + &mut paragraphs_with_shadows, + text_drop_shadows_surface_id.into(), + Some(shadow), + blur_filter.as_ref(), + ); + } + } + + // 2. Text fills + text::render( + Some(self), + None, + &shape, + &mut paragraph_builders, + Some(fills_surface_id), + None, + blur_filter.as_ref(), + ); + + // 3. Stroke drop shadows + shadows::render_text_shadows( + self, + &shape, + &mut paragraphs_with_shadows, + &mut stroke_paragraphs_with_shadows_list, + text_drop_shadows_surface_id.into(), + &drop_shadows, + &blur_filter, + ); + + // 4. Stroke fills + for stroke_paragraphs in stroke_paragraphs_list.iter_mut() { text::render( Some(self), None, &shape, - &mut paragraphs_with_shadows, - Some(innershadows_surface_id), - Some(shadow), + stroke_paragraphs, + Some(strokes_surface_id), + None, blur_filter.as_ref(), ); } + + // 5. Stroke inner shadows + shadows::render_text_shadows( + self, + &shape, + &mut paragraphs_with_shadows, + &mut stroke_paragraphs_with_shadows_list, + Some(innershadows_surface_id), + &inner_shadows, + &blur_filter, + ); + + // 6. Fill Inner shadows + if !shape.has_visible_strokes() { + for shadow in &inner_shadows { + text::render( + Some(self), + None, + &shape, + &mut paragraphs_with_shadows, + Some(innershadows_surface_id), + Some(shadow), + blur_filter.as_ref(), + ); + } + } } } } @@ -886,16 +1028,25 @@ impl RenderState { None, antialias, ); - shadows::render_stroke_inner_shadows( + if !fast_mode { + shadows::render_stroke_inner_shadows( + self, + shape, + stroke, + antialias, + innershadows_surface_id, + ); + } + } + + if !fast_mode { + shadows::render_fill_inner_shadows( self, shape, - stroke, antialias, innershadows_surface_id, ); } - - shadows::render_fill_inner_shadows(self, shape, antialias, innershadows_surface_id); // bools::debug_render_bool_paths(self, shape, shapes, modifiers, structure); } }; @@ -908,9 +1059,13 @@ impl RenderState { if apply_to_current_surface { self.apply_drawing_to_render_canvas(Some(&shape)); } - self.surfaces.apply_mut(surface_ids, |s| { - s.canvas().restore(); - }); + + // Only restore if we saved (optimization for simple shapes) + if needs_save { + self.surfaces.apply_mut(surface_ids, |s| { + s.canvas().restore(); + }); + } } pub fn update_render_context(&mut self, tile: tiles::Tile) { @@ -1031,6 +1186,7 @@ impl RenderState { // reorder by distance to the center. self.current_tile = None; self.render_in_progress = true; + self.apply_drawing_to_render_canvas(None); if sync_render { @@ -1117,18 +1273,6 @@ impl RenderState { self.nested_fills.push(Vec::new()); } - let mut paint = skia::Paint::default(); - paint.set_blend_mode(element.blend_mode().into()); - paint.set_alpha_f(element.opacity()); - - if let Some(frame_blur) = Self::frame_clip_layer_blur(element) { - let scale = self.get_scale(); - let sigma = frame_blur.value * scale; - if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) { - paint.set_image_filter(filter); - } - } - // When we're rendering the mask shape we need to set a special blend mode // called 'destination-in' that keeps the drawn content within the mask. // @see https://skia.org/docs/user/api/skblendmode_overview/ @@ -1141,16 +1285,40 @@ impl RenderState { .save_layer(&mask_rec); } - let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); - self.surfaces - .canvas(SurfaceId::Current) - .save_layer(&layer_rec); + // Only create save_layer if actually needed + // For simple shapes with default opacity and blend mode, skip expensive save_layer + // Groups with masks need a layer to properly handle the mask rendering + let needs_layer = element.needs_layer(); + + if needs_layer { + let mut paint = skia::Paint::default(); + paint.set_blend_mode(element.blend_mode().into()); + paint.set_alpha_f(element.opacity()); + + if let Some(frame_blur) = Self::frame_clip_layer_blur(element) { + let scale = self.get_scale(); + let sigma = frame_blur.value * scale; + if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) { + paint.set_image_filter(filter); + } + } + + let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); + self.surfaces + .canvas(SurfaceId::Current) + .save_layer(&layer_rec); + } self.focus_mode.enter(&element.id); } #[inline] - pub fn render_shape_exit(&mut self, element: &Shape, visited_mask: bool) { + pub fn render_shape_exit( + &mut self, + element: &Shape, + visited_mask: bool, + clip_bounds: Option, + ) { if visited_mask { // Because masked groups needs two rendering passes (first drawing // the content and then drawing the mask), we need to do an @@ -1206,7 +1374,7 @@ impl RenderState { element_strokes.to_mut().clip_content = false; self.render_shape( &element_strokes, - None, + clip_bounds, SurfaceId::Fills, SurfaceId::Strokes, SurfaceId::InnerShadows, @@ -1217,7 +1385,14 @@ impl RenderState { ); } - self.surfaces.canvas(SurfaceId::Current).restore(); + // Only restore if we created a layer (optimization for simple shapes) + // Groups with masks need restore to properly handle the mask rendering + let needs_layer = element.needs_layer(); + + if needs_layer { + self.surfaces.canvas(SurfaceId::Current).restore(); + } + self.focus_mode.exit(&element.id); } @@ -1450,6 +1625,8 @@ impl RenderState { "Error: Element with root_id {} not found in the tree.", node_render_state.id ))?; + let scale = self.get_scale(); + let mut extrect: Option = None; // If the shape is not in the tile set, then we add them. if self.tiles.get_tiles_of(node_id).is_none() { @@ -1457,22 +1634,44 @@ impl RenderState { } if visited_children { - self.render_shape_exit(element, visited_mask); + // Skip render_shape_exit for flattened containers + if !element.can_flatten() { + self.render_shape_exit(element, visited_mask, clip_bounds); + } continue; } if !node_render_state.is_root() { let transformed_element: Cow = Cow::Borrowed(element); - let scale = self.get_scale(); - let extrect = transformed_element.extrect(tree, scale); - let is_visible = extrect.intersects(self.render_area) - && !transformed_element.hidden - && !transformed_element.visually_insignificant(scale, tree); + // Aggressive early exit: check hidden first (fastest check) + if transformed_element.hidden { + continue; + } + + // For frames and groups, we must use extrect because they can have nested content + // that extends beyond their selrect. Using selrect for early exit would incorrectly + // skip frames/groups that have nested content in the current tile. + let is_container = matches!( + transformed_element.shape_type, + crate::shapes::Type::Frame(_) | crate::shapes::Type::Group(_) + ); + + let has_effects = transformed_element.has_effects_that_extend_bounds(); + + let is_visible = if is_container || has_effects { + let element_extrect = + extrect.get_or_insert_with(|| transformed_element.extrect(tree, scale)); + element_extrect.intersects(self.render_area) + && !transformed_element.visually_insignificant(scale, tree) + } else { + let selrect = transformed_element.selrect(); + selrect.intersects(self.render_area) + && !transformed_element.visually_insignificant(scale, tree) + }; if self.options.is_debug_visible() { - let shape_extrect_bounds = - self.get_shape_extrect_bounds(&transformed_element, tree); + let shape_extrect_bounds = self.get_shape_extrect_bounds(element, tree); debug::render_debug_shape(self, None, Some(shape_extrect_bounds)); } @@ -1481,10 +1680,14 @@ impl RenderState { } } - self.render_shape_enter(element, mask); + // Skip render_shape_enter/exit for flattened containers + // If a container was flattened, it doesn't affect children visually, so we skip + // the expensive enter/exit operations and process children directly + if !element.can_flatten() { + self.render_shape_enter(element, mask); + } if !node_render_state.is_root() && self.focus_mode.is_active() { - let scale: f32 = self.get_scale(); let translation = self .surfaces .get_render_context_translation(self.render_area, scale); @@ -1524,9 +1727,11 @@ impl RenderState { .save_layer(&layer_rec); // First pass: Render shadow in black to establish alpha mask + let element_extrect = + extrect.get_or_insert_with(|| element.extrect(tree, scale)); self.render_drop_black_shadow( element, - &element.extrect(tree, scale), + element_extrect, shadow, clip_bounds.clone(), scale, @@ -1541,7 +1746,6 @@ impl RenderState { if shadow_shape.hidden { continue; } - let clip_bounds = node_render_state .get_nested_shadow_clip_bounds(element, shadow); @@ -1682,14 +1886,18 @@ impl RenderState { self.apply_drawing_to_render_canvas(Some(element)); } - match element.shape_type { - Type::Frame(_) if Self::frame_clip_layer_blur(element).is_some() => { - self.nested_blurs.push(None); + // Skip nested state updates for flattened containers + // Flattened containers don't affect children, so we don't need to track their state + if !element.can_flatten() { + match element.shape_type { + Type::Frame(_) if Self::frame_clip_layer_blur(element).is_some() => { + self.nested_blurs.push(None); + } + Type::Frame(_) | Type::Group(_) => { + self.nested_blurs.push(element.blur); + } + _ => {} } - Type::Frame(_) | Type::Group(_) => { - self.nested_blurs.push(element.blur); - } - _ => {} } // Set the node as visited_children before processing children @@ -1704,24 +1912,35 @@ impl RenderState { if element.is_recursive() { let children_clip_bounds = node_render_state.get_children_clip_bounds(element, None); - let mut children_ids: Vec<_> = element.children_ids_iter(false).collect(); + + let children_ids: Vec<_> = if element.can_flatten() { + // Container was flattened: get simplified children (which skip this level) + get_simplified_children(tree, element) + } else { + // Container not flattened: use original children + element.children_ids_iter(false).copied().collect() + }; // Z-index ordering on Layouts - if element.has_layout() { + let children_ids = if element.has_layout() { + let mut ids = children_ids; if element.is_flex() && !element.is_flex_reverse() { - children_ids.reverse(); + ids.reverse(); } - children_ids.sort_by(|id1, id2| { + ids.sort_by(|id1, id2| { let z1 = tree.get(id1).map(|s| s.z_index()).unwrap_or(0); let z2 = tree.get(id2).map(|s| s.z_index()).unwrap_or(0); z2.cmp(&z1) }); - } + ids + } else { + children_ids + }; for child_id in children_ids.iter() { self.pending_nodes.push(NodeRenderState { - id: **child_id, + id: *child_id, visited_children: false, clip_bounds: children_clip_bounds.clone(), visited_mask: false, @@ -1747,6 +1966,16 @@ impl RenderState { allow_stop: bool, ) -> Result<(), String> { let mut should_stop = false; + let root_ids = { + if let Some(shape_id) = base_object { + vec![*shape_id] + } else { + let Some(root) = tree.get(&Uuid::nil()) else { + return Err(String::from("Root shape not found")); + }; + root.children_ids(false) + } + }; while !should_stop { if let Some(current_tile) = self.current_tile { @@ -1803,17 +2032,6 @@ impl RenderState { .canvas(SurfaceId::Current) .clear(self.background_color); - let root_ids = { - if let Some(shape_id) = base_object { - vec![*shape_id] - } else { - let Some(root) = tree.get(&Uuid::nil()) else { - return Err(String::from("Root shape not found")); - }; - root.children_ids(false) - } - }; - // If we finish processing every node rendering is complete // let's check if there are more pending nodes if let Some(next_tile) = self.pending_tiles.pop() { @@ -1821,21 +2039,13 @@ impl RenderState { if !self.surfaces.has_cached_tile_surface(next_tile) { if let Some(ids) = self.tiles.get_shapes_at(next_tile) { - let root_ids_map: std::collections::HashMap = root_ids - .iter() - .enumerate() - .map(|(i, id)| (*id, i)) - .collect(); - - // We only need first level shapes - let mut valid_ids: Vec = ids - .iter() - .filter(|id| root_ids_map.contains_key(id)) - .copied() - .collect(); - - // These shapes for the tile should be ordered as they are in the parent node - valid_ids.sort_by_key(|id| root_ids_map.get(id).unwrap_or(&usize::MAX)); + // We only need first level shapes, in the same order as the parent node + let mut valid_ids = Vec::with_capacity(ids.len()); + for root_id in root_ids.iter() { + if ids.contains(root_id) { + valid_ids.push(*root_id); + } + } self.pending_nodes.extend(valid_ids.into_iter().map(|id| { NodeRenderState { diff --git a/render-wasm/src/render/fills.rs b/render-wasm/src/render/fills.rs index a2a6e748a6..1d8ad98084 100644 --- a/render-wasm/src/render/fills.rs +++ b/render-wasm/src/render/fills.rs @@ -18,7 +18,7 @@ fn draw_image_fill( } let size = image.unwrap().dimensions(); - let canvas = render_state.surfaces.canvas(surface_id); + let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id); let container = &shape.selrect; let path_transform = shape.to_path_transform(); diff --git a/render-wasm/src/render/filters.rs b/render-wasm/src/render/filters.rs index bddd5ab26a..832fc32d88 100644 --- a/render-wasm/src/render/filters.rs +++ b/render-wasm/src/render/filters.rs @@ -41,7 +41,7 @@ where F: FnOnce(&mut RenderState, SurfaceId), { if let Some((image, scale)) = render_into_filter_surface(render_state, bounds, draw_fn) { - let canvas = render_state.surfaces.canvas(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 scale < 1.0 { diff --git a/render-wasm/src/render/shadows.rs b/render-wasm/src/render/shadows.rs index f5f4fbfccf..64a6d7533a 100644 --- a/render-wasm/src/render/shadows.rs +++ b/render-wasm/src/render/shadows.rs @@ -135,7 +135,7 @@ pub fn render_text_shadows( let canvas = render_state .surfaces - .canvas(surface_id.unwrap_or(SurfaceId::TextDropShadows)); + .canvas_and_mark_dirty(surface_id.unwrap_or(SurfaceId::TextDropShadows)); for shadow in shadows { let shadow_layer = SaveLayerRec::default().paint(shadow); diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index 5e4f02c8e3..103831013a 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -387,7 +387,7 @@ fn draw_image_stroke_in_container( } let size = image.unwrap().dimensions(); - let canvas = render_state.surfaces.canvas(surface_id); + let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id); let container = &shape.selrect; let path_transform = shape.to_path_transform(); let svg_attrs = shape.svg_attrs.as_ref(); @@ -606,7 +606,7 @@ fn render_internal( let scale = render_state.get_scale(); let target_surface = surface_id.unwrap_or(SurfaceId::Strokes); - let canvas = render_state.surfaces.canvas(target_surface); + let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface); let selrect = shape.selrect; let path_transform = shape.to_path_transform(); let svg_attrs = shape.svg_attrs.as_ref(); @@ -688,7 +688,7 @@ pub fn render_text_paths( let scale = render_state.get_scale(); let canvas = render_state .surfaces - .canvas(surface_id.unwrap_or(SurfaceId::Strokes)); + .canvas_and_mark_dirty(surface_id.unwrap_or(SurfaceId::Strokes)); let selrect = &shape.selrect; let svg_attrs = shape.svg_attrs.as_ref(); let mut paint: skia_safe::Handle<_> = diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index fe0edbb455..00792109d8 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -55,6 +55,8 @@ pub struct Surfaces { tiles: TileTextureCache, sampling_options: skia::SamplingOptions, margins: skia::ISize, + // Tracks which surfaces have content (dirty flag bitmask) + dirty_surfaces: u32, } #[allow(dead_code)] @@ -105,6 +107,7 @@ impl Surfaces { tiles, sampling_options, margins, + dirty_surfaces: 0, } } @@ -147,10 +150,51 @@ impl Surfaces { None } + /// Returns a mutable reference to the canvas and automatically marks + /// render surfaces as dirty when accessed. This tracks which surfaces + /// have content for optimization purposes. + pub fn canvas_and_mark_dirty(&mut self, id: SurfaceId) -> &skia::Canvas { + // Automatically mark render surfaces as dirty when accessed + // This tracks which surfaces have content for optimization + match id { + SurfaceId::Fills + | SurfaceId::Strokes + | SurfaceId::InnerShadows + | SurfaceId::TextDropShadows => { + self.mark_dirty(id); + } + _ => {} + } + self.canvas(id) + } + + /// Returns a mutable reference to the canvas without any side effects. + /// Use this when you only need to read or manipulate the canvas state + /// without marking the surface as dirty. pub fn canvas(&mut self, id: SurfaceId) -> &skia::Canvas { self.get_mut(id).canvas() } + /// Marks a surface as having content (dirty) + pub fn mark_dirty(&mut self, id: SurfaceId) { + self.dirty_surfaces |= id as u32; + } + + /// Checks if a surface has content + pub fn is_dirty(&self, id: SurfaceId) -> bool { + (self.dirty_surfaces & id as u32) != 0 + } + + /// Clears the dirty flag for a surface or set of surfaces + pub fn clear_dirty(&mut self, ids: u32) { + self.dirty_surfaces &= !ids; + } + + /// Clears all dirty flags + pub fn clear_all_dirty(&mut self) { + self.dirty_surfaces = 0; + } + pub fn flush_and_submit(&mut self, gpu_state: &mut GpuState, id: SurfaceId) { let surface = self.get_mut(id); gpu_state.context.flush_and_submit_surface(surface, None); @@ -159,9 +203,12 @@ impl Surfaces { pub fn draw_into(&mut self, from: SurfaceId, to: SurfaceId, paint: Option<&skia::Paint>) { let sampling_options = self.sampling_options; - self.get_mut(from) - .clone() - .draw(self.canvas(to), (0.0, 0.0), sampling_options, paint); + self.get_mut(from).clone().draw( + self.canvas_and_mark_dirty(to), + (0.0, 0.0), + sampling_options, + paint, + ); } pub fn apply_mut(&mut self, ids: u32, mut f: impl FnMut(&mut skia::Surface)) { @@ -212,18 +259,33 @@ impl Surfaces { pub fn update_render_context(&mut self, render_area: skia::Rect, scale: f32) { let translation = self.get_render_context_translation(render_area, scale); - self.apply_mut( - SurfaceId::Fills as u32 - | SurfaceId::Strokes as u32 - | SurfaceId::InnerShadows as u32 - | SurfaceId::TextDropShadows as u32, - |s| { - let canvas = s.canvas(); - canvas.reset_matrix(); - canvas.scale((scale, scale)); - canvas.translate(translation); - }, - ); + + // When context changes (zoom/pan/tile), clear all render surfaces first + // to remove any residual content from previous tiles, then mark as dirty + // so they get redrawn with new transformations + let surface_ids = SurfaceId::Fills as u32 + | SurfaceId::Strokes as u32 + | SurfaceId::InnerShadows as u32 + | SurfaceId::TextDropShadows as u32; + + // Clear surfaces before updating transformations to remove residual content + self.apply_mut(surface_ids, |s| { + s.canvas().clear(skia::Color::TRANSPARENT); + }); + + // Mark all render surfaces as dirty so they get redrawn + self.mark_dirty(SurfaceId::Fills); + self.mark_dirty(SurfaceId::Strokes); + self.mark_dirty(SurfaceId::InnerShadows); + self.mark_dirty(SurfaceId::TextDropShadows); + + // Update transformations + self.apply_mut(surface_ids, |s| { + let canvas = s.canvas(); + canvas.reset_matrix(); + canvas.scale((scale, scale)); + canvas.translate(translation); + }); } #[inline] @@ -264,19 +326,21 @@ impl Surfaces { pub fn draw_rect_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) { if let Some(corners) = shape.shape_type.corners() { let rrect = RRect::new_rect_radii(shape.selrect, &corners); - self.canvas(id).draw_rrect(rrect, paint); + self.canvas_and_mark_dirty(id).draw_rrect(rrect, paint); } else { - self.canvas(id).draw_rect(shape.selrect, paint); + self.canvas_and_mark_dirty(id) + .draw_rect(shape.selrect, paint); } } pub fn draw_circle_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) { - self.canvas(id).draw_oval(shape.selrect, paint); + self.canvas_and_mark_dirty(id) + .draw_oval(shape.selrect, paint); } pub fn draw_path_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) { if let Some(path) = shape.get_skia_path() { - self.canvas(id).draw_path(&path, paint); + self.canvas_and_mark_dirty(id).draw_path(&path, paint); } } @@ -304,6 +368,9 @@ impl Surfaces { self.canvas(SurfaceId::UI) .clear(skia::Color::TRANSPARENT) .reset_matrix(); + + // Clear all dirty flags after reset + self.clear_all_dirty(); } pub fn cache_current_tile_texture( diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs index 58f10cbc6c..ba14112921 100644 --- a/render-wasm/src/render/text.rs +++ b/render-wasm/src/render/text.rs @@ -192,7 +192,7 @@ pub fn render( } } - let canvas = render_state.surfaces.canvas(target_surface); + let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface); render_text_on_canvas(canvas, shape, paragraph_builders, shadow, blur); return; } @@ -371,7 +371,7 @@ pub fn render_as_path( ) { let canvas = render_state .surfaces - .canvas(surface_id.unwrap_or(SurfaceId::Fills)); + .canvas_and_mark_dirty(surface_id.unwrap_or(SurfaceId::Fills)); for (path, paint) in paths { // Note: path can be empty @@ -397,7 +397,7 @@ pub fn render_position_data( let rect = Rect::from_xywh(pd.x, pd.y, pd.width, pd.height); render_state .surfaces - .canvas(surface_id) + .canvas_and_mark_dirty(surface_id) .draw_rect(rect, &paint); } } diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index cb334a6f00..59e2c2fc18 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -920,8 +920,13 @@ impl Shape { } Type::Group(_) | Type::Frame(_) if !self.clip_content => { + // For frames and groups, we must always calculate extrect for all children + // to ensure accurate bounds that include nested content across all tiles. + // Using selrect for children can cause frames to be incorrectly omitted from + // tiles where they have nested content. for child_id in self.children_ids_iter(false) { if let Some(child_shape) = shapes_pool.get(child_id) { + // Always calculate full extrect for children to ensure accurate bounds let child_extrect = child_shape.calculate_extrect(shapes_pool, scale); rect.join(child_extrect); } @@ -1419,6 +1424,99 @@ impl Shape { !self.fills.is_empty() } + /// Determines if this frame or group can be flattened (doesn't affect children visually) + /// A container can be flattened if it has no visual effects that affect its children + /// and doesn't render its own content (no fills/strokes) + pub fn can_flatten(&self) -> bool { + // Only frames and groups can be flattened + if !matches!(self.shape_type, Type::Frame(_) | Type::Group(_)) { + return false; + } + + // Cannot flatten if it has visual effects that affect children: + + if self.clip_content { + return false; + } + + if !self.transform.is_identity() { + return false; + } + + if self.opacity != 1.0 { + return false; + } + + if self.blend_mode() != BlendMode::default() { + return false; + } + + if self.blur.is_some() { + return false; + } + + if !self.shadows.is_empty() { + return false; + } + + if let Type::Group(group) = &self.shape_type { + if group.masked { + return false; + } + } + + if self.hidden { + return false; + } + + // If the container itself has fills/strokes, it renders something visible + // We cannot flatten containers that render their own background/border + // because they need to be rendered even if they don't affect children + if self.has_fills() || self.has_visible_strokes() { + return false; + } + + true + } + + /// Checks if this shape needs a layer for rendering due to visual effects + /// (opacity < 1.0, non-default blend mode, or frame clip layer blur) + pub fn needs_layer(&self) -> bool { + self.opacity() < 1.0 + || self.blend_mode().0 != skia::BlendMode::SrcOver + || self.has_frame_clip_layer_blur() + || (matches!(self.shape_type, Type::Group(g) if g.masked)) + } + + /// Checks if this frame has clip layer blur (affects children) + /// A frame has clip layer blur if it clips content and has layer blur + pub fn has_frame_clip_layer_blur(&self) -> bool { + self.frame_clip_layer_blur().is_some() + } + + /// Returns the frame clip layer blur if this frame has one + /// A frame has clip layer blur if it clips content and has layer blur + pub fn frame_clip_layer_blur(&self) -> Option { + use crate::shapes::BlurType; + match self.shape_type { + Type::Frame(_) if self.clip_content => self.blur.filter(|blur| { + !blur.hidden && blur.blur_type == BlurType::LayerBlur && blur.value > 0.0 + }), + _ => None, + } + } + + /// Checks if this shape has visual effects that might extend its bounds beyond selrect + /// Shapes with these effects require expensive extrect calculation for accurate visibility checks + pub fn has_effects_that_extend_bounds(&self) -> bool { + !self.shadows.is_empty() + || self.blur.is_some() + || !self.strokes.is_empty() + || !self.transform.is_identity() + || !math::is_close_to(self.rotation, 0.0) + || matches!(self.shape_type, Type::Group(_) | Type::Frame(_)) + } + pub fn count_visible_inner_strokes(&self) -> usize { self.visible_strokes() .filter(|s| s.kind == StrokeKind::Inner)