Compare commits

...

66 Commits

Author SHA1 Message Date
AlexandreSi
5eb5fb3af9 Add checkbox to improve discoverability of tilemap hitboxes 2024-07-19 10:41:07 +02:00
AlexandreSi
cd80872fca Rename existing tilemap object adding external mention in the name 2024-07-19 10:19:49 +02:00
AlexandreSi
61cb6dde0e Replace path painting with rectangle painting 2024-07-19 09:29:52 +02:00
AlexandreSi
29c8de0987 Refactor 2024-07-18 15:41:05 +02:00
AlexandreSi
802e9a5128 Add possibility to select hitboxes using a rectangle 2024-07-18 14:46:38 +02:00
AlexandreSi
c82602919b Update collision mask when tilemap is dirty 2024-07-18 13:40:12 +02:00
AlexandreSi
80cbb8f542 Make it possible to select tiles with a rectangle when setting hit boxes 2024-07-18 11:11:45 +02:00
AlexandreSi
a9ceba7bef Rename file 2024-07-18 10:31:05 +02:00
AlexandreSi
7d9e72c876 Prevent having tile with ids outside of tile set 2024-07-17 16:18:43 +02:00
AlexandreSi
3baf5903d3 Clean a few things 2024-07-17 16:00:42 +02:00
AlexandreSi
6008e1045d Change width and height and opacity from network sync only if different 2024-07-17 15:16:28 +02:00
AlexandreSi
9dda9a89c5 Remove selected instance fill color when painting 2024-07-17 15:16:28 +02:00
AlexandreSi
814c6aec4f Handle rendered instance of tilemap when tilemap is empty 2024-07-17 15:16:28 +02:00
AlexandreSi
84e4158407 Fix typing 2024-07-17 15:16:28 +02:00
AlexandreSi
aa6c3bc1c8 Recompute tile set on tilemap object parameters changes 2024-07-17 15:16:28 +02:00
AlexandreSi
41dc6d0966 Improve runtime typing 2024-07-17 15:16:28 +02:00
AlexandreSi
9c19865dec Fix typing 2024-07-17 15:16:28 +02:00
AlexandreSi
bbfb7f63f0 Save to history when changing tilemap 2024-07-17 15:16:28 +02:00
AlexandreSi
2f8304c2dd Deactivate painting when pressing escape 2024-07-17 15:16:28 +02:00
AlexandreSi
c1aa88861d Add cancel painting action on press escape 2024-07-17 15:16:28 +02:00
AlexandreSi
254ab0350b Display info about tiles with hitboxes 2024-07-17 15:16:28 +02:00
AlexandreSi
b0a3c34803 Use object configuration to set hit boxes 2024-07-17 15:16:28 +02:00
AlexandreSi
63a3265310 Store tiles with hitbox in object configuration 2024-07-17 15:16:28 +02:00
AlexandreSi
d19292f43d Initiate SimpleTileMap object editor 2024-07-17 15:16:28 +02:00
AlexandreSi
3646d05b65 Simplify TileMapPainter component and types 2024-07-17 15:16:28 +02:00
AlexandreSi
25a4d04896 Remove useless react fragment 2024-07-17 15:16:28 +02:00
AlexandreSi
2df6c13ed7 Make opacity setting work for simple tile maps 2024-07-17 15:16:28 +02:00
AlexandreSi
93f84a14cb Implement missing methods for collisions to work 2024-07-17 15:16:28 +02:00
AlexandreSi
02e0ebf63b Post rebase changes 2024-07-17 15:16:28 +02:00
AlexandreSi
da3abb331a Add collision tilemap to simple tile map object 2024-07-17 15:16:28 +02:00
AlexandreSi
a5a9524ed6 Add possibility to set flipping options in tile setting action 2024-07-17 15:16:28 +02:00
AlexandreSi
c9cc1f6fdc Use reusable float point 2024-07-17 15:16:28 +02:00
AlexandreSi
de140f1190 Reposition tilemap when setting/removing tiles 2024-07-17 15:16:28 +02:00
AlexandreSi
9a82fda7fe Use affine transformation in runtime actions 2024-07-17 15:16:28 +02:00
AlexandreSi
eac92704fa Display tile id as title when hovering tileset 2024-07-17 15:16:28 +02:00
AlexandreSi
dc607d85c3 Use AffineTransformation to display tile preview and set tile at coordinates 2024-07-17 15:16:28 +02:00
AlexandreSi
e96269d899 Aff AffineTransformation util from runtime 2024-07-17 15:16:27 +02:00
AlexandreSi
05d622c5c0 Add TODO 2024-07-17 15:16:27 +02:00
AlexandreSi
0758397196 Use flipping controls when painting and previewing the tile to be set 2024-07-17 15:16:27 +02:00
AlexandreSi
65293ddd99 Update tilemap painter with icons and flipping selectors 2024-07-17 15:16:27 +02:00
AlexandreSi
225a3a67c6 Add TODO 2024-07-17 15:16:27 +02:00
AlexandreSi
a96171aacd Add actions to change change tiles in the runtime 2024-07-17 15:16:27 +02:00
AlexandreSi
1a21a0bfb3 Actually hide tilemap instance property 2024-07-17 15:16:27 +02:00
AlexandreSi
cc23301875 Initiate SimpleTileMap runtime object 2024-07-17 15:16:27 +02:00
AlexandreSi
aa513c04e6 Store tilemap on instance instead of object 2024-07-17 15:16:27 +02:00
AlexandreSi
a4e8e8c00b Add method to check if tilemap is empty 2024-07-17 15:16:27 +02:00
AlexandreSi
0d34680fcd Resize and reposition tilemap after trimming tilemap 2024-07-17 15:16:27 +02:00
AlexandreSi
4abdb9dca6 Add methods to trim tilemap from empty rows and columns 2024-07-17 15:16:27 +02:00
AlexandreSi
db05a07023 Add possibility to erase tile from the tilemap 2024-07-17 15:16:27 +02:00
AlexandreSi
78ebe58713 Add possibility to paint with click + drag 2024-07-17 15:16:27 +02:00
AlexandreSi
420c7a4429 Adapt state to support more than one tile selected 2024-07-17 15:16:27 +02:00
AlexandreSi
1442a2772e Support tilemaps with custom heights and widths 2024-07-17 15:16:27 +02:00
AlexandreSi
ce18bdd5a7 Reposition instance after adding columns/rows in negative indices 2024-07-17 15:16:27 +02:00
AlexandreSi
2f2cc1bbe3 Support setting tiles in negative indices 2024-07-17 15:16:27 +02:00
AlexandreSi
097ad4fff5 Make it possible to paint outside of the tilemap dimensions (positive) 2024-07-17 15:16:27 +02:00
AlexandreSi
2112ed789a Set tilemap attribute on object when painting 2024-07-17 15:16:27 +02:00
AlexandreSi
a41a1c14a0 Modify tilemap when clicking on scene when tile is selected 2024-07-17 15:16:27 +02:00
AlexandreSi
771d3264bb Load simple tilemap into editable tile map for scene editor renderer 2024-07-17 15:16:27 +02:00
AlexandreSi
9fa1e552e0 Complete unknown instance rectangle 2024-07-17 15:16:27 +02:00
AlexandreSi
503a0873d9 Make tile map preview position on grid 2024-07-17 15:16:27 +02:00
AlexandreSi
715480bdef Tile map tile preview in Scene editor 2024-07-17 15:16:27 +02:00
AlexandreSi
869fd7eb08 Transfer selected tile map tile data to scene editor 2024-07-17 15:16:26 +02:00
AlexandreSi
e0cb8e4953 Improve object declaration and setup tilemap painter 2024-07-17 15:16:26 +02:00
AlexandreSi
0ef4953241 WIP: Add TileMapPainter 2024-07-17 15:16:26 +02:00
AlexandreSi
d154384164 WIP introduce simple tile map object 2024-07-17 15:16:26 +02:00
AlexandreSi
504df2e0a3 Fix comment 2024-07-17 15:16:26 +02:00
42 changed files with 4465 additions and 278 deletions

View File

@@ -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(

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"}

View File

@@ -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
*

View File

@@ -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"}

View 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
);
}

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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,
};
}
/**

View File

@@ -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>();

View File

@@ -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
*

View 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;

View File

@@ -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>

View File

@@ -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!...

View File

@@ -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
*/

View File

@@ -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(

View 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;

View 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;

View File

@@ -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

View File

@@ -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();

View 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;

View File

@@ -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>
);
};

View File

@@ -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(),

View File

@@ -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',

View File

@@ -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;

View File

@@ -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,

View File

@@ -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}
/>
),
},

View File

@@ -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>

View File

@@ -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={

View 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>
));

View 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>
));

View 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>
));

View 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>
));

View 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]}]]`;
}
}

View File

@@ -72,6 +72,8 @@ export const Default = () => (
}}
wrappedEditorRef={() => {}}
pauseRendering={false}
tileMapTileSelection={null}
onSelectTileMapTile={() => {}}
/>
</DragAndDropContextProvider>
</FixedHeightFlexContainer>

View File

@@ -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>
)}