176 lines
5.5 KiB
JavaScript
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}`);
|
|
});
|
|
});
|