Skip to content

solidworks_mcp.adapters.solidworks.features

solidworks_mcp.adapters.solidworks.features

Feature-domain mixin for PyWin32 SolidWorks operations.

Attributes

_AXIS_PLANE_PAIRS module-attribute

_AXIS_PLANE_PAIRS: dict[str, tuple[str, str]] = {'x': ('Top Plane', 'Front Plane'), 'y': ('Front Plane', 'Right Plane'), 'z': ('Top Plane', 'Right Plane')}

_MIRROR_MARK_BODIES module-attribute

_MIRROR_MARK_BODIES = 256

_MIRROR_MARK_FEATURES module-attribute

_MIRROR_MARK_FEATURES = 1

_MIRROR_MARK_PLANE module-attribute

_MIRROR_MARK_PLANE = 2

_NULL_CALLOUT_SENTINEL module-attribute

_NULL_CALLOUT_SENTINEL = _NullCalloutSentinel()

_PATTERN_MARK_AXIS module-attribute

_PATTERN_MARK_AXIS = 1

_PATTERN_MARK_FEATURES module-attribute

_PATTERN_MARK_FEATURES = 4

_REF_PLANE_ANGLE module-attribute

_REF_PLANE_ANGLE = 16

_REF_PLANE_DISTANCE module-attribute

_REF_PLANE_DISTANCE = 8

_REF_PLANE_OPTION_FLIP module-attribute

_REF_PLANE_OPTION_FLIP = 256

_SW_REF_POINT_ALONG_CURVE module-attribute

_SW_REF_POINT_ALONG_CURVE = 2

_SW_REF_POINT_ALONG_CURVE_DISTANCE module-attribute

_SW_REF_POINT_ALONG_CURVE_DISTANCE = 0

_SW_REF_POINT_ALONG_CURVE_PERCENT module-attribute

_SW_REF_POINT_ALONG_CURVE_PERCENT = 1

_SW_REF_POINT_FACE_CENTER module-attribute

_SW_REF_POINT_FACE_CENTER = 4

_TREE_WALK_MEMBERS module-attribute

_TREE_WALK_MEMBERS = ('GetTypeName2', 'GetNextFeature', 'Name')

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)

Bases: Generic[T]

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.

ExtrusionParameters

Bases: BaseModel

Parameters for extrusion operations.

Attributes:

Name Type Description
auto_select bool

The auto select value.

both_directions bool

The both directions value.

depth float

The depth value.

draft_angle float

The draft angle value.

end_condition str

The end condition value.

feature_scope bool

The feature scope value.

merge_result bool

The merge result value.

reverse_direction bool

The reverse direction value.

sketch_name str | None

Explicit sketch to operate on. When set, callers (e.g. create_cut_extrude) must select exactly this sketch and fail with a clear error if it doesn't exist rather than silently falling back to some other sketch.

thin_feature bool

The thin feature value.

thin_thickness float | None

The thin thickness value.

up_to_surface str | None

The up to surface value.

LoftParameters

Bases: BaseModel

Parameters for loft operations.

Attributes:

Name Type Description
end_tangent str | None

The end tangent value.

guide_curves list[str] | None

The guide curves value.

merge_result bool

The merge result value.

profiles list[str]

The profiles value.

start_tangent str | None

The start tangent value.

RevolveParameters

Bases: BaseModel

Parameters for revolve operations.

Attributes:

Name Type Description
angle float

The angle value.

both_directions bool

The both directions value.

merge_result bool

The merge result value.

reverse_direction bool

The reverse direction value.

thin_feature bool

The thin feature value.

thin_thickness float | None

The thin thickness value.

SolidWorksFeature

Bases: BaseModel

SolidWorks feature information.

Attributes:

Name Type Description
id str | None

The id value.

name str

The name value.

parameters dict[str, Any] | None

The parameters value.

parent str | None

The parent value.

properties dict[str, Any] | None

The properties value.

type str

The type value.

Methods:
__getitem__
__getitem__(key: str) -> Any

Build internal getitem.

Parameters:

Name Type Description Default
key str

The key value.

required

Returns:

Name Type Description
Any Any

The result produced by the operation.

Source code in src/solidworks_mcp/adapters/base.py
def __getitem__(self, key: str) -> Any:
    """Build internal getitem.

    Args:
        key (str): The key value.

    Returns:
        Any: The result produced by the operation.
    """
    if self.parameters and key in self.parameters:
        return self.parameters.get(key)
    return self.model_dump().get(key)

SolidWorksFeaturesMixin

Expose SolidWorks feature methods via mixin-local implementation helpers.

Methods:
create_reference_point async
create_reference_point(mode: str, x: float, y: float, z: float, distance: float | None = None, percent: float | None = None) -> AdapterResult[dict[str, Any]]

Create a reference point on the active part.

Parameters:

Name Type Description Default
mode str

"along_curve" or "face_center".

required
x float

X of a point on the target edge/face, in millimetres.

required
y float

Y of a point on the target edge/face, in millimetres.

required
z float

Z of a point on the target edge/face, in millimetres.

required
distance float | None

For along_curve, offset from the edge start in mm.

None
percent float | None

For along_curve, position as 0-100 of edge length.

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: Delegates to

AdapterResult[dict[str, Any]]

func:_create_reference_point_impl; see it for the payload shape.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
async def create_reference_point(
    self,
    mode: str,
    x: float,
    y: float,
    z: float,
    distance: float | None = None,
    percent: float | None = None,
) -> AdapterResult[dict[str, Any]]:
    """Create a reference point on the active part.

    Args:
        mode: ``"along_curve"`` or ``"face_center"``.
        x: X of a point on the target edge/face, in millimetres.
        y: Y of a point on the target edge/face, in millimetres.
        z: Z of a point on the target edge/face, in millimetres.
        distance: For ``along_curve``, offset from the edge start in mm.
        percent: For ``along_curve``, position as 0-100 of edge length.

    Returns:
        AdapterResult[dict[str, Any]]: Delegates to
        :func:`_create_reference_point_impl`; see it for the payload shape.
    """
    return _create_reference_point_impl(
        self, mode, x, y, z, distance, percent
    )
mirror_feature async
mirror_feature(features: list[str], mirror_plane: str, merge: bool = True, mirror_bodies: bool = True) -> AdapterResult[dict[str, Any]]

Mirror solid bodies or features about a plane.

InsertMirrorFeature returns a Feature object even when it mirrored nothing, so the model's volume is measured before and after and the call is reported as failed if the volume did not grow. That check runs here rather than inside the COM closure because get_mass_properties is the read path known to work - reading CreateMassProperty().Volume inline returns None when a plane is still in the selection set, which silently disables the check.

Parameters:

Name Type Description Default
features list[str]

Names of the bodies (or features) to mirror. For a body mirror these are the names as they appear under Solid Bodies, which for a lofted wing is the loft's own name.

required
mirror_plane str

Plane to mirror about, e.g. "Right Plane".

required
merge bool

Merge the mirrored result with the original.

True
mirror_bodies bool

Mirror whole solid bodies rather than features. Defaults to True, which is what works for anything built from a loft or sweep: SolidWorks only resolves a feature mirror when the feature's own sketch sits on the mirror plane, so a wing lofted between stations offset from the centreline cannot be feature-mirrored. Measured on SW 2025: body mirroring a 1819569 mm^3 wing about the Right Plane gave 3636460 mm^3.

True

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: What was mirrored, and the volume

AdapterResult[dict[str, Any]]

before and after. ERROR when nothing was selected, the call

AdapterResult[dict[str, Any]]

failed, or the volume did not grow.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
async def mirror_feature(
    self,
    features: list[str],
    mirror_plane: str,
    merge: bool = True,
    mirror_bodies: bool = True,
) -> AdapterResult[dict[str, Any]]:
    """Mirror solid bodies or features about a plane.

    ``InsertMirrorFeature`` returns a Feature object even when it mirrored
    nothing, so the model's volume is measured before and after and the
    call is reported as failed if the volume did not grow. That check runs
    here rather than inside the COM closure because ``get_mass_properties``
    is the read path known to work - reading ``CreateMassProperty().Volume``
    inline returns ``None`` when a plane is still in the selection set,
    which silently disables the check.

    Args:
        features: Names of the bodies (or features) to mirror. For a body
            mirror these are the names as they appear under Solid Bodies,
            which for a lofted wing is the loft's own name.
        mirror_plane: Plane to mirror about, e.g. ``"Right Plane"``.
        merge: Merge the mirrored result with the original.
        mirror_bodies: Mirror whole solid bodies rather than features.
            Defaults to ``True``, which is what works for anything built
            from a loft or sweep: SolidWorks only resolves a *feature*
            mirror when the feature's own sketch sits on the mirror plane,
            so a wing lofted between stations offset from the centreline
            cannot be feature-mirrored. Measured on SW 2025: body mirroring
            a 1819569 mm^3 wing about the Right Plane gave 3636460 mm^3.

    Returns:
        AdapterResult[dict[str, Any]]: What was mirrored, and the volume
        before and after. ``ERROR`` when nothing was selected, the call
        failed, or the volume did not grow.
    """
    adapter = self._adapter(self)
    if not adapter.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )

    before = await self.get_mass_properties()
    volume_before = before.data.volume if before.is_success and before.data else None

    result = _mirror_feature_impl(
        self, features, mirror_plane, merge, mirror_bodies
    )
    if not result.is_success:
        return result

    after = await self.get_mass_properties()
    volume_after = after.data.volume if after.is_success and after.data else None

    if volume_before is None or volume_after is None:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=(
                "The mirror call returned a feature but the volume could "
                "not be read before and after, so whether any geometry was "
                "produced is unknown."
            ),
        )
    if volume_after <= volume_before * 1.001:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=(
                f"The mirror produced no new geometry (volume "
                f"{volume_before:.1f} -> {volume_after:.1f} mm3). "
                "SolidWorks returned a feature but mirrored nothing. With "
                "mirror_bodies=False this happens whenever the feature's "
                "own sketch does not sit on the mirror plane; try "
                "mirror_bodies=True to mirror the solid body instead."
            ),
        )

    data = dict(result.data or {})
    data["volume_before"] = volume_before
    data["volume_after"] = volume_after
    data["volume_ratio"] = round(volume_after / volume_before, 6)
    return AdapterResult(
        status=AdapterResultStatus.SUCCESS,
        data=data,
        execution_time=result.execution_time,
    )
pattern_circular async
pattern_circular(features: list[str], axis: str, count: int, angle: float = 360.0, equal_spacing: bool = True) -> AdapterResult[dict[str, Any]]

Pattern features around an axis.

FeatureCircularPattern5 returns a Feature object whether or not it produced anything, so the model volume is measured before and after here - in the async layer, using get_mass_properties, which is the read path that works. Reading volume inline inside the COM closure returns None while a selection is active.

Note that instances landing off the parent body are silently dropped by SolidWorks, so the volume rises by less than count - 1 times the seed. That is correct behaviour, not a failure - only no growth at all means the pattern did nothing.

Parameters:

Name Type Description Default
features list[str]

Names of the features to pattern.

required
axis str

Name of the axis to rotate about, e.g. "Axis1" from create_axis.

required
count int

Total number of instances including the original.

required
angle float

Total angle in degrees to spread them over. Defaults to a full 360.

360.0
equal_spacing bool

Space instances evenly across angle.

True

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: What was patterned and the volume

AdapterResult[dict[str, Any]]

before and after. ERROR when nothing could be selected or the

AdapterResult[dict[str, Any]]

volume did not grow.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
async def pattern_circular(
    self,
    features: list[str],
    axis: str,
    count: int,
    angle: float = 360.0,
    equal_spacing: bool = True,
) -> AdapterResult[dict[str, Any]]:
    """Pattern features around an axis.

    ``FeatureCircularPattern5`` returns a Feature object whether or not it
    produced anything, so the model volume is measured before and after
    here - in the async layer, using ``get_mass_properties``, which is the
    read path that works. Reading volume inline inside the COM closure
    returns ``None`` while a selection is active.

    Note that instances landing off the parent body are silently dropped
    by SolidWorks, so the volume rises by less than ``count - 1`` times the
    seed. That is correct behaviour, not a failure - only *no* growth at
    all means the pattern did nothing.

    Args:
        features: Names of the features to pattern.
        axis: Name of the axis to rotate about, e.g. ``"Axis1"`` from
            ``create_axis``.
        count: Total number of instances including the original.
        angle: Total angle in degrees to spread them over. Defaults to a
            full 360.
        equal_spacing: Space instances evenly across ``angle``.

    Returns:
        AdapterResult[dict[str, Any]]: What was patterned and the volume
        before and after. ``ERROR`` when nothing could be selected or the
        volume did not grow.
    """
    adapter = self._adapter(self)
    if not adapter.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )

    before = await self.get_mass_properties()
    volume_before = (
        before.data.volume if before.is_success and before.data else None
    )

    result = _pattern_circular_impl(self, features, axis, count, angle, equal_spacing)
    if not result.is_success:
        return result

    after = await self.get_mass_properties()
    volume_after = after.data.volume if after.is_success and after.data else None

    if volume_before is None or volume_after is None:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=(
                "The pattern call returned a feature but the volume could "
                "not be read before and after, so whether any instances "
                "were created is unknown."
            ),
        )
    if volume_after <= volume_before * 1.0001:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=(
                f"The circular pattern produced no geometry (volume "
                f"{volume_before:.1f} -> {volume_after:.1f} mm3). "
                f"SolidWorks returned a feature but patterned nothing. "
                f"Check that '{axis}' is a real axis in the tree and that "
                f"the instances land on the parent body."
            ),
        )

    data = dict(result.data or {})
    data["volume_before"] = volume_before
    data["volume_after"] = volume_after
    data["volume_ratio"] = round(volume_after / volume_before, 6)
    return AdapterResult(
        status=AdapterResultStatus.SUCCESS,
        data=data,
        execution_time=result.execution_time,
    )
rename_feature async
rename_feature(old_name: str, new_name: str) -> AdapterResult[dict[str, Any]]

Rename a feature on the active model's tree.

Parameters:

Name Type Description Default
old_name str

The feature's current name.

required
new_name str

The name to give it.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: Delegates to

AdapterResult[dict[str, Any]]

func:_rename_feature_impl; see it for the payload shape.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
async def rename_feature(
    self, old_name: str, new_name: str
) -> AdapterResult[dict[str, Any]]:
    """Rename a feature on the active model's tree.

    Args:
        old_name: The feature's current name.
        new_name: The name to give it.

    Returns:
        AdapterResult[dict[str, Any]]: Delegates to
        :func:`_rename_feature_impl`; see it for the payload shape.
    """
    return _rename_feature_impl(self, old_name, new_name)

SweepParameters

Bases: BaseModel

Parameters for sweep operations.

Attributes:

Name Type Description
merge_result bool

The merge result value.

path str

The path value.

twist_along_path bool

The twist along path value.

twist_angle float

The twist angle value.

_NullCalloutSentinel

Stand-in for VARIANT(VT_DISPATCH, None) when pywin32 is absent.

