""" Object Metadata and Bounding Box Extraction Module for PDF Exporter. Extracts SlicingObject names, 2D bounding boxes, and custom settings attributes. """ from typing import Any import numpy as np from numpy.typing import NDArray def get_first_attribute_value(so_item: Any, attr_name: str) -> Any: """ Extracts scalar attribute value for a SlicingObject. """ if not hasattr(so_item, 'attributes') or not isinstance(so_item.attributes, dict): return None if attr_name not in so_item.attributes: return None attr_info = so_item.attributes[attr_name] if isinstance(attr_info, dict) and 'values' in attr_info: vals = attr_info['values'] elif isinstance(attr_info, (list, tuple, np.ndarray)): vals = attr_info else: return attr_info if len(vals) == 0: return None val = vals[0] if hasattr(val, 'item'): return val.item() return val def extract_settings_items(settings: Any) -> list[tuple[str, str]]: """ Parses settings dict or object into key/suffix list. """ if settings is None: return [] items: list[tuple[str, str]] = [] if isinstance(settings, dict): for k, v in settings.items(): suffix = str(v) if v is not None else "" items.append((str(k), suffix)) elif hasattr(settings, '__dict__'): for k, v in settings.__dict__.items(): if not k.startswith('_'): suffix = str(v) if v is not None else "" items.append((str(k), suffix)) return items def extract_object_label_info(mesh_data: Any) -> list[tuple[Any, str, float, float, float, float]]: """ Extracts (slicing_object, object_name, max_x_mm, y_at_max_x_mm, width_mm, height_mm) for each SlicingObject in mesh_data. """ from ...infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject results: list[tuple[Any, str, float, float, float, float]] = [] def process_item(item: Any, fallback_name: str = ""): if item is None: return elif isinstance(item, (list, tuple)): for i, sub in enumerate(item): process_item(sub, f"{fallback_name}_{i}" if fallback_name else f"object_{i}") elif isinstance(item, SlicingGroup): for key, coll in item.collections.items(): process_item(coll, key) elif isinstance(item, SlicingCollection): coll_name = item.name or fallback_name for i, so in enumerate(item.objects): process_item(so, so.name or f"{coll_name}_{i}") elif isinstance(item, SlicingObject): if item.mesh.size > 0: name = item.name or fallback_name or "object" verts_2d = item.mesh[:, :3, :2].reshape(-1, 2) if len(verts_2d) > 0: max_idx = np.argmax(verts_2d[:, 0]) max_x = float(verts_2d[max_idx, 0]) min_x = float(verts_2d[:, 0].min()) y_val = float(verts_2d[max_idx, 1]) min_y = float(verts_2d[:, 1].min()) max_y = float(verts_2d[:, 1].max()) w_mm = max_x - min_x h_mm = max_y - min_y results.append((item, name, max_x, y_val, w_mm, h_mm)) process_item(mesh_data) return results def extract_object_bounding_boxes(mesh_data: Any) -> NDArray[np.float64]: """ Extracts 2D top-view bounding box rectangle edge segments (shape N x 2 x 2) for each SlicingObject in mesh_data. """ from ...infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject all_segments: list[np.ndarray] = [] def process_item(item: Any): if item is None: return elif isinstance(item, (list, tuple)): for sub in item: process_item(sub) elif isinstance(item, SlicingGroup): for coll in item.collections.values(): process_item(coll) elif isinstance(item, SlicingCollection): for so in item.objects: process_item(so) elif isinstance(item, SlicingObject): if item.mesh.size > 0: verts_2d = item.mesh[:, :3, :2].reshape(-1, 2) if len(verts_2d) > 0: min_x = float(verts_2d[:, 0].min()) max_x = float(verts_2d[:, 0].max()) min_y = float(verts_2d[:, 1].min()) max_y = float(verts_2d[:, 1].max()) rect = np.array([ [[min_x, min_y], [max_x, min_y]], [[max_x, min_y], [max_x, max_y]], [[max_x, max_y], [min_x, max_y]], [[min_x, max_y], [min_x, min_y]], ], dtype=np.float64) all_segments.append(rect) process_item(mesh_data) if not all_segments: return np.empty((0, 2, 2), dtype=np.float64) return np.concatenate(all_segments, axis=0) # Aliases for backward compatibility extract_object_name_positions = extract_object_label_info