Skip to content

solidworks_mcp.adapters.solidworks.dimension_layout

solidworks_mcp.adapters.solidworks.dimension_layout

Overlap-free placement of drawing dimensions (pure geometry, no COM).

Rule enforced: nothing on the sheet may overlap. A dimension's text box must not intersect another dimension's text, be crossed by another dimension's lines, sit on any view's geometry, touch an obstacle (title block, view captions, note blocks) or leave the working area of the frame. A dimension's lines must not cross another view, and its extension lines must not run across its own view to the far side.

InsertModelAnnotations3 places imported dimensions where they sit in the model sketch, which on a drawing view lands them inside the view outline, on top of the part and of each other (measured live on SW 2025).

:func:plan_layout moves each dimension outside its view, following the A3 drafting standard (drawing_standard.py): horizontal dimensions in rows above/below the part, rows 7 mm apart; vertical dimensions in columns left/right, 9 mm apart; the first row 8 mm from the outline, on the side its extension lines start from. Others (diameters, radii, angles) keep their leader on the feature and step their text outward from it. Rows are filled shortest span first, so the extension lines of longer outer dimensions clear the inner text. Every candidate position is checked against everything already placed; the first free one wins.

All coordinates are sheet metres; boxes and segments are (x0, y0, x1, y1).

Attributes

Box module-attribute

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

COLUMN_PITCH module-attribute

COLUMN_PITCH = 0.009

EDGE_TOLERANCE module-attribute

EDGE_TOLERANCE = 0.0002

FEATURE_CLEARANCE module-attribute

FEATURE_CLEARANCE = 0.006

FIRST_OFFSET module-attribute

FIRST_OFFSET = 0.008

INSIDE_MARGIN module-attribute

INSIDE_MARGIN = 0.008

LEADER_CLEARANCE module-attribute

LEADER_CLEARANCE = 0.002

LEADER_STEP module-attribute

LEADER_STEP = 0.004

MAX_STEPS module-attribute

MAX_STEPS = 12

MIN_CROSSING module-attribute

MIN_CROSSING = 0.001

ROW_PITCH module-attribute

ROW_PITCH = 0.007

STACK_GAP module-attribute

STACK_GAP = 0.0035

Segment module-attribute

Segment = tuple[float, float, float, float]

TEXT_PADDING module-attribute

TEXT_PADDING = 0.00075

Classes

DimensionGeometry dataclass

DimensionGeometry(key: str, view: str, text_box: Box, position: tuple[float, float], lines: list[Segment] = list(), orientation: str | None = None, span: float = 0.0)

One drawing dimension as read from IAnnotation::GetDisplayData.

Attributes:

Name Type Description
key str

Stable identifier (view name + index).

view str

Owning view name.

text_box Box

Current text box on the sheet.

position tuple[float, float]

IAnnotation::GetPosition (x, y) - what SetPosition moves.

lines list[Segment]

Every line segment of the dimension (extension + dimension lines).

orientation str | None

"h", "v" or None; set by :func:plan_layout.

span float

Distance between the extension lines (0 when unknown).

Functions:

_candidates

_candidates(dim: DimensionGeometry, outline: Box) -> list[tuple[str, float, float]]

Ordered (side, dx, dy) moves to try for one dimension.

