Skip to content

solidworks_mcp.adapters.solidworks.selection

solidworks_mcp.adapters.solidworks.selection

Feature selection mixin for PyWin32 SolidWorks operations.

Attributes

_SELECTED_ENTITY_TYPE_NAMES module-attribute

_SELECTED_ENTITY_TYPE_NAMES = {1: 'EDGE', 2: 'FACE', 3: 'VERTEX'}

Classes

AdapterResult dataclass

AdapterResult(status: AdapterResultStatus, data: T | None = None, error: str | None = None, execution_time: float | None = None, metadata: dict[str, Any] | None = None)

Result wrapper for adapter operations.

Attributes:

Name Type Description
data T | None

The data value.

error str | None

The error value.

execution_time float | None

The execution time value.

metadata dict[str, Any] | None

The metadata value.

status AdapterResultStatus

The status value.

Attributes
is_error property
is_error: bool

Check if operation had an error.

Returns:

Name Type Description
bool bool

True if error, otherwise False.

is_success property
is_success: bool

Check if operation was successful.

Returns:

Name Type Description
bool bool

True if success, otherwise False.

AdapterResultStatus

Bases: StrEnum

Result status for adapter operations.

Attributes:

Name Type Description
ERROR Any

The error value.

SUCCESS Any

The success value.

TIMEOUT Any

The timeout value.

WARNING Any

The warning value.

FaceEdgeSelector

FaceEdgeSelector(adapter: Any)

Resolve a face on the active part and multi-select its edges.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter (needs currentModel and _attempt).

required
Source code in src/solidworks_mcp/adapters/solidworks/face_selection.py
def __init__(self, adapter: Any) -> None:
    self._adapter = adapter
Methods:
component_faces
component_faces(component: Any) -> list[FaceInfo]

Return every face of a component's solid bodies.

Measured on SW 2026: these faces report their geometry in the component's own part coordinates, wherever it sits in the assembly, and IEntity::GetComponent on them returns the component.

Source code in src/solidworks_mcp/adapters/solidworks/face_selection.py
def component_faces(self, component: Any) -> list[FaceInfo]:
    """Return every face of a component's solid bodies.

    Measured on SW 2026: these faces report their geometry in the
    component's own part coordinates, wherever it sits in the assembly,
    and ``IEntity::GetComponent`` on them returns the component.
    """
    _flag(self._adapter, component, "GetBodies2")
    bodies = as_list(_attempt(self._adapter, lambda: component.GetBodies2(0)))
    return self._body_faces(bodies)
edges
edges(faces: list[FaceInfo], edge_type: str) -> list[tuple[Any, dict[str, Any]]]

Return (edge, description) for the faces' edges passing the filter.

Source code in src/solidworks_mcp/adapters/solidworks/face_selection.py
def edges(
    self, faces: list[FaceInfo], edge_type: str
) -> list[tuple[Any, dict[str, Any]]]:
    """Return ``(edge, description)`` for the faces' edges passing the filter."""
    out = []
    for f in faces:
        for edge in _attempt(self._adapter, lambda f=f: f.handle.GetEdges()) or []:
            info = describe_edge(self._adapter, edge)
            if edge_matches(info["curve"], edge_type):
                info["face_index"] = f.index
                out.append((edge, info))
    return out
face_at_point
face_at_point(faces: list[FaceInfo], point_mm: list[float]) -> FaceInfo

Return the face containing point_mm (closest within 0.01 mm).

Source code in src/solidworks_mcp/adapters/solidworks/face_selection.py
def face_at_point(self, faces: list[FaceInfo], point_mm: list[float]) -> FaceInfo:
    """Return the face containing ``point_mm`` (closest within 0.01 mm)."""
    q = tuple(c / 1000.0 for c in point_mm)
    best: tuple[float, FaceInfo] | None = None
    for f in faces:
        closest = _attempt(
            self._adapter, lambda f=f: list(f.handle.GetClosestPointOn(*q))
        )
        if not closest or len(closest) < 3:
            continue
        d = sum((a - b) ** 2 for a, b in zip(closest[:3], q, strict=True)) ** 0.5
        if best is None or d < best[0]:
            best = (d, f)
    if best is None or best[0] > _ON_FACE_M:
        raise LookupError(f"No face found at point {list(point_mm)} mm.")
    return best[1]
faces
faces() -> list[FaceInfo]

Return every face of every solid body on the active part.

