feat: pdf2cad
This commit is contained in:
208
src/pdf2imos/reconstruct/assembler.py
Normal file
208
src/pdf2imos/reconstruct/assembler.py
Normal file
@@ -0,0 +1,208 @@
|
||||
"""Part geometry assembly from orthographic dimension measurements."""
|
||||
import logging
|
||||
|
||||
from pdf2imos.models import (
|
||||
DimensionAnnotation,
|
||||
DimensionDirection,
|
||||
PartGeometry,
|
||||
ViewRegion,
|
||||
ViewType,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def assemble_part_geometry(
|
||||
views: list[ViewRegion],
|
||||
dimensions: dict[ViewType, list[DimensionAnnotation]],
|
||||
part_name: str = "unknown",
|
||||
tolerance_mm: float = 0.5,
|
||||
) -> PartGeometry | None:
|
||||
"""Assemble W×H×D dimensions from orthographic views into PartGeometry.
|
||||
|
||||
Args:
|
||||
views: ViewRegion list from segment_views()
|
||||
dimensions: Dict mapping ViewType → list of DimensionAnnotations for that view
|
||||
part_name: Name for the part (from title block)
|
||||
tolerance_mm: Cross-validation tolerance in mm
|
||||
|
||||
Returns:
|
||||
PartGeometry or None if assembly fails
|
||||
"""
|
||||
if not dimensions:
|
||||
logger.error("No dimensions provided for assembly")
|
||||
return None
|
||||
|
||||
# Extract dimensions by view
|
||||
front_dims = dimensions.get(ViewType.FRONT, [])
|
||||
side_dims = dimensions.get(ViewType.SIDE, [])
|
||||
top_dims = dimensions.get(ViewType.TOP, [])
|
||||
|
||||
# Fall back: if no view-specific dims, use all dims combined
|
||||
all_dims: list[DimensionAnnotation] = []
|
||||
for dims in dimensions.values():
|
||||
all_dims.extend(dims)
|
||||
|
||||
if not all_dims:
|
||||
logger.error("No dimension annotations available")
|
||||
return None
|
||||
|
||||
# Extract W, H, D
|
||||
width_mm = _extract_dimension(
|
||||
front_dims or all_dims, DimensionDirection.HORIZONTAL, "width"
|
||||
)
|
||||
height_mm = _extract_dimension(
|
||||
front_dims or all_dims, DimensionDirection.VERTICAL, "height"
|
||||
)
|
||||
|
||||
# For depth: prefer side view horizontal, then top view vertical, then smallest dim
|
||||
depth_mm: float | None = None
|
||||
if side_dims:
|
||||
depth_mm = _extract_dimension(
|
||||
side_dims, DimensionDirection.HORIZONTAL, "depth"
|
||||
)
|
||||
if depth_mm is None:
|
||||
depth_mm = _extract_dimension(
|
||||
side_dims, DimensionDirection.VERTICAL, "depth"
|
||||
)
|
||||
elif top_dims:
|
||||
depth_mm = _extract_dimension(
|
||||
top_dims, DimensionDirection.VERTICAL, "depth"
|
||||
)
|
||||
# Sanity check: if depth from top view matches height, it's misattributed
|
||||
if (
|
||||
depth_mm is not None
|
||||
and height_mm is not None
|
||||
and abs(depth_mm - height_mm) < tolerance_mm
|
||||
):
|
||||
logger.debug(
|
||||
"Top view depth (%s) matches height — seeking alternative", depth_mm
|
||||
)
|
||||
depth_mm = _extract_smallest_remaining(
|
||||
top_dims, exclude={width_mm, height_mm}
|
||||
)
|
||||
|
||||
if depth_mm is None:
|
||||
# No dedicated view or sanity check failed: use smallest remaining
|
||||
depth_mm = _extract_smallest_remaining(
|
||||
all_dims, exclude={width_mm, height_mm}
|
||||
)
|
||||
|
||||
if width_mm is None or height_mm is None:
|
||||
logger.error("Cannot assemble: width=%s, height=%s", width_mm, height_mm)
|
||||
return None
|
||||
|
||||
if depth_mm is None:
|
||||
logger.warning("Depth not found — defaulting to 18mm")
|
||||
depth_mm = 18.0
|
||||
|
||||
# Cross-validate
|
||||
_cross_validate(
|
||||
front_dims, side_dims, top_dims,
|
||||
width_mm, height_mm, depth_mm, tolerance_mm,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Assembled: %s×%s×%smm (W×H×D)", width_mm, height_mm, depth_mm
|
||||
)
|
||||
|
||||
return PartGeometry(
|
||||
width_mm=width_mm,
|
||||
height_mm=height_mm,
|
||||
depth_mm=depth_mm,
|
||||
origin=(0.0, 0.0, 0.0),
|
||||
name=part_name,
|
||||
)
|
||||
|
||||
|
||||
def _extract_dimension(
|
||||
dims: list[DimensionAnnotation],
|
||||
direction: DimensionDirection,
|
||||
dim_name: str,
|
||||
) -> float | None:
|
||||
"""Extract the largest dimension of a given direction (primary/overall dimension).
|
||||
|
||||
Returns the largest value of matching direction, or None if none found.
|
||||
"""
|
||||
matching = [d for d in dims if d.direction == direction]
|
||||
|
||||
if not matching:
|
||||
# If no exact direction match, try all dims and pick the largest
|
||||
logger.debug(
|
||||
"No %s dimension found for %s, using all", direction.name, dim_name
|
||||
)
|
||||
matching = dims
|
||||
|
||||
if not matching:
|
||||
return None
|
||||
|
||||
# Return the largest dimension (overall/total, not partial)
|
||||
return max(d.value_mm for d in matching)
|
||||
|
||||
|
||||
def _extract_smallest_remaining(
|
||||
dims: list[DimensionAnnotation],
|
||||
exclude: set[float | None],
|
||||
) -> float | None:
|
||||
"""Extract the smallest dimension value not in the exclude set."""
|
||||
values = sorted(d.value_mm for d in dims if d.value_mm not in exclude)
|
||||
return values[0] if values else None
|
||||
|
||||
|
||||
def _cross_validate(
|
||||
front_dims: list[DimensionAnnotation],
|
||||
side_dims: list[DimensionAnnotation],
|
||||
top_dims: list[DimensionAnnotation],
|
||||
width: float,
|
||||
height: float,
|
||||
depth: float,
|
||||
tolerance: float,
|
||||
) -> None:
|
||||
"""Cross-validate dimensions from different views and log warnings/info."""
|
||||
# Check front height ≈ side height
|
||||
if front_dims and side_dims:
|
||||
front_heights = [
|
||||
d.value_mm for d in front_dims
|
||||
if d.direction == DimensionDirection.VERTICAL
|
||||
]
|
||||
side_heights = [
|
||||
d.value_mm for d in side_dims
|
||||
if d.direction == DimensionDirection.VERTICAL
|
||||
]
|
||||
if front_heights and side_heights:
|
||||
front_h = max(front_heights)
|
||||
side_h = max(side_heights)
|
||||
if abs(front_h - side_h) <= tolerance:
|
||||
logger.info(
|
||||
"Cross-validation: front H (%smm) ≈ side H (%smm) ✓",
|
||||
front_h, side_h,
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"Cross-validation: front H (%smm) ≠ side H (%smm) — using front",
|
||||
front_h, side_h,
|
||||
)
|
||||
|
||||
# Check front width ≈ top width
|
||||
if front_dims and top_dims:
|
||||
front_widths = [
|
||||
d.value_mm for d in front_dims
|
||||
if d.direction == DimensionDirection.HORIZONTAL
|
||||
]
|
||||
top_widths = [
|
||||
d.value_mm for d in top_dims
|
||||
if d.direction == DimensionDirection.HORIZONTAL
|
||||
]
|
||||
if front_widths and top_widths:
|
||||
front_w = max(front_widths)
|
||||
top_w = max(top_widths)
|
||||
if abs(front_w - top_w) <= tolerance:
|
||||
logger.info(
|
||||
"Cross-validation: front W (%smm) ≈ top W (%smm) ✓",
|
||||
front_w, top_w,
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"Cross-validation: front W (%smm) ≠ top W (%smm) — using front",
|
||||
front_w, top_w,
|
||||
)
|
||||
Reference in New Issue
Block a user