# 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/sliders.py
# Pure-Python port of BOSL2's sliders.scad: simple V-groove sliders and the matching rails.
# :class:`Slider` builds a slider that rides in a :class:`Rail` V-groove; both print
# without support. *slop* on the slider tunes the printed fit.
#
# FileSummary: V-groove sliders and rails.
# DocCategory: Parts library
# FileGroup: BOSL2
"""V-groove sliders and rails."""
from __future__ import annotations
import math
from typing import Any
from pybosl2._edges_lang import Anchor
from pybosl2._helpers import union
from pybosl2.constants import BOTTOM, LEFT
from pybosl2.distributors import DistributableMatrix
from pybosl2.shapes3d import Bosl2Solid, cuboid, prismoid
from pybosl2.vnf import VNF
__all__ = ["Slider", "Rail"]
def _union(shapes: list[Any]) -> Any:
return union(shapes)
[docs]
class Slider:
"""V-groove slider (BOSL2 slider()).
The slider rides in a matching V-groove rail. Both print without support.
*slop* tunes the printed fit.
Examples:
A slider:
.. pythonscad-example::
from pybosl2.parts.sliders import Slider
Slider(l=30, base=10, wall=4, slop=0.2).show()
"""
def __init__(
self,
l: float = 30, # noqa: E741
w: float = 10,
h: float = 10,
base: float = 10,
wall: float = 5,
angle: float = 30,
slop: float = 0.0,
fn: int | None = None,
fa: float | None = None,
fs: float | None = None,
) -> None:
"""Create a V-groove slider.
Args:
l: Slider length in mm.
w: V-groove opening width in mm.
h: V-groove height in mm.
base: Base plate height in mm.
wall: Wall thickness on each side of the V-groove in mm.
angle: V-groove half-angle in degrees.
slop: Additional clearance for tuning the printed fit.
fn: Number of facets for $fn-based resolution.
fa: Minimum facet angle.
fs: Minimum facet size.
Returns:
None.
"""
self._length: float = l
self._width: float = w
self._height: float = h
full_width = w + 2 * wall
full_height = h + base
parts = [
cuboid(
[full_width, l, base - slop],
chamfer=2,
edges=[Anchor.FRONT, Anchor.BACK],
except_edges=[Anchor.BOTTOM],
anchor=BOTTOM,
fn=fn,
fa=fa,
fs=fs,
)
]
for m in DistributableMatrix.xflip_copy(offset=w / 2 + slop):
wallcube = cuboid(
[wall, l, full_height],
chamfer=2,
edges=[Anchor.RIGHT],
except_edges=[Anchor.BOTTOM],
anchor=[b + le for b, le in zip(BOTTOM, LEFT, strict=False)], # type: ignore[arg-type]
fn=fn,
fa=fa,
fs=fs,
)
parts.append(wallcube.multmatrix(m.tolist()))
bev_h = h / 2 * math.tan(math.radians(angle))
for m in DistributableMatrix.xflip_copy(offset=w / 2 + slop + 0.02):
slid = prismoid(
[h, l],
[0, l - w],
height=bev_h + 0.01,
orient=LEFT,
anchor=BOTTOM,
fn=fn,
fa=fa,
fs=fs,
)
parts.append(slid.up(base + h / 2).multmatrix(m.tolist()))
result = _union(parts).down(base + h / 2).rotate([0, 0, 90])
nb = result._native_bounds()
size = nb[1] if nb else [l, full_width, h + 2 * base]
self._solid: Bosl2Solid = Bosl2Solid(result.shape, size=size)
@property
def length(self) -> float:
"""Slider length in mm."""
return self._length
@property
def width(self) -> float:
"""Slider width in mm."""
return self._width
@property
def height(self) -> float:
"""Slider height in mm."""
return self._height
[docs]
def shape(self) -> Bosl2Solid:
"""Return the slider geometry."""
return self._solid
[docs]
def show(self) -> None:
"""Display the slider in the viewer."""
self._solid.show()
[docs]
class Rail:
"""V-groove rail (BOSL2 rail()).
A matching rail for the V-groove slider.
Examples:
A rail:
.. pythonscad-example::
from pybosl2.parts.sliders import Rail
Rail(l=100, w=10, h=10).show()
"""
def __init__(
self,
l: float = 30, # noqa: E741
w: float = 10,
h: float = 10,
chamfer: float = 1.0,
angle: float = 30,
) -> None:
"""Create a V-groove rail.
Args:
l: Rail length in mm.
w: V-groove opening width in mm.
h: Rail height in mm.
chamfer: Edge chamfer radius in mm.
angle: V-groove half-angle in degrees.
Returns:
None.
"""
self._length: float = l
self._width: float = w
self._height: float = h
attack_ang, attack_len = 30, 2
fudge = 1.177
chamf = math.sqrt(2) * chamfer
cosa = math.cos(math.radians(angle * fudge))
sina = math.sin(math.radians(angle * fudge))
saa = math.sin(math.radians(attack_ang))
caa = math.cos(math.radians(attack_ang))
z1 = h / 2
z2 = z1 - chamf * cosa
z3 = z1 - attack_len * saa
z4 = 0.0
x1 = w / 2
x2 = x1 - chamf * sina
x3 = x1 - chamf
x4 = x1 - attack_len * saa
x5 = x2 - attack_len * saa
x6 = x1 - z1 * sina
x7 = x4 - z1 * sina
y1 = l / 2
y2 = y1 - attack_len * caa
pts = [
[-x5, -y1, z3],
[x5, -y1, z3],
[x7, -y1, z4],
[x4, -y1, -z1 - 0.05],
[-x4, -y1, -z1 - 0.05],
[-x7, -y1, z4],
[-x3, -y2, z1],
[x3, -y2, z1],
[x2, -y2, z2],
[x6, -y2, z4],
[x1, -y2, -z1 - 0.05],
[-x1, -y2, -z1 - 0.05],
[-x6, -y2, z4],
[-x2, -y2, z2],
[x5, y1, z3],
[-x5, y1, z3],
[-x7, y1, z4],
[-x4, y1, -z1 - 0.05],
[x4, y1, -z1 - 0.05],
[x7, y1, z4],
[x3, y2, z1],
[-x3, y2, z1],
[-x2, y2, z2],
[-x6, y2, z4],
[-x1, y2, -z1 - 0.05],
[x1, y2, -z1 - 0.05],
[x6, y2, z4],
[x2, y2, z2],
]
faces = [
[0, 1, 2],
[0, 2, 5],
[2, 3, 4],
[2, 4, 5],
[0, 13, 6],
[0, 6, 7],
[0, 7, 1],
[1, 7, 8],
[1, 8, 9],
[1, 9, 2],
[2, 9, 10],
[2, 10, 3],
[3, 10, 11],
[3, 11, 4],
[4, 11, 12],
[4, 12, 5],
[5, 12, 13],
[5, 13, 0],
[14, 15, 16],
[14, 16, 19],
[16, 17, 18],
[16, 18, 19],
[14, 27, 20],
[14, 20, 21],
[14, 21, 15],
[15, 21, 22],
[15, 22, 23],
[15, 23, 16],
[16, 23, 24],
[16, 24, 17],
[17, 24, 25],
[17, 25, 18],
[18, 25, 26],
[18, 26, 19],
[19, 26, 27],
[19, 27, 14],
[6, 21, 20],
[6, 20, 7],
[7, 20, 27],
[7, 27, 8],
[8, 27, 26],
[8, 26, 9],
[9, 26, 25],
[9, 25, 10],
[10, 25, 24],
[10, 24, 11],
[11, 24, 23],
[11, 23, 12],
[12, 23, 22],
[12, 22, 13],
[13, 22, 21],
[13, 21, 6],
]
self._solid: Bosl2Solid = Bosl2Solid(VNF(pts, faces).polyhedron(), size=[w, l, h])
@property
def length(self) -> float:
"""Rail length in mm."""
return self._length
@property
def width(self) -> float:
"""Rail width in mm."""
return self._width
@property
def height(self) -> float:
"""Rail height in mm."""
return self._height
[docs]
def shape(self) -> Bosl2Solid:
"""Return the rail geometry."""
return self._solid
[docs]
def show(self) -> None:
"""Display the rail in the viewer."""
self._solid.show()