Files
autocad-mcp-pro/engineering/keyway.py
T
U-C4N 88a9005a6a 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.
2026-04-28 12:24:08 +03:00

174 lines
5.3 KiB
Python

"""DIN 6885 keyway geometry — bore + keyway cutter for shafts and hubs."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from backends.base import AutoCADBackend
log = logging.getLogger(__name__)
DIN6885_TABLE: list[tuple[float, float, float, float, float]] = [
# (bore_min, bore_max, key_width, key_height, depth_in_shaft_t1)
(6, 8, 2, 2, 1.2),
(8, 10, 3, 3, 1.8),
(10, 12, 4, 4, 2.5),
(12, 17, 5, 5, 3.0),
(17, 22, 6, 6, 3.5),
(22, 30, 8, 7, 4.0),
(30, 38, 10, 8, 5.0),
(38, 44, 12, 8, 5.0),
(44, 50, 14, 9, 5.5),
(50, 58, 16, 10, 6.0),
(58, 65, 18, 11, 7.0),
(65, 75, 20, 12, 7.5),
(75, 85, 22, 14, 9.0),
(85, 95, 25, 14, 9.0),
(95, 110, 28, 16, 10.0),
(110, 130, 32, 18, 11.0),
]
def keyway_dimensions(bore_diameter: float) -> dict:
"""Look up DIN 6885 key width/height/depth for a given bore diameter (mm)."""
if bore_diameter < DIN6885_TABLE[0][0]:
raise ValueError(
f"bore_diameter {bore_diameter} mm is below DIN 6885 minimum "
f"({DIN6885_TABLE[0][0]} mm)."
)
if bore_diameter > DIN6885_TABLE[-1][1]:
raise ValueError(
f"bore_diameter {bore_diameter} mm exceeds DIN 6885 maximum "
f"({DIN6885_TABLE[-1][1]} mm)."
)
for b_min, b_max, width, height, t1 in DIN6885_TABLE:
if b_min < bore_diameter <= b_max or (bore_diameter == b_min and b_min == DIN6885_TABLE[0][0]):
return {
"width": float(width),
"height": float(height),
"depth_shaft": float(t1),
"depth_hub": float(height) - float(t1),
}
raise ValueError(f"No DIN 6885 row matched bore_diameter={bore_diameter}.")
async def draw_keyed_bore(
backend: "AutoCADBackend",
*,
center: tuple[float, float],
bore_diameter: float,
keyway_width: float | None = None,
keyway_depth: float | None = None,
layer: str = "GEOMETRY",
center_layer: str = "CENTER",
) -> dict:
"""Draw bore + upward-opening keyway notch + horizontal/vertical centerlines."""
cx, cy = float(center[0]), float(center[1])
r = float(bore_diameter) / 2.0
if keyway_width is None or keyway_depth is None:
dims = keyway_dimensions(bore_diameter)
if keyway_width is None:
keyway_width = dims["width"]
if keyway_depth is None:
keyway_depth = dims["depth_hub"]
bore = await backend.entity_create_circle(cx, cy, r, layer=layer)
half_w = float(keyway_width) / 2.0
depth = float(keyway_depth)
bore_top_y = cy + r
notch_top_y = bore_top_y + depth
left_x = cx - half_w
right_x = cx + half_w
points = [
[left_x, bore_top_y],
[left_x, notch_top_y],
[right_x, notch_top_y],
[right_x, bore_top_y],
]
keyway_poly = await backend.entity_create_polyline(points, closed=False, layer=layer)
overshoot = max(r * 0.2, 5.0)
horiz = await backend.entity_create_line(
cx - r - overshoot, cy, cx + r + overshoot, cy, layer=center_layer,
)
vert = await backend.entity_create_line(
cx, cy - r - overshoot, cx, cy + r + depth + overshoot, layer=center_layer,
)
return {
"bore": bore.handle,
"keyway_polyline": keyway_poly.handle,
"centerline_h": horiz.handle,
"centerline_v": vert.handle,
}
async def draw_keyway_section(
backend: "AutoCADBackend",
*,
center: tuple[float, float],
bore_diameter: float,
face_width: float,
keyway_width: float | None = None,
keyway_depth: float | None = None,
layer: str = "GEOMETRY",
) -> dict:
"""Side cross-section: bore as two horizontal lines with rectangular keyway notch on top."""
cx, cy = float(center[0]), float(center[1])
r = float(bore_diameter) / 2.0
half_face = float(face_width) / 2.0
if keyway_width is None or keyway_depth is None:
dims = keyway_dimensions(bore_diameter)
if keyway_width is None:
keyway_width = dims["width"]
if keyway_depth is None:
keyway_depth = dims["depth_hub"]
half_kw = float(keyway_width) / 2.0
depth = float(keyway_depth)
top_y = cy + r
bot_y = cy - r
notch_top_y = top_y + depth
x_left = cx - half_face
x_right = cx + half_face
notch_left = cx - half_kw
notch_right = cx + half_kw
bore_top = await backend.entity_create_line(
x_left, top_y, x_right, top_y, layer=layer,
)
bore_bottom = await backend.entity_create_line(
x_left, bot_y, x_right, bot_y, layer=layer,
)
notch_bottom = await backend.entity_create_line(
notch_left, top_y, notch_right, top_y, layer=layer,
)
notch_left_wall = await backend.entity_create_line(
notch_left, top_y, notch_left, notch_top_y, layer=layer,
)
notch_top = await backend.entity_create_line(
notch_left, notch_top_y, notch_right, notch_top_y, layer=layer,
)
notch_right_wall = await backend.entity_create_line(
notch_right, notch_top_y, notch_right, top_y, layer=layer,
)
return {
"bore_top": bore_top.handle,
"bore_bottom": bore_bottom.handle,
"keyway": [
notch_bottom.handle,
notch_left_wall.handle,
notch_top.handle,
notch_right_wall.handle,
],
}