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,128 @@
|
||||
"""Tests for engineering.gear — pure-math + drawing orchestration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
from engineering.gear import (
|
||||
draw_gear_section_aa,
|
||||
draw_helical_gear_front_view,
|
||||
generate_full_gear_outline,
|
||||
generate_involute_flank,
|
||||
involute_xy,
|
||||
)
|
||||
from engineering.layers import (
|
||||
ensure_engineering_layers,
|
||||
ensure_standard_linetypes,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pure math (sync)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_involute_xy_at_zero_returns_base_point():
|
||||
x, y = involute_xy(10.0, 0.0)
|
||||
assert math.isclose(x, 10.0, abs_tol=1e-9)
|
||||
assert math.isclose(y, 0.0, abs_tol=1e-9)
|
||||
|
||||
|
||||
def test_generate_involute_flank_min_30_points():
|
||||
flank = generate_involute_flank(base_r=20.0, root_r=18.0, outer_r=25.0, n_points=5)
|
||||
assert len(flank) >= 30
|
||||
|
||||
|
||||
def test_generate_involute_flank_radius_progression():
|
||||
base_r, root_r, outer_r = 20.0, 18.0, 25.0
|
||||
flank = generate_involute_flank(base_r=base_r, root_r=root_r, outer_r=outer_r)
|
||||
first_r = math.hypot(*flank[0])
|
||||
last_r = math.hypot(*flank[-1])
|
||||
assert math.isclose(first_r, root_r, abs_tol=1e-6)
|
||||
assert math.isclose(last_r, outer_r, rel_tol=1e-3)
|
||||
|
||||
|
||||
def test_generate_full_gear_outline_closed():
|
||||
pts = generate_full_gear_outline(module=3.0, teeth=24)
|
||||
assert pts[0] == pts[-1]
|
||||
|
||||
|
||||
def test_generate_full_gear_outline_point_count():
|
||||
teeth = 24
|
||||
pts = generate_full_gear_outline(module=3.0, teeth=teeth)
|
||||
# At minimum: teeth * (40 right + 40 left) = 1920 unique flank points,
|
||||
# plus tip and root arc samples. Closing duplicate adds 1.
|
||||
assert len(pts) >= teeth * 40
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Drawing orchestration (async, ezdxf backend)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_helical_gear_front_view_returns_metadata(backend):
|
||||
await ensure_standard_linetypes(backend)
|
||||
await ensure_engineering_layers(backend)
|
||||
res = await draw_helical_gear_front_view(
|
||||
backend, module=3, teeth=24, helix_angle=20.0, hand="RH",
|
||||
center=(0.0, 0.0),
|
||||
)
|
||||
expected_keys = {
|
||||
"outline", "pitch_circle", "base_circle", "outer_circle", "root_circle",
|
||||
"helix_symbol", "helix_label", "bore", "keyway", "metadata",
|
||||
}
|
||||
assert expected_keys.issubset(res.keys())
|
||||
md = res["metadata"]
|
||||
for k in (
|
||||
"module", "teeth", "pressure_angle", "helix_angle", "hand", "center",
|
||||
"bore_diameter", "keyway_width", "keyway_depth",
|
||||
"pitch_radius", "outer_radius", "base_radius", "root_radius",
|
||||
):
|
||||
assert k in md, f"metadata missing key {k!r}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_helical_gear_with_bore_and_keyway_handles_present(backend):
|
||||
await ensure_standard_linetypes(backend)
|
||||
await ensure_engineering_layers(backend)
|
||||
res = await draw_helical_gear_front_view(
|
||||
backend, module=3, teeth=24, helix_angle=20.0,
|
||||
bore_diameter=25.0, keyway_width=8.0, keyway_depth=4.0,
|
||||
)
|
||||
assert res["bore"] is not None
|
||||
assert res["keyway"] is not None
|
||||
assert "bore" in res["keyway"]
|
||||
assert "keyway_polyline" in res["keyway"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_helical_gear_metadata_radii_correct(backend):
|
||||
await ensure_standard_linetypes(backend)
|
||||
await ensure_engineering_layers(backend)
|
||||
res = await draw_helical_gear_front_view(
|
||||
backend, module=3, teeth=24, helix_angle=20.0, pressure_angle=20.0,
|
||||
)
|
||||
md = res["metadata"]
|
||||
assert math.isclose(md["pitch_radius"], 36.0, abs_tol=1e-6)
|
||||
assert math.isclose(md["outer_radius"], 39.0, abs_tol=1e-6)
|
||||
assert math.isclose(md["base_radius"], 36.0 * math.cos(math.radians(20.0)), abs_tol=1e-3)
|
||||
assert math.isclose(md["root_radius"], 32.25, abs_tol=1e-6)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_section_aa_does_not_draw_helix_lines(backend):
|
||||
await ensure_standard_linetypes(backend)
|
||||
await ensure_engineering_layers(backend)
|
||||
front = await draw_helical_gear_front_view(
|
||||
backend, module=3, teeth=24, helix_angle=20.0,
|
||||
bore_diameter=25.0, keyway_width=8.0, keyway_depth=4.0,
|
||||
)
|
||||
section = await draw_gear_section_aa(
|
||||
backend, gear_metadata=front["metadata"],
|
||||
x_offset=200.0, face_width=40.0,
|
||||
)
|
||||
assert "helix" not in section
|
||||
assert "helix_symbol" not in section
|
||||
Reference in New Issue
Block a user