UNPKG

tldraw

Version:

A tiny little drawing editor.

1,088 lines (963 loc) • 29.5 kB
import { createShapeId, TLImageShape } from '@tldraw/editor' import { vi } from 'vitest' import { MIN_CROP_SIZE } from '../lib/shapes/shared/crop' import { TestEditor } from './TestEditor' vi.useFakeTimers() let editor: TestEditor afterEach(() => { editor?.dispose() }) const ids = { imageA: createShapeId('imageA'), imageB: createShapeId('imageB'), boxA: createShapeId('boxA'), } const imageWidth = 1200 const imageHeight = 800 const imageProps = { assetId: null, playing: true, url: '', w: imageWidth, h: imageHeight, } beforeEach(() => { editor = new TestEditor() // this side effect is normally added via a hook editor.sideEffects.registerAfterChangeHandler('instance_page_state', (prev, next) => { if (prev.croppingShapeId !== next.croppingShapeId) { const isInCroppingState = editor.isIn('select.crop') if (!prev.croppingShapeId && next.croppingShapeId) { if (!isInCroppingState) { editor.setCurrentTool('select.crop.idle') } } else if (prev.croppingShapeId && !next.croppingShapeId) { if (isInCroppingState) { editor.setCurrentTool('select.idle') } } } }) editor.createShapes([ { id: ids.imageA, type: 'image', x: 100, y: 100, props: imageProps, }, { id: ids.imageB, type: 'image', x: 500, y: 500, props: { ...imageProps, w: imageWidth / 2, h: imageHeight / 2, crop: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 0.5, y: 0.5 }, }, }, }, { id: ids.boxA, type: 'geo', x: 1000, y: 1000, props: { w: 100, h: 100, }, }, ]) }) describe('When in the select.idle state', () => { it('double clicking an image should transition to select.crop', () => { editor.select(ids.boxA) editor.expectToBeIn('select.idle') expect(editor.getSelectedShapeIds()).toMatchObject([ids.boxA]) expect(editor.getCroppingShapeId()).toBe(null) editor.doubleClick(550, 550, ids.imageB) editor.expectToBeIn('select.crop.idle') expect(editor.getSelectedShapeIds()).toMatchObject([ids.imageB]) expect(editor.getCroppingShapeId()).toBe(ids.imageB) editor.updateShape({ id: ids.imageB, type: 'image', props: { crop: { topLeft: { x: 0.1, y: 0.1 }, bottomRight: { x: 0.9, y: 0.9 } }, }, }) editor.undo() // back to the start of the crop (undo the crop) editor.expectToBeIn('select.crop.idle') expect(editor.getSelectedShapeIds()).toMatchObject([ids.imageB]) expect(editor.getCroppingShapeId()).toBe(ids.imageB) editor.undo() // back to start editor.expectToBeIn('select.idle') expect(editor.getSelectedShapeIds()).toMatchObject([ids.boxA]) expect(editor.getCroppingShapeId()).toBe(null) editor .redo() // select again .redo() // crop again // does not start copping again, but will redo the crop operation editor.expectToBeIn('select.idle') expect(editor.getSelectedShapeIds()).toMatchObject([ids.imageB]) expect(editor.getCroppingShapeId()).toBe(null) expect(editor.getOnlySelectedShape()!.props).toMatchObject({ crop: { topLeft: { x: 0.1, y: 0.1 }, bottomRight: { x: 0.9, y: 0.9 } }, }) }) it('when ONLY ONE image is selected double clicking a selection handle should transition to select.crop', () => { // when two shapes are selected, double click should not transition // corner (two shapes) editor .expectToBeIn('select.idle') .select(ids.imageA, ids.imageB) .doubleClick(550, 550, { target: 'selection', handle: 'bottom_right', }) .expectToBeIn('select.idle') expect(editor.getCroppingShapeId()).toBe(null) // edge (two shapes) editor .expectToBeIn('select.idle') .doubleClick(550, 550, { target: 'selection', handle: 'bottom', }) .expectToBeIn('select.idle') expect(editor.getCroppingShapeId()).toBe(null) // corner (one shape) editor .cancel() .expectToBeIn('select.idle') .select(ids.imageB) .doubleClick(550, 550, { target: 'selection', handle: 'bottom_right', }) .expectToBeIn('select.crop.idle') expect(editor.getCroppingShapeId()).toBe(ids.imageB) // edge (one shape) editor .cancel() .expectToBeIn('select.idle') .doubleClick(550, 550, { target: 'selection', handle: 'bottom', }) .expectToBeIn('select.crop.idle') expect(editor.getCroppingShapeId()).toBe(ids.imageB) }) it('when only an image is selected pressing enter should transition to select.crop', () => { // two shapes editor .expectToBeIn('select.idle') .select(ids.imageA, ids.imageB) .keyDown('Enter') .keyUp('Enter') .expectToBeIn('select.idle') // one image editor .expectToBeIn('select.idle') .select(ids.imageB) .keyDown('Enter') .keyUp('Enter') .expectToBeIn('select.crop.idle') expect(editor.getCroppingShapeId()).toBe(ids.imageB) }) it('when only an image is selected control-pointing a selection handle should transition to select.crop.pointing_crop_handle', () => { // two shapes / edge editor .cancel() .expectToBeIn('select.idle') .select(ids.imageA, ids.imageB) .pointerDown(500, 550, { target: 'selection', handle: 'bottom', ctrlKey: true, accelKey: true, }) .expectToBeIn('select.brushing') // two shapes / corner editor .cancel() .expectToBeIn('select.idle') .select(ids.imageA, ids.imageB) .pointerDown(500, 600, { target: 'selection', handle: 'bottom_left', ctrlKey: true, accelKey: true, }) .expectToBeIn('select.brushing') // one shape / edge editor .cancel() .expectToBeIn('select.idle') .select(ids.imageB) .pointerDown(500, 550, { target: 'selection', handle: 'bottom', ctrlKey: true, accelKey: true, }) .expectToBeIn('select.crop.pointing_crop_handle') // one shape / corner editor .cancel() .expectToBeIn('select.idle') .select(ids.imageB) .pointerDown(500, 600, { target: 'selection', handle: 'bottom_left', ctrlKey: true, accelKey: true, }) .expectToBeIn('select.crop.pointing_crop_handle') }) }) describe('When in the crop.idle state', () => { it('pressing escape should transition to select.idle', () => { editor .expectToBeIn('select.idle') .doubleClick(550, 550, ids.imageB) .expectToBeIn('select.crop.idle') .cancel() .expectToBeIn('select.idle') }) it('pressing enter should transition to select.idle', () => { editor .expectToBeIn('select.idle') .doubleClick(550, 550, ids.imageB) .expectToBeIn('select.crop.idle') .keyDown('Enter') .keyUp('Enter') .expectToBeIn('select.idle') }) it('pointing the canvas should point canvas', () => { editor .expectToBeIn('select.idle') .pointerMove(-100, -100) .pointerDown() .expectToBeIn('select.pointing_canvas') }) it('pointing some other shape should start pointing the shape', () => { editor .expectToBeIn('select.idle') .pointerMove(550, 500) .pointerDown() .expectToBeIn('select.pointing_shape') }) it('pointing a selection handle should enter the select.crop.pointing_crop_handle state', () => { // corner editor .expectToBeIn('select.idle') .doubleClick(550, 550, ids.imageB) .expectToBeIn('select.crop.idle') .pointerDown(500, 600, { target: 'selection', handle: 'bottom_left', ctrlKey: false }) .expectToBeIn('select.crop.pointing_crop_handle') //reset editor.cancel().cancel() // edge editor .expectToBeIn('select.idle') .doubleClick(550, 550, ids.imageB) .expectToBeIn('select.crop.idle') .pointerDown(500, 600, { target: 'selection', handle: 'bottom', ctrlKey: false }) .expectToBeIn('select.crop.pointing_crop_handle') }) it('pointing the cropping image should enter the select.crop.translating_crop state', () => { editor .expectToBeIn('select.idle') .doubleClick(550, 550, ids.imageB) .expectToBeIn('select.crop.idle') .pointerDown(500, 600, { target: 'shape', shape: editor.getShape(ids.imageB) }) .expectToBeIn('select.crop.pointing_crop') expect(editor.getCroppingShapeId()).toBe(ids.imageB) expect(editor.getSelectedShapeIds()).toMatchObject([ids.imageB]) }) it('clicking another image shape should set that shape as the new cropping shape and transition to pointing_crop', () => { editor .expectToBeIn('select.idle') .doubleClick(550, 550, ids.imageB) .expectToBeIn('select.crop.idle') .pointerDown(100, 100, { target: 'shape', shape: editor.getShape(ids.imageA) }) .expectToBeIn('select.crop.pointing_crop') expect(editor.getCroppingShapeId()).toBe(ids.imageA) expect(editor.getSelectedShapeIds()).toMatchObject([ids.imageA]) }) it('rotating will return to select.crop.idle', () => { editor .expectToBeIn('select.idle') .doubleClick(550, 550, ids.imageB) .expectToBeIn('select.crop.idle') .pointerDown(500, 600, { target: 'selection', handle: 'top_left_rotate' }) .expectToBeIn('select.pointing_rotate_handle') .pointerMove(510, 590) .expectToBeIn('select.rotating') .pointerUp() .expectToBeIn('select.crop.idle') }) it('nudges the cropped image', () => { editor .expectToBeIn('select.idle') .doubleClick(550, 550, ids.imageB) .expectToBeIn('select.crop.idle') const crop = () => editor.getShape<TLImageShape>(ids.imageB)!.props.crop! expect(crop()).toCloselyMatchObject({ topLeft: { x: 0, y: 0 }, bottomRight: { x: 0.5, y: 0.5 }, }) editor.keyDown('ArrowDown') expect(crop()).toCloselyMatchObject({ topLeft: { x: 0, y: 0.00125 }, bottomRight: { x: 0.5, y: 0.50125 }, }) editor.keyRepeat('ArrowDown') editor.keyRepeat('ArrowDown') editor.keyRepeat('ArrowDown') editor.keyRepeat('ArrowDown') editor.keyUp('ArrowDown') expect(crop()).toCloselyMatchObject({ topLeft: { x: 0, y: 0.0062 }, bottomRight: { x: 0.5, y: 0.5062 }, }) // Undoing should go back to the keydown state, all those // repeats should be ephemeral and squashed down editor.undo() expect(crop()).toCloselyMatchObject({ topLeft: { x: 0, y: 0 }, bottomRight: { x: 0.5, y: 0.5 }, }) editor.redo() expect(crop()).toCloselyMatchObject({ topLeft: { x: 0, y: 0.0062 }, bottomRight: { x: 0.5, y: 0.5062 }, }) }) }) describe('When in the select.crop.pointing_crop state', () => { it('pressing escape / cancel returns to select.crop.idle', () => { editor .expectToBeIn('select.idle') .doubleClick(550, 550, ids.imageB) .expectToBeIn('select.crop.idle') .pointerDown(550, 550, { target: 'shape', shape: editor.getShape(ids.imageB) }) .expectToBeIn('select.crop.pointing_crop') .cancel() .expectToBeIn('select.crop.idle') }) it('dragging enters select.crop.translating_crop', () => { editor .expectToBeIn('select.idle') .doubleClick(550, 550, ids.imageB) .expectToBeIn('select.crop.idle') .pointerDown(550, 550, { target: 'shape', shape: editor.getShape(ids.imageB) }) .expectToBeIn('select.crop.pointing_crop') .pointerMove(560, 560) .expectToBeIn('select.crop.translating_crop') }) }) describe('When in the select.crop.translating_crop state', () => { it('moving the pointer should adjust the crop', () => { editor .expectToBeIn('select.idle') .doubleClick(550, 550, ids.imageB) .expectToBeIn('select.crop.idle') .pointerDown(550, 550, { target: 'shape', shape: editor.getShape(ids.imageB) }) .expectToBeIn('select.crop.pointing_crop') const before = editor.getShape<TLImageShape>(ids.imageB)!.props.crop! expect(before.topLeft.x).toBe(0) expect(before.topLeft.y).toBe(0) expect(before.bottomRight.x).toBe(0.5) expect(before.bottomRight.y).toBe(0.5) // Move the pointer to the left editor .pointerMove(550 - imageWidth / 4, 550 - imageHeight / 4) .expectToBeIn('select.crop.translating_crop') // Update should have run right away const afterFirst = editor.getShape<TLImageShape>(ids.imageB)!.props.crop! expect(afterFirst.topLeft.x).toBe(0.25) expect(afterFirst.topLeft.y).toBe(0.25) expect(afterFirst.bottomRight.x).toBe(0.75) expect(afterFirst.bottomRight.y).toBe(0.75) // and back to the start editor.pointerMove(550, 550) // Update should have run right away const afterSecond = editor.getShape<TLImageShape>(ids.imageB)!.props.crop! expect(afterSecond.topLeft.x).toBe(0) expect(afterSecond.topLeft.y).toBe(0) expect(afterSecond.bottomRight.x).toBe(0.5) expect(afterSecond.bottomRight.y).toBe(0.5) // and back to the left again (first) editor.pointerMove(250, 250) const afterEnd = editor.getShape<TLImageShape>(ids.imageB)!.props.crop! editor.pointerUp() editor.undo() expect(editor.getShape<TLImageShape>(ids.imageB)!.props.crop!).toMatchObject(before) editor.redo() expect(editor.getShape<TLImageShape>(ids.imageB)!.props.crop!).toMatchObject(afterEnd) }) it('moving the pointer while holding shift should adjust the crop', () => { editor .doubleClick(550, 550, ids.imageB) .pointerDown(550, 550, { target: 'shape', shape: editor.getShape(ids.imageB) }) .keyDown('Shift') .pointerMove(550 - imageWidth / 8, 550 - imageHeight / 8) .expectToBeIn('select.crop.translating_crop') // Update should have run right away const afterShiftDown = editor.getShape<TLImageShape>(ids.imageB)!.props.crop! expect(afterShiftDown).toMatchObject({ topLeft: { x: 0.125, y: 0 }, bottomRight: { x: 0.625, y: 0.5 }, }) editor.keyUp('Shift') vi.advanceTimersByTime(500) const afterShiftUp = editor.getShape<TLImageShape>(ids.imageB)!.props.crop! expect(afterShiftUp).toMatchObject({ topLeft: { x: 0.125, y: 0.125 }, bottomRight: { x: 0.625, y: 0.625 }, }) editor.keyDown('Shift') const afterShiftDownAgain = editor.getShape<TLImageShape>(ids.imageB)!.props.crop! expect(afterShiftDownAgain).toMatchObject(afterShiftDown) }) it('pressing escape / cancel should bail on that change and transition to select.crop.idle', () => { const before = editor.getShape<TLImageShape>(ids.imageB)!.props.crop! editor .expectToBeIn('select.idle') .doubleClick(550, 550, ids.imageB) .expectToBeIn('select.crop.idle') .pointerDown(550, 550, { target: 'shape', shape: editor.getShape(ids.imageB) }) .expectToBeIn('select.crop.pointing_crop') .pointerMove(250, 250) .expectToBeIn('select.crop.translating_crop') expect(editor.getShape<TLImageShape>(ids.imageB)!.props.crop!).not.toMatchObject(before) editor.cancel().expectToBeIn('select.crop.idle') expect(editor.getShape<TLImageShape>(ids.imageB)!.props.crop!).toMatchObject(before) }) it('pressing enter / pointer up / complete should transition to select.crop.idle', () => { const before = editor.getShape<TLImageShape>(ids.imageB)!.props.crop! editor .expectToBeIn('select.idle') .doubleClick(550, 550, ids.imageB) .expectToBeIn('select.crop.idle') .pointerDown(550, 550, { target: 'shape', shape: editor.getShape(ids.imageB) }) .expectToBeIn('select.crop.pointing_crop') .pointerMove(250, 250) .expectToBeIn('select.crop.translating_crop') expect(editor.getShape<TLImageShape>(ids.imageB)!.props.crop!).not.toMatchObject(before) editor.keyDown('Enter').keyUp('Enter').expectToBeIn('select.crop.idle') expect(editor.getShape<TLImageShape>(ids.imageB)!.props.crop!).not.toMatchObject(before) }) }) describe('When in the select.crop.pointing_crop_handle state', () => { it('moving the pointer should transition to select.crop.cropping', () => { editor .cancel() .expectToBeIn('select.idle') .select(ids.imageB) .pointerDown(500, 600, { target: 'selection', handle: 'bottom_left', ctrlKey: true, accelKey: true, }) .expectToBeIn('select.crop.pointing_crop_handle') .pointerMove(510, 590) .expectToBeIn('select.crop.cropping') }) it('when entered from select.idle, pressing escape / cancel should return to idle and clear cropping idle', () => { // coming from select.idle editor .cancel() .expectToBeIn('select.idle') .select(ids.imageB) .pointerDown(500, 600, { target: 'selection', handle: 'bottom_left', ctrlKey: true, accelKey: true, }) .expectToBeIn('select.crop.pointing_crop_handle') .cancel() .expectToBeIn('select.idle') expect(editor.getCroppingShapeId()).toBe(null) }) it('when entered from select.crop.idle, pressing escape / cancel should return to select.crop.idle and preserve croppingShapeId', () => { // coming from idle editor .cancel() .expectToBeIn('select.idle') .doubleClick(550, 550, ids.imageB) .expectToBeIn('select.crop.idle') .pointerDown(500, 600, { target: 'selection', handle: 'bottom_left', ctrlKey: false }) .expectToBeIn('select.crop.pointing_crop_handle') .cancel() .expectToBeIn('select.crop.idle') expect(editor.getCroppingShapeId()).toBe(ids.imageB) }) }) describe('When in the select.crop.cropping state', () => { it('moving the pointer should adjust the crop', () => { const before = editor.getShape<TLImageShape>(ids.imageB)!.props.crop! editor .cancel() .expectToBeIn('select.idle') .select(ids.imageB) .pointerDown(500, 600, { target: 'selection', handle: 'bottom_left', ctrlKey: true, accelKey: true, }) .expectToBeIn('select.crop.pointing_crop_handle') .pointerMove(510, 590) .expectToBeIn('select.crop.cropping') expect(editor.getShape<TLImageShape>(ids.imageB)!.props.crop!).not.toMatchObject(before) }) it('escape / cancel should revert the change and transition to select.idle when that is the history state', () => { const before = editor.getShape<TLImageShape>(ids.imageB)!.props.crop! editor .cancel() .expectToBeIn('select.idle') .select(ids.imageB) .pointerDown(500, 600, { target: 'selection', handle: 'bottom_left', ctrlKey: true, accelKey: true, }) .expectToBeIn('select.crop.pointing_crop_handle') .pointerMove(510, 590) .expectToBeIn('select.crop.cropping') .cancel() .expectToBeIn('select.idle') expect(editor.getShape<TLImageShape>(ids.imageB)!.props.crop!).toMatchObject(before) }) it('escape / cancel should revert the change and transition to crop.idle when that is the history state', () => { const before = editor.getShape<TLImageShape>(ids.imageB)!.props.crop! editor .cancel() .expectToBeIn('select.idle') .doubleClick(550, 550, ids.imageB) .expectToBeIn('select.crop.idle') .pointerDown(500, 600, { target: 'selection', handle: 'bottom', ctrlKey: false }) .expectToBeIn('select.crop.pointing_crop_handle') .pointerMove(510, 590) .expectToBeIn('select.crop.cropping') .cancel() .expectToBeIn('select.crop.idle') expect(editor.getShape<TLImageShape>(ids.imageB)!.props.crop!).toMatchObject(before) }) it('pointer up / complete should commit the change and transition to crop.idle when that is the history state', () => { const before = editor.getShape<TLImageShape>(ids.imageB)!.props.crop! editor .cancel() .expectToBeIn('select.idle') .doubleClick(550, 550, ids.imageB) .expectToBeIn('select.crop.idle') .pointerDown(500, 600, { target: 'selection', handle: 'bottom', ctrlKey: false }) .expectToBeIn('select.crop.pointing_crop_handle') .pointerMove(510, 590) .expectToBeIn('select.crop.cropping') .pointerUp() .expectToBeIn('select.crop.idle') expect(editor.getShape<TLImageShape>(ids.imageB)!.props.crop!).not.toMatchObject(before) editor.undo() expect(editor.getShape<TLImageShape>(ids.imageB)!.props.crop!).toMatchObject(before) editor.redo() expect(editor.getShape<TLImageShape>(ids.imageB)!.props.crop!).not.toMatchObject(before) }) it('pointer up / complete should commit the change and transition to select.idle when that is the history state', () => { const before = editor.getShape<TLImageShape>(ids.imageB)!.props.crop! editor .cancel() .expectToBeIn('select.idle') .select(ids.imageB) .pointerDown(500, 600, { target: 'selection', handle: 'bottom_left', ctrlKey: true, accelKey: true, }) .expectToBeIn('select.crop.pointing_crop_handle') .pointerMove(510, 590) .expectToBeIn('select.crop.cropping') .pointerUp() .expectToBeIn('select.idle') expect(editor.getShape<TLImageShape>(ids.imageB)!.props.crop!).not.toMatchObject(before) editor.undo() expect(editor.getShape<TLImageShape>(ids.imageB)!.props.crop!).toMatchObject(before) editor.redo() expect(editor.getShape<TLImageShape>(ids.imageB)!.props.crop!).not.toMatchObject(before) }) }) describe('When cropping...', () => { it('Correctly stops the crop when the crop is smaller than the minimum crop size', () => { const imageX = 100 const imageY = 100 // Crop the image to 0x0 which is below mimimum crop size const moveX = imageWidth const moveY = imageHeight const stoppingCropX = (imageWidth - MIN_CROP_SIZE) / imageWidth const stoppingCropY = (imageHeight - MIN_CROP_SIZE) / imageHeight editor .select(ids.imageA) .pointerDown( imageX, imageY, { target: 'selection', handle: 'top_left', }, { ctrlKey: true } ) .expectToBeIn('select.crop.pointing_crop_handle') .expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: imageProps }) .pointerMove(imageX + moveX, imageY + moveY) .expectToBeIn('select.crop.cropping') .expectShapeToMatch({ id: ids.imageA, x: imageX + (imageWidth - MIN_CROP_SIZE), y: imageY + (imageHeight - MIN_CROP_SIZE), props: { ...imageProps, crop: { topLeft: { x: stoppingCropX, y: stoppingCropY }, bottomRight: { x: 1, y: 1 }, }, w: MIN_CROP_SIZE, h: MIN_CROP_SIZE, }, }) }) it('Correctly resets the crop when double clicking a corner', () => { editor .doubleClick(550, 550, { target: 'shape', shape: editor.getShape(ids.imageB) }) .expectToBeIn('select.crop.idle') .expectShapeToMatch({ id: ids.imageB, x: 500, y: 500, props: { w: imageWidth / 2, h: imageHeight / 2, crop: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 0.5, y: 0.5 }, }, }, }) .doubleClick(500, 500, { target: 'selection', handle: 'top_left' }) .expectToBeIn('select.crop.idle') .expectShapeToMatch({ id: ids.imageB, x: 500, y: 500, props: { w: imageWidth, h: imageHeight, crop: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 }, }, }, }) }) it('Crop the image from the top left', () => { const imageX = 100 const imageY = 100 const cropX = 0.5 const cropY = 0.75 const moveX = imageWidth * cropX const moveY = imageHeight * cropY editor .select(ids.imageA) .pointerDown( imageX, imageY, { target: 'selection', handle: 'top_left', }, { ctrlKey: true } ) .expectToBeIn('select.crop.pointing_crop_handle') .expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: imageProps }) .pointerMove(imageX + moveX, imageY + moveY) .expectToBeIn('select.crop.cropping') .expectShapeToMatch({ id: ids.imageA, x: imageX + moveX, y: imageY + moveY, props: { ...imageProps, crop: { topLeft: { x: cropX, y: cropY }, bottomRight: { x: 1, y: 1 }, }, w: imageWidth - moveX, h: imageHeight - moveY, }, }) }) it('Crop the image from the top right', () => { const imageX = 100 const imageY = 100 const cropX = 0.5 const cropY = 0.75 const moveX = imageWidth * cropX const moveY = imageHeight * cropY editor .select(ids.imageA) .pointerDown( imageX + imageWidth, imageY, { target: 'selection', handle: 'top_right', }, { ctrlKey: true } ) .expectToBeIn('select.crop.pointing_crop_handle') .expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: imageProps }) .pointerMove(imageX + imageWidth - moveX, imageY + moveY) .expectToBeIn('select.crop.cropping') .expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY + moveY, props: { ...imageProps, crop: { topLeft: { x: 0, y: cropY }, bottomRight: { x: cropX, y: 1 }, }, w: imageWidth - moveX, h: imageHeight - moveY, }, }) }) it('Crop the image from the bottom left', () => { const imageX = 100 const imageY = 100 const cropX = 0.5 const cropY = 0.75 const moveX = imageWidth * cropX const moveY = imageHeight * cropY editor .select(ids.imageA) .pointerDown( imageX, imageY + imageHeight, { target: 'selection', handle: 'bottom_left', }, { ctrlKey: true } ) .expectToBeIn('select.crop.pointing_crop_handle') .expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: imageProps }) .pointerMove(imageX + moveX, imageY + imageHeight - moveY) .expectToBeIn('select.crop.cropping') .expectShapeToMatch({ id: ids.imageA, x: imageX + moveX, y: imageY, props: { ...imageProps, crop: { topLeft: { x: cropX, y: 0 }, bottomRight: { x: 1, y: 1 - cropY }, }, w: imageWidth - moveX, h: imageHeight - moveY, }, }) }) it('Crop the image from the bottom right', () => { const imageX = 100 const imageY = 100 const cropX = 0.5 const cropY = 0.75 const moveX = imageWidth * cropX const moveY = imageHeight * cropY editor .select(ids.imageA) .pointerDown( imageX + imageWidth, imageY + imageHeight, { target: 'selection', handle: 'bottom_right', }, { ctrlKey: true } ) .expectToBeIn('select.crop.pointing_crop_handle') .expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: imageProps }) .pointerMove(imageX + imageWidth - moveX, imageY + imageHeight - moveY) .expectToBeIn('select.crop.cropping') .expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: { ...imageProps, crop: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1 - cropX, y: 1 - cropY }, }, w: imageWidth - moveX, h: imageHeight - moveY, }, }) }) it('Crop the image from the left', () => { const imageX = 100 const imageY = 100 const cropX = 0.5 const moveX = imageWidth * cropX editor .select(ids.imageA) .pointerDown( imageX, imageY + imageHeight / 2, { target: 'selection', handle: 'left', }, { ctrlKey: true } ) .expectToBeIn('select.crop.pointing_crop_handle') .expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: imageProps }) .pointerMove(imageX + moveX, imageY + imageHeight / 2) .expectToBeIn('select.crop.cropping') .expectShapeToMatch({ id: ids.imageA, x: imageX + moveX, y: imageY, props: { ...imageProps, crop: { topLeft: { x: cropX, y: 0 }, bottomRight: { x: 1, y: 1 }, }, w: imageWidth - moveX, h: imageHeight, }, }) }) it('Crop the image from the top', () => { const imageX = 100 const imageY = 100 const cropY = 0.75 const moveY = imageHeight * cropY editor .select(ids.imageA) .pointerDown( imageX + imageWidth / 2, imageY, { target: 'selection', handle: 'top', }, { ctrlKey: true } ) .expectToBeIn('select.crop.pointing_crop_handle') .expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: imageProps }) .pointerMove(imageX + imageWidth / 2, imageY + moveY) .expectToBeIn('select.crop.cropping') .expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY + moveY, props: { ...imageProps, crop: { topLeft: { x: 0, y: cropY }, bottomRight: { x: 1, y: 1 }, }, w: imageWidth, h: imageHeight - moveY, }, }) }) it('Crop the image from the right', () => { const imageX = 100 const imageY = 100 const cropX = 0.5 const moveX = imageWidth * cropX editor .select(ids.imageA) .pointerDown( imageX + imageWidth, imageY + imageHeight / 2, { target: 'selection', handle: 'right', }, { ctrlKey: true } ) .expectToBeIn('select.crop.pointing_crop_handle') .expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: imageProps }) .pointerMove(150, 150) .expectToBeIn('select.crop.cropping') .pointerMove(imageX + imageWidth - moveX, imageY + imageHeight / 2) .expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: { ...imageProps, crop: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1 - cropX, y: 1 }, }, w: imageWidth - moveX, h: imageHeight, }, }) }) it('Crop the image from the bottom', () => { const imageX = 100 const imageY = 100 const cropY = 0.75 const moveY = imageHeight * cropY editor .select(ids.imageA) .pointerDown( imageX + imageWidth / 2, imageY + imageHeight, { target: 'selection', handle: 'bottom', }, { ctrlKey: true } ) .expectToBeIn('select.crop.pointing_crop_handle') .expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: imageProps }) .pointerMove(imageX + imageWidth / 2, imageY + imageHeight - moveY) .expectToBeIn('select.crop.cropping') .expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: { ...imageProps, crop: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 - cropY }, }, w: imageWidth, h: imageHeight - moveY, }, }) }) })