# Copyright (c) 2026, pinkfish
#
# Licensed under the BSD 2-Clause License. See the LICENSE file in the project
# root for the full license text.
# SPDX-License-Identifier: BSD-2-Clause
# LibFile: pybosl2/parts/linear_bearings.py
# Pure-Python port of BOSL2's linear_bearings.scad: models of linear ball bearings that run along
# a rod, and the pillow-block housings that hold them. :meth:`LinearBearings.linear_bearing` is a
# generic bearing; :meth:`~LinearBearings.lmxuu_bearing` looks a standard LMxUU size up in
# :meth:`~LinearBearings.lmxuu_info` (a :class:`LinearBearingSpec` table). The housings clamp a
# bearing to a plate with a teardrop bore and a screw.
#
# FileSummary: Linear (LMxUU) ball bearings and their pillow-block housings.
# DocCategory: Parts library
# FileGroup: BOSL2
"""Linear (LMxUU) ball bearings and their pillow-block housings."""
from __future__ import annotations
import math
from dataclasses import dataclass
from typing import Any
from pybosl2._helpers import union
from pybosl2.shapes3d import Bosl2Solid, cuboid, teardrop, tube
__all__ = ["LinearBearings", "LinearBearingSpec"]
def _union(shapes: list[Any]) -> Any:
return union(shapes)
[docs]
@dataclass(frozen=True)
class LinearBearingSpec:
"""Dimensions of a standard LMxUU linear bearing (BOSL2 lmxuu_info())."""
outer_diameter: float # outer diameter
length: float # axial length
# the bore (shaft) diameter equals the nominal size, which keys the table.
# nominal size (shaft Ø) -> spec, transcribed from linear_bearings.scad.
_LMXUU = {
4: LinearBearingSpec(8, 12),
5: LinearBearingSpec(10, 15),
6: LinearBearingSpec(12, 19),
8: LinearBearingSpec(15, 24),
10: LinearBearingSpec(19, 29),
12: LinearBearingSpec(21, 30),
13: LinearBearingSpec(23, 32),
16: LinearBearingSpec(28, 37),
20: LinearBearingSpec(32, 42),
25: LinearBearingSpec(40, 59),
30: LinearBearingSpec(45, 64),
35: LinearBearingSpec(52, 70),
40: LinearBearingSpec(60, 80),
50: LinearBearingSpec(80, 100),
60: LinearBearingSpec(90, 110),
80: LinearBearingSpec(120, 140),
100: LinearBearingSpec(150, 175),
}
[docs]
class LinearBearings:
"""Linear (LMxUU) ball bearings and pillow-block housings (BOSL2 linear_bearings.scad).
.. seealso::
`Visual spec sheet <specs/linear_bearings.html>`_ — measurements and STL previews
"""
[docs]
@staticmethod
def lmxuu_info(size: int) -> LinearBearingSpec:
"""Return the :class:`LinearBearingSpec` (outer_diameter, length) for a standard LMxUU size (BOSL2.
lmxuu_info()).
"""
try:
return _LMXUU[int(size)]
except (KeyError, ValueError):
raise ValueError(f"Unsupported lmXuu linear bearing size: {size!r}") from None
[docs]
@staticmethod
def linear_bearing(
length: float = 24,
outer_diameter: float = 15,
inner_diameter: float = 8,
color: str | None = "silver",
fn: int | None = None,
fa: float | None = None,
fs: float | None = None,
) -> Bosl2Solid:
"""Return a generic linear ball-bearing cartridge, bore.
*inner_diameter* / outer *outer_diameter* / length *length*
(BOSL2 linear_bearing()).
Examples:
An LM8UU-sized bearing:
.. pythonscad-example::
from pybosl2.parts.linear_bearings import LinearBearings
LinearBearings.linear_bearing(length=24, outer_diameter=15, inner_diameter=8).show()
"""
body = _union(
[
tube(
inner_diameter=inner_diameter,
outer_diameter=outer_diameter,
height=length - 1,
fn=fn,
fa=fa,
fs=fs,
),
tube(
inner_diameter=outer_diameter - 1,
outer_diameter=outer_diameter,
height=length,
fn=fn,
fa=fa,
fs=fs,
),
tube(
inner_diameter=inner_diameter,
outer_diameter=inner_diameter + 1,
height=length,
fn=fn,
fa=fa,
fs=fs,
),
tube(
inner_diameter=inner_diameter + 2,
outer_diameter=outer_diameter - 2,
height=length,
fn=fn,
fa=fa,
fs=fs,
),
]
)
result = Bosl2Solid(body.shape, size=[outer_diameter, outer_diameter, length])
return result.color(color) if color else result
[docs]
@staticmethod
def lmxuu_bearing(size: int = 8, color: str | None = "silver") -> Bosl2Solid:
"""Return a standard LMxUU linear bearing for a *size* mm rod (BOSL2 lmxuu_bearing())."""
spec = LinearBearings.lmxuu_info(size)
return LinearBearings.linear_bearing(
length=spec.length, inner_diameter=size, outer_diameter=spec.outer_diameter, color=color
)
[docs]
@staticmethod
def linear_bearing_housing(
diameter: float = 15,
length: float = 24,
tab: float = 8,
gap: float = 5,
wall: float = 3,
tabwall: float = 5,
screwsize: float = 3,
fn: int | None = None,
fa: float | None = None,
fs: float | None = None,
) -> Bosl2Solid:
"""Return a pillow-block housing that clamps a linear bearing (bore.
*diameter*, length *length*) to a plate (BOSL2 linear_bearing_housing()).
The teardrop bore prints without support; the split *gap* and a *screwsize* clamp screw
through the tabs let it grip the bearing.
Args:
diameter: Outer diameter of the bearing in mm. Defaults to 15.
length: Axial length of the bearing in mm. Defaults to 24.
tab: Tab height above the clamping gap in mm. Defaults to 8.
gap: Split gap width in mm (cut between the clamp tabs). Defaults to 5.
wall: Wall thickness around the bearing in mm. Defaults to 3.
tabwall: Extra wall thickness for the clamp tabs in mm. Defaults to 5.
screwsize: Clamp screw diameter in mm. Defaults to 3.
fn: Number of fragments for cylinder resolution. Passed to the geometry primitives.
fa: Minimum fragment angle. Passed to the geometry primitives.
fs: Minimum fragment size. Passed to the geometry primitives.
Returns:
A pillow-block housing as a :class:`~pybosl2.shapes3d.Bosl2Solid`.
Examples:
An LM8UU-sized housing:
.. pythonscad-example::
from pybosl2.parts.linear_bearings import LinearBearings
LinearBearings.linear_bearing_housing(diameter=15, length=24).show()
"""
outer_diameter = diameter + 2 * wall
ogap = gap + 2 * tabwall
tabh = tab / 2 + outer_diameter / 2 * math.sqrt(2) - ogap / 2 - 1
# teardrop bearing shell + base + clamp tabs, then the bore, split gap and screw hole removed.
body = _union(
[
teardrop(diameter=outer_diameter, height=length, fn=fn, fa=fa, fs=fs).rotate(
[0, 90, 0]
), # teardrop shell, axis along X
cuboid([length, outer_diameter, outer_diameter / 2], fn=fn, fa=fa, fs=fs).down(
outer_diameter / 4
), # base
cuboid([length, ogap, outer_diameter / 2 + tab / 2], fn=fn, fa=fa, fs=fs).up(
(outer_diameter / 2 + tab / 2) / 2
), # clamp tabs
]
)
body = body - teardrop(diameter=diameter, height=length + 0.1, fn=fn, fa=fa, fs=fs).rotate(
[0, 90, 0]
) # bearing bore
body = body - cuboid([length + 0.1, gap, outer_diameter], fn=fn, fa=fa, fs=fs) # split gap
# clamp screw across the tabs (a simple clearance hole)
from pybosl2.parts.screws import ScrewHole
screw = (
ScrewHole(f"M{screwsize:g}", length=ogap + 1, fn=fn or 16, fa=fa, fs=fs).shape().rotate([90, 0, 0]).up(tabh)
)
body = body - screw
return Bosl2Solid(body.shape, size=[length, outer_diameter, outer_diameter + tab / 2])
[docs]
@staticmethod
def lmxuu_housing(
size: int = 8,
tab: float = 7,
gap: float = 5,
wall: float = 3,
tabwall: float = 5,
screwsize: float = 3,
fn: int | None = None,
fa: float | None = None,
fs: float | None = None,
) -> Bosl2Solid:
"""Return a pillow-block housing sized for a standard LMxUU bearing (BOSL2 lmxuu_housing())."""
spec = LinearBearings.lmxuu_info(size)
return LinearBearings.linear_bearing_housing(
diameter=spec.outer_diameter,
length=spec.length,
tab=tab,
gap=gap,
wall=wall,
tabwall=tabwall,
screwsize=screwsize,
fn=fn,
fa=fa,
fs=fs,
)
lmXuu_bearing = lmxuu_bearing # noqa: N815
lmXuu_housing = lmxuu_housing # noqa: N815