@codexo/exojs-tilemap 0.15.2 → 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.
- package/README.md +33 -26
- package/dist/esm/ChunkSource.d.ts +33 -0
- package/dist/esm/ChunkSource.d.ts.map +1 -0
- package/dist/esm/ChunkStreamer.d.ts +98 -0
- package/dist/esm/ChunkStreamer.d.ts.map +1 -0
- package/dist/esm/ChunkStreamer.js +201 -0
- package/dist/esm/ChunkStreamer.js.map +1 -0
- package/dist/esm/ImageLayer.d.ts +11 -3
- package/dist/esm/ImageLayer.d.ts.map +1 -0
- package/dist/esm/ImageLayer.js +71 -64
- package/dist/esm/ImageLayer.js.map +1 -1
- package/dist/esm/ImageLayerNode.d.ts +89 -0
- package/dist/esm/ImageLayerNode.d.ts.map +1 -0
- package/dist/esm/ImageLayerNode.js +205 -0
- package/dist/esm/ImageLayerNode.js.map +1 -0
- package/dist/esm/MapObject.d.ts +70 -0
- package/dist/esm/MapObject.d.ts.map +1 -0
- package/dist/esm/MapObject.js +34 -0
- package/dist/esm/MapObject.js.map +1 -0
- package/dist/esm/MapObjectSpawner.d.ts +133 -0
- package/dist/esm/MapObjectSpawner.d.ts.map +1 -0
- package/dist/esm/MapObjectSpawner.js +147 -0
- package/dist/esm/MapObjectSpawner.js.map +1 -0
- package/dist/esm/MapSpawnSession.d.ts +53 -0
- package/dist/esm/MapSpawnSession.d.ts.map +1 -0
- package/dist/esm/MapSpawnSession.js +88 -0
- package/dist/esm/MapSpawnSession.js.map +1 -0
- package/dist/esm/MapWorld.d.ts +134 -0
- package/dist/esm/MapWorld.d.ts.map +1 -0
- package/dist/esm/MapWorld.js +150 -0
- package/dist/esm/MapWorld.js.map +1 -0
- package/dist/esm/MapWorldRuntime.d.ts +182 -0
- package/dist/esm/MapWorldRuntime.d.ts.map +1 -0
- package/dist/esm/MapWorldRuntime.js +261 -0
- package/dist/esm/MapWorldRuntime.js.map +1 -0
- package/dist/esm/ObjectLayer.d.ts +93 -7
- package/dist/esm/ObjectLayer.d.ts.map +1 -0
- package/dist/esm/ObjectLayer.js +184 -167
- package/dist/esm/ObjectLayer.js.map +1 -1
- package/dist/esm/SampledChunkSource.d.ts +43 -0
- package/dist/esm/SampledChunkSource.d.ts.map +1 -0
- package/dist/esm/SampledChunkSource.js +48 -0
- package/dist/esm/SampledChunkSource.js.map +1 -0
- package/dist/esm/TileAnimator.d.ts +12 -2
- package/dist/esm/TileAnimator.d.ts.map +1 -0
- package/dist/esm/TileAnimator.js +154 -165
- package/dist/esm/TileAnimator.js.map +1 -1
- package/dist/esm/TileChunk.d.ts +30 -5
- package/dist/esm/TileChunk.d.ts.map +1 -0
- package/dist/esm/TileChunk.js +190 -188
- package/dist/esm/TileChunk.js.map +1 -1
- package/dist/esm/TileChunkNode.d.ts +18 -3
- package/dist/esm/TileChunkNode.d.ts.map +1 -0
- package/dist/esm/TileChunkNode.js +104 -85
- package/dist/esm/TileChunkNode.js.map +1 -1
- package/dist/esm/TileLayer.d.ts +104 -24
- package/dist/esm/TileLayer.d.ts.map +1 -0
- package/dist/esm/TileLayer.js +511 -469
- package/dist/esm/TileLayer.js.map +1 -1
- package/dist/esm/TileLayerNode.d.ts +52 -11
- package/dist/esm/TileLayerNode.d.ts.map +1 -0
- package/dist/esm/TileLayerNode.js +224 -166
- package/dist/esm/TileLayerNode.js.map +1 -1
- package/dist/esm/TileMap.d.ts +96 -31
- package/dist/esm/TileMap.d.ts.map +1 -0
- package/dist/esm/TileMap.js +401 -357
- package/dist/esm/TileMap.js.map +1 -1
- package/dist/esm/TileMapBand.d.ts +42 -29
- package/dist/esm/TileMapBand.d.ts.map +1 -0
- package/dist/esm/TileMapBand.js +151 -158
- package/dist/esm/TileMapBand.js.map +1 -1
- package/dist/esm/TileMapNode.d.ts +34 -18
- package/dist/esm/TileMapNode.d.ts.map +1 -0
- package/dist/esm/TileMapNode.js +122 -109
- package/dist/esm/TileMapNode.js.map +1 -1
- package/dist/esm/TileMapView.d.ts +108 -47
- package/dist/esm/TileMapView.d.ts.map +1 -0
- package/dist/esm/TileMapView.js +385 -338
- package/dist/esm/TileMapView.js.map +1 -1
- package/dist/esm/TileSet.d.ts +15 -6
- package/dist/esm/TileSet.d.ts.map +1 -0
- package/dist/esm/TileSet.js +161 -181
- package/dist/esm/TileSet.js.map +1 -1
- package/dist/esm/WangSet.d.ts +5 -4
- package/dist/esm/WangSet.d.ts.map +1 -0
- package/dist/esm/WangSet.js +70 -78
- package/dist/esm/WangSet.js.map +1 -1
- package/dist/esm/WorkerSampledChunkSource.d.ts +95 -0
- package/dist/esm/WorkerSampledChunkSource.d.ts.map +1 -0
- package/dist/esm/WorkerSampledChunkSource.js +117 -0
- package/dist/esm/WorkerSampledChunkSource.js.map +1 -0
- package/dist/esm/autoTile.d.ts +10 -5
- package/dist/esm/autoTile.d.ts.map +1 -0
- package/dist/esm/autoTile.js +164 -205
- package/dist/esm/autoTile.js.map +1 -1
- package/dist/esm/chunkGeometry.d.ts +8 -7
- package/dist/esm/chunkGeometry.d.ts.map +1 -0
- package/dist/esm/chunkGeometry.js +83 -94
- package/dist/esm/chunkGeometry.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +26 -15
- package/dist/esm/nodeBounds.d.ts +20 -0
- package/dist/esm/nodeBounds.d.ts.map +1 -0
- package/dist/esm/nodeBounds.js +40 -0
- package/dist/esm/nodeBounds.js.map +1 -0
- package/dist/esm/pixelSnap.d.ts +4 -3
- package/dist/esm/pixelSnap.d.ts.map +1 -0
- package/dist/esm/pixelSnap.js +18 -16
- package/dist/esm/pixelSnap.js.map +1 -1
- package/dist/esm/public.d.ts +23 -3
- package/dist/esm/public.d.ts.map +1 -0
- package/dist/esm/tileCollision.d.ts +190 -0
- package/dist/esm/tileCollision.d.ts.map +1 -0
- package/dist/esm/tileCollision.js +367 -0
- package/dist/esm/tileCollision.js.map +1 -0
- package/dist/esm/tileWord.d.ts +12 -0
- package/dist/esm/tileWord.d.ts.map +1 -0
- package/dist/esm/tileWord.js +16 -0
- package/dist/esm/tileWord.js.map +1 -0
- package/dist/esm/tilemapExtension.d.ts +4 -3
- package/dist/esm/tilemapExtension.d.ts.map +1 -0
- package/dist/esm/tilemapExtension.js +47 -47
- package/dist/esm/tilemapExtension.js.map +1 -1
- package/dist/esm/tilemapSerializers.d.ts +5 -4
- package/dist/esm/tilemapSerializers.d.ts.map +1 -0
- package/dist/esm/tilemapSerializers.js +48 -41
- package/dist/esm/tilemapSerializers.js.map +1 -1
- package/dist/esm/types.d.ts +34 -18
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +111 -120
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/webgl2/WebGl2TileChunkRenderer.d.ts +50 -2
- package/dist/esm/webgl2/WebGl2TileChunkRenderer.d.ts.map +1 -0
- package/dist/esm/webgl2/WebGl2TileChunkRenderer.js +366 -332
- package/dist/esm/webgl2/WebGl2TileChunkRenderer.js.map +1 -1
- package/dist/esm/webgl2/shaders/tile-chunk.frag.js +6 -0
- package/dist/esm/webgl2/shaders/tile-chunk.frag.js.map +1 -0
- package/dist/esm/webgl2/shaders/tile-chunk.vert.js +6 -0
- package/dist/esm/webgl2/shaders/tile-chunk.vert.js.map +1 -0
- package/dist/esm/webgpu/WebGpuTileChunkRenderer.d.ts +70 -4
- package/dist/esm/webgpu/WebGpuTileChunkRenderer.d.ts.map +1 -0
- package/dist/esm/webgpu/WebGpuTileChunkRenderer.js +496 -383
- package/dist/esm/webgpu/WebGpuTileChunkRenderer.js.map +1 -1
- package/dist/esm/webgpu/shaders/tile-chunk.wgsl.js +6 -0
- package/dist/esm/webgpu/shaders/tile-chunk.wgsl.js.map +1 -0
- package/package.json +8 -15
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/register.d.ts +0 -1
- package/dist/esm/register.js +0 -25
- package/dist/esm/register.js.map +0 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { ObjectPoint, TileMapObject } from './ObjectLayer';
|
|
2
|
+
import { ObjectKind } from './ObjectLayer';
|
|
3
|
+
import type { TileLayer } from './TileLayer';
|
|
4
|
+
/**
|
|
5
|
+
* The geometry kinds a {@link TileCollisionShape} can carry. Tile (GID) and
|
|
6
|
+
* text objects describe no collision area and are never emitted, so they are
|
|
7
|
+
* excluded from the union.
|
|
8
|
+
* @advanced
|
|
9
|
+
*/
|
|
10
|
+
export type TileCollisionShapeKind = typeof ObjectKind.Rectangle | typeof ObjectKind.Ellipse | typeof ObjectKind.Polygon | typeof ObjectKind.Polyline | typeof ObjectKind.Point;
|
|
11
|
+
/**
|
|
12
|
+
* A half-open tile-coordinate rectangle covering
|
|
13
|
+
* `[x, x + width)` × `[y, y + height)`.
|
|
14
|
+
*
|
|
15
|
+
* Used to scope {@link buildTileCollisionGeometry} to part of a layer - a
|
|
16
|
+
* streamed chunk, the tiles around the player, a hand-picked room - instead of
|
|
17
|
+
* always rebuilding the whole map.
|
|
18
|
+
* @advanced
|
|
19
|
+
*/
|
|
20
|
+
export interface TileRegion {
|
|
21
|
+
/** Leftmost tile column (inclusive). */
|
|
22
|
+
readonly x: number;
|
|
23
|
+
/** Topmost tile row (inclusive). */
|
|
24
|
+
readonly y: number;
|
|
25
|
+
/** Width in tiles. */
|
|
26
|
+
readonly width: number;
|
|
27
|
+
/** Height in tiles. */
|
|
28
|
+
readonly height: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* An axis-aligned collision rectangle in tile-layer pixel space (+Y down),
|
|
32
|
+
* covering one or more whole tile cells.
|
|
33
|
+
*
|
|
34
|
+
* Rectangles are synthesized geometry, not source objects: a merged run has no
|
|
35
|
+
* single originating object, so it carries no object `id`/`name`/`properties` -
|
|
36
|
+
* only the `type` string shared by every shape merged into it. Anything that
|
|
37
|
+
* does not exactly cover whole cells stays a {@link TileCollisionShape}, which
|
|
38
|
+
* does keep its source object.
|
|
39
|
+
* @advanced
|
|
40
|
+
*/
|
|
41
|
+
export interface TileCollisionRect {
|
|
42
|
+
/** Left edge in layer pixel space. */
|
|
43
|
+
readonly x: number;
|
|
44
|
+
/** Top edge in layer pixel space. */
|
|
45
|
+
readonly y: number;
|
|
46
|
+
/** Width in pixels (a whole number of tile cells). */
|
|
47
|
+
readonly width: number;
|
|
48
|
+
/** Height in pixels (a whole number of tile cells). */
|
|
49
|
+
readonly height: number;
|
|
50
|
+
/** Class/type string shared by every source shape merged into this rectangle. */
|
|
51
|
+
readonly type: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A per-tile collision shape placed in tile-layer pixel space (+Y down).
|
|
55
|
+
*
|
|
56
|
+
* Emitted for every collision shape that does not exactly cover a whole tile
|
|
57
|
+
* cell: partial boxes, ellipses, polygons, polylines, points, and rotated
|
|
58
|
+
* geometry. Shapes are never merged with one another.
|
|
59
|
+
* @advanced
|
|
60
|
+
*/
|
|
61
|
+
export interface TileCollisionShape {
|
|
62
|
+
/** Geometry discriminant. */
|
|
63
|
+
readonly kind: TileCollisionShapeKind;
|
|
64
|
+
/** X of the shape origin in layer pixel space. */
|
|
65
|
+
readonly x: number;
|
|
66
|
+
/** Y of the shape origin in layer pixel space. */
|
|
67
|
+
readonly y: number;
|
|
68
|
+
/** Bounding width in px (`0` for polygons, polylines and points). */
|
|
69
|
+
readonly width: number;
|
|
70
|
+
/** Bounding height in px (`0` for polygons, polylines and points). */
|
|
71
|
+
readonly height: number;
|
|
72
|
+
/**
|
|
73
|
+
* Rotation in degrees, clockwise, about `(x, y)`, normalised to `[0, 360)`.
|
|
74
|
+
* Always `0` for polygons and polylines - their rotation is baked into
|
|
75
|
+
* {@link points}.
|
|
76
|
+
*/
|
|
77
|
+
readonly rotation: number;
|
|
78
|
+
/** Polygon/polyline vertices relative to `(x, y)`; absent for other kinds. */
|
|
79
|
+
readonly points?: readonly ObjectPoint[];
|
|
80
|
+
/** Tile column the shape came from. */
|
|
81
|
+
readonly tx: number;
|
|
82
|
+
/** Tile row the shape came from. */
|
|
83
|
+
readonly ty: number;
|
|
84
|
+
/** The tile-local source object, for `name`/`type`/`properties` lookups. */
|
|
85
|
+
readonly source: TileMapObject;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Collision geometry extracted from a tile layer: whole-cell boxes merged into
|
|
89
|
+
* as few {@link TileCollisionRect}s as a greedy pass can manage, plus every
|
|
90
|
+
* other shape passed through individually.
|
|
91
|
+
* @advanced
|
|
92
|
+
*/
|
|
93
|
+
export interface TileCollisionGeometry {
|
|
94
|
+
/** Merged whole-cell rectangles, in row-major order. */
|
|
95
|
+
readonly rects: readonly TileCollisionRect[];
|
|
96
|
+
/** Non-whole-cell shapes, in the order their tiles were walked. */
|
|
97
|
+
readonly shapes: readonly TileCollisionShape[];
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Per-cell collision classification for a tile layer, independent of the tiles
|
|
101
|
+
* the layer renders.
|
|
102
|
+
*
|
|
103
|
+
* Returns the class/type string for the cell, or `null` when the cell carries
|
|
104
|
+
* no collision. The string is opaque: it is the merge key for adjacent cells
|
|
105
|
+
* and reaches the consumer unchanged, exactly like the `type` of a tile
|
|
106
|
+
* collision object.
|
|
107
|
+
*
|
|
108
|
+
* A source is sampled while geometry is being built and is expected to answer
|
|
109
|
+
* identically for as long as the geometry is used. Changing what it returns
|
|
110
|
+
* does not invalidate geometry that was already built; rebuild instead.
|
|
111
|
+
* @advanced
|
|
112
|
+
*/
|
|
113
|
+
export type TileCellSource = (tx: number, ty: number) => string | null;
|
|
114
|
+
/**
|
|
115
|
+
* Options for {@link buildTileCollisionGeometry}.
|
|
116
|
+
* @advanced
|
|
117
|
+
*/
|
|
118
|
+
export interface TileCollisionOptions {
|
|
119
|
+
/**
|
|
120
|
+
* Tile-coordinate region to walk. Defaults to the bounding box of the
|
|
121
|
+
* layer's currently loaded chunks, so an unbounded (streamed) layer works
|
|
122
|
+
* without the caller computing anything - or, with {@link cells} on a
|
|
123
|
+
* bounded layer, to the layer's full tile extent.
|
|
124
|
+
*/
|
|
125
|
+
readonly region?: TileRegion;
|
|
126
|
+
/**
|
|
127
|
+
* Second occupancy source, consulted once per cell of the walked region in
|
|
128
|
+
* addition to the tiles' own collision shapes. Lets collision authored per
|
|
129
|
+
* cell rather than per tile reach the same geometry, without the layer
|
|
130
|
+
* having to render a tile there.
|
|
131
|
+
*/
|
|
132
|
+
readonly cells?: TileCellSource | undefined;
|
|
133
|
+
/**
|
|
134
|
+
* Merge adjacent whole-cell boxes that share a `type` into larger
|
|
135
|
+
* rectangles. Default `true`. With `false`, every whole-cell box becomes its
|
|
136
|
+
* own single-cell rectangle.
|
|
137
|
+
*/
|
|
138
|
+
readonly merge?: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Drop source shapes before any geometry is built (e.g. by a custom
|
|
141
|
+
* property). Return `true` to keep a shape. Default keeps all of them.
|
|
142
|
+
*/
|
|
143
|
+
readonly accept?: (object: TileMapObject, tx: number, ty: number) => boolean;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Extract collision geometry from a tile layer's per-tile
|
|
147
|
+
* {@link import('./types').TileDefinition.collision} shapes.
|
|
148
|
+
*
|
|
149
|
+
* Every placed tile in `region` is resolved back to its tileset definition, and
|
|
150
|
+
* each collision shape is transformed from tile-local pixel space into layer
|
|
151
|
+
* pixel space (+Y down) - applying the tile's flip/rotation transform, the
|
|
152
|
+
* tileset draw offset, and the layer's pixel offset, so the geometry lands
|
|
153
|
+
* exactly where the tile is drawn.
|
|
154
|
+
*
|
|
155
|
+
* Shapes that end up covering exactly one whole tile cell are collected into an
|
|
156
|
+
* occupancy grid and merged into as few {@link TileCollisionRect}s as a greedy
|
|
157
|
+
* pass manages - a 200×200 solid region becomes one rectangle instead of 40 000.
|
|
158
|
+
* Cells only merge with neighbours whose source object carries the same `type`
|
|
159
|
+
* string, so a `water` run never fuses into an adjacent `solid` run. Everything
|
|
160
|
+
* else (partial boxes, ellipses, polygons, polylines, points, rotated geometry)
|
|
161
|
+
* passes through individually as a {@link TileCollisionShape}.
|
|
162
|
+
*
|
|
163
|
+
* A {@link TileCollisionOptions.cells} source feeds the same occupancy grid
|
|
164
|
+
* from outside the tiles, for collision authored per cell rather than per tile.
|
|
165
|
+
* It is consulted first, so a whole-cell tile box landing on a cell the source
|
|
166
|
+
* already claimed passes through as a shape rather than overwriting it. With a
|
|
167
|
+
* cell source and no explicit region, a bounded layer walks its full tile
|
|
168
|
+
* extent - a layer that renders nothing at all still produces geometry.
|
|
169
|
+
*
|
|
170
|
+
* This package has no physics dependency and builds no bodies: the result is
|
|
171
|
+
* plain geometry. Turning it into colliders is a short loop in application code.
|
|
172
|
+
*
|
|
173
|
+
* @param layer - The layer to walk.
|
|
174
|
+
* @param options - Region scoping, merge toggle, a source-shape filter, and an
|
|
175
|
+
* optional per-cell occupancy source.
|
|
176
|
+
* @returns Merged rectangles plus pass-through shapes; both empty when the
|
|
177
|
+
* region holds no tile carrying collision data.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```ts
|
|
181
|
+
* const { rects, shapes } = buildTileCollisionGeometry(layer);
|
|
182
|
+
*
|
|
183
|
+
* for (const rect of rects) {
|
|
184
|
+
* world.add(makeStaticBox(rect.x, rect.y, rect.width, rect.height));
|
|
185
|
+
* }
|
|
186
|
+
* ```
|
|
187
|
+
* @advanced
|
|
188
|
+
*/
|
|
189
|
+
export declare const buildTileCollisionGeometry: (layer: TileLayer, options?: TileCollisionOptions) => TileCollisionGeometry;
|
|
190
|
+
//# sourceMappingURL=tileCollision.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tileCollision.d.ts","sourceRoot":"","sources":["../../src/tileCollision.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { ObjectKind } from "./ObjectLayer.js";
|
|
2
|
+
|
|
3
|
+
//#region src/tileCollision.ts
|
|
4
|
+
const DEGREES_TO_RADIANS = Math.PI / 180;
|
|
5
|
+
/** Normalise degrees into `[0, 360)`. */
|
|
6
|
+
const normaliseDegrees = (degrees) => {
|
|
7
|
+
const wrapped = degrees % 360;
|
|
8
|
+
return wrapped < 0 ? wrapped + 360 : wrapped;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Cosine of an angle in degrees, exact on the quadrant multiples.
|
|
12
|
+
* `Math.cos(Math.PI / 2)` is `6.1e-17`, not `0`; tile transforms produce those
|
|
13
|
+
* quadrant angles constantly, and the whole-cell test below compares
|
|
14
|
+
* coordinates exactly, so the noise has to stay out of the arithmetic.
|
|
15
|
+
*/
|
|
16
|
+
const cosDegrees = (degrees) => {
|
|
17
|
+
const angle = normaliseDegrees(degrees);
|
|
18
|
+
if (angle === 0) return 1;
|
|
19
|
+
if (angle === 90 || angle === 270) return 0;
|
|
20
|
+
if (angle === 180) return -1;
|
|
21
|
+
return Math.cos(angle * DEGREES_TO_RADIANS);
|
|
22
|
+
};
|
|
23
|
+
/** Sine of an angle in degrees, exact on the quadrant multiples. */
|
|
24
|
+
const sinDegrees = (degrees) => {
|
|
25
|
+
const angle = normaliseDegrees(degrees);
|
|
26
|
+
if (angle === 0 || angle === 180) return 0;
|
|
27
|
+
if (angle === 90) return 1;
|
|
28
|
+
if (angle === 270) return -1;
|
|
29
|
+
return Math.sin(angle * DEGREES_TO_RADIANS);
|
|
30
|
+
};
|
|
31
|
+
/** `true` when the transform mirrors (an odd number of reflections). */
|
|
32
|
+
const isMirrored = (transform) => {
|
|
33
|
+
return ((transform.flipX ? 1 : 0) + (transform.flipY ? 1 : 0) + (transform.diagonal ? 1 : 0)) % 2 === 1;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Rotation in degrees contributed by a non-mirroring transform. Only the four
|
|
37
|
+
* even-parity transforms are rotations: identity, `flipX+flipY` (180°),
|
|
38
|
+
* `diagonal+flipX` (90°) and `diagonal+flipY` (270°).
|
|
39
|
+
*/
|
|
40
|
+
const transformRotation = (transform) => {
|
|
41
|
+
if (transform.diagonal) return transform.flipX ? 90 : 270;
|
|
42
|
+
return transform.flipX ? 180 : 0;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Map a point from tile-local pixel space into the transformed tile-local
|
|
46
|
+
* space of a placed tile. The diagonal flip (a transpose of the axes, which
|
|
47
|
+
* also swaps the box dimensions) is applied first, then the axis mirrors -
|
|
48
|
+
* the same order the renderer's orientation code implies.
|
|
49
|
+
*/
|
|
50
|
+
const mapLocalPoint = (px, py, boxWidth, boxHeight, transform) => {
|
|
51
|
+
let x = px;
|
|
52
|
+
let y = py;
|
|
53
|
+
let width = boxWidth;
|
|
54
|
+
let height = boxHeight;
|
|
55
|
+
if (transform.diagonal) {
|
|
56
|
+
const swapped = x;
|
|
57
|
+
x = y;
|
|
58
|
+
y = swapped;
|
|
59
|
+
width = boxHeight;
|
|
60
|
+
height = boxWidth;
|
|
61
|
+
}
|
|
62
|
+
if (transform.flipX) x = width - x;
|
|
63
|
+
if (transform.flipY) y = height - y;
|
|
64
|
+
return {
|
|
65
|
+
x,
|
|
66
|
+
y
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Layer-space position of a placed tile's top-left image corner. Mirrors the
|
|
71
|
+
* renderer's placement (see `chunkGeometry`): the tileset's draw offset shifts
|
|
72
|
+
* every tile, and a tile taller than the layer's cell is bottom-aligned within
|
|
73
|
+
* it, so collision geometry lands exactly where the tile is drawn.
|
|
74
|
+
*/
|
|
75
|
+
const tileAnchor = (layer, tileset, tx, ty) => ({
|
|
76
|
+
x: tx * layer.tileWidth + layer.offsetX + tileset.offsetX,
|
|
77
|
+
y: ty * layer.tileHeight + layer.offsetY + layer.tileHeight - tileset.tileHeight + tileset.offsetY
|
|
78
|
+
});
|
|
79
|
+
/**
|
|
80
|
+
* Place one tile-local collision object into layer pixel space, applying the
|
|
81
|
+
* tile's flip/rotation transform, the tileset draw offset and the layer offset.
|
|
82
|
+
* Returns `null` for kinds that carry no collision geometry.
|
|
83
|
+
*/
|
|
84
|
+
const placeShape = (object, layer, tileset, transform, tx, ty) => {
|
|
85
|
+
if (object.kind === ObjectKind.Tile || object.kind === ObjectKind.Text) return null;
|
|
86
|
+
const anchor = tileAnchor(layer, tileset, tx, ty);
|
|
87
|
+
const boxWidth = tileset.tileWidth;
|
|
88
|
+
const boxHeight = tileset.tileHeight;
|
|
89
|
+
const mirrored = isMirrored(transform);
|
|
90
|
+
const sourceCos = cosDegrees(object.rotation);
|
|
91
|
+
const sourceSin = sinDegrees(object.rotation);
|
|
92
|
+
if (object.kind === ObjectKind.Polygon || object.kind === ObjectKind.Polyline) {
|
|
93
|
+
const origin = mapLocalPoint(object.x, object.y, boxWidth, boxHeight, transform);
|
|
94
|
+
const originX = anchor.x + origin.x;
|
|
95
|
+
const originY = anchor.y + origin.y;
|
|
96
|
+
const points = object.points.map((point) => {
|
|
97
|
+
const localX = object.x + (sourceCos * point.x - sourceSin * point.y);
|
|
98
|
+
const localY = object.y + (sourceSin * point.x + sourceCos * point.y);
|
|
99
|
+
const mapped = mapLocalPoint(localX, localY, boxWidth, boxHeight, transform);
|
|
100
|
+
return {
|
|
101
|
+
x: anchor.x + mapped.x - originX,
|
|
102
|
+
y: anchor.y + mapped.y - originY
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
kind: object.kind,
|
|
107
|
+
x: originX,
|
|
108
|
+
y: originY,
|
|
109
|
+
width: 0,
|
|
110
|
+
height: 0,
|
|
111
|
+
rotation: 0,
|
|
112
|
+
points: mirrored ? points.reverse() : points
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (object.kind === ObjectKind.Point) {
|
|
116
|
+
const mapped = mapLocalPoint(object.x, object.y, boxWidth, boxHeight, transform);
|
|
117
|
+
return {
|
|
118
|
+
kind: ObjectKind.Point,
|
|
119
|
+
x: anchor.x + mapped.x,
|
|
120
|
+
y: anchor.y + mapped.y,
|
|
121
|
+
width: 0,
|
|
122
|
+
height: 0,
|
|
123
|
+
rotation: 0
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const halfWidth = object.width / 2;
|
|
127
|
+
const halfHeight = object.height / 2;
|
|
128
|
+
const centreLocalX = object.x + (sourceCos * halfWidth - sourceSin * halfHeight);
|
|
129
|
+
const centreLocalY = object.y + (sourceSin * halfWidth + sourceCos * halfHeight);
|
|
130
|
+
const centre = mapLocalPoint(centreLocalX, centreLocalY, boxWidth, boxHeight, transform);
|
|
131
|
+
const centreX = anchor.x + centre.x;
|
|
132
|
+
const centreY = anchor.y + centre.y;
|
|
133
|
+
const rotation = normaliseDegrees(mirrored ? -object.rotation : object.rotation + transformRotation(transform));
|
|
134
|
+
const swapAxes = mirrored && transform.diagonal;
|
|
135
|
+
const width = swapAxes ? object.height : object.width;
|
|
136
|
+
const height = swapAxes ? object.width : object.height;
|
|
137
|
+
if (rotation % 90 === 0) {
|
|
138
|
+
const axisWidth = rotation % 180 === 0 ? width : height;
|
|
139
|
+
const axisHeight = rotation % 180 === 0 ? height : width;
|
|
140
|
+
return {
|
|
141
|
+
kind: object.kind,
|
|
142
|
+
x: centreX - axisWidth / 2,
|
|
143
|
+
y: centreY - axisHeight / 2,
|
|
144
|
+
width: axisWidth,
|
|
145
|
+
height: axisHeight,
|
|
146
|
+
rotation: 0
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const cos = cosDegrees(rotation);
|
|
150
|
+
const sin = sinDegrees(rotation);
|
|
151
|
+
return {
|
|
152
|
+
kind: object.kind,
|
|
153
|
+
x: centreX - (cos * (width / 2) - sin * (height / 2)),
|
|
154
|
+
y: centreY - (sin * (width / 2) + cos * (height / 2)),
|
|
155
|
+
width,
|
|
156
|
+
height,
|
|
157
|
+
rotation
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
/** The tile-coordinate bounding box of the layer's loaded chunks, or `null`. */
|
|
161
|
+
const loadedTileRegion = (layer) => {
|
|
162
|
+
let minTx = Number.POSITIVE_INFINITY;
|
|
163
|
+
let minTy = Number.POSITIVE_INFINITY;
|
|
164
|
+
let maxTx = Number.NEGATIVE_INFINITY;
|
|
165
|
+
let maxTy = Number.NEGATIVE_INFINITY;
|
|
166
|
+
for (const chunk of layer.loadedChunks()) {
|
|
167
|
+
const startTx = chunk.cx * layer.chunkWidth;
|
|
168
|
+
const startTy = chunk.cy * layer.chunkHeight;
|
|
169
|
+
minTx = Math.min(minTx, startTx);
|
|
170
|
+
minTy = Math.min(minTy, startTy);
|
|
171
|
+
maxTx = Math.max(maxTx, startTx + chunk.width - 1);
|
|
172
|
+
maxTy = Math.max(maxTy, startTy + chunk.height - 1);
|
|
173
|
+
}
|
|
174
|
+
if (!Number.isFinite(minTx) || !Number.isFinite(minTy)) return null;
|
|
175
|
+
return {
|
|
176
|
+
x: minTx,
|
|
177
|
+
y: minTy,
|
|
178
|
+
width: maxTx - minTx + 1,
|
|
179
|
+
height: maxTy - minTy + 1
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Region a cell source spans on `layer`: the full tile extent of a bounded
|
|
184
|
+
* layer, `null` when the layer is unbounded and the caller must scope the
|
|
185
|
+
* walk itself. Chunk residency deliberately plays no part - the classification
|
|
186
|
+
* lives outside the layer, so loaded chunks cannot define its domain.
|
|
187
|
+
*/
|
|
188
|
+
const cellSourceRegion = (layer) => {
|
|
189
|
+
if (layer.width === void 0 || layer.height === void 0) return null;
|
|
190
|
+
return {
|
|
191
|
+
x: 0,
|
|
192
|
+
y: 0,
|
|
193
|
+
width: layer.width,
|
|
194
|
+
height: layer.height
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* The region to walk: the caller's, else the extent a cell source spans, else
|
|
199
|
+
* whatever is currently resident. `null` when there is nothing to walk.
|
|
200
|
+
*/
|
|
201
|
+
const resolveRegion = (layer, options) => {
|
|
202
|
+
if (options.region !== void 0) return options.region;
|
|
203
|
+
return (options.cells !== void 0 ? cellSourceRegion(layer) : null) ?? loadedTileRegion(layer);
|
|
204
|
+
};
|
|
205
|
+
/** Claim every cell of `region` the source classifies, into the occupancy grid. */
|
|
206
|
+
const claimSourceCells = (cells, region, source) => {
|
|
207
|
+
const endTx = region.x + region.width;
|
|
208
|
+
const endTy = region.y + region.height;
|
|
209
|
+
for (let ty = region.y; ty < endTy; ty++) for (let tx = region.x; tx < endTx; tx++) {
|
|
210
|
+
const type = source(tx, ty);
|
|
211
|
+
if (type !== null) cells.set(cellKey(tx, ty), type);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
/** Key of a claimed cell in the occupancy grid. */
|
|
215
|
+
const cellKey = (tx, ty) => `${tx},${ty}`;
|
|
216
|
+
/**
|
|
217
|
+
* `true` when a placed shape is an axis-aligned rectangle covering exactly the
|
|
218
|
+
* tile cell at `(cellX, cellY)` - the only geometry the merging pass handles.
|
|
219
|
+
*/
|
|
220
|
+
const coversWholeCell = (placed, layer, cellX, cellY) => placed.kind === ObjectKind.Rectangle && placed.rotation === 0 && placed.x === cellX && placed.y === cellY && placed.width === layer.tileWidth && placed.height === layer.tileHeight;
|
|
221
|
+
/** One single-cell rectangle per claimed cell, for `merge: false`. */
|
|
222
|
+
const unmergedCells = (cells, layer) => {
|
|
223
|
+
const rects = [];
|
|
224
|
+
for (const [key, type] of cells) {
|
|
225
|
+
const [tx, ty] = key.split(",").map(Number);
|
|
226
|
+
rects.push({
|
|
227
|
+
x: tx * layer.tileWidth + layer.offsetX,
|
|
228
|
+
y: ty * layer.tileHeight + layer.offsetY,
|
|
229
|
+
width: layer.tileWidth,
|
|
230
|
+
height: layer.tileHeight,
|
|
231
|
+
type
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return rects;
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* Greedy rectangle merging over the claimed cells: walk row-major, grow each
|
|
238
|
+
* unconsumed cell as far right as the type key allows, then as far down as a
|
|
239
|
+
* full row of that width allows, and consume the block. Deliberately the plain
|
|
240
|
+
* textbook pass - it is linear in the number of cells and always produces a
|
|
241
|
+
* valid, non-overlapping cover, which matters more here than minimality.
|
|
242
|
+
*/
|
|
243
|
+
const mergeCells = (cells, region, layer) => {
|
|
244
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
245
|
+
const rects = [];
|
|
246
|
+
const endTx = region.x + region.width;
|
|
247
|
+
const endTy = region.y + region.height;
|
|
248
|
+
for (let ty = region.y; ty < endTy; ty++) for (let tx = region.x; tx < endTx; tx++) {
|
|
249
|
+
const key = cells.get(cellKey(tx, ty));
|
|
250
|
+
if (key === void 0 || consumed.has(cellKey(tx, ty))) continue;
|
|
251
|
+
let width = 1;
|
|
252
|
+
while (tx + width < endTx && cells.get(cellKey(tx + width, ty)) === key && !consumed.has(cellKey(tx + width, ty))) width++;
|
|
253
|
+
let height = 1;
|
|
254
|
+
growDown: while (ty + height < endTy) {
|
|
255
|
+
for (let column = 0; column < width; column++) {
|
|
256
|
+
const candidate = cellKey(tx + column, ty + height);
|
|
257
|
+
if (cells.get(candidate) !== key || consumed.has(candidate)) break growDown;
|
|
258
|
+
}
|
|
259
|
+
height++;
|
|
260
|
+
}
|
|
261
|
+
for (let row = 0; row < height; row++) for (let column = 0; column < width; column++) consumed.add(cellKey(tx + column, ty + row));
|
|
262
|
+
rects.push({
|
|
263
|
+
x: tx * layer.tileWidth + layer.offsetX,
|
|
264
|
+
y: ty * layer.tileHeight + layer.offsetY,
|
|
265
|
+
width: width * layer.tileWidth,
|
|
266
|
+
height: height * layer.tileHeight,
|
|
267
|
+
type: key
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return rects;
|
|
271
|
+
};
|
|
272
|
+
/**
|
|
273
|
+
* Extract collision geometry from a tile layer's per-tile
|
|
274
|
+
* {@link import('./types').TileDefinition.collision} shapes.
|
|
275
|
+
*
|
|
276
|
+
* Every placed tile in `region` is resolved back to its tileset definition, and
|
|
277
|
+
* each collision shape is transformed from tile-local pixel space into layer
|
|
278
|
+
* pixel space (+Y down) - applying the tile's flip/rotation transform, the
|
|
279
|
+
* tileset draw offset, and the layer's pixel offset, so the geometry lands
|
|
280
|
+
* exactly where the tile is drawn.
|
|
281
|
+
*
|
|
282
|
+
* Shapes that end up covering exactly one whole tile cell are collected into an
|
|
283
|
+
* occupancy grid and merged into as few {@link TileCollisionRect}s as a greedy
|
|
284
|
+
* pass manages - a 200×200 solid region becomes one rectangle instead of 40 000.
|
|
285
|
+
* Cells only merge with neighbours whose source object carries the same `type`
|
|
286
|
+
* string, so a `water` run never fuses into an adjacent `solid` run. Everything
|
|
287
|
+
* else (partial boxes, ellipses, polygons, polylines, points, rotated geometry)
|
|
288
|
+
* passes through individually as a {@link TileCollisionShape}.
|
|
289
|
+
*
|
|
290
|
+
* A {@link TileCollisionOptions.cells} source feeds the same occupancy grid
|
|
291
|
+
* from outside the tiles, for collision authored per cell rather than per tile.
|
|
292
|
+
* It is consulted first, so a whole-cell tile box landing on a cell the source
|
|
293
|
+
* already claimed passes through as a shape rather than overwriting it. With a
|
|
294
|
+
* cell source and no explicit region, a bounded layer walks its full tile
|
|
295
|
+
* extent - a layer that renders nothing at all still produces geometry.
|
|
296
|
+
*
|
|
297
|
+
* This package has no physics dependency and builds no bodies: the result is
|
|
298
|
+
* plain geometry. Turning it into colliders is a short loop in application code.
|
|
299
|
+
*
|
|
300
|
+
* @param layer - The layer to walk.
|
|
301
|
+
* @param options - Region scoping, merge toggle, a source-shape filter, and an
|
|
302
|
+
* optional per-cell occupancy source.
|
|
303
|
+
* @returns Merged rectangles plus pass-through shapes; both empty when the
|
|
304
|
+
* region holds no tile carrying collision data.
|
|
305
|
+
*
|
|
306
|
+
* @example
|
|
307
|
+
* ```ts
|
|
308
|
+
* const { rects, shapes } = buildTileCollisionGeometry(layer);
|
|
309
|
+
*
|
|
310
|
+
* for (const rect of rects) {
|
|
311
|
+
* world.add(makeStaticBox(rect.x, rect.y, rect.width, rect.height));
|
|
312
|
+
* }
|
|
313
|
+
* ```
|
|
314
|
+
* @advanced
|
|
315
|
+
*/
|
|
316
|
+
const buildTileCollisionGeometry = (layer, options = {}) => {
|
|
317
|
+
const cellSource = options.cells;
|
|
318
|
+
const region = resolveRegion(layer, options);
|
|
319
|
+
if (region === null || region.width <= 0 || region.height <= 0) return {
|
|
320
|
+
rects: [],
|
|
321
|
+
shapes: []
|
|
322
|
+
};
|
|
323
|
+
const accept = options.accept;
|
|
324
|
+
const cells = /* @__PURE__ */ new Map();
|
|
325
|
+
const shapes = [];
|
|
326
|
+
if (cellSource !== void 0) claimSourceCells(cells, region, cellSource);
|
|
327
|
+
for (const { tx, ty, tile } of layer.tilesInRect(region.x, region.y, region.width, region.height)) {
|
|
328
|
+
const definition = tile.tileset.getTileDefinition(tile.localTileId);
|
|
329
|
+
if (definition?.collision === void 0) continue;
|
|
330
|
+
const cellX = tx * layer.tileWidth + layer.offsetX;
|
|
331
|
+
const cellY = ty * layer.tileHeight + layer.offsetY;
|
|
332
|
+
for (const object of definition.collision) {
|
|
333
|
+
if (accept !== void 0 && !accept(object, tx, ty)) continue;
|
|
334
|
+
const placed = placeShape(object, layer, tile.tileset, tile.transform, tx, ty);
|
|
335
|
+
if (placed === null) continue;
|
|
336
|
+
const key = cellKey(tx, ty);
|
|
337
|
+
if (coversWholeCell(placed, layer, cellX, cellY) && !cells.has(key)) {
|
|
338
|
+
cells.set(key, object.type);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
shapes.push({
|
|
342
|
+
kind: placed.kind,
|
|
343
|
+
x: placed.x,
|
|
344
|
+
y: placed.y,
|
|
345
|
+
width: placed.width,
|
|
346
|
+
height: placed.height,
|
|
347
|
+
rotation: placed.rotation,
|
|
348
|
+
...placed.points !== void 0 && { points: placed.points },
|
|
349
|
+
tx,
|
|
350
|
+
ty,
|
|
351
|
+
source: object
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (cells.size === 0) return {
|
|
356
|
+
rects: [],
|
|
357
|
+
shapes
|
|
358
|
+
};
|
|
359
|
+
return {
|
|
360
|
+
rects: options.merge === false ? unmergedCells(cells, layer) : mergeCells(cells, region, layer),
|
|
361
|
+
shapes
|
|
362
|
+
};
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
//#endregion
|
|
366
|
+
export { buildTileCollisionGeometry };
|
|
367
|
+
//# sourceMappingURL=tileCollision.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tileCollision.js","names":[],"sources":["../../src/tileCollision.ts"],"sourcesContent":["import type { ObjectPoint, TileMapObject } from './ObjectLayer';\nimport { ObjectKind } from './ObjectLayer';\nimport type { TileLayer } from './TileLayer';\nimport type { TileSet } from './TileSet';\nimport type { TileTransform } from './types';\n\n/**\n * The geometry kinds a {@link TileCollisionShape} can carry. Tile (GID) and\n * text objects describe no collision area and are never emitted, so they are\n * excluded from the union.\n * @advanced\n */\nexport type TileCollisionShapeKind =\n typeof ObjectKind.Rectangle | typeof ObjectKind.Ellipse | typeof ObjectKind.Polygon | typeof ObjectKind.Polyline | typeof ObjectKind.Point;\n\n/**\n * A half-open tile-coordinate rectangle covering\n * `[x, x + width)` × `[y, y + height)`.\n *\n * Used to scope {@link buildTileCollisionGeometry} to part of a layer - a\n * streamed chunk, the tiles around the player, a hand-picked room - instead of\n * always rebuilding the whole map.\n * @advanced\n */\nexport interface TileRegion {\n /** Leftmost tile column (inclusive). */\n readonly x: number;\n /** Topmost tile row (inclusive). */\n readonly y: number;\n /** Width in tiles. */\n readonly width: number;\n /** Height in tiles. */\n readonly height: number;\n}\n\n/**\n * An axis-aligned collision rectangle in tile-layer pixel space (+Y down),\n * covering one or more whole tile cells.\n *\n * Rectangles are synthesized geometry, not source objects: a merged run has no\n * single originating object, so it carries no object `id`/`name`/`properties` -\n * only the `type` string shared by every shape merged into it. Anything that\n * does not exactly cover whole cells stays a {@link TileCollisionShape}, which\n * does keep its source object.\n * @advanced\n */\nexport interface TileCollisionRect {\n /** Left edge in layer pixel space. */\n readonly x: number;\n /** Top edge in layer pixel space. */\n readonly y: number;\n /** Width in pixels (a whole number of tile cells). */\n readonly width: number;\n /** Height in pixels (a whole number of tile cells). */\n readonly height: number;\n /** Class/type string shared by every source shape merged into this rectangle. */\n readonly type: string;\n}\n\n/**\n * A per-tile collision shape placed in tile-layer pixel space (+Y down).\n *\n * Emitted for every collision shape that does not exactly cover a whole tile\n * cell: partial boxes, ellipses, polygons, polylines, points, and rotated\n * geometry. Shapes are never merged with one another.\n * @advanced\n */\nexport interface TileCollisionShape {\n /** Geometry discriminant. */\n readonly kind: TileCollisionShapeKind;\n /** X of the shape origin in layer pixel space. */\n readonly x: number;\n /** Y of the shape origin in layer pixel space. */\n readonly y: number;\n /** Bounding width in px (`0` for polygons, polylines and points). */\n readonly width: number;\n /** Bounding height in px (`0` for polygons, polylines and points). */\n readonly height: number;\n /**\n * Rotation in degrees, clockwise, about `(x, y)`, normalised to `[0, 360)`.\n * Always `0` for polygons and polylines - their rotation is baked into\n * {@link points}.\n */\n readonly rotation: number;\n /** Polygon/polyline vertices relative to `(x, y)`; absent for other kinds. */\n readonly points?: readonly ObjectPoint[];\n /** Tile column the shape came from. */\n readonly tx: number;\n /** Tile row the shape came from. */\n readonly ty: number;\n /** The tile-local source object, for `name`/`type`/`properties` lookups. */\n readonly source: TileMapObject;\n}\n\n/**\n * Collision geometry extracted from a tile layer: whole-cell boxes merged into\n * as few {@link TileCollisionRect}s as a greedy pass can manage, plus every\n * other shape passed through individually.\n * @advanced\n */\nexport interface TileCollisionGeometry {\n /** Merged whole-cell rectangles, in row-major order. */\n readonly rects: readonly TileCollisionRect[];\n /** Non-whole-cell shapes, in the order their tiles were walked. */\n readonly shapes: readonly TileCollisionShape[];\n}\n\n/**\n * Per-cell collision classification for a tile layer, independent of the tiles\n * the layer renders.\n *\n * Returns the class/type string for the cell, or `null` when the cell carries\n * no collision. The string is opaque: it is the merge key for adjacent cells\n * and reaches the consumer unchanged, exactly like the `type` of a tile\n * collision object.\n *\n * A source is sampled while geometry is being built and is expected to answer\n * identically for as long as the geometry is used. Changing what it returns\n * does not invalidate geometry that was already built; rebuild instead.\n * @advanced\n */\nexport type TileCellSource = (tx: number, ty: number) => string | null;\n\n/**\n * Options for {@link buildTileCollisionGeometry}.\n * @advanced\n */\nexport interface TileCollisionOptions {\n /**\n * Tile-coordinate region to walk. Defaults to the bounding box of the\n * layer's currently loaded chunks, so an unbounded (streamed) layer works\n * without the caller computing anything - or, with {@link cells} on a\n * bounded layer, to the layer's full tile extent.\n */\n readonly region?: TileRegion;\n /**\n * Second occupancy source, consulted once per cell of the walked region in\n * addition to the tiles' own collision shapes. Lets collision authored per\n * cell rather than per tile reach the same geometry, without the layer\n * having to render a tile there.\n */\n readonly cells?: TileCellSource | undefined;\n /**\n * Merge adjacent whole-cell boxes that share a `type` into larger\n * rectangles. Default `true`. With `false`, every whole-cell box becomes its\n * own single-cell rectangle.\n */\n readonly merge?: boolean;\n /**\n * Drop source shapes before any geometry is built (e.g. by a custom\n * property). Return `true` to keep a shape. Default keeps all of them.\n */\n readonly accept?: (object: TileMapObject, tx: number, ty: number) => boolean;\n}\n\nconst DEGREES_TO_RADIANS = Math.PI / 180;\n\n/** Normalise degrees into `[0, 360)`. */\nconst normaliseDegrees = (degrees: number): number => {\n const wrapped = degrees % 360;\n\n return wrapped < 0 ? wrapped + 360 : wrapped;\n};\n\n/**\n * Cosine of an angle in degrees, exact on the quadrant multiples.\n * `Math.cos(Math.PI / 2)` is `6.1e-17`, not `0`; tile transforms produce those\n * quadrant angles constantly, and the whole-cell test below compares\n * coordinates exactly, so the noise has to stay out of the arithmetic.\n */\nconst cosDegrees = (degrees: number): number => {\n const angle = normaliseDegrees(degrees);\n\n if (angle === 0) return 1;\n if (angle === 90 || angle === 270) return 0;\n if (angle === 180) return -1;\n\n return Math.cos(angle * DEGREES_TO_RADIANS);\n};\n\n/** Sine of an angle in degrees, exact on the quadrant multiples. */\nconst sinDegrees = (degrees: number): number => {\n const angle = normaliseDegrees(degrees);\n\n if (angle === 0 || angle === 180) return 0;\n if (angle === 90) return 1;\n if (angle === 270) return -1;\n\n return Math.sin(angle * DEGREES_TO_RADIANS);\n};\n\n/** `true` when the transform mirrors (an odd number of reflections). */\nconst isMirrored = (transform: TileTransform): boolean => {\n const flags = (transform.flipX ? 1 : 0) + (transform.flipY ? 1 : 0) + (transform.diagonal ? 1 : 0);\n\n return flags % 2 === 1;\n};\n\n/**\n * Rotation in degrees contributed by a non-mirroring transform. Only the four\n * even-parity transforms are rotations: identity, `flipX+flipY` (180°),\n * `diagonal+flipX` (90°) and `diagonal+flipY` (270°).\n */\nconst transformRotation = (transform: TileTransform): number => {\n if (transform.diagonal) {\n return transform.flipX ? 90 : 270;\n }\n\n return transform.flipX ? 180 : 0;\n};\n\n/**\n * Map a point from tile-local pixel space into the transformed tile-local\n * space of a placed tile. The diagonal flip (a transpose of the axes, which\n * also swaps the box dimensions) is applied first, then the axis mirrors -\n * the same order the renderer's orientation code implies.\n */\nconst mapLocalPoint = (px: number, py: number, boxWidth: number, boxHeight: number, transform: TileTransform): ObjectPoint => {\n let x = px;\n let y = py;\n let width = boxWidth;\n let height = boxHeight;\n\n if (transform.diagonal) {\n const swapped = x;\n\n x = y;\n y = swapped;\n width = boxHeight;\n height = boxWidth;\n }\n\n if (transform.flipX) x = width - x;\n if (transform.flipY) y = height - y;\n\n return { x, y };\n};\n\n/**\n * Layer-space position of a placed tile's top-left image corner. Mirrors the\n * renderer's placement (see `chunkGeometry`): the tileset's draw offset shifts\n * every tile, and a tile taller than the layer's cell is bottom-aligned within\n * it, so collision geometry lands exactly where the tile is drawn.\n */\nconst tileAnchor = (layer: TileLayer, tileset: TileSet, tx: number, ty: number): ObjectPoint => ({\n x: tx * layer.tileWidth + layer.offsetX + tileset.offsetX,\n y: ty * layer.tileHeight + layer.offsetY + layer.tileHeight - tileset.tileHeight + tileset.offsetY,\n});\n\n/** A shape in layer pixel space, before it is classified as cell or shape. */\ninterface PlacedShape {\n readonly kind: TileCollisionShapeKind;\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n readonly rotation: number;\n readonly points?: readonly ObjectPoint[];\n}\n\n/**\n * Place one tile-local collision object into layer pixel space, applying the\n * tile's flip/rotation transform, the tileset draw offset and the layer offset.\n * Returns `null` for kinds that carry no collision geometry.\n */\nconst placeShape = (object: TileMapObject, layer: TileLayer, tileset: TileSet, transform: TileTransform, tx: number, ty: number): PlacedShape | null => {\n if (object.kind === ObjectKind.Tile || object.kind === ObjectKind.Text) {\n return null;\n }\n\n const anchor = tileAnchor(layer, tileset, tx, ty);\n const boxWidth = tileset.tileWidth;\n const boxHeight = tileset.tileHeight;\n const mirrored = isMirrored(transform);\n const sourceCos = cosDegrees(object.rotation);\n const sourceSin = sinDegrees(object.rotation);\n\n if (object.kind === ObjectKind.Polygon || object.kind === ObjectKind.Polyline) {\n // Rotation is baked into the vertices: the source rotation pivots about the\n // object origin, and the tile transform may mirror the whole tile, so an\n // origin-plus-angle form would need a second angle convention. Absolute\n // vertices are exact under both.\n const origin = mapLocalPoint(object.x, object.y, boxWidth, boxHeight, transform);\n const originX = anchor.x + origin.x;\n const originY = anchor.y + origin.y;\n const points = object.points.map(point => {\n const localX = object.x + (sourceCos * point.x - sourceSin * point.y);\n const localY = object.y + (sourceSin * point.x + sourceCos * point.y);\n const mapped = mapLocalPoint(localX, localY, boxWidth, boxHeight, transform);\n\n return { x: anchor.x + mapped.x - originX, y: anchor.y + mapped.y - originY };\n });\n\n return {\n kind: object.kind,\n x: originX,\n y: originY,\n width: 0,\n height: 0,\n rotation: 0,\n // A mirror reverses the winding order; reversing the vertices restores\n // the source orientation, which convex-hull/polygon consumers rely on.\n points: mirrored ? points.reverse() : points,\n };\n }\n\n if (object.kind === ObjectKind.Point) {\n const mapped = mapLocalPoint(object.x, object.y, boxWidth, boxHeight, transform);\n\n return {\n kind: ObjectKind.Point,\n x: anchor.x + mapped.x,\n y: anchor.y + mapped.y,\n width: 0,\n height: 0,\n rotation: 0,\n };\n }\n\n // Rectangles and ellipses are symmetric about their centre, so placing the\n // centre and re-deriving the origin handles every transform uniformly.\n const halfWidth = object.width / 2;\n const halfHeight = object.height / 2;\n const centreLocalX = object.x + (sourceCos * halfWidth - sourceSin * halfHeight);\n const centreLocalY = object.y + (sourceSin * halfWidth + sourceCos * halfHeight);\n const centre = mapLocalPoint(centreLocalX, centreLocalY, boxWidth, boxHeight, transform);\n const centreX = anchor.x + centre.x;\n const centreY = anchor.y + centre.y;\n\n // Conjugating a rotation by a mirror negates it; a non-mirroring transform is\n // itself a rotation and simply adds. Only a mirror can swap the local axes,\n // and then only when the diagonal flip is part of it.\n const rotation = normaliseDegrees(mirrored ? -object.rotation : object.rotation + transformRotation(transform));\n const swapAxes = mirrored && transform.diagonal;\n const width = swapAxes ? object.height : object.width;\n const height = swapAxes ? object.width : object.height;\n\n if (rotation % 90 === 0) {\n // Quadrant-aligned geometry is axis-aligned; re-express it without a\n // rotation so whole-cell boxes on rotated tiles stay mergeable.\n const axisWidth = rotation % 180 === 0 ? width : height;\n const axisHeight = rotation % 180 === 0 ? height : width;\n\n return {\n kind: object.kind,\n x: centreX - axisWidth / 2,\n y: centreY - axisHeight / 2,\n width: axisWidth,\n height: axisHeight,\n rotation: 0,\n };\n }\n\n const cos = cosDegrees(rotation);\n const sin = sinDegrees(rotation);\n\n return {\n kind: object.kind,\n x: centreX - (cos * (width / 2) - sin * (height / 2)),\n y: centreY - (sin * (width / 2) + cos * (height / 2)),\n width,\n height,\n rotation,\n };\n};\n\n/** The tile-coordinate bounding box of the layer's loaded chunks, or `null`. */\nconst loadedTileRegion = (layer: TileLayer): TileRegion | null => {\n let minTx = Number.POSITIVE_INFINITY;\n let minTy = Number.POSITIVE_INFINITY;\n let maxTx = Number.NEGATIVE_INFINITY;\n let maxTy = Number.NEGATIVE_INFINITY;\n\n for (const chunk of layer.loadedChunks()) {\n const startTx = chunk.cx * layer.chunkWidth;\n const startTy = chunk.cy * layer.chunkHeight;\n\n minTx = Math.min(minTx, startTx);\n minTy = Math.min(minTy, startTy);\n maxTx = Math.max(maxTx, startTx + chunk.width - 1);\n maxTy = Math.max(maxTy, startTy + chunk.height - 1);\n }\n\n if (!Number.isFinite(minTx) || !Number.isFinite(minTy)) {\n return null;\n }\n\n return { x: minTx, y: minTy, width: maxTx - minTx + 1, height: maxTy - minTy + 1 };\n};\n\n/**\n * Region a cell source spans on `layer`: the full tile extent of a bounded\n * layer, `null` when the layer is unbounded and the caller must scope the\n * walk itself. Chunk residency deliberately plays no part - the classification\n * lives outside the layer, so loaded chunks cannot define its domain.\n */\nconst cellSourceRegion = (layer: TileLayer): TileRegion | null => {\n if (layer.width === undefined || layer.height === undefined) {\n return null;\n }\n\n return { x: 0, y: 0, width: layer.width, height: layer.height };\n};\n\n/**\n * The region to walk: the caller's, else the extent a cell source spans, else\n * whatever is currently resident. `null` when there is nothing to walk.\n */\nconst resolveRegion = (layer: TileLayer, options: TileCollisionOptions): TileRegion | null => {\n if (options.region !== undefined) return options.region;\n\n const fromCells = options.cells !== undefined ? cellSourceRegion(layer) : null;\n\n return fromCells ?? loadedTileRegion(layer);\n};\n\n/** Claim every cell of `region` the source classifies, into the occupancy grid. */\nconst claimSourceCells = (cells: Map<string, string>, region: TileRegion, source: TileCellSource): void => {\n const endTx = region.x + region.width;\n const endTy = region.y + region.height;\n\n for (let ty = region.y; ty < endTy; ty++) {\n for (let tx = region.x; tx < endTx; tx++) {\n const type = source(tx, ty);\n\n if (type !== null) cells.set(cellKey(tx, ty), type);\n }\n }\n};\n\n/** Key of a claimed cell in the occupancy grid. */\nconst cellKey = (tx: number, ty: number): string => `${tx},${ty}`;\n\n/**\n * `true` when a placed shape is an axis-aligned rectangle covering exactly the\n * tile cell at `(cellX, cellY)` - the only geometry the merging pass handles.\n */\nconst coversWholeCell = (placed: PlacedShape, layer: TileLayer, cellX: number, cellY: number): boolean =>\n placed.kind === ObjectKind.Rectangle &&\n placed.rotation === 0 &&\n placed.x === cellX &&\n placed.y === cellY &&\n placed.width === layer.tileWidth &&\n placed.height === layer.tileHeight;\n\n/** One single-cell rectangle per claimed cell, for `merge: false`. */\nconst unmergedCells = (cells: ReadonlyMap<string, string>, layer: TileLayer): TileCollisionRect[] => {\n const rects: TileCollisionRect[] = [];\n\n for (const [key, type] of cells) {\n const [tx, ty] = key.split(',').map(Number) as [number, number];\n\n rects.push({\n x: tx * layer.tileWidth + layer.offsetX,\n y: ty * layer.tileHeight + layer.offsetY,\n width: layer.tileWidth,\n height: layer.tileHeight,\n type,\n });\n }\n\n return rects;\n};\n\n/**\n * Greedy rectangle merging over the claimed cells: walk row-major, grow each\n * unconsumed cell as far right as the type key allows, then as far down as a\n * full row of that width allows, and consume the block. Deliberately the plain\n * textbook pass - it is linear in the number of cells and always produces a\n * valid, non-overlapping cover, which matters more here than minimality.\n */\nconst mergeCells = (cells: ReadonlyMap<string, string>, region: TileRegion, layer: TileLayer): TileCollisionRect[] => {\n const consumed = new Set<string>();\n const rects: TileCollisionRect[] = [];\n const endTx = region.x + region.width;\n const endTy = region.y + region.height;\n\n for (let ty = region.y; ty < endTy; ty++) {\n for (let tx = region.x; tx < endTx; tx++) {\n const key = cells.get(cellKey(tx, ty));\n\n if (key === undefined || consumed.has(cellKey(tx, ty))) {\n continue;\n }\n\n let width = 1;\n\n while (tx + width < endTx && cells.get(cellKey(tx + width, ty)) === key && !consumed.has(cellKey(tx + width, ty))) {\n width++;\n }\n\n let height = 1;\n\n growDown: while (ty + height < endTy) {\n for (let column = 0; column < width; column++) {\n const candidate = cellKey(tx + column, ty + height);\n\n if (cells.get(candidate) !== key || consumed.has(candidate)) {\n break growDown;\n }\n }\n\n height++;\n }\n\n for (let row = 0; row < height; row++) {\n for (let column = 0; column < width; column++) {\n consumed.add(cellKey(tx + column, ty + row));\n }\n }\n\n rects.push({\n x: tx * layer.tileWidth + layer.offsetX,\n y: ty * layer.tileHeight + layer.offsetY,\n width: width * layer.tileWidth,\n height: height * layer.tileHeight,\n type: key,\n });\n }\n }\n\n return rects;\n};\n\n/**\n * Extract collision geometry from a tile layer's per-tile\n * {@link import('./types').TileDefinition.collision} shapes.\n *\n * Every placed tile in `region` is resolved back to its tileset definition, and\n * each collision shape is transformed from tile-local pixel space into layer\n * pixel space (+Y down) - applying the tile's flip/rotation transform, the\n * tileset draw offset, and the layer's pixel offset, so the geometry lands\n * exactly where the tile is drawn.\n *\n * Shapes that end up covering exactly one whole tile cell are collected into an\n * occupancy grid and merged into as few {@link TileCollisionRect}s as a greedy\n * pass manages - a 200×200 solid region becomes one rectangle instead of 40 000.\n * Cells only merge with neighbours whose source object carries the same `type`\n * string, so a `water` run never fuses into an adjacent `solid` run. Everything\n * else (partial boxes, ellipses, polygons, polylines, points, rotated geometry)\n * passes through individually as a {@link TileCollisionShape}.\n *\n * A {@link TileCollisionOptions.cells} source feeds the same occupancy grid\n * from outside the tiles, for collision authored per cell rather than per tile.\n * It is consulted first, so a whole-cell tile box landing on a cell the source\n * already claimed passes through as a shape rather than overwriting it. With a\n * cell source and no explicit region, a bounded layer walks its full tile\n * extent - a layer that renders nothing at all still produces geometry.\n *\n * This package has no physics dependency and builds no bodies: the result is\n * plain geometry. Turning it into colliders is a short loop in application code.\n *\n * @param layer - The layer to walk.\n * @param options - Region scoping, merge toggle, a source-shape filter, and an\n * optional per-cell occupancy source.\n * @returns Merged rectangles plus pass-through shapes; both empty when the\n * region holds no tile carrying collision data.\n *\n * @example\n * ```ts\n * const { rects, shapes } = buildTileCollisionGeometry(layer);\n *\n * for (const rect of rects) {\n * world.add(makeStaticBox(rect.x, rect.y, rect.width, rect.height));\n * }\n * ```\n * @advanced\n */\nexport const buildTileCollisionGeometry = (layer: TileLayer, options: TileCollisionOptions = {}): TileCollisionGeometry => {\n const cellSource = options.cells;\n const region = resolveRegion(layer, options);\n\n if (region === null || region.width <= 0 || region.height <= 0) {\n return { rects: [], shapes: [] };\n }\n\n const accept = options.accept;\n const cells = new Map<string, string>();\n const shapes: TileCollisionShape[] = [];\n\n if (cellSource !== undefined) claimSourceCells(cells, region, cellSource);\n\n for (const { tx, ty, tile } of layer.tilesInRect(region.x, region.y, region.width, region.height)) {\n const definition = tile.tileset.getTileDefinition(tile.localTileId);\n\n if (definition?.collision === undefined) {\n continue;\n }\n\n const cellX = tx * layer.tileWidth + layer.offsetX;\n const cellY = ty * layer.tileHeight + layer.offsetY;\n\n for (const object of definition.collision) {\n if (accept !== undefined && !accept(object, tx, ty)) {\n continue;\n }\n\n const placed = placeShape(object, layer, tile.tileset, tile.transform, tx, ty);\n\n if (placed === null) {\n continue;\n }\n\n const key = cellKey(tx, ty);\n\n // A second whole-cell box on the same tile cannot claim the cell twice;\n // it falls through to the shape list so no geometry is silently dropped.\n if (coversWholeCell(placed, layer, cellX, cellY) && !cells.has(key)) {\n cells.set(key, object.type);\n continue;\n }\n\n shapes.push({\n kind: placed.kind,\n x: placed.x,\n y: placed.y,\n width: placed.width,\n height: placed.height,\n rotation: placed.rotation,\n ...(placed.points !== undefined && { points: placed.points }),\n tx,\n ty,\n source: object,\n });\n }\n }\n\n if (cells.size === 0) {\n return { rects: [], shapes };\n }\n\n const rects = options.merge === false ? unmergedCells(cells, layer) : mergeCells(cells, region, layer);\n\n return { rects, shapes };\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tile word packing, shared by both backends and by the shader sources they
|
|
3
|
+
* fill: transform-buffer row in the low 29 bits, diagonal flip in bit 29.
|
|
4
|
+
* flipX/flipY are baked into the UV bounds at write time, so the shader only
|
|
5
|
+
* needs the diagonal axis swap.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export declare const TILE_ROW_MASK = 536870911;
|
|
10
|
+
/** @internal */
|
|
11
|
+
export declare const TILE_DIAGONAL_BIT = 536870912;
|
|
12
|
+
//# sourceMappingURL=tileWord.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tileWord.d.ts","sourceRoot":"","sources":["../../src/tileWord.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,YAAa,CAAC;AAExC,gBAAgB;AAChB,eAAO,MAAM,iBAAiB,YAAa,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/tileWord.ts
|
|
2
|
+
/**
|
|
3
|
+
* Tile word packing, shared by both backends and by the shader sources they
|
|
4
|
+
* fill: transform-buffer row in the low 29 bits, diagonal flip in bit 29.
|
|
5
|
+
* flipX/flipY are baked into the UV bounds at write time, so the shader only
|
|
6
|
+
* needs the diagonal axis swap.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
const TILE_ROW_MASK = 536870911;
|
|
11
|
+
/** @internal */
|
|
12
|
+
const TILE_DIAGONAL_BIT = 536870912;
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { TILE_DIAGONAL_BIT, TILE_ROW_MASK };
|
|
16
|
+
//# sourceMappingURL=tileWord.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tileWord.js","names":[],"sources":["../../src/tileWord.ts"],"sourcesContent":["/**\n * Tile word packing, shared by both backends and by the shader sources they\n * fill: transform-buffer row in the low 29 bits, diagonal flip in bit 29.\n * flipX/flipY are baked into the UV bounds at write time, so the shader only\n * needs the diagonal axis swap.\n *\n * @internal\n */\nexport const TILE_ROW_MASK = 0x1fffffff;\n\n/** @internal */\nexport const TILE_DIAGONAL_BIT = 0x20000000;\n"],"mappings":";;;;;;;;;AAQA,MAAa,gBAAgB;;AAG7B,MAAa,oBAAoB"}
|