Files
autocad-mcp-pro/engineering/standards/apply.py
T
Umut CANandClaude Opus 5 d072df9fd4 fix(task-24a): apply_standard decides each unit write on a read taken after the dimension style switch
f7528b2 moved the five unit reads of apply_standard ahead of the style
stage so a refused read is a refusal with nothing written -- and then
decided the writes on those reads. Making a dimension style current
restores every DIM* variable from the style (ezdxf: _sync_header_to_dimstyle
on dimstyle_set_current; AutoCAD: ActiveDimStyle), so on a drawing whose
ISO-25/ANSI already carried another scale the gate read (1.0) equalled
the target, the DIMSCALE write was skipped and the drawing ended at the
style's scale while the report said `changed: {}` -- against the spec's
"LTSCALE 1, DIMSCALE 1" and the tool docstring. The pre-f7528b2 loop ran
after the switch and got this right; this is a regression of f7528b2.

The gate read stays (readable before anything is written); each variable
is read again after the style stage and the write is decided on that
value, so `changed` says what the write moved.

Measured on the running AutoCAD 2026 (25.1s, 2026-09-22, scratch
Documents.Add(), Close(False), real ComBackend after connect()):

    ISO-25 modified to DIMSCALE 2.0 (dimstyle_modify -> {'DIMSCALE': [1.0, 2.0]}),
    Annotative current, doc.GetVariable('DIMSCALE') -> 1.0
    before: apply_standard(iso, layers=False)
            -> {'changed': {}, 'applied': True}; ActiveDimStyle ISO-25;
               doc.GetVariable('DIMSCALE') -> 2.0
    after:  -> {'changed': {'DIMSCALE': [2.0, 1.0]}, 'applied': True};
               ActiveDimStyle ISO-25; doc.GetVariable('DIMSCALE') -> 1.0

ezdxf, same setup (dimstyle_create ISO-25 {DIMSCALE: 2.0}, Standard
current): before `{'INSUNITS': [6, 4]}` and DIMSCALE 2.0 afterwards;
after `{'INSUNITS': [6, 4], 'DIMSCALE': [2.0, 1.0]}` and DIMSCALE 1.0.

Tests: one regression per engine (both fail on the f7528b2 apply.py);
test_com_apply_standard_creates_both_styles_and_sets_units now pins the
gate reads first *and* the re-read-then-write sequence after CopyFrom,
instead of enshrining the stale-read order.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-22 12:37:33 +03:00

133 lines
5.7 KiB
Python

