import { createShapeId, PI, TLShapeId } from '@tldraw/editor' import { vi } from 'vitest' import { TestEditor } from '../TestEditor' let editor: TestEditor let ids: Record vi.useFakeTimers() function createVideoShape() { const ids = { video1: createShapeId('video1') } editor.createShapes([{ id: ids.video1, type: 'video', x: 0, y: 0, props: { w: 160, h: 90 } }]) return ids.video1 } beforeEach(() => { editor = new TestEditor() editor.selectAll() editor.deleteShapes(editor.getSelectedShapeIds()) ids = { boxA: createShapeId('boxA'), boxB: createShapeId('boxB'), boxC: createShapeId('boxC'), } editor.createShapes([ { id: ids.boxA, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } }, { id: ids.boxB, type: 'geo', x: 100, y: 100, props: { w: 50, h: 50 } }, { id: ids.boxC, type: 'geo', x: 400, y: 400, props: { w: 100, h: 100 } }, ]) editor.selectAll() }) describe('when less than two shapes are selected', () => { it('does nothing', () => { editor.setSelectedShapes([ids.boxB]) const fn = vi.fn() editor.store.listen(fn) editor.stretchShapes(editor.getSelectedShapeIds(), 'horizontal') vi.advanceTimersByTime(1000) expect(fn).not.toHaveBeenCalled() }) }) describe('when multiple shapes are selected', () => { it('stretches horizontally', () => { editor.selectAll() editor.stretchShapes(editor.getSelectedShapeIds(), 'horizontal') vi.advanceTimersByTime(1000) editor.expectShapeToMatch( { id: ids.boxA, x: 0, y: 0, props: { w: 500 } }, { id: ids.boxB, x: 0, y: 100, props: { w: 500 } }, { id: ids.boxC, x: 0, y: 400, props: { w: 500 } } ) }) it('stretches horizontally and preserves aspect ratio', () => { const videoA = createVideoShape() editor.selectAll() expect(editor.getSelectedShapes().length).toBe(4) editor.stretchShapes(editor.getSelectedShapeIds(), 'horizontal') vi.advanceTimersByTime(1000) const newHeight = (500 * 9) / 16 editor.expectShapeToMatch( { id: ids.boxA, x: 0, y: 0, props: { w: 500 } }, { id: ids.boxB, x: 0, y: 100, props: { w: 500 } }, { id: ids.boxC, x: 0, y: 400, props: { w: 500 } }, { id: videoA, x: 0, y: -95.625, props: { w: 500, h: newHeight } } ) }) it('stretches vertically', () => { editor.selectAll() editor.stretchShapes(editor.getSelectedShapeIds(), 'vertical') vi.advanceTimersByTime(1000) editor.expectShapeToMatch( { id: ids.boxA, x: 0, y: 0, props: { h: 500 } }, { id: ids.boxB, x: 100, y: 0, props: { h: 500 } }, { id: ids.boxC, x: 400, y: 0, props: { h: 500 } } ) }) it('stretches vertically and preserves aspect ratio', () => { const videoA = createVideoShape() editor.selectAll() expect(editor.getSelectedShapes().length).toBe(4) editor.stretchShapes(editor.getSelectedShapeIds(), 'vertical') vi.advanceTimersByTime(1000) const newWidth = (500 * 16) / 9 editor.expectShapeToMatch( { id: ids.boxA, x: 0, y: 0, props: { h: 500 } }, { id: ids.boxB, x: 100, y: 0, props: { h: 500 } }, { id: ids.boxC, x: 400, y: 0, props: { h: 500 } }, { id: videoA, x: -364.44444444444446, y: 0, props: { w: newWidth, h: 500 } } ) }) it('does, undoes and redoes command', () => { editor.markHistoryStoppingPoint('stretch') editor.stretchShapes(editor.getSelectedShapeIds(), 'horizontal') vi.advanceTimersByTime(1000) editor.expectShapeToMatch({ id: ids.boxB, x: 0, props: { w: 500 } }) editor.undo() editor.expectShapeToMatch({ id: ids.boxB, x: 100, props: { w: 50 } }) editor.redo() editor.expectShapeToMatch({ id: ids.boxB, x: 0, props: { w: 500 } }) }) }) describe('When shapes are the child of another shape.', () => { it('stretches horizontally', () => { editor.reparentShapes([ids.boxB], ids.boxA) editor.select(ids.boxB, ids.boxC) editor.stretchShapes(editor.getSelectedShapeIds(), 'horizontal') vi.advanceTimersByTime(1000) editor.expectShapeToMatch( { id: ids.boxB, x: 100, y: 100, props: { w: 400 } }, { id: ids.boxC, x: 100, y: 400, props: { w: 400 } } ) }) it('stretches vertically', () => { editor.reparentShapes([ids.boxB], ids.boxA) editor.select(ids.boxB, ids.boxC) editor.stretchShapes(editor.getSelectedShapeIds(), 'vertical') vi.advanceTimersByTime(1000) editor.expectShapeToMatch( { id: ids.boxB, x: 100, y: 100, props: { h: 400 } }, { id: ids.boxC, x: 400, y: 100, props: { h: 400 } } ) }) }) describe('When shapes are the child of a rotated shape.', () => { it('does not stretches rotated shapes (when not PI/2 rotations)', () => { editor = new TestEditor() editor.selectAll() editor.deleteShapes(editor.getSelectedShapeIds()) ids = { boxA: createShapeId('boxA'), boxB: createShapeId('boxB'), boxC: createShapeId('boxC'), } editor.createShapes([ { id: ids.boxA, type: 'geo', x: 0, y: 0, rotation: PI / 3, props: { w: 100, h: 100 } }, { id: ids.boxB, type: 'geo', x: 100, y: 100, parentId: ids.boxA, props: { w: 50, h: 50 } }, { id: ids.boxC, type: 'geo', x: 200, y: 200, props: { w: 100, h: 100 } }, ]) editor.selectAll() editor.select(ids.boxA, ids.boxC) editor.stretchShapes(editor.getSelectedShapeIds(), 'horizontal') vi.advanceTimersByTime(1000) editor.expectShapeToMatch( { id: ids.boxA, x: 0, y: 0, props: { w: 100, h: 100, }, }, { id: ids.boxB, x: 100, y: 100, props: { w: 50, h: 50, }, }, { id: ids.boxC, x: 200, y: 200, props: { w: 100, h: 100, }, } ) }) it('stretches rotated shapes when pi2 rotations', () => { editor = new TestEditor() editor.selectAll() editor.deleteShapes(editor.getSelectedShapeIds()) ids = { boxA: createShapeId('boxA'), boxB: createShapeId('boxB'), boxC: createShapeId('boxC'), } editor.createShapes([ { id: ids.boxA, type: 'geo', x: 0, y: 0, rotation: PI / 2, props: { w: 100, h: 100 } }, { id: ids.boxB, type: 'geo', x: 100, y: 100, props: { w: 50, h: 50 } }, { id: ids.boxC, type: 'geo', x: 200, y: 200, props: { w: 100, h: 100 } }, ]) editor.selectAll() editor.stretchShapes(editor.getSelectedShapeIds(), 'vertical') vi.advanceTimersByTime(1000) editor.expectShapeToMatch( { id: ids.boxA, x: 0, y: 0, rotation: PI / 2, props: { w: 300, h: 100, }, }, { id: ids.boxB, x: 100, y: 0, props: { w: 50, h: 300, }, }, { id: ids.boxC, x: 200, y: 0, props: { w: 100, h: 300, }, } ) }) }) describe('When shapes have 0-width or 0-height', () => { it('Does not error with 0-width', () => { editor.selectAll() editor.deleteShapes(editor.getSelectedShapeIds()) editor .setCurrentTool('arrow') .keyDown('shift') .pointerDown(50, 0) .pointerMove(50, 100) .pointerUp(50, 100) .keyUp('shift') .setCurrentTool('geo') .pointerDown(0, 0) .pointerMove(100, 100) .pointerUp(100, 100) editor.selectAll() // make sure we don't get any errors: editor.stretchShapes(editor.getSelectedShapeIds(), 'horizontal') editor.stretchShapes(editor.getSelectedShapeIds(), 'vertical') }) it('Does not error with 0-height', () => { editor.selectAll() editor.deleteShapes(editor.getSelectedShapeIds()) editor // draw a perfectly horiztonal arrow: .setCurrentTool('arrow') .keyDown('shift') .pointerDown(0, 50) .pointerMove(100, 50) .pointerUp(100, 50) .keyUp('shift') // plus a box: .setCurrentTool('geo') .pointerDown(0, 0) .pointerMove(100, 100) .pointerUp(100, 100) editor.selectAll() // make sure we don't get any errors: editor.stretchShapes(editor.getSelectedShapeIds(), 'horizontal') editor.stretchShapes(editor.getSelectedShapeIds(), 'vertical') }) })