Files
autocad-mcp-pro/tests/test_sheet_titleblock.py
T
Umut CANandClaude Opus 5 56e6ff7206 feat(sheet): ISO 5457 frames and ISO 7200 title blocks for A4-A0
SECTION 24 opens with sheet_frame and titleblock_apply.

engineering/sheet/frames.py transcribes ISO 216's trimmed sizes, ISO 5457's
20 mm filing margin and 10 mm edges, the grid reference system (an even number
of divisions per side, that side of the frame over 50 mm; letters from the top,
numbers from the left), the centring marks (trimmed edge to 5 mm inside the
frame) and the 10 x 5 mm corner trimming marks. Every one of those numbers is
pinned by a test against the clause it comes from, because a wrong margin is
visible on every drawing this server makes.

engineering/sheet/titleblock.py draws the ISO 7200 block -- 180 mm wide, which
is exactly the drawing-space width of a portrait A4, which is why one block
fits every size -- and carries the ISO 128-30 projection-angle symbol. Which
symbol is which is derived rather than remembered: first-angle projection puts
the view seen from the left on the right, and the view of the cone from its
small end is the small circle inside the large one, so first angle draws the
cone left and the circles right.

The old A3 module folds in. apply_iso_a3_titleblock now forwards to
apply_titleblock(size="A3", projection=None, zones=False, marks=False), which is
the geometry it always drew, and tests/test_engineering_titleblock.py passes
unchanged -- that is the regression gate for the fold.

No new ActiveX member is called: both tools reach the drawing only through
contract members the A3 title block has used on both engines since v1.0
(entity_create_polyline / _line / _circle / _text, layout_list,
layout_set_current), so there was nothing to probe live.

Declared, not glossed: the zone divisions drawn here are equal, so ISO 5457's
allowance for shorter corner fields is not implemented; the division counts,
which are what a zone reference quotes, are the standard's. ISO 7200's full
optional-field list is not transcribed, only the fields this block has a cell
for. Coverage is A4-A0; an unknown size is refused by name with the five it has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-22 19:03:14 +03:00

191 lines
7.2 KiB
Python

