feat(templates): drawing_template_save on both engines — dwt_write is a declared capability

Live AutoCAD writes a real .dwt through SaveAs(path, ac2018_Template = 66) and
puts the description into the summary Comments; headlessly a .dxf template is
written through drawing_save_as and a .dwt request is refused with capability
dwt_write, naming the DXF route and the COM route. drawing_save_as's "dwt"
entry was 5 (acR13_dxf): a DWT request wrote an R13 DXF under a .dwt name.

Probed live on AutoCAD 2026 (typelib: ac2018_Template = 66, acR13_dxf = 5,
acTemplateDwg absent; the saved file starts AC1032, the document is rebound,
Comments takes). SECTION 19 is now 6 tools; SAVEAS joins SHARED_ACAD_COMMANDS;
templates/README.md documents the two files per template.

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:48:31 +03:00
co-authored by Claude Opus 5
parent 17f7228fd6
commit b458923e19
9 changed files with 322 additions and 20 deletions
+42 -6
View File
@@ -1117,8 +1117,10 @@ class ComBackend(AutoCADBackend):
async def drawing_save_as(self, path: str, fmt: str = "dwg") -> dict:
def _sync():
doc = _acad_doc()
# AutoCAD SaveAs format constants: 12=DWG R2010, 61=DXF R2010
fmt_map = {"dwg": 12, "dxf": 61, "dwt": 5}
# AutoCAD AcSaveAsType: 12 = ac2000_dwg, 61 = ac2013_dxf, 66 = ac2018_Template.
# "dwt" was 5 (acR13_dxf) until v1.6: a DWT request wrote an R13 DXF
# under a .dwt name.
fmt_map = {"dwg": 12, "dxf": 61, "dwt": 66}
acad_fmt = fmt_map.get(fmt.lower(), 12)
doc.SaveAs(path, acad_fmt)
return {"ok": True, "path": path, "format": fmt}
@@ -2136,6 +2138,10 @@ class ComBackend(AutoCADBackend):
# scripts/smoke_settings_com.py (Task 24).
_AC_NO_ROTATION = 0
#: AcSaveAsType, AutoCAD 2026 typelib: ac2018_Template = 66 (acNative = 64,
#: ac2013_dxf = 61). The spec's "acTemplateDwg" is not a member of the enum.
_AC_SAVEAS_TEMPLATE = 66
_AC_SAVEAS_DXF = 61
#: Layout properties a ConfigName write can move as a side effect (measured
#: live for CanonicalMediaName; units / rotation are re-applied on the same
#: evidence-before-trust basis) — snapshotted before the device switch and
@@ -2442,10 +2448,40 @@ class ComBackend(AutoCADBackend):
return await self._run(_sync)
async def drawing_template_save(self, path, name=None, description=None) -> dict:
# Completed in Task 20; declared here so the ABC instantiates.
raise UnsupportedCapabilityError(
"dwt_write", "drawing_template_save: not implemented yet (Task 20)"
)
"""``SaveAs(path, ac2018_Template)`` for ``.dwt``, the DXF code for ``.dxf``.
AutoCAD's template description is its summary-info Comments field, so
``description`` is written there first (``description_written`` says
whether it took). SaveAs rebinds the active document to the new file,
as the SAVEAS command does — reported as ``document_rebound``.
Fake-tested; executed live by ``scripts/smoke_settings_com.py --build-dwt``.
"""
suffix = Path(path).suffix.lower()
if suffix not in (".dwt", ".dxf"):
raise ValueError(f"drawing_template_save: path must end in .dwt or .dxf, got {path!r}")
def _sync():
doc = _acad_doc()
description_written = False
if description is not None:
try:
doc.SummaryInfo.Comments = str(description)
description_written = True
except Exception as exc:
log.debug("SummaryInfo.Comments write failed: %s", exc)
code = self._AC_SAVEAS_TEMPLATE if suffix == ".dwt" else self._AC_SAVEAS_DXF
doc.SaveAs(path, code)
return {
"ok": True,
"path": path,
"format": suffix[1:],
"backend": "com",
"name": name or Path(path).stem,
"description_written": description_written,
"document_rebound": True,
}
return await self._run(_sync)
# ── 3D solids (native ActiveX; gated behind ENABLE_3D at the tool layer) ─
+45 -4
View File
@@ -123,6 +123,18 @@ _DWG_WRITE_REFUSAL = (
)
# A DWT is a DWG container with a template flag. ezdxf cannot write it, and a
# DXF under a .dwt name is exactly the mislabelled file the DWG refusal exists
# to prevent. The DXF route works headlessly end to end: drawing_new(template=)
# opens a .dxf template.
_DWT_WRITE_REFUSAL = (
"drawing_template_save: the headless ezdxf backend cannot write DWT — a .dwt is a DWG "
"container, and ezdxf has no DWG writer. Save the template as .dxf instead "
"(drawing_new(template='{stem}.dxf') opens it headlessly), or switch to the live COM "
"backend (AUTOCAD_MCP_BACKEND=com, needs Windows + AutoCAD) for a real .dwt."
)
def _is_dwg_path(path: str) -> bool:
"""True when `path` names a DWG file. The extension is authoritative (N2)."""
return Path(path).suffix.lower() == ".dwg"
@@ -2687,10 +2699,39 @@ class EzdxfBackend(AutoCADBackend):
return [{"name": name, "source": "catalog", "installed": None} for name in CTB_CATALOG]
async def drawing_template_save(self, path, name=None, description=None) -> dict:
# Completed in Task 20; declared here so the ABC instantiates.
raise UnsupportedCapabilityError(
"dwt_write", "drawing_template_save: not implemented yet (Task 20)"
)
"""Save the current drawing as a template — DXF only, headlessly.
``.dwt`` is refused with capability ``dwt_write`` before anything is
written; ``.dxf`` goes through ``drawing_save_as`` (the document is
rebound to the new path, as AutoCAD's SAVEAS does). A description has
no home in a DXF header — it is a DWG/DWT summary property — so it is
reported ``description_written: False`` rather than dropped silently.
"""
suffix = Path(path).suffix.lower()
if suffix == ".dwt":
raise UnsupportedCapabilityError(
"dwt_write", _DWT_WRITE_REFUSAL.format(stem=Path(path).stem)
)
if suffix != ".dxf":
raise ValueError(
f"drawing_template_save: path must end in .dxf (headless) or .dwt (live "
f"AutoCAD), got {path!r}"
)
saved = await self.drawing_save_as(path, "dxf")
result = {
"ok": True,
"path": saved["path"],
"format": "dxf",
"backend": "ezdxf",
"name": name or Path(path).stem,
"description_written": False,
}
if description is not None:
result["description_note"] = (
"DXF has no template description field; it is a DWG/DWT summary property "
"(capability 'dwgprops')"
)
return result
# ── 3D solids (unsupported headlessly — honest capability boundary) ─────