import {
	GapsSnapIndicator,
	PI,
	PI2,
	PointsSnapIndicator,
	RotateCorner,
	TLGeoShape,
	TLSelectionHandle,
	TLShapeId,
	TLShapePartial,
	TLTextShape,
	Vec,
	canonicalizeRotation,
	createShapeId,
	rotateSelectionHandle,
	toRichText,
} from '@tldraw/editor'
import { vi } from 'vitest'
import { NoteShapeUtil } from '../lib/shapes/note/NoteShapeUtil'
import { TestEditor } from './TestEditor'
import { getSnapLines } from './getSnapLines'
import { roundedBox } from './roundedBox'
import { createDrawSegments } from './test-jsx'

vi.useFakeTimers()

const ORDERED_ROTATE_CORNERS: TLSelectionHandle[] = [
	'top_left_rotate',
	'top_right_rotate',
	'bottom_right_rotate',
	'bottom_left_rotate',
]

export function rotateRotateCorner(corner: RotateCorner, rotation: number): TLSelectionHandle {
	// first find out how many 90deg we need to rotate by
	rotation = rotation % PI2
	const numSteps = Math.round(rotation / (PI / 2))

	const currentIndex = ORDERED_ROTATE_CORNERS.indexOf(corner)
	return ORDERED_ROTATE_CORNERS[(currentIndex + numSteps) % ORDERED_ROTATE_CORNERS.length]
}

const box = (id: TLShapeId, x: number, y: number, w = 10, h = 10): TLShapePartial => ({
	type: 'geo',
	id,
	x,
	y,
	props: {
		w,
		h,
	},
})

const roundedPageBounds = (shapeId: TLShapeId, accuracy = 0.01) => {
	return roundedBox(editor.getShapePageBounds(shapeId)!, accuracy)
}

// function getGapAndPointLines(snaps: SnapLine[]) {
//   const gapLines = snaps.filter((snap) => snap.type === 'gaps') as GapsSnapLine[]
//   const pointLines = snaps.filter((snap) => snap.type === 'points') as PointsSnapLine[]
//   return { gapLines, pointLines }
// }

let editor: TestEditor

afterEach(() => {
	editor?.dispose()
})

const ids = {
	boxA: createShapeId('boxA'),
	boxB: createShapeId('boxB'),
	boxC: createShapeId('boxC'),
	boxD: createShapeId('boxD'),

	boxX: createShapeId('boxX'),

	lineA: createShapeId('lineA'),

	iconA: createShapeId('iconA'),
}

beforeEach(() => {
	editor = new TestEditor()

	editor.createShapes([
		{
			id: ids.boxA,
			type: 'geo',
			x: 10,
			y: 10,
			props: {
				w: 100,
				h: 100,
			},
		},
		{
			id: ids.boxB,
			type: 'geo',
			parentId: ids.boxA,
			x: 100,
			y: 100,
			props: {
				w: 100,
				h: 100,
			},
		},
		{
			id: ids.boxC,
			type: 'geo',
			parentId: ids.boxA,
			x: 200,
			y: 200,
			props: {
				w: 100,
				h: 100,
			},
		},
	])
})

describe('When pointing a resizer handle...', () => {
	it('enters and exits the pointing_resize_handle state', () => {
		editor
			.select(ids.boxA)
			.pointerDown(60, 60, {
				target: 'selection',
				handle: 'bottom_right',
			})
			.expectToBeIn('select.pointing_resize_handle')
			.pointerUp()
			.expectToBeIn('select.idle')
	})

	it('exits the pointing_resize_handle state when cancelled', () => {
		editor
			.select(ids.boxA)
			.pointerDown(60, 60, {
				target: 'selection',
				handle: 'bottom_right',
			})
			.expectToBeIn('select.pointing_resize_handle')
			.cancel()
			.expectToBeIn('select.idle')
	})
})

describe('When dragging a resize handle...', () => {
	it('enters and exits the resizing state', () => {
		editor
			.select(ids.boxA)
			.pointerDown(60, 60, {
				target: 'selection',
				handle: 'bottom_right',
			})
			.pointerMove(10, 10)
			.expectToBeIn('select.resizing')
	})

	it('exits the resizing state on pointer up', () => {
		editor
			.select(ids.boxA)
			.pointerDown(60, 60, {
				target: 'selection',
				handle: 'bottom_right',
			})
			.pointerMove(10, 10)
			.pointerUp()
			.expectToBeIn('select.idle')
	})

	it('exits the resizing state when cancelled', () => {
		editor
			.select(ids.boxA)
			.pointerDown(60, 60, {
				target: 'selection',
				handle: 'bottom_right',
			})
			.pointerMove(10, 10)
			.cancel()
			.expectToBeIn('select.idle')
	})
})

describe('When resizing...', () => {
	it('Resizes a single shape from the top left', () => {
		editor
			.select(ids.boxA)
			.pointerDown(10, 10, {
				type: 'pointer',
				target: 'selection',
				handle: 'top_left',
			})
			.expectShapeToMatch({ id: ids.boxA, x: 10, y: 10, props: { w: 100, h: 100 } })
			.pointerMove(0, 0)
			.expectShapeToMatch({ id: ids.boxA, x: 0, y: 0, props: { w: 110, h: 110 } })
	})

	it('Resizes a single shape from the top right', () => {
		editor
			.select(ids.boxA)
			.pointerDown(60, 10, {
				target: 'selection',
				handle: 'top_right',
			})
			.expectShapeToMatch({ id: ids.boxA, x: 10, y: 10, props: { w: 100, h: 100 } })
			.pointerMove(70, 0)
			.expectShapeToMatch({ id: ids.boxA, x: 10, y: 0, props: { w: 110, h: 110 } })
	})

	it('Resizes a single shape from the bottom right', () => {
		editor
			.select(ids.boxA)
			.pointerDown(60, 60, {
				target: 'selection',
				handle: 'bottom_right',
			})
			.expectShapeToMatch({ id: ids.boxA, x: 10, y: 10, props: { w: 100, h: 100 } })
			.pointerMove(70, 70)
			.expectShapeToMatch({ id: ids.boxA, x: 10, y: 10, props: { w: 110, h: 110 } })
	})

	it('Resizes a single shape from the bottom left', () => {
		editor
			.select(ids.boxA)
			.pointerDown(10, 60, {
				target: 'selection',
				handle: 'bottom_left',
			})
			.expectShapeToMatch({ id: ids.boxA, x: 10, y: 10, props: { w: 100, h: 100 } })
			.pointerMove(0, 70)
			.expectShapeToMatch({ id: ids.boxA, x: 0, y: 10, props: { w: 110, h: 110 } })
	})
})

describe('When resizing a rotated shape...', () => {
	it.each([
		0,
		Math.PI / 2,
		// Math.PI / 4, Math.PI
	])('Resizes a shape rotated %i from the top left', (rotation) => {
		const offset = new Vec(10, 10)

		// Rotate the shape by $rotation from its top left corner

		editor.select(ids.boxA)

		const initialPagePoint = editor.getShapePageTransform(ids.boxA)!.point()

		const pt0 = Vec.From(initialPagePoint)
		const pt1 = Vec.RotWith(initialPagePoint, editor.getSelectionPageBounds()!.center, rotation)
		const pt2 = Vec.Sub(initialPagePoint, offset).rotWith(
			editor.getSelectionPageBounds()!.center!,
			rotation
		)

		editor
			.pointerDown(pt0.x, pt0.y, {
				target: 'selection',
				handle: 'top_left_rotate',
			})
			.pointerMove(pt1.x, pt1.y)
			.pointerUp()

		// The shape's point should now be at pt1 (it rotates from the top left corner)

		expect(editor.getShapePageTransform(ids.boxA)!.rotation()).toBeCloseTo(rotation)
		expect(editor.getShapePageTransform(ids.boxA)!.point()).toCloselyMatchObject(pt1)

		// Resize by moving the top left resize handle to pt2. Should be a delta of [10, 10].

		expect(Vec.Dist(editor.getShapePageTransform(ids.boxA)!.point(), pt2)).toBeCloseTo(offset.len())

		editor
			.pointerDown(pt1.x, pt1.y, {
				target: 'selection',
				handle: 'top_left',
			})
			.pointerMove(pt2.x, pt2.y)
			.pointerUp()

		// The shape should have moved its point to pt2 and be delta bigger.

		expect(editor.getShapePageTransform(ids.boxA)!.point()).toCloselyMatchObject(pt2)
		editor.expectShapeToMatch({ id: ids.boxA, props: { w: 110, h: 110 } })
	})
})

describe('When resizing mulitple shapes...', () => {
	it.each([
		[0, 0, 0, 0],
		[10, 10, 0, 0],
		[0, 0, Math.PI, 0],
		[10, 10, 0, Math.PI / 4],
	])(
		'Resizes B and C when: \n\tA = { x: %s, y: %s, rotation: %s }\n\tB = { rotation: %s }',
		(x, y, rotation, rotationB) => {
			const shapeA = editor.getShape(ids.boxA)!
			const shapeB = editor.getShape(ids.boxB)!
			const shapeC = editor.getShape(ids.boxC)!

			editor.updateShapes([
				{
					id: ids.boxA,
					type: 'geo',
					x,
					y,
					rotation,
				},
				{
					id: ids.boxB,
					parentId: ids.boxA,
					type: 'geo',
					x: 100,
					y: 100,
					rotation: rotationB,
				},
				{
					id: ids.boxC,
					parentId: ids.boxA,
					type: 'geo',
					x: 200,
					y: 200,
					rotation: rotationB,
				},
			])

			// Rotate the shape by $rotation from its top left corner

			const rotateStart = editor.getShapePageTransform(ids.boxA)!.point()
			const rotateCenter = editor.getPageCenter(shapeA)!
			const rotateEnd = Vec.RotWith(rotateStart, rotateCenter, rotation)

			editor
				.select(ids.boxA)
				.pointerDown(rotateStart.x, rotateStart.y, {
					target: 'selection',
					handle: rotateRotateCorner('top_left_rotate', -editor.getSelectionRotation()),
				})
				.pointerMove(rotateEnd.x, rotateEnd.y)
				.pointerUp()

			expect(canonicalizeRotation(shapeA.rotation) % Math.PI).toBeCloseTo(
				canonicalizeRotation(rotation) % Math.PI
			)
			expect(editor.getPageRotation(shapeB)).toBeCloseTo(rotation + rotationB)
			expect(editor.getPageRotation(shapeC)).toBeCloseTo(rotation + rotationB)

			editor.select(ids.boxB, ids.boxC)

			// Now drag to resize the selection bounds

			const initialBounds = editor.getSelectionPageBounds()!

			// oddly rotated shapes maintain aspect ratio when being resized (for now)
			const aspectRatio = initialBounds.width / initialBounds.height
			const offsetX = initialBounds.width + 200
			const offset = new Vec(offsetX, offsetX / aspectRatio)
			const resizeStart = initialBounds.point
			const resizeEnd = Vec.Sub(resizeStart, offset)

			expect(Vec.Dist(resizeStart, resizeEnd)).toBeCloseTo(offset.len())
			expect(
				Vec.Min(editor.getShapePageBounds(shapeB)!.point, editor.getShapePageBounds(shapeC)!.point)
			).toCloselyMatchObject(resizeStart)

			editor
				.pointerDown(resizeStart.x, resizeStart.y, {
					target: 'selection',
					handle: rotateSelectionHandle('top_left', -editor.getSelectionRotation()),
				})
				.pointerMove(resizeStart.x - 10, resizeStart.y - 10)
				.pointerMove(resizeEnd.x, resizeEnd.y)
				.pointerUp()

			expect(editor.getSelectionPageBounds()!.point).toCloselyMatchObject(resizeEnd)
			expect(new Vec(initialBounds.maxX, initialBounds.maxY)).toCloselyMatchObject(
				new Vec(editor.getSelectionPageBounds()!.maxX, editor.getSelectionPageBounds()!.maxY)
			)
		}
	)
})

