Skip to content

solidworks_mcp.adapters.solidworks.drawing_standard

solidworks_mcp.adapters.solidworks.drawing_standard

Standard A3 drawing generation for the PyWin32 adapter.

Implements an A3 ISO drafting standard on SolidWorks:

  • Sheet - A3 landscape 420 x 297 mm with the SolidWorks a3 - iso sheet format (zoned border, ISO title block). Working area x 15-405, y 15-282 mm; the title block area x 218-412, y 8-72 mm is kept clear.
  • Title block - TITLE from the model's Description, DWG NO. from the drawing file name (saved as the drawing number), MATERIAL / WEIGHT linked to the model's real material and mass, DRAWN by/date, revision and company from drawing properties; scale, sheet n of N and size are automatic.
  • Views - third-angle front, plan and side view plus an isometric, each in its own cell, at the largest standard scale where the part, its dimension rows and its caption fit (cells sized from the part, so it fills the sheet). Each view gets a bold capital caption with its name, e.g. FRONT VIEW.
  • Dimensions - imported from the model, each shown once, laid out in rows (first 8 mm out, rows 7 mm, columns 9 mm), trailing zeros removed.
  • Notes - a NOTES block in its own cell (material, units, projection, plus caller lines).
  • Title-block fields fit their boxes - the DWG NO. (the drawing's file name) and TITLE notes of the sheet format are shrunk, down to 2 mm, until they fill at most 90% of the room before the next field in their row; one that still runs over is an overlap. title_block_text_height_mm caps their height for smaller text.
  • No overlap anywhere - checked against the real read-back geometry. The scale is chosen once to fit the sheet and is never stepped down; overlaps are resolved by moving dimensions.

The drawing is built in separate COM steps (sheet, views, layout, dimensions, notes, check), each through _handle_com_operation so each gets its settle pause: one long burst of drawing calls is the pattern that crashed SolidWorks 2026 in its idle-time refresh (COM pitfall #26).

Attributes

BOM_GAP module-attribute

BOM_GAP = 0.005

Box module-attribute

Box = tuple[float, float, float, float]

CAPTIONS module-attribute

CAPTIONS = {'front': 'FRONT VIEW', 'plan': 'TOP VIEW', 'side': 'SIDE VIEW', 'iso': 'ISOMETRIC VIEW'}

CAPTION_HEIGHT module-attribute

CAPTION_HEIGHT = 0.0032

CAPTION_STRIP module-attribute

CAPTION_STRIP = 0.009

CELLS module-attribute

CELLS: dict[str, Box] = {'front': (0.015, 0.075, 0.215, 0.178), 'side': (0.215, 0.075, 0.405, 0.178), 'plan': (0.015, 0.178, 0.215, 0.282), 'iso': (0.215, 0.178, 0.405, 0.282), 'notes': (0.015, 0.015, 0.215, 0.075)}

FIELD_FILL module-attribute

FIELD_FILL = 0.9

FIELD_GAP module-attribute

FIELD_GAP = 0.002

FIELD_HINT module-attribute

FIELD_HINT = 'pass title_block_text_height_mm (e.g. 3) for smaller title-block text, or a shorter drawing_number or title'

FIT_H_MARGIN module-attribute

FIT_H_MARGIN = _layout.FIRST_OFFSET + _layout.COLUMN_PITCH

FIT_V_MARGIN module-attribute

FIT_V_MARGIN = _layout.FIRST_OFFSET + _layout.ROW_PITCH

ISO_MARGIN module-attribute

ISO_MARGIN = 0.006

MAX_NOTE_LINES module-attribute

MAX_NOTE_LINES = 7

MIN_FIELD_HEIGHT module-attribute

MIN_FIELD_HEIGHT = 0.002

NOTE_HEIGHT module-attribute

NOTE_HEIGHT = 0.003

SHEET_SIZE module-attribute

SHEET_SIZE = (0.42, 0.297)

STANDARD_NOTES module-attribute

STANDARD_NOTES = ('ALL DIMENSIONS IN MILLIMETRES', 'THIRD ANGLE PROJECTION', 'REMOVE ALL BURRS AND SHARP EDGES')

STANDARD_SCALES module-attribute

STANDARD_SCALES: list[tuple[float, float]] = [(5, 1), (4, 1), (3, 1), (2.5, 1), (2, 1), (1.5, 1), (1, 1), (1, 2), (1, 2.5), (1, 4), (1, 5), (1, 7.5), (1, 10), (1, 12.5), (1, 15), (1, 20), (1, 25), (1, 30), (1, 40), (1, 50), (1, 60), (1, 75), (1, 100)]

TITLE_BLOCK module-attribute

TITLE_BLOCK: Box = (0.218, 0.008, 0.412, 0.072)

TITLE_BLOCK_FIELDS module-attribute

TITLE_BLOCK_FIELDS = {'SW-File Name': 'drawing number', 'Description': 'title'}

VIEW_ZONE module-attribute

VIEW_ZONE: Box = (0.015, 0.075, 0.405, 0.282)

WORK_AREA module-attribute

WORK_AREA: Box = (0.015, 0.015, 0.405, 0.282)

_GTOL_MODIFIERS module-attribute

_GTOL_MODIFIERS = {'none': '', 'mmc': '<MOD-MMC>', 'lmc': '<MOD-LMC>'}

_GTOL_SYMBOLS module-attribute

_GTOL_SYMBOLS = {'position': '<IGTOL-POSI>', 'flatness': '<IGTOL-FLAT>', 'perpendicularity': '<IGTOL-PERP>', 'parallelism': '<IGTOL-PARA>', 'profile_surface': '<IGTOL-SPROF>', 'concentricity': '<IGTOL-CONC>'}

_ISO_DROP_POINT module-attribute

_ISO_DROP_POINT = (0.31, 0.23)

_SW_BOM_ANCHOR_BOTTOM_LEFT module-attribute

_SW_BOM_ANCHOR_BOTTOM_LEFT = 3

_SW_BOM_ANCHOR_BOTTOM_RIGHT module-attribute

_SW_BOM_ANCHOR_BOTTOM_RIGHT = 4

_SW_BOM_ANCHOR_TOP_LEFT module-attribute

_SW_BOM_ANCHOR_TOP_LEFT = 1

_SW_BOM_ANCHOR_TOP_RIGHT module-attribute

_SW_BOM_ANCHOR_TOP_RIGHT = 2

_SW_BOM_TYPES module-attribute

_SW_BOM_TYPES = {'parts_only': 1, 'top_level_only': 2, 'indented': 3}

_SW_DETAILING_DIM_TRAILING_ZERO module-attribute

_SW_DETAILING_DIM_TRAILING_ZERO = 15

_SW_DIM_REMOVE_TRAILING_ZEROES module-attribute

_SW_DIM_REMOVE_TRAILING_ZEROES = 2

_SW_HIDDEN_LINES_VISIBLE module-attribute

_SW_HIDDEN_LINES_VISIBLE = 1

_SW_NOTE_OWN_FORMAT module-attribute

_SW_NOTE_OWN_FORMAT = 0

_SW_NUMBERING_NONE module-attribute

_SW_NUMBERING_NONE = 0

_SW_PAPER_A3 module-attribute

_SW_PAPER_A3 = 8

_SW_PREF_BOM_TEMPLATES module-attribute

_SW_PREF_BOM_TEMPLATES = 22

_SW_PREF_SHEET_FORMAT_FOLDER module-attribute

_SW_PREF_SHEET_FORMAT_FOLDER = 7

_SW_SHOW_SHEET_FORMAT_DIALOG module-attribute

_SW_SHOW_SHEET_FORMAT_DIALOG = 353

_SW_TEMPLATE_CUSTOM module-attribute

_SW_TEMPLATE_CUSTOM = 12

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.

SolidWorksDrawingStandardMixin

Create complete standard A3 drawings, BOM tables and the sheet scale.

Methods:
add_datum_tag async
add_datum_tag(label: str, attach_at_mm: tuple[float, float], x_mm: float, y_mm: float) -> AdapterResult[dict[str, Any]]

Add a datum feature symbol (a datum tag) on a drawing edge.

IModelDoc2::InsertDatumTag2 attaches the tag to the edge picked at attach_at_mm; IDatumTag::SetLabel names it, and the label is read back with GetLabel.

Parameters:

Name Type Description Default
label str

The datum letter, e.g. A.

required
attach_at_mm tuple[float, float]

A sheet point on the edge or face line it marks.

required
x_mm float

Where the tag goes on the sheet.

required
y_mm float

Where the tag goes on the sheet.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The label as read back.

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
async def add_datum_tag(
    self,
    label: str,
    attach_at_mm: tuple[float, float],
    x_mm: float,
    y_mm: float,
) -> AdapterResult[dict[str, Any]]:
    """Add a datum feature symbol (a datum tag) on a drawing edge.

    ``IModelDoc2::InsertDatumTag2`` attaches the tag to the edge picked at
    ``attach_at_mm``; ``IDatumTag::SetLabel`` names it, and the label is
    read back with ``GetLabel``.

    Args:
        label: The datum letter, e.g. ``A``.
        attach_at_mm: A sheet point on the edge or face line it marks.
        x_mm: Where the tag goes on the sheet.
        y_mm: Where the tag goes on the sheet.

    Returns:
        AdapterResult[dict[str, Any]]: The label as read back.
    """
    guard = self._require_drawing()
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)
    letter = label.strip().upper()
    if len(letter) != 1 or not letter.isalpha():
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="label must be a single letter, e.g. 'A'",
        )

    def _insert() -> dict[str, Any]:
        model = self.currentModel
        if not self._select_edge_at(attach_at_mm):
            raise LookupError(f"No drawing edge found at {list(attach_at_mm)} mm")
        tag = self._get_attr_or_call(model, "InsertDatumTag2")
        self._attempt(lambda: model.ClearSelection2(True), default=None)
        if tag is None:
            raise Exception("InsertDatumTag2 returned nothing; no tag was added")
        tag = _sw_type_info.flagged(tag, "IDatumTag")
        tag.SetLabel(letter)
        self._place_annotation(tag, x_mm, y_mm)
        read = str(
            self._attempt(lambda: self._get_attr_or_call(tag, "GetLabel")) or ""
        )
        if read != letter:
            raise Exception(f"The datum tag reads {read!r}, not {letter!r}")
        return {"label": letter, "position_mm": [x_mm, y_mm]}

    return cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("add_datum_tag", _insert),
    )
