More work on tiled maps

This commit is contained in:
soywiz
2020-01-16 03:24:04 +01:00
parent 8f1d83e4e2
commit 3adcb0fa9a
6 changed files with 463 additions and 4 deletions

View File

@@ -0,0 +1,31 @@
package com.soywiz.korge.intellij.createnew
import com.intellij.ide.actions.*
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.project.*
import com.intellij.psi.*
import com.soywiz.korge.intellij.*
class NewTiledMap : CreateFileFromTemplateAction(
"Tiled Map",
"Creates new .tmx Tile Map",
KorgeIcons.TILED
), DumbAware {
override fun getActionName(directory: PsiDirectory?, newName: String, templateName: String?): String = "Tiled Map"
override fun isAvailable(dataContext: DataContext): Boolean = dataContext.project?.korge?.containsKorge ?: false
override fun buildDialog(
project: Project,
directory: PsiDirectory,
builder: CreateFileFromTemplateDialog.Builder
) {
builder
.setTitle("New Tiled Map")
.addKind("TileMap", KorgeIcons.TILED, "TiledMap")
}
//override fun createFileFromTemplate(name: String?, template: FileTemplate?, dir: PsiDirectory?): PsiFile? {
// return createFile(name, "KorgeScene.kt", dir)
//}
}

View File

@@ -2,7 +2,6 @@ package com.soywiz.korge.intellij.editor.tile
import com.intellij.ui.components.*
import com.intellij.uiDesigner.core.*
import com.soywiz.korge.tiled.*
import com.soywiz.korim.awt.*
import com.soywiz.korim.bitmap.*
import com.soywiz.korim.color.*

View File

