Skip to content

solidworks_mcp.adapters.solidworks.documentation

solidworks_mcp.adapters.solidworks.documentation

Model-data mixin for PyWin32 SolidWorks operations.

Covers the engineering data a drawing or BOM is built from: model dimensions, measurements, material, custom properties and sheet-metal flat patterns.

Every method name, argument order and enum value below was read from the SOLIDWORKS 2025 type libraries (sldworks.tlb via gen_py, swconst.tlb via pythoncom.LoadTypeLib), not from memory. Live behaviour is covered by tests/live/test_live_sw_documentation_tools.py.

Attributes

_DEFAULT_MATERIAL_DATABASE module-attribute

_DEFAULT_MATERIAL_DATABASE = 'SOLIDWORKS Materials'

_LAYOUT_PASSES module-attribute

_LAYOUT_PASSES = 3

_MEASURE_PREFIXES module-attribute

_MEASURE_PREFIXES = {'face': 'FACE', 'edge': 'EDGE', 'vertex': 'VERTEX'}

_SW_ANNOTATION_DISPLAY_DIMENSION module-attribute

_SW_ANNOTATION_DISPLAY_DIMENSION = 4

_SW_AUTODIM_ENTITIES_ALL module-attribute

_SW_AUTODIM_ENTITIES_ALL = 1

_SW_AUTODIM_HORIZONTAL_BELOW module-attribute

_SW_AUTODIM_HORIZONTAL_BELOW = -1

_SW_AUTODIM_SCHEME_BASELINE module-attribute

_SW_AUTODIM_SCHEME_BASELINE = 1

_SW_AUTODIM_STATUS_SUCCESS module-attribute

_SW_AUTODIM_STATUS_SUCCESS = 0

_SW_AUTODIM_VERTICAL_LEFT module-attribute

_SW_AUTODIM_VERTICAL_LEFT = -1

_SW_CUSTOM_INFO_TEXT module-attribute

_SW_CUSTOM_INFO_TEXT = 30

_SW_CUSTOM_PROPERTY_REPLACE_VALUE module-attribute

_SW_CUSTOM_PROPERTY_REPLACE_VALUE = 2

_SW_EXPORT_SHEET_METAL module-attribute

_SW_EXPORT_SHEET_METAL = 1

_SW_IMPORT_FROM_ENTIRE_MODEL module-attribute

_SW_IMPORT_FROM_ENTIRE_MODEL = 0

_SW_INSERT_DIMENSIONS module-attribute

_SW_INSERT_DIMENSIONS = 8

_SW_INSERT_DIMENSIONS_MARKED_FOR_DRAWING module-attribute

_SW_INSERT_DIMENSIONS_MARKED_FOR_DRAWING = 32768

_SW_INSERT_HOLE_WIZARD_LOCATION_DIMS module-attribute

_SW_INSERT_HOLE_WIZARD_LOCATION_DIMS = 131072

_SW_PREF_MATERIAL_DATABASES module-attribute

_SW_PREF_MATERIAL_DATABASES = 28

_SW_SOLID_BODY module-attribute

_SW_SOLID_BODY = 0

Classes

AdapterResult dataclass

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

Result wrapper for adapter operations.

Attributes:

Name Type Description
data T | None

The data value.

error str | None

The error value.

execution_time float | None

The execution time value.

metadata dict[str, Any] | None

The metadata value.

status AdapterResultStatus

The status value.

Attributes
is_error property
is_error: bool

Check if operation had an error.

Returns:

Name Type Description
bool bool

True if error, otherwise False.

is_success property
is_success: bool

Check if operation was successful.

Returns:

Name Type Description
bool bool

True if success, otherwise False.

AdapterResultStatus

Bases: StrEnum

Result status for adapter operations.

Attributes:

Name Type Description
ERROR Any

The error value.

SUCCESS Any

The success value.

TIMEOUT Any

The timeout value.

WARNING Any

The warning value.

SolidWorksDocumentationMixin

Expose model-data and drawing-documentation methods through a mixin.

Methods:
arrange_dimensions async
arrange_dimensions(view_name: str | None = None) -> AdapterResult[dict[str, Any]]

Move drawing dimensions so none overlaps another, then verify.

Reads every dimension's text box and lines, plans positions outside the views with :func:dimension_layout.plan_layout, applies them with IAnnotation::SetPosition, rebuilds, then re-reads the real geometry and checks the rule with :func:dimension_layout.find_overlaps. The check covers every view on the sheet, even when only one view is rearranged, because neighbouring views' dimensions can collide.

Parameters:

Name Type Description Default
view_name str | None

Only move this view's dimensions; all views when omitted.

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: overlaps_before, overlaps_after

AdapterResult[dict[str, Any]]

(each violation listed), moved and unplaced. ERROR when any

AdapterResult[dict[str, Any]]

overlap remains - the rule is not met - with the same data, so the

AdapterResult[dict[str, Any]]

caller can see which dimensions still collide.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
async def arrange_dimensions(
    self, view_name: str | None = None
) -> AdapterResult[dict[str, Any]]:
    """Move drawing dimensions so none overlaps another, then verify.

    Reads every dimension's text box and lines, plans positions outside
    the views with :func:`dimension_layout.plan_layout`, applies them with
    ``IAnnotation::SetPosition``, rebuilds, then **re-reads the real
    geometry** and checks the rule with :func:`dimension_layout.find_overlaps`.
    The check covers every view on the sheet, even when only one view is
    rearranged, because neighbouring views' dimensions can collide.

    Args:
        view_name: Only move this view's dimensions; all views when omitted.

    Returns:
        AdapterResult[dict[str, Any]]: ``overlaps_before``, ``overlaps_after``
        (each violation listed), ``moved`` and ``unplaced``. ERROR when any
        overlap remains - the rule is not met - with the same data, so the
        caller can see which dimensions still collide.
    """
    guard = self._require_drawing()
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)

    def _arrange() -> dict[str, Any]:
        return self._arrange_layout(view_name)

    result = cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("arrange_dimensions", _arrange),
    )
    if result.is_success and result.data and result.data["overlaps_after"]:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=_overlap_summary(result.data),
            data=result.data,
            execution_time=result.execution_time,
        )
    return result
export_flat_pattern async
export_flat_pattern(file_path: str) -> AdapterResult[dict[str, Any]]

Export the active sheet-metal part's flat pattern to DXF or DWG.

Wraps IPartDoc::ExportToDWG2 with swExportToDWG_ExportSheetMetal. SheetMetalOptions 1 exports the flat-pattern geometry only (no bend lines or sketches).

Parameters:

Name Type Description Default
file_path str