add_gtol async
add_gtol(characteristic: str, tolerance: float, x_mm: float, y_mm: float, diameter_zone: bool = False, material_condition: str = 'none', datums: list[str] | None = None, attach_at_mm: tuple[float, float] | None = None, dimension: tuple[str, str] | None = None) -> AdapterResult[dict[str, Any]]

Add a feature control frame (GD&T) to the active drawing.

IModelDoc2::InsertGtol creates the frame on whatever is selected: a drawing dimension (dimension; the frame sits under its text and moves with it, the usual place for a hole's position tolerance), the edge picked at attach_at_mm (with a leader), or nothing (free-standing); IGtol::SetFrameSymbols2 sets the characteristic, the Ø zone and the material condition, and IGtol::SetFrameValues the tolerance and datums. The values are read back with GetFrameValues, so a frame SolidWorks did not fill in is an error, not a success.

Parameters:

Name Type Description Default
characteristic str

position, flatness, perpendicularity, parallelism, profile_surface or concentricity.

required
tolerance float

The tolerance value, in drawing units.

required
x_mm float

Where the frame goes on the sheet.

required
y_mm float

Where the frame goes on the sheet.

required
diameter_zone bool

Prefix the tolerance with Ø (a cylindrical zone).

False
material_condition str

none, mmc (Ⓜ) or lmc (Ⓛ).

'none'
datums list[str] | None

Up to three datum letters, primary first.

None
attach_at_mm tuple[float, float] | None

A sheet point on the edge the leader points to.

None
dimension tuple[str, str] | None

(view name, model dimension name), e.g. ("Drawing View2", "D3@Sketch1"): attach the frame to that drawing dimension. x_mm/y_mm are then not used.

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The frame as read back.

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
async def add_gtol(
    self,
    characteristic: str,
    tolerance: float,
    x_mm: float,
    y_mm: float,
    diameter_zone: bool = False,
    material_condition: str = "none",
    datums: list[str] | None = None,
    attach_at_mm: tuple[float, float] | None = None,
    dimension: tuple[str, str] | None = None,
) -> AdapterResult[dict[str, Any]]:
    """Add a feature control frame (GD&T) to the active drawing.

    ``IModelDoc2::InsertGtol`` creates the frame on whatever is selected:
    a drawing dimension (``dimension``; the frame sits under its text and
    moves with it, the usual place for a hole's position tolerance), the
    edge picked at ``attach_at_mm`` (with a leader), or nothing
    (free-standing);
    ``IGtol::SetFrameSymbols2`` sets the characteristic, the Ø zone and
    the material condition, and ``IGtol::SetFrameValues`` the tolerance
    and datums. The values are read back with ``GetFrameValues``, so a
    frame SolidWorks did not fill in is an error, not a success.

    Args:
        characteristic: ``position``, ``flatness``, ``perpendicularity``,
            ``parallelism``, ``profile_surface`` or ``concentricity``.
        tolerance: The tolerance value, in drawing units.
        x_mm: Where the frame goes on the sheet.
        y_mm: Where the frame goes on the sheet.
        diameter_zone: Prefix the tolerance with Ø (a cylindrical zone).
        material_condition: ``none``, ``mmc`` (Ⓜ) or ``lmc`` (Ⓛ).
        datums: Up to three datum letters, primary first.
        attach_at_mm: A sheet point on the edge the leader points to.
        dimension: ``(view name, model dimension name)``, e.g.
            ``("Drawing View2", "D3@Sketch1")``: attach the frame to that
            drawing dimension. ``x_mm``/``y_mm`` are then not used.

    Returns:
        AdapterResult[dict[str, Any]]: The frame as read back.
    """
    guard = self._require_drawing()
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)
    problem = gtol_problem(characteristic, tolerance, material_condition, datums)
    if problem is not None:
        return AdapterResult(status=AdapterResultStatus.ERROR, error=problem)
    symbol = _GTOL_SYMBOLS[characteristic]
    modifier = _GTOL_MODIFIERS[material_condition]
    letters = [d.strip().upper() for d in datums or []]

    def _insert() -> dict[str, Any]:
        model = self.currentModel
        target = None
        if dimension is not None:
            target = self._drawing_dimension(*dimension)
            if target is None or not self._select_display_dimension(model, target):
                raise LookupError(
                    f"No dimension {dimension[1]!r} in view {dimension[0]!r}"
                )
            attached: Any = dimension[1]
        elif attach_at_mm is not None:
            if not self._select_edge_at(attach_at_mm):
                raise LookupError(
                    f"No drawing edge found at {list(attach_at_mm)} mm"
                )
            attached = True
        else:
            attached = False
        gtol = self._get_attr_or_call(model, "InsertGtol")
        self._attempt(lambda: model.ClearSelection2(True), default=None)
        if gtol is None:
            raise Exception("InsertGtol returned nothing; no frame was added")
        gtol = _sw_type_info.flagged(gtol, "IGtol")
        padded = [*letters, "", "", ""][:3]
        text = f"{tolerance:g}"
        gtol.SetFrameSymbols2(
            1, symbol, diameter_zone, modifier, False, "", "", "", ""
        )
        gtol.SetFrameValues(1, text, "", *padded)
        if dimension is None:
            self._place_annotation(gtol, x_mm, y_mm)
        self._attempt(lambda: model.GraphicsRedraw2(), default=None)
        values = self._attempt(lambda: gtol.GetFrameValues(1), default=None)
        read = [str(v) for v in values] if isinstance(values, (list, tuple)) else []
        if not read or read[0] != text or read[2:5] != padded:
            raise Exception(
                f"The frame did not take its values: read back {read or None}, "
                f"expected tolerance {text!r} and datums {padded}"
            )
        return {
            "characteristic": characteristic,
            "tolerance": text,
            "diameter_zone": diameter_zone,
            "material_condition": material_condition,
            "datums": letters,
            "attached": attached,
            "position_mm": None if dimension is not None else [x_mm, y_mm],
            "layout": (
                None
                if dimension is None
                else self._clear_attached_frame(dimension[0], target, gtol)
            ),
        }

    result = cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("add_gtol", _insert),
    )
    layout = (result.data or {}).get("layout") if result.is_success else None
    if layout and layout["overlaps_after"]:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=(
                f"The frame was added but {layout['overlaps_after']} overlap(s) "
                "remain on the sheet - the no-overlap rule is not met; see layout"
            ),
            data=result.data,
            execution_time=result.execution_time,
        )
    return result