Never passed to a real COM call (if pywin32 isn't importable, no live SolidWorks call can happen either), but must still be distinguishable from a bare None so callers can't accidentally regress to passing plain None as SelectByID2's Callout argument.

Functions:

_add_chamfer_impl

_add_chamfer_impl(adapter: Any, distance: float, edge_names: list[str]) -> AdapterResult[SolidWorksFeature]

Create an equal-distance chamfer on one or more named edges.

Each edge in edge_names is selected by name using Extension.SelectByID2 with entity type "EDGE". After all edges are in the selection set, IFeatureManager::InsertFeatureChamfer is called in equal-distance mode (type 1). A version-gated IModelDoc2::FeatureChamferType path was tried for SW 2025+ but live-verified to silently create an empty, zero-volume feature -- see the comment at its call site for how this was confirmed and why InsertFeatureChamfer is used unconditionally instead.

Parameters:

Name Type Description Default
adapter Any

A fully connected PyWin32Adapter with a non-None currentModel.

required
distance float

Chamfer distance in millimetres. Converted to metres internally.

required
edge_names list[str]

List of SolidWorks edge entity names, e.g. ["Edge<2>", "Edge<5>"], coordinate hints in metres ("x,y,z"), or "face:x,y,z" to select a whole face and chamfer every edge bounding it — see :func:_parse_edge_spec.

required

Returns:

Type Description
AdapterResult[SolidWorksFeature]

AdapterResult[SolidWorksFeature]: On success, data is a

AdapterResult[SolidWorksFeature]

SolidWorksFeature whose type is "Chamfer". On failure,

AdapterResult[SolidWorksFeature]

status is ERROR.

Raises:

Type Description
Exception

Propagated through _handle_com_operation when an edge cannot be selected or InsertFeatureChamfer returns None.

Example::

result = pywin32_feature_ops.add_chamfer(
    adapter, distance=2.0, edge_names=["Edge<2>"]
)
print(result.data.name)  # e.g. "Chamfer1"
Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _add_chamfer_impl(
    adapter: Any, distance: float, edge_names: list[str]
) -> AdapterResult[SolidWorksFeature]:
    """Create an equal-distance chamfer on one or more named edges.

    Each edge in ``edge_names`` is selected by name using
    ``Extension.SelectByID2`` with entity type ``"EDGE"``.  After all edges
    are in the selection set, ``IFeatureManager::InsertFeatureChamfer`` is
    called in equal-distance mode (type ``1``). A version-gated
    ``IModelDoc2::FeatureChamferType`` path was tried for SW 2025+ but
    live-verified to silently create an empty, zero-volume feature -- see
    the comment at its call site for how this was confirmed and why
    ``InsertFeatureChamfer`` is used unconditionally instead.

    Args:
        adapter: A fully connected ``PyWin32Adapter`` with a non-``None``
            ``currentModel``.
        distance: Chamfer distance in **millimetres**.  Converted to metres
            internally.
        edge_names: List of SolidWorks edge entity names, e.g.
            ``["Edge<2>", "Edge<5>"]``, coordinate hints in metres
            (``"x,y,z"``), or ``"face:x,y,z"`` to select a whole face and
            chamfer every edge bounding it — see :func:`_parse_edge_spec`.

    Returns:
        AdapterResult[SolidWorksFeature]: On success, ``data`` is a
        ``SolidWorksFeature`` whose ``type`` is ``"Chamfer"``.  On failure,
        ``status`` is ``ERROR``.

    Raises:
        Exception: Propagated through ``_handle_com_operation`` when an
            edge cannot be selected or ``InsertFeatureChamfer`` returns
            ``None``.

    Example::

        result = pywin32_feature_ops.add_chamfer(
            adapter, distance=2.0, edge_names=["Edge<2>"]
        )
        print(result.data.name)  # e.g. "Chamfer1"
    """
    if not adapter.currentModel:
        return AdapterResult(status=AdapterResultStatus.ERROR, error="No active model")

    def _chamfer_operation() -> SolidWorksFeature:  # pragma: no cover
        """Inner COM closure that selects edges and invokes the chamfer API.

        Returns:
            SolidWorksFeature: Populated feature descriptor.

        Raises:
            Exception: If any edge selection fails or the feature is not created.
        """
        import math

        # Clear any prior selection so the edge set is clean.
        adapter._attempt(
            lambda: adapter.currentModel.ClearSelection2(True), default=None
        )

        for idx, edge_name in enumerate(edge_names):
            sel_name, cx, cy, cz, entity_type = _parse_edge_spec(edge_name)
            append = idx > 0
            if sel_name == "":
                selected = _select_edge_by_coord(
                    adapter, cx, cy, cz, append=append, mark=0, entity_type=entity_type
                )
            else:
                selected = adapter._attempt(
                    lambda sn=sel_name, _x=cx, _y=cy, _z=cz, _ap=append, _et=entity_type: (
                        adapter.currentModel.Extension.SelectByID2(
                            sn, _et, _x, _y, _z, _ap, 0, _null_callout(), 0
                        )
                    ),
                    default=False,
                )
            if not selected:
                raise Exception(f"Failed to select edge: {edge_name}")

        fm = adapter.currentModel.FeatureManager
        _flag_feature_methods(fm, "IFeatureManager")

        # IModelDoc2.FeatureChamferType (the documented "SW 2025+" call) was
        # tried here previously, gated on RevisionNumber >= 33. Live-verified
        # (2026-09-18, SW major 34 / SW2026): it returns cleanly and a
        # "Chamfer1" feature appears in the tree, but removes zero volume --
        # a silent no-op, not a real chamfer. IFeatureManager.InsertFeatureChamfer
        # was verified on the same install to actually remove material
        # (confirmed via mass-properties volume delta), so it is used
        # unconditionally rather than version-branching to a call that has
        # never been proven to work on any SW version.
        feature_name = "Chamfer"
        feature, insert_err = adapter._attempt_with_error(
            lambda: fm.InsertFeatureChamfer(
                1,  # Options
                1,  # ChamferType = equal distance
                distance / 1000.0,  # Width in metres
                math.pi / 4,  # 45 degree angle
                0.0,
                0.0,
                0.0,
                0.0,
            )
        )
        if feature and hasattr(feature, "Name"):
            feature_name = feature.Name or "Chamfer"
        elif not feature:
            raise Exception(
                f"Failed to create chamfer (InsertFeatureChamfer: {insert_err})"
            )

        return SolidWorksFeature(
            name=feature_name,
            type="Chamfer",
            id=adapter._get_feature_id(feature) if feature else "",
            parameters={"distance": distance, "edges": edge_names},
            properties={"created": datetime.now().isoformat()},
        )

    return cast(
        AdapterResult[SolidWorksFeature],
        adapter._handle_com_operation("add_chamfer", _chamfer_operation),
    )

_add_fillet_impl

_add_fillet_impl(adapter: Any, radius: float, edge_names: list[str]) -> AdapterResult[SolidWorksFeature]

Create a constant-radius fillet on one or more named edges.

Each edge in edge_names is selected by name using Extension.SelectByID2 with entity type "EDGE". After all edges are in the selection set, FeatureFillet3 is called to build the feature.

Parameters:

Name Type Description Default
adapter Any

A fully connected PyWin32Adapter with a non-None currentModel.

required
radius float

Fillet radius in millimetres. Converted to metres internally before the COM call.

required
edge_names list[str]

List of SolidWorks edge entity names to fillet, e.g. ["Edge<1>", "Edge<2>"], coordinate hints in metres ("x,y,z"), or "face:x,y,z" to select a whole face and fillet every edge bounding it — see :func:_parse_edge_spec.

required

Returns:

Type Description
AdapterResult[SolidWorksFeature]

AdapterResult[SolidWorksFeature]: On success, data is a

AdapterResult[SolidWorksFeature]

SolidWorksFeature whose type is "Fillet". On failure,

AdapterResult[SolidWorksFeature]

status is ERROR.

Raises:

Type Description
Exception

Propagated through _handle_com_operation when an edge cannot be selected or FeatureFillet3 returns None.

Example::

result = pywin32_feature_ops.add_fillet(
    adapter, radius=3.0, edge_names=["Edge<1>", "Edge<3>"]
)
print(result.data.name)  # e.g. "Fillet1"
Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _add_fillet_impl(
    adapter: Any, radius: float, edge_names: list[str]
) -> AdapterResult[SolidWorksFeature]:
    """Create a constant-radius fillet on one or more named edges.

    Each edge in ``edge_names`` is selected by name using
    ``Extension.SelectByID2`` with entity type ``"EDGE"``.  After all edges
    are in the selection set, ``FeatureFillet3`` is called to build the
    feature.

    Args:
        adapter: A fully connected ``PyWin32Adapter`` with a non-``None``
            ``currentModel``.
        radius: Fillet radius in **millimetres**.  Converted to metres
            internally before the COM call.
        edge_names: List of SolidWorks edge entity names to fillet, e.g.
            ``["Edge<1>", "Edge<2>"]``, coordinate hints in metres
            (``"x,y,z"``), or ``"face:x,y,z"`` to select a whole face and
            fillet every edge bounding it — see :func:`_parse_edge_spec`.

    Returns:
        AdapterResult[SolidWorksFeature]: On success, ``data`` is a
        ``SolidWorksFeature`` whose ``type`` is ``"Fillet"``.  On failure,
        ``status`` is ``ERROR``.

    Raises:
        Exception: Propagated through ``_handle_com_operation`` when an
            edge cannot be selected or ``FeatureFillet3`` returns ``None``.

    Example::

        result = pywin32_feature_ops.add_fillet(
            adapter, radius=3.0, edge_names=["Edge<1>", "Edge<3>"]
        )
        print(result.data.name)  # e.g. "Fillet1"
    """
    if not adapter.currentModel:
        return AdapterResult(status=AdapterResultStatus.ERROR, error="No active model")

    def _fillet_operation() -> SolidWorksFeature:  # pragma: no cover
        """Inner COM closure that selects edges and invokes FeatureFillet3.

        Returns:
            SolidWorksFeature: Populated feature descriptor.

        Raises:
            Exception: If any edge selection fails or the feature is ``None``.
        """
        # Detect SW major version for FeatureFillet3 parameter count
        fillet_sw_major = 0
        if getattr(adapter, "swApp", None):
            rev = adapter._attempt(
                lambda: adapter._get_attr_or_call(adapter.swApp, "RevisionNumber"),
                default="0",
            )
            try:
                fillet_sw_major = int(str(rev).split(".")[0])
            except (ValueError, IndexError):
                fillet_sw_major = 0

        # Clear any prior selection so the edge set is clean.
        adapter._attempt(
            lambda: adapter.currentModel.ClearSelection2(True), default=None
        )

        for idx, edge_name in enumerate(edge_names):
            sel_name, ex, ey, ez, entity_type = _parse_edge_spec(edge_name)
            append = idx > 0  # first edge starts fresh, subsequent ones append
            if sel_name == "":
                # Coordinate-based: traverse body edges/faces and pick the closest one.
                selected = _select_edge_by_coord(
                    adapter, ex, ey, ez, append=append, entity_type=entity_type
                )
            else:
                selected = adapter._attempt(
                    lambda sn=sel_name, _x=ex, _y=ey, _z=ez, _ap=append, _et=entity_type: (
                        adapter.currentModel.Extension.SelectByID2(
                            sn, _et, _x, _y, _z, _ap, 0, _null_callout(), 0
                        )
                    ),
                    default=False,
                )
            if not selected:
                raise Exception(f"Failed to select edge: {edge_name}")

        # SW 2025+ (major >= 33): IModelDoc2.FeatureFillet3 (9 params).
        # Returns a non-zero int on success — NOT an IFeature — so we look up
        # the last modified feature afterwards to get the name/id.
        # Older builds: IFeatureManager.FeatureFillet3 (15 params, returns IFeature).
        if fillet_sw_major >= 33:
            result_code = adapter.currentModel.FeatureFillet3(
                radius / 1000.0,  # R1 in meters
                True,  # Propagate (VT_BOOL)
                0,  # Ftyp (VT_I4)
                False,  # VarRadTyp (VT_BOOL — must be bool, not int)
                0,  # OverflowType (VT_I4)
                0,  # NRadii (VT_I4)
                None,  # Radii (VT_VARIANT)
                False,  # UseHelpPoint (VT_BOOL)
                False,  # UseTangentHoldLine (VT_BOOL)
            )
            if not result_code:
                raise Exception(
                    "Failed to create fillet (IModelDoc2.FeatureFillet3 returned 0)"
                )
            feature = None  # int return — retrieve feature object below
        else:
            feature_manager = adapter.currentModel.FeatureManager
            feature = feature_manager.FeatureFillet3(
                radius / 1000.0,
                0,
                0,
                0,
                0,
                False,
                False,
                False,
                False,
                False,
                False,
                False,
                False,
                0,
                False,
            )
            if not feature:
                raise Exception("Failed to create fillet")

        # Resolve the feature name — FeatureFillet3 on SW 2025+ returns an int,
        # not an IFeature, so we can only report a default name here.
        feature_name = "Fillet"
        if feature is not None and hasattr(feature, "Name"):
            try:
                feature_name = feature.Name or "Fillet"
            except Exception:
                pass

        return SolidWorksFeature(
            name=feature_name,
            type="Fillet",
            id=adapter._get_feature_id(feature) if feature else "",
            parameters={"radius": radius, "edges": edge_names},
            properties={"created": datetime.now().isoformat()},
        )

    return cast(
        AdapterResult[SolidWorksFeature],
        adapter._handle_com_operation("add_fillet", _fillet_operation),
    )

_create_axis_impl

_create_axis_impl(adapter: Any, reference: str) -> AdapterResult[dict[str, Any]]

Create a reference axis along a principal direction.

Wraps IModelDoc2::InsertAxis2(AutoSize) - note the interface: it is on IModelDoc2, not IFeatureManager. It builds an axis from two selected planes, so an axis is requested by naming the direction and the matching plane pair is selected here.

InsertAxis2 returns a plain boolean rather than the axis, so success is confirmed by the feature count rising.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter with a valid currentModel.

required
reference str

"x", "y" or "z".

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The planes used and how the tree

AdapterResult[dict[str, Any]]

changed. ERROR for an unknown direction or when no axis appeared.

Raises:

Type Description
Exception

Propagated through _handle_com_operation.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _create_axis_impl(adapter: Any, reference: str) -> AdapterResult[dict[str, Any]]:
    """Create a reference axis along a principal direction.

    Wraps ``IModelDoc2::InsertAxis2(AutoSize)`` - note the interface: it is on
    ``IModelDoc2``, **not** ``IFeatureManager``. It builds an axis from two
    selected planes, so an axis is requested by naming the direction and the
    matching plane pair is selected here.

    ``InsertAxis2`` returns a plain boolean rather than the axis, so success is
    confirmed by the feature count rising.

    Args:
        adapter: A connected ``PyWin32Adapter`` with a valid ``currentModel``.
        reference: ``"x"``, ``"y"`` or ``"z"``.

    Returns:
        AdapterResult[dict[str, Any]]: The planes used and how the tree
        changed. ``ERROR`` for an unknown direction or when no axis appeared.

    Raises:
        Exception: Propagated through ``_handle_com_operation``.
    """
    if not adapter.currentModel:
        return AdapterResult(status=AdapterResultStatus.ERROR, error="No active model")

    key = str(reference or "").strip().lower().lstrip("+-")
    if key not in _AXIS_PLANE_PAIRS:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=(
                f"Unknown axis reference '{reference}'. "
                f"Use one of: {', '.join(sorted(_AXIS_PLANE_PAIRS))}."
            ),
        )

    plane_a, plane_b = _AXIS_PLANE_PAIRS[key]

    def _axis_operation() -> dict[str, Any]:
        before = _feature_count(adapter)
        adapter._attempt(
            lambda: adapter.currentModel.ClearSelection2(True), default=None
        )

        for index, plane in enumerate((plane_a, plane_b)):
            if not _select_reference_entity(adapter, plane, 0, append=index > 0):
                raise Exception(f"Failed to select '{plane}' for the axis")

        adapter._attempt(lambda: adapter.currentModel.InsertAxis2(True), default=None)

        after = _feature_count(adapter)
        if before is None or after is None:
            raise Exception(
                "The feature count could not be read, so whether an axis was "
                "created is unknown."
            )
        if after <= before:
            raise Exception(
                f"No reference axis was created from {plane_a} + {plane_b}. "
                "InsertAxis2 reports only a boolean and the feature tree is "
                "unchanged."
            )

        return {
            "reference": key,
            "planes": [plane_a, plane_b],
            "features_before": before,
            "features_after": after,
        }

    return cast(
        AdapterResult[dict[str, Any]],
        adapter._handle_com_operation("create_axis", _axis_operation),
    )

_create_cut_extrude_impl

_create_cut_extrude_impl(adapter: Any, params: ExtrusionParameters) -> AdapterResult[SolidWorksFeature]

Create a cut-extrude feature from an explicit or unambiguously-tracked sketch.