Output .dxf or .dwg path.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The written path and its size.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
async def export_flat_pattern(
    self, file_path: str
) -> AdapterResult[dict[str, Any]]:
    """Export the active sheet-metal part's flat pattern to DXF or DWG.

    Wraps ``IPartDoc::ExportToDWG2`` with ``swExportToDWG_ExportSheetMetal``.
    SheetMetalOptions ``1`` exports the flat-pattern geometry only (no bend
    lines or sketches).

    Args:
        file_path: Output ``.dxf`` or ``.dwg`` path.

    Returns:
        AdapterResult[dict[str, Any]]: The written path and its size.
    """
    guard = self._require_doc_type({1}, "export_flat_pattern")
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)
    out = os.path.abspath(file_path)
    if os.path.splitext(out)[1].lower() not in {".dxf", ".dwg"}:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="file_path must end in .dxf or .dwg",
        )

    def _export() -> dict[str, Any]:
        model = self.currentModel
        part = _sw_type_info.flagged(model, "IPartDoc")
        bodies = _sequence(
            self._attempt(
                lambda: part.GetBodies2(_SW_SOLID_BODY, True), default=None
            )
        )
        if not any(
            self._attempt(
                lambda b=b: _as_com(self, b, "IBody2").IsSheetMetal(), default=False
            )
            for b in bodies
        ):
            raise LookupError(
                "The active part has no sheet-metal body, so it has no flat "
                "pattern to export"
            )
        os.makedirs(os.path.dirname(out) or ".", exist_ok=True)
        model_path = str(
            self._attempt(lambda: model.GetPathName(), default="") or ""
        )
        ok = part.ExportToDWG2(
            out,
            model_path,
            _SW_EXPORT_SHEET_METAL,
            True,
            None,
            False,
            False,
            1,
            None,
        )
        if not ok or not os.path.exists(out):
            raise Exception(f"ExportToDWG2 did not write {out} (returned {ok!r})")
        return {"file_path": out, "size_bytes": os.path.getsize(out)}

    return cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("export_flat_pattern", _export),
    )
get_custom_properties async
get_custom_properties(configuration: str | None = None) -> AdapterResult[dict[str, Any]]

Read the custom properties of the active document.

Wraps IModelDocExtension::CustomPropertyManager(config) with GetNames + Get6. "" reads the file-level properties a drawing title block usually links to.

Parameters:

Name Type Description Default
configuration str | None

Configuration name; file-level when omitted.

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: properties mapping name to

AdapterResult[dict[str, Any]]

{"value", "resolved"}.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
async def get_custom_properties(
    self, configuration: str | None = None
) -> AdapterResult[dict[str, Any]]:
    """Read the custom properties of the active document.

    Wraps ``IModelDocExtension::CustomPropertyManager(config)`` with
    ``GetNames`` + ``Get6``. ``""`` reads the file-level properties a
    drawing title block usually links to.

    Args:
        configuration: Configuration name; file-level when omitted.

    Returns:
        AdapterResult[dict[str, Any]]: ``properties`` mapping name to
        ``{"value", "resolved"}``.
    """
    if not self.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )

    def _get() -> dict[str, Any]:
        manager = _sw_type_info.flagged(
            self.currentModel.Extension.CustomPropertyManager(configuration or ""),
            "ICustomPropertyManager",
        )
        props: dict[str, dict[str, str]] = {}
        names = self._attempt(
            lambda: self._get_attr_or_call(manager, "GetNames"), default=None
        )
        for name in _sequence(names):
            value, resolved = _read_custom_property(self, manager, str(name))
            props[str(name)] = {"value": value, "resolved": resolved}
        return {"configuration": configuration or "", "properties": props}

    return cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("get_custom_properties", _get),
    )
get_material async
get_material(configuration: str | None = None) -> AdapterResult[dict[str, Any]]

Read the material assigned to the active part.

Wraps IPartDoc::GetMaterialPropertyName2; see :func:_read_material for its Database out-parameter.

Parameters:

Name Type Description Default
configuration str | None

Configuration name; the active one when omitted.

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: material (None when unset),

AdapterResult[dict[str, Any]]

database and density_kg_m3, read from the material's entry

AdapterResult[dict[str, Any]]

in its .sldmat library (None when unset or not found).

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
async def get_material(
    self, configuration: str | None = None
) -> AdapterResult[dict[str, Any]]:
    """Read the material assigned to the active part.

    Wraps ``IPartDoc::GetMaterialPropertyName2``; see
    :func:`_read_material` for its ``Database`` out-parameter.

    Args:
        configuration: Configuration name; the active one when omitted.

    Returns:
        AdapterResult[dict[str, Any]]: ``material`` (``None`` when unset),
        ``database`` and ``density_kg_m3``, read from the material's entry
        in its ``.sldmat`` library (``None`` when unset or not found).
    """
    guard = self._require_doc_type({1}, "get_material")
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)

    def _get() -> dict[str, Any]:
        part = _sw_type_info.flagged(self.currentModel, "IPartDoc")
        name, database = _read_material(part, configuration)
        app = getattr(self, "swApp", None)
        density = (
            _material_density(
                _library_files(self._material_folders(app)), name, database
            )
            if name and app is not None
            else None
        )
        return {
            "material": name or None,
            "database": database or None,
            "density_kg_m3": density,
            "configuration": configuration,
        }

    return cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("get_material", _get),
    )
insert_model_dimensions async
insert_model_dimensions(view_name: str | None = None, include_hole_wizard: bool = True) -> AdapterResult[dict[str, Any]]

Bring model dimensions into drawing views, each dimension shown once.

Imports the model's "marked for drawing" dimensions view by view with IDrawingDoc::InsertModelAnnotations3 (UsePlacementInSketch off, so SolidWorks places them rather than copying sketch positions). Drafting rules applied:

  • pictorial views (isometric, dimetric, trimetric) are not dimensioned unless named explicitly;
  • a model dimension already shown in an earlier view is deleted from later views, so each dimension appears exactly once on the sheet.

Falls back to IDrawingDoc::AutoDimension only when nothing at all could be imported. Placement is then made overlap-free by :meth:arrange_dimensions.

Parameters:

Name Type Description Default
view_name str | None

One view to dimension; every orthographic view when omitted.

None
include_hole_wizard bool

Also import Hole Wizard location dimensions.

True

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: Per-view counts, duplicates removed,

AdapterResult[dict[str, Any]]

skipped pictorial views and the method used.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
async def insert_model_dimensions(
    self, view_name: str | None = None, include_hole_wizard: bool = True
) -> AdapterResult[dict[str, Any]]:
    """Bring model dimensions into drawing views, each dimension shown once.

    Imports the model's "marked for drawing" dimensions view by view with
    ``IDrawingDoc::InsertModelAnnotations3`` (``UsePlacementInSketch`` off,
    so SolidWorks places them rather than copying sketch positions).
    Drafting rules applied:

    * pictorial views (isometric, dimetric, trimetric) are not dimensioned
      unless named explicitly;
    * a model dimension already shown in an earlier view is deleted from
      later views, so each dimension appears exactly once on the sheet.

    Falls back to ``IDrawingDoc::AutoDimension`` only when nothing at all
    could be imported. Placement is then made overlap-free by
    :meth:`arrange_dimensions`.

    Args:
        view_name: One view to dimension; every orthographic view when omitted.
        include_hole_wizard: Also import Hole Wizard location dimensions.

    Returns:
        AdapterResult[dict[str, Any]]: Per-view counts, duplicates removed,
        skipped pictorial views and the ``method`` used.
    """
    guard = self._require_drawing()
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)

    def _insert() -> dict[str, Any]:
        return self._import_dimensions(view_name, include_hole_wizard)

    return cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("insert_model_dimensions", _insert),
    )
