Files
autocad-mcp-pro/tests/test_mech_materials.py
T
Umut CANandClaude Opus 5 2bb8044144 feat(mech): shared foundation - primitives, XDATA codec, standards registry, ISO 128-50 materials
engineering/mech/ opens with the four modules every track-B/G branch imports,
so the four worktrees can be cut from this commit and none of them has to
invent a name the others will conflict with.

- primitives.py: the typed descriptions the pure modules emit (Line, Arc,
  Circle, Poly, HatchArea, Text, DimIntent) with bbox / translate / rotate /
  scale, and ROLE_LAYER - the one place an ISO 128 line role becomes a layer
  name, so no module hardcodes a layer. The arc bbox sweeps its quadrants: a
  0-180 arc reports y=+r, not its endpoints, which is what keeps the view
  layout from stacking two views on top of each other.
- xdata.py: the ACADMCP_MECH codec in Track A's shape - a 1001 application
  marker, 1000 chunks of at most 255 ASCII characters, the 16 KB per-entity
  ceiling enforced before anything is written, and a foreign application, a
  corrupt chunk stream, a wrong version or an unknown kind refused with
  ValueError instead of decoded into something plausible.
- standards/__init__.py: the table registry. A size outside the coverage is
  refused against the coverage; a size inside it that is not a listed row is
  refused against the rows. Nothing is interpolated, an ambiguous variant is
  never resolved by taking the first row, and a registration whose SOURCE does
  not name the table it was transcribed from is refused.
- standards/materials.py: the section-hatch map for nine materials. ISO
  128-50 standardises only the general hatching (narrow continuous lines at 45
  degrees) - that is acadiso.pat ANSI31 and what engineering/gear.py already
  draws for steel. The distinguishing symbols are ASME Y14.2M's section lining
  as shipped in AutoCAD 2026's acadiso.pat, read off the installed file; cast
  iron's departure from ASME and the absence of any mechanical symbol for
  concrete and wood are stated in the rows themselves. Angles are 0.0 because
  the patterns already carry 45 degrees, and the two architectural patterns'
  scales are derived from the ANSI31 line spacing and recomputed by the test
  rather than chosen.

No tool is registered here.

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

127 lines
4.1 KiB
Python

"""The section-hatch map: every pattern exists in both engines, every row cites.
The ezdxf failure this guards is silent: ``Hatch.set_pattern_fill`` falls back
to ANSI31 for a name it does not know (``ezdxf/entities/polygon.py``), so a
typo'd pattern draws steel hatching on a cast-iron part and says nothing.
"""
from __future__ import annotations
import pytest
from ezdxf.tools import pattern as ezpattern
from engineering.mech.standards.materials import (
ALIASES,
MATERIAL_TABLE,
MATERIALS,
hatch_for,
)
ANSI31_SPACING = 2.2450640303 # acadiso.pat ANSI31, the ISO 128-50 general hatching
def _spacing(name: str, defs: dict) -> float:
"""The finest perpendicular line spacing in a pattern definition."""
return min(abs(line[2][1]) for line in defs[name] if abs(line[2][1]) > 1e-9)
def test_the_map_covers_the_nine_named_materials():
assert MATERIALS == (
"steel",
"cast_iron",
"aluminium",
"copper_alloy",
"plastic",
"rubber",
"concrete",
"wood",
"insulation",
)
def test_hatch_for_returns_exactly_pattern_angle_scale():
assert set(hatch_for("steel")) == {"pattern", "angle", "scale"}
@pytest.mark.parametrize(
("material", "expected"),
[
("steel", "ANSI31"),
("cast_iron", "ANSI32"),
("aluminium", "ANSI38"),
("copper_alloy", "ANSI33"),
("plastic", "ANSI34"),
("rubber", "ANSI34"),
("insulation", "ANSI37"),
("concrete", "AR-CONC"),
("wood", "JIS_WOOD"),
],
)
def test_each_material_keeps_its_pattern(material, expected):
assert hatch_for(material)["pattern"] == expected
def test_aliases_resolve_and_are_case_and_separator_insensitive():
assert hatch_for("Cast Iron")["pattern"] == "ANSI32"
assert hatch_for("cast-iron")["pattern"] == "ANSI32"
assert hatch_for("aluminum")["pattern"] == "ANSI38"
assert hatch_for("BRASS")["pattern"] == "ANSI33"
assert hatch_for("bronze")["pattern"] == "ANSI33"
assert hatch_for("copper")["pattern"] == "ANSI33"
assert set(ALIASES.values()) <= set(MATERIALS)
def test_an_unknown_material_is_refused_with_the_list():
with pytest.raises(ValueError, match="unobtainium.*cast_iron"):
hatch_for("unobtainium")
def test_every_pattern_exists_in_both_measurement_sets():
"""Guards ezdxf's silent ANSI31 fallback and AutoCAD's unknown-pattern error."""
iso = ezpattern.load(measurement=1)
imperial = ezpattern.load(measurement=0)
for material in MATERIALS:
name = hatch_for(material)["pattern"]
assert name in iso, f"{material}: {name} is not an ezdxf ISO pattern"
assert name in imperial, f"{material}: {name} is not an ezdxf imperial pattern"
def test_the_map_adds_no_rotation_because_the_patterns_already_carry_45_degrees():
defs = ezpattern.load(measurement=1)
for material in MATERIALS:
row = hatch_for(material)
assert row["angle"] == 0.0
if row["pattern"].startswith("ANSI3"):
assert defs[row["pattern"]][0][0] == 45.0
def test_the_ansi_rows_ship_at_the_pattern_definition_scale():
for material in (
"steel",
"cast_iron",
"aluminium",
"copper_alloy",
"plastic",
"rubber",
"insulation",
):
assert hatch_for(material)["scale"] == 1.0
def test_the_two_non_ansi_patterns_are_scaled_to_the_iso_line_spacing():
"""Concrete and wood are architectural patterns: AR-CONC is defined at
building scale and JIS_WOOD at 0.5 mm. Their scale is derived, not chosen —
the finest line in the pattern is brought to ANSI31's spacing."""
defs = ezpattern.load(measurement=1)
assert abs(_spacing("ANSI31", defs) - ANSI31_SPACING) < 1e-6
for material in ("concrete", "wood"):
row = hatch_for(material)
derived = round(ANSI31_SPACING / _spacing(row["pattern"], defs), 3)
assert row["scale"] == derived
def test_every_row_states_where_its_symbol_comes_from():
for material in MATERIALS:
source = MATERIAL_TABLE[material]["source"]
assert any(token in source for token in ("ISO 128-50", "ASME Y14.2M", "acadiso.pat"))