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:
U-C4N
2026-04-28 12:24:08 +03:00
parent 59f3bc5fd5
commit 88a9005a6a
16 changed files with 2320 additions and 10 deletions
+36
View File
@@ -358,6 +358,15 @@ class AutoCADBackend(ABC):
@abstractmethod
async def layer_show(self, name: str) -> dict: ...
# ── linetype management ──────────────────────────────────────────────────
@abstractmethod
async def linetype_list(self) -> list[str]: ...
@abstractmethod
async def linetype_load(
self, name: str, file: str | None = None,
) -> dict: ...
# ── block operations ─────────────────────────────────────────────────────
@abstractmethod
async def block_list(self) -> list[BlockInfo]: ...
@@ -449,6 +458,33 @@ class AutoCADBackend(ABC):
@abstractmethod
async def system_run_lisp(self, expression: str) -> dict: ...
# ── concrete helpers (use existing primitives) ──────────────────────────
async def set_layer_active(self, name: str) -> None:
"""Convenience: alias for layer_set_current. Concrete; backends need not override."""
await self.layer_set_current(name)
async def ensure_linetypes(
self, names: list[str], file: str | None = None,
) -> dict[str, str]:
"""Idempotently load each linetype in `names`. Returns {name: status}.
Status is one of: 'already_loaded', 'loaded', 'failed: <msg>'.
"""
try:
current = {ln.lower() for ln in await self.linetype_list()}
except Exception:
current = set()
results: dict[str, str] = {}
for n in names:
if n.lower() in current:
results[n] = "already_loaded"
continue
try:
await self.linetype_load(n, file=file)
results[n] = "loaded"
except Exception as exc:
results[n] = f"failed: {exc}"
return results
# ---------------------------------------------------------------------------
# Helpers