list_available_materials async
list_available_materials(search: str | None = None) -> AdapterResult[list[dict[str, str]]]

List materials from the configured SolidWorks material libraries.

Reads the custom library folders from swFileLocationsMaterialDatabases (a ;-separated list) plus the install's built-in sldmaterials folder, and parses every .sldmat file in them.

Parameters:

Name Type Description Default
search str | None

Case-insensitive substring filter on name or classification.

None

Returns:

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

AdapterResult[list[dict[str, str]]]: name, classification

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

and database per material. metadata gives the folders

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

read and the total before filtering, so an empty search can be

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

told apart from an unexpected library location.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
async def list_available_materials(
    self, search: str | None = None
) -> AdapterResult[list[dict[str, str]]]:
    """List materials from the configured SolidWorks material libraries.

    Reads the custom library folders from
    ``swFileLocationsMaterialDatabases`` (a ``;``-separated list) plus the
    install's built-in ``sldmaterials`` folder, and parses every
    ``.sldmat`` file in them.

    Args:
        search: Case-insensitive substring filter on name or classification.

    Returns:
        AdapterResult[list[dict[str, str]]]: ``name``, ``classification``
        and ``database`` per material. ``metadata`` gives the ``folders``
        read and the ``total`` before filtering, so an empty search can be
        told apart from an unexpected library location.
    """
    app = getattr(self, "swApp", None)
    if app is None:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="Not connected to SolidWorks"
        )
    needle = (search or "").strip().lower()
    searched: dict[str, Any] = {}

    def _list() -> list[dict[str, str]]:
        folder_list = self._material_folders(app)
        folders = ";".join(folder_list)
        materials: list[dict[str, str]] = []
        for path in _library_files(folder_list):
            materials.extend(_parse_material_library(path))
        searched["folders"] = folder_list
        searched["total"] = len(materials)
        if not materials:
            raise LookupError(f"No .sldmat libraries found in: {folders}")
        if needle:
            materials = [
                m
                for m in materials
                if needle in m["name"].lower()
                or needle in m["classification"].lower()
            ]
        return materials

    result = cast(
        "AdapterResult[list[dict[str, str]]]",
        self._handle_com_operation("list_available_materials", _list),
    )
    if result.is_success:
        result.metadata = dict(searched)
    return result
list_model_dimensions async
list_model_dimensions(feature_name: str | None = None) -> AdapterResult[list[dict[str, Any]]]

List every dimension in the active part or assembly.

Walks IModelDoc2::FirstFeature / IFeature::GetNextFeature and each feature's GetFirstDisplayDimension chain. full_name (e.g. "D1@Sketch1") is the name set_dimension accepts, so the pair gives edit-by-name access to every driving parameter in the model.

Parameters:

Name Type Description Default
feature_name str | None

Only list dimensions of this feature (case-insensitive).

None

Returns:

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

AdapterResult[list[dict[str, Any]]]: full_name, feature,

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

value (document units), system_value (metres / radians)

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

and read_only per dimension.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
async def list_model_dimensions(
    self, feature_name: str | None = None
) -> AdapterResult[list[dict[str, Any]]]:
    """List every dimension in the active part or assembly.

    Walks ``IModelDoc2::FirstFeature`` / ``IFeature::GetNextFeature`` and
    each feature's ``GetFirstDisplayDimension`` chain. ``full_name`` (e.g.
    ``"D1@Sketch1"``) is the name ``set_dimension`` accepts, so the pair
    gives edit-by-name access to every driving parameter in the model.

    Args:
        feature_name: Only list dimensions of this feature (case-insensitive).

    Returns:
        AdapterResult[list[dict[str, Any]]]: ``full_name``, ``feature``,
        ``value`` (document units), ``system_value`` (metres / radians)
        and ``read_only`` per dimension.
    """
    guard = self._require_doc_type({1, 2}, "list_model_dimensions")
    if guard is not None:
        return cast("AdapterResult[list[dict[str, Any]]]", guard)
    target = (feature_name or "").strip().lower()

    def _list() -> list[dict[str, Any]]:
        model = self.currentModel
        dims: list[dict[str, Any]] = []
        feature = self._attempt(lambda: model.FirstFeature(), default=None)
        guard_count = 0
        while feature is not None and guard_count < 20000:
            guard_count += 1
            feat = _sw_type_info.flagged(feature, "IFeature")
            name = str(self._attempt(lambda f=feat: f.Name, default="") or "")
            if not target or name.lower() == target:
                display = self._attempt(
                    lambda f=feat: f.GetFirstDisplayDimension(), default=None
                )
                while display is not None:
                    disp = _sw_type_info.flagged(display, "IDisplayDimension")
                    dim = self._attempt(
                        lambda d=disp: d.GetDimension2(0), default=None
                    )
                    if dim is not None:
                        dim = _sw_type_info.flagged(dim, "IDimension")
                        dims.append(
                            {
                                "full_name": _strip_document_suffix(
                                    self._attempt(
                                        lambda d=dim: d.FullName, default=None
                                    )
                                ),
                                "feature": name,
                                "value": self._attempt(
                                    lambda d=dim: d.Value, default=None
                                ),
                                "system_value": self._attempt(
                                    lambda d=dim: d.SystemValue, default=None
                                ),
                                "read_only": bool(
                                    self._attempt(
                                        lambda d=dim: d.ReadOnly, default=False
                                    )
                                ),
                            }
                        )
                    display = self._attempt(
                        lambda f=feat, d=display: f.GetNextDisplayDimension(d),
                        default=None,
                    )
            feature = self._attempt(lambda f=feat: f.GetNextFeature(), default=None)
        if target and not dims:
            raise LookupError(
                f"No dimensions found on feature {feature_name!r}; list "
                "the feature tree with list_features for exact names"
            )
        return dims

    return cast(
        "AdapterResult[list[dict[str, Any]]]",
        self._handle_com_operation("list_model_dimensions", _list),
    )
measure_entities async
measure_entities(entities: list[str]) -> AdapterResult[dict[str, Any]]

Measure one or more entities with SolidWorks' Measure tool.

Selects each entity by spec (see :func:_parse_measure_spec), then runs IModelDocExtension::CreateMeasure + IMeasure::Calculate(None), which measures the current selection. Values SolidWorks reports as -1 (not applicable to the selection) are dropped.

