"""Tests for part geometry assembly.""" import json from dataclasses import FrozenInstanceError import pymupdf import pytest from pdf2imos.extract.geometry import extract_geometry from pdf2imos.extract.text import extract_text from pdf2imos.interpret.line_classifier import classify_lines from pdf2imos.interpret.title_block import detect_title_block, extract_title_block_info from pdf2imos.interpret.view_segmenter import segment_views from pdf2imos.models import ( DimensionAnnotation, DimensionDirection, PageExtraction, PartGeometry, ViewType, ) from pdf2imos.parse.dimensions import extract_dimensions from pdf2imos.reconstruct.assembler import assemble_part_geometry def make_full_pipeline(pdf_path): """Run full pipeline up to assembly.""" doc = pymupdf.open(str(pdf_path)) page = doc[0] page_height = page.rect.height geo = extract_geometry(page) texts = extract_text(page) extraction = PageExtraction( paths=geo.paths, texts=tuple(texts), page_width=geo.page_width, page_height=page_height, ) title_rect, filtered = detect_title_block(extraction) title_info = extract_title_block_info(extraction, title_rect) if title_rect else {} views = segment_views(filtered) # Extract dimensions per view dims_by_view: dict[ViewType, list[DimensionAnnotation]] = {} for view in views: classified = classify_lines(list(view.paths)) view_dims = extract_dimensions(view, classified, page_height) dims_by_view[view.view_type] = view_dims part_name = title_info.get("part_name", "unknown") return views, dims_by_view, part_name class TestAssemblePartGeometry: def test_returns_part_geometry_or_none(self, simple_panel_pdf): views, dims_by_view, part_name = make_full_pipeline(simple_panel_pdf) result = assemble_part_geometry(views, dims_by_view, part_name) assert result is None or isinstance(result, PartGeometry) def test_panel_assembles_correctly(self, simple_panel_pdf): """simple_panel.pdf should assemble to ~600×720×18mm.""" views, dims_by_view, part_name = make_full_pipeline(simple_panel_pdf) result = assemble_part_geometry(views, dims_by_view, part_name) if result is None: pytest.skip("Assembly returned None — insufficient dimensions") # Width: ~600mm ±5mm (relaxed tolerance for fixture PDF) assert 580 <= result.width_mm <= 650, f"Width out of range: {result.width_mm}" # Height: ~720mm ±5mm assert 700 <= result.height_mm <= 750, f"Height out of range: {result.height_mm}" # Depth: ~18mm ±5mm assert 10 <= result.depth_mm <= 30, f"Depth out of range: {result.depth_mm}" def test_result_is_frozen_dataclass(self, simple_panel_pdf): views, dims_by_view, part_name = make_full_pipeline(simple_panel_pdf) result = assemble_part_geometry(views, dims_by_view, part_name) if result is None: pytest.skip("Assembly returned None") try: result.width_mm = 0 # type: ignore[misc] msg = "Should be frozen" raise AssertionError(msg) except (FrozenInstanceError, AttributeError): pass def test_origin_is_zero(self, simple_panel_pdf): views, dims_by_view, part_name = make_full_pipeline(simple_panel_pdf) result = assemble_part_geometry(views, dims_by_view, part_name) if result is None: pytest.skip("Assembly returned None") assert result.origin == (0.0, 0.0, 0.0) def test_to_dict_serializable(self, simple_panel_pdf): views, dims_by_view, part_name = make_full_pipeline(simple_panel_pdf) result = assemble_part_geometry(views, dims_by_view, part_name) if result is None: pytest.skip("Assembly returned None") d = result.to_dict() json.dumps(d) # Should not raise def test_empty_dims_returns_none(self): """No dimensions → returns None.""" result = assemble_part_geometry([], {}) assert result is None def test_cabinet_assembles(self, cabinet_basic_pdf): """cabinet_basic.pdf (600×720×400mm) assembles successfully.""" views, dims_by_view, part_name = make_full_pipeline(cabinet_basic_pdf) result = assemble_part_geometry(views, dims_by_view, part_name) if result is None: pytest.skip("Assembly returned None for cabinet") # Cabinet is 600×720×400mm — width should be 600 assert 580 <= result.width_mm <= 650, f"Cabinet width: {result.width_mm}" def test_uses_front_view_for_width_and_height(self): """Front view horizontal → width, vertical → height.""" front_dims = [ DimensionAnnotation( value_mm=600, direction=DimensionDirection.HORIZONTAL, dim_line_start=(0, 0), dim_line_end=(600, 0), text_bbox=(0, 0, 0, 0), ), DimensionAnnotation( value_mm=720, direction=DimensionDirection.VERTICAL, dim_line_start=(0, 0), dim_line_end=(0, 720), text_bbox=(0, 0, 0, 0), ), ] side_dims = [ DimensionAnnotation( value_mm=18, direction=DimensionDirection.HORIZONTAL, dim_line_start=(0, 0), dim_line_end=(18, 0), text_bbox=(0, 0, 0, 0), ), ] dims = {ViewType.FRONT: front_dims, ViewType.SIDE: side_dims} result = assemble_part_geometry([], dims, "test_panel") assert result is not None assert result.width_mm == pytest.approx(600) assert result.height_mm == pytest.approx(720) assert result.depth_mm == pytest.approx(18)