Tldraw/packages/tldraw/src/lib/shapes/note/noteCloning.test.ts

296 wiersze
7.7 KiB
TypeScript

import { Box, Vec } from '@tldraw/editor'
import { TestEditor } from '../../../test/TestEditor'
let editor: TestEditor
beforeEach(() => {
editor = new TestEditor()
// 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.currentPagePoint).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.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 })
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[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)
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({
id: newShape.id,
type: 'note',
x: handle.x + 30 - offset.x,
y: handle.y + 30 - offset.y,
})
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)
})