feat(scan-sprite): walk dir, normalise aliases, detect collisions
This commit is contained in:
48
src/scan-sprite.js
Normal file
48
src/scan-sprite.js
Normal file
@@ -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 };
|
||||
BIN
tests/fixtures/sprite-collision/Bed1.png
vendored
Normal file
BIN
tests/fixtures/sprite-collision/Bed1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 124 B |
BIN
tests/fixtures/sprite-collision/bed1.png
vendored
Normal file
BIN
tests/fixtures/sprite-collision/bed1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 124 B |
BIN
tests/fixtures/sprite-fresh/BarrelLarge.png
vendored
Normal file
BIN
tests/fixtures/sprite-fresh/BarrelLarge.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 225 B |
BIN
tests/fixtures/sprite-fresh/Bed1.png
vendored
Normal file
BIN
tests/fixtures/sprite-fresh/Bed1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 210 B |
BIN
tests/fixtures/sprite-fresh/Bench1.png
vendored
Normal file
BIN
tests/fixtures/sprite-fresh/Bench1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 173 B |
BIN
tests/fixtures/sprite-fresh/Sack.png
vendored
Normal file
BIN
tests/fixtures/sprite-fresh/Sack.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 153 B |
47
tests/scan-sprite.test.js
Normal file
47
tests/scan-sprite.test.js
Normal file
@@ -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'));
|
||||
});
|
||||
Reference in New Issue
Block a user