Parameters:

Name Type Description Default
entities list[str]

One or two entity specs, e.g. ["face:0,0.01,0"] for a face area, or two edge/face specs for distance and angle.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: Measured values in metres, square

AdapterResult[dict[str, Any]]

metres and radians, plus selected; is_parallel only for

AdapterResult[dict[str, Any]]

two entities.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
async def measure_entities(
    self, entities: list[str]
) -> AdapterResult[dict[str, Any]]:
    """Measure one or more entities with SolidWorks' Measure tool.

    Selects each entity by spec (see :func:`_parse_measure_spec`), then runs
    ``IModelDocExtension::CreateMeasure`` + ``IMeasure::Calculate(None)``,
    which measures the current selection. Values SolidWorks reports as
    ``-1`` (not applicable to the selection) are dropped.

    Args:
        entities: One or two entity specs, e.g. ``["face:0,0.01,0"]`` for a
            face area, or two edge/face specs for distance and angle.

    Returns:
        AdapterResult[dict[str, Any]]: Measured values in metres, square
        metres and radians, plus ``selected``; ``is_parallel`` only for
        two entities.
    """
    guard = self._require_doc_type({1, 2}, "measure_entities")
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)
    if not entities or len(entities) > 2:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="measure_entities takes one or two entity specs",
        )

    def _measure() -> dict[str, Any]:
        model = self.currentModel
        self._attempt(lambda: model.ClearSelection2(True), default=None)
        for index, spec in enumerate(entities):
            name, x, y, z, kind = _parse_measure_spec(spec)
            append = index > 0
            if name:
                ok = bool(
                    self._attempt(
                        lambda n=name, k=kind, a=append: (
                            model.Extension.SelectByID2(
                                n, k, 0.0, 0.0, 0.0, a, 0, _null_callout(), 0
                            )
                        ),
                        default=False,
                    )
                )
            else:
                ok = _select_edge_by_coord(self, x, y, z, append, 0, kind)
            if not ok:
                raise LookupError(
                    f"No entity found for {spec!r}; check the name or the "
                    "coordinates (metres)"
                )

        # CreateMeasure is a zero-arg method; unflagged, late binding asks
        # for it as a property and SW 2026 answers "Member not found".
        # Not wrapped in _attempt: a COM error surfaces with its message.
        extension = _sw_type_info.flagged(model.Extension, "IModelDocExtension")
        measure = extension.CreateMeasure()
        if measure is None:
            raise Exception("CreateMeasure returned nothing")
        measure = _sw_type_info.flagged(measure, "IMeasure")
        if not measure.Calculate(None):
            raise Exception("IMeasure.Calculate failed for this selection")

        result: dict[str, Any] = {"selected": list(entities)}
        for prop, key in (
            ("Distance", "distance_m"),
            ("CenterDistance", "center_distance_m"),
            ("Angle", "angle_rad"),
            ("Radius", "radius_m"),
            ("Diameter", "diameter_m"),
            ("Length", "length_m"),
            ("ArcLength", "arc_length_m"),
            ("Area", "area_m2"),
            ("Perimeter", "perimeter_m"),
            ("DeltaX", "delta_x_m"),
            ("DeltaY", "delta_y_m"),
            ("DeltaZ", "delta_z_m"),
        ):
            value = self._attempt(lambda p=prop: getattr(measure, p), default=None)
            if isinstance(value, (int, float)) and value != -1:
                result[key] = float(value)
        if len(entities) == 2:
            # Parallelism only means something between two entities.
            parallel = self._attempt(lambda: measure.IsParallel, default=None)
            if isinstance(parallel, bool):
                result["is_parallel"] = parallel
        self._attempt(lambda: model.ClearSelection2(True), default=None)
        return result

    return cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("measure_entities", _measure),
    )
set_custom_property async
set_custom_property(name: str, value: str, configuration: str | None = None) -> AdapterResult[dict[str, Any]]

Create or overwrite one text custom property on the active document.

Wraps ICustomPropertyManager::Add3(name, swCustomInfoText, value, swCustomPropertyReplaceValue) and reads the value back.

Parameters:

Name Type Description Default
name str

Property name, e.g. "PartNo" or "DrawnBy".

required
value str

Text value; may be a $PRP:"..." link.

required
configuration str | None

Configuration name; file-level when omitted.

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The stored and resolved value.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
async def set_custom_property(
    self, name: str, value: str, configuration: str | None = None
) -> AdapterResult[dict[str, Any]]:
    """Create or overwrite one text custom property on the active document.

    Wraps ``ICustomPropertyManager::Add3(name, swCustomInfoText, value,
    swCustomPropertyReplaceValue)`` and reads the value back.

    Args:
        name: Property name, e.g. ``"PartNo"`` or ``"DrawnBy"``.
        value: Text value; may be a ``$PRP:"..."`` link.
        configuration: Configuration name; file-level when omitted.

    Returns:
        AdapterResult[dict[str, Any]]: The stored and resolved value.
    """
    if not self.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )
    if not name.strip():
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="name is required"
        )

    def _set() -> dict[str, Any]:
        manager = _sw_type_info.flagged(
            self.currentModel.Extension.CustomPropertyManager(configuration or ""),
            "ICustomPropertyManager",
        )
        code = manager.Add3(
            name,
            _SW_CUSTOM_INFO_TEXT,
            str(value),
            _SW_CUSTOM_PROPERTY_REPLACE_VALUE,
        )
        stored, resolved = _read_custom_property(self, manager, name)
        if stored != str(value):
            raise Exception(
                f"Custom property {name!r} was not stored (Add3 returned {code!r}, "
                f"read back {stored!r})."
            )
        return {
            "name": name,
            "value": stored,
            "resolved": resolved,
            "configuration": configuration or "",
        }

    return cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("set_custom_property", _set),
    )
set_material async
set_material(material_name: str, database: str | None = None, configuration: str | None = None) -> AdapterResult[dict[str, Any]]

Assign a material to the active part.

Wraps IPartDoc::SetMaterialPropertyName2 (a void call), then reads the material back - the read-back is the only proof it applied. A name the library does not hold leaves the part unchanged and is reported as not found.

Parameters:

Name Type Description Default
material_name str

Material name exactly as in the library, e.g. "AISI 1020" or "6061-T6 (SS)".

required
database str | None

Library name; "SOLIDWORKS Materials" when omitted.

None
configuration str | None

