Two review findings, both root causes.
1. `_sync_header_to_dimstyle` copied the DIMSTYLE entry's ezdxf-internal
arrowhead spelling (OBLIQUE) into the DXF-facing $DIMBLK/$DIMBLK1/$DIMBLK2
header, so the saved file named a block it did not hold (the blocks are
_OBLIQUE / _DOT) and `system_get_variable("DIMBLK")` answered "OBLIQUE"
headlessly against "_OBLIQUE" on COM and in the same engine's own
`dimstyle_list` row. The header write now goes through
`reported_arrowhead` — the same read-side rule `_dimstyle_value` applies.
Pinned by header, `system_get_variable`, save/reopen (`ezdxf.readfile`
header equals the block the file holds) and the Restore-to-Standard
empty-string case in `test_builtin_arrowheads_render_save_and_report_one_spelling`.
2. The `mleaderstyle` capability refusal rested on a false engine boundary.
acax25enu.tlb (AutoCAD 2026) exposes IAcadMLeaderStyle with Name,
ArrowSize, LandingGap, TextHeight, TextStyle, ... — exactly what
`Dictionaries.Item("ACAD_MLEADERSTYLE").Item(i)` returns — and
IAcadDictionary.AddObject(keyword, "AcDbMLeaderStyle") creates one.
COM `mleaderstyle_list` now reads the four values off those objects
(`values_available: True`); COM `mleaderstyle_create` validates every value
(`validate_mleaderstyle`, shared with ezdxf), checks the clash and the text
style before AddObject, then writes the four properties. The contract
method is `@abstractmethod` again, the key is gone from both capability
maps, the fake ActiveX surface models the real call shape, and the spec
(§4.2, §8.1, §9) states the ActiveX route.
Live AutoCAD was not running on this machine (GetActiveObject: operation
unavailable; no acad.exe), so the COM route is type-library-verified and
fake-tested, not executed live — scripts/smoke_settings_com.py (Task 24)
owns that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
86 lines
3.8 KiB
Python
86 lines
3.8 KiB
Python
"""Multileader style presets: arrow, landing gap, text style and height.
|
|
|
|
Sized to match the dimension presets they sit beside (ISO-25: 2.5 mm arrows
|
|
and lettering; ANSI: 3 mm), so a leader note and a dimension on the same
|
|
sheet read at one size. Both engines author the style: ezdxf on
|
|
``doc.mleader_styles``, ActiveX through the ``ACAD_MLEADERSTYLE`` dictionary's
|
|
``AddObject(name, "AcDbMLeaderStyle")`` and the ``IAcadMLeaderStyle``
|
|
properties (see ``backends/contracts/styles.py``).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from typing import Any
|
|
|
|
from engineering.standards.names import check_name
|
|
|
|
__all__ = ["MLEADER_KEYS", "MLEADER_PRESETS", "resolve_mleaderstyle", "validate_mleaderstyle"]
|
|
|
|
MLEADER_PRESETS: dict[str, dict[str, Any]] = {
|
|
"iso": {"arrow_size": 2.5, "landing_gap": 1.0, "text_style": "ISOCP", "text_height": 2.5},
|
|
"ansi": {"arrow_size": 3.0, "landing_gap": 1.5, "text_style": "ROMANS", "text_height": 3.0},
|
|
}
|
|
|
|
MLEADER_KEYS: tuple[str, ...] = ("arrow_size", "landing_gap", "text_style", "text_height")
|
|
|
|
|
|
def _positive(key: str, value: Any, *, allow_zero: bool = False) -> float:
|
|
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
raise TypeError(f"{key}: expected a number, got {type(value).__name__}")
|
|
number = float(value)
|
|
if not math.isfinite(number):
|
|
raise ValueError(f"{key}: {value!r} is not a finite number")
|
|
if number < 0.0 or (number == 0.0 and not allow_zero):
|
|
raise ValueError(f"{key}: {value!r} must be greater than 0")
|
|
return number
|
|
|
|
|
|
def resolve_mleaderstyle(preset: str, overrides: dict | None = None) -> dict[str, Any]:
|
|
"""The preset's four values with ``overrides`` applied; refusals name the key."""
|
|
key = str(preset).strip().lower() if preset is not None else ""
|
|
if key not in MLEADER_PRESETS:
|
|
raise ValueError(f"preset: unknown value {preset!r}; use one of {sorted(MLEADER_PRESETS)}")
|
|
values = dict(MLEADER_PRESETS[key])
|
|
if overrides is None:
|
|
return values
|
|
if not isinstance(overrides, dict):
|
|
raise TypeError(f"overrides: expected a mapping, got {type(overrides).__name__}")
|
|
for raw_key, value in overrides.items():
|
|
name = str(raw_key).strip().lower()
|
|
if name not in MLEADER_KEYS:
|
|
raise ValueError(
|
|
f"overrides: {raw_key!r} is not a leader style key; allowed: {list(MLEADER_KEYS)}"
|
|
)
|
|
if name == "text_style":
|
|
values[name] = check_name(name, value, what="text style name")
|
|
elif name == "landing_gap":
|
|
values[name] = _positive(name, value, allow_zero=True)
|
|
else:
|
|
values[name] = _positive(name, value)
|
|
return values
|
|
|
|
|
|
def validate_mleaderstyle(values: Any) -> dict[str, Any]:
|
|
"""The four ``MLEADER_KEYS`` of ``values``, typed, or a refusal naming the key.
|
|
|
|
What both engines' ``mleaderstyle_create`` run before touching a table:
|
|
a missing key is ``ValueError`` (``resolve a preset first``), and each
|
|
value is checked exactly as :func:`resolve_mleaderstyle` checks an
|
|
override — so a hand-built dict is held to the preset's rules too.
|
|
Unknown keys are ignored: the row shape is the contract, not the input.
|
|
"""
|
|
if not isinstance(values, dict):
|
|
raise TypeError(f"values: expected a mapping, got {type(values).__name__}")
|
|
missing = [key for key in MLEADER_KEYS if key not in values]
|
|
if missing:
|
|
raise ValueError(
|
|
f"mleaderstyle_create: values is missing {missing}; resolve a preset first"
|
|
)
|
|
return {
|
|
"arrow_size": _positive("arrow_size", values["arrow_size"]),
|
|
"landing_gap": _positive("landing_gap", values["landing_gap"], allow_zero=True),
|
|
"text_style": check_name("text_style", values["text_style"], what="text style name"),
|
|
"text_height": _positive("text_height", values["text_height"]),
|
|
}
|