diff --git a/README.md b/README.md index 63ba5c3..6a028eb 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:** 229 tools · 8 resources · 5 prompt templates · 3990 collected tests. +> **v1.5 release snapshot:** 229 tools · 8 resources · 5 prompt templates · 3995 collected tests. > 229 is the **registered** count; a default install advertises 224 over `tools/list`, > because `ENABLE_3D` is unset. `system_about` is the runtime authority. diff --git a/backends/com_backend.py b/backends/com_backend.py index b680b97..61f98df 100644 --- a/backends/com_backend.py +++ b/backends/com_backend.py @@ -1245,6 +1245,26 @@ def _com_geometry_bbox(entity) -> dict | None: _COM_LWPOLYLINE_NAMES = ("AcDbPolyline", "AcDbLWPolyline") +# A tool filters by the DXF entity name; ActiveX names three of them differently +# (measured on AutoCAD 2026: AcDbBlockReference, AcDbPolyline and one class per +# dimension kind). The reported type stays the ActiveX spelling, so a caller +# already filtering by it is unaffected; the DXF name simply finds them too. +_DXF_TYPE_ALIASES: dict[str, frozenset[str]] = { + "INSERT": frozenset({"BLOCKREFERENCE", "MINSERTBLOCK"}), + "LWPOLYLINE": frozenset({"POLYLINE"}), +} + + +def _type_filter_matches(type_filter: str, ent_type: str) -> bool: + """True when an entity whose ActiveX type is ``ent_type`` answers ``type_filter``.""" + wanted = type_filter.upper() + if ent_type == wanted or ent_type in _DXF_TYPE_ALIASES.get(wanted, ()): + return True + # DXF has one DIMENSION entity; ActiveX has AcDbRotatedDimension, + # AcDbAlignedDimension, AcDbDiametricDimension, AcDb2LineAngularDimension, … + return wanted == "DIMENSION" and ent_type.endswith("DIMENSION") + + def _entity_info(entity) -> EntityInfo: """Convert a COM entity object to EntityInfo dataclass.""" try: @@ -5275,7 +5295,7 @@ class ComBackend(AutoCADBackend): ent_type = ent.ObjectName.replace("AcDb", "").upper() ent_layer = ent.Layer - if type_filter and type_filter.upper() != ent_type: + if type_filter and not _type_filter_matches(type_filter, ent_type): continue if layer_filter and layer_filter.lower() != ent_layer.lower(): continue @@ -5307,9 +5327,8 @@ class ComBackend(AutoCADBackend): for i in range(mspace.Count): try: ent = mspace.Item(i) - if ( - type_filter - and type_filter.upper() != ent.ObjectName.replace("AcDb", "").upper() + if type_filter and not _type_filter_matches( + type_filter, ent.ObjectName.replace("AcDb", "").upper() ): continue if layer_filter and layer_filter.lower() != ent.Layer.lower(): diff --git a/tests/test_com_backend.py b/tests/test_com_backend.py index 75fe2ba..0b258cc 100644 --- a/tests/test_com_backend.py +++ b/tests/test_com_backend.py @@ -1089,3 +1089,82 @@ async def test_drawing_audit_restores_auditctl_when_the_command_raises(sysvar_se await backend.drawing_audit() assert doc.variables["AUDITCTL"] == 0 assert doc.calls[-1] == ("SetVariable", "AUDITCTL", 0) + + +# ── type filters speak the DXF names on the live engine too ───────────────── + + +def _typed_modelspace(monkeypatch, object_names): + """A ComBackend whose model space holds one entity per ActiveX ObjectName.""" + import backends.com_backend as cb + + members = [ + _Entity( + name, + Handle=f"{i + 1:X}", + Layer="0", + color=256, + Linetype="ByLayer", + Visible=True, + GetBoundingBox=lambda: ((0.0, 0.0, 0.0), (1.0, 1.0, 0.0)), + ) + for i, name in enumerate(object_names) + ] + space = SimpleNamespace(Count=len(members), Item=lambda i: members[i]) + monkeypatch.setattr(cb, "_msp", lambda: space) + backend = ComBackend() + + async def _run_inline(func, *args, **kwargs): + return func(*args, **kwargs) + + monkeypatch.setattr(backend, "_run", _run_inline) + return backend + + +# The ObjectNames measured on AutoCAD 2026 (25.1s) for each kind of entity a +# tool filters by its DXF name: a block reference, a lightweight polyline and +# every dimension subclass ActiveX creates. +_LIVE_NAMES = [ + "AcDbBlockReference", + "AcDbPolyline", + "AcDbRotatedDimension", + "AcDbAlignedDimension", + "AcDbDiametricDimension", + "AcDbRadialDimension", + "AcDb2LineAngularDimension", + "AcDbOrdinateDimension", + "AcDbLine", +] + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("dxf_name", "expected"), + [ + ("INSERT", 1), + ("LWPOLYLINE", 1), + ("DIMENSION", 6), + ("LINE", 1), + ], +) +async def test_a_dxf_type_filter_finds_what_activex_names_differently( + monkeypatch, dxf_name, expected +): + """block_find_references asks entity_list for INSERTs; ActiveX calls the + same entity AcDbBlockReference, so on the live seat the filter matched + nothing and every block reported zero references. The DXF name a tool + passes must find the entity on both engines.""" + backend = _typed_modelspace(monkeypatch, _LIVE_NAMES) + listed = await backend.entity_list(type_filter=dxf_name, limit=100) + assert len(listed) == expected + assert await backend.entity_count(type_filter=dxf_name) == expected + + +@pytest.mark.asyncio +async def test_the_activex_type_names_still_filter_as_before(monkeypatch): + """The reported type names do not change, so a caller already filtering by + the ActiveX spelling keeps getting exactly what it got.""" + backend = _typed_modelspace(monkeypatch, _LIVE_NAMES) + assert len(await backend.entity_list(type_filter="BLOCKREFERENCE")) == 1 + assert len(await backend.entity_list(type_filter="POLYLINE")) == 1 + assert len(await backend.entity_list(type_filter="ROTATEDDIMENSION")) == 1