145 lines
5.1 KiB
Python
145 lines
5.1 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 resolve_object_properties(so_item: Any, props: list[tuple[str, str]]) -> list[tuple[str, str]]:
|
|
"""
|
|
Resolves property tuples for a SlicingObject.
|
|
If the first position (p1) exists as an attribute on so_item and the second position (p2) is empty,
|
|
replaces p2 with the value of that attribute at index zero.
|
|
"""
|
|
resolved: list[tuple[str, str]] = []
|
|
for p1, p2 in props:
|
|
if p1 and (not p2 or p2 == ""):
|
|
attr_val = get_first_attribute_value(so_item, p1)
|
|
if attr_val is not None:
|
|
if isinstance(attr_val, float):
|
|
p2_val = f"{attr_val:g}"
|
|
else:
|
|
p2_val = str(attr_val)
|
|
resolved.append((p1, p2_val))
|
|
else:
|
|
resolved.append((p1, p2))
|
|
else:
|
|
resolved.append((p1, p2))
|
|
return resolved
|
|
|
|
|
|
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
|