Source code in src/solidworks_mcp/adapters/solidworks/face_selection.py
def faces(self) -> list[FaceInfo]:
    """Return every face of every solid body on the active part."""
    part = self._model
    _flag(self._adapter, part, "GetBodies2")
    bodies = as_list(_attempt(self._adapter, lambda: part.GetBodies2(0, False)))
    return self._body_faces(bodies)
plane_geometry
plane_geometry(plane: str) -> tuple[tuple[float, float, float], tuple[float, float, float]]

Return (origin, normal) of a named reference plane.

Source code in src/solidworks_mcp/adapters/solidworks/face_selection.py
def plane_geometry(
    self, plane: str
) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
    """Return ``(origin, normal)`` of a named reference plane."""
    feature = _attempt(self._adapter, lambda: self._model.FeatureByName(plane))
    if feature is None:
        raise LookupError(f"No reference plane named {plane!r}.")
    _flag(self._adapter, feature, "GetSpecificFeature2")
    ref = _attempt(self._adapter, lambda: feature.GetSpecificFeature2())
    if ref is None:
        raise LookupError(f"{plane!r} is not a reference plane.")
    data = _attempt(self._adapter, lambda: list(ref.Transform.ArrayData))
    if not data or len(data) < 12:
        raise LookupError(f"Could not read the geometry of plane {plane!r}.")
    return plane_from_transform(data)
resolve
resolve(point: list[float] | None, side: str | None, plane: str | None) -> list[FaceInfo]

Resolve the request to one face (or several for plane).

Source code in src/solidworks_mcp/adapters/solidworks/face_selection.py
def resolve(
    self, point: list[float] | None, side: str | None, plane: str | None
) -> list[FaceInfo]:
    """Resolve the request to one face (or several for ``plane``)."""
    faces = self.faces()
    if not faces:
        raise LookupError("The active part has no solid body faces.")
    if point is not None:
        return [self.face_at_point(faces, point)]
    if side is not None:
        face = pick_side_face(faces, side_vector(side))
        if face is None:
            raise LookupError(f"No planar face faces {side!r}.")
        return [face]
    origin, normal = self.plane_geometry(str(plane))
    on_plane = faces_on_plane(faces, origin, normal)
    if not on_plane:
        raise LookupError(f"No body face lies on {plane!r}.")
    return on_plane
select
select(edges: list[tuple[Any, dict[str, Any]]], append: bool) -> int | None

Select every edge; return the selection count afterwards.

All or nothing: if any edge refuses to select, the selection is cleared before raising, so a following add_chamfer / add_fillet with empty edge_names cannot silently act on a partial set.

Raises:

Type Description
RuntimeError

When any edge refuses to select.

Source code in src/solidworks_mcp/adapters/solidworks/face_selection.py
def select(
    self, edges: list[tuple[Any, dict[str, Any]]], append: bool
) -> int | None:
    """Select every edge; return the selection count afterwards.

    All or nothing: if any edge refuses to select, the selection is
    cleared before raising, so a following ``add_chamfer`` /
    ``add_fillet`` with empty ``edge_names`` cannot silently act on a
    partial set.

    Raises:
        RuntimeError: When any edge refuses to select.
    """
    model = self._model
    sel_mgr = model.SelectionManager
    _flag(self._adapter, sel_mgr, "CreateSelectData")
    if not append:
        _attempt(self._adapter, lambda: model.ClearSelection2(True))
    select_data = _attempt(self._adapter, lambda: sel_mgr.CreateSelectData())
    failed = []
    for i, (edge, _info) in enumerate(edges):
        _flag(self._adapter, edge, "Select4")
        if not _attempt(
            self._adapter, lambda e=edge: e.Select4(True, select_data), False
        ):
            failed.append(i)
    if failed:
        _attempt(self._adapter, lambda: model.ClearSelection2(True))
        raise RuntimeError(
            f"{len(failed)} of {len(edges)} edge(s) could not be selected "
            f"(indices {failed}); the selection was cleared so nothing acts "
            "on a partial set."
        )
    return selection_count(self._adapter, model)

SolidWorksSelectionMixin

Expose feature-selection and feature-list methods through a mixin.

Methods:
get_selected_entities async
get_selected_entities() -> AdapterResult[list[dict[str, Any]]]

