@tldraw/editor
Version:
tldraw infinite canvas SDK (editor).
864 lines (726 loc) • 25.1 kB
text/typescript
import { Geometry2d, RecordProps, Rectangle2d, ShapeUtil, T, TLShape, createShapeId } from '../..'
import { createTLStore } from '../config/createTLStore'
import { Editor } from '../editor/Editor'
import { Box } from '../primitives/Box'
import { getExportDefaultBounds } from './getSvgJsx'
const TEST_SHAPE_TYPE = 'test-shape'
declare module '@tldraw/tlschema' {
export interface TLGlobalShapePropsMap {
[TEST_SHAPE_TYPE]: { w: number; h: number; x: number; y: number; isContainer?: boolean }
}
}
type ITestShape = TLShape<typeof TEST_SHAPE_TYPE>
class TestShape extends ShapeUtil<ITestShape> {
static override type = TEST_SHAPE_TYPE
static override props: RecordProps<ITestShape> = {
w: T.number,
h: T.number,
x: T.number,
y: T.number,
isContainer: T.boolean.optional(),
}
getDefaultProps(): ITestShape['props'] {
return {
w: 100,
h: 100,
x: 0,
y: 0,
isContainer: false,
}
}
getGeometry(shape: ITestShape): Geometry2d {
return new Rectangle2d({
width: shape.props.w,
height: shape.props.h,
x: shape.props.x,
y: shape.props.y,
isFilled: false,
})
}
override isExportBoundsContainer(shape: ITestShape): boolean {
return shape.props.isContainer ?? false
}
getIndicatorPath() {
return undefined
}
component() {}
}
let editor: Editor
beforeEach(() => {
editor = new Editor({
shapeUtils: [TestShape],
bindingUtils: [],
tools: [],
store: createTLStore({ shapeUtils: [TestShape], bindingUtils: [] }),
getContainer: () => document.body,
})
})
describe('getExportDefaultBounds', () => {
it('returns null box when no rendering shapes provided', () => {
const result = getExportDefaultBounds(editor, [], 32, null)
expect(result.box).toBeNull()
})
it('returns bounds for single shape with padding', () => {
const shapeId = createShapeId('test1')
editor.createShape({
id: shapeId,
type: 'test-shape',
x: 10,
y: 20,
props: { w: 100, h: 80, x: 0, y: 0 },
})
const renderingShapes = editor.getUnorderedRenderingShapes(false)
const testShape = renderingShapes.find((s) => s.id === shapeId)!
const result = getExportDefaultBounds(editor, [testShape], 32, null)
expect(result.box).toBeInstanceOf(Box)
expect(result.paddingApplied).toBe(true)
// Bounds should include 32px padding on all sides
expect(result.box?.x).toBe(10 - 32) // -22
expect(result.box?.y).toBe(20 - 32) // -12
expect(result.box?.w).toBe(100 + 64) // 164 (32px on each side)
expect(result.box?.h).toBe(80 + 64) // 144 (32px on each side)
})
it('returns union bounds for multiple shapes with padding', () => {
const shape1Id = createShapeId('test1')
const shape2Id = createShapeId('test2')
editor.createShape({
id: shape1Id,
type: 'test-shape',
x: 0,
y: 0,
props: { w: 50, h: 50, x: 0, y: 0 },
})
editor.createShape({
id: shape2Id,
type: 'test-shape',
x: 30,
y: 30,
props: { w: 60, h: 60, x: 0, y: 0 },
})
const renderingShapes = editor.getUnorderedRenderingShapes(false)
const testShapes = renderingShapes.filter((s) => [shape1Id, shape2Id].includes(s.id))
const result = getExportDefaultBounds(editor, testShapes, 32, null)
expect(result.box).toBeInstanceOf(Box)
// Raw bounds would be (0,0) to (90,90), with 32px padding on all sides
expect(result.box?.x).toBe(0 - 32) // -32
expect(result.box?.y).toBe(0 - 32) // -32
expect(result.box?.w).toBe(90 + 64) // 154
expect(result.box?.h).toBe(90 + 64) // 154
})
it('handles shapes with transforms correctly', () => {
const shapeId = createShapeId('test1')
editor.createShape({
id: shapeId,
type: 'test-shape',
x: 25,
y: 35,
props: { w: 50, h: 40, x: 0, y: 0 },
})
// Rotate the shape
editor.rotateShapesBy([shapeId], Math.PI / 4)
const renderingShapes = editor.getUnorderedRenderingShapes(false)
const testShape = renderingShapes.find((s) => s.id === shapeId)!
const result = getExportDefaultBounds(editor, [testShape], 32, null)
expect(result.box).toBeInstanceOf(Box)
// The rotated shape should have expanded bounds, plus padding
expect(result.box!.w).toBeGreaterThan(50 + 64)
expect(result.box!.h).toBeGreaterThan(40 + 64)
})
it('handles multiple overlapping shapes correctly', () => {
const shape1Id = createShapeId('test1')
const shape2Id = createShapeId('test2')
const shape3Id = createShapeId('test3')
// Create overlapping shapes
editor.createShape({
id: shape1Id,
type: 'test-shape',
x: 0,
y: 0,
props: { w: 40, h: 40, x: 0, y: 0 },
})
editor.createShape({
id: shape2Id,
type: 'test-shape',
x: 20,
y: 20,
props: { w: 40, h: 40, x: 0, y: 0 },
})
editor.createShape({
id: shape3Id,
type: 'test-shape',
x: 10,
y: 10,
props: { w: 20, h: 20, x: 0, y: 0 },
})
const renderingShapes = editor.getUnorderedRenderingShapes(false)
const testShapes = renderingShapes.filter((s) => [shape1Id, shape2Id, shape3Id].includes(s.id))
const result = getExportDefaultBounds(editor, testShapes, 32, null)
expect(result.box).toBeInstanceOf(Box)
// Raw bounds would be (0,0) to (60,60), with 32px padding on all sides
expect(result.box?.x).toBe(0 - 32) // -32
expect(result.box?.y).toBe(0 - 32) // -32
expect(result.box?.w).toBe(60 + 64) // 124 (32px on each side)
expect(result.box?.h).toBe(60 + 64) // 124 (32px on each side)
})
it('handles complex geometry with multiple shapes', () => {
const shape1Id = createShapeId('shape1')
const shape2Id = createShapeId('shape2')
const shape3Id = createShapeId('shape3')
// Create shapes with different positions and sizes
editor.createShape({
id: shape1Id,
type: 'test-shape',
x: 0,
y: 0,
props: { w: 50, h: 50, x: 0, y: 0 },
})
editor.createShape({
id: shape2Id,
type: 'test-shape',
x: 100,
y: 100,
props: { w: 60, h: 40, x: 0, y: 0 },
})
editor.createShape({
id: shape3Id,
type: 'test-shape',
x: 200,
y: 50,
props: { w: 40, h: 80, x: 0, y: 0 },
})
const renderingShapes = editor.getUnorderedRenderingShapes(false)
const testShapes = renderingShapes.filter((s) => [shape1Id, shape2Id, shape3Id].includes(s.id))
const result = getExportDefaultBounds(editor, testShapes, 32, null)
expect(result.box).toBeInstanceOf(Box)
// The bounds should encompass:
// - shape1: (0, 0) to (50, 50)
// - shape2: (100, 100) to (160, 140)
// - shape3: (200, 50) to (240, 130)
// Raw total bounds: (0, 0) to (240, 140), with 32px padding on all sides
expect(result.box!.x).toBe(0 - 32) // -32 (leftmost edge with padding)
expect(result.box!.y).toBe(0 - 32) // -32 (topmost edge with padding)
expect(result.box!.w).toBe(240 + 64) // 304 (width + 32px on each side)
expect(result.box!.h).toBe(140 + 64) // 204 (height + 32px on each side)
})
it('handles empty rendering shapes array after filtering', () => {
// Create a shape but don't include it in rendering shapes
const shapeId = createShapeId('test1')
editor.createShape({
id: shapeId,
type: 'test-shape',
x: 10,
y: 20,
props: { w: 100, h: 80, x: 0, y: 0 },
})
// Pass empty array to simulate filtered out shapes
const result = getExportDefaultBounds(editor, [], 32, null)
expect(result.box).toBeNull()
})
it('does not apply padding when exporting single frame shape', () => {
const shapeId = createShapeId('test1')
editor.createShape({
id: shapeId,
type: 'test-shape',
x: 10,
y: 20,
props: { w: 100, h: 80, x: 0, y: 0 },
})
const renderingShapes = editor.getUnorderedRenderingShapes(false)
const testShape = renderingShapes.find((s) => s.id === shapeId)!
// Pass the shape ID as singleFrameShapeId to simulate single frame export
const result = getExportDefaultBounds(editor, [testShape], 32, shapeId)
expect(result.box).toBeInstanceOf(Box)
expect(result.paddingApplied).toBe(false)
// No padding should be applied
expect(result.box?.x).toBe(10)
expect(result.box?.y).toBe(20)
expect(result.box?.w).toBe(100)
expect(result.box?.h).toBe(80)
})
describe('isExportBoundsContainer behavior', () => {
it('applies normal padding when no container shapes exist', () => {
const shape1Id = createShapeId('shape1')
const shape2Id = createShapeId('shape2')
editor.createShape({
id: shape1Id,
type: 'test-shape',
x: 0,
y: 0,
props: { w: 50, h: 50, x: 0, y: 0, isContainer: false },
})
editor.createShape({
id: shape2Id,
type: 'test-shape',
x: 10,
y: 10,
props: { w: 30, h: 30, x: 0, y: 0, isContainer: false },
})
const renderingShapes = editor.getUnorderedRenderingShapes(false)
const testShapes = renderingShapes.filter((s) => [shape1Id, shape2Id].includes(s.id))
const result = getExportDefaultBounds(editor, testShapes, 32, null)
expect(result.box).toBeInstanceOf(Box)
// Raw bounds: (0,0) to (50,50), with padding
expect(result.box?.x).toBe(-32)
expect(result.box?.y).toBe(-32)
expect(result.box?.w).toBe(50 + 64) // 114
expect(result.box?.h).toBe(50 + 64) // 114
})
it('skips padding when container shape contains all other shapes', () => {
const containerId = createShapeId('container')
const shape1Id = createShapeId('shape1')
const shape2Id = createShapeId('shape2')
// Container shape that encompasses everything
editor.createShape({
id: containerId,
type: 'test-shape',
x: 0,
y: 0,
props: { w: 100, h: 100, x: 0, y: 0, isContainer: true },
})
// Smaller shapes inside the container
editor.createShape({
id: shape1Id,
type: 'test-shape',
x: 10,
y: 10,
props: { w: 20, h: 20, x: 0, y: 0, isContainer: false },
})
editor.createShape({
id: shape2Id,
type: 'test-shape',
x: 60,
y: 60,
props: { w: 30, h: 30, x: 0, y: 0, isContainer: false },
})
const renderingShapes = editor.getUnorderedRenderingShapes(false)
const testShapes = renderingShapes.filter((s) =>
[containerId, shape1Id, shape2Id].includes(s.id)
)
const result = getExportDefaultBounds(editor, testShapes, 32, null)
expect(result.box).toBeInstanceOf(Box)
// Should use container bounds without padding: (0,0) to (100,100)
expect(result.box?.x).toBe(0)
expect(result.box?.y).toBe(0)
expect(result.box?.w).toBe(100)
expect(result.box?.h).toBe(100)
})
it('applies padding when container does not contain all shapes', () => {
const containerId = createShapeId('container')
const insideShapeId = createShapeId('inside')
const outsideShapeId = createShapeId('outside')
// Small container
editor.createShape({
id: containerId,
type: 'test-shape',
x: 0,
y: 0,
props: { w: 50, h: 50, x: 0, y: 0, isContainer: true },
})
// Shape inside container
editor.createShape({
id: insideShapeId,
type: 'test-shape',
x: 10,
y: 10,
props: { w: 20, h: 20, x: 0, y: 0, isContainer: false },
})
// Shape outside container bounds
editor.createShape({
id: outsideShapeId,
type: 'test-shape',
x: 70,
y: 70,
props: { w: 30, h: 30, x: 0, y: 0, isContainer: false },
})
const renderingShapes = editor.getUnorderedRenderingShapes(false)
const testShapes = renderingShapes.filter((s) =>
[containerId, insideShapeId, outsideShapeId].includes(s.id)
)
const result = getExportDefaultBounds(editor, testShapes, 32, null)
expect(result.box).toBeInstanceOf(Box)
// Total bounds: (0,0) to (100,100), with padding applied
expect(result.box?.x).toBe(-32)
expect(result.box?.y).toBe(-32)
expect(result.box?.w).toBe(100 + 64) // 164
expect(result.box?.h).toBe(100 + 64) // 164
})
it('works with multiple containers where one contains all', () => {
const container1Id = createShapeId('container1')
const container2Id = createShapeId('container2')
const shapeId = createShapeId('shape1')
// Small container
editor.createShape({
id: container1Id,
type: 'test-shape',
x: 10,
y: 10,
props: { w: 40, h: 40, x: 0, y: 0, isContainer: true },
})
// Large container that contains everything
editor.createShape({
id: container2Id,
type: 'test-shape',
x: 0,
y: 0,
props: { w: 100, h: 100, x: 0, y: 0, isContainer: true },
})
// Shape inside both containers
editor.createShape({
id: shapeId,
type: 'test-shape',
x: 20,
y: 20,
props: { w: 10, h: 10, x: 0, y: 0, isContainer: false },
})
const renderingShapes = editor.getUnorderedRenderingShapes(false)
const testShapes = renderingShapes.filter((s) =>
[container1Id, container2Id, shapeId].includes(s.id)
)
const result = getExportDefaultBounds(editor, testShapes, 32, null)
expect(result.box).toBeInstanceOf(Box)
// Should use the large container's bounds without padding
expect(result.box?.x).toBe(0)
expect(result.box?.y).toBe(0)
expect(result.box?.w).toBe(100)
expect(result.box?.h).toBe(100)
})
it('container behavior is overridden by single frame shape', () => {
const containerId = createShapeId('container')
const shapeId = createShapeId('shape1')
// Container that would normally prevent padding
editor.createShape({
id: containerId,
type: 'test-shape',
x: 0,
y: 0,
props: { w: 100, h: 100, x: 0, y: 0, isContainer: true },
})
// Shape inside container
editor.createShape({
id: shapeId,
type: 'test-shape',
x: 10,
y: 10,
props: { w: 20, h: 20, x: 0, y: 0, isContainer: false },
})
const renderingShapes = editor.getUnorderedRenderingShapes(false)
const testShapes = renderingShapes.filter((s) => [containerId, shapeId].includes(s.id))
// Single frame shape logic takes precedence over container logic
const result = getExportDefaultBounds(editor, testShapes, 32, containerId)
expect(result.box).toBeInstanceOf(Box)
// Should use total bounds without padding (single frame overrides container)
expect(result.box?.x).toBe(0)
expect(result.box?.y).toBe(0)
expect(result.box?.w).toBe(100)
expect(result.box?.h).toBe(100)
})
it('handles containers with inner shapes correctly', () => {
const containerId = createShapeId('container')
const innerShapeId = createShapeId('inner')
// Container shape large enough to contain inner shape
editor.createShape({
id: containerId,
type: 'test-shape',
x: 0,
y: 0,
props: { w: 200, h: 120, x: 0, y: 0, isContainer: true },
})
// Shape inside container bounds
editor.createShape({
id: innerShapeId,
type: 'test-shape',
x: 50,
y: 20,
props: { w: 100, h: 60, x: 0, y: 0, isContainer: false },
})
const renderingShapes = editor.getUnorderedRenderingShapes(false)
const testShapes = renderingShapes.filter((s) => [containerId, innerShapeId].includes(s.id))
const result = getExportDefaultBounds(editor, testShapes, 32, null)
expect(result.box).toBeInstanceOf(Box)
// Container (0,0,200,120) should contain inner shape bounds,
// so no padding should be applied
expect(result.box?.x).toBe(0)
expect(result.box?.y).toBe(0)
expect(result.box?.w).toBe(200)
expect(result.box?.h).toBe(120)
})
it('handles order sensitivity - container processed first', () => {
const containerId = createShapeId('container')
const shapeId = createShapeId('shape')
// Create container first (will be processed first due to creation order)
editor.createShape({
id: containerId,
type: 'test-shape',
x: 0,
y: 0,
props: { w: 100, h: 100, x: 0, y: 0, isContainer: true },
})
// Create regular shape second
editor.createShape({
id: shapeId,
type: 'test-shape',
x: 20,
y: 20,
props: { w: 30, h: 30, x: 0, y: 0, isContainer: false },
})
const renderingShapes = editor.getUnorderedRenderingShapes(false)
const testShapes = renderingShapes.filter((s) => [containerId, shapeId].includes(s.id))
const result = getExportDefaultBounds(editor, testShapes, 32, null)
expect(result.box).toBeInstanceOf(Box)
// Container should contain regular shape, no padding applied
expect(result.box?.x).toBe(0)
expect(result.box?.y).toBe(0)
expect(result.box?.w).toBe(100)
expect(result.box?.h).toBe(100)
})
it('handles order sensitivity - regular shape processed first', () => {
const shapeId = createShapeId('shape')
const containerId = createShapeId('container')
// Create regular shape first (will be processed first due to creation order)
editor.createShape({
id: shapeId,
type: 'test-shape',
x: 20,
y: 20,
props: { w: 30, h: 30, x: 0, y: 0, isContainer: false },
})
// Create container second
editor.createShape({
id: containerId,
type: 'test-shape',
x: 0,
y: 0,
props: { w: 100, h: 100, x: 0, y: 0, isContainer: true },
})
const renderingShapes = editor.getUnorderedRenderingShapes(false)
const testShapes = renderingShapes.filter((s) => [shapeId, containerId].includes(s.id))
const result = getExportDefaultBounds(editor, testShapes, 32, null)
expect(result.box).toBeInstanceOf(Box)
// Container should still contain regular shape, no padding applied
expect(result.box?.x).toBe(0)
expect(result.box?.y).toBe(0)
expect(result.box?.w).toBe(100)
expect(result.box?.h).toBe(100)
})
it('multiple containers - only one that contains all others skips padding', () => {
const smallContainerId = createShapeId('smallContainer')
const largeContainerId = createShapeId('largeContainer')
const shapeId = createShapeId('shape')
// Small container
editor.createShape({
id: smallContainerId,
type: 'test-shape',
x: 10,
y: 10,
props: { w: 30, h: 30, x: 0, y: 0, isContainer: true },
})
// Large container that contains the small container AND the regular shape
editor.createShape({
id: largeContainerId,
type: 'test-shape',
x: 0,
y: 0,
props: { w: 100, h: 100, x: 0, y: 0, isContainer: true },
})
// Regular shape inside both containers
editor.createShape({
id: shapeId,
type: 'test-shape',
x: 15,
y: 15,
props: { w: 10, h: 10, x: 0, y: 0, isContainer: false },
})
const renderingShapes = editor.getUnorderedRenderingShapes(false)
const testShapes = renderingShapes.filter((s) =>
[smallContainerId, largeContainerId, shapeId].includes(s.id)
)
const result = getExportDefaultBounds(editor, testShapes, 32, null)
expect(result.box).toBeInstanceOf(Box)
// Large container contains everything (including small container), no padding
expect(result.box?.x).toBe(0)
expect(result.box?.y).toBe(0)
expect(result.box?.w).toBe(100)
expect(result.box?.h).toBe(100)
})
it('multiple containers - none contains all others, padding applied', () => {
const container1Id = createShapeId('container1')
const container2Id = createShapeId('container2')
const shape1Id = createShapeId('shape1')
const shape2Id = createShapeId('shape2')
// Container 1 contains shape1 but not container2 or shape2
editor.createShape({
id: container1Id,
type: 'test-shape',
x: 0,
y: 0,
props: { w: 40, h: 40, x: 0, y: 0, isContainer: true },
})
// Container 2 contains shape2 but not container1 or shape1
editor.createShape({
id: container2Id,
type: 'test-shape',
x: 60,
y: 60,
props: { w: 40, h: 40, x: 0, y: 0, isContainer: true },
})
// Shape inside container1
editor.createShape({
id: shape1Id,
type: 'test-shape',
x: 10,
y: 10,
props: { w: 20, h: 20, x: 0, y: 0, isContainer: false },
})
// Shape inside container2
editor.createShape({
id: shape2Id,
type: 'test-shape',
x: 70,
y: 70,
props: { w: 20, h: 20, x: 0, y: 0, isContainer: false },
})
const renderingShapes = editor.getUnorderedRenderingShapes(false)
const testShapes = renderingShapes.filter((s) =>
[container1Id, container2Id, shape1Id, shape2Id].includes(s.id)
)
const result = getExportDefaultBounds(editor, testShapes, 32, null)
expect(result.box).toBeInstanceOf(Box)
// No single container contains all others, padding should be applied
// Total bounds: (0,0) to (100,100), with padding
expect(result.box?.x).toBe(-32)
expect(result.box?.y).toBe(-32)
expect(result.box?.w).toBe(100 + 64) // 164
expect(result.box?.h).toBe(100 + 64) // 164
})
it('container covers most but not all shapes - padding applied', () => {
const containerId = createShapeId('container')
const insideShapeId = createShapeId('inside')
const partiallyOutsideId = createShapeId('partiallyOutside')
// Container
editor.createShape({
id: containerId,
type: 'test-shape',
x: 0,
y: 0,
props: { w: 80, h: 80, x: 0, y: 0, isContainer: true },
})
// Shape fully inside container
editor.createShape({
id: insideShapeId,
type: 'test-shape',
x: 20,
y: 20,
props: { w: 20, h: 20, x: 0, y: 0, isContainer: false },
})
// Shape that partially extends outside container
editor.createShape({
id: partiallyOutsideId,
type: 'test-shape',
x: 70,
y: 70,
props: { w: 20, h: 20, x: 0, y: 0, isContainer: false },
})
const renderingShapes = editor.getUnorderedRenderingShapes(false)
const testShapes = renderingShapes.filter((s) =>
[containerId, insideShapeId, partiallyOutsideId].includes(s.id)
)
const result = getExportDefaultBounds(editor, testShapes, 32, null)
expect(result.box).toBeInstanceOf(Box)
// Container doesn't contain all shapes, padding applied
// Total bounds: (0,0) to (90,90), with padding
expect(result.box?.x).toBe(-32)
expect(result.box?.y).toBe(-32)
expect(result.box?.w).toBe(90 + 64) // 154
expect(result.box?.h).toBe(90 + 64) // 154
})
it('nested containers - inner container processed first', () => {
const outerContainerId = createShapeId('outerContainer')
const innerContainerId = createShapeId('innerContainer')
const shapeId = createShapeId('shape')
// Inner container (created first)
editor.createShape({
id: innerContainerId,
type: 'test-shape',
x: 20,
y: 20,
props: { w: 40, h: 40, x: 0, y: 0, isContainer: true },
})
// Outer container that contains inner container
editor.createShape({
id: outerContainerId,
type: 'test-shape',
x: 0,
y: 0,
props: { w: 100, h: 100, x: 0, y: 0, isContainer: true },
})
// Shape inside inner container
editor.createShape({
id: shapeId,
type: 'test-shape',
x: 30,
y: 30,
props: { w: 20, h: 20, x: 0, y: 0, isContainer: false },
})
const renderingShapes = editor.getUnorderedRenderingShapes(false)
const testShapes = renderingShapes.filter((s) =>
[innerContainerId, outerContainerId, shapeId].includes(s.id)
)
const result = getExportDefaultBounds(editor, testShapes, 32, null)
expect(result.box).toBeInstanceOf(Box)
// Outer container contains everything, should use outer bounds without padding
expect(result.box?.x).toBe(0)
expect(result.box?.y).toBe(0)
expect(result.box?.w).toBe(100)
expect(result.box?.h).toBe(100)
})
it('container-only shapes should not skip padding', () => {
const container1Id = createShapeId('container1')
const container2Id = createShapeId('container2')
// Two containers, neither containing the other completely
editor.createShape({
id: container1Id,
type: 'test-shape',
x: 0,
y: 0,
props: { w: 50, h: 50, x: 0, y: 0, isContainer: true },
})
editor.createShape({
id: container2Id,
type: 'test-shape',
x: 30,
y: 30,
props: { w: 50, h: 50, x: 0, y: 0, isContainer: true },
})
const renderingShapes = editor.getUnorderedRenderingShapes(false)
const testShapes = renderingShapes.filter((s) => [container1Id, container2Id].includes(s.id))
const result = getExportDefaultBounds(editor, testShapes, 32, null)
expect(result.box).toBeInstanceOf(Box)
// Neither container fully contains the other, padding should be applied
// Total bounds: (0,0) to (80,80), with padding
expect(result.box?.x).toBe(-32)
expect(result.box?.y).toBe(-32)
expect(result.box?.w).toBe(80 + 64) // 144
expect(result.box?.h).toBe(80 + 64) // 144
})
it('single container with only itself skips padding', () => {
const containerId = createShapeId('container')
// Single container shape
editor.createShape({
id: containerId,
type: 'test-shape',
x: 10,
y: 20,
props: { w: 100, h: 80, x: 0, y: 0, isContainer: true },
})
const renderingShapes = editor.getUnorderedRenderingShapes(false)
const testShapes = renderingShapes.filter((s) => s.id === containerId)
const result = getExportDefaultBounds(editor, testShapes, 32, null)
expect(result.box).toBeInstanceOf(Box)
// Single container should skip padding (it trivially contains "all other shapes")
expect(result.box?.x).toBe(10)
expect(result.box?.y).toBe(20)
expect(result.box?.w).toBe(100)
expect(result.box?.h).toBe(80)
})
})
})