@@ -17,7 +17,7 @@ import java.awt.*
import java.beans.*
import javax.swing.*
class TileMapEditorProvider : FileEditorProvider {
class TileMapEditorProvider : FileEditorProvider, DumbAware {
override fun getEditorTypeId(): String = this::class.java.name
override fun getPolicy(): FileEditorPolicy = FileEditorPolicy.PLACE_BEFORE_DEFAULT_EDITOR
@@ -36,7 +36,7 @@ class TileMapEditorProvider : FileEditorProvider {
val tmxFile = file.toVfs()
val tmx = runBlocking { tmxFile.readTiledMap() }
return object : FileEditor {
return object : FileEditor, DumbAware {
val panel by lazy { MyTileMapEditorPanel(tmx).realPanel }
override fun isModified(): Boolean {

View File

@@ -0,0 +1,411 @@
// @TODO: @WARNING: Duplicated from KorGE to be able to modify it. Please, copy again to KorGE once this is stable
package com.soywiz.korge.intellij.editor.tile
import com.soywiz.kds.iterators.*
import com.soywiz.klogger.*
import com.soywiz.kmem.*
import com.soywiz.korge.view.tiles.*
import com.soywiz.korim.bitmap.*
import com.soywiz.korim.color.*
import com.soywiz.korim.format.*
import com.soywiz.korio.compression.*
import com.soywiz.korio.compression.deflate.*
import com.soywiz.korio.file.*
import com.soywiz.korio.lang.*
import com.soywiz.korio.serialization.xml.*
import com.soywiz.korio.util.encoding.*
import com.soywiz.korma.geom.*
class TiledMapData {
var width = 0
var height = 0
var tilewidth = 0
var tileheight = 0
val pixelWidth: Int get() = width * tilewidth
val pixelHeight: Int get() = height * tileheight
val allLayers = arrayListOf<TiledMap.Layer>()
inline val patternLayers get() = allLayers.patterns
inline val imageLayers get() = allLayers.images
inline val objectLayers get() = allLayers.objects
val tilesets = arrayListOf<TileSetData>()
fun getObjectByName(name: String) = objectLayers.mapNotNull { it.getByName(name) }.firstOrNull()
val maxGid get() = tilesets.map { it.firstgid + it.tilecount }.max() ?: 0
}
fun TiledMap.Layer.Objects.Object.getPos(map: TiledMapData): IPoint =
IPoint(bounds.x / map.tilewidth, bounds.y / map.tileheight)
fun TiledMapData?.getObjectPosByName(name: String): IPoint? {
val obj = this?.getObjectByName(name) ?: return null
return obj.getPos(this)
}
data class TileSetData(
val name: String,
val firstgid: Int,
val tilewidth: Int,
val tileheight: Int,
val tilecount: Int,
val columns: Int,
val image: Xml?,
val source: String,
val width: Int,
val height: Int
)
//e: java.lang.UnsupportedOperationException: Class literal annotation arguments are not yet supported: Factory
//@AsyncFactoryClass(TiledMapFactory::class)
class TiledMap(
val data: TiledMapData,
val tilesets: List<TiledTileset>,
val tileset: TileSet
) {
val width get() = data.width
val height get() = data.height
val tilewidth get() = data.tilewidth
val tileheight get() = data.tileheight
val pixelWidth: Int get() = data.pixelWidth
val pixelHeight: Int get() = data.pixelHeight
val allLayers get() = data.allLayers
val patternLayers get() = data.patternLayers
val imageLayers get() = data.imageLayers
val objectLayers get() = data.objectLayers
class TiledTileset(val tileset: TileSet, val firstgid: Int = 0) {
}
sealed class Layer {
var name: String = ""
var visible: Boolean = true
var draworder: String = ""
var color: RGBA = Colors.WHITE
var opacity = 1.0
var offsetx: Double = 0.0
var offsety: Double = 0.0
val properties = hashMapOf<String, Any>()
class Patterns : Layer() {
//val tilemap = TileMap(Bitmap32(0, 0), )
var map: Bitmap32 = Bitmap32(0, 0)
}
data class ObjectInfo(
val id: Int, val name: String, val type: String,
val bounds: IRectangleInt,
val objprops: Map<String, Any>
)
class Objects : Layer() {
interface Object {
val info: ObjectInfo
}
interface Poly : Object {
val points: List<IPoint>
}
data class Rect(override val info: ObjectInfo) : Object
data class Ellipse(override val info: ObjectInfo) : Object
data class Polyline(override val info: ObjectInfo, override val points: List<IPoint>) : Poly
data class Polygon(override val info: ObjectInfo, override val points: List<IPoint>) : Poly
val objects = arrayListOf<Object>()
val objectsById by lazy { objects.associateBy { it.id } }
val objectsByName by lazy { objects.associateBy { it.name } }
fun getById(id: Int): Object? = objectsById[id]
fun getByName(name: String): Object? = objectsByName[name]
}
class Image : Layer() {
var width = 0
var height = 0
var source = ""
var image: Bitmap = Bitmap32(0, 0)
}
}
}
val TiledMap.Layer.Objects.Object.id get() = this.info.id
val TiledMap.Layer.Objects.Object.name get() = this.info.name
val TiledMap.Layer.Objects.Object.bounds get() = this.info.bounds
val TiledMap.Layer.Objects.Object.objprops get() = this.info.objprops
inline val Iterable<TiledMap.Layer>.patterns get() = this.filterIsInstance<TiledMap.Layer.Patterns>()
inline val Iterable<TiledMap.Layer>.images get() = this.filterIsInstance<TiledMap.Layer.Image>()
inline val Iterable<TiledMap.Layer>.objects get() = this.filterIsInstance<TiledMap.Layer.Objects>()
private val spaces = Regex("\\s+")
val tilemapLog = Logger("tilemap")
class TiledFile(val name: String)
private fun Xml.parseProperties(): Map<String, Any> {
val out = LinkedHashMap<String, Any>()
for (property in this.children("property")) {
val pname = property.str("name")
val rawValue = if (property.hasAttribute("value")) property.str("value") else property.text
val type = property.str("type", "text")
val pvalue: Any = when (type) {
"bool" -> rawValue == "true"
"color" -> Colors[rawValue]
"text" -> rawValue
"int" -> rawValue.toIntOrNull() ?: 0
"float" -> rawValue.toDoubleOrNull() ?: 0.0
"file" -> TiledFile(pname)
else -> rawValue
}
out[pname] = pvalue
//println("$pname: $pvalue")
}
return out
}
suspend fun VfsFile.readTiledMapData(): TiledMapData {
val log = tilemapLog
val file = this
val folder = this.parent.jail()
val tiledMap = TiledMapData()
val mapXml = file.readXml()
if (mapXml.nameLC != "map") error("Not a TiledMap XML TMX file starting with <map>")
tiledMap.width = mapXml.getInt("width") ?: 0
tiledMap.height = mapXml.getInt("height") ?: 0
tiledMap.tilewidth = mapXml.getInt("tilewidth") ?: 32
tiledMap.tileheight = mapXml.getInt("tileheight") ?: 32
tilemapLog.trace { "tilemap: width=${tiledMap.width}, height=${tiledMap.height}, tilewidth=${tiledMap.tilewidth}, tileheight=${tiledMap.tileheight}" }
tilemapLog.trace { "tilemap: $tiledMap" }
val elements = mapXml.allChildrenNoComments
tilemapLog.trace { "tilemap: elements=${elements.size}" }
tilemapLog.trace { "tilemap: elements=$elements" }
var maxGid = 1
//var lastBaseTexture = views.transparentTexture.base
elements.fastForEach { element ->
val elementName = element.nameLC
@Suppress("IntroduceWhenSubject") // @TODO: BUG IN KOTLIN-JS with multicase in suspend functions
when {
elementName == "tileset" -> {
tilemapLog.trace { "tileset" }
val firstgid = element.int("firstgid")
// TSX file
val element = if (element.hasAttribute("source")) {
folder[element.str("source")].readXml()
} else {
element
}
val name = element.str("name")
val tilewidth = element.int("tilewidth")
val tileheight = element.int("tileheight")
val tilecount = element.int("tilecount", -1)
val columns = element.int("columns", -1)
val image = element.child("image")
val source = image?.str("source") ?: ""
val width = image?.int("width", 0) ?: 0
val height = image?.int("height", 0) ?: 0
tiledMap.tilesets += TileSetData(
name = name,
firstgid = firstgid,
tilewidth = tilewidth,
tileheight = tileheight,
tilecount = tilecount,
columns = columns,
image = image,
source = source,
width = width,
height = height
)
//lastBaseTexture = tex.base
}
elementName == "layer" || elementName == "objectgroup" || elementName == "imagelayer" -> {
tilemapLog.trace { "layer:$elementName" }
val layer = when (element.nameLC) {
"layer" -> TiledMap.Layer.Patterns()
"objectgroup" -> TiledMap.Layer.Objects()
"imagelayer" -> TiledMap.Layer.Image()
else -> invalidOp
}
tiledMap.allLayers += layer
layer.name = element.str("name")
layer.visible = element.int("visible", 1) != 0
layer.draworder = element.str("draworder", "")
layer.color = Colors[element.str("color", "#ffffff")]
layer.opacity = element.double("opacity", 1.0)
layer.offsetx = element.double("offsetx", 0.0)
layer.offsety = element.double("offsety", 0.0)
val properties = element.child("properties")?.parseProperties()
if (properties != null) {
layer.properties.putAll(properties)
}
when (layer) {
is TiledMap.Layer.Patterns -> {
val width = element.int("width")
val height = element.int("height")
val count = width * height
val data = element.child("data")
val encoding = data?.str("encoding", "") ?: ""
val compression = data?.str("compression", "") ?: ""
@Suppress("IntroduceWhenSubject") // @TODO: BUG IN KOTLIN-JS with multicase in suspend functions
val tilesArray: IntArray = when {
encoding == "" || encoding == "xml" -> {
val items = data?.children("tile")?.map { it.int("gid") } ?: listOf()
items.toIntArray()
}
encoding == "csv" -> {
val content = data?.text ?: ""
val items = content.replace(spaces, "").split(',').map(String::toInt)
items.toIntArray()
}
encoding == "base64" -> {
val base64Content = (data?.text ?: "").trim()
val rawContent = base64Content.fromBase64()
val content = when (compression) {
"" -> rawContent
"gzip" -> rawContent.uncompress(GZIP)
"zlib" -> rawContent.uncompress(ZLib)
else -> invalidOp("Unknown compression '$compression'")
}
content.readIntArrayLE(0, count)
}
else -> invalidOp("Unhandled encoding '$encoding'")
}
if (tilesArray.size != count) invalidOp("tilesArray.size != count (${tilesArray.size} != ${count})")
layer.map = Bitmap32(width, height, RgbaArray(tilesArray))
}
is TiledMap.Layer.Image -> {
for (image in element.children("image")) {
layer.source = image.str("source")
layer.width = image.int("width")
layer.height = image.int("height")
}
}
is TiledMap.Layer.Objects -> {
for (obj in element.children("object")) {
val id = obj.int("id")
val name = obj.str("name")
val type = obj.str("type")
val bounds = obj.run { IRectangleInt(int("x"), int("y"), int("width"), int("height")) }
var rkind = RKind.RECT
var points = listOf<IPoint>()
var objprops: Map<String, Any> = LinkedHashMap()
for (kind in obj.allNodeChildren) {
val kindType = kind.nameLC
@Suppress("IntroduceWhenSubject") // @TODO: BUG IN KOTLIN-JS with multicase in suspend functions
when {
kindType == "ellipse" -> {
rkind = RKind.ELLIPSE
}
kindType == "polyline" || kindType == "polygon" -> {
val pointsStr = kind.str("points")
points = pointsStr.split(spaces).map {
val parts = it.split(',').map { it.trim().toDoubleOrNull() ?: 0.0 }
IPoint(parts[0], parts[1])
}
rkind = (if (kindType == "polyline") RKind.POLYLINE else RKind.POLYGON)
}
kindType == "properties" -> {
objprops = kind.parseProperties()
}
else -> invalidOp("Invalid object kind '$kindType'")
}
}
val info = TiledMap.Layer.ObjectInfo(id, name, type, bounds, objprops)
layer.objects += when (rkind) {
RKind.RECT -> TiledMap.Layer.Objects.Rect(info)
RKind.ELLIPSE -> TiledMap.Layer.Objects.Ellipse(info)
RKind.POLYLINE -> TiledMap.Layer.Objects.Polyline(info, points)
RKind.POLYGON -> TiledMap.Layer.Objects.Polygon(info, points)
}
}
}
}
}
}
}
return tiledMap
}
suspend fun VfsFile.readTiledMap(
hasTransparentColor: Boolean = false,
transparentColor: RGBA = Colors.FUCHSIA,
createBorder: Int = 1
): TiledMap {
val folder = this.parent.jail()
val data = readTiledMapData()
//val combinedTileset = kotlin.arrayOfNulls<Texture>(data.maxGid + 1)
val combinedTileset = arrayOfNulls<BmpSlice>(data.maxGid + 1)
data.imageLayers.fastForEach { layer ->
layer.image = folder[layer.source].readBitmapOptimized()
}
val tiledTilesets = arrayListOf<TiledMap.TiledTileset>()
data.tilesets.fastForEach { tileset ->
var bmp = folder[tileset.source].readBitmapOptimized()
// @TODO: Preprocess this, so in JS we don't have to do anything!
if (hasTransparentColor) {
bmp = bmp.toBMP32()
for (n in 0 until bmp.area) {
if (bmp.data[n] == transparentColor) bmp.data[n] = Colors.TRANSPARENT_BLACK
}
}
val ptileset = if (createBorder > 0) {
bmp = bmp.toBMP32()
val slices =
TileSet.extractBitmaps(bmp, tileset.tilewidth, tileset.tileheight, tileset.columns, tileset.tilecount)
TileSet.fromBitmaps(
tileset.tilewidth, tileset.tileheight,
slices,
border = createBorder,
mipmaps = false
)
} else {
TileSet(bmp.slice(), tileset.tilewidth, tileset.tileheight, tileset.columns, tileset.tilecount)
}
val tiledTileset = TiledMap.TiledTileset(
tileset = ptileset,
firstgid = tileset.firstgid
)
tiledTilesets += tiledTileset
//lastBaseTexture = tex.base
tilemapLog.trace { "tileset:$tiledTileset" }
for (n in 0 until ptileset.textures.size) {
combinedTileset[tileset.firstgid + n] = ptileset.textures[n]
}
}
return TiledMap(data, tiledTilesets, TileSet(combinedTileset.toList(), data.tilewidth, data.tileheight))
}
private enum class RKind {
RECT, ELLIPSE, POLYLINE, POLYGON
}

View File

@@ -134,7 +134,8 @@
<separator/>
<action id="Korge.NewFile.Atlas" class="com.soywiz.korge.intellij.createnew.NewKorgeScene"/>
<action id="Korge.NewFile.KorgeScene" class="com.soywiz.korge.intellij.createnew.NewKorgeScene"/>
<action id="Korge.NewFile.TiledMap" class="com.soywiz.korge.intellij.createnew.NewTiledMap"/>
<separator/>
</group>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.2" tiledversion="1.3.1" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="10" height="10" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
<layer id="1" name="Tile Layer 1" width="10" height="10">
<data encoding="csv">
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0
</data>
</layer>
</map>