Report the faces/edges/vertices currently highlighted in the live SolidWorks UI, so an agent can see exactly what the user clicked on instead of guessing from a screenshot (see com-api-pitfalls.md #21 for why a screenshot alone is not reliable proof of document/view state).

Refreshes currentModel from swApp.ActiveDoc first, the same way list_features does, since the user's selection lives on whatever document is actually active in the UI right now -- which may not be the document this adapter last created or touched.

Returns:

Type Description
AdapterResult[list[dict[str, Any]]]

AdapterResult[list[dict[str, Any]]]: One entry per selected

AdapterResult[list[dict[str, Any]]]

entity, each with at least index and type

AdapterResult[list[dict[str, Any]]]

("FACE"/"EDGE"/"VERTEX"/"UNKNOWN(<n>)"). Faces

AdapterResult[list[dict[str, Any]]]

additionally carry bounding_box_mm, center_mm,

AdapterResult[list[dict[str, Any]]]

area_mm2 and normal (when planar). Edges carry

AdapterResult[list[dict[str, Any]]]

start_mm/end_mm and a straight-line length_mm

AdapterResult[list[dict[str, Any]]]

(the true length of a curved edge will be longer). Vertices

AdapterResult[list[dict[str, Any]]]

carry point_mm. Any field that fails to resolve is simply

AdapterResult[list[dict[str, Any]]]

omitted rather than failing the whole call -- a partial

AdapterResult[list[dict[str, Any]]]

description is more useful than none.

Source code in src/solidworks_mcp/adapters/solidworks/selection.py
async def get_selected_entities(self) -> AdapterResult[list[dict[str, Any]]]:
    """Report the faces/edges/vertices currently highlighted in the live
    SolidWorks UI, so an agent can see exactly what the user clicked on
    instead of guessing from a screenshot (see com-api-pitfalls.md #21 for
    why a screenshot alone is not reliable proof of document/view state).

    Refreshes ``currentModel`` from ``swApp.ActiveDoc`` first, the same
    way ``list_features`` does, since the user's selection lives on
    whatever document is actually active in the UI right now -- which
    may not be the document this adapter last created or touched.

    Returns:
        AdapterResult[list[dict[str, Any]]]: One entry per selected
        entity, each with at least ``index`` and ``type``
        (``"FACE"``/``"EDGE"``/``"VERTEX"``/``"UNKNOWN(<n>)"``). Faces
        additionally carry ``bounding_box_mm``, ``center_mm``,
        ``area_mm2`` and ``normal`` (when planar). Edges carry
        ``start_mm``/``end_mm`` and a straight-line ``length_mm``
        (the true length of a curved edge will be longer). Vertices
        carry ``point_mm``. Any field that fails to resolve is simply
        omitted rather than failing the whole call -- a partial
        description is more useful than none.
    """
    sw_app = getattr(self, "swApp", None)
    active_model = getattr(sw_app, "ActiveDoc", None) if sw_app else None
    if active_model is not None:
        self.currentModel = active_model
    if not self.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )

    def _describe() -> list[dict[str, Any]]:
        model = self.currentModel
        sel_mgr = model.SelectionManager
        self._attempt(
            lambda: _sw_type_info.flag_methods(sel_mgr, "ISelectionMgr"),
            default=0,
        )
        count = (
            self._attempt(lambda: sel_mgr.GetSelectedObjectCount2(-1), default=0)
            or 0
        )

        results: list[dict[str, Any]] = []
        for i in range(1, int(count) + 1):
            raw_type = self._attempt(
                lambda i=i: sel_mgr.GetSelectedObjectType3(i, -1), default=None
            )
            obj = self._attempt(
                lambda i=i: sel_mgr.GetSelectedObject6(i, -1), default=None
            )
            if obj is None:
                continue
            self._attempt(
                lambda o=obj: _sw_type_info.flag_methods(
                    o, "IFace2", "IEdge", "IVertex"
                ),
                default=0,
            )

            type_name = _SELECTED_ENTITY_TYPE_NAMES.get(
                int(raw_type) if isinstance(raw_type, (int, float)) else -1,
                f"UNKNOWN({raw_type})",
            )
            entry: dict[str, Any] = {"index": i, "type": type_name}

            if type_name == "FACE":
                box = self._attempt(lambda o=obj: o.GetBox(), default=None)
                if box and len(box) == 6:
                    entry["bounding_box_mm"] = {
                        "min": [c * 1000 for c in box[0:3]],
                        "max": [c * 1000 for c in box[3:6]],
                    }
                    entry["center_mm"] = [
                        (box[j] + box[j + 3]) / 2 * 1000 for j in range(3)
                    ]
                area = self._attempt(lambda o=obj: o.GetArea(), default=None)
                if area is not None:
                    entry["area_mm2"] = area * 1e6
                normal = self._attempt(lambda o=obj: o.Normal, default=None)
                if normal:
                    entry["normal"] = [round(float(n), 6) for n in normal]

            elif type_name == "EDGE":
                # Shared with select_face_edges: start/end/length plus
                # curve kind and, for circles, centre/axis/radius.
                entry.update(describe_edge(self, obj))

            elif type_name == "VERTEX":
                point = self._attempt(lambda o=obj: o.GetPoint(), default=None)
                if point:
                    entry["point_mm"] = [c * 1000 for c in point]

            results.append(entry)
        return results

    return self._handle_com_operation("get_selected_entities", _describe)
