feat(templates): five bundled templates built by the server's own tools; drawing_new(template=name)

SECTION 19 grows to five tools with drawing_template_list; drawing_new takes a
bundled name (iso_a3_mech, iso_a1_arch, iso_a3_pid, ansi_b_mech, ansi_d_arch)
or a .dwt/.dxf path and reports template: {name|path, path, source}.

scripts/build_templates.py builds templates/*.dxf through EzdxfBackend
(layer set, header DIM variables until group S's styles land, drawing_settings,
sheet tab, ISO 5457 frame on A3, page_setup_apply) and --check pins them
reproducible. One drift the plan's same-process measurement could not see is
fixed at the source rather than masked: ezdxf orders CLASSES by set iteration,
so two processes disagreed on LAYOUT / ACDBPLACEHOLDER; the section is re-keyed
(required list first, then by name) before the save.

The root .gitignore ignores *.dxf as generated output; templates/*.dxf is
un-ignored there, since the bundled set is the one DXF that is committed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-21 22:03:41 +03:00
co-authored by Claude Opus 5
parent 86c5d6ad65
commit a76d9e08a3
17 changed files with 19981 additions and 22 deletions
+70 -11
View File
@@ -1082,7 +1082,7 @@ async def drawing_info(ctx: Context) -> dict:
@cad_tool(
summary="Start a blank drawing, pre-seeded with the standard engineering layers.",
summary="Start a drawing: blank with the engineering layers, or from a bundled/own template.",
cost="mutate",
)
@mcp.tool(
@@ -1090,7 +1090,11 @@ async def drawing_info(ctx: Context) -> dict:
tags={"drawing"},
)
async def drawing_new(
template: Annotated[str | None, "Optional path to .dwt template file"] = None,
template: Annotated[
str | None,
"Bundled template name (drawing_template_list: iso_a3_mech, iso_a1_arch, iso_a3_pid, "
"ansi_b_mech, ansi_d_arch) or a path to a .dwt/.dxf template file.",
] = None,
bootstrap: Annotated[
bool,
Field(
@@ -1101,19 +1105,43 @@ async def drawing_new(
] = True,
ctx: Context = None,
) -> dict:
"""Create a new empty drawing, optionally from a template (.dwt).
"""Create a new drawing, optionally from a template.
When ``bootstrap=True`` (default), the drawing is also seeded with the
standard engineering linetypes (CENTER/HIDDEN/PHANTOM) and layers
(GEOMETRY, DIM, CENTER, HIDDEN, PHANTOM, HATCH, TEXT, TITLEBLOCK).
A bundled name resolves to `templates/<name>.dxf` headlessly and
`templates/<name>.dwt` on the live engine (falling back to the DXF with
`source: "bundled_dxf"` when no `.dwt` twin is committed); a path is used
as given. The result carries `template: {name|path, path, source}`.
Refused before anything is replaced: a bare name that is not in the
catalogue (the message lists the five), a template path that does not
exist, and a path outside the allowed directories. With `bootstrap=True`
(default) the standard engineering linetypes and layers are ensured
afterwards — idempotent on a template that already has them.
"""
if template is not None:
validated_template = validate_path(template, allow_write=False)
template = str(validated_template)
await ctx.info(f"Creating new drawing (template={template}, bootstrap={bootstrap})")
from pathlib import Path as _P
from engineering.standards.templates import resolve_template
backend = _backend(ctx)
template_info: dict | None = None
if template is not None:
try:
resolved, source = resolve_template(template, backend.name)
except ValueError as exc:
raise ToolError(f"drawing_new: {exc}") from exc
if source == "path":
resolved = str(validate_path(resolved, allow_write=False))
if not _P(resolved).is_file():
raise ToolError(f"drawing_new: template file not found: {resolved}")
template_info = {"path": resolved, "source": source}
else:
template_info = {"name": template.strip().lower(), "path": resolved, "source": source}
template = resolved
await ctx.info(f"Creating new drawing (template={template}, bootstrap={bootstrap})")
raw = await backend.drawing_new(template)
result = dict(raw) if isinstance(raw, dict) else {"result": raw}
if template_info is not None:
result["template"] = template_info
if bootstrap:
try:
from engineering import (
@@ -6816,7 +6844,7 @@ async def pid_tag_parse(
# ---------------------------------------------------------------------------
# ── SECTION 19: Page Setup & Templates (4 tools) ────────────────────────────
# ── SECTION 19: Page Setup & Templates (5 tools) ────────────────────────────
# ---------------------------------------------------------------------------
@@ -6974,6 +7002,37 @@ async def batch_plot(
raise ToolError(str(exc)) from exc
@cad_tool(
summary="List the five bundled drawing templates: standard, sheet, layers, styles, page setup.",
cost="read",
)
@mcp.tool(
annotations={"title": "List Drawing Templates", "readOnlyHint": True},
tags={"template", "drawing", "query"},
)
async def drawing_template_list(ctx: Context = None) -> dict:
"""The templates `drawing_new(template=<name>)` can start from.
Each row: `standard` (ISO/ANSI), `sheet`, `paper`, `layout` (the sheet tab's
name), `layer_set` (mech / pid / iso13567), `dimstyle` / `textstyle`
(ISO-25 / ISOCP or ANSI / ROMANS), `scale`, `title_block` (the ISO 5457 A3
frame, A3 sheets only), the `settings` applied, and `files.dxf` /
`files.dwt` with `present` flags — the `.dwt` twins exist only once built
on a live AutoCAD (`scripts/smoke_settings_com.py --build-dwt`). Never
refuses; it reads a catalogue.
"""
from engineering.standards.templates import TEMPLATES_DIR, template_rows
rows = template_rows()
return {
"ok": True,
"templates": rows,
"count": len(rows),
"templates_dir": str(TEMPLATES_DIR),
"engine": _backend(ctx).name,
}
# ---------------------------------------------------------------------------
# ── RESOURCES ───────────────────────────────────────────────────────────────
# ---------------------------------------------------------------------------