@codexo/exojs-tiled 0.15.3 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +59 -66
  2. package/dist/esm/TiledLayer.d.ts +2 -1
  3. package/dist/esm/TiledLayer.d.ts.map +1 -0
  4. package/dist/esm/TiledLayer.js +106 -108
  5. package/dist/esm/TiledLayer.js.map +1 -1
  6. package/dist/esm/TiledMap.d.ts +35 -7
  7. package/dist/esm/TiledMap.d.ts.map +1 -0
  8. package/dist/esm/TiledMap.js +714 -589
  9. package/dist/esm/TiledMap.js.map +1 -1
  10. package/dist/esm/TiledObject.d.ts +7 -0
  11. package/dist/esm/TiledObject.d.ts.map +1 -0
  12. package/dist/esm/TiledObject.js +59 -51
  13. package/dist/esm/TiledObject.js.map +1 -1
  14. package/dist/esm/TiledTileset.d.ts +11 -4
  15. package/dist/esm/TiledTileset.d.ts.map +1 -0
  16. package/dist/esm/TiledTileset.js +81 -70
  17. package/dist/esm/TiledTileset.js.map +1 -1
  18. package/dist/esm/data.d.ts +25 -7
  19. package/dist/esm/data.d.ts.map +1 -0
  20. package/dist/esm/decodeLayerData.d.ts +2 -1
  21. package/dist/esm/decodeLayerData.d.ts.map +1 -0
  22. package/dist/esm/decodeLayerData.js +53 -92
  23. package/dist/esm/decodeLayerData.js.map +1 -1
  24. package/dist/esm/gid.d.ts +2 -1
  25. package/dist/esm/gid.d.ts.map +1 -0
  26. package/dist/esm/gid.js +15 -24
  27. package/dist/esm/gid.js.map +1 -1
  28. package/dist/esm/index.d.ts +1 -0
  29. package/dist/esm/index.d.ts.map +1 -0
  30. package/dist/esm/index.js +14 -12
  31. package/dist/esm/loadTiledMap.d.ts +11 -5
  32. package/dist/esm/loadTiledMap.d.ts.map +1 -0
  33. package/dist/esm/loadTiledMap.js +82 -91
  34. package/dist/esm/loadTiledMap.js.map +1 -1
  35. package/dist/esm/objectAlignment.d.ts +28 -0
  36. package/dist/esm/objectAlignment.d.ts.map +1 -0
  37. package/dist/esm/objectAlignment.js +79 -0
  38. package/dist/esm/objectAlignment.js.map +1 -0
  39. package/dist/esm/public.d.ts +17 -15
  40. package/dist/esm/public.d.ts.map +1 -0
  41. package/dist/esm/public.js +14 -0
  42. package/dist/esm/tileMapType.d.ts +31 -0
  43. package/dist/esm/tileMapType.d.ts.map +1 -0
  44. package/dist/esm/tileMapType.js +42 -0
  45. package/dist/esm/tileMapType.js.map +1 -0
  46. package/dist/esm/tiledBuildInfo.d.ts +1 -0
  47. package/dist/esm/tiledBuildInfo.d.ts.map +1 -0
  48. package/dist/esm/tiledBuildInfo.js +6 -4
  49. package/dist/esm/tiledBuildInfo.js.map +1 -1
  50. package/dist/esm/tiledExtension.d.ts +7 -6
  51. package/dist/esm/tiledExtension.d.ts.map +1 -0
  52. package/dist/esm/tiledExtension.js +23 -25
  53. package/dist/esm/tiledExtension.js.map +1 -1
  54. package/dist/esm/tiledOptions.d.ts +5 -4
  55. package/dist/esm/tiledOptions.d.ts.map +1 -0
  56. package/dist/esm/tiledOptions.js +11 -13
  57. package/dist/esm/tiledOptions.js.map +1 -1
  58. package/dist/esm/tiledSourceType.d.ts +24 -0
  59. package/dist/esm/tiledSourceType.d.ts.map +1 -0
  60. package/dist/esm/tiledSourceType.js +44 -0
  61. package/dist/esm/tiledSourceType.js.map +1 -0
  62. package/dist/esm/url.d.ts +2 -1
  63. package/dist/esm/url.d.ts.map +1 -0
  64. package/dist/esm/url.js +26 -37
  65. package/dist/esm/url.js.map +1 -1
  66. package/dist/esm/validate.d.ts +11 -10
  67. package/dist/esm/validate.d.ts.map +1 -0
  68. package/dist/esm/validate.js +485 -534
  69. package/dist/esm/validate.js.map +1 -1
  70. package/dist/esm/wangSets.d.ts +5 -4
  71. package/dist/esm/wangSets.d.ts.map +1 -0
  72. package/dist/esm/wangSets.js +90 -115
  73. package/dist/esm/wangSets.js.map +1 -1
  74. package/package.json +10 -18
  75. package/dist/esm/index.js.map +0 -1
  76. package/dist/esm/register.d.ts +0 -1
  77. package/dist/esm/register.js +0 -22
  78. package/dist/esm/register.js.map +0 -1
  79. package/dist/esm/tiledMapBinding.d.ts +0 -19
  80. package/dist/esm/tiledMapBinding.js +0 -31
  81. package/dist/esm/tiledMapBinding.js.map +0 -1
  82. package/dist/esm/tiledRuntimeMapBinding.d.ts +0 -29
  83. package/dist/esm/tiledRuntimeMapBinding.js +0 -42
  84. package/dist/esm/tiledRuntimeMapBinding.js.map +0 -1
@@ -1,603 +1,728 @@
1
- import { TextureRegion } from '@codexo/exojs';
2
- import { TileSet, TileMap, TileLayer, ImageLayer, ObjectLayer, TilePropertyKind } from '@codexo/exojs-tilemap';
3
- import { maskTiledGid, TILED_FLIPPED_DIAGONALLY_FLAG, TILED_FLIPPED_VERTICALLY_FLAG, TILED_FLIPPED_HORIZONTALLY_FLAG } from './gid.js';
4
- import { createTiledLayer, TiledGroupLayer, TiledTileLayer, TiledObjectLayer, TiledImageLayer } from './TiledLayer.js';
5
- import { resolveTiledUrl } from './url.js';
6
- import { TiledFormatError } from './validate.js';
1
+ import { getTiledObjectAnchorOffset, resolveTiledObjectAlignment } from "./objectAlignment.js";
2
+ import { TiledFormatError } from "./validate.js";
3
+ import { TILED_FLIPPED_DIAGONALLY_FLAG, TILED_FLIPPED_HORIZONTALLY_FLAG, TILED_FLIPPED_VERTICALLY_FLAG, maskTiledGid } from "./gid.js";
4
+ import { TiledGroupLayer, TiledImageLayer, TiledObjectLayer, TiledTileLayer, createTiledLayer } from "./TiledLayer.js";
5
+ import { resolveTiledUrl } from "./url.js";
6
+ import { ImageLayer, ObjectLayer, TileLayer, TileMap, TilePropertyKind, TileSet, packTile } from "@codexo/exojs-tilemap";
7
+ import { TextureRegion } from "@codexo/exojs";
7
8
 
