fix(furni): generate scale 32 from bundles with .png frame keys

resolveNitroFrame now matches spritesheet frame keys that carry a .png
suffix or namespaced naming, and isScale/scaleName preserve that suffix.
Broken source sprites (missing frames or references to icon artwork) are
skipped and reported instead of aborting the whole generation, and the
studio UI surfaces the skipped count.
This commit is contained in:
openhands committed 2026-09-12 12:46:17 +02:00
1 parent 0842788a28
commit 4d720ee03c
5 files changed
+119 -29

No files matched your search

+53 -2
View File
@@ -105,10 +105,18 @@ describe("scale 32 generation", () => {
m.visualizations.push({ ...m.visualizations[0], size: 32 });
await expect(generateScale32(await bundle(m))).rejects.toThrow("already");
});
it("rejects missing sprite references", async () => {
it("skips sprites whose artwork is missing and reports them", async () => {
const m = metadata();
m.assets.chair_64_a_2_0.source = "missing";
await expect(generateScale32(await bundle(m))).rejects.toThrow("Missing");
const result = await generateScale32(await bundle(m));
expect(result.skipped).toBe(1);
const { json } = parseNitroBundle(result.bundle);
expect(
(json.assets as Record<string, unknown>).chair_32_a_0_0,
).toBeDefined();
expect(
(json.assets as Record<string, unknown>).chair_32_a_2_0,
).toBeUndefined();
});
});
it("restores trimmed sprite geometry before reducing it", async () => {
@@ -211,3 +219,46 @@ it("fills an empty scale declaration and retains namespaced frame naming", async
.chair_chair_32_a_0_0,
).toBeDefined();
});
it("handles frame keys with a .png suffix and namespaced naming", async () => {
const m = metadata();
const frames = m.spritesheet.frames as Record<string, unknown>;
for (const key of Object.keys(frames) as string[]) {
frames[`chair_${key}.png`] = frames[key];
delete frames[key];
}
const source = createNitroBundle(
m,
await sharp({
create: { width: 6, height: 8, channels: 4, background: "#ff0000" },
})
.png()
.toBuffer(),
"chair",
);
const result = parseNitroBundle((await generateScale32(source)).bundle);
expect(
(result.json.spritesheet as { frames: Record<string, unknown> }).frames[
"chair_chair_32_a_0_0.png"
],
).toBeDefined();
expect(
(result.json.assets as Record<string, unknown>).chair_32_a_0_0,
).toBeDefined();
});
it("rejects bundles that already contain a non-empty scale32 frame", async () => {
const m = metadata();
const frames = m.spritesheet.frames as Record<string, unknown>;
for (const key of Object.keys(frames) as string[]) {
frames[`chair_${key}.png`] = frames[key];
delete frames[key];
}
(m.assets as Record<string, unknown>).chair_32_a_0_0 = { x: 0, y: 0 };
frames["chair_chair_32_a_0_0.png"] = {
frame: { x: 0, y: 0, w: 3, h: 4 },
rotated: false,
trimmed: false,
spriteSourceSize: { x: 0, y: 0, w: 3, h: 4 },
sourceSize: { w: 3, h: 4 },
};
await expect(generateScale32(await bundle(m))).rejects.toThrow("already");
});