Sketch resolution never guesses:

  1. params.sketch_name, if given, is matched (case-insensitively) against the live feature tree via :func:_profile_feature_names. No match -> immediate error listing the sketches that do exist. No COM cut is attempted.
  2. Otherwise, adapter._last_sketch_name (set by the most recent create_sketch/exit_sketch call in this session) is used only if it also matches a real name in the current tree — this catches the case where create_sketch had to fall back to a synthetic Sketch_N guess (SolidWorks didn't hand back a usable sketch object synchronously) and that guess doesn't correspond to anything real.
  3. Otherwise: immediate error asking the caller to pass sketch_name explicitly, listing available sketches. There is no third guess (no "most recent ProfileFeature in the tree", no blind Sketch<N> enumeration) — a wrong silent guess would cut the wrong profile.

Once resolved, the sketch is explicitly selected via SelectByID2 before any FeatureCut call — SolidWorks' own "implicit" active-sketch state (whatever was last open before this call) is never relied upon, since that is exactly the racy behavior that made this feature intermittently cut nothing.

Two COM API variants are attempted in order of preference:

  1. FeatureCut4 — most modern (SolidWorks 2015+).
  2. FeatureCut3 — SolidWorks 2010–2014, and any install lacking FeatureCut4. Same 26-parameter order as FeatureCut4 minus the trailing OptimizeGeometry argument (confirmed against SolidWorks API help and live macro examples — there is no separate "legacy" argument order; an earlier version of this fallback invented one and it always raised DISP_E_PARAMNOTOPTIONAL because it was missing the NormalCut argument and had D2 missing entirely).

All depth values are in millimetres and converted to metres internally.

Parameters:

Name Type Description Default
adapter Any

A fully connected PyWin32Adapter with a non-None currentModel.

required
params ExtrusionParameters

Extrusion parameter bag reused for cut parameters: - depth (float): Cut depth in mm. - draft_angle (float): Draft angle in degrees. - reverse_direction (bool): Flip the cut direction. - end_condition (str): "Blind" (default) or "ThroughAll" / "through_all". - feature_scope (bool): Limit cut to selected bodies. - auto_select (bool): Auto-select bodies in scope. - sketch_name (str | None): Sketch to cut from. See sketch resolution above.

required

Returns:

Type Description
AdapterResult[SolidWorksFeature]

AdapterResult[SolidWorksFeature]: On success, data is a

AdapterResult[SolidWorksFeature]

SolidWorksFeature whose type is "Cut-Extrude". On

AdapterResult[SolidWorksFeature]

failure, status is ERROR and error explains exactly

AdapterResult[SolidWorksFeature]

which stage failed: sketch-not-found (with the list of sketches that

AdapterResult[SolidWorksFeature]

do exist), sketch-selection-failed (the resolved name plus why

AdapterResult[SolidWorksFeature]

SelectByID2 was rejected), or every FeatureCut COM variant's

AdapterResult[SolidWorksFeature]

own error text — and, when every COM call returned falsy without

AdapterResult[SolidWorksFeature]

raising at all, an explicit note that the profile likely isn't a

AdapterResult[SolidWorksFeature]

valid closed loop intersecting solid geometry, rather than a bare

AdapterResult[SolidWorksFeature]

"failed" with no diagnostic content.

Raises:

Type Description
Exception

Propagated through _handle_com_operation for every failure stage described above.

Example::

from solidworks_mcp.adapters.base import ExtrusionParameters
from solidworks_mcp.adapters import pywin32_feature_ops

params = ExtrusionParameters(depth=10.0, end_condition="ThroughAll")
result = pywin32_feature_ops.create_cut_extrude(adapter, params)
print(result.data.name)  # e.g. "Cut-Extrude1"
Source code in src/solidworks_mcp/adapters/solidworks/features.py
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
def _create_cut_extrude_impl(
    adapter: Any, params: ExtrusionParameters
) -> AdapterResult[SolidWorksFeature]:
    """Create a cut-extrude feature from an explicit or unambiguously-tracked sketch.

    Sketch resolution never guesses:

    1. ``params.sketch_name``, if given, is matched (case-insensitively)
       against the live feature tree via :func:`_profile_feature_names`. No
       match -> immediate error listing the sketches that do exist. No COM
       cut is attempted.
    2. Otherwise, ``adapter._last_sketch_name`` (set by the most recent
       ``create_sketch``/``exit_sketch`` call in this session) is used only
       if it also matches a real name in the current tree — this catches the
       case where ``create_sketch`` had to fall back to a synthetic
       ``Sketch_N`` guess (SolidWorks didn't hand back a usable sketch object
       synchronously) and that guess doesn't correspond to anything real.
    3. Otherwise: immediate error asking the caller to pass ``sketch_name``
       explicitly, listing available sketches. There is no third guess (no
       "most recent ``ProfileFeature`` in the tree", no blind ``Sketch<N>``
       enumeration) — a wrong silent guess would cut the wrong profile.

    Once resolved, the sketch is explicitly selected via ``SelectByID2``
    *before* any ``FeatureCut`` call — SolidWorks' own "implicit" active-sketch
    state (whatever was last open before this call) is never relied upon,
    since that is exactly the racy behavior that made this feature
    intermittently cut nothing.

    Two COM API variants are attempted in order of preference:

    1. ``FeatureCut4`` — most modern (SolidWorks 2015+).
    2. ``FeatureCut3`` — SolidWorks 2010–2014, and any install lacking
       ``FeatureCut4``. Same 26-parameter order as ``FeatureCut4`` minus the
       trailing ``OptimizeGeometry`` argument (confirmed against SolidWorks
       API help and live macro examples — there is no separate "legacy"
       argument order; an earlier version of this fallback invented one and
       it always raised ``DISP_E_PARAMNOTOPTIONAL`` because it was missing
       the ``NormalCut`` argument and had ``D2`` missing entirely).

    All depth values are in millimetres and converted to metres internally.

    Args:
        adapter: A fully connected ``PyWin32Adapter`` with a non-``None``
            ``currentModel``.
        params: Extrusion parameter bag reused for cut parameters:
            - ``depth`` (float): Cut depth in mm.
            - ``draft_angle`` (float): Draft angle in degrees.
            - ``reverse_direction`` (bool): Flip the cut direction.
            - ``end_condition`` (str): ``"Blind"`` (default) or
              ``"ThroughAll"`` / ``"through_all"``.
            - ``feature_scope`` (bool): Limit cut to selected bodies.
            - ``auto_select`` (bool): Auto-select bodies in scope.
            - ``sketch_name`` (str | None): Sketch to cut from. See sketch
              resolution above.

    Returns:
        AdapterResult[SolidWorksFeature]: On success, ``data`` is a
        ``SolidWorksFeature`` whose ``type`` is ``"Cut-Extrude"``.  On
        failure, ``status`` is ``ERROR`` and ``error`` explains exactly
        which stage failed: sketch-not-found (with the list of sketches that
        do exist), sketch-selection-failed (the resolved name plus why
        ``SelectByID2`` was rejected), or every ``FeatureCut`` COM variant's
        own error text — and, when every COM call returned falsy without
        raising at all, an explicit note that the profile likely isn't a
        valid closed loop intersecting solid geometry, rather than a bare
        "failed" with no diagnostic content.

    Raises:
        Exception: Propagated through ``_handle_com_operation`` for every
            failure stage described above.

    Example::

        from solidworks_mcp.adapters.base import ExtrusionParameters
        from solidworks_mcp.adapters import pywin32_feature_ops

        params = ExtrusionParameters(depth=10.0, end_condition="ThroughAll")
        result = pywin32_feature_ops.create_cut_extrude(adapter, params)
        print(result.data.name)  # e.g. "Cut-Extrude1"
    """
    if not adapter.currentModel:
        return AdapterResult(status=AdapterResultStatus.ERROR, error="No active model")

    def _cut_operation() -> SolidWorksFeature:  # pragma: no cover
        """Inner COM closure that locates the active sketch and performs the cut.

        Normalises ``params``, resolves the end-condition constant, selects the
        sketch profile, then cascades through three ``FeatureCut`` overloads.

        Returns:
            SolidWorksFeature: Populated feature descriptor on success.

        Raises:
            Exception: When all COM cut variants return ``None``.
        """
        normalized = SimpleNamespace(
            depth=float(getattr(params, "depth", 0.0)),
            draft_angle=float(getattr(params, "draft_angle", 0.0)),
            reverse_direction=bool(getattr(params, "reverse_direction", False)),
            end_condition=str(getattr(params, "end_condition", "Blind")),
            feature_scope=bool(getattr(params, "feature_scope", False)),
            auto_select=bool(getattr(params, "auto_select", True)),
            sketch_name=getattr(params, "sketch_name", None) or None,
        )
        feature_manager = adapter.currentModel.FeatureManager

        # --- Sketch resolution: explicit > tracked-and-verified > clear
        # error. Never a silent guess — see the docstring above. Resolving
        # this up front (before any FeatureCut attempt) also lets every
        # failure below name the sketch it was actually targeting.
        tree_names = _profile_feature_names(adapter)

        def _match_tree_name(name: str) -> str | None:
            for real in tree_names:
                if real.lower() == name.lower():
                    return real
            return None

        available = ", ".join(tree_names) if tree_names else "(none found)"
        if normalized.sketch_name:
            target_sketch = _match_tree_name(normalized.sketch_name)
            if target_sketch is None:
                raise Exception(
                    f"Sketch '{normalized.sketch_name}' not found in the "
                    f"feature tree. Sketches available to cut from: {available}"
                )
        else:
            last_tracked = getattr(adapter, "_last_sketch_name", None)
            target_sketch = _match_tree_name(last_tracked) if last_tracked else None
            if target_sketch is None:
                hint = (
                    f" (the last create_sketch call returned '{last_tracked}', "
                    "which does not match any real sketch in the model — "
                    "SolidWorks likely didn't hand back a usable sketch "
                    "object synchronously)"
                    if last_tracked
                    else " (no create_sketch/exit_sketch call has run this session)"
                )
                raise Exception(
                    "No sketch_name given, and no reliably-tracked sketch to "
                    f"cut from{hint}. Pass sketch_name explicitly. Sketches "
                    f"available to cut from: {available}"
                )

        # Explicit selection before any FeatureCut attempt. SolidWorks' own
        # "implicit" active-sketch state (whatever was last open) is never
        # relied upon — that is exactly the race that made this feature
        # intermittently cut nothing while reporting no error at all.
        adapter._attempt(
            lambda: adapter.currentModel.ClearSelection2(True), default=None
        )
        # Callout must be an explicit VT_DISPATCH null, not plain None — a
        # bare None marshals as VT_NULL and SolidWorks rejects the whole call
        # with DISP_E_TYPEMISMATCH (runbook item 11). Using
        # _attempt_with_error (not _attempt) here means that error surfaces
        # in the failure message below instead of being swallowed into a
        # generic "selection failed" guess.
        select_result, select_error = adapter._attempt_with_error(
            lambda: adapter.currentModel.Extension.SelectByID2(
                target_sketch,
                "SKETCH",
                0.0,
                0.0,
                0.0,
                False,
                0,
                _null_callout(),
                0,
            )
        )
        sketch_selected = bool(select_result)
        if not sketch_selected:
            reason = (
                f" COM error: {select_error}"
                if select_error is not None
                else " SelectByID2 returned False with no COM error — the "
                "sketch may already be consumed by another feature, be "
                "suppressed, or not be a valid selectable profile."
            )
            raise Exception(
                f"Failed to select sketch '{target_sketch}' via SelectByID2."
                + reason
            )
        adapter._last_sketch_name = target_sketch

        end_condition = (normalized.end_condition or "Blind").strip().lower()
        t1 = adapter.constants["swEndCondBlind"]
        depth_m = normalized.depth / 1000.0
        if end_condition in {"throughall", "through all", "through_all"}:
            t1 = adapter.constants["swEndCondThroughAll"]
        elif end_condition in {
            "throughallboth",
            "through all both",
            "through_all_both",
        }:
            t1 = adapter.constants["swEndCondThroughAllBoth"]

        t0 = adapter.constants.get("swStartSketchPlane", 0)
        feature = None
        fallback_errors: list[str] = []
        is_through = end_condition in {
            "throughall",
            "through all",
            "through_all",
            "throughallboth",
            "through all both",
            "through_all_both",
        }

        # Detect SW major version for FeatureCut4 parameter count
        # SW 2025 (major=33) verified with 27 params; other versions use 28.
        sw_major = 0
        if getattr(adapter, "swApp", None):
            rev = adapter._attempt(
                lambda: adapter._get_attr_or_call(adapter.swApp, "RevisionNumber"),
                default="0",
            )
            try:
                sw_major = int(str(rev).split(".")[0])
            except (ValueError, IndexError):
                sw_major = 0

        # 1. FeatureCut4 (SW 2015+)
        # SW 2025 (major=33): 27 params, the 27th being OptimizeGeometry.
        # It was omitted here, so the call passed 26 and SolidWorks answered
        # "Parameter not optional" for every cut.
        # SW 2026+ (major>=34): 28 params — adds OptimizeGeometry + PFeat.
        if sw_major == 33:
            feature, cut4_error = adapter._attempt_with_error(
                lambda: feature_manager.FeatureCut4(
                    is_through,  # Sd
                    False,  # Flip
                    normalized.reverse_direction,  # Dir
                    t1,  # T1
                    adapter.constants["swEndCondBlind"],  # T2
                    depth_m,  # D1
                    0.0,  # D2
                    False,
                    False,
                    False,
                    False,  # Dchk1/2, Ddir1/2
                    normalized.draft_angle * 3.14159 / 180.0,  # Dang1
                    0.0,  # Dang2
                    False,
                    False,
                    False,
                    False,  # OffsetRev1/2, TranslateSurf1/2
                    False,  # NormalCut
                    normalized.feature_scope,  # UseFeatScope
                    normalized.auto_select,  # UseAutoSelect
                    False,  # AssemblyFeatureScope
                    False,  # AutoSelectComponents
                    False,  # PropagateFeatureToParts
                    t0,  # T0
                    0.0,  # StartOffset
                    False,  # FlipStartOffset
                    True,  # OptimizeGeometry
                )
            )
        elif sw_major >= 34:
            # SW 2026+ adds OptimizeGeometry as a 27th INPUT param.
            # PFeat is an OUT param (PARAMFLAG_FOUT|FRETVAL) — not passed by caller.
            feature, cut4_error = adapter._attempt_with_error(
                lambda: feature_manager.FeatureCut4(
                    is_through,  # Sd
                    False,  # Flip
                    normalized.reverse_direction,  # Dir
                    t1,  # T1
                    adapter.constants["swEndCondBlind"],  # T2
                    depth_m,  # D1
                    0.0,  # D2
                    False,
                    False,
                    False,
                    False,  # Dchk1/2, Ddir1/2
                    normalized.draft_angle * 3.14159 / 180.0,  # Dang1
                    0.0,  # Dang2
                    False,
                    False,
                    False,
                    False,  # OffsetRev1/2, TranslateSurf1/2
                    False,  # NormalCut
                    normalized.feature_scope,  # UseFeatScope
                    normalized.auto_select,  # UseAutoSelect
                    False,  # AssemblyFeatureScope
                    False,  # AutoSelectComponents
                    False,  # PropagateFeatureToParts
                    t0,  # T0
                    0.0,  # StartOffset
                    False,  # FlipStartOffset
                    False,  # OptimizeGeometry (added in SW 2026)
                )
            )
        else:
            feature, cut4_error = adapter._attempt_with_error(
                lambda: feature_manager.FeatureCut4(
                    True,
                    False,
                    normalized.reverse_direction,
                    t1,
                    adapter.constants["swEndCondBlind"],
                    depth_m,
                    0.0,
                    False,
                    False,
                    False,
                    False,
                    normalized.draft_angle * 3.14159 / 180.0,
                    0.0,
                    False,
                    False,
                    False,
                    False,
                    False,
                    normalized.feature_scope,
                    normalized.auto_select,
                    False,
                    False,
                    False,
                    t0,
                    0.0,
                    False,
                    False,
                )
            )
        if cut4_error is not None:
            fallback_errors.append(f"FeatureCut4: {cut4_error}")

        if not feature:
            # The sketch is already explicitly selected (above) — no
            # reselection loop here. If FeatureCut4 didn't take, retry the
            # exact same selection against the older FeatureCut3 signature
            # rather than guessing at a different sketch.
            adapter._attempt(
                lambda: adapter.currentModel.Extension.SelectByID2(
                    target_sketch,
                    "SKETCH",
                    0.0,
                    0.0,
                    0.0,
                    False,
                    0,
                    _null_callout(),
                    0,
                ),
                default=False,
            )

        if not feature:
            # 2. FeatureCut3 (SW 2010+, and any install lacking FeatureCut4)
            # Signature: Sd, Flip, Dir, T1, T2, D1, D2, Dchk1, Dchk2, Ddir1, Ddir2,
            #   Dang1, Dang2, OffsetReverse1, OffsetReverse2, TranslateSurface1,
            #   TranslateSurface2, NormalCut, UseFeatScope, UseAutoSelect,
            #   AssemblyFeatureScope, AutoSelectComponents, PropagateFeatureToParts,
            #   T0, StartOffset, FlipStartOffset
            feature, cut3_error = adapter._attempt_with_error(
                lambda: feature_manager.FeatureCut3(
                    is_through,
                    normalized.reverse_direction,
                    False,
                    t1,
                    0,
                    normalized.depth / 1000.0,
                    normalized.depth / 1000.0,
                    False,
                    False,
                    False,
                    False,
                    normalized.draft_angle * 3.14159 / 180.0,
                    0.0,
                    False,
                    False,
                    False,
                    False,
                    False,
                    normalized.feature_scope,
                    normalized.auto_select,
                    False,
                    False,
                    False,
                    t0,
                    0.0,
                    False,
                )
            )
            if cut3_error is not None:
                fallback_errors.append(f"FeatureCut3: {cut3_error}")

        if not feature:
            if fallback_errors:
                raise Exception(
                    f"Failed to create cut extrude feature from sketch "
                    f"'{target_sketch}' (SolidWorks major version {sw_major}). "
                    + " | ".join(fallback_errors)
                )
            raise Exception(
                f"SolidWorks accepted the FeatureCut call(s) for sketch "
                f"'{target_sketch}' (SolidWorks major version {sw_major}) "
                "without raising an error, but returned no feature. This "
                "means the COM call itself succeeded but the modeling "
                "engine couldn't perform the cut — check that the sketch is "
                "a fully closed profile, that it actually intersects solid "
                "geometry within the given depth, and that it hasn't "
                "already been consumed by another feature."
            )

        return SolidWorksFeature(
            name=feature.Name,
            type="Cut-Extrude",
            id=adapter._get_feature_id(feature),
            parameters={
                "depth": normalized.depth,
                "draft_angle": normalized.draft_angle,
                "reverse_direction": normalized.reverse_direction,
                "sketch_name": target_sketch,
            },
            properties={"created": datetime.now().isoformat()},
        )

    return cast(
        AdapterResult[SolidWorksFeature],
        adapter._handle_com_operation("create_cut_extrude", _cut_operation),
    )

_create_extrusion_impl

_create_extrusion_impl(adapter: Any, params: ExtrusionParameters) -> AdapterResult[SolidWorksFeature]

Create a boss-extrude feature from the active sketch profile.

Attempts the modern FeatureExtrusion3 COM call first; falls back to the legacy FeatureExtrusion2 signature when the newer overload is absent. When params.thin_feature is truthy, the thin-wall variants (FeatureExtrusionThin2 / FeatureExtruThin2) are used instead.

All depth and thickness values are provided in millimetres and converted to metres internally.

Parameters:

Name Type Description Default
adapter Any

A fully connected PyWin32Adapter instance. Must have a non-None currentModel and a valid FeatureManager.

required
params ExtrusionParameters

Extrusion parameter bag. Relevant fields: - depth (float): Extrude depth in mm. - draft_angle (float): Draft angle in degrees. Default 0. - reverse_direction (bool): Flip the extrusion direction. - thin_feature (bool): Produce a thin-wall body. - thin_thickness (float | None): Wall thickness in mm when thin_feature is True. - merge_result (bool): Merge with existing bodies. Default True. - both_directions (bool): Extrude symmetrically in both directions from the sketch plane. - auto_fillet_corners (bool): Round sharp thin-wall corners. - fillet_corners_radius (float): Corner fillet radius in mm.

required

Returns:

Type Description
AdapterResult[SolidWorksFeature]

AdapterResult[SolidWorksFeature]: On success, data is a

AdapterResult[SolidWorksFeature]

SolidWorksFeature whose type is "Extrusion". On failure,

AdapterResult[SolidWorksFeature]

status is ERROR and error contains a descriptive message.

Raises:

Type Description
Exception

Propagated through _handle_com_operation when the COM call returns None for the created feature object.

Example::

from solidworks_mcp.adapters.base import ExtrusionParameters
from solidworks_mcp.adapters import pywin32_feature_ops

params = ExtrusionParameters(depth=25.0, draft_angle=2.0)
result = pywin32_feature_ops.create_extrusion(adapter, params)
print(result.data.name)  # e.g. "Boss-Extrude1"
Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _create_extrusion_impl(
    adapter: Any, params: ExtrusionParameters
) -> AdapterResult[SolidWorksFeature]:
    """Create a boss-extrude feature from the active sketch profile.

    Attempts the modern ``FeatureExtrusion3`` COM call first; falls back to the
    legacy ``FeatureExtrusion2`` signature when the newer overload is absent.
    When ``params.thin_feature`` is truthy, the thin-wall variants
    (``FeatureExtrusionThin2`` / ``FeatureExtruThin2``) are used instead.

    All depth and thickness values are provided in millimetres and converted
    to metres internally.

    Args:
        adapter: A fully connected ``PyWin32Adapter`` instance.  Must have a
            non-``None`` ``currentModel`` and a valid ``FeatureManager``.
        params: Extrusion parameter bag.  Relevant fields:
            - ``depth`` (float): Extrude depth in mm.
            - ``draft_angle`` (float): Draft angle in degrees.  Default 0.
            - ``reverse_direction`` (bool): Flip the extrusion direction.
            - ``thin_feature`` (bool): Produce a thin-wall body.
            - ``thin_thickness`` (float | None): Wall thickness in mm when
              ``thin_feature`` is ``True``.
            - ``merge_result`` (bool): Merge with existing bodies.  Default
              ``True``.
            - ``both_directions`` (bool): Extrude symmetrically in both
              directions from the sketch plane.
            - ``auto_fillet_corners`` (bool): Round sharp thin-wall corners.
            - ``fillet_corners_radius`` (float): Corner fillet radius in mm.

    Returns:
        AdapterResult[SolidWorksFeature]: On success, ``data`` is a
        ``SolidWorksFeature`` whose ``type`` is ``"Extrusion"``.  On failure,
        ``status`` is ``ERROR`` and ``error`` contains a descriptive message.

    Raises:
        Exception: Propagated through ``_handle_com_operation`` when the COM
            call returns ``None`` for the created feature object.

    Example::

        from solidworks_mcp.adapters.base import ExtrusionParameters
        from solidworks_mcp.adapters import pywin32_feature_ops

        params = ExtrusionParameters(depth=25.0, draft_angle=2.0)
        result = pywin32_feature_ops.create_extrusion(adapter, params)
        print(result.data.name)  # e.g. "Boss-Extrude1"
    """
    if not adapter.currentModel:
        return AdapterResult(status=AdapterResultStatus.ERROR, error="No active model")

    def _extrusion_operation() -> SolidWorksFeature:  # pragma: no cover
        """Inner COM closure that builds and returns the extrusion feature.

        Normalises ``params`` into a ``SimpleNamespace`` so every attribute
        access is guaranteed safe regardless of the dataclass version.  Picks
        the thin-wall or solid branch, then tries the modern API first before
        falling back to the legacy one.

        Returns:
            SolidWorksFeature: Populated feature descriptor on success.

        Raises:
            Exception: If both API variants return ``None``.
        """
        normalized = SimpleNamespace(
            depth=float(getattr(params, "depth", 0.0)),
            draft_angle=float(getattr(params, "draft_angle", 0.0)),
            reverse_direction=bool(getattr(params, "reverse_direction", False)),
            thin_feature=bool(getattr(params, "thin_feature", False)),
            thin_thickness=getattr(params, "thin_thickness", None),
            merge_result=bool(getattr(params, "merge_result", True)),
            both_directions=bool(getattr(params, "both_directions", False)),
            auto_fillet_corners=bool(getattr(params, "auto_fillet_corners", False)),
            fillet_corners_radius=float(getattr(params, "fillet_corners_radius", 0.0)),
        )
        feature_manager = adapter.currentModel.FeatureManager

        if normalized.thin_feature and normalized.thin_thickness:
            t0 = adapter.constants.get("swStartSketchPlane", 0)
            t1 = (
                adapter.constants["swEndCondMidPlane"]
                if normalized.both_directions
                else adapter.constants["swEndCondBlind"]
            )
            try:
                feature = feature_manager.FeatureExtrusionThin2(
                    True,
                    False,
                    normalized.reverse_direction,
                    t1,
                    adapter.constants["swEndCondBlind"],
                    normalized.depth / 1000.0,
                    0.0,
                    False,
                    False,
                    False,
                    False,
                    normalized.draft_angle * 3.14159 / 180.0,
                    0.0,
                    False,
                    False,
                    False,
                    False,
                    normalized.merge_result,
                    normalized.thin_thickness / 1000.0,
                    normalized.thin_thickness / 1000.0,
                    0.0,
                    0,
                    0,
                    normalized.auto_fillet_corners,
                    normalized.fillet_corners_radius / 1000.0,
                    False,
                    True,
                    t0,
                    0.0,
                    False,
                )
            except Exception:
                feature = feature_manager.FeatureExtruThin2(
                    normalized.depth / 1000.0,
                    0.0,
                    False,
                    normalized.draft_angle * 3.14159 / 180.0,
                    0.0,
                    False,
                    False,
                    normalized.merge_result,
                    False,
                    True,
                    normalized.thin_thickness / 1000.0,
                    normalized.thin_thickness / 1000.0,
                    False,
                    False,
                    False,
                    adapter.constants["swEndCondBlind"],
                    adapter.constants["swEndCondBlind"],
                )
        else:
            t0 = adapter.constants.get("swStartSketchPlane", 0)
            try:
                feature = feature_manager.FeatureExtrusion3(
                    True,
                    False,
                    normalized.reverse_direction,
                    adapter.constants["swEndCondBlind"],
                    adapter.constants["swEndCondBlind"],
                    normalized.depth / 1000.0,
                    0.0,
                    False,
                    False,
                    False,
                    False,
                    normalized.draft_angle * 3.14159 / 180.0,
                    0.0,
                    False,
                    False,
                    False,
                    False,
                    normalized.merge_result,
                    False,
                    True,
                    t0,
                    0.0,
                    False,
                )
            except Exception:
                feature = feature_manager.FeatureExtrusion2(
                    True,
                    False,
                    normalized.reverse_direction,
                    adapter.constants["swEndCondBlind"],
                    adapter.constants["swEndCondBlind"],
                    normalized.depth / 1000.0,
                    0.0,
                    False,
                    False,
                    False,
                    False,
                    normalized.draft_angle * 3.14159 / 180.0,
                    0.0,
                    False,
                    False,
                    False,
                    False,
                    normalized.merge_result,
                    False,
                    True,
                    t0,
                    0.0,
                    False,
                )

        if not feature:
            raise Exception("Failed to create extrusion feature")

        return SolidWorksFeature(
            name=feature.Name,
            type="Extrusion",
            id=adapter._get_feature_id(feature),
            parameters={
                "depth": normalized.depth,
                "draft_angle": normalized.draft_angle,
                "reverse_direction": normalized.reverse_direction,
                "thin_feature": normalized.thin_feature,
                "thin_thickness": normalized.thin_thickness,
            },
            properties={"created": datetime.now().isoformat()},
        )

    return cast(
        AdapterResult[SolidWorksFeature],
        adapter._handle_com_operation("create_extrusion", _extrusion_operation),
    )

_create_loft_impl

_create_loft_impl(adapter: Any, params: LoftParameters) -> AdapterResult[SolidWorksFeature]

Create a lofted boss/protrusion between two or more profile sketches.

Uses IFeatureManager::InsertProtrusionBlend2. Each profile named in params.profiles is selected under mark 1 (in order — the selection order determines the loft direction), and any params.guide_curves are selected under mark 2. Because a solid is produced, every profile must be a closed contour.

Parameters:

Name Type Description Default
adapter Any

A fully connected PyWin32Adapter with a non-None currentModel.

required
params LoftParameters

Loft parameter bag. Relevant fields: - profiles (list[str]): Ordered profile sketch names; at least two are required. - guide_curves (list[str] | None): Optional guide curve names. - start_tangent / end_tangent (str | None): "normal" tangency at the start/end profile, anything else / None -> no tangency. - merge_result (bool): Merge with existing bodies.

required

Returns:

Type Description
AdapterResult[SolidWorksFeature]

AdapterResult[SolidWorksFeature]: On success, data is a

AdapterResult[SolidWorksFeature]

SolidWorksFeature whose type is "Loft". On failure,

AdapterResult[SolidWorksFeature]

status is ERROR with a descriptive message.

Raises:

Type Description
Exception

Propagated through _handle_com_operation when a profile cannot be selected or the COM call returns None.

Example::

from solidworks_mcp.adapters.base import LoftParameters

params = LoftParameters(profiles=["Sketch1", "Sketch2"])
result = await adapter.create_loft(params)
print(result.data.name)  # e.g. "Loft1"
Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _create_loft_impl(
    adapter: Any, params: LoftParameters
) -> AdapterResult[SolidWorksFeature]:
    """Create a lofted boss/protrusion between two or more profile sketches.

    Uses ``IFeatureManager::InsertProtrusionBlend2``.  Each profile named in
    ``params.profiles`` is selected under mark 1 (in order — the selection
    order determines the loft direction), and any ``params.guide_curves`` are
    selected under mark 2.  Because a solid is produced, every profile must be
    a closed contour.

    Args:
        adapter: A fully connected ``PyWin32Adapter`` with a non-``None``
            ``currentModel``.
        params: Loft parameter bag.  Relevant fields:
            - ``profiles`` (list[str]): Ordered profile sketch names; at least
              two are required.
            - ``guide_curves`` (list[str] | None): Optional guide curve names.
            - ``start_tangent`` / ``end_tangent`` (str | None): ``"normal"``
              tangency at the start/end profile, anything else / ``None`` ->
              no tangency.
            - ``merge_result`` (bool): Merge with existing bodies.

    Returns:
        AdapterResult[SolidWorksFeature]: On success, ``data`` is a
        ``SolidWorksFeature`` whose ``type`` is ``"Loft"``.  On failure,
        ``status`` is ``ERROR`` with a descriptive message.

    Raises:
        Exception: Propagated through ``_handle_com_operation`` when a profile
            cannot be selected or the COM call returns ``None``.

    Example::

        from solidworks_mcp.adapters.base import LoftParameters

        params = LoftParameters(profiles=["Sketch1", "Sketch2"])
        result = await adapter.create_loft(params)
        print(result.data.name)  # e.g. "Loft1"
    """
    if not adapter.currentModel:
        return AdapterResult(status=AdapterResultStatus.ERROR, error="No active model")

    profiles = list(getattr(params, "profiles", None) or [])
    if len(profiles) < 2:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="Loft requires at least 2 profile sketches",
        )

    def _loft_operation() -> SolidWorksFeature:  # pragma: no cover
        """Inner COM closure that selects profiles/guides and runs the loft.

        Returns:
            SolidWorksFeature: Populated feature descriptor on success.

        Raises:
            Exception: When a profile selection fails or
                ``InsertProtrusionBlend2`` returns ``None``.
        """
        guide_curves = list(getattr(params, "guide_curves", None) or [])

        adapter._attempt(
            lambda: adapter.currentModel.ClearSelection2(True), default=None
        )

        # Profiles under mark 1, in order. First replaces the selection set,
        # the rest append so SW sees them as an ordered profile group.
        for index, profile in enumerate(profiles):
            if not _select_named_feature(adapter, profile, 1, append=index > 0):
                raise Exception(f"Failed to select loft profile sketch: {profile}")

        # Optional guide curves under mark 2 (a sketch or a reference curve).
        for guide in guide_curves:
            if not _select_named_feature(adapter, guide, 2, append=True):
                raise Exception(f"Failed to select loft guide curve: {guide}")

        # swTangencyType_e: 0 = none, 1 = tangent to profile normal.
        def _tangency(value: str | None) -> int:  # pragma: no cover
            return 1 if str(value or "").strip().lower() == "normal" else 0

        start_match = _tangency(getattr(params, "start_tangent", None))
        end_match = _tangency(getattr(params, "end_tangent", None))

        feature_manager = adapter.currentModel.FeatureManager
        feature = feature_manager.InsertProtrusionBlend2(
            False,  # Closed loft
            True,  # KeepTangency
            False,  # ForceNonRational
            1.0,  # TessToleranceFactor
            start_match,  # StartMatchingType (swTangencyType_e)
            end_match,  # EndMatchingType
            1.0,  # StartTangentLength
            1.0,  # EndTangentLength
            True,  # StartTangentDir
            True,  # EndTangentDir
            False,  # IsThinBody
            0.0,  # Thickness1
            0.0,  # Thickness2
            0,  # ThinType
            bool(getattr(params, "merge_result", True)),  # Merge
            True,  # UseFeatScope
            True,  # UseAutoSelect
            2,  # GuideCurveInfluence (swGuideCurveInfluenceNextEdge)
        )

        if not feature:
            raise Exception("Failed to create loft feature")

        return SolidWorksFeature(
            name=feature.Name,
            type="Loft",
            id=adapter._get_feature_id(feature),
            parameters={
                "profiles": profiles,
                "guide_curves": guide_curves or None,
                "start_tangent": getattr(params, "start_tangent", None),
                "end_tangent": getattr(params, "end_tangent", None),
                "merge_result": bool(getattr(params, "merge_result", True)),
            },
            properties={"created": datetime.now().isoformat()},
        )

    return cast(
        AdapterResult[SolidWorksFeature],
        adapter._handle_com_operation("create_loft", _loft_operation),
    )

