Init atlas-baker tool with source-dir scan and pair-detection

Creates the sporel-tool-atlas-baker Node CLI scaffold and the first
phase of the bake pipeline: scan a source directory for *_diffuse.png
files, pair them with optional *_height.png companions, and return
alphabetically-sorted sources for deterministic re-bakes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Axel Meyer
2026-05-21 22:38:42 +02:00
commit b64e3e87a9
12 changed files with 1295 additions and 0 deletions

67
src/scan.js Normal file
View File

@@ -0,0 +1,67 @@
// src/scan.js
// Scans a source directory for *_diffuse.{png,webp,jpg,jpeg} files and
// pairs them with optional *_height.{png,webp,jpg,jpeg} companions.
//
// Returns: { sources: Array<{name, diffusePath, heightPath?}>, errors: Array<string> }
// `name` = filename without "_diffuse.<ext>" suffix.
const fs = require('node:fs');
const path = require('node:path');
const SUPPORTED_EXT = ['.png', '.webp', '.jpg', '.jpeg'];
function stripSuffix(filename, suffix) {
for (const ext of SUPPORTED_EXT) {
const full = `${suffix}${ext}`;
if (filename.toLowerCase().endsWith(full)) {
return filename.slice(0, -full.length);
}
}
return null;
}
function scanSourceDir(srcDir) {
if (!fs.existsSync(srcDir)) {
return { sources: [], errors: [`scan: source dir does not exist: ${srcDir}`] };
}
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
const diffuseMap = new Map(); // name -> path
const heightMap = new Map(); // name -> path
for (const entry of entries) {
if (!entry.isFile()) continue;
const fn = entry.name;
const dName = stripSuffix(fn, '_diffuse');
if (dName !== null) {
diffuseMap.set(dName, path.join(srcDir, fn));
continue;
}
const hName = stripSuffix(fn, '_height');
if (hName !== null) {
heightMap.set(hName, path.join(srcDir, fn));
}
}
const errors = [];
for (const hName of heightMap.keys()) {
if (!diffuseMap.has(hName)) {
errors.push(`scan: height without paired diffuse: ${hName}`);
}
}
// Sort alphabetically by name for determinism
const names = [...diffuseMap.keys()].sort((a, b) => a.localeCompare(b));
const sources = names.map(name => ({
name,
diffusePath: diffuseMap.get(name),
heightPath: heightMap.get(name) || null,
}));
if (sources.length === 0) {
errors.push(`scan: no *_diffuse.{png,webp,jpg,jpeg} found in: ${srcDir}`);
}
return { sources, errors };
}
module.exports = { scanSourceDir };