select_face_edges async
select_face_edges(point: list[float] | None = None, side: str | None = None, plane: str | None = None, edge_type: str = 'all', append: bool = False) -> AdapterResult[dict[str, Any]]

Multi-select every edge bounding one face of the active part.

See SolidWorksAdapter.select_face_edges for the contract; the face resolution and selection live in face_selection.FaceEdgeSelector.

Source code in src/solidworks_mcp/adapters/solidworks/selection.py
async def select_face_edges(
    self,
    point: list[float] | None = None,
    side: str | None = None,
    plane: str | None = None,
    edge_type: str = "all",
    append: bool = False,
) -> AdapterResult[dict[str, Any]]:
    """Multi-select every edge bounding one face of the active part.

    See ``SolidWorksAdapter.select_face_edges`` for the contract; the face
    resolution and selection live in ``face_selection.FaceEdgeSelector``.
    """
    problem = validate_face_query(point, side, plane, edge_type)
    if problem:
        return AdapterResult(status=AdapterResultStatus.ERROR, error=problem)
    if not self.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )
    from .io import _doc_type

    doc_type = _doc_type(self)
    if doc_type is not None and doc_type != 1:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=(
                "select_face_edges requires an active part document "
                f"(active document type is {doc_type})."
            ),
        )

    def _select_edges() -> dict[str, Any]:
        selector = FaceEdgeSelector(self)
        faces = selector.resolve(point, side, plane)
        edges = selector.edges(faces, edge_type)
        if not edges:
            kind = "" if edge_type == "all" else f"{edge_type} "
            raise LookupError(
                f"The selected face has no {kind}edges "
                f"(face centre {faces[0].describe()['center_mm']} mm)."
            )
        count = selector.select(edges, append)
        described = [f.describe() for f in faces]
        return {
            "face": described[0],
            "faces": described,
            "edge_type": edge_type,
            "edges": [info for _edge, info in edges],
            "selected_edges": len(edges),
            "selection_count": count,
        }

    return self._run_clean("select_face_edges", _select_edges)
select_feature async
select_feature(feature_name: str, append: bool = False) -> AdapterResult[dict[str, Any]]

Select a named feature, sketch, plane or component.

Parameters:

Name Type Description Default
feature_name str

Feature name, optionally @document qualified.

required
append bool

Add to the current selection instead of replacing it.

False

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The selector payload plus

AdapterResult[dict[str, Any]]

selection_count. ERROR - never SUCCESS with

AdapterResult[dict[str, Any]]

selected: False - when no strategy could select the name.

Source code in src/solidworks_mcp/adapters/solidworks/selection.py
async def select_feature(
    self, feature_name: str, append: bool = False
) -> AdapterResult[dict[str, Any]]:
    """Select a named feature, sketch, plane or component.

    Args:
        feature_name: Feature name, optionally ``@document`` qualified.
        append: Add to the current selection instead of replacing it.

    Returns:
        AdapterResult[dict[str, Any]]: The selector payload plus
        ``selection_count``. ERROR - never SUCCESS with
        ``selected: False`` - when no strategy could select the name.
    """
    if not self.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )

    def _select() -> dict[str, Any]:
        payload = self._feature_selector.select_feature(feature_name, append)
        if not payload or not payload.get("selected"):
            raise LookupError(
                f"Feature not found or could not be selected: {feature_name}"
            )
        payload["selection_count"] = selection_count(self, self.currentModel)
        return payload

    return self._run_clean("select_feature", _select)

Functions:

describe_edge

describe_edge(adapter: Any, edge: Any) -> dict[str, Any]

Describe an IEdge in millimetres; unresolvable fields are omitted.

Returns:

Type Description
dict[str, Any]

dict[str, Any]: curve (linear/circular/other), plus

dict[str, Any]

start_mm/end_mm/length_mm when the edge has vertices

dict[str, Any]

(straight-line length - a curved edge is longer), and

dict[str, Any]

center_mm/axis/radius_mm for circular edges.

