UNPKG

tldraw

Version:

A tiny little drawing editor.

1,638 lines (1,450 loc) 66.3 kB
import { TLImageShape, Vec, createShapeId } from '@tldraw/editor' import { getCropBox, getCroppedImageDataForAspectRatio, getCroppedImageDataForReplacedImage, getCroppedImageDataWhenZooming, } from '../lib/shapes/shared/crop' import { TestEditor } from './TestEditor' let editor: TestEditor let shape: TLImageShape const initialSize = { w: 100, h: 100 } const initialCrop = { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 }, } beforeEach(() => { editor = new TestEditor() const id = createShapeId() as TLImageShape['id'] editor.createShapes([ { id, type: 'image', x: 100, y: 100, props: { ...initialSize, crop: initialCrop, }, }, ]) shape = editor.getShape<TLImageShape>(id)! }) describe('Crop box', () => { it('Crops from the top left', () => { const results = getCropBox(shape, { handle: 'top_left', change: new Vec(10, 20), crop: initialCrop, uncroppedSize: initialSize, initialShape: shape, }) expect(results).toMatchObject({ x: 110, y: 120, props: { w: 90, h: 80, crop: { topLeft: { x: 0.1, y: 0.2 }, bottomRight: { x: 1, y: 1 }, }, }, }) }) it('Crops from the top right', () => { const results = getCropBox(shape, { handle: 'top_right', change: new Vec(-10, 20), crop: initialCrop, uncroppedSize: initialSize, initialShape: shape, }) expect(results).toMatchObject({ x: 100, y: 120, props: { w: 90, h: 80, crop: { topLeft: { x: 0, y: 0.2 }, bottomRight: { x: 0.9, y: 1 }, }, }, }) }) it('Crops from the bottom right', () => { const results = getCropBox(shape, { handle: 'bottom_right', change: new Vec(-10, -20), crop: initialCrop, uncroppedSize: initialSize, initialShape: shape, }) expect(results).toMatchObject({ x: 100, y: 100, props: { w: 90, h: 80, crop: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 0.9, y: 0.8 }, }, }, }) }) it('Crops from the bottom left', () => { const results = getCropBox(shape, { handle: 'bottom_left', change: new Vec(10, -20), crop: initialCrop, uncroppedSize: initialSize, initialShape: shape, }) expect(results).toMatchObject({ x: 110, y: 100, props: { w: 90, h: 80, crop: { topLeft: { x: 0.1, y: 0 }, bottomRight: { x: 1, y: 0.8 }, }, }, }) }) it('Crop returns undefined when the crop does not change', () => { const results = getCropBox(shape, { handle: 'top_left', change: new Vec(-10, 0), crop: initialCrop, uncroppedSize: initialSize, initialShape: shape, }) expect(results).toBeUndefined() }) it('Crop returns undefined if existing width and height is already less than minWidth and minHeight', () => { const results = getCropBox( shape, { handle: 'top_left', change: new Vec(10, 20), crop: initialCrop, uncroppedSize: initialSize, initialShape: shape, }, { minWidth: 110, minHeight: 110, } ) expect(results).toBeUndefined() }) }) describe('getCroppedImageDataWhenZooming', () => { it('maintains the aspect ratio when zooming', () => { const imageShape: TLImageShape = { ...shape, props: { ...shape.props, w: 100, h: 50, // 2:1 aspect ratio crop: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 }, }, }, } const result = getCroppedImageDataWhenZooming(0.5, imageShape) // Check that aspect ratio is preserved expect(result.w / result.h).toBeCloseTo(2, 5) // Check that crop dimensions are correct const cropWidth = result.crop.bottomRight.x - result.crop.topLeft.x const cropHeight = result.crop.bottomRight.y - result.crop.topLeft.y expect(cropWidth / cropHeight).toBe(1) }) it('maintains the aspect ratio of a non-default crop when zooming', () => { const imageShape: TLImageShape = { ...shape, props: { ...shape.props, w: 80, h: 80, // 1:1 original image crop: { // 2:1 crop aspect ratio topLeft: { x: 0.25, y: 0.375 }, bottomRight: { x: 0.75, y: 0.625 }, }, }, } const result = getCroppedImageDataWhenZooming(0.5, imageShape) // Check that the crop aspect ratio is preserved (2:1) const cropWidth = result.crop.bottomRight.x - result.crop.topLeft.x const cropHeight = result.crop.bottomRight.y - result.crop.topLeft.y expect(cropWidth / cropHeight).toBeCloseTo(2, 5) expect(result.w / result.h).toBe(1) }) it('applies zoom scaling (max 3x)', () => { const imageShape: TLImageShape = { ...shape, props: { ...shape.props, w: 100, h: 100, crop: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 }, }, }, } // Zoom to 100% (max zoom) const result = getCroppedImageDataWhenZooming(1, imageShape) // At max zoom, the crop window should be smaller const cropWidth = result.crop.bottomRight.x - result.crop.topLeft.x const cropHeight = result.crop.bottomRight.y - result.crop.topLeft.y expect(cropWidth).toBeCloseTo(0.33, 2) expect(cropHeight).toBeCloseTo(0.33, 2) expect(result.w).toBeCloseTo(99.99, 1) expect(result.h).toBeCloseTo(99.99, 1) }) it('applies custom maxZoom scaling', () => { const imageShape: TLImageShape = { ...shape, props: { ...shape.props, w: 100, h: 100, crop: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 }, }, }, } // Apply zoom with a custom maxZoom of 2x const result = getCroppedImageDataWhenZooming(0.75, imageShape, 0.8) // Verify that the crop dimensions respect the custom maxZoom const cropWidth = result.crop.bottomRight.x - result.crop.topLeft.x const cropHeight = result.crop.bottomRight.y - result.crop.topLeft.y expect(cropWidth).toBe(0.25) expect(cropHeight).toBe(0.25) expect(result.w).toBeCloseTo(74.99, 1) expect(result.h).toBeCloseTo(74.99, 1) }) it('preserves circular crops', () => { const imageShape: TLImageShape = { ...shape, props: { ...shape.props, w: 100, h: 100, crop: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 }, isCircle: true, }, }, } const result = getCroppedImageDataWhenZooming(0.5, imageShape) // Verify that isCircle property is preserved expect(result.crop.isCircle).toBe(true) }) it('preserves crop center when zooming with crop in bottom right quadrant', () => { // Create image with crop in bottom right quadrant const imageShape: TLImageShape = { ...shape, props: { ...shape.props, w: 100, h: 100, crop: { // Crop in bottom right quadrant topLeft: { x: 0.6, y: 0.6 }, bottomRight: { x: 0.9, y: 0.9 }, }, }, } // Calculate the center of the original crop const originalCropCenterX = (0.6 + 0.9) / 2 const originalCropCenterY = (0.6 + 0.9) / 2 // Apply zoom operation const result = getCroppedImageDataWhenZooming(0.5, imageShape) // Calculate center of the new crop const newCropCenterX = (result.crop.topLeft.x + result.crop.bottomRight.x) / 2 const newCropCenterY = (result.crop.topLeft.y + result.crop.bottomRight.y) / 2 // Center should be preserved expect(newCropCenterX).toBeCloseTo(originalCropCenterX, 5) expect(newCropCenterY).toBeCloseTo(originalCropCenterY, 5) }) }) describe('Circle crop preservation during resize', () => { it('preserves circle crop when resizing', () => { const circleShape: TLImageShape = { ...shape, props: { ...shape.props, w: 100, h: 100, crop: { topLeft: { x: 0.2, y: 0.2 }, bottomRight: { x: 0.8, y: 0.8 }, isCircle: true, }, }, } const results = getCropBox(circleShape, { handle: 'bottom_right', change: new Vec(-10, -15), crop: circleShape.props.crop!, uncroppedSize: initialSize, initialShape: circleShape, aspectRatioLocked: false, }) expect(results?.props.crop?.isCircle).toBe(true) }) it('preserves circle crop when resizing false', () => { const circleShape: TLImageShape = { ...shape, props: { ...shape.props, w: 100, h: 100, crop: { topLeft: { x: 0.2, y: 0.2 }, bottomRight: { x: 0.8, y: 0.8 }, isCircle: false, }, }, } const results = getCropBox(circleShape, { handle: 'bottom_right', change: new Vec(-10, -15), crop: circleShape.props.crop!, uncroppedSize: initialSize, initialShape: circleShape, aspectRatioLocked: false, }) expect(results?.props.crop?.isCircle).toBe(false) }) }) describe('getCroppedImageDataForAspectRatio', () => { it('returns full image for original aspect ratio', () => { const imageShape: TLImageShape = { ...shape, props: { ...shape.props, w: 80, h: 100, crop: { topLeft: { x: 0.2, y: 0.1 }, bottomRight: { x: 0.8, y: 0.9 }, }, }, } const result = getCroppedImageDataForAspectRatio('original', imageShape) expect(result?.crop).toEqual({ topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 }, }) expect(result?.w).toBeCloseTo(133, 0) expect(result?.h).toBeCloseTo(125, 0) }) it('creates perfect squares for square aspect ratio', () => { const imageShape: TLImageShape = { ...shape, props: { ...shape.props, w: 100, h: 50, // 2:1 aspect ratio crop: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 }, }, }, } const result = getCroppedImageDataForAspectRatio('square', imageShape) // Crop window should be square const aspectRatio = result.w / result.h expect(aspectRatio).toEqual(1) }) it('creates circular crops for circle aspect ratio', () => { const imageShape: TLImageShape = { ...shape, props: { ...shape.props, w: 100, h: 80, crop: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 }, }, }, } const result = getCroppedImageDataForAspectRatio('circle', imageShape) // Should be marked as a circle expect(result?.crop.isCircle).toBe(true) // Crop window should be 1:1 const aspectRatio = result.w / result.h expect(aspectRatio).toEqual(1) }) it('applies landscape crop to a square image', () => { // Start with a square image (100x100) const squareImage: TLImageShape = { ...shape, props: { ...shape.props, w: 100, h: 100, crop: { // Default crop (full image) topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 }, }, }, } // Apply a landscape crop const result = getCroppedImageDataForAspectRatio('landscape', squareImage) // Should have 4:3 aspect ratio (landscape) expect((result?.w as number) / (result?.h as number)).toBeCloseTo(4 / 3, 5) // Should preserve the center of the image const cropCenterX = (result!.crop.topLeft.x + result!.crop.bottomRight.x) / 2 const cropCenterY = (result!.crop.topLeft.y + result!.crop.bottomRight.y) / 2 expect(cropCenterX).toBeCloseTo(0.5, 5) expect(cropCenterY).toBeCloseTo(0.5, 5) }) it('applies portrait crop to a square image', () => { // Start with a square image (100x100) const squareImage: TLImageShape = { ...shape, props: { ...shape.props, w: 100, h: 100, crop: { // Default crop (full image) topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 }, }, }, } // Apply a portrait crop const result = getCroppedImageDataForAspectRatio('portrait', squareImage) // Should have 3:4 aspect ratio (portrait) expect((result?.w as number) / (result?.h as number)).toBeCloseTo(3 / 4, 5) // Should preserve the center of the image const cropCenterX = (result!.crop.topLeft.x + result!.crop.bottomRight.x) / 2 const cropCenterY = (result!.crop.topLeft.y + result!.crop.bottomRight.y) / 2 expect(cropCenterX).toBeCloseTo(0.5, 5) expect(cropCenterY).toBeCloseTo(0.5, 5) }) it('preserves longest dimension when changing from circle to landscape aspect ratio', () => { // Start with a smaller circular crop on a larger image const imageWithCircleCrop: TLImageShape = { ...shape, props: { ...shape.props, w: 60, // Current displayed size h: 60, crop: { // Small circular crop in center, actual crop is 40x40 pixels of a 200x200 image topLeft: { x: 0.3, y: 0.3 }, bottomRight: { x: 0.7, y: 0.7 }, isCircle: true, }, }, } // The uncropped image would be 150x150 (60 / 0.4 = 150) // So the current crop represents 40x40 absolute pixels const result = getCroppedImageDataForAspectRatio('landscape', imageWithCircleCrop) // Should have 4:3 aspect ratio (landscape) expect((result?.w as number) / (result?.h as number)).toBeCloseTo(4 / 3, 5) // The longest dimension was 40 pixels (both width and height were equal) // For landscape (4:3), if we preserve 40 pixels as width: height = 40 * (3/4) = 30 // If we preserve 40 pixels as height: width = 40 * (4/3) = 53.33 // Since both current dimensions are equal, we should preserve the first one (width) // So we expect roughly: width preserved at ~40, height = 40 * (3/4) = 30 // Calculate the actual crop dimensions in absolute pixels const cropWidth = (result!.crop.bottomRight.x - result!.crop.topLeft.x) * 150 // uncropped width const cropHeight = (result!.crop.bottomRight.y - result!.crop.topLeft.y) * 150 // uncropped height // The width should be preserved (approximately 60 pixels, the current crop width) expect(cropWidth).toBeCloseTo(60, 1) // The height should be adjusted to maintain 4:3 ratio expect(cropHeight).toBeCloseTo(60 * (3 / 4), 1) }) it('preserves longest dimension when changing from wide rectangle to square', () => { // Start with a wide rectangular crop const imageWithWideCrop: TLImageShape = { ...shape, props: { ...shape.props, w: 120, // Current displayed size h: 60, crop: { // Wide crop: 80x40 pixels of a 100x100 image topLeft: { x: 0.1, y: 0.3 }, bottomRight: { x: 0.9, y: 0.7 }, }, }, } // The uncropped image would be 150x150 (120 / 0.8 = 150) // Current crop represents 120x60 absolute pixels (80% x 40% of 150x150) const result = getCroppedImageDataForAspectRatio('square', imageWithWideCrop) // Should have 1:1 aspect ratio (square) expect((result?.w as number) / (result?.h as number)).toBeCloseTo(1, 5) // The longest dimension was width (120 pixels), so it should be preserved // For square, both width and height should be 120 pixels // Calculate the actual crop dimensions in absolute pixels const cropWidth = (result!.crop.bottomRight.x - result!.crop.topLeft.x) * 150 const cropHeight = (result!.crop.bottomRight.y - result!.crop.topLeft.y) * 150 // Both should be equal to the preserved longest dimension expect(cropWidth).toBeCloseTo(120, 1) expect(cropHeight).toBeCloseTo(120, 1) }) it('preserves longest dimension when changing from tall rectangle to wide rectangle', () => { // Start with a tall rectangular crop const imageWithTallCrop: TLImageShape = { ...shape, props: { ...shape.props, w: 40, // Current displayed size h: 100, crop: { // Tall crop: 0.3 x 0.75 relative dimensions (30% width, 75% height) topLeft: { x: 0.35, y: 0.125 }, bottomRight: { x: 0.65, y: 0.875 }, }, }, } // Calculate uncropped size: if 0.3 relative width = 40 pixels, then uncropped = 40/0.3 = 133.33 // And if 0.75 relative height = 100 pixels, then uncropped = 100/0.75 = 133.33 ✓ const uncroppedSize = 40 / 0.3 // 133.33 // Current crop represents 40x100 absolute pixels const result = getCroppedImageDataForAspectRatio('wide', imageWithTallCrop) // Should have 16:9 aspect ratio (wide) expect((result?.w as number) / (result?.h as number)).toBeCloseTo(16 / 9, 5) // With the new zoom-level preserving logic, the function now tries to maintain // the current crop zoom level and adjusts dimensions accordingly. // The actual behavior may be different from the original expectation due to // zoom level preservation and boundary constraints. // Calculate the actual crop dimensions in absolute pixels const cropWidth = (result!.crop.bottomRight.x - result!.crop.topLeft.x) * uncroppedSize const cropHeight = (result!.crop.bottomRight.y - result!.crop.topLeft.y) * uncroppedSize // With zoom level preservation, the actual dimensions will be based on maintaining // the current zoom level while respecting the 16:9 aspect ratio expect(cropWidth).toBeCloseTo(100, 1) // Updated expectation based on new logic // Height adjusted to maintain 16:9 ratio expect(cropHeight).toBeCloseTo(100 * (9 / 16), 1) }) }) describe('Resizing crop box when not aspect-ratio locked', () => { it('Resizes from the top left corner', () => { const results = getCropBox(shape, { handle: 'top_left', change: new Vec(10, 20), crop: initialCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: false, }) expect(results).toMatchObject({ x: 110, y: 120, props: { w: 90, h: 80, crop: { topLeft: { x: 0.1, y: 0.2 }, bottomRight: { x: 1, y: 1 }, }, }, }) }) it('Resizes from the top right corner', () => { const results = getCropBox(shape, { handle: 'top_right', change: new Vec(-10, 20), crop: initialCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: false, }) expect(results).toMatchObject({ x: 100, y: 120, props: { w: 90, h: 80, crop: { topLeft: { x: 0, y: 0.2 }, bottomRight: { x: 0.9, y: 1 }, }, }, }) }) it('Resizes from the bottom right corner', () => { const results = getCropBox(shape, { handle: 'bottom_right', change: new Vec(-10, -20), crop: initialCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: false, }) expect(results).toMatchObject({ x: 100, y: 100, props: { w: 90, h: 80, crop: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 0.9, y: 0.8 }, }, }, }) }) it('Resizes from the bottom left corner', () => { const results = getCropBox(shape, { handle: 'bottom_left', change: new Vec(10, -20), crop: initialCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: false, }) expect(results).toMatchObject({ x: 110, y: 100, props: { w: 90, h: 80, crop: { topLeft: { x: 0.1, y: 0 }, bottomRight: { x: 1, y: 0.8 }, }, }, }) }) it('Resizes from the top edge', () => { const results = getCropBox(shape, { handle: 'top', change: new Vec(0, 20), crop: initialCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: false, }) expect(results).toMatchObject({ x: 100, y: 120, props: { w: 100, h: 80, crop: { topLeft: { x: 0, y: 0.2 }, bottomRight: { x: 1, y: 1 }, }, }, }) }) it('Resizes from the right edge', () => { const results = getCropBox(shape, { handle: 'right', change: new Vec(-10, 0), crop: initialCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: false, }) expect(results).toMatchObject({ x: 100, y: 100, props: { w: 90, h: 100, crop: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 0.9, y: 1 }, }, }, }) }) it('Resizes from the bottom edge', () => { const results = getCropBox(shape, { handle: 'bottom', change: new Vec(0, -20), crop: initialCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: false, }) expect(results).toMatchObject({ x: 100, y: 100, props: { w: 100, h: 80, crop: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 0.8 }, }, }, }) }) it('Resizes from the left edge', () => { const results = getCropBox(shape, { handle: 'left', change: new Vec(10, 0), crop: initialCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: false, }) expect(results).toMatchObject({ x: 110, y: 100, props: { w: 90, h: 100, crop: { topLeft: { x: 0.1, y: 0 }, bottomRight: { x: 1, y: 1 }, }, }, }) }) it('When overlapping edges, does not produce a result with < 0 or > 1 for x or y crop dimensions', () => { // Test dragging top edge down beyond bottom edge const results1 = getCropBox(shape, { handle: 'top', change: new Vec(0, 150), // Try to drag top edge past bottom crop: initialCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: false, }) if (results1?.props.crop) { expect(results1.props.crop.topLeft.y).toBeGreaterThanOrEqual(0) expect(results1.props.crop.topLeft.y).toBeLessThan(results1.props.crop.bottomRight.y) expect(results1.props.crop.bottomRight.y).toBeLessThanOrEqual(1) } // Test dragging left edge right beyond right edge const results2 = getCropBox(shape, { handle: 'left', change: new Vec(150, 0), // Try to drag left edge past right crop: initialCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: false, }) if (results2?.props.crop) { expect(results2.props.crop.topLeft.x).toBeGreaterThanOrEqual(0) expect(results2.props.crop.topLeft.x).toBeLessThan(results2.props.crop.bottomRight.x) expect(results2.props.crop.bottomRight.x).toBeLessThanOrEqual(1) } // Test dragging corner to extreme position const results3 = getCropBox(shape, { handle: 'top_left', change: new Vec(150, 150), // Try to drag top-left corner beyond bottom-right crop: initialCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: false, }) if (results3?.props.crop) { expect(results3.props.crop.topLeft.x).toBeGreaterThanOrEqual(0) expect(results3.props.crop.topLeft.y).toBeGreaterThanOrEqual(0) expect(results3.props.crop.topLeft.x).toBeLessThan(results3.props.crop.bottomRight.x) expect(results3.props.crop.topLeft.y).toBeLessThan(results3.props.crop.bottomRight.y) expect(results3.props.crop.bottomRight.x).toBeLessThanOrEqual(1) expect(results3.props.crop.bottomRight.y).toBeLessThanOrEqual(1) } }) }) describe('Resizing crop box when aspect-ratio locked', () => { // When resizing from a corner, the opposite corner should be preserved // When resizing from an edge, the opposite edge's mid point should be preserved // The result should have the same aspect ratio as the initial crop // The result should never have any dimension < 0 or > 1 it('Resizes from the top left corner', () => { const testCrop = { topLeft: { x: 0.2, y: 0.3 }, bottomRight: { x: 0.8, y: 0.7 }, } const initialAspectRatio = (testCrop.bottomRight.x - testCrop.topLeft.x) / (testCrop.bottomRight.y - testCrop.topLeft.y) const results = getCropBox(shape, { handle: 'top_left', change: new Vec(10, 15), crop: testCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }) if (results?.props.crop) { // Bottom right corner should be preserved expect(results.props.crop.bottomRight.x).toBeCloseTo(testCrop.bottomRight.x, 5) expect(results.props.crop.bottomRight.y).toBeCloseTo(testCrop.bottomRight.y, 5) // Aspect ratio should be maintained const newAspectRatio = (results.props.crop.bottomRight.x - results.props.crop.topLeft.x) / (results.props.crop.bottomRight.y - results.props.crop.topLeft.y) expect(newAspectRatio).toBeCloseTo(initialAspectRatio, 5) // All coordinates should be within valid bounds expect(results.props.crop.topLeft.x).toBeGreaterThanOrEqual(0) expect(results.props.crop.topLeft.y).toBeGreaterThanOrEqual(0) expect(results.props.crop.bottomRight.x).toBeLessThanOrEqual(1) expect(results.props.crop.bottomRight.y).toBeLessThanOrEqual(1) } }) it('Resizes from the top right corner', () => { const testCrop = { topLeft: { x: 0.2, y: 0.3 }, bottomRight: { x: 0.8, y: 0.7 }, } const initialAspectRatio = (testCrop.bottomRight.x - testCrop.topLeft.x) / (testCrop.bottomRight.y - testCrop.topLeft.y) const results = getCropBox(shape, { handle: 'top_right', change: new Vec(-10, 15), crop: testCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }) if (results?.props.crop) { // Bottom left corner should be preserved expect(results.props.crop.topLeft.x).toBeCloseTo(testCrop.topLeft.x, 5) expect(results.props.crop.bottomRight.y).toBeCloseTo(testCrop.bottomRight.y, 5) // Aspect ratio should be maintained const newAspectRatio = (results.props.crop.bottomRight.x - results.props.crop.topLeft.x) / (results.props.crop.bottomRight.y - results.props.crop.topLeft.y) expect(newAspectRatio).toBeCloseTo(initialAspectRatio, 5) // All coordinates should be within valid bounds expect(results.props.crop.topLeft.x).toBeGreaterThanOrEqual(0) expect(results.props.crop.topLeft.y).toBeGreaterThanOrEqual(0) expect(results.props.crop.bottomRight.x).toBeLessThanOrEqual(1) expect(results.props.crop.bottomRight.y).toBeLessThanOrEqual(1) } }) it('Resizes from the bottom right corner', () => { const testCrop = { topLeft: { x: 0.2, y: 0.3 }, bottomRight: { x: 0.8, y: 0.7 }, } const initialAspectRatio = (testCrop.bottomRight.x - testCrop.topLeft.x) / (testCrop.bottomRight.y - testCrop.topLeft.y) const results = getCropBox(shape, { handle: 'bottom_right', change: new Vec(-10, -15), crop: testCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }) if (results?.props.crop) { // Top left corner should be preserved expect(results.props.crop.topLeft.x).toBeCloseTo(testCrop.topLeft.x, 5) expect(results.props.crop.topLeft.y).toBeCloseTo(testCrop.topLeft.y, 5) // Aspect ratio should be maintained const newAspectRatio = (results.props.crop.bottomRight.x - results.props.crop.topLeft.x) / (results.props.crop.bottomRight.y - results.props.crop.topLeft.y) expect(newAspectRatio).toBeCloseTo(initialAspectRatio, 5) // All coordinates should be within valid bounds expect(results.props.crop.topLeft.x).toBeGreaterThanOrEqual(0) expect(results.props.crop.topLeft.y).toBeGreaterThanOrEqual(0) expect(results.props.crop.bottomRight.x).toBeLessThanOrEqual(1) expect(results.props.crop.bottomRight.y).toBeLessThanOrEqual(1) } }) it('Resizes from the bottom left corner', () => { const testCrop = { topLeft: { x: 0.2, y: 0.3 }, bottomRight: { x: 0.8, y: 0.7 }, } const initialAspectRatio = (testCrop.bottomRight.x - testCrop.topLeft.x) / (testCrop.bottomRight.y - testCrop.topLeft.y) const results = getCropBox(shape, { handle: 'bottom_left', change: new Vec(10, -15), crop: testCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }) if (results?.props.crop) { // Top right corner should be preserved expect(results.props.crop.bottomRight.x).toBeCloseTo(testCrop.bottomRight.x, 5) expect(results.props.crop.topLeft.y).toBeCloseTo(testCrop.topLeft.y, 5) // Aspect ratio should be maintained const newAspectRatio = (results.props.crop.bottomRight.x - results.props.crop.topLeft.x) / (results.props.crop.bottomRight.y - results.props.crop.topLeft.y) expect(newAspectRatio).toBeCloseTo(initialAspectRatio, 5) // All coordinates should be within valid bounds expect(results.props.crop.topLeft.x).toBeGreaterThanOrEqual(0) expect(results.props.crop.topLeft.y).toBeGreaterThanOrEqual(0) expect(results.props.crop.bottomRight.x).toBeLessThanOrEqual(1) expect(results.props.crop.bottomRight.y).toBeLessThanOrEqual(1) } }) it('Resizes from the top edge', () => { const testCrop = { topLeft: { x: 0.2, y: 0.3 }, bottomRight: { x: 0.8, y: 0.7 }, } const initialAspectRatio = (testCrop.bottomRight.x - testCrop.topLeft.x) / (testCrop.bottomRight.y - testCrop.topLeft.y) const initialCenterX = (testCrop.topLeft.x + testCrop.bottomRight.x) / 2 const results = getCropBox(shape, { handle: 'top', change: new Vec(0, 15), crop: testCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }) if (results?.props.crop) { // Bottom edge and horizontal center should be preserved expect(results.props.crop.bottomRight.y).toBeCloseTo(testCrop.bottomRight.y, 5) const newCenterX = (results.props.crop.topLeft.x + results.props.crop.bottomRight.x) / 2 expect(newCenterX).toBeCloseTo(initialCenterX, 5) // Aspect ratio should be maintained const newAspectRatio = (results.props.crop.bottomRight.x - results.props.crop.topLeft.x) / (results.props.crop.bottomRight.y - results.props.crop.topLeft.y) expect(newAspectRatio).toBeCloseTo(initialAspectRatio, 5) // All coordinates should be within valid bounds expect(results.props.crop.topLeft.x).toBeGreaterThanOrEqual(0) expect(results.props.crop.topLeft.y).toBeGreaterThanOrEqual(0) expect(results.props.crop.bottomRight.x).toBeLessThanOrEqual(1) expect(results.props.crop.bottomRight.y).toBeLessThanOrEqual(1) } }) it('Resizes from the right edge', () => { const testCrop = { topLeft: { x: 0.2, y: 0.3 }, bottomRight: { x: 0.8, y: 0.7 }, } const initialAspectRatio = (testCrop.bottomRight.x - testCrop.topLeft.x) / (testCrop.bottomRight.y - testCrop.topLeft.y) const initialCenterY = (testCrop.topLeft.y + testCrop.bottomRight.y) / 2 const results = getCropBox(shape, { handle: 'right', change: new Vec(-15, 0), crop: testCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }) if (results?.props.crop) { // Left edge and vertical center should be preserved expect(results.props.crop.topLeft.x).toBeCloseTo(testCrop.topLeft.x, 5) const newCenterY = (results.props.crop.topLeft.y + results.props.crop.bottomRight.y) / 2 expect(newCenterY).toBeCloseTo(initialCenterY, 5) // Aspect ratio should be maintained const newAspectRatio = (results.props.crop.bottomRight.x - results.props.crop.topLeft.x) / (results.props.crop.bottomRight.y - results.props.crop.topLeft.y) expect(newAspectRatio).toBeCloseTo(initialAspectRatio, 5) // All coordinates should be within valid bounds expect(results.props.crop.topLeft.x).toBeGreaterThanOrEqual(0) expect(results.props.crop.topLeft.y).toBeGreaterThanOrEqual(0) expect(results.props.crop.bottomRight.x).toBeLessThanOrEqual(1) expect(results.props.crop.bottomRight.y).toBeLessThanOrEqual(1) } }) it('Resizes from the bottom edge', () => { const testCrop = { topLeft: { x: 0.2, y: 0.3 }, bottomRight: { x: 0.8, y: 0.7 }, } const initialAspectRatio = (testCrop.bottomRight.x - testCrop.topLeft.x) / (testCrop.bottomRight.y - testCrop.topLeft.y) const initialCenterX = (testCrop.topLeft.x + testCrop.bottomRight.x) / 2 const results = getCropBox(shape, { handle: 'bottom', change: new Vec(0, -15), crop: testCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }) if (results?.props.crop) { // Top edge and horizontal center should be preserved expect(results.props.crop.topLeft.y).toBeCloseTo(testCrop.topLeft.y, 5) const newCenterX = (results.props.crop.topLeft.x + results.props.crop.bottomRight.x) / 2 expect(newCenterX).toBeCloseTo(initialCenterX, 5) // Aspect ratio should be maintained const newAspectRatio = (results.props.crop.bottomRight.x - results.props.crop.topLeft.x) / (results.props.crop.bottomRight.y - results.props.crop.topLeft.y) expect(newAspectRatio).toBeCloseTo(initialAspectRatio, 5) // All coordinates should be within valid bounds expect(results.props.crop.topLeft.x).toBeGreaterThanOrEqual(0) expect(results.props.crop.topLeft.y).toBeGreaterThanOrEqual(0) expect(results.props.crop.bottomRight.x).toBeLessThanOrEqual(1) expect(results.props.crop.bottomRight.y).toBeLessThanOrEqual(1) } }) it('Resizes from the left edge', () => { const testCrop = { topLeft: { x: 0.2, y: 0.3 }, bottomRight: { x: 0.8, y: 0.7 }, } const initialAspectRatio = (testCrop.bottomRight.x - testCrop.topLeft.x) / (testCrop.bottomRight.y - testCrop.topLeft.y) const initialCenterY = (testCrop.topLeft.y + testCrop.bottomRight.y) / 2 const results = getCropBox(shape, { handle: 'left', change: new Vec(15, 0), crop: testCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }) if (results?.props.crop) { // Right edge and vertical center should be preserved expect(results.props.crop.bottomRight.x).toBeCloseTo(testCrop.bottomRight.x, 5) const newCenterY = (results.props.crop.topLeft.y + results.props.crop.bottomRight.y) / 2 expect(newCenterY).toBeCloseTo(initialCenterY, 5) // Aspect ratio should be maintained const newAspectRatio = (results.props.crop.bottomRight.x - results.props.crop.topLeft.x) / (results.props.crop.bottomRight.y - results.props.crop.topLeft.y) expect(newAspectRatio).toBeCloseTo(initialAspectRatio, 5) // All coordinates should be within valid bounds expect(results.props.crop.topLeft.x).toBeGreaterThanOrEqual(0) expect(results.props.crop.topLeft.y).toBeGreaterThanOrEqual(0) expect(results.props.crop.bottomRight.x).toBeLessThanOrEqual(1) expect(results.props.crop.bottomRight.y).toBeLessThanOrEqual(1) } }) it('When overlapping edges, does not produce a result with < 0 or > 1 for x or y crop dimensions and maintains the aspect ratio', () => { const testCrop = { topLeft: { x: 0.25, y: 0.25 }, bottomRight: { x: 0.75, y: 0.75 }, } const initialAspectRatio = (testCrop.bottomRight.x - testCrop.topLeft.x) / (testCrop.bottomRight.y - testCrop.topLeft.y) // Test extreme resize that would normally cause invalid bounds const results = getCropBox(shape, { handle: 'top_left', change: new Vec(200, 200), // Extreme change crop: testCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }) if (results?.props.crop) { // All coordinates should be within valid bounds expect(results.props.crop.topLeft.x).toBeGreaterThanOrEqual(0) expect(results.props.crop.topLeft.y).toBeGreaterThanOrEqual(0) expect(results.props.crop.bottomRight.x).toBeLessThanOrEqual(1) expect(results.props.crop.bottomRight.y).toBeLessThanOrEqual(1) // Crop should have positive dimensions const cropWidth = results.props.crop.bottomRight.x - results.props.crop.topLeft.x const cropHeight = results.props.crop.bottomRight.y - results.props.crop.topLeft.y expect(cropWidth).toBeGreaterThan(0) expect(cropHeight).toBeGreaterThan(0) // Aspect ratio should be maintained const newAspectRatio = cropWidth / cropHeight expect(newAspectRatio).toBeCloseTo(initialAspectRatio, 5) } }) // 1. Boundary collision tests describe('Boundary collision scenarios', () => { it('Handles crop starting at left boundary (x=0)', () => { const boundaryTestCrop = { topLeft: { x: 0, y: 0.3 }, bottomRight: { x: 0.4, y: 0.7 }, } const initialAspectRatio = (boundaryTestCrop.bottomRight.x - boundaryTestCrop.topLeft.x) / (boundaryTestCrop.bottomRight.y - boundaryTestCrop.topLeft.y) const results = getCropBox(shape, { handle: 'left', change: new Vec(-50, 0), // Try to move left edge beyond boundary crop: boundaryTestCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }) if (results?.props.crop) { // Left edge should stay at boundary expect(results.props.crop.topLeft.x).toBe(0) // Aspect ratio should be maintained const newAspectRatio = (results.props.crop.bottomRight.x - results.props.crop.topLeft.x) / (results.props.crop.bottomRight.y - results.props.crop.topLeft.y) expect(newAspectRatio).toBeCloseTo(initialAspectRatio, 5) // Should not exceed boundaries expect(results.props.crop.bottomRight.x).toBeLessThanOrEqual(1) expect(results.props.crop.bottomRight.y).toBeLessThanOrEqual(1) } }) it('Handles crop starting at right boundary (x=1)', () => { const boundaryTestCrop = { topLeft: { x: 0.6, y: 0.3 }, bottomRight: { x: 1, y: 0.7 }, } const initialAspectRatio = (boundaryTestCrop.bottomRight.x - boundaryTestCrop.topLeft.x) / (boundaryTestCrop.bottomRight.y - boundaryTestCrop.topLeft.y) const results = getCropBox(shape, { handle: 'right', change: new Vec(50, 0), // Try to move right edge beyond boundary crop: boundaryTestCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }) if (results?.props.crop) { // Right edge should stay at boundary expect(results.props.crop.bottomRight.x).toBe(1) // Aspect ratio should be maintained const newAspectRatio = (results.props.crop.bottomRight.x - results.props.crop.topLeft.x) / (results.props.crop.bottomRight.y - results.props.crop.topLeft.y) expect(newAspectRatio).toBeCloseTo(initialAspectRatio, 5) // Should not go below boundaries expect(results.props.crop.topLeft.x).toBeGreaterThanOrEqual(0) expect(results.props.crop.topLeft.y).toBeGreaterThanOrEqual(0) } }) it('Handles crop starting at top boundary (y=0)', () => { const boundaryTestCrop = { topLeft: { x: 0.3, y: 0 }, bottomRight: { x: 0.7, y: 0.4 }, } const initialAspectRatio = (boundaryTestCrop.bottomRight.x - boundaryTestCrop.topLeft.x) / (boundaryTestCrop.bottomRight.y - boundaryTestCrop.topLeft.y) const results = getCropBox(shape, { handle: 'top', change: new Vec(0, -50), // Try to move top edge beyond boundary crop: boundaryTestCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }) if (results?.props.crop) { // Top edge should stay at boundary expect(results.props.crop.topLeft.y).toBe(0) // Aspect ratio should be maintained const newAspectRatio = (results.props.crop.bottomRight.x - results.props.crop.topLeft.x) / (results.props.crop.bottomRight.y - results.props.crop.topLeft.y) expect(newAspectRatio).toBeCloseTo(initialAspectRatio, 5) } }) it('Handles aspect ratio constraint conflicting with boundary constraints', () => { // Start with a very wide crop near the top-left corner const conflictTestCrop = { topLeft: { x: 0.05, y: 0.05 }, bottomRight: { x: 0.95, y: 0.25 }, // Very wide (4.5:1 ratio) } const initialAspectRatio = (conflictTestCrop.bottomRight.x - conflictTestCrop.topLeft.x) / (conflictTestCrop.bottomRight.y - conflictTestCrop.topLeft.y) const results = getCropBox(shape, { handle: 'top_left', change: new Vec(-20, -20), // Try to move beyond both x=0 and y=0 crop: conflictTestCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }) if (results?.props.crop) { // Should respect boundaries expect(results.props.crop.topLeft.x).toBeGreaterThanOrEqual(0) expect(results.props.crop.topLeft.y).toBeGreaterThanOrEqual(0) expect(results.props.crop.bottomRight.x).toBeLessThanOrEqual(1) expect(results.props.crop.bottomRight.y).toBeLessThanOrEqual(1) // Should maintain positive dimensions const cropWidth = results.props.crop.bottomRight.x - results.props.crop.topLeft.x const cropHeight = results.props.crop.bottomRight.y - results.props.crop.topLeft.y expect(cropWidth).toBeGreaterThan(0) expect(cropHeight).toBeGreaterThan(0) // Should attempt to maintain aspect ratio where possible const newAspectRatio = cropWidth / cropHeight expect(newAspectRatio).toBeCloseTo(initialAspectRatio, 2) // Less precision due to boundary conflicts } }) }) // 2. Minimum size constraint tests with aspect ratio locked describe('Minimum size constraints with aspect ratio locked', () => { it('Enforces minimum size constraints even when they conflict with aspect ratio', () => { const smallCrop = { topLeft: { x: 0.4, y: 0.4 }, bottomRight: { x: 0.6, y: 0.6 }, // 20x20 crop } const results = getCropBox( shape, { handle: 'top_left', change: new Vec(15, 15), // Try to make it smaller crop: smallCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }, { minWidth: 25, // Larger than what resize would produce minHeight: 25, } ) // Should clamp to minimum size constraints if (results?.props.crop) { const cropWidth = results.props.crop.bottomRight.x - results.props.crop.topLeft.x const cropHeight = results.props.crop.bottomRight.y - results.props.crop.topLeft.y // Minimum size should be respected expect(cropWidth * initialSize.w).toBeGreaterThanOrEqual(25) expect(cropHeight * initialSize.h).toBeGreaterThanOrEqual(25) } }) it('Respects minimum width constraint while maintaining aspect ratio', () => { const testCrop = { topLeft: { x: 0.2, y: 0.2 }, bottomRight: { x: 0.6, y: 0.8 }, // 40x60 crop (2:3 ratio) } const initialAspectRatio = (testCrop.bottomRight.x - testCrop.topLeft.x) / (testCrop.bottomRight.y - testCrop.topLeft.y) const results = getCropBox( shape, { handle: 'right', change: new Vec(-25, 0), // Try to make it narrower crop: testCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }, { minWidth: 20, // Enforce minimum width minHeight: 8, } ) if (results?.props.crop) { // Width should respect minimum const cropWidth = results.props.crop.bottomRight.x - results.props.crop.topLeft.x expect(cropWidth * initialSize.w).toBeGreaterThanOrEqual(20) // Aspect ratio should be maintained const cropHeight = results.props.crop.bottomRight.y - results.props.crop.topLeft.y const newAspectRatio = cropWidth / cropHeight expect(newAspectRatio).toBeCloseTo(initialAspectRatio, 5) } }) it('Respects minimum height constraint while maintaining aspect ratio', () => { const testCrop = { topLeft: { x: 0.2, y: 0.2 }, bottomRight: { x: 0.8, y: 0.6 }, // 60x40 crop (3:2 ratio) } const initialAspectRatio = (testCrop.bottomRight.x - testCrop.topLeft.x) / (testCrop.bottomRight.y - testCrop.topLeft.y) const results = getCropBox( shape, { handle: 'bottom', change: new Vec(0, -25), // Try to make it shorter crop: testCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }, { minWidth: 8, minHeight: 20, // Enforce minimum height } ) if (results?.props.crop) { // Height should respect minimum const cropHeight = results.props.crop.bottomRight.y - results.props.crop.topLeft.y expect(cropHeight * initialSize.h).toBeGreaterThanOrEqual(20) // Aspect ratio should be maintained const cropWidth = results.props.crop.bottomRight.x - results.props.crop.topLeft.x const newAspectRatio = cropWidth / cropHeight expect(newAspectRatio).toBeCloseTo(initialAspectRatio, 5) } }) }) // 3. Different aspect ratio tests describe('Different starting aspect ratios', () => { it('Maintains very wide aspect ratio (4:1)', () => { const wideCrop = { topLeft: { x: 0.1, y: 0.4 }, bottomRight: { x: 0.9, y: 0.6 }, // 4:1 ratio } const initialAspectRatio = (wideCrop.bottomRight.x - wideCrop.topLeft.x) / (wideCrop.bottomRight.y - wideCrop.topLeft.y) expect(initialAspectRatio).toBeCloseTo(4, 1) // Verify it's actually 4:1 const results = getCropBox(shape, { handle: 'bottom_right', change: new Vec(-20, -5), crop: wideCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }) if (results?.props.crop) { const cropWidth = results.props.crop.bottomRight.x - results.props.crop.topLeft.x const cropHeight = results.props.crop.bottomRight.y - results.props.crop.topLeft.y const newAspectRatio = cropWidth / cropHeight expect(newAspectRatio).toBeCloseTo(initialAspectRatio, 5) } }) it('Maintains very tall aspect ratio (1:4)', () => { const tallCrop = { topLeft: { x: 0.4, y: 0.1 }, bottomRight: { x: 0.6, y: 0.9 }, // 1:4 ratio } const initialAspectRatio = (tallCrop.bottomRight.x - tallCrop.topLeft.x) / (tallCrop.bottomRight.y - tallCrop.topLeft.y) expect(initialAspectRatio).toBeCloseTo(0.25, 1) // Verify it's actually 1:4 const results = getCropBox(shape, { handle: 'top_left', change: new Vec(5, 20), crop: tallCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }) if (results?.props.crop) { const cropWidth = results.props.crop.bottomRight.x - results.props.crop.topLeft.x const cropHeight = results.props.crop.bottomRight.y - results.props.crop.topLeft.y const newAspectRatio = cropWidth / cropHeight expect(newAspectRatio).toBeCloseTo(initialAspectRatio, 5) } }) it('Maintains perfect square aspect ratio (1:1)', () => { const squareCrop = { topLeft: { x: 0.25, y: 0.25 }, bottomRight: { x: 0.75, y: 0.75 }, // 1:1 ratio } const initialAspectRatio = (squareCrop.bottomRight.x - squareCrop.topLeft.x) / (squareCrop.bottomRight.y - squareCrop.topLeft.y) expect(initialAspectRatio).toBe(1) // Verify it's actually 1:1 const results = getCropBox(shape, { handle: 'right', change: new Vec(-15, 0), crop: squareCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }) if (results?.props.crop) { const cropWidth = results.props.crop.bottomRight.x - results.props.crop.topLeft.x const cropHeight = results.props.crop.bottomRight.y - results.props.crop.topLeft.y const newAspectRatio = cropWidth / cropHeight expect(newAspectRatio).toBeCloseTo(1, 5) } }) it('Maintains extreme wide aspect ratio (16:1)', () => { const extremeWideCrop = { topLeft: { x: 0.05, y: 0.475 }, bottomRight: { x: 0.85, y: 0.525 }, // ~16:1 ratio } const initialAspectRatio = (extremeWideCrop.bottomRight.x - extremeWideCrop.topLeft.x) / (extremeWideCrop.bottomRight.y - extremeWideCrop.topLeft.y) expect(initialAspectRatio).toBeCloseTo(16, 1) // Verify it's extremely wide const results = getCropBox(shape, { handle: 'left', change: new Vec(20, 0), crop: extremeWideCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }) if (results?.props.crop) { const cropWidth = results.props.crop.bottomRight.x - results.props.crop.topLeft.x const cropHeight = results.props.crop.bottomRight.y - results.props.crop.topLeft.y const newAspectRatio = cropWidth / cropHeight expect(newAspectRatio).toBeCloseTo(initialAspectRatio, 3) // Less precision for extreme ratios } }) it('Maintains extreme tall aspect ratio (1:16)', () => { const extremeTallCrop = { topLeft: { x: 0.475, y: 0.05 }, bottomRight: { x: 0.525, y: 0.85 }, // ~1:16 ratio } const initialAspectRatio = (extremeTallCrop.bottomRight.x - extremeTallCrop.topLeft.x) / (extremeTallCrop.bottomRight.y - extremeTallCrop.topLeft.y) expect(initialAspectRatio).toBeCloseTo(0.0625, 1) // Verify it's extremely tall const results = getCropBox(shape, { handle: 'top', change: new Vec(0, 20), crop: extremeTallCrop, uncroppedSize: initialSize, initialShape: shape, aspectRatioLocked: true, }) if (results?.props.crop) { const cropWidth = results.props.crop.bottomRight.x - results.props.crop.topLeft.x const cropHeight = results.props.crop.bottomRight.y - results.props.crop.topLeft.y const newAspectRatio = cropWidth / cropHeight expect(newAspectRatio).toBeCloseTo(initialAspectRatio, 3) // Less precision for extreme ratios } }) }) }) describe('getCroppedImageDataForReplacedImage', () => { it('preserves aspect ratio when replacing with a wider image', () => { // Original: 100x100 square image with a 80x60 crop (4:3 aspect ratio) const originalShape: TLImageShape = { ...shape, props: { ...shape.props, w: 80, h: 60, crop: { topLeft: { x: 0.1, y: 0.2 }, bottomRight: { x: 0.9, y: 0.8 }, }, }, } // Replace with a 200x100 image (2:1 aspect ratio - wider than original crop) const result = getCroppedImageDataForReplacedImage(originalShape, 200, 100) // Should maintain 4:3 aspect ratio of the display expect(result.w / result.h).toBeCloseTo(4 / 3, 2) // With the new implementation, the crop behavior is different const cropWidth = result.crop.bottomRight.x - result.crop.topLeft.x const cropHeight = result.crop.bottomRight.y - result.crop.topLeft.y // Should maintain reasonable crop dimensions within bounds expect(cropWidth).toBeGreaterThan(0) expect(cropWidth).toBeLessThanOrEqual(1) expect(cropHeight).toBeGreaterThan(0) expect(cropHeight).toBeLessThanOrEqual(1) }) it('preserves aspect ratio when replacing with a taller image', () => { // Original: 100x100 square image with a 80x60 crop (4:3 aspect ratio) const originalShape: TLImageShape = { ...shape, props: { ...shape.props, w: 80, h: 60, crop: { topLeft: { x: 0.1, y: 0.2 }, bottomRight: { x: 0.9, y: 0.8 }, }, }, } // Replace with a 100x200 image (1:2 aspect ratio - taller than original crop) const result = getCroppedImageDataForReplacedImage(originalShape, 100, 200) // Should maintain 4:3 aspect ratio of the display expect(result.w / result.h).toBeCloseTo(4 / 3, 2) // Should maintain reasonable crop dimensions within bounds const cropWidth = result.crop.bottomRight.x - result.crop.topLeft.x const cropHeight = result.crop.bottomRight.y - result.crop.topLeft.y expect(cropWidth).toBeGreaterThan(0) expect(c