feat: integrate Draco compression for 3D models in backoffice
- Added support for uploading and compressing GLB models using Draco in the backoffice. - Implemented commands for importing and re-compressing 3D models. - Updated model resource to handle 3D model uploads and validations. - Introduced a new script for compressing GLB files with Draco. - Modified Vite configuration to remove GLB files from the build output. - Updated package dependencies to include GLTF-transform libraries for handling 3D models. - Added tests to ensure proper functionality of 3D model handling and compression.
This commit is contained in:
@@ -8,6 +8,9 @@ use App\Filament\Resources\ModelloResource\RelationManagers\FinitureRelationMana
|
||||
use App\Filament\Resources\ModelloResource\RelationManagers\ListinoRelationManager;
|
||||
use App\Filament\Resources\ModelloResource\RelationManagers\OpzioniRelationManager;
|
||||
use App\Models\Modello;
|
||||
use App\Support\Configuratore\Modelli3d;
|
||||
use App\Support\Configuratore\Modello3dNonValido;
|
||||
use Closure;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
@@ -15,6 +18,8 @@ use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Number;
|
||||
use Illuminate\Validation\Rules\Unique;
|
||||
|
||||
|
||||
@@ -89,7 +94,7 @@ class ModelloResource extends Resource
|
||||
->default(0),
|
||||
Forms\Components\Toggle::make('attivo')
|
||||
->label('Visibile nel configuratore')
|
||||
->helperText('Richiede la configurazione 3D.')
|
||||
->helperText('Richiede la configurazione 3D (con il modello .glb).')
|
||||
->default(true)
|
||||
->live(),
|
||||
]),
|
||||
@@ -103,10 +108,41 @@ class ModelloResource extends Resource
|
||||
->label('Cartella dati')
|
||||
->helperText('src/assets/data/modelli/<sistema>/<cartella>/')
|
||||
->required(fn (Get $get) => (bool) $get('attivo')),
|
||||
Forms\Components\TextInput::make('file_modello')
|
||||
->label('File modello 3D')
|
||||
->helperText('src/assets/models/<sistema>/<file>.glb')
|
||||
->required(fn (Get $get) => (bool) $get('attivo')),
|
||||
Forms\Components\Placeholder::make('modello_3d_attuale')
|
||||
->key('modello_3d_attuale')
|
||||
->label('Modello 3D attuale')
|
||||
->visible(fn (?Modello $record) => (bool) $record?->modello_3d_draco)
|
||||
->content(fn (Modello $record) => self::descriviModello3d($record))
|
||||
->hintActions([
|
||||
Forms\Components\Actions\Action::make('scarica_originale')
|
||||
->label('Originale')
|
||||
->icon('heroicon-m-arrow-down-tray')
|
||||
->visible(fn (?Modello $record) => (bool) $record?->modello_3d)
|
||||
->action(fn (Modello $record) => Storage::disk('local')->download(
|
||||
$record->modello_3d,
|
||||
$record->modello_3d_nome ?: basename($record->modello_3d),
|
||||
)),
|
||||
Forms\Components\Actions\Action::make('scarica_draco')
|
||||
->label('Compresso')
|
||||
->icon('heroicon-m-arrow-down-tray')
|
||||
->url(fn (Modello $record) => $record->modello_3d_url, shouldOpenInNewTab: true),
|
||||
])
|
||||
->columnSpanFull(),
|
||||
Forms\Components\FileUpload::make('modello_3d_upload')
|
||||
->label(fn (?Modello $record) => $record?->modello_3d_draco ? 'Sostituisci modello 3D' : 'Modello 3D (.glb)')
|
||||
->helperText('File .glb non compresso. Al salvataggio viene compresso con Draco per il configuratore; l\'originale resta archiviato nel backoffice.')
|
||||
// Il file lo gestisce Modelli3d (vedi Pages\Concerns\CaricaModello3d), non Filament.
|
||||
->storeFiles(false)
|
||||
->maxSize(fn () => config('configuratore.modelli_3d.max_mb') * 1024)
|
||||
->required(fn (Get $get, ?Modello $record) => (bool) $get('attivo') && ! $record?->modello_3d_draco)
|
||||
->rule(fn () => function (string $attribute, mixed $file, Closure $fail) {
|
||||
try {
|
||||
app(Modelli3d::class)->verificaGlb($file->getRealPath());
|
||||
} catch (Modello3dNonValido $e) {
|
||||
$fail($e->getMessage());
|
||||
}
|
||||
})
|
||||
->columnSpanFull(),
|
||||
Forms\Components\Select::make('categoria')
|
||||
->label('Categoria')
|
||||
->options(Modello::CATEGORIE)
|
||||
@@ -161,6 +197,14 @@ class ModelloResource extends Resource
|
||||
->badge()
|
||||
->formatStateUsing(fn (?string $state) => Modello::CATEGORIE[$state] ?? $state)
|
||||
->placeholder('—'),
|
||||
Tables\Columns\IconColumn::make('modello_3d_draco')
|
||||
->label('3D')
|
||||
->boolean()
|
||||
->getStateUsing(fn (Modello $record) => (bool) $record->modello_3d_draco)
|
||||
->falseColor('danger')
|
||||
->tooltip(fn (Modello $record) => $record->modello_3d_draco
|
||||
? $record->modello_3d_nome
|
||||
: 'Senza modello 3D: non compare nel configuratore'),
|
||||
Tables\Columns\TextColumn::make('listino_count')
|
||||
->label('Listino')
|
||||
->counts('listino')
|
||||
@@ -174,7 +218,7 @@ class ModelloResource extends Resource
|
||||
->sortable(),
|
||||
Tables\Columns\ToggleColumn::make('attivo')
|
||||
->label('Attivo')
|
||||
->disabled(fn (Modello $record) => ! $record->attivo && (! $record->cartella || ! $record->file_modello)),
|
||||
->disabled(fn (Modello $record) => ! $record->attivo && (! $record->cartella || ! $record->modello_3d_draco)),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('sistema')->label('Sistema')->relationship('sistema', 'nome'),
|
||||
@@ -201,6 +245,19 @@ class ModelloResource extends Resource
|
||||
]);
|
||||
}
|
||||
|
||||
/** Es. "finestra_2_ante.glb — 10,8 MB → 0,3 MB con Draco (-97%), caricato il 01/11/2026 10:00". */
|
||||
private static function descriviModello3d(Modello $record): string
|
||||
{
|
||||
$mb = fn (?int $byte) => Number::fileSize((int) $byte, precision: 1);
|
||||
$riduzione = $record->modello_3d_dimensione
|
||||
? ' (-'.round(100 - $record->modello_3d_draco_dimensione * 100 / $record->modello_3d_dimensione).'%)'
|
||||
: '';
|
||||
|
||||
return ($record->modello_3d_nome ?: basename((string) $record->modello_3d_draco))
|
||||
.' — '.$mb($record->modello_3d_dimensione).' → '.$mb($record->modello_3d_draco_dimensione).' con Draco'.$riduzione
|
||||
.($record->modello_3d_aggiornato_at ? ', caricato il '.$record->modello_3d_aggiornato_at->format('d/m/Y H:i') : '');
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ModelloResource\Pages\Concerns;
|
||||
|
||||
use App\Models\Modello;
|
||||
use App\Support\Configuratore\Modelli3d;
|
||||
use App\Support\Configuratore\Modello3dNonValido;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
/**
|
||||
* Il campo "Modello 3D" non è una colonna: il file caricato passa da Modelli3d, che salva
|
||||
* l'originale e la copia compressa con Draco. Se la compressione fallisce, il salvataggio
|
||||
* viene annullato (transazione) e l'errore compare sul campo.
|
||||
*/
|
||||
trait CaricaModello3d
|
||||
{
|
||||
protected function handleRecordCreation(array $data): Model
|
||||
{
|
||||
$file = Arr::pull($data, 'modello_3d_upload');
|
||||
|
||||
return DB::transaction(function () use ($data, $file) {
|
||||
$record = parent::handleRecordCreation($data);
|
||||
$this->caricaModello3d($record, $file);
|
||||
|
||||
return $record;
|
||||
});
|
||||
}
|
||||
|
||||
protected function handleRecordUpdate(Model $record, array $data): Model
|
||||
{
|
||||
$file = Arr::pull($data, 'modello_3d_upload');
|
||||
|
||||
return DB::transaction(function () use ($record, $data, $file) {
|
||||
$record = parent::handleRecordUpdate($record, $data);
|
||||
$this->caricaModello3d($record, $file);
|
||||
|
||||
return $record;
|
||||
});
|
||||
}
|
||||
|
||||
private function caricaModello3d(Model $record, mixed $file): void
|
||||
{
|
||||
$file = is_array($file) ? Arr::first($file) : $file;
|
||||
if (! $file instanceof TemporaryUploadedFile || ! $record instanceof Modello) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
app(Modelli3d::class)->carica($record, $file->getRealPath(), $file->getClientOriginalName());
|
||||
} catch (Modello3dNonValido $e) {
|
||||
throw ValidationException::withMessages(['data.modello_3d_upload' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
$file->delete();
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,13 @@
|
||||
namespace App\Filament\Resources\ModelloResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ModelloResource;
|
||||
use App\Filament\Resources\ModelloResource\Pages\Concerns\CaricaModello3d;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateModello extends CreateRecord
|
||||
{
|
||||
use CaricaModello3d;
|
||||
|
||||
protected static string $resource = ModelloResource::class;
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
namespace App\Filament\Resources\ModelloResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ModelloResource;
|
||||
use App\Filament\Resources\ModelloResource\Pages\Concerns\CaricaModello3d;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditModello extends EditRecord
|
||||
{
|
||||
use CaricaModello3d;
|
||||
|
||||
protected static string $resource = ModelloResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
|
||||
Reference in New Issue
Block a user