fix(task-19): a .dxf template really reaches the live seat; the fallback dimvars land on Standard
Documents.Add(<file>) accepts only a genuine DWT. Measured on AutoCAD 2026: Add(templates/iso_a3_pid.dxf) returned the default acadiso drawing (layers ['0'], Layout1/Layout2) exactly as Add() and Add(<missing.dwt>) did, so drawing_new(template=<bundled name>) reported ok:true with source 'bundled_dxf' over a blank document, and the explicit .dxf path route was just as blank. ComBackend.drawing_new now converts any non-.dwt template through AutoCAD itself (Documents.Open read-only -> SaveAs ac2018_Template, older constants tried next -> Close(False)), caches the .dwt in the temp folder keyed on path/size/mtime, hands Add the .dwt, and refuses a missing template file instead of letting Add default silently. The tool result carries template.dwt / template.dwt_cached. Re-measured end to end through server.drawing_new on the real ComBackend: ansi_b_mech -> 11 layers, tab B, DIMTAD 0 / DIMTIH 1 / DIMEXO 1.5; iso_a3_pid -> 17 layers, tab A3, second call reuses the cached .dwt (0.8 s conversion once). Fake-ActiveX tests pin the call shape and that the conversion document never stays open. Headlessly the build's fallback wrote spec 4.1 only as header DIM variables, and the renderer folds six of them, so DIMTAD/TIH/TOH/EXE/EXO/GAP/LUNIT/LWD were a no-op: ansi_b_mech dimensioned with ISO text placement while system_get_variable and AutoCAD said ANSI. The fallback now also stores each value on the Standard dimstyle; the five DXFs are rebuilt (--check passes) and a test reads the effective values off a DIMENSION drawn from each template and asserts header and style agree. 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
a76d9e08a3
commit
17f7228fd6
+92
-4
@@ -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(<file>)`` 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(<nonexistent.dwt>)`` 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(<dwt>)`` 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()
|
||||
|
||||
@@ -158,10 +158,13 @@ def resolve_template(name_or_path: str, engine: str) -> tuple[str, str]:
|
||||
|
||||
A catalogue name resolves to ``templates/<name>.dxf`` (headless) or
|
||||
``templates/<name>.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:
|
||||
|
||||
@@ -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)",
|
||||
}
|
||||
|
||||
|
||||
@@ -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(<file.dxf>)` 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:
|
||||
|
||||
+14
-14
@@ -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
|
||||
|
||||
+14
-14
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(<dxf>)`` == ``Add()`` == ``Add(<missing.dwt>)``)."""
|
||||
|
||||
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(<missing>) 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"])]
|
||||
|
||||
Reference in New Issue
Block a user