"""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, )