feat(catalog): generate derived scale 32 with preview and backup restore
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
import sharp from "sharp";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createNitroBundle,
|
||||
parseNitroBundle,
|
||||
} from "@/lib/services/swf/nitro-builder";
|
||||
import { generateScale32 } from "./nitro-scale32";
|
||||
|
||||
const metadata = () => ({
|
||||
name: "chair",
|
||||
assets: {
|
||||
chair_64_a_0_0: { x: -5, y: 7 },
|
||||
chair_64_a_2_0: { source: "chair_64_a_0_0", x: 5, y: 7, flipH: true },
|
||||
},
|
||||
visualizations: [
|
||||
{
|
||||
size: 64,
|
||||
layerCount: 1,
|
||||
layers: { 0: { x: 6, y: -4, z: 3 } },
|
||||
animations: {
|
||||
0: {
|
||||
layers: {
|
||||
0: {
|
||||
frameRepeat: 4,
|
||||
frameSequences: {
|
||||
0: { frames: { 0: { id: 0, x: 6, y: 4, randomX: 8 } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
logic: { model: { dimensions: { x: 2, y: 1, z: 1 } } },
|
||||
spritesheet: {
|
||||
frames: {
|
||||
chair_64_a_0_0: {
|
||||
frame: { x: 0, y: 0, w: 6, h: 8 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 6, h: 8 },
|
||||
sourceSize: { w: 6, h: 8 },
|
||||
},
|
||||
},
|
||||
meta: { size: { w: 6, h: 8 }, scale: "1" },
|
||||
},
|
||||
});
|
||||
async function bundle(json = metadata()) {
|
||||
return createNitroBundle(
|
||||
json,
|
||||
await sharp({
|
||||
create: { width: 6, height: 8, channels: 4, background: "#ff0000" },
|
||||
})
|
||||
.png()
|
||||
.toBuffer(),
|
||||
"chair",
|
||||
);
|
||||
}
|
||||
describe("scale 32 generation", () => {
|
||||
it("preserves originals and adds half-size artwork and offsets without changing timing or geometry", async () => {
|
||||
const source = await bundle();
|
||||
const result = await generateScale32(source);
|
||||
const { json, png } = parseNitroBundle(result.bundle);
|
||||
const m = json as ReturnType<typeof metadata>;
|
||||
expect(m.assets.chair_64_a_0_0).toEqual(metadata().assets.chair_64_a_0_0);
|
||||
expect(m.logic).toEqual(metadata().logic);
|
||||
expect(
|
||||
(json.assets as Record<string, unknown>).chair_32_a_0_0,
|
||||
).toMatchObject({ x: -2, y: 4 });
|
||||
expect(
|
||||
(json.assets as Record<string, unknown>).chair_32_a_2_0,
|
||||
).toMatchObject({
|
||||
source: "chair_32_a_0_0",
|
||||
flipH: true,
|
||||
});
|
||||
expect(
|
||||
(json.spritesheet as { frames: Record<string, { frame: unknown }> })
|
||||
.frames.chair_32_a_0_0.frame,
|
||||
).toMatchObject({ w: 3, h: 4 });
|
||||
expect(m.visualizations[1]).toMatchObject({
|
||||
size: 32,
|
||||
layers: { 0: { x: 3, y: -2, z: 3 } },
|
||||
animations: {
|
||||
0: {
|
||||
layers: {
|
||||
0: {
|
||||
frameRepeat: 4,
|
||||
frameSequences: {
|
||||
0: { frames: { 0: { id: 0, x: 3, y: 2, randomX: 4 } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(
|
||||
await sharp(png)
|
||||
.extract({ left: 0, top: 0, width: 6, height: 8 })
|
||||
.raw()
|
||||
.toBuffer(),
|
||||
).toEqual(await sharp(parseNitroBundle(source).png).raw().toBuffer());
|
||||
});
|
||||
it("never overwrites a native or partial scale32", async () => {
|
||||
const m = metadata();
|
||||
m.visualizations.push({ ...m.visualizations[0], size: 32 });
|
||||
await expect(generateScale32(await bundle(m))).rejects.toThrow("already");
|
||||
});
|
||||
it("rejects missing sprite references", async () => {
|
||||
const m = metadata();
|
||||
m.assets.chair_64_a_2_0.source = "missing";
|
||||
await expect(generateScale32(await bundle(m))).rejects.toThrow("Missing");
|
||||
});
|
||||
});
|
||||
it("restores trimmed sprite geometry before reducing it", async () => {
|
||||
const png = await sharp({
|
||||
create: { width: 2, height: 2, channels: 4, background: "#00ff00" },
|
||||
})
|
||||
.png()
|
||||
.toBuffer();
|
||||
const source = createNitroBundle(
|
||||
{
|
||||
name: "trim",
|
||||
assets: { trim_64_a_0_0: { x: 4, y: 6 } },
|
||||
visualizations: [{ size: 64 }],
|
||||
spritesheet: {
|
||||
frames: {
|
||||
trim_64_a_0_0: {
|
||||
frame: { x: 0, y: 0, w: 2, h: 2 },
|
||||
trimmed: true,
|
||||
sourceSize: { w: 6, h: 8 },
|
||||
spriteSourceSize: { x: 2, y: 4, w: 2, h: 2 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
png,
|
||||
"trim",
|
||||
);
|
||||
const generated = await generateScale32(source);
|
||||
const { json } = parseNitroBundle(generated.bundle);
|
||||
expect(
|
||||
(json.spritesheet as { frames: Record<string, { sourceSize: unknown }> })
|
||||
.frames.trim_32_a_0_0.sourceSize,
|
||||
).toEqual({ w: 3, h: 4 });
|
||||
const pixels = await sharp(
|
||||
Buffer.from(generated.after.split(",")[1], "base64"),
|
||||
)
|
||||
.raw()
|
||||
.toBuffer();
|
||||
expect([...pixels.subarray((2 * 3 + 1) * 4, (2 * 3 + 1) * 4 + 4)]).toEqual([
|
||||
0, 255, 0, 255,
|
||||
]);
|
||||
expect(pixels[3]).toBe(0);
|
||||
});
|
||||
it("unrotates atlas artwork before generating the smaller frame", async () => {
|
||||
const source = createNitroBundle(
|
||||
{
|
||||
name: "rot",
|
||||
assets: { rot_64_a_0_0: {} },
|
||||
visualizations: [{ size: 64 }],
|
||||
spritesheet: {
|
||||
frames: {
|
||||
rot_64_a_0_0: { frame: { x: 0, y: 0, w: 2, h: 6 }, rotated: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
await sharp({
|
||||
create: { width: 2, height: 6, channels: 4, background: "#ff0000" },
|
||||
})
|
||||
.png()
|
||||
.toBuffer(),
|
||||
"rot",
|
||||
);
|
||||
const generated = await generateScale32(source);
|
||||
const size = await sharp(
|
||||
Buffer.from(generated.after.split(",")[1], "base64"),
|
||||
).metadata();
|
||||
expect([size.width, size.height]).toEqual([3, 1]);
|
||||
});
|
||||
Reference in New Issue
Block a user