fix(task-11): dimensions_using_style is one filtered selection set, not a per-entity COM walk

_com_dimensions_using walked every entity of every layout and, since the
un-narrowing fix, re-dispatched each one through a dynamic proxy before
reading ObjectName (~8-10 ms per entity), which put dimstyle_modify past
the 60 s COM_CALL_TIMEOUT on a ~6k-entity drawing after its SetVariable +
CopyFrom had already landed. It is now a single
SelectionSet.Select(acSelectionSetAll, (0 . "DIMENSION") (3 . <style>)),
which answers from AutoCAD's own index whatever the drawing size.

Measured live (AutoCAD 2026, scratch doc, 1000 LINEs + 14 dimensions across
model and paper space): walk 12.7-15.8 s, selection 0.02-0.09 s, identical
handle sets; the whole dimstyle_modify tool now returns in 0.07 s.

The group-3 filter value is a wcmatch pattern, so the style name is
backtick-escaped (_wcmatch_literal): unescaped, "A.B" also selected the
dimensions of "A_B", "N#1" those of "N21", "P[1]" those of "P1", "S@X"
those of "SAX" (measured). Handles are returned in ascending handle order,
Select failures propagate, and the selection set is deleted either way.

tests/test_styles.py: the fake document carries a SelectionSets that
emulates ssget "X" over every layout with wcmatch-literal semantics and
narrowed IAcadEntity items, refusing an unescaped wildcard; three tests
cover no-per-entity-read, escaping, and cleanup-on-failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-21 22:39:08 +03:00
co-authored by Claude Opus 5
parent 03a5067d83
commit 021a4eb43d
2 changed files with 197 additions and 43 deletions
+58 -22
View File
@@ -314,30 +314,66 @@ def _same_dimvar(old: Any, new: Any) -> bool:
return str(old) == str(new)
def _com_dimensions_using(doc, style_name: str) -> list[str]:
"""Handles of every dimension, in every layout, whose StyleName is ``style_name``.
#: ``SelectionSet.Select`` mode ``acSelectionSetAll`` (AcSelect enum): every
#: entity in the database, in every layout — ``ssget "X"``.
_AC_SELECTION_SET_ALL = 5
``IAcadBlock.Item`` is declared ``IAcadEntity*`` (acax25enu.tlb), so with a
makepy cache present every member comes back narrowed to ``IAcadEntity``
and ``.StyleName`` — a member of the dimension classes, not of the entity
base — raises ``AttributeError``. Measured live (AutoCAD 2026): a rotated
dimension drawn with ``PROBE-ANSI`` answered ``[]`` because a blanket
``except`` turned that AttributeError into "no dimension uses it". Each
item is therefore un-narrowed (``_com_unnarrow``), and nothing is
swallowed: a read that fails is an error the caller sees, not an empty
list reported as fact.
#: 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.
"""
wanted = style_name.lower()
handles: list[str] = []
for index in range(doc.Layouts.Count):
block = doc.Layouts.Item(index).Block
for position in range(block.Count):
obj = _com_unnarrow(block.Item(position))
if "Dimension" not in str(obj.ObjectName):
continue
if str(obj.StyleName).lower() == wanted:
handles.append(str(obj.Handle))
return handles
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 . <style>)`` — answers from AutoCAD's own
index, so the cost does not grow with the drawing. The walk this replaced
read ``ObjectName`` on every entity of every layout through a dynamic
proxy (~8-10 ms each), which put ``dimstyle_modify`` past the 60 s
``COM_CALL_TIMEOUT`` on a drawing of ~6k entities — after its write had
already landed. Measured live (AutoCAD 2026, 1000 LINEs + 14 dimensions
across model and paper space): walk 12.7-15.8 s, this 0.02-0.09 s, same
handles. The style name is escaped as a literal (``_wcmatch_literal``)
because the filter is a wildcard pattern; the ``DIMENSION`` group-0 name
keeps parity with the headless engine's ``query("DIMENSION")``.
Handles come back in ascending handle order (database order), whatever
order the selection set reports them in. Nothing is swallowed: a
selection that fails is an error the caller sees, not an empty list
reported as fact.
"""
ss = doc.SelectionSets.Add(f"_DIMSTYLE_{uuid.uuid4().hex[:8]}")
try:
filter_types = _ai([0, 3])
filter_values = win32com.client.VARIANT(
pythoncom.VT_ARRAY | pythoncom.VT_VARIANT,
["DIMENSION", _wcmatch_literal(style_name)],
)
ss.Select(_AC_SELECTION_SET_ALL, None, None, filter_types, filter_values)
# ``Handle`` is an IAcadObject member, so it answers on the narrowed
# ``IAcadEntity`` wrapper ``SelectionSet.Item`` hands back.
handles = [str(ss.Item(index).Handle) for index in range(ss.Count)]
finally:
try:
ss.Delete()
except Exception as exc:
log.debug("SelectionSet cleanup failed: %s", exc)
return sorted(handles, key=lambda handle: int(handle, 16))
#: File suffixes AutoCAD hands to the Windows font system rather than its