describe('Reisizing a selection of multiple shapes', () => {
	beforeEach(() => {
		//    0          10        20         30
		//
		//     ┌──────────┐
		//     │          │
		//     │          │
		//     │    A     │
		//     │          │
		//     │          │
		// 10  └──────────┘
		//
		//
		//
		//
		// 20                       ┌──────────┐
		//                          │          │
		//                          │          │
		//                          │    B     │
		//                          │          │
		//                          │          │
		// 30                       └──────────┘
		editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 20, 20)])
	})
	it('works correctly when the shapes are not rotated', () => {
		editor.select(ids.boxA, ids.boxB)
		// shrink

		// 0                 15
		// ┌──────────────────┐
		// │  ┌───┐           │
		// │  │ A │           │
		// │  └───┘           │
		// │                  │
		// │           ┌───┐  │
		// │           │ B │  │
		// │           └───┘  │
		// └──────────────────O

		editor.pointerDown(30, 30, { target: 'selection', handle: 'bottom_right' })
		editor.pointerMove(15, 15)

		expect(roundedBox(editor.getSelectionPageBounds()!)).toMatchObject({ w: 15, h: 15 })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 0, y: 0, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 10, y: 10, w: 5, h: 5 })

		// strech horizontally

		//  0                       20               40                     60
		//
		//  ┌──────────────────────────────────────────────────────────────────┐
		//  │ ┌───────────────────────┐                                        │
		//  │ │                       │                                        │
		//  │ │           A           │                                        │
		//  │ │                       │                                        │
		//  │ └───────────────────────┘                                        │
		//  │                                                                  │
		//  │                                                                  │
		//  │                                        ┌───────────────────────┐ │
		//  │                                        │                       │ │
		//  │                                        │           B           │ │
		//  │                                        │                       │ │
		//  │                                        └───────────────────────┘ │
		//  └──────────────────────────────────────────────────────────────────O

		editor.pointerMove(60, 30)
		expect(roundedBox(editor.getSelectionPageBounds()!)).toMatchObject({ w: 60, h: 30 })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 0, y: 0, w: 20, h: 10 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 40, y: 20, w: 20, h: 10 })
		// stretch vertically
		//        0          10        20         30
		//       ┌─────────────────────────────────┐
		//       │ ┌──────────┐                    │
		//       │ │          │                    │
		//       │ │          │                    │
		//       │ │          │                    │
		//       │ │          │                    │
		//       │ │    A     │                    │
		//       │ │          │                    │
		//       │ │          │                    │
		//       │ │          │                    │
		// 20    │ └──────────┘                    │
		//       │                                 │
		//       │                                 │
		//       │                                 │
		//       │                                 │
		//       │                                 │
		//       │                                 │
		//       │                                 │
		// 40    │                    ┌──────────┐ │
		//       │                    │          │ │
		//       │                    │          │ │
		//       │                    │          │ │
		//       │                    │     B    │ │
		//       │                    │          │ │
		//       │                    │          │ │
		//       │                    │          │ │
		//       │                    │          │ │
		// 60    │                    └──────────┘ │
		//       └─────────────────────────────────O
		editor.pointerMove(30, 60)
		expect(roundedBox(editor.getSelectionPageBounds()!)).toMatchObject({ w: 30, h: 60 })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 0, y: 0, w: 10, h: 20 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 20, y: 40, w: 10, h: 20 })

		// invert + shrink

		// -15               0
		//   O───────────────┐
		//   │ ┌───┐         │
		//   │ │ B │         │
		//   │ └───┘         │
		//   │               │
		//   │         ┌───┐ │
		//   │         │ A │ │
		//   │         └───┘ │
		//   └───────────────┘
		editor.pointerMove(-15, -15)
		expect(roundedBox(editor.getSelectionPageBounds()!)).toMatchObject({ w: 15, h: 15 })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: -5, y: -5, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: -15, y: -15, w: 5, h: 5 })

		// resize from center

		//       -15        5    15  25         45
		//     ┌───────────────────────────────────┐
		//     │ ┌──────────┐                      │
		//     │ │          │                      │
		//     │ │    A     │                      │
		//     │ │          │                      │
		//     │ └──────────┘                      │
		//     │                                   │
		//     │                 x                 │
		//     │                                   │
		//     │                      ┌──────────┐ │
		//     │                      │          │ │
		//     │                      │    B     │ │
		//     │                      │          │ │
		//     │                      └──────────┘ │
		//     └───────────────────────────────────O
		editor.pointerMove(45, 45, { altKey: true })

		expect(roundedBox(editor.getSelectionPageBounds()!)).toMatchObject({
			w: 60,
			h: 60,
			x: -15,
			y: -15,
		})
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: -15, y: -15, w: 20, h: 20 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 25, y: 25, w: 20, h: 20 })

		// resize with aspect ratio locked

		// 0                 15
		// ┌──────────────────┐
		// │  ┌───┐           │
		// │  │ A │           │
		// │  └───┘           │
		// │                  │ <- mouse is here
		// │           ┌───┐  │
		// │           │ B │  │
		// │           └───┘  │
		// └──────────────────O

		editor.pointerMove(15, 8, { altKey: false, shiftKey: true })
		vi.advanceTimersByTime(200)

		expect(roundedBox(editor.getSelectionPageBounds()!)).toMatchObject({ w: 15, h: 15 })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 0, y: 0, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 10, y: 10, w: 5, h: 5 })

		// resize from center with aspect ratio locked

		//       -15        5    15  25         45
		//     ┌───────────────────────────────────┐
		//     │ ┌──────────┐                      │
		//     │ │          │                      │
		//     │ │    A     │                      │
		//     │ │          │                      │
		//     │ └──────────┘                      │
		//     │                                   │
		//     │                 x                 │ <- mouse is here
		//     │                                   │
		//     │                      ┌──────────┐ │
		//     │                      │          │ │
		//     │                      │    B     │ │
		//     │                      │          │ │
		//     │                      └──────────┘ │
		//     └───────────────────────────────────O
		editor.pointerMove(45, 16, { altKey: true, shiftKey: true })
		expect(roundedBox(editor.getSelectionPageBounds()!)).toMatchObject({
			w: 60,
			h: 60,
			x: -15,
			y: -15,
		})
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: -15, y: -15, w: 20, h: 20 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 25, y: 25, w: 20, h: 20 })
	})

	it('works the same when shapes are rotated by a multiple of 90 degrees', () => {
		// rotate A by 90 degrees
		editor.select(ids.boxA)
		editor.pointerDown(0, 0, { target: 'selection', handle: 'top_left_rotate' })
		editor.pointerMove(10, 0, { shiftKey: true })
		editor.pointerUp(10, 0, { shiftKey: false })

		expect(editor.getShape(ids.boxA)!.rotation).toBeCloseTo(PI / 2)

		// rotate B by -90 degrees
		editor.select(ids.boxB)
		editor.pointerDown(30, 20, { target: 'selection', handle: 'top_left_rotate' })
		editor.pointerMove(20, 20, { shiftKey: true })
		editor.pointerUp(20, 20, { shiftKey: false })
		vi.advanceTimersByTime(200)

		expect(editor.getShape(ids.boxB)!.rotation).toBeCloseTo(canonicalizeRotation(-PI / 2))

		editor.select(ids.boxA, ids.boxB)
		// shrink

		// 0                 15
		// ┌──────────────────┐
		// │  ┌───┐           │
		// │  │ A │           │
		// │  └───┘           │
		// │                  │
		// │           ┌───┐  │
		// │           │ B │  │
		// │           └───┘  │
		// └──────────────────O

		editor.pointerDown(30, 30, {
			target: 'selection',
			handle: rotateSelectionHandle('bottom_right', -editor.getSelectionRotation()),
		})
		editor.pointerMove(15, 15)

		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 0, y: 0, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 10, y: 10, w: 5, h: 5 })

		expect(roundedBox(editor.getSelectionPageBounds()!)).toMatchObject({ w: 15, h: 15 })

		// strech horizontally

		//  0                       20               40                     60
		//
		//  ┌──────────────────────────────────────────────────────────────────┐
		//  │ ┌───────────────────────┐                                        │
		//  │ │                       │                                        │
		//  │ │           A           │                                        │
		//  │ │                       │                                        │
		//  │ └───────────────────────┘                                        │
		//  │                                                                  │
		//  │                                                                  │
		//  │                                        ┌───────────────────────┐ │
		//  │                                        │                       │ │
		//  │                                        │           B           │ │
		//  │                                        │                       │ │
		//  │                                        └───────────────────────┘ │
		//  └──────────────────────────────────────────────────────────────────O

		editor.pointerMove(60, 30)
		expect(roundedBox(editor.getSelectionPageBounds()!)).toMatchObject({ w: 60, h: 30 })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 0, y: 0, w: 20, h: 10 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 40, y: 20, w: 20, h: 10 })
		// stretch vertically
		//        0          10        20         30
		//       ┌─────────────────────────────────┐
		//       │ ┌──────────┐                    │
		//       │ │          │                    │
		//       │ │          │                    │
		//       │ │          │                    │
		//       │ │          │                    │
		//       │ │    A     │                    │
		//       │ │          │                    │
		//       │ │          │                    │
		//       │ │          │                    │
		// 20    │ └──────────┘                    │
		//       │                                 │
		//       │                                 │
		//       │                                 │
		//       │                                 │
		//       │                                 │
		//       │                                 │
		//       │                                 │
		// 40    │                    ┌──────────┐ │
		//       │                    │          │ │
		//       │                    │          │ │
		//       │                    │          │ │
		//       │                    │     B    │ │
		//       │                    │          │ │
		//       │                    │          │ │
		//       │                    │          │ │
		//       │                    │          │ │
		// 60    │                    └──────────┘ │
		//       └─────────────────────────────────O
		editor.pointerMove(30, 60)
		expect(roundedBox(editor.getSelectionPageBounds()!)).toMatchObject({ w: 30, h: 60 })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 0, y: 0, w: 10, h: 20 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 20, y: 40, w: 10, h: 20 })

		// invert + shrink

		// -15               0
		//   O───────────────┐
		//   │ ┌───┐         │
		//   │ │ B │         │
		//   │ └───┘         │
		//   │               │
		//   │         ┌───┐ │
		//   │         │ A │ │
		//   │         └───┘ │
		//   └───────────────┘
		editor.pointerMove(-15, -15)
		expect(roundedBox(editor.getSelectionPageBounds()!)).toMatchObject({ w: 15, h: 15 })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: -5, y: -5, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: -15, y: -15, w: 5, h: 5 })

		// resize from center

		//       -15        5    15  25         45
		//     ┌───────────────────────────────────┐
		//     │ ┌──────────┐                      │
		//     │ │          │                      │
		//     │ │    A     │                      │
		//     │ │          │                      │
		//     │ └──────────┘                      │
		//     │                                   │
		//     │                 x                 │
		//     │                                   │
		//     │                      ┌──────────┐ │
		//     │                      │          │ │
		//     │                      │    B     │ │
		//     │                      │          │ │
		//     │                      └──────────┘ │
		//     └───────────────────────────────────O
		editor.pointerMove(45, 45, { altKey: true })
		expect(roundedBox(editor.getSelectionPageBounds()!)).toMatchObject({
			w: 60,
			h: 60,
			x: -15,
			y: -15,
		})
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: -15, y: -15, w: 20, h: 20 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 25, y: 25, w: 20, h: 20 })

		// resize with aspect ratio locked

		// 0                 15
		// ┌──────────────────┐
		// │  ┌───┐           │
		// │  │ A │           │
		// │  └───┘           │
		// │                  │ <- mouse is here
		// │           ┌───┐  │
		// │           │ B │  │
		// │           └───┘  │
		// └──────────────────O

		editor.pointerMove(15, 8, { altKey: false, shiftKey: true })
		vi.advanceTimersByTime(200)

		expect(roundedBox(editor.getSelectionPageBounds()!)).toMatchObject({ w: 15, h: 15 })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 0, y: 0, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 10, y: 10, w: 5, h: 5 })

		// resize from center with aspect ratio locked

		//       -15        5    15  25         45
		//     ┌───────────────────────────────────┐
		//     │ ┌──────────┐                      │
		//     │ │          │                      │
		//     │ │    A     │                      │
		//     │ │          │                      │
		//     │ └──────────┘                      │
		//     │                                   │
		//     │                 x                 │ <- mouse is here
		//     │                                   │
		//     │                      ┌──────────┐ │
		//     │                      │          │ │
		//     │                      │    B     │ │
		//     │                      │          │ │
		//     │                      └──────────┘ │
		//     └───────────────────────────────────O
		editor.pointerMove(45, 16, { altKey: true, shiftKey: true })
		expect(roundedBox(editor.getSelectionPageBounds()!)).toMatchObject({
			w: 60,
			h: 60,
			x: -15,
			y: -15,
		})
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: -15, y: -15, w: 20, h: 20 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 25, y: 25, w: 20, h: 20 })
	})
	it('will not change the apsect ratio on shapes that have been rotated by some number that is not a multiple of 90 degrees', () => {
		// rotate B a tiny bit
		editor.select(ids.boxB)
		editor.pointerDown(30, 20, { target: 'selection', handle: 'top_left_rotate' })
		editor.pointerMove(30, 21)
		editor.pointerUp(30, 21)
		// strech horizontally

		//  0                       20               40                     60
		//  ┌──────────────────────────────────────────────────────────────────┐
		//  │ ┌───────────────────────┐                                        │
		//  │ │                       │                                        │
		//  │ │           A           │                                        │
		//  │ │                       │                                        │
		//  │ └───────────────────────┘                                        │
		//  │                                                                  │
		//  │                                                                  │
		//  │                                                   ┌────────────┐ │
		//  │                                                   │            │ │
		//  │                                                   │     B      │ │
		//  │                                                   │            │ │
		//  │                                                   └────────────┘ │
		//  └──────────────────────────────────────────────────────────────────O

		editor.select(ids.boxA, ids.boxB)
		editor.pointerDown(30, 30, { target: 'selection', handle: 'bottom_right' })
		editor.pointerMove(60, 30)
		expect(roundedBox(editor.getSelectionPageBounds()!)).toMatchObject({ w: 60, h: 30 })
		// A should stretch
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 0, y: 0, w: 20, h: 10 })
		// B should not
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ w: 20, h: 10 })
	})
})

describe('When resizing a shape with children', () => {
	it("Offsets children when the shape's top left corner changes", () => {
		editor
			.updateShapes([
				{
					id: ids.boxC,
					type: 'geo',
					parentId: ids.boxB,
				},
			])
			.select(ids.boxA)
			.pointerDown(10, 10, {
				target: 'selection',
				handle: 'top_left',
			})
			.pointerMove(0, 0)
			// A's model should have changed by the offset
			.expectShapeToMatch({
				id: ids.boxA,
				x: 0,
				y: 0,
			})
			// B's model should have changed by the offset
			.expectShapeToMatch({
				id: ids.boxB,
				x: 110,
				y: 110,
			})
			// C's model should also have changed
			.expectShapeToMatch({
				id: ids.boxC,
				x: 220,
				y: 220,
			})
	})

	it('Offsets children when the shape is rotated', () => {
		editor
			.updateShapes([
				{
					id: ids.boxA,
					type: 'geo',
					rotation: Math.PI,
				},
			])
			.select(ids.boxA)
			.pointerDown(10, 10, {
				target: 'selection',
				handle: 'top_left',
			})
			.pointerMove(0, 0)
			.expectToBeIn('select.resizing')
			// A's model should have changed by the offset
			.expectShapeToMatch({
				id: ids.boxA,
				x: 0,
				y: 0,
			})
			// B's model should have changed by the offset
			.expectShapeToMatch({
				id: ids.boxB,
				x: 90,
				y: 90,
			})
	})

	it('Resizes a rotated draw shape', () => {
		editor
			.updateShapes([
				{
					id: ids.boxA,
					type: 'geo',
					rotation: 0,
					x: 10,
					y: 10,
				},
				{
					id: ids.boxB,
					type: 'geo',
					parentId: ids.boxA,
					rotation: 0,
					x: 0,
					y: 0,
				},
			])
			.createShapes([
				{
					id: ids.lineA,
					parentId: ids.boxA,
					rotation: Math.PI,
					type: 'draw',
					x: 100,
					y: 100,
					props: {
						segments: createDrawSegments([
							[
								{ x: 0, y: 0, z: 0.5 },
								{ x: 100, y: 100, z: 0.5 },
							],
						]),
					},
				},
			])
			.select(ids.boxB, ids.lineA)

		editor
			.pointerDown(10, 10, {
				target: 'selection',
				handle: 'top_left',
			})
			.pointerMove(0, 0)
			// .pointerMove(10, 10)
			.expectToBeIn('select.resizing')
			// A's model should have changed by the offset
			.expectShapeToMatch({
				id: ids.boxB,
				x: -10,
				y: -10,
			})
		// B's model should have changed by the offset

		expect(editor.getShape(ids.lineA)).toMatchSnapshot('draw shape after rotating')
	})
})

function getGapAndPointLines() {
	const gapLines = editor.snaps
		.getIndicators()
		.filter((snap) => snap.type === 'gaps') as GapsSnapIndicator[]
	const pointLines = editor.snaps
		.getIndicators()
		.filter((snap) => snap.type === 'points') as PointsSnapIndicator[]
	return { gapLines, pointLines }
}

