Add deterministic engineering CAD layer (gear, keyway, titleblock, validator)
Introduces an engineering/ package that lets MCP clients produce production- quality technical drawings without LLM-improvised geometry. Adds 7 new MCP tools, an 8-step pre-completion validator, and an automatic linetype + layer bootstrap on drawing_new so CENTER/HIDDEN/PHANTOM and standard mechanical layers (GEOMETRY, DIM, HATCH, TEXT, TITLEBLOCK, ...) are always available. - engineering/gear.py: pure-Python involute math, 40+ points/flank, helical + spur front views, deterministic section A-A (no helix lines in section). - engineering/keyway.py: DIN 6885 keyway table + parametric keyed bore / side-section drawers. - engineering/titleblock.py: ISO 7200 A3 (420x297 mm) title block; title text is written verbatim to the title cell. - engineering/validator.py: DrawingValidator with 8 checks (real DIMENSION entities, no hidden orphans, linetypes loaded, bore + keyway in both views when expected, title consistency e.g. forbid 'HELICAL SPUR GEAR' when helix_angle truthy, saved-to-disk + file-on-disk, screenshot exported, dwg path returned). - backends/ezdxf_backend.py: AutoCAD-only linetype fallback (HIDDEN, BORDER families) so ezdxf doc gets correct patterns when linetype_load is called for names ezdxf.tools.standards does not ship. - backends/base.py: concrete helpers set_layer_active / ensure_linetypes. - server.py: Section 12 with 7 engineering tools, drawing_new(bootstrap= True), and an ENGINEERING WORKFLOW block in the MCP instructions string. - 30 new tests covering gear math, DIN6885 lookup, ISO A3 layout, title verbatim, and every validator check. Tool count 89 -> 96.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"""Tests for engineering.titleblock — ISO 7200 A3 layout."""
|
||||
|
||||
import pytest
|
||||
|
||||
from engineering.titleblock import TitleBlockMetadata, apply_iso_a3_titleblock
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
def _meta(**overrides) -> TitleBlockMetadata:
|
||||
base = dict(
|
||||
title="HELICAL GEAR",
|
||||
drawing_no="AM-2026-001",
|
||||
part_no="GR-24T-M3",
|
||||
material="C45",
|
||||
scale="1:1",
|
||||
units="mm",
|
||||
drawn_by="Umutcan",
|
||||
checked_by="QA",
|
||||
date="2026-04-28",
|
||||
sheet="1/1",
|
||||
revision="A",
|
||||
company="Anka-Makine",
|
||||
)
|
||||
base.update(overrides)
|
||||
return TitleBlockMetadata(**base)
|
||||
|
||||
|
||||
async def test_titleblock_outer_border_dimensions(backend):
|
||||
"""Outer border bbox must equal A3 sheet (420 x 297 mm)."""
|
||||
result = await apply_iso_a3_titleblock(backend, metadata=_meta())
|
||||
bbox = result["bbox"]
|
||||
width = bbox["max"][0] - bbox["min"][0]
|
||||
height = bbox["max"][1] - bbox["min"][1]
|
||||
assert width == pytest.approx(420.0)
|
||||
assert height == pytest.approx(297.0)
|
||||
|
||||
|
||||
async def test_title_text_exact_match(backend):
|
||||
"""Title text in the drawing must equal metadata.title verbatim."""
|
||||
result = await apply_iso_a3_titleblock(
|
||||
backend, metadata=_meta(title="HELICAL GEAR"),
|
||||
)
|
||||
title_ent = await backend.entity_get(result["title_text"])
|
||||
assert title_ent.type == "TEXT"
|
||||
assert title_ent.properties["text"] == "HELICAL GEAR"
|
||||
|
||||
|
||||
async def test_titleblock_creates_text_for_each_metadata_field(backend):
|
||||
"""value_texts must contain a handle for every metadata field."""
|
||||
result = await apply_iso_a3_titleblock(backend, metadata=_meta())
|
||||
expected_keys = {
|
||||
"drawing_no", "revision", "sheet",
|
||||
"part_no", "material", "scale",
|
||||
"drawn_by", "checked_by", "date", "company",
|
||||
}
|
||||
assert set(result["value_texts"].keys()) == expected_keys
|
||||
# All handles must resolve to TEXT entities
|
||||
for key, handle in result["value_texts"].items():
|
||||
ent = await backend.entity_get(handle)
|
||||
assert ent.type == "TEXT", f"{key} expected TEXT, got {ent.type}"
|
||||
|
||||
|
||||
async def test_titleblock_origin_offset(backend):
|
||||
"""Passing origin=(100,50) must shift the bbox by that amount."""
|
||||
result = await apply_iso_a3_titleblock(
|
||||
backend, metadata=_meta(), origin=(100.0, 50.0),
|
||||
)
|
||||
bbox = result["bbox"]
|
||||
assert bbox["min"][0] == pytest.approx(100.0)
|
||||
assert bbox["min"][1] == pytest.approx(50.0)
|
||||
assert bbox["max"][0] == pytest.approx(520.0)
|
||||
assert bbox["max"][1] == pytest.approx(347.0)
|
||||
|
||||
|
||||
async def test_titleblock_borders_on_titleblock_layer(backend):
|
||||
"""Outer + inner borders + grid lines must be on TITLEBLOCK layer."""
|
||||
result = await apply_iso_a3_titleblock(backend, metadata=_meta())
|
||||
outer = await backend.entity_get(result["outer_border"])
|
||||
inner = await backend.entity_get(result["inner_border"])
|
||||
assert outer.layer == "TITLEBLOCK"
|
||||
assert inner.layer == "TITLEBLOCK"
|
||||
for handle in result["titleblock_lines"]:
|
||||
ent = await backend.entity_get(handle)
|
||||
assert ent.layer == "TITLEBLOCK"
|
||||
|
||||
|
||||
async def test_titleblock_text_on_text_layer(backend):
|
||||
"""Title and all value texts must be on TEXT layer."""
|
||||
result = await apply_iso_a3_titleblock(backend, metadata=_meta())
|
||||
title = await backend.entity_get(result["title_text"])
|
||||
assert title.layer == "TEXT"
|
||||
for handle in result["value_texts"].values():
|
||||
ent = await backend.entity_get(handle)
|
||||
assert ent.layer == "TEXT"
|
||||
|
||||
|
||||
async def test_titleblock_metadata_in_result(backend):
|
||||
"""Returned metadata dict must round-trip the input metadata."""
|
||||
md = _meta(title="SPUR GEAR", drawing_no="AM-2026-002")
|
||||
result = await apply_iso_a3_titleblock(backend, metadata=md)
|
||||
assert result["metadata"]["title"] == "SPUR GEAR"
|
||||
assert result["metadata"]["drawing_no"] == "AM-2026-002"
|
||||
assert result["metadata"]["company"] == "Anka-Makine"
|
||||
Reference in New Issue
Block a user