From fa77c484f91082265497d9a507a3b2b1aecb5149 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Mon, 27 Apr 2026 23:29:47 +0200 Subject: [PATCH] fix: enforce asset naming convention --- README.md | 11 +++++++++++ lib/asset-classification.ts | 13 ++++++++----- lib/asset-naming.ts | 24 ++++++++++++++++++++++++ lib/validate-folder.ts | 36 +++++++++++++++++++++++++++++++++++- 4 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 lib/asset-naming.ts diff --git a/README.md b/README.md index b17043b..fa60782 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,16 @@ Access the app at `http://localhost:3000` > Opacity helper textures can be named `opacity.png` for the whole model or `opacity_part-name.png` to target a mesh/material whose name contains `part-name`; alpha-channel PNGs are converted to alpha maps for the preview. > If `model.gltf` references a missing `.bin` but the folder contains exactly one other `.bin`, the preview can use it as a local fallback and shows a warning because the final upload may still be broken until the `.bin` filename matches the GLTF reference. +### Asset naming convention + +Texture filenames must start with a known asset family. Use `asset.png` to apply a texture to the whole model, or `asset_object.png` to target a specific object. + +Allowed families are defined in `lib/asset-naming.ts`: `baseColor`, `color`, `roughness`, `normal`, `normalOpengl`, `metallic`, `metalness`, `occlusionRoughnessMetallic`, `height`, `opacity`. + +Valid examples: `color.png`, `baseColor_lampe.png`, `normalOpengl_cable1.png`, `opacity_lampe.png`. + +Invalid examples: `lampe_opacity.png`, `cable1_base_color.png`, `normal_opengl_cable1.png`. Invalid or unknown asset names block the upload. + ### Production (Coolify / Docker) ```bash @@ -238,6 +248,7 @@ lib/ ├── upload-staging.ts # Temporary server-side staging and prepared asset reuse ├── upload-lock.ts # Lightweight in-memory per-folder upload lock ├── asset-classification.ts # Group assets by family for commit messages +├── asset-naming.ts # Allowed asset families and naming convention helpers ├── commit-message.ts # Commit message builder ├── parse-upload.ts # FormData parser + validation ├── validate-folder.ts # Client-side folder validation (discriminated union) diff --git a/lib/asset-classification.ts b/lib/asset-classification.ts index 57a61d5..ca084b8 100644 --- a/lib/asset-classification.ts +++ b/lib/asset-classification.ts @@ -1,21 +1,24 @@ +import { getAssetFamily } from './asset-naming' + export type AssetCategory = 'color' | 'roughness' | 'normal' | 'metalness' | 'assets' export function classifyAssetCategory(filename: string): AssetCategory { - const name = filename.toLowerCase().replace(/\.[^.]+$/, '') + const name = filename.replace(/\.[^.]+$/, '') + const family = getAssetFamily(name.split('_')[0]) - if (name.includes('base_color') || name.includes('_color') || name === 'color') { + if (family === 'baseColor' || family === 'color') { return 'color' } - if (name.includes('roughness')) { + if (family === 'roughness' || family === 'occlusionRoughnessMetallic') { return 'roughness' } - if (name.includes('normal')) { + if (family === 'normal' || family === 'normalOpengl') { return 'normal' } - if (name.includes('metallic') || name.includes('metalness')) { + if (family === 'metallic' || family === 'metalness') { return 'metalness' } diff --git a/lib/asset-naming.ts b/lib/asset-naming.ts new file mode 100644 index 0000000..4026152 --- /dev/null +++ b/lib/asset-naming.ts @@ -0,0 +1,24 @@ +export const ASSET_FAMILIES = [ + 'baseColor', + 'color', + 'roughness', + 'normal', + 'normalOpengl', + 'metallic', + 'metalness', + 'occlusionRoughnessMetallic', + 'height', + 'opacity', +] as const + +export type AssetFamily = typeof ASSET_FAMILIES[number] + +const ASSET_FAMILY_BY_KEY = new Map(ASSET_FAMILIES.map((family) => [family.toLowerCase(), family])) + +export function getAssetFamily(value: string): AssetFamily | undefined { + return ASSET_FAMILY_BY_KEY.get(value.toLowerCase()) +} + +export function formatAssetFamilies() { + return ASSET_FAMILIES.join(', ') +} diff --git a/lib/validate-folder.ts b/lib/validate-folder.ts index 8b2ee08..822fbd3 100644 --- a/lib/validate-folder.ts +++ b/lib/validate-folder.ts @@ -3,6 +3,7 @@ // --------------------------------------------------------------------------- import { ASSET_EXTENSIONS, TEXTURE_EXTENSIONS } from '@/lib/constants' +import { formatAssetFamilies, getAssetFamily } from '@/lib/asset-naming' import type { TextureFile } from '@/lib/client-types' const SUPPORT_FILE_EXT_ARRAY = [...TEXTURE_EXTENSIONS, ...ASSET_EXTENSIONS] @@ -33,6 +34,32 @@ function getReferencedBufferNames(gltf: GltfJson) { .filter((filename): filename is string => Boolean(filename)) } +function getFileExtension(filename: string) { + return filename.slice(filename.lastIndexOf('.')).toLowerCase() +} + +function getFileStem(filename: string) { + return filename.replace(/\.[^.]+$/, '') +} + +function getTextureNamingError(file: File) { + const stem = getFileStem(file.name) + const [prefix, ...targetParts] = stem.split('_') + const family = getAssetFamily(prefix) + + if (family && targetParts.every(Boolean)) return null + + const reversedParts = stem.split('_') + const reversedFamily = reversedParts.length > 1 ? getAssetFamily(reversedParts[reversedParts.length - 1]) : undefined + + if (reversedFamily) { + const target = reversedParts.slice(0, -1).join('_') + return `Convention invalide : ${file.name}. Utilisez ${reversedFamily}_${target}.${file.name.split('.').pop()} pour cibler "${target}", ou ${reversedFamily}.${file.name.split('.').pop()} pour tout le modele.` + } + + return `Asset inconnu : ${file.name}. Familles autorisees : ${formatAssetFamilies()}. Utilisez asset.png pour tout le modele ou asset_objet.png pour cibler un objet.` +} + async function getGltfWarnings(model: File, supportFiles: File[]) { const warnings: string[] = [] let parsed: unknown @@ -82,10 +109,17 @@ export async function validateFolder(files: File[]): Promise { } const supportFiles = files.filter((f) => { - const ext = f.name.slice(f.name.lastIndexOf('.')).toLowerCase() + const ext = getFileExtension(f.name) return SUPPORT_FILE_EXT_ARRAY.includes(ext) }) + const textureNamingErrors = supportFiles + .filter((file) => TEXTURE_EXTENSIONS.has(getFileExtension(file.name))) + .map(getTextureNamingError) + .filter((error): error is string => Boolean(error)) + + errors.push(...textureNamingErrors) + for (const tf of supportFiles) { textures.push({ name: tf.name, file: tf }) }