Source code in src/solidworks_mcp/adapters/solidworks/face_selection.py
def describe_edge(adapter: Any, edge: Any) -> dict[str, Any]:
    """Describe an ``IEdge`` in millimetres; unresolvable fields are omitted.

    Returns:
        dict[str, Any]: ``curve`` (``linear``/``circular``/``other``), plus
        ``start_mm``/``end_mm``/``length_mm`` when the edge has vertices
        (straight-line length - a curved edge is longer), and
        ``center_mm``/``axis``/``radius_mm`` for circular edges.
    """
    _flag(adapter, edge, *_EDGE_MEMBERS)
    entry: dict[str, Any] = {"curve": "other"}

    curve = _attempt(adapter, lambda: edge.GetCurve())
    if curve is not None:
        _flag(adapter, curve, "IsCircle", "IsLine")
        if _attempt(adapter, lambda: curve.IsCircle(), False):
            entry["curve"] = "circular"
            params = _attempt(adapter, lambda: list(curve.CircleParams))
            if params and len(params) >= 7:
                entry["center_mm"] = [c * 1000 for c in params[0:3]]
                entry["axis"] = [round(float(c), 6) + 0.0 for c in params[3:6]]
                entry["radius_mm"] = params[6] * 1000
        elif _attempt(adapter, lambda: curve.IsLine(), False):
            entry["curve"] = "linear"

    points = []
    for getter in ("GetStartVertex", "GetEndVertex"):
        vertex = _attempt(adapter, lambda g=getter: getattr(edge, g)())
        if vertex is None:
            points.append(None)
            continue
        _flag(adapter, vertex, "GetPoint")
        points.append(_attempt(adapter, lambda v=vertex: v.GetPoint()))
    start, end = points
    if start:
        entry["start_mm"] = [c * 1000 for c in start]
    if end:
        entry["end_mm"] = [c * 1000 for c in end]
    if start and end:
        entry["length_mm"] = (
            sum((a - b) ** 2 for a, b in zip(start, end, strict=True)) ** 0.5 * 1000
        )
    return entry

selection_count

selection_count(adapter: Any, model: Any) -> int | None

Number of entities currently selected in model, or None.

Source code in src/solidworks_mcp/adapters/solidworks/com_utils.py
def selection_count(adapter: Any, model: Any) -> int | None:
    """Number of entities currently selected in ``model``, or None."""
    sel_mgr = attempt(adapter, lambda: model.SelectionManager)
    if sel_mgr is None:
        return None
    flag(adapter, sel_mgr, "GetSelectedObjectCount2")
    count = attempt(adapter, lambda: sel_mgr.GetSelectedObjectCount2(-1))
    return int(count) if isinstance(count, (int, float)) else None

strip_op_prefix

strip_op_prefix(error: str | None, operation_name: str) -> str | None

Drop _handle_com_operation's generic "Error in <op>: " prefix.

Expected failures (not found, nothing to select, rebuild errors) then read as plain sentences for the LLM caller.

Source code in src/solidworks_mcp/adapters/solidworks/com_utils.py
def strip_op_prefix(error: str | None, operation_name: str) -> str | None:
    """Drop ``_handle_com_operation``'s generic ``"Error in <op>: "`` prefix.

    Expected failures (not found, nothing to select, rebuild errors) then read
    as plain sentences for the LLM caller.
    """
    if not error:
        return error
    return error.removeprefix(f"Error in {operation_name}: ")

validate_face_query

validate_face_query(point: list[float] | None, side: str | None, plane: str | None, edge_type: str) -> str | None

Check a select_face_edges request; return an error message or None.

Source code in src/solidworks_mcp/adapters/solidworks/face_selection.py
def validate_face_query(
    point: list[float] | None,
    side: str | None,
    plane: str | None,
    edge_type: str,
) -> str | None:
    """Check a select_face_edges request; return an error message or None."""
    given = [
        name
        for name, value in (("point", point), ("side", side), ("plane", plane))
        if value not in (None, "")
    ]
    if len(given) != 1:
        return (
            "Give exactly one of point, side or plane to identify the face "
            f"(got {', '.join(given) or 'none'})."
        )
    if point is not None and len(point) != 3:
        return "point must be [x, y, z] in millimetres."
    if side is not None:
        try:
            side_vector(side)
        except ValueError as e:
            return str(e)
    if edge_type not in EDGE_TYPES:
        return f"edge_type must be one of {', '.join(EDGE_TYPES)} (got {edge_type!r})."
    return None