fix(task-21): the applied ISO layer set is per document — it follows document_activate on ezdxf and is cleared on every document switch on both engines

`drawing_apply_iso_layers` recorded its standard in `_active_layer_set` on
the backend, and `_role_layer` reads it ahead of the PlanSpec to decide where
`dimension_auto` / `construction_xline` file their output. It was neither a
`_DocState` field nor cleared by `_reset_document_state`, so an iso13567
sheet next to a mech one pushed the mech drawing's dimensions onto
`M-DIMEN-T-N` (created on demand), silently — and `drawing_critique` could
not see it because the same stale set drove the critique. The attribute
predates the registry (v1.5 `drawing_new` did not reset it either), but the
per-document claim of Task 21 covered it and the measured attribute list
missed it.

- `_DocState.active_layer_set` + `_ActiveField("active_layer_set")` on
  `EzdxfBackend`: the set follows the active entry, bidirectionally.
- `PremiumContract._reset_document_state` clears it (declared on the mixin
  like `_plan_spec`), which fixes COM's `_ensure_document_state` switch and
  `drawing_new` / `drawing_open` / `disconnect` on both engines.
- tests: `test_active_layer_set_is_per_document` (headless registry, the
  reviewer's repro plus the reverse direction) and the COM activate test now
  also asserts the set is cleared and `_role_layer('dim')` is back on DIM.

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-17 23:17:39 +03:00
co-authored by Claude Opus 5
parent 64cec37cc7
commit b1c93cdd2a
3 changed files with 49 additions and 0 deletions
+7
View File
@@ -36,11 +36,18 @@ class PremiumContract(ABC):
_plan_spec: PlanSpec | None = None
_preflight_result: Any = None
# The standard `drawing_apply_iso_layers` last bootstrapped on *this*
# drawing (None until it runs) — `_active_layer_set_id` reads it ahead of
# the PlanSpec. Drawing-scoped like the plan: a live backend clears it
# when AutoCAD's active document changes, the headless registry keeps one
# per open document.
_active_layer_set: LayerSetId | None = None
def _reset_document_state(self) -> None:
"""Clear metadata that is valid only for the current drawing."""
self._plan_spec = None
self._preflight_result = None
self._active_layer_set = None
self._gdt_datums_defined = set()
self._gdt_datums_referenced = set()
+6
View File
@@ -977,6 +977,11 @@ class _DocState:
transaction_stack: list[Path] = field(default_factory=list)
plan_spec: Any = None
preflight_result: Any = None
# `drawing_apply_iso_layers` records the standard here so `_role_layer`
# files dims / scaffolding on that standard's layer; it is per drawing,
# not per backend (an iso13567 sheet next to a mech one must not push
# the mech drawing's dimensions onto M-DIMEN-T-N).
active_layer_set: str | None = None
gdt_datums_defined: set = field(default_factory=set)
gdt_datums_referenced: set = field(default_factory=set)
activated_seq: int = 0
@@ -1033,6 +1038,7 @@ class EzdxfBackend(AutoCADBackend):
_transaction_stack = _ActiveField("transaction_stack")
_plan_spec = _ActiveField("plan_spec")
_preflight_result = _ActiveField("preflight_result")
_active_layer_set = _ActiveField("active_layer_set")
_gdt_datums_defined = _ActiveField("gdt_datums_defined")
_gdt_datums_referenced = _ActiveField("gdt_datums_referenced")
+36
View File
@@ -88,6 +88,39 @@ async def test_per_document_state_follows_the_active_entry(backend):
assert backend._dirty is False
async def test_active_layer_set_is_per_document(backend):
"""The standard ``drawing_apply_iso_layers`` bootstrapped is per drawing.
It drives ``_role_layer`` — where ``dimension_auto`` and
``construction_xline`` file their output — and the critique that checks
them. Held on the backend it bled across documents: an iso13567 sheet
next to a mech one pushed the mech drawing's dimensions onto
``M-DIMEN-T-N`` (created on demand), silently, and ``drawing_critique``
could not see it because the same stale set drove the critique.
"""
await backend.drawing_apply_iso_layers("iso13567")
assert backend._role_layer("dim") == "M-DIMEN-T-N"
second = (await backend.drawing_new())["document"]
assert backend._active_layer_set is None, "a fresh drawing has no explicit set"
assert backend._role_layer("dim") == "DIM"
line = await backend.entity_create_line(0, 0, 50, 0)
dims = await backend.dimension_auto([line.handle], style="chain")
assert dims[0].layer == "DIM"
assert "M-DIMEN-T-N" not in [lyr.name for lyr in await backend.layer_list()]
# Bidirectional: switching back restores the first document's set …
await backend.document_activate("untitled-1")
assert backend._active_layer_set == "iso13567"
assert backend._role_layer("construction") == "M-CONST-E-N"
# … and a set applied on B while A is open does not reach A.
await backend.document_activate(second)
await backend.drawing_apply_iso_layers("pid")
assert backend._role_layer("dim") == "DIM"
await backend.document_activate("untitled-1")
assert backend._active_layer_set == "iso13567"
async def test_opening_a_file_twice_reloads_it_instead_of_registering_twice(backend, tmp_path):
path = tmp_path / "part.dxf"
await backend.drawing_save(str(path))
@@ -495,6 +528,7 @@ async def test_com_activate_calls_activate_and_resets_document_scope(com_backend
backend, app = com_backend
await backend._ensure_document_state()
backend._plan_spec = object()
backend._active_layer_set = "iso13567"
result = await backend.document_activate("Second.dwg")
assert result == {
"ok": True,
@@ -505,6 +539,8 @@ async def test_com_activate_calls_activate_and_resets_document_scope(com_backend
}
assert app.Documents._docs[1].calls == [("Activate", ())]
assert backend._plan_spec is None, "a document switch clears drawing-scoped state"
assert backend._active_layer_set is None, "the layer set is drawing-scoped too"
assert backend._role_layer("dim") == "DIM"
with pytest.raises(ValueError, match="no open document named 'Third.dwg'"):
await backend.document_activate("Third.dwg")