@openfairygui/functions 0.1.0

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/src/inspect.ts ADDED
@@ -0,0 +1,146 @@
1
+ import type { Document, Package } from '@openfairygui/core';
2
+
3
+ /**
4
+ * Summary report for a single resource category.
5
+ */
6
+ export interface InspectCategoryReport {
7
+ count: number;
8
+ details: Array<{ name: string; id: string; path?: string; exported?: boolean }>;
9
+ }
10
+
11
+ /**
12
+ * Full inspection report for a FairyGUI project.
13
+ */
14
+ export interface InspectReport {
15
+ projectId: string;
16
+ projectType: number;
17
+ version: string;
18
+ packages: Array<{
19
+ name: string;
20
+ id: string;
21
+ publishName: string;
22
+ resources: {
23
+ images: InspectCategoryReport;
24
+ sounds: InspectCategoryReport;
25
+ fonts: InspectCategoryReport;
26
+ movieClips: InspectCategoryReport;
27
+ components: InspectCategoryReport;
28
+ };
29
+ componentDetails: Array<{
30
+ name: string;
31
+ id: string;
32
+ childCount: number;
33
+ controllerCount: number;
34
+ transitionCount: number;
35
+ }>;
36
+ }>;
37
+ totals: {
38
+ packages: number;
39
+ images: number;
40
+ sounds: number;
41
+ fonts: number;
42
+ movieClips: number;
43
+ components: number;
44
+ displayObjects: number;
45
+ gears: number;
46
+ controllers: number;
47
+ transitions: number;
48
+ };
49
+ }
50
+
51
+ type PackageResource = ReturnType<Package['listResources']>[number];
52
+ type InspectableComponent = ReturnType<Package['listComponents']>[number];
53
+
54
+ function mapResource(resource: PackageResource): InspectCategoryReport['details'][number] {
55
+ return {
56
+ name: resource.getName(),
57
+ id: resource.getId(),
58
+ path: resource.getPath?.() ?? '/',
59
+ exported: resource.getExported?.() ?? false,
60
+ };
61
+ }
62
+
63
+ function mapComponentDetail(
64
+ component: InspectableComponent,
65
+ totals: InspectReport['totals'],
66
+ ): InspectReport['packages'][number]['componentDetails'][number] {
67
+ const children = component.listChildren();
68
+ const controllers = component.listControllers();
69
+ const transitions = component.listTransitions();
70
+
71
+ totals.displayObjects += children.length;
72
+ totals.controllers += controllers.length;
73
+ totals.transitions += transitions.length;
74
+
75
+ for (const child of children) {
76
+ totals.gears += child.listGears().length;
77
+ }
78
+
79
+ return {
80
+ name: component.getName(),
81
+ id: component.getId(),
82
+ childCount: children.length,
83
+ controllerCount: controllers.length,
84
+ transitionCount: transitions.length,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Generates a detailed report of the project contents.
90
+ *
91
+ * Unlike other transforms, `inspect()` does NOT modify the document —
92
+ * it returns a structured report.
93
+ *
94
+ * ```ts
95
+ * const report = inspect(doc);
96
+ * console.log(`${report.totals.packages} packages, ${report.totals.components} components`);
97
+ * ```
98
+ */
99
+ export function inspect(doc: Document): InspectReport {
100
+ const root = doc.getRoot();
101
+ const totals = {
102
+ packages: 0, images: 0, sounds: 0, fonts: 0,
103
+ movieClips: 0, components: 0, displayObjects: 0,
104
+ gears: 0, controllers: 0, transitions: 0,
105
+ };
106
+
107
+ const packages = root.listPackages().map((pkg) => {
108
+ totals.packages++;
109
+ const resources = pkg.listResources();
110
+
111
+ const images = resources.filter((r) => r.propertyType === 'ImageResource');
112
+ const sounds = resources.filter((r) => r.propertyType === 'SoundResource');
113
+ const fonts = resources.filter((r) => r.propertyType === 'FontResource');
114
+ const movieClips = resources.filter((r) => r.propertyType === 'MovieClipResource');
115
+ const components = pkg.listComponents();
116
+
117
+ totals.images += images.length;
118
+ totals.sounds += sounds.length;
119
+ totals.fonts += fonts.length;
120
+ totals.movieClips += movieClips.length;
121
+ totals.components += components.length;
122
+ const componentDetails = components.map((component) => mapComponentDetail(component, totals));
123
+
124
+ return {
125
+ name: pkg.getName(),
126
+ id: pkg.getId(),
127
+ publishName: pkg.getPublishName() || pkg.getName(),
128
+ resources: {
129
+ images: { count: images.length, details: images.map(mapResource) },
130
+ sounds: { count: sounds.length, details: sounds.map(mapResource) },
131
+ fonts: { count: fonts.length, details: fonts.map(mapResource) },
132
+ movieClips: { count: movieClips.length, details: movieClips.map(mapResource) },
133
+ components: { count: components.length, details: components.map(mapResource) },
134
+ },
135
+ componentDetails,
136
+ };
137
+ });
138
+
139
+ return {
140
+ projectId: root.getProjectId(),
141
+ projectType: root.getProjectType(),
142
+ version: root.getVersion(),
143
+ packages,
144
+ totals,
145
+ };
146
+ }
@@ -0,0 +1,431 @@
1
+ const NO_ROTATION = 2;
2
+ const MAX_SCORE = 2147483647;
3
+
4
+ export const MAX_RECTS_METHOD = {
5
+ BestShortSideFit: 0,
6
+ BestLongSideFit: 1,
7
+ BestAreaFit: 2,
8
+ BottomLeftRule: 3,
9
+ ContactPointRule: 4,
10
+ } as const;
11
+
12
+ export const COMPAT_NODE_RECT_FLAGS = {
13
+ DUPLICATE_PADDING: 1,
14
+ NO_ROTATION,
15
+ } as const;
16
+
17
+ export interface CompatNodeRect {
18
+ x: number;
19
+ y: number;
20
+ width: number;
21
+ height: number;
22
+ rotated: boolean;
23
+ index: number;
24
+ subIndex: number;
25
+ flags: number;
26
+ score1: number;
27
+ score2: number;
28
+ sourceKind?: string;
29
+ }
30
+
31
+ export interface CompatPage {
32
+ outputRects: CompatNodeRect[];
33
+ remainingRects: CompatNodeRect[];
34
+ occupancy: number;
35
+ width: number;
36
+ height: number;
37
+ }
38
+
39
+ export class MaxRectsCompat {
40
+ private static readonly helperRect = createNodeRect();
41
+ private binWidth = 0;
42
+ private binHeight = 0;
43
+ private allowRotations = false;
44
+ private readonly usedRectangles: CompatNodeRect[] = [];
45
+ private readonly freeRectangles: CompatNodeRect[] = [];
46
+
47
+ public init(width: number, height: number, allowRotations = false): void {
48
+ this.binWidth = width;
49
+ this.binHeight = height;
50
+ this.allowRotations = allowRotations;
51
+ this.usedRectangles.length = 0;
52
+ this.freeRectangles.length = 0;
53
+ this.freeRectangles.push({
54
+ ...createNodeRect(),
55
+ x: 0,
56
+ y: 0,
57
+ width,
58
+ height,
59
+ });
60
+ }
61
+
62
+ public insert(rect: CompatNodeRect, method: number): CompatNodeRect | null {
63
+ const newNode = this.scoreRect(rect, method);
64
+ if (newNode.height === 0) return null;
65
+ const placed = cloneNodeRect(newNode);
66
+ this.placeRect(placed);
67
+ return placed;
68
+ }
69
+
70
+ public pack(rects: CompatNodeRect[], method: number): CompatPage {
71
+ const remaining = rects.map(cloneNodeRect);
72
+ while (remaining.length > 0) {
73
+ let bestIndex = -1;
74
+ const bestNode = createNodeRect();
75
+ bestNode.score1 = MAX_SCORE;
76
+ bestNode.score2 = MAX_SCORE;
77
+ for (let index = 0; index < remaining.length; index += 1) {
78
+ const candidate = this.scoreRect(remaining[index], method);
79
+ if (
80
+ candidate.score1 < bestNode.score1 ||
81
+ (candidate.score1 === bestNode.score1 && candidate.score2 < bestNode.score2)
82
+ ) {
83
+ copyNodeRect(bestNode, candidate);
84
+ bestIndex = index;
85
+ }
86
+ }
87
+ if (bestIndex === -1) break;
88
+ this.placeRect(bestNode);
89
+ remaining.splice(bestIndex, 1);
90
+ }
91
+ const result = this.getResult();
92
+ result.remainingRects = remaining;
93
+ return result;
94
+ }
95
+
96
+ public getResult(): CompatPage {
97
+ let width = 0;
98
+ let height = 0;
99
+ for (const rect of this.usedRectangles) {
100
+ width = Math.max(width, rect.x + rect.width);
101
+ height = Math.max(height, rect.y + rect.height);
102
+ }
103
+ return {
104
+ outputRects: this.usedRectangles.map(cloneNodeRect),
105
+ remainingRects: [],
106
+ occupancy: this.getOccupancy(),
107
+ width,
108
+ height,
109
+ };
110
+ }
111
+
112
+ private getOccupancy(): number {
113
+ let usedSurface = 0;
114
+ for (const rect of this.usedRectangles) usedSurface += rect.width * rect.height;
115
+ return usedSurface / (this.binWidth * this.binHeight);
116
+ }
117
+
118
+ private placeRect(rect: CompatNodeRect): void {
119
+ for (let index = 0; index < this.freeRectangles.length; index += 1) {
120
+ if (this.splitFreeNode(this.freeRectangles[index], rect)) {
121
+ this.freeRectangles.splice(index, 1);
122
+ index -= 1;
123
+ }
124
+ }
125
+ this.pruneFreeList();
126
+ this.usedRectangles.push(rect);
127
+ }
128
+
129
+ private scoreRect(rect: CompatNodeRect, method: number): CompatNodeRect {
130
+ const helper = MaxRectsCompat.helperRect;
131
+ helper.height = 0;
132
+ let newNode: CompatNodeRect;
133
+ switch (method) {
134
+ case MAX_RECTS_METHOD.BestShortSideFit:
135
+ newNode = this.findPositionForNewNodeBestShortSideFit(rect.width, rect.height, allowRotation(rect));
136
+ break;
137
+ case MAX_RECTS_METHOD.BestLongSideFit:
138
+ newNode = this.findPositionForNewNodeBestLongSideFit(rect.width, rect.height, allowRotation(rect));
139
+ break;
140
+ case MAX_RECTS_METHOD.BestAreaFit:
141
+ newNode = this.findPositionForNewNodeBestAreaFit(rect.width, rect.height, allowRotation(rect));
142
+ break;
143
+ case MAX_RECTS_METHOD.BottomLeftRule:
144
+ newNode = this.findPositionForNewNodeBottomLeft(rect.width, rect.height, allowRotation(rect));
145
+ break;
146
+ case MAX_RECTS_METHOD.ContactPointRule:
147
+ newNode = this.findPositionForNewNodeContactPoint(rect.width, rect.height, allowRotation(rect));
148
+ newNode.score1 = -newNode.score1;
149
+ break;
150
+ default:
151
+ newNode = helper;
152
+ break;
153
+ }
154
+ if (newNode.height === 0) {
155
+ newNode.score1 = MAX_SCORE;
156
+ newNode.score2 = MAX_SCORE;
157
+ }
158
+ newNode.index = rect.index;
159
+ newNode.subIndex = rect.subIndex;
160
+ newNode.flags = rect.flags;
161
+ newNode.sourceKind = rect.sourceKind;
162
+ return cloneNodeRect(newNode);
163
+ }
164
+
165
+ private findPositionForNewNodeBottomLeft(width: number, height: number, allowRectRotation: boolean): CompatNodeRect {
166
+ const bestNode = MaxRectsCompat.helperRect;
167
+ bestNode.score1 = MAX_SCORE;
168
+ bestNode.score2 = 0;
169
+ for (const freeRect of this.freeRectangles) {
170
+ if (freeRect.width >= width && freeRect.height >= height) {
171
+ const topSideY = freeRect.y + height;
172
+ if (topSideY < bestNode.score1 || (topSideY === bestNode.score1 && freeRect.x < bestNode.score2)) {
173
+ setNodeRect(bestNode, freeRect.x, freeRect.y, width, height, false, topSideY, freeRect.x);
174
+ }
175
+ }
176
+ if (this.allowRotations && allowRectRotation && freeRect.width >= height && freeRect.height >= width) {
177
+ const topSideY = freeRect.y + width;
178
+ if (topSideY < bestNode.score1 || (topSideY === bestNode.score1 && freeRect.x < bestNode.score2)) {
179
+ setNodeRect(bestNode, freeRect.x, freeRect.y, height, width, true, topSideY, freeRect.x);
180
+ }
181
+ }
182
+ }
183
+ return bestNode;
184
+ }
185
+
186
+ private findPositionForNewNodeBestShortSideFit(width: number, height: number, allowRectRotation: boolean): CompatNodeRect {
187
+ const bestNode = MaxRectsCompat.helperRect;
188
+ bestNode.score1 = MAX_SCORE;
189
+ bestNode.score2 = 0;
190
+ for (const freeRect of this.freeRectangles) {
191
+ if (freeRect.width >= width && freeRect.height >= height) {
192
+ const leftoverHoriz = Math.abs(freeRect.width - width);
193
+ const leftoverVert = Math.abs(freeRect.height - height);
194
+ const shortSideFit = Math.min(leftoverHoriz, leftoverVert);
195
+ const longSideFit = Math.max(leftoverHoriz, leftoverVert);
196
+ if (shortSideFit < bestNode.score1 || (shortSideFit === bestNode.score1 && longSideFit < bestNode.score2)) {
197
+ setNodeRect(bestNode, freeRect.x, freeRect.y, width, height, false, shortSideFit, longSideFit);
198
+ }
199
+ }
200
+ if (this.allowRotations && allowRectRotation && freeRect.width >= height && freeRect.height >= width) {
201
+ const leftoverHoriz = Math.abs(freeRect.width - height);
202
+ const leftoverVert = Math.abs(freeRect.height - width);
203
+ const shortSideFit = Math.min(leftoverHoriz, leftoverVert);
204
+ const longSideFit = Math.max(leftoverHoriz, leftoverVert);
205
+ if (shortSideFit < bestNode.score1 || (shortSideFit === bestNode.score1 && longSideFit < bestNode.score2)) {
206
+ setNodeRect(bestNode, freeRect.x, freeRect.y, height, width, true, shortSideFit, longSideFit);
207
+ }
208
+ }
209
+ }
210
+ return bestNode;
211
+ }
212
+
213
+ private findPositionForNewNodeBestLongSideFit(width: number, height: number, allowRectRotation: boolean): CompatNodeRect {
214
+ const bestNode = MaxRectsCompat.helperRect;
215
+ bestNode.score1 = 0;
216
+ bestNode.score2 = MAX_SCORE;
217
+ for (const freeRect of this.freeRectangles) {
218
+ if (freeRect.width >= width && freeRect.height >= height) {
219
+ const leftoverHoriz = Math.abs(freeRect.width - width);
220
+ const leftoverVert = Math.abs(freeRect.height - height);
221
+ const shortSideFit = Math.min(leftoverHoriz, leftoverVert);
222
+ const longSideFit = Math.max(leftoverHoriz, leftoverVert);
223
+ if (longSideFit < bestNode.score2 || (longSideFit === bestNode.score2 && shortSideFit < bestNode.score1)) {
224
+ setNodeRect(bestNode, freeRect.x, freeRect.y, width, height, false, shortSideFit, longSideFit);
225
+ }
226
+ }
227
+ if (this.allowRotations && allowRectRotation && freeRect.width >= height && freeRect.height >= width) {
228
+ const leftoverHoriz = Math.abs(freeRect.width - height);
229
+ const leftoverVert = Math.abs(freeRect.height - width);
230
+ const shortSideFit = Math.min(leftoverHoriz, leftoverVert);
231
+ const longSideFit = Math.max(leftoverHoriz, leftoverVert);
232
+ if (longSideFit < bestNode.score2 || (longSideFit === bestNode.score2 && shortSideFit < bestNode.score1)) {
233
+ setNodeRect(bestNode, freeRect.x, freeRect.y, height, width, true, shortSideFit, longSideFit);
234
+ }
235
+ }
236
+ }
237
+ return bestNode;
238
+ }
239
+
240
+ private findPositionForNewNodeBestAreaFit(width: number, height: number, allowRectRotation: boolean): CompatNodeRect {
241
+ const bestNode = MaxRectsCompat.helperRect;
242
+ bestNode.score1 = MAX_SCORE;
243
+ bestNode.score2 = 0;
244
+ for (const freeRect of this.freeRectangles) {
245
+ const areaFit = freeRect.width * freeRect.height - width * height;
246
+ if (freeRect.width >= width && freeRect.height >= height) {
247
+ const leftoverHoriz = Math.abs(freeRect.width - width);
248
+ const leftoverVert = Math.abs(freeRect.height - height);
249
+ const shortSideFit = Math.min(leftoverHoriz, leftoverVert);
250
+ if (areaFit < bestNode.score1 || (areaFit === bestNode.score1 && shortSideFit < bestNode.score2)) {
251
+ setNodeRect(bestNode, freeRect.x, freeRect.y, width, height, false, areaFit, shortSideFit);
252
+ }
253
+ }
254
+ if (this.allowRotations && allowRectRotation && freeRect.width >= height && freeRect.height >= width) {
255
+ const leftoverHoriz = Math.abs(freeRect.width - height);
256
+ const leftoverVert = Math.abs(freeRect.height - width);
257
+ const shortSideFit = Math.min(leftoverHoriz, leftoverVert);
258
+ if (areaFit < bestNode.score1 || (areaFit === bestNode.score1 && shortSideFit < bestNode.score2)) {
259
+ setNodeRect(bestNode, freeRect.x, freeRect.y, height, width, true, areaFit, shortSideFit);
260
+ }
261
+ }
262
+ }
263
+ return bestNode;
264
+ }
265
+
266
+ private findPositionForNewNodeContactPoint(width: number, height: number, allowRectRotation: boolean): CompatNodeRect {
267
+ const bestNode = MaxRectsCompat.helperRect;
268
+ bestNode.score1 = -1;
269
+ bestNode.score2 = 0;
270
+ for (const freeRect of this.freeRectangles) {
271
+ if (freeRect.width >= width && freeRect.height >= height) {
272
+ const score = this.contactPointScoreNode(freeRect.x, freeRect.y, width, height);
273
+ if (score > bestNode.score1) {
274
+ setNodeRect(bestNode, freeRect.x, freeRect.y, width, height, false, score, bestNode.score2);
275
+ }
276
+ }
277
+ if (this.allowRotations && allowRectRotation && freeRect.width >= height && freeRect.height >= width) {
278
+ const score = this.contactPointScoreNode(freeRect.x, freeRect.y, height, width);
279
+ if (score > bestNode.score1) {
280
+ setNodeRect(bestNode, freeRect.x, freeRect.y, height, width, true, score, bestNode.score2);
281
+ }
282
+ }
283
+ }
284
+ return bestNode;
285
+ }
286
+
287
+ private contactPointScoreNode(x: number, y: number, width: number, height: number): number {
288
+ let score = 0;
289
+ if (x === 0 || x + width === this.binWidth) score += height;
290
+ if (y === 0 || y + height === this.binHeight) score += width;
291
+ for (const rect of this.usedRectangles) {
292
+ if (rect.x === x + width || rect.x + rect.width === x) {
293
+ score += commonIntervalLength(rect.y, rect.y + rect.height, y, y + height);
294
+ }
295
+ if (rect.y === y + height || rect.y + rect.height === y) {
296
+ score += commonIntervalLength(rect.x, rect.x + rect.width, x, x + width);
297
+ }
298
+ }
299
+ return score;
300
+ }
301
+
302
+ private splitFreeNode(freeNode: CompatNodeRect, usedNode: CompatNodeRect): boolean {
303
+ if (
304
+ usedNode.x >= freeNode.x + freeNode.width ||
305
+ usedNode.x + usedNode.width <= freeNode.x ||
306
+ usedNode.y >= freeNode.y + freeNode.height ||
307
+ usedNode.y + usedNode.height <= freeNode.y
308
+ ) {
309
+ return false;
310
+ }
311
+ if (usedNode.x < freeNode.x + freeNode.width && usedNode.x + usedNode.width > freeNode.x) {
312
+ if (usedNode.y > freeNode.y && usedNode.y < freeNode.y + freeNode.height) {
313
+ const newNode = cloneNodeRect(freeNode);
314
+ newNode.height = usedNode.y - newNode.y;
315
+ this.freeRectangles.push(newNode);
316
+ }
317
+ if (usedNode.y + usedNode.height < freeNode.y + freeNode.height) {
318
+ const newNode = cloneNodeRect(freeNode);
319
+ newNode.y = usedNode.y + usedNode.height;
320
+ newNode.height = freeNode.y + freeNode.height - (usedNode.y + usedNode.height);
321
+ this.freeRectangles.push(newNode);
322
+ }
323
+ }
324
+ if (usedNode.y < freeNode.y + freeNode.height && usedNode.y + usedNode.height > freeNode.y) {
325
+ if (usedNode.x > freeNode.x && usedNode.x < freeNode.x + freeNode.width) {
326
+ const newNode = cloneNodeRect(freeNode);
327
+ newNode.width = usedNode.x - newNode.x;
328
+ this.freeRectangles.push(newNode);
329
+ }
330
+ if (usedNode.x + usedNode.width < freeNode.x + freeNode.width) {
331
+ const newNode = cloneNodeRect(freeNode);
332
+ newNode.x = usedNode.x + usedNode.width;
333
+ newNode.width = freeNode.x + freeNode.width - (usedNode.x + usedNode.width);
334
+ this.freeRectangles.push(newNode);
335
+ }
336
+ }
337
+ return true;
338
+ }
339
+
340
+ private pruneFreeList(): void {
341
+ let length = this.freeRectangles.length;
342
+ let left = 0;
343
+ while (left < length) {
344
+ let right = left + 1;
345
+ while (right < length) {
346
+ if (isContainedIn(this.freeRectangles[left], this.freeRectangles[right])) {
347
+ this.freeRectangles.splice(left, 1);
348
+ length -= 1;
349
+ break;
350
+ }
351
+ if (isContainedIn(this.freeRectangles[right], this.freeRectangles[left])) {
352
+ this.freeRectangles.splice(right, 1);
353
+ length -= 1;
354
+ }
355
+ right += 1;
356
+ }
357
+ left += 1;
358
+ }
359
+ }
360
+ }
361
+
362
+ function createNodeRect(): CompatNodeRect {
363
+ return {
364
+ x: 0,
365
+ y: 0,
366
+ width: 0,
367
+ height: 0,
368
+ rotated: false,
369
+ index: 0,
370
+ subIndex: -1,
371
+ flags: 0,
372
+ score1: 0,
373
+ score2: 0,
374
+ sourceKind: undefined,
375
+ };
376
+ }
377
+
378
+ function cloneNodeRect(rect: CompatNodeRect): CompatNodeRect {
379
+ return { ...rect };
380
+ }
381
+
382
+ function copyNodeRect(target: CompatNodeRect, source: CompatNodeRect): void {
383
+ target.x = source.x;
384
+ target.y = source.y;
385
+ target.width = source.width;
386
+ target.height = source.height;
387
+ target.rotated = source.rotated;
388
+ target.index = source.index;
389
+ target.subIndex = source.subIndex;
390
+ target.flags = source.flags;
391
+ target.score1 = source.score1;
392
+ target.score2 = source.score2;
393
+ target.sourceKind = source.sourceKind;
394
+ }
395
+
396
+ function setNodeRect(
397
+ target: CompatNodeRect,
398
+ x: number,
399
+ y: number,
400
+ width: number,
401
+ height: number,
402
+ rotated: boolean,
403
+ score1: number,
404
+ score2: number,
405
+ ): void {
406
+ target.x = x;
407
+ target.y = y;
408
+ target.width = width;
409
+ target.height = height;
410
+ target.rotated = rotated;
411
+ target.score1 = score1;
412
+ target.score2 = score2;
413
+ }
414
+
415
+ function allowRotation(rect: CompatNodeRect): boolean {
416
+ return (rect.flags & NO_ROTATION) === 0;
417
+ }
418
+
419
+ function commonIntervalLength(startA: number, endA: number, startB: number, endB: number): number {
420
+ if (endA < startB || endB < startA) return 0;
421
+ return Math.min(endA, endB) - Math.max(startA, startB);
422
+ }
423
+
424
+ function isContainedIn(left: CompatNodeRect, right: CompatNodeRect): boolean {
425
+ return (
426
+ left.x >= right.x &&
427
+ left.y >= right.y &&
428
+ left.x + left.width <= right.x + right.width &&
429
+ left.y + left.height <= right.y + right.height
430
+ );
431
+ }