diff --git a/README.md b/README.md index 0864612..dd53dd3 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Live through COM on Windows, or headless through ezdxf anywhere — one typed co -> **v1.5 release snapshot:** 166 tools · 8 resources · 5 prompt templates · 2423 collected tests. +> **v1.5 release snapshot:** 176 tools · 8 resources · 5 prompt templates · 2423 collected tests. > 166 is the **registered** count; a default install advertises 161 over `tools/list`, > because `ENABLE_3D` is unset. `system_about` is the runtime authority. diff --git a/backends/base.py b/backends/base.py index 0adbddc..65be4ce 100644 --- a/backends/base.py +++ b/backends/base.py @@ -33,6 +33,7 @@ from backends.contracts import ( PremiumContract, SettingsContract, SolidContract, + StylesContract, SystemContract, TransactionContract, ViewContract, @@ -184,6 +185,7 @@ class AutoCADBackend( PremiumContract, GdtContract, SettingsContract, + StylesContract, ): """All backends implement this interface. diff --git a/backends/com_backend.py b/backends/com_backend.py index fee04bc..f9737af 100644 --- a/backends/com_backend.py +++ b/backends/com_backend.py @@ -10,6 +10,7 @@ import asyncio import io import logging import math +import os import sys import time import uuid @@ -318,6 +319,451 @@ def _regen(): log.debug("Regen failed: %s", exc) +# ── styles (track E): module helpers ───────────────────────────────────────── +# +# The ActiveX object model has no per-style getters or setters for dimension +# variables: `AcadDimStyle` exposes `Name` and `CopyFrom` and nothing else. A +# style's values are therefore written the way AutoCAD's own DIMSTYLE command +# writes them — set the DIM* system variables on the document (which makes +# them overrides of the *current* style) and `CopyFrom(document)` to save +# those settings into the target style. Assigning `ActiveDimStyle` restores +# that style's saved settings and discards unsaved overrides (AutoCAD's +# `-DIMSTYLE Restore` rule; the fake in tests/test_styles.py models exactly +# this, and scripts/smoke_settings_com.py confirms it live). Reading is +# one-sided: `GetVariable` reads the *current* style only, so `dimstyle_list` +# reports values for the current style and `values_available: False` for the +# rest rather than cycling the active style behind the operator's back. + + +def _com_named(collection, name: str): + """A collection member by name, case-insensitive (AutoCAD's rule), or None.""" + wanted = name.strip().lower() + for index in range(collection.Count): + item = collection.Item(index) + if str(item.Name).lower() == wanted: + return item + return None + + +def _com_dimvar_for_write(var: str, value: Any) -> Any: + """ActiveX takes DIMDSEP and the arrowhead/text-style names as strings.""" + if var in ("DIMDSEP", "DIMBLK", "DIMBLK1", "DIMBLK2", "DIMTXSTY"): + return str(value) + if isinstance(value, float): + return float(value) + return int(value) + + +def _com_dimvar_for_report(var: str, raw: Any) -> Any: + if var == "DIMDSEP": + if isinstance(raw, int): + return "," if raw == 0 else chr(raw) + text = str(raw) + return text[:1] if text else "," + if var in ("DIMBLK", "DIMBLK1", "DIMBLK2"): + # AutoCAD reports the block name (``_OBLIQUE``, ``""`` for closed + # filled) — already canonical; the same read-side rule as ezdxf keeps + # the two engines' rows spelled identically. + from engineering.standards.dimstyles import reported_arrowhead + + return reported_arrowhead(raw) + if var == "DIMTXSTY": + return str(raw) + return raw + + +def _same_dimvar(old: Any, new: Any) -> bool: + if isinstance(old, (int, float)) and isinstance(new, (int, float)): + return abs(float(old) - float(new)) < 1e-9 + return str(old) == str(new) + + +#: ``SelectionSet.Select`` mode ``acSelectionSetAll`` (AcSelect enum): every +#: entity in the database, in every layout — ``ssget "X"``. +_AC_SELECTION_SET_ALL = 5 + +#: Characters an ``ssget`` filter string treats as wildcards (AutoCAD's +#: ``wcmatch`` grammar). Symbol names may carry most of them: a dimension +#: style called ``A.B`` also matched ``A_B``, ``N#1`` matched ``N21``, +#: ``P[1]`` matched ``P1`` and ``S@X`` matched ``SAX`` until the name was +#: escaped (measured live, AutoCAD 2026). +_WCMATCH_SPECIALS = frozenset("*?#@.~[]-,`") + + +def _wcmatch_literal(text: str) -> str: + """``text`` spelled so an ``ssget`` filter matches it literally. + + Every wildcard-significant character is escaped with a backtick, the + grammar's own escape. A symbol name can never contain a backtick itself + (AutoCAD refuses one), so the escaped form is unambiguous; the match stays + case-insensitive, as any symbol-table name comparison is. + """ + return "".join("`" + char if char in _WCMATCH_SPECIALS else char for char in text) + + +def _com_dimensions_using(doc, style_name: str) -> list[str]: + """Handles of every DIMENSION, in every layout, whose style is ``style_name``. + + One filtered ``SelectionSet.Select(acSelectionSetAll)`` — the ``ssget "X"`` + route with ``(0 . "DIMENSION") (3 .