A linear dimension only goes to the side its extension lines start from; on the far side they would cross its own view. Rows and columns are tried from the innermost out, so dimensions that do not touch share a row; the caller's checks push one outward when its text or lines would collide.

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def _candidates(dim: DimensionGeometry, outline: Box) -> list[tuple[str, float, float]]:
    """Ordered (side, dx, dy) moves to try for one dimension.

    A linear dimension only goes to the side its extension lines start from;
    on the far side they would cross its own view. Rows and columns are tried
    from the innermost out, so dimensions that do not touch share a row; the
    caller's checks push one outward when its text or lines would collide.
    """
    tb = dim.text_box
    w, h = tb[2] - tb[0], tb[3] - tb[1]
    cx, cy = (tb[0] + tb[2]) / 2.0, (tb[1] + tb[3]) / 2.0
    ox, oy = (outline[0] + outline[2]) / 2.0, (outline[1] + outline[3]) / 2.0
    origins = extension_origins(dim)
    out: list[tuple[str, float, float]] = []
    if dim.orientation == "h":
        sides = list(dict.fromkeys("above" if o >= oy else "below" for o in origins))
        pitch = max(ROW_PITCH, h + STACK_GAP)
        for side in sides:
            for step in range(MAX_STEPS):
                off = FIRST_OFFSET + step * pitch
                target = outline[3] + off if side == "above" else outline[1] - off
                out.append((side, 0.0, target - cy))
    elif dim.orientation == "v":
        sides = list(dict.fromkeys("right" if o >= ox else "left" for o in origins))
        pitch = max(COLUMN_PITCH, w + STACK_GAP)
        for side in sides:
            for step in range(MAX_STEPS):
                off = FIRST_OFFSET + step * pitch
                target = outline[2] + off if side == "right" else outline[0] - off
                out.append((side, target - cx, 0.0))
    else:
        # A leader: step the text outward from the feature it points at,
        # along each of eight directions, away from the view's centre first.
        ax, ay = leader_anchor(dim) or (cx, cy)
        root = 0.5**0.5
        directions = [
            (1.0, 0.0), (-1.0, 0.0), (0.0, 1.0), (0.0, -1.0),
            (root, root), (-root, root), (root, -root), (-root, -root),
        ]  # fmt: skip
        directions.sort(key=lambda d: -((ax - ox) * d[0] + (ay - oy) * d[1]))
        for ux, uy in directions:
            for step in range(2 * MAX_STEPS):
                reach = FIRST_OFFSET / 2.0 + step * LEADER_STEP
                tx = ax + ux * reach + math.copysign(w / 2.0, ux or 1.0) * abs(ux)
                ty = ay + uy * reach + math.copysign(h / 2.0, uy or 1.0) * abs(uy)
                out.append((f"leader{ux:+.1f}{uy:+.1f}", tx - cx, ty - cy))
    return out

_grown

_grown(box: Box, by: float) -> Box

box grown by by on every side.

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

_inside_candidates

_inside_candidates(dim: DimensionGeometry, outline: Box) -> list[tuple[str, float, float]]

Moves putting a leader's text inside its view, toward the view's centre.

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def _inside_candidates(
    dim: DimensionGeometry, outline: Box
) -> list[tuple[str, float, float]]:
    """Moves putting a leader's text inside its view, toward the view's centre."""
    anchor = leader_anchor(dim)
    tb = dim.text_box
    w, h = tb[2] - tb[0], tb[3] - tb[1]
    cx, cy = (tb[0] + tb[2]) / 2.0, (tb[1] + tb[3]) / 2.0
    ox, oy = (outline[0] + outline[2]) / 2.0, (outline[1] + outline[3]) / 2.0
    ux, uy = ox - anchor[0], oy - anchor[1]
    length = math.hypot(ux, uy) or 1.0
    ux, uy = ux / length, uy / length
    out = []
    for step in range(2 * MAX_STEPS):
        reach = FIRST_OFFSET + step * LEADER_STEP
        tx = anchor[0] + ux * reach + math.copysign(w / 2.0, ux)
        ty = anchor[1] + uy * reach + math.copysign(h / 2.0, uy)
        out.append(("inside", tx - cx, ty - cy))
    return out

_leader_inside

_leader_inside(dim: DimensionGeometry, dims: list[DimensionGeometry], outline: Box) -> bool

A leader whose text sits clear inside its own view (see :func:clear_inside).

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def _leader_inside(
    dim: DimensionGeometry, dims: list[DimensionGeometry], outline: Box
) -> bool:
    """A leader whose text sits clear inside its own view (see :func:`clear_inside`)."""
    anchor = leader_anchor(dim)
    if classify(dim.lines)[0] is not None or anchor is None:
        return False
    points = [*feature_points(dims, dim.view, outline, skip=dim.key), anchor]
    return clear_inside(dim.text_box, outline, points)

_leader_to

_leader_to(anchor: tuple[float, float], box: Box) -> Segment

The leader from anchor to the nearest point of the text box.

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def _leader_to(anchor: tuple[float, float], box: Box) -> Segment:
    """The leader from ``anchor`` to the nearest point of the text ``box``."""
    x = min(max(anchor[0], box[0]), box[2])
    y = min(max(anchor[1], box[1]), box[3])
    return (anchor[0], anchor[1], x, y)

_projected_segments

_projected_segments(dim: DimensionGeometry, dx: float, dy: float, outline: Box) -> list[Segment]

Approximate a moved dimension's new dimension and extension lines.

