Skip to content

Commit bb4ceb9

Browse files
authored
Merge branch 'main' into feature/support-observable-property-in-property-editor
2 parents daa1eea + d79bd64 commit bb4ceb9

File tree

11 files changed

+452
-47
lines changed

11 files changed

+452
-47
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@
88
</p>
99
<!-- /BADGES -->
1010

11+
<!--
1112
Accepting Donations/Sponsorship via Bitcoin: `bc1qfmnd2jazh6czsuvvvy5rc3fxwsfvj6e8zwesdg`
13+
-->
1214

1315
<!-- SUPPORT -->
16+
<!--
1417
<h2 align="center">Support korge</h2>
1518
<p align="center">
1619
If you like korge, or want your company logo here, please consider <a href="https://github.com/sponsors/soywiz">becoming a GitHub sponsor ★</a>,<br />
1720
</p>
21+
-->
1822
<!-- /SUPPORT -->
1923

2024
## Info about the project:

korge-gradle-plugin/src/main/kotlin/korlibs/korge/gradle/texpacker/NewTexturePacker.kt

Lines changed: 134 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,62 @@ object NewTexturePacker {
4040
} }
4141
}
4242

43+
/**
44+
* Packs tilesets from the given folders into texture atlases.
45+
*
46+
* Each tileset image is split into individual tiles of the specified size.
47+
* These tiles are checked for duplicates and then packed into atlases.
48+
*
49+
* @param folders The folders containing tilesets to be packed.
50+
* @param padding The padding to apply around each tile in the atlas.
51+
* @param tileSize The size of each tile in the tilesets.
52+
* @return A list of AtlasInfo objects representing the packed atlases.
53+
*/
54+
fun packTilesets(
55+
vararg folders: File,
56+
padding: Int = 1,
57+
tileSize: Int = 16
58+
): List<AtlasInfo> {
59+
// Load all tilesets and create SimpleBitmap instances
60+
val tilesets: List<Pair<File, SimpleBitmap>> = getAllFiles(*folders).mapNotNull {
61+
try {
62+
it.relative to SimpleBitmap(it.file)
63+
} catch (e: Throwable) {
64+
e.printStackTrace()
65+
null
66+
}
67+
}
68+
// Split each tileset into tiles
69+
val images = arrayListOf<Pair<File, SimpleBitmap>>()
70+
tilesets.forEach { tileset ->
71+
val (file, image) = tileset
72+
if (image.width % tileSize != 0 || image.height % tileSize != 0) {
73+
throw IllegalArgumentException("Tileset image size must be multiple of tileSize ($tileSize): $file with size ${image.width}x${image.height}")
74+
}
75+
images += image.splitInListOfTiles(file.nameWithoutExtension, tileSize)
76+
}
77+
78+
return packImages(images, enableRotation = false, enableTrimming = false, padding = padding, trimFileName = true, removeDuplicates = true)
79+
}
80+
81+
/**
82+
* Packs images from the given folders into texture atlases.
83+
*
84+
* @param folders The folders containing images to be packed.
85+
* @param enableRotation Whether to allow rotation of images for better packing.
86+
* @param enableTrimming Whether to trim transparent pixels from images.
87+
* @param padding The padding to apply around each image in the atlas.
88+
* @param trimFileName Whether to trim the file name in the output info.
89+
* @param removeDuplicates Whether to remove duplicate images and map duplicates in atlas info.
90+
* @return A list of AtlasInfo objects representing the packed atlases.
91+
*/
4392
fun packImages(
4493
vararg folders: File,
4594
enableRotation: Boolean = true,
4695
enableTrimming: Boolean = true,
4796
padding: Int = 2,
97+
trimFileName: Boolean = false,
98+
removeDuplicates: Boolean = false
4899
): List<AtlasInfo> {
49100
val images: List<Pair<File, SimpleBitmap>> = getAllFiles(*folders).mapNotNull {
50101
try {
@@ -54,68 +105,100 @@ object NewTexturePacker {
54105
null
55106
}
56107
}
108+
return packImages(images, enableRotation, enableTrimming, padding, trimFileName, removeDuplicates)
109+
}
57110

111+
private fun packImages(
112+
images: List<Pair<File, SimpleBitmap>>,
113+
enableRotation: Boolean,
114+
enableTrimming: Boolean,
115+
padding: Int,
116+
trimFileName: Boolean,
117+
removeDuplicates: Boolean
118+
): List<AtlasInfo> {
58119
val packer = NewBinPacker.MaxRectsPacker(4096, 4096, padding * 2, NewBinPacker.IOption(
59120
smart = true,
60121
pot = true,
61122
square = false,
62123
allowRotation = enableRotation,
63-
//allowRotation = false,
64124
tag = false,
65125
border = padding
66126
))
67127

68-
packer.addArray(images.map { (file, image) ->
128+
// Handle duplicate images if requested
129+
val tileMapping = linkedMapOf<File, File>() // In case of duplicates, map duplicate file to original file
130+
val mappedImages = if (removeDuplicates) { // mappedImages will contain only unique images if removeDuplicates is true
131+
// Remove duplicate images
132+
val uniqueMap = linkedMapOf<Int, Pair<File, SimpleBitmap>>()
133+
for ((fileName, image) in images) {
134+
val file = if (trimFileName) File(fileName.nameWithoutExtension) else fileName
135+
val hash = image.hashCode()
136+
val existing = uniqueMap[hash]
137+
if (existing == null) {
138+
uniqueMap[hash] = file to image
139+
tileMapping[file] = file
140+
} else tileMapping[file] = existing.first
141+
}
142+
uniqueMap.values.toList()
143+
} else {
144+
// No duplicate removal, use all images
145+
for ((fileName, _) in images) {
146+
val file = if (trimFileName) File(fileName.nameWithoutExtension) else fileName
147+
tileMapping[file] = file
148+
}
149+
images
150+
}
151+
152+
// Add images to the packer (possibly without duplicates)
153+
packer.addArray(mappedImages.map { (file, image) ->
69154
val fullArea = Rectangle(0, 0, image.width, image.height)
70155
val trimArea = if (enableTrimming) image.trim() else fullArea
71156
val trimmedImage = image.slice(trimArea)
72157
//println(trimArea == fullArea)
158+
val fileName = if (trimFileName) File(file.nameWithoutExtension) else file
73159
NewBinPacker.Rectangle(width = trimmedImage.width, height = trimmedImage.height, raw = Info(
74-
file, fullArea, trimArea, trimmedImage
160+
fileName, fullArea, trimArea, trimmedImage
75161
))
76162
})
77163

164+
// Building atlas info which includes mapping duplicates
78165
val outAtlases = arrayListOf<AtlasInfo>()
79166
for (bin in packer.bins) {
80-
//val rwidth = bin.rects.maxOf { it.right }
81-
//val rheight = bin.rects.maxOf { it.bottom }
82-
//val maxWidth = bin.maxWidth
83-
//val maxHeight = bin.maxHeight
84-
//val out = SimpleBitmap(rwidth, rheight)
85167
val out = SimpleBitmap(bin.width, bin.height)
86-
//println("${bin.width}x${bin.height}")
87-
88168
val frames = linkedMapOf<String, Any?>()
89169

90170
for (rect in bin.rects) {
91171
val info = rect.raw as Info
92-
val fileName = info.file.name
93-
//println("$rect :: info=$info")
94172

95-
val chunk = if (rect.rot) info.trimmedImage.flipY().rotate90() else info.trimmedImage
96-
out.put(rect.x - padding, rect.y - padding, chunk.extrude(padding))
97-
//out.put(rect.x, rect.y, chunk)
173+
// Check if this rect (image) is used by any duplicate files - if so, map them all to the same rect area in the atlas
174+
val files = tileMapping.filterValues { it == info.file }.keys
175+
for (file in files) {
176+
val fileName = file.name
98177

99-
val obj = LinkedHashMap<String, Any?>()
178+
val chunk = if (rect.rot) info.trimmedImage.flipY().rotate90() else info.trimmedImage
179+
out.put(rect.x - padding, rect.y - padding, chunk.extrude(padding))
100180

101-
fun Dimension.toObj(rot: Boolean): Map<String, Any?> {
102-
val w = if (!rot) width else height
103-
val h = if (!rot) height else width
104-
return mapOf("w" to w, "h" to h)
105-
}
106-
fun Rectangle.toObj(rot: Boolean): Map<String, Any?> {
107-
return mapOf("x" to x, "y" to y) + this.size.toObj(rot)
108-
}
181+
val obj = LinkedHashMap<String, Any?>()
109182

110-
obj["frame"] = Rectangle(rect.x, rect.y, rect.width, rect.height).toObj(rect.rot)
111-
obj["rotated"] = rect.rot
112-
obj["trimmed"] = info.trimArea != info.fullArea
113-
obj["spriteSourceSize"] = info.trimArea.toObj(false)
114-
obj["sourceSize"] = info.fullArea.size.toObj(false)
183+
fun Dimension.toObj(rot: Boolean): Map<String, Any?> {
184+
val w = if (!rot) width else height
185+
val h = if (!rot) height else width
186+
return mapOf("w" to w, "h" to h)
187+
}
115188

116-
frames[fileName] = obj
117-
}
189+
fun Rectangle.toObj(rot: Boolean): Map<String, Any?> {
190+
return mapOf("x" to x, "y" to y) + this.size.toObj(rot)
191+
}
118192

193+
obj["frame"] = Rectangle(rect.x, rect.y, rect.width, rect.height).toObj(rect.rot)
194+
obj["rotated"] = rect.rot
195+
obj["trimmed"] = info.trimArea != info.fullArea
196+
obj["spriteSourceSize"] = info.trimArea.toObj(false)
197+
obj["sourceSize"] = info.fullArea.size.toObj(false)
198+
199+
frames[fileName] = obj
200+
}
201+
}
119202

120203
val atlasOut = linkedMapOf<String, Any?>(
121204
"frames" to frames,
@@ -138,3 +221,23 @@ object NewTexturePacker {
138221
return outAtlases
139222
}
140223
}
224+
225+
/**
226+
* Split the bitmap into tiles of given size and return them as a list of pairs (File, SimpleBitmap).
227+
* The File is named as "${name}_${index} without extension."
228+
*/
229+
fun SimpleBitmap.splitInListOfTiles(name: String, tileSize: Int): List<Pair<File, SimpleBitmap>> {
230+
val tiles = arrayListOf<Pair<File, SimpleBitmap>>()
231+
val tilesX = this.width / tileSize
232+
val tilesY = this.height / tileSize
233+
var index = 0
234+
for (ty in 0 until tilesY) {
235+
for (tx in 0 until tilesX) {
236+
val tileBitmap = this.slice(Rectangle(tx * tileSize, ty * tileSize, tileSize, tileSize))
237+
val tileFile = File("${name}_${index}")
238+
tiles.add(tileFile to tileBitmap)
239+
index++
240+
}
241+
}
242+
return tiles
243+
}

korge-gradle-plugin/src/main/kotlin/korlibs/korge/gradle/texpacker/SimpleBitmap.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,16 @@ class SimpleBitmap(val width: Int, val height: Int, val data: IntArray = IntArra
139139
}
140140
return out
141141
}
142+
143+
/**
144+
* Get hash code of SimpleBitmap object based on its pixel data.
145+
*/
146+
override fun hashCode(): Int {
147+
// Using two prime numbers (17, 31) here for hash code calculation in order to distribute hash codes uniformly and reduce collisions
148+
var hash = 17
149+
for (pixel in data) {
150+
hash = hash * 31 + pixel
151+
}
152+
return hash
153+
}
142154
}

0 commit comments

Comments
 (0)