export const ASSET_FAMILIES = [ 'color', 'diffuse', 'roughness', 'normal', 'metalness', 'height', 'opacity', 'orm', 'ao', ] as const export type AssetFamily = typeof ASSET_FAMILIES[number] const ASSET_FAMILY_BY_KEY = new Map(ASSET_FAMILIES.map((family) => [family.toLowerCase(), family])) const FORBIDDEN_ASSET_FAMILY_ALIASES: ReadonlyMap = new Map([ ['basecolor', 'color'], ['base_color', 'color'], ['normalopengl', 'normal'], ['normal_opengl', 'normal'], ['metallic', 'metalness'], ['occlusionroughnessmetallic', 'roughness'], ['occlusion_roughness_metallic', 'roughness'], ]) const EXPORTED_SUFFIX_ALIASES: Array<{ suffix: string; family: AssetFamily }> = [ { suffix: 'occlusionroughnessmetallic', family: 'orm' }, { suffix: 'occlusion_roughness_metallic', family: 'orm' }, { suffix: 'normal_opengl', family: 'normal' }, { suffix: 'normalopengl', family: 'normal' }, { suffix: 'base_color', family: 'color' }, { suffix: 'basecolor', family: 'color' }, { suffix: 'mixed_ao', family: 'ao' }, { suffix: 'metallic', family: 'metalness' }, { suffix: 'roughness', family: 'roughness' }, { suffix: 'normal', family: 'normal' }, { suffix: 'height', family: 'height' }, { suffix: 'opacity', family: 'opacity' }, { suffix: 'diffuse', family: 'diffuse' }, { suffix: 'color', family: 'color' }, ] export function getAssetFamily(value: string): AssetFamily | undefined { return ASSET_FAMILY_BY_KEY.get(value.toLowerCase()) } export function getForbiddenAssetFamilyAlias(value: string): AssetFamily | undefined { return FORBIDDEN_ASSET_FAMILY_ALIASES.get(value.toLowerCase()) } function getFileStem(filename: string) { return filename.replace(/\.[^.]+$/, '') } function getFileExtension(filename: string) { return filename.split('.').pop()?.toLowerCase() || '' } function normalizeTargetName(target: string) { return target .trim() .replace(/[\s-]+/g, '_') .replace(/_+/g, '_') .replace(/^_+|_+$/g, '') } function buildTextureFilename(family: AssetFamily, target: string, extension: string) { const normalizedTarget = normalizeTargetName(target) return `${family}${normalizedTarget ? `_${normalizedTarget}` : ''}.${extension}` } function getExportedTextureAlias(stem: string) { const lowerStem = stem.toLowerCase() for (const alias of EXPORTED_SUFFIX_ALIASES) { const lowerSuffix = alias.suffix.toLowerCase() if (lowerStem === lowerSuffix) { return { family: alias.family, target: '' } } if (lowerStem.endsWith(`_${lowerSuffix}`)) { return { family: alias.family, target: stem.slice(0, -lowerSuffix.length - 1), } } } return null } export function normalizeTextureFilename(filename: string) { const stem = getFileStem(filename) const extension = getFileExtension(filename) const [prefix, ...targetParts] = stem.split('_') const family = getAssetFamily(prefix) if (family) { return buildTextureFilename(family, targetParts.join('_'), extension) } const exportedAlias = getExportedTextureAlias(stem) if (exportedAlias) { return buildTextureFilename(exportedAlias.family, exportedAlias.target, extension) } return null } export function getTextureNamingError(filename: string) { const stem = getFileStem(filename) const [prefix, ...targetParts] = stem.split('_') const family = getAssetFamily(prefix) const extension = getFileExtension(filename) if (normalizeTextureFilename(filename)) return null if (family && targetParts.every(Boolean)) return null const aliasSuggestion = getForbiddenAssetFamilyAlias(prefix) if (aliasSuggestion && targetParts.every(Boolean)) { const target = targetParts.join('_') return `Convention invalide : ${filename}. Utilisez ${aliasSuggestion}_${target}.${extension} pour cibler "${target}", ou ${aliasSuggestion}.${extension} pour tout le modele.` } const reversedParts = stem.split('_') const reversedFamily = reversedParts.length > 1 ? getAssetFamily(reversedParts[reversedParts.length - 1]) : undefined const reversedAliasSuggestion = reversedParts.length > 1 ? getForbiddenAssetFamilyAlias(reversedParts[reversedParts.length - 1]) : undefined if (reversedFamily) { const target = reversedParts.slice(0, -1).join('_') return `Convention invalide : ${filename}. Utilisez ${reversedFamily}_${target}.${extension} pour cibler "${target}", ou ${reversedFamily}.${extension} pour tout le modele.` } if (reversedAliasSuggestion) { const target = reversedParts.slice(0, -1).join('_') return `Convention invalide : ${filename}. Utilisez ${reversedAliasSuggestion}_${target}.${extension} pour cibler "${target}", ou ${reversedAliasSuggestion}.${extension} pour tout le modele.` } return `Asset inconnu : ${filename}. Familles autorisees : ${formatAssetFamilies()}. Utilisez asset.png pour tout le modele ou asset_objet.png pour cibler un objet.` } export function formatAssetFamilies() { return ASSET_FAMILIES.join(', ') }