The extension lines still start where they did on the part and now reach the moved dimension line. A leader stays fixed to the feature it points at and runs from there to the moved text; moving it with the text, as before, hid a diameter leader drawn across the part and two neighbouring dimensions (measured live on SW 2026).

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def _projected_segments(
    dim: DimensionGeometry, dx: float, dy: float, outline: Box
) -> list[Segment]:
    """Approximate a moved dimension's new dimension and extension lines.

    The extension lines still start where they did on the part and now reach
    the moved dimension line. A leader stays fixed to the feature it points
    at and runs from there to the moved text; moving it with the text, as
    before, hid a diameter leader drawn across the part and two neighbouring
    dimensions (measured live on SW 2026).
    """
    orient, _span, ends = classify(dim.lines)
    box = shift_box(dim.text_box, dx, dy)
    origins = extension_origins(dim)
    if orient == "h":
        y = (box[1] + box[3]) / 2.0
        return [(ends[0], y, ends[1], y)] + [
            (s[0], o, s[0], y) for s, o in zip(dim.lines, origins, strict=False)
        ]
    if orient == "v":
        x = (box[0] + box[2]) / 2.0
        return [(x, ends[0], x, ends[1])] + [
            (o, s[1], x, s[1]) for s, o in zip(dim.lines, origins, strict=False)
        ]
    anchor = leader_anchor(dim)
    return [] if anchor is None else [_leader_to(anchor, box)]

attach_boxes

attach_boxes(dims: list[DimensionGeometry], attached: dict[str, Box]) -> None

Grow each dimension's text box by what hangs off it, such as a GD&T frame.

A frame attached to a dimension moves with it, so its box is given relative to the dimension's position and counts as part of its text.

Parameters:

Name Type Description Default
dims list[DimensionGeometry]

Dimensions as read; updated in place.

required
attached dict[str, Box]

Dimension key -> box relative to that dimension's position.

required
Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def attach_boxes(dims: list[DimensionGeometry], attached: dict[str, Box]) -> None:
    """Grow each dimension's text box by what hangs off it, such as a GD&T frame.

    A frame attached to a dimension moves with it, so its box is given
    relative to the dimension's position and counts as part of its text.

    Args:
        dims: Dimensions as read; updated in place.
        attached: Dimension key -> box relative to that dimension's position.
    """
    for dim in dims:
        rel = attached.get(dim.key)
        if rel is None:
            continue
        x, y = dim.position
        tb = dim.text_box
        dim.text_box = (
            min(tb[0], x + rel[0]),
            min(tb[1], y + rel[1]),
            max(tb[2], x + rel[2]),
            max(tb[3], y + rel[3]),
        )

box_overlap

box_overlap(a: Box, b: Box, pad: float = 0.0) -> bool

Return True when two boxes intersect (after growing each by pad).

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def box_overlap(a: Box, b: Box, pad: float = 0.0) -> bool:
    """Return True when two boxes intersect (after growing each by ``pad``)."""
    return not (
        a[2] + pad <= b[0] - pad
        or b[2] + pad <= a[0] - pad
        or a[3] + pad <= b[1] - pad
        or b[3] + pad <= a[1] - pad
    )

classify

classify(lines: list[Segment]) -> tuple[str | None, float, list[float]]

Work out orientation, span and extension-line coordinates.

SolidWorks returns a linear dimension's two extension lines first. Both vertical means a horizontal dimension (their x values are the span ends); both horizontal means a vertical one.

Returns:

Name Type Description
tuple tuple[str | None, float, list[float]]

orientation, span, and the two extension-line coordinates.

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def classify(lines: list[Segment]) -> tuple[str | None, float, list[float]]:
    """Work out orientation, span and extension-line coordinates.

    SolidWorks returns a linear dimension's two extension lines first. Both
    vertical means a horizontal dimension (their x values are the span ends);
    both horizontal means a vertical one.

    Returns:
        tuple: orientation, span, and the two extension-line coordinates.
    """
    if len(lines) >= 2:
        a, b = lines[0], lines[1]
        tol = 1e-6
        if abs(a[0] - a[2]) < tol and abs(b[0] - b[2]) < tol and abs(a[0] - b[0]) > tol:
            return "h", abs(a[0] - b[0]), sorted([a[0], b[0]])
        if abs(a[1] - a[3]) < tol and abs(b[1] - b[3]) < tol and abs(a[1] - b[1]) > tol:
            return "v", abs(a[1] - b[1]), sorted([a[1], b[1]])
    return None, 0.0, []

clear_inside

clear_inside(box: Box, outline: Box, points: list[tuple[float, float]]) -> bool