_create_reference_plane_impl

_create_reference_plane_impl(adapter: Any, reference: str, offset: float, angle: float, flip: bool) -> AdapterResult[dict[str, Any]]

Create a reference plane offset from, or angled to, an existing plane.

Wraps IFeatureManager::InsertRefPlane(FirstConstraint, FirstConstraintAngleOrDistance, Second, 0, Third, 0). The reference entity must be selected under mark 0 first, which is what the SolidWorks documentation example does before the identical call.

This is what lets a sketch be opened anywhere other than the six built-in planes. create_sketch already accepts a plane by its tree name, so the name returned here can be passed straight to it - verified live: a plane 76.2 mm off the Front Plane came back as "Plane1", and a circle sketched on it extruded to exactly the expected volume.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter with a valid currentModel.

required
reference str

Name of the reference plane or planar face.

required
offset float

Offset distance in millimetres, converted to metres.

required
angle float

Angle in degrees; used instead of offset when non-zero.

required
flip bool

Reverse the offset or angle direction.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The new plane's name and the parameters

AdapterResult[dict[str, Any]]

used. ERROR when there is no model, the reference cannot be

AdapterResult[dict[str, Any]]

selected, or no plane was added to the tree.

Raises:

Type Description
Exception

Propagated through _handle_com_operation.

Example::

plane = await adapter.create_reference_plane("Front Plane", offset=76.2)
await adapter.create_sketch(plane.data["name"])
Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _create_reference_plane_impl(
    adapter: Any,
    reference: str,
    offset: float,
    angle: float,
    flip: bool,
) -> AdapterResult[dict[str, Any]]:
    """Create a reference plane offset from, or angled to, an existing plane.

    Wraps ``IFeatureManager::InsertRefPlane(FirstConstraint,
    FirstConstraintAngleOrDistance, Second, 0, Third, 0)``. The reference
    entity must be selected under **mark 0** first, which is what the
    SolidWorks documentation example does before the identical call.

    This is what lets a sketch be opened anywhere other than the six built-in
    planes. ``create_sketch`` already accepts a plane by its tree name, so the
    name returned here can be passed straight to it - verified live: a plane
    76.2 mm off the Front Plane came back as ``"Plane1"``, and a circle
    sketched on it extruded to exactly the expected volume.

    Args:
        adapter: A connected ``PyWin32Adapter`` with a valid ``currentModel``.
        reference: Name of the reference plane or planar face.
        offset: Offset distance in **millimetres**, converted to metres.
        angle: Angle in **degrees**; used instead of ``offset`` when non-zero.
        flip: Reverse the offset or angle direction.

    Returns:
        AdapterResult[dict[str, Any]]: The new plane's name and the parameters
        used. ``ERROR`` when there is no model, the reference cannot be
        selected, or no plane was added to the tree.

    Raises:
        Exception: Propagated through ``_handle_com_operation``.

    Example::

        plane = await adapter.create_reference_plane("Front Plane", offset=76.2)
        await adapter.create_sketch(plane.data["name"])
    """
    if not adapter.currentModel:
        return AdapterResult(status=AdapterResultStatus.ERROR, error="No active model")

    if not reference:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="create_reference_plane requires a reference plane/face name",
        )

    if angle:
        # Measured on SW 2025: InsertRefPlane with the angle constraint and a
        # single selected plane adds nothing to the tree and reports no error.
        # An angled plane is under-defined by one reference - SolidWorks needs
        # a second entity (an axis or edge) under mark 1 to rotate about.
        # Refusing is better than returning a plane name that does not exist.
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=(
                "create_reference_plane does not support 'angle' yet: an "
                "angled plane needs a second reference (an axis or edge to "
                "rotate about) which this signature cannot take. Use 'offset' "
                "for parallel planes."
            ),
        )

    if not offset:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=(
                "create_reference_plane requires a non-zero offset - a plane "
                "coincident with its reference is not useful"
            ),
        )

    def _plane_operation() -> dict[str, Any]:
        before = _feature_count(adapter)
        adapter._attempt(
            lambda: adapter.currentModel.ClearSelection2(True), default=None
        )

        if not _select_reference_entity(adapter, reference, 0, append=False):
            raise Exception(
                f"Failed to select reference plane/face: {reference}. "
                "Use an existing plane name (e.g. 'Front Plane') or a planar "
                "face."
            )

        value, flip_bits = _offset_plane_distance(offset, flip)
        constraint = _REF_PLANE_DISTANCE | flip_bits

        from .. import sw_type_info

        feature_manager = sw_type_info.flagged(
            adapter.currentModel.FeatureManager, "IFeatureManager"
        )
        plane = adapter._attempt(
            lambda: feature_manager.InsertRefPlane(constraint, value, 0, 0.0, 0, 0.0),
            default=None,
        )

        after = _feature_count(adapter)
        if before is not None and after is not None and after <= before:
            raise Exception(
                f"No reference plane was added from '{reference}' - the "
                f"feature tree still has {after} feature(s)."
            )
        if plane is None:
            raise Exception(
                f"InsertRefPlane returned nothing for reference '{reference}'"
            )

        name = adapter._attempt(
            lambda: adapter._get_attr_or_call(plane, "Name"), default=None
        )
        if not name:
            # No invented fallback: a caller needs the real name to sketch on
            # the plane, and a made-up one fails later and further away.
            raise Exception(
                "The reference plane was created but its name could not be "
                "read, so it cannot be referenced by create_sketch."
            )

        return {
            "name": str(name),
            "reference": reference,
            "offset": offset or None,
            "angle": angle or None,
            "flip": flip,
            "features_before": before,
            "features_after": after,
        }

    return cast(
        AdapterResult[dict[str, Any]],
        adapter._handle_com_operation("create_reference_plane", _plane_operation),
    )