"""One call to put a drawing on a drafting standard: styles, units, layers.
Composes only backend methods that already exist (``textstyle_*``,
``dimstyle_*``, ``system_get/set_variable``, ``drawing_apply_iso_layers``),
so it runs on both engines and needs nothing from the settings facade. Units
are the five system variables both standards share; the decimal separator is
the preset's and arrives through the dimension style. ANSI layer naming is
company-specific, so both standards use the ``mech`` layer set — only styles
and the separator differ (spec §4.2).
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from engineering.standards.dimstyles import resolve_dimstyle
from engineering.standards.textstyles import TEXT_PRESETS
if TYPE_CHECKING:
from backends.base import AutoCADBackend
__all__ = ["STANDARDS", "STANDARD_SYSVARS", "apply_standard", "resolve_standard"]
STANDARDS: dict[str, dict[str, str]] = {
"iso": {"preset": "iso-25", "dimstyle": "ISO-25", "textstyle": "ISOCP", "layer_set": "mech"},
"ansi": {"preset": "ansi", "dimstyle": "ANSI", "textstyle": "ROMANS", "layer_set": "mech"},
}
#: Millimetres, decimal linear and angular units, 1:1 linetype and dimension
#: scale. Written by name so the report can say what moved.
STANDARD_SYSVARS: tuple[tuple[str, Any], ...] = (
("INSUNITS", 4),
("LUNITS", 2),
("AUNITS", 0),
("LTSCALE", 1.0),
("DIMSCALE", 1.0),
)
def resolve_standard(standard: str) -> tuple[str, dict[str, str]]:
"""``(key, row)`` for ``iso`` / ``ansi`` (any case), else ``ValueError``."""
key = str(standard).strip().lower() if standard is not None else ""
if key not in STANDARDS:
raise ValueError(f"standard: unknown value {standard!r}; use one of {sorted(STANDARDS)}")
return key, STANDARDS[key]
def _same(old: Any, new: Any) -> bool:
if old is None:
return False
try:
return abs(float(old) - float(new)) < 1e-9
except (TypeError, ValueError):
return str(old) == str(new)
async def apply_standard(
backend: AutoCADBackend, standard: str, layers: bool = True, units: bool = True
) -> dict:
"""Styles created (or reused) and made current, units written, layers bootstrapped.
Every item reports whether it was created or already present; ``settings``
reports only the variables that moved. An existing style of the standard's
name is used as it is — ``dimstyle_modify`` changes one on purpose.
Every system variable the call will write is read *before* the first
style is created, so a read the engine refuses is a refusal with nothing
written. (The units loop used to run after both styles; on the live
engine its INSUNITS read went through a member the ActiveX Application
does not have, so the call failed with ISO-25 and ISOCP already created
and current.) That first read is only the gate: making a dimension style
current restores every DIM* variable from the style (ezdxf syncs the
header; AutoCAD 2026 measured ``ActiveDimStyle = ISO-25`` with a style
DIMSCALE of 2.0 moving the document's DIMSCALE 1.0 -> 2.0), so each
variable is read again *after* the style switch and the write is decided
on that value. Deciding on the gate read skipped the DIMSCALE write
whenever a pre-existing ISO-25/ANSI carried another scale, and reported
``changed: {}`` on a drawing left at the style's scale.
"""
key, row = resolve_standard(standard)
if not isinstance(layers, bool) or not isinstance(units, bool):
raise TypeError("layers and units must be booleans")
if units: # the refusal gate: every variable readable before anything is written
for var, _value in STANDARD_SYSVARS:
try:
await backend.system_get_variable(var)
except Exception as exc:
raise RuntimeError(
f"apply_standard: could not read {var} before writing "
f"({exc}); nothing was changed"
) from exc
text_names = {entry["name"].lower() for entry in await backend.textstyle_list()}
textstyle_created = row["textstyle"].lower() not in text_names
if textstyle_created:
font_file, width, oblique = TEXT_PRESETS[row["textstyle"]]
await backend.textstyle_create(row["textstyle"], font_file, 0.0, width, oblique, False)
await backend.textstyle_set_current(row["textstyle"])
dim_names = {entry["name"].lower() for entry in await backend.dimstyle_list()}
dimstyle_created = row["dimstyle"].lower() not in dim_names
if dimstyle_created:
await backend.dimstyle_create(
row["dimstyle"], resolve_dimstyle(row["preset"], None), set_current=True
)
else:
await backend.dimstyle_set_current(row["dimstyle"])
changed: dict[str, list] = {}
if units:
for var, value in STANDARD_SYSVARS:
# Re-read after the style switch: the gate value is stale for
# DIM* once another dimension style has been made current.
old = await backend.system_get_variable(var)
if _same(old, value):
continue
await backend.system_set_variable(var, value)
changed[var] = [old, value]
layer_result = None
if layers:
applied = await backend.drawing_apply_iso_layers(row["layer_set"])
layer_result = {"layer_set": row["layer_set"], "layers": applied["layers"]}
return {
"standard": key,
"dimstyle": {"name": row["dimstyle"], "created": dimstyle_created, "current": True},
"textstyle": {"name": row["textstyle"], "created": textstyle_created, "current": True},
"settings": {"changed": changed, "applied": units},
"layers": layer_result,
}