create_standard_drawing async
create_standard_drawing(model_path: str, output_dir: str | None = None, title: str | None = None, subtitle: str | None = None, drawing_number: str | None = None, author: str = '', revision: str = 'A', company: str = '', drawn_date: str | None = None, notes: list[str] | None = None, include_isometric: bool = True, write_model_properties: bool = True, title_block_text_height_mm: float | None = None, include_bom: bool = False) -> AdapterResult[dict[str, Any]]

Create a complete A3 drawing that follows the drawing standard.

Opens the model (or uses its open copy), writes the title-block properties into it, makes a drawing on the A3 ISO format, builds the sheet in separate COM steps, writes the drawing properties, saves <drawing_number>.SLDDRW and exports a PDF.

Parameters:

Name Type Description Default
model_path str

Part (.SLDPRT) or assembly (.SLDASM) to draw.

required
output_dir str | None

Folder for the .SLDDRW and PDF; the model's folder by default.

None
title str | None

Title block TITLE; the model file name by default.

None
subtitle str | None

Company field when company is empty.

None
drawing_number str | None

DWG NO.; also the drawing file name. Default <MODEL>-Rev<revision>-01.

None
author str

DRAWN name.

''
revision str

Revision letter.

'A'
company str

Company name field.

''
drawn_date str | None

DRAWN date; today (e.g. "17 Oct 2026") by default.

None
notes list[str] | None

Extra note lines (at most seven lines in total are shown).

None
include_isometric bool

Add the isometric view in its cell.

True
write_model_properties bool

Write Description/Material/Weight custom properties into the model (the format reads them from there) and save the model.

True
title_block_text_height_mm float | None

Largest text height for the DWG NO. and TITLE fields, e.g. 3 for smaller text; they are shrunk further when they still do not fit.

None
include_bom bool

Assemblies only: a top-level BOM in the top of the isometric's cell, the isometric fitted below it.

False

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: Files, scale, views, dimension

