commonMain.ru.casperix.atlas.AtlasLoader.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of spine-jvm Show documentation
Show all versions of spine-jvm Show documentation
Signals for all occasions
package ru.casperix.atlas
import ru.casperix.math.axis_aligned.int32.Box2i
import ru.casperix.math.axis_aligned.int32.Dimension2i
import ru.casperix.math.vector.int32.Vector2i
import ru.casperix.misc.sliceSafe
import ru.casperix.renderer.pixel_map.PixelMap
import ru.casperix.signals.concrete.EitherFuture
import ru.casperix.signals.concrete.EitherSignal
import ru.casperix.atlas.PathUtil.pathParent
import ru.casperix.file_data.BlockData
import ru.casperix.file_data.FileData
import ru.casperix.file_data.FileDataLoader
import ru.casperix.multiplatform.loader.ResourceLoadError
import ru.casperix.multiplatform.loader.resourceLoader
object AtlasLoader {
private val cache = mutableMapOf>()
class PagePreview(
val info: PageProperties,
val regions: List,
)
class AtlasPageError(val path: String, val error: ResourceLoadError) : ResourceLoadError
fun load(path: String): EitherFuture {
return cache.getOrPut(path) {
loadDirect(path)
}
}
fun loadDirect(path: String): EitherFuture {
val parentPath = pathParent(path)
val atlasLoader = FileDataLoader.load(path)
val signal = EitherSignal()
atlasLoader.thenAccept {
val previewList = parse(it)
val map = mutableMapOf()
fun checkAccept() {
if (map.size == previewList.size) {
val pageList = map.entries.map { (preview, pixelMap) ->
Page(pixelMap, preview.info, preview.regions)
}
signal.accept(Atlas(pageList))
}
}
previewList.forEach { preview ->
val imagePath = listOfNotNull(parentPath, preview.info.name).joinToString(PathUtil.separator.toString())
resourceLoader.loadImage(imagePath).then({
map[preview] = it
checkAccept()
}, {
signal.reject(AtlasPageError(preview.info.name, it))
})
}
}
atlasLoader.thenReject {
signal.reject(it)
}
return signal
}
private fun parse(file: FileData): List {
return file.pageList.mapNotNull {
val title = it.blockList.firstOrNull() ?: return@mapNotNull null
val page = parseAsPageProperties(title)
val regions = it.blockList.sliceSafe(1..it.blockList.size).map {
parseAsRegion(it)
}
PagePreview(page, regions)
}
}
private fun parseAsRegion(data: BlockData): RegionAttributes = data.run {
return RegionAttributes(
name,
values["index"]?.toInt() ?: 0,
parseBounds(values["bounds"]),
parseInts(values["offsets"]),
parseRotate(values["rotate"]),
parseInts(values["split"]),
parseInts(values["pads"]),
)
}
private fun parseRotate(rawRotate: String?): OrthogonalRotate = if (rawRotate == null || rawRotate == "false") {
OrthogonalRotate.ZERO
} else if (rawRotate == "true") {
OrthogonalRotate.ONE
} else {
val raw = rawRotate.toInt().mod(360)
val rawIndex = raw / 90
if (rawIndex * 90 != raw) throw Exception("Invalid rotate: $rawRotate")
OrthogonalRotate.entries.getOrNull(rawIndex) ?: throw Exception("Invalid rotate: $rawRotate")
}
private fun parseInts(value: String?): List {
if (value == null) return emptyList()
return value.split(Regex("[,;]\\s*")).map {
it.toInt()
}
}
private fun parseSize(value: String?): Dimension2i {
val i = parseInts(value)
if (i.size == 2) {
return Dimension2i(i[0], i[1])
} else {
throw Exception("Can't parse size from: $value")
}
}
private fun parseBounds(value: String?): Box2i {
val i = parseInts(value)
if (i.size == 4) {
return Box2i.byDimension(Vector2i(i[0], i[1]), Vector2i(i[2], i[3]))
} else {
throw Exception("Can't parse bounds from: $value")
}
}
private fun parseAsPageProperties(data: BlockData): PageProperties = data.run {
val filters = values["filter"]?.split(", ").orEmpty()
return PageProperties(
name,
parseSize(values["size"]),
values["format"] ?: "",
filters.getOrNull(0) ?: "Nearest",
filters.getOrNull(1) ?: "Nearest",
values["repeat"] ?: "",
values["pma"]?.toBoolean() ?: false,
)
}
}