"""ISO 7200 title blocks for A4-A0, and the A3 geometry that must not move."""
from __future__ import annotations
import pytest
from engineering.mech.primitives import Circle, Line, Poly, Text
from engineering.sheet.titleblock import (
TB_HEIGHT,
TB_WIDTH,
TITLE_TEXT_INDEX,
TITLEBLOCK_FIELDS,
TITLEBLOCK_RULE_COUNT,
VALUE_FIELDS,
TitleBlockMetadata,
apply_titleblock,
projection_symbol_prims,
titleblock_origin,
titleblock_prims,
value_text_indices,
)
EPS = 1e-9
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)
def test_the_block_is_the_iso_7200_180_mm_wide_and_sits_in_the_frame_corner():
assert TB_WIDTH == 180.0 and TB_HEIGHT == 60.0
assert titleblock_origin("A3") == (230.0, 10.0)
assert titleblock_origin("A4", orientation="portrait") == (20.0, 10.0)
assert titleblock_origin("A0") == (999.0, 10.0)
def test_every_declared_field_is_a_metadata_attribute():
meta = _meta()
for field in TITLEBLOCK_FIELDS:
assert hasattr(meta, field), field
def test_the_a3_rules_are_exactly_where_they_have_always_been():
prims = titleblock_prims("A3", _meta(), projection=None)
rules = prims[:TITLEBLOCK_RULE_COUNT]
assert isinstance(rules[0], Poly) and rules[0].closed
assert rules[0].points == ((230.0, 10.0), (410.0, 10.0), (410.0, 70.0), (230.0, 70.0))
assert [(r.p1[1], r.p1[0], r.p2[0]) for r in rules[1:4]] == [
(20.0, 230.0, 410.0),
(35.0, 230.0, 410.0),
(50.0, 230.0, 410.0),
]
assert [(r.p1[0], r.p1[1], r.p2[1]) for r in rules[4:]] == [
(320.0, 35.0, 50.0),
(365.0, 35.0, 50.0),
(290.0, 20.0, 35.0),
(350.0, 20.0, 35.0),
(275.0, 10.0, 20.0),
(320.0, 10.0, 20.0),
(365.0, 10.0, 20.0),
]
def test_the_a3_value_texts_are_exactly_where_they_have_always_been():
prims = titleblock_prims("A3", _meta(), projection=None)
index = value_text_indices()
assert prims[index["drawing_no"]] == Text(at=(233.0, 40.0), text="AM-2026-001", height=3.5)
assert prims[index["revision"]] == Text(at=(323.0, 40.0), text="A", height=3.5)
assert prims[index["sheet"]] == Text(at=(368.0, 40.0), text="1/1", height=3.5)
assert prims[index["part_no"]] == Text(at=(233.0, 25.0), text="GR-24T-M3", height=3.5)
assert prims[index["material"]] == Text(at=(293.0, 25.0), text="C45", height=3.5)
assert prims[index["scale"]] == Text(at=(353.0, 25.0), text="1:1", height=3.5)
assert prims[index["drawn_by"]] == Text(at=(233.0, 11.5), text="Umutcan", height=3.5)
assert prims[index["checked_by"]] == Text(at=(278.0, 11.5), text="QA", height=3.5)
assert prims[index["date"]] == Text(at=(323.0, 11.5), text="2026-04-28", height=3.5)
assert prims[index["company"]] == Text(at=(368.0, 11.5), text="Anka-Makine", height=3.5)
def test_the_title_sits_at_the_pinned_index_and_carries_the_text_verbatim():
prims = titleblock_prims("A3", _meta(title="SPUR GEAR / M3 Z24"), projection=None)
title = prims[TITLE_TEXT_INDEX]
assert isinstance(title, Text)
assert title.text == "SPUR GEAR / M3 Z24"
assert title.height == 5.0
assert title.at[1] == pytest.approx(57.5, abs=EPS)
def test_projection_none_draws_no_symbol_and_first_draws_one():
plain = titleblock_prims("A3", _meta(), projection=None)
first = titleblock_prims("A3", _meta(), projection="first")
assert len(plain) == TITLE_TEXT_INDEX + 1 + 2 * len(VALUE_FIELDS)
assert len(first) == len(plain) + 4
def test_first_angle_puts_the_circles_to_the_right_of_the_cone():
first = projection_symbol_prims((0.0, 0.0), "first")
third = projection_symbol_prims((0.0, 0.0), "third")
cone_first = next(p for p in first if isinstance(p, Poly))
circles_first = [p for p in first if isinstance(p, Circle)]
assert max(x for x, _ in cone_first.points) < min(c.center[0] for c in circles_first)
cone_third = next(p for p in third if isinstance(p, Poly))
circles_third = [p for p in third if isinstance(p, Circle)]
assert min(x for x, _ in cone_third.points) > max(c.center[0] for c in circles_third)
assert {round(c.radius, 6) for c in circles_first} == {4.0, 2.0}
assert any(isinstance(p, Line) and p.role == "center" for p in first)
def test_the_cone_s_small_end_faces_the_circles_side_in_first_angle():
"""The derivation only holds if the view drawn as circles really is the
view from the cone's small end."""
cone = next(p for p in projection_symbol_prims((0.0, 0.0), "first") if isinstance(p, Poly))
left_x = min(x for x, _ in cone.points)
right_x = max(x for x, _ in cone.points)
left_height = max(y for x, y in cone.points if x == left_x) * 2
right_height = max(y for x, y in cone.points if x == right_x) * 2
assert left_height == pytest.approx(4.0, abs=EPS)
assert right_height == pytest.approx(8.0, abs=EPS)
def test_an_unknown_projection_angle_is_refused():
with pytest.raises(ValueError) as excinfo:
titleblock_prims("A3", _meta(), projection="second")
assert "second" in str(excinfo.value)
def test_a_sheet_narrower_than_the_title_block_is_refused_by_name():
"""There is no such size in A4-A0; the guard exists so a future size cannot
ship a title block hanging off the paper."""
from engineering.sheet import frames
original = dict(frames.SHEETS)
frames.SHEETS["A9"] = (100.0, 140.0)
try:
with pytest.raises(ValueError) as excinfo:
titleblock_origin("A9", orientation="portrait")
assert "180" in str(excinfo.value)
finally:
frames.SHEETS.clear()
frames.SHEETS.update(original)
@pytest.mark.asyncio
async def test_apply_titleblock_returns_the_payload_the_a3_tool_always_returned(backend):
result = await apply_titleblock(backend, size="A3", metadata=_meta(), projection=None)
assert result["bbox"] == {"min": [0.0, 0.0], "max": [420.0, 297.0]}
assert set(result["value_texts"]) == {
"drawing_no",
"revision",
"sheet",
"part_no",
"material",
"scale",
"drawn_by",
"checked_by",
"date",
"company",
}
assert len(result["titleblock_lines"]) == TITLEBLOCK_RULE_COUNT
title = await backend.entity_get(result["title_text"])
assert title.type == "TEXT" and title.properties["text"] == "HELICAL GEAR"
assert title.layer == "TEXT"
for handle in result["titleblock_lines"]:
assert (await backend.entity_get(handle)).layer == "TITLEBLOCK"
assert result["projection_symbol"] == []
@pytest.mark.asyncio
async def test_apply_titleblock_draws_every_size_with_its_symbol(backend):
for size in ("A4", "A3", "A2", "A1", "A0"):
orientation = "portrait" if size == "A4" else "landscape"
result = await apply_titleblock(
backend, size=size, metadata=_meta(), orientation=orientation
)
assert result["ok"] is True and result["size"] == size
assert len(result["projection_symbol"]) == 4