Files
autocad-mcp-pro/engineering/standards/mleaderstyles.py
T
Umut CANandClaude Opus 5 827719fdd0 fix(task-9): finite text-style numbers, one symbol-name rule, canonical DIMBLK arrowheads
Three review findings on the standards data layer, each a value the layer
let through that a backend would then write as a silent wrong number or a
drawing that can no longer be saved:

* validate_textstyle never checked math.isfinite, unlike its siblings; nan
  and inf passed every comparison and reached the STYLE record verbatim.
  It now has the same _number helper dimstyles and mleaderstyles use.
* The three private _ILLEGAL_NAME_CHARS copies omitted \n \r \t that
  security._FORBIDDEN_SYMBOL_CHARS holds, so 'a\nb' was accepted as a text
  style, DIMTXSTY or leader text style and ezdxf refused the save. The rule
  now lives once in engineering/standards/names.py (fastmcp-free), the
  three modules import it, and a test pins its character set equal to
  security's and its output equal to illegal_symbol_name_chars.
* DIMBLK/DIMBLK1/DIMBLK2 were ("str",): any string went through, and the
  test pinned '_OBLIQUE', which ezdxf's writer refuses at doc.write while
  the COM engine wants exactly that spelling. They are now ("arrow",):
  canonical_arrowhead accepts a built-in with or without its underscore in
  any case (and '', '.', CLOSEDFILLED for the closed filled default), emits
  AutoCAD's documented block-name spelling (also what ezdxf reports after a
  load), and holds user block names to the symbol-name rule; ARROWHEAD_BLOCKS
  is pinned equal to ezdxf's ARROWS vocabulary, and ezdxf_arrowhead is the
  one translation the headless writer needs, proved by exporting all twenty
  headlessly and reading each back as its canonical name.

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

60 lines
2.5 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. Only ezdxf can author an MLEADERSTYLE object;
ActiveX exposes no collection for it (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"]
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