AdapterResult[dict[str, Any]]

counts, the overlap check, every property written and each step's

AdapterResult[dict[str, Any]]

outcome. ERROR, with that data, when a step fails, the drawing or

AdapterResult[dict[str, Any]]

PDF could not be saved, or any overlap remains.

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
async def create_standard_drawing(
    self,
    model_path: str,
    output_dir: str | None = None,
    title: str | None = None,
    subtitle: str | None = None,
    drawing_number: str | None = None,
    author: str = "",
    revision: str = "A",
    company: str = "",
    drawn_date: str | None = None,
    notes: list[str] | None = None,
    include_isometric: bool = True,
    write_model_properties: bool = True,
    title_block_text_height_mm: float | None = None,
    include_bom: bool = False,
) -> AdapterResult[dict[str, Any]]:
    """Create a complete A3 drawing that follows the drawing standard.

    Opens the model (or uses its open copy), writes the title-block
    properties into it, makes a drawing on the A3 ISO format, builds the
    sheet in separate COM steps, writes the drawing properties, saves
    ``<drawing_number>.SLDDRW`` and exports a PDF.

    Args:
        model_path: Part (.SLDPRT) or assembly (.SLDASM) to draw.
        output_dir: Folder for the .SLDDRW and PDF; the model's folder by default.
        title: Title block TITLE; the model file name by default.
        subtitle: Company field when ``company`` is empty.
        drawing_number: DWG NO.; also the drawing file name. Default
            ``<MODEL>-Rev<revision>-01``.
        author: DRAWN name.
        revision: Revision letter.
        company: Company name field.
        drawn_date: DRAWN date; today (e.g. "17 Oct 2026") by default.
        notes: Extra note lines (at most seven lines in total are shown).
        include_isometric: Add the isometric view in its cell.
        write_model_properties: Write Description/Material/Weight custom
            properties into the model (the format reads them from there)
            and save the model.
        title_block_text_height_mm: Largest text height for the DWG NO.
            and TITLE fields, e.g. 3 for smaller text; they are shrunk
            further when they still do not fit.
        include_bom: Assemblies only: a top-level BOM in the top of the
            isometric's cell, the isometric fitted below it.

    Returns:
        AdapterResult[dict[str, Any]]: Files, scale, views, dimension
        counts, the overlap check, every property written and each step's
        outcome. ERROR, with that data, when a step fails, the drawing or
        PDF could not be saved, or any overlap remains.
    """
    path = os.path.abspath(model_path)
    if not os.path.isfile(path):
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error=f"Model file not found: {path}"
        )
    ext = os.path.splitext(path)[1].lower()
    if ext not in {".sldprt", ".sldasm"}:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=f"model_path must be a .SLDPRT or .SLDASM, not {ext or 'no extension'}",
        )
    if include_bom and ext != ".sldasm":
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="include_bom needs an assembly (.SLDASM); a part has no BOM",
        )
    stem = os.path.splitext(os.path.basename(path))[0]
    rev = (revision or "A").strip()
    number = drawing_number or f"{stem.upper().replace(' ', '-')}-Rev{rev}-01"
    out_dir = os.path.abspath(output_dir or os.path.dirname(path))
    drawing_path = os.path.join(out_dir, f"{number}.SLDDRW")
    pdf_path = os.path.join(out_dir, f"{number}.pdf")
    steps: list[dict[str, Any]] = []
    written: dict[str, dict[str, str]] = {}
    data: dict[str, Any] = {"drawing_number": number, "steps": steps}

    def failed(error: str) -> AdapterResult[dict[str, Any]]:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error=error, data=data
        )

    # 1. The model: open it, read its material, write the title-block properties.
    active = self._handle_com_operation(
        "activate_document", lambda: self._use_open_document(path)
    )
    if not (active.is_success and active.data):
        opened = await self.open_model(path)
        if not opened.is_success:
            return failed(
                f"Could not open model: {opened.error}. If a different file "
                "with the same name is open in SolidWorks, close it first."
            )
    material = None
    if ext == ".sldprt":
        mat = await self.get_material()
        material = (mat.data or {}).get("material") if mat.is_success else None
    model_props, drawing_props = title_block_properties(
        model_file=path,
        title=title or stem,
        subtitle=subtitle,
        author=author,
        drawn_date=drawn_date or _date.today().strftime("%d %b %Y"),
        revision=rev,
        company=company,
    )
    if ext == ".sldasm":
        # An assembly has no material of its own: a link to it shows
        # "<not specified>". Its own Material property is left as set.
        model_props.pop("Material")
    if write_model_properties:
        for name, value in model_props.items():
            _step(
                steps,
                f"model_property:{name}",
                await self.set_custom_property(name, value),
            )
        _step(steps, "save_model", await self.save_file())
        written["model"] = model_props

    # 2. A drawing on the A3 ISO format, without the Sheet Format/Size prompt.
    found = self._handle_com_operation(
        "standard_drawing_format", lambda: find_sheet_format(self)
    )
    if not (found.is_success and found.data):
        return failed(
            "Sheet format 'a3 - iso.slddrt' not found in SolidWorks' sheet "
            "format folders or under C:\\ProgramData\\SOLIDWORKS"
        )
    prompt = self._handle_com_operation(
        "standard_drawing_dialog", self._new_drawing_without_dialog
    )
    try:
        created = await self.create_drawing()
    finally:
        self._handle_com_operation(
            "standard_drawing_dialog",
            lambda: self._restore_dialog(
                prompt.data if prompt.is_success else None
            ),
        )
    if not created.is_success:
        return failed(f"Could not create drawing: {created.error}")
    _step(steps, "drawing_units_mm", await self.set_units("mm"))

    # 3. The sheet, one COM step at a time.
    s = _Sheet(
        path=path,
        sheet_format=str(found.data),
        include_isometric=include_isometric,
        include_bom=include_bom,
        material=material,
        extra_notes=list(notes or []),
        field_height=(
            title_block_text_height_mm / 1000.0
            if title_block_text_height_mm
            else None
        ),
    )
    for name, build in (
        ("sheet", self._setup_a3_sheet),
        ("views", self._create_views),
        ("layout", self._fit_views),
        ("bom", self._bom_above_iso),
        ("dimensions", self._dimension_views),
        ("notes", self._annotate),
        ("check", self._check_sheet),
    ):
        done = self._handle_com_operation(
            f"standard_drawing_{name}", lambda build=build: build(s)
        )
        _step(steps, f"build_{name}", done)
        if not done.is_success:
            return failed(f"Drawing step '{name}' failed: {done.error}")
        if name == "dimensions":
            data["dimensions"] = done.data
        if name == "bom":
            data["bom"] = done.data
        if name == "views":
            data["hidden_lines_visible"] = done.data["hidden_lines_visible"]
        if name == "check":
            data["overlap_check"] = done.data
    data.update(
        {
            "scale": scale_label(s.scale),
            "isometric_scale": scale_label(s.iso_scale) if s.iso_scale else None,
            "views": {
                n: {"role": s.role.get(n), "caption": s.captions.get(n)}
                for n in s.views
            },
            "sheet_format": s.sheet_format,
            "notes": s.note_lines,
        }
    )

    # 4. Drawing properties, then the files.
    for name, value in drawing_props.items():
        _step(
            steps,
            f"drawing_property:{name}",
            await self.set_custom_property(name, value),
        )
    written["drawing"] = drawing_props
    data["properties_written"] = written
    os.makedirs(out_dir, exist_ok=True)
    saved = await self.save_file(drawing_path)
    _step(steps, "save_drawing", saved)
    data["drawing_path"] = drawing_path if saved.is_success else None
    if not saved.is_success:
        return failed(
            f"Drawing built but not saved to {drawing_path}: {saved.error}"
        )

    # 5. The title block, now that the DWG NO. shows the saved file name.
    fitted = self._handle_com_operation(
        "standard_drawing_title_block", lambda: self._fit_title_block(s)
    )
    _step(steps, "fit_title_block", fitted)
    data["title_block"] = fitted.data if fitted.is_success else None
    data["format_links"] = s.format_links
    check = data["overlap_check"]
    for name, field_fit in (data["title_block"] or {}).items():
        if field_fit.get("fits") is False:
            check["annotation_overlaps"].append(
                {
                    "dimension": f"title block {name}",
                    "conflicts_with": "the next title-block field",
                    "kind": "field_overflow",
                }
            )
            check["overlaps_after"] = check.get("overlaps_after", 0) + 1
    _step(steps, "save_drawing_again", await self.save_file(drawing_path))
    pdf = await self.export_file(pdf_path, "pdf")
    _step(steps, "export_pdf", pdf)
    data["pdf_path"] = pdf_path if pdf.is_success else None
    if not pdf.is_success:
        return failed(
            f"Drawing saved to {drawing_path} but the PDF export failed: {pdf.error}"
        )

    overlaps = data["overlap_check"].get("overlaps_after", 0)
    overflowing = any(
        f.get("fits") is False for f in (data.get("title_block") or {}).values()
    )
    if overlaps:
        check = data["overlap_check"]
        found = [
            *check.get("remaining_overlaps", []),
            *check.get("annotation_overlaps", []),
        ]
        named = "; ".join(
            f"{o['dimension']} vs {o['conflicts_with']} ({o['kind']})"
            for o in found[:5]
        )
        return failed(
            f"Drawing saved but {overlaps} overlap(s) remain - the no-overlap "
            "rule is not met; see overlap_check"
            + (f": {named}" if named else "")
            + (f". For a title-block field, {FIELD_HINT}" if overflowing else "")
        )
    return AdapterResult(status=AdapterResultStatus.SUCCESS, data=data)
