@@ -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+ }
0 commit comments