feat(selection): add selection_get — read live viewport pickfirst selection (#1)
New COM-only Entity Query tool that reads AutoCAD's implied ("pickfirst")
selection set and returns the selected entity handles + EntityInfo, so the AI
can scope work (e.g. dimension_auto) to exactly what the user picked in the
viewport instead of the whole drawing. Resolves #1 — the layer-juggling
workaround is no longer needed.
- backends/base.py: abstract selection_get() returning a status dict
- backends/com_backend.py: reads ActiveDocument.PickfirstSelectionSet, surfaces
the PICKFIRST sysvar, handles the empty-selection case
- backends/ezdxf_backend.py: headless stub (ok=False, empty handles, same shape)
- server.py: @mcp.tool registration (readOnlyHint), SECTION 5 3 -> 4 tools
- README + CHANGELOG + pyproject: 110 -> 111 tools, v1.1.0 -> v1.1.1
Verified live on AutoCAD 2027 (COM): selection_get correctly returned the
user's grip selection across two distinct picks; 318 tests pass, ruff-clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
5583730a79
commit
dfb21a734a
@@ -487,6 +487,26 @@ class AutoCADBackend(ABC):
|
||||
@abstractmethod
|
||||
async def analysis_select_by_type(self, entity_type: str) -> list[EntityInfo]: ...
|
||||
|
||||
@abstractmethod
|
||||
async def selection_get(self) -> dict:
|
||||
"""Read the live viewport's implied ("pickfirst") selection set.
|
||||
|
||||
COM-only / meaningful only with live AutoCAD. Returns a dict::
|
||||
|
||||
{
|
||||
"ok": bool, # True on COM even for an empty pick
|
||||
"count": int,
|
||||
"handles": list[str], # entity handles to act on
|
||||
"entities": list[EntityInfo], # _dc()-converted by the server
|
||||
"pickfirst": bool | None, # state of the PICKFIRST sysvar
|
||||
"message" / "error": str, # optional guidance / failure reason
|
||||
}
|
||||
|
||||
The ezdxf headless backend has no viewport, so it returns ``ok=False``
|
||||
with an empty ``handles`` list (same shape, never raises).
|
||||
"""
|
||||
...
|
||||
|
||||
# ── view / screenshot ────────────────────────────────────────────────────
|
||||
@abstractmethod
|
||||
async def view_zoom_extents(self) -> dict: ...
|
||||
|
||||
@@ -1444,6 +1444,66 @@ class ComBackend(AutoCADBackend):
|
||||
return results
|
||||
return await self._run(_sync)
|
||||
|
||||
async def selection_get(self) -> dict:
|
||||
def _sync():
|
||||
doc = _acad_doc()
|
||||
|
||||
# PICKFIRST controls whether the noun/verb (grip) pre-selection is
|
||||
# captured at all. Surface it so an empty result is explainable.
|
||||
try:
|
||||
pickfirst = bool(doc.GetVariable("PICKFIRST"))
|
||||
except Exception as exc:
|
||||
log.debug("selection_get: PICKFIRST read failed: %s", exc)
|
||||
pickfirst = None
|
||||
|
||||
try:
|
||||
ss = doc.PickfirstSelectionSet
|
||||
except Exception as exc:
|
||||
log.debug("selection_get: PickfirstSelectionSet unavailable: %s", exc)
|
||||
return {
|
||||
"ok": False,
|
||||
"error": f"PickfirstSelectionSet unavailable: {exc}",
|
||||
"count": 0,
|
||||
"handles": [],
|
||||
"entities": [],
|
||||
"pickfirst": pickfirst,
|
||||
}
|
||||
|
||||
try:
|
||||
count = int(ss.Count)
|
||||
except Exception as exc:
|
||||
log.debug("selection_get: Count read failed: %s", exc)
|
||||
count = 0
|
||||
|
||||
entities = []
|
||||
for i in range(count):
|
||||
try:
|
||||
entities.append(_entity_info(ss.Item(i)))
|
||||
except Exception as exc:
|
||||
log.debug("selection_get: skip selection item %d: %s", i, exc)
|
||||
continue
|
||||
|
||||
handles = [e.handle for e in entities]
|
||||
result = {
|
||||
"ok": True,
|
||||
"count": len(handles),
|
||||
"handles": handles,
|
||||
"entities": entities, # _dc()-converted at the server layer
|
||||
"pickfirst": pickfirst,
|
||||
}
|
||||
if not handles:
|
||||
result["message"] = (
|
||||
"No entities are pre-selected in the AutoCAD viewport. "
|
||||
"Select entities (so grips appear) before calling, and "
|
||||
"ensure PICKFIRST=1."
|
||||
if pickfirst is not False
|
||||
else "PICKFIRST is 0; the noun/verb grip selection is disabled. "
|
||||
"Set PICKFIRST=1 (system_set_variable) and re-select."
|
||||
)
|
||||
return result
|
||||
|
||||
return await self._run(_sync)
|
||||
|
||||
# ── view / screenshot ──────────────────────────────────────────────────────
|
||||
|
||||
async def view_zoom_extents(self) -> dict:
|
||||
|
||||
@@ -1466,6 +1466,19 @@ class EzdxfBackend(AutoCADBackend):
|
||||
]
|
||||
return await self._async(_sync)
|
||||
|
||||
async def selection_get(self) -> dict:
|
||||
# COM-only: headless ezdxf has no viewport / pickfirst grip selection.
|
||||
# Return a not-supported result but keep the same shape (empty handles)
|
||||
# so callers that iterate the selection do not KeyError.
|
||||
return {
|
||||
"ok": False,
|
||||
"error": "selection_get not supported in ezdxf backend (no live AutoCAD viewport)",
|
||||
"count": 0,
|
||||
"handles": [],
|
||||
"entities": [],
|
||||
"pickfirst": None,
|
||||
}
|
||||
|
||||
# ── view / screenshot ──────────────────────────────────────────────────────
|
||||
|
||||
async def view_zoom_extents(self) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user