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}`); }); });