diff --git a/frontend/playwright/data/render-wasm/get-file-svg-attrs.json b/frontend/playwright/data/render-wasm/get-file-svg-attrs.json new file mode 100644 index 0000000000..1e1f8464f7 --- /dev/null +++ b/frontend/playwright/data/render-wasm/get-file-svg-attrs.json @@ -0,0 +1,1312 @@ +{ + "~:features": { + "~#set": [ + "fdata/path-data", + "plugins/runtime", + "design-tokens/v1", + "variants/v1", + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "render-wasm/v1", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:team-id": "~ueba8fa2e-4140-8084-8005-448635d7a724", + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "New File 4", + "~:revn": 77, + "~:modified-at": "~m1761287975462", + "~:vern": 0, + "~:id": "~u4732f3e3-7a1a-807e-8006-ff76066e631d", + "~: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", + "0004-clean-shadow-color", + "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" + ] + }, + "~:version": 67, + "~:project-id": "~ueba8fa2e-4140-8084-8005-448635da32b4", + "~:created-at": "~m1761218115001", + "~:backend": "legacy-db", + "~:data": { + "~:pages": [ + "~u4732f3e3-7a1a-807e-8006-ff76066e631e" + ], + "~:pages-index": { + "~u4732f3e3-7a1a-807e-8006-ff76066e631e": { + "~:objects": { + "~u00000000-0000-0000-0000-000000000000": { + "~#shape": { + "~:y": 0, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:name": "Root Frame", + "~:width": 0.01, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 0, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.01 + } + }, + { + "~#point": { + "~:x": 0, + "~:y": 0.01 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:r3": 0, + "~:r1": 0, + "~:id": "~u00000000-0000-0000-0000-000000000000", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 0, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 0, + "~:y": 0, + "~:width": 0.01, + "~:height": 0.01, + "~:x1": 0, + "~:y1": 0, + "~:x2": 0.01, + "~:y2": 0.01 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 0.01, + "~:flip-y": null, + "~:shapes": [ + "~u0ae05ee2-98e5-8097-8007-00802f748d0f", + "~ud816a747-c6fa-8005-8006-ffa56576e28e", + "~uc9348056-5090-8016-8006-ff760a55bce0", + "~uc9348056-5090-8016-8006-ff760a55bce1", + "~uc9348056-5090-8016-8006-ff760a55bce2", + "~uc9348056-5090-8016-8006-ff760a55bce3", + "~u42797f0c-cd4d-80fd-8006-ff7c81651cd2" + ] + } + }, + "~u0ae05ee2-98e5-8097-8007-00802f748d11": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAADOdkxE+5JJRAMAAAAlz0ZEqY5RRCsPOkTPW1FElGs0REPRWEQDAAAAalY0REftWEThPjREoQ1ZRBcmNERbMFlEAwAAABlTOkTl0FJE3+xGRF27UkRioExEfWVLRAMAAABnp1BECx5RRIKQV0QJAFZEYT1fRHWbWEQDAAAAjlFfRJNnWETCQl9EAStYRPpVX0Sh91dEAwAAAFd0X0ShQldEDaFfRBmYVkSpzl9EO+pVRAMAAADDeWBELV5TREoxYUSNolBEiy9fRIPZSkQDAAAAwARfRKNfSkRpvl5Ed+xJRKtfXkT/f0lEAwAAAN4ZYESJO09EeW9fRCEEUkRp0F5EiZ1URAMAAADKpF5ED1RVRP95XkQLB1ZE6lxeRBnFVkQDAAAAmEpeRPX6VkSwWF5EhzpXRGhFXkQHcVdEAwAAAJztVkRttFRELlFQRFGUT0TOdkxE+5JJRAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAGEEzRF03WkQDAAAAbm8zREXGWUQRiTNERUdZRM2HM0TLtFhEAwAAAFSLM0QJKVdE2j4zRCXpVUST7zJEiZ1URAMAAAB9UDJEIQRSRB6mMUSJO09ESmAzRP9/SUQDAAAAjgEzRHfsSURDuzJEo19KRGuQMkSD2UpEAwAAALqOMESNolBEQUYxRC1eU0RO8TFEO+pVRAMAAAAtRDJEKyZXRB2UMkTrVlhEa5AyROnPWUQDAAAAl5IyRCG0WkSMSTJExWZbRJXRMUTb/ltEAwAAAB/ZMUTZDVxEDkAyRL+GW0RTsTJEG/JaRAMAAACQ4TJEy7JaRK0TM0QPcVpEGEEzRF03WkQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + "~:name": "Shadow-Mask", + "~:width": null, + "~:type": "~:path", + "~:svg-attrs": { + + }, + "~:points": [ + { + "~#point": { + "~:x": 709.000024606158, + "~:y": 806.000016754209 + } + }, + { + "~#point": { + "~:x": 898.000004780172, + "~:y": 806.000016754209 + } + }, + { + "~#point": { + "~:x": 898.000004780172, + "~:y": 879.999971962692 + } + }, + { + "~#point": { + "~:x": 709.000024606158, + "~:y": 879.999971962692 + } + } + ], + "~:proportion-lock": false, + "~:center": { + "~#point": { + "~:x": 712.502830148685, + "~:y": 1273.97838944562 + } + }, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:svg-transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 4475, + "~:f": 13 + } + }, + "~:id": "~u0ae05ee2-98e5-8097-8007-00802f748d11", + "~:parent-id": "~u0ae05ee2-98e5-8097-8007-00802f748d0f", + "~:svg-viewbox": { + "~#rect": { + "~:x": 4497.08919644205, + "~:y": 61.0049, + "~:width": 188.827193754712, + "~:height": 74.2069863637553, + "~:x1": 4497.08919644205, + "~:y1": 61.0049, + "~:x2": 4685.91639019676, + "~:y2": 135.211886363755 + } + }, + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": null, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 709.000024606158, + "~:y": 806.000016754209, + "~:width": 188.999980174014, + "~:height": 73.9999552084835, + "~:x1": 709.000024606158, + "~:y1": 806.000016754209, + "~:x2": 898.000004780172, + "~:y2": 879.999971962692 + } + }, + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 0.16078432 + } + ], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~u0ae05ee2-98e5-8097-8007-00802f748d10": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 0.999999999194003, + "~:b": -2.64697796016969e-23, + "~:c": -2.9778502051909e-23, + "~:d": 0.999999999211734, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:content": { + "~#penpot/path-data": "~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" + }, + "~:name": "Shape", + "~:width": null, + "~:type": "~:path", + "~:svg-attrs": { + "~:fill": "none" + }, + "~:points": [ + { + "~#point": { + "~:x": 687.000030517578, + "~:y": 758.000026702881 + } + }, + { + "~#point": { + "~:x": 920.005705329775, + "~:y": 758.000026702881 + } + }, + { + "~#point": { + "~:x": 920.005705329775, + "~:y": 1036.12386848365 + } + }, + { + "~#point": { + "~:x": 687.000030517578, + "~:y": 1036.12386848365 + } + } + ], + "~:proportion-lock": false, + "~:center": { + "~#point": { + "~:x": 712.502874239018, + "~:y": 1327.93191715412 + } + }, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.000000000806, + "~:b": 3.30872245021211e-23, + "~:c": 2.9778502051909e-23, + "~:d": 1.00000000078827, + "~:e": 0, + "~:f": 0 + } + }, + "~:svg-transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 4475, + "~:f": 13 + } + }, + "~:id": "~u0ae05ee2-98e5-8097-8007-00802f748d10", + "~:parent-id": "~u0ae05ee2-98e5-8097-8007-00802f748d0f", + "~:svg-viewbox": { + "~#rect": { + "~:x": 4475.00000000364, + "~:y": 13, + "~:width": 233.005674812198, + "~:height": 278.123841780764, + "~:x1": 4475.00000000364, + "~:y1": 13, + "~:x2": 4708.00567481584, + "~:y2": 291.123841780764 + } + }, + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": null, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 687.000030517578, + "~:y": 758.000026702881, + "~:width": 233.005674812197, + "~:height": 278.123841780764, + "~:x1": 687.000030517578, + "~:y1": 758.000026702881, + "~:x2": 920.005705329775, + "~:y2": 1036.12386848365 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~ud816a747-c6fa-8005-8006-ffa56576e28e": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAACUJk9E/78IRAIAAAAAAAAAAAAAAAAAAAAAAAAARks+RLCGPUQCAAAAAAAAAAAAAAAAAAAAAAAAACsNa0QnRh1EAgAAAAAAAAAAAAAAAAAAAAAAAAD9PzNEJ0YdRAIAAAAAAAAAAAAAAAAAAAAAAAAA6QFgRLCGPUQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + "~:name": "svg-polygon", + "~:width": null, + "~:type": "~:path", + "~:svg-attrs": { + "~:fillRule": "evenodd" + }, + "~:points": [ + { + "~#point": { + "~:x": 717, + "~:y": 547 + } + }, + { + "~#point": { + "~:x": 940.205915893361, + "~:y": 547 + } + }, + { + "~#point": { + "~:x": 940.205915893361, + "~:y": 758.104518632358 + } + }, + { + "~#point": { + "~:x": 717, + "~:y": 758.104518632358 + } + } + ], + "~:layout-item-h-sizing": "~:fix", + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:layout-item-v-sizing": "~:fix", + "~:svg-transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~ud816a747-c6fa-8005-8006-ffa56576e28e", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:svg-viewbox": { + "~#rect": { + "~:x": 102, + "~:y": 0, + "~:width": 96, + "~:height": 90, + "~:x1": 102, + "~:y1": 0, + "~:x2": 198, + "~:y2": 90 + } + }, + "~:svg-defs": { + + }, + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-color": "#ff0000", + "~:stroke-opacity": 1, + "~:stroke-width": 5, + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:inner" + } + ], + "~:x": null, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 717, + "~:y": 547, + "~:width": 223.205915893361, + "~:height": 211.104518632358, + "~:x1": 717, + "~:y1": 547, + "~:x2": 940.205915893361, + "~:y2": 758.104518632358 + } + }, + "~:fills": [ + { + "~:fill-color": "#62d10b", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~uc9348056-5090-8016-8006-ff760a55bce2": { + "~#shape": { + "~:y": null, + "~:stroke-cap-start": "~:round", + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:index": null, + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAACgBtEAEBLRAIAAAAAAAAAAAAAAAAAAAAAAAAAAoAbRADAV0Q=" + }, + "~:name": "svg-path", + "~:width": null, + "~:type": "~:path", + "~:svg-attrs": { + "~:fill": "none", + "~:strokeLinecap": "round", + "~:strokeLinejoin": "round" + }, + "~:points": [ + { + "~#point": { + "~:x": 621.999997442874, + "~:y": 813.000011676151 + } + }, + { + "~#point": { + "~:x": 622.055645037657, + "~:y": 813.000011676151 + } + }, + { + "~#point": { + "~:x": 622.055645037657, + "~:y": 862.999969855948 + } + }, + { + "~#point": { + "~:x": 621.999997442874, + "~:y": 862.999969855948 + } + } + ], + "~:layout-item-h-sizing": "~:fix", + "~:proportion-lock": false, + "~:stroke-cap-end": "~:round", + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:layout-item-v-sizing": "~:fix", + "~:svg-transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~uc9348056-5090-8016-8006-ff760a55bce2", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:svg-viewbox": { + "~#rect": { + "~:x": 18, + "~:y": 7, + "~:width": 0.01, + "~:height": 9, + "~:x1": 18, + "~:y1": 7, + "~:x2": 18.01, + "~:y2": 16 + } + }, + "~:svg-defs": { + + }, + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-color": "#000000", + "~:stroke-opacity": 1, + "~:stroke-width": 10, + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:inner" + } + ], + "~:x": null, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 621.999997442874, + "~:y": 813.000011676151, + "~:width": 0.0556475947835224, + "~:height": 49.9999581797965, + "~:x1": 621.999997442874, + "~:y1": 813.000011676151, + "~:x2": 622.055645037657, + "~:y2": 862.999969855948 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~uc9348056-5090-8016-8006-ff760a55bce3": { + "~#shape": { + "~:y": null, + "~:stroke-cap-start": "~:round", + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:index": null, + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAACwBVEAIBSRAIAAAAAAAAAAAAAAAAAAAAAAAAAAkAbRABAWEQCAAAAAAAAAAAAAAAAAAAAAAAAAALAIEQAgFJE" + }, + "~:name": "svg-path", + "~:width": null, + "~:type": "~:path", + "~:svg-attrs": { + "~:fill": "none", + "~:strokeLinecap": "round", + "~:strokeLinejoin": "round" + }, + "~:points": [ + { + "~#point": { + "~:x": 599.000015020354, + "~:y": 842.000017004857 + } + }, + { + "~#point": { + "~:x": 643.000027656538, + "~:y": 842.000017004857 + } + }, + { + "~#point": { + "~:x": 643.000027656538, + "~:y": 865.000028441938 + } + }, + { + "~#point": { + "~:x": 599.000015020354, + "~:y": 865.000028441938 + } + } + ], + "~:layout-item-h-sizing": "~:fix", + "~:proportion-lock": false, + "~:stroke-cap-end": "~:round", + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:layout-item-v-sizing": "~:fix", + "~:svg-transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~uc9348056-5090-8016-8006-ff760a55bce3", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:svg-viewbox": { + "~#rect": { + "~:x": 14, + "~:y": 12, + "~:width": 8, + "~:height": 4, + "~:x1": 14, + "~:y1": 12, + "~:x2": 22, + "~:y2": 16 + } + }, + "~:svg-defs": { + + }, + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-color": "#000000", + "~:stroke-opacity": 1, + "~:stroke-width": 10, + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:inner" + } + ], + "~:x": null, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 599.000015020354, + "~:y": 842.000017004857, + "~:width": 44.0000126361838, + "~:height": 23.0000114370807, + "~:x1": 599.000015020354, + "~:y1": 842.000017004857, + "~:x2": 643.000027656538, + "~:y2": 865.000028441938 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~uc9348056-5090-8016-8006-ff760a55bce0": { + "~#shape": { + "~:y": null, + "~:stroke-cap-start": "~:round", + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:index": null, + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAADAAhEAcBTRAIAAAAAAAAAAAAAAAAAAAAAAAAAAwAPRAHAU0Q=" + }, + "~:name": "svg-path", + "~:width": null, + "~:type": "~:path", + "~:svg-attrs": { + "~:fill": "none", + "~:strokeLinecap": "round", + "~:strokeLinejoin": "round" + }, + "~:points": [ + { + "~#point": { + "~:x": 543.999999094532, + "~:y": 847.000034938226 + } + }, + { + "~#point": { + "~:x": 572.000037645983, + "~:y": 847.000034938226 + } + }, + { + "~#point": { + "~:x": 572.000037645983, + "~:y": 847.055682537931 + } + }, + { + "~#point": { + "~:x": 543.999999094532, + "~:y": 847.055682537931 + } + } + ], + "~:layout-item-h-sizing": "~:fix", + "~:proportion-lock": false, + "~:stroke-cap-end": "~:round", + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:layout-item-v-sizing": "~:fix", + "~:svg-transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~uc9348056-5090-8016-8006-ff760a55bce0", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:svg-viewbox": { + "~#rect": { + "~:x": 3.5, + "~:y": 13, + "~:width": 6, + "~:height": 0.01, + "~:x1": 3.5, + "~:y1": 13, + "~:x2": 9.5, + "~:y2": 13.01 + } + }, + "~:svg-defs": { + + }, + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-color": "#000000", + "~:stroke-opacity": 1, + "~:stroke-width": 10, + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:inner" + } + ], + "~:x": null, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 543.999999094532, + "~:y": 847.000034938226, + "~:width": 28.0000385514509, + "~:height": 0.0556475997042298, + "~:x1": 543.999999094532, + "~:y1": 847.000034938226, + "~:x2": 572.000037645983, + "~:y2": 847.055682537931 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~uc9348056-5090-8016-8006-ff760a55bce1": { + "~#shape": { + "~:y": null, + "~:stroke-cap-start": "~:round", + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:index": null, + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAACAAVEAMBXRAIAAAAAAAAAAAAAAAAAAAAAAAAAAkALRABAS0QCAAAAAAAAAAAAAAAAAAAAAAAAAAKAEUQAwFdE" + }, + "~:name": "svg-path", + "~:width": null, + "~:type": "~:path", + "~:svg-attrs": { + "~:fill": "none", + "~:strokeLinecap": "round", + "~:strokeLinejoin": "round" + }, + "~:points": [ + { + "~#point": { + "~:x": 532.000012233906, + "~:y": 813.000011676151 + } + }, + { + "~#point": { + "~:x": 581.999986425961, + "~:y": 813.000011676151 + } + }, + { + "~#point": { + "~:x": 581.999986425961, + "~:y": 862.999969855948 + } + }, + { + "~#point": { + "~:x": 532.000012233906, + "~:y": 862.999969855948 + } + } + ], + "~:layout-item-h-sizing": "~:fix", + "~:proportion-lock": false, + "~:stroke-cap-end": "~:round", + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:layout-item-v-sizing": "~:fix", + "~:svg-transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~uc9348056-5090-8016-8006-ff760a55bce1", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:svg-viewbox": { + "~#rect": { + "~:x": 2, + "~:y": 7, + "~:width": 9, + "~:height": 9, + "~:x1": 2, + "~:y1": 7, + "~:x2": 11, + "~:y2": 16 + } + }, + "~:svg-defs": { + + }, + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-color": "#000000", + "~:stroke-opacity": 1, + "~:stroke-width": 10, + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:inner" + } + ], + "~:x": null, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 532.000012233906, + "~:y": 813.000011676151, + "~:width": 49.9999741920556, + "~:height": 49.9999581797965, + "~:x1": 532.000012233906, + "~:y1": 813.000011676151, + "~:x2": 581.999986425961, + "~:y2": 862.999969855948 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~u0ae05ee2-98e5-8097-8007-00802f748d0f": { + "~#shape": { + "~:y": 758.000026702881, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:name": "Group-with-fills", + "~:width": 233.005674812197, + "~:type": "~:group", + "~:svg-attrs": { + + }, + "~:points": [ + { + "~#point": { + "~:x": 687.000030517578, + "~:y": 758.000026702881 + } + }, + { + "~#point": { + "~:x": 920.005705329775, + "~:y": 758.000026702881 + } + }, + { + "~#point": { + "~:x": 920.005705329775, + "~:y": 1036.12386848365 + } + }, + { + "~#point": { + "~:x": 687.000030517578, + "~:y": 1036.12386848365 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:svg-transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 4475, + "~:f": 13 + } + }, + "~:id": "~u0ae05ee2-98e5-8097-8007-00802f748d0f", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:svg-viewbox": { + "~#rect": { + "~:x": 4473.98860978118, + "~:y": 12.7111548396726, + "~:width": 233.119261878014, + "~:height": 280.600650192349, + "~:x1": 4473.98860978118, + "~:y1": 12.7111548396726, + "~:x2": 4707.10787165919, + "~:y2": 293.311805032022 + } + }, + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 687.000030517578, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 687.000030517578, + "~:y": 758.000026702881, + "~:width": 233.005674812197, + "~:height": 278.123841780764, + "~:x1": 687.000030517578, + "~:y1": 758.000026702881, + "~:x2": 920.005705329775, + "~:y2": 1036.12386848365 + } + }, + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:flip-x": false, + "~:height": 278.123841780764, + "~:flip-y": false, + "~:shapes": [ + "~u0ae05ee2-98e5-8097-8007-00802f748d10", + "~u0ae05ee2-98e5-8097-8007-00802f748d11" + ] + } + }, + "~u42797f0c-cd4d-80fd-8006-ff7c81651cd2": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAACc5hBE/78IRAIAAAAAAAAAAAAAAAAAAAAAAAAARQsARLCGPUQCAAAAAAAAAAAAAAAAAAAAAAAAADXNLEQnRh1EAgAAAAAAAAAAAAAAAAAAAAAAAAD4/+lDJ0YdRAIAAAAAAAAAAAAAAAAAAAAAAAAA7MEhRLCGPUQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + "~:name": "svg-polygon", + "~:width": null, + "~:type": "~:path", + "~:svg-attrs": { + + }, + "~:points": [ + { + "~#point": { + "~:x": 468.000003562829, + "~:y": 546.999999120513 + } + }, + { + "~#point": { + "~:x": 691.206278507297, + "~:y": 546.999999120513 + } + }, + { + "~#point": { + "~:x": 691.206278507297, + "~:y": 758.104517752871 + } + }, + { + "~#point": { + "~:x": 468.000003562829, + "~:y": 758.104517752871 + } + } + ], + "~:layout-item-h-sizing": "~:fix", + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:layout-item-v-sizing": "~:fix", + "~:svg-transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~u42797f0c-cd4d-80fd-8006-ff7c81651cd2", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:svg-viewbox": { + "~#rect": { + "~:x": 2, + "~:y": 0, + "~:width": 96, + "~:height": 90, + "~:x1": 2, + "~:y1": 0, + "~:x2": 98, + "~:y2": 90 + } + }, + "~:svg-defs": { + + }, + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-color": "#ff0000", + "~:stroke-opacity": 1, + "~:stroke-width": 5, + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:inner" + } + ], + "~:x": null, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 468.000003562829, + "~:y": 546.999999120513, + "~:width": 223.206274944468, + "~:height": 211.104518632358, + "~:x1": 468.000003562829, + "~:y1": 546.999999120513, + "~:x2": 691.206278507297, + "~:y2": 758.104517752871 + } + }, + "~:fills": [ + { + "~:fill-color": "#62d10b", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + } + }, + "~:id": "~u4732f3e3-7a1a-807e-8006-ff76066e631e", + "~:name": "Page 1" + } + }, + "~:id": "~u4732f3e3-7a1a-807e-8006-ff76066e631d", + "~:options": { + "~:components-v2": true, + "~:base-font-size": "16px" + } + } + } \ No newline at end of file diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js index c7b8810d22..5c9538ede0 100644 --- a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js +++ b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js @@ -211,3 +211,20 @@ test("Renders a file with a closed path shape with multiple segments using strok await expect(workspace.canvas).toHaveScreenshot(); }); + + +test("Renders a file with paths and svg attrs", async ({ + page, +}) => { + const workspace = new WasmWorkspacePage(page); + await workspace.setupEmptyFile(); + await workspace.mockGetFile("render-wasm/get-file-svg-attrs.json"); + + await workspace.goToWorkspace({ + id: "4732f3e3-7a1a-807e-8006-ff76066e631d", + pageId: "4732f3e3-7a1a-807e-8006-ff76066e631e", + }); + await workspace.waitForFirstRender(); + + await expect(workspace.canvas).toHaveScreenshot(); +}); diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-a-file-with-paths-and-svg-attrs-1.png b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-a-file-with-paths-and-svg-attrs-1.png new file mode 100644 index 0000000000..eb24479e66 Binary files /dev/null and b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-a-file-with-paths-and-svg-attrs-1.png differ diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index a92abfd454..954d243f92 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -370,12 +370,11 @@ (dissoc :style) (merge style) (select-keys allowed-keys)) - str (sr/serialize-path-attrs attrs) - size (count str)] - (when (pos? size) - (let [offset (mem/alloc size)] - (h/call wasm/internal-module "stringToUTF8" str offset size) - (h/call wasm/internal-module "_set_shape_path_attrs" (count attrs)))))) + fill-rule (-> attrs :fillRule sr/translate-fill-rule) + stroke-linecap (-> attrs :strokeLinecap sr/translate-stroke-linecap) + stroke-linejoin (-> attrs :strokeLinejoin sr/translate-stroke-linejoin) + fill-none (= "none" (-> attrs :fill))] + (h/call wasm/internal-module "_set_shape_svg_attrs" fill-rule stroke-linecap stroke-linejoin fill-none))) (defn set-shape-path-content "Upload path content in chunks to WASM." @@ -1160,7 +1159,10 @@ :text-direction (unchecked-get module "RawTextDirection") :text-decoration (unchecked-get module "RawTextDecoration") :text-transform (unchecked-get module "RawTextTransform") - :segment-data (unchecked-get module "RawSegmentData")}] + :segment-data (unchecked-get module "RawSegmentData") + :stroke-linecap (unchecked-get module "RawStrokeLineCap") + :stroke-linejoin (unchecked-get module "RawStrokeLineJoin") + :fill-rule (unchecked-get module "RawFillRule")}] (set! wasm/serializers serializers) (default)))) (p/fmap (fn [default] diff --git a/frontend/src/app/render_wasm/serializers.cljs b/frontend/src/app/render_wasm/serializers.cljs index f929ad5dac..1f7d0727d5 100644 --- a/frontend/src/app/render_wasm/serializers.cljs +++ b/frontend/src/app/render_wasm/serializers.cljs @@ -63,6 +63,24 @@ default (unchecked-get values "rect")] (d/nilv (unchecked-get values (d/name type)) default))) +(defn translate-stroke-linecap + [stroke-linecap] + (let [values (unchecked-get wasm/serializers "stroke-linecap") + default (unchecked-get values "butt")] + (d/nilv (unchecked-get values (d/name stroke-linecap)) default))) + +(defn translate-stroke-linejoin + [stroke-linejoin] + (let [values (unchecked-get wasm/serializers "stroke-linejoin") + default (unchecked-get values "miter")] + (d/nilv (unchecked-get values (d/name stroke-linejoin)) default))) + +(defn translate-fill-rule + [fill-rule] + (let [values (unchecked-get wasm/serializers "fill-rule") + default (unchecked-get values "nonzero")] + (d/nilv (unchecked-get values (d/name fill-rule)) default))) + (defn translate-stroke-style [stroke-style] (let [values (unchecked-get wasm/serializers "stroke-style") diff --git a/render-wasm/docs/serialization.md b/render-wasm/docs/serialization.md index 41c4741ba7..92d982b336 100644 --- a/render-wasm/docs/serialization.md +++ b/render-wasm/docs/serialization.md @@ -160,6 +160,38 @@ Stroke styles are serialized as `u8`: | 3 | Mixed | | \_ | Solid | +## Fill rules + +Fill rules are serialized as `u8` + +| Value | Field | +| ----- | ------ | +| 0 | Nonzero | +| 1 | Evenodd | +| \_ | Nonzero | + +## Stroke linecaps + +Stroke linecaps are serialized as `u8` + +| Value | Field | +| ----- | ------ | +| 0 | Butt | +| 1 | Round | +| 2 | Square | +| \_ | Butt | + +## Stroke linejoins + +Stroke linejoins are serialized as `u8` + +| Value | Field | +| ----- | ------ | +| 0 | Miter | +| 1 | Round | +| 2 | Bevel | +| \_ | Miter | + ## Bool Operations Bool operations (`bool-type`) are serialized as `u8`: diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index a810fe656a..a5d74614f0 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -764,14 +764,9 @@ impl RenderState { &shape }; - let has_fill_none = matches!( - shape.svg_attrs.get("fill").map(String::as_str), - Some("none") - ); - if shape.fills.is_empty() && !matches!(shape.shape_type, Type::Group(_)) - && !has_fill_none + && !shape.svg_attrs.fill_none { if let Some(fills_to_render) = self.nested_fills.last() { let fills_to_render = fills_to_render.clone(); diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index 8346d9af7e..cff30d7ff2 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -1,8 +1,8 @@ -use std::collections::HashMap; - use crate::math::{Matrix, Point, Rect}; -use crate::shapes::{Corners, Fill, ImageFill, Path, Shape, Stroke, StrokeCap, StrokeKind, Type}; +use crate::shapes::{ + Corners, Fill, ImageFill, Path, Shape, Stroke, StrokeCap, StrokeKind, SvgAttrs, Type, +}; use skia_safe::{self as skia, ImageFilter, RRect}; use super::{RenderState, SurfaceId}; @@ -17,7 +17,7 @@ fn draw_stroke_on_rect( rect: &Rect, selrect: &Rect, corners: &Option, - svg_attrs: &HashMap, + svg_attrs: &SvgAttrs, scale: f32, shadow: Option<&ImageFilter>, blur: Option<&ImageFilter>, @@ -53,7 +53,7 @@ fn draw_stroke_on_circle( stroke: &Stroke, rect: &Rect, selrect: &Rect, - svg_attrs: &HashMap, + svg_attrs: &SvgAttrs, scale: f32, shadow: Option<&ImageFilter>, blur: Option<&ImageFilter>, @@ -130,7 +130,7 @@ pub fn draw_stroke_on_path( path: &Path, selrect: &Rect, path_transform: Option<&Matrix>, - svg_attrs: &HashMap, + svg_attrs: &SvgAttrs, scale: f32, shadow: Option<&ImageFilter>, blur: Option<&ImageFilter>, @@ -217,7 +217,7 @@ fn handle_stroke_caps( selrect: &Rect, canvas: &skia::Canvas, is_open: bool, - svg_attrs: &HashMap, + svg_attrs: &SvgAttrs, scale: f32, blur: Option<&ImageFilter>, antialias: bool, diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 861c07aec5..ab9a9efed9 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -21,6 +21,7 @@ mod rects; mod shadows; mod shape_to_path; mod strokes; +mod svg_attrs; mod svgraw; mod text; pub mod text_paths; @@ -41,6 +42,7 @@ pub use rects::*; pub use shadows::*; pub use shape_to_path::*; pub use strokes::*; +pub use svg_attrs::*; pub use svgraw::*; pub use text::*; pub use transform::*; @@ -174,7 +176,7 @@ pub struct Shape { pub opacity: f32, pub hidden: bool, pub svg: Option, - pub svg_attrs: HashMap, + pub svg_attrs: SvgAttrs, pub shadows: Vec, pub layout_item: Option, pub extrect: OnceCell, @@ -201,7 +203,7 @@ impl Shape { hidden: false, blur: None, svg: None, - svg_attrs: HashMap::new(), + svg_attrs: SvgAttrs::default(), shadows: Vec::with_capacity(1), layout_item: None, extrect: OnceCell::new(), @@ -566,15 +568,6 @@ impl Shape { }; } - pub fn set_path_attr(&mut self, name: String, value: String) { - match self.shape_type { - Type::Path(_) | Type::Bool(_) => { - self.set_svg_attr(name, value); - } - _ => unreachable!("This shape should have path attrs"), - }; - } - pub fn set_svg_raw_content(&mut self, content: String) -> Result<(), String> { self.shape_type = Type::SVGRaw(SVGRaw::from_content(content)); Ok(()) @@ -607,10 +600,6 @@ impl Shape { self.svg = Some(svg); } - pub fn set_svg_attr(&mut self, name: String, value: String) { - self.svg_attrs.insert(name, value); - } - pub fn blend_mode(&self) -> BlendMode { self.blend_mode } @@ -1104,7 +1093,7 @@ impl Shape { if let Some(path_transform) = self.to_path_transform() { skia_path.transform(&path_transform); } - if let Some("evenodd") = self.svg_attrs.get("fill-rule").map(String::as_str) { + if self.svg_attrs.fill_rule == FillRule::Evenodd { skia_path.set_fill_type(skia::PathFillType::EvenOdd); } Some(skia_path) diff --git a/render-wasm/src/shapes/strokes.rs b/render-wasm/src/shapes/strokes.rs index fa33cb9c1d..8a22893d00 100644 --- a/render-wasm/src/shapes/strokes.rs +++ b/render-wasm/src/shapes/strokes.rs @@ -1,8 +1,10 @@ use crate::shapes::fills::{Fill, SolidColor}; use skia_safe::{self as skia, Rect}; -use std::collections::HashMap; use super::Corners; +use super::StrokeLineCap; +use super::StrokeLineJoin; +use super::SvgAttrs; #[derive(Debug, Clone, PartialEq, Copy)] pub enum StrokeStyle { @@ -159,7 +161,7 @@ impl Stroke { pub fn to_paint( &self, rect: &Rect, - svg_attrs: &HashMap, + svg_attrs: &SvgAttrs, scale: f32, antialias: bool, ) -> skia::Paint { @@ -175,11 +177,11 @@ impl Stroke { paint.set_stroke_width(width); paint.set_anti_alias(antialias); - if let Some("round") = svg_attrs.get("stroke-linecap").map(String::as_str) { + if svg_attrs.stroke_linecap == StrokeLineCap::Round { paint.set_stroke_cap(skia::paint::Cap::Round); } - if let Some("round") = svg_attrs.get("stroke-linejoin").map(String::as_str) { + if svg_attrs.stroke_linejoin == StrokeLineJoin::Round { paint.set_stroke_join(skia::paint::Join::Round); } @@ -225,7 +227,7 @@ impl Stroke { &self, is_open: bool, rect: &Rect, - svg_attrs: &HashMap, + svg_attrs: &SvgAttrs, scale: f32, antialias: bool, ) -> skia::Paint { @@ -249,7 +251,7 @@ impl Stroke { &self, is_open: bool, rect: &Rect, - svg_attrs: &HashMap, + svg_attrs: &SvgAttrs, scale: f32, antialias: bool, ) -> skia::Paint { diff --git a/render-wasm/src/shapes/svg_attrs.rs b/render-wasm/src/shapes/svg_attrs.rs new file mode 100644 index 0000000000..54e944efc5 --- /dev/null +++ b/render-wasm/src/shapes/svg_attrs.rs @@ -0,0 +1,49 @@ +#[derive(Debug, Clone, PartialEq, Copy, Default)] +pub enum FillRule { + #[default] + Nonzero, + Evenodd, +} + +#[derive(Debug, Clone, PartialEq, Copy, Default)] +pub enum StrokeLineCap { + #[default] + Butt, + Round, + Square, +} + +#[derive(Debug, Clone, PartialEq, Copy, Default)] +pub enum StrokeLineJoin { + #[default] + Miter, + Round, + Bevel, +} + +#[derive(Debug, Clone, PartialEq, Copy)] +pub struct SvgAttrs { + pub fill_rule: FillRule, + pub stroke_linecap: StrokeLineCap, + pub stroke_linejoin: StrokeLineJoin, + /// Indicates that this shape has an explicit `fill="none"` attribute. + /// + /// In SVG, the `fill` attribute is inheritable from container elements like ``. + /// However, when a shape explicitly sets `fill="none"`, it breaks the color + /// inheritance chain - the shape will not inherit fill colors from parent containers. + /// + /// This is different from having an empty fills array, as it explicitly signals + /// the intention to have no fill, preventing inheritance. + pub fill_none: bool, +} + +impl Default for SvgAttrs { + fn default() -> Self { + Self { + fill_rule: FillRule::Nonzero, + stroke_linecap: StrokeLineCap::Butt, + stroke_linejoin: StrokeLineJoin::Miter, + fill_none: false, + } + } +} diff --git a/render-wasm/src/wasm.rs b/render-wasm/src/wasm.rs index 8002e462c4..8dedf0a97f 100644 --- a/render-wasm/src/wasm.rs +++ b/render-wasm/src/wasm.rs @@ -7,4 +7,5 @@ pub mod paths; pub mod shadows; pub mod shapes; pub mod strokes; +pub mod svg_attrs; pub mod text; diff --git a/render-wasm/src/wasm/paths.rs b/render-wasm/src/wasm/paths.rs index aa70cbe3a2..4f0fc34ebb 100644 --- a/render-wasm/src/wasm/paths.rs +++ b/render-wasm/src/wasm/paths.rs @@ -225,37 +225,6 @@ pub extern "C" fn current_to_path() -> *mut u8 { mem::write_vec(result) } -// Extracts a string from the bytes slice until the next null byte (0) and returns the result as a `String`. -// Updates the `start` index to the end of the extracted string. -fn extract_string(start: &mut usize, bytes: &[u8]) -> String { - match bytes[*start..].iter().position(|&b| b == 0) { - Some(pos) => { - let end = *start + pos; - let slice = &bytes[*start..end]; - *start = end + 1; // Move the `start` pointer past the null byte - // Call to unsafe function within an unsafe block - unsafe { String::from_utf8_unchecked(slice.to_vec()) } - } - None => { - *start = bytes.len(); // Move `start` to the end if no null byte is found - String::new() - } - } -} - -#[no_mangle] -pub extern "C" fn set_shape_path_attrs(num_attrs: u32) { - with_current_shape_mut!(state, |shape: &mut Shape| { - let bytes = mem::bytes(); - let mut start = 0; - for _ in 0..num_attrs { - let name = extract_string(&mut start, &bytes); - let value = extract_string(&mut start, &bytes); - shape.set_path_attr(name, value); - } - }); -} - #[cfg(test)] mod tests { use super::*; diff --git a/render-wasm/src/wasm/svg_attrs.rs b/render-wasm/src/wasm/svg_attrs.rs new file mode 100644 index 0000000000..79b9e65e69 --- /dev/null +++ b/render-wasm/src/wasm/svg_attrs.rs @@ -0,0 +1,95 @@ +use macros::ToJs; + +use crate::shapes::{FillRule, StrokeLineCap, StrokeLineJoin}; +use crate::{with_current_shape_mut, STATE}; + +#[derive(PartialEq, ToJs)] +#[repr(u8)] +#[allow(dead_code)] +pub enum RawFillRule { + Nonzero = 0, + Evenodd = 1, +} + +impl From for RawFillRule { + fn from(value: u8) -> Self { + unsafe { std::mem::transmute(value) } + } +} + +impl From for FillRule { + fn from(value: RawFillRule) -> Self { + match value { + RawFillRule::Nonzero => FillRule::Nonzero, + RawFillRule::Evenodd => FillRule::Evenodd, + } + } +} + +#[derive(PartialEq, ToJs)] +#[repr(u8)] +#[allow(dead_code)] +pub enum RawStrokeLineCap { + Butt = 0, + Round = 1, + Square = 2, +} + +impl From for RawStrokeLineCap { + fn from(value: u8) -> Self { + unsafe { std::mem::transmute(value) } + } +} + +impl From for StrokeLineCap { + fn from(value: RawStrokeLineCap) -> Self { + match value { + RawStrokeLineCap::Butt => StrokeLineCap::Butt, + RawStrokeLineCap::Round => StrokeLineCap::Round, + RawStrokeLineCap::Square => StrokeLineCap::Square, + } + } +} + +#[derive(PartialEq, ToJs)] +#[repr(u8)] +#[allow(dead_code)] +pub enum RawStrokeLineJoin { + Miter = 0, + Round = 1, + Bevel = 2, +} + +impl From for RawStrokeLineJoin { + fn from(value: u8) -> Self { + unsafe { std::mem::transmute(value) } + } +} + +impl From for StrokeLineJoin { + fn from(value: RawStrokeLineJoin) -> Self { + match value { + RawStrokeLineJoin::Miter => StrokeLineJoin::Miter, + RawStrokeLineJoin::Round => StrokeLineJoin::Round, + RawStrokeLineJoin::Bevel => StrokeLineJoin::Bevel, + } + } +} + +#[no_mangle] +pub extern "C" fn set_shape_svg_attrs( + fill_rule: u8, + stroke_linecap: u8, + stroke_linejoin: u8, + fill_none: bool, +) { + with_current_shape_mut!(state, |shape: &mut Shape| { + let fill_rule = RawFillRule::from(fill_rule); + shape.svg_attrs.fill_rule = fill_rule.into(); + let stroke_linecap = RawStrokeLineCap::from(stroke_linecap); + shape.svg_attrs.stroke_linecap = stroke_linecap.into(); + let stroke_linejoin = RawStrokeLineJoin::from(stroke_linejoin); + shape.svg_attrs.stroke_linejoin = stroke_linejoin.into(); + shape.svg_attrs.fill_none = fill_none; + }); +}