diff --git a/gradle.properties b/gradle.properties index a9fea36..1c1e83b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,4 @@ kotlin.code.style=official # version kotlinVersion=1.3.72 -korgeVersion=1.12.2.2 \ No newline at end of file +korgeVersion=1.13.8.3 \ No newline at end of file diff --git a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TiledMap.kt b/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TiledMap.kt deleted file mode 100644 index 649051c..0000000 --- a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TiledMap.kt +++ /dev/null @@ -1,223 +0,0 @@ -// @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.klogger.* -import com.soywiz.korge.view.tiles.* -import com.soywiz.korim.bitmap.* -import com.soywiz.korim.color.* -import com.soywiz.korio.serialization.xml.* -import com.soywiz.korma.geom.* -import kotlin.reflect.* - -class TiledMapData( - var width: Int = 0, - var height: Int = 0, - var tilewidth: Int = 0, - var tileheight: Int = 0, - val allLayers: MutableList = arrayListOf(), - val tilesets: MutableList = arrayListOf() -) { - val maxGid get() = tilesets.map { it.firstgid + it.tilecount }.max() ?: 0 - val pixelWidth: Int get() = width * tilewidth - val pixelHeight: Int get() = height * tileheight - inline val tileLayers get() = allLayers.tiles - inline val imageLayers get() = allLayers.images - inline val objectLayers get() = allLayers.objects - fun getObjectByName(name: String) = objectLayers.mapNotNull { it.getByName(name) }.firstOrNull() - fun clone() = TiledMapData( - width, height, tilewidth, tileheight, - allLayers.map { it.clone() }.toMutableList(), - tilesets.map { it.clone() }.toMutableList() - ) -} - -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 TerrainData( - val name: String, - val tile: Int -) - -data class AnimationFrameData( - val tileid: Int, val duration: Int -) - -data class TerrainInfo(val info: List) { - operator fun get(x: Int, y: Int): Int? = if (x in 0..1 && y in 0..1) info[y * 2 + x] else null -} - -data class TileData( - val id: Int, - val terrain: List? = null, - val probability: Double = 1.0, - val frames: List? = null -) { - val terrainInfo = TerrainInfo(terrain ?: listOf(null, null ,null, null)) -} - -data class TileSetData constructor( - val name: String = "unknown", - val firstgid: Int = 1, - val tilewidth: Int, - val tileheight: Int, - val tilecount: Int, - val columns: Int, - val image: Xml?, - val imageSource: String, - val width: Int, - val height: Int, - val tilesetSource: String? = null, - val terrains: List = listOf(), - val tiles: List = listOf() -) { - fun clone() = copy() -} - -//e: java.lang.UnsupportedOperationException: Class literal annotation arguments are not yet supported: Factory -//@AsyncFactoryClass(TiledMapFactory::class) -class TiledMap constructor( - var data: TiledMapData, - var tilesets: MutableList -) { - 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 tileLayers get() = data.tileLayers - val imageLayers get() = data.imageLayers - val objectLayers get() = data.objectLayers - val nextGid get() = tilesets.map { it.firstgid + it.tileset.textures.size }.max() ?: 1 - - fun clone() = TiledMap(data.clone(), tilesets.map { it.clone() }.toMutableList()) - - data class TiledTileset( - val tileset: TileSet, - val data: TileSetData = TileSetData( - name = "unknown", - firstgid = 1, - tilewidth = tileset.width, - tileheight = tileset.height, - tilecount = tileset.textures.size, - columns = tileset.base.width / tileset.width, - image = null, - imageSource = "", - width = tileset.base.width, - height = tileset.base.height, - tilesetSource = null, - terrains = listOf(), - tiles = tileset.textures.mapIndexed { index, bmpSlice -> TileData(index) } - ), - val firstgid: Int = 1 - ) { - fun clone(): TiledTileset = TiledTileset(tileset.clone(), data.clone(), firstgid) - } - - sealed class Layer { - var id: Int = 1 - var name: String = "" - var visible: Boolean = true - var locked: Boolean = false - var draworder: String = "" - var color: RGBA = Colors.WHITE - var opacity = 1.0 - var offsetx: Double = 0.0 - var offsety: Double = 0.0 - val properties = LinkedHashMap() - companion object { - val BASE_PROPS = listOf( - Layer::id, - Layer::name, Layer::visible, Layer::locked, Layer::draworder, - Layer::color, Layer::opacity, Layer::offsetx, Layer::offsety - ) - } - open fun copyFrom(other: Layer) { - for (prop in BASE_PROPS) { - val p = prop as KMutableProperty1 - p.set(this, p.get(other)) - } - this.properties.clear() - this.properties.putAll(other.properties) - } - abstract fun clone(): Layer - - class Tiles( - var map: Bitmap32 = Bitmap32(0, 0), - var encoding: String = "csv", - var compression: String = "" - ) : Layer() { - val data get() = map - val width: Int get() = map.width - val height: Int get() = map.height - val area: Int get() = width * height - operator fun set(x: Int, y: Int, value: Int) = run { data.setInt(x, y, value) } - operator fun get(x: Int, y: Int): Int = data.getInt(x, y) - override fun clone(): Tiles = Tiles(map.clone(), encoding, compression).also { it.copyFrom(this) } - } - - data class ObjectInfo( - val id: Int, val name: String, val type: String, - val bounds: IRectangleInt, - val objprops: Map - ) - - class Objects( - val objects: MutableList = arrayListOf() - ) : Layer() { - interface Object { - val info: ObjectInfo - } - - interface Poly : Object { - val points: List - } - - 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) : Poly - data class Polygon(override val info: ObjectInfo, override val points: List) : Poly - - override fun clone(): Objects = Objects(objects.toMutableList()).also { it.copyFrom(this) } - - fun getById(id: Int): Object? = objects.firstOrNull { it.id == id } - fun getByName(name: String): Object? = objects.firstOrNull { it.name == name } - } - - class Image( - var width: Int = 0, - var height: Int = 0, - var source: String = "", - var image: Bitmap = Bitmap32(0, 0) - ) : Layer() { - override fun clone(): Image = Image(width, height, source, image.clone()).also { it.copyFrom(this) } - } - } -} - -private fun TileSet.clone(): TileSet = TileSet(this.textures, this.width, this.height, this.base) - -fun Bitmap.clone(): Bitmap = when (this) { - is Bitmap32 -> this.clone() - else -> TODO() -} - -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.tiles get() = this.filterIsInstance() -inline val Iterable.images get() = this.filterIsInstance() -inline val Iterable.objects get() = this.filterIsInstance() - -val tilemapLog = Logger("tilemap") - -class TiledFile(val name: String) diff --git a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TiledMapReader.kt b/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TiledMapReader.kt deleted file mode 100644 index 248593c..0000000 --- a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TiledMapReader.kt +++ /dev/null @@ -1,311 +0,0 @@ -package com.soywiz.korge.intellij.editor.tile - -import com.soywiz.kds.iterators.* -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.* - -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(data.maxGid + 1) - - data.imageLayers.fastForEach { layer -> - layer.image = try { - folder[layer.source].readBitmapOptimized() - } catch (e: Throwable) { - e.printStackTrace() - Bitmap32(layer.width, layer.height) - } - } - - val tiledTilesets = arrayListOf() - - data.tilesets.fastForEach { tileset -> - tiledTilesets += tileset.toTiledSet(folder, hasTransparentColor, transparentColor, createBorder) - } - - return TiledMap(data, tiledTilesets) -} - -private fun Xml.parseProperties(): Map { - val out = LinkedHashMap() - 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 -} - -fun parseTileSetData(element: Xml, firstgid: Int, tilesetSource: String? = null): TileSetData { - // - // - // - // - val image = element.child("image") - - return TileSetData( - name = element.str("name"), - firstgid = firstgid, - tilewidth = element.int("tilewidth"), - tileheight = element.int("tileheight"), - tilecount = element.int("tilecount", -1), - columns = element.int("columns", -1), - image = image, - tilesetSource = tilesetSource, - imageSource = image?.str("source") ?: "", - width = image?.int("width", 0) ?: 0, - height = image?.int("height", 0) ?: 0, - terrains = element.children("terraintypes").children("terrain").map { TerrainData(name = it.str("name"), tile = it.int("tile")) }, - tiles = element.children("tile").map { - TileData( - id = it.int("id"), - terrain = it.str("terrain").takeIf { it.isNotEmpty() }?.split(',')?.map { it.toIntOrNull() }, - probability = it.double("probability", 1.0), - frames = it.child("animation")?.children("frame")?.map { - AnimationFrameData(it.int("tileid"), it.int("duration")) - } - ) - } - ) -} - -suspend fun VfsFile.readTileSetData(firstgid: Int = 1): TileSetData { - return parseTileSetData(this.readXml(), firstgid, this.baseName) -} - -suspend fun TileSetData.toTiledSet( - folder: VfsFile, - hasTransparentColor: Boolean = false, - transparentColor: RGBA = Colors.FUCHSIA, - createBorder: Int = 1 -): TiledMap.TiledTileset { - val tileset = this - var bmp = try { - folder[tileset.imageSource].readBitmapOptimized() - } catch (e: Throwable) { - e.printStackTrace() - Bitmap32(tileset.width, tileset.height) - } - - // @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, - data = tileset, - firstgid = tileset.firstgid - ) - - return tiledTileset -} - -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 ") - - 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", +1) - // TSX file / embedded element - val sourcePath = element.getString("source") - val element = if (sourcePath != null) folder[sourcePath].readXml() else element - tiledMap.tilesets += parseTileSetData(element, firstgid, sourcePath) - //lastBaseTexture = tex.base - } - elementName == "layer" || elementName == "objectgroup" || elementName == "imagelayer" -> { - tilemapLog.trace { "layer:$elementName" } - val layer = when (element.nameLC) { - "layer" -> TiledMap.Layer.Tiles() - "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.Tiles -> { - 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.uint("gid") } ?: listOf() - items.toIntArray() - } - encoding == "csv" -> { - val content = data?.text ?: "" - val items = content.replace(spaces, "").split(',').map { it.toUInt().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)) - layer.encoding = encoding - layer.compression = compression - } - 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() - var objprops: Map = 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 -} - -private fun Xml.uint(name: String, defaultValue: Int = 0): Int = this.attributesLC[name]?.toUIntOrNull()?.toInt() ?: defaultValue - -private enum class RKind { - RECT, ELLIPSE, POLYLINE, POLYGON -} - -private val spaces = Regex("\\s+") diff --git a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TiledMapWriter.kt b/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TiledMapWriter.kt deleted file mode 100644 index 0041f6c..0000000 --- a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TiledMapWriter.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.soywiz.korge.intellij.editor.tile - -import com.soywiz.korio.file.* -import com.soywiz.korio.serialization.xml.* - - -suspend fun VfsFile.writeTiledMap(map: TiledMap) { - writeString(map.toXML().toString()) -} - -fun TiledMap.toXML(): Xml { - val map = this - return buildXml("map", - "version" to 1.2, - "tiledversion" to "1.3.1", - "orientation" to "orthogonal", - "renderorder" to "right-down", - "compressionlevel" to -1, - "width" to map.width, - "height" to map.height, - "tilewidth" to map.tilewidth, - "tileheight" to map.tileheight, - "infinite" to 0, - "nextlayerid" to map.allLayers.size + 1, - "nextobjectid" to map.objectLayers.size + 1 - ) { - for (tileset in map.tilesets) { - if (tileset.data.tilesetSource != null) { - node("tileset", "firstgid" to tileset.data.firstgid, "source" to tileset.data.tilesetSource) - } else { - node(tileset.data.toXML()) - } - } - for (layer in map.allLayers) { - when (layer) { - is TiledMap.Layer.Tiles -> { - node("layer", "id" to layer.id, "name" to layer.name, "width" to layer.width, "height" to layer.height) { - node("data", "encoding" to "csv") { - text(buildString(layer.area * 4) { - append("\n") - for (y in 0 until layer.height) { - for (x in 0 until layer.width) { - append(layer.data[x, y].value) - if (y != layer.height - 1 || x != layer.width - 1) append(',') - } - append("\n") - } - }) - } - } - } - else -> TODO("Unsupported layer $layer") - } - } - } -} - -fun TileSetData.toXML(): Xml { - // - // - // - return buildXml("tileset", - "firstgid" to firstgid, - "name" to name, - "tilewidth" to tilewidth, - "tileheight" to tileheight, - "tilecount" to tilecount, - "columns" to columns - ) { - node("image", "source" to imageSource, "width" to width, "height" to height) - if (terrains.isNotEmpty()) { - node("terraintypes") { - // - for (terrain in terrains) { - node("terrain", "name" to terrain.name, "tile" to terrain.tile) - } - } - } - for (tile in tiles) { - node("tile", - "id" to tile.id, - "terrain" to tile.terrain?.joinToString(",") { it?.toString() ?: "" }, - "probability" to tile.probability.takeIf { it != 1.0 } - ) { - val frames = tile.frames - if (frames != null) { - node("animation") { - for (frame in frames) { - node("frame", "tileid" to frame.tileid, "duration" to frame.duration) - } - } - } - } - } - } -} diff --git a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/MapComponent.kt b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/MapComponent.kt similarity index 99% rename from src/main/kotlin/com/soywiz/korge/intellij/editor/tile/MapComponent.kt rename to src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/MapComponent.kt index 6eb9b81..839198a 100644 --- a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/MapComponent.kt +++ b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/MapComponent.kt @@ -1,4 +1,4 @@ -package com.soywiz.korge.intellij.editor.tile +package com.soywiz.korge.intellij.editor.tiled import com.intellij.util.ui.* import com.soywiz.korge.intellij.util.* diff --git a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/MapContext.kt b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/MapContext.kt similarity index 73% rename from src/main/kotlin/com/soywiz/korge/intellij/editor/tile/MapContext.kt rename to src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/MapContext.kt index 2b22e22..082ac65 100644 --- a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/MapContext.kt +++ b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/MapContext.kt @@ -1,9 +1,9 @@ -package com.soywiz.korge.intellij.editor.tile +package com.soywiz.korge.intellij.editor.tiled import com.soywiz.korge.intellij.editor.* -import com.soywiz.korge.intellij.editor.tile.dialog.* -import com.soywiz.korge.intellij.editor.tile.editor.* -import com.soywiz.korge.intellij.util.* +import com.soywiz.korge.intellij.editor.tiled.dialog.* +import com.soywiz.korge.intellij.editor.tiled.editor.* +import com.soywiz.korge.intellij.util.ObservableProperty import com.soywiz.korim.bitmap.* import com.soywiz.korim.color.* import com.soywiz.korio.async.* diff --git a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TerrainFiller.kt b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TerrainFiller.kt similarity index 89% rename from src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TerrainFiller.kt rename to src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TerrainFiller.kt index 673ce8b..24bd3e9 100644 --- a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TerrainFiller.kt +++ b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TerrainFiller.kt @@ -1,4 +1,4 @@ -package com.soywiz.korge.intellij.editor.tile +package com.soywiz.korge.intellij.editor.tiled object TerrainFiller { fun updateTile(layer: TiledMap.Layer.Tiles, x: Int, y: Int, tileset: TiledMap.TiledTileset) { diff --git a/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TiledMap.kt b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TiledMap.kt new file mode 100644 index 0000000..f246a25 --- /dev/null +++ b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TiledMap.kt @@ -0,0 +1,378 @@ +// @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.tiled + +import com.soywiz.klogger.* +import com.soywiz.korge.view.tiles.* +import com.soywiz.korim.bitmap.* +import com.soywiz.korim.color.* +import com.soywiz.korma.geom.* +import java.util.* +import kotlin.collections.LinkedHashMap + +class TiledMapData( + var orientation: TiledMap.Orientation = TiledMap.Orientation.ORTHOGONAL, + //TODO: support render order + var renderOrder: TiledMap.RenderOrder = TiledMap.RenderOrder.RIGHT_DOWN, + var compressionLevel: Int = -1, + var width: Int = 0, + var height: Int = 0, + var tilewidth: Int = 0, + var tileheight: Int = 0, + var hexSideLength: Int? = null, + var staggerAxis: TiledMap.StaggerAxis? = null, + var staggerIndex: TiledMap.StaggerIndex? = null, + var backgroundColor: RGBA? = null, + var nextLayerId: Int = 1, + var nextObjectId: Int = 1, + var infinite: Boolean = false, + val properties: MutableMap = mutableMapOf(), + val allLayers: MutableList = arrayListOf(), + val tilesets: MutableList = arrayListOf(), + var editorSettings: TiledMap.EditorSettings? = null +) { + val pixelWidth: Int get() = width * tilewidth + val pixelHeight: Int get() = height * tileheight + inline val tileLayers get() = allLayers.tiles + inline val imageLayers get() = allLayers.images + inline val objectLayers get() = allLayers.objects + + val maxGid get() = tilesets.map { it.firstgid + it.tileCount }.max() ?: 0 + + fun getObjectByName(name: String) = objectLayers.mapNotNull { it.getByName(name) }.firstOrNull() + + fun clone() = TiledMapData( + orientation, renderOrder, compressionLevel, + width, height, tilewidth, tileheight, + hexSideLength, staggerAxis, staggerIndex, + backgroundColor, nextLayerId, nextObjectId, infinite, + LinkedHashMap(properties), + allLayers.map { it.clone() }.toMutableList(), + tilesets.map { it.clone() }.toMutableList() + ) +} + +fun TiledMap.Object.getPos(map: TiledMapData) = Point(bounds.x / map.tilewidth, bounds.y / map.tileheight) + +fun TiledMapData.getObjectPosByName(name: String) = getObjectByName(name)?.getPos(this) + +data class TerrainData( + val name: String, + val tile: Int, + val properties: Map = mapOf() +) + +data class AnimationFrameData( + val tileId: Int, val duration: Int +) + +data class TerrainInfo(val info: List) { + operator fun get(x: Int, y: Int): Int? = if (x in 0..1 && y in 0..1) info[y * 2 + x] else null +} + +class WangSet( + val name: String, + val tileId: Int, + val properties: Map = mapOf(), + val cornerColors: List = listOf(), + val edgeColors: List = listOf(), + val wangtiles: List = listOf() +) { + class WangColor( + val name: String, + val color: RGBA, + val tileId: Int, + val probability: Double = 0.0 + ) + + class WangTile( + val tileId: Int, + val wangId: Int, + val hflip: Boolean = false, + val vflip: Boolean = false, + val dflip: Boolean = false + ) +} + +data class TileData( + val id: Int, + val type: Int = -1, + val terrain: List? = null, + val probability: Double = 0.0, + val image: TiledMap.Image? = null, + val properties: Map = mapOf(), + val objectGroup: TiledMap.Layer.Objects? = null, + val frames: List? = null +) { + val terrainInfo = TerrainInfo(terrain ?: listOf(null, null, null, null)) +} + +data class TileSetData( + val name: String, + val firstgid: Int, + val tileWidth: Int, + val tileHeight: Int, + val tileCount: Int, + val spacing: Int, + val margin: Int, + val columns: Int, + val image: TiledMap.Image?, + val tileOffsetX: Int = 0, + val tileOffsetY: Int = 0, + val grid: TiledMap.Grid? = null, + val tilesetSource: String? = null, + val objectAlignment: TiledMap.ObjectAlignment = TiledMap.ObjectAlignment.UNSPECIFIED, + val terrains: List = listOf(), + val wangsets: List = listOf(), + val tiles: List = listOf(), + val properties: Map = mapOf() +) { + val width: Int get() = image?.width ?: 0 + val height: Int get() = image?.height ?: 0 + fun clone() = copy() +} + +//e: java.lang.UnsupportedOperationException: Class literal annotation arguments are not yet supported: Factory +//@AsyncFactoryClass(TiledMapFactory::class) +class TiledMap constructor( + var data: TiledMapData, + var tilesets: MutableList +) { + 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 tileLayers get() = data.tileLayers + val imageLayers get() = data.imageLayers + val objectLayers get() = data.objectLayers + val nextGid get() = tilesets.map { it.firstgid + it.tileset.textures.size }.max() ?: 1 + + fun clone() = TiledMap(data.clone(), tilesets.map { it.clone() }.toMutableList()) + + enum class Orientation(val value: String) { + ORTHOGONAL("orthogonal"), + ISOMETRIC("isometric"), + STAGGERED("staggered"), + HEXAGONAL("hexagonal") + } + + enum class RenderOrder(val value: String) { + RIGHT_DOWN("right-down"), + RIGHT_UP("right-up"), + LEFT_DOWN("left-down"), + LEFT_UP("left-up") + } + + enum class StaggerAxis(val value: String) { + X("x"), Y("y") + } + + enum class StaggerIndex(val value: String) { + EVEN("even"), ODD("odd") + } + + enum class ObjectAlignment(val value: String) { + UNSPECIFIED("unspecified"), + TOP_LEFT("topleft"), + TOP("top"), + TOP_RIGHT("topright"), + LEFT("left"), + CENTER("center"), + RIGHT("right"), + BOTTOM_LEFT("bottomleft"), + BOTTOM("bottom"), + BOTTOM_RIGHT("bottomright") + } + + class Grid( + val cellWidth: Int, + val cellHeight: Int, + val orientation: Orientation = Orientation.ORTHOGONAL + ) { + enum class Orientation(val value: String) { + ORTHOGONAL("orthogonal"), + ISOMETRIC("isometric") + } + } + + data class Object( + val id: Int, + var gid: Int?, + var name: String, + var type: String, + var bounds: Rectangle, + var rotation: Double, // in degrees + var visible: Boolean, + var objectType: Type = Type.Rectangle, + val properties: MutableMap = mutableMapOf() + ) { + enum class DrawOrder(val value: String) { + INDEX("index"), TOP_DOWN("topdown") + } + + sealed class Type { + object Rectangle : Type() + object Ellipse : Type() + object PPoint : Type() + class Polygon(val points: List) : Type() + class Polyline(val points: List) : Type() + class Text( + val fontFamily: String, + val pixelSize: Int, + val wordWrap: Boolean, + val color: RGBA, + val bold: Boolean, + val italic: Boolean, + val underline: Boolean, + val strikeout: Boolean, + val kerning: Boolean, + val hAlign: TextHAlignment, + val vAlign: TextVAlignment + ) : Type() + } + } + + enum class TextHAlignment(val value: String) { + LEFT("left"), CENTER("center"), RIGHT("right"), JUSTIFY("justify") + } + + enum class TextVAlignment(val value: String) { + TOP("top"), CENTER("center"), BOTTOM("bottom") + } + + sealed class Image(val width: Int, val height: Int, val transparent: RGBA? = null) { + class Embedded( + val format: String, + val image: Bitmap32, + val encoding: Encoding, + val compression: Compression, + transparent: RGBA? = null + ) : Image(image.width, image.height, transparent) + + class External( + val source: String, + width: Int, + height: Int, + transparent: RGBA? = null + ) : Image(width, height, transparent) + } + + enum class Encoding(val value: String?) { + BASE64("base64"), CSV("csv"), XML(null) + } + + enum class Compression(val value: String?) { + NO(null), GZIP("gzip"), ZLIB("zlib"), ZSTD("zstd") + } + + sealed class Property { + class StringT(var value: String) : Property() + class IntT(var value: Int) : Property() + class FloatT(var value: Double) : Property() + class BoolT(var value: Boolean) : Property() + class ColorT(var value: RGBA) : Property() + class FileT(var path: String) : Property() + class ObjectT(var id: Int) : Property() + } + + data class TiledTileset( + val tileset: TileSet, + val data: TileSetData = TileSetData( + name = "unknown", + firstgid = 1, + tileWidth = tileset.width, + tileHeight = tileset.height, + tileCount = tileset.textures.size, + spacing = 0, + margin = 0, + columns = tileset.base.width / tileset.width, + image = TODO(), //null + //width = tileset.base.width, + //height = tileset.base.height, + terrains = listOf(), + tiles = tileset.textures.mapIndexed { index, bmpSlice -> TileData(index) } + ), + val firstgid: Int = 1 + ) { + fun clone(): TiledTileset = TiledTileset(tileset.clone(), data.clone(), firstgid) + } + + sealed class Layer { + var id: Int = 1 + var name: String = "" + var visible: Boolean = true + var locked: Boolean = false + var opacity = 1.0 + var tintColor: RGBA? = null + var offsetx: Double = 0.0 + var offsety: Double = 0.0 + val properties: MutableMap = mutableMapOf() + + open fun copyFrom(other: Layer) { + this.id = other.id + this.name = other.name + this.visible = other.visible + this.locked = other.locked + this.opacity = other.opacity + this.tintColor = other.tintColor + this.offsetx = other.offsetx + this.offsety = other.offsety + this.properties.clear() + this.properties.putAll(other.properties) + } + + abstract fun clone(): Layer + + class Tiles( + var map: Bitmap32 = Bitmap32(0, 0), + var encoding: Encoding = Encoding.XML, + var compression: Compression = Compression.NO + ) : Layer() { + val width: Int get() = map.width + val height: Int get() = map.height + val area: Int get() = width * height + operator fun set(x: Int, y: Int, value: Int) = run { map.setInt(x, y, value) } + operator fun get(x: Int, y: Int): Int = map.getInt(x, y) + override fun clone(): Tiles = Tiles(map.clone(), encoding, compression).also { it.copyFrom(this) } + } + + class Objects( + var color: RGBA = Colors.WHITE, + var drawOrder: Object.DrawOrder = Object.DrawOrder.TOP_DOWN, + val objects: MutableList = arrayListOf() + ) : Layer() { + 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] + + override fun clone() = Objects(color, drawOrder, ArrayList(objects)).also { it.copyFrom(this) } + } + + class Image(var image: TiledMap.Image? = null) : Layer() { + override fun clone(): Image = Image(image).also { it.copyFrom(this) } + } + + class Group( + val layers: MutableList = arrayListOf() + ) : Layer() { + override fun clone(): Group = Group(ArrayList(layers)).also { it.copyFrom(this) } + } + } + + class EditorSettings( + val chunkWidth: Int = 16, + val chunkHeight: Int = 16 + ) +} + +private fun TileSet.clone(): TileSet = TileSet(this.textures, this.width, this.height, this.base) + +inline val Iterable.tiles get() = this.filterIsInstance() +inline val Iterable.images get() = this.filterIsInstance() +inline val Iterable.objects get() = this.filterIsInstance() + +val tilemapLog = Logger("tilemap") diff --git a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TileMapEditor.kt b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TiledMapEditor.kt similarity index 98% rename from src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TileMapEditor.kt rename to src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TiledMapEditor.kt index ca5cbf0..2f20619 100644 --- a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TileMapEditor.kt +++ b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TiledMapEditor.kt @@ -1,21 +1,20 @@ -package com.soywiz.korge.intellij.editor.tile +package com.soywiz.korge.intellij.editor.tiled import com.intellij.ui.components.* import com.soywiz.kmem.* import com.soywiz.korge.intellij.editor.* -import com.soywiz.korge.intellij.editor.tile.dialog.* -import com.soywiz.korge.intellij.editor.tile.editor.* +import com.soywiz.korge.intellij.editor.tiled.dialog.* +import com.soywiz.korge.intellij.editor.tiled.editor.* import com.soywiz.korge.intellij.ui.* import com.soywiz.korge.intellij.util.* +import com.soywiz.korge.intellij.util.ObservableProperty import com.soywiz.korim.bitmap.* import com.soywiz.korim.color.* import com.soywiz.korio.async.* -import com.soywiz.korio.file.* import com.soywiz.korio.file.std.* import kotlinx.coroutines.* import java.awt.* import java.awt.event.* -import javax.swing.* fun Styled.createTileMapEditor( tilemap: TiledMap = runBlocking { localCurrentDirVfs["samples/gfx/sample.tmx"].readTiledMap() }, diff --git a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TileMapEditorPanel.kt b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TiledMapEditorPanel.kt similarity index 92% rename from src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TileMapEditorPanel.kt rename to src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TiledMapEditorPanel.kt index 00ab85d..07b2a51 100644 --- a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TileMapEditorPanel.kt +++ b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TiledMapEditorPanel.kt @@ -1,14 +1,14 @@ -package com.soywiz.korge.intellij.editor.tile +package com.soywiz.korge.intellij.editor.tiled import com.soywiz.korge.intellij.editor.HistoryManager -import com.soywiz.korge.intellij.editor.tile.dialog.ProjectContext +import com.soywiz.korge.intellij.editor.tiled.dialog.ProjectContext import com.soywiz.korge.intellij.ui.styled import com.soywiz.korio.file.VfsFile import kotlinx.coroutines.runBlocking import java.awt.BorderLayout import javax.swing.JPanel -class TileMapEditorPanel( +class TiledMapEditorPanel( val tmxFile: VfsFile, val history: HistoryManager = HistoryManager(), val registerHistoryShortcuts: Boolean = true, @@ -20,7 +20,7 @@ class TileMapEditorPanel( styled.createTileMapEditor(tmx, history, registerHistoryShortcuts, projectCtx) history.onSave { runBlocking { - val xmlString = "\n" + tmx.toXML().toOuterXmlIndented().toString() + val xmlString = "\n" + tmx.toXml().toOuterXmlIndented().toString() onSaveXml(xmlString) //tmxFile.writeString(xmlString) } diff --git a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TileMapEditorProvider.kt b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TiledMapEditorProvider.kt similarity index 93% rename from src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TileMapEditorProvider.kt rename to src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TiledMapEditorProvider.kt index 75c3d52..901e797 100644 --- a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/TileMapEditorProvider.kt +++ b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TiledMapEditorProvider.kt @@ -1,4 +1,4 @@ -package com.soywiz.korge.intellij.editor.tile +package com.soywiz.korge.intellij.editor.tiled import com.intellij.diff.util.* import com.intellij.openapi.command.* @@ -9,12 +9,12 @@ import com.intellij.openapi.util.* import com.intellij.openapi.vfs.* import com.soywiz.korge.intellij.* import com.soywiz.korge.intellij.editor.* -import com.soywiz.korge.intellij.editor.tile.dialog.* +import com.soywiz.korge.intellij.editor.tiled.dialog.* import com.soywiz.korge.intellij.util.* import java.beans.* import javax.swing.* -class TileMapEditorProvider : FileEditorProvider, DumbAware { +class TiledMapEditorProvider : FileEditorProvider, DumbAware { override fun getEditorTypeId(): String = this::class.java.name override fun getPolicy(): FileEditorPolicy = FileEditorPolicy.PLACE_BEFORE_DEFAULT_EDITOR @@ -37,7 +37,7 @@ class TileMapEditorProvider : FileEditorProvider, DumbAware { val refs = arrayOf(ref) val fileEditor = object : FileEditorBase(), DumbAware { - val panel = TileMapEditorPanel( + val panel = TiledMapEditorPanel( tmxFile, history, registerHistoryShortcuts = false, projectCtx = ProjectContext(project, file), diff --git a/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TiledMapReader.kt b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TiledMapReader.kt new file mode 100644 index 0000000..df46728 --- /dev/null +++ b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TiledMapReader.kt @@ -0,0 +1,580 @@ +package com.soywiz.korge.intellij.editor.tiled + +import com.soywiz.kds.iterators.* +import com.soywiz.kmem.* +import com.soywiz.korge.intellij.editor.tiled.TiledMap.* +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.* +import kotlin.collections.set + +suspend fun VfsFile.readTiledMap( + hasTransparentColor: Boolean = false, + transparentColor: RGBA = Colors.FUCHSIA, + createBorder: Int = 1 +): TiledMap { + val folder = this.parent.jail() + val data = readTiledMapData() + + val tiledTilesets = arrayListOf() + + data.tilesets.fastForEach { tileset -> + tiledTilesets += tileset.toTiledSet(folder, hasTransparentColor, transparentColor, createBorder) + } + + return TiledMap(data, tiledTilesets) +} + +suspend fun VfsFile.readTileSetData(firstgid: Int = 1): TileSetData { + return parseTileSetData(this.readXml(), firstgid, this.baseName) +} + +suspend fun TileSetData.toTiledSet( + folder: VfsFile, + hasTransparentColor: Boolean = false, + transparentColor: RGBA = Colors.FUCHSIA, + createBorder: Int = 1 +): TiledTileset { + val tileset = this + var bmp = try { + when (tileset.image) { + is Image.Embedded -> TODO() + is Image.External -> folder[tileset.image.source].readBitmapOptimized() + null -> Bitmap32(0, 0) + } + } catch (e: Throwable) { + e.printStackTrace() + Bitmap32(tileset.width, tileset.height) + } + + // @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() + + if (tileset.spacing >= createBorder) { + // There is already separation between tiles, use it as it is + val slices = TileSet.extractBmpSlices( + bmp, + tileset.tileWidth, + tileset.tileHeight, + tileset.columns, + tileset.tileCount, + tileset.spacing, + tileset.margin + ) + TileSet(slices, tileset.tileWidth, tileset.tileHeight, bmp) + } else { + // No separation between tiles: create a new Bitmap adding that separation + val bitmaps = TileSet.extractBitmaps( + bmp, + tileset.tileWidth, + tileset.tileHeight, + tileset.columns, + tileset.tileCount, + tileset.spacing, + tileset.margin + ) + TileSet.fromBitmaps(tileset.tileWidth, tileset.tileHeight, bitmaps, border = createBorder, mipmaps = false) + } + } else { + TileSet(bmp.slice(), tileset.tileWidth, tileset.tileHeight, tileset.columns, tileset.tileCount) + } + + val tiledTileset = TiledTileset( + tileset = ptileset, + data = tileset, + firstgid = tileset.firstgid + ) + + return tiledTileset +} + +suspend fun VfsFile.readTiledMapData(): TiledMapData { + 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 ") + + //TODO: Support different orientations + val orientation = mapXml.getString("orientation") + tiledMap.orientation = when (orientation) { + "orthogonal" -> TiledMap.Orientation.ORTHOGONAL + else -> unsupported("Orientation \"$orientation\" is not supported") + } + val renderOrder = mapXml.getString("renderorder") + tiledMap.renderOrder = when (renderOrder) { + "right-down" -> RenderOrder.RIGHT_DOWN + "right-up" -> RenderOrder.RIGHT_UP + "left-down" -> RenderOrder.LEFT_DOWN + "left-up" -> RenderOrder.LEFT_UP + else -> RenderOrder.RIGHT_DOWN + } + tiledMap.compressionLevel = mapXml.getInt("compressionlevel") ?: -1 + tiledMap.width = mapXml.getInt("width") ?: 0 + tiledMap.height = mapXml.getInt("height") ?: 0 + tiledMap.tilewidth = mapXml.getInt("tilewidth") ?: 32 + tiledMap.tileheight = mapXml.getInt("tileheight") ?: 32 + tiledMap.hexSideLength = mapXml.getInt("hexsidelength") + val staggerAxis = mapXml.getString("staggeraxis") + tiledMap.staggerAxis = when (staggerAxis) { + "x" -> StaggerAxis.X + "y" -> StaggerAxis.Y + else -> null + } + val staggerIndex = mapXml.getString("staggerindex") + tiledMap.staggerIndex = when (staggerIndex) { + "even" -> StaggerIndex.EVEN + "odd" -> StaggerIndex.ODD + else -> null + } + tiledMap.backgroundColor = mapXml.getString("backgroundcolor")?.let { colorFromARGB(it, Colors.TRANSPARENT_BLACK) } + val nextLayerId = mapXml.getInt("nextlayerid") + val nextObjectId = mapXml.getInt("nextobjectid") + tiledMap.infinite = mapXml.getInt("infinite") == 1 + + mapXml.child("properties")?.parseProperties()?.let { + tiledMap.properties.putAll(it) + } + + 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" } + + elements.fastForEach { element -> + when (element.nameLC) { + "tileset" -> { + tilemapLog.trace { "tileset" } + val firstgid = element.int("firstgid", 1) + val sourcePath = element.getString("source") + val tileset = if (sourcePath != null) folder[sourcePath].readXml() else element + tiledMap.tilesets += parseTileSetData(tileset, firstgid, sourcePath) + } + "layer" -> { + val layer = element.parseTileLayer(tiledMap.infinite) + tiledMap.allLayers += layer + } + "objectgroup" -> { + val layer = element.parseObjectLayer() + tiledMap.allLayers += layer + } + "imagelayer" -> { + val layer = element.parseImageLayer() + tiledMap.allLayers += layer + } + "group" -> { + val layer = element.parseGroupLayer(tiledMap.infinite) + tiledMap.allLayers += layer + } + "editorsettings" -> { + val chunkSize = element.child("chunksize") + tiledMap.editorSettings = EditorSettings( + chunkWidth = chunkSize?.int("width", 16) ?: 16, + chunkHeight = chunkSize?.int("height", 16) ?: 16 + ) + } + } + } + + tiledMap.nextLayerId = nextLayerId ?: run { + var maxLayerId = 0 + for (layer in tiledMap.allLayers) { + if (layer.id > maxLayerId) maxLayerId = layer.id + } + maxLayerId + 1 + } + tiledMap.nextObjectId = nextObjectId ?: run { + var maxObjectId = 0 + for (objects in tiledMap.objectLayers) { + for (obj in objects.objects) { + if (obj.id > maxObjectId) maxObjectId = obj.id + } + } + maxObjectId + 1 + } + + return tiledMap +} + +fun parseTileSetData(tileset: Xml, firstgid: Int, tilesetSource: String? = null): TileSetData { + val alignment = tileset.str("objectalignment", "unspecified") + val objectAlignment = ObjectAlignment.values().find { it.value == alignment } ?: ObjectAlignment.UNSPECIFIED + val tileOffset = tileset.child("tileoffset") + + return TileSetData( + name = tileset.str("name"), + firstgid = firstgid, + tileWidth = tileset.int("tilewidth"), + tileHeight = tileset.int("tileheight"), + tileCount = tileset.int("tilecount", 0), + spacing = tileset.int("spacing", 0), + margin = tileset.int("margin", 0), + columns = tileset.int("columns", 0), + image = tileset.child("image")?.parseImage(), + tileOffsetX = tileOffset?.int("x") ?: 0, + tileOffsetY = tileOffset?.int("y") ?: 0, + grid = tileset.child("grid")?.parseGrid(), + tilesetSource = tilesetSource, + objectAlignment = objectAlignment, + terrains = tileset.children("terraintypes").children("terrain").map { it.parseTerrain() }, + wangsets = tileset.children("wangsets").children("wangset").map { it.parseWangSet() }, + properties = tileset.child("properties")?.parseProperties() ?: mapOf(), + tiles = tileset.children("tile").map { it.parseTile() } + ) +} + +private fun Xml.parseTile(): TileData { + val tile = this + fun Xml.parseFrame(): AnimationFrameData { + return AnimationFrameData(this.int("tileid"), this.int("duration")) + } + return TileData( + id = tile.int("id"), + type = tile.int("type", -1), + terrain = tile.str("terrain").takeIf { it.isNotEmpty() }?.split(',')?.map { it.toIntOrNull() }, + probability = tile.double("probability"), + image = tile.child("image")?.parseImage(), + properties = tile.child("properties")?.parseProperties() ?: mapOf(), + objectGroup = tile.child("objectgroup")?.parseObjectLayer(), + frames = tile.child("animation")?.children("frame")?.map { it.parseFrame() } + ) +} + +private fun Xml.parseTerrain(): TerrainData { + return TerrainData( + name = str("name"), + tile = int("tile"), + properties = parseProperties() + ) +} + +private fun Xml.parseWangSet(): WangSet { + fun Xml.parseWangColor(): WangSet.WangColor { + val wangcolor = this + return WangSet.WangColor( + name = wangcolor.str("name"), + color = Colors[wangcolor.str("color")], + tileId = wangcolor.int("tile"), + probability = wangcolor.double("probability") + ) + } + + fun Xml.parseWangTile(): WangSet.WangTile { + val wangtile = this + val hflip = wangtile.str("hflip") + val vflip = wangtile.str("vflip") + val dflip = wangtile.str("dflip") + return WangSet.WangTile( + tileId = wangtile.int("tileid"), + wangId = wangtile.int("wangid"), + hflip = hflip == "1" || hflip == "true", + vflip = vflip == "1" || vflip == "true", + dflip = dflip == "1" || dflip == "true" + ) + } + + val wangset = this + return WangSet( + name = wangset.str("name"), + tileId = wangset.int("tile"), + properties = wangset.parseProperties(), + cornerColors = wangset.children("wangcornercolor").map { it.parseWangColor() }, + edgeColors = wangset.children("wangedgecolor").map { it.parseWangColor() }, + wangtiles = wangset.children("wangtile").map { it.parseWangTile() } + ) +} + +private fun Xml.parseGrid(): Grid { + val grid = this + val orientation = grid.str("orientation") + return Grid( + cellWidth = grid.int("width"), + cellHeight = grid.int("height"), + orientation = Grid.Orientation.values().find { it.value == orientation } ?: Grid.Orientation.ORTHOGONAL + ) +} + +private fun Xml.parseCommonLayerData(layer: Layer) { + val element = this + layer.id = element.int("id") + layer.name = element.str("name") + layer.opacity = element.double("opacity", 1.0) + layer.visible = element.int("visible", 1) == 1 + layer.locked = element.int("locked", 0) == 1 + layer.tintColor = element.strNull("tintcolor")?.let { colorFromARGB(it, Colors.WHITE) } + layer.offsetx = element.double("offsetx") + layer.offsety = element.double("offsety") + + element.child("properties")?.parseProperties()?.let { + layer.properties.putAll(it) + } +} + +private fun Xml.parseTileLayer(infinite: Boolean): Layer.Tiles { + val layer = Layer.Tiles() + parseCommonLayerData(layer) + + val element = this + val width = element.int("width") + val height = element.int("height") + val data = element.child("data") + + val map: Bitmap32 + val encoding: Encoding + val compression: Compression + + if (data == null) { + map = Bitmap32(width, height) + encoding = Encoding.CSV + compression = Compression.NO + } else { + val enc = data.strNull("encoding") + val com = data.strNull("compression") + encoding = Encoding.values().find { it.value == enc } ?: Encoding.XML + compression = Compression.values().find { it.value == com } ?: Compression.NO + val count = width * height + + fun Xml.encodeGids(): IntArray = when (encoding) { + Encoding.XML -> { + children("tile").map { it.uint("gid").toInt() }.toIntArray() + } + Encoding.CSV -> { + text.replace(spaces, "").split(',').map { it.toUInt().toInt() }.toIntArray() + } + Encoding.BASE64 -> { + val rawContent = text.trim().fromBase64() + val content = when (compression) { + Compression.NO -> rawContent + Compression.GZIP -> rawContent.uncompress(GZIP) + Compression.ZLIB -> rawContent.uncompress(ZLib) + //TODO: support "zstd" compression + //Data.Compression.ZSTD -> rawContent.uncompress(ZSTD) + else -> invalidOp("Unknown compression '$compression'") + } + //TODO: read UIntArray + content.readIntArrayLE(0, count) + } + } + + val tiles: IntArray + if (infinite) { + tiles = IntArray(count) + data.children("chunk").forEach { chunk -> + val offsetX = chunk.int("x") + val offsetY = chunk.int("y") + val cwidth = chunk.int("width") + val cheight = chunk.int("height") + chunk.encodeGids().forEachIndexed { i, gid -> + val x = offsetX + i % cwidth + val y = offsetY + i / cwidth + tiles[x + y * (offsetX + cwidth)] = gid + } + } + } else { + tiles = data.encodeGids() + } + map = Bitmap32(width, height, RgbaArray(tiles)) + } + + layer.map = map + layer.encoding = encoding + layer.compression = compression + + return layer +} + +private fun Xml.parseObjectLayer(): Layer.Objects { + val layer = Layer.Objects() + parseCommonLayerData(layer) + + val element = this + val order = element.str("draworder", "topdown") + layer.color = colorFromARGB(element.str("color"), Colors["#a0a0a4"]) + layer.drawOrder = Object.DrawOrder.values().find { it.value == order } ?: Object.DrawOrder.TOP_DOWN + + for (obj in element.children("object")) { + val objInstance = Object( + id = obj.int("id"), + gid = obj.intNull("gid"), + name = obj.str("name"), + type = obj.str("type"), + bounds = obj.run { Rectangle(double("x"), double("y"), double("width"), double("height")) }, + rotation = obj.double("rotation"), + visible = obj.int("visible", 1) != 0 + //TODO: support object templates + //templatePath = obj.strNull("template") + ) + obj.child("properties")?.parseProperties()?.let { + objInstance.properties.putAll(it) + } + + fun Xml.readPoints() = str("points").split(spaces).map { xy -> + val parts = xy.split(',').map { it.trim().toDoubleOrNull() ?: 0.0 } + Point(parts[0], parts[1]) + } + + val ellipse = obj.child("ellipse") + val point = obj.child("point") + val polygon = obj.child("polygon") + val polyline = obj.child("polyline") + val text = obj.child("text") + val objectType: Object.Type = when { + ellipse != null -> Object.Type.Ellipse + point != null -> Object.Type.PPoint + polygon != null -> Object.Type.Polygon(polygon.readPoints()) + polyline != null -> Object.Type.Polyline(polyline.readPoints()) + text != null -> Object.Type.Text( + fontFamily = text.str("fontfamily", "sans-serif"), + pixelSize = text.int("pixelsize", 16), + wordWrap = text.int("wrap", 0) == 1, + color = colorFromARGB(text.str("color"), Colors.BLACK), + bold = text.int("bold") == 1, + italic = text.int("italic") == 1, + underline = text.int("underline") == 1, + strikeout = text.int("strikeout") == 1, + kerning = text.int("kerning", 1) == 1, + hAlign = text.str("halign", "left").let { align -> + TextHAlignment.values().find { it.value == align } ?: TextHAlignment.LEFT + }, + vAlign = text.str("valign", "top").let { align -> + TextVAlignment.values().find { it.value == align } ?: TextVAlignment.TOP + } + ) + else -> Object.Type.Rectangle + } + + objInstance.objectType = objectType + layer.objects.add(objInstance) + } + + return layer +} + +private fun Xml.parseImageLayer(): Layer.Image { + val layer = Layer.Image() + parseCommonLayerData(layer) + layer.image = child("image")?.parseImage() + return layer +} + +private fun Xml.parseGroupLayer(infinite: Boolean): Layer.Group { + val layer = Layer.Group() + parseCommonLayerData(layer) + + allChildrenNoComments.fastForEach { element -> + when (element.nameLC) { + "layer" -> { + val tileLayer = element.parseTileLayer(infinite) + layer.layers += tileLayer + } + "objectgroup" -> { + val objectLayer = element.parseObjectLayer() + layer.layers += objectLayer + } + "imagelayer" -> { + val imageLayer = element.parseImageLayer() + layer.layers += imageLayer + } + "group" -> { + val groupLayer = element.parseGroupLayer(infinite) + layer.layers += groupLayer + } + } + } + return layer +} + +private fun Xml.parseImage(): Image? { + val image = this + val width = image.int("width") + val height = image.int("height") + val trans = image.str("trans") + val transparent = when { + trans.isEmpty() -> null + trans.startsWith("#") -> Colors[trans] + else -> Colors["#$trans"] + } + val source = image.str("source") + return if (source.isNotEmpty()) { + Image.External( + source = source, + width = width, + height = height, + transparent = transparent + ) + } else { + val data = image.child("data") ?: return null + val enc = data.strNull("encoding") + val com = data.strNull("compression") + val encoding = Encoding.values().find { it.value == enc } ?: Encoding.XML + val compression = Compression.values().find { it.value == com } ?: Compression.NO + //TODO: read embedded image (png, jpg, etc.) and convert to bitmap + val bitmap = Bitmap32(width, height) + Image.Embedded( + format = image.str("format"), + image = bitmap, + encoding = encoding, + compression = compression, + transparent = transparent + ) + } +} + +private fun Xml.parseProperties(): Map { + val out = LinkedHashMap() + for (property in this.children("property")) { + val pname = property.str("name") + val rawValue = property.str("value") + val type = property.str("type", "string") + val pvalue = when (type) { + "string" -> Property.StringT(rawValue) + "int" -> Property.IntT(rawValue.toIntOrNull() ?: 0) + "float" -> Property.FloatT(rawValue.toDoubleOrNull() ?: 0.0) + "bool" -> Property.BoolT(rawValue == "true") + "color" -> Property.ColorT(colorFromARGB(rawValue, Colors.TRANSPARENT_BLACK)) + "file" -> Property.FileT(if (rawValue.isEmpty()) "." else rawValue) + "object" -> Property.ObjectT(rawValue.toIntOrNull() ?: 0) + else -> Property.StringT(rawValue) + } + out[pname] = pvalue + } + return out +} + +//TODO: move to korio +private fun Xml.uint(name: String, defaultValue: UInt = 0u): UInt = + this.attributesLC[name]?.toUIntOrNull() ?: defaultValue + +//TODO: move to korim +fun colorFromARGB(color: String, default: RGBA): RGBA { + if (!color.startsWith('#') || color.length != 9 && color.length != 7) return default + val hex = color.substring(1) + val start = if (color.length == 7) 0 else 2 + val a = if (color.length == 9) hex.substr(0, 2).toInt(16) else 0xFF + val r = hex.substr(start + 0, 2).toInt(16) + val g = hex.substr(start + 2, 2).toInt(16) + val b = hex.substr(start + 4, 2).toInt(16) + return RGBA(r, g, b, a) +} + +private val spaces = Regex("\\s+") diff --git a/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TiledMapWriter.kt b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TiledMapWriter.kt new file mode 100644 index 0000000..da19572 --- /dev/null +++ b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/TiledMapWriter.kt @@ -0,0 +1,440 @@ +package com.soywiz.korge.intellij.editor.tiled + +import com.soywiz.kmem.* +import com.soywiz.korge.intellij.editor.tiled.TiledMap.* +import com.soywiz.korim.color.* +import com.soywiz.korio.file.* +import com.soywiz.korio.serialization.xml.* +import com.soywiz.korio.util.* +import com.soywiz.korma.geom.* + +suspend fun VfsFile.writeTiledMap(map: TiledMap) { + writeString(map.toXml().toString()) +} + +fun TiledMap.toXml(): Xml { + val map = this + val mapData = map.data + return buildXml( + "map", + "version" to 1.2, + "tiledversion" to "1.3.1", + "orientation" to mapData.orientation.value, + "renderorder" to mapData.renderOrder.value, + "compressionlevel" to mapData.compressionLevel, + "width" to mapData.width, + "height" to mapData.height, + "tilewidth" to mapData.tilewidth, + "tileheight" to mapData.tileheight, + "hexsidelength" to mapData.hexSideLength, + "staggeraxis" to mapData.staggerAxis, + "staggerindex" to mapData.staggerIndex, + "backgroundcolor" to mapData.backgroundColor?.toStringARGB(), + "infinite" to mapData.infinite.toInt(), + "nextlayerid" to mapData.nextLayerId, + "nextobjectid" to mapData.nextObjectId + ) { + propertiesToXml(mapData.properties) + for (tileset in map.tilesets) { + val tilesetData = tileset.data + if (tilesetData.tilesetSource != null) { + node("tileset", "firstgid" to tilesetData.firstgid, "source" to tilesetData.tilesetSource) + } else { + node(tilesetData.toXml()) + } + } + for (layer in map.allLayers) { + when (layer) { + is Layer.Tiles -> tileLayerToXml( + layer, + mapData.infinite, + mapData.editorSettings?.chunkWidth ?: 16, + mapData.editorSettings?.chunkHeight ?: 16 + ) + is Layer.Objects -> objectLayerToXml(layer) + is Layer.Image -> imageLayerToXml(layer) + is Layer.Group -> groupLayerToXml( + layer, + mapData.infinite, + mapData.editorSettings?.chunkWidth ?: 16, + mapData.editorSettings?.chunkHeight ?: 16 + ) + } + } + val editorSettings = mapData.editorSettings + if (editorSettings != null && (editorSettings.chunkWidth != 16 || editorSettings.chunkHeight != 16)) { + node("editorsettings") { + node( + "chunksize", + "width" to editorSettings.chunkWidth, + "height" to editorSettings.chunkHeight + ) + } + } + } +} + +private fun TileSetData.toXml(): Xml { + return buildXml( + "tileset", + "firstgid" to firstgid, + "name" to name, + "tilewidth" to tileWidth, + "tileheight" to tileHeight, + "spacing" to spacing.takeIf { it > 0 }, + "margin" to margin.takeIf { it > 0 }, + "tilecount" to tileCount, + "columns" to columns, + "objectalignment" to objectAlignment.takeIf { it != ObjectAlignment.UNSPECIFIED }?.value + ) { + imageToXml(image) + if (tileOffsetX != 0 || tileOffsetY != 0) { + node("tileoffset", "x" to tileOffsetX, "y" to tileOffsetY) + } + grid?.let { grid -> + node( + "grid", + "orientation" to grid.orientation.value, + "width" to grid.cellWidth, + "height" to grid.cellHeight + ) + } + propertiesToXml(properties) + if (terrains.isNotEmpty()) { + node("terraintypes") { + for (terrain in terrains) { + node("terrain", "name" to terrain.name, "tile" to terrain.tile) { + propertiesToXml(terrain.properties) + } + } + } + } + if (tiles.isNotEmpty()) { + for (tile in tiles) { + node(tile.toXml()) + } + } + if (wangsets.isNotEmpty()) { + node("wangsets") { + for (wangset in wangsets) node(wangset.toXml()) + } + } + } +} + +private fun WangSet.toXml(): Xml { + return buildXml("wangset", "name" to name, "tile" to tileId) { + propertiesToXml(properties) + if (cornerColors.isNotEmpty()) { + for (color in cornerColors) { + node( + "wangcornercolor", + "name" to color.name, + "color" to color.color, + "tile" to color.tileId, + "probability" to color.probability.takeIf { it != 0.0 }?.niceStr + ) + } + } + if (edgeColors.isNotEmpty()) { + for (color in edgeColors) { + node( + "wangedgecolor", + "name" to color.name, + "color" to color.color.toStringARGB(), + "tile" to color.tileId, + "probability" to color.probability.takeIf { it != 0.0 }?.niceStr + ) + } + } + if (wangtiles.isNotEmpty()) { + for (wangtile in wangtiles) { + node( + "wangtile", + "tileid" to wangtile.tileId, + "wangid" to wangtile.wangId.toUInt().toString(16).toUpperCase(), + "hflip" to wangtile.hflip.takeIf { it }, + "vflip" to wangtile.vflip.takeIf { it }, + "dflip" to wangtile.dflip.takeIf { it } + ) + } + } + } +} + +private fun TileData.toXml(): Xml { + return buildXml("tile", + "id" to id, + "type" to type.takeIf { it != -1 }, + "terrain" to terrain?.joinToString(",") { it?.toString() ?: "" }, + "probability" to probability.takeIf { it != 0.0 }?.niceStr + ) { + propertiesToXml(properties) + imageToXml(image) + objectLayerToXml(objectGroup) + if (frames != null && frames.isNotEmpty()) { + node("animation") { + for (frame in frames) { + node("frame", "tileid" to frame.tileId, "duration" to frame.duration) + } + } + } + } +} + +private class Chunk(val x: Int, val y: Int, val ids: IntArray) + +private fun XmlBuilder.tileLayerToXml( + layer: Layer.Tiles, + infinite: Boolean, + chunkWidth: Int, + chunkHeight: Int +) { + node( + "layer", + "id" to layer.id, + "name" to layer.name.takeIf { it.isNotEmpty() }, + "width" to layer.width, + "height" to layer.height, + "opacity" to layer.opacity.takeIf { it != 1.0 }, + "visible" to layer.visible.toInt().takeIf { it != 1 }, + "locked" to layer.locked.toInt().takeIf { it != 0 }, + "tintcolor" to layer.tintColor, + "offsetx" to layer.offsetx.takeIf { it != 0.0 }, + "offsety" to layer.offsety.takeIf { it != 0.0 } + ) { + propertiesToXml(layer.properties) + node("data", "encoding" to layer.encoding.value, "compression" to layer.compression.value) { + if (infinite) { + val chunks = divideIntoChunks(layer.map.data.ints, chunkWidth, chunkHeight, layer.width) + chunks.forEach { chunk -> + node( + "chunk", + "x" to chunk.x, + "y" to chunk.y, + "width" to chunkWidth, + "height" to chunkHeight + ) { + when (layer.encoding) { + Encoding.XML -> { + chunk.ids.forEach { gid -> + node("tile", "gid" to gid.toUInt().takeIf { it != 0u }) + } + } + Encoding.CSV -> { + text(buildString(chunkWidth * chunkHeight * 4) { + append("\n") + for (y in 0 until chunkHeight) { + for (x in 0 until chunkWidth) { + append(chunk.ids[x + y * chunkWidth].toUInt()) + if (y != chunkHeight - 1 || x != chunkWidth - 1) append(',') + } + append("\n") + } + }) + } + Encoding.BASE64 -> { + //TODO: convert int array of gids into compressed string + } + } + } + } + } else { + when (layer.encoding) { + Encoding.XML -> { + layer.map.data.ints.forEach { gid -> + node("tile", "gid" to gid.toUInt().takeIf { it != 0u }) + } + } + Encoding.CSV -> { + text(buildString(layer.area * 4) { + append("\n") + for (y in 0 until layer.height) { + for (x in 0 until layer.width) { + append(layer.map[x, y].value.toUInt()) + if (y != layer.height - 1 || x != layer.width - 1) append(',') + } + append("\n") + } + }) + } + Encoding.BASE64 -> { + //TODO: convert int array of gids into compressed string + } + } + } + } + } +} + +private fun divideIntoChunks(array: IntArray, width: Int, height: Int, totalWidth: Int): Array { + val columns = totalWidth / width + val rows = array.size / columns + return Array(rows * columns) { i -> + val cx = i % rows + val cy = i / rows + Chunk(cx * width, cy * height, IntArray(width * height) { j -> + val tx = j % width + val ty = j / width + array[(cx * width + tx) + (cy * height + ty) * totalWidth] + }) + } +} + +private fun XmlBuilder.objectLayerToXml(layer: Layer.Objects?) { + if (layer == null) return + node( + "objectgroup", + "draworder" to layer.drawOrder.value, + "id" to layer.id, + "name" to layer.name.takeIf { it.isNotEmpty() }, + "color" to layer.color.toStringARGB().takeIf { it != "#a0a0a4" }, + "opacity" to layer.opacity.takeIf { it != 1.0 }, + "visible" to layer.visible.toInt().takeIf { it != 1 }, + "locked" to layer.locked.toInt().takeIf { it != 0 }, + "tintcolor" to layer.tintColor, + "offsetx" to layer.offsetx.takeIf { it != 0.0 }, + "offsety" to layer.offsety.takeIf { it != 0.0 } + ) { + propertiesToXml(layer.properties) + layer.objects.forEach { obj -> + node( + "object", + "id" to obj.id, + "gid" to obj.gid, + "name" to obj.name.takeIf { it.isNotEmpty() }, + "type" to obj.type.takeIf { it.isNotEmpty() }, + "x" to obj.bounds.x.takeIf { it != 0.0 }, + "y" to obj.bounds.y.takeIf { it != 0.0 }, + "width" to obj.bounds.width.takeIf { it != 0.0 }, + "height" to obj.bounds.height.takeIf { it != 0.0 }, + "rotation" to obj.rotation.takeIf { it != 0.0 }, + "visible" to obj.visible.toInt().takeIf { it != 1 } + //TODO: support object template + //"template" to obj.template + ) { + propertiesToXml(obj.properties) + + fun List.toXml() = joinToString(" ") { p -> "${p.x.niceStr},${p.y.niceStr}" } + + when (val type = obj.objectType) { + is Object.Type.Rectangle -> Unit + is Object.Type.Ellipse -> node("ellipse") + is Object.Type.PPoint -> node("point") + is Object.Type.Polygon -> node("polygon", "points" to type.points.toXml()) + is Object.Type.Polyline -> node("polyline", "points" to type.points.toXml()) + is Object.Type.Text -> node( + "text", + "fontfamily" to type.fontFamily, + "pixelsize" to type.pixelSize.takeIf { it != 16 }, + "wrap" to type.wordWrap.toInt().takeIf { it != 0 }, + "color" to type.color.toStringARGB(), + "bold" to type.bold.toInt().takeIf { it != 0 }, + "italic" to type.italic.toInt().takeIf { it != 0 }, + "underline" to type.underline.toInt().takeIf { it != 0 }, + "strikeout" to type.strikeout.toInt().takeIf { it != 0 }, + "kerning" to type.kerning.toInt().takeIf { it != 1 }, + "halign" to type.hAlign.value, + "valign" to type.vAlign.value + ) + } + } + } + } +} + +private fun XmlBuilder.imageLayerToXml(layer: Layer.Image) { + node( + "imagelayer", + "id" to layer.id, + "name" to layer.name.takeIf { it.isNotEmpty() }, + "opacity" to layer.opacity.takeIf { it != 1.0 }, + "visible" to layer.visible.toInt().takeIf { it != 1 }, + "locked" to layer.locked.toInt().takeIf { it != 0 }, + "tintcolor" to layer.tintColor, + "offsetx" to layer.offsetx.takeIf { it != 0.0 }, + "offsety" to layer.offsety.takeIf { it != 0.0 } + ) { + propertiesToXml(layer.properties) + imageToXml(layer.image) + } +} + +private fun XmlBuilder.groupLayerToXml(layer: Layer.Group, infinite: Boolean, chunkWidth: Int, chunkHeight: Int) { + node( + "group", + "id" to layer.id, + "name" to layer.name.takeIf { it.isNotEmpty() }, + "opacity" to layer.opacity.takeIf { it != 1.0 }, + "visible" to layer.visible.toInt().takeIf { it != 1 }, + "locked" to layer.locked.toInt().takeIf { it != 0 }, + "tintcolor" to layer.tintColor, + "offsetx" to layer.offsetx.takeIf { it != 0.0 }, + "offsety" to layer.offsety.takeIf { it != 0.0 } + ) { + propertiesToXml(layer.properties) + layer.layers.forEach { + when (it) { + is Layer.Tiles -> tileLayerToXml(it, infinite, chunkWidth, chunkHeight) + is Layer.Objects -> objectLayerToXml(it) + is Layer.Image -> imageLayerToXml(it) + is Layer.Group -> groupLayerToXml(it, infinite, chunkWidth, chunkHeight) + } + } + } +} + +private fun XmlBuilder.imageToXml(image: Image?) { + if (image == null) return + node( + "image", + when (image) { + is Image.Embedded -> "format" to image.format + is Image.External -> "source" to image.source + }, + "width" to image.width, + "height" to image.height, + "transparent" to image.transparent + ) { + if (image is Image.Embedded) { + node( + "data", + "encoding" to image.encoding.value, + "compression" to image.compression.value + ) { + //TODO: encode and compress image + text(image.toString()) + } + } + } +} + +private fun XmlBuilder.propertiesToXml(properties: Map) { + if (properties.isEmpty()) return + + fun property(name: String, type: String, value: Any) = + node("property", "name" to name, "type" to type, "value" to value) + + node("properties") { + properties.forEach { (name, prop) -> + when (prop) { + is Property.StringT -> property(name, "string", prop.value) + is Property.IntT -> property(name, "int", prop.value) + is Property.FloatT -> property(name, "float", prop.value) + is Property.BoolT -> property(name, "bool", prop.value.toString()) + is Property.ColorT -> property(name, "color", prop.value.toStringARGB()) + is Property.FileT -> property(name, "file", prop.path) + is Property.ObjectT -> property(name, "object", prop.id) + } + } + } +} + +//TODO: move to korim +private fun RGBA.toStringARGB(): String { + if (a == 0xFF) { + return "#%02x%02x%02x".format(r, g, b) + } else { + return "#%02x%02x%02x%02x".format(a, r, g, b) + } +} diff --git a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/dialog/ChooseFileDialog.kt b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/dialog/ChooseFileDialog.kt similarity index 87% rename from src/main/kotlin/com/soywiz/korge/intellij/editor/tile/dialog/ChooseFileDialog.kt rename to src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/dialog/ChooseFileDialog.kt index ef41768..2c634c1 100644 --- a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/dialog/ChooseFileDialog.kt +++ b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/dialog/ChooseFileDialog.kt @@ -1,4 +1,4 @@ -package com.soywiz.korge.intellij.editor.tile.dialog +package com.soywiz.korge.intellij.editor.tiled.dialog import com.intellij.openapi.fileChooser.* import com.intellij.openapi.project.* diff --git a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/dialog/ResizeMapDialog.kt b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/dialog/ResizeMapDialog.kt similarity index 94% rename from src/main/kotlin/com/soywiz/korge/intellij/editor/tile/dialog/ResizeMapDialog.kt rename to src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/dialog/ResizeMapDialog.kt index 8cffb23..79e0c59 100644 --- a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/dialog/ResizeMapDialog.kt +++ b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/dialog/ResizeMapDialog.kt @@ -1,8 +1,8 @@ -package com.soywiz.korge.intellij.editor.tile.dialog +package com.soywiz.korge.intellij.editor.tiled.dialog import com.intellij.ui.* import com.soywiz.korge.intellij.ui.* -import com.soywiz.korge.intellij.util.* +import com.soywiz.korge.intellij.util.ObservableProperty import com.soywiz.korio.async.* import com.soywiz.korma.geom.* import java.awt.event.* diff --git a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/editor/TileSetTab.kt b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/editor/TileSetTab.kt similarity index 90% rename from src/main/kotlin/com/soywiz/korge/intellij/editor/tile/editor/TileSetTab.kt rename to src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/editor/TileSetTab.kt index f723879..f0f5d3d 100644 --- a/src/main/kotlin/com/soywiz/korge/intellij/editor/tile/editor/TileSetTab.kt +++ b/src/main/kotlin/com/soywiz/korge/intellij/editor/tiled/editor/TileSetTab.kt @@ -1,10 +1,10 @@ -package com.soywiz.korge.intellij.editor.tile.editor +package com.soywiz.korge.intellij.editor.tiled.editor import com.intellij.ui.components.* import com.soywiz.kmem.* import com.soywiz.korge.intellij.* -import com.soywiz.korge.intellij.editor.tile.* -import com.soywiz.korge.intellij.editor.tile.dialog.* +import com.soywiz.korge.intellij.editor.tiled.* +import com.soywiz.korge.intellij.editor.tiled.dialog.* import com.soywiz.korge.intellij.ui.* import com.soywiz.korge.intellij.util.* import com.soywiz.korge.view.tiles.* @@ -151,9 +151,8 @@ fun Styled.tilesetTab( data class PickedSelection(val data: Bitmap32) private fun TiledMap.TiledTileset.pickerTilemap(): TiledMap { - val tileset = this.tileset - val mapWidth = this.data.columns.takeIf { it >= 0 } ?: (this.tileset.width / this.data.tilewidth) - val mapHeight = ceil(this.data.tilecount.toDouble() / this.data.columns.toDouble()).toInt() + val mapWidth = data.columns.takeIf { it >= 0 } ?: (tileset.width / data.tileWidth) + val mapHeight = ceil(data.tileCount.toDouble() / data.columns.toDouble()).toInt() return TiledMap(TiledMapData( width = mapWidth, height = mapHeight, @@ -169,14 +168,14 @@ private suspend fun tiledsetFromBitmap(file: VfsFile, tileWidth: Int, tileHeight return TileSetData( name = file.baseName.substringBeforeLast("."), firstgid = firstgid, - tilewidth = tileset.width, - tileheight = tileset.height, - tilecount = tileset.textures.size, + tileWidth = tileset.width, + tileHeight = tileset.height, + tileCount = tileset.textures.size, + //TODO: provide these values as params + spacing = 0, + margin = 0, columns = tileset.base.width / tileset.width, - image = null, - imageSource = file.baseName, - width = tileset.base.width, - height = tileset.base.height, + image = TiledMap.Image.External(file.baseName, bmp.width, bmp.height), tilesetSource = null, terrains = listOf(), tiles = listOf() diff --git a/src/main/kotlin/com/soywiz/korge/intellij/ui/UIBuilderSample.kt b/src/main/kotlin/com/soywiz/korge/intellij/ui/UIBuilderSample.kt index 7b408a9..42d35a6 100644 --- a/src/main/kotlin/com/soywiz/korge/intellij/ui/UIBuilderSample.kt +++ b/src/main/kotlin/com/soywiz/korge/intellij/ui/UIBuilderSample.kt @@ -1,6 +1,6 @@ package com.soywiz.korge.intellij.ui -import com.soywiz.korge.intellij.editor.tile.createTileMapEditor +import com.soywiz.korge.intellij.editor.tiled.createTileMapEditor import java.awt.Dimension import javax.swing.JFrame import javax.swing.UIManager diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index ae34f6f..4132a17 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -100,7 +100,7 @@ - +