describe('snapping while resizing', () => {
	beforeEach(() => {
		//     0 40 60          160 180
		//
		//   0       ┌────────────┐
		//           │      A     │
		//  40       └────────────┘
		//
		//  60 ┌──┐    80     140    ┌──┐
		//     │D │  80 ┌──────┐     │B │
		//     │  │     │      │     │  │
		//     │  │     │  X   │     │  │
		//     │  │     │      │     │  │
		//     │  │ 140 └──────┘     │  │
		// 160 └──┘                  └──┘
		//
		// 180       ┌────────────┐
		//           │ C          │
		//           └────────────┘

		editor.createShapes([
			box(ids.boxA, 60, 0, 100, 40),
			box(ids.boxB, 180, 60, 40, 100),
			box(ids.boxC, 60, 180, 100, 40),
			box(ids.boxD, 0, 60, 40, 100),
			box(ids.boxX, 80, 80, 60, 60),
		])
	})

	it('works for dragging the top edge', () => {
		// snap to top edges of D and B
		editor
			.select(ids.boxX)
			.pointerDown(115, 80, {
				target: 'selection',
				handle: 'top',
			})
			.pointerMove(115, 59, { ctrlKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 80, y: 60, props: { w: 60, h: 80 } })
		expect(editor.snaps.getIndicators().length).toBe(1)

		// moving the mouse horizontally should not change things
		editor.pointerMove(15, 65, { ctrlKey: true })
		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 80, y: 60, props: { w: 60, h: 80 } })
		expect(editor.snaps.getIndicators().length).toBe(1)

		expect(getGapAndPointLines().pointLines[0].points).toHaveLength(6)

		// snap to bottom edge of A
		editor.pointerMove(15, 43, { ctrlKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 80, y: 40, props: { w: 60, h: 100 } })
		expect(editor.snaps.getIndicators().length).toBe(1)

		expect(getGapAndPointLines().pointLines[0].points).toHaveLength(4)
	})

	it('works for dragging the right edge', () => {
		// Snap to right edges of A and C

		editor
			.select(ids.boxX)
			.pointerDown(140, 115, {
				target: 'selection',
				handle: 'right',
			})
			.pointerMove(156, 115, { ctrlKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 80, y: 80, props: { w: 80, h: 60 } })
		expect(editor.snaps.getIndicators().length).toBe(1)

		expect(getGapAndPointLines().pointLines[0].points).toHaveLength(6)

		// moving the mouse vertically should not change things
		editor.pointerMove(156, 180, { ctrlKey: true })
		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 80, y: 80, props: { w: 80, h: 60 } })

		// snap to left edge of B
		editor.pointerMove(173, 280, { ctrlKey: true })
		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 80, y: 80, props: { w: 100, h: 60 } })
		expect(editor.snaps.getIndicators().length).toBe(1)
		expect(getGapAndPointLines().pointLines[0].points).toHaveLength(4)
	})

	it('works for dragging the bottom edge', () => {
		// snap to bottom edges of B and D
		editor
			.select(ids.boxX)
			.pointerDown(115, 140, {
				target: 'selection',
				handle: 'bottom',
			})
			.pointerMove(115, 159, { ctrlKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 80, y: 80, props: { w: 60, h: 80 } })
		expect(editor.snaps.getIndicators().length).toBe(1)

		expect(getGapAndPointLines().pointLines[0].points).toHaveLength(6)

		// changing horzontal mouse position should not change things
		editor.pointerMove(315, 163, { ctrlKey: true })
		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 80, y: 80, props: { w: 60, h: 80 } })
		expect(editor.snaps.getIndicators().length).toBe(1)

		// snap to top edge of C
		editor.pointerMove(115, 183, { ctrlKey: true })
		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 80, y: 80, props: { w: 60, h: 100 } })
		expect(editor.snaps.getIndicators().length).toBe(1)

		expect(getGapAndPointLines().pointLines[0].points).toHaveLength(4)
	})

	it('works for dragging the left edge', () => {
		// snap to left edges of A and C
		editor
			.select(ids.boxX)
			.pointerDown(80, 115, {
				target: 'selection',
				handle: 'left',
			})
			.pointerMove(59, 115, { ctrlKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 60, y: 80, props: { w: 80, h: 60 } })

		expect(editor.snaps.getIndicators().length).toBe(1)
		expect(getGapAndPointLines().pointLines[0].points).toHaveLength(6)

		// moving the mouse vertically should not change things
		editor.pointerMove(63, 180, { ctrlKey: true })
		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 60, y: 80, props: { w: 80, h: 60 } })

		// snap to right edge of D
		editor.pointerMove(39, 280, { ctrlKey: true })
		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 40, y: 80, props: { w: 100, h: 60 } })

		expect(editor.snaps.getIndicators().length).toBe(1)
		expect(getGapAndPointLines().pointLines[0].points).toHaveLength(4)
	})
	it('works for dragging the top left corner', () => {
		// snap to left edges of A and C
		//           x ┌───────────────────────────┐
		//           │ │     A                     │
		//           │ │                           │
		//           x └───────────────────────────┘
		//           │
		//           │
		// ┌─────┐   │
		// │     │   │
		// │     │   x O────────────────┐
		// │  D  │   │ │                │
		// │     │   │ │                │
		// │     │   │ │      X         │
		// │     │   │ │                │
		// │     │   │ │                │
		// │     │   x └────────────────┘
		// │     │   │
		// └─────┘   │
		//           │
		//           │
		//           x ┌───────────────────────────┐
		//           │ │     c                     │
		//           │ │                           │
		//           x └───────────────────────────┘

		editor.select(ids.boxX).pointerDown(80, 80, {
			target: 'selection',
			handle: 'top_left',
		})

		editor.pointerMove(62, 81, { ctrlKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 60, y: 81, props: { w: 80, h: 59 } })

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "60,0 60,40 60,81 60,140 60,180 60,220",
      ]
    `)

		// snap to top edges of B and D
		//
		//             ┌────────────────────┐
		//             │                    │
		//             │     A              │
		//             │                    │
		//             └────────────────────┘
		//
		// x─────x────────x─────────────x─────────x─────x
		// ┌─────┐        O─────────────┐         ┌─────┐
		// │     │        │             │         │     │
		// │     │        │             │         │     │
		// │  D  │        │             │         │  B  │
		// │     │        │     X       │         │     │
		// │     │        │             │         │     │
		// │     │        │             │         │     │
		// │     │        │             │         │     │
		// │     │        └─────────────┘         │     │
		// │     │                                │     │
		// │     │                                │     │
		// └─────┘                                └─────┘
		//
		//             ┌────────────────────┐
		//             │                    │
		//             │     C              │
		//             │                    │
		//             └────────────────────┘
		editor.pointerMove(81, 58, { ctrlKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 81, y: 60, props: { w: 59, h: 80 } })

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "0,60 40,60 81,60 140,60 180,60 220,60",
      ]
    `)

		// sanp to both at the same time
		//           x ┌────────────────────┐
		//           │ │                    │
		//           │ │     A              │
		//           │ │                    │
		//           x └────────────────────┘
		//           │
		// x─────x───x──────────────────x─────────x─────x
		// ┌─────┐   │ O────────────────┐         ┌─────┐
		// │     │   │ │                │         │     │
		// │     │   │ │                │         │     │
		// │  D  │   │ │                │         │  B  │
		// │     │   │ │        X       │         │     │
		// │     │   │ │                │         │     │
		// │     │   │ │                │         │     │
		// │     │   │ │                │         │     │
		// │     │   x └────────────────┘         │     │
		// │     │   │                            │     │
		// │     │   │                            │     │
		// └─────┘   │                            └─────┘
		//           │
		//           x ┌────────────────────┐
		//           │ │                    │
		//           │ │     C              │
		//           │ │                    │
		//           x └────────────────────┘
		editor.pointerMove(59, 62, { ctrlKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 60, y: 60, props: { w: 80, h: 80 } })

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "0,60 40,60 60,60 140,60 180,60 220,60",
        "60,0 60,40 60,60 60,140 60,180 60,220",
      ]
    `)
	})
	it('works for dragging the top right corner', () => {
		//             ┌────────────────────┐ x
		//             │                    │ │
		//             │     A              │ │
		//             │                    │ │
		//             └────────────────────┘ x
		//                                    │
		// x─────x──────────x─────────────────x───x─────x
		// ┌─────┐          ┌───────────────O │   ┌─────┐
		// │     │          │               │ │   │     │
		// │     │          │               │ │   │     │
		// │  D  │          │               │ │   │  B  │
		// │     │          │   X           │ │   │     │
		// │     │          │               │ │   │     │
		// │     │          │               │ │   │     │
		// │     │          │               │ │   │     │
		// │     │          └───────────────┘ x   │     │
		// │     │                            │   │     │
		// │     │                            │   │     │
		// └─────┘                            │   └─────┘
		//                                    │
		//             ┌────────────────────┐ x
		//             │                    │ │
		//             │     C              │ │
		//             │                    │ │
		//             └────────────────────┘ x

		editor
			.select(ids.boxX)
			.pointerDown(140, 80, {
				target: 'selection',
				handle: 'top_right',
			})
			.pointerMove(161, 59, { ctrlKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 80, y: 60, props: { w: 80, h: 80 } })
	})
	it('works for dragging the bottom right corner', () => {
		//             ┌────────────────────┐ x
		//             │                    │ │
		//             │     A              │ │
		//             │                    │ │
		//             └────────────────────┘ x
		//                                    │
		//                                    │
		//                                    │
		// ┌─────┐                            │   ┌─────┐
		// │     │                            │   │     │
		// │     │          ┌───────────────┐ x   │     │
		// │  D  │          │               │ │   │  B  │
		// │     │          │   X           │ │   │     │
		// │     │          │               │ │   │     │
		// │     │          │               │ │   │     │
		// │     │          │               │ │   │     │
		// │     │          │               │ │   │     │
		// │     │          │               │ │   │     │
		// │     │          │               │ │   │     │
		// └─────┘          └───────────────O │   └─────┘
		// x─────x──────────x─────────────────x───x─────x
		//             ┌────────────────────┐ x
		//             │                    │ │
		//             │     C              │ │
		//             │                    │ │
		//             └────────────────────┘ x

		editor
			.select(ids.boxX)
			.pointerDown(140, 140, {
				target: 'selection',
				handle: 'bottom_right',
			})
			.pointerMove(161, 159, { ctrlKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 80, y: 80, props: { w: 80, h: 80 } })
	})
	it('works for dragging the bottom left corner', () => {
		//           x ┌────────────────────┐
		//           │ │                    │
		//           │ │     A              │
		//           │ │                    │
		//           x └────────────────────┘
		//           │
		//           │
		//           │
		// ┌─────┐   │                            ┌─────┐
		// │     │   │                            │     │
		// │     │   x ┌────────────────┐         │     │
		// │  D  │   │ │                │         │  B  │
		// │     │   │ │        X       │         │     │
		// │     │   │ │                │         │     │
		// │     │   │ │                │         │     │
		// │     │   │ │                │         │     │
		// │     │   │ │                │         │     │
		// │     │   │ │                │         │     │
		// │     │   │ │                │         │     │
		// └─────┘   │ O────────────────┘         └─────┘
		// x─────x───x──────────────────x─────────x─────x
		//           │
		//           x ┌────────────────────┐
		//           │ │                    │
		//           │ │     C              │
		//           │ │                    │
		//           x └────────────────────┘

		editor
			.select(ids.boxX)
			.pointerDown(80, 140, {
				target: 'selection',
				handle: 'bottom_left',
			})
			.pointerMove(59, 159, { ctrlKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 60, y: 80, props: { w: 80, h: 80 } })
	})
})

describe('snapping while resizing from center', () => {
	beforeEach(() => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40           ┌─────────────┐
		//               │             │
		//  60  ┌───┐    │             │    ┌───┐
		//      │ D │    │      X      │    │ B │
		//  80  └───┘    │             │    └───┘
		//               │             │
		// 100           └─────────────┘
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘

		editor.createShapes([
			box(ids.boxA, 60, 0, 20, 20),
			box(ids.boxB, 120, 60, 20, 20),
			box(ids.boxC, 60, 120, 20, 20),
			box(ids.boxD, 0, 60, 20, 20),
			box(ids.boxX, 40, 40, 60, 60),
		])
	})
	it('should work from the top', () => {
		editor
			.select(ids.boxX)
			.pointerDown(70, 40, {
				target: 'selection',
				handle: 'top',
			})
			.pointerMove(70, 21, { ctrlKey: true, altKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({
			x: 40,
			y: 20,
			props: { w: 60, h: 100 },
		})
		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "40,120 60,120 80,120 100,120",
        "40,20 60,20 80,20 100,20",
      ]
    `)
	})
	it('should work from the right', () => {
		editor
			.select(ids.boxX)
			.pointerDown(100, 70, {
				target: 'selection',
				handle: 'right',
			})
			.pointerMove(121, 70, { ctrlKey: true, altKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({
			x: 20,
			y: 40,
			props: { w: 100, h: 60 },
		})
	})
	it('should work from the bottom', () => {
		editor
			.select(ids.boxX)
			.pointerDown(70, 100, {
				target: 'selection',
				handle: 'bottom',
			})
			.pointerMove(70, 121, { ctrlKey: true, altKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({
			x: 40,
			y: 20,
			props: { w: 60, h: 100 },
		})
	})
	it('should work from the left', () => {
		editor
			.select(ids.boxX)
			.pointerDown(40, 70, {
				target: 'selection',
				handle: 'left',
			})
			.pointerMove(21, 70, { ctrlKey: true, altKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({
			x: 20,
			y: 40,
			props: { w: 100, h: 60 },
		})
	})

	it('should work from the top right', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40      x───────────────────────O
		//          │                       │
		//  60  ┌───x                       x───┐
		//      │ D │           X           │ B │
		//  80  └───x                       x───┘
		//          │                       │
		// 100      x───────────────────────x
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(100, 40, {
				target: 'selection',
				handle: 'top_right',
			})
			.pointerMove(123, 40, { ctrlKey: true, altKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({
			x: 20,
			y: 40,
			props: { w: 100, h: 60 },
		})
		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "120,40 120,60 120,80 120,100",
        "20,40 20,60 20,80 20,100",
      ]
    `)
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20      x─────────x───x─────────O
		//          │                       │
		//  40      │                       │
		//          │                       │
		//  60  ┌───x                       x───┐
		//      │ D │           X           │ B │
		//  80  └───x                       x───┘
		//          │                       │
		// 100      │                       │
		//          │                       │
		// 120      x─────────x───x─────────x
		//                    │ C │
		// 140                └───┘
		editor.pointerMove(123, 18, { ctrlKey: true, altKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({
			x: 20,
			y: 20,
			props: { w: 100, h: 100 },
		})

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "120,20 120,60 120,80 120,120",
        "20,120 60,120 80,120 120,120",
        "20,20 20,60 20,80 20,120",
        "20,20 60,20 80,20 120,20",
      ]
    `)
	})
	it('should work from the bottom right', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40      x───────────────────────x
		//          │                       │
		//  60  ┌───x                       x───┐
		//      │ D │           X           │ B │
		//  80  └───x                       x───┘
		//          │                       │
		// 100      x───────────────────────O
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(100, 100, {
				target: 'selection',
				handle: 'bottom_right',
			})
			.pointerMove(123, 100, { ctrlKey: true, altKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({
			x: 20,
			y: 40,
			props: { w: 100, h: 60 },
		})

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "120,40 120,60 120,80 120,100",
        "20,40 20,60 20,80 20,100",
      ]
    `)

		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20      x─────────x───x─────────x
		//          │                       │
		//  40      │                       │
		//          │                       │
		//  60  ┌───x                       x───┐
		//      │ D │           X           │ B │
		//  80  └───x                       x───┘
		//          │                       │
		// 100      │                       │
		//          │                       │
		// 120      x─────────x───x─────────O
		//                    │ C │
		// 140                └───┘
		editor.pointerMove(123, 118, { ctrlKey: true, altKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({
			x: 20,
			y: 20,
			props: { w: 100, h: 100 },
		})

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "120,20 120,60 120,80 120,120",
        "20,120 60,120 80,120 120,120",
        "20,20 20,60 20,80 20,120",
        "20,20 60,20 80,20 120,20",
      ]
    `)
	})
	it('should work from the bottom left', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40      x───────────────────────x
		//          │                       │
		//  60  ┌───x                       x───┐
		//      │ D │           X           │ B │
		//  80  └───x                       x───┘
		//          │                       │
		// 100      O───────────────────────x
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(40, 100, {
				target: 'selection',
				handle: 'bottom_left',
			})
			.pointerMove(23, 100, { ctrlKey: true, altKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({
			x: 20,
			y: 40,
			props: { w: 100, h: 60 },
		})

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "120,40 120,60 120,80 120,100",
        "20,40 20,60 20,80 20,100",
      ]
    `)

		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20      x─────────x───x─────────x
		//          │                       │
		//  40      │                       │
		//          │                       │
		//  60  ┌───x                       x───┐
		//      │ D │           X           │ B │
		//  80  └───x                       x───┘
		//          │                       │
		// 100      │                       │
		//          │                       │
		// 120      O─────────x───x─────────x
		//                    │ C │
		// 140                └───┘

		editor.pointerMove(23, 118, { ctrlKey: true, altKey: true })
		expect(editor.getShape(ids.boxX)).toMatchObject({
			x: 20,
			y: 20,
			props: { w: 100, h: 100 },
		})

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "120,20 120,60 120,80 120,120",
        "20,120 60,120 80,120 120,120",
        "20,20 20,60 20,80 20,120",
        "20,20 60,20 80,20 120,20",
      ]
    `)
	})
	it('should work from the top left', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40      O───────────────────────x
		//          │                       │
		//  60  ┌───x                       x───┐
		//      │ D │           X           │ B │
		//  80  └───x                       x───┘
		//          │                       │
		// 100      x───────────────────────x
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(40, 40, {
				target: 'selection',
				handle: 'top_left',
			})
			.pointerMove(23, 40, { ctrlKey: true, altKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({
			x: 20,
			y: 40,
			props: { w: 100, h: 60 },
		})

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "120,40 120,60 120,80 120,100",
        "20,40 20,60 20,80 20,100",
      ]
    `)

		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20      O─────────x───x─────────x
		//          │                       │
		//  40      │                       │
		//          │                       │
		//  60  ┌───x                       x───┐
		//      │ D │           X           │ B │
		//  80  └───x                       x───┘
		//          │                       │
		// 100      │                       │
		//          │                       │
		// 120      x─────────x───x─────────x
		//                    │ C │
		// 140                └───┘

		editor.pointerMove(23, 19, { ctrlKey: true, altKey: true })
		expect(editor.getShape(ids.boxX)).toMatchObject({
			x: 20,
			y: 20,
			props: { w: 100, h: 100 },
		})

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "120,20 120,60 120,80 120,120",
        "20,120 60,120 80,120 120,120",
        "20,20 20,60 20,80 20,120",
        "20,20 60,20 80,20 120,20",
      ]
    `)
	})
})