insert_bom_table async
insert_bom_table(view_name: str, template_path: str | None = None, bom_type: str = 'top_level_only', x_mm: float | None = None, y_mm: float | None = None) -> AdapterResult[dict[str, Any]]

Insert a bill-of-materials table anchored to an assembly drawing view.

Wraps IView::InsertBomTable4. With no position the table snaps to the sheet format's BOM anchor, or to the sheet's bottom-left corner when the sheet has no format. Its stationary corner is the bottom-left, so the table grows up onto the sheet; with the top-left it hung below the sheet (measured live on SW 2026). The table's box is read back and must lie on the sheet. An anchor near the top of the sheet (the ISO A3 format's) makes it grow off the top, so a table placed by the anchor is slid back onto the sheet; one placed at a given x_mm/y_mm is never moved. With no template the library default bom-standard.sldbomtbt from the BOM templates folder is used.

Parameters:

Name Type Description Default
view_name str

A view of an assembly on the active drawing.

required
template_path str | None

.sldbomtbt table template.

None
bom_type str

top_level_only, parts_only or indented.

'top_level_only'
x_mm float | None

Optional table position on the sheet.

None
y_mm float | None

Optional table position on the sheet.

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The view, BOM type, template, row

AdapterResult[dict[str, Any]]

count, table_box_mm and moved_onto_sheet; an error naming

AdapterResult[dict[str, Any]]

the box when the table still runs off the sheet.

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
async def insert_bom_table(
    self,
    view_name: str,
    template_path: str | None = None,
    bom_type: str = "top_level_only",
    x_mm: float | None = None,
    y_mm: float | None = None,
) -> AdapterResult[dict[str, Any]]:
    """Insert a bill-of-materials table anchored to an assembly drawing view.

    Wraps ``IView::InsertBomTable4``. With no position the table snaps to
    the sheet format's BOM anchor, or to the sheet's bottom-left corner
    when the sheet has no format. Its stationary corner is the
    bottom-left, so the table grows up onto the sheet; with the top-left
    it hung below the sheet (measured live on SW 2026). The table's box is
    read back and must lie on the sheet. An anchor near the top of the
    sheet (the ISO A3 format's) makes it grow off the top, so a table
    placed by the anchor is slid back onto the sheet; one placed at a
    given ``x_mm``/``y_mm`` is never moved. With no template the library
    default ``bom-standard.sldbomtbt`` from the BOM templates folder is
    used.

    Args:
        view_name: A view of an assembly on the active drawing.
        template_path: ``.sldbomtbt`` table template.
        bom_type: ``top_level_only``, ``parts_only`` or ``indented``.
        x_mm: Optional table position on the sheet.
        y_mm: Optional table position on the sheet.

    Returns:
        AdapterResult[dict[str, Any]]: The view, BOM type, template, row
        count, ``table_box_mm`` and ``moved_onto_sheet``; an error naming
        the box when the table still runs off the sheet.
    """
    guard = self._require_drawing()
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)
    if bom_type not in _SW_BOM_TYPES:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="bom_type must be top_level_only, parts_only or indented",
        )

    return cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation(
            "insert_bom_table",
            lambda: self._insert_bom(
                view_name, template_path, bom_type, x_mm, y_mm
            )[1],
        ),
    )
