Tldraw/packages/tldraw/src/test/flipShapes.test.ts

581 wiersze
13 KiB
TypeScript

import {
Mat,
PI,
TLArrowShape,
TLArrowShapeProps,
TLShapeId,
TLShapePartial,
createShapeId,
getArrowBindings,
} from '@tldraw/editor'
import { TestEditor } from './TestEditor'
let editor: TestEditor
jest.useFakeTimers()
const ids = {
boxA: createShapeId('boxA'),
boxB: createShapeId('boxB'),
boxC: createShapeId('boxC'),
boxD: createShapeId('boxD'),
}
beforeEach(() => {
editor = new TestEditor()
editor.selectAll()
editor.deleteShapes(editor.getSelectedShapeIds())
editor.createShapes([
{
id: ids.boxA,
type: 'geo',
x: 0,
y: 0,
props: {
w: 100,
h: 100,
},
},
{
id: ids.boxB,
type: 'geo',
x: 150,
y: 150,
props: {
w: 50,
h: 50,
},
},
{
id: ids.boxC,
type: 'geo',
x: 300,
y: 300,
props: {
w: 100,
h: 100,
},
},
])
})
describe('When flipping horizontally', () => {
it('Flips the selected shapes', () => {
editor.select(ids.boxA, ids.boxB, ids.boxC)
editor.mark('flipped')
editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')
editor.expectShapeToMatch(
{
id: ids.boxA,
type: 'geo',
x: 300,
},
{
id: ids.boxB,
type: 'geo',
x: 200,
},
{
id: ids.boxC,
type: 'geo',
x: 0,
}
)
})
it('Flips the provided shapes', () => {
editor.mark('flipped')
editor.flipShapes([ids.boxA, ids.boxB], 'horizontal')
editor.expectShapeToMatch(
{
id: ids.boxA,
type: 'geo',
x: 100,
},
{
id: ids.boxB,
type: 'geo',
x: 0,
}
)
})
it('Flips rotated shapes', () => {
editor.updateShapes([{ id: ids.boxA, type: 'geo', rotation: PI }])
editor.select(ids.boxA, ids.boxB)
const a = editor.getSelectionPageBounds()
editor.mark('flipped')
editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')
const b = editor.getSelectionPageBounds()
expect(a!).toCloselyMatchObject(b!)
editor.expectShapeToMatch(
{
id: ids.boxA,
type: 'geo',
x: 200,
},
{
id: ids.boxB,
type: 'geo',
x: -100,
}
)
})
it('Flips the children of rotated shapes', () => {
editor.reparentShapes([ids.boxB], ids.boxA)
editor.updateShapes([{ id: ids.boxA, type: 'geo', rotation: PI }])
editor.select(ids.boxB, ids.boxC)
const a = editor.getSelectionPageBounds()
editor.mark('flipped')
editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')
const b = editor.getSelectionPageBounds()
expect(a).toCloselyMatchObject(b!)
})
})
describe('When flipping vertically', () => {
it('Flips the selected shapes', () => {
editor.select(ids.boxA, ids.boxB, ids.boxC)
editor.mark('flipped')
editor.flipShapes(editor.getSelectedShapeIds(), 'vertical')
editor.expectShapeToMatch(
{
id: ids.boxA,
type: 'geo',
y: 300,
},
{
id: ids.boxB,
type: 'geo',
y: 200,
},
{
id: ids.boxC,
type: 'geo',
y: 0,
}
)
})
it('Flips the provided shapes', () => {
editor.mark('flipped')
editor.flipShapes([ids.boxA, ids.boxB], 'vertical')
editor.expectShapeToMatch(
{
id: ids.boxA,
type: 'geo',
y: 100,
},
{
id: ids.boxB,
type: 'geo',
y: 0,
}
)
})
it('Flips rotated shapes', () => {
editor.updateShapes([{ id: ids.boxA, type: 'geo', rotation: PI }])
editor.select(ids.boxA, ids.boxB)
const a = editor.getSelectionPageBounds()
editor.mark('flipped')
editor.flipShapes(editor.getSelectedShapeIds(), 'vertical')
const b = editor.getSelectionPageBounds()
expect(a).toCloselyMatchObject(b!)
editor.expectShapeToMatch(
{
id: ids.boxA,
type: 'geo',
y: 200,
},
{
id: ids.boxB,
type: 'geo',
y: -100,
}
)
})
it('Flips the children of rotated shapes', () => {
editor.reparentShapes([ids.boxB], ids.boxA)
editor.updateShapes([{ id: ids.boxA, type: 'geo', rotation: PI }])
editor.select(ids.boxB, ids.boxC)
const a = editor.getSelectionPageBounds()
editor.mark('flipped')
editor.flipShapes(editor.getSelectedShapeIds(), 'vertical')
const b = editor.getSelectionPageBounds()
expect(a).toCloselyMatchObject(b!)
})
})
it('Preserves the selection bounds.', () => {
editor.selectAll()
const a = editor.getSelectionPageBounds()
editor.mark('flipped')
editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')
const b = editor.getSelectionPageBounds()
expect(a).toMatchObject(b!)
editor.mark('flipped')
editor.flipShapes(editor.getSelectedShapeIds(), 'vertical')
const c = editor.getSelectionPageBounds()
expect(a).toMatchObject(c!)
})
it('Does, undoes and redoes', () => {
editor.mark('flip vertical')
editor.flipShapes([ids.boxA, ids.boxB], 'vertical')
editor.expectShapeToMatch(
{
id: ids.boxA,
type: 'geo',
y: 100,
},
{
id: ids.boxB,
type: 'geo',
y: 0,
}
)
editor.undo()
editor.expectShapeToMatch(
{
id: ids.boxA,
type: 'geo',
y: 0,
},
{
id: ids.boxB,
type: 'geo',
y: 150,
}
)
editor.redo()
editor.expectShapeToMatch(
{
id: ids.boxA,
type: 'geo',
y: 100,
},
{
id: ids.boxB,
type: 'geo',
y: 0,
}
)
})
describe('When multiple shapes are selected', () => {
it.todo('Flips the shape positions according to the selection rotation')
it.todo('Flips using the selection rotation when the shapes have a common selection rotation')
it.todo('Flips using the main axis when shapes do not have a common selection rotation')
it.todo('Flips when shapes have different parents')
})
describe('When one shape is selected', () => {
it('Does nothing if the shape is not a group', () => {
const before = editor.getShape(ids.boxA)!
editor.select(ids.boxA)
editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')
expect(editor.getShape(ids.boxA)).toMatchObject(before)
})
it('Flips the direct child shape positions if the shape is a group', async () => {
const fn = jest.fn()
editor.selectAll()
editor.groupShapes(editor.getSelectedShapeIds()) // this will also select the new group
const groupBefore = editor.getSelectedShapes()[0]
editor.on('change', fn)
editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')
// The change event should have been called
jest.runOnlyPendingTimers()
expect(fn).toHaveBeenCalled()
editor.expectShapeToMatch(
{
...groupBefore, // group should not have changed
},
{
id: ids.boxA, // group's children shapes should have been flipped
type: 'geo',
parentId: groupBefore.id,
x: 300,
y: 0,
},
{
id: ids.boxB,
type: 'geo',
parentId: groupBefore.id,
x: 200,
y: 150,
},
{
id: ids.boxC,
type: 'geo',
parentId: groupBefore.id,
x: 0,
y: 300,
}
)
})
it.todo('Flips line and arrow shapes when their parent group is flipped')
})
describe('flipping rotated shapes', () => {
const arrowLength = 100
const diamondRadius = Math.cos(Math.PI / 4) * arrowLength
const topPoint = { x: 0, y: 0 }
const rightPoint = { x: diamondRadius, y: diamondRadius }
const bottomPoint = { x: 0, y: 2 * diamondRadius }
const leftPoint = { x: -diamondRadius, y: diamondRadius }
const ids = {
arrowA: createShapeId('arrowA'),
arrowB: createShapeId('arrowB'),
arrowC: createShapeId('arrowC'),
arrowD: createShapeId('arrowD'),
}
beforeEach(() => {
editor.selectAll().deleteShapes(editor.getSelectedShapeIds())
const props: Partial<TLArrowShapeProps> = {
start: {
x: 0,
y: 0,
},
end: {
x: 100,
y: 0,
},
}
// create a diamond of rotated arrows, pointing clockwise, with the top point at 0,0
editor.createShapes([
{
// top to right
type: 'arrow',
id: ids.arrowA,
...topPoint,
rotation: Math.PI / 4,
props,
},
{
// right to bottom
type: 'arrow',
id: ids.arrowB,
...rightPoint,
rotation: (Math.PI * 3) / 4,
props,
},
{
// bottom to left
type: 'arrow',
id: ids.arrowC,
...bottomPoint,
rotation: (Math.PI * 5) / 4,
props,
},
{
// left to top
type: 'arrow',
id: ids.arrowD,
...leftPoint,
rotation: (Math.PI * 7) / 4,
props,
},
])
editor.select(ids.arrowA, ids.arrowB, ids.arrowC, ids.arrowD)
})
const getStartAndEndPoints = (id: TLShapeId) => {
const transform = editor.getShapePageTransform(id)
if (!transform) throw new Error('no transform')
const arrow = editor.getShape<TLArrowShape>(id)!
const bindings = getArrowBindings(editor, arrow)
if (bindings.start || bindings.end) throw new Error('not a point')
const start = Mat.applyToPoint(transform, arrow.props.start)
const end = Mat.applyToPoint(transform, arrow.props.end)
return { start, end }
}
test('flipping horizontally', () => {
editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')
// now arrow A should be pointing from top to left
let { start, end } = getStartAndEndPoints(ids.arrowA)
expect(start).toCloselyMatchObject(topPoint)
expect(end).toCloselyMatchObject(leftPoint)
// now arrow B should be pointing from left to bottom
;({ start, end } = getStartAndEndPoints(ids.arrowB))
expect(start).toCloselyMatchObject(leftPoint)
expect(end).toCloselyMatchObject(bottomPoint)
// now arrow C should be pointing from bottom to right
;({ start, end } = getStartAndEndPoints(ids.arrowC))
expect(start).toCloselyMatchObject(bottomPoint)
expect(end).toCloselyMatchObject(rightPoint)
// now arrow D should be pointing from right to top
;({ start, end } = getStartAndEndPoints(ids.arrowD))
expect(start).toCloselyMatchObject(rightPoint)
expect(end).toCloselyMatchObject(topPoint)
})
test('flipping vertically', () => {
editor.flipShapes(editor.getSelectedShapeIds(), 'vertical')
// arrows that have height 0 get nudged by a pixel when flipped vertically
// so we need to use a fairly loose tolerance
// now arrow A should be pointing from bottom to right
let { start, end } = getStartAndEndPoints(ids.arrowA)
expect(start).toCloselyMatchObject(bottomPoint, 5)
expect(end).toCloselyMatchObject(rightPoint, 5)
// now arrow B should be pointing from right to top
;({ start, end } = getStartAndEndPoints(ids.arrowB))
expect(start).toCloselyMatchObject(rightPoint, 5)
expect(end).toCloselyMatchObject(topPoint, 5)
// now arrow C should be pointing from top to left
;({ start, end } = getStartAndEndPoints(ids.arrowC))
expect(start).toCloselyMatchObject(topPoint, 5)
expect(end).toCloselyMatchObject(leftPoint, 5)
// now arrow D should be pointing from left to bottom
;({ start, end } = getStartAndEndPoints(ids.arrowD))
expect(start).toCloselyMatchObject(leftPoint, 5)
expect(end).toCloselyMatchObject(bottomPoint, 5)
})
})
describe('When flipping shapes that include arrows', () => {
let shapes: TLShapePartial[]
beforeEach(() => {
const box1 = createShapeId()
const box2 = createShapeId()
const box3 = createShapeId()
shapes = [
{
id: box1,
type: 'geo',
x: 0,
y: 0,
},
{
id: box2,
type: 'geo',
x: 300,
y: 300,
},
{
id: box3,
type: 'geo',
x: 300,
y: 0,
},
{
id: createShapeId(),
type: 'arrow',
x: 50,
y: 50,
props: {
bend: 200,
start: {
type: 'binding',
normalizedAnchor: { x: 0.75, y: 0.75 },
boundShapeId: box1,
isExact: false,
isPrecise: true,
},
end: {
type: 'binding',
normalizedAnchor: { x: 0.25, y: 0.25 },
boundShapeId: box1,
isExact: false,
isPrecise: true,
},
},
},
{
id: createShapeId(),
type: 'arrow',
x: 50,
y: 50,
props: {
bend: -200,
start: {
type: 'binding',
normalizedAnchor: { x: 0.75, y: 0.75 },
boundShapeId: box1,
isExact: false,
isPrecise: true,
},
end: {
type: 'binding',
normalizedAnchor: { x: 0.25, y: 0.25 },
boundShapeId: box1,
isExact: false,
isPrecise: true,
},
},
},
{
id: createShapeId(),
type: 'arrow',
x: 50,
y: 50,
props: {
bend: -200,
start: {
type: 'binding',
normalizedAnchor: { x: 0.75, y: 0.75 },
boundShapeId: box1,
isExact: false,
isPrecise: true,
},
end: {
type: 'binding',
normalizedAnchor: { x: 0.25, y: 0.25 },
boundShapeId: box3,
isExact: false,
isPrecise: true,
},
},
},
]
})
it('Flips horizontally', () => {
editor.selectAll().deleteShapes(editor.getSelectedShapeIds()).createShapes(shapes)
const boundsBefore = editor.getSelectionRotatedPageBounds()!
editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')
expect(editor.getSelectionRotatedPageBounds()).toCloselyMatchObject(boundsBefore)
})
it('Flips vertically', () => {
editor.selectAll().deleteShapes(editor.getSelectedShapeIds()).createShapes(shapes)
const boundsBefore = editor.getSelectionRotatedPageBounds()!
editor.flipShapes(editor.getSelectedShapeIds(), 'vertical')
expect(editor.getSelectionRotatedPageBounds()).toCloselyMatchObject(boundsBefore)
})
})