diff --git a/backends/com_backend.py b/backends/com_backend.py index 256aa22..d1819f2 100644 --- a/backends/com_backend.py +++ b/backends/com_backend.py @@ -169,6 +169,71 @@ def _acad_app(): return _COM_STATE["app"] +#: ``AcSaveAsType`` template constants, newest first: ac2018_Template, +#: ac2013_Template, ac2010_Template. A seat older than the first refuses it +#: with a COM error and the next one is tried. +_TEMPLATE_SAVE_FORMATS = (66, 62, 50) + + +def _template_cache_dir() -> Path: + import tempfile + + return Path(tempfile.gettempdir()) / "acadmcp-templates" + + +def _template_as_dwt(app, source: Path) -> tuple[Path, bool]: + """A real ``.dwt`` for ``source``, built once through AutoCAD and cached. + + ``Documents.Add()`` accepts only a genuine DWT: measured on AutoCAD + 2026, ``Add(templates/iso_a3_mech.dxf)`` returned the default acadiso + drawing (layers ``['0']``, tabs ``Layout1``/``Layout2``) exactly as + ``Add()`` and ``Add()`` did, and the DXF bytes renamed + ``.dwt`` fared no better. Opening the file and saving it as a template + (``SaveAs(path, ac2018_Template)``) is what yields the eleven layers and + the ``A3`` tab, so that is done here — once per file content, keyed on the + path, size and mtime, under the temp folder — and the ``.dwt`` is what + ``Add`` is handed. Returns ``(dwt_path, cached)``. + + Must only be called from the COM executor thread. The conversion document + is opened read-only and closed without saving in a ``finally``; the + ``.dwt`` on disk is the only thing it leaves behind. + """ + import hashlib + + source = Path(source).resolve() + stat = source.stat() + digest = hashlib.sha1( + f"{source}|{stat.st_size}|{stat.st_mtime_ns}".encode("utf-8", "surrogateescape") + ).hexdigest()[:12] + cache_dir = _template_cache_dir() + cache_dir.mkdir(parents=True, exist_ok=True) + dwt = cache_dir / f"{source.stem}-{digest}.dwt" + if dwt.is_file() and dwt.stat().st_size > 0: + return dwt, True + doc = app.Documents.Open(str(source), True) + try: + last_exc: Exception | None = None + for fmt in _TEMPLATE_SAVE_FORMATS: + try: + doc.SaveAs(str(dwt), fmt) + break + except _COM_ERROR as exc: + last_exc = exc + log.debug("SaveAs(%s, %d) refused: %s", dwt, fmt, exc) + else: + raise RuntimeError( + f"could not save {source.name} as a .dwt template: {last_exc}" + ) from last_exc + finally: + try: + doc.Close(False) + except Exception as exc: # the temp document must never stay open + log.warning("template conversion document did not close: %s", exc) + if not dwt.is_file() or dwt.stat().st_size == 0: + raise RuntimeError(f"AutoCAD reported the template saved but {dwt} is not on disk") + return dwt, False + + def _acad_doc(): """Return active AutoCAD document.""" app = _acad_app() @@ -993,13 +1058,36 @@ class ComBackend(AutoCADBackend): return await self._run(_sync) async def drawing_new(self, template: str | None = None) -> dict: + """``Documents.Add()``, or ``Documents.Add()`` from a template file. + + A template that is not already a ``.dwt`` (the bundled ``.dxf`` files, + an explicit ``.dxf`` path) is first converted into one through AutoCAD + itself — see ``_template_as_dwt`` for the measurement that makes this + necessary — and the result reports ``template_dwt: {path, cached}``. + A template path that is not a file is refused here rather than handed + to ``Add``, which would silently create the default drawing instead. + """ + def _sync(): app = _acad_app() - if template: - doc = app.Documents.Add(template) - else: + if not template: doc = app.Documents.Add() - return {"ok": True, "name": doc.Name} + return {"ok": True, "name": doc.Name} + source = Path(template) + if not source.is_file(): + raise FileNotFoundError( + f"template file not found: {template} (Documents.Add would silently " + "create the default drawing instead)" + ) + result: dict[str, Any] = {"ok": True} + if source.suffix.lower() == ".dwt": + dwt = source + else: + dwt, cached = _template_as_dwt(app, source) + result["template_dwt"] = {"path": str(dwt), "cached": cached} + doc = app.Documents.Add(str(dwt)) + result["name"] = doc.Name + return result result = await self._run(_sync) await self._ensure_document_state() diff --git a/engineering/standards/templates.py b/engineering/standards/templates.py index 8306b18..056b8cb 100644 --- a/engineering/standards/templates.py +++ b/engineering/standards/templates.py @@ -158,10 +158,13 @@ def resolve_template(name_or_path: str, engine: str) -> tuple[str, str]: A catalogue name resolves to ``templates/.dxf`` (headless) or ``templates/.dwt`` (live), falling back to the DXF with - ``source="bundled_dxf"`` when the ``.dwt`` twin is missing. Anything with a - path separator or a ``.dwt``/``.dxf`` suffix is a path (``source="path"``, - not checked here). A bare word that is not in the catalogue is refused - with the catalogue names. + ``source="bundled_dxf"`` when the ``.dwt`` twin is missing — the live + engine then converts that DXF into a real ``.dwt`` itself before + ``Documents.Add`` (``ComBackend.drawing_new``), because AutoCAD ignores a + DXF handed to ``Add`` and silently returns the default drawing. Anything + with a path separator or a ``.dwt``/``.dxf`` suffix is a path + (``source="path"``, not checked here). A bare word that is not in the + catalogue is refused with the catalogue names. """ raw = str(name_or_path or "").strip() if not raw: diff --git a/scripts/build_templates.py b/scripts/build_templates.py index af0e51e..447dae3 100644 --- a/scripts/build_templates.py +++ b/scripts/build_templates.py @@ -11,9 +11,15 @@ what ships. Styles: once group S's ``dimstyle_create`` / ``textstyle_create`` are on the backend they are used (ISO-25 / ISOCP, ANSI / ROMANS, set current). Until then -the same values (spec §4.1) are written as header DIM variables so the template -already dimensions correctly; the final task re-runs this script after the S -merge and re-commits the five files. +the same values (spec §4.1) are written twice — as header DIM variables, which +is what a live seat dimensions with, *and* onto the ``Standard`` dimstyle +entry, which is what the headless renderer reads (it never consults the +header; ``_with_header_dimvars`` folds six of the variables, so the other +eight — DIMTAD, DIMTIH, DIMTOH, DIMEXE, DIMEXO, DIMGAP, DIMLUNIT, DIMLWD/E — +were a header-only no-op: measured, ``ansi_b_mech`` dimensioned with ISO text +placement headlessly while AutoCAD honoured the header). Both engines now +dimension the same template the same way; the final task re-runs this script +after the S merge and re-commits the five files. ``--check`` rebuilds into a temporary folder and compares line by line after masking the values ezdxf cannot keep constant — measured on 2026-09-16, exactly @@ -115,11 +121,14 @@ async def _apply_styles(backend, spec: TemplateSpec) -> dict: ) return {"mode": "styles", "dimstyle": dim["name"], "textstyle": text["name"]} applied = {} + standard = backend._doc.dimstyles.get("Standard") # what the headless renderer reads for variable, value in _FALLBACK_DIMVARS[spec.dimstyle_preset].items(): applied[variable] = (await backend.system_set_variable(variable, value))["ok"] + standard.dxf.set(variable.lower(), value) return { "mode": "header_dimvars", "applied": applied, + "dimstyle": standard.dxf.name, "pending": "dimstyle_create/textstyle_create not on this backend yet (group S)", } diff --git a/server.py b/server.py index 5e96773..58cce6f 100644 --- a/server.py +++ b/server.py @@ -1112,6 +1112,13 @@ async def drawing_new( `source: "bundled_dxf"` when no `.dwt` twin is committed); a path is used as given. The result carries `template: {name|path, path, source}`. + On the live engine only a genuine `.dwt` reaches `Documents.Add` — + measured on AutoCAD 2026, `Add()` silently returns the default + drawing — so a `.dxf` template (bundled or a path) is first converted + through AutoCAD into a cached `.dwt` and `template.dwt` names it + (`template.dwt_cached` says whether the conversion was reused). The + drawing then really carries the template's layers, tabs and page setup. + 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` @@ -1140,7 +1147,11 @@ async def drawing_new( 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} + converted = result.pop("template_dwt", None) if template_info is not None: + if converted: # the live engine built a .dwt from the .dxf (see ComBackend.drawing_new) + template_info["dwt"] = converted["path"] + template_info["dwt_cached"] = bool(converted.get("cached")) result["template"] = template_info if bootstrap: try: diff --git a/templates/ansi_b_mech.dxf b/templates/ansi_b_mech.dxf index 3c59c40..fa99f5d 100644 --- a/templates/ansi_b_mech.dxf +++ b/templates/ansi_b_mech.dxf @@ -489,7 +489,7 @@ $SKPOLY 9 $TDCREATE 40 -2461305.9168518516 +2461305.9345486113 9 $TDUCREATE 40 @@ -497,7 +497,7 @@ $TDUCREATE 9 $TDUPDATE 40 -2461305.9168518516 +2461305.9345486113 9 $TDUUPDATE 40 @@ -929,11 +929,11 @@ $PSTYLEMODE 9 $FINGERPRINTGUID 2 -{C86D7286-E19E-4B12-95FA-2422D8FA8D21} +{C3A95304-315D-4482-8B26-4A154C8AC81B} 9 $VERSIONGUID 2 -{6AD177C1-789E-403D-83B3-B9A1E5C62637} +{020A53F4-F6E6-4A10-988E-58908DDBD5B7} 9 $EXTNAMES 290 @@ -2157,13 +2157,13 @@ Standard 40 1.0 41 -2.5 +3.0 42 -0.625 +1.5 43 3.75 44 -1.25 +1.5 45 0.0 46 @@ -2175,7 +2175,7 @@ Standard 49 2.5 140 -2.5 +3.0 141 2.5 142 @@ -2189,7 +2189,7 @@ Standard 146 1.0 147 -0.625 +1.0 148 0.0 69 @@ -2201,15 +2201,15 @@ Standard 72 0 73 -0 +1 74 -0 +1 75 0 76 0 77 -1 +0 78 8 79 @@ -3425,7 +3425,7 @@ DictionaryVariables 280 0 1 -1.4.4 @ 2026-09-21T19:00:16.253966+00:00 +1.4.4 @ 2026-09-21T19:25:45.807766+00:00 0 DICTIONARYVAR 5 @@ -3437,7 +3437,7 @@ DictionaryVariables 280 0 1 -1.4.4 @ 2026-09-21T19:00:16.261077+00:00 +1.4.4 @ 2026-09-21T19:25:45.814791+00:00 0 ENDSEC 0 diff --git a/templates/ansi_d_arch.dxf b/templates/ansi_d_arch.dxf index 1313fa4..2a5042d 100644 --- a/templates/ansi_d_arch.dxf +++ b/templates/ansi_d_arch.dxf @@ -489,7 +489,7 @@ $SKPOLY 9 $TDCREATE 40 -2461305.9168518516 +2461305.9345486113 9 $TDUCREATE 40 @@ -497,7 +497,7 @@ $TDUCREATE 9 $TDUPDATE 40 -2461305.9168518516 +2461305.9345486113 9 $TDUUPDATE 40 @@ -929,11 +929,11 @@ $PSTYLEMODE 9 $FINGERPRINTGUID 2 -{5DBCBFFD-85BB-4756-97CD-DF14393BA48D} +{EA8DAB3B-221B-4973-A128-0F97D36F69C3} 9 $VERSIONGUID 2 -{8BCC5654-8976-41B9-B27D-2DE6DFEDDA75} +{41F085E9-E128-43F7-931E-FEE570A4F621} 9 $EXTNAMES 290 @@ -2157,13 +2157,13 @@ Standard 40 1.0 41 -2.5 +3.0 42 -0.625 +1.5 43 3.75 44 -1.25 +1.5 45 0.0 46 @@ -2175,7 +2175,7 @@ Standard 49 2.5 140 -2.5 +3.0 141 2.5 142 @@ -2189,7 +2189,7 @@ Standard 146 1.0 147 -0.625 +1.0 148 0.0 69 @@ -2201,15 +2201,15 @@ Standard 72 0 73 -0 +1 74 -0 +1 75 0 76 0 77 -1 +0 78 8 79 @@ -3425,7 +3425,7 @@ DictionaryVariables 280 0 1 -1.4.4 @ 2026-09-21T19:00:16.265081+00:00 +1.4.4 @ 2026-09-21T19:25:45.818106+00:00 0 DICTIONARYVAR 5 @@ -3437,7 +3437,7 @@ DictionaryVariables 280 0 1 -1.4.4 @ 2026-09-21T19:00:16.272301+00:00 +1.4.4 @ 2026-09-21T19:25:45.824109+00:00 0 ENDSEC 0 diff --git a/templates/iso_a1_arch.dxf b/templates/iso_a1_arch.dxf index 8904c76..e43499c 100644 --- a/templates/iso_a1_arch.dxf +++ b/templates/iso_a1_arch.dxf @@ -489,7 +489,7 @@ $SKPOLY 9 $TDCREATE 40 -2461305.9168518516 +2461305.9345486113 9 $TDUCREATE 40 @@ -497,7 +497,7 @@ $TDUCREATE 9 $TDUPDATE 40 -2461305.9168518516 +2461305.9345486113 9 $TDUUPDATE 40 @@ -929,11 +929,11 @@ $PSTYLEMODE 9 $FINGERPRINTGUID 2 -{06CA53E2-CDAA-439F-9CAD-D12BED7E531C} +{569A5E5D-D8AE-4EE2-8CC8-222F1BC5A1B6} 9 $VERSIONGUID 2 -{DFE1833D-4A98-418F-8215-C1956203953D} +{CA16A478-EDAC-4921-A9AD-1AD1114D5AB2} 9 $EXTNAMES 290 @@ -3425,7 +3425,7 @@ DictionaryVariables 280 0 1 -1.4.4 @ 2026-09-21T19:00:16.220288+00:00 +1.4.4 @ 2026-09-21T19:25:45.777979+00:00 0 DICTIONARYVAR 5 @@ -3437,7 +3437,7 @@ DictionaryVariables 280 0 1 -1.4.4 @ 2026-09-21T19:00:16.228248+00:00 +1.4.4 @ 2026-09-21T19:25:45.783975+00:00 0 ENDSEC 0 diff --git a/templates/iso_a3_mech.dxf b/templates/iso_a3_mech.dxf index d2b5f9a..d0332f7 100644 --- a/templates/iso_a3_mech.dxf +++ b/templates/iso_a3_mech.dxf @@ -489,7 +489,7 @@ $SKPOLY 9 $TDCREATE 40 -2461305.9168518516 +2461305.9345486113 9 $TDUCREATE 40 @@ -497,7 +497,7 @@ $TDUCREATE 9 $TDUPDATE 40 -2461305.9168518516 +2461305.9345486113 9 $TDUUPDATE 40 @@ -929,11 +929,11 @@ $PSTYLEMODE 9 $FINGERPRINTGUID 2 -{7664E9B7-DDAC-4E40-A6C4-4BA2274F50FC} +{6791163C-4461-4A1B-8F3F-5AD608DBCCCE} 9 $VERSIONGUID 2 -{AA05654D-61E8-4264-879F-0A0EB2E7F4EE} +{9A1CD7BF-015F-4598-B3DB-2BDBFBCC12FA} 9 $EXTNAMES 290 @@ -4333,7 +4333,7 @@ DictionaryVariables 280 0 1 -1.4.4 @ 2026-09-21T19:00:16.186887+00:00 +1.4.4 @ 2026-09-21T19:25:45.743145+00:00 0 DICTIONARYVAR 5 @@ -4345,7 +4345,7 @@ DictionaryVariables 280 0 1 -1.4.4 @ 2026-09-21T19:00:16.215553+00:00 +1.4.4 @ 2026-09-21T19:25:45.772937+00:00 0 ENDSEC 0 diff --git a/templates/iso_a3_pid.dxf b/templates/iso_a3_pid.dxf index 0518178..1bed4fb 100644 --- a/templates/iso_a3_pid.dxf +++ b/templates/iso_a3_pid.dxf @@ -489,7 +489,7 @@ $SKPOLY 9 $TDCREATE 40 -2461305.9168518516 +2461305.9345486113 9 $TDUCREATE 40 @@ -497,7 +497,7 @@ $TDUCREATE 9 $TDUPDATE 40 -2461305.9168518516 +2461305.9345486113 9 $TDUUPDATE 40 @@ -929,11 +929,11 @@ $PSTYLEMODE 9 $FINGERPRINTGUID 2 -{A7BD53ED-11FA-44DC-94CD-4208EB90B212} +{B5F8B467-F9F9-4DC6-8A95-A266D8246CA8} 9 $VERSIONGUID 2 -{E08D8D46-7798-4863-A73A-C6527F804C81} +{F759C60A-BBAF-4155-A187-77BF78127E81} 9 $EXTNAMES 290 @@ -4501,7 +4501,7 @@ DictionaryVariables 280 0 1 -1.4.4 @ 2026-09-21T19:00:16.231308+00:00 +1.4.4 @ 2026-09-21T19:25:45.787707+00:00 0 DICTIONARYVAR 5 @@ -4513,7 +4513,7 @@ DictionaryVariables 280 0 1 -1.4.4 @ 2026-09-21T19:00:16.248961+00:00 +1.4.4 @ 2026-09-21T19:25:45.803766+00:00 0 ENDSEC 0 diff --git a/tests/test_templates.py b/tests/test_templates.py index fa49c77..7a98b29 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -3,6 +3,7 @@ from __future__ import annotations import importlib.util +import types from pathlib import Path import pytest @@ -157,6 +158,53 @@ async def test_drawing_new_from_the_pid_and_ansi_templates(backend): assert (await backend.drawing_settings())["settings"]["decimal_separator"] == "." +#: Spec §4.1, the eight variables `_with_header_dimvars` does not fold — the +#: ones a header-only write silently loses on the headless renderer. +_PLACEMENT = ("dimtad", "dimtih", "dimtoh", "dimexo", "dimexe", "dimgap", "dimlunit", "dimlwd") + + +async def _effective_dimvars(backend) -> dict: + """What the next dimension is really rendered with, read off the entity.""" + from ezdxf.entities import DimStyleOverride + + info = await backend.dimension_linear(0, 0, 100, 0, 50, 20) + override = DimStyleOverride(backend._doc.entitydb[info.handle]) + return {name: override.get(name) for name in _PLACEMENT + ("dimtxt", "dimasz")} + + +async def test_templates_dimension_with_their_own_preset_headlessly(backend): + """The build's fallback lands on the ``Standard`` dimstyle, not only the header. + + Measured before the fix: ``ansi_b_mech`` reported DIMTAD 0 / DIMTIH 1 from + ``system_get_variable`` while the dimension it drew used dimtad=1 / dimtih=0 + (the ISO values on ``Standard``), so the same template dimensioned + differently per engine. The header and the style must agree. + """ + await server.drawing_new(template="ansi_b_mech", ctx=_Ctx(backend)) + ansi = await _effective_dimvars(backend) + assert ansi == { + "dimtad": 0, + "dimtih": 1, + "dimtoh": 1, + "dimexo": 1.5, + "dimexe": 1.5, + "dimgap": 1.0, + "dimlunit": 2, + "dimlwd": -2, + "dimtxt": 3.0, + "dimasz": 3.0, + } + for name, value in ansi.items(): + header = await backend.system_get_variable(name.upper()) + assert float(header) == float(value), f"{name}: header {header} != style {value}" + + await server.drawing_new(template="iso_a3_mech", ctx=_Ctx(backend)) + iso = await _effective_dimvars(backend) + assert (iso["dimtad"], iso["dimtih"], iso["dimtoh"]) == (1, 0, 0) + assert (iso["dimexo"], iso["dimexe"], iso["dimgap"]) == (0.625, 1.25, 0.625) + assert (iso["dimtxt"], iso["dimasz"]) == (2.5, 2.5) + + async def test_drawing_new_unknown_template_is_refused_with_the_names(backend): await backend.entity_create_line(0, 0, 1, 1) before = (await backend.drawing_info()).entity_count @@ -185,3 +233,141 @@ async def test_drawing_template_list_tool(backend): assert result["ok"] and result["count"] == 5 assert [row["name"] for row in result["templates"]] == list(NAMES) assert result["engine"] == "ezdxf" + + +# ── COM: only a real .dwt reaches Documents.Add ───────────────────────────── + + +class _FakeDocuments: + """Records the ActiveX calls; ``Add`` on a non-``.dwt`` answers blank, as + AutoCAD 2026 does (measured: ``Add()`` == ``Add()`` == ``Add()``).""" + + def __init__(self, dwt_bytes: bytes = b"AC1032\x00template"): + self.calls: list[tuple] = [] + self.dwt_bytes = dwt_bytes + self.open_docs: list[_FakeDoc] = [] + + def Add(self, template=None): + self.calls.append(("Add", template)) + real = template is not None and str(template).lower().endswith(".dwt") + real = real and Path(template).is_file() and Path(template).read_bytes()[:6] == b"AC1032" + return _FakeDoc(self, "Drawing1.dwg", layers=["0", "GEOMETRY"] if real else ["0"]) + + def Open(self, path, read_only=False, password=None): + self.calls.append(("Open", path, read_only)) + doc = _FakeDoc(self, Path(path).name, layers=["0", "GEOMETRY"]) + self.open_docs.append(doc) + return doc + + +class _FakeDoc: + def __init__(self, documents, name, layers): + self._documents = documents + self.Name = name + self.layers = layers + self.closed = None + + def SaveAs(self, path, fmt=None): + self._documents.calls.append(("SaveAs", path, fmt)) + if fmt != 66: + raise RuntimeError(f"fake AutoCAD 2026 refuses SaveAs format {fmt}") + Path(path).write_bytes(self._documents.dwt_bytes) + + def Close(self, save=True): + self._documents.calls.append(("Close", self.Name, save)) + self.closed = save + self._documents.open_docs.remove(self) + + +@pytest.fixture +def com_new(monkeypatch, tmp_path): + pytest.importorskip("win32com.client", reason="pywin32 not installed") + from backends import com_backend as module + + documents = _FakeDocuments() + app = types.SimpleNamespace(Documents=documents) + monkeypatch.setattr(module, "_acad_app", lambda: app) + monkeypatch.setattr(module, "_template_cache_dir", lambda: tmp_path / "cache") + monkeypatch.setattr(module, "_COM_ERROR", (RuntimeError,)) + backend = module.ComBackend() + + async def _run_inline(func, *args, **kwargs): + return func(*args, **kwargs) + + async def _no_state(): + pass + + monkeypatch.setattr(backend, "_run", _run_inline) + monkeypatch.setattr(backend, "_ensure_document_state", _no_state) + return backend, documents + + +async def test_com_drawing_new_converts_a_dxf_into_a_real_dwt_once(com_new, tmp_path): + backend, documents = com_new + dxf = TEMPLATES_DIR / "iso_a3_pid.dxf" + result = await backend.drawing_new(str(dxf)) + assert result["ok"] and result["name"] == "Drawing1.dwg" + dwt = Path(result["template_dwt"]["path"]) + assert result["template_dwt"]["cached"] is False + assert dwt.suffix == ".dwt" and dwt.is_file() and dwt.parent == tmp_path / "cache" + assert documents.calls == [ + ("Open", str(dxf), True), + ("SaveAs", str(dwt), 66), + ("Close", "iso_a3_pid.dxf", False), + ("Add", str(dwt)), + ], "open read-only, save as ac2018_Template, close without saving, then Add the .dwt" + assert documents.open_docs == [], "the conversion document never stays open" + + documents.calls.clear() + again = await backend.drawing_new(str(dxf)) + assert again["template_dwt"] == {"path": str(dwt), "cached": True} + assert documents.calls == [("Add", str(dwt))], "the second call reuses the cached .dwt" + + +async def test_com_drawing_new_hands_a_dwt_straight_to_add_and_refuses_a_missing_file( + com_new, tmp_path +): + backend, documents = com_new + mine = tmp_path / "mine.dwt" + mine.write_bytes(documents.dwt_bytes) + result = await backend.drawing_new(str(mine)) + assert result == {"ok": True, "name": "Drawing1.dwg"} + assert documents.calls == [("Add", str(mine))] + + documents.calls.clear() + with pytest.raises(FileNotFoundError, match="not found"): + await backend.drawing_new(str(tmp_path / "gone.dwt")) + assert documents.calls == [], "Documents.Add() would silently create the default" + + +async def test_com_drawing_new_falls_back_to_an_older_template_format(com_new, monkeypatch): + backend, documents = com_new + accepted = [] + + def _save_as(self, path, fmt=None): + documents.calls.append(("SaveAs", path, fmt)) + if fmt == 66: + raise RuntimeError("older seat: unknown SaveAs type") + accepted.append(fmt) + Path(path).write_bytes(documents.dwt_bytes) + + monkeypatch.setattr(_FakeDoc, "SaveAs", _save_as) + result = await backend.drawing_new(str(TEMPLATES_DIR / "ansi_b_mech.dxf")) + assert result["ok"] and accepted == [62] + assert documents.open_docs == [] + + +async def test_com_drawing_new_tool_reports_the_converted_dwt(com_new): + backend, documents = com_new + result = await server.drawing_new(template="iso_a3_pid", bootstrap=False, ctx=_Ctx(backend)) + assert result["ok"] is True + assert "template_dwt" not in result + template = result["template"] + assert template["name"] == "iso_a3_pid" + assert template["source"] in ("bundled", "bundled_dxf") + if template["source"] == "bundled_dxf": # no .dwt twin committed: the live engine built one + assert template["path"] == str(TEMPLATES_DIR / "iso_a3_pid.dxf") + assert template["dwt"].endswith(".dwt") and template["dwt_cached"] is False + assert documents.calls[-1] == ("Add", template["dwt"]) + else: + assert "dwt" not in template and documents.calls == [("Add", template["path"])]