_create_reference_point_impl

_create_reference_point_impl(adapter: Any, mode: str, x: float, y: float, z: float, distance: float | None, percent: float | None) -> AdapterResult[dict[str, Any]]

Create a reference point on the active part.

Two modes:

  • "along_curve" - selects the edge under the millimetre coordinate (x, y, z) and places a point distance mm, or percent (0-100) of the edge length, from the edge start.
  • "face_center" - selects the face under (x, y, z) and places a point at its centroid.

Coordinate selection uses IModelDocExtension::SelectByID2; per this repo's runbook a ForceRebuild3 is issued first so freshly created edges are tessellated, and the Callout argument is passed an explicit VT_DISPATCH null. InsertReferencePoint returns a Feature whether or not it produced anything, so the feature count is compared before and after.

Parameters:

Name Type Description Default
adapter Any

A connected adapter with a valid currentModel.

required
mode str

"along_curve" or "face_center".

required
x float

X of a point on the target edge/face, in millimetres.

required
y float

Y of a point on the target edge/face, in millimetres.

required
z float

Z of a point on the target edge/face, in millimetres.

required
distance float | None

For along_curve, offset from the edge start in mm.

required
percent float | None

For along_curve, position as 0-100 of the edge length. Exactly one of distance / percent is required.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The mode, the coordinate and resolved

AdapterResult[dict[str, Any]]

parameter, and how the feature tree changed. ERROR for an unknown

AdapterResult[dict[str, Any]]

mode, a bad distance/percent combination, a failed selection, or when

AdapterResult[dict[str, Any]]

no new feature appeared.

Raises:

Type Description
Exception

Propagated through _handle_com_operation.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _create_reference_point_impl(
    adapter: Any,
    mode: str,
    x: float,
    y: float,
    z: float,
    distance: float | None,
    percent: float | None,
) -> AdapterResult[dict[str, Any]]:
    """Create a reference point on the active part.

    Two modes:

    * ``"along_curve"`` - selects the edge under the millimetre coordinate
      ``(x, y, z)`` and places a point ``distance`` mm, or ``percent``
      (0-100) of the edge length, from the edge start.
    * ``"face_center"`` - selects the face under ``(x, y, z)`` and places a
      point at its centroid.

    Coordinate selection uses ``IModelDocExtension::SelectByID2``; per this
    repo's runbook a ``ForceRebuild3`` is issued first so freshly created
    edges are tessellated, and the ``Callout`` argument is passed an explicit
    ``VT_DISPATCH`` null. ``InsertReferencePoint`` returns a Feature whether
    or not it produced anything, so the feature count is compared before and
    after.

    Args:
        adapter: A connected adapter with a valid ``currentModel``.
        mode: ``"along_curve"`` or ``"face_center"``.
        x: X of a point on the target edge/face, in millimetres.
        y: Y of a point on the target edge/face, in millimetres.
        z: Z of a point on the target edge/face, in millimetres.
        distance: For ``along_curve``, offset from the edge start in mm.
        percent: For ``along_curve``, position as 0-100 of the edge length.
            Exactly one of ``distance`` / ``percent`` is required.

    Returns:
        AdapterResult[dict[str, Any]]: The mode, the coordinate and resolved
        parameter, and how the feature tree changed. ``ERROR`` for an unknown
        mode, a bad distance/percent combination, a failed selection, or when
        no new feature appeared.

    Raises:
        Exception: Propagated through ``_handle_com_operation``.
    """
    if not adapter.currentModel:
        return AdapterResult(status=AdapterResultStatus.ERROR, error="No active model")

    key = str(mode or "").strip().lower()
    if key not in ("along_curve", "face_center"):
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=f"Unknown mode {mode!r}. Use 'along_curve' or 'face_center'.",
        )

    along_type = 0
    along_value = 0.0
    if key == "along_curve":
        if (distance is None) == (percent is None):
            return AdapterResult(
                status=AdapterResultStatus.ERROR,
                error="along_curve needs exactly one of distance or percent",
            )
        if distance is not None:
            if float(distance) <= 0.0:
                return AdapterResult(
                    status=AdapterResultStatus.ERROR,
                    error="distance must be positive",
                )
            along_type = _SW_REF_POINT_ALONG_CURVE_DISTANCE
            along_value = float(distance) / 1000.0
        else:
            if not 0.0 < float(percent) < 100.0:
                return AdapterResult(
                    status=AdapterResultStatus.ERROR,
                    error="percent must be between 0 and 100 (exclusive)",
                )
            along_type = _SW_REF_POINT_ALONG_CURVE_PERCENT
            along_value = float(percent) / 100.0

    def _point_operation() -> dict[str, Any]:
        from .. import sw_type_info

        model = adapter.currentModel
        before = _feature_count(adapter)
        adapter._attempt(lambda: model.ForceRebuild3(True), default=None)
        adapter._attempt(lambda: model.ClearSelection2(True), default=None)

        entity_type = "EDGE" if key == "along_curve" else "FACE"
        callout = _null_callout()
        selected = adapter._attempt(
            lambda: model.Extension.SelectByID2(
                "",
                entity_type,
                float(x) / 1000.0,
                float(y) / 1000.0,
                float(z) / 1000.0,
                False,
                0,
                callout,
                0,
            ),
            default=False,
        )
        if not selected:
            raise Exception(
                f"No {entity_type.lower()} found at ({x}, {y}, {z}) mm - the "
                "coordinate must lie on the target entity."
            )

        manager = sw_type_info.flagged(
            adapter._attempt(lambda: model.FeatureManager, default=None),
            "IFeatureManager",
        )
        if manager is None:
            raise Exception("Part has no FeatureManager")

        if key == "along_curve":
            adapter._attempt(
                lambda: manager.InsertReferencePoint(
                    _SW_REF_POINT_ALONG_CURVE, along_type, along_value, 1
                ),
                default=None,
            )
        else:
            adapter._attempt(
                lambda: manager.InsertReferencePoint(
                    _SW_REF_POINT_FACE_CENTER, 0, 0.0, 1
                ),
                default=None,
            )

        adapter._attempt(lambda: model.EditRebuild3(), default=None)
        after = _feature_count(adapter)
        if before is None or after is None:
            raise Exception(
                "The feature count could not be read, so whether a reference "
                "point was created is unknown."
            )
        if after <= before:
            raise Exception(
                "InsertReferencePoint added no feature - the selected entity "
                "may not support a reference point in this mode."
            )
        return {
            "mode": key,
            "at_mm": [x, y, z],
            "distance_mm": distance if key == "along_curve" else None,
            "percent": percent if key == "along_curve" else None,
            "features_before": before,
            "features_after": after,
        }

    return cast(
        AdapterResult[dict[str, Any]],
        adapter._handle_com_operation("create_reference_point", _point_operation),
    )

_create_revolve_impl

_create_revolve_impl(adapter: Any, params: RevolveParameters) -> AdapterResult[SolidWorksFeature]

Create a revolve feature from the active sketch profile around a centre axis.

Uses FeatureRevolve2 from the SolidWorks COM API. The sketch must already contain a centre-line that SolidWorks will use as the rotation axis.

Parameters:

Name Type Description Default
adapter Any

A fully connected PyWin32Adapter with a non-None currentModel.

required
params RevolveParameters

Revolve parameter bag. Relevant fields: - angle (float): Revolve angle in degrees. Use 360 for a full revolution. - reverse_direction (bool): Flip the revolve direction. - both_directions (bool): Revolve symmetrically in both directions. - thin_feature (bool): Produce a thin-wall body. - thin_thickness (float | None): Wall thickness in mm. - merge_result (bool): Merge with existing bodies.

required

Returns:

Type Description
AdapterResult[SolidWorksFeature]

AdapterResult[SolidWorksFeature]: On success, data is a

AdapterResult[SolidWorksFeature]

SolidWorksFeature whose type is "Revolve". On failure,

AdapterResult[SolidWorksFeature]

status is ERROR.

Raises:

Type Description
Exception

Propagated through _handle_com_operation when FeatureRevolve2 returns None.

Example::

from solidworks_mcp.adapters.base import RevolveParameters
from solidworks_mcp.adapters import pywin32_feature_ops

params = RevolveParameters(angle=360.0, merge_result=True)
result = pywin32_feature_ops.create_revolve(adapter, params)
print(result.data.name)  # e.g. "Revolve1"
Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _create_revolve_impl(
    adapter: Any, params: RevolveParameters
) -> AdapterResult[SolidWorksFeature]:
    """Create a revolve feature from the active sketch profile around a centre axis.

    Uses ``FeatureRevolve2`` from the SolidWorks COM API.  The sketch must
    already contain a centre-line that SolidWorks will use as the rotation axis.

    Args:
        adapter: A fully connected ``PyWin32Adapter`` with a non-``None``
            ``currentModel``.
        params: Revolve parameter bag.  Relevant fields:
            - ``angle`` (float): Revolve angle in degrees.  Use 360 for a
              full revolution.
            - ``reverse_direction`` (bool): Flip the revolve direction.
            - ``both_directions`` (bool): Revolve symmetrically in both
              directions.
            - ``thin_feature`` (bool): Produce a thin-wall body.
            - ``thin_thickness`` (float | None): Wall thickness in mm.
            - ``merge_result`` (bool): Merge with existing bodies.

    Returns:
        AdapterResult[SolidWorksFeature]: On success, ``data`` is a
        ``SolidWorksFeature`` whose ``type`` is ``"Revolve"``.  On failure,
        ``status`` is ``ERROR``.

    Raises:
        Exception: Propagated through ``_handle_com_operation`` when
            ``FeatureRevolve2`` returns ``None``.

    Example::

        from solidworks_mcp.adapters.base import RevolveParameters
        from solidworks_mcp.adapters import pywin32_feature_ops

        params = RevolveParameters(angle=360.0, merge_result=True)
        result = pywin32_feature_ops.create_revolve(adapter, params)
        print(result.data.name)  # e.g. "Revolve1"
    """
    if not adapter.currentModel:
        return AdapterResult(status=AdapterResultStatus.ERROR, error="No active model")

    def _revolve_operation() -> SolidWorksFeature:  # pragma: no cover
        """Inner COM closure that builds and returns the revolve feature.

        Converts the degree angle to radians and invokes ``FeatureRevolve2``.

        Returns:
            SolidWorksFeature: Populated feature descriptor on success.

        Raises:
            Exception: If ``FeatureRevolve2`` returns ``None``.
        """
        # Detect SW major version for FeatureRevolve2 API choice
        revolve_sw_major = 0
        if getattr(adapter, "swApp", None):
            rev = adapter._attempt(
                lambda: adapter._get_attr_or_call(adapter.swApp, "RevisionNumber"),
                default="0",
            )
            try:
                revolve_sw_major = int(str(rev).split(".")[0])
            except (ValueError, IndexError):
                revolve_sw_major = 0

        import math

        if revolve_sw_major == 33:  # pragma: no cover
            # IFeatureManager.FeatureRevolve2 - exact 20-parameter signature
            # read from the live gen_py type library for SW 2025:
            #   SingleDir, IsSolid, IsThin, IsCut, ReverseDir,
            #   BothDirectionUpToSameEntity, Dir1Type, Dir2Type,
            #   Dir1Angle(rad), Dir2Angle(rad), OffsetReverse1, OffsetReverse2,
            #   OffsetDistance1, OffsetDistance2, ThinType,
            #   ThinThickness1(m), ThinThickness2(m), Merge,
            #   UseFeatScope, UseAutoSelect
            # This call passed 19 arguments and put Merge where ThinType
            # belongs, so SolidWorks rejected every revolve with
            # "Parameter not optional".
            feature_manager = adapter.currentModel.FeatureManager
            is_thin = bool(params.thin_feature and params.thin_thickness)
            feature = feature_manager.FeatureRevolve2(
                not params.both_directions,  # SingleDir
                True,  # IsSolid
                is_thin,  # IsThin
                False,  # IsCut
                params.reverse_direction,  # ReverseDir
                False,  # BothDirectionUpToSameEntity
                0,  # Dir1Type (swEndCondBlind)
                0,  # Dir2Type
                params.angle * math.pi / 180.0,  # Dir1Angle (rad)
                (params.angle * math.pi / 180.0)
                if params.both_directions
                else 0.0,  # Dir2Angle
                False,  # OffsetReverse1
                False,  # OffsetReverse2
                0.0,  # OffsetDistance1
                0.0,  # OffsetDistance2
                0,  # ThinType
                (params.thin_thickness or 0.0) / 1000.0,  # ThinThickness1
                0.0,  # ThinThickness2
                params.merge_result,  # Merge
                False,  # UseFeatScope
                True,  # UseAutoSelect
            )
        else:
            feature_manager = adapter.currentModel.FeatureManager
            feature = feature_manager.FeatureRevolve2(
                not params.both_directions,
                True,
                params.thin_feature,
                False,
                params.reverse_direction,
                False,
                adapter.constants["swEndCondBlind"],
                adapter.constants["swEndCondBlind"],
                params.angle * 3.14159 / 180.0,
                (params.angle * 3.14159 / 180.0) if params.both_directions else 0.0,
                False,
                False,
                0.0,
                0.0,
                0,
                (params.thin_thickness or 0.0) / 1000.0,
                0.0,
                params.merge_result,
                False,
                True,
            )

        # IModelDoc2.FeatureRevolve2 returns None (void) on SW 2025
        if not feature and revolve_sw_major != 33:
            raise Exception("Failed to create revolve feature")

        return SolidWorksFeature(
            name=feature.Name if feature else "Revolve-Auto",
            type="Revolve",
            id=adapter._get_feature_id(feature) if feature else "revolve_auto",
            parameters={
                "angle": params.angle,
                "reverse_direction": params.reverse_direction,
                "both_directions": params.both_directions,
                "thin_feature": params.thin_feature,
                "thin_thickness": params.thin_thickness,
            },
            properties={"created": datetime.now().isoformat()},
        )

    return cast(
        AdapterResult[SolidWorksFeature],
        adapter._handle_com_operation("create_revolve", _revolve_operation),
    )

_create_sweep_impl

_create_sweep_impl(adapter: Any, params: SweepParameters) -> AdapterResult[SolidWorksFeature]

Create a swept boss/protrusion from a profile sketch along a path sketch.

Uses IFeatureManager::InsertProtrusionSwept4. Two sketches are required in the active part: a closed profile sketch and an open path sketch named by params.path. The path is selected under mark 4 and the profile under mark 1, per the SolidWorks selection-mark contract for sweeps.

Because :class:SweepParameters only names the path, the profile is inferred as the first ProfileFeature sketch in the feature tree whose name is not the path. In the common "draw profile, draw path, sweep" workflow this is unambiguous (exactly two sketches exist).

Parameters:

Name Type Description Default
adapter Any

A fully connected PyWin32Adapter with a non-None currentModel.

required
params SweepParameters

Sweep parameter bag. Relevant fields: - path (str): Name of the path sketch (e.g. "Sketch2"). - twist_along_path (bool): Apply a constant twist along the path. - twist_angle (float): Twist angle in degrees (used only when twist_along_path is true). - merge_result (bool): Merge with existing bodies.

required

Returns:

Type Description
AdapterResult[SolidWorksFeature]

AdapterResult[SolidWorksFeature]: On success, data is a

AdapterResult[SolidWorksFeature]

SolidWorksFeature whose type is "Sweep". On failure,

AdapterResult[SolidWorksFeature]

status is ERROR with a descriptive message.

Raises:

Type Description
Exception

Propagated through _handle_com_operation when the profile/path cannot be selected or the COM call returns None.

Example::

from solidworks_mcp.adapters.base import SweepParameters

