# src/layout_generator/assembler.py """ Шаг пайплайна для физической сборки 3D-сцены в непрерывном пространстве. """ import logging import os from typing import Any, List import numpy as np import scene_synthesizer as synth import trimesh from dsynth.assets.ss_assets import DefaultShelf from dsynth.scene_gen.arrangements import add_objects_to_shelf_v2, set_shelf from .base import BaseStep, LayoutContext logger: logging.Logger = logging.getLogger(__name__) class AssembleSceneStep(BaseStep): """Шаг физического рендера 3D-сцены на основе расстановки из тензорного поля.""" def process(self, context: LayoutContext) -> LayoutContext: """Сборка 3D-моделей по заданным координатам.""" if not context.is_gen or not hasattr(context, 'placed_fixtures'): return context logger.info("🛠️ Сборка 3D-сцены и физическая выкладка инвентаря...") scene: synth.Scene = synth.Scene() actual_shelf_idx: int = 0 for fixture in context.placed_fixtures: actual_shelf_idx += 1 shelf_name: str = fixture.name zone_id, shelf_id = shelf_name.split('.') shelf_cfg: Any = context.fixed_zones[zone_id][shelf_id] shelf_asset_name: Any = shelf_cfg.shelf_asset asset_obj: Any = context.product_assets_lib.get(shelf_asset_name) if asset_obj is None and shelf_asset_name: asset_obj = context.product_assets_lib.get(f"fixtures.{shelf_asset_name}") shelf: Any = DefaultShelf if asset_obj is None else asset_obj.ss_asset # Извлекаем идеальные координаты и угол, посчитанные тензорным полем safe_x: float = fixture.x safe_y: float = fixture.y rotation_deg: int = getattr(fixture, '_rotation_deg', 0) # Ставим оборудование на сцену support_data: Any = set_shelf( scene, shelf, safe_x, safe_y, rotation_deg, f'SHELF_{actual_shelf_idx}_{shelf_name}', f'support_SHELF_{actual_shelf_idx}_{shelf_name}' ) # Выкладка товаров if shelf_name in context.product_filling: placement_data: List[List[str]] = context.product_filling[shelf_name] if placement_data: try: add_objects_to_shelf_v2( scene, actual_shelf_idx, placement_data, context.product_assets_lib, support_data, shelf_cfg.x_gap, shelf_cfg.y_gap, shelf_cfg.delta_x, shelf_cfg.delta_y, shelf_cfg.start_point_x, shelf_cfg.start_point_y, shelf_cfg.filling_type, seed=context.current_seed, noise_std_x=shelf_cfg.noise_std_x, noise_std_y=shelf_cfg.noise_std_y, rotation_lower=shelf_cfg.rotation_lower, rotation_upper=shelf_cfg.rotation_upper ) except Exception as e: logger.error(f"Ошибка выкладки на полку {shelf_name}: {e}") # Динамическая генерация пола по габаритам расставленной мебели margin: float = 2.0 t: float = 0.2 if context.placed_fixtures: all_x: List[float] = [] all_y: List[float] = [] for f in context.placed_fixtures: poly, _ = f.get_polygon() all_x.extend(poly[:, 0]) all_y.extend(poly[:, 1]) min_x, max_x = min(all_x), max(all_x) min_y, max_y = min(all_y), max(all_y) else: min_x, max_x = 0.0, float(context.size_n) min_y, max_y = 0.0, float(context.size_m) cx: float = (max_x + min_x) / 2.0 cy: float = (max_y + min_y) / 2.0 w_x: float = (max_x - min_x) + margin * 2 w_y: float = (max_y - min_y) + margin * 2 temp_filename: str = f"temp_scene_{os.getpid()}.glb" scene.export(temp_filename) final_scene: trimesh.Scene = trimesh.load(temp_filename, force='scene') if os.path.exists(temp_filename): os.remove(temp_filename) floor: trimesh.Trimesh = trimesh.creation.box(extents=[w_x, w_y, t]) floor.apply_translation([cx, cy, -t/2]) floor.visual.face_colors = [180, 180, 180, 255] final_scene.add_geometry(floor, node_name="floor") R: np.ndarray = trimesh.transformations.rotation_matrix(-np.pi / 2, [1, 0, 0]) final_scene.apply_transform(R) context.final_scene = final_scene context.actual_shelf_idx = actual_shelf_idx return context