import { TLShapeId, createShapeId } from '@tldraw/editor' import { TestEditor } from './TestEditor' let editor: TestEditor beforeEach(() => { editor = new TestEditor() }) describe('bindingsIndex', () => { it('keeps a mapping from bound shapes to their bindings', () => { const ids = { box1: createShapeId('box1'), box2: createShapeId('box2'), } editor.createShapes([ { id: ids.box1, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100, fill: 'solid' } }, { id: ids.box2, type: 'geo', x: 200, y: 0, props: { w: 100, h: 100, fill: 'solid' } }, ]) editor.selectNone() editor.setCurrentTool('arrow') editor.pointerDown(50, 50) expect(editor.getOnlySelectedShape()).toBe(null) expect(editor.getArrowsBoundTo(ids.box1)).toEqual([]) editor.pointerMove(50, 55) expect(editor.getOnlySelectedShape()).not.toBe(null) const arrow = editor.getOnlySelectedShape()! expect(arrow.type).toBe('arrow') expect(editor.getArrowsBoundTo(ids.box1)).toEqual([arrow]) editor.pointerMove(250, 50) expect(editor.getArrowsBoundTo(ids.box1)).toEqual([editor.getShape(arrow.id)]) expect(editor.getArrowsBoundTo(ids.box2)).toEqual([editor.getShape(arrow.id)]) }) it('works if there are many arrows', () => { const ids = { box1: createShapeId('box1'), box2: createShapeId('box2'), } editor.createShapes([ { type: 'geo', id: ids.box1, x: 0, y: 0, props: { w: 100, h: 100 } }, { type: 'geo', id: ids.box2, x: 200, y: 0, props: { w: 100, h: 100 } }, ]) editor.setCurrentTool('arrow') // start at box 1 and end on box 2 editor.pointerDown(50, 50) expect(editor.getArrowsBoundTo(ids.box1)).toEqual([]) editor.pointerMove(250, 50) const arrow1 = editor.getOnlySelectedShape()! expect(arrow1.type).toBe('arrow') expect(editor.getArrowsBoundTo(ids.box1)).toEqual([arrow1]) expect(editor.getArrowsBoundTo(ids.box2)).toEqual([arrow1]) editor.pointerUp() expect(editor.getArrowsBoundTo(ids.box1)).toEqual([arrow1]) expect(editor.getArrowsBoundTo(ids.box2)).toEqual([arrow1]) // start at box 1 and end on the page editor.setCurrentTool('arrow') editor.pointerMove(50, 50).pointerDown().pointerMove(50, -50).pointerUp() const arrow2 = editor.getOnlySelectedShape()! expect(arrow2.type).toBe('arrow') expect(editor.getArrowsBoundTo(ids.box1)).toEqual([arrow1, arrow2]) // start outside box 1 and end in box 1 editor.setCurrentTool('arrow') editor.pointerDown(0, -50).pointerMove(50, 50).pointerUp(50, 50) const arrow3 = editor.getOnlySelectedShape()! expect(arrow3.type).toBe('arrow') expect(editor.getArrowsBoundTo(ids.box1)).toEqual([arrow1, arrow2, arrow3]) expect(editor.getArrowsBoundTo(ids.box2)).toEqual([arrow1]) // start at box 2 and end on the page editor.selectNone() editor.setCurrentTool('arrow') editor.pointerDown(250, 50) editor.expectToBeIn('arrow.pointing') editor.pointerMove(250, -50) editor.expectToBeIn('select.dragging_handle') const arrow4 = editor.getOnlySelectedShape()! expect(editor.getArrowsBoundTo(ids.box2)).toEqual([arrow1, arrow4]) editor.pointerUp(250, -50) editor.expectToBeIn('select.idle') expect(arrow4.type).toBe('arrow') expect(editor.getArrowsBoundTo(ids.box2)).toEqual([arrow1, arrow4]) // start outside box 2 and enter in box 2 editor.setCurrentTool('arrow') editor.pointerDown(250, -50).pointerMove(250, 50).pointerUp(250, 50) const arrow5 = editor.getOnlySelectedShape()! expect(arrow5.type).toBe('arrow') expect(editor.getArrowsBoundTo(ids.box1)).toEqual([arrow1, arrow2, arrow3]) expect(editor.getArrowsBoundTo(ids.box2)).toEqual([arrow1, arrow4, arrow5]) }) describe('updating shapes', () => { // ▲ │ │ ▲ // │ │ │ │ // b c e d // ┌───┼─┴─┐ ┌──┴──┼─┐ // │ │ ▼ │ │ ▼ │ │ // │ └───┼─────a───┼───► │ │ // │ 1 │ │ 2 │ // └───────┘ └───────┘ let arrowAId: TLShapeId let arrowBId: TLShapeId let arrowCId: TLShapeId let arrowDId: TLShapeId let arrowEId: TLShapeId let ids: Record beforeEach(() => { ids = { box1: createShapeId('box1'), box2: createShapeId('box2'), } editor.createShapes([ { id: ids.box1, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } }, { id: ids.box2, type: 'geo', x: 200, y: 0, props: { w: 100, h: 100 } }, ]) // span both boxes editor.setCurrentTool('arrow') editor.pointerDown(50, 50).pointerMove(250, 50).pointerUp(250, 50) arrowAId = editor.getOnlySelectedShape()!.id // start at box 1 and leave editor.setCurrentTool('arrow') editor.pointerDown(50, 50).pointerMove(50, -50).pointerUp(50, -50) arrowBId = editor.getOnlySelectedShape()!.id // start outside box 1 and enter editor.setCurrentTool('arrow') editor.pointerDown(50, -50).pointerMove(50, 50).pointerUp(50, 50) arrowCId = editor.getOnlySelectedShape()!.id // start at box 2 and leave editor.setCurrentTool('arrow') editor.pointerDown(250, 50).pointerMove(250, -50).pointerUp(250, -50) arrowDId = editor.getOnlySelectedShape()!.id // start outside box 2 and enter editor.setCurrentTool('arrow') editor.pointerDown(250, -50).pointerMove(250, 50).pointerUp(250, 50) arrowEId = editor.getOnlySelectedShape()!.id }) it('deletes the entry if you delete the bound shapes', () => { expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3) editor.deleteShapes([ids.box2]) expect(editor.getArrowsBoundTo(ids.box2)).toEqual([]) expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3) }) it('deletes the entry if you delete an arrow', () => { expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3) editor.deleteShapes([arrowEId]) expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(2) expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3) editor.deleteShapes([arrowDId]) expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(1) expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3) editor.deleteShapes([arrowCId]) expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(1) expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(2) editor.deleteShapes([arrowBId]) expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(1) expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(1) editor.deleteShapes([arrowAId]) expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(0) expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(0) }) it('deletes the entries in a batch too', () => { editor.deleteShapes([arrowAId, arrowBId, arrowCId, arrowDId, arrowEId]) expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(0) expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(0) }) it('adds new entries after initial creation', () => { expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3) expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3) // draw from box 2 to box 1 editor.setCurrentTool('arrow') editor.pointerDown(250, 50).pointerMove(50, 50).pointerUp(50, 50) expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(4) expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(4) // create a new box const box3 = createShapeId('box3') editor.createShapes([{ id: box3, type: 'geo', x: 400, y: 0, props: { w: 100, h: 100 } }]) // draw from box 2 to box 3 editor.setCurrentTool('arrow') editor.pointerDown(250, 50).pointerMove(450, 50).pointerUp(450, 50) expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(5) expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(4) expect(editor.getArrowsBoundTo(box3)).toHaveLength(1) }) it('works when copy pasting', () => { expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3) expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3) editor.selectAll() editor.duplicateShapes(editor.getSelectedShapeIds()) const [box1Clone, box2Clone] = editor .getSelectedShapes() .filter((shape) => editor.isShapeOfType(shape, 'geo')) .sort((a, b) => a.x - b.x) expect(editor.getArrowsBoundTo(box2Clone.id)).toHaveLength(3) expect(editor.getArrowsBoundTo(box1Clone.id)).toHaveLength(3) }) it('allows bound shapes to be moved', () => { expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3) expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3) editor.nudgeShapes([ids.box2], { x: 0, y: -1 }) expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3) expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3) }) it('allows the arrows bound shape to change', () => { expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3) expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3) // create another box const box3 = createShapeId('box3') editor.createShapes([{ id: box3, type: 'geo', x: 400, y: 0, props: { w: 100, h: 100 } }]) // move arrowA end from box2 to box3 const binding = editor .getBindingsInvolvingShape(ids.box2, 'arrow') .find((b) => b.props.terminal === 'end')! editor.updateBinding({ ...binding, toId: box3 }) expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(2) expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3) expect(editor.getArrowsBoundTo(box3)).toHaveLength(1) }) }) })