From f3ff70314452d253bf8d25cbf82257d8da4ce593 Mon Sep 17 00:00:00 2001 From: Umut CAN Date: Thu, 17 Sep 2026 10:14:05 +0300 Subject: [PATCH] fix(task-14): refuse CANNOSCALE on an R12 file headlessly instead of losing it at save Measured: an R12 (AC1009) document has no OBJECTS section, so ezdxf exports neither AcDbVariableDictionary/DICTIONARYVAR nor the seeded SCALE objects, and drawing_save_as keeps AC1009. `_set_annotation_scale` reported `ok: True, changed 1:1 -> 1:50` and the file reloaded as 1:1 -- the header-only write that vanishes on reload, the class this task exists to remove, and a breach of the backend's own R12 honesty convention (viewport_set_scale / lock). The write is now refused with a ValueError naming R12 and "save as R2000 or newer", after parse_scale (a malformed value is still named as such) and before either dictionary is touched, so a refused call leaves the root dictionary as it found it and drawing_settings still applies the sibling keys (limits / grid / ortho survive R12). The refusal is listed with the others in the contract and server docstrings. Pinned by two tests that save, reopen and read the R12 file back. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd --- backends/contracts/settings.py | 4 ++- backends/ezdxf_backend.py | 12 +++++++ server.py | 4 ++- tests/test_settings_facade.py | 60 ++++++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) diff --git a/backends/contracts/settings.py b/backends/contracts/settings.py index fe81457..ec2dcc4 100644 --- a/backends/contracts/settings.py +++ b/backends/contracts/settings.py @@ -42,7 +42,9 @@ class SettingsContract: Refusals (per key, in ``errors``; the other keys still apply): an unknown key; a value outside ``_SETTING_RANGES``; a malformed - ``limits`` / ``annotation_scale``; ``polar`` / ``polar_angle`` on the + ``limits`` / ``annotation_scale``; ``annotation_scale`` on an R12 file + headlessly (no OBJECTS section, so the value would vanish at save — + save as R2000 or newer first); ``polar`` / ``polar_angle`` on the headless engine (registry-saved — ``capability: registry_sysvar``); ``dimstyle`` / ``textstyle`` when the backend has no styles contract. diff --git a/backends/ezdxf_backend.py b/backends/ezdxf_backend.py index 93f5cc9..2b7148a 100644 --- a/backends/ezdxf_backend.py +++ b/backends/ezdxf_backend.py @@ -6045,6 +6045,18 @@ class EzdxfBackend(AutoCADBackend): def _set_annotation_scale(self, doc, value: Any) -> str: name, paper, drawing = parse_scale(value) # refused before any write + if self._is_r12(): + # Measured: an R12 (AC1009) file has no OBJECTS section, so ezdxf + # exports neither AcDbVariableDictionary/DICTIONARYVAR nor the + # SCALE objects. The in-memory write "succeeded" and reloaded as + # 1:1 -- the header-only-write-that-vanishes class again. Refused + # here, before either dictionary is created, so a refused call + # leaves the root dictionary as it found it. + raise ValueError( + "CANNOSCALE: R12 has no OBJECTS section, so the annotation scale " + "(AcDbVariableDictionary and ACAD_SCALELIST) would be lost on save. " + "Save as R2000 or newer first." + ) self._ensure_scale_entry(doc, name, paper, drawing) vardict = self._variable_dictionary(doc, create=True) entry = vardict.get("CANNOSCALE") diff --git a/server.py b/server.py index 30ecf63..152544d 100644 --- a/server.py +++ b/server.py @@ -5084,7 +5084,9 @@ async def drawing_settings( styles contract. On the live engine `annotation_scale` must name a scale in the drawing's scale list (SCALELISTEDIT) and AutoCAD refuses the write on a paper-space layout with no active viewport; the headless engine adds the - scale, seeding AutoCAD's default list first when the drawing has none. + scale, seeding AutoCAD's default list first when the drawing has none, + and refuses it on an R12 file (no OBJECTS section, so the value would + vanish at save — save as R2000 or newer first; limits/grid/ortho survive). Headlessly, grid/snap are stored on the active VPORT and the annotation scale in the variable dictionary — the places AutoCAD reads them from. """ diff --git a/tests/test_settings_facade.py b/tests/test_settings_facade.py index a98909e..fbb5508 100644 --- a/tests/test_settings_facade.py +++ b/tests/test_settings_facade.py @@ -387,6 +387,66 @@ async def test_cannoscalevalue_is_read_only(backend): await backend.system_set_variable("CANNOSCALEVALUE", 0.5) +async def _r12_backend(tmp_path): + import ezdxf + + from backends.ezdxf_backend import EzdxfBackend + + path = tmp_path / "r12.dxf" + ezdxf.new("R12").saveas(str(path)) + backend = EzdxfBackend() + await backend.connect() + await backend.drawing_open(str(path)) + return backend + + +async def test_annotation_scale_on_r12_is_refused_rather_than_lost(tmp_path): + """Measured: an R12 (AC1009) file has no OBJECTS section, so ezdxf exports + neither AcDbVariableDictionary nor ACAD_SCALELIST — a CANNOSCALE written to + the in-memory document reported `changed 1:1 -> 1:50` and reloaded as 1:1. + The write is refused up front, before any dictionary is created, and the + other settings on the same call still apply (limits survive R12).""" + backend = await _r12_backend(tmp_path) + try: + # ezdxf seeds an *empty* ACAD_SCALELIST at new(); the refusal must not + # populate it (the default-list seeding) nor create the variable dict. + assert len(backend._doc.rootdict.get("ACAD_SCALELIST")) == 0 + with pytest.raises(ValueError, match="R12") as excinfo: + await backend.system_set_variable("CANNOSCALE", "1:50") + assert "R2000 or newer" in str(excinfo.value) + assert backend._doc.rootdict.get("AcDbVariableDictionary") is None + assert len(backend._doc.rootdict.get("ACAD_SCALELIST")) == 0 + + res = await backend.drawing_settings( + {"annotation_scale": "1:50", "limits": [[0, 0], [420, 297]]} + ) + assert res["ok"] is False + assert "R12" in res["errors"]["annotation_scale"] + assert "annotation_scale" not in res["applied"] + assert res["applied"]["limits"] == [[0, 0], [420, 297]] + + out = tmp_path / "r12_out.dxf" + await backend.drawing_save_as(str(out)) + await backend.drawing_open(str(out)) + assert backend._doc.dxfversion == "AC1009" + read = (await backend.drawing_settings())["settings"] + assert read["annotation_scale"] == {"name": "1:1", "value": 1.0} + assert read["limits"] == [[0.0, 0.0], [420.0, 297.0]] + finally: + await backend.disconnect() + + +async def test_annotation_scale_on_r12_refuses_a_malformed_value_by_the_value(tmp_path): + """A malformed scale is the caller's mistake whatever the file version, so + it is named as such — the R12 refusal is not allowed to mask it.""" + backend = await _r12_backend(tmp_path) + try: + with pytest.raises(ValueError, match="paper:drawing"): + await backend.system_set_variable("CANNOSCALE", "fifty") + finally: + await backend.disconnect() + + def test_both_capability_maps_declare_registry_sysvar(): from backends.com_backend import ComBackend from backends.ezdxf_backend import EzdxfBackend