fix: enforce asset naming convention
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
@@ -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(', ')
|
||||
}
|
||||
+35
-1
@@ -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<ValidationResult> {
|
||||
}
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user