mirror of
https://github.com/penpot/penpot.git
synced 2025-12-12 06:24:17 +01:00
Merge pull request #7343 from penpot/elenatorro-12118-support-large-svg-files
🐛 Fix parsing large paths with multiple subpaths
This commit is contained in:
@@ -0,0 +1,567 @@
|
|||||||
|
{
|
||||||
|
"~:features": {
|
||||||
|
"~#set": [
|
||||||
|
"fdata/path-data",
|
||||||
|
"plugins/runtime",
|
||||||
|
"design-tokens/v1",
|
||||||
|
"variants/v1",
|
||||||
|
"layout/grid",
|
||||||
|
"styles/v2",
|
||||||
|
"fdata/objects-map",
|
||||||
|
"render-wasm/v1",
|
||||||
|
"components/v2",
|
||||||
|
"fdata/shape-data-type"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"~:team-id": "~u6bd7c17d-4f59-815e-8006-5c1f6882469a",
|
||||||
|
"~: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": "small_closed_path",
|
||||||
|
"~:revn": 25,
|
||||||
|
"~:modified-at": "~m1758717395171",
|
||||||
|
"~:vern": 0,
|
||||||
|
"~:id": "~u3f7c3cc4-556d-80fa-8006-da2505231c2b",
|
||||||
|
"~: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",
|
||||||
|
"0007-clear-invalid-strokes-and-fills-v2",
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"~:version": 67,
|
||||||
|
"~:project-id": "~uf084c276-e46f-8168-8006-ce89321fde44",
|
||||||
|
"~:created-at": "~m1758713852044",
|
||||||
|
"~:data": {
|
||||||
|
"~:pages": [
|
||||||
|
"~u3f7c3cc4-556d-80fa-8006-da2505231c2c"
|
||||||
|
],
|
||||||
|
"~:pages-index": {
|
||||||
|
"~u3f7c3cc4-556d-80fa-8006-da2505231c2c": {
|
||||||
|
"~: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": [
|
||||||
|
"~ue758a369-49fb-801a-8006-da250da25b70"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~ue758a369-49fb-801a-8006-da250da25b70": {
|
||||||
|
"~#shape": {
|
||||||
|
"~:y": 289.0000254924257,
|
||||||
|
"~:transform": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1,
|
||||||
|
"~:b": 0,
|
||||||
|
"~:c": 0,
|
||||||
|
"~:d": 1,
|
||||||
|
"~:e": 0,
|
||||||
|
"~:f": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:rotation": 0,
|
||||||
|
"~:name": "test",
|
||||||
|
"~:width": 1269.396990548183,
|
||||||
|
"~:type": "~:group",
|
||||||
|
"~:svg-attrs": {
|
||||||
|
"~:width": "36.938",
|
||||||
|
"~:height": "39.605"
|
||||||
|
},
|
||||||
|
"~:points": [
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 15.99999664735742,
|
||||||
|
"~:y": 289.0000254924257
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 1285.3969871955405,
|
||||||
|
"~:y": 289.0000254924257
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 1285.3969871955405,
|
||||||
|
"~:y": 1473.2282874172934
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 15.99999664735742,
|
||||||
|
"~:y": 1473.2282874172934
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~: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",
|
||||||
|
"~:id": "~ue758a369-49fb-801a-8006-da250da25b70",
|
||||||
|
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:strokes": [],
|
||||||
|
"~:x": 15.99999664735742,
|
||||||
|
"~:proportion": 1,
|
||||||
|
"~:selrect": {
|
||||||
|
"~#rect": {
|
||||||
|
"~:x": 15.99999664735742,
|
||||||
|
"~:y": 289.0000254924257,
|
||||||
|
"~:width": 1269.396990548183,
|
||||||
|
"~:height": 1184.2282619248676,
|
||||||
|
"~:x1": 15.99999664735742,
|
||||||
|
"~:y1": 289.0000254924257,
|
||||||
|
"~:x2": 1285.3969871955405,
|
||||||
|
"~:y2": 1473.2282874172934
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:fills": [],
|
||||||
|
"~:flip-x": false,
|
||||||
|
"~:height": 1184.2282619248676,
|
||||||
|
"~:flip-y": false,
|
||||||
|
"~:shapes": [
|
||||||
|
"~ue758a369-49fb-801a-8006-da250da460cc",
|
||||||
|
"~ue758a369-49fb-801a-8006-da250da8073d"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~ue758a369-49fb-801a-8006-da250da460cc": {
|
||||||
|
"~#shape": {
|
||||||
|
"~:y": 289.00001521099387,
|
||||||
|
"~:transform": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1,
|
||||||
|
"~:b": 0,
|
||||||
|
"~:c": 0,
|
||||||
|
"~:d": 1,
|
||||||
|
"~:e": 0,
|
||||||
|
"~:f": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:rotation": 0,
|
||||||
|
"~:name": "base-background",
|
||||||
|
"~:width": 1269.3871566880562,
|
||||||
|
"~:type": "~:rect",
|
||||||
|
"~:svg-attrs": {
|
||||||
|
"~:fill": "none",
|
||||||
|
"~:id": "base-background"
|
||||||
|
},
|
||||||
|
"~:points": [
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 15.99999664735742,
|
||||||
|
"~:y": 289.000015210994
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 1285.3871533354136,
|
||||||
|
"~:y": 289.000015210994
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 1285.3871533354136,
|
||||||
|
"~:y": 1473.2279626015984
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 15.99999664735742,
|
||||||
|
"~:y": 1473.2279626015984
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:r2": 0,
|
||||||
|
"~:proportion-lock": false,
|
||||||
|
"~:transform-inverse": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1,
|
||||||
|
"~:b": 0,
|
||||||
|
"~:c": 0,
|
||||||
|
"~:d": 1,
|
||||||
|
"~:e": 0,
|
||||||
|
"~:f": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:r3": 0,
|
||||||
|
"~:r1": 0,
|
||||||
|
"~:hidden": true,
|
||||||
|
"~:id": "~ue758a369-49fb-801a-8006-da250da460cc",
|
||||||
|
"~:parent-id": "~ue758a369-49fb-801a-8006-da250da25b70",
|
||||||
|
"~:svg-viewbox": {
|
||||||
|
"~#rect": {
|
||||||
|
"~:x": 0,
|
||||||
|
"~:y": 0,
|
||||||
|
"~:width": 9.773,
|
||||||
|
"~:height": 10.479,
|
||||||
|
"~:x1": 0,
|
||||||
|
"~:y1": 0,
|
||||||
|
"~:x2": 9.773,
|
||||||
|
"~:y2": 10.479
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:svg-defs": {},
|
||||||
|
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:strokes": [
|
||||||
|
{
|
||||||
|
"~:stroke-style": "~:solid",
|
||||||
|
"~:stroke-alignment": "~:outer",
|
||||||
|
"~:stroke-width": 10,
|
||||||
|
"~:stroke-color": "#cd0e8f",
|
||||||
|
"~:stroke-opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~:stroke-style": "~:solid",
|
||||||
|
"~:stroke-alignment": "~:inner",
|
||||||
|
"~:stroke-width": 10,
|
||||||
|
"~:stroke-color": "#0c31e0",
|
||||||
|
"~:stroke-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:x": 15.99999664735742,
|
||||||
|
"~:proportion": 1,
|
||||||
|
"~:r4": 0,
|
||||||
|
"~:selrect": {
|
||||||
|
"~#rect": {
|
||||||
|
"~:x": 15.99999664735742,
|
||||||
|
"~:y": 289.00001521099387,
|
||||||
|
"~:width": 1269.3871566880562,
|
||||||
|
"~:height": 1184.2279473906046,
|
||||||
|
"~:x1": 15.99999664735742,
|
||||||
|
"~:y1": 289.00001521099387,
|
||||||
|
"~:x2": 1285.3871533354136,
|
||||||
|
"~:y2": 1473.2279626015984
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#5dde7f",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:flip-x": null,
|
||||||
|
"~:height": 1184.2279473906046,
|
||||||
|
"~:flip-y": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~ue758a369-49fb-801a-8006-da250da8073d": {
|
||||||
|
"~#shape": {
|
||||||
|
"~:y": null,
|
||||||
|
"~:transform": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1,
|
||||||
|
"~:b": 0,
|
||||||
|
"~:c": 0,
|
||||||
|
"~:d": 1,
|
||||||
|
"~:e": 0,
|
||||||
|
"~:f": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:rotation": 0,
|
||||||
|
"~:content": {
|
||||||
|
"~#penpot/path-data": "~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"
|
||||||
|
},
|
||||||
|
"~:name": "svg-path",
|
||||||
|
"~:width": null,
|
||||||
|
"~:type": "~:path",
|
||||||
|
"~:svg-attrs": {
|
||||||
|
"~:style": {
|
||||||
|
"~:strokeWidth": "0.296442"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:points": [
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 16.000007651096666,
|
||||||
|
"~:y": 289.0000106633722
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 1285.3965909402777,
|
||||||
|
"~:y": 289.0000106633722
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 1285.3965909402777,
|
||||||
|
"~:y": 1473.196270402294
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 16.000007651096666,
|
||||||
|
"~:y": 1473.196270402294
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~: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": -83.989157,
|
||||||
|
"~:f": -73.223098
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:id": "~ue758a369-49fb-801a-8006-da250da8073d",
|
||||||
|
"~:parent-id": "~ue758a369-49fb-801a-8006-da250da25b70",
|
||||||
|
"~:svg-viewbox": {
|
||||||
|
"~#rect": {
|
||||||
|
"~:x": 0.000003135483506132297,
|
||||||
|
"~:y": -0.0000027832518292754405,
|
||||||
|
"~:width": 9.773072575314877,
|
||||||
|
"~:height": 10.478719602207821,
|
||||||
|
"~:x1": 0.000003135483506132297,
|
||||||
|
"~:y1": -0.0000027832518292754405,
|
||||||
|
"~:x2": 9.773075710798384,
|
||||||
|
"~:y2": 10.478716818955991
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:svg-defs": {},
|
||||||
|
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:strokes": [
|
||||||
|
{
|
||||||
|
"~:stroke-style": "~:solid",
|
||||||
|
"~:stroke-alignment": "~:outer",
|
||||||
|
"~:stroke-width": 10,
|
||||||
|
"~:stroke-color": "#cd0e8f",
|
||||||
|
"~:stroke-opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~:stroke-style": "~:solid",
|
||||||
|
"~:stroke-alignment": "~:inner",
|
||||||
|
"~:stroke-width": 10,
|
||||||
|
"~:stroke-color": "#0c31e0",
|
||||||
|
"~:stroke-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:x": null,
|
||||||
|
"~:proportion": 1,
|
||||||
|
"~:shadow": [
|
||||||
|
{
|
||||||
|
"~:color": {
|
||||||
|
"~:color": "#000000",
|
||||||
|
"~:opacity": 0.2
|
||||||
|
},
|
||||||
|
"~:spread": 0,
|
||||||
|
"~:offset-y": 20,
|
||||||
|
"~:style": "~:drop-shadow",
|
||||||
|
"~:blur": 4,
|
||||||
|
"~:hidden": false,
|
||||||
|
"~:id": "~u055fbfc6-9f69-80cb-8006-da327c4b1584",
|
||||||
|
"~:offset-x": 20
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:selrect": {
|
||||||
|
"~#rect": {
|
||||||
|
"~:x": 16.00000765109678,
|
||||||
|
"~:y": 289.0000106633721,
|
||||||
|
"~:width": 1269.3965832891809,
|
||||||
|
"~:height": 1184.196259738922,
|
||||||
|
"~:x1": 16.00000765109678,
|
||||||
|
"~:y1": 289.0000106633721,
|
||||||
|
"~:x2": 1285.3965909402777,
|
||||||
|
"~:y2": 1473.196270402294
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#5dde7f",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:flip-x": null,
|
||||||
|
"~:height": null,
|
||||||
|
"~:flip-y": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:id": "~u3f7c3cc4-556d-80fa-8006-da2505231c2c",
|
||||||
|
"~:name": "Page 1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:id": "~u3f7c3cc4-556d-80fa-8006-da2505231c2b",
|
||||||
|
"~:options": {
|
||||||
|
"~:components-v2": true,
|
||||||
|
"~:base-font-size": "16px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -195,3 +195,19 @@ test("Renders a file with shadows applied to any kind of shape", async ({
|
|||||||
|
|
||||||
await expect(workspace.canvas).toHaveScreenshot();
|
await expect(workspace.canvas).toHaveScreenshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Renders a file with a closed path shape with multiple segments using strokes and shadow", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const workspace = new WasmWorkspacePage(page);
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.mockGetFile("render-wasm/get-subpath-stroke-shadow.json");
|
||||||
|
|
||||||
|
await workspace.goToWorkspace({
|
||||||
|
id: "3f7c3cc4-556d-80fa-8006-da2505231c2b",
|
||||||
|
pageId: "3f7c3cc4-556d-80fa-8006-da2505231c2c",
|
||||||
|
});
|
||||||
|
await workspace.waitForFirstRender();
|
||||||
|
|
||||||
|
await expect(workspace.canvas).toHaveScreenshot();
|
||||||
|
});
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 212 KiB |
@@ -10,6 +10,7 @@
|
|||||||
["react-dom/server" :as rds]
|
["react-dom/server" :as rds]
|
||||||
[app.common.data :as d :refer [not-empty?]]
|
[app.common.data :as d :refer [not-empty?]]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.math :as mth]
|
||||||
[app.common.types.fills :as types.fills]
|
[app.common.types.fills :as types.fills]
|
||||||
[app.common.types.fills.impl :as types.fills.impl]
|
[app.common.types.fills.impl :as types.fills.impl]
|
||||||
[app.common.types.path :as path]
|
[app.common.types.path :as path]
|
||||||
@@ -51,6 +52,8 @@
|
|||||||
(def ^:const GRID-LAYOUT-COLUMN-U8-SIZE 8)
|
(def ^:const GRID-LAYOUT-COLUMN-U8-SIZE 8)
|
||||||
(def ^:const GRID-LAYOUT-CELL-U8-SIZE 36)
|
(def ^:const GRID-LAYOUT-CELL-U8-SIZE 36)
|
||||||
|
|
||||||
|
(def ^:const MAX_BUFFER_CHUNK_SIZE (* 256 1024))
|
||||||
|
|
||||||
(def dpr
|
(def dpr
|
||||||
(if use-dpr? (if (exists? js/window) js/window.devicePixelRatio 1.0) 1.0))
|
(if use-dpr? (if (exists? js/window) js/window.devicePixelRatio 1.0) 1.0))
|
||||||
|
|
||||||
@@ -317,15 +320,27 @@
|
|||||||
(h/call wasm/internal-module "stringToUTF8" str offset size)
|
(h/call wasm/internal-module "stringToUTF8" str offset size)
|
||||||
(h/call wasm/internal-module "_set_shape_path_attrs" (count attrs))))
|
(h/call wasm/internal-module "_set_shape_path_attrs" (count attrs))))
|
||||||
|
|
||||||
;; FIXME: revisit on heap refactor is merged to use u32 instead u8
|
|
||||||
(defn set-shape-path-content
|
(defn set-shape-path-content
|
||||||
|
"Upload path content in chunks to WASM."
|
||||||
[content]
|
[content]
|
||||||
(let [pdata (path/content content)
|
(let [chunk-size (quot MAX_BUFFER_CHUNK_SIZE 4)
|
||||||
size (path/get-byte-size content)
|
buffer-size (path/get-byte-size content)
|
||||||
offset (mem/alloc size)
|
padded-size (* 4 (mth/ceil (/ buffer-size 4)))
|
||||||
heap (mem/get-heap-u8)]
|
buffer (js/Uint8Array. padded-size)]
|
||||||
(path/write-to pdata (.-buffer heap) offset)
|
(path/write-to content (.-buffer buffer) 0)
|
||||||
(h/call wasm/internal-module "_set_shape_path_content")))
|
(h/call wasm/internal-module "_start_shape_path_buffer")
|
||||||
|
(let [heapu32 (mem/get-heap-u32)]
|
||||||
|
(loop [offset 0]
|
||||||
|
(when (< offset padded-size)
|
||||||
|
(let [end (min padded-size (+ offset (* chunk-size 4)))
|
||||||
|
chunk (.subarray buffer offset end)
|
||||||
|
chunk-u32 (js/Uint32Array. chunk.buffer chunk.byteOffset (quot (.-length chunk) 4))
|
||||||
|
offset-size (.-length chunk-u32)
|
||||||
|
heap-offset (mem/alloc->offset-32 (* 4 offset-size))]
|
||||||
|
(.set heapu32 chunk-u32 heap-offset)
|
||||||
|
(h/call wasm/internal-module "_set_shape_path_chunk_buffer")
|
||||||
|
(recur end)))))
|
||||||
|
(h/call wasm/internal-module "_set_shape_path_buffer")))
|
||||||
|
|
||||||
(defn set-shape-svg-raw-content
|
(defn set-shape-svg-raw-content
|
||||||
[content]
|
[content]
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ pub fn is_close_to(current: f32, value: f32) -> bool {
|
|||||||
(current - value).abs() <= THRESHOLD
|
(current - value).abs() <= THRESHOLD
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn are_close_points(a: impl Into<(f32, f32)>, b: impl Into<(f32, f32)>) -> bool {
|
pub fn are_close_points(a: impl Into<(f32, f32)>, b: impl Into<(f32, f32)>) -> bool {
|
||||||
let (a_x, a_y) = a.into();
|
let (a_x, a_y) = a.into();
|
||||||
let (b_x, b_y) = b.into();
|
let (b_x, b_y) = b.into();
|
||||||
|
|||||||
@@ -1334,6 +1334,9 @@ impl RenderState {
|
|||||||
// Nested shapes shadowing - apply black shadow to child shapes too
|
// Nested shapes shadowing - apply black shadow to child shapes too
|
||||||
for shadow_shape_id in element.children.iter() {
|
for shadow_shape_id in element.children.iter() {
|
||||||
let shadow_shape = tree.get(shadow_shape_id).unwrap();
|
let shadow_shape = tree.get(shadow_shape_id).unwrap();
|
||||||
|
if shadow_shape.hidden {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let clip_bounds = node_render_state.get_nested_shadow_clip_bounds(
|
let clip_bounds = node_render_state.get_nested_shadow_clip_bounds(
|
||||||
element,
|
element,
|
||||||
modifiers.get(&element.id),
|
modifiers.get(&element.id),
|
||||||
|
|||||||
@@ -14,23 +14,7 @@ pub enum Segment {
|
|||||||
Close,
|
Close,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Segment {
|
impl Segment {}
|
||||||
fn xy(&self) -> Option<Point> {
|
|
||||||
match self {
|
|
||||||
Segment::MoveTo(xy) => Some(*xy),
|
|
||||||
Segment::LineTo(xy) => Some(*xy),
|
|
||||||
Segment::CurveTo((_, _, xy)) => Some(*xy),
|
|
||||||
Segment::Close => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_close_to(&self, other: &Segment) -> bool {
|
|
||||||
match (self.xy(), other.xy()) {
|
|
||||||
(Some(a), Some(b)) => math::are_close_points(a, b),
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Path {
|
pub struct Path {
|
||||||
@@ -92,8 +76,7 @@ impl Path {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: handle error
|
let open = subpaths::is_open_path(&segments);
|
||||||
let open = subpaths::is_open_path(&segments).expect("Failed to determine if path is open");
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
segments,
|
segments,
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
use super::Segment;
|
use crate::shapes::paths::Point;
|
||||||
use crate::math::are_close_points;
|
use crate::shapes::paths::Segment;
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, String>;
|
#[derive(Debug, Clone)]
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct Subpath {
|
pub struct Subpath {
|
||||||
segments: Vec<Segment>,
|
pub segments: Vec<Segment>,
|
||||||
closed: Option<bool>,
|
closed: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,230 +15,188 @@ impl Subpath {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn starts_in(&self, other_segment: Option<&Segment>) -> bool {
|
pub fn start(&self) -> Option<Point> {
|
||||||
if let (Some(start), Some(end)) = (self.start(), other_segment) {
|
self.segments.first().and_then(|s| match s {
|
||||||
start.is_close_to(end)
|
Segment::MoveTo(p) | Segment::LineTo(p) => Some(*p),
|
||||||
} else {
|
_ => None,
|
||||||
false
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ends_in(&self, other_segment: Option<&Segment>) -> bool {
|
pub fn end(&self) -> Option<Point> {
|
||||||
if let (Some(end), Some(start)) = (self.end(), other_segment) {
|
self.segments.iter().rev().find_map(|s| match s {
|
||||||
end.is_close_to(start)
|
Segment::MoveTo(p) | Segment::LineTo(p) => Some(*p),
|
||||||
} else {
|
Segment::CurveTo((_, _, p)) => Some(*p),
|
||||||
false
|
_ => None,
|
||||||
}
|
})
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start(&self) -> Option<&Segment> {
|
|
||||||
self.segments.first()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn end(&self) -> Option<&Segment> {
|
|
||||||
self.segments.last()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.segments.is_empty()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_closed(&self) -> bool {
|
pub fn is_closed(&self) -> bool {
|
||||||
self.closed.unwrap_or_else(|| self.calculate_closed())
|
self.closed.unwrap_or_else(|| self.calculate_closed())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_segment(&mut self, segment: Segment) {
|
|
||||||
self.segments.push(segment);
|
|
||||||
self.closed = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reversed(&self) -> Self {
|
pub fn reversed(&self) -> Self {
|
||||||
let mut reversed = self.clone();
|
let mut rev = self.clone();
|
||||||
reversed.segments.reverse();
|
rev.segments.reverse();
|
||||||
reversed
|
rev.closed = None;
|
||||||
|
rev
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_closed(&self) -> bool {
|
fn calculate_closed(&self) -> bool {
|
||||||
if self.segments.is_empty() {
|
if self.segments.is_empty() {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the path ends with a Close segment
|
|
||||||
if let Some(Segment::Close) = self.segments.last() {
|
if let Some(Segment::Close) = self.segments.last() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if let (Some(first), Some(last)) = (self.start(), self.end()) {
|
||||||
// Check if the first and last points are close to each other
|
return are_close_points(first, last);
|
||||||
if let (Some(first), Some(last)) = (self.segments.first(), self.segments.last()) {
|
|
||||||
let first_point = match first {
|
|
||||||
Segment::MoveTo(xy) => xy,
|
|
||||||
_ => return false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let last_point = match last {
|
|
||||||
Segment::LineTo(xy) => xy,
|
|
||||||
Segment::CurveTo((_, _, xy)) => xy,
|
|
||||||
_ => return false,
|
|
||||||
};
|
|
||||||
|
|
||||||
return are_close_points(*first_point, *last_point);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Subpath {
|
fn are_close_points(a: Point, b: Point) -> bool {
|
||||||
fn default() -> Self {
|
let tol = 1e-1;
|
||||||
Self::new(vec![])
|
(a.0 - b.0).abs() < tol && (a.1 - b.1).abs() < tol
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum MergeMode {
|
||||||
|
EndStart,
|
||||||
|
StartEnd,
|
||||||
|
EndEnd,
|
||||||
|
StartStart,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<(&Subpath, &Subpath)> for Subpath {
|
||||||
|
type Error = &'static str;
|
||||||
|
fn try_from((a, b): (&Subpath, &Subpath)) -> Result<Self, Self::Error> {
|
||||||
|
let mut segs = a.segments.clone();
|
||||||
|
segs.extend_from_slice(&b.segments);
|
||||||
|
Ok(Subpath::new(segs))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Joins two subpaths into a single subpath
|
pub fn closed_subpaths(subpaths: Vec<Subpath>) -> Vec<Subpath> {
|
||||||
impl TryFrom<(&Subpath, &Subpath)> for Subpath {
|
let n = subpaths.len();
|
||||||
type Error = String;
|
if n == 0 {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
fn try_from((subpath, other): (&Subpath, &Subpath)) -> Result<Self> {
|
let mut used = vec![false; n];
|
||||||
if subpath.is_empty() || other.is_empty() || subpath.end() != other.start() {
|
let mut result = Vec::with_capacity(n);
|
||||||
return Err("Subpaths cannot be joined".to_string());
|
|
||||||
|
for i in 0..n {
|
||||||
|
if used[i] {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut segments = subpath.segments.clone();
|
let mut current = subpaths[i].clone();
|
||||||
segments.extend_from_slice(&other.segments);
|
used[i] = true;
|
||||||
Ok(Subpath::new(segments))
|
let mut merged_any = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if current.is_closed() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut did_merge = false;
|
||||||
|
|
||||||
|
for j in 0..n {
|
||||||
|
if used[j] || subpaths[j].is_closed() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let candidate = &subpaths[j];
|
||||||
|
let maybe_merge = [
|
||||||
|
(current.end(), candidate.start(), MergeMode::EndStart),
|
||||||
|
(current.start(), candidate.end(), MergeMode::StartEnd),
|
||||||
|
(current.end(), candidate.end(), MergeMode::EndEnd),
|
||||||
|
(current.start(), candidate.start(), MergeMode::StartStart),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.find_map(|(p1, p2, mode)| {
|
||||||
|
if let (Some(a), Some(b)) = (p1, p2) {
|
||||||
|
if are_close_points(*a, *b) {
|
||||||
|
Some(mode.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(mode) = maybe_merge {
|
||||||
|
if let Some(new_current) = try_merge(¤t, candidate, mode) {
|
||||||
|
used[j] = true;
|
||||||
|
current = new_current;
|
||||||
|
merged_any = true;
|
||||||
|
did_merge = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !did_merge {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !current.is_closed() && merged_any {
|
||||||
|
if let Some(start) = current.start() {
|
||||||
|
let mut segs = current.segments.clone();
|
||||||
|
segs.push(Segment::LineTo(start));
|
||||||
|
segs.push(Segment::Close);
|
||||||
|
current = Subpath::new(segs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_merge(current: &Subpath, candidate: &Subpath, mode: MergeMode) -> Option<Subpath> {
|
||||||
|
match mode {
|
||||||
|
MergeMode::EndStart => Subpath::try_from((current, candidate)).ok(),
|
||||||
|
MergeMode::StartEnd => Subpath::try_from((candidate, current)).ok(),
|
||||||
|
MergeMode::EndEnd => Subpath::try_from((current, &candidate.reversed())).ok(),
|
||||||
|
MergeMode::StartStart => Subpath::try_from((&candidate.reversed(), current)).ok(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Groups segments into subpaths based on MoveTo segments
|
pub fn split_into_subpaths(segments: &[Segment]) -> Vec<Subpath> {
|
||||||
fn get_subpaths(segments: &[Segment]) -> Vec<Subpath> {
|
let mut subpaths = Vec::new();
|
||||||
let mut subpaths: Vec<Subpath> = vec![];
|
let mut current_segments = Vec::new();
|
||||||
let mut current_subpath = Subpath::default();
|
|
||||||
|
|
||||||
for segment in segments {
|
for segment in segments {
|
||||||
match segment {
|
match segment {
|
||||||
Segment::MoveTo(_) => {
|
Segment::MoveTo(_) => {
|
||||||
if !current_subpath.is_empty() {
|
// Start new subpath unless current is empty
|
||||||
subpaths.push(current_subpath);
|
if !current_segments.is_empty() {
|
||||||
|
subpaths.push(Subpath::new(current_segments.clone()));
|
||||||
|
current_segments.clear();
|
||||||
}
|
}
|
||||||
current_subpath = Subpath::default();
|
current_segments.push(*segment);
|
||||||
// Add the MoveTo segment to the new subpath
|
|
||||||
current_subpath.add_segment(*segment);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
current_subpath.add_segment(*segment);
|
|
||||||
}
|
}
|
||||||
|
_ => current_segments.push(*segment),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !current_subpath.is_empty() {
|
// Push last subpath if any
|
||||||
subpaths.push(current_subpath);
|
if !current_segments.is_empty() {
|
||||||
|
subpaths.push(Subpath::new(current_segments));
|
||||||
}
|
}
|
||||||
|
|
||||||
subpaths
|
subpaths
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Computes the merged candidate and the remaining, unmerged subpaths
|
pub fn is_open_path(segments: &[Segment]) -> bool {
|
||||||
fn merge_paths(candidate: Subpath, others: Vec<Subpath>) -> Result<(Subpath, Vec<Subpath>)> {
|
let subpaths = split_into_subpaths(segments);
|
||||||
if candidate.is_closed() {
|
let closed_subpaths = closed_subpaths(subpaths);
|
||||||
return Ok((candidate, others));
|
closed_subpaths.iter().any(|sp| !sp.is_closed())
|
||||||
}
|
|
||||||
|
|
||||||
let mut merged = candidate.clone();
|
|
||||||
let mut other_without_merged = vec![];
|
|
||||||
let mut merged_any = false;
|
|
||||||
|
|
||||||
for subpath in others {
|
|
||||||
// Only merge if the candidate is not already closed and the subpath can be meaningfully connected
|
|
||||||
if !merged.is_closed() && !subpath.is_closed() {
|
|
||||||
if merged.ends_in(subpath.start()) {
|
|
||||||
if let Ok(new_merged) = Subpath::try_from((&merged, &subpath)) {
|
|
||||||
merged = new_merged;
|
|
||||||
merged_any = true;
|
|
||||||
} else {
|
|
||||||
other_without_merged.push(subpath);
|
|
||||||
}
|
|
||||||
} else if merged.starts_in(subpath.end()) {
|
|
||||||
if let Ok(new_merged) = Subpath::try_from((&subpath, &merged)) {
|
|
||||||
merged = new_merged;
|
|
||||||
merged_any = true;
|
|
||||||
} else {
|
|
||||||
other_without_merged.push(subpath);
|
|
||||||
}
|
|
||||||
} else if merged.ends_in(subpath.end()) {
|
|
||||||
if let Ok(new_merged) = Subpath::try_from((&merged, &subpath.reversed())) {
|
|
||||||
merged = new_merged;
|
|
||||||
merged_any = true;
|
|
||||||
} else {
|
|
||||||
other_without_merged.push(subpath);
|
|
||||||
}
|
|
||||||
} else if merged.starts_in(subpath.start()) {
|
|
||||||
if let Ok(new_merged) = Subpath::try_from((&subpath.reversed(), &merged)) {
|
|
||||||
merged = new_merged;
|
|
||||||
merged_any = true;
|
|
||||||
} else {
|
|
||||||
other_without_merged.push(subpath);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
other_without_merged.push(subpath);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If either subpath is closed, don't merge
|
|
||||||
other_without_merged.push(subpath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we tried to merge but failed to close, force close the merged subpath
|
|
||||||
if !merged.is_closed() && merged_any {
|
|
||||||
let mut closed_segments = merged.segments.clone();
|
|
||||||
if let Some(Segment::MoveTo(start)) = closed_segments.first() {
|
|
||||||
closed_segments.push(Segment::LineTo(*start));
|
|
||||||
closed_segments.push(Segment::Close);
|
|
||||||
}
|
|
||||||
merged = Subpath::new(closed_segments);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((merged, other_without_merged))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Searches a path for potential subpaths that can be closed and merges them
|
|
||||||
fn closed_subpaths(
|
|
||||||
current: &Subpath,
|
|
||||||
others: &[Subpath],
|
|
||||||
partial: &[Subpath],
|
|
||||||
) -> Result<Vec<Subpath>> {
|
|
||||||
let mut result = partial.to_vec();
|
|
||||||
|
|
||||||
let (new_current, new_others) = if current.is_closed() {
|
|
||||||
(current.clone(), others.to_vec())
|
|
||||||
} else {
|
|
||||||
merge_paths(current.clone(), others.to_vec())?
|
|
||||||
};
|
|
||||||
|
|
||||||
// we haven't found any matching subpaths -> advance
|
|
||||||
if new_current == *current {
|
|
||||||
result.push(current.clone());
|
|
||||||
if new_others.is_empty() {
|
|
||||||
return Ok(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
closed_subpaths(&new_others[0], &new_others[1..], &result)
|
|
||||||
}
|
|
||||||
// if diffrent, we have to search again with the merged subpaths
|
|
||||||
else {
|
|
||||||
closed_subpaths(&new_current, &new_others, &result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_open_path(segments: &[Segment]) -> Result<bool> {
|
|
||||||
let subpaths = get_subpaths(segments);
|
|
||||||
let closed_subpaths = if subpaths.len() > 1 {
|
|
||||||
closed_subpaths(&subpaths[0], &subpaths[1..], &[])?
|
|
||||||
} else {
|
|
||||||
subpaths
|
|
||||||
};
|
|
||||||
|
|
||||||
// return true if any subpath is open
|
|
||||||
Ok(closed_subpaths.iter().any(|subpath| !subpath.is_closed()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -266,8 +222,7 @@ mod tests {
|
|||||||
Segment::Close,
|
Segment::Close,
|
||||||
];
|
];
|
||||||
|
|
||||||
let result =
|
let result = subpaths::is_open_path(&segments);
|
||||||
subpaths::is_open_path(&segments).expect("Failed to determine if path is open");
|
|
||||||
assert!(result, "Path should be open");
|
assert!(result, "Path should be open");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,8 +235,7 @@ mod tests {
|
|||||||
Segment::LineTo((223.0, 582.0)),
|
Segment::LineTo((223.0, 582.0)),
|
||||||
];
|
];
|
||||||
|
|
||||||
let result =
|
let result = subpaths::is_open_path(&segments);
|
||||||
subpaths::is_open_path(&segments).expect("Failed to determine if path is open");
|
|
||||||
assert!(!result, "Path should be closed");
|
assert!(!result, "Path should be closed");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,16 +285,14 @@ mod tests {
|
|||||||
Segment::LineTo((400.1158, 610.0)),
|
Segment::LineTo((400.1158, 610.0)),
|
||||||
];
|
];
|
||||||
|
|
||||||
let result =
|
let result = subpaths::is_open_path(&segments);
|
||||||
subpaths::is_open_path(&segments).expect("Failed to determine if path is open");
|
|
||||||
assert!(result, "Path should be open");
|
assert!(result, "Path should be open");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_is_open_path_4() {
|
fn test_is_open_path_4() {
|
||||||
let segments = vec![];
|
let segments = vec![];
|
||||||
let result =
|
let result = subpaths::is_open_path(&segments);
|
||||||
subpaths::is_open_path(&segments).expect("Failed to determine if path is open");
|
|
||||||
assert!(!result, "Path should be closed");
|
assert!(!result, "Path should be closed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
use macros::ToJs;
|
use macros::ToJs;
|
||||||
use mem::SerializableResult;
|
use mem::SerializableResult;
|
||||||
use std::mem::size_of;
|
use std::mem::size_of;
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
|
||||||
use crate::shapes::{Path, Segment, ToPath};
|
use crate::shapes::{Path, Segment, ToPath};
|
||||||
use crate::{mem, with_current_shape, with_current_shape_mut, STATE};
|
use crate::{mem, with_current_shape, with_current_shape_mut, STATE};
|
||||||
@@ -151,17 +152,59 @@ impl From<Vec<RawSegmentData>> for Path {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PATH_UPLOAD_BUFFER: OnceLock<Mutex<Vec<u8>>> = OnceLock::new();
|
||||||
|
|
||||||
|
fn get_path_upload_buffer() -> &'static Mutex<Vec<u8>> {
|
||||||
|
PATH_UPLOAD_BUFFER.get_or_init(|| Mutex::new(Vec::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn start_shape_path_buffer() {
|
||||||
|
let buffer = get_path_upload_buffer();
|
||||||
|
let mut buffer = buffer.lock().unwrap();
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn set_shape_path_chunk_buffer() {
|
||||||
|
let bytes = mem::bytes();
|
||||||
|
let buffer = get_path_upload_buffer();
|
||||||
|
let mut buffer = buffer.lock().unwrap();
|
||||||
|
buffer.extend_from_slice(&bytes);
|
||||||
|
mem::free_bytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn set_shape_path_buffer() {
|
||||||
|
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||||
|
let buffer = get_path_upload_buffer();
|
||||||
|
let mut buffer = buffer.lock().unwrap();
|
||||||
|
let chunk_size = size_of::<RawSegmentData>();
|
||||||
|
if buffer.len() % chunk_size != 0 {
|
||||||
|
// FIXME
|
||||||
|
println!("Warning: buffer length is not a multiple of chunk size!");
|
||||||
|
}
|
||||||
|
let mut segments = Vec::new();
|
||||||
|
for (i, chunk) in buffer.chunks(chunk_size).enumerate() {
|
||||||
|
match RawSegmentData::try_from(chunk) {
|
||||||
|
Ok(seg) => segments.push(Segment::from(seg)),
|
||||||
|
Err(e) => println!("Error at segment {}: {}", i, e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shape.set_path_segments(segments);
|
||||||
|
buffer.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_shape_path_content() {
|
pub extern "C" fn set_shape_path_content() {
|
||||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||||
let bytes = mem::bytes();
|
let bytes = mem::bytes();
|
||||||
|
|
||||||
let segments = bytes
|
let segments = bytes
|
||||||
.chunks(size_of::<RawSegmentData>())
|
.chunks(size_of::<RawSegmentData>())
|
||||||
.map(|chunk| RawSegmentData::try_from(chunk).expect("Invalid path data"))
|
.map(|chunk| RawSegmentData::try_from(chunk).expect("Invalid path data"))
|
||||||
.map(Segment::from)
|
.map(Segment::from)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
shape.set_path_segments(segments);
|
shape.set_path_segments(segments);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user