params = SweepParameters(path="Sketch2", merge_result=True)
result = await adapter.create_sweep(params)
print(result.data.name)  # e.g. "Sweep1"
Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _create_sweep_impl(
    adapter: Any, params: SweepParameters
) -> AdapterResult[SolidWorksFeature]:
    """Create a swept boss/protrusion from a profile sketch along a path sketch.

    Uses ``IFeatureManager::InsertProtrusionSwept4``.  Two sketches are
    required in the active part: a closed **profile** sketch and an open
    **path** sketch named by ``params.path``.  The path is selected under
    mark 4 and the profile under mark 1, per the SolidWorks selection-mark
    contract for sweeps.

    Because :class:`SweepParameters` only names the path, the profile is
    inferred as the first ``ProfileFeature`` sketch in the feature tree whose
    name is **not** the path.  In the common "draw profile, draw path, sweep"
    workflow this is unambiguous (exactly two sketches exist).

    Args:
        adapter: A fully connected ``PyWin32Adapter`` with a non-``None``
            ``currentModel``.
        params: Sweep parameter bag.  Relevant fields:
            - ``path`` (str): Name of the path sketch (e.g. ``"Sketch2"``).
            - ``twist_along_path`` (bool): Apply a constant twist along the
              path.
            - ``twist_angle`` (float): Twist angle in **degrees** (used only
              when ``twist_along_path`` is true).
            - ``merge_result`` (bool): Merge with existing bodies.

    Returns:
        AdapterResult[SolidWorksFeature]: On success, ``data`` is a
        ``SolidWorksFeature`` whose ``type`` is ``"Sweep"``.  On failure,
        ``status`` is ``ERROR`` with a descriptive message.

    Raises:
        Exception: Propagated through ``_handle_com_operation`` when the
            profile/path cannot be selected or the COM call returns ``None``.

    Example::

        from solidworks_mcp.adapters.base import SweepParameters

        params = SweepParameters(path="Sketch2", merge_result=True)
        result = await adapter.create_sweep(params)
        print(result.data.name)  # e.g. "Sweep1"
    """
    if not adapter.currentModel:
        return AdapterResult(status=AdapterResultStatus.ERROR, error="No active model")

    if not getattr(params, "path", None):
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="Sweep requires a 'path' sketch name",
        )

    def _sweep_operation() -> SolidWorksFeature:  # pragma: no cover
        """Inner COM closure that selects profile + path and runs the sweep.

        Returns:
            SolidWorksFeature: Populated feature descriptor on success.

        Raises:
            Exception: When selections fail or ``InsertProtrusionSwept4``
                returns ``None``.
        """
        import math

        feature_manager = adapter.currentModel.FeatureManager

        # Resolve the path name against the actual tree sketches so the
        # profile/path comparison is on bare names, then pick the first
        # non-path sketch as the profile.
        sketch_names = _profile_feature_names(adapter)
        path_name = params.path
        for name in sketch_names:
            if name == params.path or name.lower() == params.path.lower():
                path_name = name
                break

        # Profile = the most recently created sketch that isn't the path.
        # Preferring the latest sketch handles both a sketch path (profile is
        # drawn first, so it's the only non-path sketch) and a helix/curve
        # path (the helix's base-circle sketch precedes the profile in the
        # tree, so "first non-path" would wrongly pick the base circle).
        profile_name = None
        last = getattr(adapter, "_last_sketch_name", None)
        if last and last != path_name and last in sketch_names:
            profile_name = last
        if profile_name is None:
            profile_name = next(
                (name for name in reversed(sketch_names) if name != path_name), None
            )
        if profile_name is None:
            raise Exception(
                "Sweep needs a profile sketch distinct from the path "
                f"'{params.path}'. Sketches found: {sketch_names or 'none'}"
            )

        adapter._attempt(
            lambda: adapter.currentModel.ClearSelection2(True), default=None
        )

        if not _select_named_feature(adapter, profile_name, 1, False):
            raise Exception(f"Failed to select sweep profile sketch: {profile_name}")
        if not _select_named_feature(adapter, path_name, 4, True):
            raise Exception(f"Failed to select sweep path: {path_name}")

        twist = bool(getattr(params, "twist_along_path", False))
        twist_angle_deg = float(getattr(params, "twist_angle", 0.0))
        # swTwistControlType_e: 0 = follow path, 8 = constant twist along path.
        twist_ctrl = 8 if twist else 0
        twist_angle_rad = math.radians(twist_angle_deg) if twist else 0.0

        feature = feature_manager.InsertProtrusionSwept4(
            False,  # Propagate to next tangent edge
            False,  # Alignment (go through end faces)
            twist_ctrl,  # TwistCtrlOption (swTwistControlType_e)
            False,  # KeepTangency
            False,  # BAdvancedSmoothing
            0,  # StartMatchingType (swTangencyType_e)
            0,  # EndMatchingType
            False,  # IsThinBody
            0.0,  # Thickness1
            0.0,  # Thickness2
            0,  # ThinType (swThinWallType_e)
            0,  # PathAlign
            bool(getattr(params, "merge_result", True)),  # Merge
            True,  # UseFeatScope
            True,  # UseAutoSelect
            twist_angle_rad,  # TwistAngle (radians)
            True,  # BMergeSmoothFaces
            False,  # CircularProfile
            0.0,  # CircularProfileDiameter
            0,  # Direction
        )

        if not feature:
            raise Exception("Failed to create sweep feature")

        return SolidWorksFeature(
            name=feature.Name,
            type="Sweep",
            id=adapter._get_feature_id(feature),
            parameters={
                "profile": profile_name,
                "path": path_name,
                "twist_along_path": twist,
                "twist_angle": twist_angle_deg,
                "merge_result": bool(getattr(params, "merge_result", True)),
            },
            properties={"created": datetime.now().isoformat()},
        )

    return cast(
        AdapterResult[SolidWorksFeature],
        adapter._handle_com_operation("create_sweep", _sweep_operation),
    )

_delete_feature_impl

_delete_feature_impl(adapter: Any, name: str) -> AdapterResult[dict[str, Any]]

Delete a named feature or sketch from the active model.

Selects the feature, then removes it with IModelDoc2::EditDelete. Deleting a parent also removes its children, which is SolidWorks' normal cascade and the intended "erase this and what depends on it" behaviour - so the number of features removed is reported, not assumed to be one.

EditDelete returns nothing useful, so success is confirmed by the feature being absent from the tree afterwards.

Parameters:

Name Type Description Default
adapter Any

A connected adapter with a valid currentModel.

required
name str

Name of the feature or sketch to delete.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: What was deleted and how the tree

AdapterResult[dict[str, Any]]

changed. ERROR when there is no model or the feature is absent.

Raises:

Type Description
Exception

Propagated through _handle_com_operation.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _delete_feature_impl(adapter: Any, name: str) -> AdapterResult[dict[str, Any]]:
    """Delete a named feature or sketch from the active model.

    Selects the feature, then removes it with ``IModelDoc2::EditDelete``.
    Deleting a parent also removes its children, which is SolidWorks' normal
    cascade and the intended "erase this and what depends on it" behaviour -
    so the number of features removed is reported, not assumed to be one.

    ``EditDelete`` returns nothing useful, so success is confirmed by the
    feature being absent from the tree afterwards.

    Args:
        adapter: A connected adapter with a valid ``currentModel``.
        name: Name of the feature or sketch to delete.

    Returns:
        AdapterResult[dict[str, Any]]: What was deleted and how the tree
        changed. ``ERROR`` when there is no model or the feature is absent.

    Raises:
        Exception: Propagated through ``_handle_com_operation``.
    """
    if not adapter.currentModel:
        return AdapterResult(status=AdapterResultStatus.ERROR, error="No active model")

    def _delete_operation() -> dict[str, Any]:
        before = _feature_count(adapter)
        if _select_feature_by_name(adapter, name) is None:
            raise Exception(f"Feature not found: {name}")

        adapter._attempt(
            lambda: adapter._get_attr_or_call(adapter.currentModel, "EditDelete"),
            default=None,
        )
        adapter._attempt(lambda: adapter.currentModel.EditRebuild3(), default=None)

        # EditDelete reports nothing useful, so confirm by lookup: the
        # feature must no longer resolve by name.
        bare = name.split("@", 1)[0]
        still_there = adapter._attempt(
            lambda: adapter.currentModel.FeatureByName(bare), default=None
        )
        if still_there:
            raise Exception(f"EditDelete did not remove feature: {name}")

        after = _feature_count(adapter)
        return {
            "deleted": name,
            "features_before": before,
            "features_after": after,
            # Deleting a parent takes its children too, so this is often
            # more than one.
            "features_removed": (
                before - after if before is not None and after is not None else None
            ),
        }

    return cast(
        AdapterResult[dict[str, Any]],
        adapter._handle_com_operation("delete_feature", _delete_operation),
    )

_feature_count

_feature_count(adapter: Any) -> int | None

Return how many features the active model has, or None.

Used to confirm an edit changed the tree rather than trusting a COM call that reports success without doing anything.

IFeatureManager::GetFeatureCount is used rather than walking FirstFeature/GetNextFeature: the walk needs each dispatch flagged for IFeature before Name and GetNextFeature resolve, and on SW 2025 it still yields nothing, while the count is a single reliable call. FeatureByPositionReverse was measured returning nothing usable on the same document.

Parameters:

Name Type Description Default
adapter Any

A connected adapter with a valid currentModel.

required

Returns:

Type Description
int | None

int | None: The feature count, or None when it cannot be read -

int | None

which is deliberately distinct from 0.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _feature_count(adapter: Any) -> int | None:
    """Return how many features the active model has, or ``None``.

    Used to confirm an edit changed the tree rather than trusting a COM call
    that reports success without doing anything.

    ``IFeatureManager::GetFeatureCount`` is used rather than walking
    ``FirstFeature``/``GetNextFeature``: the walk needs each dispatch flagged
    for ``IFeature`` before ``Name`` and ``GetNextFeature`` resolve, and on
    SW 2025 it still yields nothing, while the count is a single reliable
    call. ``FeatureByPositionReverse`` was measured returning nothing usable
    on the same document.

    Args:
        adapter: A connected adapter with a valid ``currentModel``.

    Returns:
        int | None: The feature count, or ``None`` when it cannot be read -
        which is deliberately distinct from ``0``.
    """
    from .. import sw_type_info

    manager = adapter._attempt(
        lambda: adapter.currentModel.FeatureManager, default=None
    )
    if manager is None:
        return None
    flagged = sw_type_info.flagged(manager, "IFeatureManager")
    count = adapter._attempt(lambda: flagged.GetFeatureCount(True), default=None)
    return int(count) if isinstance(count, (int, float)) else None

_flag_feature_members

_flag_feature_members(obj: Any, *names: str) -> None

Flag only the named members on obj.

Cheaper than :func:_flag_feature_methods inside a loop: flagging a whole interface (IFeature is ~100 names, ~27 ms) costs a fixed price per object, and sw_type_info's flag cache is keyed by id(obj), so a walk over fresh dispatches — one per feature — never hits it. Flagging just the handful of members about to be read avoids that cost.

Parameters:

Name Type Description Default
obj Any

The COM object (or test double) to flag.

required
*names str

Member names about to be read.

()
Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _flag_feature_members(obj: Any, *names: str) -> None:  # pragma: no cover
    """Flag only the named members on ``obj``.

    Cheaper than :func:`_flag_feature_methods` inside a loop: flagging a
    whole interface (``IFeature`` is ~100 names, ~27 ms) costs a fixed price
    per object, and ``sw_type_info``'s flag cache is keyed by ``id(obj)``, so
    a walk over fresh dispatches — one per feature — never hits it. Flagging
    just the handful of members about to be read avoids that cost.

    Args:
        obj: The COM object (or test double) to flag.
        *names: Member names about to be read.
    """
    try:
        from solidworks_mcp.adapters import sw_type_info

        sw_type_info.flag_members(obj, *names)
    except Exception:
        pass

_flag_feature_methods

_flag_feature_methods(obj: Any, interface: str) -> None

Best-effort method flagging for a COM object via sw_type_info.

Flagging tells pywin32 late binding to resolve names like GetTypeName2 / GetNextFeature / FirstFeature as methods. No-ops on plain test doubles (and any environment without the gen_py wrapper).

Parameters:

Name Type Description Default
obj Any

The COM object (or test double) to flag.

required
interface str

SolidWorks interface name (e.g. "IFeature").

required
Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _flag_feature_methods(obj: Any, interface: str) -> None:  # pragma: no cover
    """Best-effort method flagging for a COM object via ``sw_type_info``.

    Flagging tells pywin32 late binding to resolve names like ``GetTypeName2``
    / ``GetNextFeature`` / ``FirstFeature`` as methods.  No-ops on plain test
    doubles (and any environment without the gen_py wrapper).

    Args:
        obj: The COM object (or test double) to flag.
        interface: SolidWorks interface name (e.g. ``"IFeature"``).
    """
    try:
        from solidworks_mcp.adapters import sw_type_info

        sw_type_info.flag_methods(obj, interface)
    except Exception:
        pass

_is_suppressed

_is_suppressed(adapter: Any, feature: Any) -> bool | None

Return a feature's suppression state, or None when unreadable.

None is deliberately distinct from False: it means the state could not be read, which is not evidence that the feature is unsuppressed.

Parameters:

Name Type Description Default
adapter Any

A connected adapter.

required
feature Any

An IFeature dispatch.

required

Returns:

Type Description
bool | None

bool | None: True suppressed, False not, None unknown.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _is_suppressed(adapter: Any, feature: Any) -> bool | None:
    """Return a feature's suppression state, or ``None`` when unreadable.

    ``None`` is deliberately distinct from ``False``: it means the state could
    not be read, which is not evidence that the feature is unsuppressed.

    Args:
        adapter: A connected adapter.
        feature: An ``IFeature`` dispatch.

    Returns:
        bool | None: ``True`` suppressed, ``False`` not, ``None`` unknown.
    """
    state = adapter._attempt(
        lambda: adapter._get_attr_or_call(feature, "IsSuppressed"), default=None
    )
    if isinstance(state, bool):
        return state
    if isinstance(state, (list, tuple)) and state:
        return bool(state[0])
    if isinstance(state, int):
        return bool(state)
    return None

_mirror_feature_impl

_mirror_feature_impl(adapter: Any, features: list[str], mirror_plane: str, merge: bool, mirror_bodies: bool) -> AdapterResult[dict[str, Any]]

Select the sources and the plane, then call InsertMirrorFeature.

Wraps IFeatureManager::InsertMirrorFeature(BMirrorBody, BGeometryPattern, BMerge, BKnit). There is no ...2 overload of this call. Everything it acts on must be pre-selected under the right mark, so the marks are the whole game - see _MIRROR_MARK_*.

Whether the mirror actually produced geometry is checked by the caller, which can read volume through get_mass_properties.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter with a valid currentModel.

required
features list[str]

Body or feature names to mirror.

required
mirror_plane str

Plane name to mirror about.

required
merge bool

Merge the result with the original.

required
mirror_bodies bool

Select and mirror whole bodies rather than features.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The mirror feature's name and inputs.

Raises:

Type Description
Exception

Propagated through _handle_com_operation.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _mirror_feature_impl(
    adapter: Any,
    features: list[str],
    mirror_plane: str,
    merge: bool,
    mirror_bodies: bool,
) -> AdapterResult[dict[str, Any]]:
    """Select the sources and the plane, then call InsertMirrorFeature.

    Wraps ``IFeatureManager::InsertMirrorFeature(BMirrorBody,
    BGeometryPattern, BMerge, BKnit)``. There is no ``...2`` overload of this
    call. Everything it acts on must be pre-selected under the right mark, so
    the marks are the whole game - see ``_MIRROR_MARK_*``.

    Whether the mirror actually produced geometry is checked by the caller,
    which can read volume through ``get_mass_properties``.

    Args:
        adapter: A connected ``PyWin32Adapter`` with a valid ``currentModel``.
        features: Body or feature names to mirror.
        mirror_plane: Plane name to mirror about.
        merge: Merge the result with the original.
        mirror_bodies: Select and mirror whole bodies rather than features.

    Returns:
        AdapterResult[dict[str, Any]]: The mirror feature's name and inputs.

    Raises:
        Exception: Propagated through ``_handle_com_operation``.
    """
    if not adapter.currentModel:
        return AdapterResult(status=AdapterResultStatus.ERROR, error="No active model")

    names = [n for n in (features or []) if n]
    if not names:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="mirror_feature requires at least one body or feature name",
        )
    if not mirror_plane:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="mirror_feature requires a mirror plane name",
        )

    source_mark = _MIRROR_MARK_BODIES if mirror_bodies else _MIRROR_MARK_FEATURES
    entity_type = "SOLIDBODY" if mirror_bodies else None

    def _mirror_operation() -> dict[str, Any]:
        adapter._attempt(
            lambda: adapter.currentModel.ClearSelection2(True), default=None
        )
        callout = _null_callout()

        for index, name in enumerate(names):
            append = index > 0
            selected = False
            if entity_type is not None:
                selected = bool(
                    adapter._attempt(
                        lambda n=name, a=append: (
                            adapter.currentModel.Extension.SelectByID2(
                                n, entity_type, 0.0, 0.0, 0.0, a, source_mark,
                                callout, 0,
                            )
                        ),
                        default=False,
                    )
                )
            if not selected:
                selected = _select_named_feature(
                    adapter, name, source_mark, append=append
                )
            if not selected:
                kind = "body" if mirror_bodies else "feature"
                raise Exception(f"Failed to select {kind} to mirror: {name}")

        if not _select_reference_entity(
            adapter, mirror_plane, _MIRROR_MARK_PLANE, append=True
        ):
            raise Exception(f"Failed to select mirror plane: {mirror_plane}")

        from .. import sw_type_info

        feature_manager = sw_type_info.flagged(
            adapter.currentModel.FeatureManager, "IFeatureManager"
        )
        feature = adapter._attempt(
            lambda: feature_manager.InsertMirrorFeature(
                bool(mirror_bodies),  # BMirrorBody
                False,  # BGeometryPattern - solve the whole feature
                bool(merge),  # BMerge
                False,  # BKnit
            ),
            default=None,
        )
        if feature is None:
            raise Exception(
                f"InsertMirrorFeature returned nothing (sources={names}, "
                f"plane={mirror_plane})"
            )

        name = adapter._attempt(
            lambda: adapter._get_attr_or_call(feature, "Name"), default=None
        )
        return {
            # No invented fallback: an unreadable name is reported as None
            # rather than as the literal "Mirror", which would not resolve.
            "name": str(name) if name else None,
            "mirrored": names,
            "mirror_plane": mirror_plane,
            "merge": bool(merge),
            "mirror_bodies": bool(mirror_bodies),
        }

    return cast(
        AdapterResult[dict[str, Any]],
        adapter._handle_com_operation("mirror_feature", _mirror_operation),
    )