True when box sits well inside outline and clear of points.

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def clear_inside(box: Box, outline: Box, points: list[tuple[float, float]]) -> bool:
    """True when ``box`` sits well inside ``outline`` and clear of ``points``."""
    room = _grown(outline, -INSIDE_MARGIN)
    if not (
        room[0] <= box[0]
        and room[1] <= box[1]
        and box[2] <= room[2]
        and box[3] <= room[3]
    ):
        return False
    near = _grown(box, FEATURE_CLEARANCE)
    return not any(
        near[0] <= x <= near[2] and near[1] <= y <= near[3] for x, y in points
    )

crosses_other_view

crosses_other_view(lines: list[Segment], own_view: str, outlines: dict[str, Box]) -> str | None

Return the first other view any of lines runs across or along.

A dimension's extension lines start on its own view's geometry, so its own outline is exempt. On any other view, more than :data:MIN_CROSSING of a line inside that outline (grown by :data:EDGE_TOLERANCE, so a line lying along its edge counts) is a crossing; a line ending at the edge is not. Measured live on SW 2026: a front view's height dimension parked beyond the side view drew its extension lines along the side view's top and bottom edges, straight across it.

Parameters:

Name Type Description Default
lines list[Segment]

The dimension's line segments.

required
own_view str

The view the dimension belongs to.

required
outlines dict[str, Box]

View name -> view outline.

required

Returns:

Type Description
str | None

str | None: The crossed view's name, or None when clear.

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def crosses_other_view(
    lines: list[Segment], own_view: str, outlines: dict[str, Box]
) -> str | None:
    """Return the first other view any of ``lines`` runs across or along.

    A dimension's extension lines start on its own view's geometry, so its own
    outline is exempt. On any other view, more than :data:`MIN_CROSSING` of a
    line inside that outline (grown by :data:`EDGE_TOLERANCE`, so a line lying
    along its edge counts) is a crossing; a line ending at the edge is not.
    Measured live on SW 2026: a front view's height dimension parked beyond
    the side view drew its extension lines along the side view's top and
    bottom edges, straight across it.

    Args:
        lines: The dimension's line segments.
        own_view: The view the dimension belongs to.
        outlines: View name -> view outline.

    Returns:
        str | None: The crossed view's name, or ``None`` when clear.
    """
    t = EDGE_TOLERANCE
    for view, o in outlines.items():
        if view == own_view:
            continue
        grown = (o[0] - t, o[1] - t, o[2] + t, o[3] + t)
        if any(length_inside(s, grown) > MIN_CROSSING for s in lines):
            return view
    return None

crosses_own_view

crosses_own_view(dim: DimensionGeometry, outline: Box) -> bool

True when an extension line runs across its own view to the far side.

Measured live on SW 2026: a height dimension moved from the left of the top view to its right drew both extension lines straight across the view.

Parameters:

Name Type Description Default
dim DimensionGeometry

The dimension.

required
outline Box

Its view's outline.

required

Returns:

Name Type Description
bool bool

False for leaders, which have no extension lines.

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def crosses_own_view(dim: DimensionGeometry, outline: Box) -> bool:
    """True when an extension line runs across its own view to the far side.

    Measured live on SW 2026: a height dimension moved from the left of the
    top view to its right drew both extension lines straight across the view.

    Args:
        dim: The dimension.
        outline: Its view's outline.

    Returns:
        bool: ``False`` for leaders, which have no extension lines.
    """
    axis = 1 if classify(dim.lines)[0] == "h" else 0
    low, high = outline[axis], outline[axis + 2]
    middle = (low + high) / 2.0
    for start, line in zip(extension_origins(dim), dim.lines, strict=False):
        end = line[axis + 2] if line[axis] == start else line[axis]
        if (start < middle and end > high) or (start > middle and end < low):
            return True
    return False

extension_origins

extension_origins(dim: DimensionGeometry) -> list[float]

Where each extension line starts on the part, across the dimension.

An extension line runs from the part to just past the dimension line, so its start is the end farther from the text: a y for a horizontal dimension, an x for a vertical one.

Parameters:

Name Type Description Default
dim DimensionGeometry

The dimension.

required

Returns:

Type Description
list[float]

list[float]: One coordinate per extension line; empty for a leader.

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def extension_origins(dim: DimensionGeometry) -> list[float]:
    """Where each extension line starts on the part, across the dimension.

    An extension line runs from the part to just past the dimension line, so
    its start is the end farther from the text: a y for a horizontal
    dimension, an x for a vertical one.

    Args:
        dim: The dimension.

    Returns:
        list[float]: One coordinate per extension line; empty for a leader.
    """
    orient = classify(dim.lines)[0]
    if orient is None:
        return []
    axis = 1 if orient == "h" else 0
    centre = (dim.text_box[axis] + dim.text_box[axis + 2]) / 2.0
    return [
        max((s[axis], s[axis + 2]), key=lambda c: abs(c - centre))
        for s in dim.lines[:2]
    ]

