1 | // Aseprite Document Library |
2 | // Copyright (c) 2019-2021 Igara Studio S.A. |
3 | // |
4 | // This file is released under the terms of the MIT license. |
5 | // Read LICENSE.txt for more information. |
6 | |
7 | #ifdef HAVE_CONFIG_H |
8 | #include "config.h" |
9 | #endif |
10 | |
11 | #include "doc/tileset.h" |
12 | |
13 | #include "doc/primitives.h" |
14 | #include "doc/remap.h" |
15 | #include "doc/sprite.h" |
16 | |
17 | #include <memory> |
18 | |
19 | #define TS_TRACE(...) // TRACE(__VA_ARGS__) |
20 | |
21 | namespace doc { |
22 | |
23 | Tileset::Tileset(Sprite* sprite, |
24 | const Grid& grid, |
25 | const tileset_index ntiles) |
26 | : Object(ObjectType::Tileset) |
27 | , m_sprite(sprite) |
28 | , m_grid(grid) |
29 | , m_tiles(ntiles) |
30 | { |
31 | // The origin of tileset grids must be 0,0 (the origin is then |
32 | // specified by each cel position) |
33 | ASSERT(grid.origin() == gfx::Point(0, 0)); |
34 | |
35 | // TODO at the moment retrieving a tileset from the clipboard use no |
36 | // sprite, but in the future we should save a whole sprite in the |
37 | // clipboard |
38 | //ASSERT(sprite); |
39 | |
40 | for (tile_index ti=0; ti<ntiles; ++ti) { |
41 | ImageRef tile = makeEmptyTile(); |
42 | m_tiles[ti] = tile; |
43 | hashImage(ti, tile); |
44 | } |
45 | } |
46 | |
47 | // static |
48 | Tileset* Tileset::MakeCopyWithSameImages(const Tileset* tileset) |
49 | { |
50 | std::unique_ptr<Tileset> copy( |
51 | new Tileset(tileset->sprite(), |
52 | tileset->grid(), |
53 | tileset->size())); |
54 | copy->setName(tileset->name()); |
55 | for (tile_index ti=0; ti<copy->size(); ++ti) { |
56 | ImageRef image = tileset->get(ti); |
57 | ASSERT(image); |
58 | copy->set(ti, image); |
59 | } |
60 | return copy.release(); |
61 | } |
62 | |
63 | // static |
64 | Tileset* Tileset::MakeCopyCopyingImages(const Tileset* tileset) |
65 | { |
66 | std::unique_ptr<Tileset> copy( |
67 | new Tileset(tileset->sprite(), |
68 | tileset->grid(), |
69 | tileset->size())); |
70 | copy->setName(tileset->name()); |
71 | for (tile_index ti=0; ti<copy->size(); ++ti) { |
72 | ImageRef image = tileset->get(ti); |
73 | ASSERT(image); |
74 | // TODO can we avoid making a copy of this image |
75 | copy->set(ti, ImageRef(Image::createCopy(image.get()))); |
76 | } |
77 | return copy.release(); |
78 | } |
79 | |
80 | int Tileset::getMemSize() const |
81 | { |
82 | int size = sizeof(Tileset) + m_name.size(); |
83 | for (auto& img : const_cast<Tileset*>(this)->m_tiles) { |
84 | ASSERT(img); |
85 | size += img->getMemSize(); |
86 | } |
87 | return size; |
88 | } |
89 | |
90 | void Tileset::resize(const tile_index ntiles) |
91 | { |
92 | int oldSize = m_tiles.size(); |
93 | m_tiles.resize(ntiles); |
94 | for (tile_index ti=oldSize; ti<ntiles; ++ti) |
95 | m_tiles[ti] = makeEmptyTile(); |
96 | } |
97 | |
98 | void Tileset::remap(const Remap& remap) |
99 | { |
100 | Tiles tmp = m_tiles; |
101 | |
102 | // The notile cannot be remapped |
103 | ASSERT(remap[0] == 0); |
104 | |
105 | for (tile_index ti=1; ti<size(); ++ti) { |
106 | TS_TRACE("m_tiles[%d] = tmp[%d]\n" , remap[ti], ti); |
107 | |
108 | ASSERT(remap[ti] >= 0); |
109 | ASSERT(remap[ti] < m_tiles.size()); |
110 | if (remap[ti] >= 0 && |
111 | remap[ti] < m_tiles.size()) { |
112 | ASSERT(remap[ti] != notile); |
113 | |
114 | m_tiles[remap[ti]] = tmp[ti]; |
115 | } |
116 | } |
117 | |
118 | rehash(); |
119 | } |
120 | |
121 | void Tileset::set(const tile_index ti, |
122 | const ImageRef& image) |
123 | { |
124 | ASSERT(image); |
125 | ASSERT(image->width() == m_grid.tileSize().w); |
126 | ASSERT(image->height() == m_grid.tileSize().h); |
127 | |
128 | #if _DEBUG |
129 | if (ti == notile && !is_empty_image(image.get())) { |
130 | TRACEARGS("Warning: setting tile 0 with a non-empty image" ); |
131 | } |
132 | #endif |
133 | |
134 | removeFromHash(ti, false); |
135 | |
136 | preprocess_transparent_pixels(image.get()); |
137 | m_tiles[ti] = image; |
138 | |
139 | if (!m_hash.empty()) |
140 | hashImage(ti, image); |
141 | } |
142 | |
143 | tile_index Tileset::add(const ImageRef& image) |
144 | { |
145 | ASSERT(image); |
146 | ASSERT(image->width() == m_grid.tileSize().w); |
147 | ASSERT(image->height() == m_grid.tileSize().h); |
148 | |
149 | preprocess_transparent_pixels(image.get()); |
150 | m_tiles.push_back(image); |
151 | |
152 | const tile_index newIndex = tile_index(m_tiles.size()-1); |
153 | if (!m_hash.empty()) |
154 | hashImage(newIndex, image); |
155 | return newIndex; |
156 | } |
157 | |
158 | void Tileset::insert(const tile_index ti, |
159 | const ImageRef& image) |
160 | { |
161 | ASSERT(image); |
162 | ASSERT(image->width() == m_grid.tileSize().w); |
163 | ASSERT(image->height() == m_grid.tileSize().h); |
164 | |
165 | #if _DEBUG |
166 | if (ti == notile && !is_empty_image(image.get())) { |
167 | TRACEARGS("Warning: inserting tile 0 with a non-empty image" ); |
168 | } |
169 | #endif |
170 | |
171 | ASSERT(ti >= 0 && ti <= m_tiles.size()+1); |
172 | preprocess_transparent_pixels(image.get()); |
173 | m_tiles.insert(m_tiles.begin()+ti, image); |
174 | |
175 | if (!m_hash.empty()) { |
176 | // Fix all indexes in the hash that are greater than "ti" |
177 | for (auto& it : m_hash) |
178 | if (it.second >= ti) |
179 | ++it.second; |
180 | |
181 | // And now we can add the new image with the "ti" index |
182 | hashImage(ti, image); |
183 | } |
184 | } |
185 | |
186 | void Tileset::erase(const tile_index ti) |
187 | { |
188 | ASSERT(ti >= 0 && ti < size()); |
189 | // TODO check why this doesn't work |
190 | //removeFromHash(ti, true); |
191 | |
192 | m_tiles.erase(m_tiles.begin()+ti); |
193 | rehash(); |
194 | } |
195 | |
196 | ImageRef Tileset::makeEmptyTile() |
197 | { |
198 | ImageSpec spec = m_sprite->spec(); |
199 | spec.setSize(m_grid.tileSize()); |
200 | return ImageRef(Image::create(spec)); |
201 | } |
202 | |
203 | void Tileset::setExternal(const std::string& filename, |
204 | const tileset_index& tsi) |
205 | { |
206 | m_external.filename = filename; |
207 | m_external.tileset = tsi; |
208 | } |
209 | |
210 | bool Tileset::findTileIndex(const ImageRef& tileImage, |
211 | tile_index& ti) |
212 | { |
213 | ASSERT(tileImage); |
214 | if (!tileImage) { |
215 | ti = notile; |
216 | return false; |
217 | } |
218 | |
219 | auto& h = hashTable(); // Don't use m_hash directly in case that |
220 | // we've to regenerate the hash table. |
221 | |
222 | auto it = h.find(tileImage); |
223 | if (it != h.end()) { |
224 | ti = it->second; |
225 | return true; |
226 | } |
227 | else { |
228 | ti = notile; |
229 | return false; |
230 | } |
231 | } |
232 | |
233 | void Tileset::notifyTileContentChange(const tile_index ti) |
234 | { |
235 | #if 0 // TODO Try to do less work |
236 | |
237 | ASSERT(ti >= 0 && ti < size()); |
238 | |
239 | // If two or more tiles are exactly the same, they will have the |
240 | // same hash, so the m_hash table can be smaller than the m_tiles |
241 | // array. |
242 | if (m_hash.size() < m_tiles.size()) { |
243 | // Count how many hash elements are poiting to this tile index |
244 | int tilesWithSameHash = 0; |
245 | for (auto item : m_hash) |
246 | if (item.second == ti) |
247 | ++tilesWithSameHash; |
248 | |
249 | // In this case we re-generate the whole hash table because one or |
250 | // more tiles tile are using the hash of the modified tile. |
251 | if (tilesWithSameHash >= 2) { |
252 | rehash(); |
253 | return; |
254 | } |
255 | } |
256 | |
257 | // In other case we can do a fast-path, just removing and |
258 | // re-adding the tile to the hash table. |
259 | removeFromHash(ti, false); |
260 | if (!m_hash.empty()) |
261 | hashImage(ti, m_tiles[ti]); |
262 | |
263 | #else // Regenerate the whole hash map (at the moment this is the |
264 | // only way to make it work correctly) |
265 | |
266 | (void)ti; // unused |
267 | |
268 | if (ti >= 0 && ti < m_tiles.size() && m_tiles[ti]) |
269 | preprocess_transparent_pixels(m_tiles[ti].get()); |
270 | |
271 | rehash(); |
272 | |
273 | #endif |
274 | } |
275 | |
276 | void Tileset::notifyRegenerateEmptyTile() |
277 | { |
278 | if (size() == 0) |
279 | return; |
280 | |
281 | ImageRef image = get(doc::notile); |
282 | if (image) |
283 | doc::clear_image(image.get(), image->maskColor()); |
284 | rehash(); |
285 | } |
286 | |
287 | void Tileset::removeFromHash(const tile_index ti, |
288 | const bool adjustIndexes) |
289 | { |
290 | auto end = m_hash.end(); |
291 | for (auto it=m_hash.begin(); it!=end; ) { |
292 | if (it->second == ti) { |
293 | it = m_hash.erase(it); |
294 | end = m_hash.end(); |
295 | } |
296 | else { |
297 | if (adjustIndexes && it->second > ti) |
298 | --it->second; |
299 | ++it; |
300 | } |
301 | } |
302 | } |
303 | |
304 | #ifdef _DEBUG |
305 | void Tileset::assertValidHashTable() |
306 | { |
307 | // And empty hash table means that we've to re-generate it when it's |
308 | // needed (when findTileIndex() is used). |
309 | if (m_hash.empty()) |
310 | return; |
311 | |
312 | // If two or more tiles are exactly the same, they will have the |
313 | // same hash, so the m_hash table can be smaller than the m_tiles |
314 | // array. |
315 | if (m_hash.size() < m_tiles.size()) { |
316 | for (tile_index ti=0; ti<tile_index(m_tiles.size()); ++ti) { |
317 | auto it = m_hash.find(m_tiles[ti]); |
318 | ASSERT(it != m_hash.end()); |
319 | |
320 | // If the hash doesn't match, it is because other tile is equal |
321 | // to this one. |
322 | if (it->second != ti) { |
323 | ASSERT(is_same_image(it->first.get(), m_tiles[it->second].get())); |
324 | } |
325 | } |
326 | } |
327 | else if (m_hash.size() == m_tiles.size()) { |
328 | for (tile_index ti=0; ti<tile_index(m_tiles.size()); ++ti) { |
329 | auto it = m_hash.find(m_tiles[ti]); |
330 | ASSERT(it != m_hash.end()); |
331 | ASSERT(it->second == ti); |
332 | } |
333 | } |
334 | else { |
335 | ASSERT(false && "The hash table cannot contain more tiles than the tileset" ); |
336 | } |
337 | } |
338 | #endif |
339 | |
340 | void Tileset::hashImage(const tile_index ti, |
341 | const ImageRef& tileImage) |
342 | { |
343 | if (m_hash.find(tileImage) == m_hash.end()) |
344 | m_hash[tileImage] = ti; |
345 | } |
346 | |
347 | void Tileset::rehash() |
348 | { |
349 | // Clear the hash table, we'll lazy-rehash it when |
350 | // hashTable()/findTileIndex() is used. |
351 | m_hash.clear(); |
352 | } |
353 | |
354 | TilesetHashTable& Tileset::hashTable() |
355 | { |
356 | if (m_hash.empty()) { |
357 | // Re-hash/create the whole hash table from scratch |
358 | tile_index ti = 0; |
359 | for (auto tile : m_tiles) |
360 | hashImage(ti++, tile); |
361 | } |
362 | return m_hash; |
363 | } |
364 | |
365 | } // namespace doc |
366 | |