AcadApplication has no GetVariable/SetVariable members; they live on
AcadDocument. Measured on the running AutoCAD 2026 (25.1s, 2026-09-22,
scratch Documents.Add(), Close(False)):
hasattr(app, 'GetVariable') -> False
hasattr(app, 'SetVariable') -> False
app.GetVariable('INSUNITS') -> AttributeError: ... IAcadApplication
instance ... has no attribute 'GetVariable'
doc.GetVariable('INSUNITS') -> 4
doc.GetVariable('MEASUREMENT') -> 1
doc.GetVariable('FILEDIA') -> 1; doc.SetVariable('FILEDIA', 0) -> None,
reads back 0, restored to 1
doc.GetVariable('AUDITCTL') -> 0, 'CMDACTIVE' -> 0, 'USERS1' -> ''
Fifteen call sites in backends/com_backend.py still went through the
Application inside `try/except ... log.debug`, so on a live seat they
silently degraded: MEASUREMENT defaulted every seat to metric,
drawing_info reported units from a failed INSUNITS read, drawing_audit
never set AUDITCTL, the CMDACTIVE guards of system_run_command /
system_run_lisp never fired and run_lisp never read USERS1 back. All are
rerouted through `_acad_doc()`; the try/except stays only where an
optional read is defaulted (INSUNITS -> "Unknown", AUDITCTL absent on a
host that lacks it, USERS1 after the expression ran) and the log line
says so.
Linetype loading, measured the same way: `SendCommand("_-LINETYPE _LOAD
CENTER acadiso.lin\n\n")` loads nothing with FILEDIA=0 and leaves the
command at `Enter an option [?/Create/Load/Set]:` (LASTPROMPT), which
makes the whole seat answer RPC_E_CALL_REJECTED to every client until
ESC is pressed; with FILEDIA=1 it opens the modal "Select Linetype File"
dialog. `doc.Linetypes.Load("CENTER", "acadiso.lin")` returns None and
the table grows 3 -> 4; a repeat raises 'Duplicate record name', an
unknown name 'Undefined linetype', a missing file 'File system error'.
`_ensure_linetype_loaded` and `linetype_load` now call `Linetypes.Load`
(MEASUREMENT still read on the document to pick the .lin), touch no
FILEDIA, and report a failure with the English excepinfo description.
`apply_standard` reads every variable it will write before the first
style is created, so a read the engine refuses is a refusal with nothing
written (it used to fail after ISO-25 / ISOCP were created and current).
Fakes: every application fake refuses GetVariable/SetVariable
(tests/test_styles.py's COM fixture carried them on the Application,
enshrining the wrong shape); the members exist on document fakes only.
Regression tests per rerouted site: linetype load via Linetypes.Load with
MEASUREMENT on the document, drawing_info units from the document,
AUDITCTL set/restored on the document, CMDACTIVE/USERS1 on the document,
apply_standard refusing before any write.
Live end-to-end with the real ComBackend on a scratch document:
apply_standard('iso') -> INSUNITS 4, ISO-25 (already in acadiso.dwt,
created: false) and ISOCP (created: true) current, second call created:
false / changed: {}; linetype_load('CENTER') -> Linetypes 3 -> 4, FILEDIA
stays 1, repeat already_loaded, 'NOSUCHLT' refused by name;
_ensure_linetype_loaded('HIDDEN') -> 3 -> 4; drawing_info units 'mm';
drawing_audit AUDITCTL 0 -> 0; system_run_command ok;
system_run_lisp('(+ 40 2)') -> '42'.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
125 lines
4.9 KiB
Python
125 lines
4.9 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.)
|
|
"""
|
|
key, row = resolve_standard(standard)
|
|
if not isinstance(layers, bool) or not isinstance(units, bool):
|
|
raise TypeError("layers and units must be booleans")
|
|
|
|
current: dict[str, Any] = {}
|
|
if units:
|
|
for var, _value in STANDARD_SYSVARS:
|
|
try:
|
|
current[var] = 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:
|
|
old = current[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,
|
|
}
|