Configuration name; "" (all) when omitted.

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: Requested and applied material.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
async def set_material(
    self,
    material_name: str,
    database: str | None = None,
    configuration: str | None = None,
) -> AdapterResult[dict[str, Any]]:
    """Assign a material to the active part.

    Wraps ``IPartDoc::SetMaterialPropertyName2`` (a ``void`` call), then
    reads the material back - the read-back is the only proof it applied.
    A name the library does not hold leaves the part unchanged and is
    reported as not found.

    Args:
        material_name: Material name exactly as in the library, e.g.
            ``"AISI 1020"`` or ``"6061-T6 (SS)"``.
        database: Library name; ``"SOLIDWORKS Materials"`` when omitted.
        configuration: Configuration name; ``""`` (all) when omitted.

    Returns:
        AdapterResult[dict[str, Any]]: Requested and applied material.
    """
    guard = self._require_doc_type({1}, "set_material")
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)
    if not material_name.strip():
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="material_name is required"
        )
    library = database or _DEFAULT_MATERIAL_DATABASE

    def _set() -> dict[str, Any]:
        part = _sw_type_info.flagged(self.currentModel, "IPartDoc")
        part.SetMaterialPropertyName2(configuration or "", library, material_name)
        self._attempt(lambda: self.currentModel.EditRebuild3(), default=None)
        applied, _library = _read_material(part, configuration)
        if applied.lower() != material_name.lower():
            # SolidWorks ignores a name its library does not hold.
            raise LookupError(
                f"Material {material_name!r} not found in {library!r}; the "
                f"part still reports {applied or 'no material'}. Use "
                "list_available_materials for the exact name and library."
            )
        return {
            "material": applied,
            "database": library,
            "configuration": configuration,
        }

    return cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("set_material", _set),
    )

Functions:

_as_com

_as_com(adapter: Any, obj: Any, interface: str) -> Any

Wrap a raw dispatch and flag its methods for an interface.

Objects handed back inside arrays (GetViews and friends) arrive as raw PyIDispatch. Method flagging is a no-op on those, so every call against them raises until they are wrapped through dynamic.Dispatch.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter.

required
obj Any

The raw dispatch.

required
interface str

Interface name, e.g. "IView".

required

Returns:

Name Type Description
Any Any

The wrapped, flagged object, or None.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _as_com(adapter: Any, obj: Any, interface: str) -> Any:
    """Wrap a raw dispatch and flag its methods for an interface.

    Objects handed back inside arrays (``GetViews`` and friends) arrive as raw
    ``PyIDispatch``.  Method flagging is a no-op on those, so every call
    against them raises until they are wrapped through ``dynamic.Dispatch``.

    Args:
        adapter: A connected ``PyWin32Adapter``.
        obj: The raw dispatch.
        interface: Interface name, e.g. ``"IView"``.

    Returns:
        Any: The wrapped, flagged object, or ``None``.
    """
    wrapped = adapter._attempt(lambda: _dynamic.Dispatch(obj), default=None)
    if wrapped is None:
        return None
    adapter._attempt(
        lambda: _sw_type_info.flag_methods(wrapped, interface), default=None
    )
    return wrapped

_byref_bstr

_byref_bstr() -> Any

Return a byref string VARIANT for a SolidWorks String out-parameter.

