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
21namespace doc {
22
23Tileset::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
48Tileset* 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
64Tileset* 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
80int 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
90void 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
98void 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
121void 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
143tile_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
158void 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
186void 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
196ImageRef Tileset::makeEmptyTile()
197{
198 ImageSpec spec = m_sprite->spec();
199 spec.setSize(m_grid.tileSize());
200 return ImageRef(Image::create(spec));
201}
202
203void Tileset::setExternal(const std::string& filename,
204 const tileset_index& tsi)
205{
206 m_external.filename = filename;
207 m_external.tileset = tsi;
208}
209
210bool 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
233void 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
276void 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
287void 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
305void 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
340void 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
347void 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
354TilesetHashTable& 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