From ddafcd6486a15659d3a04ec2dacd6ed5141ec805 Mon Sep 17 00:00:00 2001 From: calic Date: Sun, 21 Jun 2026 11:42:00 +0200 Subject: [PATCH] feat(bake-sprite): --pixels-per-meter embeds atlas_meta in sprites.uv.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sprite-mode bake gains an additive --pixels-per-meter flag. When set, the value is written into sprites.uv.json under atlas_meta. pixels_per_meter. Consumers (lib-core.render path-1 via per-entity sprite_scale) divide their project canonical px/m by this number to auto-scale the rendered quad — atlas-native resolution no longer needs to match the project tile-scale. Unset (legacy bakes): atlas_meta omitted, byte-equal to v0.3.0 output. Tests: 53/53 pass; existing buildUvJson test continues to assert the unset-default shape. Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/atlas-baker.js | 2 ++ package.json | 4 ++-- src/bake-sprite.js | 6 ++++++ src/write-sprite.js | 19 +++++++++++++------ 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/bin/atlas-baker.js b/bin/atlas-baker.js index c45a4a9..5e70dd8 100644 --- a/bin/atlas-baker.js +++ b/bin/atlas-baker.js @@ -24,6 +24,8 @@ function parseArgs(argv) { case '--schema': opts.schema = next; i++; break; case '--mode': opts.mode = next; i++; break; case '--strip-prefix': opts.stripPrefix = next; i++; break; + case '--pixels-per-meter': + opts.pixelsPerMeter = parseInt(next, 10); i++; break; case '--verbose': opts.verbose = true; break; default: console.error(`unknown arg: ${a}`); diff --git a/package.json b/package.json index e95b9ae..0bd0f4d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "sporel-tool-atlas-baker", - "version": "0.3.0", - "description": "Bake paired diffuse + height PNG atlases for the Sporel map-lib v0.3.0+ format. v0.2: edge-replicated padding (Rev 4 §13.3), --schema blob-14 E1 slot validation, collision.json sidecar E2 validation + pass-through, alpha-analysis opaque flag. Accepts PNG/WebP/JPG via jimp; v0.3 adds --mode sprite for diffuse-only atlases of irregularly-sized sprites with alias→UV-table sidecar", + "version": "0.4.0", + "description": "Bake paired diffuse + height PNG atlases for the Sporel map-lib v0.3.0+ format. v0.2: edge-replicated padding (Rev 4 §13.3), --schema blob-14 E1 slot validation, collision.json sidecar E2 validation + pass-through, alpha-analysis opaque flag. Accepts PNG/WebP/JPG via jimp; v0.3 adds --mode sprite for diffuse-only atlases of irregularly-sized sprites with alias→UV-table sidecar; v0.4 adds --pixels-per-meter to sprite-mode, embedded as atlas_meta.pixels_per_meter in sprites.uv.json (consumed by render-side scale-resolution per project canonical px/m).", "bin": { "sporel-atlas-baker": "bin/atlas-baker.js" }, diff --git a/src/bake-sprite.js b/src/bake-sprite.js index 21cfa3e..8c53301 100644 --- a/src/bake-sprite.js +++ b/src/bake-sprite.js @@ -17,6 +17,7 @@ async function bakeSprite(opts) { maxSize = 4096, padPx = 1, lockPath, + pixelsPerMeter, verbose, } = opts; @@ -26,6 +27,10 @@ async function bakeSprite(opts) { if (padPx < 0 || !Number.isInteger(padPx)) { throw new Error(`bake-sprite: --pad-px must be non-negative integer, got ${padPx}`); } + if (pixelsPerMeter !== undefined && + (!Number.isFinite(pixelsPerMeter) || pixelsPerMeter <= 0)) { + throw new Error(`bake-sprite: --pixels-per-meter must be positive number, got ${pixelsPerMeter}`); + } const scan = scanSpriteSourceDir(inDir, { stripPrefix }); if (scan.errors.length > 0) { @@ -93,6 +98,7 @@ async function bakeSprite(opts) { boundsW: pack.boundsW, boundsH: pack.boundsH, lock, + pixelsPerMeter, }); if (verbose) { diff --git a/src/write-sprite.js b/src/write-sprite.js index 7d4e212..c25532c 100644 --- a/src/write-sprite.js +++ b/src/write-sprite.js @@ -19,7 +19,7 @@ async function composeSpriteAtlas(boundsW, boundsH, placed) { return atlas; } -function buildUvJson(atlasId, boundsW, boundsH, placed) { +function buildUvJson(atlasId, boundsW, boundsH, placed, pixelsPerMeter) { const sprites = {}; for (const p of placed) { sprites[p.alias] = { @@ -30,15 +30,22 @@ function buildUvJson(atlasId, boundsW, boundsH, placed) { source_file: p.sourceFile, }; } - return JSON.stringify({ + // Build payload with stable key order; only emit atlas_meta when at + // least one meta-field is set, so unannotated bakes stay byte-equal + // to the v0.3.0 output (no diff for unchanged callers). + const payload = { atlas_id: atlasId, atlas_size: [boundsW, boundsH], - sprites, - }, null, 2) + '\n'; + }; + if (pixelsPerMeter !== undefined && pixelsPerMeter !== null) { + payload.atlas_meta = { pixels_per_meter: pixelsPerMeter }; + } + payload.sprites = sprites; + return JSON.stringify(payload, null, 2) + '\n'; } async function writeSpriteOutputs(opts) { - const { atlasId, outDir, placed, boundsW, boundsH, lock } = opts; + const { atlasId, outDir, placed, boundsW, boundsH, lock, pixelsPerMeter } = opts; const atlasDir = path.join(outDir, atlasId); fs.mkdirSync(atlasDir, { recursive: true }); @@ -47,7 +54,7 @@ async function writeSpriteOutputs(opts) { fs.writeFileSync( path.join(atlasDir, 'sprites.uv.json'), - buildUvJson(atlasId, boundsW, boundsH, placed), + buildUvJson(atlasId, boundsW, boundsH, placed, pixelsPerMeter), ); fs.writeFileSync( path.join(atlasDir, 'sprites.atlas.lock.json'),