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