From f5c91c3a0f4fbf3f276073acd83ef539b098f449 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Wed, 1 Nov 2023 17:14:04 +0530 Subject: [PATCH] feat: support frames via programmatic API (#7205) * update frame id post generation * support frames via programmatic API * fix types * add test for frames * throw error when element doesn't exist * naming tweaks * update the api to use children * consider max of frame dimensions and calculated bounds of elements * consider bound elements in frame api --- src/data/__snapshots__/transform.test.ts.snap | 1410 ++++++++++++++--- src/data/transform.test.ts | 84 + src/data/transform.ts | 78 +- src/element/newElement.ts | 6 +- .../excalidraw/example/initialData.tsx | 7 + 5 files changed, 1398 insertions(+), 187 deletions(-) diff --git a/src/data/__snapshots__/transform.test.ts.snap b/src/data/__snapshots__/transform.test.ts.snap index ae846bdc..d47f4369 100644 --- a/src/data/__snapshots__/transform.test.ts.snap +++ b/src/data/__snapshots__/transform.test.ts.snap @@ -1,16 +1,151 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`Test Transform > Test Frames > should transform frames and update frame ids when regenerated 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "solid", + "frameId": "id33", + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 100, + "x": 10, + "y": 10, +} +`; + +exports[`Test Transform > Test Frames > should transform frames and update frame ids when regenerated 2`] = ` +{ + "angle": 0, + "backgroundColor": "#fff3bf", + "boundElements": [ + { + "id": "id34", + "type": "text", + }, + ], + "fillStyle": "solid", + "frameId": "id33", + "groupIds": [], + "height": 96, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "diamond", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 340, + "x": 120, + "y": 20, +} +`; + +exports[`Test Transform > Test Frames > should transform frames and update frame ids when regenerated 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 126, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "name": "My frame", + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "frame", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 470, + "x": 0, + "y": 0, +} +`; + +exports[`Test Transform > Test Frames > should transform frames and update frame ids when regenerated 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "2", + "fillStyle": "solid", + "fontFamily": 1, + "fontSize": 30, + "frameId": "id33", + "groupIds": [], + "height": 37.5, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "HELLO EXCALIDRAW", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#099268", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "HELLO EXCALIDRAW", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 160, + "x": 210, + "y": 49.25, +} +`; + exports[`Test Transform > Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 1`] = ` { "angle": 0, "backgroundColor": "#d8f5a2", "boundElements": [ { - "id": "id41", + "id": "id45", "type": "arrow", }, { - "id": "id42", + "id": "id46", "type": "arrow", }, ], @@ -45,7 +180,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "backgroundColor": "transparent", "boundElements": [ { - "id": "id42", + "id": "id46", "type": "arrow", }, ], @@ -110,7 +245,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "seed": Any, "startArrowhead": null, "startBinding": { - "elementId": "id43", + "elementId": "id47", "focus": -0.08139534883720931, "gap": 1, }, @@ -186,7 +321,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "backgroundColor": "transparent", "boundElements": [ { - "id": "id41", + "id": "id45", "type": "arrow", }, ], @@ -222,7 +357,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "baseline": 0, "boundElements": [ { - "id": "id44", + "id": "id48", "type": "arrow", }, ], @@ -266,7 +401,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "baseline": 0, "boundElements": [ { - "id": "id44", + "id": "id48", "type": "arrow", }, ], @@ -309,7 +444,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "backgroundColor": "transparent", "boundElements": [ { - "id": "id45", + "id": "id49", "type": "text", }, ], @@ -367,7 +502,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "backgroundColor": "transparent", "baseline": 0, "boundElements": null, - "containerId": "id44", + "containerId": "id48", "fillStyle": "solid", "fontFamily": 1, "fontSize": 20, @@ -401,173 +536,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t `; exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 1`] = ` -{ - "angle": 0, - "backgroundColor": "transparent", - "boundElements": [ - { - "id": "id34", - "type": "text", - }, - ], - "endArrowhead": "arrow", - "endBinding": { - "elementId": "id36", - "focus": 0, - "gap": 1, - }, - "fillStyle": "solid", - "frameId": null, - "groupIds": [], - "height": 0, - "id": Any, - "isDeleted": false, - "lastCommittedPoint": null, - "link": null, - "locked": false, - "opacity": 100, - "points": [ - [ - 0.5, - 0, - ], - [ - 99.5, - 0, - ], - ], - "roughness": 1, - "roundness": null, - "seed": Any, - "startArrowhead": null, - "startBinding": { - "elementId": "id35", - "focus": 0, - "gap": 1, - }, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 2, - "type": "arrow", - "updated": 1, - "version": 3, - "versionNonce": Any, - "width": 100, - "x": 255, - "y": 239, -} -`; - -exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 2`] = ` -{ - "angle": 0, - "backgroundColor": "transparent", - "baseline": 0, - "boundElements": null, - "containerId": "id33", - "fillStyle": "solid", - "fontFamily": 1, - "fontSize": 20, - "frameId": null, - "groupIds": [], - "height": 25, - "id": Any, - "isDeleted": false, - "lineHeight": 1.25, - "link": null, - "locked": false, - "opacity": 100, - "originalText": "HELLO WORLD!!", - "roughness": 1, - "roundness": null, - "seed": Any, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 2, - "text": "HELLO WORLD!!", - "textAlign": "center", - "type": "text", - "updated": 1, - "version": 2, - "versionNonce": Any, - "verticalAlign": "middle", - "width": 130, - "x": 240, - "y": 226.5, -} -`; - -exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 3`] = ` -{ - "angle": 0, - "backgroundColor": "transparent", - "boundElements": [ - { - "id": "id33", - "type": "arrow", - }, - ], - "fillStyle": "solid", - "frameId": null, - "groupIds": [], - "height": 100, - "id": Any, - "isDeleted": false, - "link": null, - "locked": false, - "opacity": 100, - "roughness": 1, - "roundness": null, - "seed": Any, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 2, - "type": "rectangle", - "updated": 1, - "version": 2, - "versionNonce": Any, - "width": 100, - "x": 155, - "y": 189, -} -`; - -exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 4`] = ` -{ - "angle": 0, - "backgroundColor": "transparent", - "boundElements": [ - { - "id": "id33", - "type": "arrow", - }, - ], - "fillStyle": "solid", - "frameId": null, - "groupIds": [], - "height": 100, - "id": Any, - "isDeleted": false, - "link": null, - "locked": false, - "opacity": 100, - "roughness": 1, - "roundness": null, - "seed": Any, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 2, - "type": "ellipse", - "updated": 1, - "version": 2, - "versionNonce": Any, - "width": 100, - "x": 355, - "y": 189, -} -`; - -exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 1`] = ` { "angle": 0, "backgroundColor": "transparent", @@ -625,7 +593,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when } `; -exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 2`] = ` +exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 2`] = ` { "angle": 0, "backgroundColor": "transparent", @@ -664,6 +632,173 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when } `; +exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id37", + "type": "arrow", + }, + ], + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 100, + "x": 155, + "y": 189, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id37", + "type": "arrow", + }, + ], + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "ellipse", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 100, + "x": 355, + "y": 189, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id42", + "type": "text", + }, + ], + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id44", + "focus": 0, + "gap": 1, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0.5, + 0, + ], + [ + 99.5, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": { + "elementId": "id43", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 100, + "x": 255, + "y": 239, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id41", + "fillStyle": "solid", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "HELLO WORLD!!", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "HELLO WORLD!!", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 130, + "x": 240, + "y": 226.5, +} +`; + exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 3`] = ` { "angle": 0, @@ -671,7 +806,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "baseline": 0, "boundElements": [ { - "id": "id37", + "id": "id41", "type": "arrow", }, ], @@ -715,7 +850,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "baseline": 0, "boundElements": [ { - "id": "id37", + "id": "id41", "type": "arrow", }, ], @@ -782,6 +917,141 @@ exports[`Test Transform > should not allow duplicate ids 1`] = ` } `; +exports[`Test Transform > should transform frames and update frame ids when regenerated 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "solid", + "frameId": "id33", + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 100, + "x": 10, + "y": 10, +} +`; + +exports[`Test Transform > should transform frames and update frame ids when regenerated 2`] = ` +{ + "angle": 0, + "backgroundColor": "#fff3bf", + "boundElements": [ + { + "id": "id34", + "type": "text", + }, + ], + "fillStyle": "solid", + "frameId": "id33", + "groupIds": [], + "height": 96, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "diamond", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 340, + "x": 120, + "y": 20, +} +`; + +exports[`Test Transform > should transform frames and update frame ids when regenerated 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 126, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "name": "My frame", + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "frame", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 470, + "x": 0, + "y": 0, +} +`; + +exports[`Test Transform > should transform frames and update frame ids when regenerated 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "2", + "fillStyle": "solid", + "fontFamily": 1, + "fontSize": 30, + "frameId": "id33", + "groupIds": [], + "height": 37.5, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "HELLO EXCALIDRAW", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#099268", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "HELLO EXCALIDRAW", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 160, + "x": 210, + "y": 49.25, +} +`; + exports[`Test Transform > should transform linear elements 1`] = ` { "angle": 0, @@ -2030,3 +2300,785 @@ CONTAINER", "y": 522.5735931288071, } `; + +exports[`Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 1`] = ` +{ + "angle": 0, + "backgroundColor": "#d8f5a2", + "boundElements": [ + { + "id": "id43", + "type": "arrow", + }, + { + "id": "id44", + "type": "arrow", + }, + ], + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 300, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#66a80f", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "ellipse", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 300, + "x": 630, + "y": 316, +} +`; + +exports[`Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id44", + "type": "arrow", + }, + ], + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#9c36b5", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "diamond", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 140, + "x": 96, + "y": 400, +} +`; + +exports[`Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "endArrowhead": "arrow", + "endBinding": { + "elementId": "ellipse-1", + "focus": -0.008153707962747813, + "gap": 1, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 35, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0.5, + 0.5, + ], + [ + 394.5, + 34.5, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": { + "elementId": "id45", + "focus": -0.08139534883720931, + "gap": 1, + }, + "strokeColor": "#1864ab", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 395, + "x": 247, + "y": 420, +} +`; + +exports[`Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "endArrowhead": "arrow", + "endBinding": { + "elementId": "ellipse-1", + "focus": 0.10666666666666667, + "gap": 3.834326468444573, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0.5, + 0, + ], + [ + 399.5, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": { + "elementId": "diamond-1", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#e67700", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 400, + "x": 227, + "y": 450, +} +`; + +exports[`Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 5`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id43", + "type": "arrow", + }, + ], + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 300, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 300, + "x": -53, + "y": 270, +} +`; + +exports[`Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": [ + { + "id": "id46", + "type": "arrow", + }, + ], + "containerId": null, + "fillStyle": "solid", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "HEYYYYY", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#c2255c", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "HEYYYYY", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "top", + "width": 70, + "x": 185, + "y": 226.5, +} +`; + +exports[`Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": [ + { + "id": "id46", + "type": "arrow", + }, + ], + "containerId": null, + "fillStyle": "solid", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "Whats up ?", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "Whats up ?", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "top", + "width": 100, + "x": 560, + "y": 226.5, +} +`; + +exports[`Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id47", + "type": "text", + }, + ], + "endArrowhead": "arrow", + "endBinding": { + "elementId": "text-2", + "focus": 0, + "gap": 205, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0.5, + 0, + ], + [ + 99.5, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": { + "elementId": "text-1", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 100, + "x": 255, + "y": 239, +} +`; + +exports[`Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id46", + "fillStyle": "solid", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "HELLO WORLD!!", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "HELLO WORLD!!", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 130, + "x": 240, + "y": 226.5, +} +`; + +exports[`Test arrow bindings > should bind arrows to shapes when start / end provided without ids 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id36", + "type": "text", + }, + ], + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id38", + "focus": 0, + "gap": 1, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0.5, + 0, + ], + [ + 99.5, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": { + "elementId": "id37", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 100, + "x": 255, + "y": 239, +} +`; + +exports[`Test arrow bindings > should bind arrows to shapes when start / end provided without ids 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id35", + "fillStyle": "solid", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "HELLO WORLD!!", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "HELLO WORLD!!", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 130, + "x": 240, + "y": 226.5, +} +`; + +exports[`Test arrow bindings > should bind arrows to shapes when start / end provided without ids 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id35", + "type": "arrow", + }, + ], + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 100, + "x": 155, + "y": 189, +} +`; + +exports[`Test arrow bindings > should bind arrows to shapes when start / end provided without ids 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id35", + "type": "arrow", + }, + ], + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "ellipse", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 100, + "x": 355, + "y": 189, +} +`; + +exports[`Test arrow bindings > should bind arrows to text when start / end provided without ids 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id40", + "type": "text", + }, + ], + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id42", + "focus": 0, + "gap": 1, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0.5, + 0, + ], + [ + 99.5, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": { + "elementId": "id41", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 100, + "x": 255, + "y": 239, +} +`; + +exports[`Test arrow bindings > should bind arrows to text when start / end provided without ids 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id39", + "fillStyle": "solid", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "HELLO WORLD!!", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "HELLO WORLD!!", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 130, + "x": 240, + "y": 226.5, +} +`; + +exports[`Test arrow bindings > should bind arrows to text when start / end provided without ids 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": [ + { + "id": "id39", + "type": "arrow", + }, + ], + "containerId": null, + "fillStyle": "solid", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "HEYYYYY", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "HEYYYYY", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "top", + "width": 70, + "x": 185, + "y": 226.5, +} +`; + +exports[`Test arrow bindings > should bind arrows to text when start / end provided without ids 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": [ + { + "id": "id39", + "type": "arrow", + }, + ], + "containerId": null, + "fillStyle": "solid", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "WHATS UP ?", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "WHATS UP ?", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "top", + "width": 100, + "x": 355, + "y": 226.5, +} +`; + +exports[`should not allow duplicate ids 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 200, + "id": "rect-1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 100, + "x": 300, + "y": 100, +} +`; diff --git a/src/data/transform.test.ts b/src/data/transform.test.ts index b6abfb37..7c71f33f 100644 --- a/src/data/transform.test.ts +++ b/src/data/transform.test.ts @@ -309,6 +309,90 @@ describe("Test Transform", () => { }); }); + describe("Test Frames", () => { + it("should transform frames and update frame ids when regenerated", () => { + const elementsSkeleton: ExcalidrawElementSkeleton[] = [ + { + type: "rectangle", + x: 10, + y: 10, + strokeWidth: 2, + id: "1", + }, + { + type: "diamond", + x: 120, + y: 20, + backgroundColor: "#fff3bf", + strokeWidth: 2, + label: { + text: "HELLO EXCALIDRAW", + strokeColor: "#099268", + fontSize: 30, + }, + id: "2", + }, + { + type: "frame", + children: ["1", "2"], + name: "My frame", + }, + ]; + const excaldrawElements = convertToExcalidrawElements( + elementsSkeleton, + opts, + ); + expect(excaldrawElements.length).toBe(4); + + excaldrawElements.forEach((ele) => { + expect(ele).toMatchObject({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + id: expect.any(String), + }); + }); + }); + + it("should consider max of calculated and frame dimensions when provided", () => { + const elementsSkeleton: ExcalidrawElementSkeleton[] = [ + { + type: "rectangle", + x: 10, + y: 10, + strokeWidth: 2, + id: "1", + }, + { + type: "diamond", + x: 120, + y: 20, + backgroundColor: "#fff3bf", + strokeWidth: 2, + label: { + text: "HELLO EXCALIDRAW", + strokeColor: "#099268", + fontSize: 30, + }, + id: "2", + }, + { + type: "frame", + children: ["1", "2"], + name: "My frame", + width: 800, + height: 100, + }, + ]; + const excaldrawElements = convertToExcalidrawElements( + elementsSkeleton, + opts, + ); + const frame = excaldrawElements.find((ele) => ele.type === "frame")!; + expect(frame.width).toBe(800); + expect(frame.height).toBe(126); + }); + }); + describe("Test arrow bindings", () => { it("should bind arrows to shapes when start / end provided without ids", () => { const elements = [ diff --git a/src/data/transform.ts b/src/data/transform.ts index a34ea1a6..10208261 100644 --- a/src/data/transform.ts +++ b/src/data/transform.ts @@ -5,6 +5,7 @@ import { VERTICAL_ALIGN, } from "../constants"; import { + getCommonBounds, newElement, newLinearElement, redrawTextBoundingBox, @@ -12,6 +13,7 @@ import { import { bindLinearElement } from "../element/binding"; import { ElementConstructorOpts, + newFrameElement, newImageElement, newTextElement, } from "../element/newElement"; @@ -135,9 +137,7 @@ export type ValidContainer = export type ExcalidrawElementSkeleton = | Extract< Exclude, - | ExcalidrawEmbeddableElement - | ExcalidrawFreeDrawElement - | ExcalidrawFrameElement + ExcalidrawEmbeddableElement | ExcalidrawFreeDrawElement > | ({ type: Extract; @@ -158,7 +158,12 @@ export type ExcalidrawElementSkeleton = x: number; y: number; fileId: FileId; - } & Partial); + } & Partial) + | ({ + type: "frame"; + children: readonly ExcalidrawElement["id"][]; + name?: string; + } & Partial); const DEFAULT_LINEAR_ELEMENT_PROPS = { width: 100, @@ -437,7 +442,6 @@ export const convertToExcalidrawElements = ( const elements: ExcalidrawElementSkeleton[] = JSON.parse( JSON.stringify(elementsSkeleton), ); - const elementStore = new ElementStore(); const elementsWithIds = new Map(); const oldToNewElementIdMap = new Map(); @@ -536,8 +540,15 @@ export const convertToExcalidrawElements = ( break; } + case "frame": { + excalidrawElement = newFrameElement({ + x: 0, + y: 0, + ...element, + }); + break; + } case "freedraw": - case "frame": case "embeddable": { excalidrawElement = element; break; @@ -641,5 +652,60 @@ export const convertToExcalidrawElements = ( } } } + + // Once all the excalidraw elements are created, we can add frames since we + // need to calculate coordinates and dimensions of frame which is possibe after all + // frame children are processed. + for (const [id, element] of elementsWithIds) { + if (element.type !== "frame") { + continue; + } + const frame = elementStore.getElement(id); + + if (!frame) { + throw new Error(`Excalidraw element with id ${id} doesn't exist`); + } + const childrenElements: ExcalidrawElement[] = []; + + element.children.forEach((id) => { + const newElementId = oldToNewElementIdMap.get(id); + if (!newElementId) { + throw new Error(`Element with ${id} wasn't mapped correctly`); + } + + const elementInFrame = elementStore.getElement(newElementId); + if (!elementInFrame) { + throw new Error(`Frame element with id ${newElementId} doesn't exist`); + } + Object.assign(elementInFrame, { frameId: frame.id }); + + elementInFrame?.boundElements?.forEach((boundElement) => { + const ele = elementStore.getElement(boundElement.id); + if (!ele) { + throw new Error( + `Bound element with id ${boundElement.id} doesn't exist`, + ); + } + Object.assign(ele, { frameId: frame.id }); + childrenElements.push(ele); + }); + + childrenElements.push(elementInFrame); + }); + + let [minX, minY, maxX, maxY] = getCommonBounds(childrenElements); + + const PADDING = 10; + minX = minX - PADDING; + minY = minY - PADDING; + maxX = maxX + PADDING; + maxY = maxY + PADDING; + + // Take the max of calculated and provided frame dimensions, whichever is higher + const width = Math.max(frame?.width, maxX - minX); + const height = Math.max(frame?.height, maxY - minY); + Object.assign(frame, { x: minX, y: minY, width, height }); + } + return elementStore.getElements(); }; diff --git a/src/element/newElement.ts b/src/element/newElement.ts index bbc98030..0ffd9df5 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -144,13 +144,15 @@ export const newEmbeddableElement = ( }; export const newFrameElement = ( - opts: ElementConstructorOpts, + opts: { + name?: string; + } & ElementConstructorOpts, ): NonDeleted => { const frameElement = newElementWith( { ..._newElementBase("frame", opts), type: "frame", - name: null, + name: opts?.name || null, }, {}, ); diff --git a/src/packages/excalidraw/example/initialData.tsx b/src/packages/excalidraw/example/initialData.tsx index 92c7205d..cca343ae 100644 --- a/src/packages/excalidraw/example/initialData.tsx +++ b/src/packages/excalidraw/example/initialData.tsx @@ -7,6 +7,7 @@ const elements: ExcalidrawElementSkeleton[] = [ x: 10, y: 10, strokeWidth: 2, + id: "1", }, { type: "diamond", @@ -19,6 +20,7 @@ const elements: ExcalidrawElementSkeleton[] = [ strokeColor: "#099268", fontSize: 30, }, + id: "2", }, { type: "arrow", @@ -36,6 +38,11 @@ const elements: ExcalidrawElementSkeleton[] = [ height: 230, fileId: "rocket" as FileId, }, + { + type: "frame", + children: ["1", "2"], + name: "My frame", + }, ]; export default { elements,