From 50a0a3508f5af3e2b2c9a7be0c04a6099c40ef09 Mon Sep 17 00:00:00 2001 From: Axel Meyer Date: Tue, 16 Jun 2026 21:02:06 +0200 Subject: [PATCH] feat(scan-sprite): walk dir, normalise aliases, detect collisions --- src/scan-sprite.js | 48 ++++++++++++++++++++ tests/fixtures/sprite-collision/Bed1.png | Bin 0 -> 124 bytes tests/fixtures/sprite-collision/bed1.png | Bin 0 -> 124 bytes tests/fixtures/sprite-fresh/BarrelLarge.png | Bin 0 -> 225 bytes tests/fixtures/sprite-fresh/Bed1.png | Bin 0 -> 210 bytes tests/fixtures/sprite-fresh/Bench1.png | Bin 0 -> 173 bytes tests/fixtures/sprite-fresh/Sack.png | Bin 0 -> 153 bytes tests/scan-sprite.test.js | 47 +++++++++++++++++++ 8 files changed, 95 insertions(+) create mode 100644 src/scan-sprite.js create mode 100644 tests/fixtures/sprite-collision/Bed1.png create mode 100644 tests/fixtures/sprite-collision/bed1.png create mode 100644 tests/fixtures/sprite-fresh/BarrelLarge.png create mode 100644 tests/fixtures/sprite-fresh/Bed1.png create mode 100644 tests/fixtures/sprite-fresh/Bench1.png create mode 100644 tests/fixtures/sprite-fresh/Sack.png create mode 100644 tests/scan-sprite.test.js diff --git a/src/scan-sprite.js b/src/scan-sprite.js new file mode 100644 index 0000000..c75de14 --- /dev/null +++ b/src/scan-sprite.js @@ -0,0 +1,48 @@ +// src/scan-sprite.js +// Scans a source dir for PNG/WebP/JPG sprite files (no _diffuse/_height +// suffix convention — sprite-mode treats each file as a standalone +// sprite). Normalises filenames to snake_case aliases, detects +// collisions. + +const fs = require('node:fs'); +const path = require('node:path'); +const { aliasFromFilename } = require('./alias'); + +const SUPPORTED_EXT = ['.png', '.webp', '.jpg', '.jpeg']; + +function scanSpriteSourceDir(srcDir, opts = {}) { + if (!fs.existsSync(srcDir)) { + return { sources: [], errors: [`scan-sprite: source dir does not exist: ${srcDir}`] }; + } + const entries = fs.readdirSync(srcDir, { withFileTypes: true }); + const candidates = []; + for (const e of entries) { + if (!e.isFile()) continue; + const ext = path.extname(e.name).toLowerCase(); + if (!SUPPORTED_EXT.includes(ext)) continue; + candidates.push(e.name); + } + candidates.sort(); + + const aliasToFile = new Map(); + const sources = []; + for (const filename of candidates) { + const alias = aliasFromFilename(filename, opts); + if (aliasToFile.has(alias)) { + const prev = aliasToFile.get(alias); + return { + sources: [], + errors: [`scan-sprite: alias collision "${alias}" — both "${prev}" and "${filename}" map to the same alias`], + }; + } + aliasToFile.set(alias, filename); + sources.push({ + alias, + sourceFile: filename, + sourcePath: path.join(srcDir, filename), + }); + } + return { sources, errors: [] }; +} + +module.exports = { scanSpriteSourceDir }; diff --git a/tests/fixtures/sprite-collision/Bed1.png b/tests/fixtures/sprite-collision/Bed1.png new file mode 100644 index 0000000000000000000000000000000000000000..76ed24b04966143cc57758d57c9b4eb7a6729de1 GIT binary patch literal 124 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzXHOT$kP6152N@X|7&w{@mefC& sZhp(AtYX;d>60WhdBzbHH3CdIgU3vEU*sP(0*zzvboFyt=akR{0D#OS2><{9 literal 0 HcmV?d00001 diff --git a/tests/fixtures/sprite-collision/bed1.png b/tests/fixtures/sprite-collision/bed1.png new file mode 100644 index 0000000000000000000000000000000000000000..1d98a7efe38756f7cf0f395bfacfe295e881edde GIT binary patch literal 124 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzXHOT$kP6152N@X|7&w{@mefC& sZhkAFtYX;d>60WhdBzbHH3CdI{U=Oz&2O5mfW|R+y85}Sb4q9e0D+Yw(EtDd literal 0 HcmV?d00001 diff --git a/tests/fixtures/sprite-fresh/BarrelLarge.png b/tests/fixtures/sprite-fresh/BarrelLarge.png new file mode 100644 index 0000000000000000000000000000000000000000..7b1c7c003bc928d3325d3a0157f0694c1ef3c8aa GIT binary patch literal 225 zcmeAS@N?(olHy`uVBq!ia0vp^4nUm1!3HGP9xZtRq*i#kIEGX(K0V0D$iTqSpio?S nKD)zUcX^BFoFjt>-C=&G%)HM~;qYUin;AS^{an^LB{Ts5UNdED literal 0 HcmV?d00001 diff --git a/tests/fixtures/sprite-fresh/Bed1.png b/tests/fixtures/sprite-fresh/Bed1.png new file mode 100644 index 0000000000000000000000000000000000000000..ec89ea9e1b86094635e7cfa3230de8462c48c025 GIT binary patch literal 210 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!3HERU8}DLQZqeW978G?-yURSWKdveHmE6@ moh@+V-0ts!YX!FsB;-$6%3SAHXml6oNCr<=KbLh*2~7aI^++-R literal 0 HcmV?d00001 diff --git a/tests/fixtures/sprite-fresh/Bench1.png b/tests/fixtures/sprite-fresh/Bench1.png new file mode 100644 index 0000000000000000000000000000000000000000..d75531aee1bac701fba988d44f7803def60e3391 GIT binary patch literal 173 zcmeAS@N?(olHy`uVBq!ia0vp^2|#SX!3HGXiPfJ6QYD@)jv*C{Zx1>$GAJ;#80>js zc3q<8#<|+>f@eFjTuc{91@^9R3sGJrc$I2g{%elRahxHmF9Gdm@O1TaS?83{1ORUg BJ0k!9 literal 0 HcmV?d00001 diff --git a/tests/fixtures/sprite-fresh/Sack.png b/tests/fixtures/sprite-fresh/Sack.png new file mode 100644 index 0000000000000000000000000000000000000000..9d8aec3507b8ac2c64635c22b198eea915b93cde GIT binary patch literal 153 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUt1Wy;okP61D2N@X|40xIi`aW5{ lFP*UGt=o(vo^z;xYSlk6-;G>4dnM2=22WQ%mvv4FO#t?aK6n5C literal 0 HcmV?d00001 diff --git a/tests/scan-sprite.test.js b/tests/scan-sprite.test.js new file mode 100644 index 0000000..cb1658a --- /dev/null +++ b/tests/scan-sprite.test.js @@ -0,0 +1,47 @@ +const { test } = require('node:test'); +const assert = require('node:assert'); +const path = require('node:path'); +const { scanSpriteSourceDir } = require('../src/scan-sprite'); + +const FRESH = path.resolve(__dirname, 'fixtures/sprite-fresh'); +const COLLISION = path.resolve(__dirname, 'fixtures/sprite-collision'); + +test('scan-sprite: 4 PNGs, alphabetical, no errors', () => { + const { sources, errors } = scanSpriteSourceDir(FRESH, {}); + assert.deepStrictEqual(errors, []); + assert.strictEqual(sources.length, 4); + assert.strictEqual(sources[0].alias, 'barrel_large'); + assert.strictEqual(sources[1].alias, 'bed1'); + assert.strictEqual(sources[2].alias, 'bench1'); + assert.strictEqual(sources[3].alias, 'sack'); +}); + +test('scan-sprite: sources carry sourcePath + sourceFile', () => { + const { sources } = scanSpriteSourceDir(FRESH, {}); + assert.ok(sources[1].sourcePath.endsWith('Bed1.png')); + assert.strictEqual(sources[1].sourceFile, 'Bed1.png'); +}); + +test('scan-sprite: alias collision -> loud error', () => { + const { sources, errors } = scanSpriteSourceDir(COLLISION, {}); + assert.strictEqual(sources.length, 0); + assert.strictEqual(errors.length, 1); + assert.match(errors[0], /alias collision/i); + assert.match(errors[0], /bed1/); +}); + +test('scan-sprite: missing dir -> error', () => { + const { sources, errors } = scanSpriteSourceDir('/nonexistent/path', {}); + assert.strictEqual(sources.length, 0); + assert.ok(errors[0].includes('does not exist')); +}); + +test('scan-sprite: applies stripPrefix option', () => { + const { sources, errors } = scanSpriteSourceDir(FRESH, { + stripPrefix: 'Bed', + }); + assert.deepStrictEqual(errors, []); + // Bed1 -> 1, Bench1 not stripped -> bench1, etc. + const aliases = sources.map(s => s.alias).sort(); + assert.ok(aliases.includes('1')); +});