Add atlas composers and output writers
composeAtlas blits placed tile pixels into bounds-sized RGBA + L8 PNGs, synthesizing L8=0 planes for unpaired diffuse tiles. buildAtlasJson emits the metadata with deterministic ID-sorted tile arrays and applies the optional blocks_sight regex. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
66
tests/write.test.js
Normal file
66
tests/write.test.js
Normal file
@@ -0,0 +1,66 @@
|
||||
// tests/write.test.js
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const path = require('node:path');
|
||||
const fs = require('node:fs');
|
||||
const { PNG } = require('pngjs');
|
||||
const { composeAtlas, buildAtlasJson } = require('../src/write');
|
||||
|
||||
const FIX = path.resolve(__dirname, 'fixtures/small-fresh');
|
||||
|
||||
test('compose: 3 tiles into 96x32 atlas', async () => {
|
||||
const placed = [
|
||||
{ name: 'grass', id: 1, x: 0, y: 0, w: 32, h: 32, diffusePath: path.join(FIX, 'grass_diffuse.png'), heightPath: null },
|
||||
{ name: 'stone', id: 2, x: 32, y: 0, w: 32, h: 32, diffusePath: path.join(FIX, 'stone_diffuse.png'), heightPath: null },
|
||||
{ name: 'water', id: 3, x: 64, y: 0, w: 32, h: 32, diffusePath: path.join(FIX, 'water_diffuse.png'), heightPath: path.join(FIX, 'water_height.png') },
|
||||
];
|
||||
const { diffuse, height } = await composeAtlas(placed, 128, 32);
|
||||
assert.strictEqual(diffuse.width, 128);
|
||||
assert.strictEqual(diffuse.height, 32);
|
||||
// Sample grass pixel (0,0): expect RGB ~ (110, 170, 80)
|
||||
const gIdx = 0;
|
||||
assert.strictEqual(diffuse.data[gIdx + 0], 110);
|
||||
assert.strictEqual(diffuse.data[gIdx + 1], 170);
|
||||
assert.strictEqual(diffuse.data[gIdx + 2], 80);
|
||||
// Sample stone pixel (32, 0): expect RGB ~ (120, 120, 120)
|
||||
const sIdx = (0 * 128 + 32) * 4;
|
||||
assert.strictEqual(diffuse.data[sIdx + 0], 120);
|
||||
// Height should be 0 for synth-tiles (grass, stone)
|
||||
assert.strictEqual(height.data[0], 0); // grass position
|
||||
assert.strictEqual(height.data[(0 * 128 + 32) * 4], 0); // stone position
|
||||
});
|
||||
|
||||
test('buildAtlasJson: sorted by ID, blocks_sight pattern applied', () => {
|
||||
const placed = [
|
||||
{ name: 'grass', id: 1, x: 0, y: 0, w: 32, h: 32 },
|
||||
{ name: 'stone_wall_brick', id: 2, x: 32, y: 0, w: 32, h: 32 },
|
||||
];
|
||||
const json = buildAtlasJson('demo', 1, 32, 64, 32, placed, /^stone_wall_/);
|
||||
assert.strictEqual(json.atlas_id, 'demo');
|
||||
assert.strictEqual(json.tiles.length, 2);
|
||||
assert.strictEqual(json.tiles[0].id, 1);
|
||||
assert.strictEqual(json.tiles[0].name, 'grass');
|
||||
assert.deepStrictEqual(json.tiles[0].uv, [0, 0, 32, 32]);
|
||||
assert.strictEqual(json.tiles[0].blocks_sight, undefined);
|
||||
assert.strictEqual(json.tiles[1].blocks_sight, true);
|
||||
});
|
||||
|
||||
test('compose: WebP source loads + resizes', async () => {
|
||||
// Optional integration check — only runs if FA-Starter fixture is staged.
|
||||
// The Task 10 baker run is the real-world verification; this test is a
|
||||
// cheaper inline guard against jimp.read regressions on WebP.
|
||||
const webpFixture = path.resolve(__dirname, 'fixtures/multi-format/sample_diffuse.webp');
|
||||
if (!fs.existsSync(webpFixture)) return; // skip if not staged
|
||||
|
||||
const placed = [{ name: 'sample', id: 1, x: 0, y: 0, w: 64, h: 64,
|
||||
diffusePath: webpFixture, heightPath: null }];
|
||||
const { diffuse } = await composeAtlas(placed, 64, 64);
|
||||
assert.strictEqual(diffuse.width, 64);
|
||||
assert.strictEqual(diffuse.height, 64);
|
||||
// Just confirm it didn't throw and has nonzero alpha somewhere
|
||||
let hasAlpha = false;
|
||||
for (let i = 3; i < diffuse.data.length; i += 4) {
|
||||
if (diffuse.data[i] > 0) { hasAlpha = true; break; }
|
||||
}
|
||||
assert.ok(hasAlpha, 'compose produced fully-transparent atlas');
|
||||
});
|
||||
Reference in New Issue
Block a user