From d5fb03b8d0b48fe1584b0d5a3adddae6dc8c9348 Mon Sep 17 00:00:00 2001 From: Axel Meyer Date: Tue, 16 Jun 2026 21:17:32 +0200 Subject: [PATCH] feat(write-sprite): compose atlas image + UV-JSON + lock output --- src/write-sprite.js | 58 ++++++++++++++++++++++++++++++++++++++ tests/write-sprite.test.js | 46 ++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 src/write-sprite.js create mode 100644 tests/write-sprite.test.js diff --git a/src/write-sprite.js b/src/write-sprite.js new file mode 100644 index 0000000..7d4e212 --- /dev/null +++ b/src/write-sprite.js @@ -0,0 +1,58 @@ +// src/write-sprite.js +// Compose the diffuse atlas image, build the UV-JSON, write all three +// output files into //. + +const fs = require('node:fs'); +const path = require('node:path'); +const Jimp = require('jimp'); +const { serializeSpriteLock } = require('./lock-sprite'); + +async function composeSpriteAtlas(boundsW, boundsH, placed) { + // Jimp 0.22: synchronous (w, h, color) constructor; color is RGBA32. + const atlas = new Jimp(boundsW, boundsH, 0x00000000); + for (const p of placed) { + // Each `p` carries the source image as `p.image` (jimp instance). + // Blit at (p.x, p.y) — the orchestrator has already pre-offset + // these coordinates by padPx, so x/y is the INNER sprite origin. + atlas.composite(p.image, p.x, p.y); + } + return atlas; +} + +function buildUvJson(atlasId, boundsW, boundsH, placed) { + const sprites = {}; + for (const p of placed) { + sprites[p.alias] = { + x: p.x, + y: p.y, + w: p.w, + h: p.h, + source_file: p.sourceFile, + }; + } + return JSON.stringify({ + atlas_id: atlasId, + atlas_size: [boundsW, boundsH], + sprites, + }, null, 2) + '\n'; +} + +async function writeSpriteOutputs(opts) { + const { atlasId, outDir, placed, boundsW, boundsH, lock } = opts; + const atlasDir = path.join(outDir, atlasId); + fs.mkdirSync(atlasDir, { recursive: true }); + + const atlas = await composeSpriteAtlas(boundsW, boundsH, placed); + await atlas.writeAsync(path.join(atlasDir, 'sprites.diffuse.atlas.png')); + + fs.writeFileSync( + path.join(atlasDir, 'sprites.uv.json'), + buildUvJson(atlasId, boundsW, boundsH, placed), + ); + fs.writeFileSync( + path.join(atlasDir, 'sprites.atlas.lock.json'), + serializeSpriteLock(lock), + ); +} + +module.exports = { composeSpriteAtlas, buildUvJson, writeSpriteOutputs }; diff --git a/tests/write-sprite.test.js b/tests/write-sprite.test.js new file mode 100644 index 0000000..bb16106 --- /dev/null +++ b/tests/write-sprite.test.js @@ -0,0 +1,46 @@ +const { test } = require('node:test'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); +const os = require('node:os'); +const Jimp = require('jimp'); +const { writeSpriteOutputs, buildUvJson } = require('../src/write-sprite'); + +test('buildUvJson: emits atlas_id + atlas_size + sprites table', () => { + const placed = [ + { alias: 'bed1', x: 0, y: 0, w: 64, h: 32, sourceFile: 'Bed1.png' }, + { alias: 'sack', x: 64, y: 0, w: 32, h: 32, sourceFile: 'Sack.png' }, + ]; + const json = buildUvJson('tcbasics', 96, 32, placed); + const parsed = JSON.parse(json); + assert.strictEqual(parsed.atlas_id, 'tcbasics'); + assert.deepStrictEqual(parsed.atlas_size, [96, 32]); + assert.strictEqual(parsed.sprites.bed1.x, 0); + assert.strictEqual(parsed.sprites.bed1.w, 64); + assert.strictEqual(parsed.sprites.bed1.source_file, 'Bed1.png'); + assert.strictEqual(parsed.sprites.sack.x, 64); +}); + +test('writeSpriteOutputs: produces 3 files', async (t) => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'write-sprite-test-')); + t.after(() => fs.rmSync(tmp, { recursive: true, force: true })); + + // Build a 2-sprite atlas in memory + const a = new Jimp(32, 32, 0xff0000ff); + const b = new Jimp(32, 32, 0x00ff00ff); + const placed = [ + { alias: 'red', x: 0, y: 0, w: 32, h: 32, sourceFile: 'Red.png', image: a, padPx: 0 }, + { alias: 'green', x: 32, y: 0, w: 32, h: 32, sourceFile: 'Green.png', image: b, padPx: 0 }, + ]; + await writeSpriteOutputs({ + atlasId: 'test', + outDir: tmp, + placed, + boundsW: 64, + boundsH: 32, + lock: { atlas_id: 'test', version: 1, aliases: ['red', 'green'], sources: { red: 'Red.png', green: 'Green.png' } }, + }); + assert.ok(fs.existsSync(path.join(tmp, 'test/sprites.diffuse.atlas.png'))); + assert.ok(fs.existsSync(path.join(tmp, 'test/sprites.uv.json'))); + assert.ok(fs.existsSync(path.join(tmp, 'test/sprites.atlas.lock.json'))); +});