Add GID encode/decode with range validation
Bit layout [atlas:8][tile_id:20][rot:2][res:2] matches lib-core.maps init.lua so JS-produced GIDs are byte-identical to engine-produced ones. >>> 0 forces unsigned u32 output.
This commit is contained in:
30
src/gid.js
Normal file
30
src/gid.js
Normal file
@@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
const MAX_ATLAS = 255;
|
||||
const MAX_TILE = 1048575;
|
||||
const MAX_ROT = 3;
|
||||
const MAX_U32 = 0xFFFFFFFF;
|
||||
|
||||
function checkInt(name, value, min, max) {
|
||||
if (!Number.isInteger(value) || value < min || value > max) {
|
||||
throw new RangeError(`${name} ${value} out of range [${min},${max}]`);
|
||||
}
|
||||
}
|
||||
|
||||
function encodeGid(atlas, tile, rotation = 0) {
|
||||
checkInt('atlas', atlas, 0, MAX_ATLAS);
|
||||
checkInt('tile', tile, 0, MAX_TILE);
|
||||
checkInt('rotation', rotation, 0, MAX_ROT);
|
||||
return ((atlas << 24) | (tile << 4) | (rotation << 2)) >>> 0;
|
||||
}
|
||||
|
||||
function decodeGid(gid) {
|
||||
checkInt('gid', gid, 0, MAX_U32);
|
||||
return {
|
||||
atlas: (gid >>> 24) & 0xFF,
|
||||
tile: (gid >>> 4) & 0xFFFFF,
|
||||
rotation: (gid >>> 2) & 0x3,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { encodeGid, decodeGid, MAX_ATLAS, MAX_TILE, MAX_ROT };
|
||||
83
tests/gid.test.js
Normal file
83
tests/gid.test.js
Normal file
@@ -0,0 +1,83 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { encodeGid, decodeGid } = require('../src/gid');
|
||||
|
||||
test('encodeGid: known reference value (0,12,1) = 196', () => {
|
||||
assert.equal(encodeGid(0, 12, 1), 196);
|
||||
});
|
||||
|
||||
test('encodeGid: default rotation is 0', () => {
|
||||
assert.equal(encodeGid(0, 12), 192);
|
||||
});
|
||||
|
||||
test('encodeGid: zero tuple yields 0', () => {
|
||||
assert.equal(encodeGid(0, 0, 0), 0);
|
||||
});
|
||||
|
||||
test('encodeGid: high atlas (255) produces unsigned u32', () => {
|
||||
// 255 << 24 = 0xFF000000 = 4278190080 (must NOT be negative)
|
||||
assert.equal(encodeGid(255, 0, 0), 4278190080);
|
||||
});
|
||||
|
||||
test('encodeGid: max tile_id (1048575) fits 20 bits', () => {
|
||||
// tile << 4 = 0xFFFFF0 = 16777200
|
||||
assert.equal(encodeGid(0, 1048575, 0), 16777200);
|
||||
});
|
||||
|
||||
test('encodeGid: throws on out-of-range atlas', () => {
|
||||
assert.throws(() => encodeGid(256, 0, 0), /atlas/);
|
||||
assert.throws(() => encodeGid(-1, 0, 0), /atlas/);
|
||||
});
|
||||
|
||||
test('encodeGid: throws on out-of-range tile', () => {
|
||||
assert.throws(() => encodeGid(0, 1048576, 0), /tile/);
|
||||
assert.throws(() => encodeGid(0, -1, 0), /tile/);
|
||||
});
|
||||
|
||||
test('encodeGid: throws on out-of-range rotation', () => {
|
||||
assert.throws(() => encodeGid(0, 0, 4), /rotation/);
|
||||
assert.throws(() => encodeGid(0, 0, -1), /rotation/);
|
||||
});
|
||||
|
||||
test('encodeGid: throws on non-integer inputs', () => {
|
||||
assert.throws(() => encodeGid(1.5, 0, 0), /atlas/);
|
||||
assert.throws(() => encodeGid(0, '12', 0), /tile/);
|
||||
});
|
||||
|
||||
test('decodeGid: empty (0) returns zeros', () => {
|
||||
assert.deepEqual(decodeGid(0), { atlas: 0, tile: 0, rotation: 0 });
|
||||
});
|
||||
|
||||
test('decodeGid: 196 unpacks to (0,12,1)', () => {
|
||||
assert.deepEqual(decodeGid(196), { atlas: 0, tile: 12, rotation: 1 });
|
||||
});
|
||||
|
||||
test('decodeGid: 16777232 from real demo map unpacks to (1,1,0)', () => {
|
||||
// demo_v2.map.json wall layer uses 16777232 = (1<<24)|(1<<4)
|
||||
assert.deepEqual(decodeGid(16777232), { atlas: 1, tile: 1, rotation: 0 });
|
||||
});
|
||||
|
||||
test('decodeGid: high atlas (255,0,0) round-trip', () => {
|
||||
assert.deepEqual(decodeGid(4278190080), { atlas: 255, tile: 0, rotation: 0 });
|
||||
});
|
||||
|
||||
test('decodeGid: throws on non-integer or negative', () => {
|
||||
assert.throws(() => decodeGid(-1), /gid/);
|
||||
assert.throws(() => decodeGid(1.5), /gid/);
|
||||
assert.throws(() => decodeGid(0x100000000), /gid/); // > u32 max
|
||||
});
|
||||
|
||||
test('round-trip: 100 random tuples encode→decode losslessly', () => {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const atlas = Math.floor(Math.random() * 256);
|
||||
const tile = Math.floor(Math.random() * 1048576);
|
||||
const rotation = Math.floor(Math.random() * 4);
|
||||
const gid = encodeGid(atlas, tile, rotation);
|
||||
const decoded = decodeGid(gid);
|
||||
assert.equal(decoded.atlas, atlas, `atlas mismatch for ${gid}`);
|
||||
assert.equal(decoded.tile, tile, `tile mismatch for ${gid}`);
|
||||
assert.equal(decoded.rotation, rotation, `rotation mismatch for ${gid}`);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user