Skip to content

solidworks_mcp.adapters.solidworks.features

solidworks_mcp.adapters.solidworks.features

Feature-domain mixin for PyWin32 SolidWorks operations.

Attributes

_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.

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.

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.

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, FeatureChamfer is called in equal-distance mode (type 1).

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>"].

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 FeatureChamfer 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, ``FeatureChamfer`` is called in
    equal-distance mode (type ``1``).

    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>"]``.

    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 ``FeatureChamfer`` 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

        # Detect SW major version (same pattern as fillet).
        chamfer_sw_major = 0
        if getattr(adapter, "swApp", None):
            rev = adapter._attempt(
                lambda: adapter._get_attr_or_call(adapter.swApp, "RevisionNumber"),
                default="0",
            )
            try:
                chamfer_sw_major = int(str(rev).split(".")[0])
            except (ValueError, IndexError):
                chamfer_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, cx, cy, cz = _parse_edge_spec(edge_name)
            append = idx > 0
            if sel_name == "":
                selected = _select_edge_by_coord(
                    adapter, cx, cy, cz, append=append, mark=0
                )
            else:
                selected = adapter._attempt(
                    lambda sn=sel_name, _x=cx, _y=cy, _z=cz, _ap=append: (
                        adapter.currentModel.Extension.SelectByID2(
                            sn, "EDGE", _x, _y, _z, _ap, 0, None, 0
                        )
                    ),
                    default=False,
                )
            if not selected:
                raise Exception(f"Failed to select edge: {edge_name}")

        fm = adapter.currentModel.FeatureManager
        _flag_feature_methods(fm, "IFeatureManager")
        count_before = adapter._attempt(
            lambda: int(fm.GetFeatureCount(True) or 0), default=0
        )

        feature_name = "Chamfer"
        feature = None

        if chamfer_sw_major >= 33:
            # SW 2025+ (major >= 33): IModelDoc2.FeatureChamferType (8 params,
            # VT_VOID). Detect success via feature count change.
            adapter.currentModel.FeatureChamferType(
                0,  # ChamferType: 0 = swChamferType_EqualDistance
                distance / 1000.0,  # Width in metres
                math.pi / 4,  # 45° angle
                False,  # Flip
                0.0,  # OtherDist
                0.0,  # VertexChamDist1
                0.0,  # VertexChamDist2
                0.0,  # VertexChamDist3
            )
            count_after = adapter._attempt(
                lambda: int(fm.GetFeatureCount(True) or 0), default=0
            )
            if count_after <= count_before:
                raise Exception(
                    "Failed to create chamfer (IModelDoc2.FeatureChamferType;"
                    " feature count unchanged)"
                )
        else:
            # Older SW: IFeatureManager.InsertFeatureChamfer returns IFeature.
            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° 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>"].

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>"]``.

    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 = _parse_edge_spec(edge_name)
            append = idx > 0  # first edge starts fresh, subsequent ones append
            if sel_name == "":
                # Coordinate-based: traverse body edges and pick the closest one.
                selected = _select_edge_by_coord(adapter, ex, ey, ez, append=append)
            else:
                selected = adapter._attempt(
                    lambda sn=sel_name, _x=ex, _y=ey, _z=ez, _ap=append: (
                        adapter.currentModel.Extension.SelectByID2(
                            sn, "EDGE", _x, _y, _z, _ap, 0, None, 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_cut_extrude_impl

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

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

The function first attempts to locate and select the sketch profile that should be cut. It walks the feature tree looking for the most recent ProfileFeature; if that fails, it falls back to the _last_sketch_name tracker and then to an enumerated Sketch<N> name search.

Three COM API variants are attempted in order of preference:

  1. FeatureCut4 ΓÇö most modern (SolidWorks 2015+).
  2. FeatureCut3 modern signature ΓÇö SolidWorks 2010ΓÇô2014.
  3. FeatureCut3 legacy argument order ΓÇö older installs.

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.

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 lists every API

AdapterResult[SolidWorksFeature]

variant that was tried.

Raises:

Type Description
Exception

Propagated through _handle_com_operation when all three COM variants fail.

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
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
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
def _create_cut_extrude_impl(
    adapter: Any, params: ExtrusionParameters
) -> AdapterResult[SolidWorksFeature]:
    """Create a cut-extrude feature from the active sketch profile.

    The function first attempts to locate and select the sketch profile that
    should be cut.  It walks the feature tree looking for the most recent
    ``ProfileFeature``; if that fails, it falls back to the ``_last_sketch_name``
    tracker and then to an enumerated ``Sketch<N>`` name search.

    Three COM API variants are attempted in order of preference:

    1. ``FeatureCut4`` ΓÇö most modern (SolidWorks 2015+).
    2. ``FeatureCut3`` modern signature ΓÇö SolidWorks 2010ΓÇô2014.
    3. ``FeatureCut3`` legacy argument order ΓÇö older installs.

    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.

    Returns:
        AdapterResult[SolidWorksFeature]: On success, ``data`` is a
        ``SolidWorksFeature`` whose ``type`` is ``"Cut-Extrude"``.  On
        failure, ``status`` is ``ERROR`` and ``error`` lists every API
        variant that was tried.

    Raises:
        Exception: Propagated through ``_handle_com_operation`` when all
            three COM variants fail.

    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)),
        )
        feature_manager = adapter.currentModel.FeatureManager

        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:
            # Implicit sketch context (from exit_sketch) was not picked up.
            # Try selecting the sketch explicitly before falling back to older API.
            adapter._attempt(
                lambda: adapter.currentModel.ClearSelection2(True), default=None
            )
            for candidate in (
                [adapter._last_sketch_name] if adapter._last_sketch_name else []
            ) + [f"Sketch{n}" for n in range(adapter._sketch_count, 0, -1)]:
                sel_ok = bool(
                    adapter._attempt(
                        lambda c=candidate: adapter.currentModel.Extension.SelectByID2(
                            c, "SKETCH", 0.0, 0.0, 0.0, False, 0, None, 0
                        ),
                        default=False,
                    )
                )
                if sel_ok:
                    adapter._last_sketch_name = candidate
                    break

        if not feature:
            # 2. FeatureCut3 modern (SW 2010+, 26 params, corrected for SW 2022)
            # 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_modern_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_modern_error is not None:
                fallback_errors.append(f"FeatureCut3 modern: {cut3_modern_error}")

        if not feature:
            # 3. FeatureCut3 legacy (older installs, alternate arg order)
            feature, cut3_legacy_error = adapter._attempt_with_error(
                lambda: feature_manager.FeatureCut3(
                    True,
                    False,
                    normalized.reverse_direction,
                    adapter.constants["swEndCondBlind"],
                    adapter.constants["swEndCondBlind"],
                    False,
                    False,
                    False,
                    False,
                    normalized.draft_angle * 3.14159 / 180.0,
                    0.0,
                    False,
                    False,
                    False,
                    False,
                    False,
                    normalized.feature_scope,
                    normalized.auto_select,
                    normalized.depth / 1000.0,
                    0.0,
                )
            )
            if cut3_legacy_error is not None:
                fallback_errors.append(f"FeatureCut3 legacy: {cut3_legacy_error}")

        if not feature:
            if fallback_errors:
                raise Exception(
                    "Failed to create cut extrude feature. "
                    + " | ".join(fallback_errors)
                )
            raise Exception("Failed to create cut extrude 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,
            },
            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_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),
    )

_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

_parse_edge_spec

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

Parse an edge specification string into (SelectByID2 name, x, y, z).

Supports two formats: - "Edge<1>" — name-based selection (x=y=z=0.0, SW looks up by topology name) - "x,y,z" — coordinate-based selection (name="", coordinate hint in metres)

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

    Supports two formats:
    - ``"Edge<1>"`` — name-based selection (x=y=z=0.0, SW looks up by topology name)
    - ``"x,y,z"`` — coordinate-based selection (name="", coordinate hint in metres)
    """
    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
        except ValueError:
            pass
    return edge_name, 0.0, 0.0, 0.0

_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

_select_edge_by_coord

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

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

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

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

Returns True if an edge 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,
) -> bool:
    """Select the edge nearest to (x, y, z) in metres via ``SelectByID2``.

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

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

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

    model = adapter.currentModel

    # Rebuild to tessellate geometry from recent features before the first
    # edge in the selection set (when append=False this is the first edge).
    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),
        ]

    # VT_DISPATCH null pointer — required by SelectByID2's Callout parameter.
    # Plain Python None marshals as VT_NULL which SW rejects with DISP_E_TYPEMISMATCH.
    try:
        import pythoncom
        import win32com.client as _win32com

        null_callout = _win32com.VARIANT(pythoncom.VT_DISPATCH, None)
    except Exception:
        null_callout = None  # fallback: may fail on some SW versions

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

    return False

_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))