set_sheet_scale async
set_sheet_scale(numerator: float, denominator: float) -> AdapterResult[dict[str, Any]]

Set the active drawing sheet's scale, e.g. 1:2 (ISheet::SetScale).

Parameters:

Name Type Description Default
numerator float

Scale numerator.

required
denominator float

Scale denominator.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The sheet name, the scale read

AdapterResult[dict[str, Any]]

back from the sheet and the requested one.

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
async def set_sheet_scale(
    self, numerator: float, denominator: float
) -> AdapterResult[dict[str, Any]]:
    """Set the active drawing sheet's scale, e.g. 1:2 (``ISheet::SetScale``).

    Args:
        numerator: Scale numerator.
        denominator: Scale denominator.

    Returns:
        AdapterResult[dict[str, Any]]: The sheet name, the ``scale`` read
        back from the sheet and the ``requested`` one.
    """
    guard = self._require_drawing()
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)
    if numerator <= 0 or denominator <= 0:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="Scale values must be positive"
        )

    def _scale() -> dict[str, Any]:
        drawing = _sw_type_info.flagged(self.currentModel, "IDrawingDoc")
        sheet = _sw_type_info.flagged(drawing.GetCurrentSheet(), "ISheet")
        if not sheet.SetScale(float(numerator), float(denominator), True, False):
            raise Exception("ISheet.SetScale returned False")
        self._attempt(lambda: self.currentModel.EditRebuild3(), default=None)
        name = self._attempt(lambda: sheet.GetName(), default=None)
        # GetProperties2: paper size, template, scale num, scale den, ...
        props = self._attempt(lambda: sheet.GetProperties2(), default=None)
        if not (isinstance(props, (list, tuple)) and len(props) >= 4):
            raise Exception("The sheet's scale could not be read back")
        return {
            "sheet": name,
            "scale": f"{float(props[2]):g}:{float(props[3]):g}",
            "requested": f"{numerator:g}:{denominator:g}",
        }

    return cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("set_sheet_scale", _scale),
    )

_Sheet dataclass

_Sheet(path: str, sheet_format: str, include_isometric: bool = True, include_bom: bool = False, material: str | None = None, extra_notes: list[str] = list(), views: dict[str, Any] = dict(), role: dict[str, str] = dict(), ortho: list[str] = list(), iso_name: str | None = None, scale: tuple[float, float] = (1, 1), cells: dict[str, Box] = dict(), iso_scale: tuple[float, float] | None = None, iso_size: tuple[float, float] | None = None, bom_box: Box | None = None, captions: dict[str, str] = dict(), notes: list[tuple[str, Any]] = list(), note_lines: list[str] = list(), title_block: dict[str, dict[str, Any]] = dict(), format_links: list[str] = list(), field_height: float | None = None)

What the generator's steps share while they build one drawing.

Attributes:

Name Type Description
path str

The model being drawn.

sheet_format str

The .slddrt applied to the sheet.

include_isometric bool

Whether to add the isometric view.

include_bom bool

Whether to put a BOM above the isometric.

material str | None

The part's material, for the notes.

extra_notes list[str]

The caller's note lines.

views dict[str, Any]

View name -> IView, orthographic first.

role dict[str, str]

View name -> front, plan, side or iso.

ortho list[str]

The orthographic view names.

iso_name str | None

The isometric view's name, when one was made.

scale tuple[float, float]

The sheet scale.

cells dict[str, Box]

Role -> cell on the sheet.

iso_scale tuple[float, float] | None

The isometric view's own scale.

iso_size tuple[float, float] | None

The isometric's model size, for fitting it to a cell.

bom_box Box | None

The BOM table's box on the sheet, once placed.

captions dict[str, str]

View name -> caption text.

notes list[tuple[str, Any]]

(label, IAnnotation) for every caption and note line.

note_lines list[str]

The NOTES block's text.

title_block dict[str, dict[str, Any]]

Field name -> how it was fitted to its box.

format_links list[str]

The property links the sheet format's notes show.

field_height float | None

The caller's cap on the fitted fields' text height.

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

_double_array

_double_array(values: list[float]) -> Any

VARIANT(VT_ARRAY | VT_R8) for array-valued COM properties (tuple when pywin32 is absent).

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
def _double_array(values: list[float]) -> Any:
    """``VARIANT(VT_ARRAY | VT_R8)`` for array-valued COM properties (tuple when pywin32 is absent)."""
    try:
        import pythoncom
        import win32com.client as w32

        return w32.VARIANT(
            pythoncom.VT_ARRAY | pythoncom.VT_R8, [float(v) for v in values]
        )
    except ImportError:  # non-Windows
        return tuple(values)

_grow

_grow(box: tuple[float, ...], by: float) -> Box

box grown by by on every side.

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
def _grow(box: tuple[float, ...], by: float) -> Box:
    """``box`` grown by ``by`` on every side."""
    return (box[0] - by, box[1] - by, box[2] + by, box[3] + by)

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

_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

_step

_step(steps: list[dict[str, Any]], name: str, result: AdapterResult[Any]) -> None

Record one step's outcome in the generator's steps list.

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
def _step(steps: list[dict[str, Any]], name: str, result: AdapterResult[Any]) -> None:
    """Record one step's outcome in the generator's ``steps`` list."""
    steps.append(
        {
            "step": name,
            "status": "success" if result.is_success else "error",
            "error": result.error,
        }
    )

_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

choose_layout

choose_layout(sizes: dict[str, tuple[float, float]]) -> tuple[tuple[float, float], dict[str, Box]]

Largest standard scale whose cells fit, with those cells.

Parameters:

Name Type Description Default
sizes dict[str, tuple[float, float]]

Model-space view sizes, as for :func:plan_cells.

required

Returns:

Name Type Description
tuple tuple[float, float]

The scale (num, den) and the cells; the smallest standard scale

dict[str, Box]

