@tldraw/editor 3.16.0-next.df90ce0ff566 → 3.16.0-next.e57e478c23e0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +134 -110
- package/dist-cjs/index.js +3 -5
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +8 -6
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/MenuClickCapture.js +0 -5
- package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
- package/dist-cjs/lib/components/Shape.js +7 -10
- package/dist-cjs/lib/components/Shape.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +4 -23
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +1 -1
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultScribble.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultScribble.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +9 -1
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
- package/dist-cjs/lib/config/TLUserPreferences.js +9 -3
- package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +77 -133
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +9 -4
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +13 -0
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
- package/dist-cjs/lib/exports/getSvgJsx.js +35 -16
- package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +31 -25
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js +4 -1
- package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +4 -1
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
- package/dist-cjs/lib/{utils/nearestMultiple.js → hooks/useStateAttribute.js} +15 -14
- package/dist-cjs/lib/hooks/useStateAttribute.js.map +7 -0
- package/dist-cjs/lib/license/LicenseManager.js +17 -22
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/license/LicenseProvider.js +5 -0
- package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
- package/dist-cjs/lib/license/Watermark.js +8 -8
- package/dist-cjs/lib/license/Watermark.js.map +1 -1
- package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
- package/dist-cjs/lib/options.js +7 -0
- package/dist-cjs/lib/options.js.map +2 -2
- package/dist-cjs/lib/primitives/Box.js +3 -0
- package/dist-cjs/lib/primitives/Box.js.map +2 -2
- package/dist-cjs/lib/primitives/Vec.js +0 -4
- package/dist-cjs/lib/primitives/Vec.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js +26 -18
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Group2d.js +3 -0
- package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
- package/dist-cjs/lib/utils/EditorAtom.js +45 -0
- package/dist-cjs/lib/utils/EditorAtom.js.map +7 -0
- package/dist-cjs/lib/utils/reparenting.js +2 -35
- package/dist-cjs/lib/utils/reparenting.js.map +3 -3
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +134 -110
- package/dist-esm/index.mjs +3 -5
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +8 -6
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/MenuClickCapture.mjs +0 -5
- package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
- package/dist-esm/lib/components/Shape.mjs +7 -10
- package/dist-esm/lib/components/Shape.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +4 -23
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +1 -1
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultScribble.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultScribble.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +9 -1
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
- package/dist-esm/lib/config/TLUserPreferences.mjs +9 -3
- package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +77 -133
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +9 -4
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +13 -0
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/exports/getSvgJsx.mjs +36 -16
- package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +32 -26
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs +4 -1
- package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +4 -1
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useStateAttribute.mjs +15 -0
- package/dist-esm/lib/hooks/useStateAttribute.mjs.map +7 -0
- package/dist-esm/lib/license/LicenseManager.mjs +17 -22
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseProvider.mjs +5 -0
- package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
- package/dist-esm/lib/license/Watermark.mjs +8 -8
- package/dist-esm/lib/license/Watermark.mjs.map +1 -1
- package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
- package/dist-esm/lib/options.mjs +7 -0
- package/dist-esm/lib/options.mjs.map +2 -2
- package/dist-esm/lib/primitives/Box.mjs +4 -1
- package/dist-esm/lib/primitives/Box.mjs.map +2 -2
- package/dist-esm/lib/primitives/Vec.mjs +0 -4
- package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +29 -19
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Group2d.mjs +3 -0
- package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
- package/dist-esm/lib/utils/EditorAtom.mjs +25 -0
- package/dist-esm/lib/utils/EditorAtom.mjs.map +7 -0
- package/dist-esm/lib/utils/reparenting.mjs +3 -40
- package/dist-esm/lib/utils/reparenting.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +301 -288
- package/package.json +14 -37
- package/src/index.ts +3 -9
- package/src/lib/TldrawEditor.tsx +13 -17
- package/src/lib/components/MenuClickCapture.tsx +0 -8
- package/src/lib/components/Shape.tsx +6 -12
- package/src/lib/components/default-components/DefaultCanvas.tsx +5 -22
- package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
- package/src/lib/components/default-components/DefaultScribble.tsx +1 -1
- package/src/lib/components/default-components/DefaultShapeIndicator.tsx +5 -1
- package/src/lib/config/TLUserPreferences.ts +8 -1
- package/src/lib/editor/Editor.test.ts +12 -11
- package/src/lib/editor/Editor.ts +108 -193
- package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +15 -14
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +16 -15
- package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +49 -48
- package/src/lib/editor/managers/FontManager/FontManager.test.ts +24 -23
- package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +7 -6
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +12 -11
- package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +57 -50
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +51 -26
- package/src/lib/editor/managers/TickManager/TickManager.test.ts +14 -13
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +34 -26
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +6 -1
- package/src/lib/editor/shapes/ShapeUtil.ts +35 -0
- package/src/lib/editor/types/misc-types.ts +54 -7
- package/src/lib/exports/getSvgJsx.test.ts +868 -0
- package/src/lib/exports/getSvgJsx.tsx +78 -21
- package/src/lib/hooks/useCanvasEvents.ts +45 -38
- package/src/lib/hooks/usePassThroughMouseOverEvents.ts +4 -1
- package/src/lib/hooks/usePassThroughWheelEvents.ts +6 -1
- package/src/lib/hooks/useStateAttribute.ts +15 -0
- package/src/lib/license/LicenseManager.test.ts +61 -52
- package/src/lib/license/LicenseManager.ts +32 -24
- package/src/lib/license/LicenseProvider.tsx +8 -0
- package/src/lib/license/Watermark.test.tsx +2 -1
- package/src/lib/license/Watermark.tsx +8 -8
- package/src/lib/license/useLicenseManagerState.ts +2 -2
- package/src/lib/options.ts +8 -0
- package/src/lib/primitives/Box.test.ts +126 -0
- package/src/lib/primitives/Box.ts +10 -1
- package/src/lib/primitives/Vec.ts +0 -5
- package/src/lib/primitives/geometry/Geometry2d.ts +49 -19
- package/src/lib/primitives/geometry/Group2d.ts +4 -0
- package/src/lib/utils/EditorAtom.ts +37 -0
- package/src/lib/utils/reparenting.ts +3 -69
- package/src/lib/utils/sync/LocalIndexedDb.test.ts +2 -1
- package/src/lib/utils/sync/TLLocalSyncClient.test.ts +15 -15
- package/src/version.ts +3 -3
- package/dist-cjs/lib/utils/nearestMultiple.js.map +0 -7
- package/dist-esm/lib/utils/nearestMultiple.mjs +0 -14
- package/dist-esm/lib/utils/nearestMultiple.mjs.map +0 -7
- package/src/lib/utils/nearestMultiple.ts +0 -13
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Geometry2d,
|
|
3
|
+
RecordProps,
|
|
4
|
+
Rectangle2d,
|
|
5
|
+
ShapeUtil,
|
|
6
|
+
T,
|
|
7
|
+
TLBaseShape,
|
|
8
|
+
createShapeId,
|
|
9
|
+
} from '../..'
|
|
10
|
+
import { createTLStore } from '../config/createTLStore'
|
|
11
|
+
import { Editor } from '../editor/Editor'
|
|
12
|
+
import { Box } from '../primitives/Box'
|
|
13
|
+
import { getExportDefaultBounds } from './getSvgJsx'
|
|
14
|
+
|
|
15
|
+
type ITestShape = TLBaseShape<
|
|
16
|
+
'test-shape',
|
|
17
|
+
{
|
|
18
|
+
w: number
|
|
19
|
+
h: number
|
|
20
|
+
x: number
|
|
21
|
+
y: number
|
|
22
|
+
isContainer?: boolean
|
|
23
|
+
}
|
|
24
|
+
>
|
|
25
|
+
|
|
26
|
+
class TestShape extends ShapeUtil<ITestShape> {
|
|
27
|
+
static override type = 'test-shape' as const
|
|
28
|
+
static override props: RecordProps<ITestShape> = {
|
|
29
|
+
w: T.number,
|
|
30
|
+
h: T.number,
|
|
31
|
+
x: T.number,
|
|
32
|
+
y: T.number,
|
|
33
|
+
isContainer: T.boolean.optional(),
|
|
34
|
+
}
|
|
35
|
+
getDefaultProps(): ITestShape['props'] {
|
|
36
|
+
return {
|
|
37
|
+
w: 100,
|
|
38
|
+
h: 100,
|
|
39
|
+
x: 0,
|
|
40
|
+
y: 0,
|
|
41
|
+
isContainer: false,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
getGeometry(shape: ITestShape): Geometry2d {
|
|
45
|
+
return new Rectangle2d({
|
|
46
|
+
width: shape.props.w,
|
|
47
|
+
height: shape.props.h,
|
|
48
|
+
x: shape.props.x,
|
|
49
|
+
y: shape.props.y,
|
|
50
|
+
isFilled: false,
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override isExportBoundsContainer(shape: ITestShape): boolean {
|
|
55
|
+
return shape.props.isContainer ?? false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
indicator() {}
|
|
59
|
+
component() {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let editor: Editor
|
|
63
|
+
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
editor = new Editor({
|
|
66
|
+
shapeUtils: [TestShape],
|
|
67
|
+
bindingUtils: [],
|
|
68
|
+
tools: [],
|
|
69
|
+
store: createTLStore({ shapeUtils: [TestShape], bindingUtils: [] }),
|
|
70
|
+
getContainer: () => document.body,
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('getExportDefaultBounds', () => {
|
|
75
|
+
it('returns null when no rendering shapes provided', () => {
|
|
76
|
+
const result = getExportDefaultBounds(editor, [], 32, null)
|
|
77
|
+
expect(result).toBeNull()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('returns bounds for single shape with padding', () => {
|
|
81
|
+
const shapeId = createShapeId('test1')
|
|
82
|
+
editor.createShape({
|
|
83
|
+
id: shapeId,
|
|
84
|
+
type: 'test-shape',
|
|
85
|
+
x: 10,
|
|
86
|
+
y: 20,
|
|
87
|
+
props: { w: 100, h: 80, x: 0, y: 0 },
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const renderingShapes = editor.getUnorderedRenderingShapes(false)
|
|
91
|
+
const testShape = renderingShapes.find((s) => s.id === shapeId)!
|
|
92
|
+
|
|
93
|
+
const result = getExportDefaultBounds(editor, [testShape], 32, null)
|
|
94
|
+
|
|
95
|
+
expect(result).toBeInstanceOf(Box)
|
|
96
|
+
// Bounds should include 32px padding on all sides
|
|
97
|
+
expect(result?.x).toBe(10 - 32) // -22
|
|
98
|
+
expect(result?.y).toBe(20 - 32) // -12
|
|
99
|
+
expect(result?.w).toBe(100 + 64) // 164 (32px on each side)
|
|
100
|
+
expect(result?.h).toBe(80 + 64) // 144 (32px on each side)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('returns union bounds for multiple shapes with padding', () => {
|
|
104
|
+
const shape1Id = createShapeId('test1')
|
|
105
|
+
const shape2Id = createShapeId('test2')
|
|
106
|
+
|
|
107
|
+
editor.createShape({
|
|
108
|
+
id: shape1Id,
|
|
109
|
+
type: 'test-shape',
|
|
110
|
+
x: 0,
|
|
111
|
+
y: 0,
|
|
112
|
+
props: { w: 50, h: 50, x: 0, y: 0 },
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
editor.createShape({
|
|
116
|
+
id: shape2Id,
|
|
117
|
+
type: 'test-shape',
|
|
118
|
+
x: 30,
|
|
119
|
+
y: 30,
|
|
120
|
+
props: { w: 60, h: 60, x: 0, y: 0 },
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const renderingShapes = editor.getUnorderedRenderingShapes(false)
|
|
124
|
+
const testShapes = renderingShapes.filter((s) => [shape1Id, shape2Id].includes(s.id))
|
|
125
|
+
|
|
126
|
+
const result = getExportDefaultBounds(editor, testShapes, 32, null)
|
|
127
|
+
|
|
128
|
+
expect(result).toBeInstanceOf(Box)
|
|
129
|
+
// Raw bounds would be (0,0) to (90,90), with 32px padding on all sides
|
|
130
|
+
expect(result?.x).toBe(0 - 32) // -32
|
|
131
|
+
expect(result?.y).toBe(0 - 32) // -32
|
|
132
|
+
expect(result?.w).toBe(90 + 64) // 154
|
|
133
|
+
expect(result?.h).toBe(90 + 64) // 154
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('handles shapes with transforms correctly', () => {
|
|
137
|
+
const shapeId = createShapeId('test1')
|
|
138
|
+
editor.createShape({
|
|
139
|
+
id: shapeId,
|
|
140
|
+
type: 'test-shape',
|
|
141
|
+
x: 25,
|
|
142
|
+
y: 35,
|
|
143
|
+
props: { w: 50, h: 40, x: 0, y: 0 },
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// Rotate the shape
|
|
147
|
+
editor.rotateShapesBy([shapeId], Math.PI / 4)
|
|
148
|
+
|
|
149
|
+
const renderingShapes = editor.getUnorderedRenderingShapes(false)
|
|
150
|
+
const testShape = renderingShapes.find((s) => s.id === shapeId)!
|
|
151
|
+
|
|
152
|
+
const result = getExportDefaultBounds(editor, [testShape], 32, null)
|
|
153
|
+
|
|
154
|
+
expect(result).toBeInstanceOf(Box)
|
|
155
|
+
// The rotated shape should have expanded bounds, plus padding
|
|
156
|
+
expect(result!.w).toBeGreaterThan(50 + 64)
|
|
157
|
+
expect(result!.h).toBeGreaterThan(40 + 64)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('handles multiple overlapping shapes correctly', () => {
|
|
161
|
+
const shape1Id = createShapeId('test1')
|
|
162
|
+
const shape2Id = createShapeId('test2')
|
|
163
|
+
const shape3Id = createShapeId('test3')
|
|
164
|
+
|
|
165
|
+
// Create overlapping shapes
|
|
166
|
+
editor.createShape({
|
|
167
|
+
id: shape1Id,
|
|
168
|
+
type: 'test-shape',
|
|
169
|
+
x: 0,
|
|
170
|
+
y: 0,
|
|
171
|
+
props: { w: 40, h: 40, x: 0, y: 0 },
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
editor.createShape({
|
|
175
|
+
id: shape2Id,
|
|
176
|
+
type: 'test-shape',
|
|
177
|
+
x: 20,
|
|
178
|
+
y: 20,
|
|
179
|
+
props: { w: 40, h: 40, x: 0, y: 0 },
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
editor.createShape({
|
|
183
|
+
id: shape3Id,
|
|
184
|
+
type: 'test-shape',
|
|
185
|
+
x: 10,
|
|
186
|
+
y: 10,
|
|
187
|
+
props: { w: 20, h: 20, x: 0, y: 0 },
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
const renderingShapes = editor.getUnorderedRenderingShapes(false)
|
|
191
|
+
const testShapes = renderingShapes.filter((s) => [shape1Id, shape2Id, shape3Id].includes(s.id))
|
|
192
|
+
|
|
193
|
+
const result = getExportDefaultBounds(editor, testShapes, 32, null)
|
|
194
|
+
|
|
195
|
+
expect(result).toBeInstanceOf(Box)
|
|
196
|
+
// Raw bounds would be (0,0) to (60,60), with 32px padding on all sides
|
|
197
|
+
expect(result?.x).toBe(0 - 32) // -32
|
|
198
|
+
expect(result?.y).toBe(0 - 32) // -32
|
|
199
|
+
expect(result?.w).toBe(60 + 64) // 124 (32px on each side)
|
|
200
|
+
expect(result?.h).toBe(60 + 64) // 124 (32px on each side)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('handles complex geometry with multiple shapes', () => {
|
|
204
|
+
const shape1Id = createShapeId('shape1')
|
|
205
|
+
const shape2Id = createShapeId('shape2')
|
|
206
|
+
const shape3Id = createShapeId('shape3')
|
|
207
|
+
|
|
208
|
+
// Create shapes with different positions and sizes
|
|
209
|
+
editor.createShape({
|
|
210
|
+
id: shape1Id,
|
|
211
|
+
type: 'test-shape',
|
|
212
|
+
x: 0,
|
|
213
|
+
y: 0,
|
|
214
|
+
props: { w: 50, h: 50, x: 0, y: 0 },
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
editor.createShape({
|
|
218
|
+
id: shape2Id,
|
|
219
|
+
type: 'test-shape',
|
|
220
|
+
x: 100,
|
|
221
|
+
y: 100,
|
|
222
|
+
props: { w: 60, h: 40, x: 0, y: 0 },
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
editor.createShape({
|
|
226
|
+
id: shape3Id,
|
|
227
|
+
type: 'test-shape',
|
|
228
|
+
x: 200,
|
|
229
|
+
y: 50,
|
|
230
|
+
props: { w: 40, h: 80, x: 0, y: 0 },
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
const renderingShapes = editor.getUnorderedRenderingShapes(false)
|
|
234
|
+
const testShapes = renderingShapes.filter((s) => [shape1Id, shape2Id, shape3Id].includes(s.id))
|
|
235
|
+
|
|
236
|
+
const result = getExportDefaultBounds(editor, testShapes, 32, null)
|
|
237
|
+
|
|
238
|
+
expect(result).toBeInstanceOf(Box)
|
|
239
|
+
|
|
240
|
+
// The bounds should encompass:
|
|
241
|
+
// - shape1: (0, 0) to (50, 50)
|
|
242
|
+
// - shape2: (100, 100) to (160, 140)
|
|
243
|
+
// - shape3: (200, 50) to (240, 130)
|
|
244
|
+
// Raw total bounds: (0, 0) to (240, 140), with 32px padding on all sides
|
|
245
|
+
expect(result!.x).toBe(0 - 32) // -32 (leftmost edge with padding)
|
|
246
|
+
expect(result!.y).toBe(0 - 32) // -32 (topmost edge with padding)
|
|
247
|
+
expect(result!.w).toBe(240 + 64) // 304 (width + 32px on each side)
|
|
248
|
+
expect(result!.h).toBe(140 + 64) // 204 (height + 32px on each side)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('handles empty rendering shapes array after filtering', () => {
|
|
252
|
+
// Create a shape but don't include it in rendering shapes
|
|
253
|
+
const shapeId = createShapeId('test1')
|
|
254
|
+
editor.createShape({
|
|
255
|
+
id: shapeId,
|
|
256
|
+
type: 'test-shape',
|
|
257
|
+
x: 10,
|
|
258
|
+
y: 20,
|
|
259
|
+
props: { w: 100, h: 80, x: 0, y: 0 },
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
// Pass empty array to simulate filtered out shapes
|
|
263
|
+
const result = getExportDefaultBounds(editor, [], 32, null)
|
|
264
|
+
|
|
265
|
+
expect(result).toBeNull()
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('does not apply padding when exporting single frame shape', () => {
|
|
269
|
+
const shapeId = createShapeId('test1')
|
|
270
|
+
editor.createShape({
|
|
271
|
+
id: shapeId,
|
|
272
|
+
type: 'test-shape',
|
|
273
|
+
x: 10,
|
|
274
|
+
y: 20,
|
|
275
|
+
props: { w: 100, h: 80, x: 0, y: 0 },
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
const renderingShapes = editor.getUnorderedRenderingShapes(false)
|
|
279
|
+
const testShape = renderingShapes.find((s) => s.id === shapeId)!
|
|
280
|
+
|
|
281
|
+
// Pass the shape ID as singleFrameShapeId to simulate single frame export
|
|
282
|
+
const result = getExportDefaultBounds(editor, [testShape], 32, shapeId)
|
|
283
|
+
|
|
284
|
+
expect(result).toBeInstanceOf(Box)
|
|
285
|
+
// No padding should be applied
|
|
286
|
+
expect(result?.x).toBe(10)
|
|
287
|
+
expect(result?.y).toBe(20)
|
|
288
|
+
expect(result?.w).toBe(100)
|
|
289
|
+
expect(result?.h).toBe(80)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
describe('isExportBoundsContainer behavior', () => {
|
|
293
|
+
it('applies normal padding when no container shapes exist', () => {
|
|
294
|
+
const shape1Id = createShapeId('shape1')
|
|
295
|
+
const shape2Id = createShapeId('shape2')
|
|
296
|
+
|
|
297
|
+
editor.createShape({
|
|
298
|
+
id: shape1Id,
|
|
299
|
+
type: 'test-shape',
|
|
300
|
+
x: 0,
|
|
301
|
+
y: 0,
|
|
302
|
+
props: { w: 50, h: 50, x: 0, y: 0, isContainer: false },
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
editor.createShape({
|
|
306
|
+
id: shape2Id,
|
|
307
|
+
type: 'test-shape',
|
|
308
|
+
x: 10,
|
|
309
|
+
y: 10,
|
|
310
|
+
props: { w: 30, h: 30, x: 0, y: 0, isContainer: false },
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
const renderingShapes = editor.getUnorderedRenderingShapes(false)
|
|
314
|
+
const testShapes = renderingShapes.filter((s) => [shape1Id, shape2Id].includes(s.id))
|
|
315
|
+
|
|
316
|
+
const result = getExportDefaultBounds(editor, testShapes, 32, null)
|
|
317
|
+
|
|
318
|
+
expect(result).toBeInstanceOf(Box)
|
|
319
|
+
// Raw bounds: (0,0) to (50,50), with padding
|
|
320
|
+
expect(result?.x).toBe(-32)
|
|
321
|
+
expect(result?.y).toBe(-32)
|
|
322
|
+
expect(result?.w).toBe(50 + 64) // 114
|
|
323
|
+
expect(result?.h).toBe(50 + 64) // 114
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('skips padding when container shape contains all other shapes', () => {
|
|
327
|
+
const containerId = createShapeId('container')
|
|
328
|
+
const shape1Id = createShapeId('shape1')
|
|
329
|
+
const shape2Id = createShapeId('shape2')
|
|
330
|
+
|
|
331
|
+
// Container shape that encompasses everything
|
|
332
|
+
editor.createShape({
|
|
333
|
+
id: containerId,
|
|
334
|
+
type: 'test-shape',
|
|
335
|
+
x: 0,
|
|
336
|
+
y: 0,
|
|
337
|
+
props: { w: 100, h: 100, x: 0, y: 0, isContainer: true },
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
// Smaller shapes inside the container
|
|
341
|
+
editor.createShape({
|
|
342
|
+
id: shape1Id,
|
|
343
|
+
type: 'test-shape',
|
|
344
|
+
x: 10,
|
|
345
|
+
y: 10,
|
|
346
|
+
props: { w: 20, h: 20, x: 0, y: 0, isContainer: false },
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
editor.createShape({
|
|
350
|
+
id: shape2Id,
|
|
351
|
+
type: 'test-shape',
|
|
352
|
+
x: 60,
|
|
353
|
+
y: 60,
|
|
354
|
+
props: { w: 30, h: 30, x: 0, y: 0, isContainer: false },
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
const renderingShapes = editor.getUnorderedRenderingShapes(false)
|
|
358
|
+
const testShapes = renderingShapes.filter((s) =>
|
|
359
|
+
[containerId, shape1Id, shape2Id].includes(s.id)
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
const result = getExportDefaultBounds(editor, testShapes, 32, null)
|
|
363
|
+
|
|
364
|
+
expect(result).toBeInstanceOf(Box)
|
|
365
|
+
// Should use container bounds without padding: (0,0) to (100,100)
|
|
366
|
+
expect(result?.x).toBe(0)
|
|
367
|
+
expect(result?.y).toBe(0)
|
|
368
|
+
expect(result?.w).toBe(100)
|
|
369
|
+
expect(result?.h).toBe(100)
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it('applies padding when container does not contain all shapes', () => {
|
|
373
|
+
const containerId = createShapeId('container')
|
|
374
|
+
const insideShapeId = createShapeId('inside')
|
|
375
|
+
const outsideShapeId = createShapeId('outside')
|
|
376
|
+
|
|
377
|
+
// Small container
|
|
378
|
+
editor.createShape({
|
|
379
|
+
id: containerId,
|
|
380
|
+
type: 'test-shape',
|
|
381
|
+
x: 0,
|
|
382
|
+
y: 0,
|
|
383
|
+
props: { w: 50, h: 50, x: 0, y: 0, isContainer: true },
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
// Shape inside container
|
|
387
|
+
editor.createShape({
|
|
388
|
+
id: insideShapeId,
|
|
389
|
+
type: 'test-shape',
|
|
390
|
+
x: 10,
|
|
391
|
+
y: 10,
|
|
392
|
+
props: { w: 20, h: 20, x: 0, y: 0, isContainer: false },
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
// Shape outside container bounds
|
|
396
|
+
editor.createShape({
|
|
397
|
+
id: outsideShapeId,
|
|
398
|
+
type: 'test-shape',
|
|
399
|
+
x: 70,
|
|
400
|
+
y: 70,
|
|
401
|
+
props: { w: 30, h: 30, x: 0, y: 0, isContainer: false },
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
const renderingShapes = editor.getUnorderedRenderingShapes(false)
|
|
405
|
+
const testShapes = renderingShapes.filter((s) =>
|
|
406
|
+
[containerId, insideShapeId, outsideShapeId].includes(s.id)
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
const result = getExportDefaultBounds(editor, testShapes, 32, null)
|
|
410
|
+
|
|
411
|
+
expect(result).toBeInstanceOf(Box)
|
|
412
|
+
// Total bounds: (0,0) to (100,100), with padding applied
|
|
413
|
+
expect(result?.x).toBe(-32)
|
|
414
|
+
expect(result?.y).toBe(-32)
|
|
415
|
+
expect(result?.w).toBe(100 + 64) // 164
|
|
416
|
+
expect(result?.h).toBe(100 + 64) // 164
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
it('works with multiple containers where one contains all', () => {
|
|
420
|
+
const container1Id = createShapeId('container1')
|
|
421
|
+
const container2Id = createShapeId('container2')
|
|
422
|
+
const shapeId = createShapeId('shape1')
|
|
423
|
+
|
|
424
|
+
// Small container
|
|
425
|
+
editor.createShape({
|
|
426
|
+
id: container1Id,
|
|
427
|
+
type: 'test-shape',
|
|
428
|
+
x: 10,
|
|
429
|
+
y: 10,
|
|
430
|
+
props: { w: 40, h: 40, x: 0, y: 0, isContainer: true },
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
// Large container that contains everything
|
|
434
|
+
editor.createShape({
|
|
435
|
+
id: container2Id,
|
|
436
|
+
type: 'test-shape',
|
|
437
|
+
x: 0,
|
|
438
|
+
y: 0,
|
|
439
|
+
props: { w: 100, h: 100, x: 0, y: 0, isContainer: true },
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
// Shape inside both containers
|
|
443
|
+
editor.createShape({
|
|
444
|
+
id: shapeId,
|
|
445
|
+
type: 'test-shape',
|
|
446
|
+
x: 20,
|
|
447
|
+
y: 20,
|
|
448
|
+
props: { w: 10, h: 10, x: 0, y: 0, isContainer: false },
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
const renderingShapes = editor.getUnorderedRenderingShapes(false)
|
|
452
|
+
const testShapes = renderingShapes.filter((s) =>
|
|
453
|
+
[container1Id, container2Id, shapeId].includes(s.id)
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
const result = getExportDefaultBounds(editor, testShapes, 32, null)
|
|
457
|
+
|
|
458
|
+
expect(result).toBeInstanceOf(Box)
|
|
459
|
+
// Should use the large container's bounds without padding
|
|
460
|
+
expect(result?.x).toBe(0)
|
|
461
|
+
expect(result?.y).toBe(0)
|
|
462
|
+
expect(result?.w).toBe(100)
|
|
463
|
+
expect(result?.h).toBe(100)
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it('container behavior is overridden by single frame shape', () => {
|
|
467
|
+
const containerId = createShapeId('container')
|
|
468
|
+
const shapeId = createShapeId('shape1')
|
|
469
|
+
|
|
470
|
+
// Container that would normally prevent padding
|
|
471
|
+
editor.createShape({
|
|
472
|
+
id: containerId,
|
|
473
|
+
type: 'test-shape',
|
|
474
|
+
x: 0,
|
|
475
|
+
y: 0,
|
|
476
|
+
props: { w: 100, h: 100, x: 0, y: 0, isContainer: true },
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
// Shape inside container
|
|
480
|
+
editor.createShape({
|
|
481
|
+
id: shapeId,
|
|
482
|
+
type: 'test-shape',
|
|
483
|
+
x: 10,
|
|
484
|
+
y: 10,
|
|
485
|
+
props: { w: 20, h: 20, x: 0, y: 0, isContainer: false },
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
const renderingShapes = editor.getUnorderedRenderingShapes(false)
|
|
489
|
+
const testShapes = renderingShapes.filter((s) => [containerId, shapeId].includes(s.id))
|
|
490
|
+
|
|
491
|
+
// Single frame shape logic takes precedence over container logic
|
|
492
|
+
const result = getExportDefaultBounds(editor, testShapes, 32, containerId)
|
|
493
|
+
|
|
494
|
+
expect(result).toBeInstanceOf(Box)
|
|
495
|
+
// Should use total bounds without padding (single frame overrides container)
|
|
496
|
+
expect(result?.x).toBe(0)
|
|
497
|
+
expect(result?.y).toBe(0)
|
|
498
|
+
expect(result?.w).toBe(100)
|
|
499
|
+
expect(result?.h).toBe(100)
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
it('handles containers with inner shapes correctly', () => {
|
|
503
|
+
const containerId = createShapeId('container')
|
|
504
|
+
const innerShapeId = createShapeId('inner')
|
|
505
|
+
|
|
506
|
+
// Container shape large enough to contain inner shape
|
|
507
|
+
editor.createShape({
|
|
508
|
+
id: containerId,
|
|
509
|
+
type: 'test-shape',
|
|
510
|
+
x: 0,
|
|
511
|
+
y: 0,
|
|
512
|
+
props: { w: 200, h: 120, x: 0, y: 0, isContainer: true },
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
// Shape inside container bounds
|
|
516
|
+
editor.createShape({
|
|
517
|
+
id: innerShapeId,
|
|
518
|
+
type: 'test-shape',
|
|
519
|
+
x: 50,
|
|
520
|
+
y: 20,
|
|
521
|
+
props: { w: 100, h: 60, x: 0, y: 0, isContainer: false },
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
const renderingShapes = editor.getUnorderedRenderingShapes(false)
|
|
525
|
+
const testShapes = renderingShapes.filter((s) => [containerId, innerShapeId].includes(s.id))
|
|
526
|
+
|
|
527
|
+
const result = getExportDefaultBounds(editor, testShapes, 32, null)
|
|
528
|
+
|
|
529
|
+
expect(result).toBeInstanceOf(Box)
|
|
530
|
+
// Container (0,0,200,120) should contain inner shape bounds,
|
|
531
|
+
// so no padding should be applied
|
|
532
|
+
expect(result?.x).toBe(0)
|
|
533
|
+
expect(result?.y).toBe(0)
|
|
534
|
+
expect(result?.w).toBe(200)
|
|
535
|
+
expect(result?.h).toBe(120)
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
it('handles order sensitivity - container processed first', () => {
|
|
539
|
+
const containerId = createShapeId('container')
|
|
540
|
+
const shapeId = createShapeId('shape')
|
|
541
|
+
|
|
542
|
+
// Create container first (will be processed first due to creation order)
|
|
543
|
+
editor.createShape({
|
|
544
|
+
id: containerId,
|
|
545
|
+
type: 'test-shape',
|
|
546
|
+
x: 0,
|
|
547
|
+
y: 0,
|
|
548
|
+
props: { w: 100, h: 100, x: 0, y: 0, isContainer: true },
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
// Create regular shape second
|
|
552
|
+
editor.createShape({
|
|
553
|
+
id: shapeId,
|
|
554
|
+
type: 'test-shape',
|
|
555
|
+
x: 20,
|
|
556
|
+
y: 20,
|
|
557
|
+
props: { w: 30, h: 30, x: 0, y: 0, isContainer: false },
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
const renderingShapes = editor.getUnorderedRenderingShapes(false)
|
|
561
|
+
const testShapes = renderingShapes.filter((s) => [containerId, shapeId].includes(s.id))
|
|
562
|
+
|
|
563
|
+
const result = getExportDefaultBounds(editor, testShapes, 32, null)
|
|
564
|
+
|
|
565
|
+
expect(result).toBeInstanceOf(Box)
|
|
566
|
+
// Container should contain regular shape, no padding applied
|
|
567
|
+
expect(result?.x).toBe(0)
|
|
568
|
+
expect(result?.y).toBe(0)
|
|
569
|
+
expect(result?.w).toBe(100)
|
|
570
|
+
expect(result?.h).toBe(100)
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
it('handles order sensitivity - regular shape processed first', () => {
|
|
574
|
+
const shapeId = createShapeId('shape')
|
|
575
|
+
const containerId = createShapeId('container')
|
|
576
|
+
|
|
577
|
+
// Create regular shape first (will be processed first due to creation order)
|
|
578
|
+
editor.createShape({
|
|
579
|
+
id: shapeId,
|
|
580
|
+
type: 'test-shape',
|
|
581
|
+
x: 20,
|
|
582
|
+
y: 20,
|
|
583
|
+
props: { w: 30, h: 30, x: 0, y: 0, isContainer: false },
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
// Create container second
|
|
587
|
+
editor.createShape({
|
|
588
|
+
id: containerId,
|
|
589
|
+
type: 'test-shape',
|
|
590
|
+
x: 0,
|
|
591
|
+
y: 0,
|
|
592
|
+
props: { w: 100, h: 100, x: 0, y: 0, isContainer: true },
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
const renderingShapes = editor.getUnorderedRenderingShapes(false)
|
|
596
|
+
const testShapes = renderingShapes.filter((s) => [shapeId, containerId].includes(s.id))
|
|
597
|
+
|
|
598
|
+
const result = getExportDefaultBounds(editor, testShapes, 32, null)
|
|
599
|
+
|
|
600
|
+
expect(result).toBeInstanceOf(Box)
|
|
601
|
+
// Container should still contain regular shape, no padding applied
|
|
602
|
+
expect(result?.x).toBe(0)
|
|
603
|
+
expect(result?.y).toBe(0)
|
|
604
|
+
expect(result?.w).toBe(100)
|
|
605
|
+
expect(result?.h).toBe(100)
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
it('multiple containers - only one that contains all others skips padding', () => {
|
|
609
|
+
const smallContainerId = createShapeId('smallContainer')
|
|
610
|
+
const largeContainerId = createShapeId('largeContainer')
|
|
611
|
+
const shapeId = createShapeId('shape')
|
|
612
|
+
|
|
613
|
+
// Small container
|
|
614
|
+
editor.createShape({
|
|
615
|
+
id: smallContainerId,
|
|
616
|
+
type: 'test-shape',
|
|
617
|
+
x: 10,
|
|
618
|
+
y: 10,
|
|
619
|
+
props: { w: 30, h: 30, x: 0, y: 0, isContainer: true },
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
// Large container that contains the small container AND the regular shape
|
|
623
|
+
editor.createShape({
|
|
624
|
+
id: largeContainerId,
|
|
625
|
+
type: 'test-shape',
|
|
626
|
+
x: 0,
|
|
627
|
+
y: 0,
|
|
628
|
+
props: { w: 100, h: 100, x: 0, y: 0, isContainer: true },
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
// Regular shape inside both containers
|
|
632
|
+
editor.createShape({
|
|
633
|
+
id: shapeId,
|
|
634
|
+
type: 'test-shape',
|
|
635
|
+
x: 15,
|
|
636
|
+
y: 15,
|
|
637
|
+
props: { w: 10, h: 10, x: 0, y: 0, isContainer: false },
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
const renderingShapes = editor.getUnorderedRenderingShapes(false)
|
|
641
|
+
const testShapes = renderingShapes.filter((s) =>
|
|
642
|
+
[smallContainerId, largeContainerId, shapeId].includes(s.id)
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
const result = getExportDefaultBounds(editor, testShapes, 32, null)
|
|
646
|
+
|
|
647
|
+
expect(result).toBeInstanceOf(Box)
|
|
648
|
+
// Large container contains everything (including small container), no padding
|
|
649
|
+
expect(result?.x).toBe(0)
|
|
650
|
+
expect(result?.y).toBe(0)
|
|
651
|
+
expect(result?.w).toBe(100)
|
|
652
|
+
expect(result?.h).toBe(100)
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
it('multiple containers - none contains all others, padding applied', () => {
|
|
656
|
+
const container1Id = createShapeId('container1')
|
|
657
|
+
const container2Id = createShapeId('container2')
|
|
658
|
+
const shape1Id = createShapeId('shape1')
|
|
659
|
+
const shape2Id = createShapeId('shape2')
|
|
660
|
+
|
|
661
|
+
// Container 1 contains shape1 but not container2 or shape2
|
|
662
|
+
editor.createShape({
|
|
663
|
+
id: container1Id,
|
|
664
|
+
type: 'test-shape',
|
|
665
|
+
x: 0,
|
|
666
|
+
y: 0,
|
|
667
|
+
props: { w: 40, h: 40, x: 0, y: 0, isContainer: true },
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
// Container 2 contains shape2 but not container1 or shape1
|
|
671
|
+
editor.createShape({
|
|
672
|
+
id: container2Id,
|
|
673
|
+
type: 'test-shape',
|
|
674
|
+
x: 60,
|
|
675
|
+
y: 60,
|
|
676
|
+
props: { w: 40, h: 40, x: 0, y: 0, isContainer: true },
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
// Shape inside container1
|
|
680
|
+
editor.createShape({
|
|
681
|
+
id: shape1Id,
|
|
682
|
+
type: 'test-shape',
|
|
683
|
+
x: 10,
|
|
684
|
+
y: 10,
|
|
685
|
+
props: { w: 20, h: 20, x: 0, y: 0, isContainer: false },
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
// Shape inside container2
|
|
689
|
+
editor.createShape({
|
|
690
|
+
id: shape2Id,
|
|
691
|
+
type: 'test-shape',
|
|
692
|
+
x: 70,
|
|
693
|
+
y: 70,
|
|
694
|
+
props: { w: 20, h: 20, x: 0, y: 0, isContainer: false },
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
const renderingShapes = editor.getUnorderedRenderingShapes(false)
|
|
698
|
+
const testShapes = renderingShapes.filter((s) =>
|
|
699
|
+
[container1Id, container2Id, shape1Id, shape2Id].includes(s.id)
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
const result = getExportDefaultBounds(editor, testShapes, 32, null)
|
|
703
|
+
|
|
704
|
+
expect(result).toBeInstanceOf(Box)
|
|
705
|
+
// No single container contains all others, padding should be applied
|
|
706
|
+
// Total bounds: (0,0) to (100,100), with padding
|
|
707
|
+
expect(result?.x).toBe(-32)
|
|
708
|
+
expect(result?.y).toBe(-32)
|
|
709
|
+
expect(result?.w).toBe(100 + 64) // 164
|
|
710
|
+
expect(result?.h).toBe(100 + 64) // 164
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
it('container covers most but not all shapes - padding applied', () => {
|
|
714
|
+
const containerId = createShapeId('container')
|
|
715
|
+
const insideShapeId = createShapeId('inside')
|
|
716
|
+
const partiallyOutsideId = createShapeId('partiallyOutside')
|
|
717
|
+
|
|
718
|
+
// Container
|
|
719
|
+
editor.createShape({
|
|
720
|
+
id: containerId,
|
|
721
|
+
type: 'test-shape',
|
|
722
|
+
x: 0,
|
|
723
|
+
y: 0,
|
|
724
|
+
props: { w: 80, h: 80, x: 0, y: 0, isContainer: true },
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
// Shape fully inside container
|
|
728
|
+
editor.createShape({
|
|
729
|
+
id: insideShapeId,
|
|
730
|
+
type: 'test-shape',
|
|
731
|
+
x: 20,
|
|
732
|
+
y: 20,
|
|
733
|
+
props: { w: 20, h: 20, x: 0, y: 0, isContainer: false },
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
// Shape that partially extends outside container
|
|
737
|
+
editor.createShape({
|
|
738
|
+
id: partiallyOutsideId,
|
|
739
|
+
type: 'test-shape',
|
|
740
|
+
x: 70,
|
|
741
|
+
y: 70,
|
|
742
|
+
props: { w: 20, h: 20, x: 0, y: 0, isContainer: false },
|
|
743
|
+
})
|
|
744
|
+
|
|
745
|
+
const renderingShapes = editor.getUnorderedRenderingShapes(false)
|
|
746
|
+
const testShapes = renderingShapes.filter((s) =>
|
|
747
|
+
[containerId, insideShapeId, partiallyOutsideId].includes(s.id)
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
const result = getExportDefaultBounds(editor, testShapes, 32, null)
|
|
751
|
+
|
|
752
|
+
expect(result).toBeInstanceOf(Box)
|
|
753
|
+
// Container doesn't contain all shapes, padding applied
|
|
754
|
+
// Total bounds: (0,0) to (90,90), with padding
|
|
755
|
+
expect(result?.x).toBe(-32)
|
|
756
|
+
expect(result?.y).toBe(-32)
|
|
757
|
+
expect(result?.w).toBe(90 + 64) // 154
|
|
758
|
+
expect(result?.h).toBe(90 + 64) // 154
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
it('nested containers - inner container processed first', () => {
|
|
762
|
+
const outerContainerId = createShapeId('outerContainer')
|
|
763
|
+
const innerContainerId = createShapeId('innerContainer')
|
|
764
|
+
const shapeId = createShapeId('shape')
|
|
765
|
+
|
|
766
|
+
// Inner container (created first)
|
|
767
|
+
editor.createShape({
|
|
768
|
+
id: innerContainerId,
|
|
769
|
+
type: 'test-shape',
|
|
770
|
+
x: 20,
|
|
771
|
+
y: 20,
|
|
772
|
+
props: { w: 40, h: 40, x: 0, y: 0, isContainer: true },
|
|
773
|
+
})
|
|
774
|
+
|
|
775
|
+
// Outer container that contains inner container
|
|
776
|
+
editor.createShape({
|
|
777
|
+
id: outerContainerId,
|
|
778
|
+
type: 'test-shape',
|
|
779
|
+
x: 0,
|
|
780
|
+
y: 0,
|
|
781
|
+
props: { w: 100, h: 100, x: 0, y: 0, isContainer: true },
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
// Shape inside inner container
|
|
785
|
+
editor.createShape({
|
|
786
|
+
id: shapeId,
|
|
787
|
+
type: 'test-shape',
|
|
788
|
+
x: 30,
|
|
789
|
+
y: 30,
|
|
790
|
+
props: { w: 20, h: 20, x: 0, y: 0, isContainer: false },
|
|
791
|
+
})
|
|
792
|
+
|
|
793
|
+
const renderingShapes = editor.getUnorderedRenderingShapes(false)
|
|
794
|
+
const testShapes = renderingShapes.filter((s) =>
|
|
795
|
+
[innerContainerId, outerContainerId, shapeId].includes(s.id)
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
const result = getExportDefaultBounds(editor, testShapes, 32, null)
|
|
799
|
+
|
|
800
|
+
expect(result).toBeInstanceOf(Box)
|
|
801
|
+
// Outer container contains everything, should use outer bounds without padding
|
|
802
|
+
expect(result?.x).toBe(0)
|
|
803
|
+
expect(result?.y).toBe(0)
|
|
804
|
+
expect(result?.w).toBe(100)
|
|
805
|
+
expect(result?.h).toBe(100)
|
|
806
|
+
})
|
|
807
|
+
|
|
808
|
+
it('container-only shapes should not skip padding', () => {
|
|
809
|
+
const container1Id = createShapeId('container1')
|
|
810
|
+
const container2Id = createShapeId('container2')
|
|
811
|
+
|
|
812
|
+
// Two containers, neither containing the other completely
|
|
813
|
+
editor.createShape({
|
|
814
|
+
id: container1Id,
|
|
815
|
+
type: 'test-shape',
|
|
816
|
+
x: 0,
|
|
817
|
+
y: 0,
|
|
818
|
+
props: { w: 50, h: 50, x: 0, y: 0, isContainer: true },
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
editor.createShape({
|
|
822
|
+
id: container2Id,
|
|
823
|
+
type: 'test-shape',
|
|
824
|
+
x: 30,
|
|
825
|
+
y: 30,
|
|
826
|
+
props: { w: 50, h: 50, x: 0, y: 0, isContainer: true },
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
const renderingShapes = editor.getUnorderedRenderingShapes(false)
|
|
830
|
+
const testShapes = renderingShapes.filter((s) => [container1Id, container2Id].includes(s.id))
|
|
831
|
+
|
|
832
|
+
const result = getExportDefaultBounds(editor, testShapes, 32, null)
|
|
833
|
+
|
|
834
|
+
expect(result).toBeInstanceOf(Box)
|
|
835
|
+
// Neither container fully contains the other, padding should be applied
|
|
836
|
+
// Total bounds: (0,0) to (80,80), with padding
|
|
837
|
+
expect(result?.x).toBe(-32)
|
|
838
|
+
expect(result?.y).toBe(-32)
|
|
839
|
+
expect(result?.w).toBe(80 + 64) // 144
|
|
840
|
+
expect(result?.h).toBe(80 + 64) // 144
|
|
841
|
+
})
|
|
842
|
+
|
|
843
|
+
it('single container with only itself skips padding', () => {
|
|
844
|
+
const containerId = createShapeId('container')
|
|
845
|
+
|
|
846
|
+
// Single container shape
|
|
847
|
+
editor.createShape({
|
|
848
|
+
id: containerId,
|
|
849
|
+
type: 'test-shape',
|
|
850
|
+
x: 10,
|
|
851
|
+
y: 20,
|
|
852
|
+
props: { w: 100, h: 80, x: 0, y: 0, isContainer: true },
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
const renderingShapes = editor.getUnorderedRenderingShapes(false)
|
|
856
|
+
const testShapes = renderingShapes.filter((s) => s.id === containerId)
|
|
857
|
+
|
|
858
|
+
const result = getExportDefaultBounds(editor, testShapes, 32, null)
|
|
859
|
+
|
|
860
|
+
expect(result).toBeInstanceOf(Box)
|
|
861
|
+
// Single container should skip padding (it trivially contains "all other shapes")
|
|
862
|
+
expect(result?.x).toBe(10)
|
|
863
|
+
expect(result?.y).toBe(20)
|
|
864
|
+
expect(result?.w).toBe(100)
|
|
865
|
+
expect(result?.h).toBe(80)
|
|
866
|
+
})
|
|
867
|
+
})
|
|
868
|
+
})
|