Spyspook's picture
initial commit
ce82348 verified
# src/layout_generator/exporter.py
"""
Шаг пайплайна для экспорта 3D-сцены и Draco-компрессии.
"""
import logging
import os
import subprocess
from pathlib import Path
from .base import BaseStep, LayoutContext
logger: logging.Logger = logging.getLogger(__name__)
class ExportAndCompressStep(BaseStep):
"""Сохраняет сцену на жесткий диск и сжимает для веб-рендера."""
def process(self, context: LayoutContext) -> LayoutContext:
if not context.final_scene:
return context
project_dir_path: Path = Path(context.project_dir)
data_dir: Path = project_dir_path / "data"
data_dir.mkdir(parents=True, exist_ok=True)
output_filename: str = str(data_dir / f"inventory_layout_{context.size_n}x{context.size_m}.glb")
uncompressed_filename: str = str(data_dir / f"uncompressed_layout_{context.size_n}x{context.size_m}_{os.getpid()}.glb")
context.final_scene.export(uncompressed_filename)
logger.info("🗜️ Сжимаем геометрию с помощью Draco (gltf-pipeline)...")
try:
subprocess.run(
["gltf-pipeline", "-i", uncompressed_filename, "-o", output_filename, "-d"],
check=True,
capture_output=True
)
old_size: float = os.path.getsize(uncompressed_filename) / (1024 * 1024)
new_size: float = os.path.getsize(output_filename) / (1024 * 1024)
logger.info(f"✅ Draco-компрессия завершена! Размер уменьшен с {old_size:.1f} MB до {new_size:.1f} MB.")
if os.path.exists(uncompressed_filename):
os.remove(uncompressed_filename)
except FileNotFoundError:
logger.warning("⚠️ Утилита gltf-pipeline не найдена. Сохраняем тяжелый несжатый файл.")
os.rename(uncompressed_filename, output_filename)
except Exception as e:
logger.warning(f"⚠️ Ошибка при сжатии Draco ({e}). Сохраняем несжатый файл как запасной вариант.")
if os.path.exists(output_filename):
os.remove(output_filename)
os.rename(uncompressed_filename, output_filename)
context.output_filename = output_filename
# --- Генерация финального лог-отчета ---
failed_count: int = len(context.tracker.failed_items) if context.tracker else 0
# Безопасный подсчет запланированных товаров (устойчив к изменениям формата отчета)
scheduled_total: int = 0
for profile in context.report_matrix.values():
if isinstance(profile, dict):
scheduled_total += profile.get("total", 0)
elif isinstance(profile, int):
scheduled_total += profile
logger.info("=" * 60)
logger.info("📋 ОТЧЕТ: АНАЛИЗ ПЛАНОГРАММЫ И ВМЕСТИМОСТИ")
logger.info(f"Помещение: {context.size_n}x{context.size_m}. Успешно построено стеллажей: {context.actual_shelf_idx} шт.")
logger.info(f"Вместилось на полки (запланировано): {scheduled_total} SKU")
logger.info(f"Физически размещено без коллизий: {scheduled_total - failed_count} SKU")
if failed_count > 0:
logger.warning(f"⚠️ Упало с полок (ошибка физики движка): {failed_count} SKU")
logger.info("=" * 60)
return context