_null_callout

_null_callout() -> Any

Return a VT_DISPATCH null for SelectByID2's Callout parameter.

A plain Python None marshals as VT_NULL, which SolidWorks rejects with DISP_E_TYPEMISMATCH - measured as (-2147352571, 'Type mismatch.', None, 8). See runbook item 11.

Returns:

Name Type Description
Any Any

A VARIANT(VT_DISPATCH, None), or a non-None sentinel

Any

when pywin32 is unavailable (mock/Linux runs, where no real COM call

Any

will be made anyway, but the value must still never be bare None).

Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _null_callout() -> Any:
    """Return a VT_DISPATCH null for ``SelectByID2``'s ``Callout`` parameter.

    A plain Python ``None`` marshals as ``VT_NULL``, which SolidWorks rejects
    with ``DISP_E_TYPEMISMATCH`` - measured as
    ``(-2147352571, 'Type mismatch.', None, 8)``. See runbook item 11.

    Returns:
        Any: A ``VARIANT(VT_DISPATCH, None)``, or a non-``None`` sentinel
        when pywin32 is unavailable (mock/Linux runs, where no real COM call
        will be made anyway, but the value must still never be bare ``None``).
    """
    try:
        import pythoncom
        import win32com.client as _win32com
    except ImportError:  # pragma: no cover - Windows-only path
        return _NULL_CALLOUT_SENTINEL
    return _win32com.VARIANT(pythoncom.VT_DISPATCH, None)

_offset_plane_distance

_offset_plane_distance(offset_mm: float, base_flip: bool) -> tuple[float, int]

Resolve an offset-plane Distance constraint to (distance_m, flip_bits).

IFeatureManager.InsertRefPlane's Distance constraint takes a positive magnitude; the side of the base plane is chosen by the OptionFlip bit, not by the sign of the distance. Passing a negative distance does not place the plane on the opposite side - SolidWorks clamps it to 0, collapsing the new plane onto the base. So a negative offset_mm must be converted to a positive magnitude with the flip bit toggled relative to the caller's flip request (a negative offset plus flip=True cancel out).

Credit: this fix (and the pure-helper extraction for direct unit coverage) ports the sign-resolution logic independently found and fixed by contributor @pedropaulovc in their fork (commit 3c091fd). See issue #84.

Parameters:

Name Type Description Default
offset_mm float

Signed offset from the base plane, in millimetres.

required
base_flip bool

Whether the caller requested flip.

required

Returns:

Type Description
float

tuple[float, int]: A non-negative distance in metres, and the flip

int

bits (_REF_PLANE_OPTION_FLIP or 0) to OR into the constraint.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _offset_plane_distance(offset_mm: float, base_flip: bool) -> tuple[float, int]:
    """Resolve an offset-plane Distance constraint to ``(distance_m, flip_bits)``.

    ``IFeatureManager.InsertRefPlane``'s Distance constraint takes a positive
    magnitude; the *side* of the base plane is chosen by the ``OptionFlip``
    bit, not by the sign of the distance. Passing a negative distance does
    not place the plane on the opposite side - SolidWorks clamps it to 0,
    collapsing the new plane onto the base. So a negative ``offset_mm`` must
    be converted to a positive magnitude with the flip bit toggled relative
    to the caller's ``flip`` request (a negative offset plus ``flip=True``
    cancel out).

    Credit: this fix (and the pure-helper extraction for direct unit
    coverage) ports the sign-resolution logic independently found and fixed
    by contributor @pedropaulovc in their fork (commit ``3c091fd``). See
    issue #84.

    Args:
        offset_mm: Signed offset from the base plane, in millimetres.
        base_flip: Whether the caller requested ``flip``.

    Returns:
        tuple[float, int]: A non-negative distance in metres, and the flip
        bits (``_REF_PLANE_OPTION_FLIP`` or 0) to OR into the constraint.
    """
    distance_m = float(offset_mm) / 1000.0
    flip_bits = _REF_PLANE_OPTION_FLIP if base_flip else 0
    if distance_m < 0.0:
        distance_m = -distance_m
        flip_bits ^= _REF_PLANE_OPTION_FLIP
    return distance_m, flip_bits

_parse_edge_spec

_parse_edge_spec(edge_name: str) -> tuple[str, float, float, float, str]

Parse an edge/face specification string into (name, x, y, z, entity_type).

Supports three formats: - "Edge<1>" — name-based EDGE selection (x=y=z=0.0, SW looks up by topology name) - "x,y,z" — coordinate-based EDGE selection (name="", coordinate hint in metres) - "face:x,y,z" (case-insensitive prefix) — coordinate-based FACE selection. Selecting a whole face and handing it to InsertFeatureChamfer/FeatureFillet3 chamfers/fillets every edge bounding that face in one call. This is more robust than targeting a single edge by coordinate: a point exactly on a shared boundary between two faces (e.g. a hole rim flush with the top surface) can have its nearest-edge resolution silently shift to an unrelated edge after another feature is added earlier in the tree, picking a face interior point has no such ambiguity. Confirmed live 2026-09-18: an edge-coordinate hole-rim chamfer silently landed on the block's 30mm outer edge instead (volume delta matched the straight-edge formula, not the circular-rim one); selecting the face instead reliably chamfers the real rim.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _parse_edge_spec(
    edge_name: str,
) -> tuple[str, float, float, float, str]:
    """Parse an edge/face specification string into (name, x, y, z, entity_type).

    Supports three formats:
    - ``"Edge<1>"`` — name-based EDGE selection (x=y=z=0.0, SW looks up by
      topology name)
    - ``"x,y,z"`` — coordinate-based EDGE selection (name="", coordinate hint
      in metres)
    - ``"face:x,y,z"`` (case-insensitive prefix) — coordinate-based FACE
      selection. Selecting a whole face and handing it to
      ``InsertFeatureChamfer``/``FeatureFillet3`` chamfers/fillets every
      edge bounding that face in one call. This is more robust than
      targeting a single edge by coordinate: a point exactly on a shared
      boundary between two faces (e.g. a hole rim flush with the top
      surface) can have its nearest-edge resolution silently shift to an
      unrelated edge after another feature is added earlier in the tree,
      picking a face interior point has no such ambiguity. Confirmed live
      2026-09-18: an edge-coordinate hole-rim chamfer silently landed on
      the block's 30mm outer edge instead (volume delta matched the
      straight-edge formula, not the circular-rim one); selecting the face
      instead reliably chamfers the real rim.
    """
    prefix, _, rest = edge_name.partition(":")
    if prefix.strip().lower() == "face":
        parts = rest.split(",")
        if len(parts) == 3:
            try:
                x, y, z = float(parts[0]), float(parts[1]), float(parts[2])
                return "", x, y, z, "FACE"
            except ValueError:
                pass
        return rest, 0.0, 0.0, 0.0, "FACE"

    parts = edge_name.split(",")
    if len(parts) == 3:
        try:
            x, y, z = float(parts[0]), float(parts[1]), float(parts[2])
            return "", x, y, z, "EDGE"
        except ValueError:
            pass
    return edge_name, 0.0, 0.0, 0.0, "EDGE"

_pattern_circular_impl

_pattern_circular_impl(adapter: Any, features: list[str], axis: str, count: int, angle: float, equal_spacing: bool) -> AdapterResult[dict[str, Any]]

Select the axis and features, then call FeatureCircularPattern5.

This build exposes FeatureCircularPattern through ...5; v5 is used, with the 14 arguments it declares. DName is a direction name string, so the axis is both selected under mark 1 and named in the call.

Whether anything was produced is checked by the caller, which can read volume through get_mass_properties.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter with a valid currentModel.

required
features list[str]

Feature names to pattern.

required
axis str

Axis name to rotate about.

required
count int

Total instances including the original.

required
angle float

Total spread in degrees.

required
equal_spacing bool

Space instances evenly across angle.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The pattern's name and inputs.

Raises:

Type Description
Exception

Propagated through _handle_com_operation.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _pattern_circular_impl(
    adapter: Any,
    features: list[str],
    axis: str,
    count: int,
    angle: float,
    equal_spacing: bool,
) -> AdapterResult[dict[str, Any]]:
    """Select the axis and features, then call FeatureCircularPattern5.

    This build exposes FeatureCircularPattern through ...5; v5 is used, with
    the 14 arguments it declares. ``DName`` is a direction *name string*, so
    the axis is both selected under mark 1 and named in the call.

    Whether anything was produced is checked by the caller, which can read
    volume through ``get_mass_properties``.

    Args:
        adapter: A connected ``PyWin32Adapter`` with a valid ``currentModel``.
        features: Feature names to pattern.
        axis: Axis name to rotate about.
        count: Total instances including the original.
        angle: Total spread in degrees.
        equal_spacing: Space instances evenly across ``angle``.

    Returns:
        AdapterResult[dict[str, Any]]: The pattern's name and inputs.

    Raises:
        Exception: Propagated through ``_handle_com_operation``.
    """
    if not adapter.currentModel:
        return AdapterResult(status=AdapterResultStatus.ERROR, error="No active model")

    names = [n for n in (features or []) if n]
    if not names:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="pattern_circular requires at least one feature name",
        )
    if not axis:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="pattern_circular requires an axis name to rotate about",
        )
    if int(count) < 2:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=(
                f"pattern_circular needs a count of at least 2 (got {count}); "
                "a pattern of one is just the original feature"
            ),
        )

    def _pattern_operation() -> dict[str, Any]:
        import math

        adapter._attempt(
            lambda: adapter.currentModel.ClearSelection2(True), default=None
        )
        callout = _null_callout()

        axis_selected = bool(
            adapter._attempt(
                lambda: adapter.currentModel.Extension.SelectByID2(
                    axis, "AXIS", 0.0, 0.0, 0.0, False, _PATTERN_MARK_AXIS, callout, 0
                ),
                default=False,
            )
        )
        if not axis_selected:
            raise Exception(
                f"Failed to select axis '{axis}'. Create one with create_axis "
                "first, and pass the name it returns."
            )

        for name in names:
            selected = bool(
                adapter._attempt(
                    lambda n=name: adapter.currentModel.Extension.SelectByID2(
                        n, "BODYFEATURE", 0.0, 0.0, 0.0, True,
                        _PATTERN_MARK_FEATURES, callout, 0,
                    ),
                    default=False,
                )
            )
            if not selected:
                selected = _select_named_feature(
                    adapter, name, _PATTERN_MARK_FEATURES, append=True
                )
            if not selected:
                raise Exception(f"Failed to select feature to pattern: {name}")

        from .. import sw_type_info

        feature_manager = sw_type_info.flagged(
            adapter.currentModel.FeatureManager, "IFeatureManager"
        )
        feature = adapter._attempt(
            lambda: feature_manager.FeatureCircularPattern5(
                int(count),                      # Number
                math.radians(float(angle)),      # Spacing (radians)
                False,                           # FlipDirection
                axis,                            # DName
                False,                           # GeometryPattern
                bool(equal_spacing),             # EqualSpacing
                False,                           # VaryInstance
                False,                           # SyncSubAssemblies
                False,                           # BDir2
                False,                           # BSymmetric
                0,                               # Number2
                0.0,                             # Spacing2
                "",                              # DName2
                False,                           # EqualSpacing2
            ),
            default=None,
        )
        if feature is None:
            raise Exception(
                f"FeatureCircularPattern5 returned nothing (features={names}, "
                f"axis={axis})"
            )

        pattern_name = adapter._attempt(
            lambda: adapter._get_attr_or_call(feature, "Name"), default=None
        )
        return {
            "name": str(pattern_name) if pattern_name else None,
            "patterned": names,
            "axis": axis,
            "count": int(count),
            "angle": float(angle),
            "equal_spacing": bool(equal_spacing),
        }

    return cast(
        AdapterResult[dict[str, Any]],
        adapter._handle_com_operation("pattern_circular", _pattern_operation),
    )

_profile_feature_names

_profile_feature_names(adapter: Any) -> list[str]

Return sketch (ProfileFeature) names in feature-tree order.

Walks FirstFeature -> GetNextFeature reading GetTypeName2 and collecting features whose type is "ProfileFeature" (a 2D/3D sketch). Mirrors the tree walk used by :func:_create_cut_extrude_impl, but flags each feature for IFeature and reads members through :func:_read_member so it is robust to pywin32's method-vs-property late-binding ambiguity.

Parameters:

Name Type Description Default
adapter Any

A connected adapter with a valid currentModel.

required

Returns:

Type Description
list[str]

list[str]: Bare sketch names, earliest first. Empty when the walk

list[str]

finds no sketches or the tree is inaccessible.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _profile_feature_names(adapter: Any) -> list[str]:  # pragma: no cover
    """Return sketch (``ProfileFeature``) names in feature-tree order.

    Walks ``FirstFeature`` -> ``GetNextFeature`` reading ``GetTypeName2`` and
    collecting features whose type is ``"ProfileFeature"`` (a 2D/3D sketch).
    Mirrors the tree walk used by :func:`_create_cut_extrude_impl`, but flags
    each feature for ``IFeature`` and reads members through
    :func:`_read_member` so it is robust to pywin32's method-vs-property
    late-binding ambiguity.

    Args:
        adapter: A connected adapter with a valid ``currentModel``.

    Returns:
        list[str]: Bare sketch names, earliest first.  Empty when the walk
        finds no sketches or the tree is inaccessible.
    """
    names: list[str] = []
    try:
        _flag_feature_methods(adapter.currentModel, "IModelDoc2")
        feat = _read_member(adapter.currentModel, "FirstFeature")
        # Bound the walk so a misbehaving GetNextFeature can't spin forever.
        for _ in range(5000):
            if not feat:
                break
            _flag_feature_members(feat, *_TREE_WALK_MEMBERS)
            try:
                if _read_member(feat, "GetTypeName2") == "ProfileFeature":
                    names.append(str(_read_member(feat, "Name")))
            except Exception:
                pass
            try:
                feat = _read_member(feat, "GetNextFeature")
            except Exception:
                break
    except Exception:
        pass
    return names

_read_member

_read_member(obj: Any, name: str) -> Any

Read a COM member that pywin32 may expose as a property or a method.

Late-bound pywin32 dispatches are inconsistent: an unflagged zero-arg accessor may come back as a bound method (needing a call) or as the already-resolved value — and when that value is itself a COM object it is also callable, so a naive "call if callable" check wrongly invokes its default dispatch (Member not found). This helper calls the member and falls back to the raw member if the call raises, so it yields the value in every case (flagged method, unflagged method, property-returning-object, or plain test double).

Parameters:

Name Type Description Default
obj Any

The COM object (or test double) to read from.

required
name str

Member name.

required

Returns:

Name Type Description
Any Any

The member's value, or None when the attribute is absent.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _read_member(obj: Any, name: str) -> Any:  # pragma: no cover
    """Read a COM member that pywin32 may expose as a property *or* a method.

    Late-bound pywin32 dispatches are inconsistent: an unflagged zero-arg
    accessor may come back as a bound method (needing a call) *or* as the
    already-resolved value — and when that value is itself a COM object it is
    also callable, so a naive "call if callable" check wrongly invokes its
    default dispatch (``Member not found``).  This helper calls the member and
    falls back to the raw member if the call raises, so it yields the value in
    every case (flagged method, unflagged method, property-returning-object,
    or plain test double).

    Args:
        obj: The COM object (or test double) to read from.
        name: Member name.

    Returns:
        Any: The member's value, or ``None`` when the attribute is absent.
    """
    member = getattr(obj, name, None)
    if not callable(member):
        return member
    try:
        return member()
    except Exception:
        return member

_rename_feature_impl

_rename_feature_impl(adapter: Any, old_name: str, new_name: str) -> AdapterResult[dict[str, Any]]

Rename a named feature in the active model.

IFeature::Name is a settable property, not a method - it is assigned directly, never called (see the property-vs-method flagging note in the COM threading section of CLAUDE.md). SolidWorks silently refuses a rename onto a name another feature already uses, so a clash is checked for first and the new name is read back afterwards; a mismatch is reported as a failure rather than a quiet success.

Parameters:

Name Type Description Default
adapter Any

A connected adapter with a valid currentModel.

required
old_name str

The feature's current name. Any name@document qualifier is stripped.

required
new_name str

The name to give it.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: renamed (bool), old_name and

AdapterResult[dict[str, Any]]

new_name. renamed is False with a reason when the two

AdapterResult[dict[str, Any]]

names are the same. ERROR when there is no model, the feature is

AdapterResult[dict[str, Any]]

absent, the target name is taken, or the rename did not take.

Raises:

Type Description
Exception