describe('snapping while resizing with aspect ratio locked', () => {
	beforeEach(() => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40           ┌─────────────┐
		//               │             │
		//  60  ┌───┐    │             │    ┌───┐
		//      │ D │    │      X      │    │ B │
		//  80  └───┘    │             │    └───┘
		//               │             │
		// 100           └─────────────┘
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘

		editor.createShapes([
			box(ids.boxA, 60, 0, 20, 20),
			box(ids.boxB, 120, 60, 20, 20),
			box(ids.boxC, 60, 120, 20, 20),
			box(ids.boxD, 0, 60, 20, 20),
			box(ids.boxX, 40, 40, 60, 60),
		])
	})
	it('should work from the top', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20         x──────x─O─x──────x
		//             │                 │
		//  40         │                 │
		//             │                 │
		//  60  ┌───┐  │                 │  ┌───┐
		//      │ D │  │        X        │  │ B │
		//  80  └───┘  │                 │  └───┘
		//             │                 │
		// 100         └─────────────────┘
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(70, 40, {
				target: 'selection',
				handle: 'top',
			})
			.pointerMove(70, 18, { ctrlKey: true, shiftKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 30, y: 20, props: { w: 80, h: 80 } })
		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "30,20 60,20 80,20 110,20",
      ]
    `)
	})

	it('should work from the right', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//               ┌──────────────────x
		//  40           │                  │
		//               │                  │
		//  60  ┌───┐    │                  x───┐
		//      │ D │    │      X           O B │
		//  80  └───┘    │                  x───┘
		//               │                  │
		// 100           │                  │
		//               └──────────────────x
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(100, 70, {
				target: 'selection',
				handle: 'right',
			})
			.pointerMove(123, 79, { ctrlKey: true, shiftKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 40, y: 30, props: { w: 80, h: 80 } })

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "120,30 120,60 120,80 120,110",
      ]
    `)
	})

	it('should work from the bottom', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40         ┌─────────────────┐
		//             │                 │
		//  60  ┌───┐  │                 │  ┌───┐
		//      │ D │  │        X        │  │ B │
		//  80  └───┘  │                 │  └───┘
		//             │                 │
		// 100         │                 │
		//             │                 │
		// 120         x──────x─O─x──────x
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(70, 100, {
				target: 'selection',
				handle: 'bottom',
			})
			.pointerMove(70, 123, { ctrlKey: true, shiftKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 30, y: 40, props: { w: 80, h: 80 } })

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "30,120 60,120 80,120 110,120",
      ]
    `)
	})
	it('should work from the left', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//          x──────────────────┐
		//  40      │                  │
		//          │                  │
		//  60  ┌───x                  │    ┌───┐
		//      │ D O           X      │    │ B │
		//  80  └───x                  │    └───┘
		//          │                  │
		// 100      │                  │
		//          x──────────────────┘
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘

		editor
			.select(ids.boxX)
			.pointerDown(40, 70, {
				target: 'selection',
				handle: 'left',
			})
			.pointerMove(18, 70, { ctrlKey: true, shiftKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 20, y: 30, props: { w: 80, h: 80 } })

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "20,30 20,60 20,80 20,110",
      ]
    `)
	})
	it('should work from the top right', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20           ┌────x───x────▲────x
		//               │                  │
		//  40           │                  │
		//               │                  │
		//  60  ┌───┐    │                  x───┐
		//      │ D │    │      X           │ B │
		//  80  └───┘    │                  x───┘
		//               │                  │
		// 100           └──────────────────┘
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(100, 40, {
				target: 'selection',
				handle: 'top_right',
			})
			.pointerMove(100, 18, { ctrlKey: true, shiftKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 40, y: 20, props: { w: 80, h: 80 } })

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "120,20 120,60 120,80 120,100",
        "40,20 60,20 80,20 120,20",
      ]
    `)
	})
	it('should work from the bottom right', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40           ┌──────────────────┐
		//               │                  │
		//  60  ┌───┐    │                  x───┐
		//      │ D │    │      X           │ B │
		//  80  └───┘    │                  x───┘
		//               │                  │
		// 100           │                 ─┤►
		//               │                  │
		// 120           └────x───x─────────x
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(100, 100, {
				target: 'selection',
				handle: 'bottom_right',
			})
			.pointerMove(118, 100, { ctrlKey: true, shiftKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 40, y: 40, props: { w: 80, h: 80 } })

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "120,40 120,60 120,80 120,120",
        "40,120 60,120 80,120 120,120",
      ]
    `)
	})
	it('should work from the bottom left', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40      x──────────────────┐
		//          │                  │
		//  60  ┌───x                  │    ┌───┐
		//      │ D │           X      │    │ B │
		//  80  └───x                  │    └───┘
		//          │                  │
		// 100     ◄├─                 │
		//          │                  │
		// 120      x─────────x───x────x
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(40, 100, {
				target: 'selection',
				handle: 'bottom_left',
			})
			.pointerMove(18, 100, { ctrlKey: true, shiftKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 20, y: 40, props: { w: 80, h: 80 } })

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "20,120 60,120 80,120 100,120",
        "20,40 20,60 20,80 20,120",
      ]
    `)
	})
	it('should work from the top left', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//               ▲    │ A │
		//  20      x────┬────x───x────x
		//          │                  │
		//  40      │                  │
		//          │                  │
		//  60  ┌───x                  │    ┌───┐
		//      │ D │           X      │    │ B │
		//  80  └───x                  │    └───┘
		//          │                  │
		// 100      x──────────────────┘
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(40, 40, {
				target: 'selection',
				handle: 'top_left',
			})
			.pointerMove(40, 18, { ctrlKey: true, shiftKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({ x: 20, y: 20, props: { w: 80, h: 80 } })

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "20,20 20,60 20,80 20,100",
        "20,20 60,20 80,20 100,20",
      ]
    `)
	})
})