9
+ //#region src/TiledMap.ts
8
10
  /**
9
- * A parsed and validated Tiled map (`.tmj`).
10
- *
11
- * `TiledMap` represents the parsed Tiled source format. `TileMap` is the
12
- * format-independent ExoJS runtime map used for rendering, queries, and
13
- * mutation.
14
- *
15
- * Construction validates that {@link tilesets} cover a non-overlapping,
16
- * duplicate-free range of global tile ids, and that every GID referenced by
17
- * {@link layers} (tile layer cells, infinite-map chunks, and tile object
18
- * `gid`s) falls within one of those ranges. Both checks throw
19
- * {@link TiledFormatError} on failure.
20
- */
21
- class TiledMap {
22
- /** Resolved URL this map was loaded from. */
23
- source;
24
- /** The validated raw map data this instance was built from. */
25
- data;
26
- orientation;
27
- renderOrder;
28
- class;
29
- /** Map width in tiles. */
30
- width;
31
- /** Map height in tiles. */
32
- height;
33
- /** Tile grid cell width in pixels. */
34
- tileWidth;
35
- /** Tile grid cell height in pixels. */
36
- tileHeight;
37
- infinite;
38
- backgroundColor;
39
- layers;
40
- /** Tilesets used by this map, sorted by {@link TiledTileset.firstGid} ascending. */
41
- tilesets;
42
- properties;
43
- _imageTextures;
44
- constructor(source, data, tilesets, imageTextures = new Map()) {
45
- this.source = source;
46
- this.data = data;
47
- this.orientation = data.orientation;
48
- this.renderOrder = data.renderorder;
49
- this.class = data.class ?? '';
50
- this.width = data.width;
51
- this.height = data.height;
52
- this.tileWidth = data.tilewidth;
53
- this.tileHeight = data.tileheight;
54
- this.infinite = data.infinite;
55
- this.backgroundColor = data.backgroundcolor;
56
- this.properties = data.properties ?? [];
57
- this._imageTextures = imageTextures;
58
- this.layers = data.layers.map(createTiledLayer);
59
- this.tilesets = sortAndValidateTilesetRanges(tilesets, source);
60
- checkGidCoverage(this, source);
61
- }
62
- /**
63
- * Returns the tileset that owns `gid`, or `undefined` if `gid` is `0`
64
- * (the empty-cell sentinel) or is not covered by any tileset.
65
- *
66
- * Flip/rotation flag bits are masked off before the range lookup, so the
67
- * raw GID values found in {@link TiledTileLayer.data}/`chunks` and
68
- * {@link TiledObject.gid} can be passed directly.
69
- */
70
- findTilesetForGid(gid) {
71
- const id = maskTiledGid(gid);
72
- if (id === 0) {
73
- return undefined;
74
- }
75
- for (const tileset of this.tilesets) {
76
- if (id >= tileset.firstGid && id <= tileset.lastGid) {
77
- return tileset;
78
- }
79
- }
80
- return undefined;
81
- }
82
- /** Looks up a custom property by name. */
83
- getProperty(name) {
84
- return this.properties.find(property => property.name === name);
85
- }
86
- /**
87
- * Convert this parsed Tiled source model into a format-independent runtime
88
- * {@link TileMap} from `@codexo/exojs-tilemap`.
89
- *
90
- * Only finite orthogonal maps with atlas tilesets are supported. A
91
- * non-orthogonal or infinite map, or a collection-of-images tileset, throws
92
- * {@link TiledFormatError} rather than silently producing wrong (misplaced)
93
- * or empty geometry. Tile layers become renderable `TileLayer`s, object
94
- * groups become data-only `ObjectLayer`s, and image layers become data-only
95
- * `ImageLayer`s; group layer children are flattened in document order.
96
- *
97
- * The returned `TileMap` does **not** own the tileset textures they remain
98
- * in the Loader cache. Destroying the returned map does not unload textures.
99
- */
100
- toTileMap() {
101
- // The runtime TileMap is finite + orthogonal in this release. Reject maps
102
- // we cannot convert faithfully rather than silently producing misplaced
103
- // (isometric/staggered/hexagonal) or empty (infinite/chunked) geometry.
104
- if (this.orientation !== 'orthogonal') {
105
- throw new TiledFormatError(this.source, 'orientation', `toTileMap() supports only orthogonal maps in this release, got "${this.orientation}"`);
106
- }
107
- if (this.infinite) {
108
- throw new TiledFormatError(this.source, 'infinite', 'toTileMap() supports only finite maps in this release; infinite (chunked) maps are not yet convertible');
109
- }
110
- // Build runtime tilesets. Tilesets with a per-tile image collection are
111
- // not yet supported; collection-of-images tilesets throw TiledFormatError.
112
- // Tilesets with no image at all (no texture, no tileTextures) are silently
113
- // skipped their cells appear as empty in the runtime layer.
114
- const runtimeTilesets = [];
115
- // indexToRuntime[i] = runtime TileSet for tilesets[i], or null if skipped.
116
- const indexToRuntime = [];
117
- for (let i = 0; i < this.tilesets.length; i++) {
118
- const tiledTs = this.tilesets[i];
119
- if (!tiledTs) {
120
- indexToRuntime.push(null);
121
- continue;
122
- }
123
- if (!tiledTs.texture) {
124
- if (tiledTs.tileTextures.size > 0) {
125
- throw new TiledFormatError(this.source, `tilesets/${tiledTs.name}`, `tileset "${tiledTs.name}" is a collection-of-images tileset; ` +
126
- `toTileMap() requires atlas tilesets in this release`);
127
- }
128
- // No atlas image and no per-tile images → skip; cells become empty.
129
- indexToRuntime.push(null);
130
- continue;
131
- }
132
- const tw = tiledTs.imageWidth ?? tiledTs.texture.width;
133
- const th = tiledTs.imageHeight ?? tiledTs.texture.height;
134
- const region = new TextureRegion(tiledTs.texture, { x: 0, y: 0, width: tw, height: th });
135
- const rts = new TileSet({
136
- name: tiledTs.name,
137
- texture: region,
138
- tileWidth: tiledTs.tileWidth,
139
- tileHeight: tiledTs.tileHeight,
140
- tileCount: tiledTs.tileCount,
141
- columns: tiledTs.columns,
142
- spacing: tiledTs.spacing,
143
- margin: tiledTs.margin,
144
- class: tiledTs.class,
145
- offsetX: tiledTs.tileOffset.x,
146
- offsetY: tiledTs.tileOffset.y,
147
- });
148
- // Carry per-tile metadata (properties + animation frames) into the runtime
149
- // tileset. Without this, Tiled tile animations and per-tile properties are
150
- // lost at conversion. Out-of-range entries are skipped defensively.
151
- const defs = buildTileDefinitions(tiledTs.tiles, tiledTs.tileCount);
152
- if (defs.length > 0) {
153
- rts._setDefinitions(defs);
154
- }
155
- runtimeTilesets.push(rts);
156
- indexToRuntime.push(rts);
157
- }
158
- // Collect and convert tile + object + image layers, flattening group layers.
159
- const runtimeLayers = [];
160
- const runtimeObjectLayers = [];
161
- const runtimeImageLayers = [];
162
- const convertLayers = (layers) => {
163
- for (const layer of layers) {
164
- if (layer instanceof TiledGroupLayer) {
165
- convertLayers(layer.layers);
166
- }
167
- else if (layer instanceof TiledTileLayer) {
168
- const rLayer = new TileLayer({
169
- id: layer.id,
170
- name: layer.name,
171
- width: layer.width,
172
- height: layer.height,
173
- tilesets: runtimeTilesets,
174
- tileWidth: this.tileWidth,
175
- tileHeight: this.tileHeight,
176
- visible: layer.visible,
177
- opacity: layer.opacity,
178
- offsetX: layer.offsetX,
179
- offsetY: layer.offsetY,
180
- parallaxX: layer.parallaxX,
181
- parallaxY: layer.parallaxY,
182
- class: layer.class,
183
- tintColor: parseTiledColor(layer.tintColor),
184
- });
185
- if (layer.data) {
186
- populateTileLayer(rLayer, layer.data, this.tilesets, indexToRuntime);
187
- }
188
- runtimeLayers.push(rLayer);
189
- }
190
- else if (layer instanceof TiledObjectLayer) {
191
- runtimeObjectLayers.push(convertObjectLayer(layer, this.tilesets, indexToRuntime));
192
- }
193
- else if (layer instanceof TiledImageLayer) {
194
- runtimeImageLayers.push(new ImageLayer({
195
- id: layer.id,
196
- name: layer.name,
197
- class: layer.class,
198
- image: resolveTiledUrl(layer.image, this.source),
199
- texture: this._imageTextures.get(layer.id) ?? null,
200
- visible: layer.visible,
201
- opacity: layer.opacity,
202
- offsetX: layer.offsetX,
203
- offsetY: layer.offsetY,
204
- parallaxX: layer.parallaxX,
205
- parallaxY: layer.parallaxY,
206
- tintColor: parseTiledColor(layer.tintColor),
207
- repeatX: layer.repeatX,
208
- repeatY: layer.repeatY,
209
- properties: convertProperties(layer.properties),
210
- }));
211
- }
212
- }
213
- };
214
- convertLayers(this.layers);
215
- return new TileMap({
216
- name: this.source,
217
- width: this.width,
218
- height: this.height,
219
- tileWidth: this.tileWidth,
220
- tileHeight: this.tileHeight,
221
- tilesets: runtimeTilesets,
222
- layers: runtimeLayers,
223
- objectLayers: runtimeObjectLayers,
224
- imageLayers: runtimeImageLayers,
225
- class: this.class,
226
- backgroundColor: parseTiledColor(this.backgroundColor),
227
- renderOrder: this.renderOrder ?? 'right-down',
228
- properties: convertProperties(this.properties),
229
- });
230
- }
231
- /**
232
- * Releases this map's reference to its parsed source. Tileset textures are
233
- * Loader-owned and may be shared with other maps; this does NOT destroy
234
- * them.
235
- */
236
- destroy() {
237
- // Intentionally empty: all sub-resources (textures) are Loader-owned.
238
- }
239
- }
11
+ * A parsed and validated Tiled map (`.tmj`).
12
+ *
13
+ * `TiledMap` represents the parsed Tiled source format. `TileMap` is the
14
+ * format-independent ExoJS runtime map used for rendering, queries, and
15
+ * mutation.
16
+ *
17
+ * Construction validates that {@link tilesets} cover a non-overlapping,
18
+ * duplicate-free range of global tile ids, and that every GID referenced by
19
+ * {@link layers} (tile layer cells, infinite-map chunks, and tile object
20
+ * `gid`s) falls within one of those ranges. Both checks throw
21
+ * {@link TiledFormatError} on failure.
22
+ */
23
+ var TiledMap = class {
24
+ /** Resolved URL this map was loaded from. */
25
+ source;
26
+ /** The validated raw map data this instance was built from. */
27
+ data;
28
+ orientation;
29
+ renderOrder;
30
+ class;
31
+ /** Map width in tiles. */
32
+ width;
33
+ /** Map height in tiles. */
34
+ height;
35
+ /** Tile grid cell width in pixels. */
36
+ tileWidth;
37
+ /** Tile grid cell height in pixels. */
38
+ tileHeight;
39
+ infinite;
40
+ backgroundColor;
41
+ layers;
42
+ /** Tilesets used by this map, sorted by {@link TiledTileset.firstGid} ascending. */
43
+ tilesets;
44
+ properties;
45
+ _imageTextures;
46
+ _chunkSources = /* @__PURE__ */ new Map();
47
+ constructor(source, data, tilesets, imageTextures = /* @__PURE__ */ new Map()) {
48
+ this.source = source;
49
+ this.data = data;
50
+ this.orientation = data.orientation;
51
+ this.renderOrder = data.renderorder;
52
+ this.class = data.class ?? "";
53
+ this.width = data.width;
54
+ this.height = data.height;
55
+ this.tileWidth = data.tilewidth;
56
+ this.tileHeight = data.tileheight;
57
+ this.infinite = data.infinite;
58
+ this.backgroundColor = data.backgroundcolor;
59
+ this.properties = data.properties ?? [];
60
+ this._imageTextures = imageTextures;
61
+ this.layers = data.layers.map(createTiledLayer);
62
+ this.tilesets = sortAndValidateTilesetRanges(tilesets, source);
63
+ checkGidCoverage(this, source);
64
+ }
65
+ /**
66
+ * Returns the tileset that owns `gid`, or `undefined` if `gid` is `0`
67
+ * (the empty-cell sentinel) or is not covered by any tileset.
68
+ *
69
+ * Flip/rotation flag bits are masked off before the range lookup, so the
70
+ * raw GID values found in {@link TiledTileLayer.data}/`chunks` and
71
+ * {@link TiledObject.gid} can be passed directly.
72
+ */
73
+ findTilesetForGid(gid) {
74
+ const id = maskTiledGid(gid);
75
+ if (id === 0) return;
76
+ for (const tileset of this.tilesets) if (id >= tileset.firstGid && id <= tileset.lastGid) return tileset;
77
+ }
78
+ /** Looks up a custom property by name. */
79
+ getProperty(name) {
80
+ return this.properties.find((property) => property.name === name);
81
+ }
82
+ /**
83
+ * Convert this parsed Tiled source model into a format-independent runtime
84
+ * {@link TileMap} from `@codexo/exojs-tilemap`.
85
+ *
86
+ * Only orthogonal maps with atlas tilesets are supported. A non-orthogonal
87
+ * map, a collection-of-images tileset, or a tileset with no image at all
88
+ * throws {@link TiledFormatError} rather than silently producing wrong
89
+ * (misplaced) or empty geometry. Tile
90
+ * layers become renderable `TileLayer`s, object groups become data-only
91
+ * `ObjectLayer`s, and image layers become data-only `ImageLayer`s; group
92
+ * layer children are flattened in document order.
93
+ *
94
+ * The runtime map has no group node, so each group's own style is baked into
95
+ * its children as Tiled composes it: `visible` ANDs, `opacity` and the
96
+ * parallax factors multiply, the offsets add, and tint colours multiply per
97
+ * colour channel. Object layers take the visibility, opacity and offset the
98
+ * same way - `ObjectLayer` carries no parallax or tint of its own, so a
99
+ * group's contribution to those two is dropped for object layers only.
100
+ *
101
+ * An `infinite: true` (chunked) tile layer converts to an **unbounded**
102
+ * runtime `TileLayer` with no tiles populated eagerly - its data streams in
103
+ * on demand via the {@link ChunkSource} built for it, retrievable with
104
+ * {@link getChunkSource} after this call returns.
105
+ *
106
+ * The returned `TileMap` does **not** own the tileset textures - they remain
107
+ * in the Loader cache. Destroying the returned map does not unload textures.
108
+ */
109
+ toTileMap() {
110
+ this._chunkSources.clear();
111
+ if (this.orientation !== "orthogonal") throw new TiledFormatError(this.source, "orientation", `toTileMap() supports only orthogonal maps in this release, got "${this.orientation}"`);
112
+ const runtimeTilesets = [];
113
+ const indexToRuntime = [];
114
+ for (let i = 0; i < this.tilesets.length; i++) {
115
+ const tiledTs = this.tilesets[i];
116
+ if (!tiledTs) {
117
+ indexToRuntime.push(null);
118
+ continue;
119
+ }
120
+ if (!tiledTs.texture) {
121
+ if (tiledTs.tileTextures.size > 0) throw new TiledFormatError(this.source, `tilesets/${tiledTs.name}`, `tileset "${tiledTs.name}" is a collection-of-images tileset; toTileMap() requires atlas tilesets in this release`);
122
+ throw new TiledFormatError(this.source, `tilesets/${tiledTs.name}`, `tileset "${tiledTs.name}" has no image; an atlas tileset needs an "image" that resolves to a loaded texture`);
123
+ }
124
+ const tw = tiledTs.imageWidth ?? tiledTs.texture.width;
125
+ const th = tiledTs.imageHeight ?? tiledTs.texture.height;
126
+ const region = new TextureRegion(tiledTs.texture, {
127
+ x: 0,
128
+ y: 0,
129
+ width: tw,
130
+ height: th
131
+ });
132
+ const rts = new TileSet({
133
+ name: tiledTs.name,
134
+ texture: region,
135
+ tileWidth: tiledTs.tileWidth,
136
+ tileHeight: tiledTs.tileHeight,
137
+ tileCount: tiledTs.tileCount,
138
+ columns: tiledTs.columns,
139
+ spacing: tiledTs.spacing,
140
+ margin: tiledTs.margin,
141
+ class: tiledTs.class,
142
+ offsetX: tiledTs.tileOffset.x,
143
+ offsetY: tiledTs.tileOffset.y
144
+ });
145
+ const defs = buildTileDefinitions(tiledTs.tiles, tiledTs.tileCount);
146
+ if (defs.length > 0) rts._setDefinitions(defs);
147
+ runtimeTilesets.push(rts);
148
+ indexToRuntime.push(rts);
149
+ }
150
+ const runtimeLayers = [];
151
+ const runtimeObjectLayers = [];
152
+ const runtimeImageLayers = [];
153
+ const order = [];
154
+ const convertLayers = (layers, group) => {
155
+ for (const layer of layers) if (layer instanceof TiledGroupLayer) convertLayers(layer.layers, composeGroupStyle(group, layer));
156
+ else if (layer instanceof TiledTileLayer) {
157
+ const chunked = layer.chunks !== void 0;
158
+ const rLayer = new TileLayer({
159
+ id: layer.id,
160
+ name: layer.name,
161
+ ...chunked ? {} : {
162
+ width: layer.width,
163
+ height: layer.height
164
+ },
165
+ tilesets: runtimeTilesets,
166
+ tileWidth: this.tileWidth,
167
+ tileHeight: this.tileHeight,
168
+ visible: group.visible && layer.visible,
169
+ opacity: group.opacity * layer.opacity,
170
+ offsetX: group.offsetX + layer.offsetX,
171
+ offsetY: group.offsetY + layer.offsetY,
172
+ parallaxX: group.parallaxX * layer.parallaxX,
173
+ parallaxY: group.parallaxY * layer.parallaxY,
174
+ class: layer.class,
175
+ tintColor: multiplyTiledTint(group.tintColor, parseTiledColor(layer.tintColor))
176
+ });
177
+ if (layer.data) populateTileLayer(rLayer, layer.data, this.tilesets, indexToRuntime, layer.width);
178
+ else if (chunked) this._chunkSources.set(layer.id, buildTiledChunkSource(layer, rLayer, this.tilesets, indexToRuntime, this.source));
179
+ runtimeLayers.push(rLayer);
180
+ order.push(layer.id);
181
+ } else if (layer instanceof TiledObjectLayer) runtimeObjectLayers.push(convertObjectLayer(layer, this.tilesets, indexToRuntime, this.orientation, group));
182
+ else if (layer instanceof TiledImageLayer) {
183
+ runtimeImageLayers.push(new ImageLayer({
184
+ id: layer.id,
185
+ name: layer.name,
186
+ class: layer.class,
187
+ image: resolveTiledUrl(layer.image, this.source),
188
+ texture: this._imageTextures.get(layer.id) ?? null,
189
+ visible: group.visible && layer.visible,
190
+ opacity: group.opacity * layer.opacity,
191
+ offsetX: group.offsetX + layer.offsetX,
192
+ offsetY: group.offsetY + layer.offsetY,
193
+ parallaxX: group.parallaxX * layer.parallaxX,
194
+ parallaxY: group.parallaxY * layer.parallaxY,
195
+ tintColor: multiplyTiledTint(group.tintColor, parseTiledColor(layer.tintColor)),
196
+ repeatX: layer.repeatX,
197
+ repeatY: layer.repeatY,
198
+ properties: convertProperties(layer.properties)
199
+ }));
200
+ order.push(layer.id);
201
+ }
202
+ };
203
+ convertLayers(this.layers, rootGroupStyle);
204
+ return new TileMap({
205
+ name: this.source,
206
+ ...this.infinite ? {} : {
207
+ width: this.width,
208
+ height: this.height
209
+ },
210
+ tileWidth: this.tileWidth,
211
+ tileHeight: this.tileHeight,
212
+ tilesets: runtimeTilesets,
213
+ layers: runtimeLayers,
214
+ objectLayers: runtimeObjectLayers,
215
+ imageLayers: runtimeImageLayers,
216
+ class: this.class,
217
+ backgroundColor: parseTiledColor(this.backgroundColor),
218
+ renderOrder: this.renderOrder ?? "right-down",
219
+ properties: convertProperties(this.properties),
220
+ documentOrder: order
221
+ });
222
+ }
223
+ /**
224
+ * The {@link ChunkSource} built for the chunked tile layer with the given
225
+ * Tiled layer `id`, as a side effect of the most recent {@link toTileMap}
226
+ * call. Returns `undefined` if `toTileMap()` hasn't been called yet, if
227
+ * `layerId` doesn't name a tile layer, or if that layer is finite
228
+ * (`data`-based) rather than chunked (`infinite`-map `chunks`-based).
229
+ *
230
+ * Call {@link toTileMap} before calling this - it is what builds the
231
+ * providers this method reads from.
232
+ * @advanced
233
+ */
234
+ getChunkSource(layerId) {
235
+ return this._chunkSources.get(layerId);
236
+ }
237
+ /**
238
+ * Releases this map's reference to its parsed source. Tileset textures are
239
+ * Loader-owned and may be shared with other maps; this does NOT destroy
240
+ * them.
241
+ */
242
+ destroy() {}
243
+ };
240
244
  /**
241
- * Resolve a raw (flag-bearing) Tiled GID to a runtime {@link ResolvedTile}, or
242
- * `null` for an empty cell or a GID whose tileset was skipped (no atlas image).
243
- */
244
- function resolveGid(rawGid, tiledTilesets, indexToRuntime) {
245
- if (rawGid === 0)
246
- return null;
247
- const baseGid = maskTiledGid(rawGid);
248
- const transform = {
249
- flipX: (rawGid >>> 0 & TILED_FLIPPED_HORIZONTALLY_FLAG) !== 0,
250
- flipY: (rawGid >>> 0 & TILED_FLIPPED_VERTICALLY_FLAG) !== 0,
251
- diagonal: (rawGid >>> 0 & TILED_FLIPPED_DIAGONALLY_FLAG) !== 0,
252
- };
253
- // Find owning tileset: rightmost with firstGid <= baseGid (tilesets sorted asc).
254
- let tsIdx = -1;
255
- for (let t = tiledTilesets.length - 1; t >= 0; t--) {
256
- const candidate = tiledTilesets[t];
257
- if (candidate && baseGid >= candidate.firstGid) {
258
- tsIdx = t;
259
- break;
260
- }
261
- }
262
- if (tsIdx === -1)
263
- return null;
264
- const owningTs = tiledTilesets[tsIdx];
265
- const runtimeTs = indexToRuntime[tsIdx];
266
- if (!owningTs || !runtimeTs)
267
- return null; // tileset was skipped (no atlas image)
268
- return {
269
- tileset: runtimeTs,
270
- localTileId: baseGid - owningTs.firstGid,
271
- transform,
272
- };
273
- }
274
- /** Fill a `TileLayer` from a flat GID array decoded from a Tiled tile layer. */
275
- function populateTileLayer(layer, gids, tiledTilesets, indexToRuntime) {
276
- for (let i = 0; i < gids.length; i++) {
277
- const gid = gids[i];
278
- if (gid === undefined)
279
- continue;
280
- const tile = resolveGid(gid, tiledTilesets, indexToRuntime);
281
- if (!tile)
282
- continue;
283
- layer.setTileAt(i % layer.width, Math.floor(i / layer.width), tile);
284
- }
285
- }
245
+ * Index of the tileset owning `baseGid` (flip flags already masked off), or
246
+ * `-1` when none covers it: the rightmost entry with `firstGid <= baseGid`,
247
+ * relying on `tilesets` being sorted ascending.
248
+ */
249
+ const findTilesetIndexForGid = (baseGid, tiledTilesets) => {
250
+ for (let t = tiledTilesets.length - 1; t >= 0; t--) {
251
+ const candidate = tiledTilesets[t];
252
+ if (candidate && baseGid >= candidate.firstGid) return t;
253
+ }
254
+ return -1;
255
+ };
256
+ /**
257
+ * Resolve a raw (flag-bearing) Tiled GID to a runtime {@link ResolvedTile}, or
258
+ * `null` for an empty cell or a GID no tileset covers.
259
+ */
260
+ const resolveGid = (rawGid, tiledTilesets, indexToRuntime) => {
261
+ if (rawGid === 0) return null;
262
+ const baseGid = maskTiledGid(rawGid);
263
+ const transform = {
264
+ flipX: (rawGid >>> 0 & TILED_FLIPPED_HORIZONTALLY_FLAG) !== 0,
265
+ flipY: (rawGid >>> 0 & TILED_FLIPPED_VERTICALLY_FLAG) !== 0,
266
+ diagonal: (rawGid >>> 0 & TILED_FLIPPED_DIAGONALLY_FLAG) !== 0
267
+ };
268
+ const tsIdx = findTilesetIndexForGid(baseGid, tiledTilesets);
269
+ if (tsIdx === -1) return null;
270
+ const owningTs = tiledTilesets[tsIdx];
271
+ const runtimeTs = indexToRuntime[tsIdx];
272
+ if (!owningTs || !runtimeTs) return null;
273
+ return {
274
+ tileset: runtimeTs,
275
+ localTileId: baseGid - owningTs.firstGid,
276
+ transform
277
+ };
278
+ };
279
+ /**
280
+ * Fill a `TileLayer` from a flat GID array decoded from a Tiled tile layer.
281
+ *
282
+ * @param width The source Tiled layer's width in tiles - `toTileMap()` always
283
+ * constructs `layer` as bounded with this same width, but that
284
+ * fact isn't visible to the type checker from `layer` alone
285
+ * (runtime `TileLayer.width` is `number | undefined` since it
286
+ * also supports unbounded layers), so the caller's already-known
287
+ * Tiled-source width is threaded through explicitly instead.
288
+ */
289
+ const populateTileLayer = (layer, gids, tiledTilesets, indexToRuntime, width) => {
290
+ for (let i = 0; i < gids.length; i++) {
291
+ const gid = gids[i];
292
+ if (gid === void 0) continue;
293
+ const tile = resolveGid(gid, tiledTilesets, indexToRuntime);
294
+ if (!tile) continue;
295
+ layer.setTileAt(i % width, Math.floor(i / width), tile);
296
+ }
297
+ };
298
+ /**
299
+ * Build a {@link ChunkSource} that lazily re-slices a chunked
300
+ * {@link TiledTileLayer}'s fixed-size on-disk chunks into `runtimeLayer`'s
301
+ * own chunk grid. Indexes `layer.chunks` once by on-disk coordinate;
302
+ * `getChunk` composes each request from the on-disk chunks it overlaps.
303
+ *
304
+ * @throws {TiledFormatError} if the layer's on-disk chunks don't all share
305
+ * the same width/height (Tiled always emits a uniform size in
306
+ * practice; this is a defensive guard against malformed input).
307
+ * @throws {TiledFormatError} if an on-disk chunk's `x`/`y` isn't aligned to
308
+ * the on-disk chunk grid (Tiled always emits aligned chunks; a
309
+ * misaligned chunk would otherwise silently corrupt the re-sliced
310
+ * tile indices instead of failing loudly).
311
+ */
312
+ const buildTiledChunkSource = (layer, runtimeLayer, tiledTilesets, indexToRuntime, source) => {
313
+ const index = /* @__PURE__ */ new Map();
314
+ let onDiskWidth = 0;
315
+ let onDiskHeight = 0;
316
+ for (const chunk of layer.chunks ?? []) {
317
+ if (onDiskWidth === 0) {
318
+ onDiskWidth = chunk.width;
319
+ onDiskHeight = chunk.height;
320
+ } else if (chunk.width !== onDiskWidth || chunk.height !== onDiskHeight) throw new TiledFormatError(source, `layers/${layer.name}/chunks`, `non-uniform infinite-map chunk size is not supported (expected ${onDiskWidth}x${onDiskHeight}, got ${chunk.width}x${chunk.height})`);
321
+ if (chunk.x % onDiskWidth !== 0 || chunk.y % onDiskHeight !== 0) throw new TiledFormatError(source, `layers/${layer.name}/chunks`, `misaligned infinite-map chunk in layer ${layer.id} at (${chunk.x}, ${chunk.y}) is not a multiple of the on-disk chunk size (${onDiskWidth}x${onDiskHeight})`);
322
+ index.set(`${chunk.x},${chunk.y}`, chunk);
323
+ }
324
+ return { getChunk(cx, cy) {
325
+ if (onDiskWidth === 0) return null;
326
+ const chunkWidth = runtimeLayer.chunkWidth;
327
+ const chunkHeight = runtimeLayer.chunkHeight;
328
+ const qx0 = cx * chunkWidth;
329
+ const qy0 = cy * chunkHeight;
330
+ const qx1 = qx0 + chunkWidth - 1;
331
+ const qy1 = qy0 + chunkHeight - 1;
332
+ const gx0 = Math.floor(qx0 / onDiskWidth) * onDiskWidth;
333
+ const gx1 = Math.floor(qx1 / onDiskWidth) * onDiskWidth;
334
+ const gy0 = Math.floor(qy0 / onDiskHeight) * onDiskHeight;
335
+ const gy1 = Math.floor(qy1 / onDiskHeight) * onDiskHeight;
336
+ let out = null;
337
+ for (let gy = gy0; gy <= gy1; gy += onDiskHeight) for (let gx = gx0; gx <= gx1; gx += onDiskWidth) {
338
+ const chunk = index.get(`${gx},${gy}`);
339
+ if (!chunk) continue;
340
+ const ix0 = Math.max(qx0, gx);
341
+ const ix1 = Math.min(qx1, gx + onDiskWidth - 1);
342
+ const iy0 = Math.max(qy0, gy);
343
+ const iy1 = Math.min(qy1, gy + onDiskHeight - 1);
344
+ for (let ty = iy0; ty <= iy1; ty++) for (let tx = ix0; tx <= ix1; tx++) {
345
+ const rawGid = chunk.data[(ty - gy) * onDiskWidth + (tx - gx)];
346
+ if (rawGid === void 0 || rawGid === 0) continue;
347
+ const resolved = resolveGid(rawGid, tiledTilesets, indexToRuntime);
348
+ if (!resolved) continue;
349
+ out ??= new Uint32Array(chunkWidth * chunkHeight);
350
+ const tilesetIndex = runtimeLayer.tilesets.indexOf(resolved.tileset);
351
+ out[(ty - qy0) * chunkWidth + (tx - qx0)] = packTile(tilesetIndex, resolved.localTileId, resolved.transform);
352
+ }
353
+ }
354
+ return out === null ? null : {
355
+ width: chunkWidth,
356
+ height: chunkHeight,
357
+ tiles: out
358
+ };
359
+ } };
360
+ };
286
361
  /** Convert a parsed `TiledObjectLayer` into a runtime data-only `ObjectLayer`. */
