refactor: simplify upload rules and remove destination flow
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# upload-GLTF
|
# upload-GLTF
|
||||||
|
|
||||||
A secure web interface for uploading 3D assets (GLTF/GLB + textures) to two destinations:
|
A secure web interface for uploading 3D assets (GLTF/GLB + textures) with two outputs:
|
||||||
|
|
||||||
- **Nextcloud Drive** — Archives the original files with automatic versioning (VF/V1/V2...), so artists always have a history of past versions.
|
- **Nextcloud Drive** — Archives the original files with automatic versioning (VF/V1/V2...), so artists always have a history of past versions.
|
||||||
- **GitHub** — Delivers compressed models (Draco via Blender) to the dev team's repository, ready for integration.
|
- **GitHub** — Delivers compressed models (Draco via Blender) to the dev team's repository, ready for integration.
|
||||||
@@ -87,10 +87,9 @@ The Docker image includes Blender headless (installed once at build time). On st
|
|||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
1. The user enters their access key
|
1. The user enters their access key
|
||||||
2. They pick a **destination** (`farm`, `map`, `powergrid`, `workshop`, `general`, `environment`)
|
2. They select a folder containing:
|
||||||
3. They select a folder containing:
|
- `model.glb` (**required**)
|
||||||
- `model.glb` or `model.gltf` (**required**)
|
- Any associated textures (`.png/.jpg/.jpeg/.webp`)
|
||||||
- Textures: `roughness`, `normal`, `metalness`, `color`, `displace` (`.png/.jpg/.webp`, **optional** — missing textures show a warning but don't block the upload)
|
|
||||||
4. The model is displayed in a 3D preview
|
4. The model is displayed in a 3D preview
|
||||||
5. On clicking "Envoyer":
|
5. On clicking "Envoyer":
|
||||||
- The app checks the remote Git repo for existing files and computes diffs (textures by size, models always re-pushed)
|
- The app checks the remote Git repo for existing files and computes diffs (textures by size, models always re-pushed)
|
||||||
@@ -111,7 +110,7 @@ The Drive uses a `VF` (version finale) / `Vx` (archived versions) structure:
|
|||||||
Models/
|
Models/
|
||||||
VF/ ← latest version
|
VF/ ← latest version
|
||||||
coffeetest/
|
coffeetest/
|
||||||
model.gltf
|
model.glb
|
||||||
color.jpg
|
color.jpg
|
||||||
V1/ ← first archive
|
V1/ ← first archive
|
||||||
coffeetest/
|
coffeetest/
|
||||||
@@ -131,42 +130,32 @@ All changes are pushed in a **single commit** with a formatted message:
|
|||||||
|
|
||||||
**New folder:**
|
**New folder:**
|
||||||
```
|
```
|
||||||
update: upload-gltf add a new model -> farm/my-model
|
update: upload-gltf add a new model -> my-model
|
||||||
|
|
||||||
📦 Model
|
📦 Model
|
||||||
✅ model.gltf (compressed)
|
✅ model.glb (compressed)
|
||||||
🎨 Textures
|
🎨 Textures
|
||||||
✅ color.jpg
|
✅ color.jpg
|
||||||
❌ metalness (manquant)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Update (only metalness changed):**
|
**Update (only one texture changed):**
|
||||||
```
|
```
|
||||||
update: upload-gltf update -> general/coffeetest
|
update: upload-gltf update -> coffeetest
|
||||||
|
|
||||||
📦 Model
|
📦 Model
|
||||||
↔️ model.gltf (inchange)
|
↔️ model.glb (inchange)
|
||||||
🎨 Textures
|
🎨 Textures
|
||||||
🔄 metalness.jpg
|
🔄 color_tuyaux.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
Symbols: `✅` new — `🔄` modified — `↔️` unchanged (model always re-pushed) — `❌` missing or deleted
|
Symbols: `✅` new — `🔄` modified — `↔️` unchanged (model always re-pushed) — `❌` deleted
|
||||||
|
|
||||||
8. Orphan files (present on remote but not in the new upload) are deleted in the same commit
|
8. Orphan files (present on remote but not in the new upload) are deleted in the same commit
|
||||||
9. If Blender is unavailable, the original model is pushed as-is (graceful fallback)
|
9. If Blender is unavailable, the original model is pushed as-is (graceful fallback)
|
||||||
|
|
||||||
## Destinations
|
## Destinations
|
||||||
|
|
||||||
Uploaded models are pushed to `public/models/<destination>/<folderName>/` in the target repo:
|
Uploaded models are pushed to `public/models/<folderName>/` in the target repo.
|
||||||
|
|
||||||
| Destination | Path |
|
|
||||||
|-------------|------|
|
|
||||||
| Farm | `public/models/farm/` |
|
|
||||||
| Map | `public/models/map/` |
|
|
||||||
| Powergrid | `public/models/powergrid/` |
|
|
||||||
| Workshop | `public/models/workshop/` |
|
|
||||||
| General | `public/models/general/` |
|
|
||||||
| Environment | `public/models/environment/` |
|
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
@@ -185,7 +174,6 @@ components/
|
|||||||
│ └── Modal.tsx # Shared modal wrapper + ModalActions
|
│ └── Modal.tsx # Shared modal wrapper + ModalActions
|
||||||
├── upload/
|
├── upload/
|
||||||
│ ├── SecretInput.tsx # Access key input
|
│ ├── SecretInput.tsx # Access key input
|
||||||
│ ├── DestinationPicker.tsx # Destination selector
|
|
||||||
│ ├── FolderDropzone.tsx # Folder drag & drop / picker
|
│ ├── FolderDropzone.tsx # Folder drag & drop / picker
|
||||||
│ ├── FolderCard.tsx # Folder status card (Drive + Git)
|
│ ├── FolderCard.tsx # Folder status card (Drive + Git)
|
||||||
│ ├── DriveStatusLine.tsx # Drive/Git status sub-line
|
│ ├── DriveStatusLine.tsx # Drive/Git status sub-line
|
||||||
@@ -202,7 +190,7 @@ hooks/
|
|||||||
├── useFolderEntries.ts # Folder entries state management
|
├── useFolderEntries.ts # Folder entries state management
|
||||||
└── useUploadOrchestrator.ts # Upload pipeline orchestration (Drive → Git)
|
└── useUploadOrchestrator.ts # Upload pipeline orchestration (Drive → Git)
|
||||||
lib/
|
lib/
|
||||||
├── constants.ts # Shared constants, destinations, extensions
|
├── constants.ts # Shared constants and extensions
|
||||||
├── types.ts # Server types (ParsedFile, FileDiff, etc.)
|
├── types.ts # Server types (ParsedFile, FileDiff, etc.)
|
||||||
├── client-types.ts # Client types (FolderEntry, DriveStatus, etc.)
|
├── client-types.ts # Client types (FolderEntry, DriveStatus, etc.)
|
||||||
├── upload-api.ts # Client-side API helpers (check, uploadDrive, uploadGit)
|
├── upload-api.ts # Client-side API helpers (check, uploadDrive, uploadGit)
|
||||||
|
|||||||
@@ -17,23 +17,21 @@ export async function POST(req: NextRequest) {
|
|||||||
if (authError) return authError
|
if (authError) return authError
|
||||||
|
|
||||||
let folderName: string
|
let folderName: string
|
||||||
let destination: string
|
|
||||||
let parsedFiles: Awaited<ReturnType<typeof parseMultiUpload>>['files']
|
let parsedFiles: Awaited<ReturnType<typeof parseMultiUpload>>['files']
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = await parseMultiUpload(req)
|
const parsed = await parseMultiUpload(req)
|
||||||
folderName = parsed.folderName
|
folderName = parsed.folderName
|
||||||
destination = parsed.destination
|
|
||||||
parsedFiles = parsed.files
|
parsedFiles = parsed.files
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : 'Erreur inconnue'
|
const message = err instanceof Error ? err.message : 'Erreur inconnue'
|
||||||
return NextResponse.json({ success: false, error: message }, { status: 400 })
|
return NextResponse.json({ success: false, error: message }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const folderPath = `public/models/${destination}/${folderName}`
|
const folderPath = `public/models/${folderName}`
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { filesToPush } = await prepareGitAssets({ folderName, destination, parsedFiles })
|
const { filesToPush } = await prepareGitAssets({ folderName, parsedFiles })
|
||||||
const { exists, files } = await getRemoteFolder(folderPath)
|
const { exists, files } = await getRemoteFolder(folderPath)
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
// Upload **original** files (no Blender compression) to Nextcloud Drive.
|
// Upload **original** files (no Blender compression) to Nextcloud Drive.
|
||||||
//
|
//
|
||||||
// FormData fields:
|
// FormData fields:
|
||||||
// - folderName, destination, files[], fileTypes[], textureNames[] (same as /api/upload/git)
|
// - folderName, files[], fileTypes[], textureNames[] (same as /api/upload/git)
|
||||||
// - action: "new" | "replace"
|
// - action: "new" | "replace"
|
||||||
//
|
//
|
||||||
// Versioning logic:
|
// Versioning logic:
|
||||||
|
|||||||
@@ -20,13 +20,11 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
// --- Parse all files ---
|
// --- Parse all files ---
|
||||||
let folderName: string
|
let folderName: string
|
||||||
let destination: string
|
|
||||||
let parsedFiles: Awaited<ReturnType<typeof parseMultiUpload>>['files']
|
let parsedFiles: Awaited<ReturnType<typeof parseMultiUpload>>['files']
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = await parseMultiUpload(req)
|
const parsed = await parseMultiUpload(req)
|
||||||
folderName = parsed.folderName
|
folderName = parsed.folderName
|
||||||
destination = parsed.destination
|
|
||||||
parsedFiles = parsed.files
|
parsedFiles = parsed.files
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : 'Erreur inconnue'
|
const message = err instanceof Error ? err.message : 'Erreur inconnue'
|
||||||
@@ -40,10 +38,10 @@ export async function POST(req: NextRequest) {
|
|||||||
compressed,
|
compressed,
|
||||||
compressionError,
|
compressionError,
|
||||||
textureNames,
|
textureNames,
|
||||||
} = await prepareGitAssets({ folderName, destination, parsedFiles })
|
} = await prepareGitAssets({ folderName, parsedFiles })
|
||||||
|
|
||||||
// --- Detect existing files and classify changes ---
|
// --- Detect existing files and classify changes ---
|
||||||
const folderPath = `public/models/${destination}/${folderName}`
|
const folderPath = `public/models/${folderName}`
|
||||||
let remoteFileMap: Map<string, number>
|
let remoteFileMap: Map<string, number>
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -72,7 +70,7 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
// --- Build commit message ---
|
// --- Build commit message ---
|
||||||
const commitMessage = buildCommitMessage(
|
const commitMessage = buildCommitMessage(
|
||||||
folderName, destination, modelFilename, textureNames,
|
folderName, modelFilename, textureNames,
|
||||||
compressed, isReplace, fileChanges, deletedFileNames,
|
compressed, isReplace, fileChanges, deletedFileNames,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+16
-12
@@ -5,7 +5,6 @@ import { useSecret } from '@/hooks/useSecret'
|
|||||||
import { useFolderEntries } from '@/hooks/useFolderEntries'
|
import { useFolderEntries } from '@/hooks/useFolderEntries'
|
||||||
import { useUploadOrchestrator } from '@/hooks/useUploadOrchestrator'
|
import { useUploadOrchestrator } from '@/hooks/useUploadOrchestrator'
|
||||||
import SecretInput from './upload/SecretInput'
|
import SecretInput from './upload/SecretInput'
|
||||||
import DestinationPicker from './upload/DestinationPicker'
|
|
||||||
import FolderDropzone from './upload/FolderDropzone'
|
import FolderDropzone from './upload/FolderDropzone'
|
||||||
import FolderCard from './upload/FolderCard'
|
import FolderCard from './upload/FolderCard'
|
||||||
import ActionButtons from './upload/ActionButtons'
|
import ActionButtons from './upload/ActionButtons'
|
||||||
@@ -38,8 +37,6 @@ export default function UploadZone() {
|
|||||||
isUploading,
|
isUploading,
|
||||||
globalError,
|
globalError,
|
||||||
setGlobalError,
|
setGlobalError,
|
||||||
destination,
|
|
||||||
setDestination,
|
|
||||||
overwriteConfirm,
|
overwriteConfirm,
|
||||||
setOverwriteConfirm,
|
setOverwriteConfirm,
|
||||||
noChangesFolder,
|
noChangesFolder,
|
||||||
@@ -84,18 +81,28 @@ export default function UploadZone() {
|
|||||||
onToggleVisible={toggleSecretVisible}
|
onToggleVisible={toggleSecretVisible}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DestinationPicker
|
|
||||||
destination={destination}
|
|
||||||
disabled={isUploading}
|
|
||||||
onChange={setDestination}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{entries.length === 0 && (
|
{entries.length === 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs text-gray-400 leading-relaxed text-center">
|
||||||
|
Deposez un dossier complet contenant votre modele 3D nomme
|
||||||
|
{' '}<span className="font-mono text-gray-200">model.glb</span>
|
||||||
|
{' '}ainsi que toutes les textures necessaires.
|
||||||
|
{' '}Les textures peuvent etre en
|
||||||
|
{' '}<span className="font-mono text-gray-200">.png</span>,
|
||||||
|
{' '}<span className="font-mono text-gray-200">.jpg</span>
|
||||||
|
{' '}ou <span className="font-mono text-gray-200">.webp</span>.
|
||||||
|
{' '}Utilisez un nom simple si la texture s'applique au modele entier, et un nom detaille si elle correspond a une partie precise du modele,
|
||||||
|
{' '}par exemple <span className="font-mono text-gray-200">color_fenetre.jpg</span>,
|
||||||
|
{' '}<span className="font-mono text-gray-200">roughness_tuyaux.png</span>,
|
||||||
|
{' '}<span className="font-mono text-gray-200">normal_dashboard.webp</span>
|
||||||
|
{' '}ou <span className="font-mono text-gray-200">opacity_verre.png</span>.
|
||||||
|
</p>
|
||||||
<FolderDropzone
|
<FolderDropzone
|
||||||
isUploading={isUploading}
|
isUploading={isUploading}
|
||||||
onFolderSelected={handleFolderSelected}
|
onFolderSelected={handleFolderSelected}
|
||||||
onError={setGlobalError}
|
onError={setGlobalError}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{globalError && (
|
{globalError && (
|
||||||
@@ -119,7 +126,6 @@ export default function UploadZone() {
|
|||||||
<ActionButtons
|
<ActionButtons
|
||||||
isUploading={isUploading}
|
isUploading={isUploading}
|
||||||
isSecretEmpty={isSecretEmpty}
|
isSecretEmpty={isSecretEmpty}
|
||||||
noDestination={!destination}
|
|
||||||
hasPendingOrErrors={hasPendingOrErrors}
|
hasPendingOrErrors={hasPendingOrErrors}
|
||||||
allDone={allDone}
|
allDone={allDone}
|
||||||
hasErrors={hasErrors}
|
hasErrors={hasErrors}
|
||||||
@@ -130,7 +136,6 @@ export default function UploadZone() {
|
|||||||
|
|
||||||
{overwriteConfirm && (
|
{overwriteConfirm && (
|
||||||
<OverwriteConfirmModal
|
<OverwriteConfirmModal
|
||||||
destination={destination!}
|
|
||||||
folderName={overwriteConfirm.folderName}
|
folderName={overwriteConfirm.folderName}
|
||||||
diffs={overwriteConfirm.diffs}
|
diffs={overwriteConfirm.diffs}
|
||||||
onCancel={() => setOverwriteConfirm(null)}
|
onCancel={() => setOverwriteConfirm(null)}
|
||||||
@@ -140,7 +145,6 @@ export default function UploadZone() {
|
|||||||
|
|
||||||
{noChangesFolder && (
|
{noChangesFolder && (
|
||||||
<NoChangesModal
|
<NoChangesModal
|
||||||
destination={destination!}
|
|
||||||
folderName={noChangesFolder}
|
folderName={noChangesFolder}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setNoChangesFolder(null)
|
setNoChangesFolder(null)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
interface ActionButtonsProps {
|
interface ActionButtonsProps {
|
||||||
isUploading: boolean
|
isUploading: boolean
|
||||||
isSecretEmpty: boolean
|
isSecretEmpty: boolean
|
||||||
noDestination: boolean
|
|
||||||
hasPendingOrErrors: boolean
|
hasPendingOrErrors: boolean
|
||||||
allDone: boolean
|
allDone: boolean
|
||||||
hasErrors: boolean
|
hasErrors: boolean
|
||||||
@@ -13,7 +12,6 @@ interface ActionButtonsProps {
|
|||||||
export default function ActionButtons({
|
export default function ActionButtons({
|
||||||
isUploading,
|
isUploading,
|
||||||
isSecretEmpty,
|
isSecretEmpty,
|
||||||
noDestination,
|
|
||||||
hasPendingOrErrors,
|
hasPendingOrErrors,
|
||||||
allDone,
|
allDone,
|
||||||
hasErrors,
|
hasErrors,
|
||||||
@@ -21,7 +19,7 @@ export default function ActionButtons({
|
|||||||
onCancel,
|
onCancel,
|
||||||
onReset,
|
onReset,
|
||||||
}: ActionButtonsProps) {
|
}: ActionButtonsProps) {
|
||||||
const cantUpload = isSecretEmpty || noDestination
|
const cantUpload = isSecretEmpty
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import { DESTINATIONS } from '@/lib/constants'
|
|
||||||
import type { Destination } from '@/lib/constants'
|
|
||||||
|
|
||||||
interface DestinationPickerProps {
|
|
||||||
destination: Destination | null
|
|
||||||
disabled: boolean
|
|
||||||
onChange: (value: Destination) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DestinationPicker({
|
|
||||||
destination,
|
|
||||||
disabled,
|
|
||||||
onChange,
|
|
||||||
}: DestinationPickerProps) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<label className="block text-sm font-medium text-gray-300">Destination</label>
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
{DESTINATIONS.map((dest) => (
|
|
||||||
<button
|
|
||||||
key={dest.value}
|
|
||||||
type="button"
|
|
||||||
onClick={() => onChange(dest.value)}
|
|
||||||
disabled={disabled}
|
|
||||||
className={`px-3 py-2 rounded-xl text-sm font-medium transition-all duration-150 border
|
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed
|
|
||||||
${destination === dest.value
|
|
||||||
? 'bg-white text-[#000000] border-white'
|
|
||||||
: 'bg-black-800 text-gray-400 border-white/20 hover:border-white/40 hover:text-gray-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{dest.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -170,10 +170,6 @@ export default function FolderDropzone({
|
|||||||
Deposez votre dossier ici
|
Deposez votre dossier ici
|
||||||
<span className="text-gray-500 font-normal"> ou cliquez pour parcourir</span>
|
<span className="text-gray-500 font-normal"> ou cliquez pour parcourir</span>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
|
||||||
Contenu attendu : model.glb/gltf + textures (roughness, normal, metalness, color, displace)
|
|
||||||
<br />Les originaux sont archives sur le Drive, les comprimes sont envoyes sur Git.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,14 +2,12 @@ import Modal, { ModalActions } from '@/components/ui/Modal'
|
|||||||
import { CheckIcon } from '@/components/ui/icons'
|
import { CheckIcon } from '@/components/ui/icons'
|
||||||
|
|
||||||
interface NoChangesModalProps {
|
interface NoChangesModalProps {
|
||||||
destination: string
|
|
||||||
folderName: string
|
folderName: string
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
onModify: () => void
|
onModify: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NoChangesModal({
|
export default function NoChangesModal({
|
||||||
destination,
|
|
||||||
folderName,
|
folderName,
|
||||||
onCancel,
|
onCancel,
|
||||||
onModify,
|
onModify,
|
||||||
@@ -25,7 +23,7 @@ export default function NoChangesModal({
|
|||||||
Aucun changement detecte
|
Aucun changement detecte
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-gray-400 mt-0.5">
|
<p className="text-xs text-gray-400 mt-0.5">
|
||||||
Le dossier <span className="font-mono text-gray-300">{destination}/{folderName}</span> est identique au contenu distant. Rien a envoyer.
|
Le dossier <span className="font-mono text-gray-300">public/models/{folderName}</span> est identique au contenu distant. Rien a envoyer.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import Modal, { ModalActions } from '@/components/ui/Modal'
|
|||||||
import { WarningIcon } from '@/components/ui/icons'
|
import { WarningIcon } from '@/components/ui/icons'
|
||||||
|
|
||||||
interface OverwriteConfirmModalProps {
|
interface OverwriteConfirmModalProps {
|
||||||
destination: string
|
|
||||||
folderName: string
|
folderName: string
|
||||||
diffs: FileDiff[]
|
diffs: FileDiff[]
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
@@ -11,7 +10,6 @@ interface OverwriteConfirmModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function OverwriteConfirmModal({
|
export default function OverwriteConfirmModal({
|
||||||
destination,
|
|
||||||
folderName,
|
folderName,
|
||||||
diffs,
|
diffs,
|
||||||
onCancel,
|
onCancel,
|
||||||
@@ -28,7 +26,7 @@ export default function OverwriteConfirmModal({
|
|||||||
Dossier deja existant
|
Dossier deja existant
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-gray-400 mt-0.5">
|
<p className="text-xs text-gray-400 mt-0.5">
|
||||||
<span className="font-mono text-yellow-400">{destination}/{folderName}</span> existe deja.
|
<span className="font-mono text-yellow-400">public/models/{folderName}</span> existe deja.
|
||||||
Les anciens fichiers seront archives sur le Drive, puis les nouveaux seront envoyes sur le Drive et Git.
|
Les anciens fichiers seront archives sur le Drive, puis les nouveaux seront envoyes sur le Drive et Git.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
import { useState, useRef, useCallback } from 'react'
|
import { useState, useRef, useCallback } from 'react'
|
||||||
import type { Destination } from '@/lib/constants'
|
|
||||||
import type { FolderEntry } from '@/lib/client-types'
|
import type { FolderEntry } from '@/lib/client-types'
|
||||||
import type { FileDiff } from '@/lib/types'
|
import type { FileDiff } from '@/lib/types'
|
||||||
import { checkFolderDiffs, uploadDrive, uploadGit } from '@/lib/upload-api'
|
import { checkFolderDiffs, uploadDrive, uploadGit } from '@/lib/upload-api'
|
||||||
@@ -28,7 +27,6 @@ export function useUploadOrchestrator({
|
|||||||
}: UseUploadOrchestratorParams) {
|
}: UseUploadOrchestratorParams) {
|
||||||
const [isUploading, setIsUploading] = useState(false)
|
const [isUploading, setIsUploading] = useState(false)
|
||||||
const [globalError, setGlobalError] = useState<string | null>(null)
|
const [globalError, setGlobalError] = useState<string | null>(null)
|
||||||
const [destination, setDestination] = useState<Destination | null>(null)
|
|
||||||
const [overwriteConfirm, setOverwriteConfirm] = useState<{
|
const [overwriteConfirm, setOverwriteConfirm] = useState<{
|
||||||
folderName: string
|
folderName: string
|
||||||
diffs: FileDiff[]
|
diffs: FileDiff[]
|
||||||
@@ -45,20 +43,16 @@ export function useUploadOrchestrator({
|
|||||||
// Refs for values used inside callbacks to avoid stale closures
|
// Refs for values used inside callbacks to avoid stale closures
|
||||||
const secretRef = useRef(secret)
|
const secretRef = useRef(secret)
|
||||||
secretRef.current = secret
|
secretRef.current = secret
|
||||||
const destinationRef = useRef(destination)
|
|
||||||
destinationRef.current = destination
|
|
||||||
const entriesRef = useRef(entries)
|
const entriesRef = useRef(entries)
|
||||||
entriesRef.current = entries
|
entriesRef.current = entries
|
||||||
|
|
||||||
// ---- Internal: push a single folder to Git ----
|
// ---- Internal: push a single folder to Git ----
|
||||||
const pushGit = useCallback(async (index: number, signal?: AbortSignal) => {
|
const pushGit = useCallback(async (index: number, signal?: AbortSignal) => {
|
||||||
const folderEntry = entriesRef.current[index]
|
const folderEntry = entriesRef.current[index]
|
||||||
const dest = destinationRef.current
|
|
||||||
|
|
||||||
const gitResult = await uploadGit(
|
const gitResult = await uploadGit(
|
||||||
folderEntry,
|
folderEntry,
|
||||||
secretRef.current,
|
secretRef.current,
|
||||||
dest!,
|
|
||||||
(pct) => updateEntry(index, { progress: 50 + Math.round(pct / 2) }),
|
(pct) => updateEntry(index, { progress: 50 + Math.round(pct / 2) }),
|
||||||
signal,
|
signal,
|
||||||
)
|
)
|
||||||
@@ -101,7 +95,6 @@ export function useUploadOrchestrator({
|
|||||||
const driveResult = await uploadDrive(
|
const driveResult = await uploadDrive(
|
||||||
folderEntry,
|
folderEntry,
|
||||||
secretRef.current,
|
secretRef.current,
|
||||||
destinationRef.current!,
|
|
||||||
driveAction as 'new' | 'replace',
|
driveAction as 'new' | 'replace',
|
||||||
controller.signal,
|
controller.signal,
|
||||||
)
|
)
|
||||||
@@ -129,10 +122,6 @@ export function useUploadOrchestrator({
|
|||||||
setSecretError("La cle d'acces est requise")
|
setSecretError("La cle d'acces est requise")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!destinationRef.current) {
|
|
||||||
setGlobalError('Veuillez choisir une destination')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (entriesRef.current.length === 0) return
|
if (entriesRef.current.length === 0) return
|
||||||
|
|
||||||
setSecretError(null)
|
setSecretError(null)
|
||||||
@@ -143,7 +132,6 @@ export function useUploadOrchestrator({
|
|||||||
try {
|
try {
|
||||||
const check = await checkFolderDiffs(
|
const check = await checkFolderDiffs(
|
||||||
folder,
|
folder,
|
||||||
destinationRef.current,
|
|
||||||
secretRef.current,
|
secretRef.current,
|
||||||
abortRef.current?.signal,
|
abortRef.current?.signal,
|
||||||
)
|
)
|
||||||
@@ -230,8 +218,6 @@ export function useUploadOrchestrator({
|
|||||||
isUploading,
|
isUploading,
|
||||||
globalError,
|
globalError,
|
||||||
setGlobalError,
|
setGlobalError,
|
||||||
destination,
|
|
||||||
setDestination,
|
|
||||||
overwriteConfirm,
|
overwriteConfirm,
|
||||||
setOverwriteConfirm,
|
setOverwriteConfirm,
|
||||||
noChangesFolder,
|
noChangesFolder,
|
||||||
|
|||||||
+7
-21
@@ -1,4 +1,3 @@
|
|||||||
import { REQUIRED_TEXTURES } from './constants'
|
|
||||||
import type { FileChange } from './types'
|
import type { FileChange } from './types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7,12 +6,11 @@ import type { FileChange } from './types'
|
|||||||
* Symbols:
|
* Symbols:
|
||||||
* - ✅ = new file
|
* - ✅ = new file
|
||||||
* - 🔄 = modified file
|
* - 🔄 = modified file
|
||||||
* - ❌ = missing texture (new upload) or deleted file (update)
|
* - ❌ = deleted file
|
||||||
* - Unchanged files are omitted entirely
|
* - Unchanged files are omitted entirely
|
||||||
*/
|
*/
|
||||||
export function buildCommitMessage(
|
export function buildCommitMessage(
|
||||||
folderName: string,
|
folderName: string,
|
||||||
destination: string,
|
|
||||||
modelFilename: string,
|
modelFilename: string,
|
||||||
textureNames: string[],
|
textureNames: string[],
|
||||||
compressed: boolean,
|
compressed: boolean,
|
||||||
@@ -21,8 +19,8 @@ export function buildCommitMessage(
|
|||||||
deletedFileNames: string[],
|
deletedFileNames: string[],
|
||||||
): string {
|
): string {
|
||||||
const title = isReplace
|
const title = isReplace
|
||||||
? `update: upload-gltf update -> ${destination}/${folderName}`
|
? `update: upload-gltf update -> ${folderName}`
|
||||||
: `update: upload-gltf add a new model -> ${destination}/${folderName}`
|
: `update: upload-gltf add a new model -> ${folderName}`
|
||||||
|
|
||||||
const lines: string[] = [title, '']
|
const lines: string[] = [title, '']
|
||||||
|
|
||||||
@@ -39,26 +37,14 @@ export function buildCommitMessage(
|
|||||||
lines.push(` ↔️ ${modelFilename} (inchange)`)
|
lines.push(` ↔️ ${modelFilename} (inchange)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Textures section — only show lines that have changes
|
|
||||||
const foundTextures = new Set(
|
|
||||||
textureNames.map((t) => t.toLowerCase().replace(/\.[^.]+$/, '')),
|
|
||||||
)
|
|
||||||
|
|
||||||
const textureLines: string[] = []
|
const textureLines: string[] = []
|
||||||
|
|
||||||
for (const tex of REQUIRED_TEXTURES) {
|
for (const textureName of textureNames) {
|
||||||
if (foundTextures.has(tex)) {
|
const change = fileChanges.get(textureName.toLowerCase())
|
||||||
const actual = textureNames.find(
|
|
||||||
(t) => t.toLowerCase().replace(/\.[^.]+$/, '') === tex,
|
|
||||||
)!
|
|
||||||
const change = fileChanges.get(actual.toLowerCase())
|
|
||||||
if (change === 'new') {
|
if (change === 'new') {
|
||||||
textureLines.push(` ✅ ${actual}`)
|
textureLines.push(` ✅ ${textureName}`)
|
||||||
} else if (change === 'changed') {
|
} else if (change === 'changed') {
|
||||||
textureLines.push(` 🔄 ${actual}`)
|
textureLines.push(` 🔄 ${textureName}`)
|
||||||
}
|
|
||||||
} else if (!isReplace) {
|
|
||||||
textureLines.push(` ❌ ${tex} (manquant)`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,23 +9,6 @@ export const ALL_ALLOWED_EXTENSIONS = new Set([...MODEL_EXTENSIONS, ...TEXTURE_E
|
|||||||
/** Extensions tracked by Git LFS (must match .gitattributes) */
|
/** Extensions tracked by Git LFS (must match .gitattributes) */
|
||||||
export const LFS_EXTENSIONS = new Set(['.glb', '.gltf', '.png', '.jpg', '.jpeg', '.webp'])
|
export const LFS_EXTENSIONS = new Set(['.glb', '.gltf', '.png', '.jpg', '.jpeg', '.webp'])
|
||||||
|
|
||||||
export const REQUIRED_TEXTURES = ['roughness', 'normal', 'metalness', 'color', 'displace'] as const
|
|
||||||
|
|
||||||
export const VALID_DESTINATIONS = new Set<string>([
|
|
||||||
'farm', 'map', 'powergrid', 'workshop', 'general', 'environment',
|
|
||||||
])
|
|
||||||
|
|
||||||
export const DESTINATIONS = [
|
|
||||||
{ value: 'farm', label: 'Farm' },
|
|
||||||
{ value: 'map', label: 'Map' },
|
|
||||||
{ value: 'powergrid', label: 'Powergrid' },
|
|
||||||
{ value: 'workshop', label: 'Workshop' },
|
|
||||||
{ value: 'general', label: 'General' },
|
|
||||||
{ value: 'environment', label: 'Environment' },
|
|
||||||
] as const
|
|
||||||
|
|
||||||
export type Destination = typeof DESTINATIONS[number]['value']
|
|
||||||
|
|
||||||
export const TMP_DIR = '/tmp/assets'
|
export const TMP_DIR = '/tmp/assets'
|
||||||
|
|
||||||
/** Maximum file size in bytes (100 MB) */
|
/** Maximum file size in bytes (100 MB) */
|
||||||
|
|||||||
+1
-1
@@ -51,7 +51,7 @@ async function davRequest(
|
|||||||
Authorization: auth,
|
Authorization: auth,
|
||||||
...extraHeaders,
|
...extraHeaders,
|
||||||
},
|
},
|
||||||
body: body ?? undefined,
|
body: body == null ? undefined : typeof body === 'string' ? body : new Uint8Array(body),
|
||||||
})
|
})
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|||||||
+5
-12
@@ -1,12 +1,11 @@
|
|||||||
import { extname } from 'path'
|
import { extname } from 'path'
|
||||||
import { NextRequest } from 'next/server'
|
import { NextRequest } from 'next/server'
|
||||||
import { sanitizeFilename } from './sanitize'
|
import { sanitizeFilename } from './sanitize'
|
||||||
import { ALL_ALLOWED_EXTENSIONS, MODEL_EXTENSIONS, VALID_DESTINATIONS, MAX_FILE_SIZE } from './constants'
|
import { ALL_ALLOWED_EXTENSIONS, MODEL_EXTENSIONS, MAX_FILE_SIZE } from './constants'
|
||||||
import type { ParsedFile } from './types'
|
import type { ParsedFile } from './types'
|
||||||
|
|
||||||
export interface ParsedUpload {
|
export interface ParsedUpload {
|
||||||
folderName: string
|
folderName: string
|
||||||
destination: string
|
|
||||||
files: ParsedFile[]
|
files: ParsedFile[]
|
||||||
/** Any extra string fields from the FormData (e.g. "action") */
|
/** Any extra string fields from the FormData (e.g. "action") */
|
||||||
extra: Record<string, string>
|
extra: Record<string, string>
|
||||||
@@ -14,8 +13,8 @@ export interface ParsedUpload {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a multi-file FormData upload request.
|
* Parse a multi-file FormData upload request.
|
||||||
* Validates destination, file extensions, file sizes, and returns parsed files.
|
* Validates file extensions, file sizes, and returns parsed files.
|
||||||
* Extra string fields (beyond folderName, destination, files, fileTypes, textureNames)
|
* Extra string fields (beyond folderName, files, fileTypes, textureNames)
|
||||||
* are returned in `extra`.
|
* are returned in `extra`.
|
||||||
*/
|
*/
|
||||||
export async function parseMultiUpload(req: NextRequest): Promise<ParsedUpload> {
|
export async function parseMultiUpload(req: NextRequest): Promise<ParsedUpload> {
|
||||||
@@ -23,18 +22,12 @@ export async function parseMultiUpload(req: NextRequest): Promise<ParsedUpload>
|
|||||||
const folderName = (formData.get('folderName') as string | null)?.trim() || 'assets'
|
const folderName = (formData.get('folderName') as string | null)?.trim() || 'assets'
|
||||||
const safeFolderName = sanitizeFilename(folderName).replace(/[^a-zA-Z0-9-_]/g, '-')
|
const safeFolderName = sanitizeFilename(folderName).replace(/[^a-zA-Z0-9-_]/g, '-')
|
||||||
|
|
||||||
const rawDestination = (formData.get('destination') as string | null)?.trim() || 'general'
|
|
||||||
if (!VALID_DESTINATIONS.has(rawDestination)) {
|
|
||||||
throw new Error(`Destination invalide: "${rawDestination}"`)
|
|
||||||
}
|
|
||||||
const destination = rawDestination
|
|
||||||
|
|
||||||
const rawFiles = formData.getAll('files')
|
const rawFiles = formData.getAll('files')
|
||||||
const fileTypes = formData.getAll('fileTypes') as string[]
|
const fileTypes = formData.getAll('fileTypes') as string[]
|
||||||
const textureNames = formData.getAll('textureNames') as string[]
|
const textureNames = formData.getAll('textureNames') as string[]
|
||||||
|
|
||||||
// Collect extra string fields
|
// Collect extra string fields
|
||||||
const knownKeys = new Set(['folderName', 'destination', 'files', 'fileTypes', 'textureNames'])
|
const knownKeys = new Set(['folderName', 'files', 'fileTypes', 'textureNames'])
|
||||||
const extra: Record<string, string> = {}
|
const extra: Record<string, string> = {}
|
||||||
for (const [key, value] of formData.entries()) {
|
for (const [key, value] of formData.entries()) {
|
||||||
if (!knownKeys.has(key) && typeof value === 'string') {
|
if (!knownKeys.has(key) && typeof value === 'string') {
|
||||||
@@ -91,5 +84,5 @@ export async function parseMultiUpload(req: NextRequest): Promise<ParsedUpload>
|
|||||||
parsed.push({ filename, buffer, isModel })
|
parsed.push({ filename, buffer, isModel })
|
||||||
}
|
}
|
||||||
|
|
||||||
return { folderName: safeFolderName, destination, files: parsed, extra }
|
return { folderName: safeFolderName, files: parsed, extra }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ interface PushFile {
|
|||||||
|
|
||||||
interface PrepareGitAssetsParams {
|
interface PrepareGitAssetsParams {
|
||||||
folderName: string
|
folderName: string
|
||||||
destination: string
|
|
||||||
parsedFiles: ParsedFile[]
|
parsedFiles: ParsedFile[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +26,6 @@ interface PrepareGitAssetsResult {
|
|||||||
|
|
||||||
export async function prepareGitAssets({
|
export async function prepareGitAssets({
|
||||||
folderName,
|
folderName,
|
||||||
destination,
|
|
||||||
parsedFiles,
|
parsedFiles,
|
||||||
}: PrepareGitAssetsParams): Promise<PrepareGitAssetsResult> {
|
}: PrepareGitAssetsParams): Promise<PrepareGitAssetsResult> {
|
||||||
const filesToPush: PushFile[] = []
|
const filesToPush: PushFile[] = []
|
||||||
@@ -76,7 +74,7 @@ export async function prepareGitAssets({
|
|||||||
}
|
}
|
||||||
|
|
||||||
filesToPush.push({
|
filesToPush.push({
|
||||||
path: `public/models/${destination}/${folderName}/${pf.filename}`,
|
path: `public/models/${folderName}/${pf.filename}`,
|
||||||
contentBase64: content.toString('base64'),
|
contentBase64: content.toString('base64'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-8
@@ -16,12 +16,10 @@ export interface CheckResult {
|
|||||||
|
|
||||||
function buildUploadFormData(
|
function buildUploadFormData(
|
||||||
folder: FolderEntry,
|
folder: FolderEntry,
|
||||||
destination: string,
|
|
||||||
extra?: Record<string, string>,
|
extra?: Record<string, string>,
|
||||||
): FormData {
|
): FormData {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('folderName', folder.folderName)
|
formData.append('folderName', folder.folderName)
|
||||||
formData.append('destination', destination)
|
|
||||||
|
|
||||||
if (extra) {
|
if (extra) {
|
||||||
for (const [key, value] of Object.entries(extra)) {
|
for (const [key, value] of Object.entries(extra)) {
|
||||||
@@ -52,11 +50,10 @@ function buildUploadFormData(
|
|||||||
*/
|
*/
|
||||||
export async function checkFolderDiffs(
|
export async function checkFolderDiffs(
|
||||||
folder: FolderEntry,
|
folder: FolderEntry,
|
||||||
destination: string,
|
|
||||||
secret: string,
|
secret: string,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<CheckResult> {
|
): Promise<CheckResult> {
|
||||||
const formData = buildUploadFormData(folder, destination)
|
const formData = buildUploadFormData(folder)
|
||||||
const res = await fetch('/api/upload/check', {
|
const res = await fetch('/api/upload/check', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'x-upload-secret': secret.trim() },
|
headers: { 'x-upload-secret': secret.trim() },
|
||||||
@@ -86,11 +83,10 @@ export async function checkFolderDiffs(
|
|||||||
export async function uploadDrive(
|
export async function uploadDrive(
|
||||||
folder: FolderEntry,
|
folder: FolderEntry,
|
||||||
secret: string,
|
secret: string,
|
||||||
destination: string,
|
|
||||||
action: 'new' | 'replace',
|
action: 'new' | 'replace',
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
const formData = buildUploadFormData(folder, destination, { action })
|
const formData = buildUploadFormData(folder, { action })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/upload/drive', {
|
const res = await fetch('/api/upload/drive', {
|
||||||
@@ -118,11 +114,10 @@ export async function uploadDrive(
|
|||||||
export async function uploadGit(
|
export async function uploadGit(
|
||||||
folder: FolderEntry,
|
folder: FolderEntry,
|
||||||
secret: string,
|
secret: string,
|
||||||
destination: string,
|
|
||||||
onProgress: (pct: number) => void,
|
onProgress: (pct: number) => void,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<{ success: boolean; filename?: string; error?: string }> {
|
): Promise<{ success: boolean; filename?: string; error?: string }> {
|
||||||
const formData = buildUploadFormData(folder, destination)
|
const formData = buildUploadFormData(folder)
|
||||||
|
|
||||||
onProgress(10)
|
onProgress(10)
|
||||||
|
|
||||||
|
|||||||
+9
-21
@@ -2,17 +2,11 @@
|
|||||||
// Client-side folder validation
|
// Client-side folder validation
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
import { REQUIRED_TEXTURES, TEXTURE_EXTENSIONS } from '@/lib/constants'
|
import { TEXTURE_EXTENSIONS } from '@/lib/constants'
|
||||||
import type { TextureFile } from '@/lib/client-types'
|
import type { TextureFile } from '@/lib/client-types'
|
||||||
|
|
||||||
const TEXTURE_EXT_ARRAY = [...TEXTURE_EXTENSIONS]
|
const TEXTURE_EXT_ARRAY = [...TEXTURE_EXTENSIONS]
|
||||||
|
|
||||||
function getTextureType(filename: string): string | null {
|
|
||||||
const name = filename.toLowerCase().replace(/\.[^.]+$/, '')
|
|
||||||
if ((REQUIRED_TEXTURES as readonly string[]).includes(name)) return name
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Discriminated union: either valid (with model) or invalid (with errors). */
|
/** Discriminated union: either valid (with model) or invalid (with errors). */
|
||||||
export type ValidationResult =
|
export type ValidationResult =
|
||||||
| { ok: true; model: File; textures: TextureFile[]; warnings: string[] }
|
| { ok: true; model: File; textures: TextureFile[]; warnings: string[] }
|
||||||
@@ -20,39 +14,33 @@ export type ValidationResult =
|
|||||||
|
|
||||||
export function validateFolder(files: File[]): ValidationResult {
|
export function validateFolder(files: File[]): ValidationResult {
|
||||||
const textures: TextureFile[] = []
|
const textures: TextureFile[] = []
|
||||||
const warnings: string[] = []
|
|
||||||
const errors: string[] = []
|
const errors: string[] = []
|
||||||
|
|
||||||
const modelFiles = files.filter((f) => {
|
const modelFiles = files.filter((f) => {
|
||||||
const name = f.name.toLowerCase()
|
const name = f.name.toLowerCase()
|
||||||
return name === 'model.glb' || name === 'model.gltf'
|
return name === 'model.glb'
|
||||||
})
|
})
|
||||||
|
|
||||||
if (modelFiles.length === 0) {
|
if (modelFiles.length === 0) {
|
||||||
return { ok: false, errors: ['model.glb ou model.gltf manquant (obligatoire)'] }
|
return { ok: false, errors: ['model.glb manquant (obligatoire)'] }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelFiles.length > 1) {
|
||||||
|
return { ok: false, errors: ['Un seul fichier model.glb est autorise'] }
|
||||||
}
|
}
|
||||||
|
|
||||||
const textureFiles = files.filter((f) => {
|
const textureFiles = files.filter((f) => {
|
||||||
const ext = f.name.slice(f.name.lastIndexOf('.')).toLowerCase()
|
const ext = f.name.slice(f.name.lastIndexOf('.')).toLowerCase()
|
||||||
return TEXTURE_EXT_ARRAY.includes(ext) && getTextureType(f.name) !== null
|
return TEXTURE_EXT_ARRAY.includes(ext)
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const tf of textureFiles) {
|
for (const tf of textureFiles) {
|
||||||
textures.push({ name: tf.name, file: tf })
|
textures.push({ name: tf.name, file: tf })
|
||||||
}
|
}
|
||||||
|
|
||||||
const foundTextures = new Set(
|
|
||||||
textures.map((t) => t.name.toLowerCase().replace(/\.[^.]+$/, '')),
|
|
||||||
)
|
|
||||||
for (const req of REQUIRED_TEXTURES) {
|
|
||||||
if (!foundTextures.has(req)) {
|
|
||||||
warnings.push(`${req}.webp/png/jpg manquant`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
return { ok: false, errors }
|
return { ok: false, errors }
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ok: true, model: modelFiles[0], textures, warnings }
|
return { ok: true, model: modelFiles[0], textures, warnings: [] }
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-14
@@ -10,22 +10,22 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/rest": "^22.0.1",
|
"@octokit/rest": "^22.0.1",
|
||||||
"@react-three/drei": "^10.7.0",
|
"@react-three/drei": "^10.7.7",
|
||||||
"@react-three/fiber": "^9.5.0",
|
"@react-three/fiber": "^9.6.0",
|
||||||
"next": "^16.2.1",
|
"next": "^16.2.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.2.5",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"three": "^0.183.0"
|
"three": "^0.183.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.13.0",
|
"@types/node": "^22.19.17",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/three": "^0.183.0",
|
"@types/three": "^0.183.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.5.0",
|
||||||
"postcss": "^8.5.1",
|
"postcss": "^8.5.10",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.19",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1947
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user