describe('snapping while resizing from center with aspect ratio locked', () => {
	beforeEach(() => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40           ┌─────────────┐
		//               │             │
		//  60  ┌───┐    │             │    ┌───┐
		//      │ D │    │      X      │    │ B │
		//  80  └───┘    │             │    └───┘
		//               │             │
		// 100           └─────────────┘
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘

		editor.createShapes([
			box(ids.boxA, 60, 0, 20, 20),
			box(ids.boxB, 120, 60, 20, 20),
			box(ids.boxC, 60, 120, 20, 20),
			box(ids.boxD, 0, 60, 20, 20),
			box(ids.boxX, 40, 40, 60, 60),
		])
	})
	const expectedSnapLines = [
		'120,20 120,60 120,80 120,120',
		'20,120 60,120 80,120 120,120',
		'20,20 20,60 20,80 20,120',
		'20,20 60,20 80,20 120,20',
	] as const
	it('should work from the top', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20      x─────────x─O─x─────────x
		//          │                       │
		//  40      │                       │
		//          │                       │
		//  60  ┌───x                       x───┐
		//      │ D │           X           │ B │
		//  80  └───x                       x───┘
		//          │                       │
		// 100      │                       │
		//          │                       │
		// 120      x─────────x───x─────────x
		//                    │ C │
		// 140                └───┘

		editor
			.select(ids.boxX)
			.pointerDown(70, 40, {
				target: 'selection',
				handle: 'top',
			})
			.pointerMove(70, 18, { ctrlKey: true, shiftKey: true, altKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({
			x: 20,
			y: 20,
			props: { w: 100, h: 100 },
		})

		expect(getSnapLines(editor)).toEqual(expectedSnapLines)
	})
	it('should work from the right', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20      x─────────x───x─────────x
		//          │                       │
		//  40      │                       │
		//          │                       │
		//  60  ┌───x                       x───┐
		//      │ D │           X           O B │
		//  80  └───x                       x───┘
		//          │                       │
		// 100      │                       │
		//          │                       │
		// 120      x─────────x───x─────────x
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(100, 70, {
				target: 'selection',
				handle: 'right',
			})
			.pointerMove(123, 40, { ctrlKey: true, shiftKey: true, altKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({
			x: 20,
			y: 20,
			props: { w: 100, h: 100 },
		})

		expect(getSnapLines(editor)).toEqual(expectedSnapLines)
	})
	it('should work from the bottom', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20      x─────────x───x─────────x
		//          │                       │
		//  40      │                       │
		//          │                       │
		//  60  ┌───x                       x───┐
		//      │ D │           X           │ B │
		//  80  └───x                       x───┘
		//          │                       │
		// 100      │                       │
		//          │                       │
		// 120      x─────────x─O─x─────────x
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(70, 100, {
				target: 'selection',
				handle: 'bottom',
			})
			.pointerMove(70, 121, { ctrlKey: true, shiftKey: true, altKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({
			x: 20,
			y: 20,
			props: { w: 100, h: 100 },
		})

		expect(getSnapLines(editor)).toEqual(expectedSnapLines)
	})
	it('should work from the left', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20      x─────────x───x─────────x
		//          │                       │
		//  40      │                       │
		//          │                       │
		//  60  ┌───x                       x───┐
		//      │ D O           X           │ B │
		//  80  └───x                       x───┘
		//          │                       │
		// 100      │                       │
		//          │                       │
		// 120      x─────────x───x─────────x
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(40, 70, {
				target: 'selection',
				handle: 'left',
			})
			.pointerMove(18, 87, { ctrlKey: true, shiftKey: true, altKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({
			x: 20,
			y: 20,
			props: { w: 100, h: 100 },
		})

		expect(getSnapLines(editor)).toEqual(expectedSnapLines)
	})

	it('should work from the top right', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20      x─────────x───x─────────O
		//          │                       │
		//  40      │                       │
		//          │                       │
		//  60  ┌───x                       x───┐
		//      │ D │           X           │ B │
		//  80  └───x                       x───┘
		//          │                       │
		// 100      │                       │
		//          │                       │
		// 120      x─────────x───x─────────x
		//                    │ C │
		// 140                └───┘

		editor
			.select(ids.boxX)
			.pointerDown(100, 40, {
				target: 'selection',
				handle: 'top_right',
			})
			.pointerMove(100, 18, { ctrlKey: true, shiftKey: true, altKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({
			x: 20,
			y: 20,
			props: { w: 100, h: 100 },
		})

		expect(getSnapLines(editor)).toEqual(expectedSnapLines)
	})
	it('should work from the bottom right', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20      x─────────x───x─────────x
		//          │                       │
		//  40      │                       │
		//          │                       │
		//  60  ┌───x                       x───┐
		//      │ D │           X           │ B │
		//  80  └───x                       x───┘
		//          │                       │
		// 100      │                       │
		//          │                       │
		// 120      x─────────x───x─────────O
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(100, 100, {
				target: 'selection',
				handle: 'bottom_right',
			})
			.pointerMove(123, 118, { ctrlKey: true, shiftKey: true, altKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({
			x: 20,
			y: 20,
			props: { w: 100, h: 100 },
		})

		expect(getSnapLines(editor)).toEqual(expectedSnapLines)
	})
	it('should work from the bottom left', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20      x─────────x───x─────────x
		//          │                       │
		//  40      │                       │
		//          │                       │
		//  60  ┌───x                       x───┐
		//      │ D │           X           │ B │
		//  80  └───x                       x───┘
		//          │                       │
		// 100      │                       │
		//          │                       │
		// 120      O─────────x───x─────────x
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(40, 100, {
				target: 'selection',
				handle: 'bottom_left',
			})
			.pointerMove(18, 125, { ctrlKey: true, shiftKey: true, altKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({
			x: 20,
			y: 20,
			props: { w: 100, h: 100 },
		})

		expect(getSnapLines(editor)).toEqual(expectedSnapLines)
	})
	it('should work from the top left', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20      O─────────x───x─────────x
		//          │                       │
		//  40      │                       │
		//          │                       │
		//  60  ┌───x                       x───┐
		//      │ D │           X           │ B │
		//  80  └───x                       x───┘
		//          │                       │
		// 100      │                       │
		//          │                       │
		// 120      x─────────x───x─────────x
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(40, 40, {
				target: 'selection',
				handle: 'top_left',
			})
			.pointerMove(23, 18, { ctrlKey: true, shiftKey: true, altKey: true })

		expect(editor.getShape(ids.boxX)).toMatchObject({
			x: 20,
			y: 20,
			props: { w: 100, h: 100 },
		})

		expect(getSnapLines(editor)).toEqual(expectedSnapLines)
	})
})

describe('snapping while resizing a shape that has been rotated by multiples of 90 deg', () => {
	beforeEach(() => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40           ┌─────────────┐
		//               │             │
		//  60  ┌───┐    │             │    ┌───┐
		//      │ D │    │      X      │    │ B │
		//  80  └───┘    │             │    └───┘
		//               │             │
		// 100           └─────────────┘
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘

		editor.createShapes([
			box(ids.boxA, 60, 0, 20, 20),
			box(ids.boxB, 120, 60, 20, 20),
			box(ids.boxC, 60, 120, 20, 20),
			box(ids.boxD, 0, 60, 20, 20),
			box(ids.boxX, 40, 40, 60, 60),
		])
	})

	function rotateX(times: number) {
		editor.select(ids.boxX)
		for (let i = 0; i < times; i++) {
			editor
				.pointerDown(40, 40, { target: 'selection', handle: 'top_left_rotate' })
				.pointerMove(100, 40, { shiftKey: true })
				.pointerUp(100, 40, { shiftKey: true })
		}

		expect(editor.getShapePageBounds(ids.boxX)!.x).toBeCloseTo(40)
		expect(editor.getShapePageBounds(ids.boxX)!.y).toBeCloseTo(40)
		expect(editor.getShapePageBounds(ids.boxX)!.w).toBeCloseTo(60)
		expect(editor.getShapePageBounds(ids.boxX)!.h).toBeCloseTo(60)
		expect(editor.getShape(ids.boxX)!.rotation).toEqual(
			canonicalizeRotation(((PI / 2) * times) % (PI * 2))
		)
	}

	it('should work for 90deg', () => {
		rotateX(1)
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40           ┌──────────────────x
		//               │                  │
		//  60  ┌───┐    │                  x───┐
		//      │ D │    │      X           O B │
		//  80  └───┘    │                  x───┘
		//               │                  │
		// 100           └──────────────────x
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(100, 70, { target: 'selection', handle: 'top' })
			.pointerMove(121, 70, { ctrlKey: true, shiftKey: false })
		vi.advanceTimersByTime(200)

		expect(editor.getShapePageBounds(ids.boxX)!).toMatchObject({
			x: 40,
			y: 40,
			w: 80,
			h: 60,
		})
		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "120,40 120,60 120,80 120,100",
      ]
    `)

		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40      x───────────────────────x
		//          │                       │
		//  60  ┌───x                       x───┐
		//      │ D │           X           O B │
		//  80  └───x                       x───┘
		//          │                       │
		// 100      x───────────────────────x
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘
		editor.keyDown('Alt', { altKey: true, ctrlKey: true })

		expect(editor.getShapePageBounds(ids.boxX)!).toMatchObject({
			x: 20,
			y: 40,
			w: 100,
			h: 60,
		})
		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "120,40 120,60 120,80 120,100",
        "20,40 20,60 20,80 20,100",
      ]
    `)
	})
	it('should work for 180', () => {
		rotateX(2)

		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20           x────x─O─x────x
		//               │             │
		//  40           │             │
		//               │             │
		//  60  ┌───┐    │             │    ┌───┐
		//      │ D │    │      X      │    │ B │
		//  80  └───┘    │             │    └───┘
		//               │             │
		// 100           └─────────────┘
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(70, 40, { target: 'selection', handle: 'bottom' })
			.pointerMove(70, 18, { ctrlKey: true, shiftKey: false })
		vi.advanceTimersByTime(200)

		expect(editor.getShapePageBounds(ids.boxX)!.x).toBeCloseTo(40)
		expect(editor.getShapePageBounds(ids.boxX)!.y).toBeCloseTo(20)
		expect(editor.getShapePageBounds(ids.boxX)!.w).toBeCloseTo(60)
		expect(editor.getShapePageBounds(ids.boxX)!.h).toBeCloseTo(80)

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "40,20 60,20 80,20 100,20",
      ]
    `)

		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20         x──────x─O─x──────x
		//             │                 │
		//  40         │                 │
		//             │                 │
		//  60  ┌───┐  │                 │  ┌───┐
		//      │ D │  │        X        │  │ B │
		//  80  └───┘  │                 │  └───┘
		//             │                 │
		// 100         └─────────────────┘
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘
		editor.keyDown('Shift', { ctrlKey: true })
		expect(editor.getShapePageBounds(ids.boxX)!.x).toBeCloseTo(30)
		expect(editor.getShapePageBounds(ids.boxX)!.y).toBeCloseTo(20)
		expect(editor.getShapePageBounds(ids.boxX)!.w).toBeCloseTo(80)
		expect(editor.getShapePageBounds(ids.boxX)!.h).toBeCloseTo(80)

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "30,20 60,20 80,20 110,20",
      ]
    `)
	})
	it('should work for 270deg', () => {
		rotateX(3)
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40      x──────────────────┐
		//          │                  │
		//  60  ┌───x                  │    ┌───┐
		//      │ D │           X      │    │ B │
		//  80  └───x                  │    └───┘
		//          │                  │
		// 100      │                  │
		//          │                  │
		// 120      O─────────x───x────x
		//                    │ C │
		// 140                └───┘

		editor
			.select(ids.boxX)
			.pointerDown(40, 100, { target: 'selection', handle: 'top_left' })
			.pointerMove(18, 118, { ctrlKey: true, shiftKey: false })

		expect(editor.getShapePageBounds(ids.boxX)!.x).toBeCloseTo(20)
		expect(editor.getShapePageBounds(ids.boxX)!.y).toBeCloseTo(40)
		expect(editor.getShapePageBounds(ids.boxX)!.w).toBeCloseTo(80)
		expect(editor.getShapePageBounds(ids.boxX)!.h).toBeCloseTo(80)

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "20,120 60,120 80,120 100,120",
        "20,40 20,60 20,80 20,120",
      ]
    `)

		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20      x─────────x───x─────────x
		//          │                       │
		//  40      │                       │
		//          │                       │
		//  60  ┌───x                       x───┐
		//      │ D │           X           │ B │
		//  80  └───x                       x───┘
		//          │                       │
		// 100      │                       │
		//          │                       │
		// 120      O─────────x───x─────────x
		//                    │ C │
		// 140                └───┘

		editor.keyDown('Alt', { ctrlKey: true })

		expect(editor.getShapePageBounds(ids.boxX)!.x).toBeCloseTo(20)
		expect(editor.getShapePageBounds(ids.boxX)!.y).toBeCloseTo(20)
		expect(editor.getShapePageBounds(ids.boxX)!.w).toBeCloseTo(100)
		expect(editor.getShapePageBounds(ids.boxX)!.h).toBeCloseTo(100)

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "120,20 120,60 120,80 120,120",
        "20,120 60,120 80,120 120,120",
        "20,20 20,60 20,80 20,120",
        "20,20 60,20 80,20 120,20",
      ]
    `)
	})
	it('should work for 360deg', () => {
		rotateX(4)
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40           ┌──────────────────x
		//               │                  │
		//  60  ┌───┐    │                  x───┐
		//      │ D │    │      X           O B │
		//  80  └───┘    │                  x───┘
		//               │                  │
		// 100           └──────────────────x
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(100, 70, { target: 'selection', handle: 'right' })
			.pointerMove(121, 70, { ctrlKey: true, shiftKey: false })
		vi.advanceTimersByTime(200)

		expect(editor.getShapePageBounds(ids.boxX)!.x).toBeCloseTo(40)
		expect(editor.getShapePageBounds(ids.boxX)!.y).toBeCloseTo(40)
		expect(editor.getShapePageBounds(ids.boxX)!.w).toBeCloseTo(80)
		expect(editor.getShapePageBounds(ids.boxX)!.h).toBeCloseTo(60)

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "120,40 120,60 120,80 120,100",
      ]
    `)

		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40      x───────────────────────x
		//          │                       │
		//  60  ┌───x                       x───┐
		//      │ D │           X           O B │
		//  80  └───x                       x───┘
		//          │                       │
		// 100      x───────────────────────x
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘
		editor.keyDown('Alt', { ctrlKey: true })

		expect(editor.getShapePageBounds(ids.boxX)!.x).toBeCloseTo(20)
		expect(editor.getShapePageBounds(ids.boxX)!.y).toBeCloseTo(40)
		expect(editor.getShapePageBounds(ids.boxX)!.w).toBeCloseTo(100)
		expect(editor.getShapePageBounds(ids.boxX)!.h).toBeCloseTo(60)

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "120,40 120,60 120,80 120,100",
        "20,40 20,60 20,80 20,100",
      ]
    `)
	})
})

describe('snapping while resizing an inverted shape', () => {
	beforeEach(() => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40           ┌─────────────┐
		//               │             │
		//  60  ┌───┐    │             │    ┌───┐
		//      │ D │    │      X      │    │ B │
		//  80  └───┘    │             │    └───┘
		//               │             │
		// 100           └─────────────┘
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘

		editor.createShapes([
			box(ids.boxA, 60, 0, 20, 20),
			box(ids.boxB, 120, 60, 20, 20),
			box(ids.boxC, 60, 120, 20, 20),
			box(ids.boxD, 0, 60, 20, 20),
			box(ids.boxX, 40, 40, 60, 60),
		])
	})
	it('should work for the top edge', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40
		//
		//  60  ┌───┐                       ┌───┐
		//      │ D │           X           │ B │
		//  80  └───┘                       └───┘
		//
		// 100           ┌─────────────┐
		//               │             │
		// 120           x────x─O─x────x
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(70, 40, {
				target: 'selection',
				handle: 'top',
			})
			.pointerMove(70, 123, { ctrlKey: true })

		expect(editor.getShapePageBounds(ids.boxX)!).toMatchObject({
			x: 40,
			y: 100,
			w: 60,
			h: 20,
		})

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "40,120 60,120 80,120 100,120",
      ]
    `)
	})

	it('should work for the right edge', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40      x────┐
		//          │    │
		//  60  ┌───x    │                  ┌───┐
		//      │ D O    │      X           │ B │
		//  80  └───x    │                  └───┘
		//          │    │
		// 100      x────┘
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(100, 70, {
				target: 'selection',
				handle: 'right',
			})
			.pointerMove(18, 70, { ctrlKey: true })

		expect(editor.getShapePageBounds(ids.boxX)!).toMatchObject({
			x: 20,
			y: 40,
			w: 20,
			h: 60,
		})

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "20,40 20,60 20,80 20,100",
      ]
    `)
	})

	it('should work for the bottom edge', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20           x────x─O─x────x
		//               │             │
		//  40           └─────────────┘
		//
		//  60  ┌───┐                       ┌───┐
		//      │ D │           X           │ B │
		//  80  └───┘                       └───┘
		//
		// 100
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(70, 100, {
				target: 'selection',
				handle: 'bottom',
			})
			.pointerMove(70, 23, { ctrlKey: true })

		expect(editor.getShapePageBounds(ids.boxX)!).toMatchObject({
			x: 40,
			y: 20,
			w: 60,
			h: 20,
		})

		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "40,20 60,20 80,20 100,20",
      ]
    `)
	})

	it('should work for the left edge', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40                         ┌────x
		//                             │    │
		//  60  ┌───┐                  │    x───┐
		//      │ D │           X      │    O B │
		//  80  └───┘                  │    x───┘
		//                             │    │
		// 100                         └────x
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(40, 70, {
				target: 'selection',
				handle: 'left',
			})
			.pointerMove(122, 70, { ctrlKey: true })

		expect(editor.getShapePageBounds(ids.boxX)!).toMatchObject({
			x: 100,
			y: 40,
			w: 20,
			h: 60,
		})
		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "120,40 120,60 120,80 120,100",
      ]
    `)
	})

	it('should work for the top right corner', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40
		//
		//  60  ┌───x                       ┌───┐
		//      │ D │           X           │ B │
		//  80  └───x                       └───┘
		//
		// 100      x────┐
		//          │    │
		// 120      O────x    x───x
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(100, 40, {
				target: 'selection',
				handle: 'top_right',
			})
			.pointerMove(19, 121, { ctrlKey: true })

		expect(editor.getShapePageBounds(ids.boxX)!).toMatchObject({
			x: 20,
			y: 100,
			w: 20,
			h: 20,
		})
		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "20,120 40,120 60,120 80,120",
        "20,60 20,80 20,100 20,120",
      ]
    `)
	})

	it('should work for the bottom right corner', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20      O────x    x───x
		//          │    │
		//  40      x────┘
		//
		//  60  ┌───x                       ┌───┐
		//      │ D │           X           │ B │
		//  80  └───x                       └───┘
		//
		// 100
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘

		editor
			.select(ids.boxX)
			.pointerDown(100, 100, {
				target: 'selection',
				handle: 'bottom_right',
			})
			.pointerMove(19, 21, { ctrlKey: true })

		expect(editor.getShapePageBounds(ids.boxX)!).toMatchObject({
			x: 20,
			y: 20,
			w: 20,
			h: 20,
		})
		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "20,20 20,40 20,60 20,80",
        "20,20 40,20 60,20 80,20",
      ]
    `)
	})
	it('should work for the bototm left corner', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                x───x    x────O
		//                             │    │
		//  40                         └────x
		//
		//  60  ┌───┐                       x───┐
		//      │ D │                       │ B │
		//  80  └───┘                       x───┘
		//
		// 100
		//
		// 120                ┌───┐
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(40, 100, {
				target: 'selection',
				handle: 'bottom_left',
			})
			.pointerMove(123, 21, { ctrlKey: true })

		expect(editor.getShapePageBounds(ids.boxX)!).toMatchObject({
			x: 100,
			y: 20,
			w: 20,
			h: 20,
		})
		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "120,20 120,40 120,60 120,80",
        "60,20 80,20 100,20 120,20",
      ]
    `)
	})
	it('should work for the top left corner', () => {
		//      0  20    40  60   80  100  120  140
		//   0                ┌───┐
		//                    │ A │
		//  20                └───┘
		//
		//  40
		//
		//  60  ┌───┐                       x───┐
		//      │ D │                       │ B │
		//  80  └───┘                       x───┘
		//
		// 100                         ┌────x
		//                             │    │
		// 120                x───x    x────O
		//                    │ C │
		// 140                └───┘
		editor
			.select(ids.boxX)
			.pointerDown(40, 40, {
				target: 'selection',
				handle: 'top_left',
			})
			.pointerMove(123, 118, { ctrlKey: true })

		expect(editor.getShapePageBounds(ids.boxX)!).toMatchObject({
			x: 100,
			y: 100,
			w: 20,
			h: 20,
		})
		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "120,60 120,80 120,100 120,120",
        "60,120 80,120 100,120 120,120",
      ]
    `)
	})
})