Under forced late binding an omitted out-parameter raises "Parameter not optional", and pythoncom.Missing cannot be converted to a COM VARIANT, so pass VARIANT(VT_BYREF | VT_BSTR, "") and read .value afterwards (e.g. IPartDoc::GetMaterialPropertyName2's Database; confirmed live on R2026x).

Returns:

Name Type Description
Any Any

The byref VARIANT, or a _ByrefFallback("") without pywin32.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _byref_bstr() -> Any:
    """Return a byref string VARIANT for a SolidWorks ``String`` out-parameter.

    Under forced late binding an omitted out-parameter raises "Parameter not
    optional", and ``pythoncom.Missing`` cannot be converted to a COM VARIANT,
    so pass ``VARIANT(VT_BYREF | VT_BSTR, "")`` and read ``.value`` afterwards
    (e.g. ``IPartDoc::GetMaterialPropertyName2``'s ``Database``; confirmed live
    on R2026x).

    Returns:
        Any: The byref VARIANT, or a ``_ByrefFallback("")`` without pywin32.
    """
    variant_ctor = getattr(getattr(win32com, "client", None), "VARIANT", None)
    if not callable(variant_ctor):
        return _ByrefFallback("")
    return variant_ctor(
        int(getattr(pythoncom, "VT_BYREF", 0)) | int(getattr(pythoncom, "VT_BSTR", 0)),
        "",
    )

_doc_type

_doc_type(adapter: Any) -> int | None

Return the active document's type: 1 part, 2 assembly, 3 drawing.

GetType is one of the members pywin32 late binding may expose as either a bound method or a plain value, so calling it directly returns None on some documents. _get_attr_or_call handles both shapes.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter.

required

Returns:

Type Description
int | None

int | None: The document type, or None when it cannot be read.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _doc_type(adapter: Any) -> int | None:
    """Return the active document's type: 1 part, 2 assembly, 3 drawing.

    ``GetType`` is one of the members pywin32 late binding may expose as either
    a bound method or a plain value, so calling it directly returns ``None`` on
    some documents.  ``_get_attr_or_call`` handles both shapes.

    Args:
        adapter: A connected ``PyWin32Adapter``.

    Returns:
        int | None: The document type, or ``None`` when it cannot be read.
    """
    value = adapter._attempt(
        lambda: adapter._get_attr_or_call(adapter.currentModel, "GetType"),
        default=None,
    )
    return int(value) if isinstance(value, (int, float)) else None

_library_files

_library_files(folders: list[str]) -> list[str]

The .sldmat files in folders, folder by folder, sorted by name.

Parameters:

Name Type Description Default
folders list[str]

Material library folders; missing ones are skipped.

required

Returns:

Type Description
list[str]

list[str]: Full paths.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
def _library_files(folders: list[str]) -> list[str]:
    """The ``.sldmat`` files in ``folders``, folder by folder, sorted by name.

    Args:
        folders: Material library folders; missing ones are skipped.

    Returns:
        list[str]: Full paths.
    """
    files: list[str] = []
    for folder in folders:
        if os.path.isdir(folder):
            files.extend(
                os.path.join(folder, entry)
                for entry in sorted(os.listdir(folder))
                if entry.lower().endswith(".sldmat")
            )
    return files

_material_density

_material_density(files: list[str], name: str, database: str) -> float | None

A material's mass density in kg/m³, from its .sldmat entry.

The library whose file name matches database is searched first, then the others. The value is the DENS element's value, which the libraries store in kg/m³.

Parameters:

Name Type Description Default
files list[str]

.sldmat paths to search.

required
name str

The material's name, as GetMaterialPropertyName2 gives it.

required
database str

Its library's name, the file name without extension.

required

Returns:

Type Description
float | None

float | None: The density, or None when no entry has one.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
def _material_density(files: list[str], name: str, database: str) -> float | None:
    """A material's mass density in kg/m³, from its ``.sldmat`` entry.

    The library whose file name matches ``database`` is searched first, then
    the others. The value is the ``DENS`` element's ``value``, which the
    libraries store in kg/m³.

    Args:
        files: ``.sldmat`` paths to search.
        name: The material's name, as ``GetMaterialPropertyName2`` gives it.
        database: Its library's name, the file name without extension.

    Returns:
        float | None: The density, or ``None`` when no entry has one.
    """
    wanted = database.lower()
    ordered = sorted(
        files,
        key=lambda f: os.path.splitext(os.path.basename(f))[0].lower() != wanted,
    )
    for path in ordered:
        try:
            root = ET.parse(path).getroot()
        except (ET.ParseError, OSError, DefusedXmlException):
            continue
        for element in root.iter():
            if element.tag.rsplit("}", 1)[-1] != "material":
                continue
            if element.get("name") != name:
                continue
            for prop in element.iter():
                if prop.tag.rsplit("}", 1)[-1].upper() == "DENS":
                    try:
                        return float(prop.get("value", ""))
                    except ValueError:
                        return None
    return None

_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:
        return _NULL_CALLOUT_SENTINEL
    return _win32com.VARIANT(pythoncom.VT_DISPATCH, None)

_overlap_summary

_overlap_summary(result: dict[str, Any]) -> str

Describe the overlaps arrange_dimensions could not clear.

Parameters:

Name Type Description Default
result dict[str, Any]

The layout result, carrying remaining_overlaps.

required

Returns:

Name Type Description
str str

How many overlaps remain, their kinds, the first five, and what

str

to try next.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
def _overlap_summary(result: dict[str, Any]) -> str:
    """Describe the overlaps ``arrange_dimensions`` could not clear.

    Args:
        result: The layout result, carrying ``remaining_overlaps``.

    Returns:
        str: How many overlaps remain, their kinds, the first five, and what
        to try next.
    """
    overlaps = result.get("remaining_overlaps", [])
    kinds = sorted({o["kind"] for o in overlaps})
    sample = "; ".join(
        f"{o['dimension']} vs {o['conflicts_with']} ({o['kind']})" for o in overlaps[:5]
    )
    return (
        f"{result['overlaps_after']} dimension overlap(s) remain after arranging "
        f"{result['dimensions']} dimension(s) ({', '.join(kinds)}): {sample}. "
        "Try a smaller sheet scale or a larger sheet so the views have room."
    )

_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. It is the robust way to reach a hole's rim: the rim is one edge of the face the hole opens onto, and a point well inside that face is clear of every boundary, whereas an edge point has to land on the rim itself.

Every coordinate pick must lie within _PICK_TOLERANCE_M of the edge or face it resolves to. (A 2026-09-18 note here said an edge pick aimed at a hole rim landed on a 30 mm outer edge; the test's point was on that outer edge, because it assumed the wrong height axis. See #199.)

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. It is the robust way to reach a
      hole's rim: the rim is one edge of the face the hole opens onto, and
      a point well inside that face is clear of every boundary, whereas an
      edge point has to land on the rim itself.

    Every coordinate pick must lie within ``_PICK_TOLERANCE_M`` of the edge
    or face it resolves to. (A 2026-09-18 note here said an edge pick aimed
    at a hole rim landed on a 30 mm outer edge; the test's point was on that
    outer edge, because it assumed the wrong height axis. See #199.)
    """
    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"

_parse_material_library

_parse_material_library(path: str) -> list[dict[str, str]]

Read material names and classifications from one .sldmat file.

.sldmat is XML: <classification name=...> elements hold <material name=...> children. Tags are matched by local name because the root element carries a namespace. Parsed with defusedxml: library folders can be user-configured, so entity-expansion and external-entity payloads are refused rather than expanded.

Parameters:

Name Type Description Default
path str

Path to the .sldmat file.

required

Returns:

Type Description
list[dict[str, str]]

list[dict[str, str]]: name, classification and database

list[dict[str, str]]

for each material; empty when the file cannot be parsed or is refused.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
def _parse_material_library(path: str) -> list[dict[str, str]]:
    """Read material names and classifications from one ``.sldmat`` file.

    ``.sldmat`` is XML: ``<classification name=...>`` elements hold
    ``<material name=...>`` children. Tags are matched by local name because
    the root element carries a namespace. Parsed with ``defusedxml``: library
    folders can be user-configured, so entity-expansion and external-entity
    payloads are refused rather than expanded.

    Args:
        path: Path to the ``.sldmat`` file.

    Returns:
        list[dict[str, str]]: ``name``, ``classification`` and ``database``
        for each material; empty when the file cannot be parsed or is refused.
    """
    try:
        root = ET.parse(path).getroot()
    except (ET.ParseError, OSError, DefusedXmlException):
        return []
    database = os.path.splitext(os.path.basename(path))[0]
    materials: list[dict[str, str]] = []
    for classification in root.iter():
        if classification.tag.rsplit("}", 1)[-1] != "classification":
            continue
        class_name = classification.get("name", "")
        for child in classification:
            if child.tag.rsplit("}", 1)[-1] == "material" and child.get("name"):
                materials.append(
                    {
                        "name": child.get("name", ""),
                        "classification": class_name,
                        "database": database,
                    }
                )
    return materials

_parse_measure_spec

_parse_measure_spec(spec: str) -> tuple[str, float, float, float, str]

Parse a measure_entities entity spec into SelectByID2 arguments.

Accepts the same "face:x,y,z" / "x,y,z" / "Edge<1>" forms as add_fillet (via :func:_parse_edge_spec), plus "edge:x,y,z" and "vertex:x,y,z". Coordinates are in metres.

Parameters:

Name Type Description Default
spec str

The entity spec.

required

Returns:

Type Description
tuple[str, float, float, float, str]

tuple[str, float, float, float, str]: name, x, y, z and entity type.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
def _parse_measure_spec(spec: str) -> tuple[str, float, float, float, str]:
    """Parse a ``measure_entities`` entity spec into SelectByID2 arguments.

    Accepts the same ``"face:x,y,z"`` / ``"x,y,z"`` / ``"Edge<1>"`` forms as
    ``add_fillet`` (via :func:`_parse_edge_spec`), plus ``"edge:x,y,z"`` and
    ``"vertex:x,y,z"``. Coordinates are in metres.

    Args:
        spec: The entity spec.

    Returns:
        tuple[str, float, float, float, str]: name, x, y, z and entity type.
    """
    prefix, sep, rest = spec.partition(":")
    kind = _MEASURE_PREFIXES.get(prefix.strip().lower()) if sep else None
    if kind in {"EDGE", "VERTEX"}:
        parts = rest.split(",")
        if len(parts) == 3:
            try:
                return "", float(parts[0]), float(parts[1]), float(parts[2]), kind
            except ValueError:
                pass
        return rest, 0.0, 0.0, 0.0, kind
    return _parse_edge_spec(spec)

_read_custom_property

_read_custom_property(adapter: Any, manager: Any, name: str) -> tuple[str, str]

Read one custom property's (value, resolved value).

Tries Get6 (value and resolved value) first; under late binding on SW 2025 its out-parameters raise Parameter not optional, so it falls back to Get(name), which returns the stored text only.

Parameters:

Name Type Description Default
adapter Any

The adapter, for its _attempt helper.

required
manager Any

The document's ICustomPropertyManager.

required
name str

Property name.

required

Returns:

Type Description
str

tuple[str, str]: The stored value and the resolved value; empty

str

strings when the property cannot be read.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
def _read_custom_property(adapter: Any, manager: Any, name: str) -> tuple[str, str]:
    """Read one custom property's (value, resolved value).

    Tries ``Get6`` (value and resolved value) first; under late binding on
    SW 2025 its out-parameters raise ``Parameter not optional``, so it falls
    back to ``Get(name)``, which returns the stored text only.

    Args:
        adapter: The adapter, for its ``_attempt`` helper.
        manager: The document's ``ICustomPropertyManager``.
        name: Property name.

    Returns:
        tuple[str, str]: The stored value and the resolved value; empty
        strings when the property cannot be read.
    """
    value, resolved = _unpack_custom_property(
        adapter._attempt(lambda: manager.Get6(name, False), default=None)
    )
    if value:
        return value, resolved
    plain = adapter._attempt(lambda: manager.Get(name), default=None)
    text = str(plain) if isinstance(plain, str) else ""
    return text, text

_read_material

_read_material(part: Any, configuration: str | None) -> tuple[str, str]

Read (material, database) from IPartDoc::GetMaterialPropertyName2.

Database is a String out-parameter: under late binding on SW 2026 omitting it raises "Parameter not optional", so it is passed as a byref BSTR and read back from .value. Older pywin32 builds may instead return (name, database) as a tuple; both shapes are accepted.

Parameters:

Name Type Description Default
part Any

The active IPartDoc.

required
configuration str | None

Configuration name; the active one when None.

required

Returns:

Type Description
tuple[str, str]

tuple[str, str]: Material name and library; empty strings when unset.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
def _read_material(part: Any, configuration: str | None) -> tuple[str, str]:
    """Read ``(material, database)`` from ``IPartDoc::GetMaterialPropertyName2``.

    ``Database`` is a ``String`` out-parameter: under late binding on SW 2026
    omitting it raises "Parameter not optional", so it is passed as a byref
    BSTR and read back from ``.value``. Older pywin32 builds may instead return
    ``(name, database)`` as a tuple; both shapes are accepted.

    Args:
        part: The active ``IPartDoc``.
        configuration: Configuration name; the active one when ``None``.

    Returns:
        tuple[str, str]: Material name and library; empty strings when unset.
    """
    database = _byref_bstr()
    raw = part.GetMaterialPropertyName2(configuration or "", database)
    if isinstance(raw, (list, tuple)):
        strings = [str(s) for s in raw if isinstance(s, str)]
        name = strings[0] if strings else ""
        library = strings[1] if len(strings) > 1 else ""
    else:
        name = raw if isinstance(raw, str) else ""
        library = ""
    stored = getattr(database, "value", "")
    return name, library or (stored if isinstance(stored, str) else "")

_resolve_drawing_view

_resolve_drawing_view(adapter: Any, drawing: Any, view_name: str) -> Any

Return the IView on the active drawing whose name matches.

Walks IDrawingDoc::GetViews (a list of sheets, each shaped (sheet, view, view, ...)) the same way :func:_view_names does. The match is case-insensitive.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter.

required
drawing Any

The drawing document, flagged for IDrawingDoc.

required
view_name str

The drawing-view name to look for.

required

Returns:

Name Type Description
Any Any

The flagged IView dispatch, or None when nothing matches.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _resolve_drawing_view(adapter: Any, drawing: Any, view_name: str) -> Any:
    """Return the ``IView`` on the active drawing whose name matches.

    Walks ``IDrawingDoc::GetViews`` (a list of sheets, each shaped
    ``(sheet, view, view, ...)``) the same way :func:`_view_names` does. The
    match is case-insensitive.

    Args:
        adapter: A connected ``PyWin32Adapter``.
        drawing: The drawing document, flagged for ``IDrawingDoc``.
        view_name: The drawing-view name to look for.

    Returns:
        Any: The flagged ``IView`` dispatch, or ``None`` when nothing matches.
    """
    target = str(view_name or "").strip().lower()
    if not target:
        return None
    sheets = adapter._attempt(lambda: drawing.GetViews(), default=None)
    if not isinstance(sheets, (list, tuple)):
        return None
    for sheet in sheets:
        views = sheet if isinstance(sheet, (list, tuple)) else [sheet]
        for index, view in enumerate(views):
            if index == 0 and isinstance(sheet, (list, tuple)):
                continue
            wrapped = _as_com(adapter, view, "IView")
            if wrapped is None:
                continue
            name = adapter._attempt(lambda w=wrapped: w.GetName2(), default=None)
            if name and str(name).lower() == target:
                return wrapped
    return None

_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', picker: _BodyEntityPicker | None = None) -> bool

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

Calls ForceRebuild3 on the first entity in the selection set so that recent features are fully tessellated and selectable, then resolves the point with :class:_BodyEntityPicker - the entity whose closest point is nearest, independent of the view. Only when no body edge/face can be read at all does it fall back to the view-dependent SelectByID2 pick.

entity_type is normally "EDGE", but pass "FACE" to select a whole face instead. Any other type ("VERTEX" from measure_entities) goes straight to the SelectByID2 pick: a vertex lies on its edges at distance 0, so the edge walk would select an edge in its place.

Parameters:

Name Type Description Default
adapter Any

A connected adapter with a valid currentModel.

required
x float

X in metres.

required
y float

Y in metres.

required
z float

Z in metres.

required
append bool

Add to the current selection instead of replacing it.

required
mark int

Selection mark.

0
entity_type str

"EDGE", "FACE", or another SelectByID2 type such as "VERTEX".

'EDGE'
picker _BodyEntityPicker | None

Reuse a picker (and its body walk) across several picks.

None

Returns:

Name Type Description
bool bool

True if an entity was selected; False otherwise.

Raises:

Type Description
LookupError

When the nearest entity is further than _PICK_TOLERANCE_M from the point - picking it anyway would silently act on the wrong edge.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
def _select_edge_by_coord(
    adapter: Any,
    x: float,
    y: float,
    z: float,
    append: bool,
    mark: int = 0,
    entity_type: str = "EDGE",
    picker: _BodyEntityPicker | None = None,
) -> bool:
    """Select the edge (or face) nearest to (x, y, z) in metres.

    Calls ``ForceRebuild3`` on the first entity in the selection set so that
    recent features are fully tessellated and selectable, then resolves the
    point with :class:`_BodyEntityPicker` - the entity whose closest point is
    nearest, independent of the view. Only when no body edge/face can be read
    at all does it fall back to the view-dependent ``SelectByID2`` pick.

    ``entity_type`` is normally ``"EDGE"``, but pass ``"FACE"`` to select a
    whole face instead. Any other type (``"VERTEX"`` from
    ``measure_entities``) goes straight to the ``SelectByID2`` pick: a vertex
    lies on its edges at distance 0, so the edge walk would select an edge
    in its place.

    Args:
        adapter: A connected adapter with a valid ``currentModel``.
        x: X in metres.
        y: Y in metres.
        z: Z in metres.
        append: Add to the current selection instead of replacing it.
        mark: Selection mark.
        entity_type: ``"EDGE"``, ``"FACE"``, or another ``SelectByID2``
            type such as ``"VERTEX"``.
        picker: Reuse a picker (and its body walk) across several picks.

    Returns:
        bool: True if an entity was selected; False otherwise.

    Raises:
        LookupError: When the nearest entity is further than
            ``_PICK_TOLERANCE_M`` from the point - picking it anyway would
            silently act on the wrong edge.
    """
    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)

    # Only edges and faces are walked on the body; anything else (a vertex)
    # keeps the SelectByID2 pick, which honours the requested type.
    if entity_type not in ("EDGE", "FACE"):
        return _select_by_view_pick(adapter, x, y, z, append, mark, entity_type)

    picker = picker or _BodyEntityPicker(adapter)
    getter = "GetFaces" if entity_type == "FACE" else "GetEdges"
    nearest = picker.nearest(getter, (x, y, z))
    if nearest is None:
        return _select_by_view_pick(adapter, x, y, z, append, mark, entity_type)

    entity, distance = nearest
    if distance > _PICK_TOLERANCE_M:
        kind = entity_type.lower()
        raise LookupError(
            f"No {kind} passes through ({x * 1000:g}, {y * 1000:g}, "
            f"{z * 1000:g}) mm; the nearest is {distance * 1000:.3f} mm away. "
            f"Give a point on the {kind} itself."
        )
    return picker.select(entity, append, mark)

_sequence

_sequence(raw: Any) -> list[Any]

Normalise a COM array return (None, scalar or tuple) into a list.

Parameters:

Name Type Description Default
raw Any

What the COM call returned.

required

Returns:

Type Description
list[Any]

list[Any]: The non-None items.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
def _sequence(raw: Any) -> list[Any]:
    """Normalise a COM array return (``None``, scalar or tuple) into a list.

    Args:
        raw: What the COM call returned.

    Returns:
        list[Any]: The non-``None`` items.
    """
    if raw is None:
        return []
    if isinstance(raw, (list, tuple)):
        return [item for item in raw if item is not None]
    return [raw]

_strip_document_suffix

_strip_document_suffix(full_name: Any) -> str | None

Turn "D1@Sketch1@Part10.Part" into "D1@Sketch1".

Live SW 2025 appends @<document title> to IDimension::FullName; IModelDoc2::Parameter (behind set_dimension) takes the short form.

Parameters:

Name Type Description Default
full_name Any

IDimension::FullName as read.

required

Returns:

Type Description
str | None

str | None: The name without the document suffix; None when

str | None

full_name is not text.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
def _strip_document_suffix(full_name: Any) -> str | None:
    """Turn ``"D1@Sketch1@Part10.Part"`` into ``"D1@Sketch1"``.

    Live SW 2025 appends ``@<document title>`` to ``IDimension::FullName``;
    ``IModelDoc2::Parameter`` (behind ``set_dimension``) takes the short form.

    Args:
        full_name: ``IDimension::FullName`` as read.

    Returns:
        str | None: The name without the document suffix; ``None`` when
        ``full_name`` is not text.
    """
    if not isinstance(full_name, str):
        return None
    parts = full_name.split("@")
    if len(parts) >= 3 and "." in parts[-1]:
        return "@".join(parts[:-1])
    return full_name

_unpack_custom_property

_unpack_custom_property(raw: Any) -> tuple[str, str]

Pull (value, resolved value) out of an ICustomPropertyManager::Get6 result.

Under late binding the out-parameters come back as a tuple that may or may not lead with the integer return code, depending on the pywin32 build.

Parameters:

Name Type Description Default
raw Any

Whatever Get6 returned.

required

Returns:

Type Description
str

tuple[str, str]: The raw (possibly $PRP: linked) value and the

str

resolved value; empty strings when the shape is unrecognised.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
def _unpack_custom_property(raw: Any) -> tuple[str, str]:
    """Pull (value, resolved value) out of an ``ICustomPropertyManager::Get6`` result.

    Under late binding the out-parameters come back as a tuple that may or may
    not lead with the integer return code, depending on the pywin32 build.

    Args:
        raw: Whatever ``Get6`` returned.

    Returns:
        tuple[str, str]: The raw (possibly ``$PRP:`` linked) value and the
        resolved value; empty strings when the shape is unrecognised.
    """
    if not isinstance(raw, (list, tuple)):
        return ("", "")
    items = list(raw)
    if items and isinstance(items[0], int) and not isinstance(items[0], bool):
        items = items[1:]
    strings = [str(item) for item in items if isinstance(item, str)]
    if not strings:
        return ("", "")
    value = strings[0]
    resolved = strings[1] if len(strings) > 1 else value
    return (value, resolved)

_view_names

_view_names(adapter: Any, drawing: Any) -> list[str]

Return the drawing's view names, excluding sheet formats.

CreateDrawViewFromModelView3 returns None for a model SolidWorks could not resolve, and Create3rdAngleViews2 returns a bare boolean, so the view list is the ground truth for whether views were really added.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter.

required
drawing Any

The drawing document, flagged for IDrawingDoc.

required

Returns:

Type Description
list[str]

list[str]: View names in sheet order.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _view_names(adapter: Any, drawing: Any) -> list[str]:
    """Return the drawing's view names, excluding sheet formats.

    ``CreateDrawViewFromModelView3`` returns ``None`` for a model SolidWorks
    could not resolve, and ``Create3rdAngleViews2`` returns a bare boolean, so
    the view list is the ground truth for whether views were really added.

    Args:
        adapter: A connected ``PyWin32Adapter``.
        drawing: The drawing document, flagged for ``IDrawingDoc``.

    Returns:
        list[str]: View names in sheet order.
    """
    sheets = adapter._attempt(lambda: drawing.GetViews(), default=None)
    if not isinstance(sheets, (list, tuple)):
        return []

    names: list[str] = []
    for sheet in sheets:
        views = sheet if isinstance(sheet, (list, tuple)) else [sheet]
        for index, view in enumerate(views):
            # GetViews returns (sheet, view, view, ...) per sheet; entry 0 is
            # the sheet itself, not a drawing view.
            if index == 0 and isinstance(sheet, (list, tuple)):
                continue
            wrapped = _as_com(adapter, view, "IView")
            if wrapped is None:
                continue
            name = adapter._attempt(lambda w=wrapped: w.GetName2(), default=None)
            if name:
                names.append(str(name))
    return names