/** * Test case: * * create a few random elements on canvas * creates a lasso path for each of these cases * - do not intersect / enclose at all * - intersects some, does not enclose/intersect the rest * - intersects and encloses some * - single linear element should be selected if lasso intersects/encloses it * * * special cases: * - selects only frame if frame and children both selected by lasso * - selects group if any group from group is selected */ import { type GlobalPoint, type LocalPoint, pointFrom, type Radians, type ElementsSegmentsMap, } from "@excalidraw/math"; import { getElementLineSegments } from "@excalidraw/element"; import type { ExcalidrawElement } from "@excalidraw/element/types"; import { Excalidraw } from "../index"; import { getSelectedElements } from "../scene"; import { getLassoSelectedElementIds } from "../lasso/utils"; import { API } from "./helpers/api"; import { act, render } from "./test-utils"; const { h } = window; beforeEach(async () => { localStorage.clear(); await render(); h.state.width = 1000; h.state.height = 1000; }); const updatePath = (startPoint: GlobalPoint, points: LocalPoint[]) => { act(() => { h.app.lassoTrail.startPath(startPoint[0], startPoint[1]); points.forEach((point) => { h.app.lassoTrail.addPointToPath( startPoint[0] + point[0], startPoint[1] + point[1], ); }); const elementsSegments: ElementsSegmentsMap = new Map(); for (const element of h.elements) { const segments = getElementLineSegments( element, h.app.scene.getElementsMapIncludingDeleted(), ); elementsSegments.set(element.id, segments); } const result = getLassoSelectedElementIds({ lassoPath: h.app.lassoTrail .getCurrentTrail() ?.originalPoints?.map((p) => pointFrom(p[0], p[1])) ?? [], elements: h.elements, elementsMap: h.scene.getNonDeletedElementsMap(), elementsSegments, intersectedElements: new Set(), enclosedElements: new Set(), mode: h.state.boxSelectionMode, }); act(() => h.app.lassoTrail.selectElementsFromIds(result.selectedElementIds), ); h.app.lassoTrail.endPath(); }); }; describe("Basic lasso selection tests", () => { beforeEach(() => { const elements: ExcalidrawElement[] = [ { id: "FLZN67ISZbMV-RH8SzS9W", type: "rectangle", x: 0, y: 0, width: 107.11328125, height: 90.16015625, angle: 5.40271241072378, strokeColor: "#1e1e1e", backgroundColor: "transparent", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, groupIds: [], frameId: null, index: "a8", roundness: { type: 3, }, seed: 1558764732, version: 43, versionNonce: 575357188, isDeleted: false, boundElements: [], updated: 1740723127946, link: null, locked: false, }, { id: "T3TSAFUwp--pT2b_q7Y5U", type: "diamond", x: 349.822265625, y: -201.244140625, width: 123.3828125, height: 74.66796875, angle: 0.6498998717212414, strokeColor: "#1e1e1e", backgroundColor: "transparent", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, groupIds: [], frameId: null, index: "a9", roundness: { type: 2, }, seed: 1720937276, version: 69, versionNonce: 1991578556, isDeleted: false, boundElements: [], updated: 1740723132096, link: null, locked: false, }, { id: "a9RZwSeqlZHyhses2iYZ0", type: "ellipse", x: 188.259765625, y: -48.193359375, width: 146.8984375, height: 91.01171875, angle: 0.6070652964532064, strokeColor: "#1e1e1e", backgroundColor: "transparent", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, groupIds: [], frameId: null, index: "aA", roundness: { type: 2, }, seed: 476696636, version: 38, versionNonce: 1903760444, isDeleted: false, boundElements: [], updated: 1740723125079, link: null, locked: false, }, { id: "vCw17KEn9h4sY2KMdnq0G", type: "arrow", x: -257.388671875, y: 78.583984375, width: 168.4765625, height: 153.38671875, angle: 0, strokeColor: "#1e1e1e", backgroundColor: "transparent", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, groupIds: [], frameId: null, index: "aB", roundness: { type: 2, }, seed: 1302309508, version: 19, versionNonce: 1230691388, isDeleted: false, boundElements: [], updated: 1740723110578, link: null, locked: false, points: [ [0, 0], [168.4765625, -153.38671875], ], startBinding: null, endBinding: null, startArrowhead: null, endArrowhead: "arrow", elbowed: false, }, { id: "dMsLoKhGsWQXpiKGWZ6Cn", type: "line", x: -113.748046875, y: -165.224609375, width: 206.12890625, height: 35.4140625, angle: 0, strokeColor: "#1e1e1e", backgroundColor: "transparent", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, groupIds: [], frameId: null, index: "aC", roundness: { type: 2, }, seed: 514585788, version: 18, versionNonce: 1338507580, isDeleted: false, boundElements: [], updated: 1740723112995, link: null, locked: false, points: [ [0, 0], [206.12890625, 35.4140625], ], startBinding: null, endBinding: null, startArrowhead: null, endArrowhead: null, }, { id: "1GUDjUg8ibE_4qMFtdQiK", type: "freedraw", x: 384.404296875, y: 91.580078125, width: 537.55078125, height: 288.48046875, angle: 5.5342222396022285, strokeColor: "#1e1e1e", backgroundColor: "transparent", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, groupIds: [], frameId: null, index: "aD", roundness: null, seed: 103578044, version: 167, versionNonce: 1117299588, isDeleted: false, boundElements: [], updated: 1740723137180, link: null, locked: false, points: [ [0, 0], [-0.10546875, 0], [-3.23046875, -0.859375], [-18.09765625, -4.6953125], [-54.40625, -13.765625], [-103.48046875, -23.05859375], [-155.6640625, -27.5390625], [-205.5703125, -27.96484375], [-239, -24.4765625], [-257.27734375, -17.0390625], [-270.1015625, -5.43359375], [-279.94140625, 12.12109375], [-286.828125, 36.6875], [-291.03515625, 65.63671875], [-292.5546875, 94.96875], [-291.8203125, 122.1875], [-286.140625, 144.703125], [-274.60546875, 160.01953125], [-257.1171875, 170.375], [-237.7890625, 176.1953125], [-218.85546875, 178.69921875], [-199.33984375, 181.56640625], [-182.4609375, 188.4765625], [-168.97265625, 200.14453125], [-160.83984375, 211.1875], [-156.40234375, 220.0703125], [-153.60546875, 226.12890625], [-151.3203125, 229.30078125], [-146.28125, 231.7421875], [-136.140625, 233.30859375], [-122.1953125, 233.80078125], [-108.66015625, 234.23828125], [-97.0234375, 235.0546875], [-89.6171875, 235.7421875], [-85.84375, 237.52734375], [-82.546875, 240.41796875], [-79.64453125, 243.2734375], [-75.71875, 245.99609375], [-69.734375, 248.4453125], [-59.6640625, 250.87890625], [-45.1171875, 252.4453125], [-23.9453125, 251.7265625], [7.41796875, 244.0546875], [48.58203125, 223.734375], [93.5078125, 192.859375], [135.8359375, 153.9453125], [168.875, 114.015625], [186.5625, 86.640625], [194.9765625, 71.19140625], [199.0234375, 62.671875], [199.875, 59.6171875], [200.1796875, 58.72265625], [200.4140625, 58.62109375], [200.87109375, 58.57421875], [203.1796875, 58.2734375], [208.72265625, 55.671875], [216.421875, 50.89453125], [224.546875, 45.265625], [234.40625, 36.30859375], [241.71484375, 28.14453125], [243.6875, 24.1171875], [244.6171875, 21.34375], [244.99609375, 18.5625], [243.78515625, 12.41015625], [237.6328125, -4.8125], [222.91796875, -36.03515625], [222.91796875, -36.03515625], ], pressures: [], simulatePressure: true, }, ].map( (e) => ({ ...e, angle: e.angle as Radians, index: null, created: null, } as ExcalidrawElement), ); act(() => { h.elements = elements; h.app.setActiveTool({ type: "lasso" }); h.app.setState({ boxSelectionMode: "overlap" }); }); }); it("None should be selected", () => { const startPoint = pointFrom(-533, 611); const points = [ [0, 0], [0.1015625, -0.09765625], [10.16796875, -8.15625], [25.71484375, -18.5078125], [46.078125, -28.63671875], [90.578125, -41.9140625], [113.04296875, -45.0859375], [133.95703125, -46.2890625], [152.92578125, -46.2890625], [170.921875, -44.98828125], [190.1640625, -39.61328125], [213.73046875, -29], [238.859375, -16.59375], [261.87890625, -5.80078125], [281.63671875, 2.4453125], [300.125, 9.01953125], [320.09375, 14.046875], [339.140625, 16.95703125], [358.3203125, 18.41796875], [377.5234375, 17.890625], [396.45703125, 14.53515625], [416.4921875, 8.015625], [438.796875, -1.54296875], [461.6328125, -11.5703125], [483.36328125, -21.48828125], [503.37109375, -30.87109375], [517.0546875, -36.49609375], [525.62109375, -39.6640625], [531.45703125, -41.46875], [534.1328125, -41.9375], [535.32421875, -42.09375], [544.4140625, -42.09375], [567.2265625, -42.09375], [608.1875, -38.5625], [665.203125, -27.66796875], [725.8984375, -11.30078125], [785.05078125, 8.17578125], [832.12109375, 25.55078125], [861.62109375, 36.32421875], [881.91796875, 42.203125], [896.75, 45.125], [907.04296875, 46.46484375], [917.44921875, 46.42578125], [930.671875, 42.59765625], [945.953125, 34.66796875], [964.08984375, 22.43359375], [989.8125, 2.328125], [1014.6640625, -17.79296875], [1032.7734375, -32.70703125], [1045.984375, -43.9921875], [1052.48828125, -50.1875], [1054.97265625, -53.3046875], [1055.65234375, -54.38671875], [1060.48046875, -54.83984375], [1073.03125, -55.2734375], [1095.6484375, -54], [1125.41796875, -49.05859375], [1155.33984375, -41.21484375], [1182.33203125, -33.6875], [1204.1171875, -27.75390625], [1220.95703125, -23.58203125], [1235.390625, -21.06640625], [1248.078125, -19.3515625], [1257.78125, -18.6484375], [1265.6640625, -19.22265625], [1271.5703125, -20.42578125], [1276.046875, -21.984375], [1280.328125, -25.23828125], [1284.19140625, -29.953125], [1288.22265625, -35.8125], [1292.87109375, -43.21484375], [1296.6796875, -50.44921875], [1299.3828125, -56.40234375], [1301.48828125, -61.08203125], [1302.89453125, -64.75], [1303.890625, -67.37890625], [1304.41796875, -68.953125], [1304.65234375, -69.8046875], [1304.80078125, -70.2578125], [1304.80078125, -70.2578125], ] as LocalPoint[]; updatePath(startPoint, points); const selectedElements = getSelectedElements(h.elements, h.app.state); expect(selectedElements.length).toBe(0); }); it("Intersects some, does not enclose/intersect the rest", () => { const startPoint = pointFrom(-311, 50); const points = [ [0, 0], [0.1015625, 0], [3.40234375, -2.25390625], [12.25390625, -7.84375], [22.71484375, -13.89453125], [39.09765625, -22.3359375], [58.5546875, -31.9609375], [79.91796875, -41.21875], [90.53125, -44.76953125], [99.921875, -47.16796875], [107.46484375, -48.640625], [113.92578125, -49.65625], [119.57421875, -50.1953125], [124.640625, -50.1953125], [129.49609375, -50.1953125], [134.53125, -50.1953125], [140.59375, -50.1953125], [147.27734375, -49.87109375], [154.32421875, -48.453125], [160.93359375, -46.0390625], [166.58203125, -42.8828125], [172.0078125, -38.8671875], [176.75390625, -34.1015625], [180.41796875, -29.609375], [183.09375, -25.0390625], [185.11328125, -19.70703125], [186.8828125, -13.04296875], [188.515625, -6.39453125], [189.8515625, -1.04296875], [190.9609375, 4.34375], [191.9296875, 9.3125], [193.06640625, 13.73046875], [194.21875, 17.51953125], [195.32421875, 20.83984375], [196.5625, 23.4296875], [198.2109375, 25.5234375], [200.04296875, 27.38671875], [202.1640625, 28.80078125], [204.43359375, 30.33984375], [207.10546875, 31.7109375], [210.69921875, 33.1640625], [214.6015625, 34.48828125], [218.5390625, 35.18359375], [222.703125, 35.71875], [227.16015625, 35.98828125], [232.01171875, 35.98828125], [237.265625, 35.98828125], [242.59765625, 35.015625], [247.421875, 33.4140625], [251.61328125, 31.90625], [255.84375, 30.1328125], [260.25390625, 28.62109375], [264.44140625, 27.41796875], [268.5546875, 26.34765625], [272.6171875, 25.42578125], [276.72265625, 24.37890625], [281.234375, 23.140625], [286.69921875, 22.046875], [293.5859375, 20.82421875], [300.6328125, 19.4140625], [309.83984375, 18.1640625], [320.28125, 16.7578125], [329.46875, 15.91015625], [337.453125, 15.53515625], [344.515625, 14.8203125], [350.45703125, 14.4453125], [354.64453125, 14.5546875], [358.10546875, 14.921875], [360.83203125, 15.5234375], [362.796875, 16.3671875], [364.1328125, 17.43359375], [365.13671875, 18.6015625], [365.8984375, 19.8203125], [366.71484375, 21.30078125], [368.34375, 23.59765625], [370.37890625, 26.70703125], [372.15625, 30.5], [374.16015625, 34.390625], [376.21875, 38.4921875], [378.19140625, 43.921875], [380.4140625, 50.31640625], [382.671875, 56.2890625], [384.48046875, 61.34765625], [385.7890625, 65.14453125], [386.5390625, 66.98828125], [386.921875, 67.60546875], [387.171875, 67.80859375], [388.0390625, 68.32421875], [392.23828125, 70.3671875], [403.59765625, 76.4296875], [419.5390625, 85.5], [435.5078125, 93.82421875], [451.3046875, 101.015625], [465.05078125, 107.02734375], [476.828125, 111.97265625], [487.38671875, 115.578125], [495.98046875, 118.03125], [503.203125, 120.3515625], [510.375, 122.3828125], [517.8203125, 124.32421875], [525.38671875, 126.9375], [532.9765625, 130.12890625], [539.046875, 133.22265625], [543.85546875, 136.421875], [549.28125, 140.84375], [554.41015625, 146.04296875], [558.34375, 151.4921875], [561.859375, 157.09375], [564.734375, 162.71875], [566.95703125, 168.375], [568.87109375, 174.33984375], [570.41796875, 181.26953125], [571.74609375, 189.37890625], [572.55859375, 197.3515625], [573.046875, 204.26171875], [573.7421875, 210.9453125], [574.38671875, 216.91796875], [574.75, 222.8515625], [575.0703125, 228.78515625], [575.67578125, 234.0078125], [576.26171875, 238.3515625], [576.84765625, 242.64453125], [577.328125, 247.53125], [577.6484375, 252.56640625], [577.80859375, 257.91015625], [578.12890625, 263.2578125], [578.44921875, 269.1875], [578.16796875, 275.17578125], [577.5234375, 281.078125], [576.14453125, 287.59375], [574.19921875, 296.390625], [571.96484375, 306.03125], [568.765625, 315.54296875], [564.68359375, 325.640625], [560.3671875, 335.03125], [555.93359375, 343.68359375], [551.56640625, 352.03515625], [547.86328125, 359.2734375], [543.82421875, 365.2421875], [539.91015625, 370.0078125], [537.37109375, 372.5546875], [535.4765625, 374.23828125], [533.37890625, 375.5859375], [531.2578125, 376.75390625], [528.46875, 378.96875], [524.296875, 381.8359375], [519.03515625, 385.31640625], [513.50390625, 389.2890625], [506.43359375, 394.55078125], [497.18359375, 401.51953125], [488.43359375, 408.40625], [481.15234375, 414.0703125], [475.64453125, 417.7578125], [471.55078125, 420.32421875], [468.73828125, 421.828125], [467.1640625, 422.328125], [465.9296875, 422.6953125], [464.7109375, 422.91796875], [463.2734375, 423.12890625], [462.06640625, 423.33203125], [460.88671875, 423.33203125], [459.484375, 423.33203125], [458.57421875, 423.33203125], [457.9296875, 423.10546875], [457.15234375, 422.796875], [456.3984375, 422.5625], [455.8828125, 422.41015625], [455.55859375, 422.41015625], [455.453125, 422.3203125], [455.4453125, 422.06640625], [455.4453125, 422.06640625], ] as LocalPoint[]; updatePath(startPoint, points); const selectedElements = getSelectedElements(h.elements, h.state); expect(selectedElements.length).toBe(3); expect(selectedElements.filter((e) => e.type === "arrow").length).toBe(1); expect(selectedElements.filter((e) => e.type === "rectangle").length).toBe( 1, ); expect(selectedElements.filter((e) => e.type === "freedraw").length).toBe( 1, ); }); it("Intersects some and encloses some", () => { const startPoint = pointFrom(112, -190); const points = [ [0, 0], [-0.1015625, 0], [-6.265625, 3.09375], [-18.3671875, 9.015625], [-28.3125, 13.94921875], [-38.03125, 19.0625], [-52.578125, 28.72265625], [-54.51953125, 33.00390625], [-55.39453125, 36.07421875], [-56.046875, 39.890625], [-57.06640625, 45.2734375], [-57.76171875, 51.2265625], [-57.76171875, 56.16796875], [-57.76171875, 60.96875], [-57.76171875, 65.796875], [-57.76171875, 70.54296875], [-57.33203125, 75.21484375], [-56.17578125, 79.5078125], [-54.55078125, 83.5625], [-51.88671875, 88.09375], [-48.72265625, 92.46875], [-45.32421875, 96.2421875], [-41.62890625, 100.5859375], [-37.9375, 104.92578125], [-33.94921875, 108.91796875], [-29.703125, 113.51953125], [-24.45703125, 118.49609375], [-18.66796875, 123.5390625], [-12.7109375, 128.96484375], [-6.2578125, 133.984375], [0.203125, 138.5078125], [7.1640625, 143.71875], [16.08984375, 149.9765625], [25.01953125, 156.1640625], [33.8203125, 162.25], [42.05078125, 167.79296875], [48.75390625, 172.46484375], [55.3984375, 177.90625], [61.296875, 184.12890625], [66.02734375, 191.21484375], [69.765625, 198.109375], [73.03515625, 204.79296875], [76.09375, 212.26171875], [78.984375, 219.52734375], [81.58203125, 226.34765625], [84.1640625, 232.3046875], [86.7265625, 237.16796875], [89.68359375, 241.34765625], [93.83984375, 245.12890625], [100.12109375, 249.328125], [107.109375, 253.65625], [114.08203125, 257.89453125], [122.578125, 262.31640625], [130.83984375, 266.359375], [138.33203125, 269.8671875], [144.984375, 272.3515625], [150.265625, 274.1953125], [155.42578125, 275.9296875], [159.1328125, 276.73828125], [161.2421875, 276.73828125], [165.11328125, 276.7578125], [172.546875, 276.76171875], [183.14453125, 276.76171875], [194.015625, 276.76171875], [204.1796875, 276.76171875], [213.484375, 276.76171875], [221.40625, 276.76171875], [228.47265625, 276.76171875], [234.40234375, 276.67578125], [240.28515625, 275.9765625], [246.12109375, 274.59375], [250.75390625, 272.8515625], [255.046875, 270.18359375], [259.6328125, 266.60546875], [264.04296875, 262.4375], [268.69140625, 256.69921875], [273.25390625, 249.9375], [277.85546875, 243.0546875], [282.19140625, 236.5859375], [285.24609375, 231.484375], [287.39453125, 227.1875], [289.078125, 223.78125], [290.328125, 221.28125], [291.0390625, 219.2109375], [291.40625, 217.83984375], [291.546875, 216.75390625], [291.546875, 215.84375], [291.75390625, 214.7734375], [291.9609375, 213.15234375], [291.9609375, 211.125], [291.9609375, 208.6953125], [291.9609375, 205.25], [291.9609375, 201.4453125], [291.62890625, 197.68359375], [291.0625, 194.29296875], [290.6484375, 192.21875], [290.25390625, 190.8203125], [289.88671875, 189.94140625], [289.75, 189.53125], [289.75, 189.2109375], [289.7265625, 188.29296875], [290.09375, 186.3125], [293.04296875, 182.46875], [298.671875, 177.46484375], [305.45703125, 172.13671875], [312.4921875, 167.35546875], [318.640625, 163.6875], [323.1484375, 161.0703125], [326.484375, 159.37109375], [329.8046875, 157.39453125], [332.98046875, 155.2265625], [336.09765625, 152.6875], [339.14453125, 149.640625], [342.37890625, 146.5078125], [345.96875, 143.03125], [349.4609375, 139.24609375], [353.23046875, 134.83203125], [356.68359375, 129.72265625], [359.48828125, 123.9140625], [362.76953125, 116.09765625], [367.91796875, 93.69140625], [368.23828125, 88.5546875], [368.34375, 86.2890625], [369.94921875, 80.15234375], [372.7578125, 72.04296875], [375.703125, 62.5], [378.33203125, 52.72265625], [380.109375, 44.4453125], [381.40625, 37.59375], [382.26953125, 31.95703125], [382.71875, 26.60546875], [382.81640625, 21.76171875], [382.81640625, 17.84375], [382.55859375, 13.9609375], [382.27734375, 9.65625], [381.67578125, 5.3515625], [380.40625, 1.0703125], [378.71484375, -3.2109375], [376.48046875, -7.52734375], [373.93359375, -11.71875], [370.44140625, -16.32421875], [365.86328125, -21.49609375], [359.94921875, -26.8359375], [353.33984375, -32.046875], [345.84765625, -37.30859375], [336.55859375, -43.21484375], [326.34765625, -48.5859375], [315.515625, -53.15234375], [305.375, -56.67578125], [296, -59.47265625], [286.078125, -61.984375], [276.078125, -63.78125], [266.578125, -65.09765625], [258.90625, -66.11328125], [249.8984375, -67.34765625], [238.84765625, -68.6796875], [229.19921875, -70.01171875], [219.66015625, -71.50390625], [209.109375, -72.99609375], [197.14453125, -74.625], [186.52734375, -76.421875], [176.66796875, -77.8203125], [167.26953125, -79.1328125], [159.57421875, -80.6328125], [152.75, -81.4609375], [146.4609375, -81.89453125], [139.97265625, -82.23828125], [133.546875, -82.23828125], [127.84765625, -82.23828125], [123.01953125, -82.23828125], [117.9375, -81.9140625], [112.59765625, -81.046875], [107.3046875, -79.90234375], [100.41796875, -78.45703125], [92.74609375, -76.87890625], [85.40625, -75.359375], [77.546875, -73.80859375], [69.71875, -72.6640625], [62.4921875, -71.9609375], [56.02734375, -71.23046875], [50.37109375, -70.26171875], [46.20703125, -69.32421875], [43.45703125, -68.48046875], [41.48046875, -67.5703125], [39.99609375, -66.90234375], [38.51171875, -66.23828125], [36.7734375, -65.3671875], [35.4609375, -64.359375], [34.18359375, -63.328125], [33.0078125, -62.54296875], [31.8125, -61.76953125], [30.5234375, -60.8984375], [29.4921875, -60.09765625], [28.5078125, -59.3828125], [27.24609375, -58.61328125], [25.49609375, -57.73828125], [23.7421875, -56.859375], [21.99609375, -55.984375], [20.51953125, -55.16796875], [19.4921875, -54.44140625], [18.81640625, -53.84375], [18.35546875, -53.52734375], [18.0859375, -53.46484375], [17.85546875, -53.44921875], [17.85546875, -53.44921875], ] as LocalPoint[]; updatePath(startPoint, points); const selectedElements = getSelectedElements(h.elements, h.state); expect(selectedElements.length).toBe(4); expect(selectedElements.filter((e) => e.type === "line").length).toBe(1); expect(selectedElements.filter((e) => e.type === "ellipse").length).toBe(1); expect(selectedElements.filter((e) => e.type === "diamond").length).toBe(1); expect(selectedElements.filter((e) => e.type === "freedraw").length).toBe( 1, ); }); it("Single linear element", () => { const startPoint = pointFrom(62, -200); const points = [ [0, 0], [0, 0.1015625], [-1.65625, 2.2734375], [-8.43359375, 12.265625], [-17.578125, 25.83203125], [-25.484375, 37.38671875], [-31.453125, 47.828125], [-34.92578125, 55.21875], [-37.1171875, 60.05859375], [-38.4375, 63.49609375], [-39.5, 66.6328125], [-40.57421875, 69.84375], [-41.390625, 73.53515625], [-41.9296875, 77.078125], [-42.40625, 79.71484375], [-42.66796875, 81.83203125], [-42.70703125, 83.32421875], [-42.70703125, 84.265625], [-42.70703125, 85.171875], [-42.70703125, 86.078125], [-42.70703125, 86.6484375], [-42.70703125, 87], [-42.70703125, 87.1796875], [-42.70703125, 87.4296875], [-42.70703125, 87.83203125], [-42.70703125, 88.86328125], [-42.70703125, 91.27734375], [-42.70703125, 95.0703125], [-42.44140625, 98.46875], [-42.17578125, 100.265625], [-42.17578125, 101.16015625], [-42.16015625, 101.76171875], [-42.0625, 102.12109375], [-42.0625, 102.12109375], ] as LocalPoint[]; updatePath(startPoint, points); const selectedElements = getSelectedElements(h.elements, h.state); expect(selectedElements.length).toBe(1); expect(h.app.state.selectedLinearElement).toBeDefined(); }); }); describe("Special cases", () => { it("Select only frame if its children are also selected", () => { act(() => { const elements = [ { id: "CaUA2mmuudojzY98_oVXo", type: "rectangle", x: -96.64353835077907, y: -270.1600585741129, width: 146.8359375, height: 104.921875, angle: 0, strokeColor: "#1e1e1e", backgroundColor: "transparent", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, groupIds: [], frameId: "85VShCn1P9k81JqSeOg-c", index: "aE", roundness: { type: 3, }, seed: 227442978, version: 15, versionNonce: 204983970, isDeleted: false, boundElements: [], updated: 1740959550684, link: null, locked: false, }, { id: "RZzDDA1DBJHw5OzHVNDvc", type: "diamond", x: 126.64943039922093, y: -212.4920898241129, width: 102.55859375, height: 93.80078125, angle: 0, strokeColor: "#1e1e1e", backgroundColor: "transparent", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, groupIds: [], frameId: "85VShCn1P9k81JqSeOg-c", index: "aH", roundness: { type: 2, }, seed: 955233890, version: 14, versionNonce: 2135303358, isDeleted: false, boundElements: [], updated: 1740959550684, link: null, locked: false, }, { id: "CSVDDbC9vxqgO2uDahcE9", type: "ellipse", x: -20.999007100779068, y: -87.0272460741129, width: 116.13671875, height: 70.7734375, angle: 0, strokeColor: "#1e1e1e", backgroundColor: "transparent", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, groupIds: [], frameId: "85VShCn1P9k81JqSeOg-c", index: "aI", roundness: { type: 2, }, seed: 807647870, version: 16, versionNonce: 455740962, isDeleted: false, boundElements: [], updated: 1740959550684, link: null, locked: false, }, { id: "85VShCn1P9k81JqSeOg-c", type: "frame", x: -164.95603835077907, y: -353.5155273241129, width: 451.04296875, height: 397.09765625, angle: 0, strokeColor: "#bbb", backgroundColor: "transparent", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 0, opacity: 100, groupIds: [], frameId: null, index: "aJ", roundness: null, seed: 1134892578, version: 57, versionNonce: 1699466238, isDeleted: false, boundElements: [], updated: 1740959550367, link: null, locked: false, name: null, }, ].map((e) => ({ ...e, index: null, created: null, angle: e.angle as Radians, })) as ExcalidrawElement[]; h.elements = elements; h.app.setState({ boxSelectionMode: "overlap" }); }); const startPoint = pointFrom(-352, -64); const points = [ [0, 0], [0.1015625, 0], [3.80078125, -1.05859375], [14.38671875, -5.10546875], [26.828125, -10.70703125], [38.17578125, -16.10546875], [49.6328125, -21.59375], [79.890625, -34.078125], [111.5859375, -46.4140625], [125.61328125, -51.265625], [139.20703125, -55.81640625], [151.046875, -60.27734375], [160.86328125, -64.140625], [170.15625, -67.51171875], [181.0234375, -71.5234375], [192.6796875, -75.79296875], [204.66015625, -80.19921875], [218.22265625, -85.6875], [233.359375, -91.9375], [264.22265625, -103.91796875], [280.390625, -109.80859375], [295.48046875, -114.99609375], [309.453125, -120.28125], [323.5546875, -126.125], [339.26953125, -132.6796875], [354.67578125, -139.64453125], [370.86328125, -146.53125], [384.70703125, -152.4921875], [394.7109375, -157.6796875], [405.6171875, -163.07421875], [416.390625, -167.96484375], [425.41796875, -171.6484375], [433.26171875, -174.78515625], [440.76953125, -177.68359375], [447.4140625, -179.71875], [453.3828125, -181.11328125], [458.421875, -182.13671875], [462.82421875, -182.5546875], [467.2109375, -182.640625], [472.09765625, -182.640625], [481.9609375, -182.640625], [487.23828125, -182.5859375], [492.03515625, -181.91796875], [496.76953125, -180.640625], [501.43359375, -179.2734375], [505.203125, -177.73046875], [508.33984375, -176.08984375], [511.8671875, -174.16796875], [515.9140625, -172.09375], [519.703125, -170.125], [523.6796875, -167.8828125], [528.109375, -165.3984375], [532.01953125, -163.3125], [535.28125, -161.65625], [537.62890625, -159.7734375], [539.0859375, -157.53125], [540.1640625, -155.7421875], [540.98046875, -154.2578125], [541.87890625, -152.33203125], [542.69140625, -150.0078125], [543.25390625, -147.671875], [543.90625, -145.125], [544.66796875, -142.01171875], [545.34375, -138.1484375], [546.03515625, -132.72265625], [546.41015625, -126.80078125], [546.44921875, -121.25390625], [546.38671875, -116.3046875], [545.21484375, -112], [541.50390625, -107.2421875], [536.515625, -102.83203125], [531.44140625, -98.95703125], [526.39453125, -95.23046875], [521.15234375, -91.9921875], [514.38671875, -87.984375], [506.953125, -83.19140625], [499.1171875, -77.52734375], [491.37109375, -71.6484375], [484.85546875, -66.3984375], [477.8203125, -60.21875], [469.921875, -53.26953125], [460.84765625, -45.6171875], [451.796875, -38.359375], [444.33984375, -32.48046875], [438.4296875, -27.68359375], [435.2109375, -24.84375], [433.07421875, -23.23828125], [429.7421875, -21.125], [424.8984375, -17.546875], [418.7421875, -13.01171875], [411.84375, -8.3359375], [404.80078125, -3.65625], [398.23828125, 0.6171875], [392.32421875, 4.74609375], [386.21875, 9.69921875], [379.7421875, 14.734375], [373.6015625, 19.95703125], [367.34375, 26.72265625], [360.73828125, 34.48046875], [354.1484375, 42.51953125], [347.21484375, 51.19140625], [340.59765625, 59.7265625], [334.46875, 67.703125], [328.9921875, 74.82421875], [323.78515625, 81.6796875], [318.6640625, 88.34375], [314.2109375, 93.8984375], [309.10546875, 100.66015625], [304.17578125, 107.2734375], [299.97265625, 112.421875], [295.890625, 117.99609375], [291.8828125, 123.4453125], [288.0078125, 128.25], [284.91796875, 132.265625], [282.453125, 135.66796875], [279.80078125, 139.16015625], [276.7734375, 143.53515625], [274.3515625, 147.6484375], [272.0859375, 151.0546875], [269.5546875, 154.37890625], [267.71484375, 156.73828125], [266.62890625, 158.484375], [265.5546875, 160.03125], [264.73828125, 161.30078125], [264.16015625, 162.51953125], [263.46484375, 163.734375], [262.9140625, 164.9453125], [262.05078125, 166.3046875], [261.234375, 167.390625], [260.46484375, 168.53515625], [259.5703125, 169.6640625], [258.9296875, 170.1875], [258.9296875, 170.1875], ] as LocalPoint[]; updatePath(startPoint, points); const selectedElements = getSelectedElements(h.elements, h.state); expect(selectedElements.length).toBe(1); expect(selectedElements[0].type).toBe("frame"); }); it("Selects group if any group from group is selected", () => { act(() => { const elements = [ { type: "line", version: 594, versionNonce: 1548428815, isDeleted: false, id: "FBFkTIUB1trLc6nEdp1Pu", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, angle: 0, x: 170.81219641259787, y: 391.1659993876855, strokeColor: "#1e1e1e", backgroundColor: "#846358", width: 66.16406551308279, height: 78.24124358133415, seed: 838106785, groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"], frameId: null, roundness: { type: 2, }, boundElements: [], updated: 1740960278015, link: null, locked: false, startBinding: null, endBinding: null, startArrowhead: null, endArrowhead: null, points: [ [0, 0], [-12.922669045523984, 78.24124358133415], [53.24139646755881, 78.24124358133415], [41.35254094567674, 4.2871914291142], [0, 0], ], index: "aJ", }, { type: "line", version: 947, versionNonce: 1038960225, isDeleted: false, id: "RsALsOjcB5dAyH4JNlfqJ", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, angle: 0, x: 188.53119264021603, y: 207.94959072391882, strokeColor: "#1e1e1e", backgroundColor: "#2f9e44", width: 369.2312846526558, height: 192.4489303545334, seed: 319685249, groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"], frameId: null, roundness: { type: 2, }, boundElements: [], updated: 1740960278015, link: null, locked: false, startBinding: null, endBinding: null, startArrowhead: null, endArrowhead: null, points: [ [0, 0], [-184.8271826294887, 192.4489303545334], [184.4041020231671, 192.4489303545334], [0, 0], ], index: "aK", }, { type: "line", version: 726, versionNonce: 1463389231, isDeleted: false, id: "YNXwgpVIEUFgUZpJ564wo", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, angle: 0, x: 184.66726071162367, y: 123.16737006571739, strokeColor: "#1e1e1e", backgroundColor: "#2f9e44", width: 290.9653230160535, height: 173.62827429793325, seed: 1108085345, groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"], frameId: null, roundness: { type: 2, }, boundElements: [], updated: 1740960278015, link: null, locked: false, startBinding: null, endBinding: null, startArrowhead: null, endArrowhead: null, points: [ [0, 0], [-142.34630272423374, 173.62827429793325], [148.61902029181974, 173.62827429793325], [0, 0], ], index: "aL", }, { type: "line", version: 478, versionNonce: 2081935937, isDeleted: false, id: "NV7XOz9ZIB8CbuqQIjt5k", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, angle: 0, x: 189.05565121741444, y: 54.65530340848173, strokeColor: "#1e1e1e", backgroundColor: "#2f9e44", width: 194.196753378859, height: 137.02921662223056, seed: 398333505, groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"], frameId: null, roundness: { type: 2, }, boundElements: [], updated: 1740960278015, link: null, locked: false, startBinding: null, endBinding: null, startArrowhead: null, endArrowhead: null, points: [ [0, 0], [-97.0316913876915, 135.70546644407042], [97.1650619911675, 137.02921662223056], [0, 0], ], index: "aM", }, { type: "ellipse", version: 282, versionNonce: 1337339471, isDeleted: false, id: "b7FzLnG0L3-50bqij9mGX", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, angle: 0, x: 73.9036924826674, y: 334.3607129519222, strokeColor: "#000000", backgroundColor: "#c2255c", width: 25.723148574685204, height: 25.723148574685204, seed: 654550561, groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"], frameId: null, roundness: { type: 2, }, boundElements: [], updated: 1740960278015, link: null, locked: false, index: "aN", }, { type: "ellipse", version: 292, versionNonce: 1355145761, isDeleted: false, id: "XzVfrVf3-sFJFPdOo51sb", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, angle: 0, x: 138.21156391938035, y: 380.4480208148999, strokeColor: "#000000", backgroundColor: "#f08c00", width: 25.723148574685204, height: 25.723148574685204, seed: 2060204545, groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"], frameId: null, roundness: { type: 2, }, boundElements: [], updated: 1740960278015, link: null, locked: false, index: "aO", }, { type: "ellipse", version: 288, versionNonce: 1889111151, isDeleted: false, id: "D4m0Ex4rPc1-8T-uv5vGh", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, angle: 0, x: 208.9502224997646, y: 331.14531938008656, strokeColor: "#000000", backgroundColor: "#6741d9", width: 25.723148574685204, height: 25.723148574685204, seed: 337072609, groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"], frameId: null, roundness: { type: 2, }, boundElements: [], updated: 1740960278015, link: null, locked: false, index: "aP", }, { type: "ellipse", version: 296, versionNonce: 686224897, isDeleted: false, id: "E0wxH4dAzQsv7Mj6OngC8", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, angle: 0, x: 285.04787036654153, y: 367.5864465275573, strokeColor: "#000000", backgroundColor: "#e8590c", width: 25.723148574685204, height: 25.723148574685204, seed: 670330305, groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"], frameId: null, roundness: { type: 2, }, boundElements: [], updated: 1740960278015, link: null, locked: false, index: "aQ", }, { type: "ellipse", version: 290, versionNonce: 1974216335, isDeleted: false, id: "yKv_UI6iqa6zjVgYtXVcg", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, angle: 0, x: 113.56021320197334, y: 228.25272508134577, strokeColor: "#000000", backgroundColor: "#228be6", width: 25.723148574685204, height: 25.723148574685204, seed: 495127969, groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"], frameId: null, roundness: { type: 2, }, boundElements: [], updated: 1740960278015, link: null, locked: false, index: "aR", }, { type: "ellipse", version: 290, versionNonce: 662343137, isDeleted: false, id: "udyW842HtUTlqjDEOxoPN", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, angle: 0, x: 166.0783082086228, y: 271.12463937248776, strokeColor: "#000000", backgroundColor: "#ffd43b", width: 25.723148574685204, height: 25.723148574685204, seed: 1274196353, groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"], frameId: null, roundness: { type: 2, }, boundElements: [], updated: 1740960278015, link: null, locked: false, index: "aS", }, { type: "ellipse", version: 300, versionNonce: 229014703, isDeleted: false, id: "R3VRfgkowIgnr5dFXwWXa", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, angle: 0, x: 234.67337107445002, y: 237.89890579685272, strokeColor: "#000000", backgroundColor: "#38d9a9", width: 25.723148574685204, height: 25.723148574685204, seed: 2021841249, groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"], frameId: null, roundness: { type: 2, }, boundElements: [], updated: 1740960278015, link: null, locked: false, index: "aT", }, { type: "ellipse", version: 332, versionNonce: 1670392257, isDeleted: false, id: "90W2w6zgGHdYda8UBiG2R", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, angle: 0, x: 136.0679682048231, y: 155.37047078640435, strokeColor: "#000000", backgroundColor: "#fa5252", width: 25.723148574685204, height: 25.723148574685204, seed: 344130881, groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"], frameId: null, roundness: { type: 2, }, boundElements: [], updated: 1740960278015, link: null, locked: false, index: "aU", }, { type: "ellipse", version: 337, versionNonce: 2083589839, isDeleted: false, id: "nTDHvOk2mXLUFNn--7JvS", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, angle: 0, x: 176.7962867814079, y: 102.85237577975542, strokeColor: "#000000", backgroundColor: "#9775fa", width: 25.723148574685204, height: 25.723148574685204, seed: 995276065, groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"], frameId: null, roundness: { type: 2, }, boundElements: [], updated: 1740960278015, link: null, locked: false, index: "aV", }, { type: "ellipse", version: 313, versionNonce: 1715947937, isDeleted: false, id: "iS2Q6cvQ5n_kxINfwu0HS", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, angle: 0, x: 212.16561607160025, y: 153.22687507184727, strokeColor: "#000000", backgroundColor: "#fab005", width: 25.723148574685204, height: 25.723148574685204, seed: 1885432065, groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"], frameId: null, roundness: { type: 2, }, boundElements: [], updated: 1740960278015, link: null, locked: false, index: "aW", }, { type: "line", version: 1590, versionNonce: 2078563567, isDeleted: false, id: "X4-EPaJDEnPZKN1bWhFvs", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, angle: 0, x: 158.19616469467843, y: 72.35608879274483, strokeColor: "#000000", backgroundColor: "#fab005", width: 84.29101925982515, height: 84.66090652809709, seed: 489595105, groupIds: ["uRBC-GT117eEzaf2ehdX_", "-9NzH7Fa5JaHu4ArEFpa_"], frameId: null, roundness: null, boundElements: [], updated: 1740960278015, link: null, locked: false, startBinding: null, endBinding: null, startArrowhead: null, endArrowhead: null, points: [ [0, 0], [20.062524675376327, -6.158183070239938], [30.37468761946564, 12.304817735352257], [40.379925616688446, -6.1461467434106165], [60.1779773078079, -0.29196108982718233], [54.38738874028317, -19.998927589317724], [72.41919795710177, -30.209920701755497], [54.27320673839876, -40.520131959038096], [60.381292814514666, -60.13991664051316], [40.445474389553596, -54.21058549574358], [30.40022403822941, -72.35608879274483], [20.373038782485533, -54.22107619387393], [0.4211938029164521, -59.96466401524409], [5.9466053348070105, -39.94020499773404], [-11.871821302723378, -30.212694376435106], [5.916318974536789, -20.128073448241587], [0, 0], ], index: "aX", }, { type: "line", version: 1719, versionNonce: 1758424449, isDeleted: false, id: "MHWh6yM-hxZbKvIX473TA", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", roughness: 1, opacity: 100, angle: 0, x: 166.45958905094363, y: 64.16037225967494, strokeColor: "#000000", backgroundColor: "#ffd43b", width: 61.28316986803382, height: 61.55209370467244, seed: 1330240705, groupIds: ["uRBC-GT117eEzaf2ehdX_", "-9NzH7Fa5JaHu4ArEFpa_"], frameId: null, roundness: null, boundElements: [], updated: 1740960278015, link: null, locked: false, startBinding: null, endBinding: null, startArrowhead: null, endArrowhead: null, points: [ [0, 0], [14.586312023026041, -4.477262020152575], [22.08369476864762, 8.946127856709843], [29.357929973514253, -4.468511096647962], [43.751958845119866, -0.2122681777946347], [39.54195372315489, -14.540074226137776], [52.651848904941595, -21.96390218462942], [39.45893853270161, -29.459865970613833], [43.89977789919855, -43.724287114968824], [29.40558673003957, -39.41341021563198], [22.102260835384786, -52.605965847962594], [14.812069036519228, -39.4210374010807], [0.30622587789485506, -43.5968709738604], [4.323435973028119, -29.038234309343977], [-8.631320963092225, -21.96591876454555], [4.301416496013572, -14.633968779551441], [0, 0], ], index: "aY", }, ].map((e) => ({ ...e, index: null, created: null, angle: e.angle as Radians, })) as ExcalidrawElement[]; h.elements = elements; h.app.setState({ boxSelectionMode: "overlap" }); }); const startPoint = pointFrom(117, 463); const points = [ [0, 0], [0.09765625, 0], [3.24609375, 0], [6.9765625, 0], [10.76171875, 0], [14.03125, 0], [23.24609375, 0.32421875], [28.65625, 0.6484375], [32.0546875, 0.6484375], [35.4296875, 0.6484375], [38.86328125, 0.3828125], [41.9765625, -0.109375], [45.0390625, -0.4296875], [47.74609375, -0.5234375], [49.953125, -0.73046875], [52.12890625, -0.9375], [54.25, -1.14453125], [55.9921875, -1.3828125], [57.67578125, -1.58984375], [58.8125, -1.76953125], [59.453125, -1.76953125], [60.09375, -1.76953125], [60.09375, -1.76953125], ] as LocalPoint[]; updatePath(startPoint, points); const selectedElements = getSelectedElements(h.elements, h.state); expect(selectedElements.length).toBe(16); expect(h.app.state.selectedGroupIds["-9NzH7Fa5JaHu4ArEFpa_"]).toBe(true); }); it("respects boxSelectionMode ('contain' vs 'overlap') during lasso selection", () => { const rect1 = API.createElement({ type: "rectangle", id: "rect1", x: 100, y: 100, width: 50, height: 50, }); const rect2 = API.createElement({ type: "rectangle", id: "rect2", x: 300, y: 300, width: 50, height: 50, }); h.elements = [rect1, rect2]; // Lasso path enclosing rect1 fully, and intersecting rect2 partially const startPoint = pointFrom(80, 80); const lassoPoints = [ [0, 0], [100, 0], [240, 240], [240, 320], [0, 320], [0, 0], ] as LocalPoint[]; // 1) Test 'overlap' mode -> should select both rect1 (enclosed) and rect2 (intersected) act(() => h.app.setState({ boxSelectionMode: "overlap" })); updatePath(startPoint, lassoPoints); let selected = getSelectedElements(h.elements, h.state); expect(selected.map((el) => el.id)).toContain("rect1"); expect(selected.map((el) => el.id)).toContain("rect2"); // 2) Test 'contain' mode -> should select ONLY rect1 (enclosed), not rect2 (only intersected) act(() => h.app.setState({ boxSelectionMode: "contain" })); updatePath(startPoint, lassoPoints); selected = getSelectedElements(h.elements, h.state); expect(selected.map((el) => el.id)).toContain("rect1"); expect(selected.map((el) => el.id)).not.toContain("rect2"); }); }); describe("Box selection mode (through the lasso tool)", () => { const drawLasso = (points: GlobalPoint[]) => { act(() => { h.app.lassoTrail.startPath(points[0][0], points[0][1]); points .slice(1) .forEach(([x, y]) => h.app.lassoTrail.addPointToPath(x, y)); h.app.lassoTrail.endPath(); }); }; const selectedIds = () => Object.keys(h.state.selectedElementIds).sort(); describe("lasso enclosing one element and cutting through another", () => { // NOTE: the lasso wraps around `enclosed`, while its right edge (x = 320) cuts // through `crossed` (x = 300 to 350) const LASSO: GlobalPoint[] = ( [ [80, 80], [320, 80], [320, 220], [80, 220], [80, 80], ] as [number, number][] ).map(([x, y]) => pointFrom(x, y)); beforeEach(() => { act(() => { h.elements = [ API.createElement({ type: "rectangle", id: "enclosed", x: 100, y: 100, width: 50, height: 50, }), API.createElement({ type: "rectangle", id: "crossed", x: 300, y: 100, width: 50, height: 50, }), ]; }); }); it("selects only enclosed elements with the default mode ('contain')", () => { expect(h.state.boxSelectionMode).toBe("contain"); drawLasso(LASSO); expect(selectedIds()).toEqual(["enclosed"]); }); it("selects intersected elements as well in 'overlap' mode", () => { act(() => h.app.setState({ boxSelectionMode: "overlap" })); drawLasso(LASSO); expect(selectedIds()).toEqual(["crossed", "enclosed"]); }); }); it("does not treat an element as contained when the lasso cuts through it", () => { act(() => { h.elements = [ API.createElement({ type: "rectangle", id: "rect", x: 100, y: 100, width: 50, height: 50, }), ]; }); // NOTE: the lasso surrounding the rectangle, but with a spike poking through // its top edge into its interior: all four corners are inside the lasso // polygon, yet the rectangle is not fully contained const LASSO: GlobalPoint[] = ( [ [80, 80], [120, 80], [120, 125], [130, 125], [130, 80], [170, 80], [170, 170], [80, 170], [80, 80], ] as [number, number][] ).map(([x, y]) => pointFrom(x, y)); drawLasso(LASSO); expect(h.state.boxSelectionMode).toBe("contain"); expect(selectedIds()).toEqual([]); // NOTE: the very same lasso still selects it in "overlap" mode act(() => h.app.setState({ boxSelectionMode: "overlap" })); drawLasso(LASSO); expect(selectedIds()).toEqual(["rect"]); }); it("does not select a container whose label alone is enclosed", () => { act(() => { h.elements = [ API.createElement({ type: "rectangle", id: "container", x: 100, y: 100, width: 200, height: 100, boundElements: [{ type: "text", id: "label" }], }), API.createElement({ type: "text", id: "label", x: 140, y: 130, width: 80, height: 25, containerId: "container", }), ]; }); // NOTE: the lasso wraps around the label only, staying well inside the // container it is bound to const LASSO: GlobalPoint[] = ( [ [130, 120], [230, 120], [230, 165], [130, 165], [130, 120], ] as [number, number][] ).map(([x, y]) => pointFrom(x, y)); expect(h.state.boxSelectionMode).toBe("contain"); drawLasso(LASSO); expect(selectedIds()).toEqual([]); // NOTE: reaching the label is enough to select the container in "overlap" act(() => h.app.setState({ boxSelectionMode: "overlap" })); drawLasso(LASSO); expect(selectedIds()).toEqual(["container"]); }); describe("lasso enclosing a single member of a group", () => { const LASSO: GlobalPoint[] = ( [ [80, 80], [200, 80], [200, 200], [80, 200], [80, 80], ] as [number, number][] ).map(([x, y]) => pointFrom(x, y)); beforeEach(() => { act(() => { h.elements = [ API.createElement({ type: "rectangle", id: "member1", x: 100, y: 100, width: 50, height: 50, groupIds: ["group"], }), API.createElement({ type: "rectangle", id: "member2", x: 300, y: 100, width: 50, height: 50, groupIds: ["group"], }), ]; }); }); it("selects nothing in 'contain' mode, as the group isn't complete", () => { expect(h.state.boxSelectionMode).toBe("contain"); drawLasso(LASSO); expect(selectedIds()).toEqual([]); }); it("selects the whole group in 'overlap' mode", () => { act(() => h.app.setState({ boxSelectionMode: "overlap" })); drawLasso(LASSO); expect(selectedIds()).toEqual(["member1", "member2"]); }); it("selects the whole group once all its members are enclosed", () => { expect(h.state.boxSelectionMode).toBe("contain"); drawLasso( ( [ [80, 80], [400, 80], [400, 200], [80, 200], [80, 80], ] as [number, number][] ).map(([x, y]) => pointFrom(x, y)), ); expect(selectedIds()).toEqual(["member1", "member2"]); }); }); it("contains an element whose frame-clipped part is enclosed", () => { act(() => { h.elements = [ API.createElement({ type: "frame", id: "frame", x: 0, y: 0, width: 200, height: 200, }), // NOTE: overflows the frame to the right, so only the x = 150 to 200 // part of it is visible API.createElement({ type: "rectangle", id: "overflowing", x: 150, y: 50, width: 100, height: 50, frameId: "frame", }), ]; }); // NOTE: wraps the visible part of the rectangle, while its right edge cuts // through the clipped-away part of it const LASSO: GlobalPoint[] = ( [ [140, 40], [210, 40], [210, 110], [140, 110], [140, 40], ] as [number, number][] ).map(([x, y]) => pointFrom(x, y)); expect(h.state.boxSelectionMode).toBe("contain"); drawLasso(LASSO); expect(selectedIds()).toEqual(["overflowing"]); }); it("contains elements without an outline, such as a freedraw dot", () => { act(() => { h.elements = [ API.createElement({ type: "freedraw", id: "dot", x: 100, y: 100, width: 0, height: 0, points: [pointFrom(0, 0)], }), ]; }); const LASSO: GlobalPoint[] = ( [ [80, 80], [120, 80], [120, 120], [80, 120], [80, 80], ] as [number, number][] ).map(([x, y]) => pointFrom(x, y)); expect(h.state.boxSelectionMode).toBe("contain"); drawLasso(LASSO); expect(selectedIds()).toEqual(["dot"]); }); it("contains elements whose outline lies exactly on the lasso path", () => { act(() => { h.elements = [ API.createElement({ type: "rectangle", id: "rect", x: 100, y: 100, width: 50, height: 50, }), ]; }); // NOTE: a lasso running exactly along one edge of the rectangle, from each // of the four sides in turn. Which side it is may not decide the outcome. const LASSOS = { right: [ [80, 80], [150, 80], [150, 200], [80, 200], ], left: [ [100, 80], [200, 80], [200, 200], [100, 200], ], top: [ [80, 100], [200, 100], [200, 200], [80, 200], ], bottom: [ [80, 50], [200, 50], [200, 150], [80, 150], ], } as Record; expect(h.state.boxSelectionMode).toBe("contain"); for (const [side, lasso] of Object.entries(LASSOS)) { drawLasso( [...lasso, lasso[0]].map(([x, y]) => pointFrom(x, y)), ); expect([side, selectedIds()]).toEqual([side, ["rect"]]); } }); });