describe('snapping while the grid is enabled', () => {
	it('does not snap to the grid', () => {
		// 0   20      60   80
		//  ┌───┐       ┌───┐
		//  │ A │       │ B │
		//  └───┘       └───┘

		editor.createShapes([box(ids.boxA, 0, 0, 20, 20), box(ids.boxB, 60, 0, 20, 20)])

		editor.updateInstanceState({ isGridMode: true })

		// try to move right side of A to left side of B
		// doesn't work because of the grid

		// 0   20      60   80
		//  ┌──────────┐┌───┐
		//  │ A        O│ B │
		//  └──────────┘└───┘

		editor
			.select(ids.boxA)
			.pointerDown(20, 10, {
				target: 'selection',
				handle: 'right',
			})
			.pointerMove(59, 10)

		// rounds up to nearest 10
		expect(editor.getShapePageBounds(ids.boxA)!.w).toEqual(60)

		// engage snap mode and it should indeed snap to B

		// 0   20      60   80
		//  x───────────x───x
		//  │ A         │ B │
		//  x───────────x───x
		editor.keyDown('Control')
		expect(editor.getShapePageBounds(ids.boxA)!.w).toEqual(60)
		expect(getSnapLines(editor)).toMatchInlineSnapshot(`
      [
        "0,0 60,0 80,0",
        "0,20 60,20 80,20",
        "60,0 60,20",
      ]
    `)

		// and if not snapping we can make the box any size
		editor.pointerMove(19, 10, { ctrlKey: true })
		expect(editor.getShapePageBounds(ids.boxA)!.w).toEqual(19)
	})
})

describe('resizing a shape with a child', () => {
	it('should not snap to the child', () => {
		// 0 1   11           50
		// ┌───────────────────┐
		// │ ┌───┐             │
		// │ │ B │             │
		// │ └───┘             │
		// │                   │
		// │          A        │
		// │                   │
		// │                   │
		// │                   │
		// └───────────────────┘

		editor.createShapes([
			box(ids.boxA, 0, 0, 50, 50),
			{ ...box(ids.boxB, 1, 1), parentId: ids.boxA },
		])
		editor
			.select(ids.boxA)
			.pointerDown(0, 0, { target: 'selection', handle: 'top_left' })
			.pointerMove(25, 25, { ctrlKey: true })

		expect(editor.snaps.getIndicators().length).toBe(0)
		expect(editor.getShape(ids.boxA)).toMatchObject({ x: 25, y: 25, props: { w: 25, h: 25 } })
		expect(editor.getShape(ids.boxB)).toMatchObject({ x: 0.5, y: 0.5, props: { w: 5, h: 5 } })
		expect(editor.getShapePageBounds(ids.boxB)).toMatchObject({
			x: 25.5,
			y: 25.5,
			w: 5,
			h: 5,
		})
	})
})

describe('reisizing shapes with aspect ratio locked', () => {
	beforeEach(() => {
		//    0  10          40  50
		//
		//  0 ┌───┐           ┌───┐
		//    │ A │           │ B │ rot 90
		// 10 └───┘           └───┘
		//
		//
		//
		// 40 ┌───┐           ┌───┐
		//    │ D │ rot 270   │ C │ rot 180
		// 50 └───┘           └───┘
		editor.createShapes([
			box(ids.boxA, 0, 0),
			{ ...box(ids.boxB, 50, 0), rotation: Math.PI / 2 },
			{ ...box(ids.boxC, 50, 50), rotation: Math.PI },
			{ ...box(ids.boxD, 0, 50), rotation: Math.PI * 1.5 },
		])
	})
	it('does not flip Y when resizing with left edge', () => {
		//         0  10          40  50
		//       ┌───────────────────────┐
		//  0    │ ┌───┐           ┌───┐ │         50 55     70 75
		//       │ │ A │           │ B │ │          ┌───────────┐  12.5
		// 10    │ └───┘           └───┘ │          │ B       A │
		//       │                       │          │           │  17.5
		//      ┌┼┐drag ->               │   ──►    │          ┌┼┐
		//      └┼┘                      │          │          └┼┘
		//       │                       │          │           │  32.5
		// 40    │ ┌───┐           ┌───┐ │          │ C       D │
		//       │ │ D │           │ C │ │          └───────────┘  37.5
		// 50    │ └───┘           └───┘ │
		//       └───────────────────────┘
		editor.select(ids.boxA, ids.boxB, ids.boxC, ids.boxD)
		editor
			.pointerDown(0, 25, {
				target: 'selection',
				handle: 'left',
			})
			.pointerMove(75, 25, { shiftKey: true })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 70, y: 12.5, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 50, y: 12.5, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxC)).toMatchObject({ x: 50, y: 32.5, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxD)).toMatchObject({ x: 70, y: 32.5, w: 5, h: 5 })
	})
	it('does not flip Y when resizing with right edge', () => {
		//         0  10          40  50
		//       ┌───────────────────────┐
		//  0    │ ┌───┐           ┌───┐ │        -25 -20    -5 0
		//       │ │ A │           │ B │ │          ┌───────────┐  12.5
		// 10    │ └───┘           └───┘ │          │ B       A │
		//       │                       │          │           │  17.5
		//       │                <-drag┌┼┐  ──►   ┌┼┐          │
		//       │                      └┼┘        └┼┘          │
		//       │                       │          │           │  32.5
		// 40    │ ┌───┐           ┌───┐ │          │ C       D │
		//       │ │ D │           │ C │ │          └───────────┘  37.5
		// 50    │ └───┘           └───┘ │
		//       └───────────────────────┘
		editor.select(ids.boxA, ids.boxB, ids.boxC, ids.boxD)
		editor
			.pointerDown(50, 25, {
				target: 'selection',
				handle: 'right',
			})
			.pointerMove(-25, 25, { shiftKey: true })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: -5, y: 12.5, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: -25, y: 12.5, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxC)).toMatchObject({ x: -25, y: 32.5, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxD)).toMatchObject({ x: -5, y: 32.5, w: 5, h: 5 })
	})
	it('does not flip X when resizing top edge', () => {
		//         0  10          40  50
		//                  ┌─┐
		//       ┌──────────┼┼┼──────────┐
		//       │          └─┘          │
		//  0    │ ┌───┐           ┌───┐ │
		//       │ │ A │     │     │ B │ │
		// 10    │ └───┘     ▼     └───┘ │
		//       │                       │
		//       │         drag          │
		//       │                       │
		//       │                       │
		// 40    │ ┌───┐           ┌───┐ │
		//       │ │ D │           │ C │ │
		// 50    │ └───┘           └───┘ │
		//       └───────────────────────┘
		//                   │
		//                   ▼
		//
		//          12.5 17.5   32.5 37.5
		// 50          ┌───────────┐
		//             │ D       C │
		// 55          │           │
		//             │           │
		//             │           │
		// 70          │           │
		//             │ A  ┌─┐  B │
		// 75          └────┼┼┼────┘
		//                  └─┘
		editor.select(ids.boxA, ids.boxB, ids.boxC, ids.boxD)
		editor
			.pointerDown(25, 0, {
				target: 'selection',
				handle: 'top',
			})
			.pointerMove(25, 75, { shiftKey: true })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 12.5, y: 70, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 32.5, y: 70, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxC)).toMatchObject({ x: 32.5, y: 50, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxD)).toMatchObject({ x: 12.5, y: 50, w: 5, h: 5 })
	})
	it('does not flip X when resizing bottom edge', () => {
		//         0  10          40  50
		//
		//       ┌───────────────────────┐
		//  0    │ ┌───┐           ┌───┐ │
		//       │ │ A │           │ B │ │
		// 10    │ └───┘           └───┘ │
		//       │                       │
		//       │                       │
		//       │                       │
		//       │         drag up       │
		// 40    │ ┌───┐           ┌───┐ │
		//       │ │ D │     ▲     │ C │ │
		// 50    │ └───┘     │     └───┘ │
		//       │          ┌┼┐          │
		//       └──────────┼┼┼──────────┘
		//                  └─┘
		//
		//
		//                   │
		//                   ▼
		//
		//
		//          12.5 17.5   32.5 37.5
		//                  ┌─┐
		// -25         ┌────┼┼┼────┐
		//             │ D  └─┘  C │
		// -20         │           │
		//             │           │
		//             │           │
		// -5          │           │
		//             │ A       B │
		//  0          └───────────┘
		editor.select(ids.boxA, ids.boxB, ids.boxC, ids.boxD)
		editor
			.pointerDown(25, 50, {
				target: 'selection',
				handle: 'bottom',
			})
			.pointerMove(25, -25, { shiftKey: true })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 12.5, y: -5, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 32.5, y: -5, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxC)).toMatchObject({ x: 32.5, y: -25, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxD)).toMatchObject({ x: 12.5, y: -25, w: 5, h: 5 })
	})
	it('preserves the correct alignment when dragging the top left corner around', () => {
		editor.select(ids.boxA, ids.boxB, ids.boxC, ids.boxD)
		editor.pointerDown(0, 0, { target: 'selection', handle: 'top_left' })
		//       25 30      45 50
		//         ┌───────────┐   50
		//         │ D       C │
		//         │           │   55
		//         │           │
		//         │           │
		//         │           │   70
		//         │ A       B │
		//    ──►  x───────────┘   75
		// top left corner
		// scale 0.5
		editor.pointerMove(25, 51, { shiftKey: true })

		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 25, y: 70, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 45, y: 70, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxC)).toMatchObject({ x: 45, y: 50, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxD)).toMatchObject({ x: 25, y: 50, w: 5, h: 5 })

		//
		//        50 55     70 75
		//         ┌───────────┐   50
		//         │ C       D │
		//         │           │   55
		//         │           │
		//         │           │
		//         │           │   70
		//         │ B       A │
		//         └───────────x   75
		// top left corner     ▲
		// scale 0.5           │
		editor.pointerMove(51, 75, { shiftKey: true })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 70, y: 70, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 50, y: 70, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxC)).toMatchObject({ x: 50, y: 50, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxD)).toMatchObject({ x: 70, y: 50, w: 5, h: 5 })

		// top left corner     │
		// scale 0.5           ▼
		//         ┌───────────x   25
		//         │ B       A │
		//         │           │   30
		//         │           │
		//         │           │
		//         │           │   45
		//         │ C       D │
		//         └───────────┘   50
		//       50 55     70 75
		editor.pointerMove(51, 25, { shiftKey: true })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 70, y: 25, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 50, y: 25, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxC)).toMatchObject({ x: 50, y: 45, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxD)).toMatchObject({ x: 70, y: 45, w: 5, h: 5 })
	})
	it('preserves the correct alignment when dragging the top right corner around', () => {
		editor.select(ids.boxA, ids.boxB, ids.boxC, ids.boxD)
		editor.pointerDown(50, 0, { target: 'selection', handle: 'top_right' })
		//       -25 -20    -5 0
		//         ┌───────────┐   50
		//         │ C       D │
		//         │           │   55
		//         │           │
		//         │           │
		//         │           │   70
		//         │ B       A │
		//    ──►  x───────────┘   75
		// top right corner
		// scale 0.5
		editor.pointerMove(-25, 75, { shiftKey: true })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: -5, y: 70, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: -25, y: 70, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxC)).toMatchObject({ x: -25, y: 50, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxD)).toMatchObject({ x: -5, y: 50, w: 5, h: 5 })

		//        0 5       20 25
		//         ┌───────────┐   50
		//         │ D       C │
		//         │           │   55
		//         │           │
		//         │           │
		//         │           │   70
		//         │ A       B │
		//         └───────────x   75
		// top right corner    ▲
		// scale 0.5           │
		editor.pointerMove(25, 75, { shiftKey: true })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 0, y: 70, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 20, y: 70, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxC)).toMatchObject({ x: 20, y: 50, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxD)).toMatchObject({ x: 0, y: 50, w: 5, h: 5 })

		//      top right corner
		//   │  scale 0.5
		//   ▼
		//   x───────────┐   25
		//   │ B       A │
		//   │           │   30
		//   │           │
		//   │           │
		//   │           │   45
		//   │ C       D │
		//   └───────────┘   50
		// -25 -20    -5 0
		editor.pointerMove(-25, 25, { shiftKey: true })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: -5, y: 25, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: -25, y: 25, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxC)).toMatchObject({ x: -25, y: 45, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxD)).toMatchObject({ x: -5, y: 45, w: 5, h: 5 })
	})
	it('preserves the correct alignment when dragging the bottom right corner around', () => {
		editor.select(ids.boxA, ids.boxB, ids.boxC, ids.boxD)
		editor.pointerDown(50, 50, { target: 'selection', handle: 'bottom_right' })
		//       -25 -20    -5 0
		//         ┌───────────┐    0
		//         │ B       A │
		//         │           │    5
		//         │           │
		//         │           │
		//         │           │   20
		//         │ C       D │
		//    ──►  x───────────┘   25
		// bottom right corner
		// scale 0.5
		editor.pointerMove(-25, 25, { shiftKey: true })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: -5, y: 0, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: -25, y: 0, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxC)).toMatchObject({ x: -25, y: 20, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxD)).toMatchObject({ x: -5, y: 20, w: 5, h: 5 })
		// bottom right corner │
		// scale 0.5           ▼
		//         ┌───────────x -25
		//         │ D       C │
		//         │           │ -20
		//         │           │
		//         │           │
		//         │           │  -5
		//         │ A       B │
		//         └───────────┘   0
		//        0  5      20  25
		editor.pointerMove(25, -25, { shiftKey: true })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 0, y: -5, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 20, y: -5, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxC)).toMatchObject({ x: 20, y: -25, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxD)).toMatchObject({ x: 0, y: -25, w: 5, h: 5 })
		//      bottom right corner
		//   │  scale 0.5
		//   ▼
		//   x───────────┐ -25
		//   │ C       D │
		//   │           │ -20
		//   │           │
		//   │           │
		//   │           │  -5
		//   │ B       A │
		//   └───────────┘   0
		// -25 -20    -5 0
		editor.pointerMove(-25, -25, { shiftKey: true })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: -5, y: -5, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: -25, y: -5, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxC)).toMatchObject({ x: -25, y: -25, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxD)).toMatchObject({ x: -5, y: -25, w: 5, h: 5 })
	})
	it('preserves the correct alignment when dragging the bottom left corner around', () => {
		editor.select(ids.boxA, ids.boxB, ids.boxC, ids.boxD)
		editor.pointerDown(0, 50, { target: 'selection', handle: 'bottom_left' })
		//        50 55     70 75
		//         ┌───────────┐    0
		//         │ B       A │
		//         │           │    5
		//         │           │
		//         │           │
		//         │           │   20
		//         │ C       D │
		//         └───────────x   25
		// bottom left corner  ▲
		// scale 0.5           │
		editor.pointerMove(75, 25, { shiftKey: true })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 70, y: 0, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 50, y: 0, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxC)).toMatchObject({ x: 50, y: 20, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxD)).toMatchObject({ x: 70, y: 20, w: 5, h: 5 })
		// bottom left corner  │
		// scale 0.5           ▼
		//         ┌───────────x -25
		//         │ C       D │
		//         │           │ -20
		//         │           │
		//         │           │
		//         │           │  -5
		//         │ B       A │
		//         └───────────┘   0
		//        50 55     70 75
		editor.pointerMove(75, -25, { shiftKey: true })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 70, y: -5, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 50, y: -5, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxC)).toMatchObject({ x: 50, y: -25, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxD)).toMatchObject({ x: 70, y: -25, w: 5, h: 5 })
		//     bottom left corner
		//  │  scale 0.5
		//  ▼
		//  x───────────┐ -25
		//  │ D       C │
		//  │           │ -20
		//  │           │
		//  │           │
		//  │           │  -5
		//  │ A       B │
		//  └───────────┘   0
		// 25 30    45 50
		editor.pointerMove(25, -25, { shiftKey: true })
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 25, y: -5, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 45, y: -5, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxC)).toMatchObject({ x: 45, y: -25, w: 5, h: 5 })
		expect(roundedPageBounds(ids.boxD)).toMatchObject({ x: 25, y: -25, w: 5, h: 5 })
	})
})