Propagated through _handle_com_operation.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _rename_feature_impl(
    adapter: Any, old_name: str, new_name: str
) -> AdapterResult[dict[str, Any]]:
    """Rename a named feature in the active model.

    ``IFeature::Name`` is a settable property, not a method - it is assigned
    directly, never called (see the property-vs-method flagging note in the
    COM threading section of ``CLAUDE.md``). SolidWorks silently refuses a
    rename onto a name another feature already uses, so a clash is checked
    for first and the new name is read back afterwards; a mismatch is
    reported as a failure rather than a quiet success.

    Args:
        adapter: A connected adapter with a valid ``currentModel``.
        old_name: The feature's current name. Any ``name@document`` qualifier
            is stripped.
        new_name: The name to give it.

    Returns:
        AdapterResult[dict[str, Any]]: ``renamed`` (bool), ``old_name`` and
        ``new_name``. ``renamed`` is ``False`` with a ``reason`` when the two
        names are the same. ``ERROR`` when there is no model, the feature is
        absent, the target name is taken, or the rename did not take.

    Raises:
        Exception: Propagated through ``_handle_com_operation``.
    """
    # Resync from ActiveDoc: the user may have switched documents in the
    # SolidWorks UI since the last tool call (issue #91).
    sync = getattr(adapter, "_sync_current_model_from_active", None)
    if callable(sync):
        adapter._attempt(sync, default=None)
    if not adapter.currentModel:
        return AdapterResult(status=AdapterResultStatus.ERROR, error="No active model")

    new = new_name.split("@", 1)[0].strip()
    if not new:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="new_name must not be empty"
        )

    def _rename_operation() -> dict[str, Any]:
        bare = old_name.split("@", 1)[0]
        feature = adapter._attempt(
            lambda: adapter.currentModel.FeatureByName(bare), default=None
        )
        if not feature:
            raise Exception(f"Feature not found: {old_name}")

        if new == bare:
            return {
                "renamed": False,
                "old_name": bare,
                "new_name": new,
                "reason": "old and new names are the same",
            }

        # SolidWorks silently ignores a rename onto an existing feature name,
        # which would otherwise look like a success. Catch it up front.
        clash = adapter._attempt(
            lambda: adapter.currentModel.FeatureByName(new), default=None
        )
        if clash:
            raise Exception(
                f"Cannot rename to {new!r}: a feature with that name already exists"
            )

        _, err = adapter._attempt_with_error(lambda: setattr(feature, "Name", new))
        if err is not None:
            raise Exception(f"Failed to rename {bare} to {new}: {err}")

        adapter._attempt(lambda: adapter.currentModel.EditRebuild3(), default=None)

        refreshed = adapter._attempt(
            lambda: adapter.currentModel.FeatureByName(new), default=None
        )
        if refreshed is None:
            raise Exception(
                f"Rename to {new} was accepted but no feature resolves by that "
                "name afterwards; the call did not take."
            )
        actual = adapter._attempt(
            lambda: adapter._get_attr_or_call(refreshed, "Name"), default=None
        )
        return {
            "renamed": True,
            "old_name": bare,
            "new_name": actual if isinstance(actual, str) and actual else new,
        }

    return cast(
        AdapterResult[dict[str, Any]],
        adapter._handle_com_operation("rename_feature", _rename_operation),
    )

_select_edge_by_coord

_select_edge_by_coord(adapter: Any, x: float, y: float, z: float, append: bool, mark: int = 0, entity_type: str = 'EDGE') -> bool

Select the edge (or face) nearest to (x, y, z) in metres via SelectByID2.

Calls ForceRebuild3 on the first entity in the selection set so that recent features are fully tessellated and selectable. Tries the primary coordinate and several small radial/Y offsets to improve hit probability on curved edges.

entity_type is normally "EDGE", but pass "FACE" to select a whole face instead — InsertFeatureChamfer/FeatureFillet3 then chamfer/fillet every edge bounding that face. Face selection has no equivalent to the edge case's boundary ambiguity (see _parse_edge_spec).

The Callout parameter of SelectByID2 requires a VT_DISPATCH null VARIANT — passing plain Python None triggers DISP_E_TYPEMISMATCH.

Returns True if an entity was successfully selected; False otherwise.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _select_edge_by_coord(  # pragma: no cover
    adapter: Any,
    x: float,
    y: float,
    z: float,
    append: bool,
    mark: int = 0,
    entity_type: str = "EDGE",
) -> bool:
    """Select the edge (or face) nearest to (x, y, z) in metres via ``SelectByID2``.

    Calls ``ForceRebuild3`` on the first entity in the selection set so that
    recent features are fully tessellated and selectable. Tries the primary
    coordinate and several small radial/Y offsets to improve hit probability
    on curved edges.

    ``entity_type`` is normally ``"EDGE"``, but pass ``"FACE"`` to select a
    whole face instead — ``InsertFeatureChamfer``/``FeatureFillet3`` then
    chamfer/fillet every edge bounding that face. Face selection has no
    equivalent to the edge case's boundary ambiguity (see ``_parse_edge_spec``).

    The ``Callout`` parameter of ``SelectByID2`` requires a VT_DISPATCH null
    VARIANT — passing plain Python ``None`` triggers DISP_E_TYPEMISMATCH.

    Returns True if an entity was successfully selected; False otherwise.
    """
    import math

    model = adapter.currentModel

    # Rebuild to tessellate geometry from recent features before the first
    # entity in the selection set (when append=False this is the first one).
    if not append:
        adapter._attempt(lambda: model.ForceRebuild3(True), default=None)

    r = math.sqrt(x**2 + z**2)
    if r > 0:
        # Candidates: exact point plus small radial scale-in/out and Y offsets.
        candidates = [
            (x, y, z),
            (x * 0.999, y, z * 0.999),
            (x * 1.001, y, z * 1.001),
            (x * 0.997, y, z * 0.997),
            (x * 1.003, y, z * 1.003),
            (x, y * 0.999, z),
            (x, y * 1.001, z),
        ]
    else:
        candidates = [
            (x, y, z),
            (x, y * 0.999, z),
            (x, y * 1.001, z),
        ]

    for cx, cy, cz in candidates:
        try:
            selected = bool(
                model.Extension.SelectByID2(
                    "", entity_type, cx, cy, cz, append, mark, _null_callout(), 0
                )
            )
        except Exception:
            selected = False
        if selected:
            return True

    return False

_select_feature_by_name

_select_feature_by_name(adapter: Any, name: str) -> Any

Select a feature or sketch by name and return it.

Resolves the entity with IModelDoc2::FeatureByName and selects it with IFeature::Select2(False, 0) - the same path used elsewhere in this adapter. SelectByID2 is avoided here because it needs an entity-type string that differs between sketches and solid features, and because its Callout argument raises Type mismatch unless passed an explicit VT_DISPATCH null. Any name@document qualifier is stripped first.

Parameters:

Name Type Description Default
adapter Any

A connected adapter with a valid currentModel.

required
name str

Feature or sketch name, e.g. "Boss-Extrude1" or "Sketch1".

required

Returns:

Name Type Description
Any Any

The selected IFeature, or None when it does not exist or

Any

could not be selected.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _select_feature_by_name(adapter: Any, name: str) -> Any:
    """Select a feature or sketch by name and return it.

    Resolves the entity with ``IModelDoc2::FeatureByName`` and selects it with
    ``IFeature::Select2(False, 0)`` - the same path used elsewhere in this
    adapter. ``SelectByID2`` is avoided here because it needs an entity-type
    string that differs between sketches and solid features, and because its
    ``Callout`` argument raises ``Type mismatch`` unless passed an explicit
    ``VT_DISPATCH`` null. Any ``name@document`` qualifier is stripped first.

    Args:
        adapter: A connected adapter with a valid ``currentModel``.
        name: Feature or sketch name, e.g. ``"Boss-Extrude1"`` or ``"Sketch1"``.

    Returns:
        Any: The selected ``IFeature``, or ``None`` when it does not exist or
        could not be selected.
    """
    bare = name.split("@", 1)[0]
    adapter._attempt(lambda: adapter.currentModel.ClearSelection2(True), default=None)
    feature = adapter._attempt(
        lambda: adapter.currentModel.FeatureByName(bare), default=None
    )
    if not feature:
        return None
    selected = adapter._attempt(lambda: feature.Select2(False, 0), default=False)
    return feature if selected else None

_select_named_feature

_select_named_feature(adapter: Any, name: str, mark: int, append: bool) -> bool

Select a named feature under a specific selection mark via Select2.

Sweep and loft rely on selection marks to tell SolidWorks which selection is the profile (1), guide curve (2), or sweep path (4). We resolve the feature with IModelDoc2::FeatureByName and select it with IFeature::Select2(append, mark) — the same proven path the rest of the adapter uses for plane/sketch selection. IModelDocExtension::SelectByID2 is avoided deliberately: late-bound SelectByID2 raises Type mismatch on some SolidWorks builds, whereas FeatureByName + Select2 is reliable, works for sketches and reference curves such as a helix, and needs no entity-type string.

Parameters:

Name Type Description Default
adapter Any

A connected adapter with a valid currentModel.

required
name str

Feature name (e.g. "Sketch1" or "Helix/Spiral1"). Any @document qualifier is stripped before lookup.

required
mark int

Selection mark — 1=profile, 2=guide curve, 4=sweep path.

required
append bool

True to add to the current selection set, False to replace it.

required

Returns:

Name Type Description
bool bool

True when the feature was found and selected.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _select_named_feature(
    adapter: Any,
    name: str,
    mark: int,
    append: bool,
) -> bool:
    """Select a named feature under a specific selection mark via ``Select2``.

    Sweep and loft rely on selection marks to tell SolidWorks which selection
    is the profile (1), guide curve (2), or sweep path (4).  We resolve the
    feature with ``IModelDoc2::FeatureByName`` and select it with
    ``IFeature::Select2(append, mark)`` — the same proven path the rest of the
    adapter uses for plane/sketch selection.  ``IModelDocExtension::SelectByID2``
    is avoided deliberately: late-bound ``SelectByID2`` raises
    ``Type mismatch`` on some SolidWorks builds, whereas ``FeatureByName`` +
    ``Select2`` is reliable, works for sketches *and* reference curves such as
    a helix, and needs no entity-type string.

    Args:
        adapter: A connected adapter with a valid ``currentModel``.
        name: Feature name (e.g. ``"Sketch1"`` or ``"Helix/Spiral1"``).  Any
            ``@document`` qualifier is stripped before lookup.
        mark: Selection mark — 1=profile, 2=guide curve, 4=sweep path.
        append: ``True`` to add to the current selection set, ``False`` to
            replace it.

    Returns:
        bool: ``True`` when the feature was found and selected.
    """
    bare = name.split("@", 1)[0]
    feature = adapter._attempt(
        lambda: adapter.currentModel.FeatureByName(bare), default=None
    )
    if not feature:
        return False
    return bool(adapter._attempt(lambda: feature.Select2(append, mark), default=False))

_select_reference_entity

_select_reference_entity(adapter: Any, reference: str, mark: int, append: bool) -> bool

Select a named plane or planar face under a given selection mark.

Prefers FeatureByName + IFeature::Select2, which is reliable across builds, and falls back to SelectByID2 for planar faces that are not named tree features.

Parameters:

Name Type Description Default
adapter Any

A connected adapter with a valid currentModel.

required
reference str

Plane or face name, e.g. "Front Plane" or "Plane1".

required
mark int

Selection mark the SolidWorks call expects for this role.

required
append bool

Add to the current selection rather than replacing it.

required

Returns:

Name Type Description
bool bool

True when the entity was selected.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _select_reference_entity(adapter: Any, reference: str, mark: int, append: bool) -> bool:
    """Select a named plane or planar face under a given selection mark.

    Prefers ``FeatureByName`` + ``IFeature::Select2``, which is reliable
    across builds, and falls back to ``SelectByID2`` for planar faces that are
    not named tree features.

    Args:
        adapter: A connected adapter with a valid ``currentModel``.
        reference: Plane or face name, e.g. ``"Front Plane"`` or ``"Plane1"``.
        mark: Selection mark the SolidWorks call expects for this role.
        append: Add to the current selection rather than replacing it.

    Returns:
        bool: True when the entity was selected.
    """
    if _select_named_feature(adapter, reference, mark, append=append):
        return True

    callout = _null_callout()
    for entity_type in ("PLANE", "FACE"):
        selected = adapter._attempt(
            lambda t=entity_type: adapter.currentModel.Extension.SelectByID2(
                reference, t, 0.0, 0.0, 0.0, append, mark, callout, 0
            ),
            default=False,
        )
        if selected:
            return True
    return False

_suppress_feature_impl

_suppress_feature_impl(adapter: Any, name: str, suppress: bool) -> AdapterResult[dict[str, Any]]

Suppress or unsuppress a named feature in the active model.

Suppressing rolls a feature and its children out of the model without deleting it - the reversible way to turn off a bad feature.

EditSuppress2 and EditUnsuppress2 report through the error channel only, so the resulting state is read back from IFeature::IsSuppressed rather than inferred from the call returning quietly.

Parameters:

Name Type Description Default
adapter Any

A connected adapter with a valid currentModel.

required
name str

Feature name to toggle.

required
suppress bool

True to suppress, False to unsuppress.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The feature and its state afterwards.

AdapterResult[dict[str, Any]]

suppressed is None when the state could not be read back,

AdapterResult[dict[str, Any]]

which is not the same as "not suppressed".

Raises:

Type Description
Exception

Propagated through _handle_com_operation.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _suppress_feature_impl(
    adapter: Any, name: str, suppress: bool
) -> AdapterResult[dict[str, Any]]:
    """Suppress or unsuppress a named feature in the active model.

    Suppressing rolls a feature and its children out of the model without
    deleting it - the reversible way to turn off a bad feature.

    ``EditSuppress2`` and ``EditUnsuppress2`` report through the error channel
    only, so the resulting state is read back from ``IFeature::IsSuppressed``
    rather than inferred from the call returning quietly.

    Args:
        adapter: A connected adapter with a valid ``currentModel``.
        name: Feature name to toggle.
        suppress: ``True`` to suppress, ``False`` to unsuppress.

    Returns:
        AdapterResult[dict[str, Any]]: The feature and its state afterwards.
        ``suppressed`` is ``None`` when the state could not be read back,
        which is not the same as "not suppressed".

    Raises:
        Exception: Propagated through ``_handle_com_operation``.
    """
    if not adapter.currentModel:
        return AdapterResult(status=AdapterResultStatus.ERROR, error="No active model")

    def _suppress_operation() -> dict[str, Any]:
        feature = _select_feature_by_name(adapter, name)
        if feature is None:
            raise Exception(f"Feature not found: {name}")

        was = _is_suppressed(adapter, feature)
        # EditSuppress2/EditUnsuppress2 take no arguments, and late binding
        # resolves them as properties on some dispatches: calling with ()
        # performs the edit and *then* raises "'bool' object is not callable",
        # so the operation succeeds while the caller sees a failure.
        member = "EditSuppress2" if suppress else "EditUnsuppress2"
        _, err = adapter._attempt_with_error(
            lambda: adapter._get_attr_or_call(adapter.currentModel, member)
        )
        if err is not None:
            action = "suppress" if suppress else "unsuppress"
            raise Exception(f"Failed to {action} {name}: {err}")

        adapter._attempt(lambda: adapter.currentModel.EditRebuild3(), default=None)

        # Re-resolve: the dispatch held above can go stale across a rebuild.
        refreshed = adapter._attempt(
            lambda: adapter.currentModel.FeatureByName(name.split("@", 1)[0]),
            default=None,
        )
        now = _is_suppressed(adapter, refreshed) if refreshed is not None else None
        if now is not None and now != suppress:
            action = "suppressed" if suppress else "unsuppressed"
            raise Exception(
                f"{name} reports suppressed={now} after being {action}; the "
                "call was accepted but the feature state did not change."
            )

        return {
            "feature": name,
            "requested": suppress,
            "suppressed": now,
            "was_suppressed": was,
        }

    return cast(
        AdapterResult[dict[str, Any]],
        adapter._handle_com_operation("suppress_feature", _suppress_operation),
    )

_undo_impl

_undo_impl(adapter: Any, count: int) -> AdapterResult[dict[str, Any]]

Undo the last count operations in the active model.

IModelDoc2::EditUndo2(Steps) is a Sub - it returns nothing - so a quiet call means only that it was made, not that anything was undone. SolidWorks accepts an undo with an empty stack without complaint. The feature tree is therefore compared before and after, and the payload says plainly whether it changed.

Parameters:

Name Type Description Default
adapter Any

A connected adapter with a valid currentModel.

required
count int

Number of operations to undo; values below 1 are clamped.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: How many steps were requested and

AdapterResult[dict[str, Any]]

whether the feature tree actually changed.

Raises:

Type Description
Exception

Propagated through _handle_com_operation.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _undo_impl(adapter: Any, count: int) -> AdapterResult[dict[str, Any]]:
    """Undo the last ``count`` operations in the active model.

    ``IModelDoc2::EditUndo2(Steps)`` is a ``Sub`` - it returns nothing - so a
    quiet call means only that it was made, not that anything was undone.
    SolidWorks accepts an undo with an empty stack without complaint. The
    feature tree is therefore compared before and after, and the payload says
    plainly whether it changed.

    Args:
        adapter: A connected adapter with a valid ``currentModel``.
        count: Number of operations to undo; values below 1 are clamped.

    Returns:
        AdapterResult[dict[str, Any]]: How many steps were requested and
        whether the feature tree actually changed.

    Raises:
        Exception: Propagated through ``_handle_com_operation``.
    """
    if not adapter.currentModel:
        return AdapterResult(status=AdapterResultStatus.ERROR, error="No active model")

    steps = max(1, int(count))

    def _undo_operation() -> dict[str, Any]:
        before = _feature_count(adapter)

        _, err = adapter._attempt_with_error(
            lambda: adapter.currentModel.EditUndo2(steps)
        )
        if err is not None:
            # Older builds expose only the unsuffixed overload.
            _, legacy_err = adapter._attempt_with_error(
                lambda: adapter.currentModel.EditUndo(steps)
            )
            if legacy_err is not None:
                raise Exception(f"Undo failed: {err} | legacy: {legacy_err}")

        adapter._attempt(lambda: adapter.currentModel.EditRebuild3(), default=None)
        after = _feature_count(adapter)

        # None means the count could not be read, which is not evidence that
        # nothing happened - so tree_changed stays None rather than False.
        changed = (
            None
            if before is None or after is None
            else before != after
        )
        return {
            "requested_steps": steps,
            "features_before": before,
            "features_after": after,
            # False means the call was accepted and the tree is unchanged -
            # an undo with nothing left to undo. Reported rather than dressed
            # up as a success.
            "tree_changed": changed,
        }

    return cast(
        AdapterResult[dict[str, Any]],
        adapter._handle_com_operation("undo", _undo_operation),
    )