(with default cells if even that does not fit) as a last resort.

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
def choose_layout(
    sizes: dict[str, tuple[float, float]],
) -> tuple[tuple[float, float], dict[str, Box]]:
    """Largest standard scale whose cells fit, with those cells.

    Args:
        sizes: Model-space view sizes, as for :func:`plan_cells`.

    Returns:
        tuple: The scale (num, den) and the cells; the smallest standard scale
        (with default cells if even that does not fit) as a last resort.
    """
    for scale in STANDARD_SCALES:
        cells = plan_cells(sizes, scale)
        if cells is not None:
            return scale, cells
    last = STANDARD_SCALES[-1]
    return last, plan_cells(sizes, last) or dict(CELLS)

content_box

content_box(cell: Box) -> Box

The part of a view cell left after the caption strip.

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
def content_box(cell: Box) -> Box:
    """The part of a view cell left after the caption strip."""
    return (cell[0], cell[1] + CAPTION_STRIP, cell[2], cell[3])

field_room

field_room(box: Box, others: list[Box], limit: float) -> float

How far right a title-block field may run: to the next note in its row.

A note is in the field's row when its vertical centre lies within the field's height, and it is to the right when it starts more than 5 mm after the field does (the field's own label sits at about its start).

Parameters:

Name Type Description Default
box Box

The field's text box.

required
others list[Box]

Every other note's box in the title block.

required
limit float

The title block's right edge.

required

Returns:

Name Type Description
float float

The x the field's text must end before.

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
def field_room(box: Box, others: list[Box], limit: float) -> float:
    """How far right a title-block field may run: to the next note in its row.

    A note is in the field's row when its vertical centre lies within the
    field's height, and it is to the right when it starts more than 5 mm
    after the field does (the field's own label sits at about its start).

    Args:
        box: The field's text box.
        others: Every other note's box in the title block.
        limit: The title block's right edge.

    Returns:
        float: The x the field's text must end before.
    """
    right = limit
    for o in others:
        centre = (o[1] + o[3]) / 2
        if box[1] <= centre <= box[3] and o[0] > box[0] + 0.005:
            right = min(right, o[0] - FIELD_GAP)
    return right

find_sheet_format

find_sheet_format(adapter: Any, name: str = 'a3 - iso.slddrt') -> str | None

Locate a sheet format file: SolidWorks' configured folder, then ProgramData.

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
def find_sheet_format(adapter: Any, name: str = "a3 - iso.slddrt") -> str | None:
    """Locate a sheet format file: SolidWorks' configured folder, then ProgramData."""
    app = getattr(adapter, "swApp", None)
    folders: list[str] = []
    if app is not None:
        raw = adapter._attempt(
            lambda: app.GetUserPreferenceStringValue(_SW_PREF_SHEET_FORMAT_FOLDER),
            default="",
        )
        folders.extend(f.strip() for f in str(raw or "").split(";") if f.strip())
    folders.extend(
        sorted(
            glob.glob(r"C:\ProgramData\SOLIDWORKS\SOLIDWORKS *\lang\*\sheetformat"),
            reverse=True,
        )
    )
    for folder in folders:
        candidate = os.path.join(folder, name)
        if os.path.isfile(candidate):
            return candidate
    return None

gtol_problem

gtol_problem(characteristic: str, tolerance: float, material_condition: str, datums: list[str] | None) -> str | None

Why a feature control frame's inputs are invalid, or None when valid.

Shared by the real and mock adapters so both refuse the same inputs.

Parameters:

Name Type Description Default
characteristic str

The GD&T characteristic name.

required
tolerance float

The tolerance value.

required
material_condition str

none, mmc or lmc.

required
datums list[str] | None

Datum letters, primary first.

required

Returns:

Type Description
str | None

str | None: The problem, in words.

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
def gtol_problem(
    characteristic: str,
    tolerance: float,
    material_condition: str,
    datums: list[str] | None,
) -> str | None:
    """Why a feature control frame's inputs are invalid, or ``None`` when valid.

    Shared by the real and mock adapters so both refuse the same inputs.

    Args:
        characteristic: The GD&T characteristic name.
        tolerance: The tolerance value.
        material_condition: ``none``, ``mmc`` or ``lmc``.
        datums: Datum letters, primary first.

    Returns:
        str | None: The problem, in words.
    """
    letters = [d.strip() for d in datums or []]
    if characteristic not in _GTOL_SYMBOLS:
        return f"characteristic must be one of {', '.join(_GTOL_SYMBOLS)}"
    if material_condition not in _GTOL_MODIFIERS:
        return f"material_condition must be one of {', '.join(_GTOL_MODIFIERS)}"
    if tolerance <= 0:
        return "tolerance must be positive"
    if len(letters) > 3 or any(len(d) != 1 or not d.isalpha() for d in letters):
        return "at most three datums, each a single letter"
    return None

iso_fit_scale

iso_fit_scale(size: tuple[float, float], cell: Box) -> tuple[float, float]

Largest standard scale at which the isometric view fills its cell.

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
def iso_fit_scale(size: tuple[float, float], cell: Box) -> tuple[float, float]:
    """Largest standard scale at which the isometric view fills its cell."""
    area = content_box(cell)
    for scale in STANDARD_SCALES:
        f = scale[0] / scale[1]
        if (
            size[0] * f + 2 * ISO_MARGIN <= area[2] - area[0]
            and size[1] * f + 2 * ISO_MARGIN <= area[3] - area[1]
        ):
            return scale
    return STANDARD_SCALES[-1]

note_lines

note_lines(material: str | None, extra: list[str] | None) -> list[str]

The NOTES block: a heading, then at most seven numbered lines.

Parameters:

Name Type Description Default
material str | None

The part's material, first when known.

required
extra list[str] | None

The caller's lines, after the standard ones.

required

Returns:

Type Description
list[str]

list[str]: ["NOTES", "1. ...", ...] in capitals.

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
def note_lines(material: str | None, extra: list[str] | None) -> list[str]:
    """The NOTES block: a heading, then at most seven numbered lines.

    Args:
        material: The part's material, first when known.
        extra: The caller's lines, after the standard ones.

    Returns:
        list[str]: ``["NOTES", "1. ...", ...]`` in capitals.
    """
    body = [f"MATERIAL: {material.upper()}"] if material else []
    body += list(STANDARD_NOTES)
    body += [line.upper() for line in (extra or []) if line.strip()]
    numbered = [f"{i}. {line}" for i, line in enumerate(body[:MAX_NOTE_LINES], start=1)]
    return ["NOTES", *numbered]

plan_cells

plan_cells(sizes: dict[str, tuple[float, float]], scale: tuple[float, float]) -> dict[str, Box] | None

Size the four view cells for one part at one scale, or None if it won't fit.

Third-angle grid: front bottom-left, side to its right, plan above the front, isometric top-right. Column widths follow the part's width and depth, row heights its height and depth; spare space is shared out evenly so every view is centred with room around it.

Parameters:

Name Type Description Default
sizes dict[str, tuple[float, float]]

front (width, height), plan (width, depth) and/or side (depth, height), in model metres.

required
scale tuple[float, float]

Sheet scale (num, den).

required
Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
def plan_cells(
    sizes: dict[str, tuple[float, float]], scale: tuple[float, float]
) -> dict[str, Box] | None:
    """Size the four view cells for one part at one scale, or None if it won't fit.

    Third-angle grid: front bottom-left, side to its right, plan above the
    front, isometric top-right. Column widths follow the part's width and
    depth, row heights its height and depth; spare space is shared out evenly
    so every view is centred with room around it.

    Args:
        sizes: ``front`` (width, height), ``plan`` (width, depth) and/or
            ``side`` (depth, height), in model metres.
        scale: Sheet scale (num, den).
    """
    f = scale[0] / scale[1]
    front = sizes.get("front")
    if front is None:
        return None
    width, height = front
    depth = sizes["side"][0] if "side" in sizes else sizes.get("plan", (0.0, 0.0))[1]
    left = width * f + 2 * FIT_H_MARGIN
    right = depth * f + 2 * FIT_H_MARGIN
    low = height * f + 2 * FIT_V_MARGIN + CAPTION_STRIP
    high = depth * f + 2 * FIT_V_MARGIN + CAPTION_STRIP
    zx0, zy0, zx1, zy1 = VIEW_ZONE
    if left + right > zx1 - zx0 or low + high > zy1 - zy0:
        return None
    xm = zx0 + left + (zx1 - zx0 - left - right) / 2
    ym = zy0 + low + (zy1 - zy0 - low - high) / 2
    return {
        "front": (zx0, zy0, xm, ym),
        "side": (xm, zy0, zx1, ym),
        "plan": (zx0, ym, xm, zy1),
        "iso": (xm, ym, zx1, zy1),
        "notes": CELLS["notes"],
    }

scale_label

scale_label(scale: tuple[float, float]) -> str

(1, 2) -> "1:2", (2.5, 1) -> "2.5:1".

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
def scale_label(scale: tuple[float, float]) -> str:
    """``(1, 2)`` -> ``"1:2"``, ``(2.5, 1)`` -> ``"2.5:1"``."""
    return f"{scale[0]:g}:{scale[1]:g}"

title_block_properties

title_block_properties(*, model_file: str, title: str, author: str, drawn_date: str, revision: str, company: str, subtitle: str | None = None) -> tuple[dict[str, str], dict[str, str]]

Map the standard's title-block fields onto the ISO format's properties.

The a3 - iso format reads TITLE, MATERIAL and WEIGHT from the model ($PRPSHEET) and DRAWN, REVISION and the company from the drawing ($PRP). Material and weight are links, so they always show the model's real values.

Returns:

Name Type Description
tuple tuple[dict[str, str], dict[str, str]]

(model properties, drawing properties).

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
def title_block_properties(
    *,
    model_file: str,
    title: str,
    author: str,
    drawn_date: str,
    revision: str,
    company: str,
    subtitle: str | None = None,
) -> tuple[dict[str, str], dict[str, str]]:
    """Map the standard's title-block fields onto the ISO format's properties.

    The ``a3 - iso`` format reads TITLE, MATERIAL and WEIGHT from the model
    (``$PRPSHEET``) and DRAWN, REVISION and the company from the drawing
    (``$PRP``). Material and weight are links, so they always show the model's
    real values.

    Returns:
        tuple: (model properties, drawing properties).
    """
    # SolidWorks paths are Windows paths, whatever platform this runs on.
    base = ntpath.basename(model_file)
    description = title  # keep TITLE short so it fits the box (standard section 1)
    model_props = {
        "Description": description,
        "Material": f'"SW-Material@{base}"',
        "Weight": f'"SW-Mass@{base}"',
    }
    drawing_props = {
        "DrawnBy": author,
        "DrawnDate": drawn_date,
        "Revision": revision,
        "COMPANYNAME": company or (subtitle or ""),
    }
    return model_props, {k: v for k, v in drawing_props.items() if v}

view_roles

view_roles(views: dict[str, Any], outlines: dict[str, Box | None], orientations: dict[str, str]) -> dict[str, str]

Name each orthographic view's role: front, plan or side.

The front is the view whose orientation says "front" (else the first); a view centred above the front is the plan, any other is the side.

Parameters:

Name Type Description Default
views dict[str, Any]

View name -> IView, in creation order.

required
outlines dict[str, Box | None]

View name -> outline, None when unreadable.

required
orientations dict[str, str]

View name -> orientation name, e.g. *Front.

required

Returns:

Type Description
dict[str, str]

dict[str, str]: View name -> role; a view whose outline is unknown

dict[str, str]

gets no role.

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
def view_roles(
    views: dict[str, Any], outlines: dict[str, Box | None], orientations: dict[str, str]
) -> dict[str, str]:
    """Name each orthographic view's role: ``front``, ``plan`` or ``side``.

    The front is the view whose orientation says "front" (else the first);
    a view centred above the front is the plan, any other is the side.

    Args:
        views: View name -> ``IView``, in creation order.
        outlines: View name -> outline, ``None`` when unreadable.
        orientations: View name -> orientation name, e.g. ``*Front``.

    Returns:
        dict[str, str]: View name -> role; a view whose outline is unknown
        gets no role.
    """
    names = list(views)
    front = next(
        (n for n in names if "front" in orientations.get(n, "").lower()), names[0]
    )
    role = {front: "front"}
    fo = outlines.get(front)
    for name in names:
        o = outlines.get(name)
        if name == front or o is None or fo is None:
            continue
        above = (o[1] + o[3]) / 2 > (fo[1] + fo[3]) / 2 + 1e-4
        role[name] = "plan" if above else "side"
    return role