feature_points

feature_points(dims: list[DimensionGeometry], view: str, outline: Box, skip: str | None = None) -> list[tuple[float, float]]

Points inside a view where its dimensions start: its features.

Every line end of the view's dimensions (but skip's) that lies inside the outline: extension lines start on holes and edges, leaders on the feature they point at.

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def feature_points(
    dims: list[DimensionGeometry], view: str, outline: Box, skip: str | None = None
) -> list[tuple[float, float]]:
    """Points inside a view where its dimensions start: its features.

    Every line end of the view's dimensions (but ``skip``'s) that lies inside
    the outline: extension lines start on holes and edges, leaders on the
    feature they point at.
    """
    inner = _grown(outline, -0.0005)
    return [
        p
        for d in dims
        if d.view == view and d.key != skip
        for s in d.lines
        for p in ((s[0], s[1]), (s[2], s[3]))
        if inner[0] < p[0] < inner[2] and inner[1] < p[1] < inner[3]
    ]

find_annotation_overlaps

find_annotation_overlaps(notes: dict[str, Box], dims: list[DimensionGeometry], outlines: dict[str, Box], sheet: Box | None = None, obstacles: dict[str, Box] | None = None) -> list[dict[str, str]]

Check sheet notes and captions against everything else on the sheet.

Parameters:

Name Type Description Default
notes dict[str, Box]

Label -> measured text box of each note or caption.

required
dims list[DimensionGeometry]

Dimensions with their real geometry.

required
outlines dict[str, Box]

View outlines.

required
sheet Box | None

Working area of the frame.

None
obstacles dict[str, Box] | None

Named boxes notes may not touch (title block).

None

Returns:

Type Description
list[dict[str, str]]

list[dict[str, str]]: {"dimension", "conflicts_with", "kind"} entries

list[dict[str, str]]

(dimension holds the note label, for one report format).

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def find_annotation_overlaps(
    notes: dict[str, Box],
    dims: list[DimensionGeometry],
    outlines: dict[str, Box],
    sheet: Box | None = None,
    obstacles: dict[str, Box] | None = None,
) -> list[dict[str, str]]:
    """Check sheet notes and captions against everything else on the sheet.

    Args:
        notes: Label -> measured text box of each note or caption.
        dims: Dimensions with their real geometry.
        outlines: View outlines.
        sheet: Working area of the frame.
        obstacles: Named boxes notes may not touch (title block).

    Returns:
        list[dict[str, str]]: ``{"dimension", "conflicts_with", "kind"}`` entries
        (``dimension`` holds the note label, for one report format).
    """
    problems: list[dict[str, str]] = []
    labels = list(notes)
    for i, label in enumerate(labels):
        box = notes[label]
        for other in labels[i + 1 :]:
            if box_overlap(box, notes[other], TEXT_PADDING / 2):
                problems.append(
                    {
                        "dimension": label,
                        "conflicts_with": other,
                        "kind": "note_on_note",
                    }
                )
        for view, outline in outlines.items():
            if box_overlap(box, outline, 0.0):
                problems.append(
                    {"dimension": label, "conflicts_with": view, "kind": "note_on_view"}
                )
        for name, obstacle in (obstacles or {}).items():
            if box_overlap(box, obstacle, 0.0):
                problems.append(
                    {
                        "dimension": label,
                        "conflicts_with": name,
                        "kind": "note_on_obstacle",
                    }
                )
        if sheet is not None and not inside(box, sheet):
            problems.append(
                {
                    "dimension": label,
                    "conflicts_with": "frame",
                    "kind": "note_outside_working_area",
                }
            )
        for dim in dims:
            if box_overlap(box, dim.text_box, TEXT_PADDING / 2) or any(
                segment_hits_box(s, box) for s in dim.lines
            ):
                problems.append(
                    {
                        "dimension": label,
                        "conflicts_with": dim.key,
                        "kind": "note_on_dimension",
                    }
                )
    return problems

find_overlaps

find_overlaps(dims: list[DimensionGeometry], outlines: dict[str, Box], sheet: Box | None = None, obstacles: dict[str, Box] | None = None) -> list[dict[str, str]]

List every rule violation among dimensions as they currently sit.

Parameters:

Name Type Description Default
dims list[DimensionGeometry]

Dimensions with their real (read-back) geometry.

required
outlines dict[str, Box]

View name -> view outline box.

