Files
upload-gltf/lib/validate-folder.ts
T
2026-05-12 23:49:30 +02:00

126 lines
3.9 KiB
TypeScript

import { ASSET_EXTENSIONS, TEXTURE_EXTENSIONS } from '@/lib/constants'
import { getTextureNamingError } from '@/lib/asset-naming'
import { getErrorMessage, isRecord } from '@/lib/guards'
import type { TextureFile } from '@/lib/client-types'
const SUPPORT_FILE_EXT_ARRAY = [...TEXTURE_EXTENSIONS, ...ASSET_EXTENSIONS]
interface GltfBufferReference {
uri?: string
}
interface GltfJson {
buffers?: GltfBufferReference[]
}
type ValidationResult =
| { ok: true; model: File; textures: TextureFile[]; warnings: string[] }
| { ok: false; errors: string[] }
function isGltfBufferReference(value: unknown): value is GltfBufferReference {
return isRecord(value) && (value.uri === undefined || typeof value.uri === 'string')
}
function isGltfJson(value: unknown): value is GltfJson {
if (!isRecord(value)) return false
if (value.buffers === undefined) return true
return Array.isArray(value.buffers) && value.buffers.every(isGltfBufferReference)
}
function getReferencedBufferNames(gltf: GltfJson) {
return (gltf.buffers || [])
.map((buffer) => (typeof buffer.uri === 'string' ? buffer.uri : undefined))
.filter((uri): uri is string => typeof uri === 'string' && uri.length > 0)
.filter((uri) => !uri.startsWith('data:'))
.map((uri) => decodeURIComponent(uri.split(/[?#]/)[0] || '').split(/[\\/]/).pop()?.toLowerCase())
.filter((filename): filename is string => Boolean(filename))
}
function getFileExtension(filename: string) {
return filename.slice(filename.lastIndexOf('.')).toLowerCase()
}
async function getGltfWarnings(model: File, supportFiles: File[]) {
const warnings: string[] = []
let parsed: unknown
try {
parsed = JSON.parse(await model.text())
} catch {
throw new Error('model.gltf contient un JSON invalide')
}
if (!isGltfJson(parsed)) {
throw new Error('model.gltf a une structure invalide')
}
const supportFilenames = new Set(supportFiles.map((file) => file.name.toLowerCase()))
const binFiles = supportFiles.filter((file) => file.name.toLowerCase().endsWith('.bin'))
for (const bufferName of getReferencedBufferNames(parsed)) {
if (!bufferName.endsWith('.bin') || supportFilenames.has(bufferName)) continue
if (binFiles.length === 1) {
warnings.push(
`model.gltf reference ${bufferName} mais le dossier contient ${binFiles[0].name}. La preview peut utiliser ${binFiles[0].name}, mais l'upload final risque d'etre casse. Veillez changer le nom du fichier .bin pour ne pas casser l'export.`,
)
continue
}
warnings.push(`model.gltf reference ${bufferName}, mais ce fichier .bin est absent du dossier.`)
}
return warnings
}
export async function validateFolder(files: File[]): Promise<ValidationResult> {
const textures: TextureFile[] = []
const errors: string[] = []
const modelFiles = files.filter((f) => {
const name = f.name.toLowerCase()
return name === 'model.gltf'
})
if (modelFiles.length === 0) {
return { ok: false, errors: ['model.gltf manquant (obligatoire)'] }
}
if (modelFiles.length > 1) {
return { ok: false, errors: ['Un seul fichier model.gltf est autorise'] }
}
const supportFiles = files.filter((f) => {
const ext = getFileExtension(f.name)
return SUPPORT_FILE_EXT_ARRAY.includes(ext)
})
const textureNamingErrors = supportFiles
.filter((file) => TEXTURE_EXTENSIONS.has(getFileExtension(file.name)))
.map((file) => getTextureNamingError(file.name))
.filter((error): error is string => Boolean(error))
errors.push(...textureNamingErrors)
for (const tf of supportFiles) {
textures.push({ name: tf.name, file: tf })
}
if (errors.length > 0) {
return { ok: false, errors }
}
let warnings: string[] = []
try {
warnings = await getGltfWarnings(modelFiles[0], supportFiles)
} catch (err) {
errors.push(getErrorMessage(err, 'model.gltf invalide'))
}
if (errors.length > 0) {
return { ok: false, errors }
}
return { ok: true, model: modelFiles[0], textures, warnings }
}