287
- function convertObjectLayer(layer, tiledTilesets, indexToRuntime) {
288
- const objects = [];
289
- for (const object of layer.objects) {
290
- const converted = convertObject(object, tiledTilesets, indexToRuntime);
291
- if (converted)
292
- objects.push(converted);
293
- }
294
- return new ObjectLayer({
295
- id: layer.id,
296
- name: layer.name,
297
- class: layer.class,
298
- visible: layer.visible,
299
- opacity: layer.opacity,
300
- offsetX: layer.offsetX,
301
- offsetY: layer.offsetY,
302
- drawOrder: layer.drawOrder,
303
- objects,
304
- properties: convertProperties(layer.properties),
305
- });
306
- }
362
+ const convertObjectLayer = (layer, tiledTilesets, indexToRuntime, orientation, group) => {
363
+ const objects = [];
364
+ for (const object of layer.objects) {
365
+ const converted = convertObject(object, tiledTilesets, indexToRuntime, orientation);
366
+ if (converted) objects.push(converted);
367
+ }
368
+ return new ObjectLayer({
369
+ id: layer.id,
370
+ name: layer.name,
371
+ class: layer.class,
372
+ visible: group.visible && layer.visible,
373
+ opacity: group.opacity * layer.opacity,
374
+ offsetX: group.offsetX + layer.offsetX,
375
+ offsetY: group.offsetY + layer.offsetY,
376
+ parallaxX: group.parallaxX * layer.parallaxX,
377
+ parallaxY: group.parallaxY * layer.parallaxY,
378
+ tintColor: multiplyTiledTint(group.tintColor, parseTiledColor(layer.tintColor)),
379
+ drawOrder: layer.drawOrder,
380
+ objects,
381
+ properties: convertProperties(layer.properties)
382
+ });
383
+ };
384
+ /**
385
+ * Convert one `TiledObject` to a `TileMapObject`. A tile object whose GID
386
+ * resolves to no tileset is dropped - returns `null`.
387
+ *
388
+ * Tiled stores a **tile object**'s `x`/`y` at the tileset's object-alignment
389
+ * anchor (bottom-left by default in an orthogonal map, bottom-centre in an
390
+ * isometric one), while every other object kind stores the top-left corner of
391
+ * its bounding box. The anchor is converted to the corner here so that
392
+ * `TileMapObject.x`/`y` means the same thing for every kind - see
393
+ * {@link import('@codexo/exojs-tilemap').TileObject} for the resulting
394
+ * convention.
395
+ */
396
+ const convertObject = (object, tiledTilesets, indexToRuntime, orientation) => {
397
+ const base = {
398
+ id: object.id,
399
+ name: object.name,
400
+ type: object.type,
401
+ x: object.x,
402
+ y: object.y,
403
+ width: object.width,
404
+ height: object.height,
405
+ rotation: object.rotation,
406
+ visible: object.visible,
407
+ properties: convertProperties(object.properties),
408
+ source: object
409
+ };
410
+ if (object.text) {
411
+ const t = object.text;
412
+ const color = t.color !== void 0 ? parseTiledColor(t.color) : void 0;
413
+ const textStyle = {
414
+ text: t.text,
415
+ ...color !== void 0 && { color },
416
+ ...t.fontfamily !== void 0 && { fontFamily: t.fontfamily },
417
+ ...t.pixelsize !== void 0 && { pixelSize: t.pixelsize },
418
+ ...t.bold !== void 0 && { bold: t.bold },
419
+ ...t.italic !== void 0 && { italic: t.italic },
420
+ ...t.underline !== void 0 && { underline: t.underline },
421
+ ...t.strikeout !== void 0 && { strikeout: t.strikeout },
422
+ ...t.wrap !== void 0 && { wrap: t.wrap },
423
+ ...t.halign !== void 0 && { halign: t.halign },
424
+ ...t.valign !== void 0 && { valign: t.valign }
425
+ };
426
+ return {
427
+ ...base,
428
+ kind: "text",
429
+ text: textStyle
430
+ };
431
+ }
432
+ if (object.gid !== void 0) {
433
+ const tile = resolveGid(object.gid, tiledTilesets, indexToRuntime);
434
+ if (!tile) return null;
435
+ const anchor = tileObjectAnchorOffset(object.gid, object.width, object.height, tiledTilesets, orientation);
436
+ return {
437
+ ...base,
438
+ x: base.x - anchor.x,
439
+ y: base.y - anchor.y,
440
+ kind: "tile",
441
+ tile
442
+ };
443
+ }
444
+ if (object.point) return {
445
+ ...base,
446
+ kind: "point"
447
+ };
448
+ if (object.ellipse) return {
449
+ ...base,
450
+ kind: "ellipse"
451
+ };
452
+ if (object.polygon) return {
453
+ ...base,
454
+ kind: "polygon",
455
+ points: toPoints(object.polygon)
456
+ };
457
+ if (object.polyline) return {
458
+ ...base,
459
+ kind: "polyline",
460
+ points: toPoints(object.polyline)
461
+ };
462
+ return {
463
+ ...base,
464
+ kind: "rectangle"
465
+ };
466
+ };
467
+ const toPoints = (points) => points.map((p) => ({
468
+ x: p.x,
469
+ y: p.y
470
+ }));
471
+ /** Shared zero offset for a tile object whose GID belongs to no tileset. */
472
+ const ORIGIN_OFFSET = {
473
+ x: 0,
474
+ y: 0
475
+ };
476
+ /**
477
+ * Offset from a tile object's stored anchor to the top-left corner of its
478
+ * bounding box, using the owning tileset's `objectalignment` (or Tiled's
479
+ * orientation-dependent default). Falls back to no offset when `gid` belongs
480
+ * to no tileset - such an object is dropped by the caller anyway.
481
+ */
482
+ const tileObjectAnchorOffset = (gid, width, height, tiledTilesets, orientation) => {
483
+ const owningTs = tiledTilesets[findTilesetIndexForGid(maskTiledGid(gid), tiledTilesets)];
484
+ if (!owningTs) return ORIGIN_OFFSET;
485
+ const alignment = resolveTiledObjectAlignment(owningTs.objectAlignment, orientation);
486
+ return getTiledObjectAnchorOffset(alignment, width, height);
487
+ };
488
+ /** The identity style: what a layer at the top level of the document inherits. */
489
+ const rootGroupStyle = {
490
+ visible: true,
491
+ opacity: 1,
492
+ offsetX: 0,
493
+ offsetY: 0,
494
+ parallaxX: 1,
495
+ parallaxY: 1,
496
+ tintColor: null
497
+ };
498
+ /**
499
+ * Fold `group`'s own style into the style its children inherit, composing each
500
+ * channel the way Tiled derives a nested layer's effective value: visibility
501
+ * ANDs, opacity and parallax multiply, offsets add, and tints multiply per
502
+ * colour channel.
503
+ */
504
+ const composeGroupStyle = (parent, group) => ({
505
+ visible: parent.visible && group.visible,
506
+ opacity: parent.opacity * group.opacity,
507
+ offsetX: parent.offsetX + group.offsetX,
508
+ offsetY: parent.offsetY + group.offsetY,
509
+ parallaxX: parent.parallaxX * group.parallaxX,
510
+ parallaxY: parent.parallaxY * group.parallaxY,
511
+ tintColor: multiplyTiledTint(parent.tintColor, parseTiledColor(group.tintColor))
512
+ });
307
513
  /**
308
- * Convert one `TiledObject` to a `TileMapObject`. Tile objects whose tileset
309
- * was skipped are dropped returns `null`.
310
- */
311
- function convertObject(object, tiledTilesets, indexToRuntime) {
312
- const base = {
313
- id: object.id,
314
- name: object.name,
315
- type: object.type,
316
- x: object.x,
317
- y: object.y,
318
- width: object.width,
319
- height: object.height,
320
- rotation: object.rotation,
321
- visible: object.visible,
322
- properties: convertProperties(object.properties),
323
- };
324
- if (object.text) {
325
- const t = object.text;
326
- const color = t.color !== undefined ? parseTiledColor(t.color) : undefined;
327
- const textStyle = {
328
- text: t.text,
329
- ...(color !== undefined && { color }),
330
- ...(t.fontfamily !== undefined && { fontFamily: t.fontfamily }),
331
- ...(t.pixelsize !== undefined && { pixelSize: t.pixelsize }),
332
- ...(t.bold !== undefined && { bold: t.bold }),
333
- ...(t.italic !== undefined && { italic: t.italic }),
334
- ...(t.underline !== undefined && { underline: t.underline }),
335
- ...(t.strikeout !== undefined && { strikeout: t.strikeout }),
336
- ...(t.wrap !== undefined && { wrap: t.wrap }),
337
- ...(t.halign !== undefined && { halign: t.halign }),
338
- ...(t.valign !== undefined && { valign: t.valign }),
339
- };
340
- return { ...base, kind: 'text', text: textStyle };
341
- }
342
- if (object.gid !== undefined) {
343
- const tile = resolveGid(object.gid, tiledTilesets, indexToRuntime);
344
- if (!tile)
345
- return null;
346
- return { ...base, kind: 'tile', tile };
347
- }
348
- if (object.point) {
349
- return { ...base, kind: 'point' };
350
- }
351
- if (object.ellipse) {
352
- return { ...base, kind: 'ellipse' };
353
- }
354
- if (object.polygon) {
355
- return { ...base, kind: 'polygon', points: toPoints(object.polygon) };
356
- }
357
- if (object.polyline) {
358
- return { ...base, kind: 'polyline', points: toPoints(object.polyline) };
359
- }
360
- return { ...base, kind: 'rectangle' };
361
- }
362
- const toPoints = (points) => points.map(p => ({ x: p.x, y: p.y }));
514
+ * Multiply two `0xRRGGBB` tints channel-wise (Tiled's own composition rule).
515
+ * `null` means "no tint" and acts as the identity, so an untinted group leaves
516
+ * its children's tints untouched - and vice versa.
517
+ */
518
+ const multiplyTiledTint = (a, b) => {
519
+ if (a === null) return b;
520
+ if (b === null) return a;
521
+ const red = Math.round((a >> 16 & 255) * (b >> 16 & 255) / 255);
522
+ const green = Math.round((a >> 8 & 255) * (b >> 8 & 255) / 255);
523
+ const blue = Math.round((a & 255) * (b & 255) / 255);
524
+ return red << 16 | green << 8 | blue;
525
+ };
363
526
  /**
364
- * Parse a Tiled colour string (`#RRGGBB` or `#AARRGGBB`) into a `0xRRGGBB`
365
- * integer, dropping any alpha. Returns `null` for an absent or malformed value.
366
- */
367
- function parseTiledColor(value) {
368
- if (value === undefined || value === '')
369
- return null;
370
- const hex = value.startsWith('#') ? value.slice(1) : value;
371
- let rrggbb;
372
- if (hex.length === 8) {
373
- rrggbb = hex.slice(2); // drop leading alpha
374
- }
375
- else if (hex.length === 6) {
376
- rrggbb = hex;
377
- }
378
- else {
379
- return null;
380
- }
381
- const parsed = Number.parseInt(rrggbb, 16);
382
- return Number.isNaN(parsed) ? null : parsed;
383
- }
527
+ * Parse a Tiled colour string (`#RRGGBB` or `#AARRGGBB`) into a `0xRRGGBB`
528
+ * integer, dropping any alpha. Returns `null` for an absent or malformed value.
529
+ */
530
+ const parseTiledColor = (value) => {
531
+ if (value === void 0 || value === "") return null;
532
+ const hex = value.startsWith("#") ? value.slice(1) : value;
533
+ let rrggbb;
534
+ if (hex.length === 8) rrggbb = hex.slice(2);
535
+ else if (hex.length === 6) rrggbb = hex;
536
+ else return null;
537
+ const parsed = Number.parseInt(rrggbb, 16);
538
+ return Number.isNaN(parsed) ? null : parsed;
539
+ };
384
540
  /**
385
- * Build runtime {@link TileDefinition}s from a Tiled tileset's per-tile data,
386
- * carrying over per-tile properties, animation frames, and collision shapes
387
- * (the tile's `objectgroup`). Tiles whose `id` is out of range, and animation
388
- * frames referencing out-of-range local ids, are skipped so the runtime tileset
389
- * never receives malformed definitions.
390
- */
391
- function buildTileDefinitions(tiles, tileCount) {
392
- const defs = [];
393
- for (const tile of tiles) {
394
- if (tile.id < 0 || tile.id >= tileCount)
395
- continue;
396
- const properties = tile.properties ? convertProperties(tile.properties) : undefined;
397
- const hasProps = properties !== undefined && Object.keys(properties).length > 0;
398
- let animation;
399
- if (tile.animation && tile.animation.length > 0) {
400
- const frames = tile.animation
401
- .filter(frame => frame.tileid >= 0 && frame.tileid < tileCount)
402
- .map(frame => ({ localTileId: frame.tileid, duration: frame.duration }));
403
- if (frames.length > 0)
404
- animation = frames;
405
- }
406
- let collision;
407
- if (tile.objectgroup && tile.objectgroup.objects.length > 0) {
408
- const shapes = tile.objectgroup.objects
409
- .map(obj => convertCollisionObject(obj))
410
- .filter((obj) => obj !== null);
411
- if (shapes.length > 0)
412
- collision = shapes;
413
- }
414
- if (!hasProps && animation === undefined && collision === undefined)
415
- continue;
416
- defs.push({
417
- localTileId: tile.id,
418
- ...(hasProps && { properties }),
419
- ...(animation !== undefined && { animation }),
420
- ...(collision !== undefined && { collision }),
421
- });
422
- }
423
- return defs;
424
- }
541
+ * Build runtime {@link TileDefinition}s from a Tiled tileset's per-tile data,
542
+ * carrying over per-tile properties, animation frames, and collision shapes
543
+ * (the tile's `objectgroup`). Tiles whose `id` is out of range, and animation
544
+ * frames referencing out-of-range local ids, are skipped so the runtime tileset
545
+ * never receives malformed definitions.
546
+ */
547
+ const buildTileDefinitions = (tiles, tileCount) => {
548
+ const defs = [];
549
+ for (const tile of tiles) {
550
+ if (tile.id < 0 || tile.id >= tileCount) continue;
551
+ const properties = tile.properties ? convertProperties(tile.properties) : void 0;
552
+ const hasProps = properties !== void 0 && Object.keys(properties).length > 0;
553
+ let animation;
554
+ if (tile.animation && tile.animation.length > 0) {
555
+ const frames = tile.animation.filter((frame) => frame.tileid >= 0 && frame.tileid < tileCount).map((frame) => ({
556
+ localTileId: frame.tileid,
557
+ duration: frame.duration
558
+ }));
559
+ if (frames.length > 0) animation = frames;
560
+ }
561
+ let collision;
562
+ if (tile.objectgroup && tile.objectgroup.objects.length > 0) {
563
+ const shapes = tile.objectgroup.objects.map((obj) => convertCollisionObject(obj)).filter((obj) => obj !== null);
564
+ if (shapes.length > 0) collision = shapes;
565
+ }
566
+ if (!hasProps && animation === void 0 && collision === void 0) continue;
567
+ defs.push({
568
+ localTileId: tile.id,
569
+ ...hasProps && { properties },
570
+ ...animation !== void 0 && { animation },
571
+ ...collision !== void 0 && { collision }
572
+ });
573
+ }
574
+ return defs;
575
+ };
425
576
  /**
426
- * Convert a raw `TiledObjectData` to a runtime `TileMapObject` for use as a
427
- * per-tile collision shape. Text objects and tile-object (gid) references are
428
- * dropped returns `null`. Does not perform GID resolution; collision shapes
429
- * in tile objectgroups are almost exclusively plain geometry.
430
- */
431
- function convertCollisionObject(obj) {
432
- if (obj.text)
433
- return null; // text not representable as a collision shape
434
- if (obj.gid !== undefined)
435
- return null; // tile objects require GID resolution not available here
436
- const base = {
437
- id: obj.id,
438
- name: obj.name,
439
- type: obj.type,
440
- x: obj.x,
441
- y: obj.y,
442
- width: obj.width,
443
- height: obj.height,
444
- rotation: obj.rotation,
445
- visible: obj.visible,
446
- properties: convertProperties(obj.properties ?? []),
447
- };
448
- if (obj.point === true)
449
- return { ...base, kind: 'point' };
450
- if (obj.ellipse === true)
451
- return { ...base, kind: 'ellipse' };
452
- if (obj.polygon)
453
- return { ...base, kind: 'polygon', points: toPoints(obj.polygon) };
454
- if (obj.polyline)
455
- return { ...base, kind: 'polyline', points: toPoints(obj.polyline) };
456
- return { ...base, kind: 'rectangle' };
457
- }
577
+ * Convert a raw `TiledObjectData` to a runtime `TileMapObject` for use as a
578
+ * per-tile collision shape. Text objects and tile-object (gid) references are
579
+ * dropped - returns `null`. Does not perform GID resolution; collision shapes
580
+ * in tile objectgroups are almost exclusively plain geometry.
581
+ */
582
+ const convertCollisionObject = (obj) => {
583
+ if (obj.text) return null;
584
+ if (obj.gid !== void 0) return null;
585
+ const base = {
586
+ id: obj.id,
587
+ name: obj.name,
588
+ type: obj.type,
589
+ x: obj.x,
590
+ y: obj.y,
591
+ width: obj.width,
592
+ height: obj.height,
593
+ rotation: obj.rotation,
594
+ visible: obj.visible,
595
+ properties: convertProperties(obj.properties ?? [])
596
+ };
597
+ if (obj.point === true) return {
598
+ ...base,
599
+ kind: "point"
600
+ };
601
+ if (obj.ellipse === true) return {
602
+ ...base,
603
+ kind: "ellipse"
604
+ };
605
+ if (obj.polygon) return {
606
+ ...base,
607
+ kind: "polygon",
608
+ points: toPoints(obj.polygon)
609
+ };
610
+ if (obj.polyline) return {
611
+ ...base,
612
+ kind: "polyline",
613
+ points: toPoints(obj.polyline)
614
+ };
615
+ return {
616
+ ...base,
617
+ kind: "rectangle"
618
+ };
619
+ };
458
620
  /**
459
- * Project Tiled custom properties to the generic flat property bag.
460
- * `object`-typed properties become a {@link TilePropertyKind.ObjectRef}
461
- * (the wire value is already the referenced object's numeric id Tiled has
462
- * no LDtk-style navigation fields, so those stay `undefined`). `class`-typed
463
- * properties recursively convert their nested member bag into a
464
- * {@link TileProperties}.
465
- */
466
- function convertProperties(properties) {
467
- if (properties.length === 0)
468
- return Object.freeze({});
469
- const out = {};
470
- for (const property of properties) {
471
- const value = convertPropertyValue(property);
472
- if (value !== undefined) {
473
- out[property.name] = value;
474
- }
475
- }
476
- return Object.freeze(out);
477
- }
621
+ * Project Tiled custom properties to the generic flat property bag.
622
+ * `object`-typed properties become a {@link TilePropertyKind.ObjectRef}
623
+ * (the wire value is already the referenced object's numeric id - Tiled has
624
+ * no LDtk-style navigation fields, so those stay `undefined`). `class`-typed
625
+ * properties recursively convert their nested member bag into a
626
+ * {@link TileProperties}.
627
+ */
628
+ const convertProperties = (properties) => {
629
+ if (properties.length === 0) return Object.freeze({});
630
+ const out = {};
631
+ for (const property of properties) {
632
+ const value = convertPropertyValue(property);
633
+ if (value !== void 0) out[property.name] = value;
634
+ }
635
+ return Object.freeze(out);
636
+ };
478
637
  /** Convert one {@link TiledPropertyData} to its canonical {@link TilePropertyValue}. */
479
- function convertPropertyValue(property) {
480
- switch (property.type) {
481
- case 'string':
482
- case 'int':
483
- case 'float':
484
- case 'bool':
485
- case 'color':
486
- case 'file':
487
- return property.value;
488
- case 'object':
489
- return { kind: TilePropertyKind.ObjectRef, id: property.value };
490
- case 'class':
491
- return convertClassPropertyValue(property.value, property.propertytype);
492
- default: {
493
- // Exhaustiveness check: if a new TiledPropertyType is ever added,
494
- // `property.type` will fail to narrow to `never` here and tsc will error.
495
- property.type;
496
- throw new Error(`convertProperties: unrecognised Tiled property type "${property.type}".`);
497
- }
498
- }
499
- }
638
+ const convertPropertyValue = (property) => {
639
+ switch (property.type) {
640
+ case "string":
641
+ case "int":
642
+ case "float":
643
+ case "bool":
644
+ case "color":
645
+ case "file": return property.value;
646
+ case "object": return {
647
+ kind: TilePropertyKind.ObjectRef,
648
+ id: property.value
649
+ };
650
+ case "class": return convertClassPropertyValue(property.value, property.propertytype);
651
+ default:
652
+ property.type;
653
+ throw new Error(`convertProperties: unrecognised Tiled property type "${property.type}".`);
654
+ }
655
+ };
500
656
  /**
501
- * Reserved key under which a `class`-typed property's Tiled custom-class name
502
- * ({@link TiledPropertyData.propertytype}, e.g. `"Stats"`) is stored inside the
503
- * converted nested {@link TileProperties} bag, mirroring the `ldtkUid`/
504
- * `ldtkWorldIid`-style reserved-metadata-key convention used by the LDtk
505
- * adapter (`packages/exojs-ldtk/src/ldtkToTileMap.ts`). Only set on the
506
- * top-level converted bag for a class-typed property that has a
507
- * `propertytype` nested-class members carry no `propertytype` of their own
508
- * in Tiled's data model, so recursive calls never set it.
509
- */
510
- const tiledClassNameProperty = 'tiledClassName';
657
+ * Reserved key under which a `class`-typed property's Tiled custom-class name
658
+ * ({@link TiledPropertyData.propertytype}, e.g. `"Stats"`) is stored inside the
659
+ * converted nested {@link TileProperties} bag, mirroring the `ldtkUid`/
660
+ * `ldtkWorldIid`-style reserved-metadata-key convention used by the LDtk
661
+ * adapter (`packages/exojs-ldtk/src/ldtkToTileMap.ts`). Only set on the
662
+ * top-level converted bag for a class-typed property that has a
663
+ * `propertytype` - nested-class members carry no `propertytype` of their own
664
+ * in Tiled's data model, so recursive calls never set it.
665
+ */
666
+ const tiledClassNameProperty = "tiledClassName";
511
667
  /**
512
- * Recursively convert a `class`-typed property's nested member bag into a
513
- * {@link TileProperties}. Scalar members pass through; nested-class members
514
- * recurse (Tiled classes may nest arbitrarily deep). When `propertytype` is
515
- * given (the Tiled custom-class name), it is tagged onto the bag under the
516
- * reserved {@link tiledClassNameProperty} key, applied last so it can never
517
- * be clobbered by a same-named member.
518
- */
519
- function convertClassPropertyValue(value, propertytype) {
520
- const out = {};
521
- for (const [name, member] of Object.entries(value)) {
522
- out[name] =
523
- typeof member === 'string' || typeof member === 'number' || typeof member === 'boolean'
524
- ? member
525
- : convertClassPropertyValue(member);
526
- }
527
- if (propertytype !== undefined) {
528
- out[tiledClassNameProperty] = propertytype;
529
- }
530
- return Object.freeze(out);
531
- }
532
- function sortAndValidateTilesetRanges(tilesets, source) {
533
- const sorted = [...tilesets].sort((a, b) => a.firstGid - b.firstGid);
534
- for (let i = 1; i < sorted.length; i++) {
535
- const previous = sorted[i - 1];
536
- const current = sorted[i];
537
- if (previous === undefined || current === undefined)
538
- continue;
539
- if (current.firstGid === previous.firstGid) {
540
- throw new TiledFormatError(source, 'tilesets', `duplicate firstgid ${current.firstGid} (tilesets "${previous.name}" and "${current.name}")`);
541
- }
542
- if (current.firstGid <= previous.lastGid) {
543
- throw new TiledFormatError(source, 'tilesets', `tileset "${current.name}" (firstgid ${current.firstGid}) overlaps tileset "${previous.name}" (firstgid ${previous.firstGid}, last gid ${previous.lastGid})`);
544
- }
545
- }
546
- return sorted;
547
- }
548
- function walkLayers(layers, path, visit) {
549
- for (let i = 0; i < layers.length; i++) {
550
- const layer = layers[i];
551
- if (layer === undefined)
552
- continue;
553
- const layerPath = `${path}[${i}]`;
554
- visit(layer, layerPath);
555
- if (layer instanceof TiledGroupLayer) {
556
- walkLayers(layer.layers, `${layerPath}.layers`, visit);
557
- }
558
- }
559
- }
560
- function checkGidArray(gids, map, source, path) {
561
- for (let i = 0; i < gids.length; i++) {
562
- const gid = gids[i];
563
- if (gid === undefined)
564
- continue;
565
- if (gid !== 0 && map.findTilesetForGid(gid) === undefined) {
566
- throw new TiledFormatError(source, `${path}[${i}]`, `gid ${gid} (masked: ${maskTiledGid(gid)}) is not covered by any tileset`);
567
- }
568
- }
569
- }
570
- function checkGidCoverage(map, source) {
571
- walkLayers(map.layers, 'layers', (layer, layerPath) => {
572
- if (layer instanceof TiledTileLayer) {
573
- if (layer.data !== undefined) {
574
- checkGidArray(layer.data, map, source, `${layerPath}.data`);
575
- }
576
- if (layer.chunks !== undefined) {
577
- for (let c = 0; c < layer.chunks.length; c++) {
578
- const chunk = layer.chunks[c];
579
- if (chunk === undefined)
580
- continue;
581
- checkGidArray(chunk.data, map, source, `${layerPath}.chunks[${c}].data`);
582
- }
583
- }
584
- }
585
- else if (layer instanceof TiledObjectLayer) {
586
- for (let o = 0; o < layer.objects.length; o++) {
587
- const object = layer.objects[o];
588
- if (object === undefined)
589
- continue;
590
- const gid = object.gid;
591
- // gid 0 (after masking the flip bits) is the empty-cell sentinel, the
592
- // same as in tile-layer data — accept it as "no tile" instead of
593
- // rejecting it as uncovered.
594
- if (gid !== undefined && maskTiledGid(gid) !== 0 && map.findTilesetForGid(gid) === undefined) {
595
- throw new TiledFormatError(source, `${layerPath}.objects[${o}].gid`, `gid ${gid} (masked: ${maskTiledGid(gid)}) is not covered by any tileset`);
596
- }
597
- }
598
- }
599
- });
600
- }
668
+ * Recursively convert a `class`-typed property's nested member bag into a
669
+ * {@link TileProperties}. Scalar members pass through; nested-class members
670
+ * recurse (Tiled classes may nest arbitrarily deep). When `propertytype` is
671
+ * given (the Tiled custom-class name), it is tagged onto the bag under the
672
+ * reserved {@link tiledClassNameProperty} key, applied last so it can never
673
+ * be clobbered by a same-named member.
674
+ */
675
+ const convertClassPropertyValue = (value, propertytype) => {
676
+ const out = {};
677
+ for (const [name, member] of Object.entries(value)) out[name] = typeof member === "string" || typeof member === "number" || typeof member === "boolean" ? member : convertClassPropertyValue(member);
678
+ if (propertytype !== void 0) out[tiledClassNameProperty] = propertytype;
679
+ return Object.freeze(out);
680
+ };
681
+ const sortAndValidateTilesetRanges = (tilesets, source) => {
682
+ const sorted = [...tilesets].sort((a, b) => a.firstGid - b.firstGid);
683
+ for (let i = 1; i < sorted.length; i++) {
684
+ const previous = sorted[i - 1];
685
+ const current = sorted[i];
686
+ if (previous === void 0 || current === void 0) continue;
687
+ if (current.firstGid === previous.firstGid) throw new TiledFormatError(source, "tilesets", `duplicate firstgid ${current.firstGid} (tilesets "${previous.name}" and "${current.name}")`);
688
+ if (current.firstGid <= previous.lastGid) throw new TiledFormatError(source, "tilesets", `tileset "${current.name}" (firstgid ${current.firstGid}) overlaps tileset "${previous.name}" (firstgid ${previous.firstGid}, last gid ${previous.lastGid})`);
689
+ }
690
+ return sorted;
691
+ };
692
+ const walkLayers = (layers, path, visit) => {
693
+ for (let i = 0; i < layers.length; i++) {
694
+ const layer = layers[i];
695
+ if (layer === void 0) continue;
696
+ const layerPath = `${path}[${i}]`;
697
+ visit(layer, layerPath);
698
+ if (layer instanceof TiledGroupLayer) walkLayers(layer.layers, `${layerPath}.layers`, visit);
699
+ }
700
+ };
701
+ const checkGidArray = (gids, map, source, path) => {
702
+ for (let i = 0; i < gids.length; i++) {
703
+ const gid = gids[i];
704
+ if (gid === void 0) continue;
705
+ if (gid !== 0 && map.findTilesetForGid(gid) === void 0) throw new TiledFormatError(source, `${path}[${i}]`, `gid ${gid} (masked: ${maskTiledGid(gid)}) is not covered by any tileset`);
706
+ }
707
+ };
708
+ const checkGidCoverage = (map, source) => {
709
+ walkLayers(map.layers, "layers", (layer, layerPath) => {
710
+ if (layer instanceof TiledTileLayer) {
711
+ if (layer.data !== void 0) checkGidArray(layer.data, map, source, `${layerPath}.data`);
712
+ if (layer.chunks !== void 0) for (let c = 0; c < layer.chunks.length; c++) {
713
+ const chunk = layer.chunks[c];
714
+ if (chunk === void 0) continue;
715
+ checkGidArray(chunk.data, map, source, `${layerPath}.chunks[${c}].data`);
716
+ }
717
+ } else if (layer instanceof TiledObjectLayer) for (let o = 0; o < layer.objects.length; o++) {
718
+ const object = layer.objects[o];
719
+ if (object === void 0) continue;
720
+ const gid = object.gid;
721
+ if (gid !== void 0 && maskTiledGid(gid) !== 0 && map.findTilesetForGid(gid) === void 0) throw new TiledFormatError(source, `${layerPath}.objects[${o}].gid`, `gid ${gid} (masked: ${maskTiledGid(gid)}) is not covered by any tileset`);
722
+ }
723
+ });
724
+ };
601
725
 
726
+ //#endregion
602
727
  export { TiledMap };
603
- //# sourceMappingURL=TiledMap.js.map
728
+ //# sourceMappingURL=TiledMap.js.map