required
sheet Box | None

Working area of the frame, when known.

None
obstacles dict[str, Box] | None

Named boxes nothing may touch (title block, captions, notes).

None

Returns:

Type Description
list[dict[str, str]]

list[dict[str, str]]: {"dimension", "conflicts_with", "kind"} entries.

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def find_overlaps(
    dims: list[DimensionGeometry],
    outlines: dict[str, Box],
    sheet: Box | None = None,
    obstacles: dict[str, Box] | None = None,
) -> list[dict[str, str]]:
    """List every rule violation among dimensions as they currently sit.

    Args:
        dims: Dimensions with their real (read-back) geometry.
        outlines: View name -> view outline box.
        sheet: Working area of the frame, when known.
        obstacles: Named boxes nothing may touch (title block, captions, notes).

    Returns:
        list[dict[str, str]]: ``{"dimension", "conflicts_with", "kind"}`` entries.
    """
    problems: list[dict[str, str]] = []
    for i, dim in enumerate(dims):
        for view, outline in outlines.items():
            if view == dim.view and _leader_inside(dim, dims, outline):
                continue
            if box_overlap(dim.text_box, outline, -TEXT_PADDING / 2):
                problems.append(
                    {
                        "dimension": dim.key,
                        "conflicts_with": view,
                        "kind": "on_view_geometry",
                    }
                )
        crossed = crosses_other_view(dim.lines, dim.view, outlines)
        if crossed is not None:
            problems.append(
                {
                    "dimension": dim.key,
                    "conflicts_with": crossed,
                    "kind": "line_through_view",
                }
            )
        own = outlines.get(dim.view)
        if own is not None and crosses_own_view(dim, own):
            problems.append(
                {
                    "dimension": dim.key,
                    "conflicts_with": dim.view,
                    "kind": "line_through_own_view",
                }
            )
        for label, obstacle in (obstacles or {}).items():
            if box_overlap(dim.text_box, obstacle, TEXT_PADDING / 2) or any(
                segment_hits_box(s, obstacle) for s in dim.lines
            ):
                problems.append(
                    {
                        "dimension": dim.key,
                        "conflicts_with": label,
                        "kind": "on_obstacle",
                    }
                )
        if sheet is not None and not inside(dim.text_box, sheet):
            problems.append(
                {
                    "dimension": dim.key,
                    "conflicts_with": "frame",
                    "kind": "outside_working_area",
                }
            )
        for j, other in enumerate(dims):
            if j == i:
                continue
            if j > i and box_overlap(dim.text_box, other.text_box, TEXT_PADDING):
                problems.append(
                    {
                        "dimension": dim.key,
                        "conflicts_with": other.key,
                        "kind": "text_on_text",
                    }
                )
            if any(segment_hits_box(s, dim.text_box) for s in other.lines):
                problems.append(
                    {
                        "dimension": dim.key,
                        "conflicts_with": other.key,
                        "kind": "line_through_text",
                    }
                )
    return problems

inside

inside(box: Box, area: Box) -> bool

Return True when box lies entirely within area.

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def inside(box: Box, area: Box) -> bool:
    """Return True when ``box`` lies entirely within ``area``."""
    return (
        area[0] <= box[0]
        and box[2] <= area[2]
        and area[1] <= box[1]
        and box[3] <= area[3]
    )

leader_anchor

leader_anchor(dim: DimensionGeometry) -> tuple[float, float] | None

Where a leader (diameter, radius) meets the part: the line end farthest from the text.

Parameters:

Name Type Description Default
dim DimensionGeometry

The dimension.

required

Returns:

Type Description
tuple[float, float] | None

tuple[float, float] | None: The anchor, or None with no lines.

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def leader_anchor(dim: DimensionGeometry) -> tuple[float, float] | None:
    """Where a leader (diameter, radius) meets the part: the line end farthest from the text.

    Args:
        dim: The dimension.

    Returns:
        tuple[float, float] | None: The anchor, or ``None`` with no lines.
    """
    cx = (dim.text_box[0] + dim.text_box[2]) / 2.0
    cy = (dim.text_box[1] + dim.text_box[3]) / 2.0
    ends = [(s[0], s[1]) for s in dim.lines] + [(s[2], s[3]) for s in dim.lines]
    if not ends:
        return None
    return max(ends, key=lambda p: (p[0] - cx) ** 2 + (p[1] - cy) ** 2)

length_inside

length_inside(seg: Segment, box: Box) -> float

Length of the part of a segment that lies inside a box (Liang-Barsky).

Parameters:

