# 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: bosl2/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.
# FileGroup: BOSL2
from __future__ import annotations
import math
from dataclasses import dataclass
from bosl2._helpers import union
from bosl2.shapes3d import Bosl2Solid, cuboid, teardrop, tube
__all__ = ["LinearBearings", "LinearBearingSpec"]
def _union(shapes):
return union(shapes)
@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)."""
[docs]
@staticmethod
def lmXuu_info(size: int) -> LinearBearingSpec:
"""
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}")
[docs]
@staticmethod
def linear_bearing(
length: float = 24,
outer_diameter: float = 15,
inner_diameter: float = 8,
color: str | None = "silver",
) -> Bosl2Solid:
"""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 bosl2.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,
),
tube(
inner_diameter=outer_diameter - 1,
outer_diameter=outer_diameter,
height=length,
),
tube(
inner_diameter=inner_diameter,
outer_diameter=inner_diameter + 1,
height=length,
),
tube(
inner_diameter=inner_diameter + 2,
outer_diameter=outer_diameter - 2,
height=length,
),
]
)
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:
"""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:
"""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.
"""
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]).down(outer_diameter / 4), # base
cuboid([length, ogap, outer_diameter / 2 + tab / 2]).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]) # split gap
# clamp screw across the tabs (a simple clearance hole)
from bosl2.screws import Screws
screw = (
Screws.screw_hole(f"M{screwsize:g}", length=ogap + 1, fn=fn or 16, fa=fa, fs=fs).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:
"""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,
)