Skip to content

Commit ff4256a

Browse files
Improve details of center/origin when an object is created by the AI (#8322)
1 parent 73be609 commit ff4256a

File tree

3 files changed

+409
-22
lines changed

3 files changed

+409
-22
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// @flow
2+
import { type AssetShortHeader } from '../Utils/GDevelopServices/Asset';
3+
4+
const gd: libGDevelop = global.gd;
5+
6+
/**
7+
* Returns size, origin and center for an asset short header.
8+
* Returns null for object types where this information cannot be determined statically.
9+
*/
10+
export const getObjectSizeAndOriginInfo = (
11+
object: gdObject,
12+
project: gdProject,
13+
assetShortHeader?: AssetShortHeader | null
14+
): {| size: string, origin: string, center: string |} | null => {
15+
const objectConfiguration = object.getConfiguration();
16+
const objectType = object.getType();
17+
18+
if (objectType === 'Sprite') {
19+
const spriteConfiguration = gd.asSpriteConfiguration(objectConfiguration);
20+
const animations = spriteConfiguration.getAnimations();
21+
if (
22+
animations.getAnimationsCount() > 0 &&
23+
animations.getAnimation(0).getDirectionsCount() > 0 &&
24+
animations
25+
.getAnimation(0)
26+
.getDirection(0)
27+
.getSpritesCount() > 0
28+
) {
29+
const sprite = animations
30+
.getAnimation(0)
31+
.getDirection(0)
32+
.getSprite(0);
33+
const origin = sprite.getOrigin();
34+
const originStr = `${origin.getX()};${origin.getY()}`;
35+
36+
let centerStr;
37+
if (sprite.isDefaultCenterPoint()) {
38+
centerStr =
39+
assetShortHeader != null
40+
? `${assetShortHeader.width / 2};${assetShortHeader.height / 2}`
41+
: 'center of image';
42+
} else {
43+
const center = sprite.getCenter();
44+
centerStr = `${center.getX()};${center.getY()}`;
45+
}
46+
47+
const sizeStr =
48+
assetShortHeader != null
49+
? `${assetShortHeader.width}x${assetShortHeader.height}`
50+
: 'unknown';
51+
52+
return { size: sizeStr, origin: originStr, center: centerStr };
53+
}
54+
return null;
55+
}
56+
57+
if (objectType === 'TiledSpriteObject::TiledSprite') {
58+
const config = gd.asTiledSpriteConfiguration(objectConfiguration);
59+
const width = config.getWidth();
60+
const height = config.getHeight();
61+
return {
62+
size: `${width}x${height}`,
63+
origin: '0;0',
64+
center: `${width / 2};${height / 2}`,
65+
};
66+
}
67+
68+
if (objectType === 'PanelSpriteObject::PanelSprite') {
69+
const config = gd.asPanelSpriteConfiguration(objectConfiguration);
70+
const width = config.getWidth();
71+
const height = config.getHeight();
72+
return {
73+
size: `${width}x${height}`,
74+
origin: '0;0',
75+
center: `${width / 2};${height / 2}`,
76+
};
77+
}
78+
79+
// Events-based (custom) objects: derive size from their declared area.
80+
if (project.hasEventsBasedObject(objectType)) {
81+
const eventsBasedObject = project.getEventsBasedObject(objectType);
82+
const minX = eventsBasedObject.getAreaMinX();
83+
const maxX = eventsBasedObject.getAreaMaxX();
84+
const minY = eventsBasedObject.getAreaMinY();
85+
const maxY = eventsBasedObject.getAreaMaxY();
86+
const isRenderedIn3D = eventsBasedObject.isRenderedIn3D();
87+
const minZ = isRenderedIn3D ? eventsBasedObject.getAreaMinZ() : 0;
88+
const maxZ = isRenderedIn3D ? eventsBasedObject.getAreaMaxZ() : 0;
89+
const width = maxX - minX;
90+
const height = maxY - minY;
91+
const depth = maxZ - minZ;
92+
93+
const origin = `${-minX};${-minY}${isRenderedIn3D ? `;${-minZ}` : ''}`;
94+
const center = `${width / 2};${height / 2}${
95+
isRenderedIn3D ? `;${depth / 2}` : ''
96+
}`;
97+
return {
98+
size: `${width}x${height}${isRenderedIn3D ? `x${depth}` : ''}`,
99+
origin,
100+
center,
101+
};
102+
}
103+
104+
return null;
105+
};
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
// @flow
2+
import { fakeAssetShortHeader1 } from '../fixtures/GDevelopServicesTestData';
3+
import { getObjectSizeAndOriginInfo } from './Utils';
4+
5+
const gd: libGDevelop = global.gd;
6+
7+
describe('getObjectSizeAndOriginInfo', () => {
8+
let project: gdProject;
9+
10+
beforeEach(() => {
11+
// $FlowFixMe[invalid-constructor]
12+
project = new gd.ProjectHelper.createNewGDJSProject();
13+
});
14+
15+
afterEach(() => {
16+
project.delete();
17+
});
18+
19+
describe('Sprite', () => {
20+
it('returns origin and center from first frame, and size from the asset short header', () => {
21+
const objects = project.getObjects();
22+
const object = objects.insertNewObject(
23+
project,
24+
'Sprite',
25+
'MySprite',
26+
objects.getObjectsCount()
27+
);
28+
const spriteConfig = gd.asSpriteConfiguration(object.getConfiguration());
29+
const animation = new gd.Animation();
30+
animation.setDirectionsCount(1);
31+
const sprite = new gd.Sprite();
32+
sprite.getOrigin().setX(10);
33+
sprite.getOrigin().setY(20);
34+
// Leave center as default (isDefaultCenterPoint = true)
35+
animation.getDirection(0).addSprite(sprite);
36+
spriteConfig.getAnimations().addAnimation(animation);
37+
38+
expect(
39+
getObjectSizeAndOriginInfo(object, project, fakeAssetShortHeader1)
40+
).toEqual({
41+
size: '36x36',
42+
origin: '10;20',
43+
center: '18;18', // fakeAssetShortHeader1 is 36x36, so center = 18;18
44+
});
45+
});
46+
47+
it('returns a custom center when the sprite has one set explicitly', () => {
48+
const objects = project.getObjects();
49+
const object = objects.insertNewObject(
50+
project,
51+
'Sprite',
52+
'MySpriteCustomCenter',
53+
objects.getObjectsCount()
54+
);
55+
const spriteConfig = gd.asSpriteConfiguration(object.getConfiguration());
56+
const animation = new gd.Animation();
57+
animation.setDirectionsCount(1);
58+
const sprite = new gd.Sprite();
59+
sprite.getOrigin().setX(5);
60+
sprite.getOrigin().setY(5);
61+
sprite.setDefaultCenterPoint(false);
62+
sprite.getCenter().setX(40);
63+
sprite.getCenter().setY(80);
64+
animation.getDirection(0).addSprite(sprite);
65+
spriteConfig.getAnimations().addAnimation(animation);
66+
67+
expect(
68+
getObjectSizeAndOriginInfo(object, project, fakeAssetShortHeader1)
69+
).toEqual({
70+
size: '36x36',
71+
origin: '5;5',
72+
center: '40;80',
73+
});
74+
});
75+
76+
it('returns "unknown" size and "center of image" when no asset short header is provided', () => {
77+
const objects = project.getObjects();
78+
const object = objects.insertNewObject(
79+
project,
80+
'Sprite',
81+
'MySpriteNoHeader',
82+
objects.getObjectsCount()
83+
);
84+
const spriteConfig = gd.asSpriteConfiguration(object.getConfiguration());
85+
const animation = new gd.Animation();
86+
animation.setDirectionsCount(1);
87+
const sprite = new gd.Sprite();
88+
animation.getDirection(0).addSprite(sprite);
89+
spriteConfig.getAnimations().addAnimation(animation);
90+
91+
expect(getObjectSizeAndOriginInfo(object, project, null)).toEqual({
92+
size: 'unknown',
93+
origin: '0;0',
94+
center: 'center of image',
95+
});
96+
});
97+
98+
it('returns null when the sprite has no animations', () => {
99+
const objects = project.getObjects();
100+
const object = objects.insertNewObject(
101+
project,
102+
'Sprite',
103+
'MySpriteEmpty',
104+
objects.getObjectsCount()
105+
);
106+
// No animations added — getAnimationsCount() === 0
107+
108+
expect(
109+
getObjectSizeAndOriginInfo(object, project, fakeAssetShortHeader1)
110+
).toBeNull();
111+
});
112+
});
113+
114+
describe('TiledSpriteObject::TiledSprite', () => {
115+
it('returns origin 0;0, center at image center, and size from the configuration', () => {
116+
const objects = project.getObjects();
117+
const object = objects.insertNewObject(
118+
project,
119+
'TiledSpriteObject::TiledSprite',
120+
'MyTiledSprite',
121+
objects.getObjectsCount()
122+
);
123+
const config = gd.asTiledSpriteConfiguration(object.getConfiguration());
124+
config.setWidth(200);
125+
config.setHeight(150);
126+
127+
expect(getObjectSizeAndOriginInfo(object, project, null)).toEqual({
128+
size: '200x150',
129+
origin: '0;0',
130+
center: '100;75',
131+
});
132+
});
133+
});
134+
135+
describe('PanelSpriteObject::PanelSprite', () => {
136+
it('returns origin 0;0, center at image center, and size from the configuration', () => {
137+
const objects = project.getObjects();
138+
const object = objects.insertNewObject(
139+
project,
140+
'PanelSpriteObject::PanelSprite',
141+
'MyPanelSprite',
142+
objects.getObjectsCount()
143+
);
144+
const config = gd.asPanelSpriteConfiguration(object.getConfiguration());
145+
config.setWidth(120);
146+
config.setHeight(390);
147+
148+
expect(getObjectSizeAndOriginInfo(object, project, null)).toEqual({
149+
size: '120x390',
150+
origin: '0;0',
151+
center: '60;195',
152+
});
153+
});
154+
});
155+
156+
describe('Events-based (custom) object', () => {
157+
it('returns size and center derived from the declared area bounds', () => {
158+
// Register a custom object type in the project.
159+
const extension = project.insertNewEventsFunctionsExtension('MyExt', 0);
160+
const eventsBasedObject = extension
161+
.getEventsBasedObjects()
162+
.insertNew('MyCustomObject', 0);
163+
eventsBasedObject.setAreaMinX(0);
164+
eventsBasedObject.setAreaMaxX(100);
165+
eventsBasedObject.setAreaMinY(0);
166+
eventsBasedObject.setAreaMaxY(80);
167+
168+
const objects = project.getObjects();
169+
const object = objects.insertNewObject(
170+
project,
171+
'MyExt::MyCustomObject',
172+
'MyCustomObjectInstance',
173+
objects.getObjectsCount()
174+
);
175+
176+
expect(getObjectSizeAndOriginInfo(object, project, null)).toEqual({
177+
size: '100x80',
178+
origin: '0;0',
179+
center: '50;40',
180+
});
181+
});
182+
183+
it('handles a non 0;0 origin based on area min bounds', () => {
184+
const extension = project.insertNewEventsFunctionsExtension('MyExt2', 0);
185+
const eventsBasedObject = extension
186+
.getEventsBasedObjects()
187+
.insertNew('MyOffsetObject', 0);
188+
eventsBasedObject.setAreaMinX(-10);
189+
eventsBasedObject.setAreaMaxX(90);
190+
eventsBasedObject.setAreaMinY(-20);
191+
eventsBasedObject.setAreaMaxY(40);
192+
193+
const objects = project.getObjects();
194+
const object = objects.insertNewObject(
195+
project,
196+
'MyExt2::MyOffsetObject',
197+
'MyOffsetObjectInstance',
198+
objects.getObjectsCount()
199+
);
200+
201+
expect(getObjectSizeAndOriginInfo(object, project, null)).toEqual({
202+
size: '100x60',
203+
origin: '10;20',
204+
center: '50;30',
205+
});
206+
});
207+
208+
it('handles 3D events-based objects with Z bounds included in size/origin/center', () => {
209+
const extension = project.insertNewEventsFunctionsExtension('MyExt3', 0);
210+
const eventsBasedObject = extension
211+
.getEventsBasedObjects()
212+
.insertNew('My3DObject', 0);
213+
eventsBasedObject.setAreaMinX(-10);
214+
eventsBasedObject.setAreaMaxX(90);
215+
eventsBasedObject.setAreaMinY(-20);
216+
eventsBasedObject.setAreaMaxY(40);
217+
eventsBasedObject.setAreaMinZ(-5);
218+
eventsBasedObject.setAreaMaxZ(25);
219+
eventsBasedObject.markAsRenderedIn3D(true);
220+
221+
const objects = project.getObjects();
222+
const object = objects.insertNewObject(
223+
project,
224+
'MyExt3::My3DObject',
225+
'My3DObjectInstance',
226+
objects.getObjectsCount()
227+
);
228+
229+
expect(getObjectSizeAndOriginInfo(object, project, null)).toEqual({
230+
size: '100x60x30',
231+
origin: '10;20;5',
232+
center: '50;30;15',
233+
});
234+
});
235+
});
236+
237+
describe('Unsupported object type', () => {
238+
it('returns null for object types with no static size info', () => {
239+
const objects = project.getObjects();
240+
const object = objects.insertNewObject(
241+
project,
242+
'TextObject::Text',
243+
'MyText',
244+
objects.getObjectsCount()
245+
);
246+
247+
expect(getObjectSizeAndOriginInfo(object, project, null)).toBeNull();
248+
});
249+
});
250+
});

0 commit comments

Comments
 (0)