import { extname } from 'path' import { compressTextureBuffer } from '@/lib/texture-compression' import { classifyAssetCategory } from '@/lib/asset-classification' import { normalizeTextureFilename } from '@/lib/asset-naming' import { TEXTURE_EXTENSIONS } from '@/lib/constants' import { getModelAssetPath } from '@/lib/model-paths' import type { ParsedFile, PreparedAssetSummary, PushFile } from '@/lib/types' interface PrepareGitAssetsParams { folderName: string parsedFiles: ParsedFile[] } interface PrepareGitAssetsResult { filesToPush: PushFile[] modelFilename: string assetSummaries: PreparedAssetSummary[] compressed: boolean compressionError?: string } function getTextureFilenameMap(parsedFiles: ParsedFile[]) { const filenameMap = new Map() const normalizedGroups = new Map>() for (const file of parsedFiles) { const ext = extname(file.filename).toLowerCase() if (!TEXTURE_EXTENSIONS.has(ext)) continue const normalizedFilename = normalizeTextureFilename(file.filename) if (!normalizedFilename) continue const normalizedKey = normalizedFilename.toLowerCase() const group = normalizedGroups.get(normalizedKey) || [] group.push({ original: file.filename, normalized: normalizedFilename }) normalizedGroups.set(normalizedKey, group) } for (const group of normalizedGroups.values()) { if (group.length > 1) continue const [{ original, normalized }] = group filenameMap.set(original.toLowerCase(), normalized) } return filenameMap } function getReferencedFilename(uri: string) { const cleanUri = decodeURIComponent(uri.split(/[?#]/)[0] || '') return cleanUri.split(/[\\/]/).pop()?.toLowerCase() } function rewriteGltfUris(value: unknown, filenameMap: Map): unknown { if (Array.isArray(value)) { return value.map((entry) => rewriteGltfUris(entry, filenameMap)) } if (!value || typeof value !== 'object') return value const rewritten: Record = {} for (const [key, entry] of Object.entries(value)) { if (key === 'uri' && typeof entry === 'string') { const filename = getReferencedFilename(entry) rewritten[key] = filename ? filenameMap.get(filename) || entry : entry continue } rewritten[key] = rewriteGltfUris(entry, filenameMap) } return rewritten } function prepareModelBuffer(buffer: Buffer, filenameMap: Map) { if (filenameMap.size === 0) return buffer const parsed: unknown = JSON.parse(buffer.toString('utf-8')) return Buffer.from(JSON.stringify(rewriteGltfUris(parsed, filenameMap), null, 2), 'utf-8') } export async function prepareGitAssets({ folderName, parsedFiles, }: PrepareGitAssetsParams): Promise { const filesToPush: PushFile[] = [] const assetSummaries: PreparedAssetSummary[] = [] let modelFilename = '' let compressed = false let compressionError: string | undefined const textureFilenameMap = getTextureFilenameMap(parsedFiles) for (const pf of parsedFiles) { let content = pf.buffer let filename = pf.filename if (pf.isModel) { content = prepareModelBuffer(pf.buffer, textureFilenameMap) modelFilename = pf.filename assetSummaries.push({ filename, kind: 'model', compressed: false, }) } else { filename = textureFilenameMap.get(pf.filename.toLowerCase()) || pf.filename const categoryFilename = textureFilenameMap.get(pf.filename.toLowerCase()) || normalizeTextureFilename(pf.filename) || pf.filename const category = classifyAssetCategory(categoryFilename) const textureResult = await compressTextureBuffer(filename, pf.buffer) content = textureResult.buffer compressed ||= textureResult.compressed if (textureResult.error && !compressionError) { compressionError = textureResult.error } assetSummaries.push({ filename, kind: category === 'assets' ? 'asset' : 'texture', category, compressed: textureResult.compressed, }) } filesToPush.push({ path: getModelAssetPath(folderName, filename), contentBase64: content.toString('base64'), }) } return { filesToPush, modelFilename, assetSummaries, compressed, compressionError, } }