describe('resizing a selection of mixed rotations', () => {
	beforeEach(() => {
		//    0  10          40  50
		//
		//  0 ┌───┐           ┌───┐
		//    │ A │           │ B │ rot 90
		// 10 └───┘           └───┘
		//
		//
		//
		// 40 ┌───┐           ┌───┐
		//    │ D │ rot 270   │ C │ rot 180
		// 50 └───┘           └───┘
		editor.createShapes([
			box(ids.boxA, 0, 0),
			{ ...box(ids.boxB, 50, 0), rotation: Math.PI / 2 },
			{ ...box(ids.boxC, 50, 50), rotation: Math.PI },
			{ ...box(ids.boxD, 0, 50), rotation: Math.PI * 1.5 },
		])
	})
	it('does not lock the aspect ratio if the rotations are compatible', () => {
		editor.select(ids.boxA, ids.boxB, ids.boxC, ids.boxD)
		//     0        20              80      100
		//
		//     ┌──────────────────────────────────┐
		//  0  │ ┌───────┐              ┌───────┐ │
		//     │ │   A   │              │   B   │ │
		//  5  │ └───────┘              └───────┘ │
		//     │                                  │
		//     │                                  │
		// 20  │ ┌───────┐              ┌───────┐ │
		//     │ │   D   │              │   C   │ │
		// 25  │ └───────┘              └───────┘ │
		//     └──────────────────────────────────x drag
		editor.pointerDown(50, 50, { target: 'selection', handle: 'bottom_right' }).pointerMove(100, 25)
		expect(roundedPageBounds(ids.boxA)).toMatchObject({ x: 0, y: 0, w: 20, h: 5 })
		expect(roundedPageBounds(ids.boxB)).toMatchObject({ x: 80, y: 0, w: 20, h: 5 })
		expect(roundedPageBounds(ids.boxC)).toMatchObject({ x: 80, y: 20, w: 20, h: 5 })
		expect(roundedPageBounds(ids.boxD)).toMatchObject({ x: 0, y: 20, w: 20, h: 5 })
	})
	it('does lock the aspect ratio if the rotations are not compatible', () => {
		editor.updateShapes([{ id: ids.boxC, type: 'geo', rotation: Math.PI + Math.PI / 180 }])
		editor.select(ids.boxA, ids.boxB, ids.boxC, ids.boxD)
		editor.pointerDown(50, 50, { target: 'selection', handle: 'bottom_right' }).pointerMove(100, 25)
		expect(roundedPageBounds(ids.boxA, 0.5)).toMatchObject({ x: 0, y: 0, w: 20, h: 20 })
	})

	it('does lock the aspect ratio if one of the shapes has a child with an incompatible aspect ratio', () => {
		editor.updateShapes([
			{ id: ids.boxC, type: 'geo', rotation: Math.PI + Math.PI / 180, parentId: ids.boxA },
		])

		editor.select(ids.boxA, ids.boxB, ids.boxD)
		editor.pointerDown(50, 50, { target: 'selection', handle: 'bottom_right' }).pointerMove(100, 25)
		expect(roundedPageBounds(ids.boxA, 0.5)).toMatchObject({ x: 0, y: 0, w: 20, h: 20 })
	})
})

// describe('Icons', () => {
// 	beforeEach(() => {
// 		editor =new TestEditor()

// 		editor.createShapes([
// 			{
// 				id: ids.iconA,
// 				type: 'icon',
// 				x: 0,
// 				y: 0,
// 				props: {
// 					size: 'm',
// 				},
// 			},
// 		])
// 	})

// 	it('scale correctly from each corner', () => {
// 		editor.select(ids.iconA)

// 		// Scale to 2x from bottom right corner
// 		app
// 			.pointerDown(32, 32, { target: 'selection', handle: 'bottom_right' })
// 			.pointerMove(64, 64)
// 			.pointerUp()

// 		expect(editor.getShape(ids.iconA)).toMatchObject({
// 			x: 0,
// 			y: 0,
// 			props: {
// 				scale: 2,
// 			},
// 		})
// 		expect(editor.getPageBounds(ids.iconA)).toMatchObject({
// 			width: 64,
// 			height: 64,
// 		})

// 		// Scale to 1x from top right corner
// 		app
// 			.pointerDown(64, 0, { target: 'selection', handle: 'top_right' })
// 			.pointerMove(32, 32)
// 			.pointerUp()
// 		expect(editor.getShape(ids.iconA)).toMatchObject({
// 			x: 0,
// 			y: 32,
// 			props: {
// 				scale: 1,
// 			},
// 		})
// 		expect(editor.getPageBounds(ids.iconA)).toMatchObject({
// 			width: 32,
// 			height: 32,
// 		})

// 		// Scale to 0.5x from top left corner but make sure
// 		// the min scale works
// 		app
// 			.pointerDown(0, 32, { target: 'selection', handle: 'top_left' })
// 			.pointerMove(16, 48)
// 			.pointerUp()
// 		expect(editor.getShape(ids.iconA)).toMatchObject({
// 			x: 16,
// 			y: 48,
// 			props: {
// 				scale: 0.5,
// 			},
// 		})
// 		expect(editor.getPageBounds(ids.iconA)).toMatchObject({
// 			width: 16,
// 			height: 16,
// 		})
// 	})
// })

describe('editor.resizeNoteShape', () => {
	beforeEach(() => {
		editor.getShapeUtil<NoteShapeUtil>('note').options.resizeMode = 'scale'
	})

	it('can scale when that option is set to true', () => {
		const noteBId = createShapeId('noteB')
		editor.createShapes([box(ids.boxA, 0, 0, 200, 200), { id: noteBId, type: 'note', x: 0, y: 0 }])

		// the default width and height of a note is 200
		expect(editor.getShapePageBounds(ids.boxA)).toMatchObject({ x: 0, y: 0, w: 200, h: 200 })
		expect(editor.getShapePageBounds(noteBId)).toMatchObject({ x: 0, y: 0, w: 200, h: 200 })

		editor.select(ids.boxA, noteBId)

		editor.resizeSelection({ scaleX: 2, scaleY: 2.1 }, 'bottom_right')

		expect(editor.getShapePageBounds(ids.boxA)).toMatchObject({ x: 0, y: 0, w: 420, h: 420 })

		expect(editor.getShape(noteBId)).toMatchObject({ x: 0, y: 0, props: { scale: 2.1 } }) // but scaled!

		expect(editor.getShapePageBounds(noteBId)).toMatchObject({ x: 0, y: 0, w: 420, h: 420 })
	})
})

describe('shapes that have do not resize', () => {
	it('are still translated if part of a selection', () => {
		const noteBId = createShapeId('noteB')
		editor.createShapes([box(ids.boxA, 0, 0, 200, 200), { id: noteBId, type: 'note', x: 0, y: 0 }])

		// the default width and height of a note is 200
		expect(editor.getShapePageBounds(ids.boxA)).toMatchObject({ x: 0, y: 0, w: 200, h: 200 })
		expect(editor.getShapePageBounds(noteBId)).toMatchObject({ x: 0, y: 0, w: 200, h: 200 })

		editor.select(ids.boxA, noteBId)

		editor.resizeSelection({ scaleX: 2, scaleY: 2.1 }, 'bottom_right')

		expect(editor.getShapePageBounds(ids.boxA)).toMatchObject({ x: 0, y: 0, w: 400, h: 420 })
		// noteB should be in the middle of boxA
		expect(editor.getShapePageBounds(noteBId)).toMatchObject({ x: 100, y: 110, w: 200, h: 200 })
	})

	it('can flip', () => {
		const noteBId = createShapeId('noteB')
		const noteCId = createShapeId('noteC')
		editor.createShapes([
			box(ids.boxA, 0, 0, 200, 200),
			{ id: noteBId, type: 'note', x: 300, y: 0 },
			{ id: noteCId, type: 'note', x: 0, y: 300 },
		])

		editor.select(ids.boxA, noteBId, noteCId)

		editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')

		expect(editor.getShapePageBounds(ids.boxA)).toMatchObject({ x: 300, y: 0, w: 200, h: 200 })
		expect(editor.getShapePageBounds(noteBId)).toMatchObject({ x: 0, y: 0, w: 200, h: 200 })
		expect(editor.getShapePageBounds(noteCId)).toMatchObject({ x: 300, y: 300, w: 200, h: 200 })

		editor.flipShapes(editor.getSelectedShapeIds(), 'vertical')

		expect(editor.getShapePageBounds(ids.boxA)).toMatchObject({
			x: 300,
			y: 300,
			w: 200,
			h: 200,
		})
		expect(editor.getShapePageBounds(noteBId)).toMatchObject({ x: 0, y: 300, w: 200, h: 200 })
		expect(editor.getShapePageBounds(noteCId)).toMatchObject({ x: 300, y: 0, w: 200, h: 200 })
	})
})

// describe('clicking the drag handle imprecisely', () => {
//   it('does not prevent grid snapping', () => {
//     // 0   10
//     //  ┌───┐
//     //  │ A │
//     //  └───┘

//     editor =new TestScene({
//       nodes: [box(ids.boxA, 0, 0)],
//     })

//     editor.setGrid(true)

//     // click bottom right handle with x: 2, y: -3 offset
//     app
//       .select(ids.boxA)
//       .pointerDown(12, 7, {
//         target: 'selection',
//         handle: 'bottom_right',
//       })
//       .pointerMove(20, 20)

//     // corner point is actually at 18, 23
//     // nearest grid point is at 16, 24
//     expect(roundedPageBounds(ids.boxA)).toEqual({ x: 0, y: 0, w: 16, h: 24 })
//   })
//   it('does not prevent edge snapping', () => {
//     // 0   20      60     100
//     //  ┌───┐       ┌───────┐
//     //  │ A │       │ B     │
//     //  └───┘       │       │
//     //              │       │
//     //              └───────┘

//     editor =new TestScene({
//       nodes: [box(ids.boxA, 0, 0, 20, 20), box(ids.boxB, 60, 0, 40, 40)],
//     })

//     // offset by x: 5, y: -3
//     app
//       .select(ids.boxA)
//       .pointerDown(25, 17, { target: 'selection', handle: 'bottom_right' })

//     // 0   20      60     100
//     //  x───────────x───────x
//     //  │ A         │ B     │
//     //  x───────────x   x   │
//     //              │       │
//     //              └───────┘
//     // snap bottom-right corner of A to left edge and center of B
//     editor.pointerMove(72, 9, undefined, { ctrlKey: true })
//     // actual corner point is 67, 12, should snap to 60, 20

//     expect(roundedPageBounds(ids.boxA)).toEqual({ x: 0, y: 0, w: 60, h: 20 })
//   })
// })

// describe('nodes that have aspect ratio locked', () => {
//   // no onResize return value
//   class AspectRatioAlwaysLocked extends TLBoxShape {
//     static override id = 'aspect_ratio_always_locked'
//     override canChangeAspectRatio = () => {
//       return false
//     }
//   }

