feat: pdf2cad

This commit is contained in:
2026-03-03 21:24:02 +00:00
commit 112213da6e
61 changed files with 7290 additions and 0 deletions

688
tests/test_models.py Normal file
View File

@@ -0,0 +1,688 @@
"""Tests for core data models."""
import json
from dataclasses import FrozenInstanceError
import pytest
from pdf2imos.models import (
ClassifiedLine,
DimensionAnnotation,
DimensionDirection,
DrillingAnnotation,
EdgebandAnnotation,
HardwareAnnotation,
LineRole,
MaterialAnnotation,
PageExtraction,
PartGeometry,
PartMetadata,
PipelineResult,
RawPath,
RawText,
ViewRegion,
ViewType,
)
class TestRawPath:
"""Tests for RawPath dataclass."""
def test_instantiate(self):
"""Test RawPath instantiation."""
path = RawPath(
items=(("l", 0, 0, 10, 10),),
color=(0.0, 0.0, 0.0),
fill=None,
dashes="",
width=1.0,
rect=(0.0, 0.0, 10.0, 10.0),
)
assert path.color == (0.0, 0.0, 0.0)
assert path.width == 1.0
def test_to_dict(self):
"""Test RawPath.to_dict() serialization."""
path = RawPath(
items=(("l", 0, 0, 10, 10),),
color=(0.5, 0.5, 0.5),
fill=(1.0, 1.0, 1.0),
dashes="[3 2] 0",
width=2.5,
rect=(0.0, 0.0, 10.0, 10.0),
)
d = path.to_dict()
assert d["color"] == (0.5, 0.5, 0.5)
assert d["fill"] == (1.0, 1.0, 1.0)
assert d["dashes"] == "[3 2] 0"
assert d["width"] == 2.5
assert d["rect"] == [0.0, 0.0, 10.0, 10.0]
# Verify JSON serializable
json.dumps(d)
def test_frozen(self):
"""Test that RawPath is frozen."""
path = RawPath(
items=(("l", 0, 0, 10, 10),),
color=(0.0, 0.0, 0.0),
fill=None,
dashes="",
width=1.0,
rect=(0.0, 0.0, 10.0, 10.0),
)
with pytest.raises(FrozenInstanceError):
path.width = 2.0
class TestRawText:
"""Tests for RawText dataclass."""
def test_instantiate(self):
"""Test RawText instantiation."""
text = RawText(
text="Hello",
bbox=(0.0, 0.0, 50.0, 20.0),
font="Helvetica",
size=12.0,
color=0,
)
assert text.text == "Hello"
assert text.size == 12.0
def test_to_dict(self):
"""Test RawText.to_dict() serialization."""
text = RawText(
text="Test",
bbox=(10.0, 20.0, 60.0, 40.0),
font="Arial",
size=14.0,
color=16777215,
)
d = text.to_dict()
assert d["text"] == "Test"
assert d["bbox"] == [10.0, 20.0, 60.0, 40.0]
assert d["font"] == "Arial"
assert d["size"] == 14.0
assert d["color"] == 16777215
json.dumps(d)
def test_frozen(self):
"""Test that RawText is frozen."""
text = RawText(
text="Hello",
bbox=(0.0, 0.0, 50.0, 20.0),
font="Helvetica",
size=12.0,
color=0,
)
with pytest.raises(FrozenInstanceError):
text.text = "World"
class TestPageExtraction:
"""Tests for PageExtraction dataclass."""
def test_instantiate(self):
"""Test PageExtraction instantiation."""
path = RawPath(
items=(("l", 0, 0, 10, 10),),
color=(0.0, 0.0, 0.0),
fill=None,
dashes="",
width=1.0,
rect=(0.0, 0.0, 10.0, 10.0),
)
text = RawText(
text="Test",
bbox=(0.0, 0.0, 50.0, 20.0),
font="Helvetica",
size=12.0,
color=0,
)
page = PageExtraction(
paths=(path,),
texts=(text,),
page_width=100.0,
page_height=200.0,
)
assert len(page.paths) == 1
assert len(page.texts) == 1
def test_to_dict(self):
"""Test PageExtraction.to_dict() serialization."""
path = RawPath(
items=(("l", 0, 0, 10, 10),),
color=(0.0, 0.0, 0.0),
fill=None,
dashes="",
width=1.0,
rect=(0.0, 0.0, 10.0, 10.0),
)
text = RawText(
text="Test",
bbox=(0.0, 0.0, 50.0, 20.0),
font="Helvetica",
size=12.0,
color=0,
)
page = PageExtraction(
paths=(path,),
texts=(text,),
page_width=100.0,
page_height=200.0,
)
d = page.to_dict()
assert len(d["paths"]) == 1
assert len(d["texts"]) == 1
assert d["page_width"] == 100.0
assert d["page_height"] == 200.0
json.dumps(d)
class TestViewType:
"""Tests for ViewType enum."""
def test_enum_values(self):
"""Test ViewType enum values."""
assert ViewType.FRONT.value == "front"
assert ViewType.TOP.value == "top"
assert ViewType.SIDE.value == "side"
assert ViewType.UNKNOWN.value == "unknown"
class TestViewRegion:
"""Tests for ViewRegion dataclass."""
def test_instantiate(self):
"""Test ViewRegion instantiation."""
path = RawPath(
items=(("l", 0, 0, 10, 10),),
color=(0.0, 0.0, 0.0),
fill=None,
dashes="",
width=1.0,
rect=(0.0, 0.0, 10.0, 10.0),
)
region = ViewRegion(
view_type=ViewType.FRONT,
bounds=(0.0, 0.0, 100.0, 200.0),
paths=(path,),
texts=(),
)
assert region.view_type == ViewType.FRONT
def test_to_dict(self):
"""Test ViewRegion.to_dict() serialization."""
path = RawPath(
items=(("l", 0, 0, 10, 10),),
color=(0.0, 0.0, 0.0),
fill=None,
dashes="",
width=1.0,
rect=(0.0, 0.0, 10.0, 10.0),
)
region = ViewRegion(
view_type=ViewType.TOP,
bounds=(10.0, 20.0, 110.0, 220.0),
paths=(path,),
texts=(),
)
d = region.to_dict()
assert d["view_type"] == "top"
assert d["bounds"] == [10.0, 20.0, 110.0, 220.0]
json.dumps(d)
class TestLineRole:
"""Tests for LineRole enum."""
def test_enum_values(self):
"""Test LineRole enum values."""
assert LineRole.GEOMETRY.value == "geometry"
assert LineRole.HIDDEN.value == "hidden"
assert LineRole.CENTER.value == "center"
assert LineRole.DIMENSION.value == "dimension"
assert LineRole.BORDER.value == "border"
assert LineRole.CONSTRUCTION.value == "construction"
assert LineRole.UNKNOWN.value == "unknown"
class TestClassifiedLine:
"""Tests for ClassifiedLine dataclass."""
def test_instantiate(self):
"""Test ClassifiedLine instantiation."""
path = RawPath(
items=(("l", 0, 0, 10, 10),),
color=(0.0, 0.0, 0.0),
fill=None,
dashes="",
width=1.0,
rect=(0.0, 0.0, 10.0, 10.0),
)
line = ClassifiedLine(
start=(0.0, 0.0),
end=(10.0, 10.0),
role=LineRole.GEOMETRY,
confidence=0.95,
original_path=path,
)
assert line.role == LineRole.GEOMETRY
assert line.confidence == 0.95
def test_to_dict(self):
"""Test ClassifiedLine.to_dict() serialization."""
path = RawPath(
items=(("l", 0, 0, 10, 10),),
color=(0.0, 0.0, 0.0),
fill=None,
dashes="",
width=1.0,
rect=(0.0, 0.0, 10.0, 10.0),
)
line = ClassifiedLine(
start=(5.0, 5.0),
end=(15.0, 15.0),
role=LineRole.DIMENSION,
confidence=0.85,
original_path=path,
)
d = line.to_dict()
assert d["start"] == [5.0, 5.0]
assert d["end"] == [15.0, 15.0]
assert d["role"] == "dimension"
assert d["confidence"] == 0.85
json.dumps(d)
class TestDimensionAnnotation:
"""Tests for DimensionAnnotation dataclass."""
def test_instantiate(self):
"""Test DimensionAnnotation instantiation."""
dim = DimensionAnnotation(
value_mm=100.0,
direction=DimensionDirection.HORIZONTAL,
dim_line_start=(0.0, 0.0),
dim_line_end=(100.0, 0.0),
text_bbox=(40.0, -10.0, 60.0, 0.0),
)
assert dim.value_mm == 100.0
assert dim.direction == DimensionDirection.HORIZONTAL
def test_to_dict(self):
"""Test DimensionAnnotation.to_dict() serialization."""
dim = DimensionAnnotation(
value_mm=50.5,
direction=DimensionDirection.VERTICAL,
dim_line_start=(10.0, 10.0),
dim_line_end=(10.0, 60.0),
text_bbox=(0.0, 30.0, 10.0, 40.0),
)
d = dim.to_dict()
assert d["value_mm"] == 50.5
assert d["direction"] == "vertical"
assert d["dim_line_start"] == [10.0, 10.0]
assert d["dim_line_end"] == [10.0, 60.0]
json.dumps(d)
class TestMaterialAnnotation:
"""Tests for MaterialAnnotation dataclass."""
def test_instantiate(self):
"""Test MaterialAnnotation instantiation."""
mat = MaterialAnnotation(
text="MDF 18mm white melamine",
thickness_mm=18.0,
material_type="MDF",
finish="white melamine",
)
assert mat.material_type == "MDF"
assert mat.thickness_mm == 18.0
def test_to_dict(self):
"""Test MaterialAnnotation.to_dict() serialization."""
mat = MaterialAnnotation(
text="Plywood 12mm",
thickness_mm=12.0,
material_type="plywood",
finish="natural",
)
d = mat.to_dict()
assert d["material_type"] == "plywood"
assert d["thickness_mm"] == 12.0
json.dumps(d)
class TestEdgebandAnnotation:
"""Tests for EdgebandAnnotation dataclass."""
def test_instantiate(self):
"""Test EdgebandAnnotation instantiation."""
edge = EdgebandAnnotation(
edge_id="top",
material="PVC",
thickness_mm=2.0,
)
assert edge.edge_id == "top"
assert edge.material == "PVC"
def test_to_dict(self):
"""Test EdgebandAnnotation.to_dict() serialization."""
edge = EdgebandAnnotation(
edge_id="left",
material="ABS",
thickness_mm=1.5,
)
d = edge.to_dict()
assert d["edge_id"] == "left"
assert d["material"] == "ABS"
json.dumps(d)
class TestHardwareAnnotation:
"""Tests for HardwareAnnotation dataclass."""
def test_instantiate(self):
"""Test HardwareAnnotation instantiation."""
hw = HardwareAnnotation(
type="hinge",
model="Blum 110°",
position_description="top left",
)
assert hw.type == "hinge"
assert hw.model == "Blum 110°"
def test_to_dict(self):
"""Test HardwareAnnotation.to_dict() serialization."""
hw = HardwareAnnotation(
type="handle",
model="Ergonomic",
position_description="center front",
)
d = hw.to_dict()
assert d["type"] == "handle"
json.dumps(d)
class TestDrillingAnnotation:
"""Tests for DrillingAnnotation dataclass."""
def test_instantiate(self):
"""Test DrillingAnnotation instantiation."""
drill = DrillingAnnotation(
x_mm=50.0,
y_mm=100.0,
diameter_mm=8.0,
depth_mm=10.0,
)
assert drill.x_mm == 50.0
assert drill.diameter_mm == 8.0
def test_to_dict(self):
"""Test DrillingAnnotation.to_dict() serialization."""
drill = DrillingAnnotation(
x_mm=25.0,
y_mm=75.0,
diameter_mm=5.0,
depth_mm=15.0,
)
d = drill.to_dict()
assert d["x_mm"] == 25.0
assert d["diameter_mm"] == 5.0
json.dumps(d)
class TestPartMetadata:
"""Tests for PartMetadata dataclass."""
def test_instantiate(self):
"""Test PartMetadata instantiation."""
mat = MaterialAnnotation(
text="MDF 18mm",
thickness_mm=18.0,
material_type="MDF",
finish="white",
)
edge = EdgebandAnnotation(
edge_id="top",
material="PVC",
thickness_mm=2.0,
)
metadata = PartMetadata(
materials=(mat,),
edgebanding=(edge,),
hardware=(),
drilling=(),
raw_annotations=("annotation1", "annotation2"),
)
assert len(metadata.materials) == 1
assert len(metadata.raw_annotations) == 2
def test_to_dict(self):
"""Test PartMetadata.to_dict() serialization."""
mat = MaterialAnnotation(
text="Plywood",
thickness_mm=12.0,
material_type="plywood",
finish="natural",
)
metadata = PartMetadata(
materials=(mat,),
edgebanding=(),
hardware=(),
drilling=(),
raw_annotations=(),
)
d = metadata.to_dict()
assert len(d["materials"]) == 1
assert d["materials"][0]["material_type"] == "plywood"
json.dumps(d)
class TestPartGeometry:
"""Tests for PartGeometry dataclass."""
def test_instantiate(self):
"""Test PartGeometry instantiation."""
geom = PartGeometry(
width_mm=500.0,
height_mm=800.0,
depth_mm=400.0,
origin=(0.0, 0.0, 0.0),
name="Cabinet",
)
assert geom.width_mm == 500.0
assert geom.name == "Cabinet"
def test_to_dict(self):
"""Test PartGeometry.to_dict() serialization."""
geom = PartGeometry(
width_mm=600.0,
height_mm=900.0,
depth_mm=350.0,
origin=(10.0, 20.0, 0.0),
name="Shelf",
)
d = geom.to_dict()
assert d["width_mm"] == 600.0
assert d["origin"] == [10.0, 20.0, 0.0]
assert d["name"] == "Shelf"
json.dumps(d)
def test_frozen(self):
"""Test that PartGeometry is frozen."""
geom = PartGeometry(
width_mm=500.0,
height_mm=800.0,
depth_mm=400.0,
origin=(0.0, 0.0, 0.0),
name="Cabinet",
)
with pytest.raises(FrozenInstanceError):
geom.width_mm = 600.0
class TestPipelineResult:
"""Tests for PipelineResult dataclass."""
def test_instantiate(self):
"""Test PipelineResult instantiation."""
geom = PartGeometry(
width_mm=500.0,
height_mm=800.0,
depth_mm=400.0,
origin=(0.0, 0.0, 0.0),
name="Cabinet",
)
metadata = PartMetadata(
materials=(),
edgebanding=(),
hardware=(),
drilling=(),
raw_annotations=(),
)
result = PipelineResult(
part_geometry=geom,
part_metadata=metadata,
source_pdf_path="/path/to/input.pdf",
dxf_output_path="/path/to/output.dxf",
json_output_path="/path/to/output.json",
)
assert result.source_pdf_path == "/path/to/input.pdf"
assert result.dxf_output_path == "/path/to/output.dxf"
def test_to_dict(self):
"""Test PipelineResult.to_dict() serialization."""
geom = PartGeometry(
width_mm=500.0,
height_mm=800.0,
depth_mm=400.0,
origin=(0.0, 0.0, 0.0),
name="Cabinet",
)
metadata = PartMetadata(
materials=(),
edgebanding=(),
hardware=(),
drilling=(),
raw_annotations=(),
)
result = PipelineResult(
part_geometry=geom,
part_metadata=metadata,
source_pdf_path="/input.pdf",
dxf_output_path=None,
json_output_path="/output.json",
)
d = result.to_dict()
assert d["source_pdf_path"] == "/input.pdf"
assert d["dxf_output_path"] is None
assert d["json_output_path"] == "/output.json"
json.dumps(d)
def test_frozen(self):
"""Test that PipelineResult is frozen."""
geom = PartGeometry(
width_mm=500.0,
height_mm=800.0,
depth_mm=400.0,
origin=(0.0, 0.0, 0.0),
name="Cabinet",
)
metadata = PartMetadata(
materials=(),
edgebanding=(),
hardware=(),
drilling=(),
raw_annotations=(),
)
result = PipelineResult(
part_geometry=geom,
part_metadata=metadata,
source_pdf_path="/input.pdf",
dxf_output_path=None,
json_output_path=None,
)
with pytest.raises(FrozenInstanceError):
result.source_pdf_path = "/other.pdf"
class TestJSONRoundTrip:
"""Test JSON serialization round-trip."""
def test_raw_path_roundtrip(self):
"""Test RawPath JSON round-trip."""
path = RawPath(
items=(("l", 0, 0, 10, 10),),
color=(0.5, 0.5, 0.5),
fill=(1.0, 1.0, 1.0),
dashes="[3 2] 0",
width=2.5,
rect=(0.0, 0.0, 10.0, 10.0),
)
d = path.to_dict()
json_str = json.dumps(d)
loaded = json.loads(json_str)
assert loaded["color"] == [0.5, 0.5, 0.5]
assert loaded["width"] == 2.5
def test_page_extraction_roundtrip(self):
"""Test PageExtraction JSON round-trip."""
path = RawPath(
items=(("l", 0, 0, 10, 10),),
color=(0.0, 0.0, 0.0),
fill=None,
dashes="",
width=1.0,
rect=(0.0, 0.0, 10.0, 10.0),
)
text = RawText(
text="Test",
bbox=(0.0, 0.0, 50.0, 20.0),
font="Helvetica",
size=12.0,
color=0,
)
page = PageExtraction(
paths=(path,),
texts=(text,),
page_width=100.0,
page_height=200.0,
)
d = page.to_dict()
json_str = json.dumps(d)
loaded = json.loads(json_str)
assert loaded["page_width"] == 100.0
assert len(loaded["paths"]) == 1
assert len(loaded["texts"]) == 1
def test_pipeline_result_roundtrip(self):
"""Test PipelineResult JSON round-trip."""
geom = PartGeometry(
width_mm=500.0,
height_mm=800.0,
depth_mm=400.0,
origin=(0.0, 0.0, 0.0),
name="Cabinet",
)
metadata = PartMetadata(
materials=(),
edgebanding=(),
hardware=(),
drilling=(),
raw_annotations=(),
)
result = PipelineResult(
part_geometry=geom,
part_metadata=metadata,
source_pdf_path="/input.pdf",
dxf_output_path="/output.dxf",
json_output_path="/output.json",
)
d = result.to_dict()
json_str = json.dumps(d)
loaded = json.loads(json_str)
assert loaded["source_pdf_path"] == "/input.pdf"
assert loaded["part_geometry"]["width_mm"] == 500.0