UNPKG

tldraw

Version:

A tiny little drawing editor.

311 lines (237 loc) • 8.17 kB
import { Box, TLNoteShape, Vec, toRichText } from '@tldraw/editor' import { TestEditor } from '../../../test/TestEditor' let editor: TestEditor beforeEach(() => { editor = new TestEditor({ options: { adjacentShapeMargin: 20 } }) // We don't want the camera to move when the shape gets created off screen editor.updateViewportScreenBounds(new Box(0, 0, 2000, 2000)) }) afterEach(() => { editor?.dispose() }) function testCloneHandles(x: number, y: number, rotation: number) { editor.createShape({ type: 'note', x, y, rotation }) const shape = editor.getLastCreatedShape()! editor.select(shape.id) const handles = editor.getShapeHandles(shape.id)! const positions = [new Vec(0, -220), new Vec(220, 0), new Vec(0, 220), new Vec(-220, 0)].map( (v) => v.rot(rotation).addXY(x, y) ) handles.forEach((handle, i) => { const handleInPageSpace = editor.getShapePageTransform(shape).applyToPoint(handle) editor.select(shape.id) editor.pointerMove(handleInPageSpace.x, handleInPageSpace.y) expect(editor.inputs.getCurrentPagePoint()).toMatchObject({ x: handleInPageSpace.x, y: handleInPageSpace.y, }) editor.pointerDown(handleInPageSpace.x, handleInPageSpace.y, { target: 'handle', shape, handle, }) editor.expectToBeIn('select.pointing_handle') editor.pointerUp() const newShape = editor.getLastCreatedShape() expect(newShape.id).not.toBe(shape.id) const expectedPosition = positions[i] editor.expectShapeToMatch({ id: newShape.id, type: 'note', x: expectedPosition.x, y: expectedPosition.y, }) editor.expectToBeIn('select.editing_shape') editor.cancel().undo().forceTick() }) } describe('Note clone handles', () => { it('Creates a new sticky note using handles', () => { testCloneHandles(1000, 1000, 0) }) it('Creates a new sticky when rotated', () => { testCloneHandles(1000, 1000, Math.PI / 2) }) it('Creates a new sticky when translated and rotated', () => { testCloneHandles(1000, 1000, Math.PI / 2) }) }) function testDragCloneHandles(x: number, y: number, rotation: number) { editor.createShape({ type: 'note', x, y, rotation }) const shape = editor.getLastCreatedShape()! editor.select(shape.id) const handles = editor.getShapeHandles(shape.id)! handles.forEach((handle) => { const handleInPageSpace = editor.getShapePageTransform(shape).applyToPoint(handle) editor.select(shape.id) editor.pointerMove(handleInPageSpace.x, handleInPageSpace.y) editor.pointerDown(handleInPageSpace.x, handleInPageSpace.y, { target: 'handle', shape, handle, }) editor.expectToBeIn('select.pointing_handle') editor.pointerMove(handleInPageSpace.x + 30, handleInPageSpace.y + 30) editor.forceTick() editor.expectToBeIn('select.translating') const newShape = editor.getLastCreatedShape() expect(newShape.id).not.toBe(shape.id) const offset = new Vec(100, 100).rot(rotation) editor.expectShapeToMatch({ id: newShape.id, type: 'note', x: handleInPageSpace.x + 30 - offset.x, y: handleInPageSpace.y + 30 - offset.y, }) editor.pointerUp() editor.expectToBeIn('select.editing_shape') editor.cancel().undo() }) } describe('Dragging clone handles', () => { it('Creates a new sticky note using handles', () => { testDragCloneHandles(1000, 1000, 0) }) it('Creates a new sticky when rotated', () => { testDragCloneHandles(1000, 1000, Math.PI / 2) }) it('Creates a new sticky when translated and rotated', () => { testDragCloneHandles(1000, 1000, Math.PI / 2) }) }) it('Selects an adjacent note when clicking the clone handle', () => { editor.createShape({ type: 'note', x: 1220, y: 1000 }) const shapeA = editor.getLastCreatedShape()! editor.createShape({ type: 'note', x: 1000, y: 1000 }) const shapeB = editor.getLastCreatedShape()! editor.select(shapeB.id) const handles = editor.getShapeHandles(shapeB.id)! const handle = handles[1] editor.select(shapeB.id) editor.pointerDown(handle.x, handle.y, { target: 'handle', shape: shapeB, handle, }) editor.expectToBeIn('select.pointing_handle') editor.pointerUp() // Because there's a shape already in that direction... // We didn't create a new shape; newShape is still shapeB expect(editor.getLastCreatedShape().id).toBe(shapeB.id) // the first shape is selected and we're editing it expect(editor.getSelectedShapeIds()).toEqual([shapeA.id]) editor.expectToBeIn('select.editing_shape') }) it('Creates an adjacent note when dragging the clone handle', () => { editor.createShape({ type: 'note', x: 1220, y: 1000, props: { richText: toRichText('rich hello') }, }) const shapeA = editor.getLastCreatedShape()! editor.createShape({ type: 'note', x: 1000, y: 1000, props: { richText: toRichText('rich hello') }, }) const shapeB = editor.getLastCreatedShape()! editor.select(shapeB.id) const handles = editor.getShapeHandles(shapeB.id)! const handle = handles[0] editor.select(shapeB.id) editor.pointerDown(handle.x, handle.y, { target: 'handle', shape: shapeB, handle, }) editor.expectToBeIn('select.pointing_handle') editor.pointerMove(handle.x + 30, handle.y + 30) editor.forceTick() const newShape = editor.getLastCreatedShape() expect(newShape.id).not.toBe(shapeB.id) expect(newShape.id).not.toBe(shapeA.id) const offset = new Vec(100, 100).rot(0) editor.expectShapeToMatch<TLNoteShape>({ id: newShape.id, type: 'note', x: handle.x + 30 - offset.x, y: handle.y + 30 - offset.y, props: { richText: toRichText(''), }, }) editor.pointerUp() editor.expectToBeIn('select.editing_shape') }) it('Does not put the new shape into a frame if its center is not in the frame', () => { editor.createShape({ type: 'frame', x: 1321, y: 1000 }) // one pixel too far... const frameA = editor.getLastCreatedShape()! // center no longer in the frame editor.createShape({ type: 'note', x: 1000, y: 1000 }) const shapeA = editor.getLastCreatedShape()! // to the right const handle = editor.getShapeHandles(shapeA.id)![1] editor .select(shapeA.id) .pointerDown(handle.x, handle.y, { target: 'handle', shape: shapeA, handle, }) .expectToBeIn('select.pointing_handle') .pointerUp() const newShape = editor.getLastCreatedShape() // Should be a child of the frame expect(newShape.parentId).not.toBe(frameA.id) }) it('Puts the new shape into a frame based on its center', () => { editor.createShape({ type: 'frame', x: 1320, y: 1100 }) const frameA = editor.getLastCreatedShape()! // top left won't be in the frame, but the center will (barely but yes) editor.createShape({ type: 'note', x: 1000, y: 1000 }) const shapeA = editor.getLastCreatedShape()! // to the right const handle = editor.getShapeHandles(shapeA.id)![1] editor .select(shapeA.id) .pointerDown(handle.x, handle.y, { target: 'handle', shape: shapeA, handle, }) .expectToBeIn('select.pointing_handle') .pointerUp() const newShape = editor.getLastCreatedShape() // Should be a child of the frame expect(newShape.parentId).toBe(frameA.id) }) function testNoteShapeFrameRotations(sourceRotation: number, rotation: number) { editor.createShape({ type: 'frame', x: 1220, y: 1000, rotation: rotation }) const frameA = editor.getLastCreatedShape()! // top left won't be in the frame, but the center will (barely but yes) editor.createShape({ type: 'note', x: 1000, y: 1000, rotation: sourceRotation }) const shapeA = editor.getLastCreatedShape()! // to the right const handle = editor.getShapeHandles(shapeA.id)![1] editor .select(shapeA.id) .pointerDown(handle.x, handle.y, { target: 'handle', shape: shapeA, handle, }) .expectToBeIn('select.pointing_handle') .pointerUp() const newShape = editor.getLastCreatedShape() // Should be a child of the frame expect(newShape.parentId).toBe(frameA.id) expect(editor.getShapePageTransform(newShape).rotation()).toBeCloseTo(sourceRotation) editor.cancel().undo() } it('Puts the new shape into a rotated frame and keeps the source page rotation', () => { testNoteShapeFrameRotations(0, 0.01) testNoteShapeFrameRotations(0.01, 0) testNoteShapeFrameRotations(0.01, 0.01) })