Tldraw/packages/tldraw/src/lib/shapes/line/LineShapeUtil.test.tsx

381 wiersze
9.5 KiB
TypeScript

import {
IndexKey,
TLGeoShape,
TLLineShape,
createShapeId,
sortByIndex,
structuredClone,
} from '@tldraw/editor'
import { TestEditor } from '../../../test/TestEditor'
import { TL } from '../../../test/test-jsx'
jest.mock('nanoid', () => {
let i = 0
return { nanoid: () => 'id' + i++ }
})
let editor: TestEditor
const id = createShapeId('line1')
jest.useFakeTimers()
beforeEach(() => {
editor = new TestEditor()
editor
.selectAll()
.deleteShapes(editor.getSelectedShapeIds())
.createShapes<TLLineShape>([
{
id: id,
type: 'line',
x: 150,
y: 150,
props: {
points: {
a1: { id: 'a1', index: 'a1' as IndexKey, x: 0, y: 0 },
a2: { id: 'a2', index: 'a2' as IndexKey, x: 100, y: 100 },
},
},
},
])
})
const getShape = () => editor.getShape<TLLineShape>(id)!
const getHandles = () => editor.getShapeHandles<TLLineShape>(id)!
describe('Translating', () => {
it('updates the line', () => {
editor.select(id)
editor.pointerDown(25, 25, { target: 'shape', shape: getShape() })
editor.pointerMove(50, 50) // Move shape by 25, 25
editor.expectShapeToMatch<TLLineShape>({
id: id,
x: 175,
y: 175,
})
})
it('updates the line when rotated', () => {
editor.select(id)
const shape = getShape()
editor.updateShape({ ...shape, rotation: Math.PI / 2 })
editor.pointerDown(250, 250, { target: 'shape', shape: shape })
editor.pointerMove(300, 400) // Move shape by 50, 150
editor.expectShapeToMatch<TLLineShape>({
id: id,
x: 200,
y: 300,
})
})
})
describe('Mid-point handles', () => {
it('create new handle', () => {
editor.select(id)
editor.pointerDown(200, 200, {
target: 'handle',
shape: getShape(),
handle: getHandles()[1],
})
editor.pointerMove(349, 349).pointerMove(350, 350) // Move handle by 150, 150
editor.pointerUp()
editor.expectShapeToMatch({
id: id,
props: {
points: {
a1: { id: 'a1', index: 'a1', x: 0, y: 0 },
a1V: { id: 'a1V', index: 'a1V', x: 200, y: 200 },
a2: { id: 'a2', index: 'a2', x: 100, y: 100 },
},
},
})
})
it('allows snapping with mid-point handles', () => {
editor.createShapesFromJsx([<TL.geo x={200} y={200} w={100} h={100} />])
editor.select(id)
editor
.pointerDown(200, 200, {
target: 'handle',
shape: getShape(),
handle: getHandles()[1],
})
.pointerMove(198, 230, undefined, { ctrlKey: true })
expect(editor.snaps.getIndicators()).toHaveLength(1)
expect(editor.getShapeHandles(id)).toHaveLength(5) // 3 real + 2
const points = editor.getShape<TLLineShape>(id)!.props.points
expect(points).toStrictEqual({
a1: { id: 'a1', index: 'a1', x: 0, y: 0 },
a1V: { id: 'a1V', index: 'a1V', x: 50, y: 80 },
a2: { id: 'a2', index: 'a2', x: 100, y: 100 },
})
})
it('allows snapping with created mid-point handles', () => {
editor.createShapesFromJsx([<TL.geo x={200} y={200} w={100} h={100} />])
// 2 actual points, plus 1 mid-points:
expect(getHandles()).toHaveLength(3)
// use a mid-point handle to create a new handle
editor
.select(id)
.pointerDown(200, 200, {
target: 'handle',
shape: getShape(),
handle: getHandles().sort(sortByIndex)[1]!,
})
.pointerMove(230, 200)
.pointerMove(240, 200)
.pointerMove(200, 200)
.pointerUp()
// 3 actual points, plus 2 mid-points:
expect(getHandles()).toHaveLength(5)
// now, try dragging the newly created handle. it should still snap:
editor
.pointerDown(200, 200, {
target: 'handle',
shape: getShape(),
handle: getHandles().sort(sortByIndex)[2],
})
.pointerMove(198, 230, undefined, { ctrlKey: true })
expect(editor.snaps.getIndicators()).toHaveLength(1)
expect(editor.getShapeHandles(id)).toHaveLength(5) // 3 real + 2
const points = editor.getShape<TLLineShape>(id)!.props.points
expect(points).toStrictEqual({
a1: { id: 'a1', index: 'a1', x: 0, y: 0 },
a1V: { id: 'a1V', index: 'a1V', x: 50, y: 80 },
a2: { id: 'a2', index: 'a2', x: 100, y: 100 },
})
})
})
describe('Snapping', () => {
beforeEach(() => {
editor.updateShape({
id: id,
type: 'line',
props: {
points: {
a1: { id: 'a1', index: 'a1', x: 0, y: 0 },
a2: { id: 'a2', index: 'a2', x: 100, y: 0 },
a3: { id: 'a3', index: 'a3', x: 100, y: 100 },
a4: { id: 'a4', index: 'a4', x: 0, y: 100 },
},
},
})
})
it('snaps endpoints to itself', () => {
editor.select(id)
editor
.pointerDown(0, 0, { target: 'handle', shape: getShape(), handle: getHandles()[0] })
.pointerMove(50, 95, undefined, { ctrlKey: true })
expect(editor.snaps.getIndicators()).toHaveLength(1)
editor.expectShapeToMatch({
id: id,
props: {
points: {
a1: { id: 'a1', index: 'a1', x: 50, y: 100 },
a2: { id: 'a2', index: 'a2', x: 100, y: 0 },
a3: { id: 'a3', index: 'a3', x: 100, y: 100 },
a4: { id: 'a4', index: 'a4', x: 0, y: 100 },
},
},
})
})
it('snaps endpoints to its vertices', () => {
editor.select(id)
editor
.pointerDown(0, 0, { target: 'handle', shape: getShape(), handle: getHandles()[0] })
.pointerMove(3, 95, undefined, { ctrlKey: true })
expect(editor.snaps.getIndicators()).toHaveLength(1)
editor.expectShapeToMatch({
id: id,
props: {
points: {
a1: { id: 'a1', index: 'a1', x: 0, y: 100 },
a2: { id: 'a2', index: 'a2', x: 100, y: 0 },
a3: { id: 'a3', index: 'a3', x: 100, y: 100 },
a4: { id: 'a4', index: 'a4', x: 0, y: 100 },
},
},
})
})
it("doesn't snap to the segment of the current handle", () => {
editor.select(id)
editor
.pointerDown(0, 0, { target: 'handle', shape: getShape(), handle: getHandles()[0] })
.pointerMove(5, 2, undefined, { ctrlKey: true })
expect(editor.snaps.getIndicators()).toHaveLength(0)
editor.expectShapeToMatch({
id: id,
props: {
points: {
a1: { id: 'a1', index: 'a1', x: 5, y: 2 },
a2: { id: 'a2', index: 'a2', x: 100, y: 0 },
a3: { id: 'a3', index: 'a3', x: 100, y: 100 },
a4: { id: 'a4', index: 'a4', x: 0, y: 100 },
},
},
})
})
it('snaps to vertices on other line shapes', () => {
editor.createShapesFromJsx([
<TL.line
x={150}
y={150}
points={{
a1: { id: 'a1', index: 'a1' as IndexKey, x: 200, y: 0 },
a2: { id: 'a2', index: 'a2' as IndexKey, x: 300, y: 0 },
}}
/>,
])
editor.select(id)
const handle = getHandles()[0]
editor
.pointerDown(handle.x, handle.y, { target: 'handle', shape: getShape(), handle })
.pointerMove(205, 1, undefined, { ctrlKey: true })
expect(editor.snaps.getIndicators()).toHaveLength(1)
editor.expectShapeToMatch({
id: id,
props: {
points: {
a1: { id: 'a1', index: 'a1', x: 200, y: 0 },
a2: { id: 'a2', index: 'a2', x: 100, y: 0 },
a3: { id: 'a3', index: 'a3', x: 100, y: 100 },
a4: { id: 'a4', index: 'a4', x: 0, y: 100 },
},
},
})
})
})
describe('Misc', () => {
it('preserves handle positions on spline type change', () => {
editor.select(id)
const shape = getShape()
const prevPoints = structuredClone(shape.props.points)
editor.updateShapes([
{
...shape,
props: {
spline: 'cubic',
},
},
])
editor.expectShapeToMatch<TLLineShape>({
id,
props: {
spline: 'cubic',
points: prevPoints,
},
})
})
it('resizes', () => {
editor.select(id)
editor
.pointerDown(150, 0, { target: 'selection', handle: 'bottom' })
.pointerMove(150, 600) // Resize shape by 0, 600
.expectToBeIn('select.resizing')
expect(editor.getShape(id)!).toMatchSnapshot('line shape after resize')
})
it('nudges', () => {
editor.select(id)
editor.nudgeShapes(editor.getSelectedShapeIds(), { x: 1, y: 0 })
editor.expectShapeToMatch<TLLineShape>({
id: id,
x: 151,
y: 150,
})
editor.nudgeShapes(editor.getSelectedShapeIds(), { x: 0, y: 10 })
editor.expectShapeToMatch<TLLineShape>({
id: id,
x: 151,
y: 160,
})
})
it('align', () => {
const boxID = createShapeId('box1')
editor.createShapes([{ id: boxID, type: 'geo', x: 500, y: 150, props: { w: 100, h: 50 } }])
const box = editor.getShape<TLGeoShape>(boxID)!
const line = getShape()
editor.select(boxID, id)
expect(editor.getShapePageBounds(box)!.maxX).not.toEqual(editor.getShapePageBounds(line)!.maxX)
editor.alignShapes(editor.getSelectedShapeIds(), 'right')
jest.advanceTimersByTime(1000)
expect(editor.getShapePageBounds(box)!.maxX).toEqual(editor.getShapePageBounds(line)!.maxX)
expect(editor.getShapePageBounds(box)!.maxY).not.toEqual(editor.getShapePageBounds(line)!.maxY)
editor.alignShapes(editor.getSelectedShapeIds(), 'bottom')
jest.advanceTimersByTime(1000)
expect(editor.getShapePageBounds(box)!.maxY).toEqual(editor.getShapePageBounds(line)!.maxY)
})
it('duplicates', () => {
editor.select(id)
editor.keyDown('Alt').pointerDown(25, 25, { target: 'shape', shape: getShape() })
editor.pointerMove(50, 50) // Move shape by 25, 25
editor.pointerUp().keyUp('Alt')
expect(Array.from(editor.getCurrentPageShapeIds().values()).length).toEqual(2)
})
it('deletes', () => {
editor.select(id)
editor.keyDown('Alt').pointerDown(25, 25, { target: 'shape', shape: getShape() })
editor.pointerMove(50, 50) // Move shape by 25, 25
editor.pointerUp().keyUp('Alt')
let ids = Array.from(editor.getCurrentPageShapeIds().values())
expect(ids.length).toEqual(2)
const duplicate = ids.filter((i) => i !== id)[0]
editor.select(duplicate)
editor.deleteShapes(editor.getSelectedShapeIds())
ids = Array.from(editor.getCurrentPageShapeIds().values())
expect(ids.length).toEqual(1)
expect(ids[0]).toEqual(id)
})
})