//   beforeEach(() => {
//     //   0   10  20   30
//     //    ┌───┐   ┌───┐
//     //    │ A │   │ B │
//     // 10 └───┘   └───┘
//     //
//     // 20 ┌───┐
//     //    │ C │
//     // 30 └───┘
//     editor =new TestScene({
//       shapeUtils: [TLBoxShape, AspectRatioAlwaysLocked],
//       nodes: [
//         {
//           id: ids.boxA,
//           type: 'geo',
//           x: 0,
//           y: 0,
//           width: 10,
//           height: 10,
//           isAspectRatioLocked: false,
//         },
//         {
//           id: ids.boxB,
//           type: 'geo',
//           x: 20,
//           y: 0,
//           width: 10,
//           height: 10,
//           isAspectRatioLocked: true,
//         },
//         { id: ids.boxC, type: AspectRatioAlwaysLocked.id, x: 0, y: 20, width: 10, height: 10 },
//       ],
//     })
//   })

//   it('can have their aspect ratio locked by the class property', () => {
//     //   0   10  20   30
//     //    ┌───┐   ┌───┐
//     //    │ A │   │ B │
//     // 10 └───┘   └───┘
//     //
//     // 20 ┌───┐
//     //    │ C │
//     // 30 └───x drag ->
//     app
//       .select(ids.boxC)
//       .pointerDown(10, 30, { target: 'selection', handle: 'bottom_right' })
//       .pointerMove(20, 30)
//     //   0   10  20   30
//     //    ┌───┐   ┌───┐
//     //    │ A │   │ B │
//     // 10 └───┘   └───┘
//     //
//     // 20 ┌───────┐
//     //    │ C     │
//     // 30 │       x pointer is here
//     //    │       │
//     //    └───────┘
//     expect(roundedPageBounds(ids.boxC)).toEqual({ x: 0, y: 20, w: 20, h: 20 })
//   })

//   it('can have their aspect ratio locked by the model property', () => {
//     //   0   10  20   30
//     //    ┌───┐   ┌───┐
//     //    │ A │   │ B │
//     // 10 └───┘   └───x drag ->
//     //
//     // 20 ┌───┐
//     //    │ C │
//     // 30 └───┘
//     app
//       .select(ids.boxB)
//       .pointerDown(30, 10, { target: 'selection', handle: 'bottom_right' })
//       .pointerMove(40, 10)
//     //   0   10  20   30
//     //    ┌───┐   ┌───────┐
//     //    │ A │   │ B     │
//     // 10 └───┘   │       x pointer is here
//     //            │       │
//     // 20 ┌───┐   └───────┘
//     //    │ C │
//     // 30 └───┘
//     expect(roundedPageBounds(ids.boxB)).toEqual({ x: 20, y: 0, w: 20, h: 20 })
//   })
//   it('cause the whole selection to have the aspect ratio locked (model)', () => {
//     //   0   10  20   30
//     //    ┌───┐- -┌───┐
//     //    │ A │   │ B │
//     // 10 └───┘- -└───x drag ->
//     //
//     // 20 ┌───┐
//     //    │ C │
//     // 30 └───┘
//     //

//     app
//       .select(ids.boxA, ids.boxB)
//       .pointerDown(30, 10, { target: 'selection', handle: 'bottom_right' })
//       .pointerMove(60, 10)
//     //   0   10          40       60
//     //    ┌───────┬·······┬───────┐
//     //    │ A     │       │ B     │
//     // 10 │       │       │       x pointer
//     //    │       │       │       │
//     // 20 ├───┬───┴·······┴───────┘
//     //    │ C │
//     // 30 └───┘
//     expect(roundedPageBounds(ids.boxA)).toEqual({ x: 0, y: 0, w: 20, h: 20 })
//     expect(roundedPageBounds(ids.boxB)).toEqual({ x: 40, y: 0, w: 20, h: 20 })
//   })
//   it('cause the whole selection to have the aspect ratio locked (class property)', () => {
//     //   0   10  20   30
//     //    ┌───┐   ┌───┐
//     //    │ A │   │ B │
//     // 10 └───┘   └───┘
//     //    |   |
//     // 20 ┌───┐
//     //    │ C │
//     // 30 └───x drag ->
//     app
//       .select(ids.boxA, ids.boxC)
//       .pointerDown(10, 30, { target: 'selection', handle: 'bottom_right' })
//       .pointerMove(10, 60)
//     //   0   10  20   30
//     //    ┌───────┬───┐
//     //    │ A     │ B │
//     // 10 │       ├───┘
//     //    │       │
//     //    └───────┘
//     //    |       |
//     //    |       x pointer is here
//     //    ┌───────┐
//     //    │ C     │
//     //    │       │
//     //    │       │
//     //    └───────┘
//     expect(roundedPageBounds(ids.boxA)).toEqual({ x: 0, y: 0, w: 20, h: 20 })
//     expect(roundedPageBounds(ids.boxC)).toEqual({ x: 0, y: 40, w: 20, h: 20 })
//   })
// })

// describe('bugs', () => {
// it('resizing a zero width shape', () => {
//	// Draw shapes can no longer have zero width / height
// 	const shapeId = createShapeId()
// 	app
// 		.createShapes([
// 			{
// 				id: shapeId,
// 				type: 'draw',
// 				x: 0,
// 				y: 0,
// 				props: {
// 					segments: [
// 						{
// 							type: 'straight',
// 							points: [
// 								{ x: 0, y: 0 },
// 								{ x: 0, y: 100 },
// 							],
// 						},
// 					],
// 				},
// 			},
// 		])
// 		.select(shapeId)
// 	expect(editor.selectionRotatedBounds!.width).toBe(0)
// 	editor.pointerDown(0, 100, { target: 'selection', handle: 'bottom_right' }).pointerMove(10, 110)
// 	expect(editor.selectionRotatedBounds!.width).toBe(0)
// })
// })

it('uses the cross cursor when create resizing', () => {
	editor.setCurrentTool('geo')
	editor.pointerDown(0, 0)
	editor.pointerMove(100, 100)
	editor.expectToBeIn('select.resizing')
	expect(editor.getInstanceState().cursor.type).toBe('cross')
	expect(editor.getInstanceState().cursor.rotation).toBe(0)

	editor.pointerMove(120, 120)
	expect(editor.getInstanceState().cursor.type).toBe('cross')
	expect(editor.getInstanceState().cursor.rotation).toBe(0)

	editor.pointerMove(-120, -120)
	expect(editor.getInstanceState().cursor.type).toBe('cross')
	expect(editor.getInstanceState().cursor.rotation).toBe(0)
})

describe('Resizing text from the right edge', () => {
	it('Resizes text from the right edge', () => {
		const id = createShapeId()
		editor.createShapes([{ id, type: 'text', props: { richText: toRichText('H') } }])
		editor.updateShapes([{ id, type: 'text', props: { richText: toRichText('Hello World') } }]) // auto size

		editor.select(id)

		const bounds = editor.getShapeGeometry(id).bounds

		editor.updateInstanceState({ isCoarsePointer: false })

		// Resize from the right edge
		editor.pointerDown(bounds.maxX, bounds.midY, { target: 'selection', handle: 'right' }) // right edge
		editor.expectToBeIn('select.pointing_resize_handle')
		editor.pointerMove(bounds.maxX + 5, bounds.midY, { target: 'selection', handle: 'right' })
		editor.expectToBeIn('select.resizing')
		editor.pointerUp()

		editor.expectShapeToMatch<TLTextShape>({
			id,
			type: 'text',
			props: { richText: toRichText('Hello World'), w: bounds.width + 5 },
		})
	})

	it('Resizes text from the right edge when pointer is coarse', () => {
		editor.updateInstanceState({ isCoarsePointer: true })

		const id = createShapeId()
		editor.createShapes([{ id, type: 'text', props: { richText: toRichText('H') } }])
		editor.updateShapes([{ id, type: 'text', props: { richText: toRichText('Hello World') } }]) // auto size

		editor.select(id)

		const bounds = editor.getShapeGeometry(id).bounds

		// Resize from the right edge
		editor.pointerDown(bounds.maxX, bounds.midY, { target: 'selection', handle: 'right' }) // right edge
		editor.expectToBeIn('select.pointing_resize_handle')
		editor.pointerMove(bounds.maxX + 5, bounds.midY, { target: 'selection', handle: 'right' })
		editor.expectToBeIn('select.pointing_resize_handle')
		editor.pointerMove(bounds.maxX + 10, bounds.midY, { target: 'selection', handle: 'right' })
		editor.expectToBeIn('select.resizing')
		editor.pointerUp()

		editor.expectShapeToMatch<TLTextShape>({
			id,
			type: 'text',
			props: { richText: toRichText('Hello World'), w: bounds.width + 10 },
		})
	})
})

describe('When resizing near the edges of the screen', () => {
	it('resizes past the edge of the screen', () => {
		editor.user.updateUserPreferences({ edgeScrollSpeed: 1 })
		const before = editor.getShape<TLGeoShape>(ids.boxA)!
		editor
			.select(ids.boxA)
			.pointerDown(10, 10, {
				type: 'pointer',
				target: 'selection',
				handle: 'top_left',
			})
			.pointerMove(-1, -1) // into the edge scrolling distance
		vi.advanceTimersByTime(1000)
		const after = editor.getShape<TLGeoShape>(ids.boxA)!
		expect(after.x).toBeLessThan(before.x)
		expect(after.y).toBeLessThan(before.y)
		expect(after.props.w).toBeGreaterThan(before.props.w)
		expect(after.props.h).toBeGreaterThan(before.props.h)
	})
})

describe('resizing text with autosize true', () => {
	it('resizes text from the right side', () => {
		editor.createShape({
			type: 'text',
			x: 0,
			y: 0,
			props: {
				richText: toRichText('Hello'),
				autoSize: false,
				w: 200,
			},
		})

		const shape = editor.getLastCreatedShape()

		const bounds = editor.getShapePageBounds(shape.id)!
		editor
			.select(shape)
			.pointerDown(bounds.maxX, bounds.midY, { target: 'selection', handle: 'right' }) // right edge
			.expectToBeIn('select.pointing_resize_handle')
			.pointerMove(bounds.maxX + 100, bounds.midY)
			.expectToBeIn('select.resizing')
			.expectShapeToMatch({ ...shape, x: 0, y: 0, props: { w: 300 } })
			.pointerMove(bounds.maxX - 10, bounds.midY)
			.expectShapeToMatch({ ...shape, x: 0, y: 0, props: { w: 190 } })
	})

	it('resizes text from the right side when alt key is pressed', () => {
		editor.createShape({
			type: 'text',
			x: 0,
			y: 0,
			props: {
				richText: toRichText('Hello'),
				autoSize: false,
				w: 200,
			},
		})

		const shape = editor.getLastCreatedShape()

		const bounds = editor.getShapePageBounds(shape.id)!
		editor
			.select(shape)
			.keyDown('Alt')
			.pointerDown(bounds.maxX, bounds.midY, { target: 'selection', handle: 'right' }) // right edge
			.expectToBeIn('select.pointing_resize_handle')
			.pointerMove(bounds.maxX + 100, bounds.midY)
			.expectToBeIn('select.resizing')
			.expectShapeToMatch({ ...shape, x: -100, y: 0, props: { w: 400 } })
			.pointerMove(bounds.maxX - 10, bounds.midY)
			.expectShapeToMatch({ ...shape, x: 10, y: 0, props: { w: 180 } })
	})

	it('resizes text from the left side', () => {
		editor.createShape({
			type: 'text',
			x: 0,
			y: 0,
			props: {
				richText: toRichText('Hello'),
				autoSize: false,
				w: 200,
			},
		})

		const shape = editor.getLastCreatedShape()

		const bounds = editor.getShapePageBounds(shape.id)!
		editor
			.select(shape)
			.pointerDown(bounds.minX, bounds.midY, { target: 'selection', handle: 'left' }) // right edge
			.expectToBeIn('select.pointing_resize_handle')
			.pointerMove(bounds.minX - 100, bounds.midY)
			.expectToBeIn('select.resizing')
			.expectShapeToMatch({ ...shape, x: -100, y: 0, props: { w: 300 } })
			.pointerMove(bounds.minX + 10, bounds.midY)
			.expectShapeToMatch({ ...shape, x: 10, y: 0, props: { w: 190 } })
	})

	it('resizes text from the left side when alt is pressed', () => {
		editor.createShape({
			type: 'text',
			x: 0,
			y: 0,
			props: {
				richText: toRichText('Hello'),
				autoSize: false,
				w: 200,
			},
		})

		const shape = editor.getLastCreatedShape()

		const bounds = editor.getShapePageBounds(shape.id)!
		editor
			.select(shape)
			.keyDown('Alt')
			.pointerDown(bounds.minX, bounds.midY, { target: 'selection', handle: 'left' }) // right edge
			.expectToBeIn('select.pointing_resize_handle')
			.pointerMove(bounds.minX - 100, bounds.midY)
			.expectToBeIn('select.resizing')
			.expectShapeToMatch({ ...shape, x: -100, y: 0, props: { w: 400 } })
			.pointerMove(bounds.minX + 10, bounds.midY)
			.expectShapeToMatch({ ...shape, x: 10, y: 0, props: { w: 180 } })
	})
})

describe('cancelling a resize operation', () => {
	it('undoes any changes since the start of the resize operation', () => {
		editor.createShape({
			type: 'geo',
			x: 0,
			y: 0,
			props: {
				w: 100,
				h: 100,
			},
		})

		const shape = editor.getLastCreatedShape()

		editor.select(shape)

		const bounds = editor.getShapePageBounds(shape.id)!
		editor.pointerDown(bounds.maxX, bounds.midY, { target: 'selection', handle: 'right' }) // right edge
		editor.pointerMove(bounds.maxX + 100, bounds.midY)
		expect(editor.getShapePageBounds(shape.id)).toMatchObject({ x: 0, y: 0, w: 200, h: 100 })
		editor.cancel()
		expect(editor.getShapePageBounds(shape.id)).toMatchObject({ x: 0, y: 0, w: 100, h: 100 })
	})

	it('undoes the shape creation if creating a shape', () => {
		editor.setCurrentTool('geo')
		editor.pointerDown(0, 0)
		editor.pointerMove(100, 100)
		editor.expectToBeIn('select.resizing')
		const shape = editor.getLastCreatedShape()
		expect(editor.getShapePageBounds(shape)).toMatchObject({ x: 0, y: 0, w: 100, h: 100 })
		editor.cancel()
		expect(editor.getShape(shape.id)).toBeUndefined()
	})
})
