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:
co-authored by
Claude Opus 5
parent
17f7228fd6
commit
b458923e19
@@ -27,7 +27,7 @@ Live through COM on Windows, or headless through ezdxf anywhere — one typed co
|
||||
|
||||
</div>
|
||||
|
||||
> **v1.5 release snapshot:** 171 tools · 8 resources · 5 prompt templates · 2423 collected tests.
|
||||
> **v1.5 release snapshot:** 172 tools · 8 resources · 5 prompt templates · 2423 collected tests.
|
||||
> 166 is the **registered** count; a default install advertises 161 over `tools/list`,
|
||||
> because `ENABLE_3D` is unset. `system_about` is the runtime authority.
|
||||
|
||||
@@ -104,7 +104,7 @@ Claude Desktop, Cursor, or any stdio MCP host. For HTTP: `autocad-mcp --transpor
|
||||
| Discovery | `search_tools` ranked over an AutoCAD command and synonym corpus — `FILLET`, `BPOLY`, `QSELECT`, `WBLOCK`, `OVERKILL`, `CHSPACE` each rank **#1** of the 161-tool advertised catalog |
|
||||
| Batching | `cad_batch` runs a step list in one round trip; `fields=` projects 11 result-heavy tools |
|
||||
| Paper space | tab lifecycle, viewports, `entity_change_space` (CHSPACE), `page_setup_apply` (ISO 216 / ANSI Y14.1 paper, ctb, scale, device — on both engines), `batch_plot` with every sheet size read back from its PDF's `/MediaBox`, `drawing_export_pdf(layout=…)` |
|
||||
| Templates | five bundled templates built by the server's own tools and pinned reproducible (`iso_a3_mech`, `iso_a1_arch`, `iso_a3_pid`, `ansi_b_mech`, `ansi_d_arch`) — `drawing_new(template="iso_a3_mech")`, `drawing_template_list` |
|
||||
| Templates | five bundled templates built by the server's own tools and pinned reproducible (`iso_a3_mech`, `iso_a1_arch`, `iso_a3_pid`, `ansi_b_mech`, `ansi_d_arch`) — `drawing_new(template="iso_a3_mech")`, `drawing_template_list`, `drawing_template_save` (.dwt on live AutoCAD, `dwt_write` refused headlessly) |
|
||||
| Selection | window vs crossing stated back to the caller; a polygon tested against its own shape, not its bounding box |
|
||||
| Boundaries | `boundary_trace` (BOUNDARY/BPOLY) chains loose edges into one closed polyline, arcs kept as bulges *(headless)* |
|
||||
| Measurement | `analysis_measure_entity` measures what is *in* the drawing, by handle |
|
||||
@@ -114,7 +114,7 @@ Claude Desktop, Cursor, or any stdio MCP host. For HTTP: `autocad-mcp --transpor
|
||||
| Quality loop | `drawing_preflight` → `drawing_plan` → `drawing_critique` → `drawing_refine` → `drawing_finalize` (0–100 score) |
|
||||
| Delivery | `drawing_deliver`: DXF/PDF/PNG + SHA-256 manifest + reopen-parity checks |
|
||||
|
||||
<sub>171 tools in 20 groups; <code>TOOL_PACKS=core</code> hides the nine <code>pid_*</code> tools from a client that never draws a P&ID. Plus 8 resources that cost nothing in the tool budget (<code>autocad://drawing/info</code>, <code>layers</code>, <code>blocks</code>, <code>entities/stats</code>, <code>entities/{layer_name}</code>, <code>system/status</code>, <code>pid/symbols</code>, <code>standards/isa51</code>) and 5 prompt templates.</sub>
|
||||
<sub>172 tools in 20 groups; <code>TOOL_PACKS=core</code> hides the nine <code>pid_*</code> tools from a client that never draws a P&ID. Plus 8 resources that cost nothing in the tool budget (<code>autocad://drawing/info</code>, <code>layers</code>, <code>blocks</code>, <code>entities/stats</code>, <code>entities/{layer_name}</code>, <code>system/status</code>, <code>pid/symbols</code>, <code>standards/isa51</code>) and 5 prompt templates.</sub>
|
||||
|
||||
**Two rules worth knowing.** Every coordinate in and out of a tool is WCS on both engines — the one exception is TEXT `rotation`, which stays in the entity frame because a mirrored TEXT is mirror-imaged and no scalar angle expresses that. And never read vertices back and shoelace them: that loses **28.2%** of the area on a semicircular edge, silently. `analysis_measure_entity(handle)` reads the real geometry and states its own accuracy.
|
||||
|
||||
|
||||
+42
-6
@@ -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) ─
|
||||
|
||||
|
||||
@@ -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) ─────
|
||||
|
||||
|
||||
@@ -76,6 +76,8 @@ class ToolAliases:
|
||||
# (page_setup_list vs page_setup_apply).
|
||||
# PLOT -- one sheet to PDF (drawing_export_pdf) or every sheet in one go
|
||||
# (batch_plot); a drafter typing it could mean either.
|
||||
# SAVEAS -- the same command saves a copy (drawing_save_as) or a template
|
||||
# (drawing_template_save, its "Drawing Template" file type).
|
||||
#
|
||||
# Every other AutoCAD command must map to exactly one tool.
|
||||
SHARED_ACAD_COMMANDS: frozenset[str] = frozenset(
|
||||
@@ -91,6 +93,7 @@ SHARED_ACAD_COMMANDS: frozenset[str] = frozenset(
|
||||
"PLINE",
|
||||
"PLOT",
|
||||
"QSELECT",
|
||||
"SAVEAS",
|
||||
"SETVAR",
|
||||
"ZOOM",
|
||||
}
|
||||
@@ -1462,6 +1465,16 @@ TOOL_ALIASES: dict[str, ToolAliases] = {
|
||||
"dwt list",
|
||||
),
|
||||
),
|
||||
"drawing_template_save": ToolAliases(
|
||||
acad=("SAVEAS",),
|
||||
synonyms=(
|
||||
"save as template",
|
||||
"make a dwt",
|
||||
"template from this drawing",
|
||||
"save as dwt",
|
||||
"reuse this sheet setup",
|
||||
),
|
||||
),
|
||||
"page_setup_apply": ToolAliases(
|
||||
acad=("PAGESETUP",),
|
||||
synonyms=(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"schema_version": 1,
|
||||
"package_version": "1.5.1",
|
||||
"totals": {
|
||||
"tools": 171,
|
||||
"tools": 172,
|
||||
"resources": 8,
|
||||
"prompts": 5,
|
||||
"groups": 20
|
||||
@@ -25,7 +25,7 @@
|
||||
"premium": 12,
|
||||
"solids": 5,
|
||||
"system": 7,
|
||||
"templates": 3,
|
||||
"templates": 4,
|
||||
"transactions": 3,
|
||||
"validation": 1,
|
||||
"view": 4
|
||||
@@ -741,6 +741,20 @@
|
||||
"readOnlyHint": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "drawing_template_save",
|
||||
"group": "templates",
|
||||
"cost": "mutate",
|
||||
"summary": "Save the current drawing as a template: .dwt on live AutoCAD, .dxf headlessly.",
|
||||
"tags": [
|
||||
"drawing",
|
||||
"template"
|
||||
],
|
||||
"annotations": {
|
||||
"title": "Save As Template",
|
||||
"destructiveHint": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "drawing_undo",
|
||||
"group": "drawing",
|
||||
|
||||
@@ -6855,7 +6855,7 @@ async def pid_tag_parse(
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ── SECTION 19: Page Setup & Templates (5 tools) ────────────────────────────
|
||||
# ── SECTION 19: Page Setup & Templates (6 tools) ────────────────────────────
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -7044,6 +7044,44 @@ async def drawing_template_list(ctx: Context = None) -> dict:
|
||||
}
|
||||
|
||||
|
||||
@cad_tool(
|
||||
summary="Save the current drawing as a template: .dwt on live AutoCAD, .dxf headlessly.",
|
||||
cost="mutate",
|
||||
)
|
||||
@mcp.tool(
|
||||
annotations={"title": "Save As Template", "destructiveHint": False},
|
||||
tags={"template", "drawing"},
|
||||
)
|
||||
async def drawing_template_save(
|
||||
path: Annotated[str, "Destination .dwt (live AutoCAD) or .dxf (either engine)."],
|
||||
name: Annotated[str | None, "Template name for the report (default: the file stem)."] = None,
|
||||
description: Annotated[
|
||||
str | None, "Template description → DWT summary Comments on the live engine."
|
||||
] = None,
|
||||
ctx: Context = None,
|
||||
) -> dict:
|
||||
"""AutoCAD's SAVEAS → Drawing Template, on both engines.
|
||||
|
||||
Live AutoCAD writes a real `.dwt` (`SaveAs(path, ac2018_Template)`) and
|
||||
puts `description` into the template's summary Comments; the active
|
||||
document is rebound to the new file, as SAVEAS does. Headlessly a `.dxf`
|
||||
template is written — `drawing_new(template=<path>)` opens it — and a
|
||||
`.dwt` request is refused with capability `dwt_write` (the message names
|
||||
the DXF route and the COM route); a description is reported
|
||||
`description_written: false` because DXF has nowhere to keep it. Any other
|
||||
suffix, and any path outside the allowed directories, is refused before
|
||||
a byte is written.
|
||||
|
||||
Pack: core · lean: no (`drawing_save_as` is not lean either).
|
||||
"""
|
||||
validated = validate_path(path, allow_write=True)
|
||||
await ctx.info(f"Saving template: {validated}")
|
||||
try:
|
||||
return await _backend(ctx).drawing_template_save(str(validated), name, description)
|
||||
except ValueError as exc:
|
||||
raise ToolError(str(exc)) from exc
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ── RESOURCES ───────────────────────────────────────────────────────────────
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Bundled drawing templates
|
||||
|
||||
Five templates, built by the server's own tools — never hand-edited — and
|
||||
listed by `drawing_template_list`. Start from one with
|
||||
`drawing_new(template="iso_a3_mech")`.
|
||||
|
||||
| name | standard | sheet | layer set | styles | page setup |
|
||||
|---|---|---|---|---|---|
|
||||
| `iso_a3_mech` | ISO | A3 landscape, ISO 5457 frame + ISO 7200 title block | mech | ISO-25 / ISOCP | ISO_A3, monochrome.ctb, 1:1 |
|
||||
| `iso_a1_arch` | ISO | A1 landscape | iso13567 | ISO-25 / ISOCP, annotation scale 1:50 | ISO_A1, monochrome.ctb, fit |
|
||||
| `iso_a3_pid` | ISO | A3 landscape, ISO 5457 frame + title block | pid | ISO-25 / ISOCP | ISO_A3, monochrome.ctb, 1:1 |
|
||||
| `ansi_b_mech` | ANSI (metric) | ANSI B landscape | mech | ANSI / ROMANS | ANSI_B, monochrome.ctb, 1:1 |
|
||||
| `ansi_d_arch` | ANSI (metric) | ANSI D landscape | iso13567 | ANSI / ROMANS | ANSI_D, monochrome.ctb, fit |
|
||||
|
||||
## The two files per template
|
||||
|
||||
- `<name>.dxf` — the headless twin. Produced by
|
||||
`uv run --frozen python scripts/build_templates.py`, which runs
|
||||
`drawing_new → apply_layer_set → styles → drawing_settings → layout rename →
|
||||
titleblock_apply_iso_a3 (A3 sheets) → page_setup_apply → drawing_save_as`.
|
||||
`scripts/build_templates.py --check` rebuilds into a temporary folder and
|
||||
fails if any committed DXF differs (the save-time stamps `$TDCREATE`,
|
||||
`$TDUPDATE`, `$FINGERPRINTGUID`, `$VERSIONGUID` and the two `EZDXF_META`
|
||||
entries are masked; everything else must be byte-identical).
|
||||
`tests/test_templates.py` runs the same check.
|
||||
- `<name>.dwt` — the live twin, used by `drawing_new(template=…)` on the COM
|
||||
backend. AutoCAD's DWT is a DWG container, which no headless library
|
||||
writes, so these are produced **on the live machine** by
|
||||
`uv run --frozen python scripts/smoke_settings_com.py --build-dwt` (Task 24's
|
||||
smoke): it opens each DXF over COM and calls `drawing_template_save(…dwt)`
|
||||
(`SaveAs(path, ac2018_Template)`), then the files are committed once. While
|
||||
a `.dwt` is missing, `resolve_template` falls back to the DXF and reports
|
||||
`source: "bundled_dxf"`; `drawing_template_list` shows `files.dwt.present`.
|
||||
The live engine does not open that DXF blank: `Documents.Add` accepts only
|
||||
a genuine DWT (measured on AutoCAD 2026), so the COM `drawing_new` converts
|
||||
the DXF through `SaveAs(…, ac2018_Template)` once, caches the result under
|
||||
the temp folder keyed on path, size and mtime, and reports
|
||||
`template_dwt: {path, cached}`.
|
||||
|
||||
Line endings: ezdxf writes CRLF and `.gitattributes` pins `templates/*.dxf`
|
||||
to CRLF so a checkout on any platform equals a fresh build.
|
||||
|
||||
Rebuild after changing anything the build depends on (layer sets, the title
|
||||
block, the dimension presets, the settings facade) and commit the result
|
||||
together with the change — the reproducibility test is the gate.
|
||||
@@ -10,6 +10,7 @@ import pytest
|
||||
from fastmcp.exceptions import ToolError
|
||||
|
||||
import server
|
||||
from backends.base import UnsupportedCapabilityError
|
||||
from engineering.standards.templates import (
|
||||
TEMPLATE_CATALOG,
|
||||
TEMPLATES_DIR,
|
||||
@@ -371,3 +372,116 @@ async def test_com_drawing_new_tool_reports_the_converted_dwt(com_new):
|
||||
assert documents.calls[-1] == ("Add", template["dwt"])
|
||||
else:
|
||||
assert "dwt" not in template and documents.calls == [("Add", template["path"])]
|
||||
|
||||
|
||||
# ── drawing_template_save ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_template_save_dxf_headless_round_trips(backend, tmp_path):
|
||||
await backend.layer_create("MINE", color=3)
|
||||
out = tmp_path / "mine_template.dxf"
|
||||
result = await backend.drawing_template_save(str(out), name="Mine", description="my sheet")
|
||||
assert result == {
|
||||
"ok": True,
|
||||
"path": str(out),
|
||||
"format": "dxf",
|
||||
"backend": "ezdxf",
|
||||
"name": "Mine",
|
||||
"description_written": False,
|
||||
"description_note": (
|
||||
"DXF has no template description field; it is a DWG/DWT summary property "
|
||||
"(capability 'dwgprops')"
|
||||
),
|
||||
}
|
||||
assert out.is_file()
|
||||
await server.drawing_new(template=str(out), ctx=_Ctx(backend))
|
||||
assert "MINE" in {layer.name for layer in await backend.layer_list()}
|
||||
|
||||
|
||||
async def test_template_save_dwt_headless_is_refused_with_the_capability(backend, tmp_path):
|
||||
out = tmp_path / "mine.dwt"
|
||||
with pytest.raises(UnsupportedCapabilityError) as excinfo:
|
||||
await backend.drawing_template_save(str(out))
|
||||
assert excinfo.value.capability == "dwt_write"
|
||||
message = str(excinfo.value)
|
||||
assert ".dxf" in message and "com" in message.lower()
|
||||
assert not out.exists()
|
||||
|
||||
|
||||
async def test_template_save_refuses_other_suffixes(backend, tmp_path):
|
||||
with pytest.raises(ValueError, match=r"\.dxf"):
|
||||
await backend.drawing_template_save(str(tmp_path / "mine.txt"))
|
||||
|
||||
|
||||
async def test_template_save_tool_validates_the_path(backend, tmp_path):
|
||||
with pytest.raises(ToolError):
|
||||
await server.drawing_template_save("../../outside.dxf", ctx=_Ctx(backend))
|
||||
result = await server.drawing_template_save(str(tmp_path / "t.dxf"), ctx=_Ctx(backend))
|
||||
assert result["ok"] and result["name"] == "t"
|
||||
|
||||
|
||||
class _SaveRecorder:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
self.SummaryInfo = types.SimpleNamespace()
|
||||
self.Name = "Drawing1.dwg"
|
||||
|
||||
def SaveAs(self, path, fmt=None):
|
||||
self.calls.append(("SaveAs", path, fmt))
|
||||
self.Name = Path(path).name
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def com_backend(monkeypatch):
|
||||
pytest.importorskip("win32com.client", reason="pywin32 not installed")
|
||||
from backends import com_backend as module
|
||||
|
||||
document = _SaveRecorder()
|
||||
monkeypatch.setattr(module, "_acad_doc", lambda: document)
|
||||
backend = module.ComBackend()
|
||||
|
||||
async def _run_inline(func, *args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(backend, "_run", _run_inline)
|
||||
return backend, document
|
||||
|
||||
|
||||
async def test_com_template_save_dwt_uses_ac2018_template(com_backend, tmp_path):
|
||||
backend, document = com_backend
|
||||
out = str(tmp_path / "mine.dwt")
|
||||
result = await backend.drawing_template_save(out, description="A3 mech")
|
||||
assert document.calls == [("SaveAs", out, 66)]
|
||||
assert document.SummaryInfo.Comments == "A3 mech"
|
||||
assert result == {
|
||||
"ok": True,
|
||||
"path": out,
|
||||
"format": "dwt",
|
||||
"backend": "com",
|
||||
"name": "mine",
|
||||
"description_written": True,
|
||||
"document_rebound": True,
|
||||
}
|
||||
|
||||
|
||||
async def test_com_template_save_dxf_uses_the_dxf_code(com_backend, tmp_path):
|
||||
backend, document = com_backend
|
||||
out = str(tmp_path / "mine.dxf")
|
||||
result = await backend.drawing_template_save(out)
|
||||
assert document.calls == [("SaveAs", out, 61)]
|
||||
assert result["format"] == "dxf" and result["description_written"] is False
|
||||
|
||||
|
||||
async def test_com_save_as_dwt_no_longer_writes_an_r13_dxf(com_backend, tmp_path):
|
||||
"""drawing_save_as mapped 'dwt' to 5, which is AcSaveAsType.acR13_dxf."""
|
||||
backend, document = com_backend
|
||||
out = str(tmp_path / "x.dwt")
|
||||
await backend.drawing_save_as(out, "dwt")
|
||||
assert document.calls == [("SaveAs", out, 66)]
|
||||
|
||||
|
||||
async def test_com_template_save_refuses_other_suffixes_before_saving(com_backend, tmp_path):
|
||||
backend, document = com_backend
|
||||
with pytest.raises(ValueError, match=r"\.dwt"):
|
||||
await backend.drawing_template_save(str(tmp_path / "x.dwg"))
|
||||
assert document.calls == []
|
||||
|
||||
@@ -495,7 +495,7 @@ async def test_tool_groups_is_byte_identical_to_the_source_declared_grouping():
|
||||
|
||||
|
||||
async def test_tool_group_sizes_are_unchanged():
|
||||
"""Frozen snapshot of the surface (171 tools, 20 groups).
|
||||
"""Frozen snapshot of the surface (172 tools, 20 groups).
|
||||
|
||||
Taken before @cad_tool landed at 131 tools; `batch` moved 2 -> 3 when
|
||||
v1.5.0's `cad_batch` joined `entity_batch_create`/`entity_batch_modify`, and
|
||||
@@ -523,7 +523,8 @@ async def test_tool_group_sizes_are_unchanged():
|
||||
— all tagged `layout`, which `_GROUP_TAG_PRIORITY` ranks above
|
||||
`query`/`export`, so they file with the sheets they set up. `templates`
|
||||
moved 2 -> 3 when v1.6's `drawing_template_list` joined SECTION 19
|
||||
(tagged `template`, which ranks above `drawing`). Every other number
|
||||
(tagged `template`, which ranks above `drawing`), then 3 -> 4 when
|
||||
`drawing_template_save` joined. Every other number
|
||||
here has been unchanged since the snapshot was taken.
|
||||
"""
|
||||
sizes = {label: len(names) for label, names in (await server._tool_groups()).items()}
|
||||
@@ -544,9 +545,9 @@ async def test_tool_group_sizes_are_unchanged():
|
||||
"premium": 12,
|
||||
"solids": 5,
|
||||
"system": 7,
|
||||
"templates": 3,
|
||||
"templates": 4,
|
||||
"transactions": 3,
|
||||
"validation": 1,
|
||||
"view": 4,
|
||||
}
|
||||
assert sum(sizes.values()) == 171
|
||||
assert sum(sizes.values()) == 172
|
||||
|
||||
Reference in New Issue
Block a user