nodeslicer/core/pdf_exporter/metadata.py
2026-07-31 14:25:38 +03:00

122 lines
4.3 KiB
Python

"""
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 .sa2_measures import extract_slicing_objects_bounds
sa2_objects = extract_slicing_objects_bounds(mesh_data)
if not sa2_objects:
return np.empty((0, 2, 2), dtype=np.float64)
all_segments: list[np.ndarray] = []
for item, name, min_x, max_x, min_y, max_y in sa2_objects:
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)
return np.concatenate(all_segments, axis=0)
# Aliases for backward compatibility
extract_object_name_positions = extract_object_label_info