feat: cache atlas and update nuxt version to win on compilation time
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
import { defineNuxtModule, useLogger } from "@nuxt/kit";
|
||||
import { readdir, readFile, writeFile } from "fs/promises";
|
||||
import { join, relative, parse } from "path";
|
||||
import { mkdir, readdir, readFile, writeFile } from "fs/promises";
|
||||
import { dirname, join, relative, parse } from "path";
|
||||
import { existsSync, watch } from "fs";
|
||||
import { execSync } from "child_process";
|
||||
import { execFile } from "child_process";
|
||||
import { createHash } from "crypto";
|
||||
import { promisify } from "util";
|
||||
import sharp from "sharp";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
type AtlasRect = {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -79,6 +83,7 @@ export default defineNuxtModule({
|
||||
const templateFile = join(rootDir, "app/composables/useAssets.ts.in");
|
||||
const outputFile = join(rootDir, "app/composables/useAssets.ts");
|
||||
const atlasOutputPath = join(rootDir, "public/nds/atlas.webp");
|
||||
const atlasHashFile = join(rootDir, "node_modules/.cache/asset-generator/atlas-hash");
|
||||
|
||||
const loadImages = async (
|
||||
paths: string[],
|
||||
@@ -88,10 +93,8 @@ export default defineNuxtModule({
|
||||
await Promise.all(
|
||||
paths.map(async (path) => {
|
||||
try {
|
||||
const [metadata, buffer] = await Promise.all([
|
||||
sharp(path).metadata(),
|
||||
sharp(path).png().toBuffer(),
|
||||
]);
|
||||
const buffer = await readFile(path);
|
||||
const metadata = await sharp(buffer).metadata();
|
||||
|
||||
if (metadata.width && metadata.height) {
|
||||
images.set(path, {
|
||||
@@ -256,7 +259,10 @@ ${sp} }`;
|
||||
|
||||
const generateAssets = async () => {
|
||||
try {
|
||||
const imagePaths = await scanByExt(imagesDir, IMAGE_EXTENSIONS);
|
||||
// sorted so packing (and therefore the atlas hash) is deterministic
|
||||
const imagePaths = (
|
||||
await scanByExt(imagesDir, IMAGE_EXTENSIONS)
|
||||
).sort();
|
||||
const modelPaths = await scanByExt(modelsDir, MODEL_EXTENSIONS);
|
||||
const audioPaths = await scanByExt(audioDir, AUDIO_EXTENSIONS);
|
||||
|
||||
@@ -266,43 +272,61 @@ ${sp} }`;
|
||||
let atlasHash = "";
|
||||
let atlasWidth = 0;
|
||||
let atlasHeight = 0;
|
||||
let atlasCached = false;
|
||||
|
||||
if (imagePaths.length > 0) {
|
||||
const imageMap = await loadImages(imagePaths);
|
||||
const images = Array.from(imageMap.values());
|
||||
const images = imagePaths.flatMap((path) => imageMap.get(path) ?? []);
|
||||
|
||||
const { rects, width, height } = packImages(images);
|
||||
atlasWidth = width;
|
||||
atlasHeight = height;
|
||||
|
||||
const atlasBuffer = await sharp({
|
||||
create: {
|
||||
width,
|
||||
height,
|
||||
channels: 4,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||
},
|
||||
})
|
||||
.composite(
|
||||
images.map((img) => {
|
||||
const rect = rects.get(img.path)!;
|
||||
return { input: img.buffer, left: rect.x, top: rect.y };
|
||||
}),
|
||||
)
|
||||
.webp({ lossless: true })
|
||||
.toBuffer();
|
||||
|
||||
await writeFile(atlasOutputPath, atlasBuffer);
|
||||
|
||||
execSync(
|
||||
`magick "${atlasOutputPath}" -define webp:lossless=true -define webp:method=6 "${atlasOutputPath}"`,
|
||||
{ stdio: "pipe" },
|
||||
);
|
||||
|
||||
atlasHash = Math.random().toString(36).slice(2, 10);
|
||||
|
||||
const imageTree = buildImageTree(imagePaths, rects);
|
||||
imageCode = generateImageCode(imageTree);
|
||||
|
||||
// imageCode covers names and layout, buffers cover pixels
|
||||
const hash = createHash("sha256").update(imageCode);
|
||||
for (const img of images) hash.update(img.buffer);
|
||||
atlasHash = hash.digest("hex").slice(0, 8);
|
||||
|
||||
const cachedHash = await readFile(atlasHashFile, "utf-8").catch(
|
||||
() => null,
|
||||
);
|
||||
atlasCached = cachedHash === atlasHash && existsSync(atlasOutputPath);
|
||||
|
||||
if (!atlasCached) {
|
||||
const atlasBuffer = await sharp({
|
||||
create: {
|
||||
width,
|
||||
height,
|
||||
channels: 4,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||
},
|
||||
})
|
||||
.composite(
|
||||
images.map((img) => {
|
||||
const rect = rects.get(img.path)!;
|
||||
return { input: img.buffer, left: rect.x, top: rect.y };
|
||||
}),
|
||||
)
|
||||
.webp({ lossless: true })
|
||||
.toBuffer();
|
||||
|
||||
await writeFile(atlasOutputPath, atlasBuffer);
|
||||
|
||||
await execFileAsync("magick", [
|
||||
atlasOutputPath,
|
||||
"-define",
|
||||
"webp:lossless=true",
|
||||
"-define",
|
||||
"webp:method=6",
|
||||
atlasOutputPath,
|
||||
]);
|
||||
|
||||
await mkdir(dirname(atlasHashFile), { recursive: true });
|
||||
await writeFile(atlasHashFile, atlasHash, "utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
const modelTree = buildModelTree(modelPaths);
|
||||
@@ -319,9 +343,16 @@ ${sp} }`;
|
||||
.replace("{{MODELS}}", modelCode)
|
||||
.replace("{{AUDIO}}", audioCode);
|
||||
|
||||
await writeFile(outputFile, code, "utf-8");
|
||||
// rewriting an identical file would still invalidate it in Vite
|
||||
const previousCode = await readFile(outputFile, "utf-8").catch(
|
||||
() => null,
|
||||
);
|
||||
if (previousCode !== code) {
|
||||
await writeFile(outputFile, code, "utf-8");
|
||||
}
|
||||
|
||||
logger.success(
|
||||
`Generated assets with ${imagePaths.length} images (${atlasWidth}x${atlasHeight}), ${modelPaths.length} models and ${audioPaths.length} audio files`,
|
||||
`Generated assets with ${imagePaths.length} images (${atlasWidth}x${atlasHeight}${atlasCached ? ", cached" : ""}), ${modelPaths.length} models and ${audioPaths.length} audio files`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Error generating assets:", error);
|
||||
|
||||
Reference in New Issue
Block a user