Name Type Description Default
seg Segment

The segment.

required
box Box

The box.

required

Returns:

Name Type Description
float float

0.0 when the segment misses the box.

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def length_inside(seg: Segment, box: Box) -> float:
    """Length of the part of a segment that lies inside a box (Liang-Barsky).

    Args:
        seg: The segment.
        box: The box.

    Returns:
        float: 0.0 when the segment misses the box.
    """
    x0, y0, x1, y1 = seg
    dx, dy = x1 - x0, y1 - y0
    t0, t1 = 0.0, 1.0
    for p, q in (
        (-dx, x0 - box[0]),
        (dx, box[2] - x0),
        (-dy, y0 - box[1]),
        (dy, box[3] - y0),
    ):
        if abs(p) < 1e-12:
            if q < 0:
                return 0.0
            continue
        t = q / p
        if p < 0:
            t0 = max(t0, t)
        else:
            t1 = min(t1, t)
        if t0 > t1:
            return 0.0
    return (t1 - t0) * (dx * dx + dy * dy) ** 0.5

plan_layout

plan_layout(dims: list[DimensionGeometry], outlines: dict[str, Box], sheet: Box | None = None, obstacles: dict[str, Box] | None = None, fixed: set[str] | None = None, inside_first: set[str] | None = None) -> tuple[dict[str, tuple[float, float]], list[str]]

Choose a move for every dimension so the overlap rule holds.

Parameters:

Name Type Description Default
dims list[DimensionGeometry]

Dimensions with their current geometry.

required
outlines dict[str, Box]

View name -> view outline.

required
sheet Box | None

Working area of the frame, when known.

None
obstacles dict[str, Box] | None

Named boxes nothing may touch.

None
fixed set[str] | None

Keys of dimensions that stay where they are: their real text and lines are obstacles. Used to re-plan only what a first pass left in conflict.

None
inside_first set[str] | None

Leader dimensions to try inside their own view first, near the feature (a hole note with its GD&T frame), before outside it; see :func:clear_inside.

None

Returns:

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

{key: (dx, dy)} moves, and keys that found no free spot

list[str]

