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 isAngleLike(code) { return ( code.includes("_umainImage") && (code.includes("ANGLE_") || code.includes("metal::") || code.includes("[[function_constant") || code.includes("float2") || code.includes("float3")) ); } function extractFunctionBlocks(code) { const blocks = []; const sig = /(?:^|\n)\s*(?:void|float|vec[234]|mat[234]|float[234](?:x[234])?)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\([^;]*\)\s*\{/g; let m; while ((m = sig.exec(code)) !== null) { const name = m[1]; if (name === "main0" || name === "ANGLE__0_main") continue; let i = code.indexOf("{", m.index); if (i < 0) continue; let depth = 0; let end = -1; for (let p = i; p < code.length; p++) { const ch = code[p]; if (ch === "{") depth++; else if (ch === "}") { depth--; if (depth === 0) { end = p + 1; break; } } } if (end > i) { blocks.push(code.slice(m.index, end)); sig.lastIndex = end; } } return blocks; } function convertAngleLikeToGlsl(code) { let s = code; const cut = s.indexOf("fragment ANGLE_FragmentOut"); if (cut > 0) s = s.slice(0, cut); s = s .replace(/metal::fast::normalize/g, "normalize") .replace(/metal::/g, "") .replace(/\[\[[^\]]+\]\]/g, "") .replace(/\bthread\b/g, "") .replace(/\bconstant\b/g, "") .replace(/\bprecise::tanh\b/g, "tanh") .replace(/\bfloat2x2\b/g, "mat2") .replace(/\bfloat3x3\b/g, "mat3") .replace(/\bfloat4x4\b/g, "mat4") .replace(/\bfloat2\b/g, "vec2") .replace(/\bfloat3\b/g, "vec3") .replace(/\bfloat4\b/g, "vec4") .replace(/\bint2\b/g, "ivec2") .replace(/\bint3\b/g, "ivec3") .replace(/\bint4\b/g, "ivec4") .replace(/\buint2\b/g, "uvec2") .replace(/\buint3\b/g, "uvec3") .replace(/\buint4\b/g, "uvec4") .replace(/\buint32_t\b/g, "uint") .replace(/\bANGLE_userUniforms\._uiResolution\b/g, "iResolution") .replace(/\bANGLE_userUniforms\._uiTime\b/g, "iTime") .replace(/\bANGLE_userUniforms\._uiMouse\b/g, "iMouse") .replace(/\bANGLE_userUniforms\./g, "") .replace(/ANGLE_texture\([^)]*\)/g, "vec4(0.0)") .replace(/ANGLE_texelFetch\([^)]*\)/g, "vec4(0.0)") .replace(/\bANGLE_mod\(/g, "mod(") .replace(/\bANGLE_out\(([^)]+)\)/g, "$1") .replace(/\bANGLE_swizzle_ref\(([^)]+)\)/g, "$1") .replace(/\bANGLE_elem_ref\(([^)]+)\)/g, "$1") .replace(/\bANGLE_addressof\(([^)]+)\)/g, "$1") .replace(/\bANGLE_UserUniforms\s*&\s*ANGLE_userUniforms\s*,?/g, "") .replace(/\bANGLE_TextureEnvs\s*&\s*ANGLE_textureEnvs\s*,?/g, "") .replace(/\bANGLE_NonConstGlobals\s*&\s*ANGLE_nonConstGlobals\s*,?/g, "") .replace(/\(\s*,/g, "(") .replace(/,\s*,/g, ",") .replace(/,\s*\)/g, ")") .replace(/(\d+\.\d+|\d+)f\b/g, "$1") .replace(/;\s*;/g, ";"); s = s.replace(/void\s+_umainImage\s*\([^)]*\)/g, "void _umainImage(out vec4 _ufragColor, vec2 _ufragCoord)"); const blocks = extractFunctionBlocks(s).filter((b) => /(ANGLE_sc|ANGLE_sd|_u|mainImage|ANGLE_loopForwardProgress|_umainImage)/.test(b) ); if (!blocks.length || !s.includes("_umainImage")) return ""; const hasMainImage = blocks.some((b) => /void\s+mainImage\s*\(/.test(b)); const withoutDupLoop = []; let seenLoop = false; for (const b of blocks) { if (/void\s+ANGLE_loopForwardProgress\s*\(/.test(b)) { if (seenLoop) continue; seenLoop = true; withoutDupLoop.push("void ANGLE_loopForwardProgress() {}"); continue; } withoutDupLoop.push(b); } if (!hasMainImage) { withoutDupLoop.push("void mainImage(out vec4 fragColor, in vec2 fragCoord) { _umainImage(fragColor, fragCoord); }"); } return withoutDupLoop.join("\n\n"); } function normalizeIncomingCode(code) { if (!code || typeof code !== "string") return { error: "code 不能为空", normalized: "" }; if (isAngleLike(code)) { const converted = convertAngleLikeToGlsl(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 (isAngleLike(code) || code.includes("struct ANGLE_") || item.sourceFormat === "angle-metal-auto-converted") { 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 { let shaders = await readDb(); // Runtime safeguard: always return normalized code for display layer. shaders = shaders.map((item) => { const { error, normalized } = normalizeIncomingCode(String(item.code || "")); if (error || !normalized) return item; if (normalized === item.code) return item; return { ...item, code: normalized, sourceFormat: "angle-metal-auto-converted" }; }); 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: isAngleLike(code) ? "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}`); }); });