From 479ce99b325284732d9ee245fc86972004042a06 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 23 Oct 2025 14:22:22 +0200 Subject: [PATCH] :sparkles: Improve setting svg attrs in wasm --- .../data/render-wasm/get-file-svg-attrs.json | 1312 +++++++++++++++++ .../ui/render-wasm-specs/shapes.spec.js | 17 + ...ders-a-file-with-paths-and-svg-attrs-1.png | Bin 0 -> 34880 bytes frontend/src/app/render_wasm/api.cljs | 16 +- frontend/src/app/render_wasm/serializers.cljs | 18 + render-wasm/docs/serialization.md | 32 + render-wasm/src/render.rs | 7 +- render-wasm/src/render/strokes.rs | 14 +- render-wasm/src/shapes.rs | 21 +- render-wasm/src/shapes/strokes.rs | 14 +- render-wasm/src/shapes/svg_attrs.rs | 49 + render-wasm/src/wasm.rs | 1 + render-wasm/src/wasm/paths.rs | 31 - render-wasm/src/wasm/svg_attrs.rs | 95 ++ 14 files changed, 1554 insertions(+), 73 deletions(-) create mode 100644 frontend/playwright/data/render-wasm/get-file-svg-attrs.json create mode 100644 frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-a-file-with-paths-and-svg-attrs-1.png create mode 100644 render-wasm/src/shapes/svg_attrs.rs create mode 100644 render-wasm/src/wasm/svg_attrs.rs 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 0000000000000000000000000000000000000000..eb24479e66bb72234ec5d62b96f180a0c09f8308 GIT binary patch literal 34880 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 literal 0 HcmV?d00001 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; + }); +}