Files
VFXdemo/server.js
2026-04-01 20:29:33 +08:00

176 lines
5.5 KiB
JavaScript

const express = require("express");
const fs = require("fs/promises");
const path = require("path");
const crypto = require("crypto");
const app = express();
const PORT = process.env.PORT || 5180;
const DB_PATH = path.join(__dirname, "data", "shaders.json");
app.use(express.json({ limit: "1mb" }));
app.use(express.static(__dirname));
async function ensureDb() {
await fs.mkdir(path.dirname(DB_PATH), { recursive: true });
try {
await fs.access(DB_PATH);
} catch {
await fs.writeFile(DB_PATH, "[]", "utf8");
}
}
async function readDb() {
await ensureDb();
const raw = await fs.readFile(DB_PATH, "utf8");
return JSON.parse(raw);
}
async function writeDb(list) {
await ensureDb();
await fs.writeFile(DB_PATH, JSON.stringify(list, null, 2), "utf8");
}
function extractFunctionSection(code) {
const endMarker = code.indexOf("void ANGLE__0_main");
const end = endMarker >= 0 ? endMarker : code.length;
const firstFn = code.search(
/(metal::float[234](x2)?|float|void)\s+[A-Za-z_][A-Za-z0-9_]*\s*\([^)]*\)\s*\{/m
);
if (firstFn < 0 || firstFn >= end) return "";
return code.slice(firstFn, end);
}
function convertAngleMetalToGlsl(code) {
let section = extractFunctionSection(code);
if (!section || !section.includes("_umainImage")) return "";
section = section
.replace(
/void _umainImage\s*\(\s*constant ANGLE_UserUniforms\s*&\s*ANGLE_userUniforms\s*,\s*thread metal::float4\s*&\s*([A-Za-z0-9_]+)\s*,\s*metal::float2\s*([A-Za-z0-9_]+)\s*\)/g,
"void _umainImage(out vec4 $1, vec2 $2)"
)
.replace(
/float _umap\s*\(\s*constant ANGLE_UserUniforms\s*&\s*ANGLE_userUniforms\s*,\s*metal::float3\s*([A-Za-z0-9_]+)\s*\)/g,
"float _umap(vec3 $1)"
)
.replace(
/metal::float4 _urm\s*\(\s*constant ANGLE_UserUniforms\s*&\s*ANGLE_userUniforms\s*,\s*metal::float3\s*([A-Za-z0-9_]+)\s*,\s*metal::float3\s*([A-Za-z0-9_]+)\s*\)/g,
"vec4 _urm(vec3 $1, vec3 $2)"
)
.replace(/_umap\s*\(\s*ANGLE_userUniforms\s*,/g, "_umap(")
.replace(/_urm\s*\(\s*ANGLE_userUniforms\s*,/g, "_urm(")
.replace(/ANGLE_userUniforms\._uiResolution/g, "iResolution")
.replace(/ANGLE_userUniforms\._uiTime/g, "iTime")
.replace(/metal::fast::normalize/g, "normalize")
.replace(/metal::/g, "")
.replace(/\bthread\b/g, "")
.replace(/\bconstant\b/g, "")
.replace(/\buint32_t\b/g, "uint")
.replace(/;\s*;/g, ";");
const cleaned = [
"void ANGLE_loopForwardProgress() {}",
section,
"void mainImage(out vec4 fragColor, in vec2 fragCoord) { _umainImage(fragColor, fragCoord); }",
].join("\n");
return cleaned;
}
function normalizeIncomingCode(code) {
if (!code || typeof code !== "string") return { error: "code 不能为空", normalized: "" };
if (code.includes("metal::") || code.includes("[[function_constant")) {
const converted = convertAngleMetalToGlsl(code);
if (!converted) {
return { error: "自动解构失败:请检查粘贴内容是否完整。", normalized: "" };
}
return { error: "", normalized: converted };
}
if (!code.includes("mainImage")) {
return { error: "code 必须包含 mainImage", normalized: "" };
}
return { error: "", normalized: code };
}
async function autoNormalizeStoredShaders() {
const shaders = await readDb();
let changed = false;
const next = shaders.map((item) => {
const code = String(item.code || "");
if (code.includes("metal::") || code.includes("[[function_constant")) {
const { error, normalized } = normalizeIncomingCode(code);
if (!error && normalized) {
changed = true;
return {
...item,
code: normalized,
updatedAt: new Date().toISOString(),
sourceFormat: "angle-metal-auto-converted",
};
}
}
return item;
});
if (changed) await writeDb(next);
}
app.get("/api/shaders", async (_req, res) => {
try {
const shaders = await readDb();
res.json(shaders);
} catch {
res.status(500).json({ error: "读取失败" });
}
});
app.post("/api/shaders", async (req, res) => {
const { name, author = "unknown", code } = req.body || {};
if (!name || typeof name !== "string") {
return res.status(400).json({ error: "name 必填" });
}
const { error: parseError, normalized } = normalizeIncomingCode(code);
if (parseError) {
return res.status(400).json({ error: parseError });
}
try {
const shaders = await readDb();
const item = {
id: crypto.randomUUID(),
name: name.trim(),
author: String(author || "unknown").trim() || "unknown",
code: normalized,
views: Math.floor(3000 + Math.random() * 22000),
likes: Math.floor(40 + Math.random() * 700),
createdAt: new Date().toISOString(),
sourceFormat: code.includes("metal::") ? "angle-metal-auto-converted" : "glsl",
};
shaders.unshift(item);
await writeDb(shaders);
res.status(201).json(item);
} catch {
res.status(500).json({ error: "保存失败" });
}
});
app.delete("/api/shaders/:id", async (req, res) => {
try {
const shaders = await readDb();
const next = shaders.filter((it) => it.id !== req.params.id);
if (next.length === shaders.length) {
return res.status(404).json({ error: "未找到该 shader" });
}
await writeDb(next);
res.status(204).end();
} catch {
res.status(500).json({ error: "删除失败" });
}
});
ensureDb().then(() => {
autoNormalizeStoredShaders().catch(() => {});
app.listen(PORT, () => {
console.log(`Server running: http://localhost:${PORT}`);
});
});