import logging from dataclasses import dataclass import trimesh logger = logging.getLogger("app.services.file_parser") @dataclass class FileInfo: volume_cm3: float surface_area_cm2: float bounding_box_mm: dict[str, float] is_watertight: bool triangle_count: int SUPPORTED_EXTENSIONS = {".stl", ".3mf", ".obj"} def parse_3d_file(file_path: str, file_extension: str) -> FileInfo: """Parse a 3D file and return geometric properties.""" ext = file_extension.lower().lstrip(".") logger.info("Parsing 3D file: path=%s, extension=%s", file_path, ext) logger.debug("Loading mesh with trimesh (file_type=%s)...", ext) mesh = trimesh.load(file_path, file_type=ext) logger.debug("Trimesh loaded object type: %s", type(mesh).__name__) if isinstance(mesh, trimesh.Scene): meshes = list(mesh.dump()) logger.info("File is a Scene with %d geometries, concatenating...", len(meshes)) if not meshes: logger.error("Scene contains no geometries") raise ValueError("Файл не содержит 3D-геометрии") mesh = trimesh.util.concatenate(meshes) logger.debug("Concatenated into single Trimesh") if not isinstance(mesh, trimesh.Trimesh): logger.error("Could not extract Trimesh object, got: %s", type(mesh).__name__) raise ValueError("Не удалось извлечь 3D-геометрию из файла") volume_cm3 = abs(mesh.volume) / 1000.0 surface_area_cm2 = mesh.area / 100.0 bbox = { "x": round(float(mesh.bounding_box.extents[0]), 2), "y": round(float(mesh.bounding_box.extents[1]), 2), "z": round(float(mesh.bounding_box.extents[2]), 2), } is_watertight = bool(mesh.is_watertight) triangle_count = len(mesh.faces) logger.info("Parse result: volume=%.2f cm3, area=%.2f cm2, bbox=%s, watertight=%s, triangles=%d", volume_cm3, surface_area_cm2, bbox, is_watertight, triangle_count) return FileInfo( volume_cm3=volume_cm3, surface_area_cm2=surface_area_cm2, bounding_box_mm=bbox, is_watertight=is_watertight, triangle_count=triangle_count, )