(left in place and reported by the caller's final check).

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def plan_layout(
    dims: list[DimensionGeometry],
    outlines: dict[str, Box],
    sheet: Box | None = None,
    obstacles: dict[str, Box] | None = None,
    fixed: set[str] | None = None,
    inside_first: set[str] | None = None,
) -> tuple[dict[str, tuple[float, float]], list[str]]:
    """Choose a move for every dimension so the overlap rule holds.

    Args:
        dims: Dimensions with their current geometry.
        outlines: View name -> view outline.
        sheet: Working area of the frame, when known.
        obstacles: Named boxes nothing may touch.
        fixed: Keys of dimensions that stay where they are: their real text
            and lines are obstacles. Used to re-plan only what a first pass
            left in conflict.
        inside_first: Leader dimensions to try inside their own view first,
            near the feature (a hole note with its GD&T frame), before
            outside it; see :func:`clear_inside`.

    Returns:
        tuple: ``{key: (dx, dy)}`` moves, and keys that found no free spot
        (left in place and reported by the caller's final check).
    """
    blocked = list((obstacles or {}).values())
    for dim in dims:
        dim.orientation, dim.span, _ = classify(dim.lines)
    keep = fixed or set()
    placed_boxes: list[Box] = []
    placed_lines: list[Segment] = []
    for dim in dims:
        if dim.key in keep:
            placed_boxes.append(dim.text_box)
            placed_lines.extend(dim.lines)
    order = sorted(
        (d for d in dims if d.key not in keep),
        key=lambda d: (d.view, d.orientation or "z", d.span),
    )
    moves: dict[str, tuple[float, float]] = {}
    unplaced: list[str] = []
    for dim in order:
        outline = outlines.get(dim.view)
        if outline is None:
            unplaced.append(dim.key)
            continue
        chosen = None
        candidates = _candidates(dim, outline)
        if dim.key in (inside_first or set()) and dim.orientation is None:
            candidates = _inside_candidates(dim, outline) + candidates
            features = [
                *feature_points(dims, dim.view, outline, skip=dim.key),
                leader_anchor(dim),
            ]
        for side, dx, dy in candidates:
            box = shift_box(dim.text_box, dx, dy)
            others = [
                o for v, o in outlines.items() if side != "inside" or v != dim.view
            ]
            if any(box_overlap(box, o, TEXT_PADDING) for o in others):
                continue
            if side == "inside" and not clear_inside(box, outline, features):
                continue
            if any(box_overlap(box, o, TEXT_PADDING) for o in blocked):
                continue
            if sheet is not None and not inside(box, sheet):
                continue
            if any(box_overlap(box, p, TEXT_PADDING) for p in placed_boxes):
                continue
            if any(segment_hits_box(s, box) for s in placed_lines):
                continue
            segs = _projected_segments(dim, dx, dy, outline)
            if crosses_other_view(segs, dim.view, outlines) is not None:
                continue
            clear = LEADER_CLEARANCE if dim.orientation is None else 0.0
            if any(
                segment_hits_box(s, _grown(p, clear))
                for s in segs
                for p in placed_boxes + blocked
            ):
                continue
            chosen = (side, dx, dy, box, segs)
            break
        if chosen is None:
            unplaced.append(dim.key)
            placed_boxes.append(dim.text_box)
            placed_lines.extend(dim.lines)
            continue
        _side, dx, dy, box, segs = chosen
        moves[dim.key] = (dx, dy)
        placed_boxes.append(box)
        placed_lines.extend(segs)
    return moves, unplaced

segment_hits_box

segment_hits_box(seg: Segment, box: Box) -> bool

Liang-Barsky clip: True when the segment passes through the box.

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def segment_hits_box(seg: Segment, box: Box) -> bool:
    """Liang-Barsky clip: True when the segment passes through the box."""
    x0, y0, x1, y1 = seg
    dx, dy = x1 - x0, y1 - y0
    t0, t1 = 0.0, 1.0
    for p, q in (
        (-dx, x0 - box[0]),
        (dx, box[2] - x0),
        (-dy, y0 - box[1]),
        (dy, box[3] - y0),
    ):
        if abs(p) < 1e-12:
            if q < 0:
                return False
            continue
        t = q / p
        if p < 0:
            t0 = max(t0, t)
        else:
            t1 = min(t1, t)
        if t0 > t1:
            return False
    return True

shift_box

shift_box(box: Box, dx: float, dy: float) -> Box

Translate a box.

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def shift_box(box: Box, dx: float, dy: float) -> Box:
    """Translate a box."""
    return (box[0] + dx, box[1] + dy, box[2] + dx, box[3] + dy)

shown_length

shown_length(text: str) -> int

Characters a dimension's text shows: each SolidWorks code is one symbol.

IDisplayData::GetTextAtIndex returns codes such as <MOD-DIAM> for the Ø sign; counted as typed, they make the text look three times wider than it is drawn.

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def shown_length(text: str) -> int:
    """Characters a dimension's text shows: each SolidWorks code is one symbol.

    ``IDisplayData::GetTextAtIndex`` returns codes such as ``<MOD-DIAM>``
    for the Ø sign; counted as typed, they make the text look three times
    wider than it is drawn.
    """
    return len(re.sub(r"<[^<>]*>", "X", text).strip())

text_box

text_box(x: float, y: float, width: float, height: float, angle: float) -> Box

Sheet box of a text run anchored at its lower-left corner and turned.

IDisplayData gives each text's lower-left corner, length, height and angle. A vertical dimension's text is turned 90 degrees, so it runs up the sheet from that corner and lies to its left (measured live on SW 2026).

Parameters:

Name Type Description Default
x float

Lower-left corner x.

required
y float

Lower-left corner y.

required
width float

Text length along its baseline.

required
height float

Text height.

required
angle float

Baseline angle in radians, counter-clockwise.

required

Returns:

Name Type Description
Box Box

The axis-aligned box around the turned text.

Source code in src/solidworks_mcp/adapters/solidworks/dimension_layout.py
def text_box(x: float, y: float, width: float, height: float, angle: float) -> Box:
    """Sheet box of a text run anchored at its lower-left corner and turned.

    ``IDisplayData`` gives each text's lower-left corner, length, height and
    angle. A vertical dimension's text is turned 90 degrees, so it runs up the
    sheet from that corner and lies to its left (measured live on SW 2026).

    Args:
        x: Lower-left corner x.
        y: Lower-left corner y.
        width: Text length along its baseline.
        height: Text height.
        angle: Baseline angle in radians, counter-clockwise.

    Returns:
        Box: The axis-aligned box around the turned text.
    """
    c, s = math.cos(angle), math.sin(angle)
    corners = [(0.0, 0.0), (width, 0.0), (0.0, height), (width, height)]
    xs = [x + u * c - v * s for u, v in corners]
    ys = [y + u * s + v * c for u, v in corners]
    return (min(xs), min(ys), max(xs), max(ys))