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:
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\Configuratore;
|
||||
|
||||
use App\Models\Modello;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Modelli 3D dei prodotti, caricati dal backoffice e associati al codice del modello.
|
||||
*
|
||||
* Di ogni .glb si tengono due copie:
|
||||
* - l'originale non compresso, sul disco privato (`local`), conservato per sviluppi futuri
|
||||
* (es. ricompressione con altre opzioni, altri formati);
|
||||
* - la copia compressa con Draco, sul disco `public`, l'unica servita al configuratore
|
||||
* (rotta API media, con CORS). Il nome contiene un hash del contenuto, così il browser può
|
||||
* tenerla in cache a lungo e ogni nuovo caricamento cambia URL.
|
||||
*/
|
||||
class Modelli3d
|
||||
{
|
||||
public const CARTELLA_ORIGINALI = 'modelli-3d/originali';
|
||||
|
||||
public const CARTELLA_DRACO = 'modelli-3d/draco';
|
||||
|
||||
public function __construct(private CompressoreGlb $compressore) {}
|
||||
|
||||
/**
|
||||
* Associa al modello il .glb in $sorgente: lo comprime e salva le due copie. Se il file non è
|
||||
* valido lancia Modello3dNonValido senza toccare il modello né i file precedenti.
|
||||
*/
|
||||
public function carica(Modello $modello, string $sorgente, ?string $nomeOriginale = null): void
|
||||
{
|
||||
$this->verificaGlb($sorgente);
|
||||
|
||||
$compresso = $this->comprimi($sorgente);
|
||||
|
||||
try {
|
||||
$base = $this->nomeBase($modello);
|
||||
$hash = substr(hash_file('sha256', $compresso), 0, 12);
|
||||
$originale = self::CARTELLA_ORIGINALI."/{$base}-{$hash}.glb";
|
||||
$draco = self::CARTELLA_DRACO."/{$base}-{$hash}.glb";
|
||||
|
||||
$this->scrivi('local', $originale, $sorgente);
|
||||
$this->scrivi('public', $draco, $compresso);
|
||||
|
||||
$this->aggiorna($modello, [
|
||||
'modello_3d' => $originale,
|
||||
'modello_3d_draco' => $draco,
|
||||
'modello_3d_nome' => $nomeOriginale ?: basename($sorgente),
|
||||
'modello_3d_dimensione' => filesize($sorgente),
|
||||
'modello_3d_draco_dimensione' => filesize($compresso),
|
||||
]);
|
||||
} finally {
|
||||
@unlink($compresso);
|
||||
}
|
||||
}
|
||||
|
||||
/** Rigenera la copia compressa dall'originale (es. dopo un cambio delle opzioni Draco). */
|
||||
public function ricomprimi(Modello $modello): void
|
||||
{
|
||||
if (! $modello->modello_3d || ! Storage::disk('local')->exists($modello->modello_3d)) {
|
||||
throw new Modello3dNonValido("Il modello {$modello->codice} non ha un originale da ricomprimere.");
|
||||
}
|
||||
|
||||
$compresso = $this->comprimi(Storage::disk('local')->path($modello->modello_3d));
|
||||
|
||||
try {
|
||||
$hash = substr(hash_file('sha256', $compresso), 0, 12);
|
||||
$draco = self::CARTELLA_DRACO."/{$this->nomeBase($modello)}-{$hash}.glb";
|
||||
$this->scrivi('public', $draco, $compresso);
|
||||
|
||||
$this->aggiorna($modello, [
|
||||
'modello_3d_draco' => $draco,
|
||||
'modello_3d_draco_dimensione' => filesize($compresso),
|
||||
]);
|
||||
} finally {
|
||||
@unlink($compresso);
|
||||
}
|
||||
}
|
||||
|
||||
/** Elimina i file del modello 3D (chiamato quando il modello viene cancellato). */
|
||||
public function elimina(Modello $modello): void
|
||||
{
|
||||
$this->eliminaFile($modello->modello_3d, $modello->modello_3d_draco);
|
||||
}
|
||||
|
||||
/** Controlla l'intestazione GLB (magic "glTF", versione 2) prima di avviare la compressione. */
|
||||
public function verificaGlb(string $percorso): void
|
||||
{
|
||||
$intestazione = is_file($percorso) ? (string) file_get_contents($percorso, length: 12) : '';
|
||||
|
||||
if (strlen($intestazione) < 12 || substr($intestazione, 0, 4) !== 'glTF') {
|
||||
throw new Modello3dNonValido('Il file non è un modello glTF binario (.glb).');
|
||||
}
|
||||
|
||||
if (unpack('V', substr($intestazione, 4, 4))[1] !== 2) {
|
||||
throw new Modello3dNonValido('Il file .glb deve essere in formato glTF 2.0.');
|
||||
}
|
||||
}
|
||||
|
||||
private function comprimi(string $sorgente): string
|
||||
{
|
||||
// tempnam crea il file: lo si sostituisce con uno con estensione .glb.
|
||||
$temporaneo = tempnam(sys_get_temp_dir(), 'draco');
|
||||
@unlink($temporaneo);
|
||||
$destinazione = $temporaneo.'.glb';
|
||||
|
||||
try {
|
||||
$this->compressore->comprimi($sorgente, $destinazione);
|
||||
} catch (\Throwable $e) {
|
||||
@unlink($destinazione);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $destinazione;
|
||||
}
|
||||
|
||||
/** Salva i nuovi percorsi ed elimina i file sostituiti. */
|
||||
private function aggiorna(Modello $modello, array $valori): void
|
||||
{
|
||||
$vecchi = [$modello->modello_3d, $modello->modello_3d_draco];
|
||||
|
||||
$modello->forceFill($valori + ['modello_3d_aggiornato_at' => now()])->save();
|
||||
|
||||
$this->eliminaFile(...array_diff(array_filter($vecchi), [$modello->modello_3d, $modello->modello_3d_draco]));
|
||||
}
|
||||
|
||||
private function eliminaFile(?string ...$percorsi): void
|
||||
{
|
||||
foreach (array_filter($percorsi) as $percorso) {
|
||||
$disco = str_starts_with($percorso, self::CARTELLA_ORIGINALI) ? 'local' : 'public';
|
||||
Storage::disk($disco)->delete($percorso);
|
||||
}
|
||||
}
|
||||
|
||||
private function scrivi(string $disco, string $percorso, string $file): void
|
||||
{
|
||||
$stream = fopen($file, 'rb');
|
||||
|
||||
try {
|
||||
if (! Storage::disk($disco)->writeStream($percorso, $stream)) {
|
||||
throw new Modello3dNonValido("Impossibile salvare il file {$percorso}.");
|
||||
}
|
||||
} finally {
|
||||
if (is_resource($stream)) {
|
||||
fclose($stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Es. "pvc/finestra_2_ante": sistema e codice del modello, per riconoscere i file a colpo d'occhio. */
|
||||
private function nomeBase(Modello $modello): string
|
||||
{
|
||||
$sistema = Str::slug($modello->sistema?->codice ?? 'sistema');
|
||||
|
||||
return $sistema.'/'.Str::slug($modello->codice, '_');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user