mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
66 Commits
experiment
...
experiment
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5eb5fb3af9 | ||
![]() |
cd80872fca | ||
![]() |
61cb6dde0e | ||
![]() |
29c8de0987 | ||
![]() |
802e9a5128 | ||
![]() |
c82602919b | ||
![]() |
80cbb8f542 | ||
![]() |
a9ceba7bef | ||
![]() |
7d9e72c876 | ||
![]() |
3baf5903d3 | ||
![]() |
6008e1045d | ||
![]() |
9dda9a89c5 | ||
![]() |
814c6aec4f | ||
![]() |
84e4158407 | ||
![]() |
aa6c3bc1c8 | ||
![]() |
41dc6d0966 | ||
![]() |
9c19865dec | ||
![]() |
bbfb7f63f0 | ||
![]() |
2f8304c2dd | ||
![]() |
c1aa88861d | ||
![]() |
254ab0350b | ||
![]() |
b0a3c34803 | ||
![]() |
63a3265310 | ||
![]() |
d19292f43d | ||
![]() |
3646d05b65 | ||
![]() |
25a4d04896 | ||
![]() |
2df6c13ed7 | ||
![]() |
93f84a14cb | ||
![]() |
02e0ebf63b | ||
![]() |
da3abb331a | ||
![]() |
a5a9524ed6 | ||
![]() |
c9cc1f6fdc | ||
![]() |
de140f1190 | ||
![]() |
9a82fda7fe | ||
![]() |
eac92704fa | ||
![]() |
dc607d85c3 | ||
![]() |
e96269d899 | ||
![]() |
05d622c5c0 | ||
![]() |
0758397196 | ||
![]() |
65293ddd99 | ||
![]() |
225a3a67c6 | ||
![]() |
a96171aacd | ||
![]() |
1a21a0bfb3 | ||
![]() |
cc23301875 | ||
![]() |
aa513c04e6 | ||
![]() |
a4e8e8c00b | ||
![]() |
0d34680fcd | ||
![]() |
4abdb9dca6 | ||
![]() |
db05a07023 | ||
![]() |
78ebe58713 | ||
![]() |
420c7a4429 | ||
![]() |
1442a2772e | ||
![]() |
ce18bdd5a7 | ||
![]() |
2f2cc1bbe3 | ||
![]() |
097ad4fff5 | ||
![]() |
2112ed789a | ||
![]() |
a41a1c14a0 | ||
![]() |
771d3264bb | ||
![]() |
9fa1e552e0 | ||
![]() |
503a0873d9 | ||
![]() |
715480bdef | ||
![]() |
869fd7eb08 | ||
![]() |
e0cb8e4953 | ||
![]() |
0ef4953241 | ||
![]() |
d154384164 | ||
![]() |
504df2e0a3 |
@@ -176,7 +176,7 @@ const defineTileMap = function (extension, _, gd) {
|
||||
const object = extension
|
||||
.addObject(
|
||||
'TileMap',
|
||||
_('Tilemap'),
|
||||
_('External Tilemap (Tiled/LDtk)'),
|
||||
_(
|
||||
'Displays a tiled-based map, made with the Tiled editor (https://www.mapeditor.org/) or the LDtk editor (https://ldtk.io/).'
|
||||
),
|
||||
@@ -591,6 +591,223 @@ const defineTileMap = function (extension, _, gd) {
|
||||
.setFunctionName('setHeight');
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {gd.PlatformExtension} extension
|
||||
* @param {(translationSource: string) => string} _
|
||||
* @param {GDNamespace} gd
|
||||
*/
|
||||
const defineSimpleTileMap = function (extension, _, gd) {
|
||||
var objectSimpleTileMap = new gd.ObjectJsImplementation();
|
||||
objectSimpleTileMap.updateProperty = function (
|
||||
objectContent,
|
||||
propertyName,
|
||||
newValue
|
||||
) {
|
||||
if (propertyName === 'atlasImage') {
|
||||
objectContent.atlasImage = newValue;
|
||||
return true;
|
||||
}
|
||||
if (propertyName === 'columnCount') {
|
||||
objectContent.columnCount = parseFloat(newValue);
|
||||
return true;
|
||||
}
|
||||
if (propertyName === 'rowCount') {
|
||||
objectContent.rowCount = parseFloat(newValue);
|
||||
return true;
|
||||
}
|
||||
if (propertyName === 'tileSize') {
|
||||
objectContent.tileSize = parseFloat(newValue);
|
||||
return true;
|
||||
}
|
||||
if (propertyName === 'tilesWithHitBox') {
|
||||
objectContent.tilesWithHitBox = newValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
objectSimpleTileMap.getProperties = function (objectContent) {
|
||||
var objectProperties = new gd.MapStringPropertyDescriptor();
|
||||
|
||||
objectProperties.set(
|
||||
'columnCount',
|
||||
new gd.PropertyDescriptor((objectContent.columnCount || 4).toString())
|
||||
.setType('number')
|
||||
.setLabel(_('Columns'))
|
||||
.setDescription(_('Number of columns.'))
|
||||
.setHidden(true)
|
||||
);
|
||||
objectProperties.set(
|
||||
'rowCount',
|
||||
new gd.PropertyDescriptor((objectContent.rowCount || 4).toString())
|
||||
.setType('number')
|
||||
.setLabel(_('Rows'))
|
||||
.setDescription(_('Number of rows.'))
|
||||
.setHidden(true)
|
||||
);
|
||||
objectProperties.set(
|
||||
'tileSize',
|
||||
new gd.PropertyDescriptor((objectContent.tileSize || 8).toString())
|
||||
.setType('number')
|
||||
.setLabel(_('Tile size'))
|
||||
.setDescription(_('Tile size in pixels.'))
|
||||
);
|
||||
objectProperties.set(
|
||||
'tilesWithHitBox',
|
||||
new gd.PropertyDescriptor(objectContent.tilesWithHitBox || '')
|
||||
.setType('string')
|
||||
.setLabel(_('Tile ids with hit box'))
|
||||
.setDescription(
|
||||
_('The list of tile ids with a hit box (separated by commas).')
|
||||
)
|
||||
.setHidden(true)
|
||||
);
|
||||
|
||||
objectProperties.set(
|
||||
'atlasImage',
|
||||
new gd.PropertyDescriptor(objectContent.atlasImage)
|
||||
.setType('resource')
|
||||
.addExtraInfo('image')
|
||||
.setLabel(_('Atlas image'))
|
||||
.setDescription(_('The Atlas image containing the tileset.'))
|
||||
);
|
||||
|
||||
return objectProperties;
|
||||
};
|
||||
objectSimpleTileMap.setRawJSONContent(
|
||||
JSON.stringify({
|
||||
atlasImage: '',
|
||||
rowCount: 1,
|
||||
columnCount: 1,
|
||||
tileSize: 8,
|
||||
tilesWithHitBox: '',
|
||||
})
|
||||
);
|
||||
|
||||
objectSimpleTileMap.updateInitialInstanceProperty = function (
|
||||
instance,
|
||||
propertyName,
|
||||
newValue
|
||||
) {
|
||||
if (propertyName === 'tilemap') {
|
||||
instance.setRawStringProperty('tilemap', newValue);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
objectSimpleTileMap.getInitialInstanceProperties = function (
|
||||
objectContent,
|
||||
instance
|
||||
) {
|
||||
var instanceProperties = new gd.MapStringPropertyDescriptor();
|
||||
|
||||
instanceProperties
|
||||
.getOrCreate('tilemap')
|
||||
.setValue(instance.getRawStringProperty('tileMap'))
|
||||
.setType('string')
|
||||
.setLabel('Tilemap')
|
||||
.setHidden(true);
|
||||
|
||||
return instanceProperties;
|
||||
};
|
||||
|
||||
const object = extension
|
||||
.addObject(
|
||||
'SimpleTileMap',
|
||||
_('Tilemap'),
|
||||
_('Displays a tiled-based map.'),
|
||||
'JsPlatform/Extensions/tile_map.svg',
|
||||
objectSimpleTileMap
|
||||
)
|
||||
.setCategoryFullName(_('General'))
|
||||
.addDefaultBehavior('EffectCapability::EffectBehavior')
|
||||
.addDefaultBehavior('ResizableCapability::ResizableBehavior')
|
||||
.addDefaultBehavior('ScalableCapability::ScalableBehavior')
|
||||
.addDefaultBehavior('OpacityCapability::OpacityBehavior')
|
||||
.setIncludeFile('Extensions/TileMap/simpletilemapruntimeobject.js')
|
||||
.addIncludeFile('Extensions/TileMap/TileMapRuntimeManager.js')
|
||||
.addIncludeFile('Extensions/TileMap/tilemapruntimeobject-pixi-renderer.js')
|
||||
.addIncludeFile('Extensions/TileMap/pixi-tilemap/dist/pixi-tilemap.umd.js')
|
||||
.addIncludeFile('Extensions/TileMap/collision/TransformedTileMap.js')
|
||||
.addIncludeFile('Extensions/TileMap/pako/dist/pako.min.js')
|
||||
.addIncludeFile('Extensions/TileMap/helper/TileMapHelper.js');
|
||||
|
||||
object
|
||||
.addCondition(
|
||||
'Tile',
|
||||
_('Tile id'),
|
||||
_('Check tile id at coordinates.'),
|
||||
_('The tile id at coordinates _PARAM3_;_PARAM3_'),
|
||||
'',
|
||||
'JsPlatform/Extensions/tile_map.svg',
|
||||
'JsPlatform/Extensions/tile_map.svg'
|
||||
)
|
||||
.addParameter('object', _('Tilemap'), '', false)
|
||||
.useStandardRelationalOperatorParameters(
|
||||
'number',
|
||||
gd.ParameterOptions.makeNewOptions()
|
||||
)
|
||||
.addParameter('number', _('X'), '', false)
|
||||
.addParameter('number', _('Y'), '', false)
|
||||
.setFunctionName('getTileAt');
|
||||
|
||||
object
|
||||
.addExpression(
|
||||
'Tile',
|
||||
_('Tile id'),
|
||||
_('Check tile id at coordinates.'),
|
||||
'',
|
||||
'JsPlatform/Extensions/tile_map.svg'
|
||||
)
|
||||
.addParameter('object', _('Tilemap'), '', false)
|
||||
.addParameter('number', _('X'), '', false)
|
||||
.addParameter('number', _('Y'), '', false)
|
||||
.setFunctionName('getTileAt');
|
||||
|
||||
object
|
||||
.addAction(
|
||||
'Tile',
|
||||
_('Tile id'),
|
||||
_('Set tile id at coordinates.'),
|
||||
_(
|
||||
'tile at coordinates _PARAM3_;_PARAM4_ (flip horizontally _PARAM5_, flip vertically _PARAM6_)'
|
||||
),
|
||||
'',
|
||||
'JsPlatform/Extensions/tile_map.svg',
|
||||
'JsPlatform/Extensions/tile_map.svg'
|
||||
)
|
||||
.addParameter('object', _('Tilemap'), '', false)
|
||||
.useStandardOperatorParameters(
|
||||
'number',
|
||||
gd.ParameterOptions.makeNewOptions()
|
||||
)
|
||||
.addParameter('number', _('X'), '', false)
|
||||
.addParameter('number', _('Y'), '', false)
|
||||
.addParameter('yesorno', _('Flip horizontally'), '', false)
|
||||
.setDefaultValue('false')
|
||||
.addParameter('yesorno', _('Flip vertically'), '', false)
|
||||
.setDefaultValue('false')
|
||||
.setFunctionName('setTileAt')
|
||||
.setGetter('getTileAt');
|
||||
|
||||
object
|
||||
.addAction(
|
||||
'removeTileAt',
|
||||
_('Remove tile'),
|
||||
_('Remove the tile at the given coordinates'),
|
||||
_('Remove tile at coordinates _PARAM1_;_PARAM2_'),
|
||||
'',
|
||||
'JsPlatform/Extensions/tile_map.svg',
|
||||
'JsPlatform/Extensions/tile_map.svg'
|
||||
)
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.addParameter('number', _('X'), '', false)
|
||||
.addParameter('number', _('Y'), '', false)
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('removeTileAt');
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {gd.PlatformExtension} extension
|
||||
* @param {(translationSource: string) => string} _
|
||||
@@ -769,7 +986,7 @@ const defineCollisionMask = function (extension, _, gd) {
|
||||
const object = extension
|
||||
.addObject(
|
||||
'CollisionMask',
|
||||
_('Tilemap collision mask'),
|
||||
_('External Tilemap (Tiled/LDtk) collision mask'),
|
||||
_('Invisible object handling collisions with parts of a tilemap.'),
|
||||
'JsPlatform/Extensions/tile_map_collision_mask32.svg',
|
||||
collisionMaskObject
|
||||
@@ -1035,6 +1252,7 @@ module.exports = {
|
||||
.setIcon('JsPlatform/Extensions/tile_map.svg');
|
||||
|
||||
defineTileMap(extension, _, gd);
|
||||
defineSimpleTileMap(extension, _, gd);
|
||||
defineCollisionMask(extension, _, gd);
|
||||
|
||||
return extension;
|
||||
@@ -1091,6 +1309,12 @@ module.exports = {
|
||||
helpPagePath: '/objects/tilemap',
|
||||
})
|
||||
);
|
||||
objectsEditorService.registerEditorConfiguration(
|
||||
'TileMap::SimpleTileMap',
|
||||
objectsEditorService.getDefaultObjectJsImplementationPropertiesEditor({
|
||||
helpPagePath: '/objects/tilemap',
|
||||
})
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Register renderers for instance of objects on the scene editor.
|
||||
@@ -1411,7 +1635,355 @@ module.exports = {
|
||||
);
|
||||
|
||||
/**
|
||||
* Renderer for instances of TileMap inside the IDE.
|
||||
* Renderer for instances of SimpleTileMap inside the IDE.
|
||||
*/
|
||||
class RenderedSimpleTileMapInstance extends RenderedInstance {
|
||||
_placeholderPixiObject = new PIXI.Text(
|
||||
'Select the instance\nand start painting',
|
||||
new PIXI.TextStyle({
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 20,
|
||||
align: 'center',
|
||||
padding: 5,
|
||||
})
|
||||
);
|
||||
|
||||
constructor(
|
||||
project,
|
||||
instance,
|
||||
associatedObjectConfiguration,
|
||||
pixiContainer,
|
||||
pixiResourcesLoader
|
||||
) {
|
||||
super(
|
||||
project,
|
||||
instance,
|
||||
associatedObjectConfiguration,
|
||||
pixiContainer,
|
||||
pixiResourcesLoader
|
||||
);
|
||||
|
||||
// This setting allows tile maps with more than 16K tiles.
|
||||
Tilemap.settings.use32bitIndex = true;
|
||||
|
||||
this.tileMapPixiObject = new Tilemap.CompositeTilemap();
|
||||
this._pixiObject = new PIXI.Container();
|
||||
this._pixiObject.addChild(this.tileMapPixiObject);
|
||||
this._editableTileMap = null;
|
||||
|
||||
// Implement `containsPoint` so that we can set `interactive` to true and
|
||||
// the Tilemap will properly emit events when hovered/clicked.
|
||||
// By default, this is not implemented in pixi-tilemap.
|
||||
this._pixiObject.containsPoint = (position) => {
|
||||
// Turns the world position to the local object coordinates
|
||||
const localPosition = new PIXI.Point();
|
||||
if (this.tileMapPixiObject.visible) {
|
||||
this.tileMapPixiObject.worldTransform.applyInverse(
|
||||
position,
|
||||
localPosition
|
||||
);
|
||||
} else {
|
||||
this._placeholderPixiObject.worldTransform.applyInverse(
|
||||
position,
|
||||
localPosition
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
localPosition.x >= 0 &&
|
||||
localPosition.x < this.width &&
|
||||
localPosition.y >= 0 &&
|
||||
localPosition.y < this.height
|
||||
);
|
||||
};
|
||||
this._placeholderPixiObject.interactive = true;
|
||||
this._placeholderPixiObject.anchor.x = 0.5;
|
||||
this._placeholderPixiObject.anchor.y = 0.5;
|
||||
this._pixiObject.addChild(this._placeholderPixiObject);
|
||||
this._pixiContainer.addChild(this._pixiObject);
|
||||
this.width = 48;
|
||||
this.height = 48;
|
||||
this._objectName = instance.getObjectName();
|
||||
this.update();
|
||||
this.updateTileMap();
|
||||
}
|
||||
|
||||
onRemovedFromScene() {
|
||||
super.onRemovedFromScene();
|
||||
// Keep textures because they are shared by all tile maps.
|
||||
this._pixiObject.destroy(false);
|
||||
}
|
||||
|
||||
onLoadingError() {
|
||||
this.errorPixiObject =
|
||||
this.errorPixiObject ||
|
||||
new PIXI.Sprite(this._pixiResourcesLoader.getInvalidPIXITexture());
|
||||
this._pixiContainer.addChild(this.errorPixiObject);
|
||||
this._pixiObject = this.errorPixiObject;
|
||||
}
|
||||
|
||||
onLoadingSuccess() {
|
||||
if (this.errorPixiObject) {
|
||||
this._pixiContainer.removeChild(this.errorPixiObject);
|
||||
this.errorPixiObject = null;
|
||||
this._pixiObject = this.tileMapPixiObject;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the path to the thumbnail of the specified object.
|
||||
*/
|
||||
static getThumbnail(project, resourcesLoader, objectConfiguration) {
|
||||
const atlasImageResourceName = objectConfiguration
|
||||
.getProperties()
|
||||
.get('atlasImage')
|
||||
.getValue();
|
||||
return resourcesLoader.getResourceFullUrl(
|
||||
project,
|
||||
atlasImageResourceName,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
getEditableTileMap() {
|
||||
return this._editableTileMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to reload the Tilemap
|
||||
*/
|
||||
updateTileMap() {
|
||||
const atlasImageResourceName = this._associatedObjectConfiguration
|
||||
.getProperties()
|
||||
.get('atlasImage')
|
||||
.getValue();
|
||||
const tilemapAsJSObject = JSON.parse(
|
||||
this._instance.getRawStringProperty('tilemap') || '{}'
|
||||
);
|
||||
|
||||
const tileSize = parseInt(
|
||||
this._associatedObjectConfiguration
|
||||
.getProperties()
|
||||
.get('tileSize')
|
||||
.getValue(),
|
||||
10
|
||||
);
|
||||
const columnCount = parseInt(
|
||||
this._associatedObjectConfiguration
|
||||
.getProperties()
|
||||
.get('columnCount')
|
||||
.getValue(),
|
||||
10
|
||||
);
|
||||
const rowCount = parseInt(
|
||||
this._associatedObjectConfiguration
|
||||
.getProperties()
|
||||
.get('rowCount')
|
||||
.getValue(),
|
||||
10
|
||||
);
|
||||
|
||||
const atlasTexture = this._pixiResourcesLoader.getPIXITexture(
|
||||
this._project,
|
||||
atlasImageResourceName
|
||||
);
|
||||
|
||||
const loadTileMap = () => {
|
||||
/** @type {TileMapHelper.TileMapManager} */
|
||||
const manager = TilemapHelper.TileMapManager.getManager(
|
||||
this._project
|
||||
);
|
||||
manager.getOrLoadSimpleTileMap(
|
||||
tilemapAsJSObject,
|
||||
this._objectName,
|
||||
tileSize,
|
||||
columnCount,
|
||||
rowCount,
|
||||
(tileMap) => {
|
||||
if (!tileMap) {
|
||||
this.onLoadingError();
|
||||
console.error('Could not parse tilemap.');
|
||||
return;
|
||||
}
|
||||
|
||||
this._editableTileMap = tileMap;
|
||||
|
||||
manager.getOrLoadSimpleTileMapTextureCache(
|
||||
(textureName) =>
|
||||
this._pixiResourcesLoader.getPIXITexture(
|
||||
this._project,
|
||||
textureName
|
||||
),
|
||||
atlasImageResourceName,
|
||||
tileSize,
|
||||
columnCount,
|
||||
rowCount,
|
||||
(
|
||||
/** @type {TileMapHelper.TileTextureCache | null} */
|
||||
textureCache
|
||||
) => {
|
||||
this.onLoadingSuccess();
|
||||
if (!this._editableTileMap) return;
|
||||
|
||||
this.width = this._editableTileMap.getWidth();
|
||||
this.height = this._editableTileMap.getHeight();
|
||||
TilemapHelper.PixiTileMapHelper.updatePixiTileMap(
|
||||
this.tileMapPixiObject,
|
||||
this._editableTileMap,
|
||||
textureCache,
|
||||
'all', // No notion of visibility on simple tile maps.
|
||||
0 // Only one layer is used on simple tile maps.
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (atlasTexture.valid) {
|
||||
loadTileMap();
|
||||
} else {
|
||||
// Wait for the atlas image to load.
|
||||
atlasTexture.once('update', () => {
|
||||
loadTileMap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updatePixiTileMap() {
|
||||
const atlasImageResourceName = this._associatedObjectConfiguration
|
||||
.getProperties()
|
||||
.get('atlasImage')
|
||||
.getValue();
|
||||
|
||||
const tileSize = parseInt(
|
||||
this._associatedObjectConfiguration
|
||||
.getProperties()
|
||||
.get('tileSize')
|
||||
.getValue(),
|
||||
10
|
||||
);
|
||||
const columnCount = parseInt(
|
||||
this._associatedObjectConfiguration
|
||||
.getProperties()
|
||||
.get('columnCount')
|
||||
.getValue(),
|
||||
10
|
||||
);
|
||||
const rowCount = parseInt(
|
||||
this._associatedObjectConfiguration
|
||||
.getProperties()
|
||||
.get('rowCount')
|
||||
.getValue(),
|
||||
10
|
||||
);
|
||||
/** @type {TileMapHelper.TileMapManager} */
|
||||
const manager = TilemapHelper.TileMapManager.getManager(this._project);
|
||||
|
||||
manager.getOrLoadSimpleTileMapTextureCache(
|
||||
(textureName) =>
|
||||
this._pixiResourcesLoader.getPIXITexture(
|
||||
this._project,
|
||||
textureName
|
||||
),
|
||||
atlasImageResourceName,
|
||||
tileSize,
|
||||
columnCount,
|
||||
rowCount,
|
||||
(
|
||||
/** @type {TileMapHelper.TileTextureCache | null} */
|
||||
textureCache
|
||||
) => {
|
||||
this.onLoadingSuccess();
|
||||
if (!this._editableTileMap) return;
|
||||
|
||||
this.width = this._editableTileMap.getWidth();
|
||||
this.height = this._editableTileMap.getHeight();
|
||||
TilemapHelper.PixiTileMapHelper.updatePixiTileMap(
|
||||
this.tileMapPixiObject,
|
||||
this._editableTileMap,
|
||||
textureCache,
|
||||
'all',
|
||||
0
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called to update the PIXI object on the scene editor
|
||||
*/
|
||||
update() {
|
||||
const isTileMapEmpty = this._editableTileMap
|
||||
? this._editableTileMap.isEmpty()
|
||||
: false;
|
||||
let objectToChange;
|
||||
if (isTileMapEmpty) {
|
||||
this._placeholderPixiObject.visible = true;
|
||||
this.tileMapPixiObject.visible = false;
|
||||
objectToChange = this._placeholderPixiObject;
|
||||
} else {
|
||||
this._placeholderPixiObject.visible = false;
|
||||
this.tileMapPixiObject.visible = true;
|
||||
objectToChange = this.tileMapPixiObject;
|
||||
}
|
||||
|
||||
if (!isTileMapEmpty) {
|
||||
// Don't change size of placeholder object.
|
||||
if (this._instance.hasCustomSize()) {
|
||||
objectToChange.scale.x = this.getCustomWidth() / this.width;
|
||||
objectToChange.scale.y = this.getCustomHeight() / this.height;
|
||||
} else {
|
||||
objectToChange.scale.x = 1;
|
||||
objectToChange.scale.y = 1;
|
||||
}
|
||||
|
||||
// Place the center of rotation in the center of the object. Because pivot position in Pixi
|
||||
// is in the **local coordinates of the object**, we need to find back the original width
|
||||
// and height of the object before scaling (then divide by 2 to find the center)
|
||||
const originalWidth = this.width;
|
||||
const originalHeight = this.height;
|
||||
objectToChange.pivot.x = originalWidth / 2;
|
||||
objectToChange.pivot.y = originalHeight / 2;
|
||||
}
|
||||
// Modifying the pivot position also has an impact on the transform. The instance (X,Y) position
|
||||
// of this object refers to the top-left point, but now in Pixi, as we changed the pivot, the Pixi
|
||||
// object (X,Y) position refers to the center. So we add an offset to convert from top-left to center.
|
||||
objectToChange.x =
|
||||
this._instance.getX() +
|
||||
objectToChange.pivot.x * objectToChange.scale.x;
|
||||
objectToChange.y =
|
||||
this._instance.getY() +
|
||||
objectToChange.pivot.y * objectToChange.scale.y;
|
||||
|
||||
// Rotation works as intended because we put the pivot in the center
|
||||
objectToChange.rotation = RenderedInstance.toRad(
|
||||
this._instance.getAngle()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the width of the instance, when it's not resized.
|
||||
*/
|
||||
getDefaultWidth() {
|
||||
return this.width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the height of the instance, when it's not resized.
|
||||
*/
|
||||
getDefaultHeight() {
|
||||
return this.height;
|
||||
}
|
||||
}
|
||||
|
||||
objectsRenderingService.registerInstanceRenderer(
|
||||
'TileMap::SimpleTileMap',
|
||||
RenderedSimpleTileMapInstance
|
||||
);
|
||||
|
||||
/**
|
||||
* Renderer for instances of TileMap collision mask inside the IDE.
|
||||
*/
|
||||
class RenderedCollisionMaskInstance extends RenderedInstance {
|
||||
constructor(
|
||||
|
@@ -79,6 +79,24 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
|
||||
getOrLoadSimpleTileMap(
|
||||
tileMapAsJsObject: object,
|
||||
objectName: string,
|
||||
tileSize: number,
|
||||
columnCount: number,
|
||||
rowCount: number,
|
||||
callback: (tileMap: TileMapHelper.EditableTileMap) => void
|
||||
): void {
|
||||
this._manager.getOrLoadSimpleTileMap(
|
||||
tileMapAsJsObject,
|
||||
objectName,
|
||||
tileSize,
|
||||
columnCount,
|
||||
rowCount,
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param getTexture The method that loads the atlas image file in memory.
|
||||
* @param atlasImageResourceName The resource name of the atlas image.
|
||||
@@ -106,6 +124,32 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param getTexture The method that loads the atlas image file in memory.
|
||||
* @param atlasImageResourceName The resource name of the atlas image.
|
||||
* @param tileSize
|
||||
* @param columnCount
|
||||
* @param rowCount
|
||||
* @param callback A function called when the tiles textures are split.
|
||||
*/
|
||||
getOrLoadSimpleTileMapTextureCache(
|
||||
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>,
|
||||
atlasImageResourceName: string,
|
||||
tileSize: number,
|
||||
columnCount: number,
|
||||
rowCount: number,
|
||||
callback: (textureCache: TileMapHelper.TileTextureCache | null) => void
|
||||
): void {
|
||||
this._manager.getOrLoadSimpleTileMapTextureCache(
|
||||
getTexture,
|
||||
atlasImageResourceName,
|
||||
tileSize,
|
||||
columnCount,
|
||||
rowCount,
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse both JSON and set the content of the tile set in the right
|
||||
* attribute in the tile map to merge both parsed data.
|
||||
|
@@ -30,7 +30,7 @@ namespace gdjs {
|
||||
*/
|
||||
_transformationUpToDateCount: integer = 1;
|
||||
/**
|
||||
* An reusable Point to avoid allocations.
|
||||
* A reusable Point to avoid allocations.
|
||||
*/
|
||||
private static readonly workingPoint: FloatPoint = [0, 0];
|
||||
|
||||
@@ -41,14 +41,28 @@ namespace gdjs {
|
||||
this._source = source;
|
||||
this.tag = tag;
|
||||
this._layers = new Map<integer, TransformedCollisionTileMapLayer>();
|
||||
for (const sourceLayer of source.getLayers()) {
|
||||
this._buildLayersFromTileMap(source, this._layers);
|
||||
}
|
||||
|
||||
updateFromTileMap(tileMap: TileMapHelper.EditableTileMap) {
|
||||
this._source = tileMap;
|
||||
this._layers = new Map<integer, TransformedCollisionTileMapLayer>();
|
||||
|
||||
this._buildLayersFromTileMap(tileMap, this._layers);
|
||||
}
|
||||
|
||||
_buildLayersFromTileMap(
|
||||
tileMap: TileMapHelper.EditableTileMap,
|
||||
layers: Map<integer, TransformedCollisionTileMapLayer>
|
||||
) {
|
||||
for (const sourceLayer of tileMap.getLayers()) {
|
||||
// TODO A visitor could be used to avoid a cast.
|
||||
if (!(sourceLayer instanceof TileMapHelper.EditableTileMapLayer)) {
|
||||
// TODO Collision mask for object layers is not handled.
|
||||
continue;
|
||||
}
|
||||
const tileLayer = sourceLayer as TileMapHelper.EditableTileMapLayer;
|
||||
this._layers.set(
|
||||
layers.set(
|
||||
tileLayer.id,
|
||||
new TransformedCollisionTileMapLayer(this, tileLayer)
|
||||
);
|
||||
@@ -64,7 +78,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param transformation the transformation from the time map coordinate
|
||||
* @param transformation the transformation from the tile map coordinate
|
||||
* (in pixels) to the scene coordinate (in pixels).
|
||||
*/
|
||||
setTransformation(transformation: gdjs.AffineTransformation) {
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -22,11 +22,11 @@ export declare class EditableTileMap {
|
||||
/**
|
||||
* The number of tile columns in the map.
|
||||
*/
|
||||
private readonly dimX;
|
||||
private dimX;
|
||||
/**
|
||||
* The number of tile rows in the map.
|
||||
*/
|
||||
private readonly dimY;
|
||||
private dimY;
|
||||
/**
|
||||
* @param tileWidth The width of a tile.
|
||||
* @param tileHeight The height of a tile.
|
||||
@@ -41,6 +41,27 @@ export declare class EditableTileMap {
|
||||
dimY: integer,
|
||||
tileSet: Map<integer, TileDefinition>
|
||||
);
|
||||
/**
|
||||
* Loads EditableTileMap from serialized data.
|
||||
* Uses object configuration as the source of truth as the serialized data
|
||||
* might contain expired data (if the tile set configuration has changed and
|
||||
* the serialized data was not updated).
|
||||
* @param editableTileMapAsJsObject Serialized editable tile map object
|
||||
* @param objectConfiguration
|
||||
*/
|
||||
static from(
|
||||
editableTileMapAsJsObject: any,
|
||||
{
|
||||
tileSize,
|
||||
tileSetColumnCount,
|
||||
tileSetRowCount,
|
||||
}: {
|
||||
tileSize: number;
|
||||
tileSetColumnCount: number;
|
||||
tileSetRowCount: number;
|
||||
}
|
||||
): EditableTileMap;
|
||||
toJSObject(): Object;
|
||||
/**
|
||||
* @returns The tile map width in pixels.
|
||||
*/
|
||||
@@ -65,6 +86,14 @@ export declare class EditableTileMap {
|
||||
* @returns The number of tile rows in the map.
|
||||
*/
|
||||
getDimensionY(): integer;
|
||||
/**
|
||||
* @param dim The number of tile columns in the map.
|
||||
*/
|
||||
setDimensionX(dim: integer): void;
|
||||
/**
|
||||
* @param dim The number of tile rows in the map.
|
||||
*/
|
||||
setDimensionY(dim: integer): void;
|
||||
/**
|
||||
* @param tileId The tile identifier
|
||||
* @returns The tile definition form the tile set.
|
||||
@@ -79,6 +108,11 @@ export declare class EditableTileMap {
|
||||
* @returns The new layer.
|
||||
*/
|
||||
addTileLayer(id: integer): EditableTileMapLayer;
|
||||
/**
|
||||
* @param layer the new layer to set.
|
||||
*/
|
||||
setTileLayer(layer: EditableTileMapLayer): void;
|
||||
getTileLayer(id: integer): EditableTileMapLayer | null;
|
||||
/**
|
||||
* @param id The identifier of the new layer.
|
||||
* @returns The new layer.
|
||||
@@ -108,6 +142,10 @@ export declare class EditableTileMap {
|
||||
* @param resourceName The name of the resource
|
||||
*/
|
||||
setBackgroundResourceName(resourceName: string): void;
|
||||
/**
|
||||
* Returns true if all layers contain no defined tiled.
|
||||
*/
|
||||
isEmpty(): boolean;
|
||||
}
|
||||
/**
|
||||
* A tile map layer.
|
||||
@@ -128,10 +166,12 @@ declare abstract class AbstractEditableLayer {
|
||||
*/
|
||||
constructor(tileMap: EditableTileMap, id: integer);
|
||||
setVisible(visible: boolean): void;
|
||||
toJSObject(): Object;
|
||||
/**
|
||||
* @returns true if the layer is visible.
|
||||
*/
|
||||
isVisible(): boolean;
|
||||
isEmpty(): boolean;
|
||||
}
|
||||
/**
|
||||
* A layer where tiles are placed with pixel coordinates.
|
||||
@@ -144,6 +184,7 @@ export declare class EditableObjectLayer extends AbstractEditableLayer {
|
||||
*/
|
||||
constructor(tileMap: EditableTileMap, id: integer);
|
||||
add(object: TileObject): void;
|
||||
isEmpty(): boolean;
|
||||
}
|
||||
/**
|
||||
* A tile that is placed with pixel coordinates.
|
||||
@@ -191,13 +232,20 @@ export declare class TileObject {
|
||||
* A tile map layer with tile organized in grid.
|
||||
*/
|
||||
export declare class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
private readonly _tiles;
|
||||
private _tiles;
|
||||
private _alpha;
|
||||
/**
|
||||
* @param tileMap The layer tile map.
|
||||
* @param id The layer identifier.
|
||||
*/
|
||||
constructor(tileMap: EditableTileMap, id: integer);
|
||||
buildEmptyLayer(dimensionX: number, dimensionY: number): void;
|
||||
static from(
|
||||
editableTileMapLayerAsJsObject: any,
|
||||
tileMap: EditableTileMap,
|
||||
isTileIdValid: (tileId: number) => boolean
|
||||
): EditableTileMapLayer;
|
||||
toJSObject(): Object;
|
||||
/**
|
||||
* The opacity (between 0-1) of the layer
|
||||
*/
|
||||
@@ -206,12 +254,56 @@ export declare class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
* @param alpha The opacity between 0-1
|
||||
*/
|
||||
setAlpha(alpha: float): void;
|
||||
isEmpty(): boolean;
|
||||
reduceDimensions(
|
||||
columnsToPop: number,
|
||||
columnsToShift: number,
|
||||
rowsToPop: number,
|
||||
rowsToShift: number
|
||||
): void;
|
||||
increaseDimensions(
|
||||
columnsToAppend: number,
|
||||
columnsToUnshift: number,
|
||||
rowsToAppend: number,
|
||||
rowsToUnshift: number
|
||||
): void;
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
* @param tileId The tile.
|
||||
* @param options Flipping options.
|
||||
*/
|
||||
setTile(x: integer, y: integer, tileId: integer): void;
|
||||
setTile(
|
||||
x: integer,
|
||||
y: integer,
|
||||
tileId: integer,
|
||||
options?:
|
||||
| {
|
||||
flipVertically: boolean;
|
||||
flipHorizontally: boolean;
|
||||
flipDiagonally: boolean;
|
||||
}
|
||||
| undefined
|
||||
):
|
||||
| {
|
||||
unshiftedRows: number;
|
||||
unshiftedColumns: number;
|
||||
appendedRows: number;
|
||||
appendedColumns: number;
|
||||
}
|
||||
| undefined;
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
* @param tileGID The tile GID.
|
||||
*/
|
||||
setTileGID(x: integer, y: integer, tileGID: integer): void;
|
||||
trimEmptyColumnsAndRow(): {
|
||||
shiftedRows: number;
|
||||
shiftedColumns: number;
|
||||
poppedRows: number;
|
||||
poppedColumns: number;
|
||||
};
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
|
File diff suppressed because one or more lines are too long
@@ -43,6 +43,14 @@ export declare class TileMapManager {
|
||||
pako: any,
|
||||
callback: (tileMap: EditableTileMap | null) => void
|
||||
): void;
|
||||
getOrLoadSimpleTileMap(
|
||||
tileMapAsJsObject: object,
|
||||
objectName: string,
|
||||
tileSize: number,
|
||||
tileSetColumnCount: number,
|
||||
tileSetRowCount: number,
|
||||
callback: (tileMap: EditableTileMap) => void
|
||||
): void;
|
||||
/**
|
||||
* @param loadTileMap The method that loads the Tiled JSON file in memory.
|
||||
* @param getTexture The method that loads the atlas image file in memory.
|
||||
@@ -65,6 +73,22 @@ export declare class TileMapManager {
|
||||
levelIndex: number,
|
||||
callback: (textureCache: TileTextureCache | null) => void
|
||||
): void;
|
||||
/**
|
||||
* @param getTexture The method that loads the atlas image file in memory.
|
||||
* @param atlasImageResourceName The resource name of the atlas image.
|
||||
* @param tileSize
|
||||
* @param columnCount
|
||||
* @param rowCount
|
||||
* @param callback A function called when the tiles textures are split.
|
||||
*/
|
||||
getOrLoadSimpleTileMapTextureCache(
|
||||
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>,
|
||||
atlasImageResourceName: string,
|
||||
tileSize: number,
|
||||
columnCount: number,
|
||||
rowCount: number,
|
||||
callback: (textureCache: TileTextureCache | null) => void
|
||||
): void;
|
||||
clearCaches(): void;
|
||||
}
|
||||
//# sourceMappingURL=TileMapManager.d.ts.map
|
||||
|
@@ -1 +1 @@
|
||||
{"version":3,"file":"TileMapManager.d.ts","sourceRoot":"","sources":["../../src/render/TileMapManager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEhE;;;;;;;GAOG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,aAAa,CAAiC;IACtD,OAAO,CAAC,mBAAmB,CAAkC;;IAO7D;;;OAGG;IACH,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,cAAc;IAWzD;;;OAGG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,GAAG,kBAAkB,GAAG,IAAI;IAwBrD;;;;;;;OAOG;IACH,gBAAgB,CACd,WAAW,EAAE,CACX,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,KAAK,IAAI,KAC9D,IAAI,EACT,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,GAAG,EACT,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,KAAK,IAAI,GAClD,IAAI;IAiCP;;;;;;;;OAQG;IACH,qBAAqB,CACnB,WAAW,EAAE,CACX,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,KAAK,IAAI,KAC9D,IAAI,EACT,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EACpE,sBAAsB,EAAE,MAAM,EAC9B,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,GACxD,IAAI;IAwCP,WAAW,IAAI,IAAI;CAIpB"}
|
||||
{"version":3,"file":"TileMapManager.d.ts","sourceRoot":"","sources":["../../src/render/TileMapManager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEhE;;;;;;;GAOG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,aAAa,CAAiC;IACtD,OAAO,CAAC,mBAAmB,CAAkC;;IAO7D;;;OAGG;IACH,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,cAAc;IAWzD;;;OAGG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,GAAG,kBAAkB,GAAG,IAAI;IAwBrD;;;;;;;OAOG;IACH,gBAAgB,CACd,WAAW,EAAE,CACX,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,KAAK,IAAI,KAC9D,IAAI,EACT,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,GAAG,EACT,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,KAAK,IAAI,GAClD,IAAI;IAiCP,sBAAsB,CACpB,iBAAiB,EAAE,MAAM,EACzB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,kBAAkB,EAAE,MAAM,EAC1B,eAAe,EAAE,MAAM,EAGvB,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,GAC3C,IAAI;IAYP;;;;;;;;OAQG;IACH,qBAAqB,CACnB,WAAW,EAAE,CACX,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,KAAK,IAAI,KAC9D,IAAI,EACT,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EACpE,sBAAsB,EAAE,MAAM,EAC9B,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,GACxD,IAAI;IAwCP;;;;;;;OAOG;IACH,kCAAkC,CAChC,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EACpE,sBAAsB,EAAE,MAAM,EAC9B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,GACxD,IAAI;IAqBP,WAAW,IAAI,IAAI;CAIpB"}
|
@@ -6,7 +6,7 @@ export declare namespace PixiTileMapHelper {
|
||||
/**
|
||||
* Split an atlas image into Pixi textures.
|
||||
*
|
||||
* @param tiledMap A tile map exported from Tiled.
|
||||
* @param tiledMap A tile map exported from Tiled or LDtk.
|
||||
* @param levelIndex The level of the tile map to load from.
|
||||
* @param atlasTexture The texture containing the whole tile set.
|
||||
* @param getTexture A getter to load a texture. Used if atlasTexture is not specified.
|
||||
@@ -18,6 +18,21 @@ export declare namespace PixiTileMapHelper {
|
||||
atlasTexture: PIXI.BaseTexture<PIXI.Resource> | null,
|
||||
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>
|
||||
): TileTextureCache | null;
|
||||
/**
|
||||
* Split an atlas image into Pixi textures.
|
||||
*
|
||||
* @param atlasTexture The texture containing the whole tile set.
|
||||
* @param columnCount The number of columns.
|
||||
* @param rowCount The number of rows.
|
||||
* @param tileSize The squared tile size.
|
||||
* @returns A textures cache.
|
||||
*/
|
||||
function parseSimpleTileMapAtlas(
|
||||
atlasTexture: PIXI.BaseTexture<PIXI.Resource>,
|
||||
columnCount: number,
|
||||
rowCount: number,
|
||||
tileSize: number
|
||||
): TileTextureCache;
|
||||
/**
|
||||
* Re-renders the tile map whenever its rendering settings have been changed
|
||||
*
|
||||
|
@@ -1 +1 @@
|
||||
{"version":3,"file":"TileMapPixiHelper.d.ts","sourceRoot":"","sources":["../../src/render/TileMapPixiHelper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAEL,eAAe,EAEhB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,yBAAiB,iBAAiB,CAAC;IACjC;;;;;;;;OAQG;IACH,SAAgB,UAAU,CACxB,OAAO,EAAE,kBAAkB,EAC3B,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,IAAI,EACpD,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GACnE,gBAAgB,GAAG,IAAI,CAuBzB;IAED;;;;;;;;;;;;OAYG;IACH,SAAgB,iBAAiB,CAC/B,kBAAkB,EAAE,GAAG,EACvB,OAAO,EAAE,eAAe,EACxB,YAAY,EAAE,gBAAgB,EAC9B,WAAW,EAAE,OAAO,GAAG,SAAS,GAAG,KAAK,EACxC,UAAU,EAAE,MAAM,GACjB,IAAI,CA0GN;IAED;;OAEG;IACH,SAAgB,uBAAuB,CACrC,YAAY,EAAE,IAAI,CAAC,QAAQ,EAC3B,OAAO,EAAE,eAAe,EACxB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,OAAO,EACpB,YAAY,EAAE,OAAO,EACrB,cAAc,EAAE,KAAK,EACrB,SAAS,EAAE,OAAO,EAClB,WAAW,EAAE,KAAK,GACjB,IAAI,CAgEN;CACF"}
|
||||
{"version":3,"file":"TileMapPixiHelper.d.ts","sourceRoot":"","sources":["../../src/render/TileMapPixiHelper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAEL,eAAe,EAEhB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,yBAAiB,iBAAiB,CAAC;IACjC;;;;;;;;OAQG;IACH,SAAgB,UAAU,CACxB,OAAO,EAAE,kBAAkB,EAC3B,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,IAAI,EACpD,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GACnE,gBAAgB,GAAG,IAAI,CAuBzB;IAED;;;;;;;;OAQG;IACH,SAAgB,uBAAuB,CACrC,YAAY,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EAC7C,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,gBAAgB,CAiBlB;IAED;;;;;;;;;;;;OAYG;IACH,SAAgB,iBAAiB,CAC/B,kBAAkB,EAAE,GAAG,EACvB,OAAO,EAAE,eAAe,EACxB,YAAY,EAAE,gBAAgB,EAC9B,WAAW,EAAE,OAAO,GAAG,SAAS,GAAG,KAAK,EACxC,UAAU,EAAE,MAAM,GACjB,IAAI,CA0GN;IAED;;OAEG;IACH,SAAgB,uBAAuB,CACrC,YAAY,EAAE,IAAI,CAAC,QAAQ,EAC3B,OAAO,EAAE,eAAe,EACxB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,OAAO,EACpB,YAAY,EAAE,OAAO,EACrB,cAAc,EAAE,KAAK,EACrB,SAAS,EAAE,OAAO,EAClB,WAAW,EAAE,KAAK,GACjB,IAAI,CAgEN;CACF"}
|
642
Extensions/TileMap/simpletilemapruntimeobject.ts
Normal file
642
Extensions/TileMap/simpletilemapruntimeobject.ts
Normal file
@@ -0,0 +1,642 @@
|
||||
/// <reference path="helper/TileMapHelper.d.ts" />
|
||||
namespace gdjs {
|
||||
export type SimpleTileMapObjectDataType = {
|
||||
content: {
|
||||
opacity: number;
|
||||
atlasImage: string;
|
||||
rowCount: number;
|
||||
columnCount: number;
|
||||
tileSize: number;
|
||||
tilesWithHitBox: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type SimpleTileMapObjectData = ObjectData &
|
||||
SimpleTileMapObjectDataType;
|
||||
|
||||
export type SimpleTileMapNetworkSyncDataType = {
|
||||
op: number;
|
||||
ai: string;
|
||||
wid: number;
|
||||
hei: number;
|
||||
// TODO: Support tilemap synchronization. Find an efficient way to send tiles changes.
|
||||
};
|
||||
|
||||
export type SimpleTileMapNetworkSyncData = ObjectNetworkSyncData &
|
||||
SimpleTileMapNetworkSyncDataType;
|
||||
|
||||
/**
|
||||
* Displays a SimpleTileMap object.
|
||||
*/
|
||||
export class SimpleTileMapRuntimeObject
|
||||
extends gdjs.RuntimeObject
|
||||
implements gdjs.Resizable, gdjs.Scalable, gdjs.OpacityHandler {
|
||||
/**
|
||||
* A reusable Point to avoid allocations.
|
||||
*/
|
||||
private static readonly workingPoint: FloatPoint = [0, 0];
|
||||
|
||||
_opacity: float;
|
||||
_atlasImage: string;
|
||||
_tileMapManager: gdjs.TileMap.TileMapRuntimeManager;
|
||||
_renderer: gdjs.TileMapRuntimeObjectPixiRenderer;
|
||||
_rowCount: number;
|
||||
_columnCount: number;
|
||||
_tileSize: number;
|
||||
_displayMode = 'all';
|
||||
_layerIndex = 0;
|
||||
_initialTileMapAsJsObject: object | null = null;
|
||||
_initialTilesWithHitBox: number[];
|
||||
_isTileMapDirty: boolean = false;
|
||||
_sceneToTileMapTransformation: gdjs.AffineTransformation = new gdjs.AffineTransformation();
|
||||
_collisionTileMap: gdjs.TileMap.TransformedCollisionTileMap | null = null;
|
||||
_hitBoxTag: string = 'collision';
|
||||
private _transformationIsUpToDate: boolean = false;
|
||||
|
||||
// TODO: Add a debug mode like for TileMapCollisionMaskRuntimeObject to draw?
|
||||
|
||||
constructor(
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer,
|
||||
objectData: SimpleTileMapObjectDataType
|
||||
) {
|
||||
super(instanceContainer, objectData);
|
||||
this._opacity = objectData.content.opacity;
|
||||
this._atlasImage = objectData.content.atlasImage;
|
||||
this._rowCount = objectData.content.rowCount;
|
||||
this._columnCount = objectData.content.columnCount;
|
||||
this._tileSize = objectData.content.tileSize;
|
||||
this._initialTilesWithHitBox = (objectData.content
|
||||
.tilesWithHitBox as string)
|
||||
.split(',')
|
||||
.filter((id) => !!id)
|
||||
.map((idAsString) => parseInt(idAsString, 10));
|
||||
this._tileMapManager = gdjs.TileMap.TileMapRuntimeManager.getManager(
|
||||
instanceContainer
|
||||
);
|
||||
this._renderer = new gdjs.TileMapRuntimeObjectRenderer(
|
||||
this,
|
||||
instanceContainer
|
||||
);
|
||||
|
||||
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
|
||||
this.onCreated();
|
||||
}
|
||||
|
||||
getRendererObject() {
|
||||
return this._renderer.getRendererObject();
|
||||
}
|
||||
|
||||
updatePreRender(instanceContainer: gdjs.RuntimeInstanceContainer): void {
|
||||
if (this._isTileMapDirty) {
|
||||
this._tileMapManager.getOrLoadSimpleTileMapTextureCache(
|
||||
(textureName) => {
|
||||
return (this.getInstanceContainer()
|
||||
.getGame()
|
||||
.getImageManager()
|
||||
.getPIXITexture(textureName) as unknown) as PIXI.BaseTexture<
|
||||
PIXI.Resource
|
||||
>;
|
||||
},
|
||||
this._atlasImage,
|
||||
this._tileSize,
|
||||
this._columnCount,
|
||||
this._rowCount,
|
||||
(textureCache: TileMapHelper.TileTextureCache | null) => {
|
||||
if (!textureCache) {
|
||||
// getOrLoadTextureCache already log warns and errors.
|
||||
return;
|
||||
}
|
||||
this._renderer.refreshPixiTileMap(textureCache);
|
||||
}
|
||||
);
|
||||
if (this._collisionTileMap) {
|
||||
const tileMap = this._renderer.getTileMap();
|
||||
if (tileMap) this._collisionTileMap.updateFromTileMap(tileMap);
|
||||
}
|
||||
this._isTileMapDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateFromObjectData(
|
||||
oldObjectData: SimpleTileMapObjectData,
|
||||
newObjectData: SimpleTileMapObjectData
|
||||
): boolean {
|
||||
if (oldObjectData.content.opacity !== newObjectData.content.opacity) {
|
||||
this.setOpacity(newObjectData.content.opacity);
|
||||
}
|
||||
if (
|
||||
oldObjectData.content.atlasImage !== newObjectData.content.atlasImage
|
||||
) {
|
||||
// TODO: support changing the atlas texture
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
getNetworkSyncData(): SimpleTileMapNetworkSyncData {
|
||||
return {
|
||||
...super.getNetworkSyncData(),
|
||||
op: this._opacity,
|
||||
ai: this._atlasImage,
|
||||
wid: this.getWidth(),
|
||||
hei: this.getHeight(),
|
||||
};
|
||||
}
|
||||
|
||||
updateFromNetworkSyncData(
|
||||
networkSyncData: SimpleTileMapNetworkSyncData
|
||||
): void {
|
||||
super.updateFromNetworkSyncData(networkSyncData);
|
||||
|
||||
if (
|
||||
networkSyncData.op !== undefined &&
|
||||
networkSyncData.op !== this._opacity
|
||||
) {
|
||||
this.setOpacity(networkSyncData.op);
|
||||
}
|
||||
if (
|
||||
networkSyncData.wid !== undefined &&
|
||||
networkSyncData.wid !== this.getWidth()
|
||||
) {
|
||||
this.setWidth(networkSyncData.wid);
|
||||
}
|
||||
if (
|
||||
networkSyncData.hei !== undefined &&
|
||||
networkSyncData.hei !== this.getHeight()
|
||||
) {
|
||||
this.setHeight(networkSyncData.hei);
|
||||
}
|
||||
if (networkSyncData.ai !== undefined) {
|
||||
// TODO: support changing the atlas texture
|
||||
}
|
||||
}
|
||||
|
||||
extraInitializationFromInitialInstance(
|
||||
initialInstanceData: InstanceData
|
||||
): void {
|
||||
// 1. load the tilemap from the instance.
|
||||
for (const property of initialInstanceData.stringProperties) {
|
||||
if (property.name === 'tilemap') {
|
||||
this._initialTileMapAsJsObject = JSON.parse(property.value);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Update the renderer so that it updates the tilemap object
|
||||
// (used for width and position calculations).
|
||||
this._loadInitialTileMap((tileMap: TileMapHelper.EditableTileMap) => {
|
||||
// 3. Set custom dimensions if applicable.
|
||||
if (initialInstanceData.customSize) {
|
||||
this.setWidth(initialInstanceData.width);
|
||||
this.setHeight(initialInstanceData.height);
|
||||
}
|
||||
|
||||
// 4. Update position (calculations based on renderer's dimensions).
|
||||
this._renderer.updatePosition();
|
||||
|
||||
this._collisionTileMap = new gdjs.TileMap.TransformedCollisionTileMap(
|
||||
tileMap,
|
||||
this._hitBoxTag
|
||||
);
|
||||
this.updateTransformation();
|
||||
});
|
||||
}
|
||||
|
||||
private _loadInitialTileMap(
|
||||
tileMapLoadingCallback: (tileMap: TileMapHelper.EditableTileMap) => void
|
||||
): void {
|
||||
if (!this._initialTileMapAsJsObject) return;
|
||||
|
||||
this._tileMapManager.getOrLoadSimpleTileMap(
|
||||
this._initialTileMapAsJsObject,
|
||||
this.name,
|
||||
this._tileSize,
|
||||
this._columnCount,
|
||||
this._rowCount,
|
||||
(tileMap: TileMapHelper.EditableTileMap) => {
|
||||
this._initialTilesWithHitBox.forEach((tileId) => {
|
||||
const tileDefinition = tileMap.getTileDefinition(tileId);
|
||||
if (!tileDefinition) {
|
||||
console.warn(
|
||||
`Could not set hit box for tile with id ${tileId}. Continuing.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
tileDefinition.addHitBox(this._hitBoxTag, [
|
||||
[0, 0],
|
||||
[0, tileMap.getTileHeight()],
|
||||
[tileMap.getTileWidth(), tileMap.getTileHeight()],
|
||||
[tileMap.getTileWidth(), 0],
|
||||
]);
|
||||
});
|
||||
|
||||
this._tileMapManager.getOrLoadSimpleTileMapTextureCache(
|
||||
(textureName) => {
|
||||
return (this.getInstanceContainer()
|
||||
.getGame()
|
||||
.getImageManager()
|
||||
.getPIXITexture(textureName) as unknown) as PIXI.BaseTexture<
|
||||
PIXI.Resource
|
||||
>;
|
||||
},
|
||||
this._atlasImage,
|
||||
this._tileSize,
|
||||
this._columnCount,
|
||||
this._rowCount,
|
||||
(textureCache: TileMapHelper.TileTextureCache | null) => {
|
||||
if (!textureCache) {
|
||||
// getOrLoadTextureCache already log warns and errors.
|
||||
return;
|
||||
}
|
||||
this._renderer.updatePixiTileMap(tileMap, textureCache);
|
||||
tileMapLoadingCallback(tileMap);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onDestroyed(): void {
|
||||
super.onDestroyed();
|
||||
this._renderer.destroy();
|
||||
}
|
||||
|
||||
setWidth(width: float): void {
|
||||
if (this.getWidth() === width) return;
|
||||
|
||||
this._transformationIsUpToDate = false;
|
||||
this._renderer.setWidth(width);
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
|
||||
setHeight(height: float): void {
|
||||
if (this.getHeight() === height) return;
|
||||
|
||||
this._transformationIsUpToDate = false;
|
||||
this._renderer.setHeight(height);
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
|
||||
setSize(newWidth: float, newHeight: float): void {
|
||||
this.setWidth(newWidth);
|
||||
this.setHeight(newHeight);
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the scale of the object (or the geometric mean of the X and Y scale in case they are different).
|
||||
*
|
||||
* @return the scale of the object (or the geometric mean of the X and Y scale in case they are different).
|
||||
*/
|
||||
getScale(): float {
|
||||
const scaleX = this.getScaleX();
|
||||
const scaleY = this.getScaleY();
|
||||
return scaleX === scaleY ? scaleX : Math.sqrt(scaleX * scaleY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the scale on X and Y axis of the object.
|
||||
*
|
||||
* @param scale The new scale (must be greater than 0).
|
||||
*/
|
||||
setScale(scale: float): void {
|
||||
this.setScaleX(scale);
|
||||
this.setScaleY(scale);
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the scale on X axis of the object (changing its width).
|
||||
*
|
||||
* @param scaleX The new scale (must be greater than 0).
|
||||
*/
|
||||
setScaleX(scaleX: float): void {
|
||||
if (scaleX < 0) {
|
||||
scaleX = 0;
|
||||
}
|
||||
if (this.getScaleX() === scaleX) return;
|
||||
|
||||
this._renderer.setScaleX(scaleX);
|
||||
this.invalidateHitboxes();
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the scale on Y axis of the object (changing its width).
|
||||
*
|
||||
* @param scaleY The new scale (must be greater than 0).
|
||||
*/
|
||||
setScaleY(scaleY: float): void {
|
||||
if (scaleY < 0) {
|
||||
scaleY = 0;
|
||||
}
|
||||
if (this.getScaleY() === scaleY) return;
|
||||
|
||||
this._renderer.setScaleY(scaleY);
|
||||
this.invalidateHitboxes();
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
setX(x: float): void {
|
||||
super.setX(x);
|
||||
this._renderer.updatePosition();
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
setY(y: float): void {
|
||||
super.setY(y);
|
||||
this._renderer.updatePosition();
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
setAngle(angle: float): void {
|
||||
super.setAngle(angle);
|
||||
this._renderer.updateAngle();
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
setOpacity(opacity: float): void {
|
||||
this._opacity = opacity;
|
||||
this._renderer.updateOpacity();
|
||||
this._isTileMapDirty = true;
|
||||
}
|
||||
|
||||
getOpacity(): float {
|
||||
return this._opacity;
|
||||
}
|
||||
|
||||
getWidth(): float {
|
||||
return this._renderer.getWidth();
|
||||
}
|
||||
|
||||
getHeight(): float {
|
||||
return this._renderer.getHeight();
|
||||
}
|
||||
|
||||
getScaleX(): float {
|
||||
return this._renderer.getScaleX();
|
||||
}
|
||||
|
||||
getScaleY(): float {
|
||||
return this._renderer.getScaleY();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is expensive and should not be called.
|
||||
* Prefer using {@link getHitBoxesAround} rather than getHitBoxes.
|
||||
*/
|
||||
getHitBoxes(): gdjs.Polygon[] {
|
||||
if (this.hitBoxesDirty) {
|
||||
this.updateHitBoxes();
|
||||
this.updateAABB();
|
||||
this.hitBoxesDirty = false;
|
||||
}
|
||||
return this.hitBoxes;
|
||||
}
|
||||
|
||||
updateHitBoxes(): void {
|
||||
this.updateTransformation();
|
||||
if (!this._collisionTileMap) return;
|
||||
// Update the RuntimeObject hitboxes attribute.
|
||||
for (const _ of this._collisionTileMap.getAllHitboxes(this._hitBoxTag)) {
|
||||
// RuntimeObject.hitBoxes contains the same polygons instances as the
|
||||
// hitboxes from the tiles.
|
||||
//
|
||||
// When hitboxes for a tile is asked to the model, they are updated
|
||||
// according to the new object location if needed.
|
||||
// Iterating over all the tiles forces them to update their hitboxes.
|
||||
}
|
||||
this.hitBoxesDirty = false;
|
||||
this.updateAABB();
|
||||
}
|
||||
|
||||
// This implementation doesn't use updateHitBoxes.
|
||||
// It's important for good performances.
|
||||
updateAABB(): void {
|
||||
if (this.getAngle() === 0) {
|
||||
// Fast computation of AABB for non rotated object
|
||||
this.aabb.min[0] = this.x;
|
||||
this.aabb.min[1] = this.y;
|
||||
this.aabb.max[0] = this.aabb.min[0] + this.getWidth();
|
||||
this.aabb.max[1] = this.aabb.min[1] + this.getHeight();
|
||||
} else {
|
||||
if (!this._collisionTileMap) return;
|
||||
const affineTransformation = this._collisionTileMap.getTransformation();
|
||||
|
||||
const left = 0;
|
||||
const right = this._collisionTileMap.getWidth();
|
||||
const top = 0;
|
||||
const bottom = this._collisionTileMap.getHeight();
|
||||
|
||||
const workingPoint = this.aabb.min;
|
||||
|
||||
workingPoint[0] = left;
|
||||
workingPoint[1] = top;
|
||||
affineTransformation.transform(workingPoint, workingPoint);
|
||||
const topLeftX = workingPoint[0];
|
||||
const topLeftY = workingPoint[1];
|
||||
|
||||
workingPoint[0] = right;
|
||||
workingPoint[1] = top;
|
||||
affineTransformation.transform(workingPoint, workingPoint);
|
||||
const topRightX = workingPoint[0];
|
||||
const topRightY = workingPoint[1];
|
||||
|
||||
workingPoint[0] = right;
|
||||
workingPoint[1] = bottom;
|
||||
affineTransformation.transform(workingPoint, workingPoint);
|
||||
const bottomRightX = workingPoint[0];
|
||||
const bottomRightY = workingPoint[1];
|
||||
|
||||
workingPoint[0] = left;
|
||||
workingPoint[1] = bottom;
|
||||
affineTransformation.transform(workingPoint, workingPoint);
|
||||
const bottomLeftX = workingPoint[0];
|
||||
const bottomLeftY = workingPoint[1];
|
||||
|
||||
this.aabb.min[0] = Math.min(
|
||||
topLeftX,
|
||||
topRightX,
|
||||
bottomRightX,
|
||||
bottomLeftX
|
||||
);
|
||||
this.aabb.max[0] = Math.max(
|
||||
topLeftX,
|
||||
topRightX,
|
||||
bottomRightX,
|
||||
bottomLeftX
|
||||
);
|
||||
this.aabb.min[1] = Math.min(
|
||||
topLeftY,
|
||||
topRightY,
|
||||
bottomRightY,
|
||||
bottomLeftY
|
||||
);
|
||||
this.aabb.max[1] = Math.max(
|
||||
topLeftY,
|
||||
topRightY,
|
||||
bottomRightY,
|
||||
bottomLeftY
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getHitBoxesAround(
|
||||
left: float,
|
||||
top: float,
|
||||
right: float,
|
||||
bottom: float
|
||||
): Iterable<gdjs.Polygon> {
|
||||
// This implementation doesn't call updateHitBoxes.
|
||||
// It's important for good performances because there is no need to
|
||||
// update the whole collision mask where only a few hitboxes must be
|
||||
// checked.
|
||||
this.updateTransformation();
|
||||
if (!this._collisionTileMap) return [];
|
||||
return this._collisionTileMap.getHitboxesAround(
|
||||
this._hitBoxTag,
|
||||
left,
|
||||
top,
|
||||
right,
|
||||
bottom
|
||||
);
|
||||
}
|
||||
|
||||
updateTransformation() {
|
||||
if (this._transformationIsUpToDate) {
|
||||
return;
|
||||
}
|
||||
const absScaleX = Math.abs(this._renderer.getScaleX());
|
||||
const absScaleY = Math.abs(this._renderer.getScaleY());
|
||||
|
||||
this._sceneToTileMapTransformation.setToIdentity();
|
||||
|
||||
// Translation
|
||||
this._sceneToTileMapTransformation.translate(this.getX(), this.getY());
|
||||
|
||||
// Rotation
|
||||
const angleInRadians = (this.getAngle() * Math.PI) / 180;
|
||||
this._sceneToTileMapTransformation.rotateAround(
|
||||
angleInRadians,
|
||||
this.getCenterX(),
|
||||
this.getCenterY()
|
||||
);
|
||||
|
||||
// Scale
|
||||
this._sceneToTileMapTransformation.scale(absScaleX, absScaleY);
|
||||
if (this._collisionTileMap) {
|
||||
const collisionTileMapTransformation = this._collisionTileMap.getTransformation();
|
||||
collisionTileMapTransformation.copyFrom(
|
||||
this._sceneToTileMapTransformation
|
||||
);
|
||||
this._collisionTileMap.setTransformation(
|
||||
collisionTileMapTransformation
|
||||
);
|
||||
}
|
||||
this._sceneToTileMapTransformation.invert();
|
||||
this._transformationIsUpToDate = true;
|
||||
}
|
||||
|
||||
getGridCoordinatesFromSceneCoordinates(
|
||||
x: number,
|
||||
y: number
|
||||
): [number, number] {
|
||||
this.updateTransformation();
|
||||
|
||||
const gridCoordinates: FloatPoint =
|
||||
SimpleTileMapRuntimeObject.workingPoint;
|
||||
this._sceneToTileMapTransformation.transform([x, y], gridCoordinates);
|
||||
|
||||
const columnIndex = Math.floor(gridCoordinates[0] / this._tileSize);
|
||||
const rowIndex = Math.floor(gridCoordinates[1] / this._tileSize);
|
||||
|
||||
return [columnIndex, rowIndex];
|
||||
}
|
||||
|
||||
getTileAt(x: number, y: number): integer {
|
||||
const [
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
] = this.getGridCoordinatesFromSceneCoordinates(x, y);
|
||||
return this._renderer.getTileId(columnIndex, rowIndex, 0);
|
||||
}
|
||||
|
||||
setTileAt(
|
||||
tileId: number,
|
||||
x: number,
|
||||
y: number,
|
||||
flipHorizontally: boolean,
|
||||
flipVertically: boolean
|
||||
) {
|
||||
const [
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
] = this.getGridCoordinatesFromSceneCoordinates(x, y);
|
||||
const addedData = this._renderer.setTileId(
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
0,
|
||||
tileId,
|
||||
{ flipHorizontally, flipVertically, flipDiagonally: false }
|
||||
);
|
||||
this._isTileMapDirty = true;
|
||||
if (addedData) {
|
||||
const {
|
||||
unshiftedRows,
|
||||
unshiftedColumns,
|
||||
appendedColumns,
|
||||
appendedRows,
|
||||
} = addedData;
|
||||
const scaleX = this.getScaleX();
|
||||
const scaleY = this.getScaleY();
|
||||
this.setX(this.getX() - unshiftedColumns * (this._tileSize * scaleX));
|
||||
this.setY(this.getY() - unshiftedRows * (this._tileSize * scaleY));
|
||||
if (
|
||||
unshiftedColumns > 0 ||
|
||||
unshiftedRows > 0 ||
|
||||
appendedColumns > 0 ||
|
||||
appendedRows > 0
|
||||
) {
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
}
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
removeTileAt(x: number, y: number) {
|
||||
const [
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
] = this.getGridCoordinatesFromSceneCoordinates(x, y);
|
||||
this._renderer.removeTile(columnIndex, rowIndex, 0);
|
||||
this._isTileMapDirty = true;
|
||||
const removedData = this._renderer.trimEmptyColumnsAndRows(0);
|
||||
if (removedData) {
|
||||
const {
|
||||
shiftedRows,
|
||||
shiftedColumns,
|
||||
poppedColumns,
|
||||
poppedRows,
|
||||
} = removedData;
|
||||
this.setX(
|
||||
this.getX() + shiftedColumns * (this._tileSize * this.getScaleX())
|
||||
);
|
||||
this.setY(
|
||||
this.getY() + shiftedRows * (this._tileSize * this.getScaleY())
|
||||
);
|
||||
if (
|
||||
shiftedColumns > 0 ||
|
||||
shiftedRows > 0 ||
|
||||
poppedColumns > 0 ||
|
||||
poppedRows > 0
|
||||
) {
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
}
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
}
|
||||
gdjs.registerObject(
|
||||
'TileMap::SimpleTileMap',
|
||||
gdjs.SimpleTileMapRuntimeObject
|
||||
);
|
||||
}
|
@@ -7,7 +7,9 @@ namespace gdjs {
|
||||
* @class TileMapRuntimeObjectPixiRenderer
|
||||
*/
|
||||
export class TileMapRuntimeObjectPixiRenderer {
|
||||
private _object: any;
|
||||
private _object:
|
||||
| gdjs.TileMapRuntimeObject
|
||||
| gdjs.SimpleTileMapRuntimeObject;
|
||||
private _tileMap: TileMapHelper.EditableTileMap | null = null;
|
||||
|
||||
private _pixiObject: PIXI.tilemap.CompositeTilemap;
|
||||
@@ -17,7 +19,9 @@ namespace gdjs {
|
||||
* @param instanceContainer The gdjs.RuntimeScene in which the object is
|
||||
*/
|
||||
constructor(
|
||||
runtimeObject: gdjs.TileMapRuntimeObject,
|
||||
runtimeObject:
|
||||
| gdjs.TileMapRuntimeObject
|
||||
| gdjs.SimpleTileMapRuntimeObject,
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer
|
||||
) {
|
||||
this._object = runtimeObject;
|
||||
@@ -55,11 +59,28 @@ namespace gdjs {
|
||||
this._pixiObject,
|
||||
tileMap,
|
||||
textureCache,
|
||||
// @ts-ignore
|
||||
this._object._displayMode,
|
||||
this._object._layerIndex
|
||||
);
|
||||
}
|
||||
|
||||
refreshPixiTileMap(textureCache: TileMapHelper.TileTextureCache) {
|
||||
if (!this._tileMap) return;
|
||||
TileMapHelper.PixiTileMapHelper.updatePixiTileMap(
|
||||
this._pixiObject,
|
||||
this._tileMap,
|
||||
textureCache,
|
||||
// @ts-ignore
|
||||
this._object._displayMode,
|
||||
this._object._layerIndex
|
||||
);
|
||||
}
|
||||
|
||||
getTileMap(): TileMapHelper.EditableTileMap | null {
|
||||
return this._tileMap;
|
||||
}
|
||||
|
||||
updatePosition(): void {
|
||||
this._pixiObject.pivot.x = this.getTileMapWidth() / 2;
|
||||
this._pixiObject.pivot.y = this.getTileMapHeight() / 2;
|
||||
@@ -73,6 +94,20 @@ namespace gdjs {
|
||||
|
||||
updateOpacity(): void {
|
||||
this._pixiObject.alpha = this._object._opacity / 255;
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return;
|
||||
for (const layer of tileMap.getLayers()) {
|
||||
if (
|
||||
(this._object._displayMode === 'index' &&
|
||||
this._object._layerIndex !== layer.id) ||
|
||||
(this._object._displayMode === 'visible' && !layer.isVisible())
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (layer instanceof TileMapHelper.EditableTileMapLayer) {
|
||||
layer.setAlpha(this._pixiObject.alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getTileMapWidth() {
|
||||
@@ -123,6 +158,69 @@ namespace gdjs {
|
||||
return this._pixiObject.scale.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
* @param layerIndex The layer index.
|
||||
* @returns The tile's id.
|
||||
*/
|
||||
getTileId(x: integer, y: integer, layerIndex: integer): integer {
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return -1;
|
||||
const layer = tileMap.getTileLayer(layerIndex);
|
||||
if (!layer) return -1;
|
||||
return layer.getTileId(x, y) || -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
* @param layerIndex The layer index.
|
||||
* @param tileId The tile's id.
|
||||
*/
|
||||
setTileId(
|
||||
x: integer,
|
||||
y: integer,
|
||||
layerIndex: integer,
|
||||
tileId: number,
|
||||
options?:
|
||||
| {
|
||||
flipVertically: boolean;
|
||||
flipHorizontally: boolean;
|
||||
flipDiagonally: boolean;
|
||||
}
|
||||
| undefined
|
||||
) {
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return;
|
||||
const layer = tileMap.getTileLayer(layerIndex);
|
||||
if (!layer) return;
|
||||
return layer.setTile(x, y, tileId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
* @param layerIndex The layer index.
|
||||
*/
|
||||
removeTile(x: integer, y: integer, layerIndex: integer) {
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return;
|
||||
const layer = tileMap.getTileLayer(layerIndex);
|
||||
if (!layer) return;
|
||||
return layer.removeTile(x, y);
|
||||
}
|
||||
/**
|
||||
* @param layerIndex The layer index.
|
||||
*/
|
||||
trimEmptyColumnsAndRows(layerIndex: integer) {
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return;
|
||||
const layer = tileMap.getTileLayer(layerIndex);
|
||||
if (!layer) return;
|
||||
return layer.trimEmptyColumnsAndRow();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
// Keep textures because they are shared by all tile maps.
|
||||
this._pixiObject.destroy(false);
|
||||
|
@@ -33,7 +33,7 @@ namespace gdjs {
|
||||
TilemapNetworkSyncDataType;
|
||||
|
||||
/**
|
||||
* Displays a Tilemap object (mapeditor.org supported).
|
||||
* Displays a Tilemap object (LDtk and Tiled).
|
||||
*/
|
||||
export class TileMapRuntimeObject
|
||||
extends gdjs.RuntimeObject
|
||||
@@ -413,6 +413,7 @@ namespace gdjs {
|
||||
setOpacity(opacity: float): void {
|
||||
this._opacity = opacity;
|
||||
this._renderer.updateOpacity();
|
||||
// TODO: Set all tile layers alpha and re-render tilemap.
|
||||
}
|
||||
|
||||
getOpacity(): float {
|
||||
|
@@ -24,11 +24,11 @@ export class EditableTileMap {
|
||||
/**
|
||||
* The number of tile columns in the map.
|
||||
*/
|
||||
private readonly dimX: integer;
|
||||
private dimX: integer;
|
||||
/**
|
||||
* The number of tile rows in the map.
|
||||
*/
|
||||
private readonly dimY: integer;
|
||||
private dimY: integer;
|
||||
|
||||
/**
|
||||
* @param tileWidth The width of a tile.
|
||||
@@ -54,6 +54,66 @@ export class EditableTileMap {
|
||||
this._layers = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads EditableTileMap from serialized data.
|
||||
* Uses object configuration as the source of truth as the serialized data
|
||||
* might contain expired data (if the tile set configuration has changed and
|
||||
* the serialized data was not updated).
|
||||
* @param editableTileMapAsJsObject Serialized editable tile map object
|
||||
* @param objectConfiguration
|
||||
*/
|
||||
static from(
|
||||
editableTileMapAsJsObject: any,
|
||||
{
|
||||
tileSize,
|
||||
tileSetColumnCount,
|
||||
tileSetRowCount,
|
||||
}: { tileSize: number; tileSetColumnCount: number; tileSetRowCount: number }
|
||||
): EditableTileMap {
|
||||
const tileSet = new Map<number, TileDefinition>();
|
||||
|
||||
// TODO: Actually save and load tile set when useful.
|
||||
new Array(tileSetColumnCount * tileSetRowCount)
|
||||
.fill(0)
|
||||
.forEach((_, index) => {
|
||||
tileSet.set(index, new TileDefinition(0));
|
||||
});
|
||||
|
||||
const tileMap = new EditableTileMap(
|
||||
tileSize || editableTileMapAsJsObject.tileWidth,
|
||||
tileSize || editableTileMapAsJsObject.tileHeight,
|
||||
editableTileMapAsJsObject.dimX || 1,
|
||||
editableTileMapAsJsObject.dimY || 1,
|
||||
tileSet
|
||||
);
|
||||
|
||||
if (editableTileMapAsJsObject.layers) {
|
||||
editableTileMapAsJsObject.layers.forEach((layerAsJsObject: any) => {
|
||||
tileMap.setTileLayer(
|
||||
EditableTileMapLayer.from(
|
||||
layerAsJsObject,
|
||||
tileMap,
|
||||
(tileId) => tileId < tileSetColumnCount * tileSetRowCount
|
||||
)
|
||||
);
|
||||
});
|
||||
} else {
|
||||
tileMap.addTileLayer(0);
|
||||
}
|
||||
|
||||
return tileMap;
|
||||
}
|
||||
|
||||
toJSObject(): Object {
|
||||
return {
|
||||
tileWidth: this.tileWidth,
|
||||
tileHeight: this.tileHeight,
|
||||
dimX: this.dimX,
|
||||
dimY: this.dimY,
|
||||
layers: this._layers.map((layer) => layer.toJSObject()),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The tile map width in pixels.
|
||||
*/
|
||||
@@ -96,6 +156,20 @@ export class EditableTileMap {
|
||||
return this.dimY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dim The number of tile columns in the map.
|
||||
*/
|
||||
setDimensionX(dim: integer): void {
|
||||
this.dimX = dim;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dim The number of tile rows in the map.
|
||||
*/
|
||||
setDimensionY(dim: integer): void {
|
||||
this.dimY = dim;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param tileId The tile identifier
|
||||
* @returns The tile definition form the tile set.
|
||||
@@ -121,6 +195,19 @@ export class EditableTileMap {
|
||||
return layer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param layer the new layer to set.
|
||||
*/
|
||||
setTileLayer(layer: EditableTileMapLayer): void {
|
||||
this._layers.push(layer);
|
||||
}
|
||||
|
||||
getTileLayer(id: integer): EditableTileMapLayer | null {
|
||||
const matchingLayer = this._layers.find((layer) => layer.id === id);
|
||||
if (!(matchingLayer instanceof EditableTileMapLayer)) return null;
|
||||
return matchingLayer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id The identifier of the new layer.
|
||||
* @returns The new layer.
|
||||
@@ -182,6 +269,13 @@ export class EditableTileMap {
|
||||
setBackgroundResourceName(resourceName: string): void {
|
||||
this._backgroundResourceName = resourceName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if all layers contain no defined tiled.
|
||||
*/
|
||||
isEmpty(): boolean {
|
||||
return this._layers.every((layer) => layer.isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,12 +305,20 @@ abstract class AbstractEditableLayer {
|
||||
this.visible = visible;
|
||||
}
|
||||
|
||||
toJSObject(): Object {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if the layer is visible.
|
||||
*/
|
||||
isVisible(): boolean {
|
||||
return this.visible;
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -237,6 +339,10 @@ export class EditableObjectLayer extends AbstractEditableLayer {
|
||||
add(object: TileObject): void {
|
||||
this.objects.push(object);
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.objects.length === 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -321,7 +427,7 @@ export class TileObject {
|
||||
* A tile map layer with tile organized in grid.
|
||||
*/
|
||||
export class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
private readonly _tiles: Int32Array[];
|
||||
private _tiles: Int32Array[];
|
||||
private _alpha: float;
|
||||
|
||||
/**
|
||||
@@ -330,14 +436,60 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
*/
|
||||
constructor(tileMap: EditableTileMap, id: integer) {
|
||||
super(tileMap, id);
|
||||
this._tiles = [];
|
||||
this._tiles.length = this.tileMap.getDimensionY();
|
||||
for (let index = 0; index < this._tiles.length; index++) {
|
||||
this._tiles[index] = new Int32Array(this.tileMap.getDimensionX());
|
||||
}
|
||||
this.buildEmptyLayer(
|
||||
this.tileMap.getDimensionX(),
|
||||
this.tileMap.getDimensionY()
|
||||
);
|
||||
this._alpha = 1;
|
||||
}
|
||||
|
||||
buildEmptyLayer(dimensionX: number, dimensionY: number) {
|
||||
this._tiles = [];
|
||||
this._tiles.length = dimensionY;
|
||||
for (let index = 0; index < this._tiles.length; index++) {
|
||||
this._tiles[index] = new Int32Array(dimensionX);
|
||||
}
|
||||
}
|
||||
|
||||
static from(
|
||||
editableTileMapLayerAsJsObject: any,
|
||||
tileMap: EditableTileMap,
|
||||
isTileIdValid: (tileId: number) => boolean
|
||||
): EditableTileMapLayer {
|
||||
const layer = new EditableTileMapLayer(
|
||||
tileMap,
|
||||
editableTileMapLayerAsJsObject.id
|
||||
);
|
||||
layer.setAlpha(editableTileMapLayerAsJsObject.alpha);
|
||||
editableTileMapLayerAsJsObject.tiles.forEach((row: Int32Array, y: number) =>
|
||||
row.forEach((tileGID, x) => {
|
||||
const tileId = FlippingHelper.getTileId(tileGID);
|
||||
if (isTileIdValid(tileId)) {
|
||||
layer.setTileGID(x, y, tileGID);
|
||||
}
|
||||
})
|
||||
);
|
||||
return layer;
|
||||
}
|
||||
|
||||
toJSObject(): Object {
|
||||
return {
|
||||
id: this.id,
|
||||
alpha: this._alpha,
|
||||
tiles: this._tiles.map((row, y) =>
|
||||
// Array.from is needed to convert Int32Array to Array. Otherwise, JSON.stringify
|
||||
// serializes it as an object with index as keys.
|
||||
Array.from(
|
||||
row.map(
|
||||
(_, x) =>
|
||||
// -1 corresponds to null value
|
||||
this.getTileGID(x, y) || -1
|
||||
)
|
||||
)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The opacity (between 0-1) of the layer
|
||||
*/
|
||||
@@ -352,17 +504,169 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
this._alpha = alpha;
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this._tiles.every((row) => row.every((cell) => cell === 0));
|
||||
}
|
||||
|
||||
reduceDimensions(
|
||||
columnsToPop: number,
|
||||
columnsToShift: number,
|
||||
rowsToPop: number,
|
||||
rowsToShift: number
|
||||
) {
|
||||
const initialRowCount = this._tiles.length;
|
||||
const initialColumnCount = this._tiles[0].length;
|
||||
|
||||
if (rowsToPop > 0 || rowsToShift > 0) {
|
||||
this._tiles = this._tiles.slice(
|
||||
rowsToShift,
|
||||
rowsToPop ? -rowsToPop : undefined
|
||||
);
|
||||
}
|
||||
if (columnsToPop > 0 || columnsToShift > 0) {
|
||||
this._tiles.forEach((row, rowIndex) => {
|
||||
this._tiles[rowIndex] = this._tiles[rowIndex].slice(
|
||||
columnsToShift,
|
||||
columnsToPop ? -columnsToPop : undefined
|
||||
);
|
||||
});
|
||||
}
|
||||
// TODO: Instead of setting the dimensions directly, should it call a method on
|
||||
// EditableTileMap that will iterates over all the layers to change their dimensions?
|
||||
this.tileMap.setDimensionX(
|
||||
initialColumnCount - columnsToPop - columnsToShift
|
||||
);
|
||||
this.tileMap.setDimensionY(initialRowCount - rowsToPop - rowsToShift);
|
||||
}
|
||||
|
||||
increaseDimensions(
|
||||
columnsToAppend: number,
|
||||
columnsToUnshift: number,
|
||||
rowsToAppend: number,
|
||||
rowsToUnshift: number
|
||||
) {
|
||||
const initialRowCount = this._tiles.length;
|
||||
const initialColumnCount = this._tiles[0].length;
|
||||
if (columnsToAppend > 0 || columnsToUnshift > 0) {
|
||||
this._tiles.forEach((row, rowIndex) => {
|
||||
const newRow = new Int32Array(
|
||||
initialColumnCount + columnsToAppend + columnsToUnshift
|
||||
).fill(0);
|
||||
newRow.set(row, columnsToUnshift);
|
||||
this._tiles[rowIndex] = newRow;
|
||||
});
|
||||
}
|
||||
if (rowsToAppend > 0 || rowsToUnshift > 0) {
|
||||
this._tiles.unshift(
|
||||
...new Array(rowsToUnshift)
|
||||
.fill(0)
|
||||
.map(() =>
|
||||
new Int32Array(
|
||||
initialColumnCount + columnsToAppend + columnsToUnshift
|
||||
).fill(0)
|
||||
)
|
||||
);
|
||||
|
||||
this._tiles.length = initialRowCount + rowsToAppend + rowsToUnshift;
|
||||
|
||||
for (
|
||||
let rowIndex = initialRowCount + rowsToUnshift;
|
||||
rowIndex < this._tiles.length;
|
||||
rowIndex++
|
||||
) {
|
||||
this._tiles[rowIndex] = new Int32Array(
|
||||
initialColumnCount + columnsToAppend + columnsToUnshift
|
||||
).fill(0);
|
||||
}
|
||||
}
|
||||
// TODO: Instead of setting the dimensions directly, should it call a method on
|
||||
// EditableTileMap that will iterates over all the layers to change their dimensions?
|
||||
this.tileMap.setDimensionX(
|
||||
initialColumnCount + columnsToAppend + columnsToUnshift
|
||||
);
|
||||
this.tileMap.setDimensionY(initialRowCount + rowsToAppend + rowsToUnshift);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
* @param tileId The tile.
|
||||
* @param options Flipping options.
|
||||
*/
|
||||
setTile(x: integer, y: integer, tileId: integer): void {
|
||||
setTile(
|
||||
x: integer,
|
||||
y: integer,
|
||||
tileId: integer,
|
||||
options:
|
||||
| {
|
||||
flipVertically: boolean;
|
||||
flipHorizontally: boolean;
|
||||
flipDiagonally: boolean;
|
||||
}
|
||||
| undefined = {
|
||||
flipVertically: false,
|
||||
flipHorizontally: false,
|
||||
flipDiagonally: false,
|
||||
}
|
||||
):
|
||||
| {
|
||||
unshiftedRows: number;
|
||||
unshiftedColumns: number;
|
||||
appendedRows: number;
|
||||
appendedColumns: number;
|
||||
}
|
||||
| undefined {
|
||||
const definition = this.tileMap.getTileDefinition(tileId);
|
||||
if (!definition) {
|
||||
console.error(`Invalid tile definition index: ${tileId}`);
|
||||
return;
|
||||
}
|
||||
const rowsToAdd = Math.max(0, y - (this._tiles.length - 1));
|
||||
const columnsToAdd = Math.max(0, x - (this._tiles[0].length - 1));
|
||||
const rowsToUnshift = Math.abs(Math.min(0, y));
|
||||
const columnsToUnshift = Math.abs(Math.min(0, x));
|
||||
if (rowsToAdd || columnsToAdd || rowsToUnshift || columnsToUnshift) {
|
||||
this.increaseDimensions(
|
||||
columnsToAdd,
|
||||
columnsToUnshift,
|
||||
rowsToAdd,
|
||||
rowsToUnshift
|
||||
);
|
||||
}
|
||||
// Dimensions have been changed to support setting tiles in positions below 0.
|
||||
// So we adapt the indices.
|
||||
const newX = x + columnsToUnshift;
|
||||
const newY = y + rowsToUnshift;
|
||||
const tilesRow = this._tiles[newY];
|
||||
if (!tilesRow || x >= tilesRow.length) {
|
||||
// Coordinates are out of bounds, don't do anything.
|
||||
return;
|
||||
}
|
||||
|
||||
tilesRow[newX] =
|
||||
FlippingHelper.setFlippedHorizontally(
|
||||
FlippingHelper.setFlippedVertically(
|
||||
FlippingHelper.setFlippedDiagonally(tileId, options.flipDiagonally),
|
||||
options.flipVertically
|
||||
),
|
||||
options.flipHorizontally
|
||||
) +
|
||||
// +1 because 0 mean null
|
||||
1;
|
||||
return {
|
||||
unshiftedRows: rowsToUnshift,
|
||||
unshiftedColumns: columnsToUnshift,
|
||||
appendedRows: rowsToAdd,
|
||||
appendedColumns: columnsToAdd,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
* @param tileGID The tile GID.
|
||||
*/
|
||||
setTileGID(x: integer, y: integer, tileGID: integer): void {
|
||||
const tilesRow = this._tiles[y];
|
||||
if (!tilesRow || x >= tilesRow.length) {
|
||||
// Coordinates are out of bounds, don't do anything.
|
||||
@@ -370,7 +674,73 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
}
|
||||
|
||||
// +1 because 0 mean null
|
||||
tilesRow[x] = tileId + 1;
|
||||
tilesRow[x] = tileGID + 1;
|
||||
}
|
||||
|
||||
trimEmptyColumnsAndRow(): {
|
||||
shiftedRows: number;
|
||||
shiftedColumns: number;
|
||||
poppedRows: number;
|
||||
poppedColumns: number;
|
||||
} {
|
||||
let rowsToShift = 0,
|
||||
rowsToPop = 0;
|
||||
const initialDimensionX = this.getDimensionX();
|
||||
const initialDimensionY = this.getDimensionY();
|
||||
const columnsToShiftByRow = new Array(this._tiles.length).fill(
|
||||
this._tiles[0].length
|
||||
);
|
||||
const columnsToPopByRow = new Array(this._tiles.length).fill(
|
||||
this._tiles[0].length
|
||||
);
|
||||
let isFirstNonEmptyRowFound = false;
|
||||
for (let y = 0; y < this._tiles.length; y++) {
|
||||
const row = this._tiles[y];
|
||||
let isFirstNonEmptyColumnFound = false;
|
||||
for (let x = 0; x < row.length; x++) {
|
||||
const cell = row[x];
|
||||
if (cell !== 0) {
|
||||
columnsToPopByRow[y] = row.length - 1 - x;
|
||||
if (!isFirstNonEmptyColumnFound) {
|
||||
columnsToShiftByRow[y] = x;
|
||||
isFirstNonEmptyColumnFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
const isRowEmpty = !isFirstNonEmptyColumnFound;
|
||||
if (!isRowEmpty) {
|
||||
rowsToPop = this._tiles.length - 1 - y;
|
||||
if (!isFirstNonEmptyRowFound) {
|
||||
rowsToShift = y;
|
||||
isFirstNonEmptyRowFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isFirstNonEmptyRowFound) {
|
||||
// The tile map is empty. Instead of having an object with null width and height,
|
||||
// the tile map is resized to have a size of 1x1 with an empty tile. This is useful
|
||||
// in the editor. It might need to have a different behavior in the runtime.
|
||||
this.buildEmptyLayer(1, 1);
|
||||
// TODO: Instead of setting the dimensions directly, should it call a method on
|
||||
// EditableTileMap that will iterates over all the layers to change their dimensions?
|
||||
this.tileMap.setDimensionX(1);
|
||||
this.tileMap.setDimensionY(1);
|
||||
return {
|
||||
shiftedColumns: 0,
|
||||
shiftedRows: 0,
|
||||
poppedColumns: initialDimensionX - 1,
|
||||
poppedRows: initialDimensionY - 1,
|
||||
};
|
||||
}
|
||||
const columnsToShift = Math.min(...columnsToShiftByRow);
|
||||
const columnsToPop = Math.min(...columnsToPopByRow);
|
||||
this.reduceDimensions(columnsToPop, columnsToShift, rowsToPop, rowsToShift);
|
||||
return {
|
||||
shiftedRows: rowsToShift,
|
||||
shiftedColumns: columnsToShift,
|
||||
poppedRows: rowsToPop,
|
||||
poppedColumns: columnsToPop,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -117,6 +117,27 @@ export class TileMapManager {
|
||||
);
|
||||
}
|
||||
|
||||
getOrLoadSimpleTileMap(
|
||||
tileMapAsJsObject: object,
|
||||
objectName: string,
|
||||
tileSize: number,
|
||||
tileSetColumnCount: number,
|
||||
tileSetRowCount: number,
|
||||
// Logic using callback has been set up to mimic what has been done for other
|
||||
// loading methods. But it could be refactored to directly return the tile map.
|
||||
callback: (tileMap: EditableTileMap) => void
|
||||
): void {
|
||||
// TODO: Is it useful to cache the tilemap since it belongs to an instance?
|
||||
// const key = `${objectName}|${tileSize}|${tileSetColumnCount}|${tileSetRowCount}`;
|
||||
|
||||
const editableTileMap = EditableTileMap.from(tileMapAsJsObject, {
|
||||
tileSize,
|
||||
tileSetColumnCount,
|
||||
tileSetRowCount,
|
||||
});
|
||||
callback(editableTileMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param loadTileMap The method that loads the Tiled JSON file in memory.
|
||||
* @param getTexture The method that loads the atlas image file in memory.
|
||||
@@ -178,6 +199,42 @@ export class TileMapManager {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param getTexture The method that loads the atlas image file in memory.
|
||||
* @param atlasImageResourceName The resource name of the atlas image.
|
||||
* @param tileSize
|
||||
* @param columnCount
|
||||
* @param rowCount
|
||||
* @param callback A function called when the tiles textures are split.
|
||||
*/
|
||||
getOrLoadSimpleTileMapTextureCache(
|
||||
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>,
|
||||
atlasImageResourceName: string,
|
||||
tileSize: number,
|
||||
columnCount: number,
|
||||
rowCount: number,
|
||||
callback: (textureCache: TileTextureCache | null) => void
|
||||
): void {
|
||||
const key = `${atlasImageResourceName}|${tileSize}|${columnCount}|${rowCount}`;
|
||||
|
||||
this._textureCacheCaches.getOrLoad(
|
||||
key,
|
||||
(textureCacheLoadingCallback) => {
|
||||
const atlasTexture = atlasImageResourceName
|
||||
? getTexture(atlasImageResourceName)
|
||||
: null;
|
||||
const textureCache = PixiTileMapHelper.parseSimpleTileMapAtlas(
|
||||
atlasTexture,
|
||||
columnCount,
|
||||
rowCount,
|
||||
tileSize
|
||||
);
|
||||
textureCacheLoadingCallback(textureCache);
|
||||
},
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
clearCaches(): void {
|
||||
this._tileMapCache = new ResourceCache<EditableTileMap>();
|
||||
this._textureCacheCaches = new ResourceCache<TileTextureCache>();
|
||||
|
@@ -14,7 +14,7 @@ export namespace PixiTileMapHelper {
|
||||
/**
|
||||
* Split an atlas image into Pixi textures.
|
||||
*
|
||||
* @param tiledMap A tile map exported from Tiled.
|
||||
* @param tiledMap A tile map exported from Tiled or LDtk.
|
||||
* @param levelIndex The level of the tile map to load from.
|
||||
* @param atlasTexture The texture containing the whole tile set.
|
||||
* @param getTexture A getter to load a texture. Used if atlasTexture is not specified.
|
||||
@@ -50,6 +50,39 @@ export namespace PixiTileMapHelper {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split an atlas image into Pixi textures.
|
||||
*
|
||||
* @param atlasTexture The texture containing the whole tile set.
|
||||
* @param columnCount The number of columns.
|
||||
* @param rowCount The number of rows.
|
||||
* @param tileSize The squared tile size.
|
||||
* @returns A textures cache.
|
||||
*/
|
||||
export function parseSimpleTileMapAtlas(
|
||||
atlasTexture: PIXI.BaseTexture<PIXI.Resource>,
|
||||
columnCount: number,
|
||||
rowCount: number,
|
||||
tileSize: number
|
||||
): TileTextureCache {
|
||||
const textureCache = new TileTextureCache();
|
||||
for (let x = 0; x < columnCount; x++) {
|
||||
for (let y = 0; y < rowCount; y++) {
|
||||
const rect = new PIXI.Rectangle(
|
||||
x * tileSize,
|
||||
y * tileSize,
|
||||
tileSize,
|
||||
tileSize
|
||||
);
|
||||
|
||||
const texture = new PIXI.Texture(atlasTexture, rect);
|
||||
|
||||
textureCache.setTexture(rowCount * x + y, texture);
|
||||
}
|
||||
}
|
||||
return textureCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-renders the tile map whenever its rendering settings have been changed
|
||||
*
|
||||
|
120
newIDE/app/src/InstancesEditor/ClickInterceptor.js
Normal file
120
newIDE/app/src/InstancesEditor/ClickInterceptor.js
Normal file
@@ -0,0 +1,120 @@
|
||||
// @flow
|
||||
|
||||
import * as PIXI from 'pixi.js-legacy';
|
||||
import ViewPosition from './ViewPosition';
|
||||
import { type TileMapTileSelection } from './TileSetVisualizer';
|
||||
|
||||
type Coordinates = {| x: number, y: number |};
|
||||
|
||||
type Props = {|
|
||||
viewPosition: ViewPosition,
|
||||
getTileMapTileSelection: () => ?TileMapTileSelection,
|
||||
onClick: (scenePathCoordinates: Array<Coordinates>) => void,
|
||||
|};
|
||||
|
||||
class ClickInterceptor {
|
||||
viewPosition: ViewPosition;
|
||||
getTileMapTileSelection: () => ?TileMapTileSelection;
|
||||
onClick: (scenePathCoordinates: Array<Coordinates>) => void;
|
||||
pointerPathCoordinates: ?Array<Coordinates>;
|
||||
_shouldCancelClick: boolean = false;
|
||||
_isIntercepting: boolean = false;
|
||||
|
||||
pixiContainer: PIXI.Container;
|
||||
interceptingSprite: PIXI.sprite;
|
||||
|
||||
constructor({ viewPosition, getTileMapTileSelection, onClick }: Props) {
|
||||
this.viewPosition = viewPosition;
|
||||
this.onClick = onClick;
|
||||
this.getTileMapTileSelection = getTileMapTileSelection;
|
||||
this.interceptingSprite = new PIXI.Sprite();
|
||||
this.interceptingSprite.alpha = 0;
|
||||
this.interceptingSprite.interactive = true;
|
||||
this.pointerPathCoordinates = null;
|
||||
this.interceptingSprite.addEventListener(
|
||||
'pointerdown',
|
||||
(e: PIXI.FederatedPointerEvent) => {
|
||||
this._shouldCancelClick = false;
|
||||
this._isIntercepting = true;
|
||||
const sceneCoordinates = this.viewPosition.toSceneCoordinates(
|
||||
e.global.x,
|
||||
e.global.y
|
||||
);
|
||||
this.pointerPathCoordinates = [
|
||||
{ x: sceneCoordinates[0], y: sceneCoordinates[1] },
|
||||
];
|
||||
}
|
||||
);
|
||||
this.interceptingSprite.addEventListener(
|
||||
'pointerup',
|
||||
(e: PIXI.FederatedPointerEvent) => {
|
||||
this._isIntercepting = false;
|
||||
if (!this.pointerPathCoordinates) return;
|
||||
if (this._shouldCancelClick) {
|
||||
this._shouldCancelClick = false;
|
||||
return;
|
||||
}
|
||||
this.onClick(this.pointerPathCoordinates);
|
||||
this.pointerPathCoordinates = null;
|
||||
}
|
||||
);
|
||||
this.interceptingSprite.addEventListener(
|
||||
'pointermove',
|
||||
(e: PIXI.FederatedPointerEvent) => {
|
||||
if (this._shouldCancelClick) return;
|
||||
const pointerPathCoordinates = this.pointerPathCoordinates;
|
||||
if (!pointerPathCoordinates) return;
|
||||
|
||||
const sceneCoordinates = this.viewPosition.toSceneCoordinates(
|
||||
e.global.x,
|
||||
e.global.y
|
||||
);
|
||||
|
||||
if (pointerPathCoordinates[1]) {
|
||||
pointerPathCoordinates[1] = {
|
||||
x: sceneCoordinates[0],
|
||||
y: sceneCoordinates[1],
|
||||
};
|
||||
} else {
|
||||
pointerPathCoordinates.push({
|
||||
x: sceneCoordinates[0],
|
||||
y: sceneCoordinates[1],
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
this.pixiContainer = new PIXI.Container();
|
||||
this.pixiContainer.addChild(this.interceptingSprite);
|
||||
}
|
||||
|
||||
getPixiObject(): PIXI.Container {
|
||||
return this.pixiContainer;
|
||||
}
|
||||
|
||||
getPointerPathCoordinates(): ?Array<Coordinates> {
|
||||
return this.pointerPathCoordinates;
|
||||
}
|
||||
|
||||
isIntercepting(): boolean {
|
||||
return this._isIntercepting;
|
||||
}
|
||||
|
||||
cancelClickInterception() {
|
||||
this._shouldCancelClick = true;
|
||||
if (this.pointerPathCoordinates) {
|
||||
this.pointerPathCoordinates = null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
this.pixiContainer.removeChildren(0);
|
||||
const tileMapTileSelection = this.getTileMapTileSelection();
|
||||
if (!tileMapTileSelection) return;
|
||||
this.pixiContainer.position.x = 0;
|
||||
this.pixiContainer.position.y = 0;
|
||||
this.interceptingSprite.width = this.viewPosition.getWidth();
|
||||
this.interceptingSprite.height = this.viewPosition.getHeight();
|
||||
this.pixiContainer.addChild(this.interceptingSprite);
|
||||
}
|
||||
}
|
||||
export default ClickInterceptor;
|
@@ -28,6 +28,9 @@ import {
|
||||
reorderInstanceSchemaForCustomProperties,
|
||||
} from './CompactPropertiesSchema';
|
||||
import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope.flow';
|
||||
import TileSetVisualizer, {
|
||||
type TileMapTileSelection,
|
||||
} from '../TileSetVisualizer';
|
||||
|
||||
export const styles = {
|
||||
paper: {
|
||||
@@ -59,6 +62,8 @@ type Props = {|
|
||||
unsavedChanges?: ?UnsavedChanges,
|
||||
i18n: I18nType,
|
||||
historyHandler?: HistoryHandler,
|
||||
tileMapTileSelection: ?TileMapTileSelection,
|
||||
onSelectTileMapTile: (?TileMapTileSelection) => void,
|
||||
|};
|
||||
|
||||
export type CompactInstancePropertiesEditorInterface = {|
|
||||
@@ -80,6 +85,8 @@ const CompactInstancePropertiesEditor = ({
|
||||
editInstanceVariables,
|
||||
onInstancesModified,
|
||||
projectScopedContainersAccessor,
|
||||
tileMapTileSelection,
|
||||
onSelectTileMapTile,
|
||||
}: Props) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
@@ -181,6 +188,9 @@ const CompactInstancePropertiesEditor = ({
|
||||
|
||||
if (!object || !instance || !instanceSchema) return null;
|
||||
|
||||
const shouldDisplayTileSetVisualizer =
|
||||
object.getType() === 'TileMap::SimpleTileMap';
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
componentTitle={<Trans>Instance properties</Trans>}
|
||||
@@ -202,6 +212,23 @@ const CompactInstancePropertiesEditor = ({
|
||||
onInstancesModified={onInstancesModified}
|
||||
/>
|
||||
</Column>
|
||||
{shouldDisplayTileSetVisualizer && (
|
||||
<>
|
||||
<Column>
|
||||
<Spacer />
|
||||
<Separator />
|
||||
<TileSetVisualizer
|
||||
project={project}
|
||||
objectConfiguration={object.getConfiguration()}
|
||||
tileMapTileSelection={tileMapTileSelection}
|
||||
onSelectTileMapTile={onSelectTileMapTile}
|
||||
showPaintingToolbar
|
||||
allowMultipleSelection={false}
|
||||
interactive
|
||||
/>
|
||||
</Column>
|
||||
</>
|
||||
)}
|
||||
{object && shouldDisplayVariablesList ? (
|
||||
<>
|
||||
<Column>
|
||||
|
@@ -159,7 +159,7 @@ export default class LayerRenderer {
|
||||
var renderedInstance:
|
||||
| RenderedInstance
|
||||
| Rendered3DInstance
|
||||
| null = this.getRendererOfInstance(instance);
|
||||
| null = this.getOrCreateRendererOfInstance(instance);
|
||||
if (!renderedInstance) return;
|
||||
|
||||
const pixiObject: PIXI.DisplayObject | null = renderedInstance.getPixiObject();
|
||||
@@ -233,6 +233,13 @@ export default class LayerRenderer {
|
||||
return this._threePlaneMesh;
|
||||
}
|
||||
|
||||
getRendererOfInstance(
|
||||
instance: gdInitialInstance
|
||||
): RenderedInstance | Rendered3DInstance | null {
|
||||
if (!this.renderedInstances.hasOwnProperty(instance.ptr)) return null;
|
||||
return this.renderedInstances[instance.ptr];
|
||||
}
|
||||
|
||||
getUnrotatedInstanceLeft = (instance: gdInitialInstance) => {
|
||||
return (
|
||||
instance.getX() -
|
||||
@@ -263,7 +270,7 @@ export default class LayerRenderer {
|
||||
};
|
||||
|
||||
getUnrotatedInstanceSize = (instance: gdInitialInstance) => {
|
||||
const renderedInstance = this.getRendererOfInstance(instance);
|
||||
const renderedInstance = this.getOrCreateRendererOfInstance(instance);
|
||||
const hasCustomSize = instance.hasCustomSize();
|
||||
const hasCustomDepth = instance.hasCustomDepth();
|
||||
const width = hasCustomSize
|
||||
@@ -369,7 +376,7 @@ export default class LayerRenderer {
|
||||
return bounds;
|
||||
}
|
||||
|
||||
getRendererOfInstance = (instance: gdInitialInstance) => {
|
||||
getOrCreateRendererOfInstance = (instance: gdInitialInstance) => {
|
||||
var renderedInstance = this.renderedInstances[instance.ptr];
|
||||
if (renderedInstance === undefined) {
|
||||
//No renderer associated yet, the instance must have been just created!...
|
||||
|
@@ -329,6 +329,14 @@ export default class InstancesRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
getRendererOfInstance(layerName: string, instance: gdInitialInstance) {
|
||||
if (!this.layersRenderers.hasOwnProperty(layerName)) {
|
||||
return null;
|
||||
}
|
||||
const layerRenderer = this.layersRenderers[layerName];
|
||||
return layerRenderer.getRendererOfInstance(instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up rendered layers that are not existing anymore
|
||||
*/
|
||||
|
@@ -31,6 +31,7 @@ type Props = {|
|
||||
keyboardShortcuts: KeyboardShortcuts,
|
||||
onPanMove: (deltaX: number, deltaY: number, x: number, y: number) => void,
|
||||
onPanEnd: () => void,
|
||||
getFillColor: (isLocked: boolean) => {| color: number, alpha: number |},
|
||||
|};
|
||||
|
||||
const getButtonSizes = (screenType: ScreenType) => {
|
||||
@@ -84,6 +85,7 @@ export default class SelectedInstances {
|
||||
keyboardShortcuts: KeyboardShortcuts;
|
||||
onPanMove: (deltaX: number, deltaY: number, x: number, y: number) => void;
|
||||
onPanEnd: () => void;
|
||||
getFillColor: (isLocked: boolean) => {| color: number, alpha: number |};
|
||||
|
||||
pixiContainer = new PIXI.Container();
|
||||
rectanglesContainer = new PIXI.Container();
|
||||
@@ -112,6 +114,7 @@ export default class SelectedInstances {
|
||||
keyboardShortcuts,
|
||||
onPanMove,
|
||||
onPanEnd,
|
||||
getFillColor,
|
||||
}: Props) {
|
||||
this.instanceMeasurer = instanceMeasurer;
|
||||
this.onResize = onResize;
|
||||
@@ -124,6 +127,7 @@ export default class SelectedInstances {
|
||||
this.keyboardShortcuts = keyboardShortcuts;
|
||||
this.onPanMove = onPanMove;
|
||||
this.onPanEnd = onPanEnd;
|
||||
this.getFillColor = getFillColor;
|
||||
|
||||
this.pixiContainer.addChild(this.rectanglesContainer);
|
||||
|
||||
@@ -326,9 +330,9 @@ export default class SelectedInstances {
|
||||
);
|
||||
|
||||
this.selectedRectangles[i].clear();
|
||||
const selectionRectangleColor = instance.isLocked() ? 0xbc5753 : 0x6868e8;
|
||||
this.selectedRectangles[i].beginFill(selectionRectangleColor);
|
||||
this.selectedRectangles[i].lineStyle(1, selectionRectangleColor, 1);
|
||||
const { color, alpha } = this.getFillColor(instance.isLocked());
|
||||
this.selectedRectangles[i].beginFill(color, alpha);
|
||||
this.selectedRectangles[i].lineStyle(1, color, 1);
|
||||
this.selectedRectangles[i].fill.alpha = 0.3;
|
||||
this.selectedRectangles[i].alpha = 0.8;
|
||||
this.selectedRectangles[i].drawRect(
|
||||
|
359
newIDE/app/src/InstancesEditor/TileMapTilePreview.js
Normal file
359
newIDE/app/src/InstancesEditor/TileMapTilePreview.js
Normal file
@@ -0,0 +1,359 @@
|
||||
// @flow
|
||||
|
||||
import * as PIXI from 'pixi.js-legacy';
|
||||
import getObjectByName from '../Utils/GetObjectByName';
|
||||
import InstancesSelection from './InstancesSelection';
|
||||
import PixiResourcesLoader from '../ObjectsRendering/PixiResourcesLoader';
|
||||
import ViewPosition from './ViewPosition';
|
||||
import RenderedInstance from '../ObjectsRendering/Renderers/RenderedInstance';
|
||||
import Rendered3DInstance from '../ObjectsRendering/Renderers/Rendered3DInstance';
|
||||
import { type TileMapTileSelection } from './TileSetVisualizer';
|
||||
import { AffineTransformation } from '../Utils/AffineTransformation';
|
||||
|
||||
export const updateSceneToTileMapTransformation = (
|
||||
instance: gdInitialInstance,
|
||||
renderedInstance: RenderedInstance | Rendered3DInstance,
|
||||
sceneToTileMapTransformation: AffineTransformation,
|
||||
tileMapToSceneTransformation: AffineTransformation
|
||||
): ?{ scaleX: number, scaleY: number } => {
|
||||
// TODO: Do not re-calculate if instance angle, position, dimensions and ptr are the same?
|
||||
let scaleX = 1,
|
||||
scaleY = 1;
|
||||
if (instance.hasCustomSize()) {
|
||||
if (
|
||||
// $FlowIgnore
|
||||
!renderedInstance.getEditableTileMap
|
||||
) {
|
||||
console.error(
|
||||
`Instance of ${instance.getObjectName()} seems to not be a RenderedSimpleTileMapInstance (method getEditableTileMap does not exist).`
|
||||
);
|
||||
return;
|
||||
}
|
||||
// $FlowIgnore
|
||||
const editableTileMap = renderedInstance.getEditableTileMap();
|
||||
if (!editableTileMap) {
|
||||
console.error(
|
||||
`Could not find the editable tile map for instance of object ${instance.getObjectName()}.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
scaleX = instance.getCustomWidth() / editableTileMap.getWidth();
|
||||
scaleY = instance.getCustomHeight() / editableTileMap.getHeight();
|
||||
}
|
||||
const absScaleX = Math.abs(scaleX);
|
||||
const absScaleY = Math.abs(scaleY);
|
||||
|
||||
tileMapToSceneTransformation.setToIdentity();
|
||||
|
||||
// Translation
|
||||
tileMapToSceneTransformation.translate(instance.getX(), instance.getY());
|
||||
|
||||
// Rotation
|
||||
const angleInRadians = (instance.getAngle() * Math.PI) / 180;
|
||||
tileMapToSceneTransformation.rotateAround(
|
||||
angleInRadians,
|
||||
renderedInstance.getCenterX(),
|
||||
renderedInstance.getCenterY()
|
||||
);
|
||||
|
||||
// Scale
|
||||
tileMapToSceneTransformation.scale(absScaleX, absScaleY);
|
||||
|
||||
sceneToTileMapTransformation.copyFrom(tileMapToSceneTransformation);
|
||||
sceneToTileMapTransformation.invert();
|
||||
return { scaleX, scaleY };
|
||||
};
|
||||
|
||||
export const getTileSet = (object: gdObject) => {
|
||||
const columnCount = parseFloat(
|
||||
object
|
||||
.getConfiguration()
|
||||
.getProperties()
|
||||
.get('columnCount')
|
||||
.getValue()
|
||||
);
|
||||
const rowCount = parseFloat(
|
||||
object
|
||||
.getConfiguration()
|
||||
.getProperties()
|
||||
.get('rowCount')
|
||||
.getValue()
|
||||
);
|
||||
const tileSize = parseFloat(
|
||||
object
|
||||
.getConfiguration()
|
||||
.getProperties()
|
||||
.get('tileSize')
|
||||
.getValue()
|
||||
);
|
||||
return { rowCount, columnCount, tileSize };
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the list of tiles corresponding to the user selection.
|
||||
* If only one coordinate is present, only one tile is places at the slot the
|
||||
* pointer points to.
|
||||
* If two coordinates are present, tiles are displayed to form a rectangle with the
|
||||
* two coordinates being the top left and bottom right corner of the rectangle.
|
||||
*/
|
||||
export const getTilesGridCoordinatesFromPointerSceneCoordinates = ({
|
||||
coordinates,
|
||||
tileSize,
|
||||
sceneToTileMapTransformation,
|
||||
}: {|
|
||||
coordinates: Array<{| x: number, y: number |}>,
|
||||
tileSize: number,
|
||||
sceneToTileMapTransformation: AffineTransformation,
|
||||
|}): Array<{| x: number, y: number |}> => {
|
||||
if (coordinates.length === 0) return [];
|
||||
|
||||
const tilesCoordinatesInTileMapGrid = [];
|
||||
|
||||
if (coordinates.length === 1) {
|
||||
const coordinatesInTileMapGrid = [0, 0];
|
||||
sceneToTileMapTransformation.transform(
|
||||
[coordinates[0].x, coordinates[0].y],
|
||||
coordinatesInTileMapGrid
|
||||
);
|
||||
coordinatesInTileMapGrid[0] = Math.floor(
|
||||
coordinatesInTileMapGrid[0] / tileSize
|
||||
);
|
||||
coordinatesInTileMapGrid[1] = Math.floor(
|
||||
coordinatesInTileMapGrid[1] / tileSize
|
||||
);
|
||||
tilesCoordinatesInTileMapGrid.push({
|
||||
x: coordinatesInTileMapGrid[0],
|
||||
y: coordinatesInTileMapGrid[1],
|
||||
});
|
||||
}
|
||||
if (coordinates.length === 2) {
|
||||
const topLeftCornerCoordinatesInTileMap = [0, 0];
|
||||
const bottomRightCornerCoordinatesInTileMap = [0, 0];
|
||||
|
||||
sceneToTileMapTransformation.transform(
|
||||
[
|
||||
Math.min(coordinates[0].x, coordinates[1].x),
|
||||
Math.min(coordinates[0].y, coordinates[1].y),
|
||||
],
|
||||
topLeftCornerCoordinatesInTileMap
|
||||
);
|
||||
topLeftCornerCoordinatesInTileMap[0] = Math.floor(
|
||||
topLeftCornerCoordinatesInTileMap[0] / tileSize
|
||||
);
|
||||
topLeftCornerCoordinatesInTileMap[1] = Math.floor(
|
||||
topLeftCornerCoordinatesInTileMap[1] / tileSize
|
||||
);
|
||||
|
||||
sceneToTileMapTransformation.transform(
|
||||
[
|
||||
Math.max(coordinates[0].x, coordinates[1].x),
|
||||
Math.max(coordinates[0].y, coordinates[1].y),
|
||||
],
|
||||
bottomRightCornerCoordinatesInTileMap
|
||||
);
|
||||
bottomRightCornerCoordinatesInTileMap[0] = Math.floor(
|
||||
bottomRightCornerCoordinatesInTileMap[0] / tileSize
|
||||
);
|
||||
bottomRightCornerCoordinatesInTileMap[1] = Math.floor(
|
||||
bottomRightCornerCoordinatesInTileMap[1] / tileSize
|
||||
);
|
||||
|
||||
for (
|
||||
let columnIndex = topLeftCornerCoordinatesInTileMap[0];
|
||||
columnIndex <= bottomRightCornerCoordinatesInTileMap[0];
|
||||
columnIndex++
|
||||
) {
|
||||
for (
|
||||
let rowIndex = topLeftCornerCoordinatesInTileMap[1];
|
||||
rowIndex <= bottomRightCornerCoordinatesInTileMap[1];
|
||||
rowIndex++
|
||||
) {
|
||||
tilesCoordinatesInTileMapGrid.push({ x: columnIndex, y: rowIndex });
|
||||
}
|
||||
}
|
||||
}
|
||||
return tilesCoordinatesInTileMapGrid;
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
layout: gdLayout | null,
|
||||
instancesSelection: InstancesSelection,
|
||||
getCoordinatesToRender: () => {| x: number, y: number |}[],
|
||||
getTileMapTileSelection: () => ?TileMapTileSelection,
|
||||
getRendererOfInstance: gdInitialInstance =>
|
||||
| RenderedInstance
|
||||
| Rendered3DInstance
|
||||
| null,
|
||||
viewPosition: ViewPosition,
|
||||
|};
|
||||
|
||||
class TileMapTilePreview {
|
||||
project: gdProject;
|
||||
layout: gdLayout | null;
|
||||
instancesSelection: InstancesSelection;
|
||||
getCoordinatesToRender: () => {| x: number, y: number |}[];
|
||||
getTileMapTileSelection: () => ?TileMapTileSelection;
|
||||
getRendererOfInstance: gdInitialInstance =>
|
||||
| RenderedInstance
|
||||
| Rendered3DInstance
|
||||
| null;
|
||||
toCanvasCoordinates: (x: number, y: number) => [number, number];
|
||||
viewPosition: ViewPosition;
|
||||
cache: Map<string, PIXI.Texture>;
|
||||
sceneToTileMapTransformation: AffineTransformation;
|
||||
tileMapToSceneTransformation: AffineTransformation;
|
||||
|
||||
preview: PIXI.Container;
|
||||
|
||||
constructor({
|
||||
instancesSelection,
|
||||
getCoordinatesToRender,
|
||||
project,
|
||||
layout,
|
||||
getTileMapTileSelection,
|
||||
getRendererOfInstance,
|
||||
viewPosition,
|
||||
}: Props) {
|
||||
this.project = project;
|
||||
this.layout = layout;
|
||||
this.instancesSelection = instancesSelection;
|
||||
this.getCoordinatesToRender = getCoordinatesToRender;
|
||||
this.getTileMapTileSelection = getTileMapTileSelection;
|
||||
this.getRendererOfInstance = getRendererOfInstance;
|
||||
this.viewPosition = viewPosition;
|
||||
this.preview = new PIXI.Container();
|
||||
this.cache = new Map();
|
||||
this.sceneToTileMapTransformation = new AffineTransformation();
|
||||
this.tileMapToSceneTransformation = new AffineTransformation();
|
||||
}
|
||||
|
||||
getPixiObject(): PIXI.Container {
|
||||
return this.preview;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.preview.removeChildren(0);
|
||||
const tileMapTileSelection = this.getTileMapTileSelection();
|
||||
if (!tileMapTileSelection) {
|
||||
return;
|
||||
}
|
||||
const selection = this.instancesSelection.getSelectedInstances();
|
||||
if (selection.length !== 1) return;
|
||||
const instance = selection[0];
|
||||
const associatedObjectName = instance.getObjectName();
|
||||
const object = getObjectByName(
|
||||
this.project.getObjects(),
|
||||
this.layout ? this.layout.getObjects() : null,
|
||||
associatedObjectName
|
||||
);
|
||||
if (!object || object.getType() !== 'TileMap::SimpleTileMap') return;
|
||||
const { tileSize } = getTileSet(object);
|
||||
let texture;
|
||||
if (tileMapTileSelection.kind === 'single') {
|
||||
const atlasResourceName = object
|
||||
.getConfiguration()
|
||||
.getProperties()
|
||||
.get('atlasImage')
|
||||
.getValue();
|
||||
if (!atlasResourceName) return;
|
||||
const cacheKey = `${atlasResourceName}-${tileSize}-${
|
||||
tileMapTileSelection.coordinates.x
|
||||
}-${tileMapTileSelection.coordinates.y}`;
|
||||
texture = this.cache.get(cacheKey);
|
||||
if (!texture) {
|
||||
const atlasTexture = PixiResourcesLoader.getPIXITexture(
|
||||
this.project,
|
||||
atlasResourceName
|
||||
);
|
||||
|
||||
const rect = new PIXI.Rectangle(
|
||||
tileMapTileSelection.coordinates.x * tileSize,
|
||||
tileMapTileSelection.coordinates.y * tileSize,
|
||||
tileSize,
|
||||
tileSize
|
||||
);
|
||||
|
||||
texture = new PIXI.Texture(atlasTexture, rect);
|
||||
this.cache.set(cacheKey, texture);
|
||||
}
|
||||
} else if (tileMapTileSelection.kind === 'erase') {
|
||||
texture = PIXI.Texture.from(
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAARSURBVHgBY7h58+Z/BhgAcQA/VAcVLiw46wAAAABJRU5ErkJggg=='
|
||||
);
|
||||
texture.baseTexture.scaleMode = PIXI.SCALE_MODES.NEAREST;
|
||||
}
|
||||
|
||||
const renderedInstance = this.getRendererOfInstance(instance);
|
||||
if (
|
||||
!renderedInstance ||
|
||||
renderedInstance.constructor.name !== 'RenderedSimpleTileMapInstance'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scales = updateSceneToTileMapTransformation(
|
||||
instance,
|
||||
renderedInstance,
|
||||
this.sceneToTileMapTransformation,
|
||||
this.tileMapToSceneTransformation
|
||||
);
|
||||
if (!scales) return;
|
||||
const { scaleX, scaleY } = scales;
|
||||
const coordinates = this.getCoordinatesToRender();
|
||||
if (coordinates.length === 0) return;
|
||||
const tileSizeInCanvas = this.viewPosition.toCanvasScale(tileSize);
|
||||
const spriteWidth = tileSizeInCanvas * scaleX;
|
||||
const spriteHeight = tileSizeInCanvas * scaleY;
|
||||
|
||||
const spritesCoordinatesInTileMapGrid = getTilesGridCoordinatesFromPointerSceneCoordinates(
|
||||
{
|
||||
coordinates,
|
||||
tileSize,
|
||||
sceneToTileMapTransformation: this.sceneToTileMapTransformation,
|
||||
}
|
||||
);
|
||||
|
||||
const workingPoint = [0, 0];
|
||||
|
||||
spritesCoordinatesInTileMapGrid.forEach(({ x, y }) => {
|
||||
let sprite;
|
||||
|
||||
if (tileMapTileSelection.kind === 'single') {
|
||||
// TODO: Find a way not to regenerate the sprites on each render.
|
||||
sprite = new PIXI.Sprite(texture);
|
||||
if (tileMapTileSelection.flipHorizontally) {
|
||||
sprite.scale.x *= -1;
|
||||
}
|
||||
if (tileMapTileSelection.flipVertically) {
|
||||
sprite.scale.y *= -1;
|
||||
}
|
||||
} else {
|
||||
sprite = new PIXI.TilingSprite(texture, 2, 2);
|
||||
sprite.tileScale.x = this.viewPosition.toCanvasScale(scaleX);
|
||||
sprite.tileScale.y = this.viewPosition.toCanvasScale(scaleY);
|
||||
}
|
||||
sprite.anchor.x = 0.5;
|
||||
sprite.anchor.y = 0.5;
|
||||
sprite.width = spriteWidth;
|
||||
sprite.height = spriteHeight;
|
||||
|
||||
this.tileMapToSceneTransformation.transform(
|
||||
[x * tileSize + tileSize / 2, y * tileSize + tileSize / 2],
|
||||
workingPoint
|
||||
);
|
||||
|
||||
sprite.x = this.viewPosition.toCanvasScale(workingPoint[0]);
|
||||
sprite.y = this.viewPosition.toCanvasScale(workingPoint[1]);
|
||||
sprite.angle = instance.getAngle();
|
||||
|
||||
this.preview.addChild(sprite);
|
||||
});
|
||||
|
||||
const canvasCoordinates = this.viewPosition.toCanvasCoordinates(0, 0);
|
||||
this.preview.position.x = canvasCoordinates[0];
|
||||
this.preview.position.y = canvasCoordinates[1];
|
||||
}
|
||||
}
|
||||
|
||||
export default TileMapTilePreview;
|
640
newIDE/app/src/InstancesEditor/TileSetVisualizer.js
Normal file
640
newIDE/app/src/InstancesEditor/TileSetVisualizer.js
Normal file
@@ -0,0 +1,640 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import { createStyles, makeStyles } from '@material-ui/core/styles';
|
||||
import { Column, Line } from '../UI/Grid';
|
||||
import { CorsAwareImage } from '../UI/CorsAwareImage';
|
||||
import ResourcesLoader from '../ResourcesLoader';
|
||||
import Erase from '../UI/CustomSvgIcons/Erase';
|
||||
import Brush from '../UI/CustomSvgIcons/Brush';
|
||||
import IconButton from '../UI/IconButton';
|
||||
import { LineStackLayout } from '../UI/Layout';
|
||||
import FlipHorizontal from '../UI/CustomSvgIcons/FlipHorizontal';
|
||||
import FlipVertical from '../UI/CustomSvgIcons/FlipVertical';
|
||||
import useForceUpdate from '../Utils/UseForceUpdate';
|
||||
|
||||
const styles = {
|
||||
tileContainer: { flex: 1, position: 'relative', display: 'flex' },
|
||||
atlasImage: { flex: 1, imageRendering: 'pixelated' },
|
||||
icon: { fontSize: 18 },
|
||||
};
|
||||
|
||||
const useStylesForTile = (highlighted: boolean) =>
|
||||
makeStyles(theme =>
|
||||
createStyles({
|
||||
tile: {
|
||||
position: 'absolute',
|
||||
boxSizing: 'border-box',
|
||||
border: highlighted ? '2px solid red' : undefined,
|
||||
'&:hover': {
|
||||
border: highlighted ? '2px solid pink' : '1px solid white',
|
||||
},
|
||||
},
|
||||
})
|
||||
)();
|
||||
|
||||
type TileMapCoordinates = {| x: number, y: number |};
|
||||
|
||||
/**
|
||||
* Returns the tile id in a tile set.
|
||||
* This id corresponds to the index of the tile if the tile set
|
||||
* is flattened so that each column is put right after the previous one.
|
||||
* Example:
|
||||
* 1 | 4 | 7
|
||||
* 2 | 5 | 8
|
||||
* 3 | 6 | 9
|
||||
* @param argument Object that contains x the horizontal position of the tile, y the vertical position and rowCount the number of rows in the tile set.
|
||||
* @returns the id of the tile.
|
||||
*/
|
||||
export const getTileIdFromGridCoordinates = ({
|
||||
x,
|
||||
y,
|
||||
rowCount,
|
||||
}: {|
|
||||
x: number,
|
||||
y: number,
|
||||
rowCount: number,
|
||||
|}): number => x * rowCount + y;
|
||||
|
||||
/**
|
||||
* Returns the coordinates of a tile in a tile set given its id.
|
||||
* This id corresponds to the index of the tile if the tile set
|
||||
* is flattened so that each column is put right after the previous one.
|
||||
* Example:
|
||||
* 1 | 4 | 7
|
||||
* 2 | 5 | 8
|
||||
* 3 | 6 | 9
|
||||
* @param argument Object that contains id the id of the tile and rowCount the number of rows in the tile set.
|
||||
* @returns the id of the tile.
|
||||
*/
|
||||
export const getGridCoordinatesFromTileId = ({
|
||||
id,
|
||||
rowCount,
|
||||
}: {|
|
||||
id: number,
|
||||
rowCount: number,
|
||||
|}): {| x: number, y: number |} => {
|
||||
const y = id % rowCount;
|
||||
const x = (id - y) / rowCount;
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
const getGridCoordinatesFromPointerCoordinates = ({
|
||||
displayedTileSize,
|
||||
pointerX,
|
||||
pointerY,
|
||||
columnCount,
|
||||
rowCount,
|
||||
}: {|
|
||||
displayedTileSize: number,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
columnCount: number,
|
||||
rowCount: number,
|
||||
|}): TileMapCoordinates => {
|
||||
const x = Math.min(Math.floor(pointerX / displayedTileSize), columnCount - 1);
|
||||
const y = Math.min(Math.floor(pointerY / displayedTileSize), rowCount - 1);
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
const addOrRemoveCoordinatesInArray = (
|
||||
array: TileMapCoordinates[],
|
||||
newCoordinates: TileMapCoordinates
|
||||
) => {
|
||||
const indexInArray = array.findIndex(
|
||||
coordinates =>
|
||||
coordinates.x === newCoordinates.x && coordinates.y === newCoordinates.y
|
||||
);
|
||||
if (indexInArray === -1) {
|
||||
array.push(newCoordinates);
|
||||
} else {
|
||||
array.splice(indexInArray, 1);
|
||||
}
|
||||
};
|
||||
|
||||
type TileProps = {|
|
||||
x: number,
|
||||
y: number,
|
||||
size: number,
|
||||
highlighted?: boolean,
|
||||
width?: number,
|
||||
height?: number,
|
||||
title?: string,
|
||||
|};
|
||||
|
||||
const Tile = ({
|
||||
x,
|
||||
y,
|
||||
size,
|
||||
width = 1,
|
||||
height = 1,
|
||||
highlighted,
|
||||
title,
|
||||
}: TileProps) => {
|
||||
const classes = useStylesForTile(!!highlighted);
|
||||
return (
|
||||
<div
|
||||
className={classes.tile}
|
||||
style={{
|
||||
left: x * size,
|
||||
top: y * size,
|
||||
width: size * width,
|
||||
height: size * height,
|
||||
}}
|
||||
// TODO: find a way to display title on mobile.
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export type TileMapTileSelection =
|
||||
| {|
|
||||
kind: 'single',
|
||||
coordinates: TileMapCoordinates,
|
||||
flipHorizontally: boolean,
|
||||
flipVertically: boolean,
|
||||
|}
|
||||
| {|
|
||||
kind: 'multiple',
|
||||
coordinates: TileMapCoordinates[],
|
||||
|}
|
||||
| {|
|
||||
kind: 'erase',
|
||||
|};
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
objectConfiguration: gdObjectConfiguration,
|
||||
tileMapTileSelection: ?TileMapTileSelection,
|
||||
onSelectTileMapTile: (?TileMapTileSelection) => void,
|
||||
allowMultipleSelection: boolean,
|
||||
showPaintingToolbar: boolean,
|
||||
interactive: boolean,
|
||||
onAtlasImageLoaded?: (
|
||||
e: SyntheticEvent<HTMLImageElement>,
|
||||
atlasResourceName: string
|
||||
) => void,
|
||||
|};
|
||||
|
||||
const TileSetVisualizer = ({
|
||||
project,
|
||||
objectConfiguration,
|
||||
tileMapTileSelection,
|
||||
onSelectTileMapTile,
|
||||
allowMultipleSelection,
|
||||
showPaintingToolbar,
|
||||
interactive,
|
||||
onAtlasImageLoaded,
|
||||
}: Props) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
const atlasResourceName = objectConfiguration
|
||||
.getProperties()
|
||||
.get('atlasImage')
|
||||
.getValue();
|
||||
const [
|
||||
shouldFlipVertically,
|
||||
setShouldFlipVertically,
|
||||
] = React.useState<boolean>(false);
|
||||
const [
|
||||
shouldFlipHorizontally,
|
||||
setShouldFlipHorizontally,
|
||||
] = React.useState<boolean>(false);
|
||||
const [
|
||||
lastSelectedTile,
|
||||
setLastSelectedTile,
|
||||
] = React.useState<?TileMapCoordinates>(null);
|
||||
const tileContainerRef = React.useRef<?HTMLDivElement>(null);
|
||||
const columnCount = parseFloat(
|
||||
objectConfiguration
|
||||
.getProperties()
|
||||
.get('columnCount')
|
||||
.getValue()
|
||||
);
|
||||
const rowCount = parseFloat(
|
||||
objectConfiguration
|
||||
.getProperties()
|
||||
.get('rowCount')
|
||||
.getValue()
|
||||
);
|
||||
const [clickStartCoordinates, setClickStartCoordinates] = React.useState<?{|
|
||||
x: number,
|
||||
y: number,
|
||||
|}>(null);
|
||||
const [
|
||||
rectangularSelectionTilePreview,
|
||||
setRectangularSelectionTilePreview,
|
||||
] = React.useState<?{|
|
||||
topLeftCoordinates: TileMapCoordinates,
|
||||
width: number,
|
||||
height: number,
|
||||
|}>(null);
|
||||
|
||||
const [hoveredTile, setHoveredTile] = React.useState<?{
|
||||
x: number,
|
||||
y: number,
|
||||
}>(null);
|
||||
|
||||
const imageWidth = tileContainerRef.current
|
||||
? parseFloat(
|
||||
getComputedStyle(tileContainerRef.current).width.replace('px', '')
|
||||
)
|
||||
: 0;
|
||||
const displayedTileSize = imageWidth ? imageWidth / columnCount : null;
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
forceUpdate();
|
||||
},
|
||||
// Force update component after first mount to make sure displayedTileSize
|
||||
// can be computed after ref has been set.
|
||||
[forceUpdate]
|
||||
);
|
||||
|
||||
const onPointerDown = React.useCallback((event: PointerEvent) => {
|
||||
if (!(event.currentTarget instanceof HTMLDivElement)) {
|
||||
return;
|
||||
}
|
||||
const bounds = event.currentTarget.getBoundingClientRect();
|
||||
const mouseX = event.clientX - bounds.left + 1;
|
||||
const mouseY = event.clientY - bounds.top + 1;
|
||||
|
||||
setClickStartCoordinates({ x: mouseX, y: mouseY });
|
||||
}, []);
|
||||
|
||||
const onPointerMove = React.useCallback(
|
||||
(event: PointerEvent) => {
|
||||
if (
|
||||
!clickStartCoordinates ||
|
||||
!(event.currentTarget instanceof HTMLDivElement) ||
|
||||
!displayedTileSize ||
|
||||
!allowMultipleSelection
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const bounds = event.currentTarget.getBoundingClientRect();
|
||||
|
||||
const mouseX = event.clientX - bounds.left + 1;
|
||||
const mouseY = event.clientY - bounds.top + 1;
|
||||
const { x, y } = getGridCoordinatesFromPointerCoordinates({
|
||||
pointerX: mouseX,
|
||||
pointerY: mouseY,
|
||||
columnCount,
|
||||
rowCount,
|
||||
displayedTileSize,
|
||||
});
|
||||
const { x: startX, y: startY } = getGridCoordinatesFromPointerCoordinates(
|
||||
{
|
||||
pointerX: clickStartCoordinates.x,
|
||||
pointerY: clickStartCoordinates.y,
|
||||
columnCount,
|
||||
rowCount,
|
||||
displayedTileSize,
|
||||
}
|
||||
);
|
||||
|
||||
setRectangularSelectionTilePreview({
|
||||
topLeftCoordinates: {
|
||||
x: Math.min(x, startX),
|
||||
y: Math.min(y, startY),
|
||||
},
|
||||
width: Math.abs(x - startX) + 1,
|
||||
height: Math.abs(y - startY) + 1,
|
||||
});
|
||||
},
|
||||
[
|
||||
displayedTileSize,
|
||||
columnCount,
|
||||
rowCount,
|
||||
allowMultipleSelection,
|
||||
clickStartCoordinates,
|
||||
]
|
||||
);
|
||||
|
||||
const onPointerUp = React.useCallback(
|
||||
(event: MouseEvent) => {
|
||||
try {
|
||||
if (
|
||||
!(event.currentTarget instanceof HTMLDivElement) ||
|
||||
!displayedTileSize
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const bounds = event.currentTarget.getBoundingClientRect();
|
||||
|
||||
const mouseX = event.clientX - bounds.left + 1;
|
||||
const mouseY = event.clientY - bounds.top + 1;
|
||||
const { x, y } = getGridCoordinatesFromPointerCoordinates({
|
||||
pointerX: mouseX,
|
||||
pointerY: mouseY,
|
||||
columnCount,
|
||||
rowCount,
|
||||
displayedTileSize,
|
||||
});
|
||||
if (!allowMultipleSelection) {
|
||||
if (
|
||||
tileMapTileSelection &&
|
||||
tileMapTileSelection.kind === 'single' &&
|
||||
tileMapTileSelection.coordinates.x === x &&
|
||||
tileMapTileSelection.coordinates.y === y
|
||||
) {
|
||||
onSelectTileMapTile(null);
|
||||
} else {
|
||||
onSelectTileMapTile({
|
||||
kind: 'single',
|
||||
coordinates: { x, y },
|
||||
flipHorizontally: shouldFlipHorizontally,
|
||||
flipVertically: shouldFlipVertically,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!clickStartCoordinates) return;
|
||||
|
||||
const {
|
||||
x: startX,
|
||||
y: startY,
|
||||
} = getGridCoordinatesFromPointerCoordinates({
|
||||
pointerX: clickStartCoordinates.x,
|
||||
pointerY: clickStartCoordinates.y,
|
||||
columnCount,
|
||||
rowCount,
|
||||
displayedTileSize,
|
||||
});
|
||||
const newSelection =
|
||||
tileMapTileSelection && tileMapTileSelection.kind === 'multiple'
|
||||
? { ...tileMapTileSelection }
|
||||
: { kind: 'multiple', coordinates: [] };
|
||||
if (startX === x && startY === y) {
|
||||
if (
|
||||
tileMapTileSelection &&
|
||||
// Click on a tile.
|
||||
tileMapTileSelection.kind === 'multiple'
|
||||
) {
|
||||
addOrRemoveCoordinatesInArray(newSelection.coordinates, {
|
||||
x,
|
||||
y,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for (
|
||||
let columnIndex = Math.min(startX, x);
|
||||
columnIndex <= Math.max(startX, x);
|
||||
columnIndex++
|
||||
) {
|
||||
for (
|
||||
let rowIndex = Math.min(startY, y);
|
||||
rowIndex <= Math.max(startY, y);
|
||||
rowIndex++
|
||||
) {
|
||||
if (newSelection && newSelection.kind === 'multiple') {
|
||||
addOrRemoveCoordinatesInArray(newSelection.coordinates, {
|
||||
x: columnIndex,
|
||||
y: rowIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onSelectTileMapTile(newSelection);
|
||||
} finally {
|
||||
setClickStartCoordinates(null);
|
||||
setRectangularSelectionTilePreview(null);
|
||||
}
|
||||
},
|
||||
[
|
||||
displayedTileSize,
|
||||
columnCount,
|
||||
rowCount,
|
||||
tileMapTileSelection,
|
||||
onSelectTileMapTile,
|
||||
shouldFlipHorizontally,
|
||||
shouldFlipVertically,
|
||||
allowMultipleSelection,
|
||||
clickStartCoordinates,
|
||||
]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
// On dismount, remove tile map selection.
|
||||
return () => onSelectTileMapTile(null);
|
||||
},
|
||||
[onSelectTileMapTile]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (tileMapTileSelection && tileMapTileSelection.kind === 'single') {
|
||||
setLastSelectedTile({
|
||||
x: tileMapTileSelection.coordinates.x,
|
||||
y: tileMapTileSelection.coordinates.y,
|
||||
});
|
||||
}
|
||||
},
|
||||
[tileMapTileSelection]
|
||||
);
|
||||
|
||||
const onHoverAtlas = React.useCallback(
|
||||
(event: MouseEvent) => {
|
||||
if (
|
||||
!(event.currentTarget instanceof HTMLDivElement) ||
|
||||
!displayedTileSize
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const bounds = event.currentTarget.getBoundingClientRect();
|
||||
|
||||
const mouseX = event.clientX - bounds.left + 1;
|
||||
const mouseY = event.clientY - bounds.top + 1;
|
||||
const { x, y } = getGridCoordinatesFromPointerCoordinates({
|
||||
pointerX: mouseX,
|
||||
pointerY: mouseY,
|
||||
columnCount,
|
||||
rowCount,
|
||||
displayedTileSize,
|
||||
});
|
||||
setHoveredTile({ x, y });
|
||||
},
|
||||
[displayedTileSize, columnCount, rowCount]
|
||||
);
|
||||
|
||||
const interactionCallbacks = {
|
||||
onMouseMove: onHoverAtlas,
|
||||
onPointerDown: onPointerDown,
|
||||
onPointerUp: onPointerUp,
|
||||
onPointerMove: onPointerMove,
|
||||
};
|
||||
|
||||
return (
|
||||
<Column noMargin>
|
||||
{showPaintingToolbar && (
|
||||
<LineStackLayout alignItems="center">
|
||||
<IconButton
|
||||
size="small"
|
||||
selected={
|
||||
!!tileMapTileSelection && tileMapTileSelection.kind === 'erase'
|
||||
}
|
||||
onClick={e => {
|
||||
if (
|
||||
!!tileMapTileSelection &&
|
||||
tileMapTileSelection.kind === 'erase'
|
||||
)
|
||||
onSelectTileMapTile(null);
|
||||
else onSelectTileMapTile({ kind: 'erase' });
|
||||
}}
|
||||
>
|
||||
<Erase style={styles.icon} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
selected={
|
||||
!!tileMapTileSelection && tileMapTileSelection.kind === 'single'
|
||||
}
|
||||
onClick={e => {
|
||||
if (
|
||||
!!tileMapTileSelection &&
|
||||
tileMapTileSelection.kind === 'single'
|
||||
)
|
||||
onSelectTileMapTile(null);
|
||||
else
|
||||
onSelectTileMapTile({
|
||||
kind: 'single',
|
||||
coordinates: lastSelectedTile || { x: 0, y: 0 },
|
||||
flipHorizontally: shouldFlipHorizontally,
|
||||
flipVertically: shouldFlipVertically,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Brush style={styles.icon} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
selected={shouldFlipHorizontally}
|
||||
onClick={e => {
|
||||
const newShouldFlipHorizontally = !shouldFlipHorizontally;
|
||||
setShouldFlipHorizontally(newShouldFlipHorizontally);
|
||||
if (
|
||||
!!tileMapTileSelection &&
|
||||
tileMapTileSelection.kind === 'single'
|
||||
) {
|
||||
onSelectTileMapTile({
|
||||
...tileMapTileSelection,
|
||||
flipHorizontally: newShouldFlipHorizontally,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FlipHorizontal style={styles.icon} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
selected={shouldFlipVertically}
|
||||
onClick={e => {
|
||||
const newShouldFlipVertically = !shouldFlipVertically;
|
||||
setShouldFlipVertically(newShouldFlipVertically);
|
||||
if (
|
||||
!!tileMapTileSelection &&
|
||||
tileMapTileSelection.kind === 'single'
|
||||
) {
|
||||
onSelectTileMapTile({
|
||||
...tileMapTileSelection,
|
||||
flipVertically: newShouldFlipVertically,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FlipVertical style={styles.icon} />
|
||||
</IconButton>
|
||||
</LineStackLayout>
|
||||
)}
|
||||
<Line justifyContent="stretch">
|
||||
{atlasResourceName && (
|
||||
<div
|
||||
style={styles.tileContainer}
|
||||
ref={tileContainerRef}
|
||||
{...(interactive ? interactionCallbacks : undefined)}
|
||||
>
|
||||
<CorsAwareImage
|
||||
style={styles.atlasImage}
|
||||
alt={atlasResourceName}
|
||||
src={ResourcesLoader.getResourceFullUrl(
|
||||
project,
|
||||
atlasResourceName,
|
||||
{}
|
||||
)}
|
||||
onLoad={e => {
|
||||
if (onAtlasImageLoaded)
|
||||
onAtlasImageLoaded(e, atlasResourceName);
|
||||
}}
|
||||
/>
|
||||
|
||||
{interactive && hoveredTile && displayedTileSize && (
|
||||
<Tile
|
||||
key={`hovered-tile`}
|
||||
size={displayedTileSize}
|
||||
x={hoveredTile.x}
|
||||
y={hoveredTile.y}
|
||||
title={getTileIdFromGridCoordinates({
|
||||
x: hoveredTile.x,
|
||||
y: hoveredTile.y,
|
||||
rowCount,
|
||||
}).toString()}
|
||||
/>
|
||||
)}
|
||||
{tileMapTileSelection &&
|
||||
tileMapTileSelection.kind === 'single' &&
|
||||
displayedTileSize && (
|
||||
<Tile
|
||||
key={`selected-tile`}
|
||||
highlighted
|
||||
size={displayedTileSize}
|
||||
x={tileMapTileSelection.coordinates.x}
|
||||
y={tileMapTileSelection.coordinates.y}
|
||||
title={getTileIdFromGridCoordinates({
|
||||
x: tileMapTileSelection.coordinates.x,
|
||||
y: tileMapTileSelection.coordinates.y,
|
||||
rowCount,
|
||||
}).toString()}
|
||||
/>
|
||||
)}
|
||||
{tileMapTileSelection &&
|
||||
tileMapTileSelection.kind === 'multiple' &&
|
||||
displayedTileSize &&
|
||||
tileMapTileSelection.coordinates.map(coordinates => {
|
||||
const id = getTileIdFromGridCoordinates({
|
||||
x: coordinates.x,
|
||||
y: coordinates.y,
|
||||
rowCount,
|
||||
});
|
||||
return (
|
||||
<Tile
|
||||
key={`selected-tile-${id}`}
|
||||
highlighted
|
||||
size={displayedTileSize}
|
||||
x={coordinates.x}
|
||||
y={coordinates.y}
|
||||
title={id.toString()}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{interactive &&
|
||||
rectangularSelectionTilePreview &&
|
||||
displayedTileSize && (
|
||||
<Tile
|
||||
key={`preview-tile`}
|
||||
highlighted
|
||||
size={displayedTileSize}
|
||||
x={rectangularSelectionTilePreview.topLeftCoordinates.x}
|
||||
y={rectangularSelectionTilePreview.topLeftCoordinates.y}
|
||||
width={rectangularSelectionTilePreview.width}
|
||||
height={rectangularSelectionTilePreview.height}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Line>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default TileSetVisualizer;
|
@@ -90,6 +90,11 @@ export default class ViewPosition {
|
||||
this.instancesEditorSettings.zoomFactor === 0
|
||||
? a
|
||||
: a / Math.abs(this.instancesEditorSettings.zoomFactor);
|
||||
/**
|
||||
* Convert a length from scene referential to canvas referential.
|
||||
*/
|
||||
toCanvasScale = (a: number): number =>
|
||||
a * Math.abs(this.instancesEditorSettings.zoomFactor);
|
||||
|
||||
/**
|
||||
* Convert a point from the "world" coordinates (for example, an object position) to the
|
||||
|
@@ -42,6 +42,15 @@ import {
|
||||
getWheelStepZoomFactor,
|
||||
} from '../Utils/ZoomUtils';
|
||||
import Background from './Background';
|
||||
import TileMapTilePreview, {
|
||||
getTileSet,
|
||||
getTilesGridCoordinatesFromPointerSceneCoordinates,
|
||||
updateSceneToTileMapTransformation,
|
||||
} from './TileMapTilePreview';
|
||||
import { type TileMapTileSelection } from './TileSetVisualizer';
|
||||
import ClickInterceptor from './ClickInterceptor';
|
||||
import getObjectByName from '../Utils/GetObjectByName';
|
||||
import { AffineTransformation } from '../Utils/AffineTransformation';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
export const instancesEditorId = 'instances-editor-canvas';
|
||||
@@ -96,6 +105,8 @@ export type InstancesEditorPropsWithoutSizeAndScroll = {|
|
||||
) => void,
|
||||
pauseRendering: boolean,
|
||||
instancesEditorShortcutsCallbacks: InstancesEditorShortcutsCallbacks,
|
||||
tileMapTileSelection: ?TileMapTileSelection,
|
||||
onSelectTileMapTile: (?TileMapTileSelection) => void,
|
||||
|};
|
||||
|
||||
type Props = {|
|
||||
@@ -124,6 +135,8 @@ export default class InstancesEditor extends Component<Props> {
|
||||
_instancesAdder: InstancesAdder;
|
||||
selectionRectangle: SelectionRectangle;
|
||||
selectedInstances: SelectedInstances;
|
||||
tileMapTilePreview: TileMapTilePreview;
|
||||
clickInterceptor: ClickInterceptor;
|
||||
highlightedInstance: HighlightedInstance;
|
||||
instancesResizer: InstancesResizer;
|
||||
instancesRotator: InstancesRotator;
|
||||
@@ -174,6 +187,7 @@ export default class InstancesEditor extends Component<Props> {
|
||||
this.keyboardShortcuts = new KeyboardShortcuts({
|
||||
shortcutCallbacks: {
|
||||
onMove: this.moveSelection,
|
||||
onEscape: this.onPressEscape,
|
||||
...this.props.instancesEditorShortcutsCallbacks,
|
||||
},
|
||||
});
|
||||
@@ -410,6 +424,12 @@ export default class InstancesEditor extends Component<Props> {
|
||||
this.highlightedInstance.getPixiObject()
|
||||
);
|
||||
}
|
||||
if (this.tileMapTilePreview) {
|
||||
this.uiPixiContainer.removeChild(this.tileMapTilePreview.getPixiObject());
|
||||
}
|
||||
if (this.clickInterceptor) {
|
||||
this.uiPixiContainer.removeChild(this.clickInterceptor.getPixiObject());
|
||||
}
|
||||
if (this.selectedInstances) {
|
||||
this.uiPixiContainer.removeChild(
|
||||
this.selectedInstances.getPixiContainer()
|
||||
@@ -470,11 +490,26 @@ export default class InstancesEditor extends Component<Props> {
|
||||
onRotateEnd: this._onRotateEnd,
|
||||
instanceMeasurer: this.instancesRenderer.getInstanceMeasurer(),
|
||||
toCanvasCoordinates: this.viewPosition.toCanvasCoordinates,
|
||||
getFillColor: this.getSelectedInstancesObjectFillColor,
|
||||
screenType: this.props.screenType,
|
||||
keyboardShortcuts: this.keyboardShortcuts,
|
||||
onPanMove: this._onPanMove,
|
||||
onPanEnd: this._onPanEnd,
|
||||
});
|
||||
this.tileMapTilePreview = new TileMapTilePreview({
|
||||
instancesSelection: this.props.instancesSelection,
|
||||
project: props.project,
|
||||
layout: props.layout,
|
||||
getTileMapTileSelection: this.getTileMapTileSelection,
|
||||
getRendererOfInstance: this.getRendererOfInstance,
|
||||
getCoordinatesToRender: this.getCoordinatesToRenderTileMapPreview,
|
||||
viewPosition: this.viewPosition,
|
||||
});
|
||||
this.clickInterceptor = new ClickInterceptor({
|
||||
getTileMapTileSelection: this.getTileMapTileSelection,
|
||||
viewPosition: this.viewPosition,
|
||||
onClick: this._onInterceptClick,
|
||||
});
|
||||
this.highlightedInstance = new HighlightedInstance({
|
||||
instanceMeasurer: this.instancesRenderer.getInstanceMeasurer(),
|
||||
toCanvasCoordinates: this.viewPosition.toCanvasCoordinates,
|
||||
@@ -515,6 +550,8 @@ export default class InstancesEditor extends Component<Props> {
|
||||
this.uiPixiContainer.addChild(this.selectedInstances.getPixiContainer());
|
||||
this.uiPixiContainer.addChild(this.highlightedInstance.getPixiObject());
|
||||
this.uiPixiContainer.addChild(this.statusBar.getPixiObject());
|
||||
this.uiPixiContainer.addChild(this.tileMapTilePreview.getPixiObject());
|
||||
this.uiPixiContainer.addChild(this.clickInterceptor.getPixiObject());
|
||||
|
||||
this.background = new Background({
|
||||
width: this.props.width,
|
||||
@@ -658,6 +695,17 @@ export default class InstancesEditor extends Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
getTileMapTileSelection = () => {
|
||||
return this.props.tileMapTileSelection;
|
||||
};
|
||||
|
||||
getSelectedInstancesObjectFillColor = (
|
||||
isLocked: boolean
|
||||
): {| color: number, alpha: number |} => {
|
||||
if (this.props.tileMapTileSelection) return { color: 0xfff, alpha: 0 };
|
||||
return { color: isLocked ? 0xbc5753 : 0x6868e8, alpha: 1 };
|
||||
};
|
||||
|
||||
getZoomFactor = () => {
|
||||
return this.props.instancesEditorSettings.zoomFactor;
|
||||
};
|
||||
@@ -703,6 +751,154 @@ export default class InstancesEditor extends Component<Props> {
|
||||
this.lastCursorY = y;
|
||||
};
|
||||
|
||||
_onInterceptClick = (sceneCoordinates: Array<{| x: number, y: number |}>) => {
|
||||
const {
|
||||
tileMapTileSelection,
|
||||
instancesSelection,
|
||||
project,
|
||||
layout,
|
||||
} = this.props;
|
||||
if (!tileMapTileSelection) {
|
||||
return;
|
||||
}
|
||||
const selectedInstances = instancesSelection.getSelectedInstances();
|
||||
if (selectedInstances.length !== 1) return;
|
||||
const selectedInstance = selectedInstances[0];
|
||||
const object = getObjectByName(
|
||||
project.getObjects(),
|
||||
layout ? layout.getObjects() : null,
|
||||
selectedInstance.getObjectName()
|
||||
);
|
||||
if (!object) return;
|
||||
const renderedInstance = this.getRendererOfInstance(selectedInstance);
|
||||
if (
|
||||
object.getType() === 'TileMap::SimpleTileMap' &&
|
||||
renderedInstance &&
|
||||
renderedInstance.constructor.name === 'RenderedSimpleTileMapInstance'
|
||||
) {
|
||||
const editableTileMap = renderedInstance.getEditableTileMap();
|
||||
if (!editableTileMap) {
|
||||
console.error(
|
||||
`Could not find the editable tile map for instance of object ${selectedInstance.getObjectName()}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const sceneToTileMapTransformation = new AffineTransformation();
|
||||
const tileMapToSceneTransformation = new AffineTransformation();
|
||||
const scales = updateSceneToTileMapTransformation(
|
||||
selectedInstance,
|
||||
renderedInstance,
|
||||
sceneToTileMapTransformation,
|
||||
tileMapToSceneTransformation
|
||||
);
|
||||
if (!scales) return;
|
||||
const { scaleX, scaleY } = scales;
|
||||
const tileSet = getTileSet(object);
|
||||
const editableTileMapLayer = editableTileMap.getTileLayer(0);
|
||||
const tileMapGridCoordinates = getTilesGridCoordinatesFromPointerSceneCoordinates(
|
||||
{
|
||||
coordinates: sceneCoordinates,
|
||||
tileSize: tileSet.tileSize,
|
||||
sceneToTileMapTransformation,
|
||||
}
|
||||
);
|
||||
|
||||
if (tileMapTileSelection.kind === 'single') {
|
||||
// TODO: Optimize list execution to make sure the most important size changing operations are done first.
|
||||
let cumulatedUnshiftedRows = 0,
|
||||
cumulatedUnshiftedColumns = 0;
|
||||
tileMapGridCoordinates.forEach(({ x: gridX, y: gridY }) => {
|
||||
const {
|
||||
unshiftedRows,
|
||||
unshiftedColumns,
|
||||
appendedRows,
|
||||
appendedColumns,
|
||||
} = editableTileMapLayer.setTile(
|
||||
// If rows or columns have been unshifted in the previous tile setting operations,
|
||||
// we have to take them into account for the current coordinates.
|
||||
gridX + cumulatedUnshiftedColumns,
|
||||
gridY + cumulatedUnshiftedRows,
|
||||
tileSet.rowCount * tileMapTileSelection.coordinates.x +
|
||||
tileMapTileSelection.coordinates.y,
|
||||
{
|
||||
flipVertically: tileMapTileSelection.flipVertically,
|
||||
flipHorizontally: tileMapTileSelection.flipHorizontally,
|
||||
flipDiagonally: false,
|
||||
}
|
||||
);
|
||||
cumulatedUnshiftedRows += unshiftedRows;
|
||||
cumulatedUnshiftedColumns += unshiftedColumns;
|
||||
// The instance angle is not considered when moving the instance after
|
||||
// rows/columns were added/removed because the instance position does not
|
||||
// include the rotation transformation. Otherwise, we could have used
|
||||
// tileMapToSceneTransformation to get the new position.
|
||||
selectedInstance.setX(
|
||||
selectedInstance.getX() -
|
||||
unshiftedColumns * (tileSet.tileSize * scaleX)
|
||||
);
|
||||
selectedInstance.setY(
|
||||
selectedInstance.getY() -
|
||||
unshiftedRows * (tileSet.tileSize * scaleY)
|
||||
);
|
||||
if (selectedInstance.hasCustomSize()) {
|
||||
selectedInstance.setCustomWidth(
|
||||
selectedInstance.getCustomWidth() +
|
||||
tileSet.tileSize * scaleX * (appendedColumns + unshiftedColumns)
|
||||
);
|
||||
selectedInstance.setCustomHeight(
|
||||
selectedInstance.getCustomHeight() +
|
||||
tileSet.tileSize * scaleY * (appendedRows + unshiftedRows)
|
||||
);
|
||||
}
|
||||
});
|
||||
this.props.onInstancesResized([selectedInstance]);
|
||||
} else if (tileMapTileSelection.kind === 'erase') {
|
||||
tileMapGridCoordinates.forEach(({ x: gridX, y: gridY }) => {
|
||||
editableTileMapLayer.removeTile(gridX, gridY);
|
||||
});
|
||||
const {
|
||||
shiftedRows,
|
||||
shiftedColumns,
|
||||
poppedRows,
|
||||
poppedColumns,
|
||||
} = editableTileMapLayer.trimEmptyColumnsAndRow();
|
||||
// The instance angle is not considered when moving the instance after
|
||||
// rows/columns were added/removed because the instance position does not
|
||||
// include the rotation transformation. Otherwise, we could have used
|
||||
// tileMapToSceneTransformation to get the new position.
|
||||
selectedInstance.setX(
|
||||
selectedInstance.getX() + shiftedColumns * (tileSet.tileSize * scaleX)
|
||||
);
|
||||
selectedInstance.setY(
|
||||
selectedInstance.getY() + shiftedRows * (tileSet.tileSize * scaleY)
|
||||
);
|
||||
if (selectedInstance.hasCustomSize()) {
|
||||
selectedInstance.setCustomWidth(
|
||||
selectedInstance.getCustomWidth() -
|
||||
tileSet.tileSize * scaleX * (poppedColumns + shiftedColumns)
|
||||
);
|
||||
selectedInstance.setCustomHeight(
|
||||
selectedInstance.getCustomHeight() -
|
||||
tileSet.tileSize * scaleY * (poppedRows + shiftedRows)
|
||||
);
|
||||
}
|
||||
this.props.onInstancesResized([selectedInstance]);
|
||||
}
|
||||
renderedInstance.updatePixiTileMap();
|
||||
selectedInstance.setRawStringProperty(
|
||||
'tilemap',
|
||||
JSON.stringify(editableTileMap.toJSObject())
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getRendererOfInstance = (instance: gdInitialInstance) => {
|
||||
return this.instancesRenderer.getRendererOfInstance(
|
||||
instance.getLayer(),
|
||||
instance
|
||||
);
|
||||
};
|
||||
|
||||
_onDownBackground = (x: number, y: number, event?: PointerEvent) => {
|
||||
this.lastCursorX = x;
|
||||
this.lastCursorY = y;
|
||||
@@ -1068,6 +1264,14 @@ export default class InstancesEditor extends Component<Props> {
|
||||
this.onInstancesMovedDebounced(unlockedSelectedInstances);
|
||||
};
|
||||
|
||||
onPressEscape = () => {
|
||||
if (this.clickInterceptor && this.clickInterceptor.isIntercepting()) {
|
||||
this.clickInterceptor.cancelClickInterception();
|
||||
} else if (this.props.tileMapTileSelection) {
|
||||
this.props.onSelectTileMapTile(null);
|
||||
}
|
||||
};
|
||||
|
||||
scrollBy(x: number, y: number) {
|
||||
this.fpsLimiter.notifyInteractionHappened();
|
||||
this.viewPosition.scrollBy(x, y);
|
||||
@@ -1209,6 +1413,17 @@ export default class InstancesEditor extends Component<Props> {
|
||||
);
|
||||
};
|
||||
|
||||
getCoordinatesToRenderTileMapPreview = () => {
|
||||
const clickInterceptorPointerPathCoordinates = this.clickInterceptor.getPointerPathCoordinates();
|
||||
if (clickInterceptorPointerPathCoordinates) {
|
||||
return clickInterceptorPointerPathCoordinates;
|
||||
}
|
||||
const lastCursorSceneCoordinates = this.getLastCursorSceneCoordinates();
|
||||
return [
|
||||
{ x: lastCursorSceneCoordinates[0], y: lastCursorSceneCoordinates[1] },
|
||||
];
|
||||
};
|
||||
|
||||
getViewPosition = (): ?ViewPosition => {
|
||||
return this.viewPosition;
|
||||
};
|
||||
@@ -1226,6 +1441,8 @@ export default class InstancesEditor extends Component<Props> {
|
||||
this.canvasCursor.render();
|
||||
this.grid.render();
|
||||
this.highlightedInstance.render();
|
||||
this.tileMapTilePreview.render();
|
||||
this.clickInterceptor.render();
|
||||
this.selectedInstances.render();
|
||||
this.selectionRectangle.render();
|
||||
this.windowBorder.render();
|
||||
|
224
newIDE/app/src/ObjectEditor/Editors/SimpleTileMapEditor.js
Normal file
224
newIDE/app/src/ObjectEditor/Editors/SimpleTileMapEditor.js
Normal file
@@ -0,0 +1,224 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import type { EditorProps } from './EditorProps.flow';
|
||||
import ScrollView from '../../UI/ScrollView';
|
||||
import { ColumnStackLayout, ResponsiveLineStackLayout } from '../../UI/Layout';
|
||||
import SemiControlledTextField from '../../UI/SemiControlledTextField';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import useForceUpdate from '../../Utils/UseForceUpdate';
|
||||
import { PropertyResourceSelector } from './PropertyFields';
|
||||
import TileSetVisualizer, {
|
||||
getGridCoordinatesFromTileId,
|
||||
getTileIdFromGridCoordinates,
|
||||
} from '../../InstancesEditor/TileSetVisualizer';
|
||||
import type { TileMapTileSelection } from '../../InstancesEditor/TileSetVisualizer';
|
||||
import { Column } from '../../UI/Grid';
|
||||
import AlertMessage from '../../UI/AlertMessage';
|
||||
import Checkbox from '../../UI/Checkbox';
|
||||
|
||||
const SimpleTileMapEditor = ({
|
||||
objectConfiguration,
|
||||
project,
|
||||
layout,
|
||||
object,
|
||||
onObjectUpdated,
|
||||
onSizeUpdated,
|
||||
resourceManagementProps,
|
||||
renderObjectNameField,
|
||||
}: EditorProps) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
const objectProperties = objectConfiguration.getProperties();
|
||||
const tileSize = parseFloat(objectProperties.get('tileSize').getValue());
|
||||
const rowCount = parseFloat(objectProperties.get('rowCount').getValue());
|
||||
const columnCount = parseFloat(
|
||||
objectProperties.get('columnCount').getValue()
|
||||
);
|
||||
const [configureHitBoxes, setConfigureHitBoxes] = React.useState<boolean>(
|
||||
false
|
||||
);
|
||||
const [error, setError] = React.useState<React.Node>(null);
|
||||
const atlasImage = objectProperties.get('atlasImage').getValue();
|
||||
const previousAtlasImageResourceName = React.useRef<string>(atlasImage);
|
||||
const tilesWithHitBox = objectProperties
|
||||
.get('tilesWithHitBox')
|
||||
.getValue()
|
||||
.split(',')
|
||||
.filter(value => !!value)
|
||||
.map(idAsString => parseInt(idAsString, 10));
|
||||
const tilesWithHitBoxCoordinates = tilesWithHitBox.map(id =>
|
||||
getGridCoordinatesFromTileId({ id, rowCount })
|
||||
);
|
||||
const [
|
||||
loadedAtlasImageDimensions,
|
||||
setLoadedAtlasImageDimensions,
|
||||
] = React.useState<?{|
|
||||
width: number,
|
||||
height: number,
|
||||
|}>(null);
|
||||
|
||||
const tileMapTileSelection = {
|
||||
kind: 'multiple',
|
||||
coordinates: tilesWithHitBoxCoordinates,
|
||||
};
|
||||
|
||||
const recomputeTileSet = React.useCallback(
|
||||
(
|
||||
dimensions: {|
|
||||
width: number,
|
||||
height: number,
|
||||
|},
|
||||
tileSize: number
|
||||
) => {
|
||||
const newRowCount = dimensions.height / tileSize;
|
||||
const newColumnCount = dimensions.width / tileSize;
|
||||
|
||||
if (rowCount === newRowCount && columnCount === newColumnCount) {
|
||||
return;
|
||||
}
|
||||
objectConfiguration.updateProperty('rowCount', newRowCount.toString());
|
||||
objectConfiguration.updateProperty(
|
||||
'columnCount',
|
||||
newColumnCount.toString()
|
||||
);
|
||||
const newMaxId = newColumnCount * newRowCount - 1;
|
||||
objectConfiguration.updateProperty(
|
||||
'tilesWithHitBox',
|
||||
tilesWithHitBox.filter(tileId => tileId <= newMaxId).join(',')
|
||||
);
|
||||
},
|
||||
[columnCount, rowCount, objectConfiguration, tilesWithHitBox]
|
||||
);
|
||||
|
||||
const setTileSize = React.useCallback(
|
||||
(value: number) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
objectConfiguration.updateProperty('tileSize', value.toString());
|
||||
if (loadedAtlasImageDimensions) {
|
||||
recomputeTileSet(loadedAtlasImageDimensions, value);
|
||||
}
|
||||
if (onObjectUpdated) onObjectUpdated();
|
||||
forceUpdate();
|
||||
},
|
||||
[
|
||||
forceUpdate,
|
||||
objectConfiguration,
|
||||
recomputeTileSet,
|
||||
loadedAtlasImageDimensions,
|
||||
onObjectUpdated,
|
||||
]
|
||||
);
|
||||
|
||||
const onChangeTilesWithHitBox = React.useCallback(
|
||||
(tileMapTileSelection: ?TileMapTileSelection) => {
|
||||
if (!tileMapTileSelection || tileMapTileSelection.kind !== 'multiple') {
|
||||
return;
|
||||
}
|
||||
objectConfiguration.updateProperty(
|
||||
'tilesWithHitBox',
|
||||
tileMapTileSelection.coordinates
|
||||
.map(coordinates =>
|
||||
getTileIdFromGridCoordinates({ ...coordinates, rowCount })
|
||||
)
|
||||
.join(',')
|
||||
);
|
||||
if (onObjectUpdated) onObjectUpdated();
|
||||
forceUpdate();
|
||||
},
|
||||
[rowCount, objectConfiguration, forceUpdate, onObjectUpdated]
|
||||
);
|
||||
|
||||
const onChangeAtlasImage = React.useCallback(
|
||||
() => {
|
||||
if (onObjectUpdated) onObjectUpdated();
|
||||
onSizeUpdated();
|
||||
forceUpdate();
|
||||
},
|
||||
[forceUpdate, onObjectUpdated, onSizeUpdated]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (!loadedAtlasImageDimensions) return;
|
||||
const _rowCount = loadedAtlasImageDimensions.height / tileSize;
|
||||
const _columnCount = loadedAtlasImageDimensions.width / tileSize;
|
||||
if (!Number.isInteger(_rowCount) || !Number.isInteger(_columnCount)) {
|
||||
setError(
|
||||
<Trans>
|
||||
The new atlas image dimensions is not compatible with the tile size.
|
||||
</Trans>
|
||||
);
|
||||
}
|
||||
},
|
||||
[tileSize, loadedAtlasImageDimensions]
|
||||
);
|
||||
|
||||
const onAtlasImageLoaded = React.useCallback(
|
||||
(e: SyntheticEvent<HTMLImageElement>, atlasResourceName: string) => {
|
||||
setError(null);
|
||||
const newDimensions = {
|
||||
width: e.currentTarget.naturalWidth,
|
||||
height: e.currentTarget.naturalHeight,
|
||||
};
|
||||
setLoadedAtlasImageDimensions(newDimensions);
|
||||
if (previousAtlasImageResourceName.current === atlasResourceName) {
|
||||
// The resource did not change, do nothing.
|
||||
return;
|
||||
}
|
||||
previousAtlasImageResourceName.current = atlasResourceName;
|
||||
|
||||
recomputeTileSet(newDimensions, tileSize);
|
||||
},
|
||||
[tileSize, recomputeTileSet]
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<ColumnStackLayout noMargin>
|
||||
{!!renderObjectNameField && renderObjectNameField()}
|
||||
<SemiControlledTextField
|
||||
floatingLabelFixed
|
||||
floatingLabelText={<Trans>Tile size</Trans>}
|
||||
onChange={value => setTileSize(Math.max(parseInt(value, 10) || 0, 0))}
|
||||
value={tileSize.toString()}
|
||||
/>
|
||||
<PropertyResourceSelector
|
||||
objectConfiguration={objectConfiguration}
|
||||
propertyName="atlasImage"
|
||||
project={project}
|
||||
resourceManagementProps={resourceManagementProps}
|
||||
onChange={onChangeAtlasImage}
|
||||
/>
|
||||
{error && <AlertMessage kind="error">{error}</AlertMessage>}
|
||||
{atlasImage && (
|
||||
<ResponsiveLineStackLayout>
|
||||
<Column noMargin expand>
|
||||
<Checkbox
|
||||
checked={configureHitBoxes}
|
||||
onCheck={(e, checked) => setConfigureHitBoxes(checked)}
|
||||
label={<Trans>Configure tiles with hit boxes</Trans>}
|
||||
/>
|
||||
</Column>
|
||||
<Column noMargin expand>
|
||||
<TileSetVisualizer
|
||||
project={project}
|
||||
objectConfiguration={objectConfiguration}
|
||||
tileMapTileSelection={tileMapTileSelection}
|
||||
onSelectTileMapTile={onChangeTilesWithHitBox}
|
||||
showPaintingToolbar={false}
|
||||
allowMultipleSelection
|
||||
onAtlasImageLoaded={onAtlasImageLoaded}
|
||||
interactive={configureHitBoxes}
|
||||
/>
|
||||
</Column>
|
||||
</ResponsiveLineStackLayout>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimpleTileMapEditor;
|
@@ -308,246 +308,238 @@ const SpineEditor = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollView ref={scrollView}>
|
||||
<ColumnStackLayout noMargin>
|
||||
{renderObjectNameField && renderObjectNameField()}
|
||||
<AlertMessage
|
||||
kind="warning"
|
||||
renderRightButton={() => (
|
||||
<FlatButton
|
||||
style={styles.neverShrinkingButton}
|
||||
label={<Trans>Purchase Spine</Trans>}
|
||||
onClick={() =>
|
||||
Window.openExternalURL(
|
||||
'https://esotericsoftware.com/spine-purchase'
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<Trans>
|
||||
You must own a Spine license to publish a game with a Spine
|
||||
object.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
<PropertyResourceSelector
|
||||
objectConfiguration={objectConfiguration}
|
||||
propertyName="spineResourceName"
|
||||
project={project}
|
||||
resourceManagementProps={resourceManagementProps}
|
||||
onChange={onChangeSpineResourceName}
|
||||
/>
|
||||
{!spineData.skeleton && spineData.loadingErrorReason ? (
|
||||
<AlertMessage kind="error">
|
||||
{spineData.loadingErrorReason === 'invalid-spine-resource' ? (
|
||||
<Trans>
|
||||
The selected resource is not a proper Spine resource.
|
||||
</Trans>
|
||||
) : spineData.loadingErrorReason ===
|
||||
'missing-texture-atlas-name' ? (
|
||||
<Trans>Missing texture atlas name in the Spine file.</Trans>
|
||||
) : spineData.loadingErrorReason ===
|
||||
'spine-resource-loading-error' ? (
|
||||
<Trans>
|
||||
Error while loading the Spine resource (
|
||||
{spineData.loadingError
|
||||
? spineData.loadingError.message
|
||||
: 'Unknown error'}
|
||||
).
|
||||
</Trans>
|
||||
) : spineData.loadingErrorReason === 'invalid-atlas-resource' ? (
|
||||
<Trans>
|
||||
The Atlas embedded in the Spine fine can't be located.
|
||||
</Trans>
|
||||
) : spineData.loadingErrorReason ===
|
||||
'missing-texture-resources' ? (
|
||||
<Trans>Missing texture for an atlas in the Spine file.</Trans>
|
||||
) : spineData.loadingErrorReason ===
|
||||
'atlas-resource-loading-error' ? (
|
||||
<Trans>
|
||||
Error while loading the Spine Texture Atlas resource (
|
||||
{spineData.loadingError
|
||||
? spineData.loadingError.message
|
||||
: 'Unknown error'}
|
||||
).
|
||||
</Trans>
|
||||
) : null}
|
||||
</AlertMessage>
|
||||
) : null}
|
||||
<Text size="block-title" noMargin>
|
||||
<Trans>Default size</Trans>
|
||||
</Text>
|
||||
<PropertyField
|
||||
objectConfiguration={objectConfiguration}
|
||||
propertyName="scale"
|
||||
/>
|
||||
{sourceSelectOptions.length && (
|
||||
<>
|
||||
<Text size="block-title">Animations</Text>
|
||||
<Column noMargin expand useFullHeight>
|
||||
{spineConfiguration.getAnimationsCount() === 0 ? (
|
||||
<Column noMargin expand justifyContent="center">
|
||||
<EmptyPlaceholder
|
||||
title={<Trans>Add your first animation</Trans>}
|
||||
description={
|
||||
<Trans>
|
||||
Import one or more animations that are available in
|
||||
this Spine file.
|
||||
</Trans>
|
||||
}
|
||||
actionLabel={<Trans>Add an animation</Trans>}
|
||||
onAction={addAnimation}
|
||||
/>
|
||||
</Column>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{mapFor(
|
||||
0,
|
||||
spineConfiguration.getAnimationsCount(),
|
||||
animationIndex => {
|
||||
const animation = spineConfiguration.getAnimation(
|
||||
animationIndex
|
||||
);
|
||||
|
||||
const animationRef =
|
||||
justAddedAnimationName === animation.getName()
|
||||
? justAddedAnimationElement
|
||||
: null;
|
||||
|
||||
return (
|
||||
<DragSourceAndDropTarget
|
||||
key={animationIndex}
|
||||
beginDrag={() => {
|
||||
draggedAnimationIndex.current = animationIndex;
|
||||
return {};
|
||||
}}
|
||||
canDrag={() => true}
|
||||
canDrop={() => true}
|
||||
drop={() => {
|
||||
moveAnimation(animationIndex);
|
||||
}}
|
||||
>
|
||||
{({
|
||||
connectDragSource,
|
||||
connectDropTarget,
|
||||
isOver,
|
||||
canDrop,
|
||||
}) =>
|
||||
connectDropTarget(
|
||||
<div
|
||||
key={animationIndex}
|
||||
style={styles.rowContainer}
|
||||
>
|
||||
{isOver && (
|
||||
<DropIndicator canDrop={canDrop} />
|
||||
)}
|
||||
<div
|
||||
ref={animationRef}
|
||||
style={{
|
||||
...styles.rowContent,
|
||||
backgroundColor:
|
||||
gdevelopTheme.list.itemsBackgroundColor,
|
||||
}}
|
||||
>
|
||||
<Line noMargin expand alignItems="center">
|
||||
{connectDragSource(
|
||||
<span>
|
||||
<Column>
|
||||
<DragHandleIcon />
|
||||
</Column>
|
||||
</span>
|
||||
)}
|
||||
<Text noMargin noShrink>
|
||||
<Trans>
|
||||
Animation #{animationIndex}
|
||||
</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
<SemiControlledTextField
|
||||
margin="none"
|
||||
commitOnBlur
|
||||
errorText={nameErrors[animationIndex]}
|
||||
translatableHintText={t`Optional animation name`}
|
||||
value={animation.getName()}
|
||||
onChange={text =>
|
||||
changeAnimationName(
|
||||
animationIndex,
|
||||
text
|
||||
)
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() =>
|
||||
removeAnimation(animationIndex)
|
||||
}
|
||||
>
|
||||
<Trash />
|
||||
</IconButton>
|
||||
</Line>
|
||||
<Spacer />
|
||||
</div>
|
||||
<Spacer />
|
||||
<ColumnStackLayout expand>
|
||||
<SelectField
|
||||
id="animation-source-field"
|
||||
value={animation.getSource()}
|
||||
onChange={(event, value) => {
|
||||
animation.setSource(event.target.value);
|
||||
forceUpdate();
|
||||
}}
|
||||
margin="dense"
|
||||
fullWidth
|
||||
floatingLabelText={
|
||||
<Trans>Spine animation name</Trans>
|
||||
}
|
||||
translatableHintText={t`Choose an animation`}
|
||||
>
|
||||
{sourceSelectOptions}
|
||||
</SelectField>
|
||||
<Checkbox
|
||||
label={<Trans>Loop</Trans>}
|
||||
checked={animation.shouldLoop()}
|
||||
onCheck={(e, checked) => {
|
||||
animation.setShouldLoop(checked);
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
</ColumnStackLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</DragSourceAndDropTarget>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Column>
|
||||
<Column noMargin>
|
||||
<ResponsiveLineStackLayout
|
||||
justifyContent="space-between"
|
||||
noColumnMargin
|
||||
>
|
||||
<FlatButton
|
||||
label={<Trans>Scan missing animations</Trans>}
|
||||
onClick={scanNewAnimations}
|
||||
/>
|
||||
<RaisedButton
|
||||
label={<Trans>Add an animation</Trans>}
|
||||
primary
|
||||
onClick={addAnimation}
|
||||
icon={<Add />}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
</Column>
|
||||
</>
|
||||
<ScrollView ref={scrollView}>
|
||||
<ColumnStackLayout noMargin>
|
||||
{renderObjectNameField && renderObjectNameField()}
|
||||
<AlertMessage
|
||||
kind="warning"
|
||||
renderRightButton={() => (
|
||||
<FlatButton
|
||||
style={styles.neverShrinkingButton}
|
||||
label={<Trans>Purchase Spine</Trans>}
|
||||
onClick={() =>
|
||||
Window.openExternalURL(
|
||||
'https://esotericsoftware.com/spine-purchase'
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</ScrollView>
|
||||
</>
|
||||
>
|
||||
<Trans>
|
||||
You must own a Spine license to publish a game with a Spine object.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
<PropertyResourceSelector
|
||||
objectConfiguration={objectConfiguration}
|
||||
propertyName="spineResourceName"
|
||||
project={project}
|
||||
resourceManagementProps={resourceManagementProps}
|
||||
onChange={onChangeSpineResourceName}
|
||||
/>
|
||||
{!spineData.skeleton && spineData.loadingErrorReason ? (
|
||||
<AlertMessage kind="error">
|
||||
{spineData.loadingErrorReason === 'invalid-spine-resource' ? (
|
||||
<Trans>
|
||||
The selected resource is not a proper Spine resource.
|
||||
</Trans>
|
||||
) : spineData.loadingErrorReason ===
|
||||
'missing-texture-atlas-name' ? (
|
||||
<Trans>Missing texture atlas name in the Spine file.</Trans>
|
||||
) : spineData.loadingErrorReason ===
|
||||
'spine-resource-loading-error' ? (
|
||||
<Trans>
|
||||
Error while loading the Spine resource (
|
||||
{spineData.loadingError
|
||||
? spineData.loadingError.message
|
||||
: 'Unknown error'}
|
||||
).
|
||||
</Trans>
|
||||
) : spineData.loadingErrorReason === 'invalid-atlas-resource' ? (
|
||||
<Trans>
|
||||
The Atlas embedded in the Spine fine can't be located.
|
||||
</Trans>
|
||||
) : spineData.loadingErrorReason === 'missing-texture-resources' ? (
|
||||
<Trans>Missing texture for an atlas in the Spine file.</Trans>
|
||||
) : spineData.loadingErrorReason ===
|
||||
'atlas-resource-loading-error' ? (
|
||||
<Trans>
|
||||
Error while loading the Spine Texture Atlas resource (
|
||||
{spineData.loadingError
|
||||
? spineData.loadingError.message
|
||||
: 'Unknown error'}
|
||||
).
|
||||
</Trans>
|
||||
) : null}
|
||||
</AlertMessage>
|
||||
) : null}
|
||||
<Text size="block-title" noMargin>
|
||||
<Trans>Default size</Trans>
|
||||
</Text>
|
||||
<PropertyField
|
||||
objectConfiguration={objectConfiguration}
|
||||
propertyName="scale"
|
||||
/>
|
||||
{sourceSelectOptions.length && (
|
||||
<>
|
||||
<Text size="block-title">Animations</Text>
|
||||
<Column noMargin expand useFullHeight>
|
||||
{spineConfiguration.getAnimationsCount() === 0 ? (
|
||||
<Column noMargin expand justifyContent="center">
|
||||
<EmptyPlaceholder
|
||||
title={<Trans>Add your first animation</Trans>}
|
||||
description={
|
||||
<Trans>
|
||||
Import one or more animations that are available in this
|
||||
Spine file.
|
||||
</Trans>
|
||||
}
|
||||
actionLabel={<Trans>Add an animation</Trans>}
|
||||
onAction={addAnimation}
|
||||
/>
|
||||
</Column>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{mapFor(
|
||||
0,
|
||||
spineConfiguration.getAnimationsCount(),
|
||||
animationIndex => {
|
||||
const animation = spineConfiguration.getAnimation(
|
||||
animationIndex
|
||||
);
|
||||
|
||||
const animationRef =
|
||||
justAddedAnimationName === animation.getName()
|
||||
? justAddedAnimationElement
|
||||
: null;
|
||||
|
||||
return (
|
||||
<DragSourceAndDropTarget
|
||||
key={animationIndex}
|
||||
beginDrag={() => {
|
||||
draggedAnimationIndex.current = animationIndex;
|
||||
return {};
|
||||
}}
|
||||
canDrag={() => true}
|
||||
canDrop={() => true}
|
||||
drop={() => {
|
||||
moveAnimation(animationIndex);
|
||||
}}
|
||||
>
|
||||
{({
|
||||
connectDragSource,
|
||||
connectDropTarget,
|
||||
isOver,
|
||||
canDrop,
|
||||
}) =>
|
||||
connectDropTarget(
|
||||
<div
|
||||
key={animationIndex}
|
||||
style={styles.rowContainer}
|
||||
>
|
||||
{isOver && <DropIndicator canDrop={canDrop} />}
|
||||
<div
|
||||
ref={animationRef}
|
||||
style={{
|
||||
...styles.rowContent,
|
||||
backgroundColor:
|
||||
gdevelopTheme.list.itemsBackgroundColor,
|
||||
}}
|
||||
>
|
||||
<Line noMargin expand alignItems="center">
|
||||
{connectDragSource(
|
||||
<span>
|
||||
<Column>
|
||||
<DragHandleIcon />
|
||||
</Column>
|
||||
</span>
|
||||
)}
|
||||
<Text noMargin noShrink>
|
||||
<Trans>Animation #{animationIndex}</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
<SemiControlledTextField
|
||||
margin="none"
|
||||
commitOnBlur
|
||||
errorText={nameErrors[animationIndex]}
|
||||
translatableHintText={t`Optional animation name`}
|
||||
value={animation.getName()}
|
||||
onChange={text =>
|
||||
changeAnimationName(
|
||||
animationIndex,
|
||||
text
|
||||
)
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() =>
|
||||
removeAnimation(animationIndex)
|
||||
}
|
||||
>
|
||||
<Trash />
|
||||
</IconButton>
|
||||
</Line>
|
||||
<Spacer />
|
||||
</div>
|
||||
<Spacer />
|
||||
<ColumnStackLayout expand>
|
||||
<SelectField
|
||||
id="animation-source-field"
|
||||
value={animation.getSource()}
|
||||
onChange={(event, value) => {
|
||||
animation.setSource(event.target.value);
|
||||
forceUpdate();
|
||||
}}
|
||||
margin="dense"
|
||||
fullWidth
|
||||
floatingLabelText={
|
||||
<Trans>Spine animation name</Trans>
|
||||
}
|
||||
translatableHintText={t`Choose an animation`}
|
||||
>
|
||||
{sourceSelectOptions}
|
||||
</SelectField>
|
||||
<Checkbox
|
||||
label={<Trans>Loop</Trans>}
|
||||
checked={animation.shouldLoop()}
|
||||
onCheck={(e, checked) => {
|
||||
animation.setShouldLoop(checked);
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
</ColumnStackLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</DragSourceAndDropTarget>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Column>
|
||||
<Column noMargin>
|
||||
<ResponsiveLineStackLayout
|
||||
justifyContent="space-between"
|
||||
noColumnMargin
|
||||
>
|
||||
<FlatButton
|
||||
label={<Trans>Scan missing animations</Trans>}
|
||||
onClick={scanNewAnimations}
|
||||
/>
|
||||
<RaisedButton
|
||||
label={<Trans>Add an animation</Trans>}
|
||||
primary
|
||||
onClick={addAnimation}
|
||||
icon={<Add />}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
</Column>
|
||||
</>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -11,6 +11,7 @@ import CustomObjectPropertiesEditor from './Editors/CustomObjectPropertiesEditor
|
||||
import Cube3DEditor from './Editors/Cube3DEditor';
|
||||
import Model3DEditor from './Editors/Model3DEditor';
|
||||
import SpineEditor from './Editors/SpineEditor';
|
||||
import SimpleTileMapEditor from './Editors/SimpleTileMapEditor';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -162,6 +163,21 @@ const ObjectsEditorService = {
|
||||
gd.asObjectJsImplementation(objectConfiguration),
|
||||
helpPagePath: '/objects/spine',
|
||||
},
|
||||
'TileMap::SimpleTileMap': {
|
||||
component: SimpleTileMapEditor,
|
||||
createNewObject: (
|
||||
objectConfiguration: gdObjectConfiguration
|
||||
): gdObjectConfiguration =>
|
||||
gd
|
||||
.asObjectJsImplementation(objectConfiguration)
|
||||
.clone()
|
||||
.release(),
|
||||
castToObjectType: (
|
||||
objectConfiguration: gdObjectConfiguration
|
||||
): gdObjectJsImplementation =>
|
||||
gd.asObjectJsImplementation(objectConfiguration),
|
||||
helpPagePath: '/objects/tilemap', // TODO: create wiki page.
|
||||
},
|
||||
'TiledSpriteObject::TiledSprite': {
|
||||
component: TiledSpriteEditor,
|
||||
createNewObject: (): gdTiledSpriteObject => new gd.TiledSpriteObject(),
|
||||
|
@@ -105,8 +105,9 @@ const objectTypeToDefaultName = {
|
||||
'BBText::BBText': 'NewBBText',
|
||||
'BitmapText::BitmapTextObject': 'NewBitmapText',
|
||||
'TextEntryObject::TextEntry': 'NewTextEntry',
|
||||
'TileMap::TileMap': 'NewTileMap',
|
||||
'TileMap::CollisionMask': 'NewTileMapMask',
|
||||
'TileMap::SimpleTileMap': 'NewTileMap',
|
||||
'TileMap::TileMap': 'NewExternalTileMap',
|
||||
'TileMap::CollisionMask': 'NewExternalTileMapMask',
|
||||
'MyDummyExtension::DummyObject': 'NewDummyObject',
|
||||
'Lighting::LightObject': 'NewLight',
|
||||
'TextInput::TextInputObject': 'NewTextInput',
|
||||
|
@@ -52,6 +52,7 @@ export default class RenderedUnknownInstance extends RenderedInstance {
|
||||
this._pixiObject.lineTo(width / 2, -height / 2);
|
||||
this._pixiObject.lineTo(width / 2, height / 2);
|
||||
this._pixiObject.lineTo(-width / 2, height / 2);
|
||||
this._pixiObject.lineTo(-width / 2, -height / 2);
|
||||
this._pixiObject.endFill();
|
||||
|
||||
this._pixiObject.position.x = this._instance.getX() + width / 2;
|
||||
|
@@ -18,6 +18,7 @@ import Rectangle from '../Utils/Rectangle';
|
||||
import ViewPosition from '../InstancesEditor/ViewPosition';
|
||||
import { type ObjectFolderOrObjectWithContext } from '../ObjectsList/EnumerateObjectFolderOrObject';
|
||||
import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope.flow';
|
||||
import { type TileMapTileSelection } from '../InstancesEditor/TileSetVisualizer';
|
||||
|
||||
export type SceneEditorsDisplayProps = {|
|
||||
project: gdProject,
|
||||
@@ -93,6 +94,9 @@ export type SceneEditorsDisplayProps = {|
|
||||
isInstanceOf3DObject: gdInitialInstance => boolean,
|
||||
onSelectAllInstancesOfObjectInLayout: string => void,
|
||||
|
||||
tileMapTileSelection: ?TileMapTileSelection,
|
||||
onSelectTileMapTile: (?TileMapTileSelection) => void,
|
||||
|
||||
canInstallPrivateAsset: () => boolean,
|
||||
|
||||
instancesEditorSettings: InstancesEditorSettings,
|
||||
|
@@ -264,6 +264,8 @@ const MosaicEditorsDisplay = React.forwardRef<
|
||||
ref={instancesPropertiesEditorRef}
|
||||
unsavedChanges={props.unsavedChanges}
|
||||
historyHandler={props.historyHandler}
|
||||
tileMapTileSelection={props.tileMapTileSelection}
|
||||
onSelectTileMapTile={props.onSelectTileMapTile}
|
||||
/>
|
||||
)}
|
||||
</I18n>
|
||||
@@ -337,6 +339,8 @@ const MosaicEditorsDisplay = React.forwardRef<
|
||||
editorRef.current = editor;
|
||||
}}
|
||||
pauseRendering={!props.isActive}
|
||||
tileMapTileSelection={props.tileMapTileSelection}
|
||||
onSelectTileMapTile={props.onSelectTileMapTile}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
@@ -270,6 +270,8 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
|
||||
}
|
||||
pauseRendering={!props.isActive}
|
||||
showObjectInstancesIn3D={values.use3DEditor}
|
||||
tileMapTileSelection={props.tileMapTileSelection}
|
||||
onSelectTileMapTile={props.onSelectTileMapTile}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
<div style={styles.bottomContainer} id={swipeableDrawerContainerId}>
|
||||
@@ -358,6 +360,8 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
|
||||
ref={instancesPropertiesEditorRef}
|
||||
unsavedChanges={props.unsavedChanges}
|
||||
historyHandler={props.historyHandler}
|
||||
tileMapTileSelection={props.tileMapTileSelection}
|
||||
onSelectTileMapTile={props.onSelectTileMapTile}
|
||||
/>
|
||||
)}
|
||||
</I18n>
|
||||
|
@@ -74,6 +74,7 @@ import {
|
||||
} from '../MainFrame/ResourcesWatcher';
|
||||
import { unserializeFromJSObject } from '../Utils/Serializer';
|
||||
import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope.flow';
|
||||
import { type TileMapTileSelection } from '../InstancesEditor/TileSetVisualizer';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -155,6 +156,8 @@ type State = {|
|
||||
|
||||
selectedObjectFolderOrObjectsWithContext: Array<ObjectFolderOrObjectWithContext>,
|
||||
selectedLayer: string,
|
||||
|
||||
tileMapTileSelection: ?TileMapTileSelection,
|
||||
|};
|
||||
|
||||
type CopyCutPasteOptions = {|
|
||||
@@ -203,6 +206,7 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
message: '',
|
||||
touchScreenMessage: '',
|
||||
},
|
||||
tileMapTileSelection: null,
|
||||
|
||||
selectedObjectFolderOrObjectsWithContext: [],
|
||||
selectedLayer: BASE_LAYER_NAME,
|
||||
@@ -770,6 +774,10 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
//TODO: Save for redo with debounce (and cancel on unmount)
|
||||
};
|
||||
|
||||
onSelectTileMapTile = (tileMapTileSelection: ?TileMapTileSelection) => {
|
||||
this.setState({ tileMapTileSelection });
|
||||
};
|
||||
|
||||
_onSelectInstances = (
|
||||
instances: Array<gdInitialInstance>,
|
||||
multiSelect: boolean,
|
||||
@@ -1819,6 +1827,8 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
onSelectLayer={(layer: string) =>
|
||||
this.setState({ selectedLayer: layer })
|
||||
}
|
||||
tileMapTileSelection={this.state.tileMapTileSelection}
|
||||
onSelectTileMapTile={this.onSelectTileMapTile}
|
||||
onExportAssets={this.openObjectExporterDialog}
|
||||
onDeleteObjects={this._onDeleteObjects}
|
||||
getValidatedObjectOrGroupName={
|
||||
|
23
newIDE/app/src/UI/CustomSvgIcons/Brush.js
Normal file
23
newIDE/app/src/UI/CustomSvgIcons/Brush.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import SvgIcon from '@material-ui/core/SvgIcon';
|
||||
|
||||
export default React.memo(props => (
|
||||
<SvgIcon {...props} width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M21.5303 3.46968C21.0563 2.99571 20.4755 2.78594 19.9003 2.81755C19.3421 2.84822 18.8367 3.10266 18.4697 3.46972L10.4697 11.4697C10.329 11.6103 10.25 11.8011 10.25 12C10.25 12.1989 10.329 12.3897 10.4697 12.5303L12.4697 14.5303C12.7626 14.8232 13.2374 14.8232 13.5303 14.5303L21.5303 6.53037C22.2835 5.77721 22.474 4.41333 21.5303 3.46968ZM19.5303 4.53035C19.6633 4.39739 19.8298 4.32369 19.9825 4.31529C20.1183 4.30783 20.2874 4.34808 20.4697 4.53035C20.7512 4.81183 20.7165 5.22292 20.4697 5.46971L13 12.9394L12.0607 12L19.5303 4.53035Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.8439 14.0184C10.2686 13.4728 9.49152 13.2221 8.75022 13.2209C8.00846 13.2197 7.22821 13.468 6.64784 14.0183C6.01224 14.621 5.83462 15.3315 5.71016 15.8293L5.68747 15.9196C5.55628 16.4361 5.45661 16.7185 5.13128 16.9631C5.00974 17.0544 4.73495 17.1482 4.28337 17.2003C3.85921 17.2493 3.4019 17.2501 3 17.2501C2.58579 17.2501 2.25 17.5858 2.25 18.0001C2.25 18.3714 2.41905 18.7307 2.61028 19.0276C2.81517 19.3457 3.10842 19.6882 3.48395 20.0443C4.36206 20.8769 5.73344 21.2501 7 21.2501C7.66561 21.2501 8.32448 21.1399 8.99504 20.7889C9.65514 20.4434 10.2755 19.8913 10.9146 19.0883C11.5901 18.2396 11.9316 17.3086 11.9127 16.399C11.8936 15.4848 11.5104 14.6505 10.8439 14.0184ZM7.67994 15.1068C7.93568 14.8643 8.32349 14.7202 8.74771 14.7209C9.17238 14.7216 9.55926 14.8674 9.81173 15.1068C10.1997 15.4747 10.4026 15.9333 10.413 16.4303C10.4234 16.9318 10.2377 17.5302 9.74099 18.1542C9.18355 18.8546 8.7161 19.2418 8.29946 19.4599C7.89328 19.6725 7.48651 19.7501 7 19.7501C5.96232 19.7501 5.02026 19.4339 4.51605 18.9558C4.42508 18.8696 4.34315 18.7873 4.26969 18.7093C4.33165 18.7038 4.39362 18.6975 4.45534 18.6904C4.9515 18.6331 5.5722 18.5082 6.03259 18.1621C6.80654 17.5803 7.00351 16.8314 7.14131 16.2889C7.28346 15.7292 7.36637 15.4041 7.67994 15.1068Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M14 20.25C13.5858 20.25 13.25 20.5858 13.25 21C13.25 21.4143 13.5858 21.75 14 21.75H21C21.4142 21.75 21.75 21.4143 21.75 21C21.75 20.5858 21.4142 20.25 21 20.25H14Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SvgIcon>
|
||||
));
|
13
newIDE/app/src/UI/CustomSvgIcons/Erase.js
Normal file
13
newIDE/app/src/UI/CustomSvgIcons/Erase.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import SvgIcon from '@material-ui/core/SvgIcon';
|
||||
|
||||
export default React.memo(props => (
|
||||
<SvgIcon {...props} width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M11.306 4.04284C12.3613 2.98572 14.0729 2.98572 15.1281 4.04283L19.9591 8.88213C21.0136 9.93854 21.0136 11.6507 19.9591 12.7071L19.9573 12.709L12.3782 20.25H21C21.4142 20.25 21.75 20.5858 21.75 21C21.75 21.4142 21.4142 21.75 21 21.75H9.99997C9.94275 21.75 9.88702 21.7436 9.83347 21.7315C9.16214 21.7952 8.47084 21.6813 7.84006 21.4818C6.52542 21.0661 5.24553 20.2211 4.38985 19.3624C3.52372 18.4931 2.72163 17.2636 2.39922 15.9505C2.07071 14.6126 2.2357 13.129 3.41443 11.9482L11.306 4.04284ZM12.3676 5.10256C12.8369 4.63248 13.5973 4.63248 14.0665 5.10256L18.8975 9.94186C19.3672 10.4124 19.3675 11.1756 18.8983 11.6466L18.8975 11.6475L14.2912 16.2306L7.76941 9.70878L12.3676 5.10256ZM6.70967 10.7704L13.2279 17.2886L10.7361 19.7678L10.7342 19.7697C10.2108 20.2942 9.3703 20.3925 8.29231 20.0517C7.2331 19.7167 6.15709 19.0108 5.45239 18.3036C4.73369 17.5823 4.09946 16.5845 3.85595 15.5928C3.61854 14.6259 3.75803 13.7272 4.47603 13.0079L6.70967 10.7704Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SvgIcon>
|
||||
));
|
15
newIDE/app/src/UI/CustomSvgIcons/FlipHorizontal.js
Normal file
15
newIDE/app/src/UI/CustomSvgIcons/FlipHorizontal.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import SvgIcon from '@material-ui/core/SvgIcon';
|
||||
|
||||
export default React.memo(props => (
|
||||
<SvgIcon {...props} width="24" height="24" viewBox="2 2 20 20" fill="none">
|
||||
<path
|
||||
d="M4.5 17.5C4.2355 17.5 3.99056 17.3607 3.85537 17.1333C3.72018 16.906 3.71473 16.6243 3.84103 16.3919L10.091 4.89187C10.2548 4.59046 10.6023 4.43861 10.9348 4.52312C11.2672 4.60763 11.5 4.90696 11.5 5.25001V16.75C11.5 17.1642 11.1642 17.5 10.75 17.5H4.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M20.1446 17.1333C20.0094 17.3607 19.7645 17.5 19.5 17.5H13.25C12.8358 17.5 12.5 17.1642 12.5 16.75V5.25001C12.5 4.90696 12.7328 4.60763 13.0652 4.52312C13.3977 4.43861 13.7452 4.59046 13.909 4.89187L20.159 16.3919C20.2853 16.6243 20.2798 16.906 20.1446 17.1333ZM14 8.20065V16H18.2388L14 8.20065Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SvgIcon>
|
||||
));
|
15
newIDE/app/src/UI/CustomSvgIcons/FlipVertical.js
Normal file
15
newIDE/app/src/UI/CustomSvgIcons/FlipVertical.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import SvgIcon from '@material-ui/core/SvgIcon';
|
||||
|
||||
export default React.memo(props => (
|
||||
<SvgIcon {...props} width="24" height="24" viewBox="2 2 20 20" fill="none">
|
||||
<path
|
||||
d="M17.1334 3.85537C17.3607 3.99056 17.5001 4.2355 17.5001 4.5V10.75C17.5001 11.1642 17.1643 11.5 16.7501 11.5H5.25007C4.90702 11.5 4.6077 11.2672 4.52318 10.9348C4.43867 10.6023 4.59052 10.2548 4.89194 10.091L16.3919 3.84103C16.6243 3.71473 16.9061 3.72018 17.1334 3.85537ZM8.20071 10H16.0001V5.76122L8.20071 10Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M17.5001 19.5C17.5001 19.7645 17.3607 20.0094 17.1334 20.1446C16.9061 20.2798 16.6243 20.2853 16.3919 20.159L4.89194 13.909C4.59052 13.7452 4.43867 13.3977 4.52318 13.0652C4.6077 12.7328 4.90702 12.5 5.25007 12.5H16.7501C17.1643 12.5 17.5001 12.8358 17.5001 13.25V19.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SvgIcon>
|
||||
));
|
508
newIDE/app/src/Utils/AffineTransformation.js
Normal file
508
newIDE/app/src/Utils/AffineTransformation.js
Normal file
@@ -0,0 +1,508 @@
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* Helper to transform coordinates from a referential to another, given an
|
||||
* affine transformation (scale, translate, rotate) to go from one to the other.
|
||||
* This file is a copy of the file affinetransformation.ts used in GDJS runtime.
|
||||
* TODO: Put file in SharedLibs and configure it so that it's shared between IDE
|
||||
* and runtime.
|
||||
*/
|
||||
|
||||
type FloatPoint = [number, number];
|
||||
|
||||
const nearlyEqual = (a: number, b: number, epsilon: number): boolean => {
|
||||
const absA = Math.abs(a);
|
||||
const absB = Math.abs(b);
|
||||
const diff = Math.abs(a - b);
|
||||
|
||||
if (a === b) {
|
||||
// shortcut, handles infinities
|
||||
return true;
|
||||
} else if (
|
||||
// eslint-disable-next-line
|
||||
a == 0 ||
|
||||
// eslint-disable-next-line
|
||||
b == 0 ||
|
||||
absA + absB < Number.EPSILON
|
||||
) {
|
||||
// a or b is zero or both are extremely close to it
|
||||
// relative error is less meaningful here
|
||||
return diff < epsilon * Number.EPSILON;
|
||||
} else {
|
||||
// use relative error
|
||||
return diff / Math.min(absA + absB, Number.MAX_VALUE) < epsilon;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* An affine transformation that can transform points.
|
||||
*/
|
||||
export class AffineTransformation {
|
||||
matrix: Float32Array;
|
||||
|
||||
/**
|
||||
* Initialize to the identity.
|
||||
*/
|
||||
constructor() {
|
||||
// | 1 0 0 |
|
||||
// | 0 1 0 |
|
||||
// | 0 0 1 |
|
||||
this.matrix = new Float32Array([1, 0, 0, 1, 0, 0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to the identity.
|
||||
*/
|
||||
setToIdentity() {
|
||||
const matrix = this.matrix;
|
||||
// | 1 0 0 |
|
||||
// | 0 1 0 |
|
||||
// | 0 0 1 |
|
||||
matrix[0] = 1;
|
||||
matrix[1] = 0;
|
||||
matrix[2] = 0;
|
||||
matrix[3] = 1;
|
||||
matrix[4] = 0;
|
||||
matrix[5] = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this transformation is the identity.
|
||||
*/
|
||||
isIdentity(): boolean {
|
||||
const matrix = this.matrix;
|
||||
return (
|
||||
matrix[0] === 1 &&
|
||||
matrix[1] === 0 &&
|
||||
matrix[2] === 0 &&
|
||||
matrix[3] === 1 &&
|
||||
matrix[4] === 0 &&
|
||||
matrix[5] === 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is equals to another transformation.
|
||||
* @param other The transformation to check.
|
||||
*/
|
||||
equals(other: AffineTransformation): boolean {
|
||||
const matrix = this.matrix;
|
||||
const otherMatrix = other.matrix;
|
||||
return (
|
||||
this === other ||
|
||||
(matrix[0] === otherMatrix[0] &&
|
||||
matrix[1] === otherMatrix[1] &&
|
||||
matrix[2] === otherMatrix[2] &&
|
||||
matrix[3] === otherMatrix[3] &&
|
||||
matrix[4] === otherMatrix[4] &&
|
||||
matrix[5] === otherMatrix[5])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is almost equals to another transformation.
|
||||
* @param other The transformation to check.
|
||||
* @param epsilon The relative margin error.
|
||||
*/
|
||||
nearlyEquals(other: AffineTransformation, epsilon: number): boolean {
|
||||
const matrix = this.matrix;
|
||||
const otherMatrix = other.matrix;
|
||||
return (
|
||||
this === other ||
|
||||
(nearlyEqual(matrix[0], otherMatrix[0], epsilon) &&
|
||||
nearlyEqual(matrix[1], otherMatrix[1], epsilon) &&
|
||||
nearlyEqual(matrix[2], otherMatrix[2], epsilon) &&
|
||||
nearlyEqual(matrix[3], otherMatrix[3], epsilon) &&
|
||||
nearlyEqual(matrix[4], otherMatrix[4], epsilon) &&
|
||||
nearlyEqual(matrix[5], otherMatrix[5], epsilon))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a transformation.
|
||||
* @param other The transformation to copy.
|
||||
*/
|
||||
copyFrom(other: AffineTransformation) {
|
||||
const matrix = this.matrix;
|
||||
const otherMatrix = other.matrix;
|
||||
|
||||
matrix[0] = otherMatrix[0];
|
||||
matrix[1] = otherMatrix[1];
|
||||
matrix[2] = otherMatrix[2];
|
||||
matrix[3] = otherMatrix[3];
|
||||
matrix[4] = otherMatrix[4];
|
||||
matrix[5] = otherMatrix[5];
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to a translation.
|
||||
*
|
||||
* @param x The horizontal translation value.
|
||||
* @param y The vertical translation value.
|
||||
*/
|
||||
setToTranslation(tx: number, ty: number) {
|
||||
const matrix = this.matrix;
|
||||
// | m0 m2 m4 | | 1 0 tx |
|
||||
// | m1 m3 m5 | = | 0 1 ty |
|
||||
// | 0 0 1 | | 0 0 1 |
|
||||
matrix[0] = 1;
|
||||
matrix[1] = 0;
|
||||
matrix[2] = 0;
|
||||
matrix[3] = 1;
|
||||
matrix[4] = tx;
|
||||
matrix[5] = ty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenate a translation.
|
||||
*
|
||||
* @param tx The horizontal translation value.
|
||||
* @param ty The vertical translation value.
|
||||
*/
|
||||
translate(tx: number, ty: number) {
|
||||
var matrix = this.matrix;
|
||||
// 1 0 tx
|
||||
// 0 1 ty
|
||||
// 0 0 1
|
||||
// m0 m2 m4
|
||||
// m1 m3 m5
|
||||
// 0 0 1
|
||||
matrix[4] = matrix[0] * tx + matrix[2] * ty + matrix[4];
|
||||
matrix[5] = matrix[1] * tx + matrix[3] * ty + matrix[5];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to a scale.
|
||||
*
|
||||
* @param sx The horizontal scale value.
|
||||
* @param sy The vertical scale value.
|
||||
*/
|
||||
setToScale(sx: number, sy: number) {
|
||||
const matrix = this.matrix;
|
||||
// | m0 m2 m4 | | sx 0 0 |
|
||||
// | m1 m3 m5 | = | 0 sy 0 |
|
||||
// | 0 0 1 | | 0 0 1 |
|
||||
matrix[0] = sx;
|
||||
matrix[1] = 0;
|
||||
matrix[2] = 0;
|
||||
matrix[3] = sy;
|
||||
matrix[4] = 0;
|
||||
matrix[5] = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenate a scale.
|
||||
*
|
||||
* @param sx The horizontal scale value.
|
||||
* @param sy The vertical scale value.
|
||||
*/
|
||||
scale(sx: number, sy: number) {
|
||||
const matrix = this.matrix;
|
||||
// sx 0 0
|
||||
// 0 sy 0
|
||||
// 0 0 1
|
||||
// m0 m2 m4
|
||||
// m1 m3 m5
|
||||
// 0 0 1
|
||||
matrix[0] *= sx;
|
||||
matrix[1] *= sx;
|
||||
matrix[2] *= sy;
|
||||
matrix[3] *= sy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to a rotation.
|
||||
*
|
||||
* @param angle The angle of rotation in radians.
|
||||
*/
|
||||
setToRotation(theta: number) {
|
||||
const matrix = this.matrix;
|
||||
let cost = Math.cos(theta);
|
||||
let sint = Math.sin(theta);
|
||||
|
||||
// Avoid rounding errors around 0.
|
||||
if (cost === -1 || cost === 1) {
|
||||
sint = 0;
|
||||
}
|
||||
if (sint === -1 || sint === 1) {
|
||||
cost = 0;
|
||||
}
|
||||
|
||||
// | m0 m2 m4 | | cost -sint 0 |
|
||||
// | m1 m3 m5 | = | sint cost 0 |
|
||||
// | 0 0 1 | | 0 0 1 |
|
||||
matrix[0] = cost;
|
||||
matrix[1] = sint;
|
||||
matrix[2] = -sint;
|
||||
matrix[3] = cost;
|
||||
matrix[4] = 0;
|
||||
matrix[5] = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenate a rotation.
|
||||
*
|
||||
* @param angle The angle of rotation in radians.
|
||||
*/
|
||||
rotate(angle: number) {
|
||||
const matrix = this.matrix;
|
||||
let cost = Math.cos(angle);
|
||||
let sint = Math.sin(angle);
|
||||
|
||||
// Avoid rounding errors around 0.
|
||||
if (cost === -1 || cost === 1) {
|
||||
sint = 0;
|
||||
}
|
||||
if (sint === -1 || sint === 1) {
|
||||
cost = 0;
|
||||
}
|
||||
|
||||
// cost -sint 0
|
||||
// sint cost 0
|
||||
// 0 0 1
|
||||
// m0 m2 m4
|
||||
// m1 m3 m5
|
||||
// 0 0 1
|
||||
|
||||
const m0 = matrix[0];
|
||||
const m1 = matrix[1];
|
||||
const m2 = matrix[2];
|
||||
const m3 = matrix[3];
|
||||
|
||||
matrix[0] = m0 * cost + m2 * sint;
|
||||
matrix[1] = m1 * cost + m3 * sint;
|
||||
matrix[2] = m0 * -sint + m2 * cost;
|
||||
matrix[3] = m1 * -sint + m3 * cost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to a rotation.
|
||||
*
|
||||
* @param angle The angle of rotation in radians.
|
||||
* @param anchorX The rotation anchor point X.
|
||||
* @param anchorY The rotation anchor point Y.
|
||||
*/
|
||||
setToRotationAround(angle: number, anchorX: number, anchorY: number) {
|
||||
const matrix = this.matrix;
|
||||
let cost = Math.cos(angle);
|
||||
let sint = Math.sin(angle);
|
||||
|
||||
// Avoid rounding errors around 0.
|
||||
if (cost === -1 || cost === 1) {
|
||||
sint = 0;
|
||||
}
|
||||
if (sint === -1 || sint === 1) {
|
||||
cost = 0;
|
||||
}
|
||||
|
||||
// | m0 m2 m4 | | cost -sint x-x*cost+y*sint |
|
||||
// | m1 m3 m5 | = | sint cost y-x*sint-y*cost |
|
||||
// | 0 0 1 | | 0 0 1 |
|
||||
matrix[0] = cost;
|
||||
matrix[1] = sint;
|
||||
matrix[2] = -sint;
|
||||
matrix[3] = cost;
|
||||
matrix[4] = anchorX - anchorX * cost + anchorY * sint;
|
||||
matrix[5] = anchorY - anchorX * sint + anchorY * cost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenate a rotation.
|
||||
*
|
||||
* @param angle The angle of rotation in radians.
|
||||
* @param anchorX The rotation anchor point X.
|
||||
* @param anchorY The rotation anchor point Y.
|
||||
*/
|
||||
rotateAround(angle: number, anchorX: number, anchorY: number) {
|
||||
this.translate(anchorX, anchorY);
|
||||
this.rotate(angle);
|
||||
// First: translate anchor to origin
|
||||
this.translate(-anchorX, -anchorY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to an horizontal flip.
|
||||
*
|
||||
* @param anchorX The flip anchor point X.
|
||||
*/
|
||||
setToFlipX(anchorX: number) {
|
||||
const matrix = this.matrix;
|
||||
// | m0 m2 m4 | | -1 0 2x |
|
||||
// | m1 m3 m5 | = | 0 1 0 |
|
||||
// | 0 0 1 | | 0 0 1 |
|
||||
matrix[0] = -1;
|
||||
matrix[1] = 0;
|
||||
matrix[2] = 0;
|
||||
matrix[3] = 1;
|
||||
matrix[4] = 2 * anchorX;
|
||||
matrix[5] = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenate an horizontal flip.
|
||||
*
|
||||
* @param anchorX The flip anchor point X.
|
||||
*/
|
||||
flipX(anchorX: number) {
|
||||
this.translate(anchorX, 0);
|
||||
this.scale(-1, 1);
|
||||
// First: translate anchor to origin
|
||||
this.translate(-anchorX, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to an vertical flip.
|
||||
*
|
||||
* @param anchorY The flip anchor point Y.
|
||||
*/
|
||||
setToFlipY(anchorY: number) {
|
||||
const matrix = this.matrix;
|
||||
// | m0 m2 m4 | | 1 0 0 |
|
||||
// | m1 m3 m5 | = | 0 -1 2x |
|
||||
// | 0 0 1 | | 0 0 1 |
|
||||
matrix[0] = -1;
|
||||
matrix[1] = 0;
|
||||
matrix[2] = 0;
|
||||
matrix[3] = 1;
|
||||
matrix[4] = 0;
|
||||
matrix[5] = 2 * anchorY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenate an vertical flip.
|
||||
*
|
||||
* @param anchorY The flip anchor point Y.
|
||||
*/
|
||||
flipY(anchorY: number) {
|
||||
this.translate(0, anchorY);
|
||||
this.scale(1, -1);
|
||||
// First: translate anchor to origin
|
||||
this.translate(0, -anchorY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenate a transformation after this one.
|
||||
* @param other The transformation to concatenate.
|
||||
*/
|
||||
concatenate(other: AffineTransformation) {
|
||||
const matrix = this.matrix;
|
||||
const otherMatrix = other.matrix;
|
||||
|
||||
const m0 = matrix[0];
|
||||
const m1 = matrix[1];
|
||||
const m2 = matrix[2];
|
||||
const m3 = matrix[3];
|
||||
const m4 = matrix[4];
|
||||
const m5 = matrix[5];
|
||||
|
||||
const o0 = otherMatrix[0];
|
||||
const o1 = otherMatrix[1];
|
||||
const o2 = otherMatrix[2];
|
||||
const o3 = otherMatrix[3];
|
||||
const o4 = otherMatrix[4];
|
||||
const o5 = otherMatrix[5];
|
||||
|
||||
// o0 o2 o4
|
||||
// o1 o3 o5
|
||||
// 0 0 1
|
||||
// m0 m2 m4
|
||||
// m1 m3 m5
|
||||
// 0 0 1
|
||||
matrix[0] = o0 * m0 + o1 * m2;
|
||||
matrix[1] = o0 * m1 + o1 * m3;
|
||||
matrix[2] = o2 * m0 + o3 * m2;
|
||||
matrix[3] = o2 * m1 + o3 * m3;
|
||||
matrix[4] = o4 * m0 + o5 * m2 + m4;
|
||||
matrix[5] = o4 * m1 + o5 * m3 + m5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenate a transformation before this one.
|
||||
* @param other The transformation to concatenate.
|
||||
*/
|
||||
preConcatenate(other: AffineTransformation) {
|
||||
const matrix = this.matrix;
|
||||
const otherMatrix = other.matrix;
|
||||
|
||||
const m0 = matrix[0];
|
||||
const m1 = matrix[1];
|
||||
const m2 = matrix[2];
|
||||
const m3 = matrix[3];
|
||||
const m4 = matrix[4];
|
||||
const m5 = matrix[5];
|
||||
|
||||
const o0 = otherMatrix[0];
|
||||
const o1 = otherMatrix[1];
|
||||
const o2 = otherMatrix[2];
|
||||
const o3 = otherMatrix[3];
|
||||
const o4 = otherMatrix[4];
|
||||
const o5 = otherMatrix[5];
|
||||
|
||||
// m0 m2 m4
|
||||
// m1 m3 m5
|
||||
// 0 0 1
|
||||
// o0 o2 o4
|
||||
// o1 o3 o5
|
||||
// 0 0 1
|
||||
matrix[0] = m0 * o0 + m1 * o2;
|
||||
matrix[1] = m0 * o1 + m1 * o3;
|
||||
matrix[2] = m2 * o0 + m3 * o2;
|
||||
matrix[3] = m2 * o1 + m3 * o3;
|
||||
matrix[4] = m4 * o0 + m5 * o2 + o4;
|
||||
matrix[5] = m4 * o1 + m5 * o3 + o5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a point.
|
||||
*
|
||||
* @param source The point to transform.
|
||||
* @param destination The Point to store the transformed coordinates.
|
||||
*/
|
||||
transform(source: FloatPoint, destination: FloatPoint) {
|
||||
const matrix = this.matrix;
|
||||
// x
|
||||
// y
|
||||
// 1
|
||||
// m0 m2 m4
|
||||
// m1 m3 m5
|
||||
// 0 0 1
|
||||
const x = matrix[0] * source[0] + matrix[2] * source[1] + matrix[4];
|
||||
const y = matrix[1] * source[0] + matrix[3] * source[1] + matrix[5];
|
||||
destination[0] = x;
|
||||
destination[1] = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invert the matrix.
|
||||
*/
|
||||
invert() {
|
||||
const matrix = this.matrix;
|
||||
|
||||
const m0 = matrix[0];
|
||||
const m1 = matrix[1];
|
||||
const m2 = matrix[2];
|
||||
const m3 = matrix[3];
|
||||
const m4 = matrix[4];
|
||||
const m5 = matrix[5];
|
||||
|
||||
const n = m0 * m3 - m1 * m2;
|
||||
|
||||
matrix[0] = m3 / n;
|
||||
matrix[1] = -m1 / n;
|
||||
matrix[2] = -m2 / n;
|
||||
matrix[3] = m0 / n;
|
||||
matrix[4] = (m2 * m5 - m3 * m4) / n;
|
||||
matrix[5] = -(m0 * m5 - m1 * m4) / n;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
toString() {
|
||||
const matrix = this.matrix;
|
||||
return `[[${matrix[0]} ${matrix[1]}] [${matrix[2]} ${matrix[3]}] [${
|
||||
matrix[4]
|
||||
} ${matrix[5]}]]`;
|
||||
}
|
||||
}
|
@@ -72,6 +72,8 @@ export const Default = () => (
|
||||
}}
|
||||
wrappedEditorRef={() => {}}
|
||||
pauseRendering={false}
|
||||
tileMapTileSelection={null}
|
||||
onSelectTileMapTile={() => {}}
|
||||
/>
|
||||
</DragAndDropContextProvider>
|
||||
</FixedHeightFlexContainer>
|
||||
|
@@ -37,6 +37,8 @@ export const InstanceSprite2d = () => (
|
||||
editInstanceVariables={action('edit instance variables')}
|
||||
onGetInstanceSize={() => [100, 101, 102]}
|
||||
onEditObjectByName={action('edit object')}
|
||||
tileMapTileSelection={null}
|
||||
onSelectTileMapTile={() => {}}
|
||||
/>
|
||||
</SerializedObjectDisplay>
|
||||
)}
|
||||
@@ -63,6 +65,8 @@ export const InstanceCube3d = () => (
|
||||
editInstanceVariables={action('edit instance variables')}
|
||||
onGetInstanceSize={() => [100, 101, 102]}
|
||||
onEditObjectByName={action('edit object')}
|
||||
tileMapTileSelection={null}
|
||||
onSelectTileMapTile={() => {}}
|
||||
/>
|
||||
</SerializedObjectDisplay>
|
||||
)}
|
||||
@@ -89,6 +93,8 @@ export const InstanceTextInput = () => (
|
||||
editInstanceVariables={action('edit instance variables')}
|
||||
onGetInstanceSize={() => [120, 40, 0]}
|
||||
onEditObjectByName={action('edit object')}
|
||||
tileMapTileSelection={null}
|
||||
onSelectTileMapTile={() => {}}
|
||||
/>
|
||||
</SerializedObjectDisplay>
|
||||
)}
|
||||
|
Reference in New Issue
Block a user