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, '_'); } }