@@ -74,18 +74,31 @@ public struct TextureAtlas {
7474extension TextureAtlasBuilder {
7575 struct Texture : Equatable , Hashable , Sendable {
7676 let resourcePath : String
77- let width : Int
78- let height : Int
79- let imageData : Data
80- let coordinate : ( x: Int , y: Int )
81-
77+ var dataIndex : Array < Data > . Index
78+
8279 nonisolated static func == ( lhs: Texture , rhs: Texture ) -> Bool {
8380 return lhs. resourcePath == rhs. resourcePath
8481 }
8582 nonisolated func hash( into hasher: inout Hasher ) {
8683 hasher. combine ( resourcePath)
8784 }
8885 }
86+ struct TextureData : Equatable , Hashable , Sendable {
87+ let width : Int
88+ let height : Int
89+ let imageData : Data
90+ var coordinate : ( x: Int , y: Int )
91+
92+ nonisolated static func == ( lhs: TextureData , rhs: TextureData ) -> Bool {
93+ guard lhs. width == rhs. width && lhs. height == rhs. height else { return false }
94+ return lhs. imageData. elementsEqual ( rhs. imageData)
95+ }
96+ nonisolated func hash( into hasher: inout Hasher ) {
97+ hasher. combine ( width)
98+ hasher. combine ( height)
99+ hasher. combine ( imageData)
100+ }
101+ }
89102
90103 func textureBlocksWide( texturePixelWidth: Float ) -> Int {
91104 return Int ( ceil ( texturePixelWidth / Float( blockSize) ) )
@@ -98,6 +111,7 @@ extension TextureAtlasBuilder {
98111
99112public final class TextureAtlasBuilder {
100113 var textures : [ TextureAtlasBuilder . Texture ] = [ ]
114+ var textureDatas : [ TextureAtlasBuilder . TextureData ] = [ ]
101115
102116 /// true if this builder has changed since a TextureAtlas was generated
103117 public private( set) var needsGenerate : Bool = true
@@ -110,31 +124,50 @@ public final class TextureAtlasBuilder {
110124 var searchGrid : SearchGrid = SearchGrid ( )
111125
112126 public init ( blockSize: Int ) {
113- assert ( blockSize > 0 , " blockSize must be positive. " )
127+ assert ( blockSize >= 0 , " blockSize must be positive. " )
114128 assert ( blockSize <= 1024 , " blockSize cannot be greater than 1024. " )
115129 self . blockSize = blockSize
116130 }
117131
132+ /// - returns: `true` if the atlas already has the texture in question
118133 public func containsTexture( withPath unresolvedPath: String ) -> Bool {
119134 return textures. contains ( where: { $0. resourcePath == unresolvedPath} )
120135 }
121136
122- public func insertTexture( withPath unresolvedPath: String ) throws {
137+ /**
138+ Adds a new texture, or updates an existing texture.
139+
140+ - parameter unresolvedPath: The resource path to the texture data.
141+ - parameter sacrificePerformanceForSize: When `true` additional checks are performed to merge textures that are the same but with different paths. Resulting in a smaller atlas, at the cost of performance.
142+ */
143+ public func insertTexture( withPath unresolvedPath: String , sacrificePerformanceForSize: Bool = false ) throws {
144+ if containsTexture ( withPath: unresolvedPath) {
145+ // Cleanup old values
146+ // If the texture has changed on disk we want to replace it
147+ removeTexture ( withPath: unresolvedPath)
148+ }
149+
123150 let importer = PNGImporter ( )
124151 try importer. synchronousPrepareToImportResourceFrom ( path: unresolvedPath)
125152 let png = try importer. loadTexture ( options: . none)
126153 let width = Int ( png. size. width)
127154 let height = Int ( png. size. height)
128- let coord = searchGrid. firstUnoccupiedFor (
129- width: textureBlocksWide ( texturePixelWidth: png. size. width) ,
130- height: textureBlocksTall ( texturePixelHeight: png. size. height) ,
131- markOccupied: true
132- )
133- let texture = Texture ( resourcePath: unresolvedPath, width: width, height: height, imageData: png. data, coordinate: coord)
155+
156+ var textureData = TextureData ( width: width, height: height, imageData: png. data, coordinate: ( 0 , 0 ) )
157+ var dataIndex = self . textureDatas. endIndex
158+ if sacrificePerformanceForSize, let existingIndex = self . textureDatas. firstIndex ( where: { $0 == textureData} ) {
159+ dataIndex = existingIndex
160+ } else {
161+ let coord = searchGrid. firstUnoccupiedFor (
162+ width: textureBlocksWide ( texturePixelWidth: png. size. width) ,
163+ height: textureBlocksTall ( texturePixelHeight: png. size. height) ,
164+ markOccupied: true
165+ )
166+ textureData. coordinate = coord
167+ textureDatas. append ( textureData)
168+ }
134169
135- // Cleanup old values
136- // If the texture has changed on disk we want to replace it
137- removeTexture ( withPath: unresolvedPath)
170+ let texture = Texture ( resourcePath: unresolvedPath, dataIndex: dataIndex)
138171
139172 // Append new value
140173 self . textures. append ( texture)
@@ -146,13 +179,33 @@ public final class TextureAtlasBuilder {
146179 @discardableResult
147180 public func removeTexture( withPath unresolvedPath: String ) -> Bool {
148181 if let existing = textures. firstIndex ( where: { $0. resourcePath == unresolvedPath} ) {
182+ // Remove the texture
149183 let texture = self . textures. remove ( at: existing)
150- searchGrid. markAsOccupied ( false ,
151- x: texture. coordinate. x,
152- y: texture. coordinate. y,
153- width: textureBlocksWide ( texturePixelWidth: Float ( texture. width) ) ,
154- height: textureBlocksTall ( texturePixelHeight: Float ( texture. height) )
155- )
184+ let textureData = self . textureDatas [ texture. dataIndex]
185+
186+ // If the TextureData is no longer referenced, remove it
187+ if self . textures. contains ( where: { $0. dataIndex == texture. dataIndex} ) == false {
188+ // Free the grid area
189+ searchGrid. markAsOccupied (
190+ false ,
191+ x: textureData. coordinate. x,
192+ y: textureData. coordinate. y,
193+ width: textureBlocksWide ( texturePixelWidth: Float ( textureData. width) ) ,
194+ height: textureBlocksTall ( texturePixelHeight: Float ( textureData. height) )
195+ )
196+
197+ // Reindex the Texture dataIndex values
198+ self . textures = self . textures. map ( { texture in
199+ var texture = texture
200+ if texture. dataIndex > texture. dataIndex {
201+ texture. dataIndex -= 1
202+ }
203+ return texture
204+ } )
205+
206+ // Remove the data
207+ self . textureDatas. remove ( at: texture. dataIndex)
208+ }
156209
157210 self . needsGenerate = true
158211
@@ -169,12 +222,14 @@ public final class TextureAtlasBuilder {
169222 var imageData : Data = Data ( repeating: 0 , count: Int ( textureSize. width * textureSize. height) * 4 )
170223 imageData. withUnsafeMutableBytes { ( bytes: UnsafeMutableRawBufferPointer ) in
171224 for texture in textures {
172- var coord = texture. coordinate
225+ let textureData = self . textureDatas [ texture. dataIndex]
226+
227+ var coord = textureData. coordinate
173228 coord. x *= blockSize
174229 coord. y *= blockSize
175- let srcWidth = texture . width * 4
230+ let srcWidth = textureData . width * 4
176231 let x = ( coord. x * 4 )
177- for row in 0 ..< texture . height {
232+ for row in 0 ..< textureData . height {
178233 let srcStart = srcWidth * row
179234 let srcRange = srcStart ..< ( srcStart + srcWidth)
180235
@@ -185,7 +240,7 @@ public final class TextureAtlasBuilder {
185240 let dstIndex = dstRange [ i + dstRange. lowerBound]
186241 let srcIndex = srcRange [ i + srcRange. lowerBound]
187242
188- bytes [ dstIndex] = texture . imageData [ srcIndex]
243+ bytes [ dstIndex] = textureData . imageData [ srcIndex]
189244 }
190245 }
191246 }
@@ -198,10 +253,11 @@ public final class TextureAtlasBuilder {
198253 blockSize: blockSize,
199254 textures: textures. indices. map ( {
200255 let texture = textures [ $0]
256+ let textureData = textureDatas [ texture. dataIndex]
201257 return TextureAtlas . Texture (
202258 path: texture. resourcePath,
203- size: Size2 ( Float ( texture . width) , Float ( texture . height) ) ,
204- coordinate: texture . coordinate
259+ size: Size2 ( Float ( textureData . width) , Float ( textureData . height) ) ,
260+ coordinate: textureData . coordinate
205261 )
206262 } )
207263 )
0 commit comments