KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/io/ImageData.kt

146 lines
4.1 KiB
Kotlin

package ru.dbotthepony.kstarbound.io
import org.lwjgl.stb.STBImage
import org.lwjgl.system.MemoryUtil.memAddress
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNIT
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITi
import ru.dbotthepony.kvector.vector.Vector2i
import ru.dbotthepony.kvector.vector.Vector4i
import java.lang.ref.Cleaner
import java.nio.ByteBuffer
private val cleaner = Cleaner.create { Thread(it, "STB Image Cleaner") }
class ImageData(val data: ByteBuffer, val width: Int, val height: Int, val amountOfChannels: Int) {
init {
val address = memAddress(data)
cleaner.register(data) { STBImage.nstbi_image_free(address) }
check(width >= 0) { "Invalid width $width" }
check(height >= 0) { "Invalid height $height" }
check(amountOfChannels in 1 .. 4) { "Unknown number of channels $amountOfChannels" }
}
operator fun get(x: Int, y: Int): Int {
require(x in 0 until width) { "X out of bounds: $x" }
require(y in 0 until height) { "Y out of bounds: $y" }
val offset = y * width * amountOfChannels + x * amountOfChannels
when (amountOfChannels) {
4 -> return data[offset].toInt() or
data[offset + 1].toInt().shl(8) or
data[offset + 2].toInt().shl(16) or
data[offset + 3].toInt().shl(24)
3 -> return data[offset].toInt() or
data[offset + 1].toInt().shl(8) or
data[offset + 2].toInt().shl(16)
2 -> return data[offset].toInt() or
data[offset + 1].toInt().shl(8)
1 -> return data[offset].toInt()
else -> throw IllegalStateException()
}
}
operator fun get(x: Int, y: Int, flip: Boolean): Int {
require(x in 0 until width) { "X out of bounds: $x" }
require(y in 0 until height) { "Y out of bounds: $y" }
if (flip) {
return this[width - x - 1, y]
} else {
return this[x, y]
}
}
fun isTransparent(x: Int, y: Int, flip: Boolean): Boolean {
if (x !in 0 until width) return true
if (y !in 0 until height) return true
if (amountOfChannels != 4) return false
return this[x, y, flip] and 0xFF != 0x0
}
fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): List<Vector2i> {
if (amountOfChannels != 3 && amountOfChannels != 4) throw IllegalStateException("Can not check world space taken by image with $amountOfChannels color channels")
val xDivL = pixelOffset.x % PIXELS_IN_STARBOUND_UNITi
val yDivB = pixelOffset.y % PIXELS_IN_STARBOUND_UNITi
val xDivR = (pixelOffset.x + width) % PIXELS_IN_STARBOUND_UNITi
val yDivT = (pixelOffset.y + height) % PIXELS_IN_STARBOUND_UNITi
val leftMostX = pixelOffset.x / PIXELS_IN_STARBOUND_UNITi - (if (xDivL != 0) 1 else 0)
val bottomMostY = pixelOffset.y / PIXELS_IN_STARBOUND_UNITi - (if (yDivB != 0) 1 else 0)
val rightMostX = (pixelOffset.x + width) / PIXELS_IN_STARBOUND_UNITi + (if (xDivR != 0) 1 else 0)
val topMostY = (pixelOffset.y + height) / PIXELS_IN_STARBOUND_UNITi + (if (yDivT != 0) 1 else 0)
val result = ArrayList<Vector2i>()
for (y in bottomMostY .. topMostY) {
for (x in leftMostX .. rightMostX) {
val left = x * PIXELS_IN_STARBOUND_UNITi
val bottom = y * PIXELS_IN_STARBOUND_UNITi
var transparentPixels = 0
for (sX in 0 until PIXELS_IN_STARBOUND_UNITi) {
for (sY in 0 until PIXELS_IN_STARBOUND_UNITi) {
if (isTransparent(xDivL + sX + left, yDivB + sY + bottom, flip)) {
transparentPixels++
}
}
}
if (transparentPixels * FILL_RATIO >= spaceScan) {
result.add(Vector2i(x, y))
}
}
}
return result
}
val nonEmptyRegion by lazy {
if (amountOfChannels == 4) {
var x0 = 0
var y0 = 0
search@for (y in 0 until height) {
for (x in 0 until width) {
if (this[x, y] and 0xFF != 0x0) {
x0 = x
y0 = y
break@search
}
}
}
var x1 = x0
var y1 = y0
search@for (y in height - 1 downTo y0) {
for (x in width - 1 downTo x0) {
if (this[x, y] and 0xFF != 0x0) {
x1 = x
y1 = y
break@search
}
}
}
return@lazy Vector4i(x0, y0, x1, y1)
}
Vector4i(0, 0, width, height)
}
companion object {
const val FILL_RATIO = 1 / (PIXELS_IN_STARBOUND_UNIT * PIXELS_IN_STARBOUND_UNIT)
}
}